318 lines
20 KiB
Go
318 lines
20 KiB
Go
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 input.RequiresClarification:
|
||
return work.HelpPath{Kind: work.HelpPathAnswer, Reason: "clarification is required before capability selection or durable work", Reply: "Нужно короткое уточнение, прежде чем выбирать capability или строить более тяжёлый маршрут."}
|
||
case isReminderRequest(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 isEmailReviewRequest(lower):
|
||
return work.HelpPath{Kind: work.HelpPathDurableWork, NeedsDurableGoal: true, NeedsDurableWork: true, NeedsDurablePlan: true, Reason: "mail triage/review needs structured reading and reasoning without claiming outbound send"}
|
||
case isExplicitEmailGapRequest(lower):
|
||
return work.HelpPath{Kind: work.HelpPathCapabilityGap, SuggestedCapabilityKinds: []capability.CapabilityKind{capability.CapabilityKindSendEmail}, CapabilityGap: true, Reason: "email sending needs a communication capability implementation and explicit authority", Reply: "Сейчас у меня ещё не подключена возможность отправлять почту. Я зафиксирую этот capability gap, чтобы позже к нему можно было вернуться."}
|
||
case isUnsupportedCapabilityRequest(lower):
|
||
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 needsDurableWorkFromHints(input):
|
||
return work.HelpPath{Kind: work.HelpPathDurableWork, NeedsDurableGoal: true, NeedsDurableWork: true, NeedsDurablePlan: true, Reason: "semantic hints indicate project/code work even if cognition may still be useful upstream"}
|
||
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"}
|
||
case containsAny(lower, "объясни", "расскажи", "что делает"):
|
||
return work.HelpPath{Kind: work.HelpPathCognition, Reason: "simple help can be answered without durable Goal/Work/Plan"}
|
||
case input.NeedsCognition && needsCognitiveAnswer(input):
|
||
return work.HelpPath{Kind: work.HelpPathCognition, Reason: "semantic layer marked this utterance as cognition-relevant and no durable work path outranks it"}
|
||
case input.NeedsCognition && (input.SpeechAct == work.SpeechActRef("QUESTION") || input.SpeechAct == work.SpeechActRef("REQUEST_TO_INVESTIGATE")):
|
||
return work.HelpPath{Kind: work.HelpPathCognition, Reason: "semantic layer marked this utterance as cognition-relevant"}
|
||
default:
|
||
return work.HelpPath{Kind: work.HelpPathAnswer, Reason: "ordinary interaction does not require durable Work"}
|
||
}
|
||
}
|
||
|
||
func needsDurableWorkFromHints(input work.PlanningInput) bool {
|
||
for _, hint := range input.CapabilityHints {
|
||
switch hint {
|
||
case capability.CapabilityKindModifyCode, capability.CapabilityKindRunTests, capability.CapabilityKindInspectProject, capability.CapabilityKindReadDatabase, capability.CapabilityKindQueryLogs:
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
func needsCognitiveAnswer(input work.PlanningInput) bool {
|
||
for _, hint := range input.CapabilityHints {
|
||
if hint == capability.CapabilityKindCognitiveReasoning {
|
||
return true
|
||
}
|
||
}
|
||
lower := strings.ToLower(input.Message)
|
||
return containsAny(lower, "модель", "gemma4", "qwen", "спроси", "обратись", "посчитай")
|
||
}
|
||
|
||
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)
|
||
if input.SecondarySpeechAct == work.SpeechActRef("CORRECTION") {
|
||
addStep(candidate, work.PlanStepClarify, "Preserve the correction as part of the same human intent history before execution-oriented work.", len(candidate.Steps)+1, millis)
|
||
}
|
||
switch {
|
||
case isReminderRequest(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 isEmailReviewRequest(lower):
|
||
addRequirement(candidate, capability.CapabilityKindReadEmail, "read incoming communications", epistemics.VerificationUnknown, millis)
|
||
addRequirement(candidate, capability.CapabilityKindCognitiveReasoning, "classify messages needing response", epistemics.VerificationUnknown, 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 isExplicitEmailGapRequest(lower):
|
||
addRequirement(candidate, capability.CapabilityKindSendEmail, "send email only after authority", 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)
|
||
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)
|
||
if containsAny(lower, "патч", "исправ") {
|
||
addRequirement(candidate, capability.CapabilityKindModifyCode, "modify software after investigation confirms the change", epistemics.VerificationUnknown, millis)
|
||
addRequirement(candidate, capability.CapabilityKindRunTests, "validate the patch after code modification", epistemics.VerificationNotApplicable, 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)
|
||
if containsAny(lower, "патч", "исправ") {
|
||
addStep(candidate, work.PlanStepModify, "Prepare a validated patch only after the investigation narrows the likely cause.", 4, millis)
|
||
addStep(candidate, work.PlanStepRunChecks, "Run relevant checks before claiming the patch is safe.", 5, millis)
|
||
} else {
|
||
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)
|
||
default:
|
||
for _, hint := range input.CapabilityHints {
|
||
addRequirement(candidate, hint, "semantic candidate hinted that this capability may be needed", epistemics.VerificationUnknown, millis)
|
||
}
|
||
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
|
||
}
|
||
|
||
func isReminderRequest(lower string) bool {
|
||
return containsAny(lower, "напомни", "напомин")
|
||
}
|
||
|
||
func isEmailReviewRequest(lower string) bool {
|
||
return containsAny(lower, "посмотри новые письма", "какие нужно ответить", "на какие нужно ответить", "разбери почту")
|
||
}
|
||
|
||
func isExplicitEmailGapRequest(lower string) bool {
|
||
hasMailObject := containsAny(lower, "письмо", "письма", "почту", "почта")
|
||
hasMailAction := containsAny(lower, "отправь", "ответить", "ответь", "напиши")
|
||
return hasMailObject && hasMailAction
|
||
}
|
||
|
||
func isUnsupportedCapabilityRequest(lower string) bool {
|
||
return containsAny(lower, "не умеешь", "подключи интеграцию", "подключи сервис", "интеграц", "неизвестн", "нет capability", "создай adapter", "создай адаптер", "сервис, которого сейчас нет")
|
||
}
|