feat: personalize and stream mentor chat
This commit is contained in:
@@ -50,7 +50,7 @@ from heaven_engine import (
|
||||
hexagram_from_lines,
|
||||
)
|
||||
from llm_strategy import LLMCompilerError, compile_strategy_with_llm, test_llm_connection
|
||||
from mentor_agent import MentorAgentError, MentorSkillRegistry, chat_with_mentor
|
||||
from mentor_agent import MentorAgentError, MentorSkillRegistry, stream_with_mentor
|
||||
from realtime_aggregator import WebRealtimeAggregator
|
||||
from screener import (
|
||||
FACTOR_FIELDS,
|
||||
@@ -1353,11 +1353,26 @@ class DashboardService:
|
||||
]
|
||||
if not mentors:
|
||||
raise ValueError("游资skills 目录中没有可用的 SKILL.md。")
|
||||
stored_preferences = self.database.list_mentor_preferences(self.current_user_id)
|
||||
preferences = {item["mentor_id"]: item for item in stored_preferences}
|
||||
for default_order, mentor in enumerate(mentors):
|
||||
preference = preferences.get(str(mentor.get("id") or ""), {})
|
||||
mentor["pinned"] = bool(preference.get("pinned"))
|
||||
mentor["sort_order"] = int(preference.get("sort_order", 10000 + default_order))
|
||||
mentors.sort(
|
||||
key=lambda item: (
|
||||
not bool(item.get("pinned")),
|
||||
int(item.get("sort_order") or 0),
|
||||
)
|
||||
)
|
||||
for sort_order, mentor in enumerate(mentors):
|
||||
mentor["sort_order"] = sort_order
|
||||
snapshot = self.database.get_snapshot(normalized_date)
|
||||
actual_date = str((snapshot or {}).get("meta", {}).get("trade_date") or normalized_date)
|
||||
return {
|
||||
"trade_date": actual_date,
|
||||
"mentors": mentors,
|
||||
"preferences_configured": bool(stored_preferences),
|
||||
"llm": {
|
||||
"configured": self.llm_configured,
|
||||
"model": self.llm_primary_model if self.llm_configured else "",
|
||||
@@ -1366,7 +1381,38 @@ class DashboardService:
|
||||
},
|
||||
}
|
||||
|
||||
def mentor_chat(self, payload: dict[str, Any]) -> dict[str, Any]:
|
||||
def save_mentor_preferences(self, payload: dict[str, Any]) -> dict[str, Any]:
|
||||
available_ids = [
|
||||
skill.skill_id
|
||||
for skill in self.mentor_skills.list_skills(
|
||||
include_private=self.membership()["is_admin"]
|
||||
)
|
||||
]
|
||||
available = set(available_ids)
|
||||
raw_order = payload.get("order")
|
||||
raw_pinned = payload.get("pinned")
|
||||
if not isinstance(raw_order, list) or not isinstance(raw_pinned, list):
|
||||
raise ValueError("问师排序格式不正确。")
|
||||
ordered_ids: list[str] = []
|
||||
for raw_id in raw_order:
|
||||
mentor_id = validate_text(raw_id, "问师角色", 100, required=True)
|
||||
if mentor_id not in available:
|
||||
raise ValueError("问师排序中包含不可用的思维模型。")
|
||||
if mentor_id not in ordered_ids:
|
||||
ordered_ids.append(mentor_id)
|
||||
ordered_ids.extend(mentor_id for mentor_id in available_ids if mentor_id not in ordered_ids)
|
||||
pinned_ids = {
|
||||
validate_text(raw_id, "问师角色", 100, required=True)
|
||||
for raw_id in raw_pinned
|
||||
}
|
||||
if not pinned_ids.issubset(available):
|
||||
raise ValueError("问师置顶中包含不可用的思维模型。")
|
||||
self.database.save_mentor_preferences(
|
||||
self.current_user_id, ordered_ids, pinned_ids
|
||||
)
|
||||
return {"saved": True}
|
||||
|
||||
def mentor_stream(self, payload: dict[str, Any]):
|
||||
mentor_id = validate_text(payload.get("mentor_id"), "问师角色", 100, required=True)
|
||||
question = validate_text(payload.get("question"), "问题", 2000, required=True)
|
||||
trade_date = normalize_date(str(payload.get("trade_date") or date.today().isoformat()))
|
||||
@@ -1377,66 +1423,73 @@ class DashboardService:
|
||||
context = self._build_mentor_context(trade_date, question)
|
||||
|
||||
source = self.ensure_llm_access("mentor")
|
||||
primary_error = ""
|
||||
result = None
|
||||
compiler = "primary"
|
||||
profiles = []
|
||||
if self.llm_configured:
|
||||
try:
|
||||
result = chat_with_mentor(
|
||||
skill,
|
||||
context,
|
||||
question,
|
||||
history,
|
||||
profiles.append(
|
||||
(
|
||||
"primary",
|
||||
self.llm_primary_api_key,
|
||||
self.llm_primary_base_url,
|
||||
self.llm_primary_model,
|
||||
)
|
||||
except MentorAgentError as exc:
|
||||
primary_error = str(exc)
|
||||
if result is None and self.llm_fallback_configured:
|
||||
try:
|
||||
result = chat_with_mentor(
|
||||
skill,
|
||||
context,
|
||||
question,
|
||||
history,
|
||||
)
|
||||
if self.llm_fallback_configured:
|
||||
profiles.append(
|
||||
(
|
||||
"fallback",
|
||||
self.llm_fallback_api_key,
|
||||
self.llm_fallback_base_url,
|
||||
self.llm_fallback_model,
|
||||
)
|
||||
compiler = "fallback"
|
||||
except MentorAgentError as exc:
|
||||
fallback_error = str(exc)
|
||||
self.record_llm_usage(
|
||||
"mentor", source, self.llm_fallback_model, "failed"
|
||||
)
|
||||
|
||||
def generate():
|
||||
started = time.perf_counter()
|
||||
last_error: Exception | None = None
|
||||
for compiler, api_key, base_url, model in profiles:
|
||||
try:
|
||||
upstream = iter(
|
||||
stream_with_mentor(
|
||||
skill, context, question, history, api_key, base_url, model
|
||||
)
|
||||
)
|
||||
first = next(upstream)
|
||||
except (MentorAgentError, StopIteration) as exc:
|
||||
last_error = exc
|
||||
continue
|
||||
answer_parts = [first]
|
||||
yield {"type": "delta", "content": first}
|
||||
try:
|
||||
for chunk in upstream:
|
||||
answer_parts.append(chunk)
|
||||
yield {"type": "delta", "content": chunk}
|
||||
except MentorAgentError as exc:
|
||||
self.record_llm_usage("mentor", source, model, "failed")
|
||||
raise ValueError("智能解读连接中断,请稍后重试。") from exc
|
||||
answer = "".join(answer_parts).strip()
|
||||
latency_ms = round((time.perf_counter() - started) * 1000)
|
||||
self.database.save_mentor_exchange(
|
||||
self.current_user_id,
|
||||
mentor_id,
|
||||
trade_date,
|
||||
question,
|
||||
answer,
|
||||
context["data_trade_date"],
|
||||
)
|
||||
raise ValueError("智能解读服务暂不可用,请稍后重试。") from exc
|
||||
if result is None:
|
||||
self.record_llm_usage("mentor", source, self.llm_primary_model, "failed")
|
||||
raise ValueError("智能解读服务暂不可用,请稍后重试。")
|
||||
self.record_llm_usage(
|
||||
"mentor",
|
||||
source,
|
||||
str(result.get("model") or ""),
|
||||
"success",
|
||||
int(result.get("latency_ms") or 0),
|
||||
)
|
||||
self.database.save_mentor_exchange(
|
||||
self.current_user_id,
|
||||
mentor_id,
|
||||
trade_date,
|
||||
question,
|
||||
str(result.get("answer") or ""),
|
||||
context["data_trade_date"],
|
||||
)
|
||||
return {
|
||||
**result,
|
||||
"mentor": skill.public(),
|
||||
"compiler": compiler,
|
||||
"requested_trade_date": trade_date,
|
||||
"data_trade_date": context["data_trade_date"],
|
||||
"notice": "智能解读已自动切换可用服务。" if compiler == "fallback" else "",
|
||||
}
|
||||
self.record_llm_usage("mentor", source, model, "success", latency_ms)
|
||||
yield {
|
||||
"type": "meta",
|
||||
"data_trade_date": context["data_trade_date"],
|
||||
"notice": "智能解读已自动切换可用服务。"
|
||||
if compiler == "fallback"
|
||||
else "",
|
||||
}
|
||||
return
|
||||
failed_model = profiles[-1][3] if profiles else self.llm_primary_model
|
||||
self.record_llm_usage("mentor", source, failed_model, "failed")
|
||||
raise ValueError("智能解读服务暂不可用,请稍后重试。") from last_error
|
||||
|
||||
return generate()
|
||||
|
||||
def mentor_messages(self, mentor_id: str, trade_date: str) -> list[dict[str, Any]]:
|
||||
mentor_id = validate_text(mentor_id, "问师角色", 100, required=True)
|
||||
@@ -3929,8 +3982,15 @@ class RequestHandler(BaseHTTPRequestHandler):
|
||||
if parsed.path == "/api/screener/tracking/refresh":
|
||||
self.refresh_screener_tracking()
|
||||
return
|
||||
if parsed.path == "/api/mentors/preferences":
|
||||
try:
|
||||
result = SERVICE.save_mentor_preferences(self.read_json_body())
|
||||
self.send_json({"ok": True, **result})
|
||||
except (ValueError, json.JSONDecodeError) as exc:
|
||||
self.send_json({"error": str(exc)}, HTTPStatus.BAD_REQUEST)
|
||||
return
|
||||
if parsed.path == "/api/mentors/chat":
|
||||
self.mentor_chat()
|
||||
self.stream_mentor_chat()
|
||||
return
|
||||
if parsed.path == "/api/heaven/hexagram":
|
||||
self.heaven_hexagram()
|
||||
@@ -4439,13 +4499,29 @@ class RequestHandler(BaseHTTPRequestHandler):
|
||||
except Exception as exc:
|
||||
self.send_json({"error": f"跟踪刷新失败:{exc}"}, HTTPStatus.INTERNAL_SERVER_ERROR)
|
||||
|
||||
def mentor_chat(self) -> None:
|
||||
def stream_mentor_chat(self) -> None:
|
||||
try:
|
||||
body = self.read_json_body()
|
||||
result = SERVICE.mentor_chat(body)
|
||||
self.send_json({"ok": True, **result})
|
||||
stream = SERVICE.mentor_stream(body)
|
||||
except (ValueError, json.JSONDecodeError) as exc:
|
||||
self.send_json({"error": str(exc)}, HTTPStatus.BAD_REQUEST)
|
||||
return
|
||||
self.send_response(HTTPStatus.OK)
|
||||
self.send_header("Content-Type", "application/x-ndjson; charset=utf-8")
|
||||
self.send_header("Cache-Control", "no-cache, no-transform")
|
||||
self.send_header("X-Accel-Buffering", "no")
|
||||
self.send_header("Connection", "close")
|
||||
self.end_headers()
|
||||
try:
|
||||
for event in stream:
|
||||
self._write_stream_event(event)
|
||||
self._write_stream_event({"type": "done"})
|
||||
except (ValueError, MentorAgentError) as exc:
|
||||
self._write_stream_event({"type": "error", "error": str(exc)})
|
||||
except (BrokenPipeError, ConnectionResetError):
|
||||
pass
|
||||
finally:
|
||||
self.close_connection = True
|
||||
|
||||
def heaven_hexagram(self) -> None:
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user