diff --git a/cmd/core-runtime/main.go b/cmd/core-runtime/main.go index c57d8b2..a28790e 100644 --- a/cmd/core-runtime/main.go +++ b/cmd/core-runtime/main.go @@ -702,13 +702,25 @@ func main() { researchBacked := false if hasChatRoute(routePlan, chatRouteResearch) && semantic.Interpretation.Clarification != interaction.ClarificationRequired { if planID, idErr := ids.New(); idErr == nil { - 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: effectiveTaskMessage, SourceClasses: []string{"web-search"}, FreshnessPolicy: "current"}, - PropositionKey: "question:" + string(utterance.ID), Query: effectiveTaskMessage, - }) - cancelResearch() - planningReply, researchBacked = renderResearchReply(result, researchErr) + parts := interactiondomain.IndependentRequestParts(effectiveTaskMessage) + replies := make([]string, 0, len(parts)) + for index, part := range parts { + researchCtx, cancelResearch := context.WithTimeout(r.Context(), 8*time.Second) + result, researchErr := researchClient.SearchWeb(researchCtx, epistemics.WebSearchRequest{ + Plan: epistemics.ResearchPlan{ID: epistemics.ResearchPlanID(fmt.Sprintf("plan:%s:%d", planID, index)), Question: part, SourceClasses: []string{"web-search"}, FreshnessPolicy: "current"}, + PropositionKey: fmt.Sprintf("question:%s:%d", utterance.ID, index), Query: part, + }) + cancelResearch() + reply, backed := renderResearchReply(result, researchErr) + if backed { + researchBacked = true + } + if len(parts) > 1 { + reply = "По запросу «" + part + "»: " + reply + } + replies = append(replies, reply) + } + planningReply = strings.Join(replies, "\n\n") if researchBacked { semantic.Reply = planningReply } diff --git a/modules/interaction/domain/semantics_test.go b/modules/interaction/domain/semantics_test.go index e7e92cc..ab31c00 100644 --- a/modules/interaction/domain/semantics_test.go +++ b/modules/interaction/domain/semantics_test.go @@ -69,6 +69,13 @@ func TestStripAssistantVocative(t *testing.T) { } } +func TestIndependentRequestParts(t *testing.T) { + parts := IndependentRequestParts("Дед, узнай погоду в Сочи и найди ресторан") + if len(parts) != 2 || parts[0] != "узнай погоду в Сочи" || parts[1] != "найди ресторан" { + t.Fatalf("unexpected request parts: %#v", parts) + } +} + func TestTravelSearchClarification(t *testing.T) { if got := TravelSearchClarification("Найди билеты", nil); got == "" { t.Fatal("underspecified travel request must ask for missing route and date") diff --git a/modules/interaction/domain/travel.go b/modules/interaction/domain/travel.go index 91a653a..04be7c4 100644 --- a/modules/interaction/domain/travel.go +++ b/modules/interaction/domain/travel.go @@ -25,6 +25,28 @@ func StripAssistantVocative(message string) string { return trimmed } +// IndependentRequestParts keeps separate user actions visible to the +// orchestrator. It is intentionally limited to the ordinary coordinating +// conjunction; the cognitive route remains responsible for ambiguous prose. +func IndependentRequestParts(message string) []string { + cleaned := StripAssistantVocative(message) + parts := strings.Split(cleaned, " и ") + if len(parts) < 2 { + return []string{cleaned} + } + result := make([]string, 0, len(parts)) + for _, part := range parts { + part = strings.TrimSpace(part) + if part != "" { + result = append(result, part) + } + } + if len(result) < 2 { + return []string{cleaned} + } + return result +} + // TravelSearchClarification keeps an incomplete travel request in dialogue // instead of reducing it to an unrelated generic web search. func TravelSearchClarification(message string, history []interaction.HumanUtterance) string {