From ac721216ea5156bfa4034ddbe30ccb87839cebe5 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Mon, 24 Aug 2026 20:23:17 +0300 Subject: [PATCH] feat: add semantic chat route orchestration --- cmd/core-runtime/chat_orchestrator.go | 71 ++++++++++++++++++++++ cmd/core-runtime/chat_orchestrator_test.go | 41 +++++++++++++ cmd/core-runtime/main.go | 9 ++- 3 files changed, 119 insertions(+), 2 deletions(-) create mode 100644 cmd/core-runtime/chat_orchestrator.go create mode 100644 cmd/core-runtime/chat_orchestrator_test.go diff --git a/cmd/core-runtime/chat_orchestrator.go b/cmd/core-runtime/chat_orchestrator.go new file mode 100644 index 0000000..a2e45ea --- /dev/null +++ b/cmd/core-runtime/chat_orchestrator.go @@ -0,0 +1,71 @@ +package main + +import ( + "sort" + + "git.cin.su/m/Assistant/contracts/capability" + interactiondomain "git.cin.su/m/Assistant/modules/interaction/domain" +) + +type chatRouteKind string + +const ( + chatRouteResearch chatRouteKind = "RESEARCH" + chatRouteConversation chatRouteKind = "CONVERSATION" + chatRouteAction chatRouteKind = "ACTION" + chatRouteAgent chatRouteKind = "AGENT" +) + +type chatRoute struct { + Kind chatRouteKind + Capability capability.CapabilityKind + Profile string + Order int +} + +// buildChatRoutePlan creates one bounded route per meaning-bearing capability. +// A failed research route must not suppress the local conversational route or +// an unrelated action route. +func buildChatRoutePlan(result interactiondomain.SemanticResult) []chatRoute { + seen := make(map[capability.CapabilityKind]struct{}) + routes := make([]chatRoute, 0, len(result.CapabilityHints)+1) + add := func(kind chatRouteKind, capabilityKind capability.CapabilityKind, profile string, order int) { + if capabilityKind != "" { + if _, exists := seen[capabilityKind]; exists { + return + } + seen[capabilityKind] = struct{}{} + } + routes = append(routes, chatRoute{Kind: kind, Capability: capabilityKind, Profile: profile, Order: order}) + } + for _, hint := range result.CapabilityHints { + switch capability.CapabilityKind(hint) { + case capability.CapabilityKindWebResearch: + add(chatRouteResearch, capability.CapabilityKindWebResearch, "truth-research-v1", 10) + case capability.CapabilityKindCognitiveReasoning, capability.CapabilityKindVerifyFact: + add(chatRouteConversation, capability.CapabilityKind(hint), "local-reasoning-v1", 20) + case capability.CapabilityKindScheduleReminder: + add(chatRouteAction, capability.CapabilityKindScheduleReminder, "continuity-reminder-v1", 30) + case capability.CapabilityKindModifyCode, capability.CapabilityKindRunTests, capability.CapabilityKindInspectProject: + add(chatRouteAgent, capability.CapabilityKind(hint), "software-agent-v1", 40) + case capability.CapabilityKindSendEmail, capability.CapabilityKindReadEmail: + add(chatRouteAgent, capability.CapabilityKind(hint), "communication-agent-v1", 50) + case capability.CapabilityKindReadDatabase, capability.CapabilityKindQueryLogs: + add(chatRouteAgent, capability.CapabilityKind(hint), "diagnostics-agent-v1", 60) + } + } + if result.NeedsCognition || len(routes) == 0 { + add(chatRouteConversation, capability.CapabilityKindCognitiveReasoning, "local-reasoning-v1", 20) + } + sort.SliceStable(routes, func(i, j int) bool { return routes[i].Order < routes[j].Order }) + return routes +} + +func hasChatRoute(routes []chatRoute, kind chatRouteKind) bool { + for _, route := range routes { + if route.Kind == kind { + return true + } + } + return false +} diff --git a/cmd/core-runtime/chat_orchestrator_test.go b/cmd/core-runtime/chat_orchestrator_test.go new file mode 100644 index 0000000..a70150f --- /dev/null +++ b/cmd/core-runtime/chat_orchestrator_test.go @@ -0,0 +1,41 @@ +package main + +import ( + "testing" + + "git.cin.su/m/Assistant/contracts/capability" + interactiondomain "git.cin.su/m/Assistant/modules/interaction/domain" +) + +func TestBuildChatRoutePlanPreservesMultipleMeaningfulRoutes(t *testing.T) { + planned := buildChatRoutePlan(interactiondomain.SemanticResult{ + NeedsCognition: true, + CapabilityHints: []string{"WEB_RESEARCH", "MODIFY_CODE", "RUN_TESTS", "SCHEDULE_REMINDER"}, + }) + if len(planned) != 5 { + t.Fatalf("expected five deduplicated routes, got %+v", planned) + } + want := []struct { + kind chatRouteKind + profile string + cap capability.CapabilityKind + }{ + {chatRouteResearch, "truth-research-v1", capability.CapabilityKindWebResearch}, + {chatRouteConversation, "local-reasoning-v1", capability.CapabilityKindCognitiveReasoning}, + {chatRouteAction, "continuity-reminder-v1", capability.CapabilityKindScheduleReminder}, + {chatRouteAgent, "software-agent-v1", capability.CapabilityKindModifyCode}, + {chatRouteAgent, "software-agent-v1", capability.CapabilityKindRunTests}, + } + for index, expected := range want { + if planned[index].Kind != expected.kind || planned[index].Profile != expected.profile || planned[index].Capability != expected.cap { + t.Fatalf("route %d = %+v, want %+v", index, planned[index], expected) + } + } +} + +func TestBuildChatRoutePlanAlwaysKeepsLocalConversationRoute(t *testing.T) { + planned := buildChatRoutePlan(interactiondomain.SemanticResult{NeedsCognition: true}) + if len(planned) != 1 || planned[0].Kind != chatRouteConversation || planned[0].Profile != "local-reasoning-v1" { + t.Fatalf("unexpected local route: %+v", planned) + } +} diff --git a/cmd/core-runtime/main.go b/cmd/core-runtime/main.go index 36531e1..07699a6 100644 --- a/cmd/core-runtime/main.go +++ b/cmd/core-runtime/main.go @@ -512,6 +512,7 @@ func main() { http.Error(w, "interpret chat message", http.StatusServiceUnavailable) return } + routePlan := buildChatRoutePlan(semantic) cognitiveAttempted := false cognitiveAvailable := true semanticInterpretationRequested := shouldRequestSemanticInterpretation(semantic) @@ -625,13 +626,17 @@ func main() { needsCognition = false } researchBacked := false - if hasCapabilityHint(semantic.CapabilityHints, "WEB_RESEARCH") && semantic.Interpretation.Clarification != interaction.ClarificationRequired { + if hasChatRoute(routePlan, chatRouteResearch) && semantic.Interpretation.Clarification != interaction.ClarificationRequired { if planID, idErr := ids.New(); idErr == nil { - result, researchErr := researchClient.SearchWeb(r.Context(), epistemics.WebSearchRequest{ + researchCtx, cancelResearch := context.WithTimeout(r.Context(), 8*time.Second) + result, researchErr := researchClient.SearchWeb(researchCtx, epistemics.WebSearchRequest{ Plan: epistemics.ResearchPlan{ID: epistemics.ResearchPlanID("plan:" + planID), Question: utterance.Content, SourceClasses: []string{"web-search"}, FreshnessPolicy: "current"}, PropositionKey: "question:" + string(utterance.ID), Query: utterance.Content, }) + cancelResearch() planningReply, researchBacked = renderResearchReply(result, researchErr) + // A failed source must not turn the complete conversation into a + // runtime failure or invite an unsupported local guess for a fact. needsCognition = false } } -- 2.54.0