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.
7. Respond in the same language the user writes in.
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) ────────────────────────
@@ -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",
"function": {
@@ -323,6 +342,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 == "list_materials":
return await _tool_list_materials(db, tenant_id, **arguments)
elif name == "find_product_renders":
return await _tool_find_product_renders(db, tenant_id, **arguments)
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}"})
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(
db: AsyncSession, tenant_id: str,
product_name: str = "", product_id: str = "", transparent_only: bool = False,