Files
chatballs/apps/backend/hub_platform/integrations/models.py
T
AndreyandClaude Opus 4.8 f0b03d91bc feat(conversations): M2a — live MAX inbound -> AI -> reply
Conversations domain (ADR-HUB-0002/0003/0006): Contact, ConnectionIdentity,
Conversation (lifecycle/control_mode/expected_responder + assigned_operator)
and Message. Bind a messenger Integration to a Channel (+ poll_marker). MAX
transport does long-poll /updates and POST /messages (defensive parsing, raw
update logged). Worker now polls MAX connections every 3s and ingests inbound
idempotently (InboxEvent): contact/conversation/message, and when the dialog is
AI-controlled runs run_channel_turn and replies back to MAX.

Verified inbound polling against the live edevs MAX bot (received bot_started).
Operator takeover UI is M2b.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 11:42:11 +03:00

63 lines
2.9 KiB
Python

from django.db import models
from hub_platform.identity.crypto import EncryptedCharField
# Интеграции: провайдеры (LLM) и подключения (боты/виджеты). ADR-HUB-0020.
# Привязка подключения к каналу обработки появляется в M1 (ADR-HUB-0019).
class IntegrationKind(models.TextChoices):
LLM_PROVIDER = "LLM_PROVIDER", "LLM-провайдер"
MESSENGER = "MESSENGER", "Подключение-мессенджер"
class IntegrationProvider(models.TextChoices):
OPENROUTER = "OPENROUTER", "OpenRouter"
MAX = "MAX", "MAX"
TELEGRAM = "TELEGRAM", "Telegram"
WEB = "WEB", "Web-виджет"
class IntegrationStatus(models.TextChoices):
UNCHECKED = "UNCHECKED", "Не проверено"
OK = "OK", "Подключено"
ERROR = "ERROR", "Ошибка"
# Какой провайдер к какому роду относится.
PROVIDER_KIND = {
IntegrationProvider.OPENROUTER: IntegrationKind.LLM_PROVIDER,
IntegrationProvider.MAX: IntegrationKind.MESSENGER,
IntegrationProvider.TELEGRAM: IntegrationKind.MESSENGER,
IntegrationProvider.WEB: IntegrationKind.MESSENGER,
}
class Integration(models.Model):
organization = models.ForeignKey("identity.Organization", on_delete=models.PROTECT, related_name="integrations")
kind = models.CharField(max_length=16, choices=IntegrationKind.choices)
provider = models.CharField(max_length=16, choices=IntegrationProvider.choices)
name = models.CharField(max_length=255)
# Зашифрованный секрет: ключ провайдера или токен бота (Fernet).
secret = EncryptedCharField(max_length=1024, blank=True)
# Несекретная конфигурация: base_url, модель по умолчанию и т.п.
config = models.JSONField(default=dict, blank=True)
status = models.CharField(max_length=16, choices=IntegrationStatus.choices, default=IntegrationStatus.UNCHECKED)
# Подключение (бот/виджет) привязано к каналу обработки (ADR-HUB-0019, M2).
channel = models.ForeignKey("channels.Channel", on_delete=models.SET_NULL, null=True, blank=True, related_name="connections")
# Курсор Long Polling (marker MAX / offset Telegram).
poll_marker = models.CharField(max_length=64, blank=True)
last_checked_at = models.DateTimeField(null=True, blank=True)
last_error = models.TextField(blank=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
ordering = ["provider", "name"]
constraints = [
models.UniqueConstraint(fields=["organization", "provider", "name"], name="uniq_integration_org_provider_name"),
]
def __str__(self) -> str:
return f"{self.provider}:{self.name}"