{
  "name": "Orchestrator",
  "nodes": [
    {
      "parameters": {
        "public": true,
        "initialMessages": "Give financial metrics for AAPL",
        "options": {}
      },
      "type": "@n8n/n8n-nodes-langchain.chatTrigger",
      "typeVersion": 1.3,
      "position": [
        -416,
        -128
      ],
      "id": "864688c6-1c41-4eab-85ef-8efb51566c9a",
      "name": "When chat message received"
    },
    {
      "parameters": {
        "model": "llama3:latest",
        "options": {}
      },
      "type": "@n8n/n8n-nodes-langchain.lmChatOllama",
      "typeVersion": 1,
      "position": [
        -176,
        48
      ],
      "id": "29165b1f-3e8d-4c17-acce-32940c94fa71",
      "name": "Ollama Chat Model"
    },
    {
      "parameters": {
        "messages": {
          "messageValues": [
            {
              "message": "You are a tool dispatcher. Analyze the user's request and return a JSON object with the tool name and parameters. Do not include any explanation or markdown formatting. Return only the JSON object.\n\nAvailable tools and their parameters:\n\n- **patent-monitor**\n  - days_back (integer, required): Number of days to look back for patents\n  - email (string, optional): Email address for notifications\n\n- **fin-metrics**\n  - ticker (string, required): Stock ticker symbol (e.g., \"AAPL\", \"TSLA\", \"MSFT\")\n  - narrative (boolean, required): Whether to parse narrative content from SEC filings. Analyze the user's question to determine if they want narrative analysis (mentions of \"insights\", \"analysis\", \"full report\", \"detailed\") or just metrics/numbers.\n\n- **news-sentiment**\n  - company (string, required): Company name to search for recent news\n  - email (string, optional): Email address to send the sentiment report\n\n- **forecasting**\n  - ticker (string, required): Stock ticker symbol to forecast\n  - email (string, required): Email address to send forecast results\n  - horizon (integer, optional, default: 30): Number of days to forecast\n\nReturn ONLY valid JSON in this exact format with no explanation:\n```json\n{\n  \"tool\": \"tool-name\",\n  \"params\": {\n    \"param1\": \"value1\",\n    \"param2\": value2\n  }\n}"
            }
          ]
        },
        "batching": {}
      },
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "typeVersion": 1.7,
      "position": [
        -80,
        -176
      ],
      "id": "c1b88e65-54b4-4227-a7ca-bd10a9eeb1a1",
      "name": "The Brain"
    },
    {
      "parameters": {
        "jsCode": "// Get the raw LLM output\nlet raw = $json[\"text\"] || $json[\"data\"] || \"\";\n\n// Clean up formatting issues\nraw = raw\n  .replace(/```(json)?/gi, '')\n  .replace(/\\\\n/g, '')\n  .replace(/\\n/g, '')\n  .trim();\n\nlet parsed = {};\ntry {\n  parsed = JSON.parse(raw);\n} catch (e) {\n  return [{\n    json: {\n      tool: \"error\",\n      params: { message: \"Failed to parse JSON\", raw },\n      url: null\n    }\n  }];\n}\n\n// Define tool configurations with URLs and expected parameter schemas\nconst toolConfigs = {\n  \"patent-monitor\": {\n    url: \"http://localhost:5678/webhook/patent-monitor\",\n    schema: {\n      days_back: \"number\",  // required\n      email: \"string\"       // optional\n    }\n  },\n  \"fin-metrics\": {\n    url: \"http://localhost:5678/webhook/sec-metrics\",\n    schema: {\n      ticker: \"string\"  ,\n      narrative: \"boolean\" // required\n    }\n  },\n  \"news-sentiment\": {\n    url: \"http://localhost:5678/webhook/news-sentiment\",\n    schema: {\n      company: \"string\",    // required\n      email: \"string\"       // optional\n    }\n  },\n  \"forecasting\": {\n    url: \"http://localhost:5678/webhook/forecasting\",\n    schema: {\n      ticker: \"string\",    // required\n      email: \"string\",       // required\n      horizon: \"number\"\n    }\n  }\n};\n\n// Validate and structure parameters\nfunction validateParams(tool, params) {\n  const config = toolConfigs[tool];\n  if (!config) {\n    return { \n      valid: false, \n      error: `Unknown tool: ${tool}`,\n      params: null \n    };\n  }\n\n  const validatedParams = {};\n  const schema = config.schema;\n\n  // Check each parameter against schema\n  for (const [key, value] of Object.entries(params)) {\n    if (schema.hasOwnProperty(key)) {\n      const expectedType = schema[key];\n      const actualType = Array.isArray(value) ? \"array\" : typeof value;\n      \n      if (actualType === expectedType || (actualType === \"object\" && expectedType === \"object\")) {\n        validatedParams[key] = value;\n      } else {\n        return {\n          valid: false,\n          error: `Parameter '${key}' should be ${expectedType}, got ${actualType}`,\n          params: null\n        };\n      }\n    }\n  }\n\n  return { valid: true, params: validatedParams };\n}\n\n// Function to build URL with query parameters (manual construction)\nfunction buildUrlWithParams(baseUrl, params) {\n  if (!params || Object.keys(params).length === 0) {\n    return baseUrl;\n  }\n  \n  // Manually build query string\n  const queryParts = [];\n  for (const [key, value] of Object.entries(params)) {\n    queryParts.push(`${encodeURIComponent(key)}=${encodeURIComponent(value)}`);\n  }\n  \n  const queryString = queryParts.join('&');\n  return `${baseUrl}?${queryString}`;\n}\n\n// Validate the parsed tool and params\nconst validation = validateParams(parsed.tool, parsed.params || {});\n\nif (!validation.valid) {\n  return [{\n    json: {\n      tool: \"error\",\n      params: { \n        message: validation.error,\n        original_tool: parsed.tool,\n        original_params: parsed.params\n      },\n      url: null\n    }\n  }];\n}\n\n// Get base URL and build full URL with query parameters\nconst baseUrl = toolConfigs[parsed.tool].url;\nconst fullUrl = buildUrlWithParams(baseUrl, validation.params);\n\n// Build final payload\nreturn [{\n  json: {\n    tool: parsed.tool,\n    params: validation.params,\n    baseUrl: baseUrl,\n    url: fullUrl,\n    metadata: {\n      timestamp: new Date().toISOString(),\n      validated: true\n    }\n  }\n}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        272,
        -176
      ],
      "id": "9dc7e552-9bb0-44af-8339-6f719b2b093b",
      "name": "The  Processor"
    },
    {
      "parameters": {
        "url": "={{ $json.url }}",
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        480,
        -176
      ],
      "id": "95408abc-d8fe-46e9-abd0-e3b5fac723f4",
      "name": "The Router"
    },
    {
      "id": "b9b4953f-3e1c-5473-b83e-c06bdadb82f4",
      "name": "Publication · Overview and setup",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -1216,
        -176
      ],
      "parameters": {
        "content": "# Orchestrator\n\n## Purpose and structure\nThis folder contains the **Orchestrator** workflow. Its executable nodes use `chainLlm`, `chatTrigger`, `code`, `httpRequest`, `lmChatOllama`. The table and connection map below are derived from the JSON, not an inferred guarantee of behavior.\n\n## Setup\nImport inactive. Configure your own credentials and replace REPLACE_WITH_* values. Review external destinations and Code nodes before running.\n\n## Entry points\nWhen chat message received\n\n## Verification\nSanitized source candidate; not runtime-tested. This overview is generated from node metadata. Read the folder README for the connection map, credential types, and license.\n\n## Attribution\nNik Bear Brown; rights confirmed 2026-10-19\nMIT; preserve source notices.",
        "width": 700,
        "height": 800,
        "color": 1
      }
    }
  ],
  "connections": {
    "Ollama Chat Model": {
      "ai_languageModel": [
        [
          {
            "node": "The Brain",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "When chat message received": {
      "main": [
        [
          {
            "node": "The Brain",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "The Brain": {
      "main": [
        [
          {
            "node": "The  Processor",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "The  Processor": {
      "main": [
        [
          {
            "node": "The Router",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1"
  }
}
