feat: rich product metadata extraction from STEP files

Extract volume, surface area, part count, assembly hierarchy, and
complexity from STEP files via OCC B-rep analysis.

Backend:
- extract_rich_metadata() in step_processor.py: computes per-part volume
  (BRepGProp), surface area, triangle/vertex count, assembly depth,
  instance count, complexity score, largest part identification
- cad_metadata JSONB column on Product model (DB migration)
- Auto-populated during STEP processing (non-fatal, 10s timeout)
- Also stored in cad_files.mesh_attributes["rich_metadata"]
- Batch re-extract endpoint: POST /admin/settings/reextract-rich-metadata

AI Agent:
- search_products returns part_count, volume_cm3, complexity, largest_part
- query_database tool description documents cad_metadata schema

Frontend:
- ProductDetail page: CAD Metadata section with stat cards
  (parts, volume, surface area, complexity, triangles, assembly depth)
- Admin System Tools: "Re-extract Rich Metadata" button for backfill

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-15 18:49:50 +01:00
parent 0ffc86589a
commit cfccdd5397
12 changed files with 645 additions and 170 deletions
+82 -168
View File
@@ -1,208 +1,122 @@
# Plan: Tenant AI Chat Agent (Actionable)
# Plan: Rich Product Metadata Extraction from STEP Files
## 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.
The AI chat agent was asked "What is the biggest product from my order?" and couldn't answer because dimensional data wasn't available in tool results. While `cad_files.mesh_attributes` already stores bounding box dimensions, much more metadata is extractable from STEP files via OCC that would make the AI agent and the product library significantly more useful.
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?"
**Currently extracted**: part names, bounding box (xyz), sharp edges, smooth angle
**Available but not extracted**: per-part volume, surface area, assembly hierarchy, instance counts, embedded colors, triangle counts, geometric complexity
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_config` JSONB
- WebSocket system scoped by tenant for real-time events
- `ai_validation` Celery queue (concurrency=8)
- Azure OpenAI integration boilerplate in `azure_ai.py`
**Goal**: Expand the STEP metadata extraction to compute richer product characteristics and store them in a structured `cad_metadata` JSONB field, accessible to the AI agent, product search, and frontend.
## 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 |
| `backend/app/services/step_processor.py` | Expand `extract_step_metadata()` with volume, surface area, hierarchy, complexity |
| `backend/app/domains/products/models.py` | Add `cad_metadata` JSONB column to Product |
| `backend/alembic/versions/XXX_add_cad_metadata.py` | Migration |
| `backend/app/domains/pipeline/tasks/extract_metadata.py` | Populate `cad_metadata` after STEP processing |
| `backend/app/domains/products/schemas.py` | Expose `cad_metadata` in ProductOut |
| `backend/app/services/chat_service.py` | Include metadata in search_products and system prompt |
| `frontend/src/pages/ProductDetail.tsx` | Display rich metadata (volume, part count, complexity) |
## Tasks (in order)
### [ ] Task 1: ChatMessage model + migration
### [ ] Task 1: Expand STEP metadata extraction
- **File**: `backend/app/models/chat.py` (new)
- **What**: Create a ChatMessage model:
- **File**: `backend/app/services/step_processor.py`
- **What**: Expand `extract_step_metadata()` to compute additional properties after the existing bbox/edge extraction. Add a new function `extract_rich_metadata(doc, shape_tool)` that returns:
```python
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
{
"part_count": 42, # Number of leaf parts
"assembly_depth": 3, # Max nesting depth
"total_volume_cm3": 1250.4, # Sum of all part volumes (cm³)
"total_surface_area_cm2": 3400.2, # Sum of all surface areas (cm²)
"total_triangle_count": 45000, # After tessellation
"total_vertex_count": 23000, # After tessellation
"largest_part": { # Part with largest volume
"name": "OuterRing",
"volume_cm3": 450.2,
},
"smallest_dimension_mm": 0.5, # Smallest bbox dimension across all parts
"instance_count": 36, # Total instances (parts may repeat)
"unique_part_count": 12, # Distinct shapes
"complexity_score": "high", # low/medium/high based on triangle count
}
```
- **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
Use OCC:
- `GProp_GProps` + `BRepGProp.VolumeProperties()` for volume
- `BRepGProp.SurfaceProperties()` for surface area
- `Poly_Triangulation` for triangle/vertex counts (after tessellation)
- Assembly tree walk (already done in `_collect_part_key_map`) for hierarchy depth + instance count
- **Acceptance gate**: `extract_rich_metadata()` returns all fields for a test STEP file
- **Dependencies**: None
### [ ] Task 2: Chat service — Azure OpenAI with function calling
### [ ] Task 2: Add cad_metadata column to Product model
- **File**: `backend/app/services/chat_service.py` (new)
- **What**: Service with Azure OpenAI **tool use / function calling**:
1. Takes a user message + session_id + tenant_id + user_id
2. Loads tenant Azure credentials from `tenant_config`
3. Defines **tools** the LLM can call (JSON schema for each):
- `list_orders(status, limit)` — list tenant's orders
- `search_products(query, category, limit)` — search products
- `create_order(product_ids, output_type_name, render_overrides, material_override)` — create & submit
- `dispatch_renders(order_id)` — dispatch renders for an order
- `get_order_status(order_id)` — check render progress
- `set_material_override(order_id, material_name)` — batch material override
- `set_render_overrides(order_id, overrides)` — batch render overrides
- `get_render_stats()` — throughput stats
- `check_materials(order_id)` — unmapped materials check
- `query_database(sql)` — read-only SQL (SELECT only, tenant-scoped)
4. Calls Azure OpenAI with `tools` parameter — the LLM decides which tool to call
5. Executes the tool call internally (same functions as MCP server but tenant-scoped)
6. Returns tool result to LLM for a natural language response
7. Stores conversation in ChatMessage table
- **File**: `backend/app/domains/products/models.py`
- **What**: Add `cad_metadata: Mapped[dict | None] = mapped_column(JSONB, nullable=True, default=None)` to the Product model. This stores the rich metadata at the product level (not cad_file) because products are the user-facing entity.
- **Migration**: `alembic revision --autogenerate -m "add cad_metadata to products"`
- **Also**: Add to ProductOut schema in `backend/app/domains/products/schemas.py`
- **Acceptance gate**: Column exists, schema includes it
- **Dependencies**: None
**Tenant isolation**: All DB queries filter by `tenant_id`. The `query_database` tool auto-appends `WHERE tenant_id = '{tenant_id}'` or validates tenant scope.
### [ ] Task 3: Populate cad_metadata during STEP processing
**Tool execution**: Uses the existing backend API functions directly (not HTTP calls) — import from the routers/services.
- **File**: `backend/app/domains/pipeline/tasks/extract_metadata.py`
- **What**: After `process_step_file` extracts objects and queues thumbnail, call `extract_rich_metadata()` and store the result on the Product's `cad_metadata` field. Also store it on `cad_files.mesh_attributes` (merge with existing data).
- **Also**: Add a "reextract metadata" admin action that re-runs this for all existing products
- **Acceptance gate**: After STEP processing, product.cad_metadata is populated with volume, part_count, etc.
- **Dependencies**: Tasks 1, 2
```python
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 4: Expose metadata in AI agent tools
### [ ] 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
- `GET /api/chat/sessions` — list user's chat sessions
- Returns: `[{ session_id, last_message, message_count, created_at }]`
- `GET /api/chat/sessions/{session_id}/messages` — get conversation history
- Returns: `[{ id, role, content, created_at }]`
- `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:
```typescript
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
- **File**: `backend/app/services/chat_service.py`
- **What**:
1. Update `_tool_search_products()` to include `cad_metadata` fields (part_count, total_volume_cm3, complexity_score) in results
2. Update `query_database` tool description to mention `products.cad_metadata` JSONB field
3. Update system prompt to mention available metadata
- **Acceptance gate**: AI agent can answer "What is the biggest product?" using volume data
- **Dependencies**: Task 3
### [ ] Task 5: Frontend — ChatPanel component
### [ ] Task 5: Display rich metadata on ProductDetail page
- **File**: `frontend/src/components/chat/ChatPanel.tsx` (new)
- **What**: Slide-out chat panel (right side, similar to notification panels in modern apps):
1. **Header**: "AI Assistant" title + close button + session selector
2. **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
3. **Input area**: Text input + send button (Enter to send)
4. **Loading state**: Typing indicator while waiting for AI response
5. **Session management**: "New conversation" button, session history dropdown
6. **Context awareness**: When opened from an order/product page, auto-includes context
- **File**: `frontend/src/pages/ProductDetail.tsx`
- **What**: Add a "CAD Metadata" section on the product detail page showing:
- Part count + unique parts + instances
- Total volume (cm³) + surface area (cm²)
- Largest part name + volume
- Complexity score badge (low/medium/high)
- Triangle/vertex count
- Assembly depth
- **Acceptance gate**: Metadata displayed on product page; empty gracefully when not available
- **Dependencies**: Task 2
**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: Batch re-extract metadata for existing products
### [ ] Task 6: Frontend — Chat toggle in Layout
- **File**: `frontend/src/components/layout/Layout.tsx`
- **What**: Add a chat toggle button:
1. Floating button in bottom-right corner (or in the sidebar)
2. Icon: `MessageSquare` from lucide-react
3. Badge with unread count (optional, for future)
4. Click toggles ChatPanel visibility
5. 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
- **File**: `backend/app/api/routers/admin.py`
- **What**: Add a "Re-extract Rich Metadata" button in System Tools that queues a Celery task to re-process all completed STEP files and populate `cad_metadata` for all products.
- **Acceptance gate**: Button triggers batch job; existing products get metadata populated
- **Dependencies**: Tasks 1, 3
## Migration Check
**Yes** — one new table `chat_messages` with UUID PK, FK to tenants and users.
**Yes** — one new JSONB column on `products` table.
## Order Recommendation
1. Backend model + migration (Task 1)
2. Backend service (Task 2)
3. Backend API (Task 3)
4. Frontend types (Task 4)
5. Frontend chat UI (Task 5)
6. Frontend layout integration (Task 6)
1. Task 1 (extraction logic) + Task 2 (model + migration) — parallel
2. Task 3 (wire up in pipeline)
3. Task 4 (AI agent) + Task 5 (frontend) — parallel
4. Task 6 (batch re-extract)
## Risks / Open Questions
1. **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")
1. **Volume calculation accuracy**: OCC `BRepGProp` computes exact B-rep volume, not mesh-based. This is accurate but can be slow for very complex shapes. Cap at 5s per file.
2. **Token costs**: Each message uses Azure OpenAI tokens. Consider adding token counting and a configurable monthly limit per tenant.
2. **Performance**: Rich metadata extraction adds ~100-500ms per STEP file. This is acceptable since STEP processing already takes 1-5s.
3. **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.
3. **Existing products**: ~45 products with STEP files need backfill. Task 6 handles this.
4. **Streaming responses**: Azure OpenAI supports streaming. V1 uses a simple request/response. V2 could stream via WebSocket for real-time typing effect.
5. **openai package**: The `openai` Python package must be installed in the backend container. Check if it's already a dependency (it may be via `azure_ai.py`).
4. **Triangle count varies**: Depends on tessellation settings (deflection angles). Store the count at the current tessellation quality for reference, with a note that it's approximate.