mirror of
https://github.com/dartdavros/chatballs.git
synced 2026-10-10 19:34:59 +03:00
Знания: плоская сущность Knowledge (заголовок, описание, MD) + файловые вложения с оригинальными именами (retrieval из md/txt/pdf/docx, публичная ссылка для клиента). Агент: без релизов, инструкции из трёх частей (персонализация/тон/инструкции) + выбор знаний из библиотеки; лимит диалогов упразднён (остался dailyCostUsd). channel.system_prompt/model удалены — AI-поведение только на агенте. Продукт — техническая запись без summary/sales_description. Data-миграция переносит опубликованные документы и активные релизы в новую модель. Internal-UI: раздел Знания, новая карточка агента, упрощённые продукты, release-страницы удалены. ADR-0005/0007/0017 → superseded; SPEC-HUB-0012 переписан. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
91 lines
5.2 KiB
Python
91 lines
5.2 KiB
Python
from dataclasses import dataclass
|
|
|
|
from hub_platform.ai.invocation import invoke_chat
|
|
from hub_platform.ai.models import AIAgent, KnowledgeFragment
|
|
from hub_platform.ai.provider.base import ChatMessage, ChatResult
|
|
from hub_platform.ai.retrieval import KnowledgeRetriever
|
|
|
|
# Гард стиля для мессенджеров: гарантирует простой текст вне зависимости от
|
|
# того, что написано в авторских инструкциях.
|
|
MESSENGER_STYLE_GUARD = (
|
|
"Пиши ответ простым текстом для мессенджера: без markdown-разметки — "
|
|
"никаких **, ##, маркированных списков с -, таблиц, ссылок вида [текст](url). "
|
|
"Короткие абзацы. Не придумывай факты, контакты, ссылки, цены и условия, "
|
|
"которых нет в знаниях; если данных нет — честно скажи и предложи оператора."
|
|
)
|
|
|
|
# Протокол передачи оператору: модель добавляет технический токен, система его
|
|
# ловит, ставит диалог в очередь и уведомляет операторов (ADR-HUB-0003).
|
|
HANDOFF_TOKEN = "<<HANDOFF>>"
|
|
HANDOFF_PROTOCOL = (
|
|
"Если по правилам нужно подключить живого оператора (клиент просит человека; "
|
|
"вопрос вне базы знаний; индивидуальные условия, скидка, счёт, оплата от юрлица, "
|
|
"документы; жалоба, спор или проблема с оплатой/доступом), в самом конце ответа "
|
|
f"добавь отдельной строкой технический токен {HANDOFF_TOKEN}. Не упоминай этот "
|
|
"токен в тексте и не показывай его пользователю — просто заверши им сообщение."
|
|
)
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class AgentTurnResult:
|
|
result: ChatResult
|
|
fragments: list[KnowledgeFragment]
|
|
handoff_suggested: bool
|
|
|
|
|
|
def agent_system_prompt(agent: AIAgent) -> str:
|
|
# Порядок частей фиксирован (ADR-HUB-0023): Персонализация -> Тон -> Инструкции.
|
|
parts = [part.strip() for part in (agent.persona, agent.tone, agent.instructions) if part.strip()]
|
|
return "\n\n".join(parts)
|
|
|
|
|
|
def knowledge_catalog(agent: AIAgent) -> str:
|
|
"""Каталог выбранных знаний для системного промпта: заголовок, краткое
|
|
описание и публичные ссылки вложений (агент может отдать ссылку клиенту)."""
|
|
lines: list[str] = []
|
|
items = agent.knowledge_items.filter(is_enabled=True).prefetch_related("attachments")
|
|
for knowledge in items:
|
|
line = f"- {knowledge.title}"
|
|
if knowledge.description.strip():
|
|
line += f" — {knowledge.description.strip()}"
|
|
lines.append(line)
|
|
for attachment in knowledge.attachments.all():
|
|
lines.append(f" файл: {attachment.original_name} — {attachment.public_url()}")
|
|
if not lines:
|
|
return ""
|
|
return "Тебе доступны следующие знания (детали подтягиваются автоматически по запросу). Ссылки на файлы можно давать клиенту:\n" + "\n".join(lines)
|
|
|
|
|
|
def run_agent_turn(*, agent: AIAgent, message: str, history: list[dict] | None = None, style_guard: bool = True) -> AgentTurnResult:
|
|
fragments = KnowledgeRetriever().retrieve(agent=agent, query=message, limit=5)
|
|
|
|
messages: list[ChatMessage] = []
|
|
system_prompt = agent_system_prompt(agent)
|
|
if system_prompt:
|
|
messages.append(ChatMessage(role="system", content=system_prompt))
|
|
if style_guard:
|
|
messages.append(ChatMessage(role="system", content=MESSENGER_STYLE_GUARD + "\n\n" + HANDOFF_PROTOCOL))
|
|
catalog = knowledge_catalog(agent)
|
|
if catalog:
|
|
messages.append(ChatMessage(role="system", content=catalog))
|
|
if fragments:
|
|
knowledge = "\n\n".join(
|
|
f"[{fragment.knowledge.title}#{fragment.chunk_index}] {fragment.content}" for fragment in fragments
|
|
)
|
|
messages.append(ChatMessage(role="system", content="Отвечай только на основе этих знаний:\n" + knowledge))
|
|
for item in history or []:
|
|
messages.append(ChatMessage(role=str(item.get("role", "user")), content=str(item.get("content", ""))))
|
|
messages.append(ChatMessage(role="user", content=message))
|
|
|
|
result = invoke_chat(
|
|
channel=agent.channel,
|
|
messages=messages,
|
|
purpose="agent_chat",
|
|
model=agent.model,
|
|
params=agent.model_params or None,
|
|
used_fragment_ids=[fragment.id for fragment in fragments],
|
|
)
|
|
|
|
# Нет основания в знаниях -> кандидат на передачу оператору (ADR-HUB-0003).
|
|
return AgentTurnResult(result=result, fragments=fragments, handoff_suggested=not fragments)
|