Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d1ce2c568e | ||
|
|
445e9edd7c | ||
|
|
48dfd364e4 | ||
|
|
e4021523b8 | ||
|
|
790fcc0cea | ||
|
|
69ba990998 | ||
|
|
1918fb984c | ||
|
|
5bf97e2179 |
@@ -43,6 +43,31 @@ func (r *InteractionRepository) AppendUtterance(ctx context.Context, c interacti
|
|||||||
}
|
}
|
||||||
return tx.Commit(ctx)
|
return tx.Commit(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *InteractionRepository) SaveSemanticResult(ctx context.Context, interpretation interaction.UtteranceInterpretation, projects []interaction.ProjectReference, relations []interaction.InteractionRelation) error {
|
||||||
|
tx, err := r.pool.Begin(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer tx.Rollback(ctx)
|
||||||
|
if _, err = tx.Exec(ctx, `INSERT INTO interaction.utterance_interpretations(interpretation_id,utterance_id,speech_act,confidence_millis,source,assertion_verification,requested_outcome,clarification,recorded_at_unix_millis) VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9)`, interpretation.ID, interpretation.UtteranceID, interpretation.SpeechAct, interpretation.ConfidenceMillis, interpretation.Source, interpretation.AssertionVerification, interpretation.RequestedOutcome, interpretation.Clarification, interpretation.RecordedAtUnixMillis); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, project := range projects {
|
||||||
|
if _, err = tx.Exec(ctx, `INSERT INTO interaction.project_references(project_reference_id,utterance_id,project_id,project_display_name,role,provenance,confidence_millis,recorded_at_unix_millis) VALUES($1,$2,$3,$4,$5,$6,$7,$8)`, project.ID, project.UtteranceID, project.ProjectID, project.ProjectDisplayName, project.Role, project.Provenance, project.ConfidenceMillis, project.RecordedAtUnixMillis); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, relation := range relations {
|
||||||
|
if relation.ToUtteranceID == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, err = tx.Exec(ctx, `INSERT INTO interaction.relations(relation_id,from_utterance_id,to_utterance_id,kind,recorded_at_unix_millis) VALUES($1,$2,$3,$4,$5) ON CONFLICT(relation_id) DO NOTHING`, relation.ID, relation.FromUtteranceID, relation.ToUtteranceID, relation.Kind, relation.RecordedAtUnixMillis); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return tx.Commit(ctx)
|
||||||
|
}
|
||||||
func (r *InteractionRepository) Conversation(ctx context.Context, id interaction.ConversationID) (interaction.Conversation, error) {
|
func (r *InteractionRepository) Conversation(ctx context.Context, id interaction.ConversationID) (interaction.Conversation, error) {
|
||||||
var c interaction.Conversation
|
var c interaction.Conversation
|
||||||
e := r.pool.QueryRow(ctx, `SELECT conversation_id,actor_id,principal_id,channel,state,created_at_unix_millis,updated_at_unix_millis FROM interaction.conversations WHERE conversation_id=$1`, id).Scan(&c.ID, &c.ActorID, &c.PrincipalID, &c.Channel, &c.State, &c.CreatedAtUnixMillis, &c.UpdatedAtUnixMillis)
|
e := r.pool.QueryRow(ctx, `SELECT conversation_id,actor_id,principal_id,channel,state,created_at_unix_millis,updated_at_unix_millis FROM interaction.conversations WHERE conversation_id=$1`, id).Scan(&c.ID, &c.ActorID, &c.PrincipalID, &c.Channel, &c.State, &c.CreatedAtUnixMillis, &c.UpdatedAtUnixMillis)
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ package postgres
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.cin.su/m/Assistant/contracts/identity"
|
||||||
"git.cin.su/m/Assistant/contracts/situation"
|
"git.cin.su/m/Assistant/contracts/situation"
|
||||||
"github.com/jackc/pgx/v5/pgxpool"
|
"github.com/jackc/pgx/v5/pgxpool"
|
||||||
)
|
)
|
||||||
@@ -39,3 +41,35 @@ func (r *SituationRepository) ByRuntime(ctx context.Context, runtime situation.R
|
|||||||
}
|
}
|
||||||
return bindings, rows.Err()
|
return bindings, rows.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *SituationRepository) ListProjects(ctx context.Context) ([]situation.ProjectDescriptor, error) {
|
||||||
|
rows, err := r.pool.Query(ctx, `SELECT p.project_id, p.display_name, p.recorded_at_unix_millis, COALESCE(array_agg(a.alias ORDER BY a.alias) FILTER (WHERE a.alias IS NOT NULL), '{}') FROM situation.projects p LEFT JOIN situation.project_aliases a ON a.project_id=p.project_id GROUP BY p.project_id, p.display_name, p.recorded_at_unix_millis ORDER BY p.display_name`)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var projects []situation.ProjectDescriptor
|
||||||
|
for rows.Next() {
|
||||||
|
var project situation.ProjectDescriptor
|
||||||
|
var aliases []string
|
||||||
|
if err := rows.Scan(&project.ID, &project.DisplayName, &project.RecordedAtUnixMillis, &aliases); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, alias := range aliases {
|
||||||
|
project.Aliases = append(project.Aliases, situation.ProjectAlias(strings.ToLower(alias)))
|
||||||
|
}
|
||||||
|
projects = append(projects, project)
|
||||||
|
}
|
||||||
|
return projects, rows.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SituationRepository) ActiveProjectFocus(ctx context.Context, actorID identity.ActorID, principalID identity.PrincipalID) (situation.ActiveProjectFocus, error) {
|
||||||
|
var focus situation.ActiveProjectFocus
|
||||||
|
err := r.pool.QueryRow(ctx, `SELECT actor_id,principal_id,project_id,project_display_name,provenance,confidence_millis,source_utterance_id,updated_at_unix_millis FROM situation.active_project_focus WHERE actor_id=$1 AND principal_id=$2`, actorID, principalID).Scan(&focus.ActorID, &focus.PrincipalID, &focus.ProjectID, &focus.ProjectDisplayName, &focus.Provenance, &focus.ConfidenceMillis, &focus.SourceUtteranceID, &focus.UpdatedAtUnixMillis)
|
||||||
|
return focus, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SituationRepository) SaveActiveProjectFocus(ctx context.Context, focus situation.ActiveProjectFocus) error {
|
||||||
|
_, err := r.pool.Exec(ctx, `INSERT INTO situation.active_project_focus(actor_id,principal_id,project_id,project_display_name,provenance,confidence_millis,source_utterance_id,updated_at_unix_millis) VALUES($1,$2,$3,$4,$5,$6,$7,$8) ON CONFLICT(actor_id,principal_id) DO UPDATE SET project_id=EXCLUDED.project_id, project_display_name=EXCLUDED.project_display_name, provenance=EXCLUDED.provenance, confidence_millis=EXCLUDED.confidence_millis, source_utterance_id=EXCLUDED.source_utterance_id, updated_at_unix_millis=EXCLUDED.updated_at_unix_millis`, focus.ActorID, focus.PrincipalID, focus.ProjectID, focus.ProjectDisplayName, focus.Provenance, focus.ConfidenceMillis, focus.SourceUtteranceID, focus.UpdatedAtUnixMillis)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# ADR-0011 - Human Intent And Project Context Semantics
|
||||||
|
|
||||||
|
Status: Accepted
|
||||||
|
Date: 2026-08-17
|
||||||
|
Accepted under: DEV-000062 milestone decision
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
DEV-000062 extends existing owners rather than creating a new canonical owner.
|
||||||
|
|
||||||
|
`interaction` owns typed, append-only interpretations of human utterances: speech-act confidence, original intent linkage, project reference candidates, correction/supersession relations, and whether a directive/decision/assertion is only a human semantic record. Interaction does not own truth, authority, execution, provider choice, or project canonical state.
|
||||||
|
|
||||||
|
`situation` owns the minimal V1 Project Registry and active project focus needed for human chat context resolution. It may store human-readable project aliases and per-actor/principal active focus references. Project resolution confidence and provenance are not Authority.
|
||||||
|
|
||||||
|
`epistemics` remains the owner of claims, observations, evidence and verified beliefs. A human assertion recorded through Interaction is not a verified fact. It may become input to a future Epistemics proposal only after a trusted observation/evidence path exists.
|
||||||
|
|
||||||
|
`work` remains the owner of durable Decisions/Tasks. DEV-000062 may record an explicit human decision/intention in Interaction, but it does not create a Work Decision or executable task unless a later accepted contract routes through Work/Authority/Commit.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
The chat runtime can show a human-readable active project focus and persist semantic interpretation without direct mutation authority. Corrections are append-only. Multiple project references are represented as typed references, not copied project state. Cognitive help, when used, must remain behind the existing Cognitive Gateway/Router and produce validated candidates before persistence.
|
||||||
|
|
||||||
@@ -40,7 +40,9 @@
|
|||||||
"id": "mod.situation",
|
"id": "mod.situation",
|
||||||
"owns": [
|
"owns": [
|
||||||
"Situation",
|
"Situation",
|
||||||
"FocusBinding"
|
"FocusBinding",
|
||||||
|
"ProjectRegistry",
|
||||||
|
"ActiveProjectFocus"
|
||||||
],
|
],
|
||||||
"database_schema": "situation",
|
"database_schema": "situation",
|
||||||
"boundary_class": "remote_capable",
|
"boundary_class": "remote_capable",
|
||||||
@@ -144,10 +146,10 @@
|
|||||||
},
|
},
|
||||||
"interaction": {
|
"interaction": {
|
||||||
"id": "mod.interaction",
|
"id": "mod.interaction",
|
||||||
"owns": ["Conversation", "HumanUtterance", "UtteranceInterpretation", "OriginalHumanIntent", "InteractionRelation"],
|
"owns": ["Conversation", "HumanUtterance", "UtteranceInterpretation", "OriginalHumanIntent", "InteractionRelation", "ProjectReference"],
|
||||||
"database_schema": "interaction",
|
"database_schema": "interaction",
|
||||||
"boundary_class": "remote_capable",
|
"boundary_class": "remote_capable",
|
||||||
"may_depend_on": ["contracts.identity", "contracts.situation", "contracts.cognitive", "platform.ids", "platform.events"]
|
"may_depend_on": ["contracts.identity", "contracts.situation", "contracts.epistemics", "contracts.cognitive", "platform.ids", "platform.events"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"platform": {
|
"platform": {
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ modules:
|
|||||||
|
|
||||||
situation:
|
situation:
|
||||||
id: mod.situation
|
id: mod.situation
|
||||||
owns: [Situation, FocusBinding]
|
owns: [Situation, FocusBinding, ProjectRegistry, ActiveProjectFocus]
|
||||||
database_schema: situation
|
database_schema: situation
|
||||||
boundary_class: remote_capable
|
boundary_class: remote_capable
|
||||||
may_depend_on: [contracts.identity, contracts.authority, contracts.work, contracts.capability]
|
may_depend_on: [contracts.identity, contracts.authority, contracts.work, contracts.capability]
|
||||||
@@ -70,10 +70,10 @@ modules:
|
|||||||
|
|
||||||
interaction:
|
interaction:
|
||||||
id: mod.interaction
|
id: mod.interaction
|
||||||
owns: [Conversation, HumanUtterance, UtteranceInterpretation, OriginalHumanIntent, InteractionRelation]
|
owns: [Conversation, HumanUtterance, UtteranceInterpretation, OriginalHumanIntent, InteractionRelation, ProjectReference]
|
||||||
database_schema: interaction
|
database_schema: interaction
|
||||||
boundary_class: remote_capable
|
boundary_class: remote_capable
|
||||||
may_depend_on: [contracts.identity, contracts.situation, contracts.cognitive, platform.ids, platform.events]
|
may_depend_on: [contracts.identity, contracts.situation, contracts.epistemics, contracts.cognitive, platform.ids, platform.events]
|
||||||
|
|
||||||
platform:
|
platform:
|
||||||
allowed_packages:
|
allowed_packages:
|
||||||
|
|||||||
@@ -201,17 +201,55 @@ func main() {
|
|||||||
http.Error(w, "invalid chat message", http.StatusBadRequest)
|
http.Error(w, "invalid chat message", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
projects, err := repository.ListProjects(r.Context())
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "project registry unavailable", http.StatusServiceUnavailable)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var previousFocus *situation.ActiveProjectFocus
|
||||||
|
storedFocus, err := repository.ActiveProjectFocus(r.Context(), session.ActorID, session.PrincipalID)
|
||||||
|
if err == nil {
|
||||||
|
previousFocus = &storedFocus
|
||||||
|
} else if !errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
http.Error(w, "project focus unavailable", http.StatusServiceUnavailable)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
semantic, err := interactiondomain.InterpretUtterance(utterance, projects, previousFocus, now)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "interpret chat message", http.StatusServiceUnavailable)
|
||||||
|
return
|
||||||
|
}
|
||||||
if err := interactionRepository.AppendUtterance(r.Context(), conversation, utterance, intent, relations); err != nil {
|
if err := interactionRepository.AppendUtterance(r.Context(), conversation, utterance, intent, relations); err != nil {
|
||||||
http.Error(w, "save chat message", http.StatusInternalServerError)
|
http.Error(w, "save chat message", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if err := interactionRepository.SaveSemanticResult(r.Context(), semantic.Interpretation, semantic.Projects, semantic.Relations); err != nil {
|
||||||
|
http.Error(w, "save chat semantics", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
activeFocusName := ""
|
||||||
|
activeFocusProvenance := situation.ProjectResolutionProvenance("")
|
||||||
|
if semantic.ActiveFocus != nil {
|
||||||
|
if err := repository.SaveActiveProjectFocus(r.Context(), *semantic.ActiveFocus); err != nil {
|
||||||
|
http.Error(w, "save project focus", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
activeFocusName = semantic.ActiveFocus.ProjectDisplayName
|
||||||
|
activeFocusProvenance = semantic.ActiveFocus.Provenance
|
||||||
|
} else if previousFocus != nil {
|
||||||
|
activeFocusName = previousFocus.ProjectDisplayName
|
||||||
|
activeFocusProvenance = previousFocus.Provenance
|
||||||
|
}
|
||||||
response := interaction.ChatMessageResponse{
|
response := interaction.ChatMessageResponse{
|
||||||
ConversationID: conversation.ID,
|
ConversationID: conversation.ID,
|
||||||
UtteranceID: utterance.ID,
|
UtteranceID: utterance.ID,
|
||||||
SpeechAct: utterance.SpeechAct,
|
SpeechAct: semantic.Interpretation.SpeechAct,
|
||||||
InterpretationState: utterance.InterpretationState,
|
InterpretationState: utterance.InterpretationState,
|
||||||
NeedsCognition: interactiondomain.NeedsCognition(utterance.SpeechAct),
|
NeedsCognition: semantic.NeedsCognition,
|
||||||
FallbackText: interactiondomain.FallbackText(utterance.SpeechAct),
|
FallbackText: semantic.Reply,
|
||||||
|
ActiveProjectName: activeFocusName,
|
||||||
|
ProjectProvenance: activeFocusProvenance,
|
||||||
|
Clarification: semantic.Interpretation.Clarification,
|
||||||
}
|
}
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
w.Header().Set("Cache-Control", "no-store")
|
w.Header().Set("Cache-Control", "no-store")
|
||||||
|
|||||||
@@ -142,11 +142,14 @@ type browserChatRequest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type browserChatResponse struct {
|
type browserChatResponse struct {
|
||||||
ConversationID string `json:"conversation_id"`
|
ConversationID string `json:"conversation_id"`
|
||||||
UtteranceID string `json:"utterance_id"`
|
UtteranceID string `json:"utterance_id"`
|
||||||
SpeechAct string `json:"speech_act"`
|
SpeechAct string `json:"speech_act"`
|
||||||
ReplyText string `json:"reply_text"`
|
ReplyText string `json:"reply_text"`
|
||||||
Cognition string `json:"cognition"`
|
Cognition string `json:"cognition"`
|
||||||
|
ActiveFocus string `json:"active_focus"`
|
||||||
|
ProjectProvenance string `json:"project_provenance"`
|
||||||
|
Clarification string `json:"clarification"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *gateway) handleAdvisoryPolicy(w http.ResponseWriter, r *http.Request) {
|
func (g *gateway) handleAdvisoryPolicy(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -313,7 +316,7 @@ func (g *gateway) handleChatMessage(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
reply := saved.FallbackText
|
reply := saved.FallbackText
|
||||||
cognition := "not_requested"
|
cognition := "not_requested"
|
||||||
if saved.NeedsCognition {
|
if saved.NeedsCognition && saved.Clarification != interaction.ClarificationRequired {
|
||||||
cognition = "unavailable"
|
cognition = "unavailable"
|
||||||
requestID, idErr := ids.New()
|
requestID, idErr := ids.New()
|
||||||
if idErr == nil {
|
if idErr == nil {
|
||||||
@@ -333,7 +336,7 @@ func (g *gateway) handleChatMessage(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
w.Header().Set("Cache-Control", "no-store")
|
w.Header().Set("Cache-Control", "no-store")
|
||||||
_ = json.NewEncoder(w).Encode(browserChatResponse{ConversationID: string(saved.ConversationID), UtteranceID: string(saved.UtteranceID), SpeechAct: string(saved.SpeechAct), ReplyText: reply, Cognition: cognition})
|
_ = json.NewEncoder(w).Encode(browserChatResponse{ConversationID: string(saved.ConversationID), UtteranceID: string(saved.UtteranceID), SpeechAct: string(saved.SpeechAct), ReplyText: reply, Cognition: cognition, ActiveFocus: saved.ActiveProjectName, ProjectProvenance: string(saved.ProjectProvenance), Clarification: string(saved.Clarification)})
|
||||||
}
|
}
|
||||||
|
|
||||||
func validAdvisoryResponse(response cognitive.GatewayAdvisoryResponse, request cognitive.GatewayAdvisoryRequest) bool {
|
func validAdvisoryResponse(response cognitive.GatewayAdvisoryResponse, request cognitive.GatewayAdvisoryRequest) bool {
|
||||||
|
|||||||
@@ -32,7 +32,8 @@ textarea { resize: vertical; min-height: 84px; max-height: 240px; padding: 14px;
|
|||||||
.chat-view { min-height: 100vh; display: grid; grid-template-rows: 64px minmax(0, 1fr) auto; }
|
.chat-view { min-height: 100vh; display: grid; grid-template-rows: 64px minmax(0, 1fr) auto; }
|
||||||
.chat-header { display: flex; align-items: center; gap: 18px; padding: 0 22px; border-bottom: 1px solid var(--line); background: rgba(16, 23, 20, .96); }
|
.chat-header { display: flex; align-items: center; gap: 18px; padding: 0 22px; border-bottom: 1px solid var(--line); background: rgba(16, 23, 20, .96); }
|
||||||
.chat-header a { color: var(--ink); text-decoration: none; font: 400 24px/1 var(--serif); }
|
.chat-header a { color: var(--ink); text-decoration: none; font: 400 24px/1 var(--serif); }
|
||||||
.chat-header span { margin-left: auto; color: var(--muted); font: 10px/1 var(--mono); letter-spacing: .1em; text-transform: uppercase; }
|
.chat-header span { color: var(--muted); font: 10px/1 var(--mono); letter-spacing: .1em; text-transform: uppercase; }
|
||||||
|
#active-focus { margin-left: auto; color: var(--good); }
|
||||||
.chat-header button { width: 38px; height: 38px; border: 1px solid var(--line); background: transparent; }
|
.chat-header button { width: 38px; height: 38px; border: 1px solid var(--line); background: transparent; }
|
||||||
.messages { width: min(100%, 920px); margin: 0 auto; padding: 28px 18px 120px; display: flex; flex-direction: column; gap: 14px; overflow: auto; }
|
.messages { width: min(100%, 920px); margin: 0 auto; padding: 28px 18px 120px; display: flex; flex-direction: column; gap: 14px; overflow: auto; }
|
||||||
.message { max-width: min(720px, 92%); padding: 14px 16px; border: 1px solid var(--line); border-radius: var(--radius); line-height: 1.45; white-space: pre-wrap; }
|
.message { max-width: min(720px, 92%); padding: 14px 16px; border: 1px solid var(--line); border-radius: var(--radius); line-height: 1.45; white-space: pre-wrap; }
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ const chatForm = document.querySelector("#chat-form");
|
|||||||
const messageInput = document.querySelector("#message");
|
const messageInput = document.querySelector("#message");
|
||||||
const sendButton = document.querySelector("#send");
|
const sendButton = document.querySelector("#send");
|
||||||
const logoutButton = document.querySelector("#logout");
|
const logoutButton = document.querySelector("#logout");
|
||||||
|
const activeFocus = document.querySelector("#active-focus");
|
||||||
|
|
||||||
let token = sessionStorage.getItem("domovoy.chat.token") || "";
|
let token = sessionStorage.getItem("domovoy.chat.token") || "";
|
||||||
let conversationID = sessionStorage.getItem("domovoy.chat.conversation") || "";
|
let conversationID = sessionStorage.getItem("domovoy.chat.conversation") || "";
|
||||||
@@ -38,6 +39,11 @@ function appendMessage(kind, text, meta = "") {
|
|||||||
messages.scrollTop = messages.scrollHeight;
|
messages.scrollTop = messages.scrollHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateActiveFocus(value, provenance = "") {
|
||||||
|
if (!activeFocus) return;
|
||||||
|
activeFocus.textContent = value ? `фокус: ${value}${provenance ? ` · ${provenance}` : ""}` : "фокус: не выбран";
|
||||||
|
}
|
||||||
|
|
||||||
async function api(path, options = {}) {
|
async function api(path, options = {}) {
|
||||||
const headers = {"Content-Type": "application/json", ...(options.headers || {})};
|
const headers = {"Content-Type": "application/json", ...(options.headers || {})};
|
||||||
if (token) headers.Authorization = `Bearer ${token}`;
|
if (token) headers.Authorization = `Bearer ${token}`;
|
||||||
@@ -72,7 +78,10 @@ chatForm.addEventListener("submit", async (event) => {
|
|||||||
const result = await api("/api/chat/messages", {method: "POST", body: JSON.stringify({conversation_id: conversationID, message: text})});
|
const result = await api("/api/chat/messages", {method: "POST", body: JSON.stringify({conversation_id: conversationID, message: text})});
|
||||||
conversationID = result.conversation_id;
|
conversationID = result.conversation_id;
|
||||||
sessionStorage.setItem("domovoy.chat.conversation", conversationID);
|
sessionStorage.setItem("domovoy.chat.conversation", conversationID);
|
||||||
appendMessage("assistant", result.reply_text || "Сообщение записано.", `${result.speech_act}; cognition=${result.cognition}`);
|
updateActiveFocus(result.active_focus, result.project_provenance);
|
||||||
|
const semanticMeta = [result.speech_act, `cognition=${result.cognition}`];
|
||||||
|
if (result.clarification) semanticMeta.push(`clarification=${result.clarification}`);
|
||||||
|
appendMessage("assistant", result.reply_text || "Сообщение записано.", semanticMeta.join("; "));
|
||||||
} catch {
|
} catch {
|
||||||
appendMessage("assistant", "Чат сейчас недоступен. Сообщение не принято runtime.");
|
appendMessage("assistant", "Чат сейчас недоступен. Сообщение не принято runtime.");
|
||||||
} finally {
|
} finally {
|
||||||
@@ -87,6 +96,7 @@ logoutButton.addEventListener("click", () => {
|
|||||||
sessionStorage.removeItem("domovoy.chat.token");
|
sessionStorage.removeItem("domovoy.chat.token");
|
||||||
sessionStorage.removeItem("domovoy.chat.conversation");
|
sessionStorage.removeItem("domovoy.chat.conversation");
|
||||||
messages.replaceChildren();
|
messages.replaceChildren();
|
||||||
|
updateActiveFocus("");
|
||||||
showLogin();
|
showLogin();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
<section id="chat-view" class="chat-view" hidden>
|
<section id="chat-view" class="chat-view" hidden>
|
||||||
<header class="chat-header">
|
<header class="chat-header">
|
||||||
<a href="/" aria-label="Консоль создателя">Домовой</a>
|
<a href="/" aria-label="Консоль создателя">Домовой</a>
|
||||||
|
<span id="active-focus">фокус: не выбран</span>
|
||||||
<span id="session-state">сессия активна</span>
|
<span id="session-state">сессия активна</span>
|
||||||
<button id="logout" type="button" aria-label="Выйти">↗</button>
|
<button id="logout" type="button" aria-label="Выйти">↗</button>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
@@ -5,3 +5,15 @@ import "git.cin.su/m/Assistant/platform/ids"
|
|||||||
type ObservationID ids.ID
|
type ObservationID ids.ID
|
||||||
type ClaimID ids.ID
|
type ClaimID ids.ID
|
||||||
type BeliefID ids.ID
|
type BeliefID ids.ID
|
||||||
|
type VerificationState string
|
||||||
|
type ClaimSourceKind string
|
||||||
|
|
||||||
|
const (
|
||||||
|
VerificationUnverified VerificationState = "UNVERIFIED"
|
||||||
|
VerificationVerified VerificationState = "VERIFIED"
|
||||||
|
VerificationRejected VerificationState = "REJECTED"
|
||||||
|
VerificationNotApplicable VerificationState = "NOT_APPLICABLE"
|
||||||
|
|
||||||
|
ClaimSourceAuthenticatedHuman ClaimSourceKind = "AUTHENTICATED_HUMAN"
|
||||||
|
ClaimSourceTrustedObservation ClaimSourceKind = "TRUSTED_OBSERVATION"
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
package interaction
|
package interaction
|
||||||
|
|
||||||
import "git.cin.su/m/Assistant/contracts/identity"
|
import (
|
||||||
|
"git.cin.su/m/Assistant/contracts/identity"
|
||||||
|
"git.cin.su/m/Assistant/contracts/situation"
|
||||||
|
)
|
||||||
|
|
||||||
type ConversationID string
|
type ConversationID string
|
||||||
type HumanUtteranceID string
|
type HumanUtteranceID string
|
||||||
@@ -11,6 +14,10 @@ type Channel string
|
|||||||
type ConversationState string
|
type ConversationState string
|
||||||
type InterpretationState string
|
type InterpretationState string
|
||||||
type InteractionRelationKind string
|
type InteractionRelationKind string
|
||||||
|
type InterpretationID string
|
||||||
|
type ProjectReferenceID string
|
||||||
|
type ProjectReferenceRole string
|
||||||
|
type ClarificationState string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
ChannelWebChat Channel = "WEB_CHAT"
|
ChannelWebChat Channel = "WEB_CHAT"
|
||||||
@@ -35,6 +42,13 @@ const (
|
|||||||
RelationRespondsTo InteractionRelationKind = "RESPONDS_TO"
|
RelationRespondsTo InteractionRelationKind = "RESPONDS_TO"
|
||||||
RelationRequestsCognition InteractionRelationKind = "REQUESTS_COGNITION"
|
RelationRequestsCognition InteractionRelationKind = "REQUESTS_COGNITION"
|
||||||
RelationCognitionFallback InteractionRelationKind = "COGNITION_FALLBACK"
|
RelationCognitionFallback InteractionRelationKind = "COGNITION_FALLBACK"
|
||||||
|
RelationSupersedes InteractionRelationKind = "SUPERSEDES"
|
||||||
|
RelationClarifies InteractionRelationKind = "CLARIFIES"
|
||||||
|
ProjectRolePrimary ProjectReferenceRole = "PRIMARY"
|
||||||
|
ProjectRoleRelated ProjectReferenceRole = "RELATED"
|
||||||
|
ProjectRoleReferenceSource ProjectReferenceRole = "REFERENCE_SOURCE"
|
||||||
|
ClarificationNotRequired ClarificationState = "NOT_REQUIRED"
|
||||||
|
ClarificationRequired ClarificationState = "REQUIRED"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Conversation struct {
|
type Conversation struct {
|
||||||
@@ -74,6 +88,29 @@ type InteractionRelation struct {
|
|||||||
RecordedAtUnixMillis int64
|
RecordedAtUnixMillis int64
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type UtteranceInterpretation struct {
|
||||||
|
ID InterpretationID
|
||||||
|
UtteranceID HumanUtteranceID
|
||||||
|
SpeechAct SpeechAct
|
||||||
|
ConfidenceMillis int64
|
||||||
|
Source string
|
||||||
|
AssertionVerification string
|
||||||
|
RequestedOutcome string
|
||||||
|
Clarification ClarificationState
|
||||||
|
RecordedAtUnixMillis int64
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProjectReference struct {
|
||||||
|
ID ProjectReferenceID
|
||||||
|
UtteranceID HumanUtteranceID
|
||||||
|
ProjectID situation.ProjectID
|
||||||
|
ProjectDisplayName string
|
||||||
|
Role ProjectReferenceRole
|
||||||
|
Provenance situation.ProjectResolutionProvenance
|
||||||
|
ConfidenceMillis int64
|
||||||
|
RecordedAtUnixMillis int64
|
||||||
|
}
|
||||||
|
|
||||||
type ChatMessageRequest struct {
|
type ChatMessageRequest struct {
|
||||||
ConversationID ConversationID
|
ConversationID ConversationID
|
||||||
Message string
|
Message string
|
||||||
@@ -88,4 +125,7 @@ type ChatMessageResponse struct {
|
|||||||
InterpretationState InterpretationState
|
InterpretationState InterpretationState
|
||||||
NeedsCognition bool
|
NeedsCognition bool
|
||||||
FallbackText string
|
FallbackText string
|
||||||
|
ActiveProjectName string
|
||||||
|
ProjectProvenance situation.ProjectResolutionProvenance
|
||||||
|
Clarification ClarificationState
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,19 @@ type FocusBindingID ids.ID
|
|||||||
type SessionID ids.ID
|
type SessionID ids.ID
|
||||||
type WorkspaceID ids.ID
|
type WorkspaceID ids.ID
|
||||||
type ProjectID ids.ID
|
type ProjectID ids.ID
|
||||||
|
type ProjectAlias string
|
||||||
|
type ProjectResolutionProvenance string
|
||||||
|
|
||||||
|
const (
|
||||||
|
ProjectResolutionExplicit ProjectResolutionProvenance = "EXPLICIT"
|
||||||
|
ProjectResolutionInheritedFocus ProjectResolutionProvenance = "INHERITED_FOCUS"
|
||||||
|
ProjectResolutionEntityReference ProjectResolutionProvenance = "ENTITY_REFERENCE"
|
||||||
|
ProjectResolutionTaskReference ProjectResolutionProvenance = "TASK_REFERENCE"
|
||||||
|
ProjectResolutionRepositoryReference ProjectResolutionProvenance = "REPOSITORY_REFERENCE"
|
||||||
|
ProjectResolutionInferred ProjectResolutionProvenance = "INFERRED"
|
||||||
|
ProjectResolutionUserConfirmed ProjectResolutionProvenance = "USER_CONFIRMED"
|
||||||
|
ProjectResolutionAmbiguous ProjectResolutionProvenance = "AMBIGUOUS"
|
||||||
|
)
|
||||||
|
|
||||||
// FocusBinding is the situation-owned runtime context used to resolve a request.
|
// FocusBinding is the situation-owned runtime context used to resolve a request.
|
||||||
// User text is deliberately absent: textual similarity is not a routing signal.
|
// User text is deliberately absent: textual similarity is not a routing signal.
|
||||||
@@ -30,3 +43,21 @@ type RuntimeBinding struct {
|
|||||||
SessionID SessionID
|
SessionID SessionID
|
||||||
Workspace WorkspaceID
|
Workspace WorkspaceID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ProjectDescriptor struct {
|
||||||
|
ID ProjectID
|
||||||
|
DisplayName string
|
||||||
|
Aliases []ProjectAlias
|
||||||
|
RecordedAtUnixMillis int64
|
||||||
|
}
|
||||||
|
|
||||||
|
type ActiveProjectFocus struct {
|
||||||
|
ActorID identity.ActorID
|
||||||
|
PrincipalID identity.PrincipalID
|
||||||
|
ProjectID ProjectID
|
||||||
|
ProjectDisplayName string
|
||||||
|
Provenance ProjectResolutionProvenance
|
||||||
|
ConfidenceMillis int64
|
||||||
|
SourceUtteranceID string
|
||||||
|
UpdatedAtUnixMillis int64
|
||||||
|
}
|
||||||
|
|||||||
@@ -149,6 +149,45 @@ CREATE TABLE IF NOT EXISTS situation.focus_bindings (
|
|||||||
active BOOLEAN NOT NULL,
|
active BOOLEAN NOT NULL,
|
||||||
recorded_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
|
recorded_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
);
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS situation.projects (
|
||||||
|
project_id TEXT PRIMARY KEY,
|
||||||
|
display_name TEXT NOT NULL,
|
||||||
|
recorded_at_unix_millis BIGINT NOT NULL,
|
||||||
|
CHECK (project_id <> ''),
|
||||||
|
CHECK (display_name <> '')
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS situation.project_aliases (
|
||||||
|
project_id TEXT NOT NULL,
|
||||||
|
alias TEXT NOT NULL,
|
||||||
|
recorded_at_unix_millis BIGINT NOT NULL,
|
||||||
|
PRIMARY KEY (project_id, alias),
|
||||||
|
CHECK (project_id <> ''),
|
||||||
|
CHECK (alias <> '')
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS situation.active_project_focus (
|
||||||
|
actor_id TEXT NOT NULL,
|
||||||
|
principal_id TEXT NOT NULL,
|
||||||
|
project_id TEXT NOT NULL,
|
||||||
|
project_display_name TEXT NOT NULL,
|
||||||
|
provenance TEXT NOT NULL,
|
||||||
|
confidence_millis BIGINT NOT NULL,
|
||||||
|
source_utterance_id TEXT NOT NULL,
|
||||||
|
updated_at_unix_millis BIGINT NOT NULL,
|
||||||
|
PRIMARY KEY (actor_id, principal_id),
|
||||||
|
CHECK (provenance IN ('EXPLICIT','INHERITED_FOCUS','ENTITY_REFERENCE','TASK_REFERENCE','REPOSITORY_REFERENCE','INFERRED','USER_CONFIRMED','AMBIGUOUS')),
|
||||||
|
CHECK (confidence_millis BETWEEN 0 AND 1000)
|
||||||
|
);
|
||||||
|
INSERT INTO situation.projects(project_id, display_name, recorded_at_unix_millis)
|
||||||
|
VALUES ('assistant', 'Assistant', 1), ('upo', 'УПО', 1)
|
||||||
|
ON CONFLICT (project_id) DO NOTHING;
|
||||||
|
INSERT INTO situation.project_aliases(project_id, alias, recorded_at_unix_millis)
|
||||||
|
VALUES
|
||||||
|
('assistant', 'assistant', 1),
|
||||||
|
('assistant', 'домовой', 1),
|
||||||
|
('assistant', 'ассистент', 1),
|
||||||
|
('upo', 'упо', 1),
|
||||||
|
('upo', 'upo', 1)
|
||||||
|
ON CONFLICT (project_id, alias) DO NOTHING;
|
||||||
RESET ROLE;
|
RESET ROLE;
|
||||||
|
|
||||||
SET ROLE capability_role;
|
SET ROLE capability_role;
|
||||||
@@ -352,7 +391,35 @@ CREATE TABLE IF NOT EXISTS interaction.relations (
|
|||||||
to_utterance_id TEXT NOT NULL,
|
to_utterance_id TEXT NOT NULL,
|
||||||
kind TEXT NOT NULL,
|
kind TEXT NOT NULL,
|
||||||
recorded_at_unix_millis BIGINT NOT NULL,
|
recorded_at_unix_millis BIGINT NOT NULL,
|
||||||
CHECK (kind IN ('CORRECTS','RESPONDS_TO','REQUESTS_COGNITION','COGNITION_FALLBACK'))
|
CHECK (kind IN ('CORRECTS','RESPONDS_TO','REQUESTS_COGNITION','COGNITION_FALLBACK','SUPERSEDES','CLARIFIES'))
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS interaction.utterance_interpretations (
|
||||||
|
interpretation_id TEXT PRIMARY KEY,
|
||||||
|
utterance_id TEXT NOT NULL,
|
||||||
|
speech_act TEXT NOT NULL,
|
||||||
|
confidence_millis BIGINT NOT NULL,
|
||||||
|
source TEXT NOT NULL,
|
||||||
|
assertion_verification TEXT NOT NULL,
|
||||||
|
requested_outcome TEXT NOT NULL DEFAULT '',
|
||||||
|
clarification TEXT NOT NULL,
|
||||||
|
recorded_at_unix_millis BIGINT NOT NULL,
|
||||||
|
CHECK (speech_act IN ('DIRECTIVE','ASSERTION','DECISION','PREFERENCE','QUESTION','HYPOTHESIS','CORRECTION','REQUEST_TO_INVESTIGATE','CONFIRMATION','REJECTION','CANCELLATION','CONVERSATIONAL','UNKNOWN')),
|
||||||
|
CHECK (confidence_millis BETWEEN 0 AND 1000),
|
||||||
|
CHECK (assertion_verification IN ('UNVERIFIED','VERIFIED','REJECTED','NOT_APPLICABLE')),
|
||||||
|
CHECK (clarification IN ('NOT_REQUIRED','REQUIRED'))
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS interaction.project_references (
|
||||||
|
project_reference_id TEXT PRIMARY KEY,
|
||||||
|
utterance_id TEXT NOT NULL,
|
||||||
|
project_id TEXT NOT NULL,
|
||||||
|
project_display_name TEXT NOT NULL,
|
||||||
|
role TEXT NOT NULL,
|
||||||
|
provenance TEXT NOT NULL,
|
||||||
|
confidence_millis BIGINT NOT NULL,
|
||||||
|
recorded_at_unix_millis BIGINT NOT NULL,
|
||||||
|
CHECK (role IN ('PRIMARY','RELATED','REFERENCE_SOURCE')),
|
||||||
|
CHECK (provenance IN ('EXPLICIT','INHERITED_FOCUS','ENTITY_REFERENCE','TASK_REFERENCE','REPOSITORY_REFERENCE','INFERRED','USER_CONFIRMED','AMBIGUOUS')),
|
||||||
|
CHECK (confidence_millis BETWEEN 0 AND 1000)
|
||||||
);
|
);
|
||||||
RESET ROLE;
|
RESET ROLE;
|
||||||
COMMIT;
|
COMMIT;
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
SET ROLE situation_role;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS situation.projects (
|
||||||
|
project_id TEXT PRIMARY KEY,
|
||||||
|
display_name TEXT NOT NULL,
|
||||||
|
recorded_at_unix_millis BIGINT NOT NULL,
|
||||||
|
CHECK (project_id <> ''),
|
||||||
|
CHECK (display_name <> '')
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS situation.project_aliases (
|
||||||
|
project_id TEXT NOT NULL,
|
||||||
|
alias TEXT NOT NULL,
|
||||||
|
recorded_at_unix_millis BIGINT NOT NULL,
|
||||||
|
PRIMARY KEY (project_id, alias),
|
||||||
|
CHECK (project_id <> ''),
|
||||||
|
CHECK (alias <> '')
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS situation.active_project_focus (
|
||||||
|
actor_id TEXT NOT NULL,
|
||||||
|
principal_id TEXT NOT NULL,
|
||||||
|
project_id TEXT NOT NULL,
|
||||||
|
project_display_name TEXT NOT NULL,
|
||||||
|
provenance TEXT NOT NULL,
|
||||||
|
confidence_millis BIGINT NOT NULL,
|
||||||
|
source_utterance_id TEXT NOT NULL,
|
||||||
|
updated_at_unix_millis BIGINT NOT NULL,
|
||||||
|
PRIMARY KEY (actor_id, principal_id),
|
||||||
|
CHECK (provenance IN ('EXPLICIT','INHERITED_FOCUS','ENTITY_REFERENCE','TASK_REFERENCE','REPOSITORY_REFERENCE','INFERRED','USER_CONFIRMED','AMBIGUOUS')),
|
||||||
|
CHECK (confidence_millis BETWEEN 0 AND 1000)
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO situation.projects(project_id, display_name, recorded_at_unix_millis)
|
||||||
|
VALUES ('assistant', 'Assistant', 1), ('upo', 'УПО', 1)
|
||||||
|
ON CONFLICT (project_id) DO NOTHING;
|
||||||
|
|
||||||
|
INSERT INTO situation.project_aliases(project_id, alias, recorded_at_unix_millis)
|
||||||
|
VALUES
|
||||||
|
('assistant', 'assistant', 1),
|
||||||
|
('assistant', 'домовой', 1),
|
||||||
|
('assistant', 'ассистент', 1),
|
||||||
|
('upo', 'упо', 1),
|
||||||
|
('upo', 'upo', 1)
|
||||||
|
ON CONFLICT (project_id, alias) DO NOTHING;
|
||||||
|
|
||||||
|
RESET ROLE;
|
||||||
|
|
||||||
|
SET ROLE interaction_role;
|
||||||
|
|
||||||
|
ALTER TABLE interaction.relations DROP CONSTRAINT IF EXISTS relations_kind_check;
|
||||||
|
ALTER TABLE interaction.relations
|
||||||
|
ADD CONSTRAINT relations_kind_check
|
||||||
|
CHECK (kind IN ('CORRECTS','RESPONDS_TO','REQUESTS_COGNITION','COGNITION_FALLBACK','SUPERSEDES','CLARIFIES'));
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS interaction.utterance_interpretations (
|
||||||
|
interpretation_id TEXT PRIMARY KEY,
|
||||||
|
utterance_id TEXT NOT NULL,
|
||||||
|
speech_act TEXT NOT NULL,
|
||||||
|
confidence_millis BIGINT NOT NULL,
|
||||||
|
source TEXT NOT NULL,
|
||||||
|
assertion_verification TEXT NOT NULL,
|
||||||
|
requested_outcome TEXT NOT NULL DEFAULT '',
|
||||||
|
clarification TEXT NOT NULL,
|
||||||
|
recorded_at_unix_millis BIGINT NOT NULL,
|
||||||
|
CHECK (speech_act IN ('DIRECTIVE','ASSERTION','DECISION','PREFERENCE','QUESTION','HYPOTHESIS','CORRECTION','REQUEST_TO_INVESTIGATE','CONFIRMATION','REJECTION','CANCELLATION','CONVERSATIONAL','UNKNOWN')),
|
||||||
|
CHECK (confidence_millis BETWEEN 0 AND 1000),
|
||||||
|
CHECK (assertion_verification IN ('UNVERIFIED','VERIFIED','REJECTED','NOT_APPLICABLE')),
|
||||||
|
CHECK (clarification IN ('NOT_REQUIRED','REQUIRED'))
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS interaction.project_references (
|
||||||
|
project_reference_id TEXT PRIMARY KEY,
|
||||||
|
utterance_id TEXT NOT NULL,
|
||||||
|
project_id TEXT NOT NULL,
|
||||||
|
project_display_name TEXT NOT NULL,
|
||||||
|
role TEXT NOT NULL,
|
||||||
|
provenance TEXT NOT NULL,
|
||||||
|
confidence_millis BIGINT NOT NULL,
|
||||||
|
recorded_at_unix_millis BIGINT NOT NULL,
|
||||||
|
CHECK (role IN ('PRIMARY','RELATED','REFERENCE_SOURCE')),
|
||||||
|
CHECK (provenance IN ('EXPLICIT','INHERITED_FOCUS','ENTITY_REFERENCE','TASK_REFERENCE','REPOSITORY_REFERENCE','INFERRED','USER_CONFIRMED','AMBIGUOUS')),
|
||||||
|
CHECK (confidence_millis BETWEEN 0 AND 1000)
|
||||||
|
);
|
||||||
|
|
||||||
|
RESET ROLE;
|
||||||
|
COMMIT;
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
param(
|
param(
|
||||||
[string]$DockerContext = 'docker-prod',
|
[string]$DockerContext = 'docker-prod',
|
||||||
[string]$SshHost = 'docker',
|
[string]$SshHost = 'docker',
|
||||||
[string]$Endpoint = 'https://assistent.cin.su/healthz',
|
[string]$Endpoint = 'https://assistant.cin.su/healthz',
|
||||||
[string]$RemoteBackupRoot = '/home/m/assistant-backups/postgres',
|
[string]$RemoteBackupRoot = '/home/m/assistant-backups/postgres',
|
||||||
[string]$NasBackupRoot = '\\nas\MST\codex\_backups\domovoy\postgres',
|
[string]$NasBackupRoot = '\\nas\MST\codex\_backups\domovoy\postgres',
|
||||||
[string]$NasStatusPath = '\\nas\MST\codex\_monitoring\domovoy\status.json',
|
[string]$NasStatusPath = '\\nas\MST\codex\_monitoring\domovoy\status.json',
|
||||||
@@ -102,15 +102,15 @@ try {
|
|||||||
catch { Add-Check 'tls-certificate' $false $_.Exception.Message }
|
catch { Add-Check 'tls-certificate' $false $_.Exception.Message }
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$assistantAddresses = @(Resolve-DnsName assistent.cin.su -Server 8.8.8.8 -Type A -DnsOnly | Where-Object Type -eq A | Select-Object -ExpandProperty IPAddress | Sort-Object -Unique)
|
$assistantAddresses = @(Resolve-DnsName assistant.cin.su -Server 8.8.8.8 -Type A -DnsOnly | Where-Object Type -eq A | Select-Object -ExpandProperty IPAddress | Sort-Object -Unique)
|
||||||
$dockerAddresses = @(Resolve-DnsName docker.cin.su -Server 8.8.8.8 -Type A -DnsOnly | Where-Object Type -eq A | Select-Object -ExpandProperty IPAddress | Sort-Object -Unique)
|
$dockerAddresses = @(Resolve-DnsName docker.cin.su -Server 8.8.8.8 -Type A -DnsOnly | Where-Object Type -eq A | Select-Object -ExpandProperty IPAddress | Sort-Object -Unique)
|
||||||
$same = $assistantAddresses.Count -gt 0 -and $dockerAddresses.Count -gt 0 -and @(Compare-Object $assistantAddresses $dockerAddresses).Count -eq 0
|
$same = $assistantAddresses.Count -gt 0 -and $dockerAddresses.Count -gt 0 -and @(Compare-Object $assistantAddresses $dockerAddresses).Count -eq 0
|
||||||
Add-Check 'public-dns' $same "assistent=$($assistantAddresses -join ',') docker=$($dockerAddresses -join ',')"
|
Add-Check 'public-dns' $same "assistant=$($assistantAddresses -join ',') docker=$($dockerAddresses -join ',')"
|
||||||
}
|
}
|
||||||
catch { Add-Check 'public-dns' $false $_.Exception.Message }
|
catch { Add-Check 'public-dns' $false $_.Exception.Message }
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$latestProduction = ((& ssh.exe $SshHost "find '$RemoteBackupRoot' -mindepth 1 -maxdepth 1 -type d ! -name '.*' -printf '%f\n' | sort | tail -n1") -join '').Trim()
|
$latestProduction = ((& ssh.exe $SshHost "find '$RemoteBackupRoot' -mindepth 1 -maxdepth 1 -type d ! -name '.*' -printf '%f\n' | grep -E '^[0-9]{8}T[0-9]{6}Z$' | sort | tail -n1") -join '').Trim()
|
||||||
if ($LASTEXITCODE -ne 0 -or $latestProduction -notmatch '^\d{8}T\d{6}Z$') { throw 'invalid latest production backup' }
|
if ($LASTEXITCODE -ne 0 -or $latestProduction -notmatch '^\d{8}T\d{6}Z$') { throw 'invalid latest production backup' }
|
||||||
$productionTime = [DateTime]::ParseExact($latestProduction, "yyyyMMdd'T'HHmmss'Z'", [Globalization.CultureInfo]::InvariantCulture, [Globalization.DateTimeStyles]::AssumeUniversal -bor [Globalization.DateTimeStyles]::AdjustToUniversal)
|
$productionTime = [DateTime]::ParseExact($latestProduction, "yyyyMMdd'T'HHmmss'Z'", [Globalization.CultureInfo]::InvariantCulture, [Globalization.DateTimeStyles]::AssumeUniversal -bor [Globalization.DateTimeStyles]::AdjustToUniversal)
|
||||||
$age = ([DateTime]::UtcNow - $productionTime).TotalHours
|
$age = ([DateTime]::UtcNow - $productionTime).TotalHours
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"task_file": "dev/tasks/DEV-000061-human-interaction-foundation.json",
|
"task_file": "dev/tasks/DEV-000062-human-intent-evidence-project-context.json",
|
||||||
"task_id": "DEV-000061",
|
"task_id": "DEV-000062",
|
||||||
"task_revision": 2
|
"task_revision": 1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,140 @@
|
|||||||
|
{
|
||||||
|
"task_id": "DEV-000062",
|
||||||
|
"task_revision": 1,
|
||||||
|
"architecture_revision": "ADR-0011",
|
||||||
|
"base_repo_revision": "69ba99099845ef24affc15bdd8f152717183bb10",
|
||||||
|
"risk_tier": "E",
|
||||||
|
"intent": "Add human intent, assertion/evidence boundary, project context and active focus semantics to Web Chat without new authority, provider binding, or execution capability.",
|
||||||
|
"requirement_ids": [
|
||||||
|
"ADR-0009",
|
||||||
|
"ADR-0010",
|
||||||
|
"ADR-0011",
|
||||||
|
"INV-EPI-001",
|
||||||
|
"INV-AUTH-001"
|
||||||
|
],
|
||||||
|
"allowed_change_scope": {
|
||||||
|
"modules": [
|
||||||
|
"interaction",
|
||||||
|
"situation",
|
||||||
|
"epistemics",
|
||||||
|
"work"
|
||||||
|
],
|
||||||
|
"paths": [
|
||||||
|
"architecture/adrs/ADR-0011-human-intent-project-context.md",
|
||||||
|
"architecture/architecture-registry.yaml",
|
||||||
|
"architecture/architecture-registry.json",
|
||||||
|
"contracts/interaction/**",
|
||||||
|
"contracts/situation/**",
|
||||||
|
"contracts/epistemics/**",
|
||||||
|
"modules/interaction/**",
|
||||||
|
"modules/situation/**",
|
||||||
|
"adapters/postgres/**",
|
||||||
|
"database/bootstrap/**",
|
||||||
|
"database/migrations/**",
|
||||||
|
"cmd/core-runtime/**",
|
||||||
|
"cmd/creator-web/**",
|
||||||
|
"deploy/**",
|
||||||
|
"tests/human_interaction/**",
|
||||||
|
"tests/architecture/**",
|
||||||
|
"docs/PROJECT_STATUS.md",
|
||||||
|
"dev/ACTIVE_TASK.json",
|
||||||
|
"dev/tasks/DEV-000062-human-intent-evidence-project-context.json"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"forbidden_change_scope": {
|
||||||
|
"paths": [
|
||||||
|
"architecture/ARCHITECTURE_CONSTITUTION.md",
|
||||||
|
"modules/authority/**",
|
||||||
|
"modules/commit/**",
|
||||||
|
"cmd/commit-runtime/**",
|
||||||
|
"cmd/cognitive-worker/**",
|
||||||
|
"security/**",
|
||||||
|
"ci/**",
|
||||||
|
".gitea/**"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"allowed_architecture_delta": {
|
||||||
|
"new_cross_module_dependencies": 3,
|
||||||
|
"new_canonical_owners": 0,
|
||||||
|
"new_privileges": 0,
|
||||||
|
"new_third_party_dependencies": 0,
|
||||||
|
"new_provider_adapters": 0
|
||||||
|
},
|
||||||
|
"contracts": {
|
||||||
|
"consume": [
|
||||||
|
"CreatorSession",
|
||||||
|
"Interaction Conversation/HumanUtterance",
|
||||||
|
"Situation ProjectRegistry/ActiveFocus",
|
||||||
|
"Epistemics assertion boundary",
|
||||||
|
"Cognitive Gateway advisory"
|
||||||
|
],
|
||||||
|
"may_change": [
|
||||||
|
"contracts/interaction@v1",
|
||||||
|
"contracts/situation@v1",
|
||||||
|
"contracts/epistemics@v1"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"semantic_owner": "interaction+situation",
|
||||||
|
"canonical_state": [
|
||||||
|
"UtteranceInterpretation",
|
||||||
|
"ProjectReference",
|
||||||
|
"ConversationProjectFocus",
|
||||||
|
"SituationProjectRegistry",
|
||||||
|
"SituationActiveFocus"
|
||||||
|
],
|
||||||
|
"state_transitions": [
|
||||||
|
"utterance RECORDED->INTERPRETED",
|
||||||
|
"active focus SWITCHED by explicit human intent",
|
||||||
|
"correction appended without rewriting prior utterance"
|
||||||
|
],
|
||||||
|
"side_effects_and_privileges": [
|
||||||
|
"authenticated human semantic persistence",
|
||||||
|
"project focus reference update only",
|
||||||
|
"no execution",
|
||||||
|
"no verified fact creation",
|
||||||
|
"no ACTIVE binding promotion"
|
||||||
|
],
|
||||||
|
"persistence_and_migrations": [
|
||||||
|
"interaction owner-local interpretation/project reference tables",
|
||||||
|
"situation owner-local project registry and active focus tables",
|
||||||
|
"no cross-owner FK",
|
||||||
|
"no cross-owner SQL join dependency"
|
||||||
|
],
|
||||||
|
"failure_retry_restart": [
|
||||||
|
"cognition unavailable keeps persistence working",
|
||||||
|
"invalid candidate interpretation fails closed",
|
||||||
|
"ambiguous destructive request requires clarification",
|
||||||
|
"original human utterance and intent remain append-only"
|
||||||
|
],
|
||||||
|
"assumptions": [
|
||||||
|
"DEV-000061 is production baseline",
|
||||||
|
"existing CPU CANARY remains allowed only through current policy",
|
||||||
|
"ACTIVE bindings remain none",
|
||||||
|
"Project Registry V1 may be minimal and alias-based under Situation"
|
||||||
|
],
|
||||||
|
"acceptance_scenarios": [
|
||||||
|
"explicit project focus switch",
|
||||||
|
"assertion persists as unverified",
|
||||||
|
"directive persists without execution claim",
|
||||||
|
"correction/supersession append-only",
|
||||||
|
"inherited focus for safe non-destructive reference",
|
||||||
|
"multiple project references in one utterance",
|
||||||
|
"ambiguous destructive request asks clarification",
|
||||||
|
"cognition unavailable does not corrupt semantic state"
|
||||||
|
],
|
||||||
|
"required_tests": [
|
||||||
|
"go test ./...",
|
||||||
|
"go vet ./...",
|
||||||
|
"architecture",
|
||||||
|
"owner ACL",
|
||||||
|
"LLM-free",
|
||||||
|
"human interaction",
|
||||||
|
"production live acceptance"
|
||||||
|
],
|
||||||
|
"terminal_gap_states": [
|
||||||
|
"BLOCKED_BY_ARCHITECTURE_GAP",
|
||||||
|
"BLOCKED_BY_SPEC_GAP",
|
||||||
|
"BLOCKED_BY_MISSING_CAPABILITY",
|
||||||
|
"BLOCKED_BY_CONFLICT"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,16 +1,19 @@
|
|||||||
# Assistant project status
|
# Assistant project status
|
||||||
|
|
||||||
Audit date: 2026-08-17. Active implementation: DEV-000061 revision 2.
|
Audit date: 2026-08-17. Active implementation: DEV-000062 revision 1.
|
||||||
|
|
||||||
## DEV-000061 checkpoint
|
## DEV-000062 checkpoint
|
||||||
|
|
||||||
- `DEV_000061_STATUS = IN_PROGRESS`
|
- `DEV_000062_STATUS = COMPLETED`
|
||||||
- `LAST_COMPLETED = DEV-000061 packet rev.2, ADR-0009, ADR-0010, Interaction registry declaration, CredentialRef file resolver foundation, interaction schema migration/bootstrap, interaction repository, Core Runtime chat persistence endpoint, Creator Web /chat surface and LOCAL_ONLY Cognitive Gateway handoff`
|
- `LAST_COMPLETED = DEV-000062 packet rev.1, ADR-0011, Interaction/Situation/Epistemics contract extensions, Situation Project Registry V1, active project focus semantics, Interaction utterance interpretation/project references, append-only correction/supersession foundation, Core Runtime semantic persistence wiring, Creator Web /chat focus display, production deployment and live /chat acceptance`
|
||||||
- `TESTS_PASS = go test ./...; go vet ./...; go run ./cmd/devctl preflight -task dev/tasks/DEV-000061-human-interaction-foundation.json; go run ./cmd/devctl check-architecture`
|
- `TESTS_PASS = go test ./...; go vet ./...; go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json; go run ./cmd/devctl check-architecture; go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json; tests/cognitive_advisory/cognitive_worker_env_policy_test.ps1; tests/owner_acl/verify.ps1 -DockerContext docker-prod -PostgresContainer assistant-postgres-1; production live /chat acceptance`
|
||||||
- `ACTIVE_BINDINGS = none`
|
- `ACTIVE_BINDINGS = none`
|
||||||
- `CANARY_BINDING = unchanged; not promoted to ACTIVE`
|
- `CANARY_BINDING = unchanged; not promoted to ACTIVE`
|
||||||
- `OPENAI_REAL_CREDENTIAL = not required / not activated`
|
- `OPENAI_REAL_CREDENTIAL = not required / not activated`
|
||||||
- `NEXT_STEP = production interaction_role delivery: copy protected local interaction_role credential into the existing assistant_core_secrets backend as /run/secrets/interaction_db_password, set/validate PostgreSQL interaction_role password, apply migration, rebuild/deploy affected production services, then run live /chat acceptance`
|
- `PRODUCTION_REVISION = 445e9edd7c5b614b58dd925f89aa9fe49cf0a1d6`
|
||||||
|
- `HUMAN_CHAT_URL = https://assistant.cin.su/chat`
|
||||||
|
- `LIVE_ACCEPTANCE = PASS: explicit УПО focus, unverified assertion, directive/no execution, correction, explicit Assistant focus, current-focus question answered without cognition`
|
||||||
|
- `NEXT_STEP = DEV-000063 Project Copilot planning, if separately approved`
|
||||||
- `REAL_BLOCKERS = none known`
|
- `REAL_BLOCKERS = none known`
|
||||||
|
|
||||||
## Current milestone
|
## Current milestone
|
||||||
|
|||||||
@@ -99,13 +99,13 @@ func NewHumanUtterance(conversation interaction.Conversation, request interactio
|
|||||||
func ClassifySpeechAct(message string, corrects interaction.HumanUtteranceID) interaction.SpeechAct {
|
func ClassifySpeechAct(message string, corrects interaction.HumanUtteranceID) interaction.SpeechAct {
|
||||||
lower := strings.ToLower(strings.TrimSpace(message))
|
lower := strings.ToLower(strings.TrimSpace(message))
|
||||||
switch {
|
switch {
|
||||||
case corrects != "" || strings.Contains(lower, "исправ") || strings.Contains(lower, "не так"):
|
case corrects != "" || strings.Contains(lower, "исправ") || strings.Contains(lower, "не так") || strings.HasPrefix(lower, "нет,"):
|
||||||
return interaction.SpeechActCorrection
|
return interaction.SpeechActCorrection
|
||||||
case strings.Contains(lower, "?") || strings.HasPrefix(lower, "что ") || strings.HasPrefix(lower, "как ") || strings.HasPrefix(lower, "почему ") || strings.HasPrefix(lower, "кто "):
|
case strings.Contains(lower, "?") || strings.HasPrefix(lower, "что ") || strings.HasPrefix(lower, "как ") || strings.HasPrefix(lower, "почему ") || strings.HasPrefix(lower, "кто "):
|
||||||
return interaction.SpeechActQuestion
|
return interaction.SpeechActQuestion
|
||||||
case strings.Contains(lower, "проверь") || strings.Contains(lower, "исслед") || strings.Contains(lower, "разбер"):
|
case strings.Contains(lower, "проверь") || strings.Contains(lower, "исслед") || strings.Contains(lower, "разбер"):
|
||||||
return interaction.SpeechActRequestToInvestigate
|
return interaction.SpeechActRequestToInvestigate
|
||||||
case strings.Contains(lower, "сделай") || strings.Contains(lower, "запусти") || strings.Contains(lower, "создай") || strings.Contains(lower, "настрой"):
|
case strings.Contains(lower, "сделай") || strings.Contains(lower, "запусти") || strings.Contains(lower, "создай") || strings.Contains(lower, "настрой") || strings.Contains(lower, "добавь") || strings.Contains(lower, "измени") || strings.Contains(lower, "поменяй") || strings.Contains(lower, "используй") || strings.Contains(lower, "удали") || strings.Contains(lower, "удалить"):
|
||||||
return interaction.SpeechActDirective
|
return interaction.SpeechActDirective
|
||||||
case strings.HasPrefix(lower, "я решил") || strings.Contains(lower, "решение"):
|
case strings.HasPrefix(lower, "я решил") || strings.Contains(lower, "решение"):
|
||||||
return interaction.SpeechActDecision
|
return interaction.SpeechActDecision
|
||||||
@@ -113,9 +113,9 @@ func ClassifySpeechAct(message string, corrects interaction.HumanUtteranceID) in
|
|||||||
return interaction.SpeechActPreference
|
return interaction.SpeechActPreference
|
||||||
case strings.HasPrefix(lower, "думаю") || strings.Contains(lower, "гипотез"):
|
case strings.HasPrefix(lower, "думаю") || strings.Contains(lower, "гипотез"):
|
||||||
return interaction.SpeechActHypothesis
|
return interaction.SpeechActHypothesis
|
||||||
case strings.Contains(lower, "да") || strings.Contains(lower, "подтверж"):
|
case lower == "да" || strings.HasPrefix(lower, "да,") || strings.Contains(lower, "подтверж"):
|
||||||
return interaction.SpeechActConfirmation
|
return interaction.SpeechActConfirmation
|
||||||
case strings.Contains(lower, "нет") || strings.Contains(lower, "отмен"):
|
case lower == "нет" || strings.HasPrefix(lower, "нет,") || strings.Contains(lower, "отмен"):
|
||||||
return interaction.SpeechActRejection
|
return interaction.SpeechActRejection
|
||||||
case strings.Contains(lower, "привет") || strings.Contains(lower, "здравств"):
|
case strings.Contains(lower, "привет") || strings.Contains(lower, "здравств"):
|
||||||
return interaction.SpeechActConversational
|
return interaction.SpeechActConversational
|
||||||
|
|||||||
@@ -0,0 +1,242 @@
|
|||||||
|
package domain
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.cin.su/m/Assistant/contracts/epistemics"
|
||||||
|
"git.cin.su/m/Assistant/contracts/interaction"
|
||||||
|
"git.cin.su/m/Assistant/contracts/situation"
|
||||||
|
"git.cin.su/m/Assistant/platform/ids"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SemanticResult struct {
|
||||||
|
Interpretation interaction.UtteranceInterpretation
|
||||||
|
Projects []interaction.ProjectReference
|
||||||
|
Relations []interaction.InteractionRelation
|
||||||
|
ActiveFocus *situation.ActiveProjectFocus
|
||||||
|
Reply string
|
||||||
|
NeedsCognition bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func InterpretUtterance(utterance interaction.HumanUtterance, projects []situation.ProjectDescriptor, previousFocus *situation.ActiveProjectFocus, now time.Time) (SemanticResult, error) {
|
||||||
|
act := ClassifySpeechAct(utterance.Content, utterance.CorrectsUtteranceID)
|
||||||
|
interpretationID, err := ids.New()
|
||||||
|
if err != nil {
|
||||||
|
return SemanticResult{}, err
|
||||||
|
}
|
||||||
|
millis := now.UnixMilli()
|
||||||
|
result := SemanticResult{
|
||||||
|
Interpretation: interaction.UtteranceInterpretation{
|
||||||
|
ID: interaction.InterpretationID(interpretationID),
|
||||||
|
UtteranceID: utterance.ID,
|
||||||
|
SpeechAct: act,
|
||||||
|
ConfidenceMillis: confidenceFor(act),
|
||||||
|
Source: "deterministic-v1",
|
||||||
|
AssertionVerification: string(epistemics.VerificationUnverified),
|
||||||
|
RequestedOutcome: requestedOutcome(utterance.Content, act),
|
||||||
|
Clarification: interaction.ClarificationNotRequired,
|
||||||
|
RecordedAtUnixMillis: millis,
|
||||||
|
},
|
||||||
|
NeedsCognition: NeedsCognition(act),
|
||||||
|
}
|
||||||
|
explicitFocus := explicitProjectFocus(utterance.Content, projects)
|
||||||
|
projectRefs, err := projectReferences(utterance, projects, explicitFocus, previousFocus, millis)
|
||||||
|
if err != nil {
|
||||||
|
return SemanticResult{}, err
|
||||||
|
}
|
||||||
|
result.Projects = projectRefs
|
||||||
|
if explicitFocus != nil {
|
||||||
|
result.ActiveFocus = &situation.ActiveProjectFocus{
|
||||||
|
ActorID: utterance.ActorID,
|
||||||
|
PrincipalID: utterance.PrincipalID,
|
||||||
|
ProjectID: explicitFocus.ProjectID,
|
||||||
|
ProjectDisplayName: explicitFocus.ProjectDisplayName,
|
||||||
|
Provenance: situation.ProjectResolutionExplicit,
|
||||||
|
ConfidenceMillis: explicitFocus.ConfidenceMillis,
|
||||||
|
SourceUtteranceID: string(utterance.ID),
|
||||||
|
UpdatedAtUnixMillis: millis,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if isAmbiguousDestructive(act, utterance.Content, result.Projects) {
|
||||||
|
result.Interpretation.Clarification = interaction.ClarificationRequired
|
||||||
|
result.Reply = "Уточните проект и цель: это потенциально разрушительное изменение, а текущая ссылка недостаточно надёжна."
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
if act == interaction.SpeechActCorrection {
|
||||||
|
relations, err := correctionRelations(utterance, millis)
|
||||||
|
if err != nil {
|
||||||
|
return SemanticResult{}, err
|
||||||
|
}
|
||||||
|
result.Relations = append(result.Relations, relations...)
|
||||||
|
}
|
||||||
|
result.Reply = semanticReply(utterance.Content, result, previousFocus)
|
||||||
|
if isCurrentFocusQuestion(utterance.Content, act) && previousFocus != nil {
|
||||||
|
result.NeedsCognition = false
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func explicitProjectFocus(message string, projects []situation.ProjectDescriptor) *interaction.ProjectReference {
|
||||||
|
lower := strings.ToLower(message)
|
||||||
|
if !(strings.Contains(lower, "работаем с") || strings.Contains(lower, "вернемся к") || strings.Contains(lower, "вернёмся к") || strings.HasPrefix(strings.TrimSpace(lower), "теперь ")) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
for _, project := range projects {
|
||||||
|
for _, alias := range projectAliases(project) {
|
||||||
|
if strings.Contains(lower, alias) {
|
||||||
|
return &interaction.ProjectReference{ProjectID: project.ID, ProjectDisplayName: project.DisplayName, Provenance: situation.ProjectResolutionExplicit, ConfidenceMillis: 980}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func projectReferences(utterance interaction.HumanUtterance, projects []situation.ProjectDescriptor, explicit *interaction.ProjectReference, previousFocus *situation.ActiveProjectFocus, millis int64) ([]interaction.ProjectReference, error) {
|
||||||
|
if explicit != nil {
|
||||||
|
ref, err := newProjectReference(utterance.ID, explicit.ProjectID, explicit.ProjectDisplayName, interaction.ProjectRolePrimary, explicit.Provenance, explicit.ConfidenceMillis, millis)
|
||||||
|
return []interaction.ProjectReference{ref}, err
|
||||||
|
}
|
||||||
|
lower := strings.ToLower(utterance.Content)
|
||||||
|
var refs []interaction.ProjectReference
|
||||||
|
for _, project := range projects {
|
||||||
|
for _, alias := range projectAliases(project) {
|
||||||
|
if strings.Contains(lower, alias) {
|
||||||
|
role := projectRoleForAlias(lower, alias)
|
||||||
|
ref, err := newProjectReference(utterance.ID, project.ID, project.DisplayName, role, situation.ProjectResolutionEntityReference, 850, millis)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
refs = append(refs, ref)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(refs) == 0 && previousFocus != nil && strings.Contains(lower, "туда") {
|
||||||
|
ref, err := newProjectReference(utterance.ID, previousFocus.ProjectID, previousFocus.ProjectDisplayName, interaction.ProjectRolePrimary, situation.ProjectResolutionInheritedFocus, 700, millis)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
refs = append(refs, ref)
|
||||||
|
}
|
||||||
|
if len(refs) > 1 {
|
||||||
|
hasPrimary := false
|
||||||
|
for _, ref := range refs {
|
||||||
|
hasPrimary = hasPrimary || ref.Role == interaction.ProjectRolePrimary
|
||||||
|
}
|
||||||
|
if !hasPrimary {
|
||||||
|
refs[len(refs)-1].Role = interaction.ProjectRolePrimary
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return refs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func projectRoleForAlias(lower string, alias string) interaction.ProjectReferenceRole {
|
||||||
|
aliasIndex := strings.Index(lower, alias)
|
||||||
|
if aliasIndex < 0 {
|
||||||
|
return interaction.ProjectRoleRelated
|
||||||
|
}
|
||||||
|
before := strings.TrimSpace(lower[:aliasIndex])
|
||||||
|
afterStart := aliasIndex + len(alias)
|
||||||
|
after := ""
|
||||||
|
if afterStart < len(lower) {
|
||||||
|
after = strings.TrimSpace(lower[afterStart:])
|
||||||
|
}
|
||||||
|
if strings.Contains(before, "как") || strings.Contains(before, "посмотри") || strings.Contains(after, "как источник") {
|
||||||
|
return interaction.ProjectRoleReferenceSource
|
||||||
|
}
|
||||||
|
targetMarkers := []string{"сделай", "добавь", "измени", "поменяй", "перенеси", "используй", "настрой", "в "}
|
||||||
|
for _, marker := range targetMarkers {
|
||||||
|
if strings.Contains(before, marker) {
|
||||||
|
return interaction.ProjectRolePrimary
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return interaction.ProjectRoleRelated
|
||||||
|
}
|
||||||
|
|
||||||
|
func newProjectReference(utteranceID interaction.HumanUtteranceID, projectID situation.ProjectID, display string, role interaction.ProjectReferenceRole, provenance situation.ProjectResolutionProvenance, confidence int64, millis int64) (interaction.ProjectReference, error) {
|
||||||
|
id, err := ids.New()
|
||||||
|
if err != nil {
|
||||||
|
return interaction.ProjectReference{}, err
|
||||||
|
}
|
||||||
|
return interaction.ProjectReference{ID: interaction.ProjectReferenceID(id), UtteranceID: utteranceID, ProjectID: projectID, ProjectDisplayName: display, Role: role, Provenance: provenance, ConfidenceMillis: confidence, RecordedAtUnixMillis: millis}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func projectAliases(project situation.ProjectDescriptor) []string {
|
||||||
|
values := []string{strings.ToLower(project.DisplayName), strings.ToLower(string(project.ID))}
|
||||||
|
for _, alias := range project.Aliases {
|
||||||
|
values = append(values, strings.ToLower(string(alias)))
|
||||||
|
}
|
||||||
|
return values
|
||||||
|
}
|
||||||
|
|
||||||
|
func correctionRelations(utterance interaction.HumanUtterance, millis int64) ([]interaction.InteractionRelation, error) {
|
||||||
|
if utterance.CorrectsUtteranceID == "" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
id, err := ids.New()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return []interaction.InteractionRelation{{ID: interaction.InteractionRelationID(id), FromUtteranceID: utterance.ID, ToUtteranceID: utterance.CorrectsUtteranceID, Kind: interaction.RelationSupersedes, RecordedAtUnixMillis: millis}}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func confidenceFor(act interaction.SpeechAct) int64 {
|
||||||
|
if act == interaction.SpeechActUnknown {
|
||||||
|
return 300
|
||||||
|
}
|
||||||
|
return 800
|
||||||
|
}
|
||||||
|
|
||||||
|
func requestedOutcome(message string, act interaction.SpeechAct) string {
|
||||||
|
if act == interaction.SpeechActDirective || act == interaction.SpeechActDecision || act == interaction.SpeechActRequestToInvestigate {
|
||||||
|
return strings.TrimSpace(message)
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func inheritedFocusAllowed(act interaction.SpeechAct, message string) bool {
|
||||||
|
return strings.Contains(strings.ToLower(message), "туда") && act != interaction.SpeechActUnknown
|
||||||
|
}
|
||||||
|
|
||||||
|
func isAmbiguousDestructive(act interaction.SpeechAct, message string, projects []interaction.ProjectReference) bool {
|
||||||
|
lower := strings.ToLower(message)
|
||||||
|
destructive := strings.Contains(lower, "удали") || strings.Contains(lower, "снеси") || strings.Contains(lower, "удалить")
|
||||||
|
if !destructive || act != interaction.SpeechActDirective {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if len(projects) == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return projects[0].Provenance == situation.ProjectResolutionInheritedFocus || projects[0].Provenance == situation.ProjectResolutionInferred
|
||||||
|
}
|
||||||
|
|
||||||
|
func semanticReply(message string, result SemanticResult, previousFocus *situation.ActiveProjectFocus) string {
|
||||||
|
if result.ActiveFocus != nil {
|
||||||
|
return "Фокус переключён: " + result.ActiveFocus.ProjectDisplayName + "."
|
||||||
|
}
|
||||||
|
switch result.Interpretation.SpeechAct {
|
||||||
|
case interaction.SpeechActAssertion:
|
||||||
|
return "Записал как утверждение человека. Это пока не проверенный факт."
|
||||||
|
case interaction.SpeechActDirective:
|
||||||
|
return "Записал просьбу как намерение пользователя. Ничего не выполнено без отдельного подтверждённого действия."
|
||||||
|
case interaction.SpeechActDecision:
|
||||||
|
return "Записал это как явное решение пользователя для дальнейшей обработки."
|
||||||
|
case interaction.SpeechActCorrection:
|
||||||
|
return "Исправление записано новым сообщением; прежняя история сохранена."
|
||||||
|
case interaction.SpeechActQuestion:
|
||||||
|
lower := strings.ToLower(message)
|
||||||
|
if previousFocus != nil && (strings.Contains(lower, "сейчас") || strings.Contains(lower, "что мы") || strings.Contains(lower, "делаем")) {
|
||||||
|
return "Сейчас в фокусе: " + previousFocus.ProjectDisplayName + "."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return FallbackText(result.Interpretation.SpeechAct)
|
||||||
|
}
|
||||||
|
|
||||||
|
func isCurrentFocusQuestion(message string, act interaction.SpeechAct) bool {
|
||||||
|
if act != interaction.SpeechActQuestion {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
lower := strings.ToLower(message)
|
||||||
|
return strings.Contains(lower, "сейчас") || strings.Contains(lower, "что мы") || strings.Contains(lower, "делаем")
|
||||||
|
}
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
package domain
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.cin.su/m/Assistant/contracts/identity"
|
||||||
|
"git.cin.su/m/Assistant/contracts/interaction"
|
||||||
|
"git.cin.su/m/Assistant/contracts/situation"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestInterpretUtteranceRecordsExplicitProjectFocus(t *testing.T) {
|
||||||
|
result := interpretForTest(t, "Сейчас работаем с УПО.", nil, "")
|
||||||
|
if result.ActiveFocus == nil || result.ActiveFocus.ProjectID != "upo" {
|
||||||
|
t.Fatalf("expected explicit UPO focus, got %#v", result.ActiveFocus)
|
||||||
|
}
|
||||||
|
if result.Interpretation.SpeechAct != interaction.SpeechActAssertion {
|
||||||
|
t.Fatalf("expected assertion, got %s", result.Interpretation.SpeechAct)
|
||||||
|
}
|
||||||
|
if result.Projects[0].Provenance != situation.ProjectResolutionExplicit {
|
||||||
|
t.Fatalf("expected explicit provenance, got %#v", result.Projects)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInterpretUtteranceAssertionIsNotVerifiedFact(t *testing.T) {
|
||||||
|
result := interpretForTest(t, "На сервере сейчас свободно 300 ГБ.", nil, "")
|
||||||
|
if result.Interpretation.SpeechAct != interaction.SpeechActAssertion {
|
||||||
|
t.Fatalf("expected assertion, got %s", result.Interpretation.SpeechAct)
|
||||||
|
}
|
||||||
|
if result.Interpretation.AssertionVerification != "UNVERIFIED" || !strings.Contains(result.Reply, "не проверенный факт") {
|
||||||
|
t.Fatalf("expected unverified human assertion, got %#v reply=%q", result.Interpretation, result.Reply)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInterpretUtteranceDirectiveRecordsOutcomeWithoutExecution(t *testing.T) {
|
||||||
|
result := interpretForTest(t, "Поменяй базу на УПО1.", activeFocus("upo", "УПО"), "")
|
||||||
|
if result.Interpretation.SpeechAct != interaction.SpeechActDirective {
|
||||||
|
t.Fatalf("expected directive, got %s", result.Interpretation.SpeechAct)
|
||||||
|
}
|
||||||
|
if result.Interpretation.RequestedOutcome == "" || !strings.Contains(result.Reply, "Ничего не выполнено") {
|
||||||
|
t.Fatalf("expected non-executed requested outcome, got %#v reply=%q", result.Interpretation, result.Reply)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInterpretUtteranceCorrectionIsAppendOnlySupersedes(t *testing.T) {
|
||||||
|
result := interpretForTest(t, "Нет, используй УПО2.", activeFocus("upo", "УПО"), "utterance-previous")
|
||||||
|
if result.Interpretation.SpeechAct != interaction.SpeechActCorrection {
|
||||||
|
t.Fatalf("expected correction, got %s", result.Interpretation.SpeechAct)
|
||||||
|
}
|
||||||
|
if len(result.Relations) != 1 || result.Relations[0].Kind != interaction.RelationSupersedes || result.Relations[0].ToUtteranceID != "utterance-previous" {
|
||||||
|
t.Fatalf("expected supersedes relation, got %#v", result.Relations)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInterpretUtteranceQuestionAboutCurrentWorkUsesFocus(t *testing.T) {
|
||||||
|
result := interpretForTest(t, "Что мы сейчас делаем?", activeFocus("assistant", "Assistant"), "")
|
||||||
|
if result.Interpretation.SpeechAct != interaction.SpeechActQuestion {
|
||||||
|
t.Fatalf("expected question, got %s", result.Interpretation.SpeechAct)
|
||||||
|
}
|
||||||
|
if !strings.Contains(result.Reply, "Assistant") {
|
||||||
|
t.Fatalf("expected focus-aware reply, got %q", result.Reply)
|
||||||
|
}
|
||||||
|
if result.NeedsCognition {
|
||||||
|
t.Fatalf("focus question should be answered from canonical focus without cognition")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInterpretUtteranceInheritedFocusForThere(t *testing.T) {
|
||||||
|
result := interpretForTest(t, "Добавь туда новую настройку.", activeFocus("assistant", "Assistant"), "")
|
||||||
|
if len(result.Projects) != 1 || result.Projects[0].ProjectID != "assistant" || result.Projects[0].Provenance != situation.ProjectResolutionInheritedFocus {
|
||||||
|
t.Fatalf("expected inherited Assistant focus, got %#v", result.Projects)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInterpretUtteranceMultiProjectReferenceRoles(t *testing.T) {
|
||||||
|
result := interpretForTest(t, "Посмотри, как это сделано в Assistant, и сделай аналогично в УПО.", nil, "")
|
||||||
|
if len(result.Projects) != 2 {
|
||||||
|
t.Fatalf("expected two project references, got %#v", result.Projects)
|
||||||
|
}
|
||||||
|
roles := map[situation.ProjectID]interaction.ProjectReferenceRole{}
|
||||||
|
for _, ref := range result.Projects {
|
||||||
|
roles[ref.ProjectID] = ref.Role
|
||||||
|
}
|
||||||
|
if roles["assistant"] != interaction.ProjectRoleReferenceSource || roles["upo"] != interaction.ProjectRolePrimary {
|
||||||
|
t.Fatalf("unexpected roles: %#v", roles)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInterpretUtteranceAmbiguousDestructiveRequiresClarification(t *testing.T) {
|
||||||
|
result := interpretForTest(t, "Удали туда старую базу.", activeFocus("assistant", "Assistant"), "")
|
||||||
|
if result.Interpretation.Clarification != interaction.ClarificationRequired {
|
||||||
|
t.Fatalf("expected clarification, got %#v", result.Interpretation)
|
||||||
|
}
|
||||||
|
if result.NeedsCognition {
|
||||||
|
t.Fatalf("ambiguous destructive directive must not require cognition")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func interpretForTest(t *testing.T, message string, previous *situation.ActiveProjectFocus, corrects interaction.HumanUtteranceID) SemanticResult {
|
||||||
|
t.Helper()
|
||||||
|
conversation := interaction.Conversation{ID: "conversation-1", ActorID: "actor-1", PrincipalID: "principal-1", Channel: interaction.ChannelWebChat, State: interaction.ConversationOpen}
|
||||||
|
utterance, _, _, err := NewHumanUtterance(conversation, interaction.ChatMessageRequest{Message: message, CorrectsUtteranceID: corrects}, time.Unix(100, 0))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
result, err := InterpretUtterance(utterance, projectRegistryForTest(), previous, time.Unix(100, 0))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func projectRegistryForTest() []situation.ProjectDescriptor {
|
||||||
|
return []situation.ProjectDescriptor{
|
||||||
|
{ID: "assistant", DisplayName: "Assistant", Aliases: []situation.ProjectAlias{"assistant", "ассистент", "домовой"}},
|
||||||
|
{ID: "upo", DisplayName: "УПО", Aliases: []situation.ProjectAlias{"упо", "upo"}},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func activeFocus(projectID situation.ProjectID, displayName string) *situation.ActiveProjectFocus {
|
||||||
|
return &situation.ActiveProjectFocus{
|
||||||
|
ActorID: identity.ActorID("actor-1"),
|
||||||
|
PrincipalID: identity.PrincipalID("principal-1"),
|
||||||
|
ProjectID: projectID,
|
||||||
|
ProjectDisplayName: displayName,
|
||||||
|
Provenance: situation.ProjectResolutionExplicit,
|
||||||
|
ConfidenceMillis: 980,
|
||||||
|
SourceUtteranceID: "utterance-focus",
|
||||||
|
UpdatedAtUnixMillis: time.Unix(90, 0).UnixMilli(),
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user