feat(interviews): LLM stub mode for deterministic CI tests
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
29be754ff4
commit
eb3c3629c1
|
|
@ -32,6 +32,31 @@ class LLMClient:
|
||||||
base_url=self.base_url
|
base_url=self.base_url
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _stub_key(self, messages: list[dict]) -> str:
|
||||||
|
user_msg = next((m["content"] for m in reversed(messages) if m.get("role") == "user"), "")
|
||||||
|
sys_msg = next((m["content"] for m in messages if m.get("role") == "system"), "")
|
||||||
|
# Allow callers to embed an explicit stub_key=... token
|
||||||
|
for chunk in user_msg.split():
|
||||||
|
if chunk.startswith("stub_key="):
|
||||||
|
return chunk[len("stub_key="):]
|
||||||
|
import hashlib
|
||||||
|
return hashlib.sha256((sys_msg + "|" + user_msg).encode("utf-8")).hexdigest()[:12]
|
||||||
|
|
||||||
|
def _stub_response(self, messages: list[dict]) -> str:
|
||||||
|
import json as _json
|
||||||
|
return _json.dumps(self._stub_response_json(messages), ensure_ascii=False)
|
||||||
|
|
||||||
|
def _stub_response_json(self, messages: list[dict]) -> dict:
|
||||||
|
key = self._stub_key(messages)
|
||||||
|
# Deterministic centered Likert + plausible open text
|
||||||
|
digit = sum(ord(c) for c in key) % 5 + 1
|
||||||
|
return {
|
||||||
|
"stub_key": key,
|
||||||
|
"responses": {"item_001": digit, "item_002": digit, "item_003": (digit % 5) + 1},
|
||||||
|
"confidence": {"item_001": 0.7, "item_002": 0.7, "item_003": 0.6},
|
||||||
|
"open_comment": f"stub:{key}",
|
||||||
|
}
|
||||||
|
|
||||||
def chat(
|
def chat(
|
||||||
self,
|
self,
|
||||||
messages: List[Dict[str, str]],
|
messages: List[Dict[str, str]],
|
||||||
|
|
@ -51,6 +76,10 @@ class LLMClient:
|
||||||
Returns:
|
Returns:
|
||||||
模型响应文本
|
模型响应文本
|
||||||
"""
|
"""
|
||||||
|
from app.config import Config
|
||||||
|
if getattr(Config, "LLM_STUB_MODE", False):
|
||||||
|
return self._stub_response(messages)
|
||||||
|
|
||||||
kwargs = {
|
kwargs = {
|
||||||
"model": self.model,
|
"model": self.model,
|
||||||
"messages": messages,
|
"messages": messages,
|
||||||
|
|
@ -84,6 +113,10 @@ class LLMClient:
|
||||||
Returns:
|
Returns:
|
||||||
解析后的JSON对象
|
解析后的JSON对象
|
||||||
"""
|
"""
|
||||||
|
from app.config import Config
|
||||||
|
if getattr(Config, "LLM_STUB_MODE", False):
|
||||||
|
return self._stub_response_json(messages)
|
||||||
|
|
||||||
response = self.chat(
|
response = self.chat(
|
||||||
messages=messages,
|
messages=messages,
|
||||||
temperature=temperature,
|
temperature=temperature,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
import json
|
||||||
|
from app.utils.llm_client import LLMClient
|
||||||
|
|
||||||
|
|
||||||
|
def test_stub_mode_returns_deterministic_canned_json(monkeypatch):
|
||||||
|
monkeypatch.setenv("LLM_STUB_MODE", "true")
|
||||||
|
from app.config import Config
|
||||||
|
Config.LLM_STUB_MODE = True
|
||||||
|
client = LLMClient(api_key="x", base_url="x", model="x")
|
||||||
|
messages = [
|
||||||
|
{"role": "system", "content": "You are persona_42. Return JSON."},
|
||||||
|
{"role": "user", "content": "stub_key=longitudinal:item_001"},
|
||||||
|
]
|
||||||
|
out1 = client.chat_json(messages=messages, temperature=0.0)
|
||||||
|
out2 = client.chat_json(messages=messages, temperature=0.0)
|
||||||
|
assert out1 == out2
|
||||||
|
assert isinstance(out1, dict)
|
||||||
Loading…
Reference in New Issue