fix(projekt-matching): retry chat_json on empty/unparseable vLLM completion

This commit is contained in:
tlg
2026-07-09 15:33:11 +02:00
parent cf088f0bd3
commit 20796b3a68
2 changed files with 32 additions and 13 deletions

View File

@@ -7,6 +7,7 @@ from __future__ import annotations
import json import json
import re import re
import time
import requests import requests
@@ -131,7 +132,9 @@ 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):
last = None
for attempt in range(attempts):
resp = _post(base, model, messages, schema) resp = _post(base, model, messages, schema)
if resp.status_code != 200: if resp.status_code != 200:
raise LlmError(f"vLLM HTTP {resp.status_code}: " raise LlmError(f"vLLM HTTP {resp.status_code}: "
@@ -143,7 +146,9 @@ def chat_json(base, model, messages, schema):
try: try:
return json.loads(content) return json.loads(content)
except json.JSONDecodeError as exc: except json.JSONDecodeError as exc:
raise LlmError(f"LLM lieferte kein JSON: {exc}: {content[:200]}") 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):

View File

@@ -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": [