From 97d43716a2ad1db447e0e659586959423f18afd4 Mon Sep 17 00:00:00 2001 From: LineWalker Date: Wed, 15 Jul 2026 11:48:00 +0800 Subject: [PATCH] fix(linsight): thread task-mode skill selection to the submit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up to the None≡[] skill gate: with skills now strictly opt-in, the daily task-mode path (ChatView → useAiChat → unified /chat/completions → _to_linsight_submit) exposed a latent gap — it never carried the picked skills, so the stored SV had skills=None and NOTHING was materialized. The picker looked broken ("workspace has no such skill file"). Previously this was masked because None loaded every enabled skill. Thread the selection end-to-end: - APIChatCompletion gains `skills` (Track H); _to_linsight_submit maps it onto the linsight submit schema (None/[] = no skills, opt-in list = those). - useAiChat sends the picked skill names (taskModeSkillsState('new')) on task-mode turns only; the daily chain ignores the field. - tests: _to_linsight_submit forwards a selection / stays None when absent. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../bisheng/api/v1/schema/chat_schema.py | 7 ++++++ .../domain/services/chat_service.py | 3 +++ .../workstation/test_unified_chat_entry.py | 25 +++++++++++++++++++ src/frontend/client/src/hooks/useAiChat.ts | 12 +++++++-- 4 files changed, 45 insertions(+), 2 deletions(-) diff --git a/src/backend/bisheng/api/v1/schema/chat_schema.py b/src/backend/bisheng/api/v1/schema/chat_schema.py index 779eea7b9..fc063f38b 100644 --- a/src/backend/bisheng/api/v1/schema/chat_schema.py +++ b/src/backend/bisheng/api/v1/schema/chat_schema.py @@ -100,6 +100,13 @@ class APIChatCompletion(BaseModel): # daily chain; the turn still lives in the same conversation (chat_id). task_mode: bool | None = False + # --- F035 Track H: per-turn selected skill names (task mode only) --- + # The skills the user picked in the daily task-mode input. Threaded onto the + # linsight submit schema by ``_to_linsight_submit``; ``None``/``[]`` both mean + # "no skills this turn" (skills are opt-in, see materialize_session_skills). + # Ignored on the plain daily chain. + skills: list[str] | None = None + # --- Preserved (new code-path also honours use_knowledge_base / files) --- use_knowledge_base: UseKnowledgeBaseParam | None = None files: list[dict] | None = None diff --git a/src/backend/bisheng/workstation/domain/services/chat_service.py b/src/backend/bisheng/workstation/domain/services/chat_service.py index c2534c7fe..29d781c44 100644 --- a/src/backend/bisheng/workstation/domain/services/chat_service.py +++ b/src/backend/bisheng/workstation/domain/services/chat_service.py @@ -2031,4 +2031,7 @@ def _to_linsight_submit(data: APIChatCompletion): model=data.model or None, files=submit_files, tools=submit_tools, + # F035 Track H: thread the daily task-mode skill picker onto the submit so + # only the user's explicit selection is materialized (None/[] = no skills). + skills=data.skills, ) diff --git a/src/backend/test/workstation/test_unified_chat_entry.py b/src/backend/test/workstation/test_unified_chat_entry.py index eda8f504a..c8e0180ef 100644 --- a/src/backend/test/workstation/test_unified_chat_entry.py +++ b/src/backend/test/workstation/test_unified_chat_entry.py @@ -106,3 +106,28 @@ def test_to_linsight_submit_no_knowledge_defaults_false(): assert submit.org_knowledge_enabled is False assert submit.personal_knowledge_enabled is False + + +def test_to_linsight_submit_forwards_selected_skills(): + """The daily task-mode skill picker must reach the submit schema. + + Regression: _to_linsight_submit dropped skills entirely, so the stored SV had + skills=None. That was masked while None meant "load all enabled skills"; once + None means "no skills", an explicit pick that never arrived silently loaded + nothing — the picker looked broken. + """ + data = APIChatCompletion( + clientTimestamp="t", conversationId="c", model="m", text="hi", task_mode=True, skills=["morning-report"] + ) + + submit = chat_service._to_linsight_submit(data) + + assert submit.skills == ["morning-report"] + + +def test_to_linsight_submit_no_skills_stays_none(): + data = APIChatCompletion(clientTimestamp="t", conversationId="c", model="m", text="hi", task_mode=True) + + submit = chat_service._to_linsight_submit(data) + + assert submit.skills is None diff --git a/src/frontend/client/src/hooks/useAiChat.ts b/src/frontend/client/src/hooks/useAiChat.ts index 00c6bc02a..d9096acbd 100644 --- a/src/frontend/client/src/hooks/useAiChat.ts +++ b/src/frontend/client/src/hooks/useAiChat.ts @@ -17,7 +17,7 @@ import useAiChatSSE, { type SSESubmission } from "~/hooks/useAiChatSSE"; import { useGetBsConfig } from "~/hooks/queries/data-provider"; import { useLinsightManager } from "~/hooks/useLinsightManager"; import { startLinsight, getLinsightSessionVersionList } from "~/api/linsight"; -import { SopStatus } from "~/store/linsight"; +import { SopStatus, taskModeSkillsState } from "~/store/linsight"; const NO_PARENT = "00000000-0000-0000-0000-000000000000"; @@ -46,6 +46,10 @@ export default function useAiChat(initialConversationId: string = "new", isLings // emits the new ChatResponse SSE format). Empty array keeps us on the // legacy flow so existing tests / old clients aren't disrupted. const [selectedAgentTools] = useRecoilState(store.selectedAgentTools); + // F035 Track H: skills picked in the daily task-mode input live in the shared + // 'new' atom (AiChatInput keys the picker there). Threaded onto the task-mode + // turn's submit payload so the user's explicit selection is what gets loaded. + const [dailyTaskSkills] = useRecoilState(taskModeSkillsState('new')); // Admin-level org-KB toggle. Knowledge spaces remain available even when // the org knowledge base feature is disabled, so we only strip org ids. const { data: bsConfig } = useGetBsConfig(); @@ -244,6 +248,10 @@ export default function useAiChat(initialConversationId: string = "new", isLings // F035 Track J (TJ-6): route this turn to the linsight task kernel // via the SAME unified entry. Backend replies with a handoff event. task_mode: taskMode, + // F035 Track H: send the picked skill names on task-mode turns so + // the backend materializes exactly those (empty = none). Omitted + // outside task mode (the daily chain ignores it). + skills: taskMode ? dailyTaskSkills.map((s) => s.name) : [], }; // Correlation key for the user (question) message. Starts as the @@ -609,7 +617,7 @@ export default function useAiChat(initialConversationId: string = "new", isLings setIsStreaming(true); setSseSubmission(submission); }, - [conversationId, isStreaming, chatModel, selectedOrgKbs, searchType, selectedAgentTools, isLingsi, createLinsight, updateLinsight, localize, queryClient] + [conversationId, isStreaming, chatModel, selectedOrgKbs, searchType, selectedAgentTools, dailyTaskSkills, isLingsi, createLinsight, updateLinsight, localize, queryClient] ); // --- Stop generating ---