1
This commit is contained in:
parent
ad0247c2a8
commit
ce6a87fd19
@ -21,7 +21,7 @@ from app.db import chat_assistant_db
|
|||||||
from app.db.analytics import get_pipeline_runs
|
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.llm_insights import compute_input_hash, repair_mojibake_json, repair_mojibake_text
|
||||||
from app.db.schema import get_conn
|
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
|
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:
|
if resp.status_code >= 400:
|
||||||
return {"status": "failed", "error": f"http_{resp.status_code}:{resp.text[:300]}", "model": model}
|
return {"status": "failed", "error": f"http_{resp.status_code}:{resp.text[:300]}", "model": model}
|
||||||
data = resp.json()
|
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)
|
content = repair_mojibake_text(content)
|
||||||
parsed = repair_mojibake_json(json.loads(content))
|
parsed = repair_mojibake_json(_parse_json_object_text(content))
|
||||||
if not isinstance(parsed, dict):
|
|
||||||
raise ValueError("llm_output_not_object")
|
|
||||||
parsed.setdefault("summary", parsed.get("answer", "")[:80])
|
parsed.setdefault("summary", parsed.get("answer", "")[:80])
|
||||||
parsed.setdefault("answer_style", _answer_style_for_intent(context.get("intent")))
|
parsed.setdefault("answer_style", _answer_style_for_intent(context.get("intent")))
|
||||||
parsed.setdefault("evidence", [])
|
parsed.setdefault("evidence", [])
|
||||||
|
|||||||
@ -76,6 +76,44 @@ def _parse_insight_payload(content):
|
|||||||
return 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):
|
def _call_llm_json(prompt_version, payload):
|
||||||
params = get_llm_params()
|
params = get_llm_params()
|
||||||
api_key = os.getenv(str(params.get("api_key_env") or "OPENAI_API_KEY"), "").strip()
|
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:
|
if resp.status_code >= 400:
|
||||||
return {"status": "failed", "error": f"http_{resp.status_code}", "raw": resp.text[:1000], "model": model}
|
return {"status": "failed", "error": f"http_{resp.status_code}", "raw": resp.text[:1000], "model": model}
|
||||||
data = resp.json()
|
data = resp.json()
|
||||||
content = (((data.get("choices") or [{}])[0]).get("message") or {}).get("content") or "{}"
|
message = (((data.get("choices") or [{}])[0]).get("message") or {})
|
||||||
parsed = json.loads(content)
|
content = _message_content_text(message)
|
||||||
if not isinstance(parsed, dict):
|
parsed = _parse_json_object_text(content)
|
||||||
raise ValueError("llm_output_not_object")
|
|
||||||
return {"status": "success", "content": parsed, "model": model}
|
return {"status": "success", "content": parsed, "model": model}
|
||||||
except json.JSONDecodeError as exc:
|
except json.JSONDecodeError as exc:
|
||||||
return {"status": "failed", "error": f"invalid_json:{exc}", "model": model}
|
return {"status": "failed", "error": f"invalid_json:{exc}", "model": model}
|
||||||
|
|||||||
@ -125,6 +125,60 @@ def test_invalid_json_is_marked_failed(monkeypatch, temp_db):
|
|||||||
assert row["status"] == "failed"
|
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):
|
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="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")
|
_insert_recommendation(temp_db, symbol="DDD/USDT", action_status="等回踩", execution_status="wait_pullback", display_bucket="realtime", rec_time="2026-05-01T12:00:00")
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user