feat: 新增追问模式和iOS本地化,重构后端输出模型
This commit is contained in:
@@ -13,7 +13,7 @@ from core.logging import get_logger
|
||||
from schemas.agent.forwarded_props import RuntimeMode
|
||||
from schemas.enums import AgentChatMessageRole, AgentChatSessionStatus
|
||||
from schemas.agent.system_agent import AgentType
|
||||
from schemas.agent.runtime_models import AgentOutput, FollowUpOutput, ToolAgentOutput
|
||||
from schemas.agent.runtime_models import FollowUpOutput, PersistedAgentOutput, ToolAgentOutput
|
||||
from schemas.agent.visibility import SystemVisibilityBit, bit_mask
|
||||
from schemas.domain.chat_message import AgentChatMessageMetadata
|
||||
from schemas.domain.chat_session import SessionStateSnapshot
|
||||
@@ -107,6 +107,11 @@ class SqlAlchemyEventStore:
|
||||
content_value = self._event_value(event, "answer")
|
||||
content = content_value if isinstance(content_value, str) else ""
|
||||
if not content:
|
||||
self._logger.warning(
|
||||
"text_message_end skipped: empty answer",
|
||||
run_id=self._event_value(event, "runId"),
|
||||
status=self._event_value(event, "status"),
|
||||
)
|
||||
return
|
||||
|
||||
input_tokens = self._to_int(self._event_value(event, "inputTokens"))
|
||||
@@ -148,7 +153,7 @@ class SqlAlchemyEventStore:
|
||||
return
|
||||
|
||||
if runtime_mode == RuntimeMode.CHAT.value:
|
||||
worker_output = AgentOutput.model_validate(worker_output_payload)
|
||||
worker_output = PersistedAgentOutput.model_validate(worker_output_payload)
|
||||
else:
|
||||
worker_output = FollowUpOutput.model_validate(worker_output_payload)
|
||||
agent_type = AgentType.WORKER
|
||||
|
||||
@@ -2,6 +2,8 @@ from __future__ import annotations
|
||||
|
||||
from core.agentscope.prompts.sections import wrap_section
|
||||
from core.agentscope.prompts.worker_rules import (
|
||||
get_follow_up_output_rules,
|
||||
get_follow_up_role_playing,
|
||||
get_worker_output_rules,
|
||||
get_worker_role_playing,
|
||||
)
|
||||
@@ -13,9 +15,14 @@ def build_agent_prompt(
|
||||
agent_type: AgentType,
|
||||
llm_config: SystemAgentLLMConfig | None = None,
|
||||
language: str = "zh-CN",
|
||||
runtime_mode: str = "chat",
|
||||
) -> str:
|
||||
_ = agent_type, llm_config
|
||||
role_playing = get_worker_role_playing(language)
|
||||
output_rules = get_worker_output_rules(language)
|
||||
if runtime_mode == "follow_up":
|
||||
role_playing = get_follow_up_role_playing(language)
|
||||
output_rules = get_follow_up_output_rules(language)
|
||||
else:
|
||||
role_playing = get_worker_role_playing(language)
|
||||
output_rules = get_worker_output_rules(language)
|
||||
content = f"[role_playing]\n{role_playing}\n\n[output_json_rules]\n{output_rules}"
|
||||
return wrap_section("agent", content)
|
||||
|
||||
@@ -111,6 +111,7 @@ def build_system_prompt(
|
||||
llm_config: SystemAgentLLMConfig | None = None,
|
||||
tools: Sequence[Tool | dict[str, Any]] | None = None,
|
||||
now_utc: datetime | None = None,
|
||||
runtime_mode: str = "chat",
|
||||
) -> str:
|
||||
sections: list[str | None] = [
|
||||
_build_time_context(now_utc=now_utc),
|
||||
@@ -119,6 +120,7 @@ def build_system_prompt(
|
||||
agent_type=agent_type,
|
||||
llm_config=llm_config,
|
||||
language=language,
|
||||
runtime_mode=runtime_mode,
|
||||
),
|
||||
build_tools_prompt(tools=tools) if tools else None,
|
||||
]
|
||||
|
||||
@@ -9,12 +9,14 @@ _LANGUAGE_INSTRUCTIONS = {
|
||||
"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"
|
||||
"- Write in natural, idiomatic American English that any ordinary person can understand\n"
|
||||
"- The ONLY Chinese characters allowed in your response:\n"
|
||||
" * sign_level enum values (上上签 / 中上签 / 中下签 / 下下签)\n"
|
||||
" * Hexagram name on first mention only (e.g. 风山渐)\n"
|
||||
"- If you write Chinese sentences in your English response, you have FAILED this task\n"
|
||||
"═══════════════════════════════════════════════════════════════════════════════",
|
||||
"═══════════════════════════════════════════════════════════════════════════════\n"
|
||||
"REMINDER: RESPOND IN ENGLISH. Translate all Chinese terms to English.\n"
|
||||
"REMINDER: Respond in English. Minimize Chinese characters. Write for ordinary people.\n"
|
||||
"═══════════════════════════════════════════════════════════════════════════════",
|
||||
),
|
||||
"zh-Hant": (
|
||||
@@ -42,7 +44,11 @@ _SCOPE_INSTRUCTIONS = {
|
||||
"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."
|
||||
"WHEN REFUSING:\n"
|
||||
"- Set status=\"refused\"\n"
|
||||
"- Set sign_level=\"下下签\"\n"
|
||||
"- Set answer to a brief explanation of why you cannot answer (in English)\n"
|
||||
"- Leave conclusion, focus_points, advice, keywords as empty lists"
|
||||
),
|
||||
"zh-Hant": (
|
||||
"【範圍檢查 - 以下情況請拒絕:】\n"
|
||||
@@ -50,7 +56,11 @@ _SCOPE_INSTRUCTIONS = {
|
||||
"2. 請求塔羅、八字、紫微、星座等其他方法\n"
|
||||
"3. 問題與占卜無關(編程、天氣等)\n"
|
||||
"\n"
|
||||
"拒答時:在回應 JSON 中設置 status=\"refused\"。"
|
||||
"拒答時:\n"
|
||||
"- 設置 status=\"refused\"\n"
|
||||
"- 設置 sign_level=\"下下签\"\n"
|
||||
"- 設置 answer 為拒答原因簡述(使用繁體中文)\n"
|
||||
"- conclusion、focus_points、advice、keywords 留空列表"
|
||||
),
|
||||
"zh-CN": (
|
||||
"【范围检查 - 以下情况请拒绝:】\n"
|
||||
@@ -58,7 +68,11 @@ _SCOPE_INSTRUCTIONS = {
|
||||
"2. 请求塔罗、八字、紫微、星座等其他方法\n"
|
||||
"3. 问题与占卜无关(编程、天气等)\n"
|
||||
"\n"
|
||||
"拒答时:在响应 JSON 中设置 status=\"refused\"。"
|
||||
"拒答时:\n"
|
||||
"- 设置 status=\"refused\"\n"
|
||||
"- 设置 sign_level=\"下下签\"\n"
|
||||
"- 设置 answer 为拒答原因简述(使用简体中文)\n"
|
||||
"- conclusion、focus_points、advice、keywords 留空列表"
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
@@ -207,10 +207,10 @@ You must explicitly output your reasoning in the following order:
|
||||
- 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.
|
||||
Write in natural, idiomatic American English. Speak like a wise, plain-spoken advisor — not a mystical fortune teller.
|
||||
Do not use fortune-cookie language, vague metaphors, or poetic mysticism.
|
||||
Every explanation must be grounded in the hexagram data, but expressed in terms an ordinary American would understand.
|
||||
Your goal is to make ancient Chinese wisdom accessible and useful to a modern Western reader, not to sound esoteric or profound.
|
||||
|
||||
[Sign-Level Reference Anchoring]
|
||||
Sign-level assessment should integrate hexagram backdrop and movement/change auspiciousness, referencing the following principles:
|
||||
@@ -224,30 +224,112 @@ When the hexagram shows mixed auspicious and inauspicious signs, weigh "hexagram
|
||||
|
||||
_WORKER_OUTPUT_RULES_ZH_CN = """\
|
||||
按输出要求严格返回对应的json对象。
|
||||
conclusion:必须结合本卦变卦与关键爻位,不可空谈,至少给出2-4条关键依据。
|
||||
focus_points:本次解读的核心关注点列表,每项为简短陈述,3-5项适中,应从卦象关键信息中提炼。
|
||||
advice:必须逐条对应卦象依据(哪一爻、何种生克冲合旺衰),给出可执行动作,优先回答:最该防什么、最该做什么、何时可推进、何时应暂缓。
|
||||
keywords:中文优先四字,必须来自本次卦象核心判断。
|
||||
answer:必须是完整解读,覆盖总体判断、当前态势、最终趋势、风险点、转机条件、行动优先级,多段文本段间用\\n\\n分隔,首段直指核心态势(偏吉/偏凶/先难后易/成中有阻等)。
|
||||
conclusion:分条输出(至少3条),用语朴实直白,像一个正常人在说话。
|
||||
第1条:先说明这是什么卦,这个卦一般代表什么含义。用白话解释,不要引用古文或卦辞。
|
||||
第2条:结合卦象变动,说明可能有哪些变数和关键影响因素。
|
||||
后续条:给出一个正常人可以听懂的结论,如这件事能不能成、有利因素是什么、风险在哪里。可以用"可能""大概率""倾向于""暂时不太理想"等词汇下判断,不要模棱两可。
|
||||
focus_points:断卦要点,告诉读者这个卦具体该怎么看。每项一句话,3-5项。允许使用专业术语(如用神、忌神、世应、动爻、月建、日辰、旺衰、生克冲合等),但每条要点必须既有专业判断也有通俗结论。例如:"用神妻财旺相持世,财运根基稳固""忌神兄弟动而受制,竞争阻碍可化解""变爻回头生世,最终趋势向好"。不要写成建议或泛泛的分析。
|
||||
advice:行动建议+卦象原因。每条建议先给出最直白的行动指引(该做什么、怎么做),然后简要附上卦象依据说明为什么这样建议。每条至少2-3句话,要详实具体。覆盖方向:最该做什么、最该防什么、合适时机、何时暂缓。参考写法:"先从自己家里或办公室开始找,重点检查沙发缝隙和抽屉底部,因为变卦父母子水为世爻,暗示钱可能被覆盖物盖住或掉进角落。""回忆一下丢钱前接触过哪些人,尤其是同事或家人,因为兄弟爻主同辈,可能有人无意中捡到但还没还,可以委婉问一圈。"
|
||||
keywords:中文四字词语,用词通俗、生活化,符合正常人的阅读理解。例如"可以推进""时机未到""注意风险""贵人相助"等。不要用玄奥冷僻的词。
|
||||
answer:具体解析,必须用分点形式输出(和conclusion、advice一样分条)。这里可以引入对卦象爻位的深度解读,但要把专业概念用白话讲透,让没学过六爻的人也能看懂意思。不要云里雾里,不要大段古文引用。
|
||||
sign_level:枚举值固定,必须且只能填以下四个值之一:上上签/中上签/中下签/下下签。"""
|
||||
|
||||
_WORKER_OUTPUT_RULES_ZH_HANT = """\
|
||||
按輸出要求嚴格返回對應的json對象。
|
||||
conclusion:必須結合本卦變卦與關鍵爻位,不可空談,至少給出2-4條關鍵依據。
|
||||
focus_points:本次解讀的核心關注點列表,每項為簡短陳述,3-5項適中,應從卦象關鍵信息中提煉。
|
||||
advice:必須逐條對應卦象依據(哪一爻、何種生剋沖合旺衰),給出可執行動作,優先回答:最該防什麼、最該做什麼、何時可推進、何時應暫緩。
|
||||
keywords:繁體中文優先四字,必須來自本次卦象核心判斷。
|
||||
answer:必須是完整解讀,覆蓋總體判斷、當前態勢、最終趨勢、風險點、轉機條件、行動優先級,多段文本段間用\\n\\n分隔,首段直指核心態勢(偏吉/偏凶/先難後易/成中有阻等)。
|
||||
conclusion:分條輸出(至少3條),用語樸實直白,像一個正常人在說話。
|
||||
第1條:先說明這是什麼卦,這個卦一般代表什麼含義。用白話解釋,不要引用古文或卦辭。
|
||||
第2條:結合卦象變動,說明可能有哪些變數和關鍵影響因素。
|
||||
後續條:給出一個正常人可以聽懂的結論,如這件事能不能成、有利因素是什麼、風險在哪裡。可以用"可能""大概率""傾向於""暫時不太理想"等詞彙下判斷,不要模稜兩可。
|
||||
focus_points:斷卦要點,告訴讀者這個卦具體該怎麼看。每項一句話,3-5項。允許使用專業術語(如用神、忌神、世應、動爻、月建、日辰、旺衰、生剋沖合等),但每條要點必須既有專業判斷也有通俗結論。例如:"用神妻財旺相持世,財運根基穩固""忌神兄弟動而受制,競爭阻礙可化解""變爻回頭生世,最終趨勢向好"。不要寫成建議或泛泛的分析。
|
||||
advice:行動建議+卦象原因。每條建議先給出最直白的行動指引(該做什麼、怎麼做),然後簡要附上卦象依據說明為什麼這樣建議。每條至少2-3句話,要詳實具體。覆蓋方向:最該做什麼、最該防什麼、合適時機、何時暫緩。參考寫法:"先從自己家裡或辦公室開始找,重點檢查沙發縫隙和抽屜底部,因為變卦父母子水為世爻,暗示錢可能被覆蓋物蓋住或掉進角落。""回憶一下丟錢前接觸過哪些人,尤其是同事或家人,因為兄弟爻主同輩,可能有人無意中撿到但還沒還,可以委婉問一圈。"
|
||||
keywords:中文四字詞語,用詞通俗、生活化,符合正常人的閱讀理解。例如"可以推進""時機未到""注意風險""貴人相助"等。不要用玄奧冷僻的詞。
|
||||
answer:具體解析,必須用分點形式輸出(和conclusion、advice一樣分條)。這裡可以引入對卦象爻位的深度解讀,但要把專業概念用白話講透,讓沒學過六爻的人也能看懂意思。不要雲裡霧裡,不要大段古文引用。
|
||||
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 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."""
|
||||
|
||||
═══════════════════════════════════════════════════════════════════════════════
|
||||
CRITICAL: WRITE FOR ORDINARY AMERICAN READERS
|
||||
═══════════════════════════════════════════════════════════════════════════════
|
||||
- This is a divination app for everyday people, not scholars of Chinese metaphysics.
|
||||
- Write in natural, idiomatic American English. Every sentence must make sense to someone who knows nothing about I Ching, Five Elements, or Chinese philosophy.
|
||||
- DO NOT use Chinese characters anywhere except:
|
||||
(a) the sign_level enum values (上上签 / 中上签 / 中下签 / 下下签)
|
||||
(b) the hexagram name when first mentioned (e.g. "Hexagram 53 - 风山渐 (Gradual Progress)")
|
||||
- NEVER write Chinese in parentheses to explain terms. Instead of "Strong(旺)", just say "strong" or explain "the element 旺, meaning strong or vigorous in this context".
|
||||
- NEVER transliterate Chinese terms. Write concepts directly in English.
|
||||
- Minimize ALL Chinese characters in your output. If it's not a sign_level or hexagram name, it should be English.
|
||||
═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
conclusion: A numbered list (at least 3 items), written in plain, conversational English.
|
||||
Item 1: Name the hexagram and explain in everyday language what it generally suggests — like you are explaining it to a friend over coffee.
|
||||
Item 2: Describe what key dynamics or possible changes the reading reveals, using ordinary concepts anyone can follow.
|
||||
Item 3+: Give a clear, honest assessment an ordinary person can understand. Is the situation likely to work out? What could change things? What should they watch for? Use natural qualifiers like "likely", "looks promising", "may be difficult", "there is a real chance", "the odds favor", etc. Do not sound mystical, absolute, or like a fortune cookie.
|
||||
|
||||
focus_points: Judgment points — tell the reader specifically what the key interpretive elements are in this hexagram reading. 3-5 items, each one sentence. Use professional hexagram terms from the detail chart (six relations like Officer/Wealth/Parent/Children/Sibling, Self/Response, moving lines, Month Branch, Day Branch, prosperity/weakness, clash/union, generation/restriction). Each point must combine a technical observation with a plain-language conclusion. Examples: "The Wealth line is strong at the Self position — your financial foundation is solid." "The Sibling line moves but is constrained — competition will fade." "A changing line turns back to support the Self line — the overall outcome leans positive." Do NOT write these as advice or vague analysis.
|
||||
|
||||
advice: PURE action advice — tell the user what to DO, with ZERO hexagram content. Foreign users cannot understand Liu Yao terminology, so give only plain, direct life advice. EVERY ONE of the following is STRICTLY FORBIDDEN in advice:
|
||||
- Any hexagram terminology: Officer, Wealth, Parent, Children, Sibling, Self line, Response line, moving line, changing line, line positions, Month Branch, Day Branch, Five Elements, prosperity/weakness, clash/union, generation/restriction, void, hidden, etc.
|
||||
- Any hexagram-referencing phrases: "this hexagram shows/suggests/indicates...", "according to the lines...", "based on the reading...", "because the X line is...", "the hexagram type suggests..."
|
||||
- Any Chinese characters, pinyin, or transliterated terms.
|
||||
CORRECT example (pure advice, what we want): "Start by searching your home thoroughly — focus on low places, under furniture, and inside drawers. Ask family members or roommates if they've seen it. If nothing turns up right away, wait a day and check the same spots again, as things can surface where you least expect."
|
||||
WRONG example (mixed with hexagram, what we must avoid): "Since the Wealth line is changing and moving toward the Self line, you should search around water sources or places covered by papers. The Sibling line suggests asking colleagues."
|
||||
Each piece of advice must be at least 2-3 detailed, concrete sentences. Cover: what to actually do, what to watch out for, good timing, when to hold back.
|
||||
|
||||
keywords: 2-4 plain English phrases (3-6 words each), using everyday vocabulary. They should feel like natural takeaways, such as "A good time to act", "Watch your spending", "Help is coming", "Wait and see". Nothing mystical or jargon-like.
|
||||
|
||||
answer: A detailed breakdown in numbered sections (same format as conclusion and advice). Go deeper into the reading here, explaining the hexagram patterns and what they mean. CRITICAL RULE: if you use any technical concept (e.g. a line is "strong" or "weak", a clash, a hidden influence), you MUST immediately explain what that concept means in plain English. The reading should feel insightful and clear, not obscure. Write for someone curious who genuinely wants to understand, not for a fellow practitioner of Chinese metaphysics.
|
||||
|
||||
sign_level: Must be exactly one of: 上上签 / 中上签 / 中下签 / 下下签. Always use these Chinese enum values."""
|
||||
|
||||
_FOLLOW_UP_ROLE_PLAYING_ZH = """\
|
||||
你是一名六爻解卦师,正在回答用户对上一轮解卦结果的追问。用户已经收到过完整的卦象解读,现在对这个结果有后续疑问。这是唯一的一轮追问,你必须给出明确的最终回答。
|
||||
|
||||
【核心原则】
|
||||
- 直接回答用户的追问,给出明确结论,不要模棱两可。
|
||||
- 可以引用上一轮的卦象信息来辅助回答,但不要复述整个卦象解读。
|
||||
- 不要反问用户,不要以"你想了解哪方面?"等方式回复——这是唯一一次追问机会,直接给出你的判断。
|
||||
- 保持专业但平实的语气,不要故弄玄虚。
|
||||
"""
|
||||
|
||||
_FOLLOW_UP_ROLE_PLAYING_ZH_HANT = """\
|
||||
你是一名六爻解卦師,正在回答使用者對上一輪解卦結果的追問。使用者已經收到過完整的卦象解讀,現在對這個結果有後續疑問。這是唯一的一輪追問,你必須給出明確的最終回答。
|
||||
|
||||
【核心原則】
|
||||
- 直接回答使用者的追問,給出明確結論,不要模稜兩可。
|
||||
- 可以引用上一輪的卦象資訊來輔助回答,但不要複述整個卦象解讀。
|
||||
- 不要反問使用者,不要以"你想了解哪方面?"等方式回覆——這是唯一一次追問機會,直接給出你的判斷。
|
||||
- 保持專業但平實的語氣,不要故弄玄虛。
|
||||
"""
|
||||
|
||||
_FOLLOW_UP_ROLE_PLAYING_EN = """\
|
||||
You are a Liu Yao divination advisor responding to a follow-up question. The user has already received a complete hexagram reading. They now have a follow-up question about that reading or its implications. This is the ONLY round of follow-up — you MUST give a definitive, final answer.
|
||||
|
||||
[Core Principles]
|
||||
- Answer the user's follow-up question directly and give a clear, unambiguous conclusion.
|
||||
- You may reference the previous reading when helpful, but do NOT repeat the entire hexagram analysis.
|
||||
- Do NOT ask the user follow-up questions or say things like "what aspect would you like to know more about?" — this is their only follow-up chance, so give your judgment directly.
|
||||
- Keep your tone professional but plain-spoken. Write in natural American English. Do not sound mystical.
|
||||
"""
|
||||
|
||||
_FOLLOW_UP_OUTPUT_RULES_ZH_CN = """\
|
||||
按输出要求返回对应的json对象。这是追问模式,只需要填 status 和 answer。
|
||||
answer:直接回答用户的追问,给出明确的最终判断。不要重新做一遍完整解卦。不要反问用户。重点回答用户问什么,篇幅控制在必要范围内。
|
||||
status:正常回答填"success",拒绝回答填"refused"。
|
||||
"""
|
||||
|
||||
_FOLLOW_UP_OUTPUT_RULES_ZH_HANT = """\
|
||||
按輸出要求返回對應的json對象。這是追問模式,只需要填 status 和 answer。
|
||||
answer:直接回答使用者的追問,給出明確的最終判斷。不要重新做一遍完整解卦。不要反問使用者。重點回答使用者問什麼,篇幅控制在必要範圍內。
|
||||
status:正常回答填"success",拒絕回答填"refused"。
|
||||
"""
|
||||
|
||||
_FOLLOW_UP_OUTPUT_RULES_EN = """\
|
||||
Return the JSON object. This is follow-up mode — only status and answer are needed.
|
||||
answer: Answer the user's follow-up question directly with a clear, definitive judgment. Do NOT redo the full hexagram analysis. Do NOT ask the user follow-up questions. Focus on what the user asked, keep it concise but thorough.
|
||||
status: Use "success" for normal responses, "refused" if the question should not be answered.
|
||||
"""
|
||||
|
||||
|
||||
def get_worker_role_playing(language: str) -> str:
|
||||
@@ -264,3 +346,19 @@ def get_worker_output_rules(language: str) -> str:
|
||||
if language.startswith("zh-Hant") or language.startswith("zh_Hant"):
|
||||
return _WORKER_OUTPUT_RULES_ZH_HANT
|
||||
return _WORKER_OUTPUT_RULES_ZH_CN
|
||||
|
||||
|
||||
def get_follow_up_role_playing(language: str) -> str:
|
||||
if language.startswith("en"):
|
||||
return _FOLLOW_UP_ROLE_PLAYING_EN
|
||||
if language.startswith("zh-Hant") or language.startswith("zh_Hant"):
|
||||
return _FOLLOW_UP_ROLE_PLAYING_ZH_HANT
|
||||
return _FOLLOW_UP_ROLE_PLAYING_ZH
|
||||
|
||||
|
||||
def get_follow_up_output_rules(language: str) -> str:
|
||||
if language.startswith("en"):
|
||||
return _FOLLOW_UP_OUTPUT_RULES_EN
|
||||
if language.startswith("zh-Hant") or language.startswith("zh_Hant"):
|
||||
return _FOLLOW_UP_OUTPUT_RULES_ZH_HANT
|
||||
return _FOLLOW_UP_OUTPUT_RULES_ZH_CN
|
||||
|
||||
@@ -46,7 +46,7 @@ class JsonReActAgent(ReActAgent):
|
||||
*,
|
||||
output_model: type[BaseModel],
|
||||
) -> dict[str, Any]:
|
||||
_, payload = await finalize_json_response(
|
||||
_, result = await finalize_json_response(
|
||||
model=self.model,
|
||||
formatter=self.formatter,
|
||||
base_messages=[
|
||||
@@ -56,4 +56,4 @@ class JsonReActAgent(ReActAgent):
|
||||
output_model=output_model,
|
||||
retries=self._finalize_retries,
|
||||
)
|
||||
return payload
|
||||
return result.model_dump(mode="json", by_alias=True)
|
||||
|
||||
@@ -55,7 +55,7 @@ from core.agentscope.runtime.protocols import PipelineLike
|
||||
@dataclass(frozen=True)
|
||||
class StageExecutionResult:
|
||||
message: Msg
|
||||
payload: dict[str, Any]
|
||||
validated_output: WorkerAgentOutputLite | FollowUpOutput
|
||||
response_metadata: dict[str, Any]
|
||||
|
||||
|
||||
@@ -242,7 +242,7 @@ class AgentScopeRunner:
|
||||
derived_divination=derived_divination,
|
||||
language=language,
|
||||
)
|
||||
worker_output = worker_output_model.model_validate(worker_result.payload)
|
||||
worker_output = worker_result.validated_output
|
||||
await self._emit_step_event(
|
||||
pipeline=pipeline,
|
||||
run_input=run_input,
|
||||
@@ -284,9 +284,10 @@ class AgentScopeRunner:
|
||||
llm_config=stage_config.llm_config,
|
||||
tools=None,
|
||||
now_utc=datetime.now(timezone.utc),
|
||||
runtime_mode=runtime_mode.value,
|
||||
)
|
||||
|
||||
_, worker_payload_raw = await finalize_json_response(
|
||||
_, worker_payload = await finalize_json_response(
|
||||
model=tracking_model,
|
||||
formatter=formatter,
|
||||
base_messages=[Msg("system", system_prompt, "system"), *input_messages],
|
||||
@@ -294,7 +295,6 @@ class AgentScopeRunner:
|
||||
retries=2,
|
||||
language=language,
|
||||
)
|
||||
worker_payload = worker_output_model.model_validate(worker_payload_raw)
|
||||
response_metadata = self._llm_pricing_service.build_usage_metadata(
|
||||
model=stage_config.model_code,
|
||||
usage_summary=tracking_model.usage_summary(),
|
||||
@@ -312,9 +312,8 @@ class AgentScopeRunner:
|
||||
name=stage_config.agent_type.value,
|
||||
role="assistant",
|
||||
content=worker_payload.answer,
|
||||
metadata=worker_payload.model_dump(mode="json", exclude_none=True),
|
||||
),
|
||||
payload=worker_payload.model_dump(mode="json", exclude_none=True),
|
||||
validated_output=worker_payload,
|
||||
response_metadata=response_metadata,
|
||||
)
|
||||
|
||||
@@ -336,7 +335,7 @@ class AgentScopeRunner:
|
||||
question=raw_user_text, language=language
|
||||
)
|
||||
|
||||
if derived_divination is not None and context_messages:
|
||||
if context_messages:
|
||||
last = context_messages[-1]
|
||||
if last.role == "user":
|
||||
context_messages[-1] = Msg(
|
||||
@@ -346,11 +345,6 @@ class AgentScopeRunner:
|
||||
)
|
||||
return context_messages
|
||||
|
||||
if context_messages:
|
||||
last = context_messages[-1]
|
||||
if last.role == "user":
|
||||
return context_messages
|
||||
|
||||
user_msg = Msg(name="user", role="user", content=user_text)
|
||||
return [*context_messages, user_msg]
|
||||
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
from collections.abc import Awaitable
|
||||
from typing import Any, Protocol
|
||||
from typing import Any, Protocol, TypeVar
|
||||
|
||||
from core.agentscope.utils.parsing import extract_text_content, parse_json_dict
|
||||
from pydantic import BaseModel, ValidationError
|
||||
@@ -14,6 +13,8 @@ from core.logging import get_logger
|
||||
|
||||
logger = get_logger("core.agentscope.utils.json_finalize")
|
||||
|
||||
T = TypeVar("T", bound=BaseModel)
|
||||
|
||||
|
||||
class FormatterProtocol(Protocol):
|
||||
def format(self, *args: Any, **kwargs: Any) -> Awaitable[Any]: ...
|
||||
@@ -41,13 +42,13 @@ def build_json_finalize_instruction(
|
||||
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.
|
||||
════════════════════════════════════════════════════════════════════════════════
|
||||
ENGLISH OUTPUT REQUIRED - MINIMIZE CHINESE CHARACTERS
|
||||
════════════════════════════════════════════════════════════════════════════════
|
||||
Return JSON only. All string values (except sign_level and hexagram name) must be in English.
|
||||
Write in natural American English for ordinary readers.
|
||||
The sign_level MUST be one of: 上上签 / 中上签 / 中下签 / 下下签
|
||||
═══════════════════════════════════════════════════════════════════════════════"""
|
||||
════════════════════════════════════════════════════════════════════════════════"""
|
||||
elif language.startswith("zh-Hant"):
|
||||
language_part = """
|
||||
|
||||
@@ -73,10 +74,10 @@ async def finalize_json_response(
|
||||
model: Any,
|
||||
formatter: FormatterProtocol,
|
||||
base_messages: list[Msg],
|
||||
output_model: type[BaseModel],
|
||||
output_model: type[T],
|
||||
retries: int,
|
||||
language: str | None = None,
|
||||
) -> tuple[Any, dict[str, Any]]:
|
||||
) -> tuple[Any, T]:
|
||||
schema_json = json.dumps(
|
||||
output_model.model_json_schema(),
|
||||
ensure_ascii=True,
|
||||
@@ -119,59 +120,10 @@ async def finalize_json_response(
|
||||
|
||||
try:
|
||||
validated = output_model.model_validate(payload)
|
||||
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
|
||||
return response, validated
|
||||
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 "<root>"]
|
||||
return []
|
||||
|
||||
@@ -6,8 +6,9 @@ from schemas.agent.forwarded_props import (
|
||||
)
|
||||
from schemas.agent.forwarded_props import RuntimeMode
|
||||
from schemas.agent.runtime_models import (
|
||||
AgentOutput,
|
||||
PersistedAgentOutput,
|
||||
RunStatus,
|
||||
SignLevel,
|
||||
ToolAgentOutput,
|
||||
ToolStatus,
|
||||
WorkerAgentOutputLite,
|
||||
@@ -18,11 +19,12 @@ from schemas.agent.visibility import SystemVisibilityBit, VisibilityMask, bit_ma
|
||||
|
||||
__all__ = [
|
||||
"AgentType",
|
||||
"AgentOutput",
|
||||
"PersistedAgentOutput",
|
||||
"ForwardedPropsPayload",
|
||||
"ClientTimeContext",
|
||||
"RunStatus",
|
||||
"RuntimeMode",
|
||||
"SignLevel",
|
||||
"SystemAgentLLMConfig",
|
||||
"SystemVisibilityBit",
|
||||
"ToolAgentOutput",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from enum import Enum
|
||||
from typing import Any, Literal
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
@@ -20,6 +20,13 @@ class ToolStatus(str, Enum):
|
||||
PARTIAL = "partial"
|
||||
|
||||
|
||||
class SignLevel(str, Enum):
|
||||
BEST = "上上签"
|
||||
GOOD = "中上签"
|
||||
MODERATE = "中下签"
|
||||
WORST = "下下签"
|
||||
|
||||
|
||||
class ErrorInfo(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
@@ -43,13 +50,13 @@ class ToolAgentOutput(BaseModel):
|
||||
class WorkerAgentOutputLite(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
status: RunStatus = RunStatus.SUCCESS
|
||||
sign_level: Literal["上上签", "中上签", "中下签", "下下签"] | None = None
|
||||
status: RunStatus
|
||||
sign_level: SignLevel
|
||||
answer: str = Field(min_length=1, max_length=4000)
|
||||
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(default_factory=list, max_length=6)
|
||||
keywords: list[str] = Field(default_factory=list, max_length=8)
|
||||
answer: str = ""
|
||||
error: ErrorInfo | None = None
|
||||
|
||||
|
||||
@@ -61,14 +68,21 @@ class FollowUpOutput(BaseModel):
|
||||
error: ErrorInfo | None = None
|
||||
|
||||
|
||||
class AgentOutput(WorkerAgentOutputLite):
|
||||
class PersistedAgentOutput(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
status: RunStatus
|
||||
sign_level: SignLevel
|
||||
answer: str = Field(min_length=1, max_length=4000)
|
||||
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(default_factory=list, max_length=6)
|
||||
keywords: list[str] = Field(default_factory=list, max_length=8)
|
||||
error: ErrorInfo | None = None
|
||||
divination_derived: DerivedDivinationData | None = None
|
||||
|
||||
|
||||
WorkerAgentOutput = WorkerAgentOutputLite
|
||||
RuntimeAgentOutput = AgentOutput | FollowUpOutput
|
||||
RuntimeAgentOutput = PersistedAgentOutput | FollowUpOutput
|
||||
|
||||
|
||||
def resolve_worker_output_model(
|
||||
|
||||
@@ -6,7 +6,7 @@ from typing import Any, ClassVar
|
||||
from uuid import UUID
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
from schemas.agent.runtime_models import AgentOutput, FollowUpOutput
|
||||
from schemas.agent.runtime_models import FollowUpOutput, PersistedAgentOutput
|
||||
|
||||
from ..agent import AgentType, ToolAgentOutput
|
||||
|
||||
@@ -25,7 +25,7 @@ class AgentChatMessageMetadata(BaseModel):
|
||||
agent_type: AgentType | None = None
|
||||
user_message_attachments: list[UserMessageAttachment] | None = None
|
||||
tool_agent_output: ToolAgentOutput | None = None
|
||||
agent_output: AgentOutput | FollowUpOutput | None = None
|
||||
agent_output: PersistedAgentOutput | FollowUpOutput | None = None
|
||||
|
||||
|
||||
class AgentChatMessage(BaseModel):
|
||||
|
||||
@@ -7,7 +7,7 @@ from uuid import UUID
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
from schemas.agent.runtime_models import ErrorInfo
|
||||
from schemas.agent.runtime_models import ErrorInfo, RunStatus, SignLevel
|
||||
from schemas.domain.chat_message import AgentChatMessage
|
||||
from schemas.domain.divination import DerivedDivinationData
|
||||
|
||||
@@ -220,8 +220,8 @@ class HistoryMessage(BaseModel):
|
||||
class HistoryAgentOutput(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
status: Literal["success", "failed"] | None = None
|
||||
sign_level: Literal["上上签", "中上签", "中下签", "下下签"] | None = None
|
||||
status: RunStatus | None = None
|
||||
sign_level: SignLevel | None = None
|
||||
conclusion: list[str] = Field(default_factory=list)
|
||||
focus_points: list[str] = Field(default_factory=list)
|
||||
advice: list[str] = Field(default_factory=list)
|
||||
|
||||
Reference in New Issue
Block a user