feat: chat agent knows current page context (order/product)
Frontend: Layout extracts order/product UUID from URL path and passes
to ChatPanel as contextType/contextId. When on /orders/{uuid}, the
chat knows which order you're viewing.
Backend: System prompt now loads actual entity data for the context:
- Order: order_number, status, line counts (completed/processing/failed)
- Product: name, PIM-ID, category, STEP file status
The AI understands "this order", "current product" etc. Example:
"What's the status of this order?" → agent knows you mean SA-2026-00164
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -682,8 +682,60 @@ async def chat_with_agent(
|
|||||||
|
|
||||||
# Build context-aware system prompt
|
# Build context-aware system prompt
|
||||||
system_content = SYSTEM_PROMPT
|
system_content = SYSTEM_PROMPT
|
||||||
if context_type and context_id:
|
|
||||||
system_content += f"\n\nCurrent context: {context_type} {context_id}"
|
# Load entity details for the current page context
|
||||||
|
if context_type == "order" and context_id:
|
||||||
|
try:
|
||||||
|
ctx_result = await db.execute(
|
||||||
|
text("""
|
||||||
|
SELECT o.order_number, o.status, o.notes, o.created_at,
|
||||||
|
COUNT(ol.id) AS line_count,
|
||||||
|
COUNT(ol.id) FILTER (WHERE ol.render_status = 'completed') AS completed,
|
||||||
|
COUNT(ol.id) FILTER (WHERE ol.render_status = 'processing') AS processing,
|
||||||
|
COUNT(ol.id) FILTER (WHERE ol.render_status = 'failed') AS failed,
|
||||||
|
COUNT(ol.id) FILTER (WHERE ol.render_status = 'pending') AS pending
|
||||||
|
FROM orders o
|
||||||
|
LEFT JOIN order_lines ol ON ol.order_id = o.id
|
||||||
|
WHERE o.id = :ctx_id AND o.tenant_id = :tid
|
||||||
|
GROUP BY o.id
|
||||||
|
"""),
|
||||||
|
{"ctx_id": context_id, "tid": tenant_id},
|
||||||
|
)
|
||||||
|
row = ctx_result.mappings().first()
|
||||||
|
if row:
|
||||||
|
system_content += (
|
||||||
|
f"\n\nThe user is currently viewing order {row['order_number']} "
|
||||||
|
f"(ID: {context_id}, status: {row['status']}, "
|
||||||
|
f"{row['line_count']} lines: {row['completed']} completed, "
|
||||||
|
f"{row['processing']} processing, {row['failed']} failed, "
|
||||||
|
f"{row['pending']} pending). "
|
||||||
|
f"When the user says 'this order' or 'current order', they mean {row['order_number']} ({context_id})."
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
system_content += f"\n\nThe user is viewing order {context_id}."
|
||||||
|
elif context_type == "product" and context_id:
|
||||||
|
try:
|
||||||
|
ctx_result = await db.execute(
|
||||||
|
text("""
|
||||||
|
SELECT p.name, p.pim_id, p.category_key, p.baureihe,
|
||||||
|
CASE WHEN p.cad_file_id IS NOT NULL THEN true ELSE false END AS has_step
|
||||||
|
FROM products p
|
||||||
|
WHERE p.id = :ctx_id AND p.tenant_id = :tid
|
||||||
|
"""),
|
||||||
|
{"ctx_id": context_id, "tid": tenant_id},
|
||||||
|
)
|
||||||
|
row = ctx_result.mappings().first()
|
||||||
|
if row:
|
||||||
|
system_content += (
|
||||||
|
f"\n\nThe user is currently viewing product '{row['name']}' "
|
||||||
|
f"(ID: {context_id}, PIM-ID: {row['pim_id']}, "
|
||||||
|
f"category: {row['category_key']}, "
|
||||||
|
f"has STEP: {row['has_step']}). "
|
||||||
|
f"When the user says 'this product' or 'current product', they mean '{row['name']}' ({context_id})."
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
system_content += f"\n\nThe user is viewing product {context_id}."
|
||||||
|
|
||||||
system_content += f"\n\nThe user's tenant_id is '{tenant_id}'. Always filter queries by this tenant_id."
|
system_content += f"\n\nThe user's tenant_id is '{tenant_id}'. Always filter queries by this tenant_id."
|
||||||
|
|
||||||
messages: list[dict] = [{"role": "system", "content": system_content}]
|
messages: list[dict] = [{"role": "system", "content": system_content}]
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Outlet, NavLink, useNavigate, Link } from 'react-router-dom'
|
import { Outlet, NavLink, useNavigate, Link, useLocation } from 'react-router-dom'
|
||||||
import { LayoutDashboard, Package, Settings, LogOut, FlaskConical, Activity, Library, Plus, SlidersHorizontal, Building2, GitBranch, Image, BellRing, Receipt, Server, Upload, Menu, X, MessageSquare } from 'lucide-react'
|
import { LayoutDashboard, Package, Settings, LogOut, FlaskConical, Activity, Library, Plus, SlidersHorizontal, Building2, GitBranch, Image, BellRing, Receipt, Server, Upload, Menu, X, MessageSquare } from 'lucide-react'
|
||||||
import { useAuthStore, isAdmin as checkIsAdmin, isPrivileged as checkIsPrivileged } from '../../store/auth'
|
import { useAuthStore, isAdmin as checkIsAdmin, isPrivileged as checkIsPrivileged } from '../../store/auth'
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
@@ -36,9 +36,20 @@ const adminOnlyNav = [
|
|||||||
export default function Layout() {
|
export default function Layout() {
|
||||||
const { user, logout } = useAuthStore()
|
const { user, logout } = useAuthStore()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
const location = useLocation()
|
||||||
const [sidebarOpen, setSidebarOpen] = useState(false)
|
const [sidebarOpen, setSidebarOpen] = useState(false)
|
||||||
const [chatOpen, setChatOpen] = useState(false)
|
const [chatOpen, setChatOpen] = useState(false)
|
||||||
|
|
||||||
|
// Extract page context from URL for the chat agent
|
||||||
|
const chatContext = (() => {
|
||||||
|
const path = location.pathname
|
||||||
|
const orderMatch = path.match(/^\/orders\/([0-9a-f-]{36})/)
|
||||||
|
if (orderMatch) return { type: 'order', id: orderMatch[1] }
|
||||||
|
const productMatch = path.match(/^\/products\/([0-9a-f-]{36})/)
|
||||||
|
if (productMatch) return { type: 'product', id: productMatch[1] }
|
||||||
|
return { type: undefined, id: undefined }
|
||||||
|
})()
|
||||||
|
|
||||||
const { data: activity } = useQuery({
|
const { data: activity } = useQuery({
|
||||||
queryKey: ['worker-activity'],
|
queryKey: ['worker-activity'],
|
||||||
queryFn: getWorkerActivity,
|
queryFn: getWorkerActivity,
|
||||||
@@ -260,7 +271,7 @@ export default function Layout() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Chat panel */}
|
{/* Chat panel */}
|
||||||
{chatOpen && <ChatPanel open={chatOpen} onClose={() => setChatOpen(false)} />}
|
{chatOpen && <ChatPanel open={chatOpen} onClose={() => setChatOpen(false)} contextType={chatContext.type} contextId={chatContext.id} />}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user