feat: protocol generation — LLM integration, prompt templates, Celery task

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-03 22:24:53 +05:00
parent 849eb0ff48
commit 5c009bc368
7 changed files with 323 additions and 0 deletions
View File
+101
View File
@@ -0,0 +1,101 @@
DEFAULT_TEMPLATES = [
{
"name": "Обследование процессов клиента",
"description": "Протокол встречи по обследованию бизнес-процессов клиента",
"type": "client_survey",
"system_prompt": (
"Ты — опытный бизнес-аналитик. Твоя задача — составить формальный протокол встречи "
"по обследованию бизнес-процессов клиента на основе транскрипции записи встречи. "
"Протокол должен быть структурированным, точным и содержать все ключевые детали. "
"Не придумывай информацию, которой нет в транскрипции. "
"Если какой-то раздел не удаётся заполнить по транскрипции, укажи 'Не обсуждалось'."
),
"user_prompt": (
"Составь протокол встречи по следующей транскрипции. "
"Ответ должен быть в формате JSON с ключами:\n"
"- date_participants_topic: дата, участники и тема встречи\n"
"- process_description: описание обследуемого процесса (задачи бизнеса, подпроцесс)\n"
"- current_state: текущее состояние (as-is)\n"
"- problems: выявленные проблемы и узкие места\n"
"- agreements: договорённости и решения\n"
"- open_questions: открытые вопросы (о которых не договорились)\n"
"- tasks: задачи с ответственными и сроками (массив объектов с полями: task, responsible, deadline)\n"
"- next_steps: следующие шаги\n\n"
"Транскрипция:\n{transcription}"
),
"output_schema": {
"sections": [
"date_participants_topic", "process_description", "current_state",
"problems", "agreements", "open_questions", "tasks", "next_steps",
]
},
"is_default": True,
},
{
"name": "Вводная встреча-знакомство с клиентом",
"description": "Протокол первой встречи с клиентом — выявление целей, параметров проекта, ЛПР",
"type": "client_intro",
"system_prompt": (
"Ты — опытный бизнес-аналитик. Твоя задача — составить формальный протокол вводной "
"встречи-знакомства с клиентом на основе транскрипции. "
"Фокус на выявлении целей клиента, параметров проекта, ограничений и ключевых лиц. "
"Не придумывай информацию, которой нет в транскрипции. "
"Если какой-то раздел не удаётся заполнить, укажи 'Не обсуждалось'."
),
"user_prompt": (
"Составь протокол вводной встречи-знакомства с клиентом по следующей транскрипции. "
"Ответ должен быть в формате JSON с ключами:\n"
"- date_participants_topic: дата, участники и тема встречи\n"
"- client_goals: верхнеуровневые цели клиента\n"
"- project_parameters: параметры будущего проекта\n"
"- budget_constraints: бюджетные ограничения\n"
"- time_constraints: ограничения по срокам\n"
"- decision_makers: лица, принимающие решения (ЛПР)\n"
"- beneficiaries: бенефициары\n"
"- influencers: лица, влияющие на принятие решения\n"
"- agreements: договорённости и решения\n"
"- open_questions: открытые вопросы\n"
"- next_steps: следующие шаги\n\n"
"Транскрипция:\n{transcription}"
),
"output_schema": {
"sections": [
"date_participants_topic", "client_goals", "project_parameters",
"budget_constraints", "time_constraints", "decision_makers",
"beneficiaries", "influencers", "agreements", "open_questions", "next_steps",
]
},
"is_default": False,
},
{
"name": "Внутренняя рабочая встреча",
"description": "Протокол внутренней рабочей встречи команды",
"type": "internal",
"system_prompt": (
"Ты — опытный менеджер проектов. Твоя задача — составить формальный протокол "
"внутренней рабочей встречи на основе транскрипции. "
"Протокол должен быть кратким, структурированным и фокусироваться на решениях и задачах. "
"Не придумывай информацию, которой нет в транскрипции. "
"Если какой-то раздел не удаётся заполнить, укажи 'Не обсуждалось'."
),
"user_prompt": (
"Составь протокол внутренней рабочей встречи по следующей транскрипции. "
"Ответ должен быть в формате JSON с ключами:\n"
"- date_participants_topic: дата, участники и тема\n"
"- discussed_topics: обсуждённые вопросы\n"
"- problems: выявленные проблемы\n"
"- decisions: принятые решения\n"
"- open_questions: открытые вопросы\n"
"- tasks: задачи с ответственными и сроками (массив объектов с полями: task, responsible, deadline)\n"
"- next_steps: следующие шаги\n\n"
"Транскрипция:\n{transcription}"
),
"output_schema": {
"sections": [
"date_participants_topic", "discussed_topics", "problems",
"decisions", "open_questions", "tasks", "next_steps",
]
},
"is_default": False,
},
]
+40
View File
@@ -0,0 +1,40 @@
import uuid
from datetime import datetime
from typing import Any
from pydantic import BaseModel
from app.models.prompt_template import TemplateType
class TemplateCreate(BaseModel):
name: str
description: str = ""
type: TemplateType = TemplateType.custom
system_prompt: str
user_prompt: str
output_schema: dict[str, Any] = {}
is_default: bool = False
class TemplateUpdate(BaseModel):
name: str | None = None
description: str | None = None
system_prompt: str | None = None
user_prompt: str | None = None
output_schema: dict[str, Any] | None = None
is_default: bool | None = None
class TemplateResponse(BaseModel):
id: uuid.UUID
name: str
description: str
type: TemplateType
system_prompt: str
user_prompt: str
output_schema: dict[str, Any]
is_default: bool
created_at: datetime
model_config = {"from_attributes": True}
+30
View File
@@ -0,0 +1,30 @@
import uuid
from datetime import datetime
from typing import Any
from pydantic import BaseModel
from app.models.protocol import ProtocolStatus
class ProtocolResponse(BaseModel):
id: uuid.UUID
transcription_id: uuid.UUID
template_id: uuid.UUID
content: dict[str, Any]
raw_text: str
llm_model: str
status: ProtocolStatus
created_at: datetime
updated_at: datetime
model_config = {"from_attributes": True}
class ProtocolList(BaseModel):
items: list[ProtocolResponse]
total: int
class ProtocolRegenerate(BaseModel):
template_id: uuid.UUID | None = None
@@ -0,0 +1,51 @@
import json
import uuid
from litellm import completion
from sqlalchemy import select
from sqlalchemy.orm import Session
from app.config import settings
from app.models.prompt_template import PromptTemplate
def generate_protocol_text(transcription_text: str, template: PromptTemplate) -> dict:
user_message = template.user_prompt.replace("{transcription}", transcription_text)
response = completion(
model=settings.litellm_model,
api_key=settings.litellm_api_key,
api_base=settings.litellm_api_base or None,
messages=[
{"role": "system", "content": template.system_prompt},
{"role": "user", "content": user_message},
],
temperature=0.3,
response_format={"type": "json_object"},
)
raw_text = response.choices[0].message.content
try:
content = json.loads(raw_text)
except json.JSONDecodeError:
content = {"raw": raw_text}
return {
"content": content,
"raw_text": raw_text,
"model": response.model,
}
def get_default_template(db: Session) -> PromptTemplate | None:
result = db.execute(
select(PromptTemplate).where(PromptTemplate.is_default == True)
)
return result.scalar_one_or_none()
def get_template_by_id(db: Session, template_id: uuid.UUID) -> PromptTemplate | None:
result = db.execute(
select(PromptTemplate).where(PromptTemplate.id == template_id)
)
return result.scalar_one_or_none()
+60
View File
@@ -0,0 +1,60 @@
import uuid
from sqlalchemy import select
from app.tasks.celery_app import celery_app
from app.tasks.transcription import SyncSession
from app.models.protocol import Protocol, ProtocolStatus
from app.models.transcription import Transcription
from app.services.protocol_generator import (
generate_protocol_text,
get_default_template,
get_template_by_id,
)
@celery_app.task(bind=True, name="app.tasks.protocol.generate_protocol")
def generate_protocol(self, transcription_id: str, template_id: str | None = None):
trans_uuid = uuid.UUID(transcription_id)
with SyncSession() as db:
transcription = db.execute(
select(Transcription).where(Transcription.id == trans_uuid)
).scalar_one_or_none()
if not transcription:
return {"error": "Transcription not found"}
if template_id:
template = get_template_by_id(db, uuid.UUID(template_id))
else:
template = get_default_template(db)
if not template:
return {"error": "No template found"}
protocol = Protocol(
transcription_id=trans_uuid,
template_id=template.id,
llm_model="",
status=ProtocolStatus.generating,
)
db.add(protocol)
db.commit()
db.refresh(protocol)
try:
result = generate_protocol_text(transcription.text, template)
protocol.content = result["content"]
protocol.raw_text = result["raw_text"]
protocol.llm_model = result["model"]
protocol.status = ProtocolStatus.done
db.commit()
return {"protocol_id": str(protocol.id), "status": "done"}
except Exception as e:
protocol.status = ProtocolStatus.error
db.commit()
raise self.retry(exc=e, max_retries=2, countdown=60)