feat: chat agent can find and show existing product renders

Added find_product_renders tool to the AI agent:
- Searches completed renders by product name/ID
- Returns viewable image URLs (relative paths)
- Supports transparent_only filter
- AI formats results as Markdown images/links in chat

Frontend:
- ChatPanel ReactMarkdown renders <img> and <a> tags
- Images shown inline (max 200px height, rounded, bordered)
- Links open in new tab with accent color

System prompt updated to instruct AI to use Markdown image syntax
when showing renders: ![description](/renders/...)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-15 19:52:45 +01:00
parent 531994cccd
commit 29f7103a8b
2 changed files with 100 additions and 1 deletions
+98 -1
View File
@@ -29,7 +29,9 @@ You can:
- Check material mapping status
- Query the database for statistics
Always be concise and helpful. Execute actions immediately without asking for confirmation — the user expects you to act, not ask. If creating an order, also submit and dispatch it automatically. Combine multiple steps into one action when possible. Respond in the same language the user writes in."""
Always be concise and helpful. Execute actions immediately without asking for confirmation — the user expects you to act, not ask. If creating an order, also submit and dispatch it automatically. Combine multiple steps into one action when possible. Respond in the same language the user writes in.
When the user asks to SEE a product image or render, use find_product_renders to get URLs, then format them as Markdown image links: ![description](url) or as clickable links: [View render](url). The URLs are relative paths like /renders/... that work in the browser."""
# ── Tool definitions (OpenAI function-calling schema) ────────────────────────
@@ -232,6 +234,33 @@ TOOLS = [
},
},
},
{
"type": "function",
"function": {
"name": "find_product_renders",
"description": "Find existing render images for a product. Returns URLs that can be shown to the user as clickable links or inline images. Use this when the user asks to SEE a product image, rendering, or thumbnail.",
"parameters": {
"type": "object",
"properties": {
"product_name": {
"type": "string",
"description": "Product name or partial match.",
"default": "",
},
"product_id": {
"type": "string",
"description": "Product UUID (if known).",
"default": "",
},
"transparent_only": {
"type": "boolean",
"description": "Only return renders with transparent background.",
"default": False,
},
},
},
},
},
]
@@ -287,6 +316,8 @@ async def _execute_tool(
return await _tool_get_render_stats(db, tenant_id)
elif name == "check_materials":
return await _tool_check_materials(db, tenant_id, user_id, **arguments)
elif name == "find_product_renders":
return await _tool_find_product_renders(db, tenant_id, **arguments)
elif name == "query_database":
return await _tool_query_database(db, tenant_id, **arguments)
else:
@@ -596,6 +627,72 @@ async def _tool_check_materials(db: AsyncSession, tenant_id: str, user_id: str =
return json.dumps({"error": f"Failed to check materials: {exc}"})
async def _tool_find_product_renders(
db: AsyncSession, tenant_id: str,
product_name: str = "", product_id: str = "", transparent_only: bool = False,
) -> str:
"""Find existing render images for a product, returning viewable URLs."""
conditions = ["o.tenant_id = :tenant_id", "ol.render_status = 'completed'", "ol.result_path IS NOT NULL"]
params: dict = {"tenant_id": tenant_id}
if product_id:
conditions.append("p.id = :pid")
params["pid"] = product_id
elif product_name:
conditions.append("p.name ILIKE :pname")
params["pname"] = f"%{product_name}%"
else:
return json.dumps({"error": "Provide product_name or product_id"})
if transparent_only:
conditions.append("ot.transparent_bg = true")
where = " AND ".join(conditions)
sql = f"""
SELECT p.name AS product_name, p.id AS product_id,
ot.name AS output_type, ot.transparent_bg,
ol.result_path, ol.id AS line_id,
ol.render_completed_at
FROM order_lines ol
JOIN orders o ON o.id = ol.order_id
JOIN products p ON p.id = ol.product_id
LEFT JOIN output_types ot ON ot.id = ol.output_type_id
WHERE {where}
ORDER BY ol.render_completed_at DESC
LIMIT 10
"""
result = await db.execute(text(sql), params)
rows = result.mappings().all()
if not rows:
return json.dumps({"message": "No completed renders found for this product.", "renders": []})
renders = []
for r in rows:
path = r["result_path"] or ""
# Convert internal path to servable URL
url = None
if "/renders/" in path:
url = path[path.index("/renders/"):]
elif "/thumbnails/" in path:
url = path[path.index("/thumbnails/"):]
renders.append({
"product_name": r["product_name"],
"product_id": str(r["product_id"]),
"output_type": r["output_type"],
"transparent_bg": r["transparent_bg"],
"image_url": url,
"line_id": str(r["line_id"]),
"rendered_at": str(r["render_completed_at"]),
})
return json.dumps({
"message": f"Found {len(renders)} render(s). Show the user these image URLs as clickable links.",
"renders": renders,
}, indent=2, default=str)
async def _tool_query_database(db: AsyncSession, tenant_id: str, sql: str = "") -> str:
"""Execute a read-only SQL query, tenant-scoped."""
sql_upper = sql.strip().upper()