Files
Mieterportal/n8n/assist-workflow.json
T
ah-admin 6eec3ac437 v1.37: KI-Assistent N8N-Workflow fertig eingerichtet; alle Formulare 2-spaltig mit neuem Design
- N8N: Workflow serverseitig in DB eingespielt (Webhook mp-assist, Code-Node mit
  Kimi-Tool-Calling, keine Credentials), E2E live verifiziert
- Formulare: shared FormSection-Komponente (2 gleich breite Spalten, Wide fuer
  Volllbreiten-Felder), 10 Formulare umgebaut, Dialoge verbreitert
- Design: Inter + Sora Fonts, Teal-Akzente, Icons in Sektionskoepfen
2026-10-04 21:30:34 +02:00

77 lines
6.9 KiB
JSON

{
"name": "Mieterportal - KI-Assistent (Kimi)",
"nodes": [
{
"parameters": {
"httpMethod": "POST",
"path": "mp-assist",
"responseMode": "responseNode",
"options": {}
},
"id": "ea0e27df-a1f6-49bd-9771-5cb3ec263ba2",
"name": "Webhook Portal",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"position": [
240,
300
],
"webhookId": "ea0e27df-a1f6-49bd-9771-5cb3ec263ba2"
},
{
"parameters": {
"mode": "runOnceForAllItems",
"jsCode": "\nconst body = $json.body || {};\nconst headers = $json.headers || {};\nconst frage = String(body.frage || '').trim();\nconst verlauf = Array.isArray(body.verlauf) ? body.verlauf : [];\n\n// Sicherung: Shared-Secret pruefen (nur internes Docker-Netz erreichbar)\nif (headers['x-assist-secret'] !== '<ASSIST_SHARED_SECRET>') {\n throw new Error('Ungueltiges Shared Secret');\n}\nif (!frage) return [{ json: { antwort: 'Leere Frage.' } }];\n\nconst PORTAL = 'http://mp-api:3000';\nconst ASSIST_KEY = '<ASSIST_KEY>';\nconst KIMI_URL = 'https://api.moonshot.ai/v1/chat/completions';\n\nconst TOOLS = [\n { type: 'function', function: { name: 'leerstaende',\n description: 'Liste der leerstehenden oder unklaren Wohneinheiten mit Haus, Nummer, Etage und Flaeche. Keine Parameter.',\n parameters: { type: 'object', properties: {}, additionalProperties: false } } },\n { type: 'function', function: { name: 'mieteinnahmen',\n description: 'Monatliches Miet-Soll (Warm- und Kaltmieten aller Mieter), gesamt und je Haus. Optional: haus (Teil des Hausnamens).',\n parameters: { type: 'object', properties: { haus: { type: 'string', description: 'optionaler Filter, Teil des Hausnamens' } }, additionalProperties: false } } },\n { type: 'function', function: { name: 'offene_schaeden',\n description: 'Offene Schadensmeldungen mit Titel, Kategorie, Prioritaet, Status, Mieter und Wohnung, nach Dringlichkeit sortiert. Keine Parameter.',\n parameters: { type: 'object', properties: {}, additionalProperties: false } } },\n { type: 'function', function: { name: 'mieter_kontakt',\n description: 'Kontaktdaten (Telefon, E-Mail) eines Mieters. Genau einen Parameter: name (Nach- oder Vorname) ODER wohnung (Hausadresse oder Wohnungsnummer).',\n parameters: { type: 'object', properties: {\n name: { type: 'string' },\n wohnung: { type: 'string' } }, additionalProperties: false } } },\n { type: 'function', function: { name: 'zaehler_anomalien',\n description: 'Zaehler mit Auffaelligkeiten im neuesten Verlaufseintrag (Ruecklauf, Stillstand, Verbrauchsspitze) mit Zaehlernummer, Art, Haus, Stand, Datum und Grund. Keine Parameter.',\n parameters: { type: 'object', properties: {}, additionalProperties: false } } },\n { type: 'function', function: { name: 'kosten',\n description: 'Kosten eines Jahres, gesamt und je Haus/Kostenart, aufgeteilt in umlegbar und nicht umlegbar. Pflicht: jahr (z. B. 2026), optional: haus.',\n parameters: { type: 'object', properties: {\n jahr: { type: 'integer', description: 'Pflicht, z. B. 2026' },\n haus: { type: 'string' } }, required: ['jahr'], additionalProperties: false } } },\n];\n\nasync function toolAusfuehren(name, args) {\n const parts = [];\n for (const k of Object.keys(args || {})) {\n const v = args[k];\n if (v !== undefined && v !== null && String(v).trim() !== '') parts.push(encodeURIComponent(k) + '=' + encodeURIComponent(String(v)));\n }\n const qs = parts.join('&');\n return await this.helpers.httpRequest({\n method: 'GET',\n url: PORTAL + '/api/assist/tools/' + name + (qs ? '?' + qs : ''),\n headers: { 'x-assist-key': ASSIST_KEY },\n json: true,\n });\n}\n\nconst messages = [\n { role: 'system', content: 'Du bist der KI-Assistent des Mieterportals \"Hinterstein Immobilien\". Du antwortest ausschliesslich der Verwaltung auf Deutsch, kurz und sachlich. Beantworte Fragen zum Wohnungsbestand, Mietern, Mieteinnahmen, Zaehlern, Kosten und Schadensmeldungen mit den bereitgestellten Tools. Rufe immer zuerst die passenden Tools ab, bevor du Zahlen nennst - halluziniere keine Daten. Wenn ein Tool keine Treffer liefert, sage das offen. Personenbezogene Daten (Telefon, E-Mail) nur nennen, wenn explizit danach gefragt wird.' },\n ...verlauf.slice(-20).map((m) => ({ role: m.rolle === 'user' ? 'user' : 'assistant', content: String(m.text).slice(0, 4000) })),\n { role: 'user', content: frage },\n];\n\nlet antwort = null;\ntry {\nfor (let runde = 0; runde < 6 && !antwort; runde++) {\n const resp = await this.helpers.httpRequest({\n method: 'POST',\n url: KIMI_URL,\n headers: { Authorization: 'Bearer ' + '<KIMI_API_KEY>', 'Content-Type': 'application/json' },\n body: {\n model: 'kimi-k2.6',\n messages,\n tools: TOOLS,\n },\n json: true,\n timeout: 60000,\n });\n const msg = (resp.choices || [])[0]?.message;\n if (!msg) break;\n if (Array.isArray(msg.tool_calls) && msg.tool_calls.length) {\n messages.push(msg);\n for (const tc of msg.tool_calls) {\n let ergebnis;\n try {\n const args = JSON.parse(tc.function?.arguments || '{}');\n ergebnis = await toolAusfuehren(tc.function.name, args);\n } catch (e) {\n ergebnis = { fehler: String(e.message || e).slice(0, 300) };\n }\n messages.push({ role: 'tool', tool_call_id: tc.id, content: JSON.stringify(ergebnis).slice(0, 6000) });\n }\n } else {\n antwort = String(msg.content || '').trim() || 'Keine Antwort erhalten.';\n }\n}\n} catch (e) {\n const status = e && e.response && e.response.status;\n if (status === 429) return [{ json: { antwort: 'Der Assistent ist kurzzeitig ausgelastet (Rate-Limit der KI). Bitte in einer Minute erneut fragen.' } }];\n return [{ json: { antwort: 'Fehler beim Assistenten: ' + String(e.message || e).slice(0, 200) } }];\n}\nreturn [{ json: { antwort: antwort || 'Die Antwort konnte nicht erzeugt werden.' } }];\n"
},
"id": "9f84d4b5-1dda-41e1-a2e7-587145582751",
"name": "Assistent (Kimi + Tools)",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
480,
300
]
},
{
"parameters": {
"respondWith": "json",
"responseBody": "={{ { antwort: $json.antwort } }}"
},
"id": "56f1d427-f049-48b2-bbc1-bb43750e778d",
"name": "Antwort an Portal",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.1,
"position": [
720,
300
]
}
],
"connections": {
"Webhook Portal": {
"main": [
[
{
"node": "Assistent (Kimi + Tools)",
"type": "main",
"index": 0
}
]
]
},
"Assistent (Kimi + Tools)": {
"main": [
[
{
"node": "Antwort an Portal",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "v1"
}
}