feat(bot): LLM gateway with GigaChat and offline mock providers
CI / ruff · pytest (pull_request) Successful in 23s
CI / ruff · pytest (pull_request) Successful in 23s
Single entry point app/llm/gateway.py: extract_item, extract_checkin, risk_classify, phrase, summarize_for_doctor. Two-level design: LLMProvider (transport, GigaChat) and LLMTasks (PromptedTasks over a provider, or rule-based MockTasks). Shared policy in the gateway: assert_scrubbed backstop, confidence threshold 0.5, risk failure -> NONE, forbidden-output filter with config/texts/phrases.yaml fallbacks. Refs #7 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
"""`LLMTasks` on top of any `LLMProvider`: builds prompts, delegates transport and validation."""
|
||||
|
||||
from app.core.types import ScrubbedText
|
||||
from app.llm import prompts
|
||||
from app.llm.base import LLMProvider
|
||||
from app.llm.schemas import CheckinFacts, ItemScore, RiskVerdict
|
||||
|
||||
|
||||
class PromptedTasks:
|
||||
def __init__(self, provider: LLMProvider, name: str = "gigachat") -> None:
|
||||
self._provider = provider
|
||||
self.name = name
|
||||
|
||||
async def extract_item(
|
||||
self, scale_item_id: str, item_text: str, max_score: int, answer: ScrubbedText
|
||||
) -> ItemScore | None:
|
||||
system = prompts.extract_item_system(scale_item_id, item_text, max_score)
|
||||
result = await self._provider.complete_json(
|
||||
system, answer, ItemScore, validation_context={"max_score": max_score}
|
||||
)
|
||||
if result is not None and result.item != scale_item_id:
|
||||
# The model sometimes echoes the question instead of the id; the id is ours, not its.
|
||||
result = result.model_copy(update={"item": scale_item_id})
|
||||
return result
|
||||
|
||||
async def extract_checkin(self, text: ScrubbedText) -> CheckinFacts | None:
|
||||
return await self._provider.complete_json(prompts.extract_checkin_system(), text, CheckinFacts)
|
||||
|
||||
async def risk_classify(self, text: ScrubbedText) -> RiskVerdict | None:
|
||||
return await self._provider.complete_json(prompts.risk_system(), text, RiskVerdict)
|
||||
|
||||
async def phrase(self, intent: str, payload: ScrubbedText, base_text: str) -> str | None:
|
||||
# `payload` already carries `base_text` (built by the gateway), so the model sees the meaning to keep.
|
||||
return await self._provider.complete_text(prompts.phrase_system(intent), payload)
|
||||
|
||||
async def summarize_for_doctor(self, structured: ScrubbedText, base_text: str) -> str | None:
|
||||
return await self._provider.complete_text(prompts.summary_system(), structured)
|
||||
Reference in New Issue
Block a user