92 lines
3.8 KiB
Python
92 lines
3.8 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
import urllib.error
|
|
import urllib.request
|
|
from collections.abc import Iterator
|
|
from typing import Any
|
|
|
|
|
|
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
|
|
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 {}
|
|
delta = choice.get("delta") or {}
|
|
content = delta.get("content")
|
|
if content is None:
|
|
content = (choice.get("message") or {}).get("content")
|
|
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()
|