This commit is contained in:
aaron 2026-06-08 08:43:18 +08:00
parent ad0247c2a8
commit ce6a87fd19
3 changed files with 98 additions and 9 deletions

View File

@ -21,7 +21,7 @@ from app.db import chat_assistant_db
from app.db.analytics import get_pipeline_runs
from app.db.llm_insights import compute_input_hash, repair_mojibake_json, repair_mojibake_text
from app.db.schema import get_conn
from app.services.llm_insights import get_llm_params
from app.services.llm_insights import _message_content_text, _parse_json_object_text, get_llm_params
from app.services.market_overview import get_crypto_market_overview
@ -616,11 +616,9 @@ def _call_chat_llm(message: str, context: dict, history=None) -> dict:
if resp.status_code >= 400:
return {"status": "failed", "error": f"http_{resp.status_code}:{resp.text[:300]}", "model": model}
data = resp.json()
content = (((data.get("choices") or [{}])[0]).get("message") or {}).get("content") or "{}"
content = _message_content_text(((data.get("choices") or [{}])[0]).get("message") or {})
content = repair_mojibake_text(content)
parsed = repair_mojibake_json(json.loads(content))
if not isinstance(parsed, dict):
raise ValueError("llm_output_not_object")
parsed = repair_mojibake_json(_parse_json_object_text(content))
parsed.setdefault("summary", parsed.get("answer", "")[:80])
parsed.setdefault("answer_style", _answer_style_for_intent(context.get("intent")))
parsed.setdefault("evidence", [])

View File

@ -76,6 +76,44 @@ def _parse_insight_payload(content):
return content
def _message_content_text(message: dict) -> str:
content = (message or {}).get("content")
if isinstance(content, str):
return content.strip()
if isinstance(content, list):
parts = []
for item in content:
if isinstance(item, str):
parts.append(item)
elif isinstance(item, dict):
parts.append(str(item.get("text") or item.get("content") or ""))
return "\n".join(x for x in parts if x).strip()
return ""
def _parse_json_object_text(content: str) -> dict:
text = str(content or "").strip()
if not text:
raise ValueError("llm_empty_content")
if text.startswith("```"):
text = text.strip("`").strip()
if text.lower().startswith("json"):
text = text[4:].strip()
try:
parsed = json.loads(text)
except json.JSONDecodeError:
start = text.find("{")
end = text.rfind("}")
if start < 0 or end <= start:
raise
parsed = json.loads(text[start:end + 1])
if not isinstance(parsed, dict):
raise ValueError("llm_output_not_object")
if not parsed:
raise ValueError("llm_empty_json_object")
return parsed
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()
@ -113,10 +151,9 @@ def _call_llm_json(prompt_version, payload):
if resp.status_code >= 400:
return {"status": "failed", "error": f"http_{resp.status_code}", "raw": resp.text[:1000], "model": model}
data = resp.json()
content = (((data.get("choices") or [{}])[0]).get("message") or {}).get("content") or "{}"
parsed = json.loads(content)
if not isinstance(parsed, dict):
raise ValueError("llm_output_not_object")
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}
except json.JSONDecodeError as exc:
return {"status": "failed", "error": f"invalid_json:{exc}", "model": model}

View File

@ -125,6 +125,60 @@ def test_invalid_json_is_marked_failed(monkeypatch, temp_db):
assert row["status"] == "failed"
def test_llm_empty_content_is_not_marked_success(monkeypatch):
monkeypatch.setattr(llm_insights, "get_llm_params", lambda: {
"enabled": True,
"base_url": "https://llm.example/v1",
"api_key_env": "TEST_LLM_KEY",
"model": "deepseek-v4-pro",
"timeout": 5,
"max_tokens": 100,
"modules": {},
})
monkeypatch.setenv("TEST_LLM_KEY", "test-key")
class Resp:
status_code = 200
text = "{}"
def json(self):
return {"choices": [{"message": {"content": ""}}]}
monkeypatch.setattr(llm_insights.requests, "post", lambda *args, **kwargs: Resp())
result = llm_insights._call_llm_json("test_prompt", {"symbol": "AAA/USDT"})
assert result["status"] == "failed"
assert "llm_empty_content" in result["error"]
def test_llm_json_code_fence_is_parsed(monkeypatch):
monkeypatch.setattr(llm_insights, "get_llm_params", lambda: {
"enabled": True,
"base_url": "https://llm.example/v1",
"api_key_env": "TEST_LLM_KEY",
"model": "deepseek-v4-pro",
"timeout": 5,
"max_tokens": 100,
"modules": {},
})
monkeypatch.setenv("TEST_LLM_KEY", "test-key")
class Resp:
status_code = 200
text = "{}"
def json(self):
return {"choices": [{"message": {"content": "```json\n{\"summary\":\"ok\"}\n```"}}]}
monkeypatch.setattr(llm_insights.requests, "post", lambda *args, **kwargs: Resp())
result = llm_insights._call_llm_json("test_prompt", {"symbol": "AAA/USDT"})
assert result["status"] == "success"
assert result["content"]["summary"] == "ok"
def test_only_key_samples_generate_insights(monkeypatch, temp_db):
_insert_recommendation(temp_db, symbol="CCC/USDT", action_status="观察", execution_status="observe", display_bucket="watch_pool", state_reason="普通观察")
_insert_recommendation(temp_db, symbol="DDD/USDT", action_status="等回踩", execution_status="wait_pullback", display_bucket="realtime", rec_time="2026-05-01T12:00:00")