AutomationFlowsAI & RAG › Entitaets_agent V1.0

Entitaets_agent V1.0

03_Entitaets_Agent v1.0. Uses n8n-nodes-smart-memory, httpRequest. Webhook trigger; 26 nodes.

Webhook trigger★★★★☆ complexity26 nodesN8N Nodes Smart MemoryHTTP Request
AI & RAG Trigger: Webhook Nodes: 26 Complexity: ★★★★☆ Added:

The workflow JSON

Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →

Download .json
{
  "name": "03_Entitaets_Agent v1.0",
  "nodes": [
    {
      "parameters": {
        "operation": "clear",
        "sessionId": "={{ $('SET_Agent_Prompt').item.json.entitaet_id }}"
      },
      "type": "n8n-nodes-smart-memory.smartMemory",
      "typeVersion": 1,
      "position": [
        944,
        2144
      ],
      "id": "8b91e2c3-5a72-416a-9a03-2aeb32ab9646",
      "name": "MEM_Clear"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "e6beace6-879d-476a-95cd-e309269a1a8b",
              "name": "Agent_output",
              "value": "={{ $('HTTP_Formatter_LLM').item.json.choices[0].message.content || $('HTTP_Formatter_LLM').item.json.choices[0].message.reasoning_content || '' }}",
              "type": "object"
            },
            {
              "id": "f333d759-454e-4fd3-be02-faf47fa5dabe",
              "name": "entitaet_id",
              "value": "={{ $('SET_Agent_Prompt').item.json.entitaet_id }}",
              "type": "string"
            }
          ]
        },
        "options": {
          "ignoreConversionErrors": true
        }
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        1136,
        2144
      ],
      "id": "a516fc81-52cd-48c4-8239-faade30a5242",
      "name": "SET_Response"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "c8e7bf0a-9314-4c63-a4cc-fdbc236e31e4",
              "name": "rawPromptText",
              "value": "======================================================================\nUSER-INPUT: PROTOKOLLANT & JSON-KONSOLIDIERUNG\n=====================================================================\n\nDEINE AUFGABE:\nDu erh\u00e4ltst den finalen Text-Output von Agent 1.\n1. Extrahiere GND-ID, Name, Confidence und Begr\u00fcndung.\n2. Analysiere die \"DEBUG_TAGS\" und berechne den unsicherheit_score.\n3. Gie\u00dfe alles in das exakt folgende JSON-Schema.\n\n============================\n\nAGENT 1 OUTPUT:\n{{ $json.final_response }}\n\nEntit\u00e4t ID\n{{ $('SET_Agent_Prompt').item.json.entitaet_original_id }}\n\n============================\n\nZIEL-SCHEMA (Verpflichtend):\n{\n  \"entitaet_id\": \"<string>\",\n  \"gnd_id\": \"<string|null> \u2014 NUR numerische GND-ID (z.B. \\\"118873393\\\") oder null. NIEMALS ein Name, Stra\u00dfenname oder sonstige Beschreibung. Wenn Agent 1 keinen GND-Treffer hatte: null.\",\n  \"official_name\": \"<string|null>\",\n  \"confidence\": \"<high|medium|low|no_match>\",\n  \"research_notes\": \"<string>\",\n  \"additional_data\": {\n    \"start_date\": \"<string|null>\",\n    \"end_date\": \"<string|null>\",\n    \"role_or_profession\": \"<string|null>\",\n    \"wikipedia_url\": \"<string|null>\"\n  },\n  \"_debug_llm5\": \"<object|null>\"\n}",
              "type": "string"
            },
            {
              "id": "c06e74dd-1e46-4c00-a165-62ba24d88fde",
              "name": "rawSystempromptText",
              "value": "======================================================================\nARCHIV-LLM - SYSTEM: PROTOKOLLANT & JSON-STABILISATOR\n=====================================================================\n\nI. MANDAT\nDu bist Agent 2. Du liest den Text-Output von Agent 1 und \u00fcberf\u00fchrst ihn fehlerfrei in das geforderte JSON-Schema. Du bist zudem f\u00fcr die Berechnung des Debug-Scores zust\u00e4ndig.\n\nII. JSON-PFLICHTEN\n- Halte dich strikt an die vorgegebenen Datentypen.\n- `gnd_id` ist IMMER eine reine Zahlenkette (\"118873393\") oder null \u2014 niemals ein Name, Stra\u00dfenname, Ortsname oder sonstige Beschreibung. Wenn Agent 1 keinen verifizierten GND-Treffer liefert: null setzen.\n- Extrahiere die `entitaet_id` ganz oben aus dem initialen User-Input der Historie.\n\nIII. DEBUGGING-VERARBEITUNG\n1. Lese die \"DEBUG_TAGS\" aus der finalen Nachricht von Agent 1 aus.\n2. Erkenne eigene Konflikte beim Durchlesen des Chat-Verlaufs (z.B. WIKIPEDIA_GND_KONFLIKT, LOW_CONFIDENCE).\n3. Berechne den `unsicherheit_score`: Addiere 0.2 f\u00fcr jedes gefundene Flag (Maximalwert 1.0).\n4. Wenn keine Flags gefunden wurden und die Confidence 'high' ist, setze das Feld `_debug_llm5` auf `null`.\n5. Erfinde niemals Fehler, die in den Daten nicht ersichtlich sind.\n\nIV. OUTPUT\nGib ausschlie\u00dflich das rohe, geparste JSON zur\u00fcck. Keine Begr\u00fc\u00dfung, keine Formatierungs-Tags wie ```json.\n\nWICHTIGE OUTPUT-REGELN (ABSOLUTER ZWANG):\n1. VERBOT VON MARKDOWN: Gib NIEMALS Markdown-Formatierungen wie ```json oder ``` aus.\n2. NACKTES JSON: Der allererste Buchstabe deiner Antwort muss ein { oder [ sein, der letzte ein } oder ]. Kein Text davor, kein Text danach.\n3. NULL-CONSTRAINT: Wenn die Input-Confidence \"no_match\" lautet oder keine ID gefunden wurde, MUSS der Wert f\u00fcr `gnd_id` exakt null (ohne Anf\u00fchrungszeichen) sein. Schreibe NICHT \"null\", \"None\" oder \"\".\n4. KEY-STABILIT\u00c4T: Erfinde keine eigenen JSON-Keys. Nutze exakt das vorgegebene Schema.\n5. ESCAPING-PFLICHT (JSON-BRUCH VERHINDERN): > Wenn du Textwerte (insbesondere im Feld research_notes oder begruendung) \u00fcbernimmst, musst du alle darin enthaltenen doppelten Anf\u00fchrungszeichen zwingend mit einem Backslash maskieren (z.B. \\\"M\u00fcller, Hans\\\"), Zeilenumbr\u00fcche als \\n schreiben und Tabulatoren entfernen. Das JSON muss maschinenlesbar bleiben!",
              "type": "string"
            },
            {
              "id": "847273f0-88f9-48b0-bbd9-3dc3d3b8644c",
              "name": "entitaet_id",
              "value": "={{ $('SET_Agent_Prompt').item.json.entitaet_id }}",
              "type": "string"
            },
            {
              "id": "35440f8b-3d8e-4300-a98c-268bd468df28",
              "name": "entitaet_original_id",
              "value": "={{ $('SET_Agent_Prompt').item.json.entitaet_original_id }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        384,
        2144
      ],
      "id": "49c1e281-a0c8-479a-9f62-1b8d8653d6f0",
      "name": "SET_Formatter_Prompt"
    },
    {
      "parameters": {
        "jsCode": "// \u2550\u2550\u2550 REQ_Formatter | v1.0 \u2550\u2550\u2550\n// Zweck: Baut phi-4-Formatter-Request (finales JSON)\n\n// phi4_mini JSON-Formatter \u2014 text-only, leichte Aufgabe\nreturn {\n  json: {\n    model: 'phi-4-mini',\n    response_format: {\n      type: 'json_schema',\n      json_schema: {\n        name: 'entitaet_verifikation',\n        strict: true,\n        schema: {\n          type: 'object',\n          properties: {\n            entitaet_id:    { type: 'string' },\n            gnd_id: {\n              anyOf: [\n                { type: 'string', pattern: '^[0-9X]+$' },\n                { type: 'null' }\n              ]\n            },\n            official_name:  { anyOf: [{ type: 'string' }, { type: 'null' }] },\n            confidence:     { type: 'string', enum: ['high','medium','low','no_match'] },\n            research_notes: { type: 'string' },\n            additional_data: {\n              type: 'object',\n              properties: {\n                start_date:         { anyOf: [{ type: 'string' }, { type: 'null' }] },\n                end_date:           { anyOf: [{ type: 'string' }, { type: 'null' }] },\n                role_or_profession: { anyOf: [{ type: 'string' }, { type: 'null' }] },\n                wikipedia_url:      { anyOf: [{ type: 'string' }, { type: 'null' }] }\n              },\n              additionalProperties: false\n            },\n            _debug_llm5: { anyOf: [{ type: 'object' }, { type: 'null' }] }\n          },\n          required: ['entitaet_id','gnd_id','confidence','research_notes'],\n          additionalProperties: false\n        }\n      }\n    },\n    messages: [\n      { role: 'system', content: $json.rawSystempromptText },\n      { role: 'user',   content: $json.rawPromptText }\n    ],\n    temperature: 0.1,\n    top_p: 0.9,\n    top_k: 20,\n    presence_penalty: 0.0,\n    frequency_penalty: 0.0,\n    max_tokens: 1024\n  }\n};"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        592,
        2144
      ],
      "id": "cf6fc259-bdde-4acb-b732-8156475a620d",
      "name": "REQ_Formatter",
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://YOUR_LLM_ENDPOINT/v1/chat/completions",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "Bearer YOUR_API_KEY"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ $json }}",
        "options": {
          "timeout": 60000
        }
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        768,
        2144
      ],
      "id": "2e4fc35d-a7ba-453e-883e-c9462555ef51",
      "name": "HTTP_Formatter_LLM",
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "jsCode": "// \u2550\u2550\u2550 REQ_Agent | v1.0 \u2550\u2550\u2550\n// Zweck: Baut Recherche-Agent-Request (qwen, Tool-Use)\n\n// Recherche-Kern (Tool-Use, text-only) \u2014 qwen3.6-35b-mtp-64k-np2-sask_03\n// Kein Bild-Download: GND-Recherche ist eine reine Text-/Tool-Aufgabe.\nconst item = $input.item.json;\n\nif (!item.rawPromptText || !item.rawSystempromptText || !item.rawToolsText) {\n  throw new Error('Input-Felder unvollst\u00e4ndig (rawPromptText, rawSystempromptText oder rawToolsText fehlt)');\n}\n\nconst toolsPayload = typeof item.rawToolsText === 'string'\n  ? JSON.parse(item.rawToolsText)\n  : item.rawToolsText;\n\nconst messages = [\n  { role: 'system', content: item.rawSystempromptText },\n  { role: 'user', content: item.row_scan_magenta\n    ? [\n        { type: 'image_url', image_url: { url: item.row_scan_magenta } },\n        { type: 'text', text: item.rawPromptText }\n      ]\n    : item.rawPromptText }\n];\n\nconst requestBody = {\n  model: 'qwen3.6-35b-mtp-64k-np2-sask_03',\n  messages,\n  temperature: 0.1,\n  top_p: 0.8,\n  top_k: 20,\n  presence_penalty: 0.0,\n  frequency_penalty: 0.0,\n  max_tokens: 32768,\n  parallel_tool_calls: false,\n  chat_template_kwargs: { enable_thinking: true },\n  thinking: { type: \"enabled\", budget_tokens: 8192 },\n  tools: toolsPayload\n};\n\n// bodyKey in staticData schreiben \u2014 FN_Merge_Tool_Response braucht\n// den kompletten Request-Body (messages + model + params + tools)\n// execId als Pr\u00e4fix verhindert Kollisionen bei parallelen Executions\nconst staticData = $getWorkflowStaticData('global');\nconst bodyKey = `${$execution.id}_${item.entitaet_id}_body`;\nstaticData[bodyKey] = requestBody;\n\nreturn { json: requestBody };\n"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -720,
        2160
      ],
      "id": "3b16bbfc-bc92-403b-82c0-957b88918b15",
      "name": "REQ_Agent",
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://YOUR_LLM_ENDPOINT/v1/chat/completions",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "Bearer YOUR_API_KEY"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ $json }}",
        "options": {
          "timeout": 600000
        }
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        -560,
        2160
      ],
      "id": "5ed5b855-e617-4636-a23f-c5826a080a20",
      "name": "HTTP_Agent_LLM",
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "c8e7bf0a-9314-4c63-a4cc-fdbc236e31e4",
              "name": "rawPromptText",
              "value": "========================================================================\nUSER-INPUT: ENTIT\u00c4T-VALIDIERUNG & KONTEXT\n=======================================================================\n\n### \u26d4\ufe0f PHASE 1: NEGATIV-LISTE (HARD GATEKEEPING)\nPr\u00fcfe den NAME ZUR RECHERCHE auf Ausschlusskriterien:\n1. INVENTARNUMMERN: R\u00f6misch+Zahl (XI, IV/3), \"No.\", \"Nr.\", \"Inv.\"\n2. TECHNIK / MA\u00dfE: \"cm\", \"hoch\", \"breit\", \"\u00d8\", reine Zahlenreihen.\n3. INITIALEN: Ohne Nachnamen wie \"C.B.\", \"A.M.\"\n4. TYP-KONFLIKT: Institution als Person gesucht o.\u00e4.\n\u2192 Trifft ein Kriterium zu: Recherche abbrechen, confidence = \"no_match\".\n\n### \ud83c\udfaf PHASE 2: DEIN AUFTRAG\n\n{{ $json.row_scan_magenta ? '\ud83d\udcce VISUELLER BELEG: Der Zeilenscan (Magenta-Variante mit Spaltenhilfslinien) liegt als Bildanhang bei \u2014 nutze ihn zur visuellen Verifikation von OCR-Korrekturen und Schreibvarianten.' : '(Kein Bildanhang verf\u00fcgbar \u2014 st\u00fctze dich ausschlie\u00dflich auf Text und Tool-Ergebnisse. Behaupte KEINE visuellen Befunde.)' }}\n\nNAME ZUR RECHERCHE: {{ $json.entitaet_name_original }}\nTYP DER ENTIT\u00c4T: {{ $json.entitaet_typ_mapped }}\nERWARTETER ZEITRAUM: {{ $json.year_span }}\nGLINER2 SCORE: {{ $json.entitaet_gliner2_score }}\n\nKONTEXT-TEXT (NUR zur Disambiguierung \u2013 kein Suchauftrag!):\nQuellspalte {{ $json.entitaet_quelle_spalte }}: {{ $json.entitaet_quelle_spalte_text }}\nZeilen-Kontext: {{ JSON.stringify($json.cleaned_spalten) }}\n\nWICHTIG: Im Kontext-Text k\u00f6nnen weitere Namen, Stra\u00dfen und Orte stehen. Diese sind NICHT dein Suchauftrag \u2014 ignoriere sie vollst\u00e4ndig. Suche ausschlie\u00dflich nach dem NAME ZUR RECHERCHE oben. Kein Tool-Call f\u00fcr andere Entit\u00e4ten aus dem Kontext-Text.\nDiese sind NICHT dein Suchauftrag. Du recherchierst NUR den \nNAME ZUR RECHERCHE oben.\n\n\n### \ud83d\udd2c PHASE 2b: SCHRIFT-VALIDATOR BEFUND (OPTIONAL)\n{{ JSON.stringify($json.validierung) }}\n\nPr\u00fcfe den Befund f\u00fcr die Quellspalte {{ $json.entitaet_quelle_spalte }}\n\nSCHREIBER-VERWECHSLUNGSPAARE (Profil dieser Schreiberhand):\n{{ $json.schreiber_verwechslungspaare || \"Kein Schreiberprofil vorhanden \u2014 generische Paare aus II-E Stufe 3 nutzen\" }}.\nWenn dort ZWEIFEL oder STARK_ABWEICHEND markiert ist und der NAME\nZUR RECHERCHE das fragliche zeichen_transkribus enth\u00e4lt:\n\nTYP = PlaceOrGeographicName mit Stra\u00dfentyp (DIREKT-STRATEGIE):\n\u2192 Suche zus\u00e4tzlich mit zeichen_alternativ als erstem Buchstaben\n\u2192 DEBUG_TAG: OCR_VARIANT_VALIDATOR\n\nTYP = Person oder CorporateBody:\n\u2192 Nutze den Befund als zus\u00e4tzlichen Disambiguierungs-Hinweis\n\u2192 Wenn GND-Treffer den alternativen Buchstaben best\u00e4tigt:\n   confidence erh\u00f6hen, DEBUG_TAG: OCR_VARIANT_VALIDATOR\n\u2192 SUCH-ANKER-REGEL bleibt: suche zuerst mit dem Original-Namen,\n   dann mit zeichen_alternativ als Variante falls kein Treffer\n\nDer Befund ist ein Indiz \u2014 kein Befehl.\n\n### \ud83d\udd0e PHASE 3: WERKZEUGWAHL NACH TYP\n\nTYP = Person\n\u2192 search_gnd_personen mit NAME ZUR RECHERCHE\n\nTYP = PlaceOrGeographicName UND Name enth\u00e4lt KEINEN Stra\u00dfentyp\n(kein Str./Stra\u00dfe/Allee/Weg/Platz/Gasse/Damm/Ufer/Chaussee)\n\u2192 search_gnd_orte mit NAME ZUR RECHERCHE\n\nTYP = PlaceOrGeographicName UND Name enth\u00e4lt Stra\u00dfentyp\n\u2192 GND NICHT aufrufen\n\u2192 SOFORT SearXNG: \"[NAME ZUR RECHERCHE] Berlin\"\n\u2192 Auf Korrekturen/\u00c4hnlichkeitsvorschl\u00e4ge der Suchmaschine achten\n\u2192 Vollst\u00e4ndige Strategie: siehe System-Prompt II-D\n\nTYP = CorporateBody\n\u2192 search_gnd_organisationen mit NAME ZUR RECHERCHE\n\n### ZEITRAUM-ABGLEICH\nGleiche Lebens-/Wirkungsdaten mit ERWARTETER ZEITRAUM ab.\nPersonen die vor dem Zeitraum verstorben sind \u2192 DATE_MISMATCH \u2192 verwerfen.\n\nSobald alle Tools ausgewertet: finales JSON ausgeben.",
              "type": "string"
            },
            {
              "id": "847273f0-88f9-48b0-bbd9-3dc3d3b8644c",
              "name": "entitaet_id",
              "value": "={{ $json.entitaet_id }}",
              "type": "string"
            },
            {
              "id": "35440f8b-3d8e-4300-a98c-268bd468df28",
              "name": "entitaet_original_id",
              "value": "={{ $json.entitaet_original_id }}",
              "type": "string"
            },
            {
              "id": "dcbf43b8-b9ec-45d5-b66d-506f9d9ea7f3",
              "name": "rawToolsText",
              "value": "=[{\"type\": \"function\", \"function\": {\"name\": \"search_gnd_personen\", \"description\": \"Nutze dieses Tool AUSSCHLIESSLICH, um nach Personen in der GND zu suchen. Liefert Lebensdaten und Berufe. NICHT f\u00fcr Stra\u00dfennamen oder Orte verwenden.\", \"parameters\": {\"type\": \"object\", \"properties\": {\"suchbegriff\": {\"type\": \"string\", \"description\": \"Der exakte Name der Person (z.B. Johann Wolfgang von Goethe oder Kant).\"}}, \"required\": [\"suchbegriff\"]}}}, {\"type\": \"function\", \"function\": {\"name\": \"search_gnd_orte\", \"description\": \"Nutze dieses Tool NUR f\u00fcr St\u00e4dte, L\u00e4nder, Regionen und historische Stadtbezirke (z.B. Berlin, Sch\u00f6neberg, Berlin-Charlottenburg). NIEMALS f\u00fcr Stra\u00dfennamen verwenden \u2013 Stra\u00dfen sind in der GND nicht erfasst. Stra\u00dfennamen erkennst du an den Endungen: Str., Stra\u00dfe, Allee, Weg, Platz, Gasse, Damm, Ufer, Chaussee \u2192 f\u00fcr diese IMMER nur SearXNG nutzen.\", \"parameters\": {\"type\": \"object\", \"properties\": {\"suchbegriff\": {\"type\": \"string\", \"description\": \"Der exakte Name der Stadt, Region oder des Stadtbezirks (z.B. Berlin, Leipzig, Sch\u00f6neberg). Niemals ein Stra\u00dfenname.\"}}, \"required\": [\"suchbegriff\"]}}}, {\"type\": \"function\", \"function\": {\"name\": \"search_gnd_organisationen\", \"description\": \"Nutze dieses Tool AUSSCHLIESSLICH, um nach Firmen, Universit\u00e4ten, K\u00f6rperschaften oder Vereinen zu suchen. NICHT f\u00fcr Personen oder Stra\u00dfen verwenden.\", \"parameters\": {\"type\": \"object\", \"properties\": {\"suchbegriff\": {\"type\": \"string\", \"description\": \"Der exakte Name der Organisation (z.B. Universit\u00e4t Leipzig).\"}}, \"required\": [\"suchbegriff\"]}}}, {\"type\": \"function\", \"function\": {\"name\": \"search_wikipedia\", \"description\": \"Durchsucht die deutsche Wikipedia nach Artikeln f\u00fcr biografische Basisdaten, Kontextrecherche oder historische Stra\u00dfeninformationen (z.B. umbenannte Berliner Stra\u00dfen).\", \"parameters\": {\"type\": \"object\", \"properties\": {\"srsearch\": {\"type\": \"string\", \"description\": \"Der Suchbegriff f\u00fcr den Wikipedia-Artikel (z.B. 'Bamberger Stra\u00dfe Berlin' oder 'Hermann Bachmann Maler').\"}}, \"required\": [\"srsearch\"]}}}, {\"type\": \"function\", \"function\": {\"name\": \"search_berliner_strassen\", \"description\": \"Amtliches + historisches Berliner Strassenregister (konsolidiert aus Amtsverzeichnis, Wikipedia, Kauperts, OSM). PFLICHT-Tool fuer JEDE Strassen-Verifikation \u2014 vor SearXNG nutzen. Findet aktuelle UND fruehere Strassennamen mit Fuzzy-Toleranz fuer OCR-Varianten. Liefert: amtlicher Name, fruehere Namen mit Zeitraeumen, Ortsteil(e), historische Postbezirke, Quellen-Konsens-Score (1-4, hoeher = sicherer).\", \"parameters\": {\"type\": \"object\", \"properties\": {\"suchbegriff\": {\"type\": \"string\", \"description\": \"Strassenname \u2014 auch OCR-Variante oder vermuteter historischer Name\"}}, \"required\": [\"suchbegriff\"]}}}, {\"type\": \"function\", \"function\": {\"name\": \"search_berlin_geodaten\", \"description\": \"Lookup fuer historische Berliner Raumgliederungen: 54 Postbezirke (z.B. 'W 30' -> Schoeneberg, Wilmersdorf), 23 Alt-Bezirke, 97 Ortsteile mit heutiger Bezirkszuordnung. Nutze dieses Tool um Postbezirke aus dem Dokumentkontext aufzuloesen, BEVOR du die Geo-Konsistenz eines Strassen-Treffers beurteilst.\", \"parameters\": {\"type\": \"object\", \"properties\": {\"suchbegriff\": {\"type\": \"string\", \"description\": \"Postbezirk (z.B. 'W 30', 'SW 11'), Alt-Bezirk oder Ortsteil\"}}, \"required\": [\"suchbegriff\"]}}}, {\"type\": \"function\", \"function\": {\"name\": \"SearXNG\", \"description\": \"Web-Aggregator fuer allgemeine Recherche. Fuer Strassennamen NUR als letzter Fallback, wenn search_berliner_strassen keinen Treffer liefert. Einfache Queries ohne site:-Filter. Bezirks-Mapping: W 30 = Schoeneberg/Wilmersdorf, W 35 = Tiergarten, N = Wedding, SW = Kreuzberg/Tempelhof, NW = Tiergarten/Moabit. Falls SearXNG eine Namenskorrektur nahelegt, ist das KEINE Verifikation \u2014 nutze den Kandidaten fuer eine erneute search_berliner_strassen-Suche.\", \"parameters\": {\"type\": \"object\", \"properties\": {\"q\": {\"type\": \"string\", \"description\": \"Pr\u00e4zise Suchphrase. F\u00fcr Berliner Stra\u00dfen zwingend: 'site:berlin.kauperts.de [Stra\u00dfenname] [Hausnummer oder Bezirk] Berlin'. F\u00fcr Personen: '[Name] [Beruf] [Ort]'.\"}}, \"required\": [\"q\"]}}}, {\"type\": \"function\", \"function\": {\"name\": \"get_kauperts_details\", \"description\": \"Ruft aktuelle Adressdaten einer Berliner Stra\u00dfe von kauperts.de ab (PLZ, Amtsgericht, Finanzamt, Polizeiabschnitt, \u00d6PNV). NUR aufrufen wenn search_berliner_strassen eine kauperts_url zur\u00fcckgegeben hat. Die URL direkt aus dem Stra\u00dfen-Suchergebnis \u00fcbernehmen \u2014 NICHT raten oder konstruieren.\", \"parameters\": {\"type\": \"object\", \"properties\": {\"url\": {\"type\": \"string\", \"description\": \"Die kauperts_url aus dem search_berliner_strassen-Ergebnis, z.B. https://berlin.kauperts.de/strassen/bamberger-strasse-10777-10779-berlin/\"}}, \"required\": [\"url\"]}}}]",
              "type": "string"
            },
            {
              "id": "e0403e61-1136-4aa8-8323-727eb2095d2f",
              "name": "row_scan_magenta",
              "value": "={{ $json.row_scan_magenta }}",
              "type": "string"
            },
            {
              "id": "8c6690b3-b024-458e-a379-5ed4f7ec8146",
              "name": "rawSystempromptText",
              "value": "ARCHIV-LLM - SYSTEM: FORENSISCHER RECHERCHE-KERN (NATIVE TOOLS)\n\nI. MANDAT\nDu bist der zentrale Recherche-Kern der Museum-Inventory-Reader-Pipeline. Deine Aufgabe ist es,\nhistorische Entit\u00e4ten (Personen, Orte, Organisationen) mithilfe der dir\nbereitgestellten Werkzeuge (Tools) forensisch zu verifizieren.\n\nI-A. ABSOLUTES GRUNDGESETZ (WICHTIGSTE REGEL)\nDu erh\u00e4ltst pro Aufruf GENAU EINE Entit\u00e4t zur Recherche.\nDiese Entit\u00e4t hat einen NAMEN und einen TYP.\nDu recherchierst AUSSCHLIESSLICH diesen einen Namen.\n\nDer KONTEXT-TEXT (die vollst\u00e4ndige Zellinformation aus der Quelle)\ndient NUR zur Disambiguierung von Treffern \u2014 er ist kein Suchauftrag.\nWenn der Kontext-Text weitere Namen, Stra\u00dfen oder Orte enth\u00e4lt:\nIGNORIERE SIE. Du sollst nur den einen genannten Namen verifizieren.\n\nBeispiel:\n- NAME ZUR RECHERCHE: \"Bachmann, Hermann\"\n- TYP: Person\n- KONTEXT: \"Bachmann, Hermann Berlin W. 30. Lamberger Str. 49\"\n\u2192 Du suchst NUR nach \"Bachmann, Hermann\" als Person.\n\u2192 \"Lamberger Str.\" im Kontext ist kein Suchauftrag f\u00fcr dich.\n\nI-B. JSON-PFLICHT\nSobald du KEINE Werkzeuge mehr ben\u00f6tigst, gib deine finale Antwort zwingend\nals valides JSON-Objekt aus (siehe Abschnitt V).\n\nII. FORENSISCHE KERNREGELN & VALIDIERUNG\n\n- TOOL-ZWANG: Erfinde niemals eine GND-ID oder Namen aus deinem\n  internen Wissen. Jede Identifikation erfordert zwingend einen\n  erfolgreichen Tool-Aufruf. Ohne Treffer ist das Ergebnis \"no_match\".\n\n- VORNAMEN-SCHUTZ: Ein im Original lesbarer, vollst\u00e4ndiger Vorname\n  darf NIEMALS durch einen anderen Vornamen substituiert werden.\n  Liefern Tools nur abweichende Vornamen \u2192 no_match\n  (DEBUG_TAG: VORNAME_SUBSTITUIERT_ABGELEHNT).\n\n- STRIKTER EPOCHEN-ABGLEICH: Wenn die Lebensdaten eines GND-Treffers\n  vor dem Dokumentenzeitraum liegen, verwirfe den Treffer konsequent\n  (DEBUG_TAG: DATE_MISMATCH).\n\n- SUCH-ANKER-REGEL: Nutze als Suchbegriff f\u00fcr GND-Tools ausschlie\u00dflich\n  den NAME ZUR RECHERCHE. Der Kontext-Text dient nur zur Filterung.\n\n- GND-FUZZINESS & TOOL-\u00d6KONOMIE: Alle GND-Werkzeuge arbeiten mit\n  Fuzzy-Search. Der erste Aufruf deckt bereits ALLE Schreibweisen ab.\n  Wiederholter Aufruf mit identischem Begriff liefert NIEMALS neue Daten.\n\n- STRATEGIE-ZWANG BEI SACKGASSEN: Wenn ein GND-Tool keine Evidenz\n  liefert, darfst du es f\u00fcr denselben Begriff NICHT nochmal aufrufen.\n  Zwei Optionen:\n  1) Wechsle das Werkzeug (SearXNG oder Wikipedia)\n  2) \u00c4ndere den Suchbegriff fundamental\n\nII-D. STRASSENNAMEN: REGISTER-STRATEGIE\nGilt NUR wenn der TYP \"PlaceOrGeographicName\" ist UND der NAME ZUR\nRECHERCHE einen Stra\u00dfentyp enth\u00e4lt (\"Str.\", \"Strasse\", \"Allee\", \"Weg\",\n\"Platz\", \"Gasse\", \"Damm\", \"Ufer\", \"Chaussee\").\n\n1. GND UEBERSPRINGEN: search_gnd_orte NICHT aufrufen.\n\n2. PFLICHT: search_berliner_strassen mit dem Original-Namen.\n   Das Register kennt aktuelle UND historische Strassennamen und\n   toleriert OCR-Varianten (Fuzzy).\n\n3. BEI NO_MATCH: Bilde MAXIMAL 2 Varianten nach II-E und suche\n   erneut im Register.\n\n5. KAUPERTS-DETAILS (optional, bei Bedarf):\n   Wenn das Stra\u00dfen-Suchergebnis ein kauperts_url-Feld enth\u00e4lt:\n   \u2192 get_kauperts_details(url=<kauperts_url>) aufrufen f\u00fcr PLZ,\n     Amtsgericht, Finanzamt, \u00d6PNV.\n   Wenn kauperts_url eine Liste ist: die URL w\u00e4hlen die zum\n   Ortsteil aus dem Kontext passt.\n   NICHT aufrufen wenn keine kauperts_url vorhanden ist.\n\n4. TREFFER-PRUEFUNG (GEO-KONSISTENZ, ABSOLUTE PFLICHT):\n   a) Steht im Kontext ein historischer Postbezirk (W 30, SW 11 ...),\n      loese ihn mit search_berlin_geodaten auf \u2192 liefert Ortsteile.\n      search_berlin_geodaten NUR hier aufrufen \u2014 NICHT f\u00fcr Postbezirke\n      aus dem Kontext als eigenst\u00e4ndige Suchanfrage.\n   b) Pruefe: liegt der Strassen-Treffer in einem dieser Ortsteile?\n   c) NEIN \u2192 Treffer SOFORT verwerfen.\n      KEIN \"passt nicht ganz aber ist trotzdem moeglich\".\n\n5. KORREKTUR-DOKUMENTATION bei abweichendem Treffer:\n   \u2192 ocr_korrektur_kandidat = amtlicher Name aus dem Register\n   \u2192 confidence = maximal \"medium\"\n   \u2192 DEBUG_TAG: OCR_VARIANT_USED\n\n6. SearXNG NUR als letzter Fallback wenn das Register auch mit\n   Varianten nichts liefert. Keine site:-Filter.\n   Eine Suchmaschinen-Autokorrektur ist KEINE Verifikation \u2014\n   sie ist nur Anlass f\u00fcr eine erneute Register-Suche.\n\nII-E. SCHREIBVARIANTEN-STRATEGIE (OCR-VARIANTEN)\nQuellen f\u00fcr Verwechslungspaare, in dieser Rangfolge:\n  1. SCHRIFT-VALIDATOR BEFUND (zeilengenau, siehe User-Prompt) \u2014 hat Vorrang\n  2. SCHREIBER-VERWECHSLUNGSPAARE (Profil dieser Schreiberhand,\n     siehe User-Prompt) \u2014 nutze exakt diese Paare\n  3. NUR wenn beides fehlt: generische Handschrift-Paare\n     B<->L, V<->U, m<->rn, n<->u, e<->c, 1<->7\n\nREGELN:\n1. HYPOTHESE STATT RATEN: Maximal 2 Varianten, nur durch Austausch\n   EINES Zeichens aus den Paaren der h\u00f6chsten verf\u00fcgbaren Quelle,\n   bevorzugt am Wortanfang. NIEMALS ganze Namen substituieren \u2014\n   der VORNAMEN-SCHUTZ gilt unver\u00e4ndert.\n2. CROSS-CHECK-PFLICHT: Eine Variante gilt erst als Korrektur nach\n   unabh\u00e4ngiger Best\u00e4tigung: bei Stra\u00dfen der Register-Treffer mit\n   Geo-Konsistenz, bei Personen ein GND/Wikipedia-Treffer, dessen\n   Lebensdaten UND Wirkungsort zum Kontext passen.\n3. DECKELUNG: Jeder Variante-basierte Treffer: confidence maximal\n   \"medium\", ocr_korrektur_kandidat setzen, DEBUG_TAG: OCR_VARIANT_USED.\n\nII-F. MULTIPLE_CANDIDATES-PROTOKOLL\nFindet search_gnd_* mehrere Kandidaten mit \u00e4hnlichem Score:\n1. DATUMSABGLEICH: Pr\u00fcfe Lebensdaten gegen year_span aus dem Kontext.\n   Eindeutiger Widerspruch \u2192 Kandidat ausschlie\u00dfen.\n2. EXTERNE BEST\u00c4TIGUNG: Rufe EINEN weiteren Tool-Call auf\n   (Wikipedia oder SearXNG) mit Name + Epoche/Ort als Anker.\n   Beispiel: \"Hermann Bachmann Maler Berlin 1950er\"\n3. SOFORTSTOPP: Sobald EIN Kandidat durch zwei unabh\u00e4ngige Quellen\n   best\u00e4tigt ist (GND-Lebensdaten + externer Beleg) \u2192 sofort finales\n   JSON ausgeben. KEIN weiterer Tool-Call.\n4. KEIN CONTEXT-BLEEDING: Weitere Namen, Stra\u00dfen oder Orte im\n   Kontext-Text sind NIEMALS ein Suchauftrag \u2014 auch nicht als\n   \"Hilfs-Verifikation\".\n\nII-G. GND-DATENQUALIT\u00c4T: KANDIDAT NICHT VERWERFEN BEI L\u00dcCKEN\nDer GND-Index kann unvollst\u00e4ndige Eintr\u00e4ge enthalten \u2014 Felder fehlen,\nsind null, oder enthalten interne Referenz-IDs statt Klartext.\n\nGRUNDREGEL: Wenn die GND-Felder eines Kandidaten nicht ausreichen um\nihn sicher zu best\u00e4tigen oder auszuschlie\u00dfen \u2014 verwirf ihn NICHT.\nNutze stattdessen weitere Tools zur Verifikation.\n\nVORGEHEN:\n1. Pr\u00fcfe ob Lebensdaten (birth/death) zeitlich zum Kontext passen.\n   Wenn ja \u2192 Kandidat ist weiterhin aktiv, nicht verwerfen.\n2. Rufe ZWINGEND mindestens einen weiteren Tool-Call auf:\n   BEVORZUGT: search_wikipedia mit Name + Epoche als Anker\n   FALLBACK:  SearXNG mit Name + year_span aus dem Kontext\n3. Best\u00e4tigt die externe Quelle den Kandidaten \u2192 confidence \"medium\".\n   Widerspricht sie ihm \u2192 Kandidat verwerfen.\n   Liefert sie nichts Verwertbares \u2192 confidence \"low\", NICHT \"no_match\".\n\nno_match bedeutet: es gibt keinen plausiblen Kandidaten.\nEs bedeutet NICHT: der GND-Eintrag hat leere Felder.\n\nKONFIDENZ-SEMANTIK (VERBINDLICH, gilt f\u00fcr ALLE Entit\u00e4tstypen):\nconfidence bewertet ausschlie\u00dflich die Sicherheit der IDENTIFIKATION.\nEine Identifikation liegt vor, wenn gnd_id gesetzt ist ODER\noffizieller_name durch ein Register/eine Quelle konkret belegt ist\n(z.B. Stra\u00dfenregister, Geodaten-Register: Postbezirke, Ortsteile).\n- Identifikation vorhanden \u2192 confidence = high/medium/low nach Beleglage.\n  gnd_id darf dabei null sein (Stra\u00dfen und Postbezirke haben oft\n  keinen GND-Eintrag) \u2014 das senkt confidence NICHT automatisch.\n- KEINE Identifikation (gnd_id = null UND offizieller_name = null)\n  \u2192 IMMER confidence = \"no_match\", auch wenn die Negativ-Erkenntnis\n  selbst sicher ist. Die Sicherheit der Negativ-Begr\u00fcndung geh\u00f6rt in\n  \"begruendung\", niemals in confidence.\n\nIII. HARD GATEKEEPING (NEGATIV-LISTE)\nPr\u00fcfe den NAME ZUR RECHERCHE vorab gegen:\nInventarnummern (R\u00f6mische Ziffern, \"Nr.\", \"Inv.\"), Ma\u00dfe (\"cm\", \"\u00d8\"),\nreine Initialen, OCR-Kauderwelsch.\nBei Treffer: Sofortiger Abbruch mit no_match.\n\nIV. DEBUGGING-TAGS\nMULTIPLE_CANDIDATES, GOOGLE_ONLY_EVIDENCE, DATE_MISMATCH,\nOCR_VARIANT_USED, OCR_VARIANT_VALIDATOR, VORNAME_SUBSTITUIERT_ABGELEHNT,\nGND_NAME_MISMATCH, STRASSE_KORREKTUR_GEFUNDEN\nFalls keine Probleme: NONE.\n\nV. FINALES SCHEMAFORMAT\n{\n  \"gnd_id\": \"ID oder null\",\n  \"offizieller_name\": \"Name oder null\",\n  \"confidence\": \"high/medium/low/no_match\",\n  \"quellen\": \"genutzte Tools und Engines\",\n  \"begruendung\": \"Detaillierte historische Herleitung\",\n  \"ocr_korrektur_kandidat\": \"Korrigierter Stra\u00dfenname oder null\",\n  \"debug_tags\": \"Zutreffende Tags oder NONE\"\n}\n\n=== KERNREGELN (WIEDERHOLUNG) ===\n- VORNAMEN-SCHUTZ: niemals Vornamen austauschen oder erg\u00e4nzen.\n- STRASSEN: immer zuerst search_berliner_strassen, SearXNG nur Fallback.\n- OCR-VARIANTEN: max. 2, nur dokumentierte Zeichenpaare,\n  Cross-Check-Pflicht, confidence maximal \"medium\".\n- GND-L\u00dcCKEN: nicht verwerfen, externe Verifikation.\n- Kein Treffer ist besser als ein falscher Treffer: no_match ist ein\n  legitimes Ergebnis.",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        -896,
        2160
      ],
      "id": "28029f90-ee8a-4843-a5d1-45d559bdb4af",
      "name": "SET_Agent_Prompt"
    },
    {
      "parameters": {
        "rules": {
          "values": [
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 3
                },
                "conditions": [
                  {
                    "leftValue": "={{ $json.tool_name }}",
                    "rightValue": "search_gnd_personen",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "id": "971981da-9e40-4f6d-9a52-bddaf2eaf1b3"
                  }
                ],
                "combinator": "and"
              }
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 3
                },
                "conditions": [
                  {
                    "id": "9500e17f-f241-44d5-839a-aad36a2dfd65",
                    "leftValue": "={{ $json.tool_name }}",
                    "rightValue": "search_gnd_orte",
                    "operator": {
                      "type": "string",
                      "operation": "equals",
                      "name": "filter.operator.equals"
                    }
                  }
                ],
                "combinator": "and"
              }
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 3
                },
                "conditions": [
                  {
                    "id": "9c0a2b24-168a-4250-9f1c-33c9d1fe33da",
                    "leftValue": "={{ $json.tool_name }}",
                    "rightValue": "search_gnd_organisationen",
                    "operator": {
                      "type": "string",
                      "operation": "equals",
                      "name": "filter.operator.equals"
                    }
                  }
                ],
                "combinator": "and"
              }
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 3
                },
                "conditions": [
                  {
                    "id": "0a571f2c-7c24-4591-8830-4e08166121fb",
                    "leftValue": "={{ $json.tool_name }}",
                    "rightValue": "search_wikipedia",
                    "operator": {
                      "type": "string",
                      "operation": "equals",
                      "name": "filter.operator.equals"
                    }
                  }
                ],
                "combinator": "and"
              }
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 3
                },
                "conditions": [
                  {
                    "leftValue": "={{ $json.tool_name }}",
                    "rightValue": "search_berliner_strassen",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "id": "route-strassen-v1"
                  }
                ],
                "combinator": "and"
              }
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 3
                },
                "conditions": [
                  {
                    "leftValue": "={{ $json.tool_name }}",
                    "rightValue": "search_berlin_geodaten",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "id": "route-geodaten-v1"
                  }
                ],
                "combinator": "and"
              }
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 3
                },
                "conditions": [
                  {
                    "id": "9c9162a0-98fc-4f8b-a15f-0f4610435fad",
                    "leftValue": "={{ $json.tool_name }}",
                    "rightValue": "SearXNG",
                    "operator": {
                      "type": "string",
                      "operation": "equals",
                      "name": "filter.operator.equals"
                    }
                  }
                ],
                "combinator": "and"
              }
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 3
                },
                "conditions": [
                  {
                    "leftValue": "={{ $json.tool_name }}",
                    "rightValue": "get_kauperts_details",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "id": "971981da-9e40-4f6d-9a52-bddaf2eaf1b3"
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "get_kauperts_details"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 3
                },
                "conditions": [
                  {
                    "id": "cffb3f63-74be-4c24-854b-51c9df8dfee8",
                    "leftValue": "={{ $json.tool_name }}",
                    "rightValue": "none",
                    "operator": {
                      "type": "string",
                      "operation": "equals",
                      "name": "filter.operator.equals"
                    }
                  }
                ],
                "combinator": "and"
              }
            }
          ]
        },
        "options": {
          "fallbackOutput": "extra"
        }
      },
      "type": "n8n-nodes-base.switch",
      "typeVersion": 3.4,
      "position": [
        -240,
        2080
      ],
      "id": "8db75221-e087-4be7-a473-9ec7ff8e4ed1",
      "name": "SW_Tool_Router"
    },
    {
      "parameters": {
        "url": "http://gnd_opensearch:9200/gnd_personen/_search",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={\n  \"size\": 10,\n  \"query\": {\n    \"multi_match\": {\n      \"query\": \"{{ $json.suchbegriff }}\",\n      \"fields\": [\n        \"preferred_name^5\",\n        \"alternate_names^3\",\n        \"profession^2\",\n        \"place_of_activity^2\",\n        \"place_of_birth^1\",\n        \"affiliation^1\",\n        \"info^1\"\n      ],\n      \"fuzziness\": \"AUTO\"\n    }\n  }\n}",
        "options": {
          "timeout": 10000
        },
        "body": "={\n  \"size\": 5,\n  \"_source\": [\"gnd_id\", \"preferred_name\", \"alternate_names\", \"birth\", \"death\", \"gender\", \"academic_degree\", \"place_of_birth\", \"place_of_death\", \"place_of_activity\", \"affiliation\", \"info\", \"wikipedia_url\", \"wikidata_url\"],\n  \"query\": {\n    \"multi_match\": {\n      \"query\": \"{{ $json.suchbegriff }}\",\n      \"fields\": [\n        \"preferred_name^5\",\n        \"alternate_names^3\",\n        \"place_of_activity^2\",\n        \"place_of_birth^2\",\n        \"affiliation^2\",\n        \"academic_degree^1\",\n        \"info^1\"\n      ],\n      \"fuzziness\": \"AUTO\"\n    }\n  }\n}"
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        0,
        1680
      ],
      "id": "9f4d91c7-fc2d-44e0-a1ed-fbf3fb624e22",
      "name": "HTTP_Tool_GND_Personen",
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "jsCode": "// \u2550\u2550\u2550 PARSE_Agent_Step | v1.0 \u2550\u2550\u2550\n// Zweck: Liest Tool-Call oder finalen Stopp; verwaltet StaticData\n\n// =====================================================================\n// SUBWORKFLOW CODE-NODE: Code in JavaScript1 (Isolierter Extractor)\n// =====================================================================\n\nconst staticData = $getWorkflowStaticData('global');\nconst inputData = $input.item.json;\nconst choice = inputData.choices?.[0];\nconst toolCall = choice?.message?.tool_calls?.[0];\n\nconst entityId = $('FN_Normalize_Input').first().json.entitaet_id; \n\n// Dynamische, kollisionsfreie Schl\u00fcssel generieren\nconst historyKey = `${$execution.id}_${entityId}_history`;\nconst extractorKey = `${$execution.id}_${entityId}_extractor`;\nconst toolsKey = `${$execution.id}_${entityId}_used_tools`;\n\n// Fall A: Die GPU will ein Tool aufrufen\nif (toolCall) {\n  const toolName = toolCall.function.name;\n  const rawArgs = toolCall.function['arguments'];\n  const parsedArgs = typeof rawArgs === 'string' ? JSON.parse(rawArgs) : rawArgs;\n  const suchbegriff = parsedArgs.suchbegriff || parsedArgs.q || parsedArgs.srsearch || null;\n\n  // Speichern in der isolierten Schublade dieser Entit\u00e4t\n  staticData[extractorKey] = {\n    tool_name: toolName,\n    tool_call_id: toolCall.id,\n    suchbegriff: suchbegriff,\n    raw_assistant_message: choice.message\n  };\n\n  return {\n    json: {\n      has_tool_call: true,\n      tool_name: toolName,\n      suchbegriff: suchbegriff\n    }\n  };\n} \n\n// Fall B: Finaler Stopp (Qwen ist fertig!)\nconst finalContent = choice?.message?.content || null;\n\n// Nur den Speicher DIESER spezifischen Entit\u00e4t l\u00f6schen\ndelete staticData[historyKey];\ndelete staticData[extractorKey];\ndelete staticData[toolsKey];\ndelete staticData[`${$execution.id}_${entityId}_turns`]; // FIX Bug 17: Z\u00e4hler aufr\u00e4umen\n\nreturn {\n  json: {\n    has_tool_call: false,\n    tool_name: 'none',\n    final_response: finalContent\n  }\n};"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -400,
        2160
      ],
      "id": "08b64af2-8a86-4a44-9bac-d430124e1598",
      "name": "PARSE_Agent_Step",
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "jsCode": "// \u2550\u2550\u2550 FN_Merge_Tool_Response | v1.0 \u2550\u2550\u2550\n// Zweck: F\u00fchrt Tool-Antwort in History; Tool-Blacklist; Iterationsgrenze (MAX_TURNS)\n\n// =====================================================================\n// SUBWORKFLOW CODE-NODE: Merge_Tool_Response (Isoliert & Versch\u00e4rft)\n// =====================================================================\n\nconst staticData = $getWorkflowStaticData('global');\n\n// Keine $()-Node-Referenzen \u2014 alles aus staticData\n// entityId \u00fcber extractorKey ermitteln: PARSE_Agent_Step legt ihn pro Execution ab\n// Format: {executionId}_{entityId}_extractor \u2192 wir suchen den passenden Key\nconst execId = $execution.id;\nlet entityId = null;\nlet extractorData = null;\nfor (const key of Object.keys(staticData)) {\n  if (key.startsWith(execId + '_') && key.endsWith('_extractor')) {\n    entityId = key.slice(execId.length + 1, -('_extractor'.length));\n    extractorData = staticData[key];\n    break;\n  }\n}\nif (!entityId || !extractorData) {\n  throw new Error(`Kritischer Fehler: Keine aktiven Tool-Call-Metadaten f\u00fcr Execution ${execId} im Speicher gefunden.`);\n}\n\nconst bodyKey = `${execId}_${entityId}_body`;\nconst originalBody = staticData[bodyKey];\nif (!originalBody) {\n  throw new Error(`Kritischer Fehler: Kein originalBody f\u00fcr ${entityId} in staticData (bodyKey: ${bodyKey}).`);\n}\n\nconst historyKey = `${execId}_${entityId}_history`;\nconst extractorKey = `${execId}_${entityId}_extractor`;\nconst toolsKey = `${execId}_${entityId}_used_tools`;\n\n// Isolierte Historie und Tool-Tracker initialisieren, falls erste Runde\nif (!staticData[historyKey]) {\n  staticData[historyKey] = [...originalBody.messages];\n}\nif (!staticData[toolsKey]) {\n  staticData[toolsKey] = [];\n}\n\n// Das genutzte Tool f\u00fcr dieses Paket auf die Blacklist setzen\nstaticData[toolsKey].push(extractorData.tool_name);\n\n// SCHRITT 1: Original-Assistant-Message NATIV sichern (mit tool_calls)\n// Natives tool-Message-Format statt assistant-Stub + user-Message.\n// Nur der tats\u00e4chlich ausgef\u00fchrte Call wird \u00fcbernommen (parallel_tool_calls: false).\nconst rawMsg = extractorData.raw_assistant_message || {};\nconst executedCall =\n  (Array.isArray(rawMsg.tool_calls) ? rawMsg.tool_calls : [])\n    .find(t => t.id === extractorData.tool_call_id)\n  || {\n       id: extractorData.tool_call_id || `call_${Date.now()}`,\n       type: 'function',\n       function: {\n         name: extractorData.tool_name,\n         arguments: JSON.stringify({ suchbegriff: extractorData.suchbegriff })\n       }\n     };\n\nstaticData[historyKey].push({\n  role: 'assistant',\n  content: (typeof rawMsg.content === 'string' && rawMsg.content.trim()) ? rawMsg.content : null,\n  tool_calls: [executedCall]\n});\n\n// SCHRITT 2: Tool-Ergebnis als natives role:\"tool\" mit tool_call_id\nconst currentToolOutput = $input.item.json;\nlet safeToolString = JSON.stringify(currentToolOutput);\n\n// KEVLAR-PANZER: API-Antwort bei 60.000 Zeichen kappen\nif (safeToolString.length > 60000) {\n  safeToolString = safeToolString.substring(0, 60000) + \"\\n\\n... [ACHTUNG SYSTEM-NOTIZ: TEXT WURDE ABGESCHNITTEN, DA ZU LANG. NUTZE DIE BIS HIERHIN VORHANDENEN INFORMATIONEN.]\";\n}\n\nstaticData[historyKey].push({\n  role: 'tool',\n  tool_call_id: executedCall.id,\n  content: safeToolString\n});\n\n// SCHRITT 3: Recherche-Direktive als kompakte User-Nachricht\nstaticData[historyKey].push({\n  role: \"user\",\n  content: `WICHTIGE RECHERCHE-DIREKTIVE:\\nDa \"${extractorData.tool_name}\" mit automatischer Fuzziness sucht, sind ALLE relevanten Treffer f\u00fcr \"${extractorData.suchbegriff}\" bereits im Tool-Ergebnis enthalten. Eine erneute Abfrage von \"${extractorData.tool_name}\" mit demselben Begriff ist STRENGSTENS VERBOTEN.\\n\\nEntscheide jetzt:\\n- Gibt es im Web (SearXNG / Wikipedia) Belege, um einen der Kandidaten zu verifizieren (z.B. Wohnort-Abgleich)? -> Dann rufe SearXNG oder search_wikipedia auf.\\n- Kannst du den Suchbegriff sinnvoll variieren? -> Dann nutze ein Tool mit NEUEM Begriff.\\n- Gibt es keine Spur? -> Beende die Suche sofort mit dem finalen JSON (\"confidence\": \"no_match\").`\n});\n\n// Tools dynamisch basierend auf dem isolierten Tracker filtern\n// FIX Bug 17: harte Iterationsgrenze gegen Endlos-Loop\nconst turnKey = `${$execution.id}_${entityId}_turns`;\nstaticData[turnKey] = (staticData[turnKey] || 0) + 1;\nconst MAX_TURNS = 6;\nif (staticData[turnKey] >= MAX_TURNS) {\n  return { json: {\n    model: originalBody.model,\n    response_format: originalBody.response_format,\n    messages: [...staticData[historyKey], { role: 'user', content: '[SYSTEM] Maximale Recherchetiefe erreicht. Liefere JETZT das finale JSON. Falls kein eindeutiger Treffer: \\\"confidence\\\":\\\"no_match\\\".' }],\n    temperature: originalBody.temperature,\n    top_p: originalBody.top_p,\n    top_k: originalBody.top_k,\n    presence_penalty: 0.5,\n    frequency_penalty: 0.5,\n    max_tokens: originalBody.max_tokens,\n    tools: []\n  }};\n}\n\nconst filteredTools = originalBody.tools.filter(tool => {\n  if (tool.function.name.startsWith('search_gnd_')) {\n    return !staticData[toolsKey].includes(tool.function.name);\n  }\n  return true;\n});\n\n// Den modifizierten Body sicher an die GPU zur\u00fcckgeben\nreturn {\n  json: {\n    model: originalBody.model,\n    response_format: originalBody.response_format,\n    messages: [...staticData[historyKey]], // Die l\u00fcckenlose, isolierte Kette\n    temperature: originalBody.temperature,\n    top_p: originalBody.top_p,\n    top_k: originalBody.top_k,\n    presence_penalty: 0.5,\n    frequency_penalty: 0.5,\n    max_tokens: originalBody.max_tokens,\n    tools: filteredTools\n  }\n};"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        560,
        2992
      ],
      "id": "f36fe156-3f2d-4085-aadf-0335e2e949c7",
      "name": "FN_Merge_Tool_Response",
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "url": "http://gnd_opensearch:9200/gnd_orte/_search",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={\n  \"size\": 10,\n  \"query\": {\n    \"multi_match\": {\n      \"query\": \"{{ $json.suchbegriff }}\",\n      \"fields\": [\n        \"preferred_name^5\",\n        \"alternate_names^3\",\n        \"parent_region^2\",\n        \"architect^1\",\n        \"info^1\"\n      ],\n      \"fuzziness\": \"AUTO\"\n    }\n  }\n}",
        "options": {
          "timeout": 10000
        },
        "body": "={\n  \"size\": 5,\n  \"_source\": [\"gnd_id\", \"preferred_name\", \"alternate_names\", \"parent_region\", \"start\", \"end\", \"info\", \"wikipedia_url\", \"wikidata_url\", \"geonames_url\"],\n  \"query\": {\n    \"multi_match\": {\n      \"query\": \"{{ $json.suchbegriff }}\",\n      \"fields\": [\n        \"preferred_name^5\",\n        \"alternate_names^3\",\n        \"parent_region^2\",\n        \"info^1\"\n      ],\n      \"fuzziness\": \"AUTO\"\n    }\n  }\n}"
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        0,
        1872
      ],
      "id": "2ae8549f-f36b-4af2-b0b0-776a43d55aee",
      "name": "HTTP_Tool_GND_Orte",
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "url": "http://gnd_opensearch:9200/gnd_organisationen/_search",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={\n  \"size\": 5,\n  \"query\": {\n    \"multi_match\": {\n      \"query\": \"{{ $json.suchbegriff }}\",\n      \"fields\": [\n        \"preferred_name^5\",\n        \"alternate_names^4\",\n        \"abbreviation^4\",\n        \"location^2\",\n        \"area_of_activity^1\",\n        \"info^1\"\n      ],\n      \"fuzziness\": \"AUTO\"\n    }\n  }\n}",
        "options": {
          "timeout": 10000
        },
        "body": "={\n  \"size\": 5,\n  \"_source\": [\"gnd_id\", \"preferred_name\", \"alternate_names\", \"abbreviation\", \"location\", \"start\", \"end\", \"info\", \"wikipedia_url\", \"wikidata_url\"],\n  \"query\": {\n    \"multi_match\": {\n      \"query\": \"{{ $json.suchbegriff }}\",\n      \"fields\": [\n        \"preferred_name^5\",\n        \"alternate_names^4\",\n        \"abbreviation^4\",\n        \"location^2\",\n        \"info^1\"\n      ],\n      \"fuzziness\": \"AUTO\"\n    }\n  }\n}"
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        0,
        2096
      ],
      "id": "6796167c-af51-4db8-8fb2-7e50518bcd42",
      "name": "HTTP_Tool_GND_Organisationen",
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "url": "https://de.wikipedia.org/w/api.php",
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "action",
              "value": "query"
            },
            {
              "name": "list",
              "value": "search"
            },
            {
              "name": "srsearch",
              "value": "={{ $json.suchbegriff }}"
            },
            {
              "name": "format",
              "value": "json"
            },
            {
              "name": "srlimit",
              "value": "5"
            },
            {
              "name": "utf8",
              "value": "1"
            }
          ]
        },
        "sendBody": false,
        "options": {
          "timeout": 30000
        }
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        0,
        2512
      ],
      "id": "e3c70955-a1c1-4c27-a5ce-eefcc7fc53ab",
      "name": "HTTP_Tool_Wikipedia",
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "url": "http://searxng:8080/search",
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "q",
              "value": "={{ $json.suchbegriff }}"
            },
            {
              "name": "format",
              "value": "json"
            }
          ]
        },
        "options": {
          "timeout": 30000
        }
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        0,
        2720
      ],
      "id": "a16e5c8a-0e65-4bd4-8ffc-232eee7d50e0",
      "name": "HTTP_Tool_SearXNG",
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "03_Entitaets_Agent",
        "responseMode": "responseNode",
        "options": {}
      },
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2.1,
      "position": [
        -1248,
        2160
      ],
      "id": "e6a1c865-f40f-4ae3-ae90-d74f148f0ca4",
      "name": "WH_Entity_Verify"
    },
    {
      "parameters": {
        "jsCode": "// \u2550\u2550\u2550 FN_Normalize_Input | v1.0 \u2550\u2550\u2550\n// Zweck: Vereinheitlicht Input (Webhook-Body vs. direkt)\n\n// =====================================================================\n// DATA NORMALIZER: Macht die Daten einheitlich (egal ob Start oder Webhook)\n// =====================================================================\n\nconst data = $input.item.json;\n\n// Wenn 'body' existiert (Webhook), packe es aus. Ansonsten nimm die Daten direkt (Start-Node).\nconst cleanData = data.body ? data.body : data;\n\nreturn {\n  json: cleanData\n};"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -1040,
        2160
      ],
      "id": "e725eb8d-220f-40ac-bbc3-73cf749e70f2",
      "name": "FN_Normalize_Input"
    },
    {
      "parameters": {
        "options": {}
      },
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.5,
      "position": [
        1488,
        2144
      ],
      "id": "d553c559-ca36-4d7f-bbe3-d99270c37a3d",
      "name": "RESP_Webhook"
    },
    {
      "parameters": {
        "content": "## 03 \u00b7 Entit\u00e4ts-Agent\n**Version:** v1.0\n\nWebhook-getriggerter Sub-Workflow. Recherchiert genau eine Entit\u00e4t\nvia GND + Stra\u00dfenregister + Wikipedia + SearXNG.",
        "height": 200,
        "width": 480,
        "color": 4
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -1264,
        1712
      ],
      "id": "c60419a2-21e3-4037-9830-1d70eed723f6",
      "name": "INFO_Version"
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={\"confidence\":\"no_match\",\"research_notes\":\"Subflow-Fehler oder Timeout (Fehler-Pfad)\",\"entitaet_id\":\"{{ $json.entitaet_id || 'unknown' }}\"}",
        "options": {
          "responseCode": 200
        }
      },
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.5,
      "position": [
        1488,
        2384
      ],
      "id": "f73a6d63-b8b7-4245-86fc-896521972f0f",
      "name": "RESP_Error"
    },
    {
      "parameters": {
        "url": "http://gnd_opensearch:9200/berliner_strassen/_search",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={\n  \"size\": 5,\n  \"query\": {\n    \"bool\": {\n      \"should\": [\n        { \"match\": { \"name\": { \"query\": \"{{ $json.suchbegriff }}\", \"fuzziness\": \"AUTO\", \"boost\": 5 } } },\n        { \"nested\": {\n            \"path\": \"fruehere_namen\",\n            \"query\": { \"match\": { \"fruehere_namen.name\": { \"query\": \"{{ $json.suchbegriff }}\", \"fuzziness\": \"AUTO\", \"boost\": 4 } } }\n        } }\n      ]\n    }\n  }\n}",
        "options": {
          "timeout": 10000
        }
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        0,
        1480
      ],
      "id": "http-tool-strassen-v1",
      "name": "HTTP_Tool_Strassen",
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "url": "http://gnd_opensearch:9200/berlin_geodaten/_search",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={\n  \"size\": 5,\n  \"query\": {\n    \"multi_match\": {\n      \"query\": \"{{ $json.suchbegriff }}\",\n      \"fields\": [\"name^3\", \"aliases^2\", \"ortsteile\", \"bezirk_alt\"]\n    }\n  }\n}",
        "options": {
          "timeout": 10000
        }
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        0,
        1280
      ],
      "id": "http-tool-geodaten-v1",
      "name": "HTTP_Tool_Geodaten",
      "onError": "continueErrorOutput"
    },
    {
      "id": "f5668dd9-664d-4331-89c9-89d3dc27fefe",
      "name": "SET_Kauperts_Request",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2400,
        600
      ],
      "parameters": {
        "jsCode": "// \u2550\u2550\u2550 SET_Kauperts_Request | v1.0 \u2550\u2550\u2550\n// Zweck: Baut crawl4ai-Request f\u00fcr Kauperts-URL\n// Eingabe: tool_input.url (aus get_kauperts_details Tool-Call)\n\nconst staticData = $getWorkflowStaticData('global');\nconst execId = $execution.id;\nlet toolInput = null;\nfor (const key of Object.keys(staticData)) {\n  if (key.startsWith(execId + '_') && key.endsWith('_extractor')) {\n    const data = staticData[key];\n    if (data && data.tool_name === 'get_kauperts_details') {\n      toolInput = data.tool_input;\n      break;\n    }\n  }\n}\n\nif (!toolInput || !toolInput.url) {\n  throw new Error('get_kauperts_details: keine URL im Tool-Input');\n}\n\nconst url = toolInput.url;\n\n// crawl4ai-Request: nur strukturierte Extraktion, kein Screenshot\nreturn {\n  json: {\n    url: url,\n    crawler_params: {\n      headless: true,\n      page_timeout: 15000,\n    },\n    extraction_strategy: {\n      type: \"JsonCssExtractionStrategy\",\n      params: {\n        schema: {\n          name: \"KaupertsStra\u00dfe\",\n          baseSelector: \"body\",\n          fields: [\n            { name: \"name\",             selector: \"h1\",                           type: \"text\" },\n            { name: \"plz\",              selector: \".plz, [class*='plz']\",          type: \"text\" },\n            { name: \"ortsteil\",         selector: \".ortsteil, [class*='ortsteil']\",type: \"text\" },\n            { name: \"amtsgericht\",      selector: \".amtsgericht\",                  type: \"text\" },\n         
Pro

For the full experience including quality scoring and batch install features for each workflow upgrade to Pro

About this workflow

03_Entitaets_Agent v1.0. Uses n8n-nodes-smart-memory, httpRequest. Webhook trigger; 26 nodes.

Source: https://github.com/sebastianruffberlin/museum-inventory-reader/blob/96500b26b10d6104116c4e5547e023bf45ea2850/n8n/03_Entitaets_Agent.json — original creator credit. Request a take-down →

More AI & RAG workflows → · Browse all categories →

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

AI & RAG

Jigsaw API key for image processing, I use this as a gatekeeper/second pair of eyes. LINK to their website https://jigsawstack.com/ SECOND A postgress DATABASE (I use Supabase) LlamaCloud for the pars

HTTP Request, Postgres, Stop And Error +2
AI & RAG

Onsite Photos to Jobs (SMS Agent). Uses dataTable, twilio, httpRequest, airtable. Webhook trigger; 62 nodes.

Data Table, Twilio, HTTP Request +1
AI & RAG

AML Alert Triage. Uses httpRequest. Webhook trigger; 57 nodes.

HTTP Request
AI & RAG

W1 - IN WhatsApp Adapter (Secure + Fast ACK). Uses postgres, redis, httpRequest. Webhook trigger; 50 nodes.

Postgres, Redis, HTTP Request
AI & RAG

W1 - IN WhatsApp Adapter (Secure + Fast ACK). Uses postgres, redis, httpRequest. Webhook trigger; 48 nodes.

Postgres, Redis, HTTP Request