feat: AI agent knows material library — list_materials tool with alias search

Added list_materials tool to the chat agent:
- Searches SCHAEFFLER library materials by name, description, or alias
- Returns material name + schaeffler_code + aliases
- Enables: "zeig mir ein Bild mit Durotect-Material" → agent searches
  for "durotect" → finds SCHAEFFLER_020101_Durotect-Blue → uses as
  material_override

System prompt updated with rules 10-11:
- Explains alias → library material mapping
- Always use full SCHAEFFLER name for material_override

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-16 10:05:05 +01:00
parent 02669c395c
commit ef4c8eefc9
+63 -1
View File
@@ -38,7 +38,9 @@ RULES:
6. Combine multiple steps into one action. If creating an order, also submit and dispatch it automatically. 6. Combine multiple steps into one action. If creating an order, also submit and dispatch it automatically.
7. Respond in the same language the user writes in. 7. Respond in the same language the user writes in.
8. Be concise — short answers are better than long ones. 8. Be concise — short answers are better than long ones.
9. When the user says "beliebig", "any", "random", "irgendein" — just pick one yourself, don't ask back.""" 9. When the user says "beliebig", "any", "random", "irgendein" — just pick one yourself, don't ask back.
10. Material system: Materials have SCHAEFFLER library names (e.g. SCHAEFFLER_020101_Durotect-Blue). Common names like "Durotect", "Stahl", "Bronze" are aliases that map to these library names. When the user asks for a material by a common name, use list_materials to find the correct SCHAEFFLER name, then use that for material_override.
11. When setting material_override, always use the full SCHAEFFLER library name (e.g. SCHAEFFLER_020101_Durotect-Blue), never the alias."""
# ── Tool definitions (OpenAI function-calling schema) ──────────────────────── # ── Tool definitions (OpenAI function-calling schema) ────────────────────────
@@ -241,6 +243,23 @@ TOOLS = [
}, },
}, },
}, },
{
"type": "function",
"function": {
"name": "list_materials",
"description": "List available SCHAEFFLER library materials with their aliases. Use this to find the correct material name for material_override. Materials have names like SCHAEFFLER_010101_Steel-Bare. Aliases map common names (Stahl, Bronze, Durotect, etc.) to these library materials. When user asks for a material by a common name, search aliases to find the correct SCHAEFFLER library material name.",
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "Search term to filter materials by name or alias (e.g. 'durotect', 'steel', 'bronze'). Empty for all.",
"default": "",
},
},
},
},
},
{ {
"type": "function", "type": "function",
"function": { "function": {
@@ -323,6 +342,8 @@ async def _execute_tool(
return await _tool_get_render_stats(db, tenant_id) return await _tool_get_render_stats(db, tenant_id)
elif name == "check_materials": elif name == "check_materials":
return await _tool_check_materials(db, tenant_id, user_id, **arguments) return await _tool_check_materials(db, tenant_id, user_id, **arguments)
elif name == "list_materials":
return await _tool_list_materials(db, tenant_id, **arguments)
elif name == "find_product_renders": elif name == "find_product_renders":
return await _tool_find_product_renders(db, tenant_id, **arguments) return await _tool_find_product_renders(db, tenant_id, **arguments)
elif name == "query_database": elif name == "query_database":
@@ -634,6 +655,47 @@ async def _tool_check_materials(db: AsyncSession, tenant_id: str, user_id: str =
return json.dumps({"error": f"Failed to check materials: {exc}"}) return json.dumps({"error": f"Failed to check materials: {exc}"})
async def _tool_list_materials(db: AsyncSession, tenant_id: str, query: str = "") -> str:
"""List library materials with their aliases."""
sql = """
SELECT m.id, m.name, m.schaeffler_code, m.description,
COALESCE(
(SELECT json_agg(ma.alias) FROM material_aliases ma WHERE ma.material_id = m.id),
'[]'::json
) AS aliases
FROM materials m
WHERE m.schaeffler_code IS NOT NULL
"""
params: dict = {}
if query:
sql += """
AND (m.name ILIKE :q OR m.description ILIKE :q
OR EXISTS (SELECT 1 FROM material_aliases ma WHERE ma.material_id = m.id AND ma.alias ILIKE :q))
"""
params["q"] = f"%{query}%"
sql += " ORDER BY m.name LIMIT 30"
result = await db.execute(text(sql), params)
rows = result.mappings().all()
if not rows:
return json.dumps({"message": f"No materials found matching '{query}'.", "materials": []})
materials = []
for r in rows:
aliases = r["aliases"] if isinstance(r["aliases"], list) else []
materials.append({
"name": r["name"],
"schaeffler_code": r["schaeffler_code"],
"description": r["description"],
"aliases": aliases[:10], # cap to avoid token bloat
})
return json.dumps({
"message": f"Found {len(materials)} material(s). Use the 'name' field for material_override.",
"materials": materials,
}, indent=2, default=str)
async def _tool_find_product_renders( async def _tool_find_product_renders(
db: AsyncSession, tenant_id: str, db: AsyncSession, tenant_id: str,
product_name: str = "", product_id: str = "", transparent_only: bool = False, product_name: str = "", product_id: str = "", transparent_only: bool = False,