From fc9fd93c360031fb9592ffc201dc755b59372396 Mon Sep 17 00:00:00 2001 From: steven_li Date: Wed, 10 Jun 2026 16:11:05 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E5=A4=9A=E8=AF=AD?= =?UTF-8?q?=E8=A8=80=E6=8F=90=E7=A4=BA=E8=AF=8D=E6=9C=AC=E5=9C=B0=E5=8C=96?= =?UTF-8?q?=E5=92=8C=E7=95=8C=E9=9D=A2=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加 prompt_locale 参数支持简体中文、繁体中文和英文提示词本地化 - 移除内置 agents 配置以简化系统架构 - 更新 ContextBuilder 使用动态提示词模板而非硬编码内容 - 在 AgentLoop、Web 接口和 AgentService 中传递 locale 参数 - 添加输出语言指令确保用户界面内容按指定语言生成 - 扩展前端 LanguageSwitcher 组件支持三种语言选项 - 优化 Header 和侧边栏组件的响应式布局和文本截断处理 - 更新测试用例验证不同语言环境下的提示词正确性 --- app-instance/backend/agents/registry.json | 143 +- .../backend/beaver/engine/context/builder.py | 11 +- app-instance/backend/beaver/engine/loop.py | 4 + .../backend/beaver/interfaces/web/app.py | 2 + .../beaver/interfaces/web/schemas/chat.py | 1 + .../backend/beaver/prompts/__init__.py | 5 + .../backend/beaver/prompts/main_agent.py | 55 + .../backend/beaver/prompts/main_agent/en.md | 7 + .../beaver/prompts/main_agent/zh-Hans.md | 7 + .../beaver/prompts/main_agent/zh-Hant.md | 7 + .../backend/beaver/services/agent_service.py | 29 +- .../tests/unit/test_context_builder.py | 23 + .../tests/unit/test_task_mode_feedback.py | 48 + .../backend/tests/unit/test_websocket_chat.py | 9 +- app-instance/frontend/app/(app)/page.tsx | 7 +- .../app/(app)/tasks/[taskId]/page.tsx | 3 +- .../frontend/app/(app)/tasks/page.tsx | 8 +- app-instance/frontend/components/Header.tsx | 12 +- .../frontend/components/LanguageSwitcher.tsx | 59 +- .../chat-workbench/AgentTeamBlock.tsx | 11 +- .../chat-workbench/ArtifactSidebar.tsx | 2 +- .../CurrentSessionProgressSidebar.tsx | 26 +- .../task-detail/TaskAcceptanceCard.tsx | 4 +- .../components/task-detail/TaskLiveHeader.tsx | 2 +- .../components/task-detail/TaskSideRail.tsx | 4 +- .../task-detail/TaskTimelineCard.tsx | 32 +- .../task-runtime/TaskRuntimeShared.tsx | 4 +- .../frontend/components/ui/select.tsx | 2 +- app-instance/frontend/lib/api.ts | 24 +- app-instance/frontend/lib/i18n/core.test.ts | 32 + app-instance/frontend/lib/i18n/core.ts | 517 +++++- .../frontend/lib/task-timeline-view.test.ts | 2 + .../frontend/lib/task-timeline-view.ts | 4 + .../frontend/lib/task-timeline.test.ts | 42 + app-instance/frontend/lib/task-timeline.ts | 70 +- .../skill-replay-eval/index.html | 592 +++---- docs/product-discovery/README.md | 11 + .../beaver/PRD-beaver-agent-sandbox.md | 489 ++++++ docs/product-discovery/beaver/README.md | 30 + docs/product-discovery/beaver/index.html | 1277 +++++++++++++++ .../beaver/launch-maintenance-runbook.md | 455 ++++++ .../beaver/product-architecture-brief.md | 439 +++++ .../beaver/product-discovery-report.md | 494 ++++++ .../product-discovery/beaver/product-prd.html | 1449 +++++++++++++++++ .../beaver/validation-plan.md | 378 +++++ .../PRD-skill-replay-eval.md | 387 +++++ .../skill-replay-eval/README.md | 13 + .../launch-maintenance-runbook.md | 356 ++++ .../product-discovery-report.md | 512 ++++++ .../plans/2026-06-08-skill-replay-eval.md | 6 + .../2026-06-08-skill-replay-eval-design.md | 6 + 51 files changed, 7493 insertions(+), 619 deletions(-) create mode 100644 app-instance/backend/beaver/prompts/__init__.py create mode 100644 app-instance/backend/beaver/prompts/main_agent.py create mode 100644 app-instance/backend/beaver/prompts/main_agent/en.md create mode 100644 app-instance/backend/beaver/prompts/main_agent/zh-Hans.md create mode 100644 app-instance/backend/beaver/prompts/main_agent/zh-Hant.md create mode 100644 app-instance/frontend/lib/i18n/core.test.ts create mode 100644 docs/product-discovery/README.md create mode 100644 docs/product-discovery/beaver/PRD-beaver-agent-sandbox.md create mode 100644 docs/product-discovery/beaver/README.md create mode 100644 docs/product-discovery/beaver/index.html create mode 100644 docs/product-discovery/beaver/launch-maintenance-runbook.md create mode 100644 docs/product-discovery/beaver/product-architecture-brief.md create mode 100644 docs/product-discovery/beaver/product-discovery-report.md create mode 100644 docs/product-discovery/beaver/product-prd.html create mode 100644 docs/product-discovery/beaver/validation-plan.md create mode 100644 docs/product-discovery/skill-replay-eval/PRD-skill-replay-eval.md create mode 100644 docs/product-discovery/skill-replay-eval/README.md create mode 100644 docs/product-discovery/skill-replay-eval/launch-maintenance-runbook.md create mode 100644 docs/product-discovery/skill-replay-eval/product-discovery-report.md diff --git a/app-instance/backend/agents/registry.json b/app-instance/backend/agents/registry.json index 2d58775..4896bb3 100644 --- a/app-instance/backend/agents/registry.json +++ b/app-instance/backend/agents/registry.json @@ -1,145 +1,4 @@ { - "agents": [ - { - "agent_id": "researcher", - "capabilities": [ - "research", - "analysis", - "source review", - "requirements" - ], - "created_at": "2026-05-27T05:25:11.756341+00:00", - "description": "Finds facts, references, constraints, and implementation options.", - "display_name": "Researcher", - "metadata": {}, - "model": null, - "name": "researcher", - "priority": 50, - "provider_name": null, - "role": "research", - "skill_names": [], - "source": "builtin", - "status": "active", - "system_prompt": "You are a research specialist. Gather concise evidence and tradeoffs for the parent task.", - "tags": [ - "planning", - "research" - ], - "tool_hints": [], - "updated_at": "2026-05-27T05:25:11.756349+00:00" - }, - { - "agent_id": "implementer", - "capabilities": [ - "implementation", - "coding", - "refactor", - "integration" - ], - "created_at": "2026-05-27T05:25:11.756351+00:00", - "description": "Builds scoped implementation slices and proposes concrete changes.", - "display_name": "Implementer", - "metadata": {}, - "model": null, - "name": "implementer", - "priority": 45, - "provider_name": null, - "role": "implementation", - "skill_names": [], - "source": "builtin", - "status": "active", - "system_prompt": "You are an implementation specialist. Produce practical, scoped implementation output.", - "tags": [ - "coding", - "build" - ], - "tool_hints": [], - "updated_at": "2026-05-27T05:25:11.756353+00:00" - }, - { - "agent_id": "reviewer", - "capabilities": [ - "review", - "quality", - "risk", - "verification" - ], - "created_at": "2026-05-27T05:25:11.756355+00:00", - "description": "Reviews plans, code, outputs, and risks before final synthesis.", - "display_name": "Reviewer", - "metadata": {}, - "model": null, - "name": "reviewer", - "priority": 45, - "provider_name": null, - "role": "review", - "skill_names": [], - "source": "builtin", - "status": "active", - "system_prompt": "You are a review specialist. Focus on defects, missing requirements, and risks.", - "tags": [ - "review", - "quality" - ], - "tool_hints": [], - "updated_at": "2026-05-27T05:25:11.756356+00:00" - }, - { - "agent_id": "tester", - "capabilities": [ - "testing", - "verification", - "regression", - "qa" - ], - "created_at": "2026-05-27T05:25:11.756358+00:00", - "description": "Designs and executes verification checks for task outputs.", - "display_name": "Tester", - "metadata": {}, - "model": null, - "name": "tester", - "priority": 40, - "provider_name": null, - "role": "testing", - "skill_names": [], - "source": "builtin", - "status": "active", - "system_prompt": "You are a testing specialist. Identify focused checks and report pass/fail evidence.", - "tags": [ - "test", - "quality" - ], - "tool_hints": [], - "updated_at": "2026-05-27T05:25:11.756358+00:00" - }, - { - "agent_id": "documenter", - "capabilities": [ - "documentation", - "explanation", - "migration notes", - "release notes" - ], - "created_at": "2026-05-27T05:25:11.756360+00:00", - "description": "Writes and reconciles user-facing and internal documentation updates.", - "display_name": "Documenter", - "metadata": {}, - "model": null, - "name": "documenter", - "priority": 35, - "provider_name": null, - "role": "documentation", - "skill_names": [], - "source": "builtin", - "status": "active", - "system_prompt": "You are a documentation specialist. Produce concise docs aligned with the implementation.", - "tags": [ - "docs", - "communication" - ], - "tool_hints": [], - "updated_at": "2026-05-27T05:25:11.756360+00:00" - } - ], + "agents": [], "version": 1 } diff --git a/app-instance/backend/beaver/engine/context/builder.py b/app-instance/backend/beaver/engine/context/builder.py index 00df740..c229635 100644 --- a/app-instance/backend/beaver/engine/context/builder.py +++ b/app-instance/backend/beaver/engine/context/builder.py @@ -27,13 +27,7 @@ from dataclasses import dataclass, field from typing import Any from beaver.memory.curated.snapshot import MemorySnapshot - - -BEAVER_USER_ASSISTANT_IDENTITY_PROMPT = ( - "You are 海狸 (Beaver), an AI assistant developed by 博维资讯系统有限公司. " - "When communicating with users, keep this identity consistent. " - "If users ask who you are, say that you are 海狸 (Beaver), 博维资讯系统有限公司研发的 AI 助手." -) +from beaver.prompts import get_main_agent_prompt @dataclass(slots=True) @@ -113,6 +107,7 @@ class ContextBuildInput: """ base_system_prompt: str = "" + prompt_locale: str | None = None history: list[dict[str, Any]] = field(default_factory=list) current_user_input: str | list[dict[str, Any]] | None = None memory_snapshot: MemorySnapshot | None = None @@ -171,7 +166,7 @@ class ContextBuilder: - activated skill 正文放到显式消息里,避免 system prompt 持续膨胀 """ - sections: list[str] = [BEAVER_USER_ASSISTANT_IDENTITY_PROMPT] + sections: list[str] = [get_main_agent_prompt(build_input.prompt_locale)] base_system_prompt = (build_input.base_system_prompt or "").strip() if base_system_prompt: diff --git a/app-instance/backend/beaver/engine/loop.py b/app-instance/backend/beaver/engine/loop.py index 3d06b47..a1a98c2 100644 --- a/app-instance/backend/beaver/engine/loop.py +++ b/app-instance/backend/beaver/engine/loop.py @@ -224,6 +224,7 @@ class AgentLoop: title: str | None = None, execution_context: str | None = None, skill_selection_context: str | None = None, + prompt_locale: str | None = None, model: str | None = None, provider_name: str | None = None, api_key: str | None = None, @@ -275,6 +276,7 @@ class AgentLoop: title=title, execution_context=execution_context, skill_selection_context=skill_selection_context, + prompt_locale=prompt_locale, model=model, provider_name=provider_name, api_key=api_key, @@ -314,6 +316,7 @@ class AgentLoop: title: str | None = None, execution_context: str | None = None, skill_selection_context: str | None = None, + prompt_locale: str | None = None, model: str | None = None, provider_name: str | None = None, api_key: str | None = None, @@ -572,6 +575,7 @@ class AgentLoop: build_input = ContextBuildInput( base_system_prompt=self.profile.system_prompt, + prompt_locale=prompt_locale, history=session_manager.get_history( resolved_session_id, max_messages=max(1, self.profile.max_context_messages), diff --git a/app-instance/backend/beaver/interfaces/web/app.py b/app-instance/backend/beaver/interfaces/web/app.py index 9b88f86..24a5981 100644 --- a/app-instance/backend/beaver/interfaces/web/app.py +++ b/app-instance/backend/beaver/interfaces/web/app.py @@ -2463,6 +2463,7 @@ def create_app( "user_id": payload.user_id, "title": payload.title, "execution_context": payload.execution_context, + "prompt_locale": payload.prompt_locale, "model": payload.model, "provider_name": payload.provider_name, "embedding_model": payload.embedding_model, @@ -2578,6 +2579,7 @@ def create_app( "user_id": _clean_text(payload.get("user_id")) or None, "title": _clean_text(payload.get("title")) or None, "execution_context": _clean_text(payload.get("execution_context")) or None, + "prompt_locale": _clean_text(payload.get("prompt_locale")) or None, "model": _clean_text(payload.get("model")) or None, "provider_name": _clean_text(payload.get("provider_name")) or None, "embedding_model": _clean_text(payload.get("embedding_model")) or None, diff --git a/app-instance/backend/beaver/interfaces/web/schemas/chat.py b/app-instance/backend/beaver/interfaces/web/schemas/chat.py index a22cfc2..66af3c6 100644 --- a/app-instance/backend/beaver/interfaces/web/schemas/chat.py +++ b/app-instance/backend/beaver/interfaces/web/schemas/chat.py @@ -55,6 +55,7 @@ class WebChatRequest(BaseModel): user_id: str | None = None title: str | None = None execution_context: str | None = None + prompt_locale: str | None = None model: str | None = None provider_name: str | None = None embedding_model: str | None = None diff --git a/app-instance/backend/beaver/prompts/__init__.py b/app-instance/backend/beaver/prompts/__init__.py new file mode 100644 index 0000000..ca3a497 --- /dev/null +++ b/app-instance/backend/beaver/prompts/__init__.py @@ -0,0 +1,5 @@ +"""Prompt templates used by Beaver runtime components.""" + +from .main_agent import get_main_agent_prompt + +__all__ = ["get_main_agent_prompt"] diff --git a/app-instance/backend/beaver/prompts/main_agent.py b/app-instance/backend/beaver/prompts/main_agent.py new file mode 100644 index 0000000..064a982 --- /dev/null +++ b/app-instance/backend/beaver/prompts/main_agent.py @@ -0,0 +1,55 @@ +"""Locale-aware main agent prompt loading.""" + +from __future__ import annotations + +from functools import lru_cache +from pathlib import Path + +DEFAULT_MAIN_AGENT_PROMPT_LOCALE = "zh-Hans" + +_PROMPT_FILES = { + "zh-Hans": "zh-Hans.md", + "zh-Hant": "zh-Hant.md", + "en": "en.md", +} + +_LOCALE_ALIASES = { + "zh": "zh-Hans", + "zh-cn": "zh-Hans", + "zh-hans": "zh-Hans", + "zh-sg": "zh-Hans", + "zh-hant": "zh-Hant", + "zh-tw": "zh-Hant", + "zh-hk": "zh-Hant", + "zh-mo": "zh-Hant", + "en": "en", + "en-us": "en", + "en-gb": "en", +} + + +def get_main_agent_prompt(locale: str | None = None) -> str: + """Return the main-agent identity prompt for a prompt locale.""" + + prompt_locale = normalize_main_agent_prompt_locale(locale) + return _load_main_agent_prompt(prompt_locale) + + +def normalize_main_agent_prompt_locale(locale: str | None = None) -> str: + cleaned = (locale or DEFAULT_MAIN_AGENT_PROMPT_LOCALE).strip() + if not cleaned: + return DEFAULT_MAIN_AGENT_PROMPT_LOCALE + normalized = _LOCALE_ALIASES.get(cleaned.lower()) + if normalized: + return normalized + return cleaned if cleaned in _PROMPT_FILES else DEFAULT_MAIN_AGENT_PROMPT_LOCALE + + +@lru_cache(maxsize=len(_PROMPT_FILES)) +def _load_main_agent_prompt(locale: str) -> str: + filename = _PROMPT_FILES.get(locale, _PROMPT_FILES[DEFAULT_MAIN_AGENT_PROMPT_LOCALE]) + path = Path(__file__).with_name("main_agent") / filename + if not path.exists(): + fallback_path = Path(__file__).with_name("main_agent") / _PROMPT_FILES[DEFAULT_MAIN_AGENT_PROMPT_LOCALE] + return fallback_path.read_text(encoding="utf-8").strip() + return path.read_text(encoding="utf-8").strip() diff --git a/app-instance/backend/beaver/prompts/main_agent/en.md b/app-instance/backend/beaver/prompts/main_agent/en.md new file mode 100644 index 0000000..7478dbd --- /dev/null +++ b/app-instance/backend/beaver/prompts/main_agent/en.md @@ -0,0 +1,7 @@ +You are Beaver, an AI assistant developed by Boway Information Systems Co., Ltd. + +When communicating with users, keep this identity consistent. If users ask who you are, say that you are Beaver, an AI assistant developed by Boway Information Systems Co., Ltd. + +# Language + +Use English for user-facing replies, task titles, summaries, plans, and final reports while this prompt is active. If the user explicitly asks for another language, follow that request. diff --git a/app-instance/backend/beaver/prompts/main_agent/zh-Hans.md b/app-instance/backend/beaver/prompts/main_agent/zh-Hans.md new file mode 100644 index 0000000..722ca34 --- /dev/null +++ b/app-instance/backend/beaver/prompts/main_agent/zh-Hans.md @@ -0,0 +1,7 @@ +你是海狸 (Beaver),博维资讯系统有限公司研发的 AI 助手。 + +与用户沟通时,保持这个身份一致。用户问你是谁时,说明你是海狸 (Beaver),博维资讯系统有限公司研发的 AI 助手。 + +# 语言 + +使用简体中文进行面向用户的回复、任务标题、摘要、计划和最终报告。若用户明确要求其他语言,则按用户要求执行。 diff --git a/app-instance/backend/beaver/prompts/main_agent/zh-Hant.md b/app-instance/backend/beaver/prompts/main_agent/zh-Hant.md new file mode 100644 index 0000000..2dfca47 --- /dev/null +++ b/app-instance/backend/beaver/prompts/main_agent/zh-Hant.md @@ -0,0 +1,7 @@ +你是海狸 (Beaver),博維資訊系統有限公司研發的 AI 助手。 + +與使用者溝通時,保持這個身份一致。使用者問你是誰時,說明你是海狸 (Beaver),博維資訊系統有限公司研發的 AI 助手。 + +# 語言 + +使用繁體中文進行面向使用者的回覆、任務標題、摘要、計劃和最終報告。若使用者明確要求其他語言,則按使用者要求執行。 diff --git a/app-instance/backend/beaver/services/agent_service.py b/app-instance/backend/beaver/services/agent_service.py index c3d87a6..3cefd0d 100644 --- a/app-instance/backend/beaver/services/agent_service.py +++ b/app-instance/backend/beaver/services/agent_service.py @@ -22,6 +22,7 @@ from beaver.engine import AgentLoop, AgentProfile, AgentRunResult, EngineLoader from beaver.engine.providers import make_provider_bundle from beaver.foundation.events import InboundMessage, OutboundMessage from beaver.foundation.models import CronJob, CronRunRecord +from beaver.prompts.main_agent import normalize_main_agent_prompt_locale from beaver.tasks import ( EvidenceBuilder, MainAgentRouter, @@ -622,6 +623,7 @@ class AgentService: session_id=session_id, description=message, metadata={ + "prompt_locale": normalize_main_agent_prompt_locale(kwargs.get("prompt_locale")), "router_reason": decision.reason, **({"short_title": decision.short_title} if decision.short_title else {}), }, @@ -749,6 +751,8 @@ class AgentService: session_manager = self._require_loaded(loaded, "session_manager") base_execution_context = kwargs.get("execution_context") + prompt_locale = kwargs.get("prompt_locale") or task.metadata.get("prompt_locale") + output_language_instruction = self._output_language_instruction(prompt_locale) provider_bundle = kwargs.get("provider_bundle") or self._make_provider_bundle_for_task(loaded, kwargs) kwargs = dict(kwargs) team_provider_bundle_factory = kwargs.pop("team_provider_bundle_factory", None) @@ -843,8 +847,11 @@ class AgentService: "allow_candidate_generation": False, } ) - if team_execution_context: - attempt_kwargs["execution_context"] = self._join_context(base_execution_context, team_execution_context) + attempt_kwargs["execution_context"] = self._join_context( + base_execution_context, + output_language_instruction, + team_execution_context, + ) if plan.is_team and team_execution_context: attempt_kwargs["include_tools"] = False attempt_kwargs["max_tool_iterations"] = 0 @@ -979,6 +986,24 @@ class AgentService: "short_title": decision.short_title, } + @staticmethod + def _output_language_instruction(prompt_locale: str | None) -> str: + locale = normalize_main_agent_prompt_locale(prompt_locale) + if locale == "en": + return ( + "Output language: English. Use English for user-facing task titles, summaries, plans, " + "and final answers unless the user explicitly requests another language." + ) + if locale == "zh-Hant": + return ( + "輸出語言:繁體中文。除非使用者明確要求其他語言,所有面向使用者的任務標題、摘要、" + "計劃與最終回答都使用繁體中文。" + ) + return ( + "输出语言:简体中文。除非用户明确要求其他语言,所有面向用户的任务标题、摘要、" + "计划与最终回答都使用简体中文。" + ) + @staticmethod def _skill_names_for_run(loaded: Any, run_id: str) -> list[str]: store = getattr(loaded, "run_memory_store", None) diff --git a/app-instance/backend/tests/unit/test_context_builder.py b/app-instance/backend/tests/unit/test_context_builder.py index 42eb8ae..6cfd925 100644 --- a/app-instance/backend/tests/unit/test_context_builder.py +++ b/app-instance/backend/tests/unit/test_context_builder.py @@ -26,3 +26,26 @@ def test_context_builder_injects_current_date_and_time() -> None: assert "Local UTC offset: +08:00" in system_prompt assert '"today", "tomorrow", "now", "this week", and "next month"' in system_prompt assert result.messages[-1] == {"role": "user", "content": "今天几号?"} + + +def test_context_builder_uses_simplified_main_agent_prompt_by_default() -> None: + system_prompt = ContextBuilder().build_system_prompt(ContextBuildInput()) + + assert "你是海狸 (Beaver)" in system_prompt + assert "博维资讯系统有限公司研发的 AI 助手" in system_prompt + assert "使用简体中文进行面向用户的回复" in system_prompt + + +def test_context_builder_uses_traditional_main_agent_prompt_for_zh_hant() -> None: + system_prompt = ContextBuilder().build_system_prompt(ContextBuildInput(prompt_locale="zh-Hant")) + + assert "你是海狸 (Beaver)" in system_prompt + assert "博維資訊系統有限公司研發的 AI 助手" in system_prompt + assert "使用繁體中文進行面向使用者的回覆" in system_prompt + + +def test_context_builder_uses_english_main_agent_prompt_for_en() -> None: + system_prompt = ContextBuilder().build_system_prompt(ContextBuildInput(prompt_locale="en")) + + assert "You are Beaver, an AI assistant developed by Boway Information Systems Co., Ltd." in system_prompt + assert "Use English for user-facing replies" in system_prompt diff --git a/app-instance/backend/tests/unit/test_task_mode_feedback.py b/app-instance/backend/tests/unit/test_task_mode_feedback.py index d15432d..af2ff70 100644 --- a/app-instance/backend/tests/unit/test_task_mode_feedback.py +++ b/app-instance/backend/tests/unit/test_task_mode_feedback.py @@ -15,6 +15,7 @@ class StubProvider(LLMProvider): def __init__(self, responses: list[LLMResponse]) -> None: super().__init__() self._responses = list(responses) + self.seen_messages: list[list[dict]] = [] async def chat( self, @@ -26,6 +27,7 @@ class StubProvider(LLMProvider): ) -> LLMResponse: if not self._responses: raise AssertionError("No stubbed provider responses left") + self.seen_messages.append(messages) return self._responses.pop(0) def get_default_model(self) -> str: @@ -99,6 +101,52 @@ def test_task_run_records_evidence_and_waits_for_acceptance(tmp_path: Path) -> N assert "validated" not in event_types +def test_task_mode_injects_prompt_locale_output_language(tmp_path: Path) -> None: + service = AgentService( + loader=EngineLoader( + workspace=tmp_path, + task_execution_planner=StubTaskExecutionPlanner(), + ) + ) + main_provider = StubProvider( + [ + LLMResponse( + content="Done", + finish_reason="stop", + provider_name="stub", + model="stub-model", + ) + ] + ) + bundle = ProviderBundle( + main_runtime=SimpleNamespace(model="stub-model", provider_name="stub"), + main_provider=main_provider, + auxiliary_runtime=SimpleNamespace(model="stub-model", provider_name="stub"), + auxiliary_provider=StubProvider([_route_response("new_task", "Product summary")]), + ) + + result = asyncio.run( + service.process_direct( + "Summarize the uploaded report in English", + session_id="web:locale-task", + prompt_locale="en", + provider_bundle=bundle, + ) + ) + + assert result.task_id + assert main_provider.seen_messages + system_prompt = main_provider.seen_messages[-1][0]["content"] + assert "Use English for user-facing replies" in system_prompt + assert "Output language: English." in system_prompt + + task_service = service.create_loop().boot().task_service + assert task_service is not None + task = task_service.get_task(result.task_id) + assert task is not None + assert task.metadata["prompt_locale"] == "en" + + def test_unrelated_simple_chat_auto_accepts_active_task(tmp_path: Path) -> None: service = AgentService( loader=EngineLoader( diff --git a/app-instance/backend/tests/unit/test_websocket_chat.py b/app-instance/backend/tests/unit/test_websocket_chat.py index 4bab30b..dcf8bf1 100644 --- a/app-instance/backend/tests/unit/test_websocket_chat.py +++ b/app-instance/backend/tests/unit/test_websocket_chat.py @@ -73,6 +73,7 @@ def test_websocket_message_returns_chat_metadata_and_session_updated() -> None: { "type": "message", "content": "hello", + "prompt_locale": "zh-Hant", "metadata": {"source": "test"}, "attachments": [{"file_id": "file-1", "name": "a.txt"}], } @@ -89,6 +90,7 @@ def test_websocket_message_returns_chat_metadata_and_session_updated() -> None: "user_id": None, "title": None, "execution_context": None, + "prompt_locale": "zh-Hant", "model": None, "provider_name": None, "embedding_model": None, @@ -134,6 +136,7 @@ def test_websocket_message_uses_direct_processing_when_loop_is_not_running() -> "user_id": None, "title": None, "execution_context": None, + "prompt_locale": None, "model": None, "provider_name": None, "embedding_model": None, @@ -149,7 +152,10 @@ def test_rest_chat_uses_direct_processing_when_loop_is_not_running() -> None: app = create_app(service=service, manage_service_lifecycle=False) with TestClient(app) as client: - response = client.post("/api/chat", json={"session_id": "web:alpha", "message": "hello"}) + response = client.post( + "/api/chat", + json={"session_id": "web:alpha", "message": "hello", "prompt_locale": "en"}, + ) assert response.status_code == 200 assert service.calls == [ @@ -160,6 +166,7 @@ def test_rest_chat_uses_direct_processing_when_loop_is_not_running() -> None: "user_id": None, "title": None, "execution_context": None, + "prompt_locale": "en", "model": None, "provider_name": None, "embedding_model": None, diff --git a/app-instance/frontend/app/(app)/page.tsx b/app-instance/frontend/app/(app)/page.tsx index 9ed2e01..1dc67b7 100644 --- a/app-instance/frontend/app/(app)/page.tsx +++ b/app-instance/frontend/app/(app)/page.tsx @@ -23,6 +23,7 @@ import { getSession, getSessionProcess, listSessions, + promptLocaleForAppLocale, sendMessage, submitChatFeedback, uploadFile, @@ -44,7 +45,7 @@ function isSessionUpdatedEvent(data: WsEvent | Record): data is return data.type === 'session_updated' && typeof data.session_id === 'string'; } -function activeTaskStatusLabel(status: string, locale: 'zh-CN' | 'en-US') { +function activeTaskStatusLabel(status: string, locale: string) { if (status === 'needs_revision') return pickAppText(locale, '待修改', 'Needs revision'); if (status === 'awaiting_acceptance') return pickAppText(locale, '待验收', 'Awaiting acceptance'); if (status === 'running') return pickAppText(locale, '进行中', 'Running'); @@ -140,8 +141,9 @@ export default function ChatPage() { liveRuns: processRuns, liveEvents: processEvents, liveArtifacts: processArtifacts, + locale, }), - [activeTaskDetail, processArtifacts, processEvents, processRuns] + [activeTaskDetail, locale, processArtifacts, processEvents, processRuns] ); const loadSessions = useCallback(async () => { @@ -400,6 +402,7 @@ export default function ChatPage() { type: 'message', content: msgContent, thinking_enabled: thinkingModeEnabled, + prompt_locale: promptLocaleForAppLocale(locale), }; if (attachments.length > 0) { wsPayload.attachments = attachments; diff --git a/app-instance/frontend/app/(app)/tasks/[taskId]/page.tsx b/app-instance/frontend/app/(app)/tasks/[taskId]/page.tsx index 189b1de..b58c946 100644 --- a/app-instance/frontend/app/(app)/tasks/[taskId]/page.tsx +++ b/app-instance/frontend/app/(app)/tasks/[taskId]/page.tsx @@ -97,8 +97,9 @@ export default function TaskDetailPage() { liveRuns: processRuns, liveEvents: processEvents, liveArtifacts: processArtifacts, + locale, }), - [backendTask, processArtifacts, processEvents, processRuns] + [backendTask, locale, processArtifacts, processEvents, processRuns] ); const timelineCards = timelineView?.cards ?? []; diff --git a/app-instance/frontend/app/(app)/tasks/page.tsx b/app-instance/frontend/app/(app)/tasks/page.tsx index 1c1171d..81a6e9b 100644 --- a/app-instance/frontend/app/(app)/tasks/page.tsx +++ b/app-instance/frontend/app/(app)/tasks/page.tsx @@ -222,7 +222,7 @@ function OrdinaryTaskCard({ onDelete, }: { task: BackendTask; - locale: 'zh-CN' | 'en-US'; + locale: string; onDelete: () => void; }) { const title = task.short_title || String(task.metadata?.short_title || '') || task.description || task.goal || task.task_id; @@ -284,7 +284,7 @@ function OrdinaryTaskCard({ ); } -function taskStatusLabel(status: string, locale: 'zh-CN' | 'en-US') { +function taskStatusLabel(status: string, locale: string) { const labels: Record = { open: ['已创建', 'Open'], running: ['执行中', 'Running'], @@ -297,7 +297,7 @@ function taskStatusLabel(status: string, locale: 'zh-CN' | 'en-US') { return label ? pickAppText(locale, label[0], label[1]) : status; } -function taskSourceLabel(task: BackendTask, locale: 'zh-CN' | 'en-US') { +function taskSourceLabel(task: BackendTask, locale: string) { if (task.metadata?.source === 'scheduled_run') { return pickAppText(locale, '定时通知修改', 'Scheduled notification revision'); } @@ -520,7 +520,7 @@ function ScheduledJobCard({ onRemove, }: { job: CronJob; - locale: 'zh-CN' | 'en-US'; + locale: string; formatTime: (ms: number | null) => string; onToggle: (checked: boolean) => void; onRun: () => void; diff --git a/app-instance/frontend/components/Header.tsx b/app-instance/frontend/components/Header.tsx index 51b28e9..b6d3498 100644 --- a/app-instance/frontend/components/Header.tsx +++ b/app-instance/frontend/components/Header.tsx @@ -155,7 +155,7 @@ const Header = () => {
-