from __future__ import annotations import json import urllib.error import urllib.request from collections.abc import Iterator from typing import Any from llm_stream import OpenAIStreamAccumulator class ReviewAssistantError(RuntimeError): pass def stream_review_assistant( context: dict[str, Any], question: str, history: list[dict[str, str]], api_key: str, base_url: str, model: str, timeout: int = 120, ) -> Iterator[str]: if not api_key or not model: raise ReviewAssistantError("智能解读服务尚未配置。") messages = [{"role": "system", "content": _system_prompt(context)}] messages.extend(history[-12:]) messages.append({"role": "user", "content": question}) request = urllib.request.Request( f"{base_url.rstrip('/')}/chat/completions", data=json.dumps( {"model": model, "messages": messages, "stream": True}, ensure_ascii=False ).encode("utf-8"), headers={ "Content-Type": "application/json", "Authorization": f"Bearer {api_key}", "User-Agent": "XiaobaiReviewWeb/1.0", "Accept": "text/event-stream", }, method="POST", ) try: with urllib.request.urlopen(request, timeout=timeout) as response: yielded = False accumulator = OpenAIStreamAccumulator() for raw_line in response: line = raw_line.decode("utf-8", errors="replace").strip() if not line or line.startswith(":"): continue if line.startswith("data:"): line = line[5:].strip() if line == "[DONE]": break try: payload = json.loads(line) except json.JSONDecodeError: continue choices = payload.get("choices") or [] if not choices: continue choice = choices[0] or {} content = accumulator.feed(choice) if content: yielded = True yield str(content) if not yielded: raise ReviewAssistantError("智能解读未返回有效内容。") except urllib.error.HTTPError as exc: raise ReviewAssistantError(f"智能解读服务暂不可用({exc.code})。") from exc except (urllib.error.URLError, TimeoutError, OSError) as exc: raise ReviewAssistantError("智能解读连接中断,请稍后重试。") from exc def _system_prompt(context: dict[str, Any]) -> str: context_json = json.dumps(context, ensure_ascii=False, separators=(",", ":")) return f""" 你是“小白复盘”的统一复盘助手。你负责把网页中已经存在的市场统计、策略跟踪、提醒、复盘笔记和手工交易日志连接起来,帮助用户复盘和形成下一步观察计划。 最高优先级规则: 1. 只能使用下方“网页复盘数据”,数据缺失就明确说明,不得补造行情、交易或胜率。 2. 不自动下单,不声称已执行任何操作,不修改策略、提醒、笔记或交易日志。 3. 不承诺收益,不给无条件买卖指令。建议必须写成条件、失效条件和风险边界。 4. 区分市场事实、用户记录和你的推断。引用数字时写明数据日期。 5. 优先结合用户自己的策略跟踪与交易日志寻找可验证的重复模式;样本不足时明确标注。 6. 使用中文,先直接回答,再给数据依据和下一步观察。避免空泛口号,不展示模型、接口或内部工程信息。 7. 控制在 800 个中文字符以内,除非用户明确要求展开。 网页复盘数据: {context_json} """.strip()