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>
9.7 KiB
Plan: Tenant AI Chat Agent (Actionable)
Context
Each tenant has Azure OpenAI credentials stored in tenant_config JSONB. The goal is an actionable AI agent where users can type natural language commands to control the render pipeline — create orders, dispatch renders, check status, set overrides — scoped to their tenant.
Example interactions:
- "Render all Kugellager products as WebP at 1024x1024"
- "What's the status of my last order?"
- "Set material override to Steel-Bare on order SA-2026-00160"
- "How many renders failed this week?"
The agent uses function calling (Azure OpenAI tool use) — the LLM decides which API action to execute, the backend executes it, and returns the result. Tenants are fully isolated — each uses their own Azure API key and only sees their own data.
What exists:
- Per-tenant Azure OpenAI credentials in
tenant_configJSONB - WebSocket system scoped by tenant for real-time events
ai_validationCelery queue (concurrency=8)- Azure OpenAI integration boilerplate in
azure_ai.py
Affected Files
| File | Change |
|---|---|
backend/app/models/chat.py |
NEW — ChatMessage model |
backend/app/models/__init__.py |
Import ChatMessage |
backend/app/api/routers/chat.py |
NEW — Chat API endpoints |
backend/app/services/chat_service.py |
NEW — Azure OpenAI chat + DB context |
backend/app/main.py |
Register chat router |
backend/alembic/versions/XXX_add_chat_messages.py |
Migration |
frontend/src/api/chat.ts |
NEW — Chat API types + functions |
frontend/src/components/chat/ChatPanel.tsx |
NEW — Chat UI component |
frontend/src/components/layout/Layout.tsx |
Add chat toggle button |
Tasks (in order)
[ ] Task 1: ChatMessage model + migration
- File:
backend/app/models/chat.py(new) - What: Create a ChatMessage model:
class ChatMessage(Base): __tablename__ = "chat_messages" id: UUID PK tenant_id: UUID FK → tenants.id (nullable, indexed) user_id: UUID FK → users.id (nullable) session_id: UUID (groups messages in a conversation, indexed) role: String(20) — "user", "assistant", "system" content: Text context_type: String(50) nullable — "order", "product", "general" context_id: UUID nullable — order_id or product_id token_count: Integer nullable — track usage created_at: DateTime - Also: Import in
backend/app/models/__init__.py - Migration:
alembic revision --autogenerate -m "add chat_messages table" - Acceptance gate: Table exists in DB; model importable
- Dependencies: None
[ ] Task 2: Chat service — Azure OpenAI with function calling
-
File:
backend/app/services/chat_service.py(new) -
What: Service with Azure OpenAI tool use / function calling:
- Takes a user message + session_id + tenant_id + user_id
- Loads tenant Azure credentials from
tenant_config - Defines tools the LLM can call (JSON schema for each):
list_orders(status, limit)— list tenant's orderssearch_products(query, category, limit)— search productscreate_order(product_ids, output_type_name, render_overrides, material_override)— create & submitdispatch_renders(order_id)— dispatch renders for an orderget_order_status(order_id)— check render progressset_material_override(order_id, material_name)— batch material overrideset_render_overrides(order_id, overrides)— batch render overridesget_render_stats()— throughput statscheck_materials(order_id)— unmapped materials checkquery_database(sql)— read-only SQL (SELECT only, tenant-scoped)
- Calls Azure OpenAI with
toolsparameter — the LLM decides which tool to call - Executes the tool call internally (same functions as MCP server but tenant-scoped)
- Returns tool result to LLM for a natural language response
- Stores conversation in ChatMessage table
Tenant isolation: All DB queries filter by
tenant_id. Thequery_databasetool auto-appendsWHERE tenant_id = '{tenant_id}'or validates tenant scope.Tool execution: Uses the existing backend API functions directly (not HTTP calls) — import from the routers/services.
tools = [ { "type": "function", "function": { "name": "search_products", "description": "Search products by name, PIM-ID, or category", "parameters": { "type": "object", "properties": { "query": {"type": "string"}, "category": {"type": "string"}, } } } }, # ... more tools ] response = client.chat.completions.create( model=deployment, messages=messages, tools=tools, tool_choice="auto", ) # Handle tool_calls in response, execute, return result -
Acceptance gate: User can say "show my last 5 orders" and get real data back via function calling
-
Dependencies: Task 1
[ ] Task 3: Chat API endpoints
- File:
backend/app/api/routers/chat.py(new) - What: FastAPI router with endpoints:
POST /api/chat/messages— send a message, get AI response- Body:
{ message: str, session_id: str | None, context_type: str | None, context_id: str | None } - Creates session_id if not provided
- Returns:
{ session_id: str, message: ChatMessageOut, response: ChatMessageOut } - Auth:
get_current_user— uses user's tenant AI config
- Body:
GET /api/chat/sessions— list user's chat sessions- Returns:
[{ session_id, last_message, message_count, created_at }]
- Returns:
GET /api/chat/sessions/{session_id}/messages— get conversation history- Returns:
[{ id, role, content, created_at }]
- Returns:
DELETE /api/chat/sessions/{session_id}— delete a conversation
- Also: Register router in
backend/app/main.py - Acceptance gate: POST /api/chat/messages returns an AI response using tenant credentials
- Dependencies: Task 2
[ ] Task 4: Frontend — Chat API types
- File:
frontend/src/api/chat.ts(new) - What: TypeScript interfaces and API functions:
interface ChatMessage { id: string; role: 'user' | 'assistant' | 'system'; content: string; created_at: string } interface ChatSession { session_id: string; last_message: string; message_count: number; created_at: string } interface ChatResponse { session_id: string; message: ChatMessage; response: ChatMessage } function sendMessage(message: string, sessionId?: string, contextType?: string, contextId?: string): Promise<ChatResponse> function getSessions(): Promise<ChatSession[]> function getSessionMessages(sessionId: string): Promise<ChatMessage[]> function deleteSession(sessionId: string): Promise<void> - Acceptance gate: Types compile; functions callable
- Dependencies: Task 3
[ ] Task 5: Frontend — ChatPanel component
-
File:
frontend/src/components/chat/ChatPanel.tsx(new) -
What: Slide-out chat panel (right side, similar to notification panels in modern apps):
- Header: "AI Assistant" title + close button + session selector
- Message list: Scrollable area with role-based styling:
- User messages: right-aligned, accent background
- Assistant messages: left-aligned, surface background, markdown support
- Timestamps below each message
- Input area: Text input + send button (Enter to send)
- Loading state: Typing indicator while waiting for AI response
- Session management: "New conversation" button, session history dropdown
- Context awareness: When opened from an order/product page, auto-includes context
Styling:
- Fixed right panel (w-96, full height)
- Backdrop overlay on mobile
- Smooth slide-in animation
- Use existing CSS variables (surface, content, accent)
- lucide-react icons (MessageSquare, Send, Loader2, X, Plus)
-
Acceptance gate: Panel opens/closes, messages send and display, AI responds
-
Dependencies: Task 4
[ ] Task 6: Frontend — Chat toggle in Layout
- File:
frontend/src/components/layout/Layout.tsx - What: Add a chat toggle button:
- Floating button in bottom-right corner (or in the sidebar)
- Icon:
MessageSquarefrom lucide-react - Badge with unread count (optional, for future)
- Click toggles ChatPanel visibility
- Only show when tenant has
ai_enabled = true
- Acceptance gate: Button visible for users with AI-enabled tenant; clicking opens/closes ChatPanel
- Dependencies: Task 5
Migration Check
Yes — one new table chat_messages with UUID PK, FK to tenants and users.
Order Recommendation
- Backend model + migration (Task 1)
- Backend service (Task 2)
- Backend API (Task 3)
- Frontend types (Task 4)
- Frontend chat UI (Task 5)
- Frontend layout integration (Task 6)
Risks / Open Questions
-
Azure OpenAI availability: If tenant hasn't configured AI credentials, the chat should show a helpful message ("AI not configured — ask your admin to set up Azure OpenAI in Tenant Settings")
-
Token costs: Each message uses Azure OpenAI tokens. Consider adding token counting and a configurable monthly limit per tenant.
-
Context enrichment: The system prompt could include live data (order counts, render status). This makes the AI more helpful but costs more tokens. Start simple, enhance later.
-
Streaming responses: Azure OpenAI supports streaming. V1 uses a simple request/response. V2 could stream via WebSocket for real-time typing effect.
-
openai package: The
openaiPython package must be installed in the backend container. Check if it's already a dependency (it may be viaazure_ai.py).