Files
Hartmut 59ce61098c feat: tenant AI chat agent with function calling
Actionable AI assistant that uses per-tenant Azure OpenAI credentials
to execute natural language commands against the render pipeline.

Backend:
- ChatMessage model + migration (session-based conversations)
- Chat service with 10 OpenAI function-calling tools:
  list_orders, search_products, create_order, dispatch_renders,
  get_order_status, set_material_override, set_render_overrides,
  get_render_stats, check_materials, query_database
- All tools tenant-scoped (queries filtered by tenant_id)
- Write operations use httpx to call backend API internally
- Chat API: POST /chat/messages, GET /chat/sessions, DELETE session
- Conversation history preserved in DB (last 50 messages per session)

Frontend:
- Slide-out ChatPanel (right side, w-96, animated)
- User/assistant message styling with avatars and timestamps
- Session management (new chat, session history, delete)
- Typing indicator while waiting for AI response
- Floating chat button in bottom-right corner
- Error state for unconfigured AI tenants

Example: "Render all Kugellager products as WebP at 1024x1024"
→ Agent calls search_products + create_order + dispatch_renders

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 12:46:21 +01:00

29 lines
1.4 KiB
Python

"""Chat message model for tenant AI agent conversations."""
import uuid
from datetime import datetime
from sqlalchemy import String, DateTime, Text, ForeignKey, Integer
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy.dialects.postgresql import UUID
from app.database import Base
class ChatMessage(Base):
__tablename__ = "chat_messages"
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
tenant_id: Mapped[uuid.UUID | None] = mapped_column(
UUID(as_uuid=True), ForeignKey("tenants.id", ondelete="CASCADE"), nullable=True, index=True
)
user_id: Mapped[uuid.UUID | None] = mapped_column(
UUID(as_uuid=True), ForeignKey("users.id", ondelete="SET NULL"), nullable=True
)
session_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), nullable=False, index=True)
role: Mapped[str] = mapped_column(String(20), nullable=False) # "user", "assistant", "system"
content: Mapped[str] = mapped_column(Text, nullable=False)
context_type: Mapped[str | None] = mapped_column(String(50), nullable=True) # "order", "product", "general"
context_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), nullable=True)
token_count: Mapped[int | None] = mapped_column(Integer, nullable=True)
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, nullable=False)