chore: create Multica handoff checkpoint
This commit is contained in:
@@ -15,6 +15,11 @@ class MentorAgentError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
FOLLOW_UP_START = "<XIAOBAI_FOLLOW_UPS>"
|
||||
FOLLOW_UP_END = "</XIAOBAI_FOLLOW_UPS>"
|
||||
MAX_FOLLOW_UP_LENGTH = 80
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class MentorSkill:
|
||||
skill_id: str
|
||||
@@ -185,6 +190,8 @@ def stream_with_mentor(
|
||||
base_url: str,
|
||||
model: str,
|
||||
timeout: int = 90,
|
||||
*,
|
||||
follow_ups: list[str] | None = None,
|
||||
) -> Iterator[str]:
|
||||
if not api_key or not model:
|
||||
raise MentorAgentError("LLM API Key 或模型尚未配置。")
|
||||
@@ -193,8 +200,10 @@ def stream_with_mentor(
|
||||
messages = [{"role": "system", "content": system_prompt}]
|
||||
messages.extend(history[-10:])
|
||||
messages.append({"role": "user", "content": question})
|
||||
if follow_ups is not None:
|
||||
follow_ups.clear()
|
||||
try:
|
||||
yield from llm_transport.stream_chat_completion(
|
||||
upstream = llm_transport.stream_chat_completion(
|
||||
api_key=api_key,
|
||||
base_url=base_url,
|
||||
model=model,
|
||||
@@ -202,6 +211,7 @@ def stream_with_mentor(
|
||||
timeout=timeout,
|
||||
user_agent="XiaobaiReviewWeb/0.6",
|
||||
)
|
||||
yield from _stream_answer_and_collect_follow_ups(upstream, follow_ups)
|
||||
except llm_transport.OpenAIEmptyResponseError as exc:
|
||||
raise MentorAgentError("问师模型未返回有效内容。") from exc
|
||||
except llm_transport.OpenAIHTTPError as exc:
|
||||
@@ -223,6 +233,10 @@ def _build_system_prompt(skill: MentorSkill, market_context: dict[str, Any]) ->
|
||||
5. 优先回答用户真正的问题。市场分析通常按“判断、数据依据、思维模型下的应对、失效条件”组织;纯交易心理或方法问题可以自然回答,不强制套模板。
|
||||
6. 保留该 Skill 的核心心智模型和表达节奏,但不要复述身份履历,不要宣称自己就是真人,不攻击或贬低用户。
|
||||
7. 使用中文,信息密度高,避免空泛口号。引用数字时标明数据日期。
|
||||
8. 正文结束后必须输出2至3条与本轮问题和正文直接相关的追问。追问用于帮助用户继续核实条件、风险或失效边界,不得引入正文没有依据的新事实,不得给出无条件买卖指令。严格使用以下机器结构,不要放进Markdown代码块,结束标签后不要再输出文字:
|
||||
<XIAOBAI_FOLLOW_UPS>
|
||||
["追问一?","追问二?","追问三?"]
|
||||
</XIAOBAI_FOLLOW_UPS>
|
||||
|
||||
网页市场数据:
|
||||
{context_json}
|
||||
@@ -233,6 +247,67 @@ def _build_system_prompt(skill: MentorSkill, market_context: dict[str, Any]) ->
|
||||
""".strip()
|
||||
|
||||
|
||||
def _stream_answer_and_collect_follow_ups(
|
||||
chunks: Iterator[str], follow_ups: list[str] | None
|
||||
) -> Iterator[str]:
|
||||
buffer = ""
|
||||
collecting = False
|
||||
for raw_chunk in chunks:
|
||||
chunk = str(raw_chunk or "")
|
||||
if not chunk:
|
||||
continue
|
||||
buffer += chunk
|
||||
if collecting:
|
||||
continue
|
||||
marker_index = buffer.find(FOLLOW_UP_START)
|
||||
if marker_index >= 0:
|
||||
if marker_index:
|
||||
yield buffer[:marker_index]
|
||||
buffer = buffer[marker_index + len(FOLLOW_UP_START):]
|
||||
collecting = True
|
||||
continue
|
||||
overlap = _marker_prefix_overlap(buffer, FOLLOW_UP_START)
|
||||
emit_length = len(buffer) - overlap
|
||||
if emit_length:
|
||||
yield buffer[:emit_length]
|
||||
buffer = buffer[emit_length:]
|
||||
|
||||
if not collecting:
|
||||
if buffer:
|
||||
yield buffer
|
||||
return
|
||||
raw_follow_ups = buffer.split(FOLLOW_UP_END, 1)[0].strip()
|
||||
parsed = _parse_follow_ups(raw_follow_ups)
|
||||
if follow_ups is not None and len(parsed) >= 2:
|
||||
follow_ups.extend(parsed)
|
||||
|
||||
|
||||
def _marker_prefix_overlap(value: str, marker: str) -> int:
|
||||
max_length = min(len(value), len(marker) - 1)
|
||||
for length in range(max_length, 0, -1):
|
||||
if value.endswith(marker[:length]):
|
||||
return length
|
||||
return 0
|
||||
|
||||
|
||||
def _parse_follow_ups(payload: str) -> list[str]:
|
||||
try:
|
||||
values = json.loads(payload)
|
||||
except (TypeError, json.JSONDecodeError):
|
||||
return []
|
||||
if not isinstance(values, list):
|
||||
return []
|
||||
result: list[str] = []
|
||||
for value in values:
|
||||
question = re.sub(r"\s+", " ", str(value or "")).strip()
|
||||
if not question or len(question) > MAX_FOLLOW_UP_LENGTH or question in result:
|
||||
continue
|
||||
result.append(question)
|
||||
if len(result) == 3:
|
||||
break
|
||||
return result
|
||||
|
||||
|
||||
def _parse_frontmatter(content: str) -> dict[str, str]:
|
||||
if not content.startswith("---"):
|
||||
return {}
|
||||
|
||||
@@ -129,9 +129,10 @@ class MentorServiceMixin:
|
||||
|
||||
def generate():
|
||||
answer_parts: list[str] = []
|
||||
follow_ups: list[str] = []
|
||||
events = self.llm_gateway.stream(
|
||||
"mentor",
|
||||
f"mentor-skill-v1:{skill.skill_id}",
|
||||
f"mentor-skill-v2:{skill.skill_id}",
|
||||
lambda profile: stream_with_mentor(
|
||||
skill,
|
||||
context,
|
||||
@@ -140,6 +141,7 @@ class MentorServiceMixin:
|
||||
profile.api_key,
|
||||
profile.base_url,
|
||||
profile.model,
|
||||
follow_ups=follow_ups,
|
||||
),
|
||||
(MentorAgentError,),
|
||||
)
|
||||
@@ -160,6 +162,7 @@ class MentorServiceMixin:
|
||||
yield {
|
||||
"type": "meta",
|
||||
"data_trade_date": context["data_trade_date"],
|
||||
"follow_ups": follow_ups or self._mentor_follow_up_fallback(question),
|
||||
"notice": "智能解读已自动切换可用服务。"
|
||||
if event.role == "fallback"
|
||||
else "",
|
||||
@@ -167,6 +170,27 @@ class MentorServiceMixin:
|
||||
|
||||
return generate()
|
||||
|
||||
@staticmethod
|
||||
def _mentor_follow_up_fallback(question: str) -> list[str]:
|
||||
normalized = question.strip()
|
||||
if any(keyword in normalized for keyword in ("风险", "亏损", "回撤", "止损")):
|
||||
return [
|
||||
"这些风险最早会从哪些信号中暴露?",
|
||||
"哪些变化会让当前风险判断失效?",
|
||||
"如果风险继续扩大,仓位预案应如何调整?",
|
||||
]
|
||||
if any(keyword in normalized for keyword in ("股票", "个股", "代码", "怎么看")):
|
||||
return [
|
||||
"这个判断最关键的确认信号是什么?",
|
||||
"哪些变化会让当前结论失效?",
|
||||
"明日盘中应该优先观察哪些数据?",
|
||||
]
|
||||
return [
|
||||
"这个判断最关键的确认依据是什么?",
|
||||
"哪些变化会让当前结论失效?",
|
||||
"下一步应该优先观察什么?",
|
||||
]
|
||||
|
||||
def mentor_messages(self, mentor_id: str, trade_date: str) -> list[dict[str, Any]]:
|
||||
mentor_id = validate_text(mentor_id, "问师角色", 100, required=True)
|
||||
trade_date = normalize_date(trade_date)
|
||||
|
||||
Reference in New Issue
Block a user