DEV-000062 human intent evidence project context #33

Merged
m merged 1 commits from codex/human-intent-evidence-project-context-v1 into main 2026-08-17 11:43:25 +00:00
21 changed files with 908 additions and 33 deletions
@@ -43,6 +43,31 @@ func (r *InteractionRepository) AppendUtterance(ctx context.Context, c interacti
}
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) {
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)
+34
View File
@@ -3,7 +3,9 @@ package postgres
import (
"context"
"fmt"
"strings"
"git.cin.su/m/Assistant/contracts/identity"
"git.cin.su/m/Assistant/contracts/situation"
"github.com/jackc/pgx/v5/pgxpool"
)
@@ -39,3 +41,35 @@ func (r *SituationRepository) ByRuntime(ctx context.Context, runtime situation.R
}
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.
+5 -3
View File
@@ -40,7 +40,9 @@
"id": "mod.situation",
"owns": [
"Situation",
"FocusBinding"
"FocusBinding",
"ProjectRegistry",
"ActiveProjectFocus"
],
"database_schema": "situation",
"boundary_class": "remote_capable",
@@ -144,10 +146,10 @@
},
"interaction": {
"id": "mod.interaction",
"owns": ["Conversation", "HumanUtterance", "UtteranceInterpretation", "OriginalHumanIntent", "InteractionRelation"],
"owns": ["Conversation", "HumanUtterance", "UtteranceInterpretation", "OriginalHumanIntent", "InteractionRelation", "ProjectReference"],
"database_schema": "interaction",
"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": {
+3 -3
View File
@@ -20,7 +20,7 @@ modules:
situation:
id: mod.situation
owns: [Situation, FocusBinding]
owns: [Situation, FocusBinding, ProjectRegistry, ActiveProjectFocus]
database_schema: situation
boundary_class: remote_capable
may_depend_on: [contracts.identity, contracts.authority, contracts.work, contracts.capability]
@@ -70,10 +70,10 @@ modules:
interaction:
id: mod.interaction
owns: [Conversation, HumanUtterance, UtteranceInterpretation, OriginalHumanIntent, InteractionRelation]
owns: [Conversation, HumanUtterance, UtteranceInterpretation, OriginalHumanIntent, InteractionRelation, ProjectReference]
database_schema: interaction
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:
allowed_packages:
+41 -3
View File
@@ -201,17 +201,55 @@ func main() {
http.Error(w, "invalid chat message", http.StatusBadRequest)
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 {
http.Error(w, "save chat message", http.StatusInternalServerError)
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{
ConversationID: conversation.ID,
UtteranceID: utterance.ID,
SpeechAct: utterance.SpeechAct,
SpeechAct: semantic.Interpretation.SpeechAct,
InterpretationState: utterance.InterpretationState,
NeedsCognition: interactiondomain.NeedsCognition(utterance.SpeechAct),
FallbackText: interactiondomain.FallbackText(utterance.SpeechAct),
NeedsCognition: semantic.NeedsCognition,
FallbackText: semantic.Reply,
ActiveProjectName: activeFocusName,
ProjectProvenance: activeFocusProvenance,
Clarification: semantic.Interpretation.Clarification,
}
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", "no-store")
+10 -7
View File
@@ -142,11 +142,14 @@ type browserChatRequest struct {
}
type browserChatResponse struct {
ConversationID string `json:"conversation_id"`
UtteranceID string `json:"utterance_id"`
SpeechAct string `json:"speech_act"`
ReplyText string `json:"reply_text"`
Cognition string `json:"cognition"`
ConversationID string `json:"conversation_id"`
UtteranceID string `json:"utterance_id"`
SpeechAct string `json:"speech_act"`
ReplyText string `json:"reply_text"`
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) {
@@ -313,7 +316,7 @@ func (g *gateway) handleChatMessage(w http.ResponseWriter, r *http.Request) {
}
reply := saved.FallbackText
cognition := "not_requested"
if saved.NeedsCognition {
if saved.NeedsCognition && saved.Clarification != interaction.ClarificationRequired {
cognition = "unavailable"
requestID, idErr := ids.New()
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("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 {
+2 -1
View File
@@ -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-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 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; }
.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; }
+11 -1
View File
@@ -7,6 +7,7 @@ const chatForm = document.querySelector("#chat-form");
const messageInput = document.querySelector("#message");
const sendButton = document.querySelector("#send");
const logoutButton = document.querySelector("#logout");
const activeFocus = document.querySelector("#active-focus");
let token = sessionStorage.getItem("domovoy.chat.token") || "";
let conversationID = sessionStorage.getItem("domovoy.chat.conversation") || "";
@@ -38,6 +39,11 @@ function appendMessage(kind, text, meta = "") {
messages.scrollTop = messages.scrollHeight;
}
function updateActiveFocus(value, provenance = "") {
if (!activeFocus) return;
activeFocus.textContent = value ? `фокус: ${value}${provenance ? ` · ${provenance}` : ""}` : "фокус: не выбран";
}
async function api(path, options = {}) {
const headers = {"Content-Type": "application/json", ...(options.headers || {})};
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})});
conversationID = result.conversation_id;
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 {
appendMessage("assistant", "Чат сейчас недоступен. Сообщение не принято runtime.");
} finally {
@@ -87,6 +96,7 @@ logoutButton.addEventListener("click", () => {
sessionStorage.removeItem("domovoy.chat.token");
sessionStorage.removeItem("domovoy.chat.conversation");
messages.replaceChildren();
updateActiveFocus("");
showLogin();
});
+1
View File
@@ -26,6 +26,7 @@
<section id="chat-view" class="chat-view" hidden>
<header class="chat-header">
<a href="/" aria-label="Консоль создателя">Домовой</a>
<span id="active-focus">фокус: не выбран</span>
<span id="session-state">сессия активна</span>
<button id="logout" type="button" aria-label="Выйти">↗</button>
</header>
+12
View File
@@ -5,3 +5,15 @@ import "git.cin.su/m/Assistant/platform/ids"
type ObservationID ids.ID
type ClaimID 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"
)
+41 -1
View File
@@ -1,6 +1,9 @@
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 HumanUtteranceID string
@@ -11,6 +14,10 @@ type Channel string
type ConversationState string
type InterpretationState string
type InteractionRelationKind string
type InterpretationID string
type ProjectReferenceID string
type ProjectReferenceRole string
type ClarificationState string
const (
ChannelWebChat Channel = "WEB_CHAT"
@@ -35,6 +42,13 @@ const (
RelationRespondsTo InteractionRelationKind = "RESPONDS_TO"
RelationRequestsCognition InteractionRelationKind = "REQUESTS_COGNITION"
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 {
@@ -74,6 +88,29 @@ type InteractionRelation struct {
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 {
ConversationID ConversationID
Message string
@@ -88,4 +125,7 @@ type ChatMessageResponse struct {
InterpretationState InterpretationState
NeedsCognition bool
FallbackText string
ActiveProjectName string
ProjectProvenance situation.ProjectResolutionProvenance
Clarification ClarificationState
}
+31
View File
@@ -12,6 +12,19 @@ type FocusBindingID ids.ID
type SessionID ids.ID
type WorkspaceID 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.
// User text is deliberately absent: textual similarity is not a routing signal.
@@ -30,3 +43,21 @@ type RuntimeBinding struct {
SessionID SessionID
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
}
+68 -1
View File
@@ -149,6 +149,45 @@ CREATE TABLE IF NOT EXISTS situation.focus_bindings (
active BOOLEAN NOT NULL,
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;
SET ROLE capability_role;
@@ -352,7 +391,35 @@ CREATE TABLE IF NOT EXISTS interaction.relations (
to_utterance_id TEXT NOT NULL,
kind TEXT 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;
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;
+3 -3
View File
@@ -1,5 +1,5 @@
{
"task_file": "dev/tasks/DEV-000061-human-interaction-foundation.json",
"task_id": "DEV-000061",
"task_revision": 2
"task_file": "dev/tasks/DEV-000062-human-intent-evidence-project-context.json",
"task_id": "DEV-000062",
"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"
]
}
+6 -6
View File
@@ -1,16 +1,16 @@
# 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`
- `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`
- `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`
- `DEV_000062_STATUS = IN_PROGRESS`
- `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`
- `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`
- `ACTIVE_BINDINGS = none`
- `CANARY_BINDING = unchanged; not promoted to ACTIVE`
- `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`
- `NEXT_STEP = commit, push, PR/promotion, apply migration 016, rebuild/deploy affected production services if required, then run live DEV-000062 /chat acceptance`
- `REAL_BLOCKERS = none known`
## Current milestone
+4 -4
View File
@@ -99,13 +99,13 @@ func NewHumanUtterance(conversation interaction.Conversation, request interactio
func ClassifySpeechAct(message string, corrects interaction.HumanUtteranceID) interaction.SpeechAct {
lower := strings.ToLower(strings.TrimSpace(message))
switch {
case corrects != "" || strings.Contains(lower, "исправ") || strings.Contains(lower, "не так"):
case corrects != "" || strings.Contains(lower, "исправ") || strings.Contains(lower, "не так") || strings.HasPrefix(lower, "нет,"):
return interaction.SpeechActCorrection
case strings.Contains(lower, "?") || strings.HasPrefix(lower, "что ") || strings.HasPrefix(lower, "как ") || strings.HasPrefix(lower, "почему ") || strings.HasPrefix(lower, "кто "):
return interaction.SpeechActQuestion
case strings.Contains(lower, "проверь") || strings.Contains(lower, "исслед") || strings.Contains(lower, "разбер"):
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
case strings.HasPrefix(lower, "я решил") || strings.Contains(lower, "решение"):
return interaction.SpeechActDecision
@@ -113,9 +113,9 @@ func ClassifySpeechAct(message string, corrects interaction.HumanUtteranceID) in
return interaction.SpeechActPreference
case strings.HasPrefix(lower, "думаю") || strings.Contains(lower, "гипотез"):
return interaction.SpeechActHypothesis
case strings.Contains(lower, "да") || strings.Contains(lower, "подтверж"):
case lower == "да" || strings.HasPrefix(lower, "да,") || strings.Contains(lower, "подтверж"):
return interaction.SpeechActConfirmation
case strings.Contains(lower, "нет") || strings.Contains(lower, "отмен"):
case lower == "нет" || strings.HasPrefix(lower, "нет,") || strings.Contains(lower, "отмен"):
return interaction.SpeechActRejection
case strings.Contains(lower, "привет") || strings.Contains(lower, "здравств"):
return interaction.SpeechActConversational
+231
View File
@@ -0,0 +1,231 @@
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)
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)
}
@@ -0,0 +1,129 @@
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)
}
}
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(),
}
}