Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d8f994e07e | ||
|
|
9d50a3ec63 | ||
|
|
2d142cdfdb | ||
|
|
06b6cc3f9d | ||
|
|
4fc85511c4 | ||
|
|
18750c2ae6 | ||
|
|
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)
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -39,3 +39,39 @@ func (r *WorkRepository) Load(ctx context.Context, id work.TaskID) (work.TaskTar
|
||||
}
|
||||
return target, nil
|
||||
}
|
||||
|
||||
func (r *WorkRepository) SavePlanningResult(ctx context.Context, result work.PlanningResult) error {
|
||||
tx, err := r.pool.Begin(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
goal := result.Goal
|
||||
if _, err = tx.Exec(ctx, `INSERT INTO work.goals(goal_id,source_intent_id,source_utterance_id,actor_id,principal_id,scope_id,project_id,project_display_name,desired_outcome,constraints_text,state,provenance,revision,created_at_unix_millis,updated_at_unix_millis) VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15) ON CONFLICT(goal_id) DO NOTHING`, goal.ID, goal.SourceIntentID, goal.SourceUtteranceID, goal.ActorID, goal.PrincipalID, goal.ScopeID, goal.ProjectID, goal.ProjectDisplayName, goal.DesiredOutcome, goal.Constraints, goal.State, goal.Provenance, goal.Revision, goal.CreatedAtUnixMillis, goal.UpdatedAtUnixMillis); err != nil {
|
||||
return err
|
||||
}
|
||||
candidate := result.Candidate
|
||||
if _, err = tx.Exec(ctx, `INSERT INTO work.plan_candidates(plan_candidate_id,goal_id,source,state,summary,validation_summary,requires_clarification,accepted_at_unix_millis,recorded_at_unix_millis) VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9) ON CONFLICT(plan_candidate_id) DO NOTHING`, candidate.ID, candidate.GoalID, candidate.Source, candidate.State, candidate.Summary, candidate.ValidationSummary, candidate.RequiresClarification, candidate.AcceptedAtUnixMillis, candidate.RecordedAtUnixMillis); err != nil {
|
||||
return err
|
||||
}
|
||||
plan := result.Plan
|
||||
if _, err = tx.Exec(ctx, `INSERT INTO work.work_plans(work_plan_id,goal_id,accepted_candidate_id,revision,state,summary,recorded_at_unix_millis) VALUES($1,$2,$3,$4,$5,$6,$7) ON CONFLICT(work_plan_id) DO NOTHING`, plan.ID, plan.GoalID, plan.AcceptedCandidateID, plan.Revision, plan.State, plan.Summary, plan.RecordedAtUnixMillis); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, requirement := range candidate.Requirements {
|
||||
if _, err = tx.Exec(ctx, `INSERT INTO work.capability_requirements(capability_requirement_id,plan_candidate_id,capability_kind,capability_id,purpose,state,epistemic_precondition,recorded_at_unix_millis) VALUES($1,$2,$3,$4,$5,$6,$7,$8) ON CONFLICT(capability_requirement_id) DO NOTHING`, requirement.ID, requirement.PlanCandidateID, requirement.CapabilityKind, requirement.CapabilityID, requirement.Purpose, requirement.State, requirement.EpistemicPrecondition, requirement.RecordedAtUnixMillis); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, step := range candidate.Steps {
|
||||
if _, err = tx.Exec(ctx, `INSERT INTO work.plan_steps(plan_step_id,plan_candidate_id,ordinal,kind,description,requires_capability_id,recorded_at_unix_millis) VALUES($1,$2,$3,$4,$5,$6,$7) ON CONFLICT(plan_step_id) DO NOTHING`, step.ID, step.PlanCandidateID, step.Ordinal, step.Kind, step.Description, step.RequiresCapabilityID, step.RecordedAtUnixMillis); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, delegation := range candidate.Delegations {
|
||||
if _, err = tx.Exec(ctx, `INSERT INTO work.delegation_candidates(delegation_candidate_id,plan_candidate_id,capability_requirement_id,kind,executor_hint,state,reason,recorded_at_unix_millis) VALUES($1,$2,$3,$4,$5,$6,$7,$8) ON CONFLICT(delegation_candidate_id) DO NOTHING`, delegation.ID, delegation.PlanCandidateID, delegation.CapabilityRequirementID, delegation.Kind, delegation.ExecutorHint, delegation.State, delegation.Reason, delegation.RecordedAtUnixMillis); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return tx.Commit(ctx)
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
# ADR-0012 - Intent To Work Planning Foundation
|
||||
|
||||
Status: Accepted
|
||||
Date: 2026-08-17
|
||||
Accepted under: DEV-000063 milestone decision
|
||||
|
||||
## Decision
|
||||
|
||||
`work` remains the canonical owner for Goal, Task, Decision and the new V1 planning foundation. No new canonical owner is introduced.
|
||||
|
||||
Help is the central user-facing concept. Goal, Work and Plan are optional durable coordination primitives, not mandatory records for every conversation. A simple explanation, ordinary answer, short cognitive response, direct reminder capability, safe deterministic operation, or capability-gap response may bypass durable Work entirely.
|
||||
|
||||
Human chat must not reduce Domovoy to `chat -> prompt -> model` or to a Codex-specific task generator. The canonical pipeline is:
|
||||
|
||||
When durable coordination is actually needed, the canonical path is:
|
||||
|
||||
`HumanUtterance -> OriginalHumanIntent -> Goal -> PlanCandidate -> accepted Work/Plan state -> CapabilityRequirement -> DelegationCandidate/Action/Investigation/Observation/Outcome`.
|
||||
|
||||
Prompts, Codex instructions, mail payloads, reminders and provider requests are executor-adapter representations. They are not canonical human intent, Goal or Plan.
|
||||
|
||||
## Ownership
|
||||
|
||||
- `interaction` continues to own append-only human utterances, original intent provenance and typed utterance interpretations.
|
||||
- `work` owns durable Goals, lifecycle, planning state, plan candidates, accepted plan summaries, capability requirements and general delegation candidates.
|
||||
- `capability` owns available capability/binding registry and health. Work may reference capability requirements by typed IDs/kinds but does not select a concrete provider as canonical truth.
|
||||
- `epistemics` owns verified/unverified/unknown evidence semantics. Work planning must preserve epistemic state and may add verification requirements before risky work.
|
||||
- `authority` and `commit` remain required for any real mutation or external side effect. Planning creates no permission and no execution claim.
|
||||
|
||||
## Boundaries
|
||||
|
||||
Work PlanCandidates are structured candidates that must be validated before persistence or acceptance. Cognitive planning may later propose candidates only through the Cognitive Gateway/Router; model output never directly mutates canonical Work state. DEV-000063 may implement deterministic V1 candidate planning and keep cognitive candidate generation as a typed extension point.
|
||||
|
||||
CapabilityRequirement is executor-neutral. Examples include `READ_EMAIL`, `SEND_EMAIL`, `READ_DATABASE`, `QUERY_LOGS`, `INSPECT_PROJECT`, `MODIFY_CODE`, `RUN_TESTS`, `WEB_RESEARCH`, `COGNITIVE_REASONING`, and `SCHEDULE_REMINDER`. This is an extensible vocabulary, not a closed ontology and not an executor list.
|
||||
|
||||
DelegationCandidate is general. `Codex` is only a possible future implementation for `MODIFY_CODE`; mail, research, scheduler, device and database adapters can use the same canonical Work model without changing OriginalHumanIntent or Goal.
|
||||
|
||||
## Consequences
|
||||
|
||||
`/chat` can choose a HelpPath first. Only the durable-work path creates a planning-only Goal and Plan. Other paths can answer, use cognition, report a missing capability, or describe a direct capability requirement without materializing a Goal/Plan. It must not pretend work was completed. Completion remains tied to outcome semantics, not to a model response.
|
||||
|
||||
Project is only one possible context reference. The Work foundation stores project context as optional opaque references and must later permit people, emails, documents, images, servers, organizations, research subjects and other context types without redesigning Goal/Plan.
|
||||
|
||||
Capability gaps are first-class planning information. "I cannot do this yet" is not necessarily terminal in future milestones: the system may later discover, connect, configure or build a missing capability and return to the original need. DEV-000063 does not implement self-extension; it preserves the extension path.
|
||||
@@ -40,7 +40,9 @@
|
||||
"id": "mod.situation",
|
||||
"owns": [
|
||||
"Situation",
|
||||
"FocusBinding"
|
||||
"FocusBinding",
|
||||
"ProjectRegistry",
|
||||
"ActiveProjectFocus"
|
||||
],
|
||||
"database_schema": "situation",
|
||||
"boundary_class": "remote_capable",
|
||||
@@ -55,14 +57,24 @@
|
||||
"id": "mod.work",
|
||||
"owns": [
|
||||
"Goal",
|
||||
"HelpPath",
|
||||
"GoalLifecycle",
|
||||
"Task",
|
||||
"Decision"
|
||||
"Decision",
|
||||
"PlanCandidate",
|
||||
"WorkPlan",
|
||||
"PlanStep",
|
||||
"CapabilityRequirement",
|
||||
"DelegationCandidate"
|
||||
],
|
||||
"database_schema": "work",
|
||||
"boundary_class": "remote_capable",
|
||||
"may_depend_on": [
|
||||
"contracts.identity",
|
||||
"contracts.authority",
|
||||
"contracts.epistemics",
|
||||
"contracts.capability",
|
||||
"platform.ids",
|
||||
"platform.events"
|
||||
]
|
||||
},
|
||||
@@ -144,10 +156,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": {
|
||||
|
||||
@@ -20,17 +20,17 @@ 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]
|
||||
|
||||
work:
|
||||
id: mod.work
|
||||
owns: [Goal, Task, Decision]
|
||||
owns: [Goal, HelpPath, GoalLifecycle, Task, Decision, PlanCandidate, WorkPlan, PlanStep, CapabilityRequirement, DelegationCandidate]
|
||||
database_schema: work
|
||||
boundary_class: remote_capable
|
||||
may_depend_on: [contracts.identity, contracts.authority, platform.events]
|
||||
may_depend_on: [contracts.identity, contracts.authority, contracts.epistemics, contracts.capability, platform.ids, platform.events]
|
||||
|
||||
epistemics:
|
||||
id: mod.epistemics
|
||||
@@ -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:
|
||||
|
||||
@@ -6,7 +6,9 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"git.cin.su/m/Assistant/adapters/postgres"
|
||||
"git.cin.su/m/Assistant/contracts/authority"
|
||||
"git.cin.su/m/Assistant/contracts/capability"
|
||||
"git.cin.su/m/Assistant/contracts/epistemics"
|
||||
"git.cin.su/m/Assistant/contracts/identity"
|
||||
"git.cin.su/m/Assistant/contracts/interaction"
|
||||
secretcontracts "git.cin.su/m/Assistant/contracts/secrets"
|
||||
@@ -16,6 +18,7 @@ import (
|
||||
identitydomain "git.cin.su/m/Assistant/modules/identity/domain"
|
||||
interactiondomain "git.cin.su/m/Assistant/modules/interaction/domain"
|
||||
situationdomain "git.cin.su/m/Assistant/modules/situation/domain"
|
||||
workdomain "git.cin.su/m/Assistant/modules/work/domain"
|
||||
platformsecrets "git.cin.su/m/Assistant/platform/secrets"
|
||||
"git.cin.su/m/Assistant/runtime/health"
|
||||
"github.com/jackc/pgx/v5"
|
||||
@@ -201,17 +204,100 @@ 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
|
||||
}
|
||||
var planningReply string
|
||||
workPlanned := false
|
||||
needsCognition := semantic.NeedsCognition
|
||||
projectID := ""
|
||||
if semantic.ActiveFocus != nil {
|
||||
projectID = string(semantic.ActiveFocus.ProjectID)
|
||||
} else if previousFocus != nil {
|
||||
projectID = string(previousFocus.ProjectID)
|
||||
}
|
||||
planningInput := work.PlanningInput{
|
||||
ConversationID: work.ConversationRef(conversation.ID),
|
||||
UtteranceID: work.SourceUtteranceRef(utterance.ID),
|
||||
IntentID: work.SourceIntentRef(intent.ID),
|
||||
ActorID: session.ActorID,
|
||||
PrincipalID: session.PrincipalID,
|
||||
ScopeID: authority.ScopeID("domovoy-global-scope"),
|
||||
ProjectID: work.ProjectRef(projectID),
|
||||
ProjectDisplayName: activeFocusName,
|
||||
ProjectProvenance: work.ProjectProvenanceRef(activeFocusProvenance),
|
||||
Message: utterance.Content,
|
||||
SpeechAct: work.SpeechActRef(semantic.Interpretation.SpeechAct),
|
||||
AssertionVerification: epistemics.VerificationState(semantic.Interpretation.AssertionVerification),
|
||||
ReceivedAtUnixMillis: utterance.ReceivedAtUnixMillis,
|
||||
}
|
||||
helpPath := workdomain.DetermineHelpPath(planningInput)
|
||||
if helpPath.Reply != "" {
|
||||
planningReply = helpPath.Reply
|
||||
needsCognition = false
|
||||
}
|
||||
if helpPath.NeedsDurableWork {
|
||||
planning, err := workdomain.BuildPlanningResult(planningInput, now)
|
||||
if err != nil {
|
||||
http.Error(w, "plan chat work", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
if err := workRepository.SavePlanningResult(r.Context(), planning); err != nil {
|
||||
http.Error(w, "save work plan", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
planningReply = planning.Reply
|
||||
workPlanned = true
|
||||
needsCognition = false
|
||||
}
|
||||
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: needsCognition,
|
||||
FallbackText: semantic.Reply,
|
||||
ActiveProjectName: activeFocusName,
|
||||
ProjectProvenance: activeFocusProvenance,
|
||||
Clarification: semantic.Interpretation.Clarification,
|
||||
WorkPlanned: workPlanned,
|
||||
PlanningReply: planningReply,
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Header().Set("Cache-Control", "no-store")
|
||||
|
||||
@@ -142,11 +142,15 @@ 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"`
|
||||
WorkPlanned bool `json:"work_planned"`
|
||||
}
|
||||
|
||||
func (g *gateway) handleAdvisoryPolicy(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -313,7 +317,10 @@ func (g *gateway) handleChatMessage(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
reply := saved.FallbackText
|
||||
cognition := "not_requested"
|
||||
if saved.NeedsCognition {
|
||||
if saved.PlanningReply != "" {
|
||||
reply = saved.PlanningReply
|
||||
}
|
||||
if saved.NeedsCognition && !saved.WorkPlanned && saved.Clarification != interaction.ClarificationRequired {
|
||||
cognition = "unavailable"
|
||||
requestID, idErr := ids.New()
|
||||
if idErr == nil {
|
||||
@@ -333,7 +340,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), WorkPlanned: saved.WorkPlanned})
|
||||
}
|
||||
|
||||
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-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; }
|
||||
|
||||
@@ -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,11 @@ 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.work_planned) semanticMeta.push("work=planned");
|
||||
if (result.clarification) semanticMeta.push(`clarification=${result.clarification}`);
|
||||
appendMessage("assistant", result.reply_text || "Сообщение записано.", semanticMeta.join("; "));
|
||||
} catch {
|
||||
appendMessage("assistant", "Чат сейчас недоступен. Сообщение не принято runtime.");
|
||||
} finally {
|
||||
@@ -87,6 +97,7 @@ logoutButton.addEventListener("click", () => {
|
||||
sessionStorage.removeItem("domovoy.chat.token");
|
||||
sessionStorage.removeItem("domovoy.chat.conversation");
|
||||
messages.replaceChildren();
|
||||
updateActiveFocus("");
|
||||
showLogin();
|
||||
});
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
|
||||
type CapabilityID ids.ID
|
||||
type CapabilityBindingID ids.ID
|
||||
type CapabilityKind string
|
||||
|
||||
type BindingState string
|
||||
type BindingProtocol string
|
||||
@@ -16,6 +17,18 @@ const (
|
||||
BindingRetired BindingState = "RETIRED"
|
||||
|
||||
ProtocolIdempotentReceiptV1 BindingProtocol = "IDEMPOTENT_RECEIPT_V1"
|
||||
|
||||
CapabilityKindReadEmail CapabilityKind = "READ_EMAIL"
|
||||
CapabilityKindSendEmail CapabilityKind = "SEND_EMAIL"
|
||||
CapabilityKindReadDatabase CapabilityKind = "READ_DATABASE"
|
||||
CapabilityKindQueryLogs CapabilityKind = "QUERY_LOGS"
|
||||
CapabilityKindInspectProject CapabilityKind = "INSPECT_PROJECT"
|
||||
CapabilityKindModifyCode CapabilityKind = "MODIFY_CODE"
|
||||
CapabilityKindRunTests CapabilityKind = "RUN_TESTS"
|
||||
CapabilityKindWebResearch CapabilityKind = "WEB_RESEARCH"
|
||||
CapabilityKindCognitiveReasoning CapabilityKind = "COGNITIVE_REASONING"
|
||||
CapabilityKindScheduleReminder CapabilityKind = "SCHEDULE_REMINDER"
|
||||
CapabilityKindVerifyFact CapabilityKind = "VERIFY_FACT"
|
||||
)
|
||||
|
||||
// Binding is Capability-owned correctness state. SecretRef is an opaque
|
||||
|
||||
@@ -5,3 +5,16 @@ 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"
|
||||
VerificationUnknown VerificationState = "UNKNOWN"
|
||||
VerificationNotApplicable VerificationState = "NOT_APPLICABLE"
|
||||
|
||||
ClaimSourceAuthenticatedHuman ClaimSourceKind = "AUTHENTICATED_HUMAN"
|
||||
ClaimSourceTrustedObservation ClaimSourceKind = "TRUSTED_OBSERVATION"
|
||||
)
|
||||
|
||||
@@ -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,9 @@ type ChatMessageResponse struct {
|
||||
InterpretationState InterpretationState
|
||||
NeedsCognition bool
|
||||
FallbackText string
|
||||
ActiveProjectName string
|
||||
ProjectProvenance situation.ProjectResolutionProvenance
|
||||
Clarification ClarificationState
|
||||
WorkPlanned bool
|
||||
PlanningReply string
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -2,13 +2,88 @@ package work
|
||||
|
||||
import (
|
||||
"git.cin.su/m/Assistant/contracts/authority"
|
||||
"git.cin.su/m/Assistant/contracts/capability"
|
||||
"git.cin.su/m/Assistant/contracts/epistemics"
|
||||
"git.cin.su/m/Assistant/contracts/identity"
|
||||
"git.cin.su/m/Assistant/platform/ids"
|
||||
)
|
||||
|
||||
type GoalID ids.ID
|
||||
type TaskID ids.ID
|
||||
type DecisionID ids.ID
|
||||
type PlanCandidateID ids.ID
|
||||
type WorkPlanID ids.ID
|
||||
type PlanStepID ids.ID
|
||||
type CapabilityRequirementID ids.ID
|
||||
type DelegationCandidateID ids.ID
|
||||
type TaskRevision uint64
|
||||
type GoalRevision uint64
|
||||
type PlanRevision uint64
|
||||
type GoalState string
|
||||
type PlanCandidateState string
|
||||
type PlanStepKind string
|
||||
type CapabilityRequirementState string
|
||||
type DelegationCandidateState string
|
||||
type DelegationKind string
|
||||
type SourceIntentRef string
|
||||
type SourceUtteranceRef string
|
||||
type ConversationRef string
|
||||
type ProjectRef string
|
||||
type ProjectProvenanceRef string
|
||||
type SpeechActRef string
|
||||
type HelpPathKind string
|
||||
|
||||
const (
|
||||
GoalProposed GoalState = "PROPOSED"
|
||||
GoalUnderstood GoalState = "UNDERSTOOD"
|
||||
GoalPlanning GoalState = "PLANNING"
|
||||
GoalReady GoalState = "READY"
|
||||
GoalInProgress GoalState = "IN_PROGRESS"
|
||||
GoalBlocked GoalState = "BLOCKED"
|
||||
GoalCompleted GoalState = "COMPLETED"
|
||||
GoalFailed GoalState = "FAILED"
|
||||
GoalCancelled GoalState = "CANCELLED"
|
||||
GoalSuperseded GoalState = "SUPERSEDED"
|
||||
|
||||
PlanCandidateProposed PlanCandidateState = "PROPOSED"
|
||||
PlanCandidateValidated PlanCandidateState = "VALIDATED"
|
||||
PlanCandidateAccepted PlanCandidateState = "ACCEPTED"
|
||||
PlanCandidateRejected PlanCandidateState = "REJECTED"
|
||||
|
||||
PlanStepInvestigate PlanStepKind = "INVESTIGATE"
|
||||
PlanStepObserve PlanStepKind = "OBSERVE"
|
||||
PlanStepVerify PlanStepKind = "VERIFY"
|
||||
PlanStepReason PlanStepKind = "REASON"
|
||||
PlanStepReport PlanStepKind = "REPORT"
|
||||
PlanStepPrepare PlanStepKind = "PREPARE"
|
||||
PlanStepCommunicate PlanStepKind = "COMMUNICATE"
|
||||
PlanStepSchedule PlanStepKind = "SCHEDULE"
|
||||
PlanStepModify PlanStepKind = "MODIFY"
|
||||
PlanStepRunChecks PlanStepKind = "RUN_CHECKS"
|
||||
PlanStepClarify PlanStepKind = "CLARIFY"
|
||||
PlanStepNoExecution PlanStepKind = "NO_EXECUTION"
|
||||
|
||||
CapabilityRequirementRequired CapabilityRequirementState = "REQUIRED"
|
||||
CapabilityRequirementAvailable CapabilityRequirementState = "AVAILABLE"
|
||||
CapabilityRequirementMissing CapabilityRequirementState = "MISSING"
|
||||
CapabilityRequirementBlocked CapabilityRequirementState = "BLOCKED"
|
||||
|
||||
DelegationCandidateProposed DelegationCandidateState = "PROPOSED"
|
||||
DelegationCandidateSelected DelegationCandidateState = "SELECTED"
|
||||
DelegationCandidateBlocked DelegationCandidateState = "BLOCKED"
|
||||
|
||||
DelegationKindAgent DelegationKind = "AGENT"
|
||||
DelegationKindTool DelegationKind = "TOOL"
|
||||
DelegationKindAdapter DelegationKind = "ADAPTER"
|
||||
DelegationKindHuman DelegationKind = "HUMAN"
|
||||
DelegationKindUnavailable DelegationKind = "UNAVAILABLE"
|
||||
|
||||
HelpPathAnswer HelpPathKind = "ANSWER"
|
||||
HelpPathCognition HelpPathKind = "COGNITION"
|
||||
HelpPathDirectCapability HelpPathKind = "DIRECT_CAPABILITY"
|
||||
HelpPathDurableWork HelpPathKind = "DURABLE_WORK"
|
||||
HelpPathCapabilityGap HelpPathKind = "CAPABILITY_GAP"
|
||||
)
|
||||
|
||||
// TaskTarget carries the integrity context that a work-owned mutation must match.
|
||||
type TaskTarget struct {
|
||||
@@ -17,3 +92,112 @@ type TaskTarget struct {
|
||||
ScopeID authority.ScopeID
|
||||
Revision TaskRevision
|
||||
}
|
||||
|
||||
type Goal struct {
|
||||
ID GoalID
|
||||
SourceIntentID SourceIntentRef
|
||||
SourceUtteranceID SourceUtteranceRef
|
||||
ActorID identity.ActorID
|
||||
PrincipalID identity.PrincipalID
|
||||
ScopeID authority.ScopeID
|
||||
ProjectID ProjectRef
|
||||
ProjectDisplayName string
|
||||
DesiredOutcome string
|
||||
Constraints string
|
||||
State GoalState
|
||||
Provenance ProjectProvenanceRef
|
||||
Revision GoalRevision
|
||||
CreatedAtUnixMillis int64
|
||||
UpdatedAtUnixMillis int64
|
||||
}
|
||||
|
||||
type CapabilityRequirement struct {
|
||||
ID CapabilityRequirementID
|
||||
PlanCandidateID PlanCandidateID
|
||||
CapabilityKind capability.CapabilityKind
|
||||
CapabilityID capability.CapabilityID
|
||||
Purpose string
|
||||
State CapabilityRequirementState
|
||||
EpistemicPrecondition epistemics.VerificationState
|
||||
RecordedAtUnixMillis int64
|
||||
}
|
||||
|
||||
type PlanStep struct {
|
||||
ID PlanStepID
|
||||
PlanCandidateID PlanCandidateID
|
||||
Ordinal int
|
||||
Kind PlanStepKind
|
||||
Description string
|
||||
RequiresCapabilityID CapabilityRequirementID
|
||||
RecordedAtUnixMillis int64
|
||||
}
|
||||
|
||||
type DelegationCandidate struct {
|
||||
ID DelegationCandidateID
|
||||
PlanCandidateID PlanCandidateID
|
||||
CapabilityRequirementID CapabilityRequirementID
|
||||
Kind DelegationKind
|
||||
ExecutorHint string
|
||||
State DelegationCandidateState
|
||||
Reason string
|
||||
RecordedAtUnixMillis int64
|
||||
}
|
||||
|
||||
type PlanCandidate struct {
|
||||
ID PlanCandidateID
|
||||
GoalID GoalID
|
||||
Source string
|
||||
State PlanCandidateState
|
||||
Summary string
|
||||
ValidationSummary string
|
||||
RequiresClarification bool
|
||||
AcceptedAtUnixMillis int64
|
||||
RecordedAtUnixMillis int64
|
||||
Steps []PlanStep
|
||||
Requirements []CapabilityRequirement
|
||||
Delegations []DelegationCandidate
|
||||
}
|
||||
|
||||
type WorkPlan struct {
|
||||
ID WorkPlanID
|
||||
GoalID GoalID
|
||||
AcceptedCandidateID PlanCandidateID
|
||||
Revision PlanRevision
|
||||
State PlanCandidateState
|
||||
Summary string
|
||||
RecordedAtUnixMillis int64
|
||||
}
|
||||
|
||||
type PlanningInput struct {
|
||||
ConversationID ConversationRef
|
||||
UtteranceID SourceUtteranceRef
|
||||
IntentID SourceIntentRef
|
||||
ActorID identity.ActorID
|
||||
PrincipalID identity.PrincipalID
|
||||
ScopeID authority.ScopeID
|
||||
ProjectID ProjectRef
|
||||
ProjectDisplayName string
|
||||
ProjectProvenance ProjectProvenanceRef
|
||||
Message string
|
||||
SpeechAct SpeechActRef
|
||||
AssertionVerification epistemics.VerificationState
|
||||
ReceivedAtUnixMillis int64
|
||||
}
|
||||
|
||||
type PlanningResult struct {
|
||||
Goal Goal
|
||||
Candidate PlanCandidate
|
||||
Plan WorkPlan
|
||||
Reply string
|
||||
}
|
||||
|
||||
type HelpPath struct {
|
||||
Kind HelpPathKind
|
||||
NeedsDurableGoal bool
|
||||
NeedsDurableWork bool
|
||||
NeedsDurablePlan bool
|
||||
SuggestedCapabilityKinds []capability.CapabilityKind
|
||||
CapabilityGap bool
|
||||
Reason string
|
||||
Reply string
|
||||
}
|
||||
|
||||
@@ -135,6 +135,97 @@ CREATE TABLE IF NOT EXISTS work.tasks (
|
||||
task_id TEXT PRIMARY KEY, goal_id TEXT NOT NULL, scope_id TEXT NOT NULL,
|
||||
revision BIGINT NOT NULL, CHECK (revision > 0)
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS work.goals (
|
||||
goal_id TEXT PRIMARY KEY,
|
||||
source_intent_id TEXT NOT NULL,
|
||||
source_utterance_id TEXT NOT NULL,
|
||||
actor_id TEXT NOT NULL,
|
||||
principal_id TEXT NOT NULL,
|
||||
scope_id TEXT NOT NULL,
|
||||
project_id TEXT NOT NULL DEFAULT '',
|
||||
project_display_name TEXT NOT NULL DEFAULT '',
|
||||
desired_outcome TEXT NOT NULL,
|
||||
constraints_text TEXT NOT NULL,
|
||||
state TEXT NOT NULL,
|
||||
provenance TEXT NOT NULL DEFAULT '',
|
||||
revision BIGINT NOT NULL,
|
||||
created_at_unix_millis BIGINT NOT NULL,
|
||||
updated_at_unix_millis BIGINT NOT NULL,
|
||||
CHECK (desired_outcome <> ''),
|
||||
CHECK (state IN ('PROPOSED','UNDERSTOOD','PLANNING','READY','IN_PROGRESS','BLOCKED','COMPLETED','FAILED','CANCELLED','SUPERSEDED')),
|
||||
CHECK (revision > 0)
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS work.plan_candidates (
|
||||
plan_candidate_id TEXT PRIMARY KEY,
|
||||
goal_id TEXT NOT NULL,
|
||||
source TEXT NOT NULL,
|
||||
state TEXT NOT NULL,
|
||||
summary TEXT NOT NULL,
|
||||
validation_summary TEXT NOT NULL,
|
||||
requires_clarification BOOLEAN NOT NULL,
|
||||
accepted_at_unix_millis BIGINT NOT NULL DEFAULT 0,
|
||||
recorded_at_unix_millis BIGINT NOT NULL,
|
||||
CHECK (goal_id <> ''),
|
||||
CHECK (source <> ''),
|
||||
CHECK (summary <> ''),
|
||||
CHECK (state IN ('PROPOSED','VALIDATED','ACCEPTED','REJECTED'))
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS work.work_plans (
|
||||
work_plan_id TEXT PRIMARY KEY,
|
||||
goal_id TEXT NOT NULL,
|
||||
accepted_candidate_id TEXT NOT NULL,
|
||||
revision BIGINT NOT NULL,
|
||||
state TEXT NOT NULL,
|
||||
summary TEXT NOT NULL,
|
||||
recorded_at_unix_millis BIGINT NOT NULL,
|
||||
CHECK (goal_id <> ''),
|
||||
CHECK (accepted_candidate_id <> ''),
|
||||
CHECK (revision > 0),
|
||||
CHECK (state IN ('PROPOSED','VALIDATED','ACCEPTED','REJECTED'))
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS work.capability_requirements (
|
||||
capability_requirement_id TEXT PRIMARY KEY,
|
||||
plan_candidate_id TEXT NOT NULL,
|
||||
capability_kind TEXT NOT NULL,
|
||||
capability_id TEXT NOT NULL DEFAULT '',
|
||||
purpose TEXT NOT NULL,
|
||||
state TEXT NOT NULL,
|
||||
epistemic_precondition TEXT NOT NULL,
|
||||
recorded_at_unix_millis BIGINT NOT NULL,
|
||||
CHECK (plan_candidate_id <> ''),
|
||||
CHECK (capability_kind <> ''),
|
||||
CHECK (purpose <> ''),
|
||||
CHECK (state IN ('REQUIRED','AVAILABLE','MISSING','BLOCKED')),
|
||||
CHECK (epistemic_precondition IN ('UNVERIFIED','VERIFIED','REJECTED','UNKNOWN','NOT_APPLICABLE'))
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS work.plan_steps (
|
||||
plan_step_id TEXT PRIMARY KEY,
|
||||
plan_candidate_id TEXT NOT NULL,
|
||||
ordinal INTEGER NOT NULL,
|
||||
kind TEXT NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
requires_capability_id TEXT NOT NULL DEFAULT '',
|
||||
recorded_at_unix_millis BIGINT NOT NULL,
|
||||
CHECK (plan_candidate_id <> ''),
|
||||
CHECK (ordinal > 0),
|
||||
CHECK (kind IN ('INVESTIGATE','OBSERVE','VERIFY','REASON','REPORT','PREPARE','COMMUNICATE','SCHEDULE','MODIFY','RUN_CHECKS','CLARIFY','NO_EXECUTION')),
|
||||
CHECK (description <> '')
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS work.delegation_candidates (
|
||||
delegation_candidate_id TEXT PRIMARY KEY,
|
||||
plan_candidate_id TEXT NOT NULL,
|
||||
capability_requirement_id TEXT NOT NULL,
|
||||
kind TEXT NOT NULL,
|
||||
executor_hint TEXT NOT NULL DEFAULT '',
|
||||
state TEXT NOT NULL,
|
||||
reason TEXT NOT NULL,
|
||||
recorded_at_unix_millis BIGINT NOT NULL,
|
||||
CHECK (plan_candidate_id <> ''),
|
||||
CHECK (capability_requirement_id <> ''),
|
||||
CHECK (kind IN ('AGENT','TOOL','ADAPTER','HUMAN','UNAVAILABLE')),
|
||||
CHECK (state IN ('PROPOSED','SELECTED','BLOCKED')),
|
||||
CHECK (reason <> '')
|
||||
);
|
||||
RESET ROLE;
|
||||
|
||||
SET ROLE situation_role;
|
||||
@@ -149,6 +240,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 +482,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;
|
||||
@@ -0,0 +1,104 @@
|
||||
BEGIN;
|
||||
|
||||
SET ROLE work_role;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS work.goals (
|
||||
goal_id TEXT PRIMARY KEY,
|
||||
source_intent_id TEXT NOT NULL,
|
||||
source_utterance_id TEXT NOT NULL,
|
||||
actor_id TEXT NOT NULL,
|
||||
principal_id TEXT NOT NULL,
|
||||
scope_id TEXT NOT NULL,
|
||||
project_id TEXT NOT NULL DEFAULT '',
|
||||
project_display_name TEXT NOT NULL DEFAULT '',
|
||||
desired_outcome TEXT NOT NULL,
|
||||
constraints_text TEXT NOT NULL,
|
||||
state TEXT NOT NULL,
|
||||
provenance TEXT NOT NULL DEFAULT '',
|
||||
revision BIGINT NOT NULL,
|
||||
created_at_unix_millis BIGINT NOT NULL,
|
||||
updated_at_unix_millis BIGINT NOT NULL,
|
||||
CHECK (desired_outcome <> ''),
|
||||
CHECK (state IN ('PROPOSED','UNDERSTOOD','PLANNING','READY','IN_PROGRESS','BLOCKED','COMPLETED','FAILED','CANCELLED','SUPERSEDED')),
|
||||
CHECK (revision > 0)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS work.plan_candidates (
|
||||
plan_candidate_id TEXT PRIMARY KEY,
|
||||
goal_id TEXT NOT NULL,
|
||||
source TEXT NOT NULL,
|
||||
state TEXT NOT NULL,
|
||||
summary TEXT NOT NULL,
|
||||
validation_summary TEXT NOT NULL,
|
||||
requires_clarification BOOLEAN NOT NULL,
|
||||
accepted_at_unix_millis BIGINT NOT NULL DEFAULT 0,
|
||||
recorded_at_unix_millis BIGINT NOT NULL,
|
||||
CHECK (goal_id <> ''),
|
||||
CHECK (source <> ''),
|
||||
CHECK (summary <> ''),
|
||||
CHECK (state IN ('PROPOSED','VALIDATED','ACCEPTED','REJECTED'))
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS work.work_plans (
|
||||
work_plan_id TEXT PRIMARY KEY,
|
||||
goal_id TEXT NOT NULL,
|
||||
accepted_candidate_id TEXT NOT NULL,
|
||||
revision BIGINT NOT NULL,
|
||||
state TEXT NOT NULL,
|
||||
summary TEXT NOT NULL,
|
||||
recorded_at_unix_millis BIGINT NOT NULL,
|
||||
CHECK (goal_id <> ''),
|
||||
CHECK (accepted_candidate_id <> ''),
|
||||
CHECK (revision > 0),
|
||||
CHECK (state IN ('PROPOSED','VALIDATED','ACCEPTED','REJECTED'))
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS work.capability_requirements (
|
||||
capability_requirement_id TEXT PRIMARY KEY,
|
||||
plan_candidate_id TEXT NOT NULL,
|
||||
capability_kind TEXT NOT NULL,
|
||||
capability_id TEXT NOT NULL DEFAULT '',
|
||||
purpose TEXT NOT NULL,
|
||||
state TEXT NOT NULL,
|
||||
epistemic_precondition TEXT NOT NULL,
|
||||
recorded_at_unix_millis BIGINT NOT NULL,
|
||||
CHECK (plan_candidate_id <> ''),
|
||||
CHECK (capability_kind <> ''),
|
||||
CHECK (purpose <> ''),
|
||||
CHECK (state IN ('REQUIRED','AVAILABLE','MISSING','BLOCKED')),
|
||||
CHECK (epistemic_precondition IN ('UNVERIFIED','VERIFIED','REJECTED','UNKNOWN','NOT_APPLICABLE'))
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS work.plan_steps (
|
||||
plan_step_id TEXT PRIMARY KEY,
|
||||
plan_candidate_id TEXT NOT NULL,
|
||||
ordinal INTEGER NOT NULL,
|
||||
kind TEXT NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
requires_capability_id TEXT NOT NULL DEFAULT '',
|
||||
recorded_at_unix_millis BIGINT NOT NULL,
|
||||
CHECK (plan_candidate_id <> ''),
|
||||
CHECK (ordinal > 0),
|
||||
CHECK (kind IN ('INVESTIGATE','OBSERVE','VERIFY','REASON','REPORT','PREPARE','COMMUNICATE','SCHEDULE','MODIFY','RUN_CHECKS','CLARIFY','NO_EXECUTION')),
|
||||
CHECK (description <> '')
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS work.delegation_candidates (
|
||||
delegation_candidate_id TEXT PRIMARY KEY,
|
||||
plan_candidate_id TEXT NOT NULL,
|
||||
capability_requirement_id TEXT NOT NULL,
|
||||
kind TEXT NOT NULL,
|
||||
executor_hint TEXT NOT NULL DEFAULT '',
|
||||
state TEXT NOT NULL,
|
||||
reason TEXT NOT NULL,
|
||||
recorded_at_unix_millis BIGINT NOT NULL,
|
||||
CHECK (plan_candidate_id <> ''),
|
||||
CHECK (capability_requirement_id <> ''),
|
||||
CHECK (kind IN ('AGENT','TOOL','ADAPTER','HUMAN','UNAVAILABLE')),
|
||||
CHECK (state IN ('PROPOSED','SELECTED','BLOCKED')),
|
||||
CHECK (reason <> '')
|
||||
);
|
||||
|
||||
RESET ROLE;
|
||||
COMMIT;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
param(
|
||||
[string]$DockerContext = 'docker-prod',
|
||||
[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]$NasBackupRoot = '\\nas\MST\codex\_backups\domovoy\postgres',
|
||||
[string]$NasStatusPath = '\\nas\MST\codex\_monitoring\domovoy\status.json',
|
||||
@@ -102,15 +102,15 @@ try {
|
||||
catch { Add-Check 'tls-certificate' $false $_.Exception.Message }
|
||||
|
||||
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)
|
||||
$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 }
|
||||
|
||||
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' }
|
||||
$productionTime = [DateTime]::ParseExact($latestProduction, "yyyyMMdd'T'HHmmss'Z'", [Globalization.CultureInfo]::InvariantCulture, [Globalization.DateTimeStyles]::AssumeUniversal -bor [Globalization.DateTimeStyles]::AdjustToUniversal)
|
||||
$age = ([DateTime]::UtcNow - $productionTime).TotalHours
|
||||
|
||||
@@ -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-000063-intent-work-planning-foundation.json",
|
||||
"task_id": "DEV-000063",
|
||||
"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"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
{
|
||||
"task_id": "DEV-000063",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ADR-0012",
|
||||
"base_repo_revision": "18750c2ae69c90336900b333821cfbcf68769c16",
|
||||
"risk_tier": "E",
|
||||
"intent": "Add general HelpPath and optional Intent -> Goal -> Work Planning foundation without making Domovoy a Codex prompt generator or granting execution authority.",
|
||||
"requirement_ids": [
|
||||
"ADR-0012",
|
||||
"INV-AUTH-001",
|
||||
"INV-EPI-001",
|
||||
"INV-OWN-001"
|
||||
],
|
||||
"allowed_change_scope": {
|
||||
"modules": [
|
||||
"work",
|
||||
"interaction",
|
||||
"situation",
|
||||
"epistemics",
|
||||
"capability"
|
||||
],
|
||||
"paths": [
|
||||
"architecture/adrs/ADR-0012-intent-work-planning-foundation.md",
|
||||
"architecture/architecture-registry.yaml",
|
||||
"architecture/architecture-registry.json",
|
||||
"contracts/work/**",
|
||||
"contracts/interaction/**",
|
||||
"contracts/capability/**",
|
||||
"contracts/epistemics/**",
|
||||
"modules/work/**",
|
||||
"modules/interaction/**",
|
||||
"adapters/postgres/**",
|
||||
"database/bootstrap/**",
|
||||
"database/migrations/**",
|
||||
"cmd/core-runtime/**",
|
||||
"cmd/creator-web/**",
|
||||
"tests/work_planning/**",
|
||||
"tests/architecture/**",
|
||||
"docs/PROJECT_STATUS.md",
|
||||
"dev/ACTIVE_TASK.json",
|
||||
"dev/tasks/DEV-000063-intent-work-planning-foundation.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": [
|
||||
"Interaction OriginalHumanIntent",
|
||||
"Interaction UtteranceInterpretation",
|
||||
"Situation ActiveProjectFocus",
|
||||
"Epistemics VerificationState",
|
||||
"Capability IDs/health/registry concepts",
|
||||
"Authority boundary concepts"
|
||||
],
|
||||
"may_change": [
|
||||
"contracts/work@v1",
|
||||
"contracts/interaction@v1",
|
||||
"contracts/capability@v1"
|
||||
]
|
||||
},
|
||||
"semantic_owner": "work",
|
||||
"canonical_state": [
|
||||
"Goal",
|
||||
"HelpPath",
|
||||
"GoalLifecycle",
|
||||
"PlanCandidate",
|
||||
"WorkPlan",
|
||||
"PlanStep",
|
||||
"CapabilityRequirement",
|
||||
"DelegationCandidate"
|
||||
],
|
||||
"state_transitions": [
|
||||
"help path ANSWER/COGNITION/DIRECT_CAPABILITY/DURABLE_WORK/CAPABILITY_GAP selected before durable planning",
|
||||
"goal PROPOSED->UNDERSTOOD->PLANNING->READY when durable work is actually needed",
|
||||
"plan candidate PROPOSED->VALIDATED->ACCEPTED",
|
||||
"no transition to IN_PROGRESS without future execution authority"
|
||||
],
|
||||
"side_effects_and_privileges": [
|
||||
"planning-only canonical Work persistence",
|
||||
"ordinary answer/direct capability/capability gap paths without durable Work persistence",
|
||||
"no external mutation",
|
||||
"no Commit/Executor invocation",
|
||||
"no ACTIVE binding promotion",
|
||||
"cognitive routing only through existing gateway when later enabled"
|
||||
],
|
||||
"persistence_and_migrations": [
|
||||
"work owner-local goals",
|
||||
"work owner-local plan candidates",
|
||||
"work owner-local plan steps",
|
||||
"work owner-local capability requirements",
|
||||
"work owner-local delegation candidates",
|
||||
"no cross-owner FK",
|
||||
"no cross-owner SQL join dependency"
|
||||
],
|
||||
"failure_retry_restart": [
|
||||
"intent persists even if planning falls back deterministically",
|
||||
"Goal/Work/Plan are optional and not created for simple help",
|
||||
"invalid plan candidate fails closed",
|
||||
"cognition unavailable keeps deterministic planning available",
|
||||
"planning never claims execution/completion"
|
||||
],
|
||||
"assumptions": [
|
||||
"DEV-000062 is production baseline",
|
||||
"Work owner is canonical Goal/Task/Decision owner per Core Semantic Model",
|
||||
"existing CPU CANARY remains not ACTIVE",
|
||||
"docker-test prohibited"
|
||||
],
|
||||
"acceptance_scenarios": [
|
||||
"investigate UPO performance creates investigation plan, not Codex prompt",
|
||||
"add support X in Assistant creates MODIFY_CODE requirement without hardcoded executor",
|
||||
"review new email creates READ_EMAIL and COGNITIVE_REASONING requirements without Codex",
|
||||
"reminder creates SCHEDULE_REMINDER requirement",
|
||||
"unverified disk assertion creates verification requirement before risky model install",
|
||||
"send email creates SEND_EMAIL planning without execution or authority claim",
|
||||
"cognition unavailable still persists intent/goal/plan when durable work is needed",
|
||||
"simple explanation can use answer/cognition path without durable Goal/Work/Plan",
|
||||
"capability gap remains executor-neutral and future self-extension-ready"
|
||||
],
|
||||
"required_tests": [
|
||||
"go test ./...",
|
||||
"go vet ./...",
|
||||
"architecture",
|
||||
"owner ACL",
|
||||
"LLM-free",
|
||||
"work planning",
|
||||
"production live acceptance"
|
||||
],
|
||||
"terminal_gap_states": [
|
||||
"BLOCKED_BY_ARCHITECTURE_GAP",
|
||||
"BLOCKED_BY_SPEC_GAP",
|
||||
"BLOCKED_BY_MISSING_CAPABILITY",
|
||||
"BLOCKED_BY_CONFLICT"
|
||||
]
|
||||
}
|
||||
+24
-6
@@ -1,16 +1,34 @@
|
||||
# Assistant project status
|
||||
|
||||
Audit date: 2026-08-17. Active implementation: DEV-000061 revision 2.
|
||||
Audit date: 2026-08-17. Active implementation: DEV-000063 revision 1.
|
||||
|
||||
## DEV-000061 checkpoint
|
||||
## DEV-000063 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_000063_STATUS = COMPLETED`
|
||||
- `LAST_COMPLETED = ADR-0012, HelpPath optionality foundation, Work-owned Goal/PlanCandidate/WorkPlan/PlanStep/CapabilityRequirement/DelegationCandidate contracts, deterministic planning V1, optional durable Work creation from /chat, capability-gap/self-extension readiness tests, migration 017, production deployment and live /chat acceptance`
|
||||
- `HELP_PATH_OPTIONALITY = enabled; simple answer/cognition/direct capability/capability gap can bypass durable Goal/Work/Plan`
|
||||
- `GOAL_WORK_PLAN_OPTIONALITY = enabled; durable records are created only for complex work/planning cases`
|
||||
- `PROJECT_NEUTRALITY = project context is optional opaque Work reference; Project is not mandatory context`
|
||||
- `CODEX_NEUTRALITY = no Codex-specific canonical entity or executor selection`
|
||||
- `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`
|
||||
- `LIVE_ACCEPTANCE = PASS: simple help uses cognition without Work; reminder uses direct scheduling-capability path without Work; complex УПО investigation creates one Work Goal/Plan; capability gap remains executor-neutral without Work`
|
||||
- `PRODUCTION_REVISION = 9d50a3ec635782b59ab3335808d6d1e026be48a4`
|
||||
- `PRODUCTION_HEALTH = healthy`
|
||||
- `NEXT_STEP = DEV-000064 capability resolution/acquisition or concrete scheduler/mail/codex adapter foundation, if separately approved`
|
||||
- `REAL_BLOCKERS = none known`
|
||||
|
||||
## DEV-000062 checkpoint
|
||||
|
||||
- `DEV_000062_STATUS = COMPLETED`
|
||||
- `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 -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`
|
||||
- `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`
|
||||
- `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`
|
||||
|
||||
## Current milestone
|
||||
|
||||
@@ -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, "кто "):
|
||||
case strings.Contains(lower, "?") || strings.HasPrefix(lower, "что ") || strings.HasPrefix(lower, "как ") || strings.HasPrefix(lower, "почему ") || strings.HasPrefix(lower, "кто ") || strings.Contains(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, "удалить") || 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
|
||||
|
||||
@@ -36,6 +36,19 @@ func TestNewHumanUtteranceAppendsCorrectionRelation(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewHumanUtteranceClassifiesReminderAndCapabilityConnectionAsDirective(t *testing.T) {
|
||||
conversation := interaction.Conversation{ID: "conversation-1", ActorID: "actor-1", PrincipalID: "principal-1", Channel: interaction.ChannelWebChat, State: interaction.ConversationOpen}
|
||||
for _, message := range []string{"Напомни завтра позвонить врачу.", "Подключи интеграцию с неизвестным сервисом."} {
|
||||
utterance, _, _, err := NewHumanUtterance(conversation, interaction.ChatMessageRequest{Message: message}, time.Unix(10, 0))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if utterance.SpeechAct != interaction.SpeechActDirective {
|
||||
t.Fatalf("expected directive for %q, got %s", message, utterance.SpeechAct)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewConversationRequiresActorAndPrincipal(t *testing.T) {
|
||||
_, err := NewConversation(identity.ActorID(""), identity.PrincipalID("principal-1"), time.Unix(10, 0))
|
||||
if err == nil {
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,257 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/capability"
|
||||
"git.cin.su/m/Assistant/contracts/epistemics"
|
||||
"git.cin.su/m/Assistant/contracts/work"
|
||||
"git.cin.su/m/Assistant/platform/ids"
|
||||
)
|
||||
|
||||
var ErrInvalidPlanningInput = errors.New("invalid planning input")
|
||||
var ErrInvalidPlanCandidate = errors.New("invalid plan candidate")
|
||||
|
||||
func BuildPlanningResult(input work.PlanningInput, now time.Time) (work.PlanningResult, error) {
|
||||
if input.UtteranceID == "" || input.IntentID == "" || input.ActorID == "" || input.PrincipalID == "" || strings.TrimSpace(input.Message) == "" {
|
||||
return work.PlanningResult{}, ErrInvalidPlanningInput
|
||||
}
|
||||
millis := now.UnixMilli()
|
||||
goalID, err := ids.New()
|
||||
if err != nil {
|
||||
return work.PlanningResult{}, err
|
||||
}
|
||||
candidateID, err := ids.New()
|
||||
if err != nil {
|
||||
return work.PlanningResult{}, err
|
||||
}
|
||||
planID, err := ids.New()
|
||||
if err != nil {
|
||||
return work.PlanningResult{}, err
|
||||
}
|
||||
goal := work.Goal{
|
||||
ID: work.GoalID(goalID),
|
||||
SourceIntentID: input.IntentID,
|
||||
SourceUtteranceID: input.UtteranceID,
|
||||
ActorID: input.ActorID,
|
||||
PrincipalID: input.PrincipalID,
|
||||
ScopeID: input.ScopeID,
|
||||
ProjectID: input.ProjectID,
|
||||
ProjectDisplayName: input.ProjectDisplayName,
|
||||
DesiredOutcome: desiredOutcome(input),
|
||||
Constraints: "planning-only; no execution authority; no external mutation",
|
||||
State: work.GoalReady,
|
||||
Provenance: input.ProjectProvenance,
|
||||
Revision: 1,
|
||||
CreatedAtUnixMillis: millis,
|
||||
UpdatedAtUnixMillis: millis,
|
||||
}
|
||||
candidate := work.PlanCandidate{
|
||||
ID: work.PlanCandidateID(candidateID),
|
||||
GoalID: goal.ID,
|
||||
Source: "deterministic-planner-v1",
|
||||
State: work.PlanCandidateAccepted,
|
||||
Summary: planSummary(input),
|
||||
ValidationSummary: "validated: structured deterministic candidate; executor-neutral; authority not granted",
|
||||
RequiresClarification: false,
|
||||
AcceptedAtUnixMillis: millis,
|
||||
RecordedAtUnixMillis: millis,
|
||||
}
|
||||
addScenarioPlan(input, millis, &candidate)
|
||||
if hasRiskyUnverifiedAssertion(input) {
|
||||
addRequirement(&candidate, capability.CapabilityKindVerifyFact, "verify the user-provided resource assertion before risky work", epistemics.VerificationUnverified, millis)
|
||||
addStep(&candidate, work.PlanStepVerify, "Verify the reported server free-space/capacity using trusted observations before any large model installation.", len(candidate.Steps)+1, millis)
|
||||
}
|
||||
if err := ValidatePlanCandidate(candidate); err != nil {
|
||||
return work.PlanningResult{}, err
|
||||
}
|
||||
plan := work.WorkPlan{ID: work.WorkPlanID(planID), GoalID: goal.ID, AcceptedCandidateID: candidate.ID, Revision: 1, State: candidate.State, Summary: candidate.Summary, RecordedAtUnixMillis: millis}
|
||||
return work.PlanningResult{Goal: goal, Candidate: candidate, Plan: plan, Reply: planningReply(goal, candidate)}, nil
|
||||
}
|
||||
|
||||
func DetermineHelpPath(input work.PlanningInput) work.HelpPath {
|
||||
lower := strings.ToLower(input.Message)
|
||||
switch {
|
||||
case containsAny(lower, "объясни", "расскажи", "что делает"):
|
||||
return work.HelpPath{Kind: work.HelpPathCognition, Reason: "simple help can be answered without durable Goal/Work/Plan"}
|
||||
case containsAny(lower, "напомни", "напомин"):
|
||||
return work.HelpPath{Kind: work.HelpPathDirectCapability, SuggestedCapabilityKinds: []capability.CapabilityKind{capability.CapabilityKindScheduleReminder}, Reason: "reminder is a direct scheduling-capability path, not a coding/project workflow", Reply: "Понял: нужен reminder/scheduler capability. В DEV-000063 я фиксирую это как безопасный путь помощи; само напоминание ещё не зарегистрировано, потому что scheduling adapter не подключён."}
|
||||
case containsAny(lower, "не умеешь", "подключи интеграцию", "интеграц", "неизвестн", "нет capability", "создай adapter", "создай адаптер"):
|
||||
return work.HelpPath{Kind: work.HelpPathCapabilityGap, SuggestedCapabilityKinds: []capability.CapabilityKind{}, CapabilityGap: true, Reason: "requested help needs a currently missing capability and may later trigger acquisition/self-extension", Reply: "Похоже, для этой просьбы нужна capability, которой сейчас нет. Это не задача конкретного executor автоматически: в будущем Домовой сможет найти, подключить или создать подходящую capability и вернуться к исходной потребности."}
|
||||
case requiresDurableWork(lower):
|
||||
return work.HelpPath{Kind: work.HelpPathDurableWork, NeedsDurableGoal: true, NeedsDurableWork: true, NeedsDurablePlan: true, Reason: "request needs durable coordination, planning, investigation, delegation, or outcome tracking"}
|
||||
default:
|
||||
return work.HelpPath{Kind: work.HelpPathAnswer, Reason: "ordinary interaction does not require durable Work"}
|
||||
}
|
||||
}
|
||||
|
||||
func ValidatePlanCandidate(candidate work.PlanCandidate) error {
|
||||
if candidate.ID == "" || candidate.GoalID == "" || candidate.Source == "" || candidate.Summary == "" || len(candidate.Steps) == 0 {
|
||||
return ErrInvalidPlanCandidate
|
||||
}
|
||||
switch candidate.State {
|
||||
case work.PlanCandidateProposed, work.PlanCandidateValidated, work.PlanCandidateAccepted, work.PlanCandidateRejected:
|
||||
default:
|
||||
return ErrInvalidPlanCandidate
|
||||
}
|
||||
ordinals := map[int]bool{}
|
||||
for _, step := range candidate.Steps {
|
||||
if step.ID == "" || step.PlanCandidateID != candidate.ID || step.Ordinal < 1 || step.Kind == "" || step.Description == "" || ordinals[step.Ordinal] {
|
||||
return ErrInvalidPlanCandidate
|
||||
}
|
||||
ordinals[step.Ordinal] = true
|
||||
}
|
||||
seenRequirements := map[work.CapabilityRequirementID]bool{}
|
||||
for _, requirement := range candidate.Requirements {
|
||||
if requirement.ID == "" || requirement.PlanCandidateID != candidate.ID || requirement.CapabilityKind == "" || requirement.Purpose == "" || requirement.State == "" || requirement.EpistemicPrecondition == "" {
|
||||
return ErrInvalidPlanCandidate
|
||||
}
|
||||
seenRequirements[requirement.ID] = true
|
||||
}
|
||||
for _, delegation := range candidate.Delegations {
|
||||
if delegation.ID == "" || delegation.PlanCandidateID != candidate.ID || delegation.CapabilityRequirementID == "" || delegation.Kind == "" || delegation.State == "" || !seenRequirements[delegation.CapabilityRequirementID] {
|
||||
return ErrInvalidPlanCandidate
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func addScenarioPlan(input work.PlanningInput, millis int64, candidate *work.PlanCandidate) {
|
||||
lower := strings.ToLower(input.Message)
|
||||
switch {
|
||||
case containsAny(lower, "тормозит", "медлен", "производительн", "разберись", "почему"):
|
||||
addRequirement(candidate, capability.CapabilityKindInspectProject, "inspect project context", epistemics.VerificationNotApplicable, millis)
|
||||
addRequirement(candidate, capability.CapabilityKindReadDatabase, "collect database performance observations", epistemics.VerificationUnknown, millis)
|
||||
addRequirement(candidate, capability.CapabilityKindQueryLogs, "collect runtime/log observations", epistemics.VerificationUnknown, millis)
|
||||
addRequirement(candidate, capability.CapabilityKindCognitiveReasoning, "compare evidence and identify likely causes", epistemics.VerificationUnknown, millis)
|
||||
addStep(candidate, work.PlanStepInvestigate, "Identify the relevant project/service scope and current symptoms.", 1, millis)
|
||||
addStep(candidate, work.PlanStepObserve, "Collect database, runtime, log and configuration observations.", 2, millis)
|
||||
addStep(candidate, work.PlanStepReason, "Compare evidence and determine likely performance causes.", 3, millis)
|
||||
addStep(candidate, work.PlanStepReport, "Report findings and what remains unverified.", 4, millis)
|
||||
case containsAny(lower, "добавь", "поддержк", "возможность", "реализ"):
|
||||
addRequirement(candidate, capability.CapabilityKindModifyCode, "modify software project", epistemics.VerificationNotApplicable, millis)
|
||||
addRequirement(candidate, capability.CapabilityKindRunTests, "validate software change", epistemics.VerificationNotApplicable, millis)
|
||||
addStep(candidate, work.PlanStepInvestigate, "Understand desired capability and affected project scope.", 1, millis)
|
||||
addStep(candidate, work.PlanStepModify, "Prepare a code-change work item for a capable coding executor.", 2, millis)
|
||||
addStep(candidate, work.PlanStepRunChecks, "Run relevant tests and architecture checks before outcome claim.", 3, millis)
|
||||
case containsAny(lower, "письм", "почт"):
|
||||
addRequirement(candidate, capability.CapabilityKindReadEmail, "read incoming communications", epistemics.VerificationUnknown, millis)
|
||||
if containsAny(lower, "ответ", "ответить", "скажи") {
|
||||
addRequirement(candidate, capability.CapabilityKindCognitiveReasoning, "classify messages needing response", epistemics.VerificationUnknown, millis)
|
||||
}
|
||||
if containsAny(lower, "отправь", "напиши") {
|
||||
addRequirement(candidate, capability.CapabilityKindSendEmail, "send email only after authority", epistemics.VerificationNotApplicable, millis)
|
||||
}
|
||||
addStep(candidate, work.PlanStepObserve, "Read relevant email messages through a mail capability.", 1, millis)
|
||||
addStep(candidate, work.PlanStepReason, "Classify which messages need a response and why.", 2, millis)
|
||||
addStep(candidate, work.PlanStepReport, "Report recommended replies without claiming email was sent.", 3, millis)
|
||||
case containsAny(lower, "напомни", "завтра", "напомин"):
|
||||
addRequirement(candidate, capability.CapabilityKindScheduleReminder, "schedule a future reminder after temporal interpretation", epistemics.VerificationNotApplicable, millis)
|
||||
addStep(candidate, work.PlanStepSchedule, "Interpret the requested reminder time and reminder subject.", 1, millis)
|
||||
addStep(candidate, work.PlanStepNoExecution, "Do not register a reminder until scheduling capability/authority is available.", 2, millis)
|
||||
case containsAny(lower, "отправь", "ивану", "сообщи"):
|
||||
addRequirement(candidate, capability.CapabilityKindSendEmail, "communicate with the named recipient after channel/authority resolution", epistemics.VerificationNotApplicable, millis)
|
||||
addStep(candidate, work.PlanStepPrepare, "Resolve recipient and appropriate communication channel.", 1, millis)
|
||||
addStep(candidate, work.PlanStepCommunicate, "Prepare the message for a communication adapter; do not send without authority.", 2, millis)
|
||||
default:
|
||||
addRequirement(candidate, capability.CapabilityKindCognitiveReasoning, "clarify/decompose ambiguous human intent", epistemics.VerificationUnknown, millis)
|
||||
addStep(candidate, work.PlanStepClarify, "Clarify desired outcome and required capabilities.", 1, millis)
|
||||
candidate.RequiresClarification = true
|
||||
}
|
||||
for _, requirement := range candidate.Requirements {
|
||||
addDelegation(candidate, requirement, millis)
|
||||
}
|
||||
}
|
||||
|
||||
func requiresDurableWork(lower string) bool {
|
||||
return containsAny(lower, "тормозит", "медлен", "производительн", "разберись", "почему", "добавь", "поддержк", "возможность", "реализ", "письм", "почт", "отправь", "ивану", "сообщи", "установи", "большую модель")
|
||||
}
|
||||
|
||||
func addRequirement(candidate *work.PlanCandidate, kind capability.CapabilityKind, purpose string, precondition epistemics.VerificationState, millis int64) {
|
||||
id, err := ids.New()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
candidate.Requirements = append(candidate.Requirements, work.CapabilityRequirement{ID: work.CapabilityRequirementID(id), PlanCandidateID: candidate.ID, CapabilityKind: kind, Purpose: purpose, State: work.CapabilityRequirementMissing, EpistemicPrecondition: precondition, RecordedAtUnixMillis: millis})
|
||||
}
|
||||
|
||||
func addStep(candidate *work.PlanCandidate, kind work.PlanStepKind, description string, ordinal int, millis int64) {
|
||||
id, err := ids.New()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var requirementID work.CapabilityRequirementID
|
||||
if len(candidate.Requirements) > 0 {
|
||||
requirementID = candidate.Requirements[len(candidate.Requirements)-1].ID
|
||||
}
|
||||
candidate.Steps = append(candidate.Steps, work.PlanStep{ID: work.PlanStepID(id), PlanCandidateID: candidate.ID, Ordinal: ordinal, Kind: kind, Description: description, RequiresCapabilityID: requirementID, RecordedAtUnixMillis: millis})
|
||||
}
|
||||
|
||||
func addDelegation(candidate *work.PlanCandidate, requirement work.CapabilityRequirement, millis int64) {
|
||||
id, err := ids.New()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
candidate.Delegations = append(candidate.Delegations, work.DelegationCandidate{ID: work.DelegationCandidateID(id), PlanCandidateID: candidate.ID, CapabilityRequirementID: requirement.ID, Kind: delegationKind(requirement.CapabilityKind), ExecutorHint: "", State: work.DelegationCandidateBlocked, Reason: "no concrete executor selected in DEV-000063 planning-only mode", RecordedAtUnixMillis: millis})
|
||||
}
|
||||
|
||||
func delegationKind(kind capability.CapabilityKind) work.DelegationKind {
|
||||
switch kind {
|
||||
case capability.CapabilityKindModifyCode, capability.CapabilityKindCognitiveReasoning, capability.CapabilityKindWebResearch:
|
||||
return work.DelegationKindAgent
|
||||
case capability.CapabilityKindReadEmail, capability.CapabilityKindSendEmail, capability.CapabilityKindReadDatabase, capability.CapabilityKindQueryLogs, capability.CapabilityKindScheduleReminder, capability.CapabilityKindInspectProject, capability.CapabilityKindRunTests, capability.CapabilityKindVerifyFact:
|
||||
return work.DelegationKindAdapter
|
||||
default:
|
||||
return work.DelegationKindUnavailable
|
||||
}
|
||||
}
|
||||
|
||||
func desiredOutcome(input work.PlanningInput) string {
|
||||
lower := strings.ToLower(input.Message)
|
||||
switch {
|
||||
case containsAny(lower, "тормозит", "производительн"):
|
||||
return "identify likely performance cause with sufficient evidence"
|
||||
case containsAny(lower, "письм", "почт"):
|
||||
return "review communications and identify what needs response"
|
||||
case containsAny(lower, "напомни", "напомин"):
|
||||
return "create a future reminder when scheduling capability is available"
|
||||
case containsAny(lower, "отправь", "сообщи"):
|
||||
return "communicate requested information to the recipient after authority"
|
||||
case containsAny(lower, "добавь", "поддержк", "возможность"):
|
||||
return "software project gains the requested capability after validated implementation"
|
||||
default:
|
||||
return strings.TrimSpace(input.Message)
|
||||
}
|
||||
}
|
||||
|
||||
func planSummary(input work.PlanningInput) string {
|
||||
return "Planning foundation for: " + desiredOutcome(input)
|
||||
}
|
||||
|
||||
func planningReply(goal work.Goal, candidate work.PlanCandidate) string {
|
||||
requirements := make([]string, 0, len(candidate.Requirements))
|
||||
for _, requirement := range candidate.Requirements {
|
||||
requirements = append(requirements, string(requirement.CapabilityKind))
|
||||
}
|
||||
project := goal.ProjectDisplayName
|
||||
if project == "" {
|
||||
project = "текущий контекст"
|
||||
}
|
||||
return "Понял цель: " + goal.DesiredOutcome + ". Контекст: " + project + ". План сохранён в безопасном режиме: нужны capability " + strings.Join(requirements, ", ") + ". Исполнение этих инструментов пока не выполнено и требует отдельной capability/authority."
|
||||
}
|
||||
|
||||
func hasRiskyUnverifiedAssertion(input work.PlanningInput) bool {
|
||||
lower := strings.ToLower(input.Message)
|
||||
return input.AssertionVerification == epistemics.VerificationUnverified && containsAny(lower, "300 гб", "свободно", "большую модель", "установи", "модель")
|
||||
}
|
||||
|
||||
func containsAny(value string, needles ...string) bool {
|
||||
for _, needle := range needles {
|
||||
if strings.Contains(value, needle) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/capability"
|
||||
"git.cin.su/m/Assistant/contracts/epistemics"
|
||||
"git.cin.su/m/Assistant/contracts/identity"
|
||||
"git.cin.su/m/Assistant/contracts/work"
|
||||
)
|
||||
|
||||
func TestPlanningInvestigationIsNotCodexPrompt(t *testing.T) {
|
||||
result := planForTest(t, "Разберись, почему УПО тормозит.", "REQUEST_TO_INVESTIGATE", epistemics.VerificationNotApplicable)
|
||||
requireKinds(t, result, capability.CapabilityKindInspectProject, capability.CapabilityKindReadDatabase, capability.CapabilityKindQueryLogs, capability.CapabilityKindCognitiveReasoning)
|
||||
for _, delegation := range result.Candidate.Delegations {
|
||||
if strings.Contains(strings.ToLower(delegation.ExecutorHint), "codex") {
|
||||
t.Fatalf("planning must not hardcode Codex executor: %#v", delegation)
|
||||
}
|
||||
}
|
||||
if strings.Contains(strings.ToLower(result.Candidate.Summary), "prompt") {
|
||||
t.Fatalf("canonical plan must not be prompt-centric: %q", result.Candidate.Summary)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlanningCodeChangeRequiresModifyCodeWithoutExecutor(t *testing.T) {
|
||||
result := planForTest(t, "В Assistant добавь поддержку X.", "DIRECTIVE", epistemics.VerificationNotApplicable)
|
||||
requireKinds(t, result, capability.CapabilityKindModifyCode, capability.CapabilityKindRunTests)
|
||||
for _, delegation := range result.Candidate.Delegations {
|
||||
if delegation.ExecutorHint != "" {
|
||||
t.Fatalf("executor must remain unselected in canonical planning: %#v", delegation)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlanningEmailReviewIsNotCodexSpecific(t *testing.T) {
|
||||
result := planForTest(t, "Посмотри новые письма и скажи, на какие нужно ответить.", "DIRECTIVE", epistemics.VerificationNotApplicable)
|
||||
requireKinds(t, result, capability.CapabilityKindReadEmail, capability.CapabilityKindCognitiveReasoning)
|
||||
requireNoKind(t, result, capability.CapabilityKindModifyCode)
|
||||
}
|
||||
|
||||
func TestPlanningReminderUsesSchedulingRequirement(t *testing.T) {
|
||||
result := planForTest(t, "Напомни завтра позвонить врачу.", "DIRECTIVE", epistemics.VerificationNotApplicable)
|
||||
requireKinds(t, result, capability.CapabilityKindScheduleReminder)
|
||||
requireNoKind(t, result, capability.CapabilityKindModifyCode)
|
||||
}
|
||||
|
||||
func TestPlanningRiskyUnverifiedAssertionRequiresVerification(t *testing.T) {
|
||||
result := planForTest(t, "На сервере 300 ГБ свободно, установи большую модель.", "ASSERTION", epistemics.VerificationUnverified)
|
||||
requireKinds(t, result, capability.CapabilityKindVerifyFact)
|
||||
found := false
|
||||
for _, requirement := range result.Candidate.Requirements {
|
||||
if requirement.CapabilityKind == capability.CapabilityKindVerifyFact && requirement.EpistemicPrecondition == epistemics.VerificationUnverified {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Fatalf("expected unverified verification precondition: %#v", result.Candidate.Requirements)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlanningSendEmailPreservesAuthorityBoundary(t *testing.T) {
|
||||
result := planForTest(t, "Отправь Ивану письмо.", "DIRECTIVE", epistemics.VerificationNotApplicable)
|
||||
requireKinds(t, result, capability.CapabilityKindSendEmail)
|
||||
if result.Goal.State == work.GoalCompleted || !strings.Contains(result.Reply, "требует отдельной capability/authority") {
|
||||
t.Fatalf("planning must not claim execution/completion: goal=%#v reply=%q", result.Goal, result.Reply)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlanningFallbackPersistsWithoutCognition(t *testing.T) {
|
||||
result := planForTest(t, "Сделай что-нибудь полезное.", "DIRECTIVE", epistemics.VerificationUnknown)
|
||||
if result.Goal.ID == "" || result.Candidate.ID == "" || result.Plan.ID == "" {
|
||||
t.Fatalf("expected persisted planning identities: %#v", result)
|
||||
}
|
||||
if result.Candidate.Source != "deterministic-planner-v1" {
|
||||
t.Fatalf("expected deterministic fallback source: %s", result.Candidate.Source)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHelpPathSimpleAnswerDoesNotRequireDurableGoalWorkPlan(t *testing.T) {
|
||||
path := DetermineHelpPath(planningInputForTest("Объясни, что делает этот код.", "QUESTION", epistemics.VerificationNotApplicable))
|
||||
if path.Kind != work.HelpPathCognition || path.NeedsDurableGoal || path.NeedsDurableWork || path.NeedsDurablePlan {
|
||||
t.Fatalf("simple help must not require durable planning: %#v", path)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHelpPathReminderCanUseDirectCapabilityWithoutWorkflow(t *testing.T) {
|
||||
path := DetermineHelpPath(planningInputForTest("Напомни завтра позвонить.", "DIRECTIVE", epistemics.VerificationNotApplicable))
|
||||
if path.Kind != work.HelpPathDirectCapability || path.NeedsDurableWork || len(path.SuggestedCapabilityKinds) != 1 || path.SuggestedCapabilityKinds[0] != capability.CapabilityKindScheduleReminder {
|
||||
t.Fatalf("reminder should be direct scheduling capability path: %#v", path)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHelpPathComplexWorkMaterializesDurablePlanning(t *testing.T) {
|
||||
path := DetermineHelpPath(planningInputForTest("Разберись, почему УПО тормозит.", "REQUEST_TO_INVESTIGATE", epistemics.VerificationNotApplicable))
|
||||
if path.Kind != work.HelpPathDurableWork || !path.NeedsDurableGoal || !path.NeedsDurableWork || !path.NeedsDurablePlan {
|
||||
t.Fatalf("complex investigation should need durable planning: %#v", path)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHelpPathCapabilityGapDoesNotBecomeCodexTask(t *testing.T) {
|
||||
path := DetermineHelpPath(planningInputForTest("Подключи интеграцию с неизвестным сервисом, которого сейчас нет.", "DIRECTIVE", epistemics.VerificationNotApplicable))
|
||||
if path.Kind != work.HelpPathCapabilityGap || !path.CapabilityGap {
|
||||
t.Fatalf("expected capability gap path: %#v", path)
|
||||
}
|
||||
if strings.Contains(strings.ToLower(path.Reply), "codex") {
|
||||
t.Fatalf("capability gap must not be represented as Codex task: %#v", path)
|
||||
}
|
||||
}
|
||||
|
||||
func planForTest(t *testing.T, message string, act string, verification epistemics.VerificationState) work.PlanningResult {
|
||||
t.Helper()
|
||||
result, err := BuildPlanningResult(planningInputForTest(message, act, verification), time.Unix(100, 0))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := ValidatePlanCandidate(result.Candidate); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func planningInputForTest(message string, act string, verification epistemics.VerificationState) work.PlanningInput {
|
||||
return work.PlanningInput{
|
||||
ConversationID: "conversation-1",
|
||||
UtteranceID: "utterance-1",
|
||||
IntentID: "intent-1",
|
||||
ActorID: identity.ActorID("actor-1"),
|
||||
PrincipalID: identity.PrincipalID("principal-1"),
|
||||
ProjectID: work.ProjectRef("upo"),
|
||||
ProjectDisplayName: "УПО",
|
||||
ProjectProvenance: work.ProjectProvenanceRef("EXPLICIT"),
|
||||
Message: message,
|
||||
SpeechAct: work.SpeechActRef(act),
|
||||
AssertionVerification: verification,
|
||||
ReceivedAtUnixMillis: time.Unix(100, 0).UnixMilli(),
|
||||
}
|
||||
}
|
||||
|
||||
func requireKinds(t *testing.T, result work.PlanningResult, kinds ...capability.CapabilityKind) {
|
||||
t.Helper()
|
||||
actual := map[capability.CapabilityKind]bool{}
|
||||
for _, requirement := range result.Candidate.Requirements {
|
||||
actual[requirement.CapabilityKind] = true
|
||||
}
|
||||
for _, kind := range kinds {
|
||||
if !actual[kind] {
|
||||
t.Fatalf("missing capability kind %s in %#v", kind, result.Candidate.Requirements)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func requireNoKind(t *testing.T, result work.PlanningResult, kind capability.CapabilityKind) {
|
||||
t.Helper()
|
||||
for _, requirement := range result.Candidate.Requirements {
|
||||
if requirement.CapabilityKind == kind {
|
||||
t.Fatalf("unexpected capability kind %s in %#v", kind, result.Candidate.Requirements)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user