fix(projekt-matching): retry chat_json on empty/unparseable vLLM completion
This commit is contained in:
@@ -7,6 +7,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
|
import time
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
@@ -131,19 +132,23 @@ def _post(base, model, messages, schema):
|
|||||||
timeout=1500)
|
timeout=1500)
|
||||||
|
|
||||||
|
|
||||||
def chat_json(base, model, messages, schema):
|
def chat_json(base, model, messages, schema, attempts=3):
|
||||||
resp = _post(base, model, messages, schema)
|
last = None
|
||||||
if resp.status_code != 200:
|
for attempt in range(attempts):
|
||||||
raise LlmError(f"vLLM HTTP {resp.status_code}: "
|
resp = _post(base, model, messages, schema)
|
||||||
f"{getattr(resp, 'text', '')[:300]}")
|
if resp.status_code != 200:
|
||||||
content = resp.json()["choices"][0]["message"]["content"] or ""
|
raise LlmError(f"vLLM HTTP {resp.status_code}: "
|
||||||
content = THINK_RE.sub("", content).strip()
|
f"{getattr(resp, 'text', '')[:300]}")
|
||||||
if content.startswith("```"):
|
content = resp.json()["choices"][0]["message"]["content"] or ""
|
||||||
content = re.sub(r"^```[a-z]*\s*|\s*```$", "", content).strip()
|
content = THINK_RE.sub("", content).strip()
|
||||||
try:
|
if content.startswith("```"):
|
||||||
return json.loads(content)
|
content = re.sub(r"^```[a-z]*\s*|\s*```$", "", content).strip()
|
||||||
except json.JSONDecodeError as exc:
|
try:
|
||||||
raise LlmError(f"LLM lieferte kein JSON: {exc}: {content[:200]}")
|
return json.loads(content)
|
||||||
|
except json.JSONDecodeError as exc:
|
||||||
|
last = LlmError(f"LLM lieferte kein JSON: {exc}: {content[:200]}")
|
||||||
|
time.sleep(10 * (attempt + 1))
|
||||||
|
raise last
|
||||||
|
|
||||||
|
|
||||||
def extract_project(base, model, page_text):
|
def extract_project(base, model, page_text):
|
||||||
|
|||||||
@@ -35,6 +35,20 @@ def test_chat_json_strips_markdown_fences():
|
|||||||
assert out == {"a": 2}
|
assert out == {"a": 2}
|
||||||
|
|
||||||
|
|
||||||
|
def test_chat_json_retries_on_empty_content():
|
||||||
|
good = json.dumps({"a": 1})
|
||||||
|
with mock.patch("projektmatch.llm.time.sleep", lambda s: None), \
|
||||||
|
mock.patch("projektmatch.llm.requests.post", fake_post(["", good])):
|
||||||
|
assert llm.chat_json("http://v/v1", "m", [], {"type": "object"}) == {"a": 1}
|
||||||
|
|
||||||
|
|
||||||
|
def test_chat_json_raises_after_exhausted_retries():
|
||||||
|
with mock.patch("projektmatch.llm.time.sleep", lambda s: None), \
|
||||||
|
mock.patch("projektmatch.llm.requests.post", fake_post(["", "", ""])):
|
||||||
|
with pytest.raises(llm.LlmError, match="kein JSON"):
|
||||||
|
llm.chat_json("http://v/v1", "m", [], {"type": "object"})
|
||||||
|
|
||||||
|
|
||||||
def test_match_cv_retries_on_missing_nr_then_raises():
|
def test_match_cv_retries_on_missing_nr_then_raises():
|
||||||
reqs = [{"nr": 1, "text": "Python"}, {"nr": 2, "text": "K8s"}]
|
reqs = [{"nr": 1, "text": "Python"}, {"nr": 2, "text": "K8s"}]
|
||||||
partial = json.dumps({"ratings": [
|
partial = json.dumps({"ratings": [
|
||||||
|
|||||||
Reference in New Issue
Block a user