77 lines
3.5 KiB
Python
77 lines
3.5 KiB
Python
"""Локальный Qwen-инсайт по обезличенной агрегированной аналитике группы."""
|
||
|
||
import json
|
||
|
||
from app.config import get_settings
|
||
from app.dialog.llm import LlmClient, LlmRequest, LlmUnavailable, is_loopback_url
|
||
|
||
|
||
class InsightInvalid(ValueError):
|
||
pass
|
||
|
||
|
||
async def generate_group_insight(data: dict) -> dict:
|
||
settings = get_settings()
|
||
if (not settings.llm_model_caller or not is_loopback_url(
|
||
settings.llm_base_url, allow_docker_host=settings.allow_docker_host_models
|
||
)):
|
||
raise LlmUnavailable("локальная модель аналитики не настроена")
|
||
schema = {"type": "json_object", "schema": {
|
||
"type": "object",
|
||
"properties": {
|
||
"summary": {"type": "string", "minLength": 20, "maxLength": 700},
|
||
"priorities": {
|
||
"type": "array", "minItems": 1, "maxItems": 3,
|
||
"items": {"type": "string", "minLength": 8, "maxLength": 240},
|
||
},
|
||
},
|
||
"required": ["summary", "priorities"],
|
||
"additionalProperties": False,
|
||
}}
|
||
prompt = {
|
||
"active_trainees": data["active_trainees"],
|
||
"scored_attempts": data["scored_attempts"],
|
||
"average_score": data["average_score"],
|
||
"typical_errors": [{
|
||
"code": item["code"],
|
||
"title": item["title"],
|
||
"rate_percent": item["rate_percent"],
|
||
"rule_recommendation": item["recommendation"],
|
||
} for item in data["errors"][:8]],
|
||
}
|
||
request = LlmRequest(
|
||
model=settings.llm_model_caller,
|
||
messages=[{
|
||
"role": "system",
|
||
"content": (
|
||
"Ты методист учебного центра системы-112. По обезличенной агрегированной "
|
||
"статистике сформулируй краткий русский вывод и от одной до трёх конкретных "
|
||
"тем следующего занятия. Не придумывай персональные данные, новые числа, "
|
||
"диагнозы или нормативы. Верни только JSON по заданной схеме. /no_think"
|
||
),
|
||
}, {"role": "user", "content": json.dumps(prompt, ensure_ascii=False)}],
|
||
temperature=0.0,
|
||
max_tokens=420,
|
||
response_format=schema,
|
||
strip_reasoning=True,
|
||
)
|
||
client = LlmClient(base_url=settings.llm_base_url, timeout=25)
|
||
try:
|
||
raw = await client.complete(request, use_cache=True)
|
||
finally:
|
||
await client.aclose()
|
||
try:
|
||
payload = json.loads(raw)
|
||
except json.JSONDecodeError as exc:
|
||
raise InsightInvalid("модель вернула не JSON") from exc
|
||
if set(payload) != {"summary", "priorities"}:
|
||
raise InsightInvalid("неверный набор полей инсайта")
|
||
summary = payload["summary"]
|
||
priorities = payload["priorities"]
|
||
if not isinstance(summary, str) or not 20 <= len(summary.strip()) <= 700:
|
||
raise InsightInvalid("неверная длина вывода")
|
||
if (not isinstance(priorities, list) or not 1 <= len(priorities) <= 3
|
||
or any(not isinstance(item, str) or not 8 <= len(item.strip()) <= 240
|
||
for item in priorities)):
|
||
raise InsightInvalid("неверные приоритеты")
|
||
return {"summary": summary.strip(), "priorities": [item.strip() for item in priorities]}
|