Files
chatballs/apps/backend/hub_platform/ai/invocation.py
T
AndreyandClaude Fable 5 a086675283 refactor(ai): упрощение агента и знаний (ADR-HUB-0023)
Знания: плоская сущность 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>
2026-07-10 17:08:50 +03:00

71 lines
3.2 KiB
Python

import time
from django.conf import settings
from hub_platform.ai import limits, pricing
from hub_platform.ai.models import LlmInvocation, LlmInvocationStatus
from hub_platform.ai.pii import redact
from hub_platform.ai.provider.base import ChatMessage, ChatResult, EmbeddingResult, ProviderError
from hub_platform.ai.provider.factory import get_provider
from hub_platform.ai.provider.resilience import CircuitBreaker, call_with_resilience
_breaker = CircuitBreaker()
def invoke_chat(*, channel, messages: list[ChatMessage], purpose: str, model: str | None = None, params: dict | None = None, used_fragment_ids: list | None = None) -> ChatResult:
agent = channel.ai_agent
model = model or agent.model
try:
limits.assert_within_limits(channel, agent)
except limits.LimitExceeded as error:
LlmInvocation.objects.create(
channel=channel, product=channel.product, purpose=purpose, operation="chat", model=model,
status=LlmInvocationStatus.BLOCKED, error=str(error),
)
raise
# ADR-HUB-0011: отдельное очищенное представление сообщений для LLM.
safe_messages = [ChatMessage(role=m.role, content=redact(m.content)) for m in messages]
provider = get_provider()
started = time.monotonic()
try:
result: ChatResult = call_with_resilience(
lambda: provider.chat(messages=safe_messages, model=model, params=params),
retries=settings.HUB_AI_MAX_RETRIES,
breaker=_breaker,
)
except ProviderError as error:
LlmInvocation.objects.create(
channel=channel, product=channel.product, purpose=purpose, operation="chat", model=model,
status=LlmInvocationStatus.ERROR, error=str(error)[:1000],
latency_ms=int((time.monotonic() - started) * 1000),
)
raise
LlmInvocation.objects.create(
channel=channel, product=channel.product, purpose=purpose, operation="chat", model=result.model,
prompt_tokens=result.prompt_tokens, completion_tokens=result.completion_tokens, total_tokens=result.total_tokens,
cost_micros=result.cost_micros or pricing.cost_micros(result.model, result.prompt_tokens, result.completion_tokens),
latency_ms=int((time.monotonic() - started) * 1000), status=LlmInvocationStatus.SUCCESS,
used_fragment_ids=used_fragment_ids or [],
)
return result
def embed_texts(*, channel=None, texts: list[str], model: str, purpose: str = "retrieval") -> list[EmbeddingResult]:
# Знания авторские (не клиентские PII), поэтому redaction не требуется.
provider = get_provider()
results: list[EmbeddingResult] = call_with_resilience(
lambda: provider.embed(texts=texts, model=model),
retries=settings.HUB_AI_MAX_RETRIES,
breaker=_breaker,
)
tokens = sum(result.tokens for result in results)
LlmInvocation.objects.create(
channel=channel, product=(channel.product if channel else None), purpose=purpose, operation="embedding", model=model,
prompt_tokens=tokens, total_tokens=tokens, cost_micros=pricing.cost_micros(model, tokens, 0),
status=LlmInvocationStatus.SUCCESS,
)
return results