From 40dc2e07fe9a1050d0f81b67c15fdab3680234fb Mon Sep 17 00:00:00 2001
From: aaron <>
Date: Mon, 8 Jun 2026 09:06:34 +0800
Subject: [PATCH] 1
---
app/services/llm_insights.py | 80 ++++++++++++++++++++++---------
app/web/routes_recommendations.py | 3 +-
static/llm_insights.html | 1 +
tests/test_llm_insights.py | 42 ++++++++++++++++
4 files changed, 103 insertions(+), 23 deletions(-)
diff --git a/app/services/llm_insights.py b/app/services/llm_insights.py
index 968f852..f14ee06 100644
--- a/app/services/llm_insights.py
+++ b/app/services/llm_insights.py
@@ -114,6 +114,33 @@ def _parse_json_object_text(content: str) -> dict:
return parsed
+def _chat_completion_request(base_url, api_key, model, messages, max_tokens, timeout, *, response_format=True):
+ body = {
+ "model": model,
+ "messages": messages,
+ "temperature": 0.2,
+ "max_tokens": max_tokens,
+ }
+ if response_format:
+ body["response_format"] = {"type": "json_object"}
+ return requests.post(
+ f"{base_url}/chat/completions",
+ headers={"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"},
+ json=body,
+ timeout=timeout,
+ )
+
+
+def _parse_llm_response(resp, model):
+ if resp.status_code >= 400:
+ return {"status": "failed", "error": f"http_{resp.status_code}", "raw": resp.text[:1000], "model": model}
+ data = resp.json()
+ message = (((data.get("choices") or [{}])[0]).get("message") or {})
+ content = _message_content_text(message)
+ parsed = _parse_json_object_text(content)
+ return {"status": "success", "content": parsed, "model": model}
+
+
def _call_llm_json(prompt_version, payload):
params = get_llm_params()
api_key = os.getenv(str(params.get("api_key_env") or "OPENAI_API_KEY"), "").strip()
@@ -132,29 +159,38 @@ def _call_llm_json(prompt_version, payload):
"input": payload,
"output_schema_hint": "JSON object with concise Chinese fields only",
})
+ messages = [
+ {"role": "system", "content": system_prompt},
+ {"role": "user", "content": user_prompt},
+ ]
try:
- resp = requests.post(
- f"{base_url}/chat/completions",
- headers={"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"},
- json={
- "model": model,
- "messages": [
- {"role": "system", "content": system_prompt},
- {"role": "user", "content": user_prompt},
- ],
- "temperature": 0.2,
- "max_tokens": max_tokens,
- "response_format": {"type": "json_object"},
- },
- timeout=timeout,
- )
- if resp.status_code >= 400:
- return {"status": "failed", "error": f"http_{resp.status_code}", "raw": resp.text[:1000], "model": model}
- data = resp.json()
- message = (((data.get("choices") or [{}])[0]).get("message") or {})
- content = _message_content_text(message)
- parsed = _parse_json_object_text(content)
- return {"status": "success", "content": parsed, "model": model}
+ resp = _chat_completion_request(base_url, api_key, model, messages, max_tokens, timeout, response_format=True)
+ first = _parse_llm_response(resp, model)
+ if first.get("status") == "success":
+ return first
+ return first
+ except ValueError as exc:
+ # Some OpenAI-compatible providers occasionally return an empty message
+ # when strict JSON response_format is enabled. Retry once with prompt-only
+ # JSON enforcement so the job does not fail silently on provider quirks.
+ retryable = str(exc) in ("llm_empty_content", "llm_empty_json_object")
+ if not retryable:
+ return {"status": "failed", "error": str(exc)[:1000], "model": model}
+ try:
+ retry_messages = [
+ {
+ "role": "system",
+ "content": system_prompt + " Return one valid JSON object. No markdown, no prose.",
+ },
+ {"role": "user", "content": user_prompt},
+ ]
+ resp = _chat_completion_request(base_url, api_key, model, retry_messages, max_tokens, timeout, response_format=False)
+ result = _parse_llm_response(resp, model)
+ if result.get("status") == "success":
+ result["retry"] = "without_response_format"
+ return result
+ except Exception as retry_exc:
+ return {"status": "failed", "error": f"{exc}; retry_failed:{str(retry_exc)[:800]}", "model": model}
except json.JSONDecodeError as exc:
return {"status": "failed", "error": f"invalid_json:{exc}", "model": model}
except Exception as exc:
diff --git a/app/web/routes_recommendations.py b/app/web/routes_recommendations.py
index f778d9c..e567996 100644
--- a/app/web/routes_recommendations.py
+++ b/app/web/routes_recommendations.py
@@ -34,6 +34,7 @@ def _friendly_llm_item(item):
type_label = {
"recommendation": "推荐解释",
"sentiment": "舆情解读",
+ "sentiment_batch": "批量舆情分析",
"review": "复盘 memo",
}.get(target_type, target_type or "未知任务")
status_label = {
@@ -41,7 +42,7 @@ def _friendly_llm_item(item):
"failed": "失败",
"skipped": "跳过",
}.get(status, status or "未知")
- subject = payload.get("symbol") or payload.get("related_symbol") or payload.get("title") or payload.get("run_date") or item.get("target_id")
+ subject = payload.get("symbol") or payload.get("related_symbol") or payload.get("title") or payload.get("run_date") or payload.get("target_id") or item.get("target_id")
summary = content.get("summary") or content.get("memo") or content.get("why_now_or_not") or content.get("raw") or item.get("error") or ""
return {
"id": item.get("id"),
diff --git a/static/llm_insights.html b/static/llm_insights.html
index 1742340..7e08388 100644
--- a/static/llm_insights.html
+++ b/static/llm_insights.html
@@ -17,6 +17,7 @@
+