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 @@ +