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]}
|