diff --git a/cmd/creator-web/chat_test.go b/cmd/creator-web/chat_test.go index f24140b..b01b301 100644 --- a/cmd/creator-web/chat_test.go +++ b/cmd/creator-web/chat_test.go @@ -9,6 +9,7 @@ import ( "git.cin.su/m/Assistant/contracts/capability" "git.cin.su/m/Assistant/contracts/cognitive" + "git.cin.su/m/Assistant/contracts/continuity" "git.cin.su/m/Assistant/contracts/identity" "git.cin.su/m/Assistant/contracts/interaction" ) @@ -127,3 +128,44 @@ func TestChatMessageReturnsHumanCapabilityMetadata(t *testing.T) { t.Fatalf("unexpected browser response: %#v", response) } } + +func TestChatAttentionQuestionUsesContinuityToday(t *testing.T) { + core := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch { + case r.Method == http.MethodGet && r.URL.Path == "/v1/creator/sessions/current": + _ = json.NewEncoder(w).Encode(identity.CreatorSession{ActorID: "actor-1", PrincipalID: "principal-1"}) + case r.Method == http.MethodPost && r.URL.Path == "/v1/creator/chat/messages": + w.WriteHeader(http.StatusCreated) + _ = json.NewEncoder(w).Encode(interaction.ChatMessageResponse{ConversationID: "conversation-1", UtteranceID: "utterance-1", SpeechAct: interaction.SpeechActQuestion, FallbackText: "Сейчас в фокусе: УПО."}) + default: + t.Fatalf("unexpected core route %s %s", r.Method, r.URL.Path) + } + })) + defer core.Close() + continuityRuntime := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet || r.URL.Path != "/v1/creator/today" { + t.Fatalf("unexpected continuity route %s %s", r.Method, r.URL.Path) + } + _ = json.NewEncoder(w).Encode(continuity.TodayResult{Items: []continuity.TodayItem{{Title: "проверить уведомление", HumanState: "доставлено", DeliveryState: "уведомление отправлено"}}}) + })) + defer continuityRuntime.Close() + handler, err := newHandler(core.URL, continuityRuntime.URL, "http://commit", core.Client(), "http://cognitive") + if err != nil { + t.Fatal(err) + } + request := httptest.NewRequest(http.MethodPost, "/api/chat/messages", strings.NewReader(`{"message":"Что сейчас требует моего внимания?"}`)) + request.Header.Set("Authorization", "Bearer session-token") + request.Header.Set("Content-Type", "application/json") + recorder := httptest.NewRecorder() + handler.ServeHTTP(recorder, request) + if recorder.Code != http.StatusOK { + t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String()) + } + var response browserChatResponse + if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil { + t.Fatal(err) + } + if !strings.Contains(response.ReplyText, "проверить уведомление") || strings.Contains(response.ReplyText, "УПО") { + t.Fatalf("attention reply did not use canonical today: %#v", response) + } +} diff --git a/cmd/creator-web/server.go b/cmd/creator-web/server.go index 9125440..4844893 100644 --- a/cmd/creator-web/server.go +++ b/cmd/creator-web/server.go @@ -13,6 +13,7 @@ import ( "git.cin.su/m/Assistant/contracts/capability" "git.cin.su/m/Assistant/contracts/cognitive" + "git.cin.su/m/Assistant/contracts/continuity" "git.cin.su/m/Assistant/contracts/identity" "git.cin.su/m/Assistant/contracts/interaction" "git.cin.su/m/Assistant/contracts/situation" @@ -323,6 +324,15 @@ func (g *gateway) handleChatMessage(w http.ResponseWriter, r *http.Request) { if saved.PlanningReply != "" { reply = saved.PlanningReply } + if asksAttention(browserRequest.Message) { + var today continuity.TodayResult + status, err = g.getTrustedJSON(r, g.continuity, "/v1/creator/today", "", authorization, &today) + if err == nil && status == http.StatusOK { + reply = renderTodayReply(today) + cognition = "not_requested" + saved.NeedsCognition = false + } + } if saved.NeedsCognition && !saved.WorkPlanned && saved.Clarification != interaction.ClarificationRequired { cognition = "unavailable" requestID, idErr := ids.New() @@ -346,6 +356,45 @@ func (g *gateway) handleChatMessage(w http.ResponseWriter, r *http.Request) { _ = 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, CapabilityStatus: string(saved.CapabilityStatus), CapabilityKind: string(saved.CapabilityKind), ReminderScheduled: saved.Reminder != nil}) } +func asksAttention(message string) bool { + lower := strings.ToLower(message) + return strings.Contains(lower, "требует моего внимания") || strings.Contains(lower, "требует внимания") || strings.Contains(lower, "что сейчас важно") +} + +func renderTodayReply(today continuity.TodayResult) string { + if len(today.Items) == 0 { + return "Сейчас ничего срочного не требует внимания." + } + limit := len(today.Items) + if limit > 5 { + limit = 5 + } + var builder strings.Builder + builder.WriteString("Сейчас требует внимания:\n") + for index := 0; index < limit; index++ { + item := today.Items[index] + title := strings.TrimSpace(item.Title) + if title == "" { + title = "событие Домового" + } + state := strings.TrimSpace(item.HumanState) + if state == "" { + state = "ожидает внимания" + } + builder.WriteString("- ") + builder.WriteString(title) + builder.WriteString(" — ") + builder.WriteString(state) + if item.DeliveryState != "" { + builder.WriteString(" (") + builder.WriteString(item.DeliveryState) + builder.WriteString(")") + } + builder.WriteString("\n") + } + return strings.TrimSpace(builder.String()) +} + func validAdvisoryResponse(response cognitive.GatewayAdvisoryResponse, request cognitive.GatewayAdvisoryRequest) bool { if response.Version != cognitive.CreatorAdvisoryV1 || response.RequestID != request.RequestID || len(response.ReferencedExcerptIDs) != 0 { return false