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>
38 lines
1.8 KiB
Python
38 lines
1.8 KiB
Python
"""`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)
|