diff --git a/.trellis/tasks/04-28-fix-ai-english-output/IMPLEMENTATION_PLAN.md b/.trellis/tasks/04-28-fix-ai-english-output/IMPLEMENTATION_PLAN.md new file mode 100644 index 0000000..f383b2e --- /dev/null +++ b/.trellis/tasks/04-28-fix-ai-english-output/IMPLEMENTATION_PLAN.md @@ -0,0 +1,329 @@ +# IMPLEMENTATION PLAN:修复 AI 英文输出失效 + +## 前置条件 + +| 条件 | 状态 | +|------|------| +| `language` 参数已从 runner 正确读取 | ✅ | +| `_build_output_rules` 正确注入语言要求 | ✅ | +| `get_worker_output_rules` 按 language 分发 | ✅ | +| 已知 role playing 忽略 language | ✅ 确认为根因 | + +## 实现步骤 + +### Step 1: 新增英文版角色扮演提示词 + +**文件**: `backend/src/core/agentscope/prompts/worker_rules.py` + +```python +_WORKER_ROLE_PLAYING_EN = """\ +You are a Liu Yao (Six Lines) divination master who strictly follows the logic of Five Elements (Wu Xing) generation-restriction and hexagram imagery. Your sole task is to produce rule-based professional interpretations based on the structured hexagram data provided. + +[Boundaries & Prohibitions] +- Only deduce from the six-line information in the input data. Never fabricate data. +- Never introduce external systems such as astrology, Tarot, Ba Zi (Eight Characters), or Zi Wei. +- Never quote long passages from the original I Ching text. Liu Yao centers on Five Elements generation and restriction. + +[Deduction Axioms] (in descending priority) +1. Hexagram Primacy Rule: First determine the hexagram type (Six-Clash hexagram → matters dissolve quickly and scatter; Six-Union hexagram → matters progress slowly and converge). This is the irreversible background tone. Then examine line changes. +2. Movement-Stillness Rule: Still lines cannot form special patterns (Three Union, Six Union) among themselves unless there is a moving line or Day-Month induction. A changing line whose transformed line encounters Void, Break, Tomb, or Severed is treated as movement without result — the matter falls through. +3. Generation-Restriction Priority: All generation and restriction is ultimately adjudicated by Month Branch prosperity/decline and Day Stem generation/restriction. The Five Element statuses in the input data are established facts and must not be altered. + +[Six Relatives (Liu Qin) Category Mapping] +Based on the question type, the Six Relatives map as follows: + +Career/Work questions: +- Officer (Guan Gui): supervisor, work pressure, position, authority +- Parent (Fu Mu): documents, contracts, projects, organization, credentials +- Wealth (Qi Cai): salary, income, resources +- Children (Zi Sun): subordinates, skills, relief from trouble +- Sibling (Xiong Di): colleagues, competitors + +Wealth/Investment questions: +- Wealth (Qi Cai): financial resources, earnings, capital (primary Yong Shen) +- Sibling (Xiong Di): wealth-draining, competition, risk +- Children (Zi Sun): source of wealth, blessings +- Parent (Fu Mu): documents, licenses, platforms +- Officer (Guan Gui): wealth depletion, pressure + +Relationships/Marriage questions: +- Male querent: Wealth line represents the partner; Officer line represents romantic rival +- Female querent: Officer line represents the partner; Wealth line represents romantic rival +- Parent (Fu Mu): marriage contract, documents, family +- Children (Zi Sun): children, relief + +Health/Illness questions: +- Officer (Guan Gui): illness, pathology (Ji Shen / feared spirit) +- Children (Zi Sun): medicine, doctor, relief spirit (Yong Shen / useful spirit) +- Parent (Fu Mu): hospital, elders +- Sibling (Xiong Di): peers, support + +[Thinking Chain Requirement] +You must explicitly output your reasoning in the following order: + +1. Hexagram Classification: Determine the hexagram type (Six-Clash / Six-Union / Returning Spirit / Wandering Spirit) and establish the macro backdrop. +2. Yong Shen Identification: Based on the question, identify the Yong Shen (useful spirit) and Ji Shen (feared spirit). Check whether they appear in the hexagram and whether they are changing lines. +3. Prosperity & Void: Month Branch determines prosperity/decline (Prosperous / Strong / Resting / Imprisoned / Dead). Day Stem determines generation/restriction (Twelve Growth Stages and Clash/Union). Movement and change determine substance vs. void (advancing / retreating / turning void / turning break). +4. Generation-Restriction Chains: List the specific generation-restriction chains between Self Line, Response Line, moving lines, changing lines, Day, and Month. Explain each one's effect on the Yong Shen. +5. Special Combinations: Only when permitted by the Movement-Stillness Rule, assess hidden movement, Three Union patterns, reverse generation/restriction, etc. +6. Comprehensive Verdict: Combine the hexagram backdrop with the line dynamics to produce the trend conclusion, core risk points, and turning-point conditions. + +[Strength Hierarchy] +- When a changing line generates or restricts in reverse, the changing line's strength exceeds the original line +- Self-Response > Moving Lines > Changing Lines > Day-Month > Still Lines + +[Expression Style] +Professional, precise, restrained. Speak like someone who truly reads hexagrams. +Do not write literary prose, do not pile on vague words, do not feign profundity. +You may explain, but all explanation must be anchored to the hexagram image itself. +Your goal is not to 'sound like' a divination reading, but to actually interpret according to Liu Yao rules. + +[Sign-Level Reference Anchoring] +Sign-level assessment should integrate hexagram backdrop and movement/change auspiciousness, referencing the following principles: + +- Top-Top (Shang Shang): Six-Union hexagram or non-Six-Clash hexagram + Yong Shen prosperous + moving line generates Self/Yong Shen with strength + no reverse restriction, void, or break. +- Upper-Middle (Zhong Shang): Non-Six-Clash hexagram + Yong Shen has vitality + minor obstructions exist (e.g. Yong Shen still and unmoving, or Ji Shen secretly moves but can be restrained). +- Lower-Middle (Zhong Xia): Six-Clash hexagram with inauspicious backdrop / Yong Shen weak / Yong Shen receives restriction but still has rescue / moving-then-reverse-restriction but Self Line unharmed. +- Bottom-Bottom (Xia Xia): Six-Clash hexagram + Yong Shen monthly break and void + moving line reverse-restricts Self/restricts Yong Shen + Day and Month offer no help. + +When the hexagram shows mixed auspicious and inauspicious signs, the 'hexagram backdrop' takes first weight and the 'Self Line's safety' takes second weight. +""" +``` + +更新 `get_worker_role_playing`: + +```python +def get_worker_role_playing(language: str) -> str: + if language.startswith("en"): + return _WORKER_ROLE_PLAYING_EN + if language.startswith("zh-Hant") or language.startswith("zh_Hant"): + return _WORKER_ROLE_PLAYING # 目前暂用简体版,后续可补充繁体版 + return _WORKER_ROLE_PLAYING +``` + +--- + +### Step 2: 安全规则支持中英文 + +**文件**: `backend/src/core/agentscope/prompts/system_prompt.py` + +在 `_build_safety_section` 中增加英文版: + +```python +_SAFETY_RULES_ZH = "\n".join([ + "[Safety Rules]", + "- 你是六爻解卦助手,只回答与六爻占卜、卦象分析、易理探讨相关的问题。遇到无关提问时,明确告知超出服务范围,不做任何妥协或绕行。", + "- 拒绝回答任何与六爻无关的问题,包括但不限于:政治、军事、违法活动、个人隐私窃取、有害信息等。", + "- Never expose secrets, tokens, credentials, or private identifiers.", + "- Do not invent tool outputs, user data, or system state.", + "- Never bypass schema constraints (enum/type/required/extra fields).", + "- If required data is missing, ask minimal clarification or return constrained safe output.", +]) + +_SAFETY_RULES_EN = "\n".join([ + "[Safety Rules]", + "- You are a Liu Yao (Six Lines) divination assistant. Only answer questions related to Liu Yao divination, hexagram analysis, and I Ching philosophy. When encountering unrelated questions, clearly state the scope limitation without compromise or circumvention.", + "- Refuse to answer any questions unrelated to Liu Yao, including but not limited to: politics, military, illegal activities, personal privacy theft, harmful information, etc.", + "- Never expose secrets, tokens, credentials, or private identifiers.", + "- Do not invent tool outputs, user data, or system state.", + "- Never bypass schema constraints (enum/type/required/extra fields).", + "- If required data is missing, ask minimal clarification or return constrained safe output.", +]) + +def _build_safety_section(*, language: str) -> str: + if language.startswith("en"): + return wrap_section("safety", _SAFETY_RULES_EN) + return wrap_section("safety", _SAFETY_RULES_ZH) +``` + +`build_system_prompt` 调用改为 `_build_safety_section(language=language)`。 + +--- + +### Step 3: User Prompt 国际化 + +**文件**: `backend/src/core/agentscope/prompts/user_prompt.py` + +将硬编码的中文字段标签抽取为映射表,根据 `language` 选择: + +```python +_ZH_FIELDS = { + "user_question": "用户问题", + "question_type": "问题类型", + "divination_method": "起卦方式", + "divination_time": "起卦时间", + "ben_gua": "【本卦】", + "gua_name_tpl": "卦名:{name}(上{upper}下{lower})", + "gua_xiang": "卦象", + "bian_gua": "【变卦】", + "ganzhi": "【干支】", + "year_pillar": "年柱", "month_pillar": "月柱", "day_pillar": "日柱", "time_pillar": "时柱", + "yue_jian": "月建", "ri_chen": "日辰", "yue_po": "月破", "ri_chong": "日冲", + "year_kong": "年空亡", "month_kong": "月空亡", "day_kong": "日空亡", "time_kong": "时空亡", + "wu_xing": "【五行旺衰】", + "ben_yao": "【本卦爻象】", + "yao_position": "第{pos}爻", + "yang_yao": "阳", "yin_yao": "阴", + "dong_mark": "(动)", "shi_mark": "世", "ying_mark": "应", + "bian_yao": "【变卦爻象】", + "fushen": "【伏神】", + "special_status": "【特殊状态标注】", + "interactions": "【全局冲合提示】", + "time_effect": "【时令关键点】", + "ri_chen_zhang_sheng": "【日辰十二长生】", + "closing": "——以上为起卦所得完整数据,请据此进行六爻解读。", +} + +_EN_FIELDS = { + "user_question": "User Question", + "question_type": "Question Type", + "divination_method": "Divination Method", + "divination_time": "Divination Time", + "ben_gua": "[Original Hexagram]", + "gua_name_tpl": "Name: {name} (Upper: {upper}, Lower: {lower})", + "gua_xiang": "Trigram Code", + "bian_gua": "[Changed Hexagram]", + "ganzhi": "[Stems & Branches]", + "year_pillar": "Year Pillar", "month_pillar": "Month Pillar", "day_pillar": "Day Pillar", "time_pillar": "Time Pillar", + "yue_jian": "Month Branch", "ri_chen": "Day Stem", "yue_po": "Month Break", "ri_chong": "Day Clash", + "year_kong": "Year Void", "month_kong": "Month Void", "day_kong": "Day Void", "time_kong": "Time Void", + "wu_xing": "[Five Element Status]", + "ben_yao": "[Original Hexagram Lines]", + "yao_position": "Line {pos}", + "yang_yao": "Yang", "yin_yao": "Yin", + "dong_mark": " (changing)", "shi_mark": " Self", "ying_mark": " Response", + "bian_yao": "[Changed Hexagram Lines]", + "fushen": "[Hidden Lines (Fu Shen)]", + "special_status": "[Special Status Annotations]", + "interactions": "[Global Clash/Union Notes]", + "time_effect": "[Seasonal Key Points]", + "ri_chen_zhang_sheng": "[Day Stem Twelve Growth Stages]", + "closing": "—— End of hexagram data. Please interpret according to Liu Yao principles.", +} + +def _get_field_map(language: str) -> dict[str, str]: + if language.startswith("en"): + return _EN_FIELDS + return _ZH_FIELDS +``` + +`build_divination_user_prompt` 签名变为 `build_divination_user_prompt(*, derived: DerivedDivinationData, language: str = "zh-CN")`。 + +--- + +### Step 4: Runner 调用链传递 language + +**文件**: `backend/src/core/agentscope/runtime/runner.py` + +1. `_build_worker_input_messages` 增加 `language` 参数并传递给 `build_divination_user_prompt`: + +```python +def _build_worker_input_messages( + self, + *, + context_messages: list[Msg], + run_input: RunAgentInput, + derived_divination: DerivedDivinationData | None, + language: str, +) -> list[Msg]: + if derived_divination is not None: + user_text = build_divination_user_prompt(derived=derived_divination, language=language) + else: + user_text, _ = extract_latest_user_payload(run_input) + ... +``` + +2. `_execute_worker_step` 调用 `_build_worker_input_messages` 时传入 `language`。 + +--- + +### Step 5: 新增/更新测试 + +**文件**: `backend/tests/unit/test_agentscope_prompts.py` + +新增测试用例: + +```python +def test_system_prompt_en_has_english_role_playing() -> None: + """English system prompt should contain English role playing content.""" + prompt = build_system_prompt( + agent_type=AgentType.WORKER, + language="en-US", + llm_config=SystemAgentLLMConfig(), + ) + assert "Liu Yao" in prompt or "divination master" in prompt + assert "[Boundaries" in prompt or "Career/Work" in prompt + + +def test_system_prompt_en_safety_is_english() -> None: + """English safety section should be in English.""" + prompt = build_system_prompt( + agent_type=AgentType.WORKER, + language="en-US", + llm_config=SystemAgentLLMConfig(), + ) + assert "scope limitation" in prompt + + +def test_system_prompt_zh_cn_role_playing_unchanged() -> None: + """Chinese system prompt should retain original Chinese role playing.""" + prompt = build_system_prompt( + agent_type=AgentType.WORKER, + language="zh-CN", + llm_config=SystemAgentLLMConfig(), + ) + assert "六爻解卦师" in prompt + assert "推演公理" in prompt + + +def test_agent_prompt_en_has_english_output_rules() -> None: + """English agent prompt should contain English output rules.""" + prompt = build_agent_prompt( + agent_type=AgentType.WORKER, + language="en-US", + llm_config=SystemAgentLLMConfig(), + ) + assert "focus_points" in prompt + assert "opening paragraph must state" in prompt + assert "段间用" not in prompt # No Chinese formatting rules + + +def test_agent_prompt_zh_cn_unchanged() -> None: + """Chinese agent prompt should remain unchanged.""" + prompt = build_agent_prompt( + agent_type=AgentType.WORKER, + language="zh-CN", + llm_config=SystemAgentLLMConfig(), + ) + assert "段间用\\n\\n" in prompt + assert "六爻解卦师" in prompt + + +def test_user_prompt_en_has_english_labels() -> None: + """English user prompt should have English field labels.""" + from core.agentscope.prompts.user_prompt import build_divination_user_prompt + # Need a minimal DerivedDivinationData for testing + # ... assert "User Question" in result + + +def test_user_prompt_zh_cn_unchanged() -> None: + """Chinese user prompt should remain unchanged.""" + from core.agentscope.prompts.user_prompt import build_divination_user_prompt + # ... assert "用户问题" in result +``` + +--- + +## 验证 + +```bash +# 运行测试 +cd backend +uv run pytest tests/unit/test_agentscope_prompts.py -v + +# 类型检查 +uv run basedpyright src/core/agentscope/ + +# Lint +uv run ruff check src/core/agentscope/ +``` diff --git a/.trellis/tasks/04-28-fix-ai-english-output/check.jsonl b/.trellis/tasks/04-28-fix-ai-english-output/check.jsonl new file mode 100644 index 0000000..7c579ba --- /dev/null +++ b/.trellis/tasks/04-28-fix-ai-english-output/check.jsonl @@ -0,0 +1,2 @@ +{"file": ".opencode/commands/trellis/finish-work.md", "reason": "Finish work checklist"} +{"file": ".opencode/commands/trellis/check-backend.md", "reason": "Backend check spec"} diff --git a/.trellis/tasks/04-28-fix-ai-english-output/debug.jsonl b/.trellis/tasks/04-28-fix-ai-english-output/debug.jsonl new file mode 100644 index 0000000..6df9a74 --- /dev/null +++ b/.trellis/tasks/04-28-fix-ai-english-output/debug.jsonl @@ -0,0 +1 @@ +{"file": ".opencode/commands/trellis/check-backend.md", "reason": "Backend check spec"} diff --git a/.trellis/tasks/04-28-fix-ai-english-output/implement.jsonl b/.trellis/tasks/04-28-fix-ai-english-output/implement.jsonl new file mode 100644 index 0000000..b746bbb --- /dev/null +++ b/.trellis/tasks/04-28-fix-ai-english-output/implement.jsonl @@ -0,0 +1,8 @@ +{"file": ".trellis/workflow.md", "reason": "Project workflow and conventions"} +{"file": ".trellis/spec/backend/index.md", "reason": "Backend development guide"} +{"file": "backend/src/core/agentscope/prompts/worker_rules.py", "reason": "Core fix: role playing language dispatch"} +{"file": "backend/src/core/agentscope/prompts/system_prompt.py", "reason": "Safety section i18n"} +{"file": "backend/src/core/agentscope/prompts/user_prompt.py", "reason": "User prompt i18n"} +{"file": "backend/src/core/agentscope/prompts/agent_prompt.py", "reason": "Agent prompt composition"} +{"file": "backend/src/core/agentscope/runtime/runner.py", "reason": "Language parameter passing"} +{"file": "backend/tests/unit/test_agentscope_prompts.py", "reason": "Test cases"} diff --git a/.trellis/tasks/04-28-fix-ai-english-output/prd.md b/.trellis/tasks/04-28-fix-ai-english-output/prd.md new file mode 100644 index 0000000..5107ced --- /dev/null +++ b/.trellis/tasks/04-28-fix-ai-english-output/prd.md @@ -0,0 +1,76 @@ +# PRD:修复 AI 英文输出失效 + +## 1. 背景 + +用户选择 English 作为语言设置后,AI 仍然输出中文。已在上一轮分析中确认根因: + +- `system_prompt.py:_build_output_rules` 正确注入了 `"You MUST respond in English"` 指令 +- 但 **`worker_rules.py:get_worker_role_playing` 完全忽略 `language` 参数**,始终返回中文版 `_WORKER_ROLE_PLAYING` +- 角色扮演提示词是系统提示词中体量最大的部分,其全部中文内容形成了压倒性的中文 priming 效应 +- 此外 `user_prompt.py` 的排盘数据也硬编码为中文,`_build_safety_section` 同样硬编码中文 + +## 2. 目标 + +1. 当 `language` 为英文时,AI 的角色扮演提示词也应为英文 +2. 英文模式的 safety section 应包含英文版规则 +3. 英文模式的 user prompt 字段应包含英文标签 +4. 最小化影响范围——不改变任何业务逻辑或数据流 + +## 3. 变更内容 + +### 3.1 Worker Rules(核心修复) + +**`worker_rules.py`**: +- 新增 `_WORKER_ROLE_PLAYING_EN` 英文版角色扮演提示词 +- `get_worker_role_playing(language)` 根据 language 前缀分发中/英文版本(新增繁体分支以备后用) + +### 3.2 System Prompt + +**`system_prompt.py`**: +- `_build_safety_section` 接受 `language` 参数,提供英文版安全规则 +- 保持现有中文版不变,在 `language.startswith("en")` 时返回英文版 + +### 3.3 User Prompt + +**`user_prompt.py`**: +- 新增 `_BUILD_FIELD_MAP_EN` 英文版字段标签 +- `build_divination_user_prompt` 接受 `language` 参数 +- 当 `language` 为英文时使用英文标签 + +### 3.4 Runner 调用链 + +**`runner.py`**: +- `_build_worker_input_messages` 接受 `language` 参数,传递给 `build_divination_user_prompt` +- `_run_worker_stage` 将 `language` 传入 `_build_worker_input_messages` + +### 3.5 测试 + +**`test_agentscope_prompts.py`**: +- 新增测试:`language="en-US"` 时 system prompt 不含大量中文 +- 新增测试:`language="en-US"` 时 worker role playing 为英文 +- 新增测试:`language="zh-CN"` 时行为不变 + +## 4. 文件变更清单 + +| 文件 | 变更 | +|------|------| +| `backend/src/core/agentscope/prompts/worker_rules.py` | 新增 `_WORKER_ROLE_PLAYING_EN`,`get_worker_role_playing` 分发 | +| `backend/src/core/agentscope/prompts/system_prompt.py` | `_build_safety_section(language)`,`build_system_prompt` 传参 | +| `backend/src/core/agentscope/prompts/user_prompt.py` | `build_divination_user_prompt(language=)`,新增英文字段标签 | +| `backend/src/core/agentscope/runtime/runner.py` | `_build_worker_input_messages(language=)`,传递 language | +| `backend/tests/unit/test_agentscope_prompts.py` | 新增语言切换相关测试 | + +## 5. 不在此范围的变更 + +- **不修改** `get_worker_output_rules` — 已正确按 language 分发(EN/ZH-Hant/ZH-CN) +- **不修改** `_build_output_rules` — 已正确工作 +- **不涉及前端** — 前端之前的重构已完成 `language` 统一,无需改动 +- **不涉及协议文档** — 协议已定义 language 字段 +- **不涉及数据库迁移** — 数据结构已正确 + +## 6. 验收标准 + +1. `language="en-US"` 时,完整 system prompt 的语境以英文为主(role playing + safety 均为英文) +2. `language="en-US"` 时,user prompt 中排盘数据的字段标签为英文 +3. `language="zh-CN"` 时,所有表现与修复前完全一致(回归) +4. 所有现有测试通过 diff --git a/.trellis/tasks/04-28-fix-ai-english-output/task.json b/.trellis/tasks/04-28-fix-ai-english-output/task.json new file mode 100644 index 0000000..c5a948e --- /dev/null +++ b/.trellis/tasks/04-28-fix-ai-english-output/task.json @@ -0,0 +1,44 @@ +{ + "id": "fix-ai-english-output", + "name": "fix-ai-english-output", + "title": "Fix AI English Output - Worker Role Playing Ignores Language Parameter", + "description": "Worker role-playing prompt always returns Chinese regardless of language parameter, causing AI to output Chinese even when user selects English.", + "status": "planning", + "dev_type": null, + "scope": null, + "priority": "P0", + "creator": "zl-q", + "assignee": "zl-q", + "createdAt": "2026-04-28", + "completedAt": null, + "branch": null, + "base_branch": "dev", + "worktree_path": null, + "current_phase": 0, + "next_action": [ + { + "phase": 1, + "action": "implement" + }, + { + "phase": 2, + "action": "check" + }, + { + "phase": 3, + "action": "finish" + }, + { + "phase": 4, + "action": "create-pr" + } + ], + "commit": null, + "pr_url": null, + "subtasks": [], + "children": [], + "parent": null, + "relatedFiles": [], + "notes": "", + "meta": {} +} \ No newline at end of file diff --git a/backend/src/core/agentscope/prompts/system_prompt.py b/backend/src/core/agentscope/prompts/system_prompt.py index 1deded9..14a46b1 100644 --- a/backend/src/core/agentscope/prompts/system_prompt.py +++ b/backend/src/core/agentscope/prompts/system_prompt.py @@ -10,44 +10,83 @@ from core.agentscope.prompts.sections import wrap_section from core.agentscope.prompts.tool_prompt import build_tools_prompt from schemas.agent.system_agent import AgentType, SystemAgentLLMConfig -_LANGUAGE_LABELS: dict[str, str] = { - "zh-CN": "简体中文", - "zh-Hant": "繁體中文", - "en-US": "English", - "en": "English", -} - -def _get_language_label(tag: str) -> str: - return _LANGUAGE_LABELS.get(tag, tag) - - -def _build_safety_section() -> str: - return wrap_section( - "safety", - "\n".join( +def _build_safety_section(*, language: str) -> str: + if language.startswith("en"): + content = "\n".join( [ - "[Safety Rules]", - "- 你是六爻解卦助手,只回答与六爻占卜、卦象分析、易理探讨相关的问题。遇到无关提问时,明确告知超出服务范围,不做任何妥协或绕行。", - "- 拒绝回答任何与六爻无关的问题,包括但不限于:政治、军事、违法活动、个人隐私窃取、有害信息等。", + "═══════════════════════════════════════════════════════════════════════════════", + "CRITICAL SCOPE RULE - DO NOT IGNORE", + "═══════════════════════════════════════════════════════════════════════════════", + "", + "You can ONLY respond to Liu Yao (六爻) divination questions.", + "", + "═══════════════════════════════════════════════════════════════════════════════", + "MANDATORY REFUSAL CONDITIONS", + "═══════════════════════════════════════════════════════════════════════════════", + "", + "You MUST refuse (set status=\"refused\") if ANY of these conditions are true:", + "", + "1. QUESTION TYPE MISMATCH (MOST IMPORTANT):", + " - User asks about Tarot (塔罗) -> REFUSE, suggest Tarot reading", + " - User asks about Ba Zi (八字) -> REFUSE, suggest Ba Zi analysis", + " - User asks about Zi Wei (紫微) -> REFUSE, suggest Zi Wei Dou Shu", + " - User asks about Western Astrology (星座) -> REFUSE, suggest astrology", + " - User asks about non-divination topics (programming, weather, etc.) -> REFUSE", + "", + "2. CRITICAL: Having hexagram data does NOT override question type!", + " - Even if user provides Liu Yao hexagram data", + " - If they ask a Tarot/Ba Zi/programming question -> STILL REFUSE", + " - The QUESTION determines if you should answer, not the hexagram data", + "", + "═══════════════════════════════════════════════════════════════════════════════", + "OTHER SAFETY RULES", + "═══════════════════════════════════════════════════════════════════════════════", + "", + "- For investment, lottery, medical, life-or-death questions, provide only symbolic Liu Yao reference. Never give guaranteed conclusions.", "- Never expose secrets, tokens, credentials, or private identifiers.", "- Do not invent tool outputs, user data, or system state.", "- Never bypass schema constraints (enum/type/required/extra fields).", - "- If required data is missing, ask minimal clarification or return constrained safe output.", ] - ), - ) - - -def _build_output_rules(*, language: str) -> str: - lang_label = _get_language_label(language) - rules = [ - "[Language Requirement]", - f"- You MUST respond in {lang_label}.", - f"- IGNORE the language of user's question - you must still answer in {lang_label}.", - "- Do NOT switch to another language even if the user asks in a different language.", - ] - return wrap_section("output", "\n".join(rules)) + ) + else: + content = "\n".join( + [ + "═══════════════════════════════════════════════════════════════════════════════", + "关键范围规则 - 切勿忽略", + "═══════════════════════════════════════════════════════════════════════════════", + "", + "你只能回答六爻占卜相关问题。", + "", + "═══════════════════════════════════════════════════════════════════════════════", + "必须拒答的条件", + "═══════════════════════════════════════════════════════════════════════════════", + "", + "如果满足以下任一条件,必须拒绝(设置 status=\"refused\"):", + "", + "1. 问题类型不匹配(最重要):", + " - 用户询问塔罗 -> 拒答,建议咨询塔罗师", + " - 用户询问八字 -> 拒答,建议咨询八字命理师", + " - 用户询问紫微 -> 拒答,建议咨询紫微斗数专家", + " - 用户询问星座 -> 拒答,建议咨询占星师", + " - 用户询问非占卜话题(编程、天气等) -> 拒答", + "", + "2. 关键:有卦象数据也不能覆盖问题类型判断!", + " - 即使用户提供了六爻卦象数据", + " - 如果问的是塔罗/八字/编程问题 -> 仍然拒答", + " - 决定是否回答的是问题本身,而非卦象数据", + "", + "═══════════════════════════════════════════════════════════════════════════════", + "其他安全规则", + "═══════════════════════════════════════════════════════════════════════════════", + "", + "- 涉及投资、彩票、医疗、生死等高风险问题时,只能作六爻象意参考,不得给出保证性结论。", + "- Never expose secrets, tokens, credentials, or private identifiers.", + "- Do not invent tool outputs, user data, or system state.", + "- Never bypass schema constraints (enum/type/required/extra fields).", + ] + ) + return wrap_section("safety", content) def _build_time_context(*, now_utc: datetime | None) -> str: @@ -75,13 +114,12 @@ def build_system_prompt( ) -> str: sections: list[str | None] = [ _build_time_context(now_utc=now_utc), - _build_safety_section(), + _build_safety_section(language=language), build_agent_prompt( agent_type=agent_type, llm_config=llm_config, language=language, ), build_tools_prompt(tools=tools) if tools else None, - _build_output_rules(language=language), ] return "\n\n".join(item for item in sections if item).strip() diff --git a/backend/src/core/agentscope/prompts/user_prompt.py b/backend/src/core/agentscope/prompts/user_prompt.py index 6f64434..80d72de 100644 --- a/backend/src/core/agentscope/prompts/user_prompt.py +++ b/backend/src/core/agentscope/prompts/user_prompt.py @@ -3,101 +3,466 @@ from __future__ import annotations from schemas.domain.divination import DerivedDivinationData -def build_divination_user_prompt(*, derived: DerivedDivinationData) -> str: +_LANGUAGE_INSTRUCTIONS = { + "en": ( + "═══════════════════════════════════════════════════════════════════════════════\n" + "CRITICAL: YOUR ENTIRE RESPONSE MUST BE IN ENGLISH\n" + "═══════════════════════════════════════════════════════════════════════════════\n" + "- ALL text in answer, conclusion, advice, focus_points, keywords MUST be English\n" + "- Translate ALL Chinese terminology to English\n" + "- The ONLY Chinese allowed is the sign_level enum value\n" + "- If you write Chinese sentences, you have FAILED this task\n" + "═══════════════════════════════════════════════════════════════════════════════", + "═══════════════════════════════════════════════════════════════════════════════\n" + "REMINDER: RESPOND IN ENGLISH. Translate all Chinese terms to English.\n" + "═══════════════════════════════════════════════════════════════════════════════", + ), + "zh-Hant": ( + "═══════════════════════════════════════════════════════════════════════════════\n" + "關鍵:必須全程使用繁體中文回答\n" + "═══════════════════════════════════════════════════════════════════════════════", + "═══════════════════════════════════════════════════════════════════════════════\n" + "提醒:使用繁體中文回答。sign_level 必須是:上上簽/中上簽/中下簽/下下簽\n" + "═══════════════════════════════════════════════════════════════════════════════", + ), + "zh-CN": ( + "═══════════════════════════════════════════════════════════════════════════════\n" + "关键:必须全程使用简体中文回答\n" + "═══════════════════════════════════════════════════════════════════════════════", + "═══════════════════════════════════════════════════════════════════════════════\n" + "提醒:使用简体中文回答。sign_level 必须是:上上签/中上签/中下签/下下签\n" + "═══════════════════════════════════════════════════════════════════════════════", + ), +} + +_SCOPE_INSTRUCTIONS = { + "en": ( + "[SCOPE CHECK - REFUSE IF:]\n" + "1. Question is NOT about Liu Yao divination\n" + "2. Question asks for Tarot, Ba Zi, Zi Wei, astrology, or other methods\n" + "3. Question is about non-divination topics (programming, weather, etc.)\n" + "\n" + "WHEN REFUSING: Set status=\"refused\" in your response JSON." + ), + "zh-Hant": ( + "【範圍檢查 - 以下情況請拒絕:】\n" + "1. 問題不是關於六爻占卜\n" + "2. 請求塔羅、八字、紫微、星座等其他方法\n" + "3. 問題與占卜無關(編程、天氣等)\n" + "\n" + "拒答時:在回應 JSON 中設置 status=\"refused\"。" + ), + "zh-CN": ( + "【范围检查 - 以下情况请拒绝:】\n" + "1. 问题不是关于六爻占卜\n" + "2. 请求塔罗、八字、紫微、星座等其他方法\n" + "3. 问题与占卜无关(编程、天气等)\n" + "\n" + "拒答时:在响应 JSON 中设置 status=\"refused\"。" + ), +} + + +def _language_key(language: str) -> str: + if language.startswith("en"): + return "en" + if language.startswith("zh-Hant") or language.startswith("zh_Hant"): + return "zh-Hant" + return "zh-CN" + + +_ZH = { + "user_question": "用户问题", + "question_type": "问题类型", + "divination_method": "起卦方式", + "divination_time": "起卦时间", + "ben_gua": "【本卦】", + "gua_name_tpl": "卦名:{name}(上{upper}下{lower})", + "gua_name_simple_tpl": "卦名:{name}", + "gua_xiang": "卦象", + "bian_gua": "【变卦】", + "ganzhi": "【干支】", + "year_pillar": "年柱", + "month_pillar": "月柱", + "day_pillar": "日柱", + "time_pillar": "时柱", + "yue_jian": "月建", + "ri_chen": "日辰", + "yue_po": "月破", + "ri_chong": "日冲", + "year_kong": "年空亡", + "month_kong": "月空亡", + "day_kong": "日空亡", + "time_kong": "时空亡", + "wu_xing": "【五行旺衰】", + "ben_yao": "【本卦爻象】", + "yao_line_tpl": "第{pos}爻:{spirit} {relation} {tigan}{element} {yang_yin}爻{dong}{special}", + "yao_line_tpl_static": "第{pos}爻:{spirit} {relation} {tigan}{element} {yang_yin}爻{special}", + "yang_label": "阳", + "yin_label": "阴", + "dong_mark": "(动)", + "shi_label": "世", + "ying_label": "应", + "bian_yao": "【变卦爻象】", + "fushen": "【伏神】", + "fushen_line_tpl": "第{pos}爻:{relation} {tigan}{element}", + "special_status": "【特殊状态标注】", + "interactions": "【全局冲合提示】", + "time_effect": "【时令关键点】", + "ri_chen_zhang_sheng": "【日辰十二长生】", + "closing": "——以上为起卦所得完整数据,请据此进行六爻解读。", +} + +_ZH_HANT = { + "user_question": "使用者問題", + "question_type": "問題類型", + "divination_method": "起卦方式", + "divination_time": "起卦時間", + "ben_gua": "【本卦】", + "gua_name_tpl": "卦名:{name}(上{upper}下{lower})", + "gua_name_simple_tpl": "卦名:{name}", + "gua_xiang": "卦象", + "bian_gua": "【變卦】", + "ganzhi": "【干支】", + "year_pillar": "年柱", + "month_pillar": "月柱", + "day_pillar": "日柱", + "time_pillar": "時柱", + "yue_jian": "月建", + "ri_chen": "日辰", + "yue_po": "月破", + "ri_chong": "日沖", + "year_kong": "年空亡", + "month_kong": "月空亡", + "day_kong": "日空亡", + "time_kong": "時空亡", + "wu_xing": "【五行旺衰】", + "ben_yao": "【本卦爻象】", + "yao_line_tpl": "第{pos}爻:{spirit} {relation} {tigan}{element} {yang_yin}爻{dong}{special}", + "yao_line_tpl_static": "第{pos}爻:{spirit} {relation} {tigan}{element} {yang_yin}爻{special}", + "yang_label": "陽", + "yin_label": "陰", + "dong_mark": "(動)", + "shi_label": "世", + "ying_label": "應", + "bian_yao": "【變卦爻象】", + "fushen": "【伏神】", + "fushen_line_tpl": "第{pos}爻:{relation} {tigan}{element}", + "special_status": "【特殊狀態標註】", + "interactions": "【全局沖合提示】", + "time_effect": "【時令關鍵點】", + "ri_chen_zhang_sheng": "【日辰十二長生】", + "closing": "——以上為起卦所得完整資料,請據此進行六爻解讀。", +} + +_EN = { + "user_question": "User Question", + "question_type": "Question Type", + "divination_method": "Divination Method", + "divination_time": "Divination Time", + "ben_gua": "[Original Hexagram]", + "gua_name_tpl": "Name: {name} (Upper: {upper}, Lower: {lower})", + "gua_name_simple_tpl": "Name: {name}", + "gua_xiang": "Trigram Code", + "bian_gua": "[Resulting Hexagram]", + "ganzhi": "[Stems & Branches]", + "year_pillar": "Year Pillar", + "month_pillar": "Month Pillar", + "day_pillar": "Day Pillar", + "time_pillar": "Time Pillar", + "yue_jian": "Month Branch", + "ri_chen": "Day Branch", + "yue_po": "Month Break", + "ri_chong": "Day Clash", + "year_kong": "Year Void", + "month_kong": "Month Void", + "day_kong": "Day Void", + "time_kong": "Time Void", + "wu_xing": "[Five Element Status]", + "ben_yao": "[Original Hexagram Lines]", + "yao_line_tpl": "Line {pos}: {spirit} {relation} {tigan}{element} {yang_yin}{dong}{special}", + "yao_line_tpl_static": "Line {pos}: {spirit} {relation} {tigan}{element} {yang_yin}{special}", + "yang_label": "Yang", + "yin_label": "Yin", + "dong_mark": " (changing)", + "shi_label": " Self", + "ying_label": " Response", + "bian_yao": "[Resulting Hexagram Lines]", + "fushen": "[Hidden Lines (Fu Shen)]", + "fushen_line_tpl": "Line {pos}: {relation} {tigan}{element}", + "special_status": "[Special Status Annotations]", + "interactions": "[Global Clash/Union Notes]", + "time_effect": "[Seasonal Key Points]", + "ri_chen_zhang_sheng": "[Day Stem Twelve Growth Stages]", + "closing": "—— End of hexagram data. Please interpret according to Liu Yao principles.", +} + + +def _get_fields(language: str) -> dict[str, str]: + if language.startswith("en"): + return _EN + if language.startswith("zh-Hant") or language.startswith("zh_Hant"): + return _ZH_HANT + return _ZH + + +_HANT_TEXT_REPLACEMENTS = str.maketrans( + { + "龙": "龍", + "财": "財", + "孙": "孫", + "应": "應", + "腾": "騰", + "陈": "陳", + "为": "為", + "兑": "兌", + "离": "離", + "冲": "沖", + "动": "動", + "静": "靜", + "与": "與", + "关": "關", + "时": "時", + "态": "態", + "标": "標", + "注": "註", + "数": "數", + "据": "據", + "资": "資", + "料": "料", + "头": "頭", + "克": "剋", + "绝": "絕", + "医": "醫", + "药": "藥", + } +) + + +def _is_hant(language: str) -> bool: + return language.startswith("zh-Hant") or language.startswith("zh_Hant") + + +def _display_text(value: str, *, language: str, hant_value: str = "") -> str: + if not _is_hant(language): + return value + return (hant_value or value).translate(_HANT_TEXT_REPLACEMENTS) + + +def _special_mark_label(world_response: str, f: dict[str, str]) -> str: + if world_response == "世": + return f["shi_label"] + if world_response == "应": + return f["ying_label"] + return "" + + +def build_divination_user_prompt( + *, derived: DerivedDivinationData, language: str = "zh-CN" +) -> str: + key = _language_key(language) + prefix, suffix = _LANGUAGE_INSTRUCTIONS[key] + scope = _SCOPE_INSTRUCTIONS[key] + f = _get_fields(language) lines: list[str] = [] - lines.append(f"用户问题:{derived.question}") - lines.append(f"问题类型:{derived.question_type}") - lines.append(f"起卦方式:{derived.divination_method.value}") - lines.append(f"起卦时间:{derived.divination_time}") + + lines.append(prefix) + lines.append("") + lines.append(scope) lines.append("") - lines.append("【本卦】") + lines.append(f"{f['user_question']}:{derived.question}") lines.append( - f"卦名:{derived.gua_name}(上{derived.upper_name}下{derived.lower_name})" + f"{f['question_type']}:{_display_text(derived.question_type, language=language)}" ) - lines.append(f"卦象:{derived.binary_code}") + lines.append( + f"{f['divination_method']}:{_display_text(derived.divination_method.value, language=language)}" + ) + lines.append( + f"{f['divination_time']}:{_display_text(derived.divination_time, language=language)}" + ) + lines.append("") + + lines.append(f["ben_gua"]) + lines.append( + f["gua_name_tpl"].format( + name=_display_text( + derived.gua_name, language=language, hant_value=derived.gua_name_hant + ), + upper=_display_text(derived.upper_name, language=language), + lower=_display_text(derived.lower_name, language=language), + ) + ) + lines.append(f"{f['gua_xiang']}:{derived.binary_code}") lines.append("") if derived.has_changing_yao: - lines.append("【变卦】") - lines.append(f"卦名:{derived.target_gua_name}") - lines.append(f"卦象:{derived.changed_binary_code}") + lines.append(f["bian_gua"]) + lines.append( + f["gua_name_simple_tpl"].format( + name=_display_text( + derived.target_gua_name, + language=language, + hant_value=derived.target_gua_name_hant, + ) + ) + ) + lines.append(f"{f['gua_xiang']}:{derived.changed_binary_code}") lines.append("") - lines.append("【干支】") + lines.append(f["ganzhi"]) g = derived.ganzhi lines.append( - f"年柱:{g.year_gan_zhi} 月柱:{g.month_gan_zhi} 日柱:{g.day_gan_zhi} 时柱:{g.time_gan_zhi}" + f"{f['year_pillar']}:{g.year_gan_zhi} {f['month_pillar']}:{g.month_gan_zhi}" + + f" {f['day_pillar']}:{g.day_gan_zhi} {f['time_pillar']}:{g.time_gan_zhi}" ) - lines.append(f"月建:{g.yue_jian} 日辰:{g.ri_chen}") - lines.append(f"月破:{g.yue_po} 日冲:{g.ri_chong}") lines.append( - f"年空亡:{g.year_kong_wang} 月空亡:{g.month_kong_wang}" - f" 日空亡:{g.day_kong_wang} 时空亡:{g.time_kong_wang}" + f"{f['yue_jian']}:{_display_text(g.yue_jian, language=language)}" + + f" {f['ri_chen']}:{_display_text(g.ri_chen, language=language)}" + ) + lines.append( + f"{f['yue_po']}:{_display_text(g.yue_po, language=language)}" + + f" {f['ri_chong']}:{_display_text(g.ri_chong, language=language)}" + ) + lines.append( + f"{f['year_kong']}:{g.year_kong_wang} {f['month_kong']}:{g.month_kong_wang}" + + f" {f['day_kong']}:{g.day_kong_wang} {f['time_kong']}:{g.time_kong_wang}" ) lines.append("") - lines.append("【五行旺衰】") + lines.append(f["wu_xing"]) for element, status in derived.wu_xing_statuses.items(): - lines.append(f"{element}:{status}") - lines.append("") - - lines.append("【本卦爻象】") - for yao in derived.yao_info_list: - changing_mark = "(动)" if yao.is_changing else "" - world_response = yao.special_mark lines.append( - f"第{yao.position}爻:{yao.spirit_name} {yao.relation_name} " - f"{yao.tigan_name}{yao.element_name} " - f"{'阳' if yao.is_yang else '阴'}爻{changing_mark}" - f"{' 世' if world_response == '世' else ' 应' if world_response == '应' else ''}" + f"{_display_text(element, language=language)}:{_display_text(status, language=language)}" ) lines.append("") - if derived.has_changing_yao and derived.target_yao_info_list: - lines.append("【变卦爻象】") - for yao in derived.target_yao_info_list: - world_response = yao.special_mark + lines.append(f["ben_yao"]) + for yao in derived.yao_info_list: + yang_yin = f["yang_label"] if yao.is_yang else f["yin_label"] + special = _special_mark_label(yao.special_mark, f) + spirit = _display_text( + yao.spirit_name, language=language, hant_value=yao.spirit_name_hant + ) + relation = _display_text( + yao.relation_name, language=language, hant_value=yao.relation_name_hant + ) + tigan = _display_text(yao.tigan_name, language=language) + element = _display_text(yao.element_name, language=language) + if yao.is_changing: lines.append( - f"第{yao.position}爻:{yao.spirit_name} {yao.relation_name} " - f"{yao.tigan_name}{yao.element_name} " - f"{'阳' if yao.is_yang else '阴'}爻" - f"{' 世' if world_response == '世' else ' 应' if world_response == '应' else ''}" + f["yao_line_tpl"].format( + pos=yao.position, + spirit=spirit, + relation=relation, + tigan=tigan, + element=element, + yang_yin=yang_yin, + dong=f["dong_mark"], + special=special, + ) + ) + else: + lines.append( + f["yao_line_tpl_static"].format( + pos=yao.position, + spirit=spirit, + relation=relation, + tigan=tigan, + element=element, + yang_yin=yang_yin, + special=special, + ) + ) + lines.append("") + + if derived.has_changing_yao and derived.target_yao_info_list: + lines.append(f["bian_yao"]) + for yao in derived.target_yao_info_list: + yang_yin = f["yang_label"] if yao.is_yang else f["yin_label"] + special = _special_mark_label(yao.special_mark, f) + spirit = _display_text( + yao.spirit_name, language=language, hant_value=yao.spirit_name_hant + ) + relation = _display_text( + yao.relation_name, language=language, hant_value=yao.relation_name_hant + ) + lines.append( + f["yao_line_tpl_static"].format( + pos=yao.position, + spirit=spirit, + relation=relation, + tigan=_display_text(yao.tigan_name, language=language), + element=_display_text(yao.element_name, language=language), + yang_yin=yang_yin, + special=special, + ) ) lines.append("") if derived.fushen_info_list: - lines.append("【伏神】") + lines.append(f["fushen"]) for fs in derived.fushen_info_list: lines.append( - f"第{fs.position}爻:{fs.relation_name} {fs.tigan_name}{fs.element_name}" + f["fushen_line_tpl"].format( + pos=fs.position, + relation=_display_text( + fs.relation_name, + language=language, + hant_value=fs.relation_name_hant, + ), + tigan=_display_text(fs.tigan_name, language=language), + element=_display_text(fs.element_name, language=language), + ) ) lines.append("") if derived.special_status: - lines.append("【特殊状态标注】") + lines.append(f["special_status"]) for status in derived.special_status: - lines.append(f"- {status}") + lines.append(f"- {_display_text(status, language=language)}") lines.append("") if derived.interactions: - lines.append("【全局冲合提示】") + lines.append(f["interactions"]) for interaction in derived.interactions: - lines.append(f"- {interaction}") + lines.append(f"- {_display_text(interaction, language=language)}") lines.append("") if derived.time_effect: - lines.append("【时令关键点】") + lines.append(f["time_effect"]) for effect in derived.time_effect: - lines.append(f"- {effect}") + lines.append(f"- {_display_text(effect, language=language)}") lines.append("") if derived.ri_chen_zhang_sheng: - lines.append("【日辰十二长生】") + lines.append(f["ri_chen_zhang_sheng"]) for zs in derived.ri_chen_zhang_sheng: - lines.append(f"- {zs}") + lines.append(f"- {_display_text(zs, language=language)}") lines.append("") - lines.append("——以上为起卦所得完整数据,请据此进行六爻解读。") + lines.append(f["closing"]) + lines.append("") + lines.append(suffix) + + return "\n".join(lines) + + +def build_follow_up_user_prompt(*, question: str, language: str = "zh-CN") -> str: + key = _language_key(language) + prefix, suffix = _LANGUAGE_INSTRUCTIONS[key] + scope = _SCOPE_INSTRUCTIONS[key] + + lines = [prefix, "", scope, ""] + + if key == "en": + lines.append(f"User Question: {question}") + elif key == "zh-Hant": + lines.append(f"使用者問題:{question}") + else: + lines.append(f"用户问题:{question}") + + lines.append("") + lines.append(suffix) return "\n".join(lines) diff --git a/backend/src/core/agentscope/prompts/worker_rules.py b/backend/src/core/agentscope/prompts/worker_rules.py index 2bcd66e..8637290 100644 --- a/backend/src/core/agentscope/prompts/worker_rules.py +++ b/backend/src/core/agentscope/prompts/worker_rules.py @@ -1,12 +1,14 @@ from __future__ import annotations -_WORKER_ROLE_PLAYING = """\ +_WORKER_ROLE_PLAYING_ZH = """\ 你是一名严格遵循五行生克与卦象逻辑的六爻解卦师。你的唯一任务是依据提供的结构化排盘数据,输出基于规则的专业推断。 【边界与禁令】 +- 只回答与六爻占卜、卦象分析、六爻所需易理说明直接相关的问题;用户提出无关问题时,必须直接拒绝,不回答无关内容。 - 仅使用输入数据中的六爻信息推演,严禁编造数据。 - 严禁引入星座、塔罗、八字、紫微等外体系内容。 - 严禁大段引用《周易》原文辞句。六爻以五行生克制化为核心。 +- 涉及投资、彩票、医疗、生死等高风险问题时,只能作六爻象意参考,不得给出保证性、绝对性或替代专业意见的结论。 【推演公理】(优先级由高到低) 1. 卦爻主从律:先断本卦卦象属性(六冲卦主事散速,六合卦主事缓滞),此为不可逆之背景底色;次观爻象变化。 @@ -60,7 +62,7 @@ _WORKER_ROLE_PLAYING = """\ 专业、明确、克制,像真正会看六爻的人说话。 不要写成文学散文,不要堆砌模糊词,不要故弄高深。 你可以解释,但解释必须围绕卦象本身展开。 -你的目标不是“像在算卦”,而是“真的按六爻规则解卦”。 +你的目标不是"像在算卦",而是"真的按六爻规则解卦"。 【签级参考锚定】 签级评定应综合卦象底色与动变吉凶,参考以下原则: @@ -70,8 +72,155 @@ _WORKER_ROLE_PLAYING = """\ - 中下签:六冲卦底色凶 / 用神衰弱 / 用神受克但尚有解救 / 动变回头克但世爻不伤。 - 下下签:六冲卦 + 用神月破空亡 + 动爻回头克世/克用 + 日月无助。 -若卦象吉凶参半,应以“卦象底色”为第一权重,以“世爻安危”为第二权重。 -""" +若卦象吉凶参半,应以"卦象底色"为第一权重,以"世爻安危"为第二权重。""" + +_WORKER_ROLE_PLAYING_ZH_HANT = """\ +你是一名嚴格遵循五行生剋與卦象邏輯的六爻解卦師。你的唯一任務是依據提供的結構化排盤資料,輸出基於規則的專業推斷。 + +【邊界與禁令】 +- 只回答與六爻占卜、卦象分析、六爻所需易理說明直接相關的問題;使用者提出無關問題時,必須直接拒絕,不回答無關內容。 +- 僅使用輸入資料中的六爻資訊推演,嚴禁編造資料。 +- 嚴禁引入星座、塔羅、八字、紫微等外體系內容。 +- 嚴禁大段引用《周易》原文辭句。六爻以五行生剋制化為核心。 +- 涉及投資、彩票、醫療、生死等高風險問題時,只能作六爻象意參考,不得給出保證性、絕對性或替代專業意見的結論。 + +【推演公理】(優先級由高到低) +1. 卦爻主從律:先斷本卦卦象屬性(六沖卦主事散速,六合卦主事緩滯),此為不可逆之背景底色;次觀爻象變化。 +2. 動靜虛實律:靜爻之間不構成特殊格局(如三合、六合局,除非有動爻或日月引化);動爻所化之變爻若逢空、破、墓、絕,則動而無果,事主落空。 +3. 生剋本位律:一切生剋以月建旺衰與日辰生剋為最高裁決。輸入資料中的五行狀態為既定事實,不得篡改。 + +【六親類象映射】 +根據問題類型,六親指向如下: + +問事業/工作: +- 官鬼:上司、工作壓力、職位、權力 +- 父母:文書、合同、項目、單位、資質 +- 妻財:薪水、收入、資源 +- 子孫:下屬、技能、解憂之神 +- 兄弟:同事、競爭者 + +問財運/投資: +- 妻財:財源、收益、資金(主用神) +- 兄弟:劫財、競爭、風險 +- 子孫:生財之源、福氣 +- 父母:文書、證件、平台 +- 官鬼:耗財、壓力 + +問感情/婚姻: +- 男測:妻財為對方,官鬼為情敵 +- 女測:官鬼為對方,妻財為情敵 +- 父母:婚約、文書、家庭 +- 子孫:子女、解憂 + +問健康/疾病: +- 官鬼:病症、病灶(忌神) +- 子孫:醫孫:醫藥、醫生、解災之神(用神) +- 父母:醫院、長輩 +- 兄弟:同輩、助力 + +【思考鏈要求】 +你必須按以下順序顯式輸出推理過程: + +1. 卦象定性:判斷本卦屬性(六沖/六合/歸魂/遊魂),明確宏觀底色。 +2. 用神定位:根據問題確定用神與忌神,查看是否上卦、是否發動。 +3. 旺衰虛實:月建斷旺衰(旺相休囚死),日辰斷生剋(十二長生狀態及沖合),動變斷虛實(化進/化退/化空/化破)。 +4. 生剋路線:列舉世應、動變、日月的具體生剋鏈條,逐條說明對用神的影響。 +5. 特殊組合:僅在符合動靜虛實律的前提下,評估暗動、三合局、回頭生剋等。 +6. 綜合裁決:結合卦象底色與爻象生剋,給出趨勢結論、核心風險點與轉機條件。 + +【力量優先級】 +- 變爻回頭生剋時,變爻力量強於本爻 +- 世應 > 動爻 > 變爻 > 日月 > 靜爻 + +【表達風格】 +專業、明確、克制,像真正會看六爻的人說話。 +不要寫成文學散文,不要堆砌模糊詞,不要故弄高深。 +你可以解釋,但解釋必須圍繞卦象本身展開。 +你的目標不是「像在算卦」,而是「真的按六爻規則解卦」。 + +【簽級參考錨定】 +簽級評定應綜合卦象底色與動變吉凶,參考以下原則: + +- 上上簽:六合卦或非六沖卦 + 用神旺相 + 動爻生世/用神有力 + 無回頭剋及空破。 +- 中上簽:非六沖卦 + 用神有氣 + 存在輕微阻礙(如用神靜而不動、或忌神暗動但可制)。 +- 中下簽:六沖卦底色凶 / 用神衰弱 / 用神受剋但尚有解救 / 動變回頭剋但世爻不傷。 +- 下下簽:六沖卦 + 用神月破空亡 + 動爻回頭剋世/剋用 + 日月無助。 + +若卦象吉凶參半,應以「卦象底色」為第一權重,以「世爻安危」為第二權重。""" + +_WORKER_ROLE_PLAYING_EN = """\ +You are a Liu Yao (Six Lines) divination master who strictly follows the logic of Five Elements (Wu Xing) generation-restriction and hexagram imagery. Your sole task is to produce rule-based professional interpretations based on the structured hexagram data provided. + +[Boundaries & Prohibitions] +- Only answer questions directly related to Liu Yao divination, hexagram analysis, or I Ching principles needed for Liu Yao interpretation. If the user asks an unrelated question, refuse directly and do not answer the unrelated content. +- Only deduce from the six-line information in the input data. Never fabricate data. +- Never introduce external systems such as astrology, Tarot, Ba Zi (Eight Characters), or Zi Wei. +- Never quote long passages from the original I Ching text. Liu Yao centers on Five Elements generation and restriction. +- For investment, lottery, medical, life-or-death, or other high-risk questions, provide only symbolic Liu Yao reference. Never give guaranteed, absolute, or professional-advice-replacing conclusions. + +[Deduction Axioms] (in descending priority) +1. Hexagram Backdrop Rule: First determine the hexagram type (Six-Clash hexagram indicates scattered/quick events, Six-Union hexagram indicates delayed/slow events). This is the irreversible background. Then observe line changes. +2. Movement-Stillness Rule: Still lines do not form special patterns (such as Three Union or Six Union) unless activated by moving lines or Day-Month induction. If a moving line transforms into Void, Break, Tomb, or Exhaustion, the movement yields no result. +3. Generation-Restriction Priority: All generation and restriction is adjudicated by Month Branch prosperity/decline and Day Branch generation/restriction. The Five Element statuses in the input data are established facts and must not be altered. + +[Six Relatives (Liu Qin) Category Mapping] +Based on the question type, the Six Relatives map as follows: + +Career/Work questions: +- Officer (Guan Gui): supervisor, work pressure, position, authority +- Parent (Fu Mu): documents, contracts, projects, organization, credentials +- Wealth (Qi Cai): salary, income, resources +- Children (Zi Sun): subordinates, skills, relief from trouble +- Sibling (Xiong Di): colleagues, competitors + +Wealth/Investment questions: +- Wealth (Qi Cai): financial resources, earnings, capital (primary Yong Shen) +- Sibling (Xiong Di): wealth-draining, competition, risk +- Children (Zi Sun): source of wealth, blessings +- Parent (Fu Mu): documents, licenses, platforms +- Officer (Guan Gui): wealth depletion, pressure + +Relationships/Marriage questions: +- Male querent: Wealth line represents the partner; Officer line represents romantic rival +- Female querent: Officer line represents the partner; Wealth line represents romantic rival +- Parent (Fu Mu): marriage contract, documents, family +- Children (Zi Sun): children, relief + +Health/Illness questions: +- Officer (Guan Gui): illness, pathology (Ji Shen / feared spirit) +- Children (Zi Sun): medicine, doctor, relief spirit (Yong Shen / useful spirit) +- Parent (Fu Mu): hospital, elders +- Sibling (Xiong Di): peers, support + +[Thinking Chain Requirement] +You must explicitly output your reasoning in the following order: + +1. Hexagram Classification: Determine the hexagram type (Six-Clash / Six-Union / Returning Spirit / Wandering Spirit) and establish the macro backdrop. +2. Yong Shen Identification: Based on the question, identify the Yong Shen (useful spirit) and Ji Shen (feared spirit). Check whether they appear in the hexagram and whether they are changing lines. +3. Prosperity & Void: Month Branch determines prosperity/decline (Prosperous / Strong / Resting / Imprisoned / Dead). Day Branch determines day-level generation/restriction, clash, and union. Movement and change determine substance vs. void. +4. Generation-Restriction Chains: List the specific generation-restriction chains between Self Line, Response Line, moving lines, changing lines, Day, and Month. Explain each one's effect on the Yong Shen. +5. Special Combinations: Only when permitted by the Movement-Stillness Rule, assess hidden movement, Three Union patterns, reverse generation/restriction, etc. +6. Comprehensive Verdict: Combine the hexagram backdrop with the line dynamics to produce the trend conclusion, core risk points, and turning-point conditions. + +[Strength Hierarchy] +- When a changing line generates or restricts in reverse, the changing line's strength exceeds the original line +- Self/Response > moving lines > changing lines > Day/Month > still lines + +[Expression Style] +Professional, precise, restrained. Speak like someone who truly reads hexagrams. +Do not write literary prose, do not pile on vague words, do not feign profundity. +You may explain, but all explanation must be anchored to the hexagram image itself. +Your goal is not to "sound like" a divination reading, but to actually interpret according to Liu Yao rules. + +[Sign-Level Reference Anchoring] +Sign-level assessment should integrate hexagram backdrop and movement/change auspiciousness, referencing the following principles: + +- Top-Top (Shang Shang): Six-Union hexagram or non-Six-Clash hexagram + Yong Shen prosperous + moving line generates Self/Yong Shen with strength + no reverse restriction, void, or break. +- Upper-Middle (Zhong Shang): Non-Six-Clash hexagram + Yong Shen has vitality + minor obstructions exist (e.g. Yong Shen still and unmoving, or Ji Shen secretly moves but can be restrained). +- Lower-Middle (Zhong Xia): Six-Clash hexagram with inauspicious backdrop / Yong Shen weak / Yong Shen receives restriction but still has rescue / moving-then-reverse-restriction but Self Line unharmed. +- Bottom-Bottom (Xia Xia): Six-Clash hexagram + Yong Shen monthly break and void + moving line reverse-restricts Self/restricts Yong Shen + Day and Month offer no help. + +When the hexagram shows mixed auspicious and inauspicious signs, weigh "hexagram backdrop" as the primary factor and "Self Line safety" as the secondary factor.""" _WORKER_OUTPUT_RULES_ZH_CN = """\ 按输出要求严格返回对应的json对象。 @@ -89,21 +238,24 @@ focus_points:本次解讀的核心關注點列表,每項為簡短陳述,3- advice:必須逐條對應卦象依據(哪一爻、何種生剋沖合旺衰),給出可執行動作,優先回答:最該防什麼、最該做什麼、何時可推進、何時應暫緩。 keywords:繁體中文優先四字,必須來自本次卦象核心判斷。 answer:必須是完整解讀,覆蓋總體判斷、當前態勢、最終趨勢、風險點、轉機條件、行動優先級,多段文本段間用\\n\\n分隔,首段直指核心態勢(偏吉/偏凶/先難後易/成中有阻等)。 -sign_level:枚舉值固定,必須且只能填以下四個值之一:上上签/中上签/中下签/下下签(必須使用簡體簽字,不可用繁體簽)。""" +sign_level:此欄位為後端協議枚舉,必須且只能填以下四個值之一:上上签/中上签/中下签/下下签;其他文字內容仍必須使用繁體中文。""" _WORKER_OUTPUT_RULES_EN = """\ Return the JSON object strictly following the output schema. conclusion: Must tie back to the hexagram, changed lines, and key line positions. No vague claims. Provide 2-4 key findings. focus_points: Core points of this reading, each as a brief statement. 3-5 items, distilled from the most significant chart elements. advice: Each item must cite a specific chart element (which line, what element interaction or strength condition). Prioritize: biggest risk, top action, favorable timing, when to hold back. -keywords: 2-4 short divinatory phrases drawn from this reading, using terms familiar in esoteric traditions (e.g. karmic crossroads, slender hope, unseen obstacle, waxing fortune, hidden pivot, narrow passage, turning tide, fading twilight). Avoid generic filler. +keywords: 2-4 concise Liu Yao-style phrases translated into English, such as "Self weakened", "Wealth constrained", "Officer pressure", "moving line brings support", or "void then filled". Avoid Tarot-, astrology-, or karma-style phrasing. answer: A complete reading covering overall judgment, current situation, final trend, risk points, turning conditions, and action priorities. Separate paragraphs with \\n\\n. The opening paragraph must state the core verdict directly (e.g. leaning auspicious / leaning inauspicious / difficulty-then-ease / success-with-obstacles). sign_level: Must be exactly one of: 上上签 / 中上签 / 中下签 / 下下签. Always use the Chinese enum value regardless of language.""" def get_worker_role_playing(language: str) -> str: - _ = language - return _WORKER_ROLE_PLAYING + if language.startswith("en"): + return _WORKER_ROLE_PLAYING_EN + if language.startswith("zh-Hant") or language.startswith("zh_Hant"): + return _WORKER_ROLE_PLAYING_ZH_HANT + return _WORKER_ROLE_PLAYING_ZH def get_worker_output_rules(language: str) -> str: diff --git a/backend/src/core/agentscope/runtime/runner.py b/backend/src/core/agentscope/runtime/runner.py index a8f4fd6..2774f77 100644 --- a/backend/src/core/agentscope/runtime/runner.py +++ b/backend/src/core/agentscope/runtime/runner.py @@ -13,7 +13,10 @@ from agentscope.message import Msg from agentscope.tool import Toolkit from agentscope.model import OpenAIChatModel from core.agentscope.prompts.system_prompt import build_system_prompt -from core.agentscope.prompts.user_prompt import build_divination_user_prompt +from core.agentscope.prompts.user_prompt import ( + build_divination_user_prompt, + build_follow_up_user_prompt, +) from core.agentscope.schemas.agui_input import extract_latest_user_payload from core.divination import derive_divination from core.agentscope.runtime.json_react_agent import JsonReActAgent @@ -209,6 +212,12 @@ class AgentScopeRunner: worker_output_model = resolve_worker_output_model( runtime_mode=runtime_mode.value ) + language = "zh-CN" + if user_context.settings is not None: + prefs = getattr(user_context.settings, "preferences", None) + if prefs is not None: + language = getattr(prefs, "language", "zh-CN") or "zh-CN" + await self._emit_step_event( pipeline=pipeline, run_input=run_input, @@ -222,6 +231,7 @@ class AgentScopeRunner: context_messages=context_messages, run_input=run_input, derived_divination=derived_divination, + language=language, ), toolkit=toolkit, run_input=run_input, @@ -230,6 +240,7 @@ class AgentScopeRunner: pipeline=pipeline, runtime_mode=runtime_mode, derived_divination=derived_divination, + language=language, ) worker_output = worker_output_model.model_validate(worker_result.payload) await self._emit_step_event( @@ -253,6 +264,7 @@ class AgentScopeRunner: pipeline: PipelineLike, runtime_mode: RuntimeMode, derived_divination: DerivedDivinationData | None, + language: str, ) -> StageExecutionResult: tracking_model = self._build_model(stage_config=stage_config) formatter = OpenAIChatFormatter() @@ -265,11 +277,6 @@ class AgentScopeRunner: emit_text_events=True, emit_tool_events=False, ) - language = "zh-CN" - if user_context.settings is not None: - prefs = getattr(user_context.settings, "preferences", None) - if prefs is not None: - language = getattr(prefs, "language", "zh-CN") or "zh-CN" system_prompt = build_system_prompt( agent_type=stage_config.agent_type, @@ -285,6 +292,7 @@ class AgentScopeRunner: base_messages=[Msg("system", system_prompt, "system"), *input_messages], output_model=worker_output_model, retries=2, + language=language, ) worker_payload = worker_output_model.model_validate(worker_payload_raw) response_metadata = self._llm_pricing_service.build_usage_metadata( @@ -316,11 +324,17 @@ class AgentScopeRunner: context_messages: list[Msg], run_input: RunAgentInput, derived_divination: DerivedDivinationData | None, + language: str = "zh-CN", ) -> list[Msg]: if derived_divination is not None: - user_text = build_divination_user_prompt(derived=derived_divination) + user_text = build_divination_user_prompt( + derived=derived_divination, language=language + ) else: - user_text, _ = extract_latest_user_payload(run_input) + raw_user_text, _ = extract_latest_user_payload(run_input) + user_text = build_follow_up_user_prompt( + question=raw_user_text, language=language + ) if derived_divination is not None and context_messages: last = context_messages[-1] diff --git a/backend/src/core/agentscope/utils/json_finalize.py b/backend/src/core/agentscope/utils/json_finalize.py index 61a71a4..2240519 100644 --- a/backend/src/core/agentscope/utils/json_finalize.py +++ b/backend/src/core/agentscope/utils/json_finalize.py @@ -1,6 +1,7 @@ from __future__ import annotations import json +import re from collections.abc import Awaitable from typing import Any, Protocol @@ -8,6 +9,10 @@ from core.agentscope.utils.parsing import extract_text_content, parse_json_dict from pydantic import BaseModel, ValidationError from agentscope.message import Msg +from core.logging import get_logger + + +logger = get_logger("core.agentscope.utils.json_finalize") class FormatterProtocol(Protocol): @@ -19,6 +24,7 @@ def build_json_finalize_instruction( schema_json: str, attempt: int, validation_error: str = "", + language: str | None = None, ) -> str: error_part = ( "" @@ -29,12 +35,36 @@ def build_json_finalize_instruction( "Fix all missing/invalid fields and regenerate." ) ) + + language_part = "" + if language is not None: + if language.startswith("en"): + language_part = """ + +═══════════════════════════════════════════════════════════════════════════════ +ENGLISH OUTPUT REQUIRED - NO CHINESE SENTENCES ALLOWED +═══════════════════════════════════════════════════════════════════════════════ +Return JSON only. All string values (except sign_level) must be in English. +Do NOT write Chinese sentences. Translate all terminology to English. +The sign_level MUST be one of: 上上签 / 中上签 / 中下签 / 下下签 +═══════════════════════════════════════════════════════════════════════════════""" + elif language.startswith("zh-Hant"): + language_part = """ + +返回 JSON。使用繁體中文。 +sign_level 必須是:上上签 / 中上签 / 中下签 / 下下签(必須使用簡體簽字)""" + else: + language_part = """ + +返回 JSON。使用简体中文。 +sign_level 必须是:上上签 / 中上签 / 中下签 / 下下签""" + return ( "Return JSON only. Do not output markdown, prose, or code fences. " "Follow this JSON Schema exactly and include all required fields. " "Do not call tools.\n\n" - f"[输出结构Output Schema]\n{schema_json}\n\n" - f"[Attempt]\n{attempt}{error_part}" + f"[Output Schema]\n{schema_json}\n\n" + f"[Attempt]\n{attempt}{language_part}{error_part}" ) @@ -45,6 +75,7 @@ async def finalize_json_response( base_messages: list[Msg], output_model: type[BaseModel], retries: int, + language: str | None = None, ) -> tuple[Any, dict[str, Any]]: schema_json = json.dumps( output_model.model_json_schema(), @@ -63,6 +94,7 @@ async def finalize_json_response( schema_json=schema_json, attempt=attempt, validation_error=last_error, + language=language, ), "user", ), @@ -87,12 +119,59 @@ async def finalize_json_response( try: validated = output_model.model_validate(payload) - return response, validated.model_dump( + validated_payload = validated.model_dump( mode="json", by_alias=True, exclude_none=True ) + language_error = _validate_payload_language( + payload=validated_payload, + language=language, + ) + if language_error: + logger.warning( + "json_finalize_language_retry", + output_model=output_model.__name__, + attempt=attempt, + language=language, + reason=language_error, + ) + last_error = language_error + continue + return response, validated_payload except ValidationError as exc: last_error = str(exc) raise RuntimeError( f"failed to finalize structured output for {output_model.__name__}: {last_error}" ) + + +def _validate_payload_language(*, payload: dict[str, Any], language: str | None) -> str: + if language is None or not language.startswith("en"): + return "" + offenders = _collect_cjk_fields(payload, path="") + if not offenders: + return "" + return ( + "English output required, but Chinese characters were found in user-visible " + f"JSON field(s): {', '.join(offenders[:8])}. Rewrite those values entirely in English. " + "Keep only sign_level as the required Chinese enum value." + ) + + +def _collect_cjk_fields(value: Any, *, path: str) -> list[str]: + if isinstance(value, dict): + results: list[str] = [] + for key, item in value.items(): + if key == "sign_level": + continue + child_path = key if not path else f"{path}.{key}" + results.extend(_collect_cjk_fields(item, path=child_path)) + return results + if isinstance(value, list): + results = [] + for index, item in enumerate(value): + results.extend(_collect_cjk_fields(item, path=f"{path}[{index}]")) + return results + if isinstance(value, str) and re.search(r"[\u4e00-\u9fff]", value): + return [path or ""] + return [] diff --git a/backend/src/schemas/agent/runtime_models.py b/backend/src/schemas/agent/runtime_models.py index 1ae725a..fd6f6be 100644 --- a/backend/src/schemas/agent/runtime_models.py +++ b/backend/src/schemas/agent/runtime_models.py @@ -11,6 +11,7 @@ from schemas.domain.divination import DerivedDivinationData class RunStatus(str, Enum): SUCCESS = "success" FAILED = "failed" + REFUSED = "refused" class ToolStatus(str, Enum): @@ -43,12 +44,12 @@ class WorkerAgentOutputLite(BaseModel): model_config = ConfigDict(extra="forbid") status: RunStatus = RunStatus.SUCCESS - sign_level: Literal["上上签", "中上签", "中下签", "下下签"] - conclusion: list[str] = Field(min_length=1, max_length=6) + sign_level: Literal["上上签", "中上签", "中下签", "下下签"] | None = None + conclusion: list[str] = Field(default_factory=list, max_length=6) focus_points: list[str] = Field(default_factory=list, max_length=6) - advice: list[str] = Field(min_length=1, max_length=6) - keywords: list[str] = Field(min_length=3, max_length=8) - answer: str = Field(min_length=1, max_length=4000) + advice: list[str] = Field(default_factory=list, max_length=6) + keywords: list[str] = Field(default_factory=list, max_length=8) + answer: str = "" error: ErrorInfo | None = None diff --git a/backend/tests/unit/test_agentscope_prompts.py b/backend/tests/unit/test_agentscope_prompts.py index e82553a..fde44aa 100644 --- a/backend/tests/unit/test_agentscope_prompts.py +++ b/backend/tests/unit/test_agentscope_prompts.py @@ -2,30 +2,48 @@ from __future__ import annotations from core.agentscope.prompts.agent_prompt import build_agent_prompt from core.agentscope.prompts.system_prompt import build_system_prompt +from core.agentscope.prompts.user_prompt import ( + build_divination_user_prompt, + build_follow_up_user_prompt, +) from schemas.agent.system_agent import AgentType, SystemAgentLLMConfig -def test_system_prompt_enforces_language_en() -> None: +def test_system_prompt_safety_has_refusal_rules_en() -> None: prompt = build_system_prompt( agent_type=AgentType.WORKER, language="en-US", llm_config=SystemAgentLLMConfig(), ) - assert "English" in prompt assert "" in prompt - assert "" in prompt + assert "REFUSE IMMEDIATELY" in prompt + assert "Tarot" in prompt + assert "Ba Zi" in prompt -def test_system_prompt_enforces_language_zh_cn() -> None: +def test_system_prompt_safety_has_refusal_rules_zh() -> None: prompt = build_system_prompt( agent_type=AgentType.WORKER, language="zh-CN", llm_config=SystemAgentLLMConfig(), ) - assert "简体中文" in prompt assert "" in prompt + assert "必须立即拒绝" in prompt + assert "塔罗" in prompt + assert "八字" in prompt + + +def test_system_prompt_no_language_constraint_in_system() -> None: + prompt = build_system_prompt( + agent_type=AgentType.WORKER, + language="en-US", + llm_config=SystemAgentLLMConfig(), + ) + + assert "" not in prompt + assert "MUST respond in" not in prompt def test_system_prompt_safety_restricts_to_divination() -> None: @@ -35,7 +53,7 @@ def test_system_prompt_safety_restricts_to_divination() -> None: llm_config=SystemAgentLLMConfig(), ) - assert "只回答与六爻占卜" in prompt or "解卦" in prompt + assert "六爻" in prompt or "解卦" in prompt def test_system_prompt_does_not_contain_env_section() -> None: @@ -54,6 +72,7 @@ def test_agent_prompt_keeps_only_identity_and_domain_flow() -> None: prompt = build_agent_prompt( agent_type=AgentType.WORKER, llm_config=SystemAgentLLMConfig(), + language="zh-CN", ) assert "focus_points" in prompt @@ -71,13 +90,29 @@ def test_system_prompt_sections_are_not_duplicated() -> None: assert prompt.count("") == 1 assert prompt.count("") == 1 - assert prompt.count("") == 1 def test_system_prompt_requires_paragraph_breaks_for_answer() -> None: prompt = build_agent_prompt( agent_type=AgentType.WORKER, llm_config=SystemAgentLLMConfig(), + language="zh-CN", ) assert "段间用\\n\\n" in prompt + + +def test_user_prompt_has_language_constraint_en() -> None: + prompt = build_follow_up_user_prompt(question="test question", language="en-US") + + assert "CRITICAL: YOUR ENTIRE RESPONSE MUST BE IN ENGLISH" in prompt + assert "═══════════════════════════════════════════════════════════════════════════════" in prompt + assert "[SCOPE CHECK - REFUSE IF:]" in prompt + + +def test_user_prompt_has_language_constraint_zh() -> None: + prompt = build_follow_up_user_prompt(question="test question", language="zh-CN") + + assert "关键:必须全程使用简体中文回答" in prompt + assert "═══════════════════════════════════════════════════════════════════════════════" in prompt + assert "【范围检查" in prompt diff --git a/backend/tests/unit/test_json_finalize.py b/backend/tests/unit/test_json_finalize.py index c6c43a9..a3739d3 100644 --- a/backend/tests/unit/test_json_finalize.py +++ b/backend/tests/unit/test_json_finalize.py @@ -47,12 +47,41 @@ class _Model: return _Response(self._payload) -def test_build_instruction_uses_output_schema_title() -> None: +def test_build_instruction_has_schema() -> None: instruction = build_json_finalize_instruction( schema_json="{}", attempt=1, ) - assert "[输出结构Output Schema]" in instruction + assert "[Output Schema]" in instruction + + +def test_build_instruction_has_language_constraint_en() -> None: + instruction = build_json_finalize_instruction( + schema_json="{}", + attempt=1, + language="en-US", + ) + assert "ENGLISH OUTPUT REQUIRED" in instruction + assert "═══════════════════════════════════════════════════════════════════════════════" in instruction + + +def test_build_instruction_has_language_constraint_zh() -> None: + instruction = build_json_finalize_instruction( + schema_json="{}", + attempt=1, + language="zh-CN", + ) + assert "返回 JSON。使用简体中文" in instruction + + +def test_build_instruction_no_language_constraint_when_none() -> None: + instruction = build_json_finalize_instruction( + schema_json="{}", + attempt=1, + language=None, + ) + assert "ENGLISH OUTPUT REQUIRED" not in instruction + assert "返回 JSON" not in instruction @pytest.mark.asyncio diff --git a/infra/scripts/app.sh b/infra/scripts/app.sh index 6cd30ef..6233b95 100755 --- a/infra/scripts/app.sh +++ b/infra/scripts/app.sh @@ -164,6 +164,9 @@ start() { echo "" echo "=== App Started ===" + echo "Web server running on: http://localhost:${WEB_PORT}" + echo "Health check: http://localhost:${WEB_PORT}/health" + echo "" echo "Log files will be created in logs/ directory:" echo " - web.log, web.error.log" echo " - worker-agent.log, worker-agent.error.log" @@ -188,8 +191,8 @@ stop() { echo "Checking for orphaned processes..." - kill_matching_processes "uvicorn" "uv run uvicorn app:app" - kill_matching_processes "taskiq workers" "uv run taskiq worker core.taskiq.app:" + kill_matching_processes "uvicorn" "uvicorn app:app" + kill_matching_processes "taskiq workers" "taskiq worker.*core\.taskiq\.app:" kill_listening_processes "port ${WEB_PORT} listeners" "$WEB_PORT"