260 lines
16 KiB
Go
260 lines
16 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 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, "отправь", "письм", "почт"):
|
||
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 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
|
||
}
|