{
  "name": "Tagging_sub",
  "nodes": [
    {
      "parameters": {
        "model": "google/gemini-2.5-pro",
        "options": {}
      },
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenRouter",
      "typeVersion": 1,
      "position": [
        368,
        304
      ],
      "id": "d74fa437-2971-4e5d-b735-3bbb11ff748e",
      "name": "OpenRouter Chat Model",
      "credentials": {
        "openRouterApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "promptType": "define",
        "text": "=Du bist der REFEREE. Deine Aufgabe ist es, f\u00fcr eine Liste von Schlagworten (Batch) die jeweils korrekten GND-IDs zu finden.\n\nDEINE STRATEGIE\nDu erh\u00e4ltst einen Batch von bis zu 10 Schlagworten.\n\nAnalysiere jedes Schlagwort einzeln im Kontext des Objekts (Caption & Metadaten) UND nutze die mitgelieferten Felder rationale und judge_context zwingend zur Disambiguierung, falls ein Begriff mehrere unterschiedliche Bedeutungen hat.\n\nNutze das Tool search_gnd f\u00fcr jedes Keyword, um in der lokalen Datenbank nach Normdaten zu suchen. Du kannst und sollst mehrere Tool-Calls parallel oder hintereinander absetzen.\n\nWICHTIG: Das Tool durchsucht vorrangig Sachbegriffe (Materialien, Objekte, historische Konzepte, abstrakte Dinge).\n\n\u00dcbergib dem Tool den Suchbegriff. Wenn der Begriff mehrdeutig ist, kannst du auch Synonyme testen.\n\nValidiere die Tool-Ergebnisse gegen den visuellen Befund und die Metadaten. Achte im Tool-Ergebnis besonders auf das Feld \"definition\", um abzugleichen, ob es sich um das richtige Konzept handelt.\n\nFAST-FAIL REGELN (Zeit-Optimierung)\nBevor du das Tool nutzt oder tief nachdenkst, wende diese \"Short-Circuit\"-Logik an:\n\nZUSTANDS- UND QUALIT\u00c4TSBEGRIFFE:\nBegriffe wie \"besch\u00e4digt\", \"abgenutzt\", \"fleckig\", \"Kratzer\", \"unscharf\" oder \"r\u00f6tlich\" sind rein deskriptiv. Die GND f\u00fchrt hierf\u00fcr meist keine Sachschlagworte f\u00fcr die Bilderschlie\u00dfung.\n-> AKTION: Setze sofort \"gnd_id\": null und \"gnd_confidence\": \"no_match\". KEIN Tool-Call n\u00f6tig!\n\n1-TOOL-LIMIT F\u00dcR SACHBEGRIFFE:\nWenn eine Suche in search_gnd keine Treffer liefert, die EXAKT oder SEHR NAHE am Keyword liegen:\n-> AKTION: Suche NICHT endlos weiter. Setze \"gnd_id\": null.\n\nCLUSTER-FILTER:\nKeywords aus dem Cluster \"Visuelle_Merkmale\" (au\u00dfer konkrete Techniken wie \"Sepia\") sind fast immer \"no_match\". Pr\u00fcfe sie extrem streng und brich bei Zweifeln sofort ab.\n\nOUTPUT FORMAT (STRENGSTENS EINZUHALTEN)\nAntworte AUSSCHLIESSLICH mit einem validen JSON-Array. Jedes Element im Array entspricht einem Keyword aus dem Eingabe-Batch.\nDu musst die Ursprungsdaten (Schlagwort, Begr\u00fcndungen von LLM2 und LLM3) 1:1 aus dem Input-Batch \u00fcbernehmen und um deine eigenen GND-Ergebnisse erg\u00e4nzen.\n\nNutze exakt diese Struktur f\u00fcr jedes Item im Array:\n\n[\n{\n\"source_cluster\": \"Der Name des Clusters aus dem Input\",\n\"keyword_original\": \"Das 'keyword' aus dem Input-Batch\",\n\"llm2_rationale\": \"Das Feld 'rationale' aus dem Input-Batch (1:1 kopieren)\",\n\"llm3_judge_comment\": \"Das Feld 'judge_context' aus dem Input-Batch (1:1 kopieren)\",\n\"gnd_id\": \"4130439-1\",\n\"gnd_preferred_name\": \"Name laut GND (oder null)\",\n\"gnd_confidence\": \"high\" | \"medium\" | \"low\" | \"no_match\",\n\"llm4_reasoning\": \"Deine eigene Begr\u00fcndung, warum diese GND-ID passt oder warum keine gefunden wurde.\",\n\"additional_data\": {\n\"alternate_names\": \"String (kommaseparierte Synonyme laut GND oder null)\",\n\"definition\": \"String (die Definition laut GND oder null)\"\n}\n}\n]\n\nWICHTIG:\n\n\u00dcbernehme \"source_cluster\", \"keyword_original\", \"llm2_rationale\" und \"llm3_judge_comment\" EXAKT und ohne inhaltliche \u00c4nderungen aus deinem Input.\n\nDie Felder in \"additional_data\" nimmst du aus dem Output des Tools \"search_gnd\".\n\nKEIN Markdown (wie json ... ), nur das rohe Array, beginnend mit [ und endend mit ].",
        "hasOutputParser": true,
        "options": {
          "systemMessage": "===================================\n--- DER BATCH (10 SCHLAGWORTE) ---\n==================================\n\nHier sind die zu pr\u00fcfenden Keywords und ihre Cluster:\n{{ JSON.stringify($json.keyword_batch) }}\n\n==================================\n--- KONTEXT (ZUR ENTSCHEIDUNGSHILFE) ---\n==================================\n\nVISUELLER BEFUND (Caption):\n\"{{ $json.caption }}\"\n\nMETADATEN (Hintergrund):\n\"{{ $json.metadata }}\"\n\n==================================\n--- DEINE AUFGABE ---\n==================================\n\nBearbeite jetzt alle Schlagworte im Batch. F\u00fchre f\u00fcr jedes eine GND-Suche durch und gib das validierte JSON-Array zur\u00fcck.",
          "returnIntermediateSteps": true,
          "passthroughBinaryImages": true
        }
      },
      "type": "@n8n/n8n-nodes-langchain.agent",
      "typeVersion": 2.2,
      "position": [
        608,
        64
      ],
      "id": "c9bb91c9-10b2-42a1-a7e9-cd87bae52167",
      "name": "AI Agent",
      "alwaysOutputData": true,
      "retryOnFail": true,
      "waitBetweenTries": 5000,
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "amount": 3
      },
      "type": "n8n-nodes-base.wait",
      "typeVersion": 1.1,
      "position": [
        1008,
        288
      ],
      "id": "72c3535b-b810-46d4-a378-34b74bf288b5",
      "name": "Wait"
    },
    {
      "parameters": {
        "inputSource": "passthrough"
      },
      "id": "eb6fb377-b283-4deb-8032-d8f3352f3aa2",
      "typeVersion": 1.1,
      "name": "Start",
      "type": "n8n-nodes-base.executeWorkflowTrigger",
      "position": [
        -240,
        0
      ]
    },
    {
      "parameters": {
        "options": {}
      },
      "type": "n8n-nodes-base.splitInBatches",
      "typeVersion": 3,
      "position": [
        368,
        0
      ],
      "id": "afe98793-98f7-48a2-a173-19322766fad3",
      "name": "Loop Over Items"
    },
    {
      "parameters": {
        "jsCode": "/*\n=========================================================\nAGENT BATCH PARSER (v7 - Lineage & Multi-Batch Support)\n- Iteriert \u00fcber ALLE Batches (nicht nur den ersten)\n- Extrahiert JSON-Array aus Agenten-String\n- Handhabt Markdown-Backticks & Zeilenumbr\u00fcche\n- Extrahiert die Data Lineage (LLM2, 3 und 4)\n=========================================================\n*/\n\nconst items = $input.all();\nconst allParsedResults = [];\n\nfor (const itemWrapper of items) {\n    const item = itemWrapper.json;\n    const outputText = item.output || item.text || \"\"; \n\n    try {\n        // 1. Suche den Start und das Ende des Arrays im String\n        const arrayStart = outputText.indexOf('[');\n        const arrayEnd = outputText.lastIndexOf(']');\n        \n        if (arrayStart === -1 || arrayEnd === -1) {\n            allParsedResults.push({\n                json: { error: \"Kein JSON-Array im Output\", raw_output: outputText }\n            });\n            continue; // Mache mit dem n\u00e4chsten Batch weiter\n        }\n\n        const cleanJsonString = outputText.substring(arrayStart, arrayEnd + 1);\n        const agentResults = JSON.parse(cleanJsonString);\n\n        // 2. Jedes Ergebnis im Array als n8n-Item vereinzeln\n        for (const res of agentResults) {\n            allParsedResults.push({\n                json: {\n                    // IDs aus dem Flow-Kontext\n                    parent_id: item.parent_id || item.ID || \"N/A\", \n                    \n                    // Identifikation\n                    keyword_original: res.keyword_original || res.keyword || \"N/A\",\n                    source_cluster: res.source_cluster || \"Unsorted\",\n                    \n                    // GND Daten (mit Fallbacks auf alte Nomenklatur)\n                    gnd_id: res.gnd_id || null,\n                    preferred_name: res.gnd_preferred_name || res.preferred_name || null,\n                    confidence: res.gnd_confidence || res.confidence || \"no_match\",\n                    \n                    // DATA LINEAGE (Die Begr\u00fcndungen der LLMs)\n                    llm2_rationale: res.llm2_rationale || null,\n                    llm3_judge_comment: res.llm3_judge_comment || null,\n                    llm4_reasoning: res.llm4_reasoning || res.reasoning || null,\n                    \n                    // Metadaten flachklopfen\n                    meta_start_date: res.additional_data?.start_date || null,\n                    meta_end_date: res.additional_data?.end_date || null,\n                    meta_role: res.additional_data?.role_or_profession || null,\n                    meta_location: res.additional_data?.location_primary || null\n                }\n            });\n        }\n\n    } catch (e) {\n        // Fehlerfall f\u00fcr diesen spezifischen Batch\n        allParsedResults.push({\n            json: {\n                error: \"Parsing Error\",\n                message: e.message,\n                raw_output: outputText\n            }\n        });\n    }\n}\n\nreturn allParsedResults;"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        992,
        64
      ],
      "id": "67e53c20-6b06-409b-857e-7104885bf84f",
      "name": "Code in JavaScript1"
    },
    {
      "parameters": {
        "jsCode": "/*\n=========================================================\nFINAL AGGREGATOR (v19 - Post-Parser)\n- Aggregiert die vom Parser vereinzelten Items\n- Gruppiert nach 'source_cluster'\n=========================================================\n*/\n\nconst items = $input.all();\nconst enrichedResult = {};\n\nfor (const itemWrapper of items) {\n    const data = itemWrapper.json;\n    \n    // Fehlerhafte Items \u00fcberspringen\n    if (data.error) continue;\n    \n    const cluster = data.source_cluster || \"Unsorted\";\n    if (!enrichedResult[cluster]) {\n        enrichedResult[cluster] = [];\n    }\n\n    // Eintrag zusammenbauen\n    const entry = {\n        term: data.keyword_original,\n        gnd_name: data.preferred_name,\n        gnd_id: data.gnd_id,\n        confidence: data.confidence,\n        \n        // Data Lineage\n        llm2_rationale: data.llm2_rationale,\n        llm3_judge_comment: data.llm3_judge_comment,\n        llm4_reasoning: data.llm4_reasoning,\n        \n        // Metadaten (nur falls vorhanden)\n        ...(data.meta_start_date && { born_founded: data.meta_start_date }),\n        ...(data.meta_end_date && { died_dissolved: data.meta_end_date }),\n        ...(data.meta_role && { role_type: data.meta_role }),\n        ...(data.meta_location && { location: data.meta_location })\n    };\n\n    enrichedResult[cluster].push(entry);\n}\n\nreturn {\n    json: {\n        gnd_result_string: JSON.stringify(enrichedResult, null, 2)\n    }\n};"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        608,
        -96
      ],
      "id": "f3fe7d54-0ece-4c00-a58d-aaa050713337",
      "name": "Code in JavaScript2"
    },
    {
      "parameters": {
        "jsCode": "/*\n=========================================================\nBATCH SPLITTER (FINAL VERSION)\n- Gruppiert Keywords in 10er Batches\n- Extrahiert 'term', 'why' (rationale) & 'judge_comment' (judge_context)\n- Filtert technische Systemfelder aus den Metadaten\n- Sichere Fallbacks f\u00fcr Caption & Metadata\n=========================================================\n*/\n\nconst items = $input.all();\nconst outputBatches = [];\nconst BATCH_SIZE = 10;\n\n// Felder, die aus den Metadaten gel\u00f6scht werden sollen\nconst excludeKeys = [\n    \"tags_gemini_2.5_pro\",\n    \"_archived\",\n    \"_creator\",\n    \"_ctime\",\n    \"_last_modifier\",\n    \"_mtime\"\n];\n\n// Hilfsfunktion zur Reinigung der Metadaten-Strings\nfunction cleanMetadata(rawString) {\n    if (!rawString) return \"\";\n    return rawString\n        .split('\\n')\n        .filter(line => !excludeKeys.some(key => line.trim().startsWith(key + \":\")))\n        .join('\\n')\n        .trim();\n}\n\nconst allowedClusters = [\n    \"Objekttyp\", \"Thema_Ph\u00e4nomen\", \"Inhalt_Motiv\", \"Funktion_Zweck\",\n    \"Visuelle_Merkmale\", \"Form_Gestalt\", \"Bestandteile\", \"Gebrauchskontext\",\n    \"Kultureller_Kontext\", \"Emotion_Atmosph\u00e4re\", \"Farbe_Nuancen\"\n];\n\nfor (const item of items) {\n    const json = item.json;\n    \n    // Sichere Fallbacks: Greift alle m\u00f6glichen Schreibweisen ab\n    const rawCaption = json.Caption || json.caption || \"\";\n    const rawMeta = json.Metadata || json.metadata || json.context_data_string || \"\";\n    \n    // Metadaten bereinigen\n    const cleanedMeta = cleanMetadata(rawMeta);\n\n    // Keywords und Begr\u00fcndungen sammeln\n    const allKeywords = [];\n    for (const cluster of allowedClusters) {\n        let keywordArray = json[cluster]; \n        \n        if (!keywordArray) continue;\n\n        if (typeof keywordArray === 'string') {\n            try { keywordArray = JSON.parse(keywordArray); } catch (e) { keywordArray = []; }\n        }\n\n        if (Array.isArray(keywordArray)) {\n            keywordArray.forEach(kw => {\n                if (kw && kw.term && typeof kw.term === 'string' && kw.term.trim() !== \"\") {\n                    allKeywords.push({\n                        keyword: kw.term.trim(),\n                        source_cluster: cluster,\n                        rationale: kw.why || \"\",             // Neu: LLM2 Begr\u00fcndung\n                        judge_context: kw.judge_comment || \"\" // Neu: LLM3 Urteil\n                    });\n                }\n            });\n        }\n    }\n\n    // In 10er Batches aufteilen und Output generieren\n    for (let i = 0; i < allKeywords.length; i += BATCH_SIZE) {\n        const batchChunk = allKeywords.slice(i, i + BATCH_SIZE);\n        \n        outputBatches.push({\n            json: {\n                keyword_batch: batchChunk, \n                caption: rawCaption,    \n                metadata: cleanedMeta,  \n                batch_info: {\n                    current: Math.floor(i / BATCH_SIZE) + 1,\n                    total: Math.ceil(allKeywords.length / BATCH_SIZE)\n                }\n            }\n        });\n    }\n}\n\nreturn outputBatches;"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        160,
        0
      ],
      "id": "813a8968-f61e-411d-892a-57c47d108ce5",
      "name": "Code in JavaScript3"
    },
    {
      "parameters": {
        "toolDescription": "Der exakte Suchbegriff f\u00fcr die GND. Bitte nur das nackte Wort \u00fcbergeben (z.B. Preu\u00dfen oder Handschrift). Keine Sonderzeichen, keine Lucene-Syntax und keine Formatierungen!",
        "method": "POST",
        "url": "http://gnd_elasticsearch:9200/gnd_index/_search",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={\n  \"size\": 5,\n  \"query\": {\n    \"multi_match\": {\n      \"query\": \"{{ $fromAI('suchbegriff', 'Der exakte Suchbegriff f\u00fcr die GND. Bitte nur das nackte Wort \u00fcbergeben (z.B. Preu\u00dfen oder Handschrift).') }}\",\n      \"fields\": [\n        \"preferred_name^4\",\n        \"alternate_names^3\",\n        \"definition^1\"\n      ],\n      \"fuzziness\": \"AUTO\"\n    }\n  }\n}",
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequestTool",
      "typeVersion": 4.4,
      "position": [
        704,
        304
      ],
      "id": "50ca3d34-b2bb-4592-9656-9297b2318b3b",
      "name": "search_gnd"
    }
  ],
  "connections": {
    "OpenRouter Chat Model": {
      "ai_languageModel": [
        [
          {
            "node": "AI Agent",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "AI Agent": {
      "main": [
        [
          {
            "node": "Code in JavaScript1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Start": {
      "main": [
        [
          {
            "node": "Code in JavaScript3",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Loop Over Items": {
      "main": [
        [
          {
            "node": "Code in JavaScript2",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "AI Agent",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait": {
      "main": [
        [
          {
            "node": "Loop Over Items",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Code in JavaScript1": {
      "main": [
        [
          {
            "node": "Wait",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Code in JavaScript3": {
      "main": [
        [
          {
            "node": "Loop Over Items",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "search_gnd": {
      "ai_tool": [
        [
          {
            "node": "AI Agent",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": true,
  "settings": {
    "executionOrder": "v1",
    "binaryMode": "separate",
    "availableInMCP": false
  },
  "versionId": "f7fbcfc3-03f3-465f-a204-892cd027b036",
  "id": "132peiuMeOoe9GA2Pcg91",
  "tags": []
}