From 8adf1fd46160f5de9aadcd2e02ac3e781beb4a9d Mon Sep 17 00:00:00 2001 From: Ashu Ghildiyal Date: Thu, 4 Sep 2025 16:09:41 +0530 Subject: [PATCH 01/39] ai-plugin: add mcp adapter Signed-off-by: Ashu Ghildiyal Signed-off-by: ashu8912 --- ai-assistant/package-lock.json | 764 +++++++++++++++++- ai-assistant/package.json | 3 + ai-assistant/src/ai/mcp/client.ts | 44 + ai-assistant/src/ai/mcp/electron-client.ts | 147 ++++ .../src/langchain/LangChainManager.ts | 87 +- .../src/langchain/tools/ToolManager.ts | 62 +- ai-assistant/src/types/electron.d.ts | 37 + 7 files changed, 1115 insertions(+), 29 deletions(-) create mode 100644 ai-assistant/src/ai/mcp/client.ts create mode 100644 ai-assistant/src/ai/mcp/electron-client.ts create mode 100644 ai-assistant/src/types/electron.d.ts diff --git a/ai-assistant/package-lock.json b/ai-assistant/package-lock.json index cafe6e970f..52483a30e2 100644 --- a/ai-assistant/package-lock.json +++ b/ai-assistant/package-lock.json @@ -12,9 +12,12 @@ "@langchain/community": "^0.3.42", "@langchain/core": "^0.3.51", "@langchain/google-genai": "^0.2.5", + "@langchain/langgraph": "^0.4.9", + "@langchain/mcp-adapters": "^0.6.0", "@langchain/mistralai": "^0.2.0", "@langchain/ollama": "^0.2.0", "@langchain/openai": "^0.5.5", + "@modelcontextprotocol/sdk": "^1.17.5", "@monaco-editor/react": "^4.5.2", "@types/prismjs": "^1.26.5", "@types/react-syntax-highlighter": "^15.5.13", @@ -2706,6 +2709,106 @@ "uuid": "dist/esm/bin/uuid" } }, + "node_modules/@langchain/langgraph": { + "version": "0.4.9", + "resolved": "https://registry.npmjs.org/@langchain/langgraph/-/langgraph-0.4.9.tgz", + "integrity": "sha512-+rcdTGi4Ium4X/VtIX3Zw4RhxEkYWpwUyz806V6rffjHOAMamg6/WZDxpJbrP33RV/wJG1GH12Z29oX3Pqq3Aw==", + "license": "MIT", + "dependencies": { + "@langchain/langgraph-checkpoint": "^0.1.1", + "@langchain/langgraph-sdk": "~0.1.0", + "uuid": "^10.0.0", + "zod": "^3.25.32" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@langchain/core": ">=0.3.58 < 0.4.0", + "zod-to-json-schema": "^3.x" + }, + "peerDependenciesMeta": { + "zod-to-json-schema": { + "optional": true + } + } + }, + "node_modules/@langchain/langgraph-checkpoint": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@langchain/langgraph-checkpoint/-/langgraph-checkpoint-0.1.1.tgz", + "integrity": "sha512-h2bP0RUikQZu0Um1ZUPErQLXyhzroJqKRbRcxYRTAh49oNlsfeq4A3K4YEDRbGGuyPZI/Jiqwhks1wZwY73AZw==", + "license": "MIT", + "dependencies": { + "uuid": "^10.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@langchain/core": ">=0.2.31 <0.4.0 || ^1.0.0-alpha" + } + }, + "node_modules/@langchain/langgraph-sdk": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@langchain/langgraph-sdk/-/langgraph-sdk-0.1.0.tgz", + "integrity": "sha512-1EKwzwJpgpNqLcRuGG+kLvvhAaPiFWZ9shl/obhL8qDKtYdbR67WCYE+2jUObZ8vKQuCoul16ewJ78g5VrZlKA==", + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.15", + "p-queue": "^6.6.2", + "p-retry": "4", + "uuid": "^9.0.0" + }, + "peerDependencies": { + "@langchain/core": ">=0.2.31 <0.4.0", + "react": "^18 || ^19", + "react-dom": "^18 || ^19" + }, + "peerDependenciesMeta": { + "@langchain/core": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/@langchain/langgraph-sdk/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@langchain/mcp-adapters": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@langchain/mcp-adapters/-/mcp-adapters-0.6.0.tgz", + "integrity": "sha512-NHQNH9NciLhxlCnL/4HDebiYT3UQvpBfF5KPlIi/uSXn8te/bYjPV64gUyAloNNo+fjj4qDvKP1/nHj0r7fKFw==", + "license": "MIT", + "dependencies": { + "@modelcontextprotocol/sdk": "^1.12.1", + "debug": "^4.4.0", + "zod": "^3.24.2" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "extended-eventsource": "^1.x" + }, + "peerDependencies": { + "@langchain/core": "^0.3.66" + } + }, "node_modules/@langchain/mistralai": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/@langchain/mistralai/-/mistralai-0.2.1.tgz", @@ -2836,6 +2939,51 @@ "zod": ">= 3" } }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.17.5", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.17.5.tgz", + "integrity": "sha512-QakrKIGniGuRVfWBdMsDea/dx1PNE739QJ7gCM41s9q+qaCYTHCdsIBXQVVXry3mfWAiaM9kT22Hyz53Uw8mfg==", + "license": "MIT", + "dependencies": { + "ajv": "^6.12.6", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.0.1", + "express-rate-limit": "^7.5.0", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.23.8", + "zod-to-json-schema": "^3.24.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "license": "MIT" + }, "node_modules/@monaco-editor/loader": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.5.0.tgz", @@ -5274,7 +5422,6 @@ "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true, "license": "MIT" }, "node_modules/@types/json5": { @@ -6106,6 +6253,40 @@ "integrity": "sha512-VtUwTNU8fpMwvWGn4xE93ywbogTYsuT+AUxAXOeelbXuQVIwNmC5YLeho9sH4vZ4ITW8414TTAOG1nW6uIVHCA==", "license": "MIT" }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/acorn": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", @@ -6745,6 +6926,26 @@ "dev": true, "license": "MIT" }, + "node_modules/body-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", + "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.0", + "http-errors": "^2.0.0", + "iconv-lite": "^0.6.3", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.0", + "type-is": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/boolbase": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", @@ -7056,6 +7257,15 @@ "dev": true, "license": "MIT" }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/cac": { "version": "6.7.14", "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", @@ -7102,7 +7312,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -7644,6 +7853,27 @@ "dev": true, "license": "MIT" }, + "node_modules/content-disposition": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", + "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/convert-source-map": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", @@ -7655,12 +7885,20 @@ "version": "0.7.2", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" } }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, "node_modules/core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", @@ -7668,6 +7906,19 @@ "dev": true, "license": "MIT" }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/cosmiconfig": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", @@ -7791,7 +8042,6 @@ "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -8319,6 +8569,15 @@ "node": ">=0.4.0" } }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -8525,6 +8784,12 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, "node_modules/electron-to-chromium": { "version": "1.5.193", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.193.tgz", @@ -8569,6 +8834,15 @@ "dev": true, "license": "MIT" }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/encoding-sniffer": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz", @@ -8897,6 +9171,12 @@ "node": ">=6" } }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -9521,6 +9801,15 @@ "node": ">=0.10.0" } }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/event-target-shim": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", @@ -9546,6 +9835,27 @@ "node": ">=0.8.x" } }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", + "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/evp_bytestokey": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", @@ -9604,12 +9914,97 @@ "integrity": "sha512-4EMSHGOPSwAfBiibw3ndnP0AvjDWLsMvGOvWEZ2F96IGk0bIVdjQisOHxReSkE13mHcfbuCiXw+G4y0zv6N8Eg==", "license": "MIT" }, + "node_modules/express": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", + "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.0", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.1.tgz", + "integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/express/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", "license": "MIT" }, + "node_modules/extended-eventsource": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/extended-eventsource/-/extended-eventsource-1.7.0.tgz", + "integrity": "sha512-s8rtvZuYcKBpzytHb5g95cHbZ1J99WeMnV18oKc5wKoxkHzlzpPc/bNAm7Da2Db0BDw0CAu1z3LpH+7UsyzIpw==", + "license": "MIT", + "optional": true + }, "node_modules/fake-indexeddb": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/fake-indexeddb/-/fake-indexeddb-6.0.1.tgz", @@ -9624,7 +10019,6 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, "license": "MIT" }, "node_modules/fast-equals": { @@ -9678,7 +10072,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, "license": "MIT" }, "node_modules/fast-levenshtein": { @@ -9818,6 +10211,23 @@ "node": ">=8" } }, + "node_modules/finalhandler": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", + "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/find-root": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", @@ -9972,6 +10382,24 @@ "node": ">= 12.20" } }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/fs-extra": { "version": "11.3.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.0.tgz", @@ -10817,6 +11245,31 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-errors/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/http-proxy-agent": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", @@ -10978,7 +11431,6 @@ "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "devOptional": true, "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" @@ -11081,7 +11533,6 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, "license": "ISC" }, "node_modules/inline-style-parser": { @@ -11126,6 +11577,15 @@ "node": ">= 0.10" } }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/is-alphabetical": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", @@ -11564,6 +12024,12 @@ "devOptional": true, "license": "MIT" }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, "node_modules/is-regex": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", @@ -11779,7 +12245,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, "license": "ISC" }, "node_modules/isomorphic-timers-promises": { @@ -13221,6 +13686,15 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/memoize-one": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", @@ -13238,6 +13712,18 @@ "map-or-similar": "^1.5.0" } }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -14142,6 +14628,15 @@ "dev": true, "license": "MIT" }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/nice-grpc": { "version": "2.1.12", "resolved": "https://registry.npmjs.org/nice-grpc/-/nice-grpc-2.1.12.tgz", @@ -14463,7 +14958,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -14473,7 +14967,6 @@ "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -14608,11 +15101,22 @@ "whatwg-fetch": "^3.6.20" } }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, "license": "ISC", "dependencies": { "wrappy": "1" @@ -14988,6 +15492,15 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/path-browserify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", @@ -15019,7 +15532,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -15182,6 +15694,15 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pkce-challenge": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz", + "integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, "node_modules/pkg-dir": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-5.0.0.tgz", @@ -15435,6 +15956,19 @@ "node": ">=12.0.0" } }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -15500,7 +16034,6 @@ "version": "6.14.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", - "dev": true, "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.1.0" @@ -15641,6 +16174,46 @@ "safe-buffer": "^5.1.0" } }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.1.tgz", + "integrity": "sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.7.0", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/raw-body/node_modules/iconv-lite": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", + "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/react": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", @@ -16695,6 +17268,32 @@ "fsevents": "~2.3.2" } }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/router/node_modules/path-to-regexp": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/rrweb-cssom": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz", @@ -16839,7 +17438,6 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "devOptional": true, "license": "MIT" }, "node_modules/sanitize-html": { @@ -16916,6 +17514,64 @@ "dev": true, "license": "MIT" }, + "node_modules/send": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "mime-types": "^3.0.1", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/send/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/send/node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -16972,6 +17628,12 @@ "dev": true, "license": "MIT" }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, "node_modules/sha.js": { "version": "2.4.12", "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.12.tgz", @@ -16997,7 +17659,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -17010,7 +17671,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -17101,7 +17761,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -17121,7 +17780,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -17138,7 +17796,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -17157,7 +17814,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -17318,7 +17974,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -18201,6 +18856,15 @@ "node": ">=10.13.0" } }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, "node_modules/token-types": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/token-types/-/token-types-4.2.1.tgz", @@ -18361,6 +19025,41 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/type-is/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/type-is/node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/typed-array-buffer": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", @@ -18600,6 +19299,15 @@ "node": ">= 10.0.0" } }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/unplugin": { "version": "1.16.1", "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.16.1.tgz", @@ -18649,7 +19357,6 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, "license": "BSD-2-Clause", "dependencies": { "punycode": "^2.1.0" @@ -18766,6 +19473,15 @@ "node": ">= 10.13.0" } }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/vfile": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", @@ -19473,7 +20189,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -19729,7 +20444,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, "license": "ISC" }, "node_modules/ws": { diff --git a/ai-assistant/package.json b/ai-assistant/package.json index 47f9d48498..f017c3e988 100644 --- a/ai-assistant/package.json +++ b/ai-assistant/package.json @@ -38,9 +38,12 @@ "@langchain/community": "^0.3.42", "@langchain/core": "^0.3.51", "@langchain/google-genai": "^0.2.5", + "@langchain/langgraph": "^0.4.9", + "@langchain/mcp-adapters": "^0.6.0", "@langchain/mistralai": "^0.2.0", "@langchain/ollama": "^0.2.0", "@langchain/openai": "^0.5.5", + "@modelcontextprotocol/sdk": "^1.17.5", "@monaco-editor/react": "^4.5.2", "@types/prismjs": "^1.26.5", "@types/react-syntax-highlighter": "^15.5.13", diff --git a/ai-assistant/src/ai/mcp/client.ts b/ai-assistant/src/ai/mcp/client.ts new file mode 100644 index 0000000000..8838b3af12 --- /dev/null +++ b/ai-assistant/src/ai/mcp/client.ts @@ -0,0 +1,44 @@ +import { MultiServerMCPClient } from "@langchain/mcp-adapters"; + +const client = new MultiServerMCPClient({ + // Global tool configuration options + // Whether to throw on errors if a tool fails to load (optional, default: true) + throwOnLoadError: true, + // Whether to prefix tool names with the server name (optional, default: false) + prefixToolNameWithServerName: false, + // Optional additional prefix for tool names (optional, default: "") + additionalToolNamePrefix: "", + + // Use standardized content block format in tool outputs + useStandardContentBlocks: true, + + // Server configuration + mcpServers: { + // adds the Inspektor Gadget MCP server + "inspektor-gadget": { + transport: "stdio", + command: "docker", + args: [ + "run", + "-i", + "--rm", + "--mount", + "type=bind,src=${env:HOME}/.kube/config,dst=/kubeconfig", + "ghcr.io/inspektor-gadget/ig-mcp-server:latest", + "-gadget-discoverer=artifacthub" + ], + // Restart configuration for stdio transport + restart: { + enabled: true, + maxAttempts: 3, + delayMs: 2000, // Slightly longer delay for Docker container startup + }, + }, + } +}); + +const tools = async function() { + return await client.getTools(); +}; + +export default tools; \ No newline at end of file diff --git a/ai-assistant/src/ai/mcp/electron-client.ts b/ai-assistant/src/ai/mcp/electron-client.ts new file mode 100644 index 0000000000..af73ea5913 --- /dev/null +++ b/ai-assistant/src/ai/mcp/electron-client.ts @@ -0,0 +1,147 @@ +// Frontend MCP client that communicates with Electron main process +// This replaces the direct MCP client import to avoid spawn issues in renderer process + +interface MCPTool { + name: string; + description?: string; + inputSchema?: any; +} + +interface MCPResponse { + success: boolean; + tools?: MCPTool[]; + result?: any; + error?: string; + toolCallId?: string; +} + +interface ElectronMCPApi { + getTools: () => Promise; + executeTool: (toolName: string, args: Record, toolCallId?: string) => Promise; + getStatus: () => Promise<{ isInitialized: boolean; hasClient: boolean }>; + resetClient: () => Promise; +} + +declare global { + interface Window { + desktopApi?: { + mcp: ElectronMCPApi; + }; + } +} + +class ElectronMCPClient { + private isElectron: boolean; + + constructor() { + this.isElectron = typeof window !== 'undefined' && + typeof window.desktopApi !== 'undefined' && + typeof window.desktopApi.mcp !== 'undefined'; + } + + /** + * Check if running in Electron environment with MCP support + */ + isAvailable(): boolean { + return this.isElectron; + } + + /** + * Get available MCP tools from Electron main process + */ + async getTools(): Promise { + if (!this.isElectron) { + console.warn('MCP client not available - not running in Electron environment'); + return []; + } + + try { + const response = await window.desktopApi!.mcp.getTools(); + console.log( + "mcp response from getting tools is", + response + ); + console.log("mcp window desktop api", window.desktopApi!.mcp.getTools) + if (response.success && response.tools) { + console.log('Retrieved MCP tools from Electron:', response.tools.length, 'tools'); + return response.tools; + } else { + console.warn('Failed to get MCP tools:', response.error); + return []; + } + } catch (error) { + console.error('Error getting MCP tools from Electron:', error); + return []; + } + } + + /** + * Execute an MCP tool via Electron main process + */ + async executeTool( + toolName: string, + args: Record, + toolCallId?: string + ): Promise { + if (!this.isElectron) { + throw new Error('MCP client not available - not running in Electron environment'); + } + + try { + const response = await window.desktopApi!.mcp.executeTool(toolName, args, toolCallId); + + if (response.success) { + return response.result; + } else { + throw new Error(response.error || 'Unknown error executing MCP tool'); + } + } catch (error) { + console.error(`Error executing MCP tool ${toolName}:`, error); + throw error; + } + } + + /** + * Get MCP client status from Electron main process + */ + async getStatus(): Promise<{ isInitialized: boolean; hasClient: boolean }> { + if (!this.isElectron) { + return { isInitialized: false, hasClient: false }; + } + + try { + return await window.desktopApi!.mcp.getStatus(); + } catch (error) { + console.error('Error getting MCP status:', error); + return { isInitialized: false, hasClient: false }; + } + } + + /** + * Reset/restart MCP client in Electron main process + */ + async resetClient(): Promise { + if (!this.isElectron) { + return false; + } + + try { + const response = await window.desktopApi!.mcp.resetClient(); + return response.success; + } catch (error) { + console.error('Error resetting MCP client:', error); + return false; + } + } +} + +// Export a function that returns tools (compatible with existing interface) +const tools = async function(): Promise { + const client = new ElectronMCPClient(); + console.log("mcp electron client is ", client); + return await client.getTools(); +}; + +// Export both the client class and the tools function for flexibility +export { ElectronMCPClient }; +export default tools; diff --git a/ai-assistant/src/langchain/LangChainManager.ts b/ai-assistant/src/langchain/LangChainManager.ts index ceddb1bb69..fcc71223ce 100644 --- a/ai-assistant/src/langchain/LangChainManager.ts +++ b/ai-assistant/src/langchain/LangChainManager.ts @@ -29,6 +29,7 @@ export default class LangChainManager extends AIManager { private currentAbortController: AbortController | null = null; private promptTemplate: ChatPromptTemplate; private outputParser: StringOutputParser; + private useDirectToolCalling: boolean = false; constructor(providerId: string, config: Record, enabledTools?: string[]) { super(); @@ -178,7 +179,7 @@ export default class LangChainManager extends AIManager { } } - configureTools(tools: any[], kubernetesContext: KubernetesToolContext): void { + async configureTools(tools: any[], kubernetesContext: KubernetesToolContext): Promise { console.log('🔧 Configuring tools for LangChain with context:', { toolCount: tools.length, selectedClusters: kubernetesContext.selectedClusters, @@ -188,10 +189,32 @@ export default class LangChainManager extends AIManager { // Configure the Kubernetes context for the KubernetesTool this.toolManager.configureKubernetesContext(kubernetesContext); + // Get all tools (including MCP tools) + const allTools = this.toolManager.getLangChainTools(); + console.log(`🔧 Total tools available: ${allTools.length} (Regular: ${tools.length}, MCP: ${this.toolManager.getMCPTools().length})`); + // Bind all tools to the model for compatible providers (OpenAI, Azure, etc.) this.boundModel = this.toolManager.bindToModel(this.model, this.providerId); - console.log('🔧 Tools bound to model successfully, boundModel exists:', !!this.boundModel); + // Enable direct tool calling for better performance + if (allTools.length > 0 && this.canUseDirectToolCalling()) { + this.useDirectToolCalling = true; + console.log('🔧 Direct tool calling enabled for', allTools.length, 'tools'); + } + + console.log('🔧 Tools configured:', { + boundModel: !!this.boundModel, + directToolCalling: this.useDirectToolCalling, + toolCount: allTools.length + }); + } + + /** + * Check if the current provider can use direct tool calling + */ + private canUseDirectToolCalling(): boolean { + // All major providers support direct tool calling + return ['openai', 'azure', 'anthropic', 'mistral', 'gemini'].includes(this.providerId); } // Helper method to prepare chat history for prompt template @@ -241,6 +264,11 @@ export default class LangChainManager extends AIManager { this.currentAbortController = new AbortController(); try { + // Use direct tool calling if enabled + if (this.useDirectToolCalling) { + return await this.handleDirectToolCallingRequest(message); + } + const modelToUse = this.boundModel || this.model; // For local models, use simplified approach @@ -255,6 +283,61 @@ export default class LangChainManager extends AIManager { } } + // Handle requests using direct tool calling (single LLM call) + private async handleDirectToolCallingRequest(message: string): Promise { + try { + console.log('🔧 Using direct tool calling for request:', message); + + const modelToUse = this.boundModel || this.model; + + // Prepare input for the model with tools + const chainInput = { + systemPrompt: this.createSystemPrompt(), + chatHistory: this.prepareChatHistory(), + input: message, + }; + + // Convert chain input to messages + const messages = [ + new SystemMessage(chainInput.systemPrompt), + ...chainInput.chatHistory, + new HumanMessage(chainInput.input), + ]; + + // Single LLM call with tool capabilities + const response = await modelToUse.invoke(messages, { + signal: this.currentAbortController?.signal, + }); + + this.currentAbortController = null; + + // Handle tool calls if present + if (response.tool_calls?.length) { + console.log('🔧 Tool calls detected, processing...'); + return await this.handleToolCalls(response); + } else { + console.log('💬 No tool calls detected, treating as regular message'); + // Handle regular response + const assistantPrompt: Prompt = { + role: 'assistant', + content: this.extractTextContent(response.content), + }; + this.history.push(assistantPrompt); + return assistantPrompt; + } + + } catch (error) { + console.error('Error in direct tool calling request:', error); + + // If direct tool calling fails, fall back to regular approach + console.log('🔄 Falling back to chain-based approach'); + this.useDirectToolCalling = false; + + const modelToUse = this.boundModel || this.model; + return await this.handleChainBasedRequest(message, modelToUse); + } + } + // Handle requests for local models (simplified) private async handleLocalModelRequest(message: string, model: BaseChatModel): Promise { const systemMessage = new SystemMessage(this.createSystemPrompt()); diff --git a/ai-assistant/src/langchain/tools/ToolManager.ts b/ai-assistant/src/langchain/tools/ToolManager.ts index d13b41132d..1e13a6f0a2 100644 --- a/ai-assistant/src/langchain/tools/ToolManager.ts +++ b/ai-assistant/src/langchain/tools/ToolManager.ts @@ -1,14 +1,19 @@ import { BaseChatModel } from '@langchain/core/language_models/chat_models'; +import { DynamicTool } from '@langchain/core/tools'; import { Prompt } from '../../ai/manager'; import { KubernetesTool, KubernetesToolContext } from './kubernetes'; import { AVAILABLE_TOOLS, getToolByName } from './registry'; import { ToolBase, ToolResponse } from './ToolBase'; +import tools, { ElectronMCPClient } from '../../ai/mcp/electron-client'; export class ToolManager { private tools: ToolBase[] = []; private toolHandlers: Map = new Map(); + private mcpTools: DynamicTool[] = []; + private mcpClient: ElectronMCPClient; constructor(enabledToolIds?: string[]) { + this.mcpClient = new ElectronMCPClient(); this.initializeTools(enabledToolIds); } @@ -16,6 +21,9 @@ export class ToolManager { * Initialize only enabled tools from the registry */ private initializeTools(enabledToolIds?: string[]): void { + // Initialize MCP tools with proper error handling + this.initializeMCPTools(); + for (const ToolClass of AVAILABLE_TOOLS) { const tempTool = new ToolClass(); if (enabledToolIds && !enabledToolIds.includes(tempTool.config.name)) { @@ -30,6 +38,45 @@ export class ToolManager { } } } + + /** + * Initialize MCP tools from Electron main process + */ + private async initializeMCPTools(): Promise { + try { + console.log('Initializing MCP tools from Electron...'); + const mcpToolsData = await tools(); + + if (mcpToolsData && mcpToolsData.length > 0) { + console.log(`Successfully loaded ${mcpToolsData.length} MCP tools from Electron`); + + // Convert MCP tools to LangChain DynamicTool format + this.mcpTools = mcpToolsData.map(toolData => + new DynamicTool({ + name: toolData.name, + description: toolData.description || `MCP tool: ${toolData.name}`, + schema: toolData.inputSchema, + func: async (args: any) => { + try { + const result = await this.mcpClient.executeTool(toolData.name, args); + return typeof result === 'string' ? result : JSON.stringify(result); + } catch (error) { + console.error(`Error executing MCP tool ${toolData.name}:`, error); + throw error; + } + } + }) + ); + + console.log(`Converted ${this.mcpTools.length} MCP tools to LangChain format`); + } else { + console.log('No MCP tools available or MCP client not initialized'); + } + } catch (error) { + console.warn('Failed to initialize MCP tools from Electron:', error instanceof Error ? error.message : 'Unknown error'); + // Continue without MCP tools - this is not a fatal error + } + } /** * Configure external dependencies for tools that need them */ @@ -67,10 +114,18 @@ export class ToolManager { } /** - * Get all configured tools as LangChain tools + * Get all configured tools as LangChain tools (including MCP tools) */ getLangChainTools() { - return this.tools.map(tool => tool.createLangChainTool()); + const regularTools = this.tools.map(tool => tool.createLangChainTool()); + return [...regularTools, ...this.mcpTools]; + } + + /** + * Get all MCP tools as LangChain tools + */ + getMCPTools() { + return this.mcpTools; } /** @@ -112,6 +167,9 @@ export class ToolManager { return model; } + console.log(`Binding ${langChainTools.length} tools to ${providerId} model:`, + langChainTools.map(t => t.name)); + return model.bindTools(langChainTools); } catch (error) { console.error(`Error binding tools to ${providerId} model:`, error); diff --git a/ai-assistant/src/types/electron.d.ts b/ai-assistant/src/types/electron.d.ts new file mode 100644 index 0000000000..f00bcbf663 --- /dev/null +++ b/ai-assistant/src/types/electron.d.ts @@ -0,0 +1,37 @@ +// Type definitions for the Electron desktop API exposed to the renderer process + +interface MCPTool { + name: string; + description?: string; + inputSchema?: any; +} + +interface MCPResponse { + success: boolean; + tools?: MCPTool[]; + result?: any; + error?: string; + toolCallId?: string; +} + +interface ElectronMCPApi { + getTools: () => Promise; + executeTool: (toolName: string, args: Record, toolCallId?: string) => Promise; + getStatus: () => Promise<{ isInitialized: boolean; hasClient: boolean }>; + resetClient: () => Promise; +} + +interface DesktopApi { + send: (channel: string, data: unknown) => void; + receive: (channel: string, func: (...args: unknown[]) => void) => void; + removeListener: (channel: string, func: (...args: unknown[]) => void) => void; + mcp: ElectronMCPApi; +} + +declare global { + interface Window { + desktopApi?: DesktopApi; + } +} + +export { MCPTool, MCPResponse, ElectronMCPApi, DesktopApi }; From 2bbc05759b93cb61e8acfa1daafe1dde9732154f Mon Sep 17 00:00:00 2001 From: Ashu Ghildiyal Date: Mon, 8 Sep 2025 22:58:28 +0530 Subject: [PATCH 02/39] ai-plugin: Add mcp setting and electron mcp client Signed-off-by: Ashu Ghildiyal Signed-off-by: ashu8912 --- ai-assistant/src/ai/mcp/electron-client.ts | 2 + .../src/components/settings/MCPSettings.tsx | 411 ++++++++++++++++++ 2 files changed, 413 insertions(+) create mode 100644 ai-assistant/src/components/settings/MCPSettings.tsx diff --git a/ai-assistant/src/ai/mcp/electron-client.ts b/ai-assistant/src/ai/mcp/electron-client.ts index af73ea5913..e09f8510cb 100644 --- a/ai-assistant/src/ai/mcp/electron-client.ts +++ b/ai-assistant/src/ai/mcp/electron-client.ts @@ -20,6 +20,8 @@ interface ElectronMCPApi { executeTool: (toolName: string, args: Record, toolCallId?: string) => Promise; getStatus: () => Promise<{ isInitialized: boolean; hasClient: boolean }>; resetClient: () => Promise; + getConfig: () => Promise<{ success: boolean; config?: any; error?: string }>; + updateConfig: (config: any) => Promise; } declare global { diff --git a/ai-assistant/src/components/settings/MCPSettings.tsx b/ai-assistant/src/components/settings/MCPSettings.tsx new file mode 100644 index 0000000000..0706893796 --- /dev/null +++ b/ai-assistant/src/components/settings/MCPSettings.tsx @@ -0,0 +1,411 @@ +import { SectionBox } from '@kinvolk/headlamp-plugin/lib/components/common'; +import { + Box, + Button, + Card, + CardContent, + Divider, + FormControlLabel, + IconButton, + Switch, + TextField, + Typography, +} from '@mui/material'; +import { Icon } from '@iconify/react'; +import React, { useEffect, useState } from 'react'; +import { pluginStore } from '../../utils'; + +// Helper function to check if running in Electron +const isElectron = (): boolean => { + return typeof window !== 'undefined' && + typeof window.desktopApi !== 'undefined' && + typeof window.desktopApi.mcp !== 'undefined'; +}; + +export interface MCPServer { + name: string; + command: string; + args: string[]; + enabled: boolean; +} + +export interface MCPConfig { + enabled: boolean; + servers: MCPServer[]; +} + +interface MCPSettingsProps { + config?: MCPConfig; + onConfigChange?: (config: MCPConfig) => void; +} + +const defaultMCPServer: MCPServer = { + name: '', + command: 'npx', + args: [], + enabled: true, +}; + +export function MCPSettings({ config, onConfigChange }: MCPSettingsProps) { + const [mcpConfig, setMCPConfig] = useState( + config || { + enabled: false, + servers: [], + } + ); + + const [newServerName, setNewServerName] = useState(''); + const [newServerCommand, setNewServerCommand] = useState('npx'); + const [newServerArgs, setNewServerArgs] = useState(''); + + useEffect(() => { + // Load MCP config from Electron if available + if (isElectron()) { + loadMCPConfigFromElectron(); + } else { + // Fallback to plugin store for non-Electron environments + const savedConfig = pluginStore.get(); + if (savedConfig?.mcpConfig) { + setMCPConfig(savedConfig.mcpConfig); + } + } + }, []); + + const loadMCPConfigFromElectron = async () => { + if (!isElectron()) return; + + try { + const response = await window.desktopApi!.mcp.getConfig(); + if (response.success && response.config) { + setMCPConfig(response.config); + } + } catch (error) { + console.error('Error loading MCP config from Electron:', error); + // Fallback to plugin store + const savedConfig = pluginStore.get(); + if (savedConfig?.mcpConfig) { + setMCPConfig(savedConfig.mcpConfig); + } + } + }; + + const handleConfigChange = async (newConfig: MCPConfig) => { + setMCPConfig(newConfig); + + if (isElectron()) { + // Save to Electron settings and restart MCP client + try { + const response = await window.desktopApi!.mcp.updateConfig(newConfig); + if (!response.success) { + console.error('Error updating MCP config in Electron:', response.error); + // Still save to plugin store as fallback + const currentConfig = pluginStore.get() || {}; + pluginStore.update({ + ...currentConfig, + mcpConfig: newConfig, + }); + } + } catch (error) { + console.error('Error updating MCP config:', error); + // Fallback to plugin store + const currentConfig = pluginStore.get() || {}; + pluginStore.update({ + ...currentConfig, + mcpConfig: newConfig, + }); + } + } else { + // Save to plugin store for non-Electron environments + const currentConfig = pluginStore.get() || {}; + pluginStore.update({ + ...currentConfig, + mcpConfig: newConfig, + }); + } + + // Also notify parent if callback provided + if (onConfigChange) { + onConfigChange(newConfig); + } + }; + + const handleToggleEnabled = async () => { + const newConfig = { ...mcpConfig, enabled: !mcpConfig.enabled }; + + // If enabling MCP for the first time and no servers exist, add default Inspektor Gadget server + if (newConfig.enabled && mcpConfig.servers.length === 0) { + const defaultServer: MCPServer = { + name: 'inspektor-gadget', + command: 'docker', + args: [ + 'run', '-i', '--rm', + '--mount', 'type=bind,src=%USERPROFILE%\\.kube\\config,dst=/kubeconfig', + 'ghcr.io/inspektor-gadget/ig-mcp-server:latest', + '-gadget-discoverer=artifacthub' + ], + enabled: true, + }; + + newConfig.servers = [defaultServer]; + } + + await handleConfigChange(newConfig); + }; + + const handleAddServer = () => { + if (!newServerName.trim() || !newServerCommand.trim()) { + return; + } + + const argsArray = newServerArgs.trim() + ? newServerArgs.split(' ').map(arg => arg.trim()).filter(arg => arg.length > 0) + : []; + + const newServer: MCPServer = { + name: newServerName.trim(), + command: newServerCommand.trim(), + args: argsArray, + enabled: true, + }; + + const newConfig = { + ...mcpConfig, + servers: [...mcpConfig.servers, newServer], + }; + + handleConfigChange(newConfig); + + // Clear form + setNewServerName(''); + setNewServerCommand('npx'); + setNewServerArgs(''); + }; + + const handleRemoveServer = (index: number) => { + const newConfig = { + ...mcpConfig, + servers: mcpConfig.servers.filter((_, i) => i !== index), + }; + handleConfigChange(newConfig); + }; + + const handleToggleServer = (index: number) => { + const newServers = [...mcpConfig.servers]; + newServers[index] = { ...newServers[index], enabled: !newServers[index].enabled }; + + const newConfig = { ...mcpConfig, servers: newServers }; + handleConfigChange(newConfig); + }; + + const handleUpdateServer = (index: number, field: keyof MCPServer, value: string | string[]) => { + const newServers = [...mcpConfig.servers]; + if (field === 'args' && typeof value === 'string') { + newServers[index] = { + ...newServers[index], + args: value.split(' ').map(arg => arg.trim()).filter(arg => arg.length > 0) + }; + } else { + newServers[index] = { ...newServers[index], [field]: value }; + } + + const newConfig = { ...mcpConfig, servers: newServers }; + handleConfigChange(newConfig); + }; + + // Only show MCP settings in Electron + if (!isElectron()) { + return ( + + + MCP server configuration is only available in the desktop app. + + + ); + } + + return ( + + + + Model Context Protocol (MCP) allows AI assistants to connect to external tools and data sources. + Configure MCP servers here to extend the AI assistant's capabilities. + + + + } + label="Enable MCP Servers" + /> + + + {mcpConfig.enabled && ( + <> + {/* Existing Servers */} + {mcpConfig.servers.length > 0 && ( + + + Configured Servers + + {mcpConfig.servers.map((server, index) => ( + + + + handleToggleServer(index)} + size="small" + /> + } + label={server.name || `Server ${index + 1}`} + /> + handleRemoveServer(index)} + color="error" + size="small" + > + + + + + handleUpdateServer(index, 'name', e.target.value)} + fullWidth + size="small" + sx={{ mb: 1 }} + /> + + handleUpdateServer(index, 'command', e.target.value)} + fullWidth + size="small" + sx={{ mb: 1 }} + /> + + handleUpdateServer(index, 'args', e.target.value)} + fullWidth + size="small" + helperText="Space-separated arguments" + /> + + + ))} + + )} + + {/* Add New Server Form */} + + + + Add New MCP Server + + + + setNewServerName(e.target.value)} + placeholder="e.g., filesystem" + size="small" + /> + + setNewServerCommand(e.target.value)} + placeholder="e.g., npx" + size="small" + /> + + setNewServerArgs(e.target.value)} + placeholder="e.g., -y @danielsuguimoto/readonly-server-filesystem C:\\Users\\username\\Desktop" + helperText="Space-separated arguments. Use Windows paths like C:\\Users\\username\\Desktop" + size="small" + multiline + rows={2} + /> + + + + + + + {/* Example Configuration */} + + + Example Configuration for Windows: + + + {JSON.stringify({ + "Server Name": "filesystem", + "Command": "npx", + "Arguments": "-y @danielsuguimoto/readonly-server-filesystem C:\\Users\\username\\Desktop C:\\Users\\username\\Documents" + }, null, 2)} + + + Make sure to use actual Windows paths that exist on your system. + + + + {/* Troubleshooting Section */} + + + 📋 Troubleshooting Tips: + + + • Use the "Test MCP Connection" button to check if servers are running + + + • Check Electron DevTools Console (F12) for error messages + + + • Ensure NPM packages can be installed: run `npm install -g @danielsuguimoto/readonly-server-filesystem` first + + + • Use existing Windows paths like `C:\Users\[username]\Desktop` + + + + )} + + ); +} From e5f427b53b1599f64af548bb7acc9c10eb4613cb Mon Sep 17 00:00:00 2001 From: Ashu Ghildiyal Date: Mon, 8 Sep 2025 22:58:50 +0530 Subject: [PATCH 03/39] ai-plugin: Use mcp settings Signed-off-by: Ashu Ghildiyal Signed-off-by: ashu8912 --- ai-assistant/src/components/settings/index.ts | 1 + ai-assistant/src/index.tsx | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/ai-assistant/src/components/settings/index.ts b/ai-assistant/src/components/settings/index.ts index ba38a4cf84..e25059f67a 100644 --- a/ai-assistant/src/components/settings/index.ts +++ b/ai-assistant/src/components/settings/index.ts @@ -1,2 +1,3 @@ export { default as ModelSelector } from './ModelSelector'; export { default as TermsDialog } from './TermsDialog'; +export { MCPSettings } from './MCPSettings'; diff --git a/ai-assistant/src/index.tsx b/ai-assistant/src/index.tsx index c1d653042c..1ee10e2d08 100644 --- a/ai-assistant/src/index.tsx +++ b/ai-assistant/src/index.tsx @@ -22,6 +22,7 @@ import { import React from 'react'; import { useHistory } from 'react-router-dom'; import { ModelSelector } from './components'; +import { MCPSettings } from './components/settings/MCPSettings'; import { getDefaultConfig } from './config/modelConfig'; import { isTestModeCheck } from './helper'; import AIPrompt from './modal'; @@ -460,6 +461,10 @@ function Settings() { ))} + + {/* MCP Servers Section */} + + ); } From ee6abf83bdc46ea46dbe9b0110ab8f34f3a04e8a Mon Sep 17 00:00:00 2001 From: Ashu Ghildiyal Date: Mon, 8 Sep 2025 23:00:13 +0530 Subject: [PATCH 04/39] ai-plugin: Add ability to approve a tool request Signed-off-by: Ashu Ghildiyal Signed-off-by: ashu8912 --- .../components/common/ToolApprovalDialog.tsx | 307 ++++++++++++++++++ ai-assistant/src/components/common/index.ts | 1 + ai-assistant/src/hooks/useToolApproval.ts | 69 ++++ .../src/langchain/LangChainManager.ts | 91 +++++- .../src/langchain/tools/ToolManager.ts | 174 +++++++++- ai-assistant/src/modal.tsx | 24 ++ ai-assistant/src/utils/ToolApprovalManager.ts | 181 +++++++++++ 7 files changed, 832 insertions(+), 15 deletions(-) create mode 100644 ai-assistant/src/components/common/ToolApprovalDialog.tsx create mode 100644 ai-assistant/src/hooks/useToolApproval.ts create mode 100644 ai-assistant/src/utils/ToolApprovalManager.ts diff --git a/ai-assistant/src/components/common/ToolApprovalDialog.tsx b/ai-assistant/src/components/common/ToolApprovalDialog.tsx new file mode 100644 index 0000000000..aa98647fec --- /dev/null +++ b/ai-assistant/src/components/common/ToolApprovalDialog.tsx @@ -0,0 +1,307 @@ +import React, { useState } from 'react'; +import { + Dialog, + DialogTitle, + DialogContent, + DialogActions, + Button, + Typography, + Box, + Chip, + Alert, + Accordion, + AccordionSummary, + AccordionDetails, + List, + ListItem, + ListItemText, + IconButton, + Tooltip, + FormGroup, + FormControlLabel, + Checkbox, +} from '@mui/material'; +import { Icon } from '@iconify/react'; + +interface ToolCall { + id: string; + name: string; + description?: string; + arguments: Record; + type: 'mcp' | 'regular'; +} + +interface ToolApprovalDialogProps { + open: boolean; + toolCalls: ToolCall[]; + onApprove: (approvedToolIds: string[]) => void; + onDeny: () => void; + onClose: () => void; + loading?: boolean; +} + +const ToolApprovalDialog: React.FC = ({ + open, + toolCalls, + onApprove, + onDeny, + onClose, + loading = false, +}) => { + const [selectedToolIds, setSelectedToolIds] = useState( + toolCalls.map(tool => tool.id) + ); + const [rememberChoice, setRememberChoice] = useState(false); + + // Reset selection when toolCalls change + React.useEffect(() => { + if (open) { + setSelectedToolIds(toolCalls.map(tool => tool.id)); + setRememberChoice(false); + } + }, [toolCalls, open]); + + const handleSelectAll = () => { + setSelectedToolIds(toolCalls.map(tool => tool.id)); + }; + + const handleDeselectAll = () => { + setSelectedToolIds([]); + }; + + const handleToolToggle = (toolId: string) => { + setSelectedToolIds(prev => + prev.includes(toolId) + ? prev.filter(id => id !== toolId) + : [...prev, toolId] + ); + }; + + const handleApprove = () => { + onApprove(selectedToolIds); + }; + + const mcpTools = toolCalls.filter(tool => tool.type === 'mcp'); + const regularTools = toolCalls.filter(tool => tool.type === 'regular'); + + const getToolIcon = (toolName: string, toolType: 'mcp' | 'regular') => { + if (toolType === 'mcp') { + return 'mdi:docker'; // Inspektor Gadget runs in Docker + } + + // Regular Kubernetes tools + if (toolName.includes('kubernetes') || toolName.includes('k8s')) { + return 'mdi:kubernetes'; + } + return 'mdi:tool'; + }; + + const formatArguments = (args: Record) => { + return Object.entries(args) + .filter(([_, value]) => value !== undefined && value !== null && value !== '') + .map(([key, value]) => ( + + + + )); + }; + + const renderToolSection = (tools: ToolCall[], title: string, color: 'primary' | 'secondary') => { + if (tools.length === 0) return null; + + return ( + + + {title} + + + {tools.map(tool => ( + + } + sx={{ + '& .MuiAccordionSummary-content': { + alignItems: 'center' + } + }} + > + handleToolToggle(tool.id)} + onClick={(e) => e.stopPropagation()} + /> + } + label="" + sx={{ mr: 1 }} + onClick={(e) => e.stopPropagation()} + /> + + + + {tool.name} + + {tool.description && ( + + {tool.description} + + )} + + {tool.type === 'mcp' && ( + + )} + + + + Arguments to be passed: + + + {formatArguments(tool.arguments)} + + + + ))} + + ); + }; + + return ( + + + + + Tool Execution Approval Required + + {!loading && ( + + + + )} + + + + + The AI Assistant wants to execute {toolCalls.length} tool{toolCalls.length > 1 ? 's' : ''} + to complete your request. Please review and approve the tools you want to allow. + + + {mcpTools.length > 0 && ( + + + MCP Tools (Inspektor Gadget) + + + These tools will execute debugging commands in your Kubernetes clusters through + Inspektor Gadget containers. They provide deep system-level insights but require + elevated permissions. + + + )} + + + + + + + {renderToolSection(regularTools, 'Kubernetes Tools', 'primary')} + {renderToolSection(mcpTools, 'MCP Tools (Inspektor Gadget)', 'secondary')} + + + setRememberChoice(e.target.checked)} + /> + } + label={ + + Remember my choice for this session (auto-approve similar tools) + + } + /> + + + + + + + {selectedToolIds.length} of {toolCalls.length} tools selected + + + + + + + + + ); +}; + +export default ToolApprovalDialog; diff --git a/ai-assistant/src/components/common/index.ts b/ai-assistant/src/components/common/index.ts index 166c50f080..33f23fbc1b 100644 --- a/ai-assistant/src/components/common/index.ts +++ b/ai-assistant/src/components/common/index.ts @@ -1,4 +1,5 @@ export { default as ApiConfirmationDialog } from './ApiConfirmationDialog'; export { default as LogsButton } from './LogsButton'; export { default as LogsDialog } from './LogsDialog'; +export { default as ToolApprovalDialog } from './ToolApprovalDialog'; export { default as YamlDisplay } from './YamlDisplay'; diff --git a/ai-assistant/src/hooks/useToolApproval.ts b/ai-assistant/src/hooks/useToolApproval.ts new file mode 100644 index 0000000000..ef8c64ec5a --- /dev/null +++ b/ai-assistant/src/hooks/useToolApproval.ts @@ -0,0 +1,69 @@ +import { useCallback, useEffect, useState } from 'react'; +import { toolApprovalManager, ToolApprovalRequest } from '../utils/ToolApprovalManager'; + +export interface UseToolApprovalResult { + showApprovalDialog: boolean; + pendingRequest: ToolApprovalRequest | null; + handleApprove: (approvedToolIds: string[], rememberChoice?: boolean) => void; + handleDeny: () => void; + handleClose: () => void; + isProcessing: boolean; +} + +export const useToolApproval = (): UseToolApprovalResult => { + const [showApprovalDialog, setShowApprovalDialog] = useState(false); + const [pendingRequest, setPendingRequest] = useState(null); + const [isProcessing, setIsProcessing] = useState(false); + + // Listen for approval requests from the manager + useEffect(() => { + const handleApprovalRequest = (request: ToolApprovalRequest) => { + setPendingRequest(request); + setShowApprovalDialog(true); + setIsProcessing(false); + }; + + toolApprovalManager.on('approval-requested', handleApprovalRequest); + + return () => { + toolApprovalManager.off('approval-requested', handleApprovalRequest); + }; + }, []); + + const handleApprove = useCallback((approvedToolIds: string[], rememberChoice = false) => { + if (!pendingRequest) return; + + setIsProcessing(true); + toolApprovalManager.approveTools(pendingRequest.requestId, approvedToolIds, rememberChoice); + + // Close dialog after a brief delay to show processing state + setTimeout(() => { + setShowApprovalDialog(false); + setPendingRequest(null); + setIsProcessing(false); + }, 500); + }, [pendingRequest]); + + const handleDeny = useCallback(() => { + if (!pendingRequest) return; + + toolApprovalManager.denyTools(pendingRequest.requestId); + setShowApprovalDialog(false); + setPendingRequest(null); + setIsProcessing(false); + }, [pendingRequest]); + + const handleClose = useCallback(() => { + // Close is essentially a denial - user dismissed the dialog + handleDeny(); + }, [handleDeny]); + + return { + showApprovalDialog, + pendingRequest, + handleApprove, + handleDeny, + handleClose, + isProcessing, + }; +}; diff --git a/ai-assistant/src/langchain/LangChainManager.ts b/ai-assistant/src/langchain/LangChainManager.ts index fcc71223ce..91c36d8a88 100644 --- a/ai-assistant/src/langchain/LangChainManager.ts +++ b/ai-assistant/src/langchain/LangChainManager.ts @@ -20,6 +20,7 @@ import AIManager, { Prompt } from '../ai/manager'; import { basePrompt } from '../ai/prompts'; import { apiErrorPromptTemplate, toolFailurePromptTemplate } from './PromptTemplates'; import { KubernetesToolContext, ToolManager } from './tools'; +import { toolApprovalManager, ToolCall } from '../utils/ToolApprovalManager'; export default class LangChainManager extends AIManager { private model: BaseChatModel; @@ -217,6 +218,32 @@ export default class LangChainManager extends AIManager { return ['openai', 'azure', 'anthropic', 'mistral', 'gemini'].includes(this.providerId); } + /** + * Get description for a tool (for approval dialog) + */ + private getToolDescription(toolName: string, isMCPTool: boolean): string { + if (isMCPTool) { + // MCP tool descriptions can be more specific based on tool name + if (toolName.includes('trace') || toolName.includes('profile')) { + return 'Traces system calls and processes for debugging'; + } else if (toolName.includes('network') || toolName.includes('socket')) { + return 'Monitors network connections and traffic'; + } else if (toolName.includes('top') || toolName.includes('process')) { + return 'Shows running processes and resource usage'; + } else if (toolName.includes('exec') || toolName.includes('run')) { + return 'Executes commands in containers'; + } else { + return `Inspektor Gadget debugging tool: ${toolName}`; + } + } else { + // Regular Kubernetes tools + if (toolName.includes('kubernetes')) { + return 'Executes Kubernetes API operations'; + } + return `Kubernetes management tool: ${toolName}`; + } + } + // Helper method to prepare chat history for prompt template private prepareChatHistory(): BaseMessage[] { // Filter out system messages and display-only messages to avoid conflicts with the system message in the prompt template @@ -473,8 +500,68 @@ export default class LangChainManager extends AIManager { }; this.history.push(assistantPrompt); - // Process tool calls - await this.processToolCalls(toolCalls, assistantPrompt); + // Prepare tool calls for approval + const toolCallsForApproval: ToolCall[] = toolCalls.map(tc => { + const toolName = tc.function.name; + const mcpTools = this.toolManager.getMCPTools(); + const isMCPTool = mcpTools.some(tool => tool.name === toolName); + + return { + id: tc.id, + name: toolName, + description: this.getToolDescription(toolName, isMCPTool), + arguments: JSON.parse(tc.function.arguments), + type: isMCPTool ? 'mcp' : 'regular' + }; + }); + + try { + // Request approval for tool execution + console.log('🔐 Requesting approval for', toolCallsForApproval.length, 'tools'); + const approvedToolIds = await toolApprovalManager.requestApproval(toolCallsForApproval); + + console.log('✅ Tools approved:', approvedToolIds.length, 'of', toolCallsForApproval.length); + + // Filter tool calls to only execute approved ones + const approvedToolCalls = toolCalls.filter(tc => approvedToolIds.includes(tc.id)); + const deniedToolCalls = toolCalls.filter(tc => !approvedToolIds.includes(tc.id)); + + // Add denied tool responses to history + for (const deniedTool of deniedToolCalls) { + this.history.push({ + role: 'tool', + content: JSON.stringify({ + error: true, + message: 'Tool execution denied by user', + userFriendlyMessage: `The execution of ${deniedTool.function.name} was denied by the user.` + }), + toolCallId: deniedTool.id, + name: deniedTool.function.name, + }); + } + + // Process approved tool calls + if (approvedToolCalls.length > 0) { + await this.processToolCalls(approvedToolCalls, assistantPrompt); + } + + } catch (error) { + console.log('❌ Tool approval denied or failed:', error.message); + + // Add denial responses for all tools + for (const toolCall of toolCalls) { + this.history.push({ + role: 'tool', + content: JSON.stringify({ + error: true, + message: error.message || 'Tool execution denied', + userFriendlyMessage: `Tool execution was denied: ${error.message || 'User chose not to proceed'}` + }), + toolCallId: toolCall.id, + name: toolCall.function.name, + }); + } + } // Check if we should process follow-up const toolResponses = this.history.filter( diff --git a/ai-assistant/src/langchain/tools/ToolManager.ts b/ai-assistant/src/langchain/tools/ToolManager.ts index 1e13a6f0a2..90bbe7019d 100644 --- a/ai-assistant/src/langchain/tools/ToolManager.ts +++ b/ai-assistant/src/langchain/tools/ToolManager.ts @@ -58,8 +58,22 @@ export class ToolManager { schema: toolData.inputSchema, func: async (args: any) => { try { - const result = await this.mcpClient.executeTool(toolData.name, args); - return typeof result === 'string' ? result : JSON.stringify(result); + // Handle argument mapping for MCP tools + // LangChain may wrap args in different formats, need to handle properly + let mappedArgs = this.mapMCPToolArguments(args, toolData.inputSchema); + + console.log(`MCP tool ${toolData.name} called with original args:`, JSON.stringify(args)); + console.log(`MCP tool ${toolData.name} calling with mapped args:`, JSON.stringify(mappedArgs)); + + const result = await this.mcpClient.executeTool(toolData.name, mappedArgs); + console.log(`MCP tool ${toolData.name} returned result:`, result); + console.log(`MCP tool ${toolData.name} result type:`, typeof result); + + // Ensure we return a string response + const response = typeof result === 'string' ? result : JSON.stringify(result); + console.log(`MCP tool ${toolData.name} final response:`, response); + + return response; } catch (error) { console.error(`Error executing MCP tool ${toolData.name}:`, error); throw error; @@ -77,6 +91,107 @@ export class ToolManager { // Continue without MCP tools - this is not a fatal error } } + + /** + * Map LangChain tool arguments to MCP tool expected format + * Handles common argument wrapping patterns and schema mismatches + */ + private mapMCPToolArguments(args: any, inputSchema?: any): any { + console.log('Mapping MCP tool arguments:', { args, inputSchema }); + + if (!inputSchema) { + // If no schema, return args as-is + return args; + } + + const schemaProps = inputSchema?.properties; + + // Handle tools that expect no parameters + if (!schemaProps || Object.keys(schemaProps).length === 0) { + // Return empty object for tools that expect no parameters + console.log('Tool expects no parameters, returning empty object'); + return {}; + } + + // Handle empty/null/undefined args + if (!args || args === '' || args === '""' || (typeof args === 'object' && Object.keys(args).length === 0)) { + // Check if the tool has required parameters + const requiredProps = inputSchema?.required || []; + if (requiredProps.length === 0) { + // Tool has no required parameters, safe to return empty object + console.log('Tool has no required parameters, returning empty object'); + return {}; + } else { + // Tool has required parameters but got empty args + console.log('Tool has required parameters but got empty args, returning empty object (will let validation handle it)'); + return {}; + } + } + + // If args is wrapped in an "input" key but the schema doesn't expect it + if (args && typeof args === 'object' && 'input' in args && !schemaProps.input) { + const inputValue = args.input; + + // Handle empty input + if (!inputValue || inputValue === '' || inputValue === '""') { + const requiredProps = inputSchema?.required || []; + if (requiredProps.length === 0) { + console.log('Unwrapped input is empty and no required params, returning empty object'); + return {}; + } + } + + // If the input is a primitive value and the schema has only one property + const schemaPropertyNames = Object.keys(schemaProps); + if (schemaPropertyNames.length === 1 && (typeof inputValue === 'string' || typeof inputValue === 'number' || typeof inputValue === 'boolean')) { + // Map the primitive value to the single expected property + console.log(`Mapping primitive value to single property: ${schemaPropertyNames[0]}`); + return { [schemaPropertyNames[0]]: inputValue }; + } + + // If the input is an object, try to unwrap it + if (typeof inputValue === 'object' && inputValue !== null) { + console.log('Unwrapping object input'); + return inputValue; + } + + // For primitive values with multiple schema properties, try common mappings + if (typeof inputValue === 'string') { + // Try common parameter names + if (schemaProps.path) { + console.log('Mapping string to path parameter'); + return { path: inputValue }; + } + if (schemaProps.directory) { + console.log('Mapping string to directory parameter'); + return { directory: inputValue }; + } + if (schemaProps.file) { + console.log('Mapping string to file parameter'); + return { file: inputValue }; + } + if (schemaProps.name) { + console.log('Mapping string to name parameter'); + return { name: inputValue }; + } + + // If no common mapping found, map to first property + if (schemaPropertyNames.length > 0) { + console.log(`Mapping string to first property: ${schemaPropertyNames[0]}`); + return { [schemaPropertyNames[0]]: inputValue }; + } + } + + // Return the unwrapped input and let the tool handle validation + console.log('Returning unwrapped input value'); + return inputValue; + } + + // If args structure matches or is already properly formatted, return as-is + console.log('Args appear to be in correct format, returning as-is'); + return args; + } + /** * Configure external dependencies for tools that need them */ @@ -128,6 +243,19 @@ export class ToolManager { return this.mcpTools; } + /** + * Check if a specific tool is configured (including MCP tools) + */ + hasTool(toolName: string): boolean { + // Check regular tools first + if (this.toolHandlers.has(toolName)) { + return true; + } + + // Check MCP tools + return this.mcpTools.some(tool => tool.name === toolName); + } + /** * Execute a tool by name with the given arguments */ @@ -148,12 +276,39 @@ export class ToolManager { metadata: { error: 'tool_disabled', toolName }, }; } - const tool = this.toolHandlers.get(toolName); - if (!tool) { - throw new Error(`Tool ${toolName} not found`); + + // Check if it's a regular tool first + const regularTool = this.toolHandlers.get(toolName); + if (regularTool) { + return await regularTool.handler(args, toolCallId, pendingPrompt); } - return await tool.handler(args, toolCallId, pendingPrompt); + // Check if it's an MCP tool + const mcpTool = this.mcpTools.find(tool => tool.name === toolName); + if (mcpTool) { + try { + const result = await mcpTool.func(args); + return { + content: result, + shouldAddToHistory: true, + shouldProcessFollowUp: false, + metadata: { toolName, source: 'mcp' }, + }; + } catch (error) { + console.error(`Error executing MCP tool ${toolName}:`, error); + return { + content: JSON.stringify({ + error: true, + message: `Error executing MCP tool: ${error instanceof Error ? error.message : 'Unknown error'}`, + }), + shouldAddToHistory: true, + shouldProcessFollowUp: false, + metadata: { error: 'mcp_execution_error', toolName }, + }; + } + } + + throw new Error(`Tool ${toolName} not found`); } /** @@ -183,11 +338,4 @@ export class ToolManager { getToolNames(): string[] { return this.tools.map(tool => tool.config.name); } - - /** - * Check if a specific tool is configured - */ - hasTool(toolName: string): boolean { - return this.toolHandlers.has(toolName); - } } diff --git a/ai-assistant/src/modal.tsx b/ai-assistant/src/modal.tsx index 1882f20819..85a5eff154 100644 --- a/ai-assistant/src/modal.tsx +++ b/ai-assistant/src/modal.tsx @@ -12,12 +12,15 @@ import { AIInputSection, ApiConfirmationDialog, PromptSuggestions, + ToolApprovalDialog, } from './components'; import { getProviderById } from './config/modelConfig'; import EditorDialog from './editordialog'; import { isTestModeCheck } from './helper'; import { useClusterWarnings } from './hooks/useClusterWarnings'; import { useKubernetesToolUI } from './hooks/useKubernetesToolUI'; +import { useToolApproval } from './hooks/useToolApproval'; +import { toolApprovalManager } from './utils/ToolApprovalManager'; import LangChainManager from './langchain/LangChainManager'; import { getSettingsURL, useGlobalState } from './utils'; import { generateContextDescription } from './utils/contextGenerator'; @@ -77,6 +80,9 @@ export default function AIPrompt(props: { // Use the custom hook to get warnings for clusters const clusterWarnings = useClusterWarnings(clusterNames); + // Tool approval management + const toolApproval = useToolApproval(); + const [activeConfig, setActiveConfig] = useState(null); const [availableConfigs, setAvailableConfigs] = useState([]); @@ -779,6 +785,8 @@ export default function AIPrompt(props: { aiManager?.reset(); updateHistory(); } + // Clear tool approval session when history is cleared + toolApprovalManager.clearSession(); }} onConfigChange={(config, model) => { setActiveConfig(config); @@ -791,6 +799,22 @@ export default function AIPrompt(props: { + {/* Tool Approval Dialog */} + ({ + id: tool.id, + name: tool.name, + description: tool.description, + arguments: tool.arguments, + type: tool.type + })) || []} + onApprove={toolApproval.handleApprove} + onDeny={toolApproval.handleDeny} + onClose={toolApproval.handleClose} + loading={toolApproval.isProcessing} + /> + {/* Editor Dialog */} {!isDelete && showEditor && ( ; + type: 'mcp' | 'regular'; +} + +export interface ToolApprovalRequest { + requestId: string; + toolCalls: ToolCall[]; + resolve: (approvedToolIds: string[]) => void; + reject: (error: Error) => void; +} + +export class ToolApprovalManager extends EventEmitter { + private static instance: ToolApprovalManager | null = null; + private pendingRequest: ToolApprovalRequest | null = null; + private autoApproveSettings: Map = new Map(); + private sessionAutoApproval: boolean = false; + + private constructor() { + super(); + } + + public static getInstance(): ToolApprovalManager { + if (!ToolApprovalManager.instance) { + ToolApprovalManager.instance = new ToolApprovalManager(); + } + return ToolApprovalManager.instance; + } + + /** + * Request approval for tool execution + */ + public async requestApproval(toolCalls: ToolCall[]): Promise { + // Check if session auto-approval is enabled + if (this.sessionAutoApproval) { + console.log('Auto-approving tools due to session setting'); + return toolCalls.map(tool => tool.id); + } + + // Check for individual tool auto-approvals + const autoApprovedTools: string[] = []; + const needsApprovalTools: ToolCall[] = []; + + for (const tool of toolCalls) { + if (this.autoApproveSettings.get(tool.name)) { + autoApprovedTools.push(tool.id); + } else { + needsApprovalTools.push(tool); + } + } + + // If all tools are auto-approved, return them + if (needsApprovalTools.length === 0) { + console.log('All tools auto-approved:', autoApprovedTools); + return autoApprovedTools; + } + + // If there's already a pending request, reject the previous one + if (this.pendingRequest) { + this.pendingRequest.reject(new Error('Request superseded by new tool approval request')); + } + + return new Promise((resolve, reject) => { + const requestId = `tool-approval-${Date.now()}-${Math.random()}`; + + this.pendingRequest = { + requestId, + toolCalls: needsApprovalTools, + resolve: (approvedToolIds: string[]) => { + // Combine auto-approved and manually approved tools + const allApprovedIds = [...autoApprovedTools, ...approvedToolIds]; + this.pendingRequest = null; + resolve(allApprovedIds); + }, + reject: (error: Error) => { + this.pendingRequest = null; + reject(error); + } + }; + + // Emit event for UI components to listen to + this.emit('approval-requested', this.pendingRequest); + }); + } + + /** + * Approve tools from the UI + */ + public approveTools(requestId: string, approvedToolIds: string[], rememberChoice = false): void { + if (!this.pendingRequest || this.pendingRequest.requestId !== requestId) { + console.warn('No matching pending request for approval:', requestId); + return; + } + + // Handle remember choice + if (rememberChoice) { + // If all tools were approved, enable session auto-approval + const allToolIds = this.pendingRequest.toolCalls.map(tool => tool.id); + if (approvedToolIds.length === allToolIds.length) { + this.sessionAutoApproval = true; + console.log('Session auto-approval enabled'); + } else { + // Remember individual tool approvals + for (const toolCall of this.pendingRequest.toolCalls) { + if (approvedToolIds.includes(toolCall.id)) { + this.autoApproveSettings.set(toolCall.name, true); + } + } + console.log('Individual tool approvals saved'); + } + } + + this.pendingRequest.resolve(approvedToolIds); + } + + /** + * Deny all tools from the UI + */ + public denyTools(requestId: string): void { + if (!this.pendingRequest || this.pendingRequest.requestId !== requestId) { + console.warn('No matching pending request for denial:', requestId); + return; + } + + this.pendingRequest.reject(new Error('User denied tool execution')); + } + + /** + * Get current pending request + */ + public getPendingRequest(): ToolApprovalRequest | null { + return this.pendingRequest; + } + + /** + * Clear session settings (called when user explicitly clears or starts new session) + */ + public clearSession(): void { + this.sessionAutoApproval = false; + this.autoApproveSettings.clear(); + console.log('Tool approval session settings cleared'); + } + + /** + * Set session auto-approval + */ + public setSessionAutoApproval(enabled: boolean): void { + this.sessionAutoApproval = enabled; + } + + /** + * Check if session auto-approval is enabled + */ + public isSessionAutoApprovalEnabled(): boolean { + return this.sessionAutoApproval; + } + + /** + * Get auto-approval settings for debugging + */ + public getAutoApprovalSettings(): { + sessionAutoApproval: boolean; + toolSettings: Array<{ toolName: string; autoApprove: boolean }>; + } { + return { + sessionAutoApproval: this.sessionAutoApproval, + toolSettings: Array.from(this.autoApproveSettings.entries()).map(([toolName, autoApprove]) => ({ + toolName, + autoApprove, + })), + }; + } +} + +// Export singleton instance +export const toolApprovalManager = ToolApprovalManager.getInstance(); From 6861926e35ba5e5ec192ef49ed20aa72d1a27c80 Mon Sep 17 00:00:00 2001 From: Ashu Ghildiyal Date: Wed, 10 Sep 2025 09:49:57 +0530 Subject: [PATCH 05/39] ai-plugin: Fix tool response and update MCPSettings Signed-off-by: Ashu Ghildiyal Signed-off-by: ashu8912 --- .../src/components/settings/MCPSettings.tsx | 404 ++++++++---------- .../src/langchain/LangChainManager.ts | 95 +++- 2 files changed, 268 insertions(+), 231 deletions(-) diff --git a/ai-assistant/src/components/settings/MCPSettings.tsx b/ai-assistant/src/components/settings/MCPSettings.tsx index 0706893796..e375c84b56 100644 --- a/ai-assistant/src/components/settings/MCPSettings.tsx +++ b/ai-assistant/src/components/settings/MCPSettings.tsx @@ -2,14 +2,11 @@ import { SectionBox } from '@kinvolk/headlamp-plugin/lib/components/common'; import { Box, Button, - Card, - CardContent, - Divider, FormControlLabel, - IconButton, Switch, TextField, Typography, + Alert, } from '@mui/material'; import { Icon } from '@iconify/react'; import React, { useEffect, useState } from 'react'; @@ -54,9 +51,8 @@ export function MCPSettings({ config, onConfigChange }: MCPSettingsProps) { } ); - const [newServerName, setNewServerName] = useState(''); - const [newServerCommand, setNewServerCommand] = useState('npx'); - const [newServerArgs, setNewServerArgs] = useState(''); + const [jsonConfig, setJsonConfig] = useState(''); + const [jsonError, setJsonError] = useState(''); useEffect(() => { // Load MCP config from Electron if available @@ -71,6 +67,11 @@ export function MCPSettings({ config, onConfigChange }: MCPSettingsProps) { } }, []); + // Update JSON config when mcpConfig changes + useEffect(() => { + setJsonConfig(JSON.stringify(mcpConfig, null, 2)); + }, [mcpConfig]); + const loadMCPConfigFromElectron = async () => { if (!isElectron()) return; @@ -132,84 +133,121 @@ export function MCPSettings({ config, onConfigChange }: MCPSettingsProps) { const handleToggleEnabled = async () => { const newConfig = { ...mcpConfig, enabled: !mcpConfig.enabled }; - // If enabling MCP for the first time and no servers exist, add default Inspektor Gadget server + // If enabling MCP for the first time and no servers exist, add default servers if (newConfig.enabled && mcpConfig.servers.length === 0) { - const defaultServer: MCPServer = { - name: 'inspektor-gadget', - command: 'docker', - args: [ - 'run', '-i', '--rm', - '--mount', 'type=bind,src=%USERPROFILE%\\.kube\\config,dst=/kubeconfig', - 'ghcr.io/inspektor-gadget/ig-mcp-server:latest', - '-gadget-discoverer=artifacthub' - ], - enabled: true, - }; + const defaultServers: MCPServer[] = [ + { + name: 'inspektor-gadget', + command: 'docker', + args: [ + 'run', '-i', '--rm', + '--mount', 'type=bind,src=%USERPROFILE%\\.kube\\config,dst=/root/.kube/config,readonly', + '--mount', 'type=bind,src=%USERPROFILE%\\.minikube,dst=/root/.minikube,readonly', + 'ghcr.io/inspektor-gadget/ig-mcp-server:latest', + '-gadget-discoverer=artifacthub' + ], + enabled: false, // Disabled by default to avoid errors + }, + { + name: 'filesystem', + command: 'npx', + args: [ + '-y', '@danielsuguimoto/readonly-server-filesystem', + 'C:\\Users\\' + (process.env.USERNAME || 'username') + '\\Desktop' + ], + enabled: true, + } + ]; - newConfig.servers = [defaultServer]; + newConfig.servers = defaultServers; } await handleConfigChange(newConfig); }; - const handleAddServer = () => { - if (!newServerName.trim() || !newServerCommand.trim()) { - return; - } - - const argsArray = newServerArgs.trim() - ? newServerArgs.split(' ').map(arg => arg.trim()).filter(arg => arg.length > 0) - : []; - const newServer: MCPServer = { - name: newServerName.trim(), - command: newServerCommand.trim(), - args: argsArray, - enabled: true, - }; - const newConfig = { - ...mcpConfig, - servers: [...mcpConfig.servers, newServer], - }; - - handleConfigChange(newConfig); + const handleJsonConfigChange = (value: string) => { + setJsonConfig(value); + setJsonError(''); + }; - // Clear form - setNewServerName(''); - setNewServerCommand('npx'); - setNewServerArgs(''); + const validateAndApplyJsonConfig = () => { + try { + const parsedConfig = JSON.parse(jsonConfig) as MCPConfig; + + // Validate the structure + if (typeof parsedConfig.enabled !== 'boolean') { + throw new Error('enabled field must be a boolean'); + } + + if (!Array.isArray(parsedConfig.servers)) { + throw new Error('servers field must be an array'); + } + + // Validate each server + parsedConfig.servers.forEach((server, index) => { + if (typeof server.name !== 'string') { + throw new Error(`Server ${index}: name must be a string`); + } + if (typeof server.command !== 'string') { + throw new Error(`Server ${index}: command must be a string`); + } + if (!Array.isArray(server.args)) { + throw new Error(`Server ${index}: args must be an array`); + } + if (typeof server.enabled !== 'boolean') { + throw new Error(`Server ${index}: enabled must be a boolean`); + } + }); + + // Apply the config + handleConfigChange(parsedConfig); + setJsonError(''); + } catch (error) { + setJsonError(error instanceof Error ? error.message : 'Invalid JSON configuration'); + } }; - const handleRemoveServer = (index: number) => { - const newConfig = { - ...mcpConfig, - servers: mcpConfig.servers.filter((_, i) => i !== index), - }; - handleConfigChange(newConfig); + const resetJsonToCurrentConfig = () => { + setJsonConfig(JSON.stringify(mcpConfig, null, 2)); + setJsonError(''); }; - const handleToggleServer = (index: number) => { - const newServers = [...mcpConfig.servers]; - newServers[index] = { ...newServers[index], enabled: !newServers[index].enabled }; - - const newConfig = { ...mcpConfig, servers: newServers }; - handleConfigChange(newConfig); + const getExampleConfig = (): MCPConfig => { + return { + enabled: true, + servers: [ + { + name: "inspektor-gadget", + command: "docker", + args: [ + "run", "-i", "--rm", + "--mount", "type=bind,src=%USERPROFILE%\\.kube\\config,dst=/root/.kube/config,readonly", + "--mount", "type=bind,src=%USERPROFILE%\\.minikube,dst=/root/.minikube,readonly", + "ghcr.io/inspektor-gadget/ig-mcp-server:latest", + "-gadget-discoverer=artifacthub" + ], + enabled: false + }, + { + name: "filesystem", + command: "npx", + args: [ + "-y", "@danielsuguimoto/readonly-server-filesystem", + "C:\\Users\\username\\Desktop", + "C:\\Users\\username\\Documents" + ], + enabled: true + } + ] + }; }; - const handleUpdateServer = (index: number, field: keyof MCPServer, value: string | string[]) => { - const newServers = [...mcpConfig.servers]; - if (field === 'args' && typeof value === 'string') { - newServers[index] = { - ...newServers[index], - args: value.split(' ').map(arg => arg.trim()).filter(arg => arg.length > 0) - }; - } else { - newServers[index] = { ...newServers[index], [field]: value }; - } - - const newConfig = { ...mcpConfig, servers: newServers }; - handleConfigChange(newConfig); + const loadExampleConfig = () => { + const exampleConfig = getExampleConfig(); + setJsonConfig(JSON.stringify(exampleConfig, null, 2)); + setJsonError(''); }; // Only show MCP settings in Electron @@ -244,168 +282,86 @@ export function MCPSettings({ config, onConfigChange }: MCPSettingsProps) { {mcpConfig.enabled && ( <> - {/* Existing Servers */} - {mcpConfig.servers.length > 0 && ( - - - Configured Servers - - {mcpConfig.servers.map((server, index) => ( - - - - handleToggleServer(index)} - size="small" - /> - } - label={server.name || `Server ${index + 1}`} - /> - handleRemoveServer(index)} - color="error" - size="small" - > - - - - - handleUpdateServer(index, 'name', e.target.value)} - fullWidth - size="small" - sx={{ mb: 1 }} - /> - - handleUpdateServer(index, 'command', e.target.value)} - fullWidth - size="small" - sx={{ mb: 1 }} - /> - - handleUpdateServer(index, 'args', e.target.value)} - fullWidth - size="small" - helperText="Space-separated arguments" - /> - - - ))} - - )} - - {/* Add New Server Form */} - - - - Add New MCP Server - - - - setNewServerName(e.target.value)} - placeholder="e.g., filesystem" - size="small" - /> + {/* JSON Configuration */} + + + JSON Configuration Editor + + + Edit your MCP servers configuration as JSON. This allows you to easily add, remove, + and modify multiple servers at once. + - setNewServerCommand(e.target.value)} - placeholder="e.g., npx" - size="small" - /> + {jsonError && ( + + {jsonError} + + )} setNewServerArgs(e.target.value)} - placeholder="e.g., -y @danielsuguimoto/readonly-server-filesystem C:\\Users\\username\\Desktop" - helperText="Space-separated arguments. Use Windows paths like C:\\Users\\username\\Desktop" - size="small" + label="MCP Configuration JSON" + value={jsonConfig} + onChange={(e) => handleJsonConfigChange(e.target.value)} multiline - rows={2} - /> - - - - - + }} + helperText="Edit the JSON configuration above. Make sure to keep the proper structure." + /> + + + + + + - {/* Example Configuration */} - - - Example Configuration for Windows: - - - {JSON.stringify({ - "Server Name": "filesystem", - "Command": "npx", - "Arguments": "-y @danielsuguimoto/readonly-server-filesystem C:\\Users\\username\\Desktop C:\\Users\\username\\Documents" - }, null, 2)} - - - Make sure to use actual Windows paths that exist on your system. - - + {/* Schema Documentation */} + + + 📝 Configuration Schema: + + + {JSON.stringify({ + "enabled": "boolean - Enable/disable MCP servers", + "servers": [ + { + "name": "string - Unique server name", + "command": "string - Executable command", + "args": ["array of strings - Command arguments"], + "enabled": "boolean - Enable/disable this server" + } + ] + }, null, 2)} + + - {/* Troubleshooting Section */} - - - 📋 Troubleshooting Tips: - - - • Use the "Test MCP Connection" button to check if servers are running - - - • Check Electron DevTools Console (F12) for error messages - - - • Ensure NPM packages can be installed: run `npm install -g @danielsuguimoto/readonly-server-filesystem` first - - - • Use existing Windows paths like `C:\Users\[username]\Desktop` - - - - )} + + )} ); } diff --git a/ai-assistant/src/langchain/LangChainManager.ts b/ai-assistant/src/langchain/LangChainManager.ts index 91c36d8a88..ba139d19bc 100644 --- a/ai-assistant/src/langchain/LangChainManager.ts +++ b/ai-assistant/src/langchain/LangChainManager.ts @@ -262,6 +262,26 @@ export default class LangChainManager extends AIManager { return systemPromptContent; } + // Helper method to create system prompt specifically for tool response processing + private createToolResponseSystemPrompt(): string { + const baseSystemPrompt = this.createSystemPrompt(); + + // Add specific instructions for tool response processing + const toolResponseInstructions = ` + +IMPORTANT: You have just received tool execution results. Your task is to: + +1. ANALYZE the tool results and provide a clear, helpful response to the user +2. SUMMARIZE the information in a user-friendly way +3. DO NOT call additional tools unless the user explicitly requests more actions +4. FOCUS on explaining what the tools found or accomplished +5. If the tool results show data (like file listings, directories, etc.), present them in a clear, formatted way + +The user is waiting for you to explain what the tools discovered. Provide a direct, informative response based on the tool results.`; + + return baseSystemPrompt + toolResponseInstructions; + } + private convertPromptsToMessages(prompts: Prompt[]): BaseMessage[] { return prompts.map(prompt => { switch (prompt.role) { @@ -845,9 +865,12 @@ Format your response to make the errors prominent and actionable.`, public async processToolResponses(): Promise { // Check if there are any tool responses in the history if (!this.hasToolResponses()) { + console.log('🔍 No tool responses found in history'); return this.getLastAssistantMessage(); } + console.log('🔍 Processing tool responses from history'); + // Validate tool call/response alignment this.validateToolCallAlignment(); @@ -861,7 +884,7 @@ Format your response to make the errors prominent and actionable.`, // Process the response const response = await chain.invoke({ messages: messages.slice(1), // Exclude system message for the chain - systemPrompt: this.createSystemPrompt(), + systemPrompt: this.createToolResponseSystemPrompt(), // Use specialized prompt for tool responses }); return this.handleToolResponseResult(response); @@ -872,7 +895,8 @@ Format your response to make the errors prominent and actionable.`, // Helper method to check if there are tool responses private hasToolResponses(): boolean { - return this.history.some(prompt => prompt.role === 'tool' && prompt.toolCallId); + const toolResponses = this.history.filter(prompt => prompt.role === 'tool' && prompt.toolCallId); + return toolResponses.length > 0; } // Helper method to get the last assistant message @@ -1023,8 +1047,10 @@ Format your response to make the errors prominent and actionable.`, }); } - // Check response size - const responseSize = prompt.content.length; + let content = prompt.content; + + // Check response size after optimization handling + const responseSize = content.length; if (currentSize + responseSize > maxSize) { console.warn(`Tool response size exceeds limit (${currentSize + responseSize}/${maxSize})`); return ( @@ -1034,7 +1060,7 @@ Format your response to make the errors prominent and actionable.`, } // Sanitize content - return this.sanitizeContent(prompt.content); + return this.sanitizeContent(content); } // Find the last assistant message with tool calls @@ -1058,9 +1084,65 @@ Format your response to make the errors prominent and actionable.`, // Analyze and potentially correct kubectl suggestions const correctedResponse = await this.analyzeAndCorrectResponse(response); + const extractedContent = this.extractTextContent(correctedResponse.content); + + // If the model returned empty content but has tool calls, it's trying to call more tools + // Instead of allowing this, we should provide a fallback response based on the tool results + if ((!extractedContent || extractedContent.trim().length === 0) && response.tool_calls?.length > 0) { + + // Get the most recent tool responses from history + const recentToolResponses = this.history + .filter(prompt => prompt.role === 'tool' && prompt.toolCallId) + .slice(-3) // Get last 3 tool responses + .map(response => ({ + name: response.name, + content: response.content + })); + + // Create a fallback response based on tool results + let fallbackContent = 'I executed the requested tools and here are the results:\n\n'; + + recentToolResponses.forEach((toolResponse, index) => { + const toolName = toolResponse.name || 'tool'; + let content = toolResponse.content; + + // Try to parse and clean up the content + try { + const parsed = JSON.parse(content); + if (parsed.error) { + content = `Error: ${parsed.message || 'Tool execution failed'}`; + } else if (parsed.userFriendlyMessage) { + content = parsed.userFriendlyMessage; + } else if (typeof parsed === 'object') { + content = JSON.stringify(parsed, null, 2); + } + } catch (e) { + // Content is not JSON, use as-is but clean it up + content = content.toString().trim(); + } + + fallbackContent += `**${toolName}:**\n${content}\n\n`; + }); + + const assistantPrompt: Prompt = { + role: 'assistant', + content: fallbackContent.trim(), + toolCalls: [], // Don't include additional tool calls + }; + + // Clean up history to prevent message order issues + const lastAssistantWithToolsIndex = this.findLastAssistantWithTools(); + if (lastAssistantWithToolsIndex >= 0) { + this.history = this.history.slice(0, lastAssistantWithToolsIndex + 1); + } + + this.history.push(assistantPrompt); + return assistantPrompt; + } + const assistantPrompt: Prompt = { role: 'assistant', - content: this.extractTextContent(correctedResponse.content), + content: extractedContent, toolCalls: correctedResponse.tool_calls?.map(tc => ({ id: tc.id, @@ -1072,7 +1154,6 @@ Format your response to make the errors prominent and actionable.`, })) || [], }; - console.log('Assistant prompt created from response'); // Clean up history to prevent message order issues const lastAssistantWithToolsIndex = this.findLastAssistantWithTools(); From a8e0d2314ffcb04a5264eed79a3ced3fb50d55cb Mon Sep 17 00:00:00 2001 From: ashu8912 Date: Sun, 21 Sep 2025 16:26:14 +0530 Subject: [PATCH 06/39] feat: add MCP output formatting and display components - Add MCPOutputFormatter for AI-powered formatting of MCP tool outputs - Add MCPOutputDisplay component for rendering formatted MCP results - Add MCPFormattedMessage component for displaying MCP messages in chat - Support multiple output types: table, metrics, list, graph, text, error - Include insights, warnings, and actionable items in formatted output - Add comprehensive argument processing and validation Signed-off-by: ashu8912 --- .../components/chat/MCPFormattedMessage.tsx | 255 +++++ .../mcpOutput/MCPArgumentProcessor.ts | 470 +++++++++ .../components/mcpOutput/MCPOutputDisplay.tsx | 915 ++++++++++++++++++ .../src/components/mcpOutput/index.ts | 2 + .../formatters/MCPOutputFormatter.ts | 413 ++++++++ 5 files changed, 2055 insertions(+) create mode 100644 ai-assistant/src/components/chat/MCPFormattedMessage.tsx create mode 100644 ai-assistant/src/components/mcpOutput/MCPArgumentProcessor.ts create mode 100644 ai-assistant/src/components/mcpOutput/MCPOutputDisplay.tsx create mode 100644 ai-assistant/src/components/mcpOutput/index.ts create mode 100644 ai-assistant/src/langchain/formatters/MCPOutputFormatter.ts diff --git a/ai-assistant/src/components/chat/MCPFormattedMessage.tsx b/ai-assistant/src/components/chat/MCPFormattedMessage.tsx new file mode 100644 index 0000000000..d65e5626e2 --- /dev/null +++ b/ai-assistant/src/components/chat/MCPFormattedMessage.tsx @@ -0,0 +1,255 @@ +import { Icon } from '@iconify/react'; +import { Alert, Box, Paper, Typography } from '@mui/material'; +import React, { useCallback } from 'react'; +import { usePromptWidth } from '../../contexts/PromptWidthContext'; +import { FormattedMCPOutput } from '../../langchain/formatters/MCPOutputFormatter'; +import MCPOutputDisplay from '../mcpOutput/MCPOutputDisplay'; + +interface MCPFormattedMessageProps { + content: string; + isAssistant?: boolean; + onRetryTool?: (toolName: string, args: Record) => void; +} + +interface ParsedMCPContent { + formatted: boolean; + mcpOutput: FormattedMCPOutput; + raw: string; + isError?: boolean; +} + +const MCPFormattedMessage: React.FC = ({ + content, + isAssistant = true, + onRetryTool, +}) => { + const widthContext = usePromptWidth(); + console.log("From context width ", widthContext.promptWidth) + // Try to parse the content as formatted MCP output + const parseContent = (): ParsedMCPContent | null => { + try { + const parsed = JSON.parse(content); + if (parsed.formatted && parsed.mcpOutput) { + return parsed as ParsedMCPContent; + } + } catch (error) { + // Not formatted MCP content + } + return null; + }; + + const mcpContent = parseContent(); + + // If not formatted MCP content, return null (let other components handle it) + if (!mcpContent) { + return null; + } + + const handleExport = (format: 'json' | 'csv' | 'txt') => { + const { mcpOutput } = mcpContent; + let exportData: string; + let filename: string; + let mimeType: string; + + switch (format) { + case 'json': + exportData = JSON.stringify(mcpOutput.data, null, 2); + filename = `${mcpOutput.metadata?.toolName || 'mcp-output'}.json`; + mimeType = 'application/json'; + break; + case 'csv': + if (mcpOutput.type === 'table' && mcpOutput.data.headers && mcpOutput.data.rows) { + const csvContent = [ + mcpOutput.data.headers.join(','), + ...mcpOutput.data.rows.map((row: any[]) => + row.map(cell => `"${String(cell).replace(/"/g, '""')}"`).join(',') + ) + ].join('\n'); + exportData = csvContent; + } else { + exportData = JSON.stringify(mcpOutput.data, null, 2); + } + filename = `${mcpOutput.metadata?.toolName || 'mcp-output'}.csv`; + mimeType = 'text/csv'; + break; + case 'txt': + exportData = mcpContent.raw; + filename = `${mcpOutput.metadata?.toolName || 'mcp-output'}.txt`; + mimeType = 'text/plain'; + break; + default: + return; + } + + // Create and download the file + const blob = new Blob([exportData], { type: mimeType }); + const url = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = filename; + link.click(); + URL.revokeObjectURL(url); + }; + + const handleRetry = useCallback(() => { + if (!onRetryTool) { + console.log('Retry requested but no retry handler available'); + return; + } + + try { + // Parse the content to extract originalArgs and tool information + const parsedContent = JSON.parse(content); + + // Look for originalArgs in the parsed content + const originalArgs = parsedContent.originalArgs; + + if (!originalArgs) { + console.error('No originalArgs found in content for retry'); + return; + } + + // Extract tool name from the formatted output or use a fallback + let toolName = ''; + if (parsedContent.mcpOutput?.metadata?.toolName) { + toolName = parsedContent.mcpOutput.metadata.toolName; + } else if (parsedContent.toolName) { + toolName = parsedContent.toolName; + } else { + console.error('No tool name found in content for retry'); + return; + } + + console.log('Retrying tool:', toolName, 'with args:', originalArgs); + onRetryTool(toolName, originalArgs); + } catch (error) { + console.error('Failed to parse content for retry:', error); + } + }, [content, onRetryTool]); + + return ( + + {isAssistant && ( + + + + {mcpContent.isError || mcpContent.mcpOutput.type === 'error' + ? 'Tool Error - AI Analysis' + : 'AI-Formatted Tool Output' + } + + {(mcpContent.isError || mcpContent.mcpOutput.type === 'error') && ( + + Tool Execution Failed + + )} + + )} + + + + {/* Show processing info if available and not an error */} + {mcpContent.mcpOutput.metadata && !(mcpContent.isError || mcpContent.mcpOutput.type === 'error') && ( + + + + Processed by AI in {mcpContent.mcpOutput.metadata.processingTime}ms + {mcpContent.mcpOutput.insights && mcpContent.mcpOutput.insights.length > 0 && ( + <> • {mcpContent.mcpOutput.insights.length} insights generated + )} + {mcpContent.mcpOutput.actionable_items && mcpContent.mcpOutput.actionable_items.length > 0 && ( + <> • {mcpContent.mcpOutput.actionable_items.length} action items + )} + + + )} + + {/* Show error-specific info */} + {(mcpContent.isError || mcpContent.mcpOutput.type === 'error') && mcpContent.mcpOutput.metadata && ( + + + + Error analyzed by AI in {mcpContent.mcpOutput.metadata.processingTime}ms + • Tool: {mcpContent.mcpOutput.metadata.toolName} + + + )} + + ); +}; + +// Helper component to detect and render MCP content in existing messages +export const withMCPFormatting =

( + Component: React.ComponentType

+) => { + return (props: P & { content: string }) => { + const mcpFormatted = ; + + // If content is formatted MCP output, show formatted version + try { + const parsed = JSON.parse(props.content); + if (parsed.formatted && parsed.mcpOutput) { + return mcpFormatted; + } + } catch { + // Not JSON or not formatted MCP content, use original component + } + + return ; + }; +}; + +export default MCPFormattedMessage; \ No newline at end of file diff --git a/ai-assistant/src/components/mcpOutput/MCPArgumentProcessor.ts b/ai-assistant/src/components/mcpOutput/MCPArgumentProcessor.ts new file mode 100644 index 0000000000..06b8a1df1a --- /dev/null +++ b/ai-assistant/src/components/mcpOutput/MCPArgumentProcessor.ts @@ -0,0 +1,470 @@ +import tools from '../../ai/mcp/electron-client'; + +export interface MCPToolSchema { + name: string; + description?: string; + inputSchema?: { + type: string; + properties?: Record; + required?: string[]; + }; +} + +export interface UserContext { + userMessage?: string; + conversationHistory?: Array<{ role: string; content: string }>; + kubernetesContext?: { + selectedClusters?: string[]; + namespace?: string; + currentResource?: any; + }; + lastToolResults?: Record; + timeContext?: Date; +} + +export interface ProcessedArguments { + original: Record; + processed: Record; + schema: MCPToolSchema | null; + suggestions: Record; + errors: string[]; + intelligentFills: Record; +} + +export class MCPArgumentProcessor { + private static toolSchemas: Map = new Map(); + private static schemasLoaded = false; + + /** + * Load MCP tool schemas + */ + static async loadSchemas(): Promise { + if (this.schemasLoaded) return; + + try { + const mcpTools = await tools(); + if (mcpTools && Array.isArray(mcpTools)) { + mcpTools.forEach(tool => { + this.toolSchemas.set(tool.name, { + name: tool.name, + description: tool.description, + inputSchema: tool.inputSchema + }); + }); + this.schemasLoaded = true; + console.log('MCPArgumentProcessor: Loaded schemas for', this.toolSchemas.size, 'tools'); + } + } catch (error) { + console.error('Failed to load MCP tool schemas:', error); + } + } + + /** + * Validate and process arguments for MCP tools (simplified version) + * Main argument generation is now handled by AI in LangChainManager + */ + static async processArguments( + toolName: string, + aiProcessedArgs: Record = {}, + userContext?: UserContext + ): Promise { + await this.loadSchemas(); + + const schema = this.toolSchemas.get(toolName); + const errors: string[] = []; + const processed = { ...aiProcessedArgs }; + const intelligentFills: Record = {}; + + if (!schema) { + errors.push(`No schema found for tool: ${toolName}`); + return { + original: aiProcessedArgs, + processed, + schema: null, + suggestions: {}, + errors, + intelligentFills + }; + } + + // Ensure required fields have appropriate values (even if empty objects/arrays) + if (schema.inputSchema?.required) { + for (const requiredField of schema.inputSchema.required) { + if (!(requiredField in processed) || processed[requiredField] === undefined) { + const fieldSchema = schema.inputSchema.properties?.[requiredField]; + if (fieldSchema) { + // Provide appropriate empty value based on type + processed[requiredField] = this.getEmptyValueForRequiredField(fieldSchema); + intelligentFills[requiredField] = { + value: processed[requiredField], + reason: `Required field provided with empty ${fieldSchema.type}`, + confidence: 0.8 + }; + } + } + } + } + + // Generate suggestions for UI display + const suggestions = this.generateIntelligentSuggestions(schema, userContext); + + // Validate processed arguments + const validationErrors = this.validateArgumentsWithEmptyObjectSupport(processed, schema); + errors.push(...validationErrors); + + return { + original: aiProcessedArgs, + processed, + schema, + suggestions, + errors, + intelligentFills + }; + } + + /** + * Get appropriate empty value for required field + */ + private static getEmptyValueForRequiredField(fieldSchema: any): any { + const type = fieldSchema.type; + + switch (type) { + case 'object': + return {}; + case 'array': + return []; + case 'string': + return fieldSchema.default || ''; + case 'number': + case 'integer': + return fieldSchema.default || fieldSchema.minimum || 0; + case 'boolean': + return fieldSchema.default !== undefined ? fieldSchema.default : false; + default: + return null; + } + } + + /** + * Generate intelligent suggestions based on tool schema and context + */ + private static generateIntelligentSuggestions( + schema: MCPToolSchema, + userContext?: UserContext + ): Record { + const suggestions: Record = {}; + + if (!schema.inputSchema?.properties) return suggestions; + + const properties = schema.inputSchema.properties; + + // Generate suggestions based on property types and names + for (const [key, propertySchema] of Object.entries(properties)) { + const suggestion = this.generatePropertySuggestion(key, propertySchema, userContext); + if (suggestion !== undefined) { + suggestions[key] = suggestion; + } + } + + return suggestions; + } + + /** + * Generate suggestion for a specific property + */ + private static generatePropertySuggestion( + propertyName: string, + propertySchema: any, + userContext?: UserContext + ): any { + const type = propertySchema.type; + const description = propertySchema.description?.toLowerCase() || ''; + + // Check context data for matching values + if (userContext) { + // Check kubernetes context + if (userContext.kubernetesContext) { + const k8sContext = userContext.kubernetesContext; + if (propertyName.toLowerCase().includes('namespace') && k8sContext.namespace) { + return k8sContext.namespace; + } + if (propertyName.toLowerCase().includes('cluster') && k8sContext.selectedClusters?.length) { + return k8sContext.selectedClusters[0]; + } + } + + // Check last tool results + if (userContext.lastToolResults) { + const lowerPropName = propertyName.toLowerCase(); + for (const [contextKey, contextValue] of Object.entries(userContext.lastToolResults)) { + if (contextKey.toLowerCase().includes(lowerPropName) || + lowerPropName.includes(contextKey.toLowerCase())) { + return contextValue; + } + } + } + } + + // Generate suggestions based on property name and type + switch (type) { + case 'string': + return this.suggestStringValue(propertyName, description, propertySchema); + case 'number': + case 'integer': + return this.suggestNumberValue(propertyName, description, propertySchema); + case 'boolean': + return this.suggestBooleanValue(propertyName); + case 'array': + return this.suggestArrayValue(); + case 'object': + return this.suggestObjectValue(); + default: + return undefined; + } + } + + /** + * Suggest string values based on property name and context + */ + private static suggestStringValue( + propertyName: string, + description: string, + schema: any + ): string | undefined { + const lowerName = propertyName.toLowerCase(); + const lowerDesc = description.toLowerCase(); + + // Check for enum values + if (schema.enum && Array.isArray(schema.enum)) { + return schema.enum[0]; // Default to first enum value + } + + // Path-related suggestions + if (lowerName.includes('path') || lowerName.includes('directory') || lowerName.includes('dir')) { + if (lowerDesc.includes('current') || lowerDesc.includes('working')) { + return '.'; + } + if (lowerDesc.includes('home')) { + return '~'; + } + return '/Users/ashughildiyal/Desktop'; // Safe default path + } + + // File-related suggestions + if (lowerName.includes('file') || lowerName.includes('filename')) { + return ''; + } + + // Name suggestions + if (lowerName.includes('name') && !lowerName.includes('filename')) { + return ''; + } + + // Command suggestions + if (lowerName.includes('command') || lowerName.includes('cmd')) { + return ''; + } + + // Query suggestions + if (lowerName.includes('query') || lowerName.includes('search')) { + return ''; + } + + return undefined; + } + + /** + * Suggest number values + */ + private static suggestNumberValue( + propertyName: string, + description: string, + schema: any + ): number | undefined { + const lowerName = propertyName.toLowerCase(); + + // Check for default in schema + if (schema.default !== undefined) { + return schema.default; + } + + // Check for minimum value + if (schema.minimum !== undefined) { + return schema.minimum; + } + + // Common number patterns + if (lowerName.includes('port')) { + return 8080; + } + if (lowerName.includes('timeout')) { + return 30; + } + if (lowerName.includes('limit') || lowerName.includes('max')) { + return 100; + } + if (lowerName.includes('count')) { + return 10; + } + + return undefined; + } + + /** + * Suggest boolean values + */ + private static suggestBooleanValue( + propertyName: string + ): boolean | undefined { + const lowerName = propertyName.toLowerCase(); + // Note: description analysis could be added here for more intelligent suggestions + + // Common boolean patterns + if (lowerName.includes('enable') || lowerName.includes('enabled')) { + return false; // Conservative default + } + if (lowerName.includes('disable') || lowerName.includes('disabled')) { + return false; + } + if (lowerName.includes('recursive') || lowerName.includes('recurse')) { + return false; + } + if (lowerName.includes('force')) { + return false; + } + if (lowerName.includes('verbose')) { + return false; + } + + return undefined; + } + + /** + * Suggest array values + */ + private static suggestArrayValue(): any[] | undefined { + // Return empty array for optional arrays + return []; + } + + /** + * Suggest object values + */ + private static suggestObjectValue(): Record | undefined { + // Return empty object for optional objects + return {}; + } + + + + + + + + + + + + + /** + * Clean up arguments by removing empty non-required fields + */ + static cleanupArguments( + args: Record, + schema: MCPToolSchema + ): Record { + if (!schema.inputSchema) return args; + + const cleaned: Record = {}; + const required = schema.inputSchema.required || []; + const properties = schema.inputSchema.properties || {}; + + for (const [key, value] of Object.entries(args)) { + const isRequired = required.includes(key); + const propertySchema = properties[key]; + const hasDefault = propertySchema?.default !== undefined; + + // Include if: + // 1. Required field + // 2. Has a non-empty value + // 3. Has a default value defined in schema + if (isRequired || this.hasActualValue(value) || hasDefault) { + cleaned[key] = value; + } + } + + return cleaned; + } + + /** + * Check if a value is meaningful (not empty/null/undefined) + */ + private static hasActualValue(value: any): boolean { + if (value === null || value === undefined || value === '') { + return false; + } + + if (Array.isArray(value)) { + return value.length > 0; + } + + if (typeof value === 'object') { + return Object.keys(value).length > 0; + } + + return true; + } + + /** + * Validate arguments against schema with support for empty objects/arrays + */ + private static validateArgumentsWithEmptyObjectSupport( + args: Record, + schema: MCPToolSchema + ): string[] { + const errors: string[] = []; + + if (!schema.inputSchema) return errors; + + const required = schema.inputSchema.required || []; + const properties = schema.inputSchema.properties || {}; + + // Check required fields (allow empty objects/arrays for required fields) + for (const requiredField of required) { + if (!(requiredField in args) || args[requiredField] === undefined || args[requiredField] === null) { + errors.push(`Required field '${requiredField}' is missing`); + } + } + + // Check type validation + for (const [key, value] of Object.entries(args)) { + if (properties[key] && value !== undefined && value !== null) { + const expectedType = properties[key].type; + const actualType = Array.isArray(value) ? 'array' : typeof value; + + if (expectedType && actualType !== expectedType) { + errors.push(`Field '${key}' should be ${expectedType}, got ${actualType}`); + } + } + } + + return errors; + } + + + /** + * Get tool schema + */ + static async getToolSchema(toolName: string): Promise { + await this.loadSchemas(); + return this.toolSchemas.get(toolName) || null; + } + + /** + * Get all available tool names + */ + static async getAvailableTools(): Promise { + await this.loadSchemas(); + return Array.from(this.toolSchemas.keys()); + } +} \ No newline at end of file diff --git a/ai-assistant/src/components/mcpOutput/MCPOutputDisplay.tsx b/ai-assistant/src/components/mcpOutput/MCPOutputDisplay.tsx new file mode 100644 index 0000000000..c7ab0437d4 --- /dev/null +++ b/ai-assistant/src/components/mcpOutput/MCPOutputDisplay.tsx @@ -0,0 +1,915 @@ +import { Icon } from '@iconify/react'; +import { + Alert, + Box, + Button, + Card, + CardContent, + CardHeader, + Chip, + Collapse, + Divider, + Grid, + IconButton, + Paper, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Tooltip, + Typography, + useTheme, +} from '@mui/material'; +import React, { useEffect, useState } from 'react'; +import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; +import { oneDark, oneLight } from 'react-syntax-highlighter/dist/esm/styles/prism'; +import { usePromptWidth } from '../../contexts/PromptWidthContext'; +import { FormattedMCPOutput } from '../../langchain/formatters/MCPOutputFormatter'; + +interface MCPOutputDisplayProps { + output: FormattedMCPOutput; + onRetry?: () => void; + onExport?: (format: 'json' | 'csv' | 'txt') => void; + compact?: boolean; +} + +function calculateWidth(width: string): string { + if (!width) return '800px'; // fallback + + // Handle viewport width (vw) units + if (width.includes('vw')) { + const vwValue = parseFloat(width.replace('vw', '')); + const pixelWidth = Math.floor(window.innerWidth * (vwValue / 100)); + const adjustedWidth = Math.max(300, pixelWidth - 30); // Subtract 40px, minimum 300px + return `${adjustedWidth}px`; + } + + // Handle pixel (px) units + if (width.includes('px')) { + const pixelValue = parseInt(width.replace('px', ''), 10); + const adjustedWidth = Math.max(300, pixelValue - 30); // Subtract 40px, minimum 300px + return `${adjustedWidth}px`; + } + + // Handle numeric values (assume pixels) + const numericValue = parseInt(width, 10); + if (!isNaN(numericValue)) { + const adjustedWidth = Math.max(300, numericValue - 30); // Subtract 40px, minimum 300px + return `${adjustedWidth}px`; + } + + // Fallback for any other format + return '780px'; // 800px - 40px +} +const MCPOutputDisplay: React.FC = ({ + output, + onRetry, + onExport, + compact = false, +}) => { + const theme = useTheme(); + const { promptWidth } = usePromptWidth(); + const [expanded, setExpanded] = useState(!compact); + const [showRawData, setShowRawData] = useState(false); + const [showExportMenu, setShowExportMenu] = useState(false); + const [width, setWidth] = useState(calculateWidth(promptWidth?.toString() || '800px')); // Default width if not provided + const isDarkMode = theme.palette.mode === 'dark'; + const syntaxTheme = isDarkMode ? oneDark : oneLight; + + useEffect(() => { + const calculatedWidth = calculateWidth(promptWidth?.toString() || '800px'); + setWidth(calculatedWidth); + }, [promptWidth]) + // Get status color based on type or warnings + const getStatusColor = () => { + if (output.type === 'error') return 'error'; + if (output.warnings && output.warnings.length > 0) return 'warning'; + return 'primary'; + }; + + // Get icon based on output type + const getTypeIcon = () => { + switch (output.type) { + case 'table': return 'mdi:table'; + case 'metrics': return 'mdi:chart-line'; + case 'list': return 'mdi:format-list-bulleted'; + case 'graph': return 'mdi:chart-bar'; + case 'text': return 'mdi:text'; + case 'error': return 'mdi:alert-circle'; + default: return 'mdi:file-document'; + } + }; + + const renderContent = () => { + switch (output.type) { + case 'table': + return ; + case 'metrics': + return ; + case 'list': + return ; + case 'graph': + return ; + case 'text': + return ; + case 'error': + return ; + default: + return ; + } + }; + + return ( + + + + } + title={ + + + {output.title} + + + {output.metadata && ( + + + + )} + + } + subheader={output.summary} + action={ + + {onExport && ( + + setShowExportMenu(!showExportMenu)} + sx={{mr:2}} + > + + + + )} + + setShowRawData(!showRawData)} + color={showRawData ? 'primary' : 'default'} + sx={{mr:2}} + > + + + + {compact && ( + setExpanded(!expanded)} + > + + + )} + + } + sx={{ pb: 1 }} + /> + + + *': { + width: '100%', + minWidth: 0, + overflowWrap: 'break-word', + wordWrap: 'break-word', + } + }}> + {/* Warnings */} + {output.warnings && output.warnings.length > 0 && ( + + + Warnings: + + {output.warnings.map((warning, index) => ( + + • {warning} + + ))} + + )} + + {/* Main Content */} + {renderContent()} + + {/* Insights */} + {output.insights && output.insights.length > 0 && ( + + + + Key Insights: + + {output.insights.map((insight, index) => ( + + • {insight} + + ))} + + )} + + {/* Actionable Items */} + {output.actionable_items && output.actionable_items.length > 0 && ( + + + + Recommended Actions: + + {output.actionable_items.map((item, index) => ( + + • {item} + + ))} + + )} + + {/* Raw Data Collapse */} + + + + Raw Data: + + + {JSON.stringify(output.data, null, 2)} + + + + {/* Metadata */} + {output.metadata && ( + + + + + + )} + + + + + ); +}; + +// Table Display Component +const TableDisplay: React.FC<{ data: any; width: string }> = ({ data, width }) => { + const [sortBy, setSortBy] = useState(data.sortBy || null); + const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('asc'); + + const handleSort = (column: string) => { + if (sortBy === column) { + setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc'); + } else { + setSortBy(column); + setSortOrder('asc'); + } + }; + + const sortedRows = React.useMemo(() => { + if (!sortBy || !data.rows) return data.rows; + + const columnIndex = data.headers.indexOf(sortBy); + if (columnIndex === -1) return data.rows; + + return [...data.rows].sort((a, b) => { + const aVal = a[columnIndex]; + const bVal = b[columnIndex]; + + const comparison = String(aVal).localeCompare(String(bVal), undefined, { numeric: true }); + return sortOrder === 'asc' ? comparison : -comparison; + }); + }, [data.rows, data.headers, sortBy, sortOrder]); + + return ( + + + + + {data.headers.map((header: string, index: number) => ( + handleSort(header)} + sx={{ cursor: 'pointer', fontWeight: 'bold' }} + > + + {header} + {sortBy === header && ( + + )} + + + ))} + + + + {sortedRows?.map((row: any[], rowIndex: number) => ( + + {row.map((cell: any, cellIndex: number) => ( + + {typeof cell === 'object' ? JSON.stringify(cell) : String(cell)} + + ))} + + ))} + +
+
+ ); +}; + +// Metrics Display Component +const MetricsDisplay: React.FC<{ data: any; width: string }> = ({ data, width }) => { + const getStatusColor = (status: string) => { + switch (status) { + case 'error': return 'error'; + case 'warning': return 'warning'; + case 'info': return 'info'; + default: return 'primary'; + } + }; + + return ( + + {/* Primary Metrics */} + {data.primary && ( + + {data.primary.map((metric: any, index: number) => ( + + + + {metric.value} + + + {metric.label} + + + + ))} + + )} + + {/* Secondary Metrics */} + {data.secondary && ( + + {data.secondary.map((metric: any, index: number) => ( + + + + {metric.value} + + + {metric.label} + + + + ))} + + )} + + {/* Trends */} + {data.trends && ( + + + Trends: + + + {data.trends.map((trend: any, index: number) => ( + + + + ))} + + + )} + + ); +}; + +// List Display Component +const ListDisplay: React.FC<{ data: any; width: string }> = ({ data, width }) => { + const getStatusColor = (status: string) => { + switch (status) { + case 'error': return 'error.main'; + case 'warning': return 'warning.main'; + case 'info': return 'info.main'; + default: return 'text.primary'; + } + }; + + return ( + + {data.items?.map((item: any, index: number) => ( + + + {item.text} + + {item.metadata && ( + + {item.metadata} + + )} + + ))} + + ); +}; + +// Graph Display Component (placeholder for now) +const GraphDisplay: React.FC<{ data: any; width: string }> = ({ data, width }) => { + return ( + + + + Graph Visualization + + + {data.description || 'Chart visualization would appear here'} + + + Chart Type: {data.chartType} • {data.datasets?.length || 0} datasets + + + ); +}; + +// Text Display Component +const TextDisplay: React.FC<{ data: any; theme: any; width: string }> = ({ data, theme, width }) => { + return ( + + {data.highlights && data.highlights.length > 0 && ( + + {data.highlights.map((highlight: string, index: number) => ( + + ))} + + )} + + + {data.content} + + + + ); +}; + +// Error Display Component +const ErrorDisplay: React.FC<{ data: any; onRetry?: () => void; width: string }> = ({ data, onRetry, width }) => { + + // Extract concise error message for common error types + const getDisplayMessage = (errorData: any) => { + const message = errorData.message || 'Tool Execution Error'; + + // Handle file not found errors specifically + if (message.includes('ENOENT') || message.includes('no such file')) { + return 'File Not Found Error'; + } + + // Handle schema mismatch errors + if (message.includes('schema mismatch')) { + return 'Tool Configuration Error'; + } + + return message; + }; + + return ( + + + + + + + {getDisplayMessage(data)} + + {data.details && ( + + + Error Details: + + + {data.details} + + + )} + + + + + {data.suggestions && data.suggestions.length > 0 && ( + + + + Troubleshooting Suggestions: + + + {data.suggestions.map((suggestion: string, index: number) => ( + + {suggestion} + + ))} + + + )} + + + {onRetry && ( + + )} + + + + ); +}; + +// Raw Display Component +const RawDisplay: React.FC<{ data: any; theme: any; width: string }> = ({ data, theme, width }) => { + return ( + + + {JSON.stringify(data, null, 2)} + + + ); +}; + +export default MCPOutputDisplay; \ No newline at end of file diff --git a/ai-assistant/src/components/mcpOutput/index.ts b/ai-assistant/src/components/mcpOutput/index.ts new file mode 100644 index 0000000000..b587f0be96 --- /dev/null +++ b/ai-assistant/src/components/mcpOutput/index.ts @@ -0,0 +1,2 @@ +export { default as MCPOutputDisplay } from './MCPOutputDisplay'; +export type { FormattedMCPOutput } from '../../langchain/formatters/MCPOutputFormatter'; \ No newline at end of file diff --git a/ai-assistant/src/langchain/formatters/MCPOutputFormatter.ts b/ai-assistant/src/langchain/formatters/MCPOutputFormatter.ts new file mode 100644 index 0000000000..34be62834b --- /dev/null +++ b/ai-assistant/src/langchain/formatters/MCPOutputFormatter.ts @@ -0,0 +1,413 @@ +import { BaseChatModel } from '@langchain/core/language_models/chat_models'; +import { HumanMessage, SystemMessage } from '@langchain/core/messages'; + +export interface FormattedMCPOutput { + type: 'table' | 'metrics' | 'list' | 'graph' | 'text' | 'error' | 'raw'; + title: string; + summary: string; + data: any; + insights?: string[]; + warnings?: string[]; + actionable_items?: string[]; + metadata?: { + toolName: string; + responseSize: number; + processingTime: number; + dataPoints?: number; + }; +} + +export interface MCPFormatterOptions { + maxTokens?: number; + includeInsights?: boolean; + includeActionableItems?: boolean; + formatStyle?: 'detailed' | 'compact' | 'minimal'; +} + +export class MCPOutputFormatter { + private model: BaseChatModel; + private readonly SYSTEM_PROMPT = `You are an expert data analyst specializing in Kubernetes debugging and system monitoring data. Your task is to analyze raw tool outputs and format them in a user-friendly way. + +CRITICAL INSTRUCTIONS: +1. ALWAYS respond with valid JSON in the exact schema provided +2. Analyze the raw data to identify patterns, anomalies, and key insights +3. Format data appropriately based on its type (tables for structured data, metrics for numbers, etc.) +4. Provide actionable insights and recommendations +5. Highlight any security issues, performance problems, or anomalies +6. Keep summaries concise but informative +7. If data contains sensitive information, sanitize it appropriately + +RESPONSE SCHEMA: +{ + "type": "table" | "metrics" | "list" | "graph" | "text" | "error" | "raw", + "title": "Clear, descriptive title", + "summary": "Brief summary of what the data shows", + "data": "Formatted data structure (varies by type)", + "insights": ["Key insights from the data"], + "warnings": ["Any security or performance warnings"], + "actionable_items": ["Specific actions the user should consider"], + "metadata": { + "toolName": "Name of the tool that generated this data", + "responseSize": "Size in bytes", + "processingTime": "Time taken to process", + "dataPoints": "Number of data points if applicable" + } +} + +DATA TYPE FORMATTING GUIDELINES: + +TABLE: For structured data like process lists, network connections, resource usage +{ + "type": "table", + "data": { + "headers": ["Column1", "Column2", ...], + "rows": [["value1", "value2", ...], ...], + "sortBy": "column_name", + "highlightRows": [row_indices_with_issues] + } +} + +ERROR: For tool failures, schema mismatches, or execution errors +{ + "type": "error", + "data": { + "message": "Clear, user-friendly error description", + "details": "Technical error details or original error message", + "suggestions": [ + "Check the tool configuration and parameters", + "Verify input data format matches expected schema", + "Review tool documentation for correct usage" + ] + } +} + +METRICS: For numerical data, statistics, and KPIs +{ + "type": "metrics", + "data": { + "primary": [{"label": "CPU Usage", "value": "85%", "status": "warning"}], + "secondary": [{"label": "Memory", "value": "4.2GB", "status": "normal"}], + "trends": [{"label": "Network I/O", "value": "↑ 15%", "status": "info"}] + } +} + +LIST: For simple lists, logs, or sequential data +{ + "type": "list", + "data": { + "items": [ + {"text": "Item description", "status": "normal|warning|error", "metadata": "additional info"} + ], + "grouped": false + } +} + +GRAPH: For time-series or relationship data +{ + "type": "graph", + "data": { + "chartType": "line|bar|pie|scatter", + "datasets": [{"label": "Dataset name", "data": [...]}], + "labels": [...], + "description": "What the graph represents" + } +} + +TEXT: For unstructured text, logs, or narrative explanations +{ + "type": "text", + "data": { + "content": "Formatted text content", + "language": "json|yaml|shell|text", + "highlights": ["Important phrases to highlight"] + } +} + +ERROR: For error responses or failed tool executions +{ + "type": "error", + "data": { + "message": "User-friendly error message", + "details": "Technical details if available", + "suggestions": ["Possible solutions or next steps"] + } +} + +Remember: Focus on making complex data accessible and actionable for Kubernetes operators and developers.`; + + constructor(model: BaseChatModel) { + this.model = model; + } + + /** + * Format MCP tool output using AI analysis + */ + async formatMCPOutput( + rawOutput: string, + toolName: string, + options: MCPFormatterOptions = {} + ): Promise { + const startTime = Date.now(); + + try { + // Prepare options with defaults + const opts = { + maxTokens: 4000, + includeInsights: true, + includeActionableItems: true, + formatStyle: 'detailed', + ...options + } as Required; + + // Prepare the analysis prompt + const analysisPrompt = this.buildAnalysisPrompt(rawOutput, toolName, opts); + + // Send to AI for analysis and formatting + const messages = [ + new SystemMessage(this.SYSTEM_PROMPT), + new HumanMessage(analysisPrompt) + ]; + + const response = await this.model.invoke(messages, { + max_tokens: opts.maxTokens + }); + + const processingTime = Date.now() - startTime; + + // Parse the AI response + const formattedOutput = this.parseAIResponse(response.content as string, { + toolName, + responseSize: rawOutput.length, + processingTime + }); + + return formattedOutput; + + } catch (error) { + console.error('Error formatting MCP output:', error); + + // Fallback to basic formatting + return this.createFallbackFormat(rawOutput, toolName, Date.now() - startTime); + } + } + + /** + * Build the analysis prompt for the AI + */ + private buildAnalysisPrompt( + rawOutput: string, + toolName: string, + options: Required + ): string { + const truncatedOutput = this.truncateIfNeeded(rawOutput, 10000); // Limit to ~10k chars + + // Detect if this is likely an error + const isError = this.detectError(rawOutput); + const errorHint = isError ? '\n\nIMPORTANT: This appears to be an error response. Use "error" type and provide helpful troubleshooting guidance.' : ''; + + return `Analyze and format this ${toolName} tool output: + +TOOL: ${toolName} +RAW OUTPUT: +${truncatedOutput} + +FORMAT STYLE: ${options.formatStyle} +INCLUDE INSIGHTS: ${options.includeInsights} +INCLUDE ACTIONABLE ITEMS: ${options.includeActionableItems} + +Please analyze this data and respond with properly formatted JSON following the schema. +Pay special attention to: +1. Identifying the most appropriate visualization type (or "error" if this is an error) +2. For errors: Provide clear, actionable troubleshooting steps +3. For data: Extract key metrics and patterns +4. Highlighting any security or performance issues +5. Providing actionable recommendations + +${errorHint} + +${truncatedOutput.length < rawOutput.length ? + `\n[Note: Output was truncated from ${rawOutput.length} to ${truncatedOutput.length} characters for analysis]` : + '' +}`; + } + + /** + * Detect if raw output indicates an error + */ + private detectError(rawOutput: string): boolean { + try { + const parsed = JSON.parse(rawOutput); + return parsed.success === false || + parsed.error === true || + (typeof parsed.error === 'string' && parsed.error.length > 0) || + rawOutput.toLowerCase().includes('schema mismatch'); + } catch { + const lower = rawOutput.toLowerCase(); + return lower.includes('error') || + lower.includes('failed') || + lower.includes('exception') || + lower.includes('schema mismatch'); + } + } + + /** + * Parse AI response and validate structure + */ + private parseAIResponse( + aiResponse: string, + metadata: { toolName: string; responseSize: number; processingTime: number } + ): FormattedMCPOutput { + try { + // Extract JSON from response (handle potential markdown wrapping) + const jsonMatch = aiResponse.match(/```json\n?([\s\S]*?)\n?```/) || + aiResponse.match(/\{[\s\S]*\}/); + + if (!jsonMatch) { + throw new Error('No JSON found in AI response'); + } + + const parsed = JSON.parse(jsonMatch[1] || jsonMatch[0]); + + // Validate required fields and add metadata + const formatted: FormattedMCPOutput = { + type: parsed.type || 'text', + title: parsed.title || `${metadata.toolName} Output`, + summary: parsed.summary || 'Analysis completed', + data: parsed.data || { content: aiResponse }, + insights: parsed.insights || [], + warnings: parsed.warnings || [], + actionable_items: parsed.actionable_items || [], + metadata: { + ...metadata, + dataPoints: this.estimateDataPoints(parsed.data) + } + }; + + return formatted; + + } catch (error) { + console.error('Error parsing AI response:', error); + throw error; + } + } + + /** + * Create fallback formatting when AI processing fails + */ + private createFallbackFormat( + rawOutput: string, + toolName: string, + processingTime: number + ): FormattedMCPOutput { + // Try to detect if it's JSON + let data: any; + let type: FormattedMCPOutput['type'] = 'text'; + + try { + const parsed = JSON.parse(rawOutput); + + if (Array.isArray(parsed)) { + type = 'list'; + data = { + items: parsed.slice(0, 100).map((item, index) => ({ + text: typeof item === 'string' ? item : JSON.stringify(item), + status: 'normal', + metadata: `Item ${index + 1}` + })) + }; + } else if (typeof parsed === 'object') { + type = 'text'; + data = { + content: JSON.stringify(parsed, null, 2), + language: 'json' + }; + } else { + type = 'text'; + data = { content: String(parsed) }; + } + } catch { + // Not JSON, treat as text + type = 'text'; + data = { + content: rawOutput.length > 5000 ? + rawOutput.substring(0, 5000) + '\n\n[Output truncated...]' : + rawOutput, + language: 'text' + }; + } + + return { + type, + title: `${toolName} Output`, + summary: `Raw output from ${toolName}. AI formatting was not available.`, + data, + insights: [], + warnings: ['AI formatting failed - showing raw output'], + actionable_items: ['Consider checking the AI service connection'], + metadata: { + toolName, + responseSize: rawOutput.length, + processingTime, + dataPoints: this.estimateDataPoints(data) + } + }; + } + + /** + * Truncate output if too large for AI processing + */ + private truncateIfNeeded(output: string, maxLength: number): string { + if (output.length <= maxLength) { + return output; + } + + // Try to truncate at a reasonable boundary + const truncated = output.substring(0, maxLength); + const lastNewline = truncated.lastIndexOf('\n'); + const lastBrace = truncated.lastIndexOf('}'); + const lastBracket = truncated.lastIndexOf(']'); + + // Use the best boundary we can find + const cutPoint = Math.max(lastNewline, lastBrace, lastBracket); + + return cutPoint > maxLength * 0.8 ? + output.substring(0, cutPoint) : + truncated; + } + + /** + * Estimate number of data points in the formatted data + */ + private estimateDataPoints(data: any): number { + if (!data) return 0; + + if (Array.isArray(data)) { + return data.length; + } + + if (data.rows && Array.isArray(data.rows)) { + return data.rows.length; + } + + if (data.items && Array.isArray(data.items)) { + return data.items.length; + } + + if (data.primary && Array.isArray(data.primary)) { + return data.primary.length + (data.secondary?.length || 0); + } + + if (typeof data === 'object') { + return Object.keys(data).length; + } + + return 1; + } + + /** + * Quick format for simple cases without AI processing + */ + formatSimple(rawOutput: string, toolName: string): FormattedMCPOutput { + return this.createFallbackFormat(rawOutput, toolName, 0); + } +} \ No newline at end of file From 1697659297240477b580bfc592142510310310e8 Mon Sep 17 00:00:00 2001 From: ashu8912 Date: Sun, 21 Sep 2025 16:26:36 +0530 Subject: [PATCH 07/39] ai-plugin: Add prompt width context for component state management - Add PromptWidthContext to eliminate prop drilling for width state - Add PromptWidthProvider wrapper in AIPanelComponent - Enable components to access prompt width through usePromptWidth hook - Improve state management across the component hierarchy Signed-off-by: ashu8912 --- .../src/contexts/PromptWidthContext.tsx | 34 +++++++++++++++++++ ai-assistant/src/index.tsx | 5 +++ 2 files changed, 39 insertions(+) create mode 100644 ai-assistant/src/contexts/PromptWidthContext.tsx diff --git a/ai-assistant/src/contexts/PromptWidthContext.tsx b/ai-assistant/src/contexts/PromptWidthContext.tsx new file mode 100644 index 0000000000..8f3ab64410 --- /dev/null +++ b/ai-assistant/src/contexts/PromptWidthContext.tsx @@ -0,0 +1,34 @@ +import React, { createContext, ReactNode,useContext, useState } from 'react'; + +interface PromptWidthContextType { + promptWidth: string; + setPromptWidth: (width: string) => void; +} + +const PromptWidthContext = createContext(undefined); + +interface PromptWidthProviderProps { + children: ReactNode; + initialWidth?: string; +} + +export const PromptWidthProvider: React.FC = ({ + children, + initialWidth = '400px' +}) => { + const [promptWidth, setPromptWidth] = useState(initialWidth); + + return ( + + {children} + + ); +}; + +export const usePromptWidth = (): PromptWidthContextType => { + const context = useContext(PromptWidthContext); + if (context === undefined) { + throw new Error('usePromptWidth must be used within a PromptWidthProvider'); + } + return context; +}; \ No newline at end of file diff --git a/ai-assistant/src/index.tsx b/ai-assistant/src/index.tsx index 1ee10e2d08..a49feb3600 100644 --- a/ai-assistant/src/index.tsx +++ b/ai-assistant/src/index.tsx @@ -24,6 +24,7 @@ import { useHistory } from 'react-router-dom'; import { ModelSelector } from './components'; import { MCPSettings } from './components/settings/MCPSettings'; import { getDefaultConfig } from './config/modelConfig'; +import { PromptWidthProvider } from './contexts/PromptWidthContext'; import { isTestModeCheck } from './helper'; import AIPrompt from './modal'; import { getSettingsURL, PLUGIN_NAME, pluginStore, useGlobalState, usePluginConfig } from './utils'; @@ -108,15 +109,19 @@ const AIPanelComponent = React.memo(() => { zIndex: 10, }} /> + + ); }); + AIPanelComponent.displayName = 'AIPanelComponent'; // Register UI Panel component that uses the shared state to show/hide From 1e2ecc30b1b32c14b46400f4fe0fde65808db3f5 Mon Sep 17 00:00:00 2001 From: ashu8912 Date: Sun, 21 Sep 2025 16:26:58 +0530 Subject: [PATCH 08/39] ai-plugin: Add inline tool approval system - Add InlineToolConfirmation component for embedded tool approval UI - Add InlineToolApprovalManager for managing tool confirmation state - Extend ToolApprovalDialog with inline approval capabilities - Add support for tool confirmation in Prompt interface - Enable seamless tool approval workflow within chat interface Signed-off-by: ashu8912 --- ai-assistant/src/ai/manager.ts | 16 + .../common/InlineToolConfirmation.tsx | 673 ++++++++++++++++++ .../components/common/ToolApprovalDialog.tsx | 35 +- ai-assistant/src/components/common/index.ts | 2 +- .../src/utils/InlineToolApprovalManager.ts | 286 ++++++++ 5 files changed, 993 insertions(+), 19 deletions(-) create mode 100644 ai-assistant/src/components/common/InlineToolConfirmation.tsx create mode 100644 ai-assistant/src/utils/InlineToolApprovalManager.ts diff --git a/ai-assistant/src/ai/manager.ts b/ai-assistant/src/ai/manager.ts index 55429a3dda..c09795c308 100644 --- a/ai-assistant/src/ai/manager.ts +++ b/ai-assistant/src/ai/manager.ts @@ -1,3 +1,11 @@ +export type ToolCall = { + id: string; + name: string; + description?: string; + arguments: Record; + type: 'mcp' | 'regular'; +}; + export type Prompt = { role: string; content: string; @@ -9,6 +17,14 @@ export type Prompt = { contentFilterError?: boolean; alreadyDisplayed?: boolean; isDisplayOnly?: boolean; // Mark messages that shouldn't be sent to LLM + requestId?: string; // For tracking tool confirmation messages + // Add support for inline tool confirmations + toolConfirmation?: { + tools: ToolCall[]; + onApprove: (approvedToolIds: string[]) => void; + onDeny: () => void; + loading?: boolean; + }; }; export default abstract class AIManager { diff --git a/ai-assistant/src/components/common/InlineToolConfirmation.tsx b/ai-assistant/src/components/common/InlineToolConfirmation.tsx new file mode 100644 index 0000000000..e9e8e68436 --- /dev/null +++ b/ai-assistant/src/components/common/InlineToolConfirmation.tsx @@ -0,0 +1,673 @@ +import { Icon } from '@iconify/react'; +import { + Alert, + Box, + Button, + Card, + CardContent, + Chip, + CircularProgress, + Collapse, + Divider, + FormControl, + FormControlLabel, + FormHelperText, + IconButton, + InputLabel, + List, + ListItem, + ListItemText, + MenuItem, + Select, + Switch, + TextField, + Tooltip, + Typography, + useTheme, +} from '@mui/material'; +import React, { useEffect,useState } from 'react'; +import { MCPArgumentProcessor, type ProcessedArguments, type UserContext } from '../mcpOutput/MCPArgumentProcessor'; + +interface ToolCall { + id: string; + name: string; + description?: string; + arguments: Record; + type: 'mcp' | 'regular'; +} + +interface InlineToolConfirmationProps { + toolCalls: ToolCall[]; + onApprove: (approvedToolIds: string[]) => void; + onDeny: () => void; + loading?: boolean; + compact?: boolean; + userContext?: UserContext; +} + +const InlineToolConfirmation: React.FC = ({ + toolCalls, + onApprove, + onDeny, + loading = false, + compact = false, + userContext, +}) => { + const theme = useTheme(); + const [selectedToolIds] = useState( + toolCalls.map(tool => tool.id) + ); + const [showDetails, setShowDetails] = useState(!compact); + + // State to track processed arguments for each tool + const [processedArguments, setProcessedArguments] = useState>({}); + const [editedArguments, setEditedArguments] = useState>>({}); + const [argumentsInitialized, setArgumentsInitialized] = useState(false); + + // State to track deny action + const [isDenying, setIsDenying] = useState(false); + const [isApproving, setIsApproving] = useState(false); + + // Initialize arguments with intelligent processing + useEffect(() => { + const initializeArguments = async () => { + const processed: Record = {}; + const edited: Record> = {}; + + for (const tool of toolCalls) { + if (tool.type === 'mcp') { + // Process MCP tool arguments with intelligent defaults + const processedArgs = await MCPArgumentProcessor.processArguments( + tool.name, + tool.arguments, + userContext + ); + processed[tool.id] = processedArgs; + edited[tool.id] = { ...processedArgs.processed }; + } else { + // For regular tools, use arguments as-is + edited[tool.id] = { ...tool.arguments }; + } + } + + setProcessedArguments(processed); + setEditedArguments(edited); + setArgumentsInitialized(true); + }; + + if (!argumentsInitialized) { + initializeArguments(); + } + }, [toolCalls, argumentsInitialized]); + + const handleApprove = async () => { + if (isApproving || isDenying) return; // Prevent double-clicks + + if (selectedToolIds.length === 0) { + handleDeny(); + return; + } + + setIsApproving(true); + + try { + // Update the original toolCalls with edited arguments + toolCalls.forEach(tool => { + if (editedArguments[tool.id]) { + const edited = editedArguments[tool.id]; + + if (tool.type === 'mcp') { + // For MCP tools, clean up arguments before sending + const processedArgs = processedArguments[tool.id]; + if (processedArgs?.schema) { + // Use the argument processor to clean up the final arguments + const cleaned = MCPArgumentProcessor.cleanupArguments(edited, processedArgs.schema); + tool.arguments = cleaned; + } else { + tool.arguments = edited; + } + } else { + // For regular tools, use edited arguments as-is + tool.arguments = edited; + } + } + }); + + onApprove(selectedToolIds); + } catch (error) { + console.error('Error during tool approval:', error); + setIsApproving(false); + } + }; + + const handleDeny = async () => { + if (isDenying || isApproving) return; // Prevent double-clicks and conflicts + + setIsDenying(true); + + try { + onDeny(); + } catch (error) { + console.error('Error during tool denial:', error); + setIsDenying(false); + } + }; + + + const getToolIcon = (toolName: string, toolType: 'mcp' | 'regular') => { + if (toolType === 'mcp') { + return 'mdi:connection'; // Use connection icon for MCP tools + } + + if (toolName.includes('kubernetes') || toolName.includes('k8s')) { + return 'mdi:kubernetes'; + } + return 'mdi:tool'; + }; + + const renderArgumentField = ( + toolId: string, + fieldName: string, + fieldValue: any, + fieldSchema: any, + _isRequired: boolean, + hasError: boolean + ) => { + const fieldType = fieldSchema?.type || 'string'; + const fieldDescription = fieldSchema?.description; + const enumValues = fieldSchema?.enum; + + const currentValue = editedArguments[toolId]?.[fieldName] ?? fieldValue; + + const handleFieldChange = (newValue: any) => { + setEditedArguments(prev => ({ + ...prev, + [toolId]: { + ...prev[toolId], + [fieldName]: newValue + } + })); + }; + + // Render different input types based on schema + if (fieldType === 'boolean') { + return ( + handleFieldChange(e.target.checked)} + size="small" + /> + } + label={fieldName} + sx={{ ml: 0 }} + /> + ); + } + + if (enumValues && Array.isArray(enumValues)) { + return ( + + {fieldName} + + {fieldDescription && ( + {fieldDescription} + )} + + ); + } + + if (fieldType === 'number' || fieldType === 'integer') { + return ( + handleFieldChange( + fieldType === 'integer' ? parseInt(e.target.value) || 0 : parseFloat(e.target.value) || 0 + )} + helperText={fieldDescription} + error={hasError} + sx={{ minWidth: 200 }} + inputProps={{ + min: fieldSchema?.minimum, + max: fieldSchema?.maximum, + step: fieldType === 'integer' ? 1 : 'any' + }} + /> + ); + } + + // Default to text field for strings and other types + const isMultiline = fieldType === 'object' || fieldType === 'array' || + (typeof currentValue === 'string' && currentValue.length > 50); + + return ( + { + let newValue = e.target.value; + + // Try to parse as JSON for object/array fields + if (fieldType === 'object' || fieldType === 'array') { + try { + newValue = JSON.parse(e.target.value); + } catch { + // Keep as string if not valid JSON + } + } + + handleFieldChange(newValue); + }} + multiline={isMultiline} + rows={isMultiline ? 3 : 1} + helperText={fieldDescription} + error={hasError} + sx={{ minWidth: 300 }} + placeholder={fieldSchema?.example || `Enter ${fieldName}...`} + /> + ); + }; + + const renderArgumentsForTool = (tool: ToolCall, toolId: string) => { + if (tool.type !== 'mcp' || !argumentsInitialized) { + // Render simple view for regular tools or while loading + return Object.entries(tool.arguments).map(([key, value]) => ( + + {key}:} + secondary={ + + {typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value)} + + } + /> + + )); + } + + const processedArgs = processedArguments[toolId]; + if (!processedArgs) return null; + + const { schema, errors } = processedArgs; + const properties = schema?.inputSchema?.properties || {}; + const required = schema?.inputSchema?.required || []; + const currentArgs = editedArguments[toolId] || {}; + + // Show argument fields based on schema + const fields = Object.entries(properties).map(([fieldName, fieldSchema]) => { + const isRequired = required.includes(fieldName); + const hasError = errors.some(error => error.includes(fieldName)); + const fieldValue = currentArgs[fieldName]; + + return ( + + + + {fieldName} + + {isRequired && ( + + )} + {!isRequired && ( + + )} + + {/* Show intelligent fill indicator */} + {processedArgs.intelligentFills[fieldName] && ( + + } + /> + + )} + + {fieldSchema.description && ( + + + + + + )} + + + {/* Show intelligent fill details */} + {processedArgs.intelligentFills[fieldName] && ( + } + > + + AI Analysis: {processedArgs.intelligentFills[fieldName].reason} + {processedArgs.intelligentFills[fieldName].confidence < 0.8 && ( + <> • Please verify this value + )} + + + )} + + {renderArgumentField(toolId, fieldName, fieldValue, fieldSchema, isRequired, hasError)} + + ); + }); + + // Show validation errors + if (errors.length > 0) { + fields.push( + + + Validation Issues: + + {errors.map((error, index) => ( + + • {error} + + ))} + + ); + } + + return fields; + }; + + const mcpTools = toolCalls.filter(tool => tool.type === 'mcp'); + const regularTools = toolCalls.filter(tool => tool.type === 'regular'); + + // Check if any action is in progress + const isActionInProgress = loading || isApproving || isDenying; + + if (loading || isApproving) { + return ( + + + + + {isApproving ? 'Approving and executing tools...' : 'Executing approved tools...'} + + + + ); + } + + // Show denying state + if (isDenying) { + return ( + + + + + Denying tool execution... + + + + ); + } + + return ( + + + {/* Header */} + + + + Tool Execution Required + + 1 ? 's' : ''}`} + size="small" + variant="outlined" + color="primary" + /> + + + {/* Summary */} + + {compact + ? `Allow execution of ${toolCalls.length} tool${toolCalls.length > 1 ? 's' : ''}?` + : `The following tool${toolCalls.length > 1 ? 's' : ''} need${toolCalls.length > 1 ? '' : 's'} permission to execute:` + } + + + {/* Tool summary when compact */} + {compact && ( + + {toolCalls.map((tool) => ( + } + /> + ))} + + )} + + {/* Expandable details */} + {compact && !showDetails && ( + + {/* MCP Tools */} + {mcpTools.length > 0 && ( + + + MCP Tools: + + {mcpTools.map(tool => ( + + + + + {tool.name} + + + + {tool.description && ( + + {tool.description} + + )} + + ))} + + )} + + {/* Regular Tools */} + {regularTools.length > 0 && ( + + + System Tools: + + {regularTools.map(tool => ( + + + + + {tool.name} + + + {tool.description && ( + + {tool.description} + + )} + + ))} + + )} + + )} + + {/* Toggle details button for compact mode */} + {compact && ( + + + + )} + + {/* Detailed view */} + + + {toolCalls.map((tool, index) => ( + + {index > 0 && } + + + + {tool.name} + + {tool.type === 'mcp' && ( + + )} + + + {tool.description && ( + + {tool.description} + + )} + + {(Object.keys(tool.arguments).length > 0 || tool.type === 'mcp') && ( + + + Arguments {tool.type === 'mcp' ? '(editable):' : ':'} + + {tool.type === 'mcp' ? ( + + {renderArgumentsForTool(tool, tool.id)} + + ) : ( + + {Object.entries(tool.arguments).map(([key, value]) => ( + + {key}:} + secondary={ + + {typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value)} + + } + /> + + ))} + + )} + + )} + + ))} + + + + {/* Warning */} + + + {mcpTools.length > 0 + ? 'MCP tool arguments have been intelligently analyzed and pre-filled based on your request. Arguments marked "AI-filled" were extracted from your message or context. Review and modify as needed.' + : 'These tools will access external systems. Review the details before approving.' + } + + + + {/* Loading state for argument processing */} + {!argumentsInitialized && mcpTools.length > 0 && ( + + + + Processing intelligent argument suggestions... + + + )} + + {/* Action buttons */} + + + + + + + ); +}; + +export default InlineToolConfirmation; \ No newline at end of file diff --git a/ai-assistant/src/components/common/ToolApprovalDialog.tsx b/ai-assistant/src/components/common/ToolApprovalDialog.tsx index aa98647fec..325cdaed13 100644 --- a/ai-assistant/src/components/common/ToolApprovalDialog.tsx +++ b/ai-assistant/src/components/common/ToolApprovalDialog.tsx @@ -1,27 +1,26 @@ -import React, { useState } from 'react'; +import { Icon } from '@iconify/react'; import { - Dialog, - DialogTitle, - DialogContent, - DialogActions, - Button, - Typography, - Box, - Chip, - Alert, Accordion, - AccordionSummary, AccordionDetails, + AccordionSummary, + Alert, + Box, + Button, + Checkbox, + Chip, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + FormControlLabel, + FormGroup, + IconButton, List, ListItem, ListItemText, - IconButton, - Tooltip, - FormGroup, - FormControlLabel, - Checkbox, + Typography, } from '@mui/material'; -import { Icon } from '@iconify/react'; +import React, { useState } from 'react'; interface ToolCall { id: string; @@ -98,7 +97,7 @@ const ToolApprovalDialog: React.FC = ({ const formatArguments = (args: Record) => { return Object.entries(args) - .filter(([_, value]) => value !== undefined && value !== null && value !== '') + .filter(([, value]) => value !== undefined && value !== null && value !== '') .map(([key, value]) => ( void; + reject: (error: Error) => void; + // Reference to the AI manager for adding messages to history + aiManager?: any; + // Callback to update the specific message with loading state + updateMessage?: (loading: boolean) => void; +} + +export class InlineToolApprovalManager extends EventEmitter { + private static instance: InlineToolApprovalManager | null = null; + private pendingRequest: InlineToolApprovalRequest | null = null; + private autoApproveSettings: Map = new Map(); + private sessionAutoApproval: boolean = false; + + private constructor() { + super(); + } + + public static getInstance(): InlineToolApprovalManager { + if (!InlineToolApprovalManager.instance) { + InlineToolApprovalManager.instance = new InlineToolApprovalManager(); + } + return InlineToolApprovalManager.instance; + } + + /** + * Extract user context from AI manager + */ + private extractUserContext(aiManager: any): UserContext { + const userContext: UserContext = { + timeContext: new Date() + }; + + try { + // Extract user message from history + const history = aiManager.history || []; + const lastUserMessage = history + .filter((msg: any) => msg.role === 'user') + .pop(); + + if (lastUserMessage) { + userContext.userMessage = lastUserMessage.content; + } + + // Extract conversation history (last 5 messages for context) + userContext.conversationHistory = history + .slice(-5) + .map((msg: any) => ({ + role: msg.role, + content: msg.content + })); + + // Extract kubernetes context if available + if (aiManager.toolManager?.kubernetesContext) { + userContext.kubernetesContext = { + selectedClusters: aiManager.toolManager.kubernetesContext.selectedClusters, + namespace: aiManager.toolManager.kubernetesContext.namespace, + currentResource: aiManager.toolManager.kubernetesContext.currentResource + }; + } + + // Extract last tool results + const lastToolResults = history + .filter((msg: any) => msg.role === 'tool') + .slice(-3); + + if (lastToolResults.length > 0) { + userContext.lastToolResults = {}; + lastToolResults.forEach((toolMsg: any, index: number) => { + try { + const parsed = JSON.parse(toolMsg.content); + userContext.lastToolResults![`tool_${index}`] = parsed; + } catch { + userContext.lastToolResults![`tool_${index}`] = toolMsg.content; + } + }); + } + + } catch (error) { + console.warn('Failed to extract user context:', error); + } + + return userContext; + } + + /** + * Request approval for tool execution via inline chat message + */ + async requestApproval(toolCalls: any[], aiManager: any): Promise { + // Check if session auto-approval is enabled + if (this.sessionAutoApproval) { + console.log('Auto-approving tools due to session setting'); + return toolCalls.map(tool => tool.id); + } + + // Check for individual tool auto-approvals + const autoApprovedTools: string[] = []; + const needsApprovalTools: ToolCall[] = []; + + for (const tool of toolCalls) { + if (this.autoApproveSettings.get(tool.name)) { + autoApprovedTools.push(tool.id); + } else { + needsApprovalTools.push(tool); + } + } + + // If all tools are auto-approved, return them + if (needsApprovalTools.length === 0) { + console.log('All tools auto-approved:', autoApprovedTools); + return autoApprovedTools; + } + + // If there's already a pending request, reject the previous one + if (this.pendingRequest) { + this.pendingRequest.reject(new Error('Request superseded by new tool approval request')); + } + + // Extract user context for intelligent argument processing + const userContext = this.extractUserContext(aiManager); + + return new Promise((resolve, reject) => { + const requestId = `tool-approval-${Date.now()}-${Math.random()}`; + + const handleApprove = (approvedToolIds: string[]) => { + // Combine auto-approved and manually approved tools + const allApprovedIds = [...autoApprovedTools, ...approvedToolIds]; + + // Update the message to show loading state + if (this.pendingRequest?.updateMessage) { + this.pendingRequest.updateMessage(true); + } + + this.pendingRequest = null; + resolve(allApprovedIds); + }; + + const handleDeny = () => { + this.pendingRequest = null; + reject(new Error('User denied tool execution')); + }; + + const updateMessage = (loading: boolean) => { + // Emit an event to update the UI + if (this.pendingRequest) { + this.emit('update-confirmation', { + requestId: this.pendingRequest.requestId, + toolConfirmation: { + tools: this.pendingRequest.toolCalls, + onApprove: handleApprove, + onDeny: handleDeny, + loading: loading, + requestId: this.pendingRequest.requestId, // Include requestId + userContext: userContext // Include user context + } + }); + } + }; + + this.pendingRequest = { + requestId, + toolCalls: needsApprovalTools, + resolve: handleApprove, + reject: handleDeny, + aiManager, + updateMessage + }; + + // Emit event to add the tool confirmation message to chat history + this.emit('request-confirmation', { + requestId, + toolConfirmation: { + tools: needsApprovalTools, + onApprove: handleApprove, + onDeny: handleDeny, + loading: false, + requestId: requestId, // Include requestId in the tool confirmation + userContext: userContext // Pass user context for intelligent argument processing + }, + aiManager + }); + }); + } + + /** + * Approve tools (called from inline confirmation component) + */ + public approveTools(requestId: string, approvedToolIds: string[], rememberChoice = false): void { + if (!this.pendingRequest || this.pendingRequest.requestId !== requestId) { + console.warn('No matching pending request for approval:', requestId); + return; + } + + // Handle remember choice + if (rememberChoice) { + // If all tools were approved, enable session auto-approval + const allToolIds = this.pendingRequest.toolCalls.map(tool => tool.id); + if (approvedToolIds.length === allToolIds.length) { + this.sessionAutoApproval = true; + console.log('Session auto-approval enabled'); + } else { + // Remember individual tool approvals + for (const toolCall of this.pendingRequest.toolCalls) { + if (approvedToolIds.includes(toolCall.id)) { + this.autoApproveSettings.set(toolCall.name, true); + } + } + console.log('Individual tool approvals saved'); + } + } + + this.pendingRequest.resolve(approvedToolIds); + } + + /** + * Deny all tools (called from inline confirmation component) + */ + public denyTools(requestId: string): void { + if (!this.pendingRequest || this.pendingRequest.requestId !== requestId) { + console.warn('No matching pending request for denial:', requestId); + return; + } + + this.pendingRequest.reject(new Error('User denied tool execution')); + } + + /** + * Get current pending request + */ + public getPendingRequest(): InlineToolApprovalRequest | null { + return this.pendingRequest; + } + + /** + * Clear session settings (called when user explicitly clears or starts new session) + */ + public clearSession(): void { + this.sessionAutoApproval = false; + this.autoApproveSettings.clear(); + console.log('Tool approval session settings cleared'); + } + + /** + * Set session auto-approval + */ + public setSessionAutoApproval(enabled: boolean): void { + this.sessionAutoApproval = enabled; + console.log(`Session auto-approval ${enabled ? 'enabled' : 'disabled'}`); + } + + /** + * Get session auto-approval status + */ + public isSessionAutoApprovalEnabled(): boolean { + return this.sessionAutoApproval; + } + + /** + * Set auto-approval for a specific tool + */ + public setToolAutoApproval(toolName: string, enabled: boolean): void { + if (enabled) { + this.autoApproveSettings.set(toolName, true); + } else { + this.autoApproveSettings.delete(toolName); + } + console.log(`Tool "${toolName}" auto-approval ${enabled ? 'enabled' : 'disabled'}`); + } + + /** + * Check if a tool has auto-approval enabled + */ + public isToolAutoApprovalEnabled(toolName: string): boolean { + return this.autoApproveSettings.get(toolName) === true; + } +} + +// Export singleton instance +export const inlineToolApprovalManager = InlineToolApprovalManager.getInstance(); \ No newline at end of file From f4169efd2bc9da1546c1a7db5d88822bedef0230 Mon Sep 17 00:00:00 2001 From: ashu8912 Date: Sun, 21 Sep 2025 16:27:12 +0530 Subject: [PATCH 09/39] ai-plugin: Add tools dialog for managing available tools - Add ToolsDialog component for displaying and managing AI tools - Provide interface for viewing tool configurations and capabilities - Export ToolsDialog in assistant components index Signed-off-by: ashu8912 --- .../src/components/assistant/ToolsDialog.tsx | 315 ++++++++++++++++++ .../src/components/assistant/index.ts | 1 + 2 files changed, 316 insertions(+) create mode 100644 ai-assistant/src/components/assistant/ToolsDialog.tsx diff --git a/ai-assistant/src/components/assistant/ToolsDialog.tsx b/ai-assistant/src/components/assistant/ToolsDialog.tsx new file mode 100644 index 0000000000..3c35dc3a92 --- /dev/null +++ b/ai-assistant/src/components/assistant/ToolsDialog.tsx @@ -0,0 +1,315 @@ +import { Icon } from '@iconify/react'; +import { + Box, + Button, + Chip, + CircularProgress, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + Divider, + List, + ListItem, + ListItemSecondaryAction, + ListItemText, + Switch, + Typography, +} from '@mui/material'; +import React, { useEffect,useState } from 'react'; +import tools from '../../ai/mcp/electron-client'; +import { AVAILABLE_TOOLS } from '../../langchain/tools/registry'; + +interface MCPTool { + name: string; + description?: string; + inputSchema?: any; +} + +interface ToolsDialogProps { + open: boolean; + onClose: () => void; + enabledTools: string[]; + onToolsChange: (enabledTools: string[]) => void; +} + +export const ToolsDialog: React.FC = ({ + open, + onClose, + enabledTools, + onToolsChange, +}) => { + const [localEnabledTools, setLocalEnabledTools] = useState(enabledTools); + const [mcpTools, setMcpTools] = useState([]); + const [loadingMcpTools, setLoadingMcpTools] = useState(false); + + // Load MCP tools when dialog opens + useEffect(() => { + if (open) { + loadMcpTools(); + } + }, [open]); + + // Sync local state when enabledTools prop changes + useEffect(() => { + setLocalEnabledTools(enabledTools); + }, [enabledTools]); + + const loadMcpTools = async () => { + setLoadingMcpTools(true); + try { + const mcpToolsData = await tools(); + setMcpTools(mcpToolsData || []); + } catch (error) { + console.warn('Failed to load MCP tools:', error); + setMcpTools([]); + } finally { + setLoadingMcpTools(false); + } + }; + + const handleToggleTool = (toolName: string) => { + setLocalEnabledTools(prev => + prev.includes(toolName) + ? prev.filter(name => name !== toolName) + : [...prev, toolName] + ); + }; + + const handleSave = () => { + onToolsChange(localEnabledTools); + onClose(); + }; + + const handleCancel = () => { + setLocalEnabledTools(enabledTools); + onClose(); + }; + + const getToolIcon = (toolName: string, toolType?: string) => { + if (toolType === 'mcp' || toolName.includes('mcp')) { + return 'mdi:connection'; + } + if (toolName.includes('kubernetes') || toolName.includes('k8s')) { + return 'mdi:kubernetes'; + } + return 'mdi:tool'; + }; + + const renderMcpToolList = () => ( + <> + + + MCP Tools + + + External Model Context Protocol tools (always enabled) + + + + {loadingMcpTools ? ( + + + + Loading MCP tools... + + + ) : ( + + {mcpTools.length > 0 ? ( + mcpTools.map((tool, index) => ( + + + + + + + {tool.name} + + + + } + secondary={tool.description || 'External MCP tool'} + /> + + handleToggleTool(tool.name)} + checked={localEnabledTools.includes(tool.name)} + color="primary" + /> + + + )) + ) : ( + + + No MCP tools available. Configure MCP servers to see tools here. + + } + /> + + )} + + )} + + ); + + // Get tool categories + const kubernetesTools = AVAILABLE_TOOLS.filter(ToolClass => { + const tempTool = new ToolClass(); + return tempTool.config.name.includes('kubernetes') || tempTool.config.name.includes('k8s'); + }); + + const otherTools = AVAILABLE_TOOLS.filter(ToolClass => { + const tempTool = new ToolClass(); + return !tempTool.config.name.includes('kubernetes') && !tempTool.config.name.includes('k8s'); + }); + + const renderToolList = (tools: any[], title: string, subtitle?: string) => ( + <> + + + {title} + + {subtitle && ( + + {subtitle} + + )} + + + + {tools.map(ToolClass => { + const tempTool = new ToolClass(); + const toolName = tempTool.config.name; + const isEnabled = localEnabledTools.includes(toolName); + + return ( + + + + + + + {tempTool.config.displayName || toolName} + + {tempTool.config.category && ( + + )} + + } + secondary={tempTool.config.description} + /> + + handleToggleTool(toolName)} + checked={isEnabled} + color="primary" + /> + + + ); + })} + + + ); + + return ( +

+ + + + + Manage Tools + + + + + + + + Enable or disable tools that the AI can use. Changes will take effect immediately and will be saved to your settings. + + + {/* Kubernetes Tools */} + {kubernetesTools.length > 0 && ( + <> + {renderToolList( + kubernetesTools, + "Kubernetes Tools", + "Tools for interacting with Kubernetes clusters" + )} + + + )} + + {/* Other Tools */} + {otherTools.length > 0 && ( + renderToolList( + otherTools, + "System Tools", + "General purpose tools for various operations" + ) + )} + + {/* MCP Tools */} + {renderMcpToolList()} + + {(kubernetesTools.length > 0 || otherTools.length > 0) && ( + + )} + + + + + + + + ); +}; + +export default ToolsDialog; \ No newline at end of file diff --git a/ai-assistant/src/components/assistant/index.ts b/ai-assistant/src/components/assistant/index.ts index 6a238feba3..1761ad3c87 100644 --- a/ai-assistant/src/components/assistant/index.ts +++ b/ai-assistant/src/components/assistant/index.ts @@ -3,3 +3,4 @@ export { default as AIChatContent } from './AIChatContent'; export { AIInputSection } from './AIInputSection'; export { PromptSuggestions } from './PromptSuggestions'; export { default as TestModeInput } from './TestModeInput'; +export { ToolsDialog } from './ToolsDialog'; From 74893d38d8301ffed235f3ffe650889e559394a9 Mon Sep 17 00:00:00 2001 From: ashu8912 Date: Sun, 21 Sep 2025 16:27:44 +0530 Subject: [PATCH 10/39] ai-plugin: Enhance tool manager with retry functionality and MCP integration - Add originalArgs storage in tool response metadata for retry capability - Integrate MCPOutputFormatter for AI-powered output formatting - Improve MCP tool argument mapping and validation - Add comprehensive error handling and formatting for tool failures - Include originalArgs in all formatted outputs (success, error, fallback) - Enable tool retry functionality through stored execution arguments Signed-off-by: ashu8912 --- .../src/langchain/tools/ToolManager.ts | 510 +++++++++++++++--- 1 file changed, 435 insertions(+), 75 deletions(-) diff --git a/ai-assistant/src/langchain/tools/ToolManager.ts b/ai-assistant/src/langchain/tools/ToolManager.ts index 90bbe7019d..9313674d7e 100644 --- a/ai-assistant/src/langchain/tools/ToolManager.ts +++ b/ai-assistant/src/langchain/tools/ToolManager.ts @@ -1,29 +1,32 @@ import { BaseChatModel } from '@langchain/core/language_models/chat_models'; import { DynamicTool } from '@langchain/core/tools'; import { Prompt } from '../../ai/manager'; +import tools from '../../ai/mcp/electron-client'; +import {MCPOutputFormatter } from '../formatters/MCPOutputFormatter'; import { KubernetesTool, KubernetesToolContext } from './kubernetes'; import { AVAILABLE_TOOLS, getToolByName } from './registry'; import { ToolBase, ToolResponse } from './ToolBase'; -import tools, { ElectronMCPClient } from '../../ai/mcp/electron-client'; export class ToolManager { private tools: ToolBase[] = []; private toolHandlers: Map = new Map(); - private mcpTools: DynamicTool[] = []; - private mcpClient: ElectronMCPClient; + private mcpTools: any[] = []; + private mcpToolsInitialized: boolean = false; + private mcpInitializationPromise: Promise | null = null; + private boundModel: BaseChatModel | null = null; + private providerId: string | null = null; + private mcpFormatter: MCPOutputFormatter | null = null; - constructor(enabledToolIds?: string[]) { - this.mcpClient = new ElectronMCPClient(); - this.initializeTools(enabledToolIds); + constructor(private kubernetesContext?: KubernetesToolContext, enabledToolIds?: string[]) { + this.initializeTools(); + this.mcpInitializationPromise = this.initializeMCPTools(enabledToolIds); } /** * Initialize only enabled tools from the registry */ private initializeTools(enabledToolIds?: string[]): void { - // Initialize MCP tools with proper error handling - this.initializeMCPTools(); - + // Initialize regular tools first for (const ToolClass of AVAILABLE_TOOLS) { const tempTool = new ToolClass(); if (enabledToolIds && !enabledToolIds.includes(tempTool.config.name)) { @@ -37,12 +40,15 @@ export class ToolManager { console.error(`Failed to load tool ${ToolClass.name}:`, error); } } + + // Initialize MCP tools asynchronously but start immediately + this.initializeMCPTools(enabledToolIds); } /** * Initialize MCP tools from Electron main process */ - private async initializeMCPTools(): Promise { + private async initializeMCPTools(enabledToolIds?: string[]): Promise { try { console.log('Initializing MCP tools from Electron...'); const mcpToolsData = await tools(); @@ -50,8 +56,17 @@ export class ToolManager { if (mcpToolsData && mcpToolsData.length > 0) { console.log(`Successfully loaded ${mcpToolsData.length} MCP tools from Electron`); + // Filter MCP tools by enabled status + const filteredMcpTools = enabledToolIds + ? mcpToolsData.filter(toolData => enabledToolIds.includes(toolData.name)) + : mcpToolsData; + + if (enabledToolIds) { + console.log(`Filtered to ${filteredMcpTools.length} enabled MCP tools out of ${mcpToolsData.length} total`); + } + console.log("Filtered MCP tools:", filteredMcpTools); // Convert MCP tools to LangChain DynamicTool format - this.mcpTools = mcpToolsData.map(toolData => + this.mcpTools = filteredMcpTools.map(toolData => new DynamicTool({ name: toolData.name, description: toolData.description || `MCP tool: ${toolData.name}`, @@ -60,17 +75,23 @@ export class ToolManager { try { // Handle argument mapping for MCP tools // LangChain may wrap args in different formats, need to handle properly - let mappedArgs = this.mapMCPToolArguments(args, toolData.inputSchema); + const mappedArgs = this.mapMCPToolArguments(args, toolData.inputSchema); console.log(`MCP tool ${toolData.name} called with original args:`, JSON.stringify(args)); + console.log(`MCP tool ${toolData.name} input schema:`, JSON.stringify(toolData.inputSchema)); console.log(`MCP tool ${toolData.name} calling with mapped args:`, JSON.stringify(mappedArgs)); - const result = await this.mcpClient.executeTool(toolData.name, mappedArgs); + // Execute MCP tool through Electron API + const result = await window.desktopApi?.mcp.executeTool(toolData.name, mappedArgs); console.log(`MCP tool ${toolData.name} returned result:`, result); - console.log(`MCP tool ${toolData.name} result type:`, typeof result); + + // Extract actual result from MCP response + const actualResult = result?.result || result; + console.log(`MCP tool ${toolData.name} actual result:`, actualResult); + console.log(`MCP tool ${toolData.name} result type:`, typeof actualResult); // Ensure we return a string response - const response = typeof result === 'string' ? result : JSON.stringify(result); + const response = typeof actualResult === 'string' ? actualResult : JSON.stringify(actualResult); console.log(`MCP tool ${toolData.name} final response:`, response); return response; @@ -83,11 +104,20 @@ export class ToolManager { ); console.log(`Converted ${this.mcpTools.length} MCP tools to LangChain format`); + this.mcpToolsInitialized = true; + + // If we have a bound model, rebind with the new MCP tools + if (this.boundModel && this.providerId) { + console.log('🔄 Rebinding model with newly loaded MCP tools'); + this.boundModel = this.bindToModel(this.boundModel, this.providerId); + } } else { console.log('No MCP tools available or MCP client not initialized'); + this.mcpToolsInitialized = true; } } catch (error) { console.warn('Failed to initialize MCP tools from Electron:', error instanceof Error ? error.message : 'Unknown error'); + this.mcpToolsInitialized = true; // Continue without MCP tools - this is not a fatal error } } @@ -122,74 +152,225 @@ export class ToolManager { console.log('Tool has no required parameters, returning empty object'); return {}; } else { - // Tool has required parameters but got empty args - console.log('Tool has required parameters but got empty args, returning empty object (will let validation handle it)'); - return {}; + // Tool has required parameters but got empty args - create default structure + console.log('Tool has required parameters but got empty args, creating default parameter structure'); + return this.createDefaultParameterStructure(inputSchema); } } - // If args is wrapped in an "input" key but the schema doesn't expect it - if (args && typeof args === 'object' && 'input' in args && !schemaProps.input) { - const inputValue = args.input; + // First, check if args is properly structured for the schema + if (args && typeof args === 'object') { + // Remove any non-schema fields like 'input' that shouldn't be there + const schemaPropertyNames = Object.keys(schemaProps); + const cleanArgs: any = {}; - // Handle empty input - if (!inputValue || inputValue === '' || inputValue === '""') { - const requiredProps = inputSchema?.required || []; - if (requiredProps.length === 0) { - console.log('Unwrapped input is empty and no required params, returning empty object'); - return {}; + // Copy only fields that exist in the schema + for (const [key, value] of Object.entries(args)) { + if (schemaPropertyNames.includes(key)) { + cleanArgs[key] = value; } } - // If the input is a primitive value and the schema has only one property - const schemaPropertyNames = Object.keys(schemaProps); - if (schemaPropertyNames.length === 1 && (typeof inputValue === 'string' || typeof inputValue === 'number' || typeof inputValue === 'boolean')) { - // Map the primitive value to the single expected property - console.log(`Mapping primitive value to single property: ${schemaPropertyNames[0]}`); - return { [schemaPropertyNames[0]]: inputValue }; + // If we found valid schema fields, use the cleaned args + if (Object.keys(cleanArgs).length > 0) { + console.log('Found valid schema fields, using cleaned args:', cleanArgs); + return this.filterMCPArguments(cleanArgs, inputSchema); } - // If the input is an object, try to unwrap it - if (typeof inputValue === 'object' && inputValue !== null) { - console.log('Unwrapping object input'); - return inputValue; - } - - // For primitive values with multiple schema properties, try common mappings - if (typeof inputValue === 'string') { - // Try common parameter names - if (schemaProps.path) { - console.log('Mapping string to path parameter'); - return { path: inputValue }; + // If no valid schema fields found, check for 'input' wrapper + if ('input' in args && !schemaProps.input) { + const inputValue = args.input; + + // Handle empty input + if (!inputValue || inputValue === '' || inputValue === '""') { + const requiredProps = inputSchema?.required || []; + if (requiredProps.length === 0) { + console.log('Unwrapped input is empty and no required params, returning empty object'); + return {}; + } } - if (schemaProps.directory) { - console.log('Mapping string to directory parameter'); - return { directory: inputValue }; + + // If the input is a primitive value and the schema has only one property + if (schemaPropertyNames.length === 1 && (typeof inputValue === 'string' || typeof inputValue === 'number' || typeof inputValue === 'boolean')) { + // Map the primitive value to the single expected property + console.log(`Mapping primitive input to single property: ${schemaPropertyNames[0]}`); + return { [schemaPropertyNames[0]]: inputValue }; } - if (schemaProps.file) { - console.log('Mapping string to file parameter'); - return { file: inputValue }; + + // If the input is an object, try to unwrap it + if (typeof inputValue === 'object' && inputValue !== null) { + console.log('Unwrapping object input'); + return this.filterMCPArguments(inputValue, inputSchema); } - if (schemaProps.name) { - console.log('Mapping string to name parameter'); - return { name: inputValue }; + + // For primitive values with multiple schema properties, try common mappings + if (typeof inputValue === 'string') { + // Try common parameter names + if (schemaProps.query) { + console.log('Mapping string input to query parameter'); + return { query: inputValue }; + } + if (schemaProps.path) { + console.log('Mapping string input to path parameter'); + return { path: inputValue }; + } + if (schemaProps.directory) { + console.log('Mapping string input to directory parameter'); + return { directory: inputValue }; + } + if (schemaProps.file) { + console.log('Mapping string input to file parameter'); + return { file: inputValue }; + } + if (schemaProps.name) { + console.log('Mapping string input to name parameter'); + return { name: inputValue }; + } + + // If no common mapping found, map to first required property, then first property + const requiredProps = inputSchema?.required || []; + const targetProp = requiredProps.length > 0 ? requiredProps[0] : schemaPropertyNames[0]; + if (targetProp) { + console.log(`Mapping string input to target property: ${targetProp}`); + return { [targetProp]: inputValue }; + } } - // If no common mapping found, map to first property - if (schemaPropertyNames.length > 0) { - console.log(`Mapping string to first property: ${schemaPropertyNames[0]}`); - return { [schemaPropertyNames[0]]: inputValue }; + // Return the unwrapped input and let the tool handle validation + console.log('Returning unwrapped input value'); + return inputValue; + } + } + + // If args structure matches or is already properly formatted, filter and return + console.log('Args appear to be in correct format, filtering empty values'); + return this.filterMCPArguments(args, inputSchema); + } + + /** + * Filter MCP arguments to only include required fields and fields with actual values + */ + private filterMCPArguments(args: any, inputSchema?: any): any { + if (!inputSchema || !args || typeof args !== 'object') { + return args; + } + + const schemaProps = inputSchema?.properties; + const requiredProps = inputSchema?.required || []; + + if (!schemaProps) { + return args; + } + + const filteredArgs: any = {}; + + // Always include all required properties, even if they have empty/default values + for (const requiredProp of requiredProps) { + if (requiredProp in args) { + filteredArgs[requiredProp] = args[requiredProp]; + } else { + // If required property is missing, add a default value based on schema + const propSchema = schemaProps[requiredProp]; + if (propSchema) { + if (propSchema.type === 'string') { + filteredArgs[requiredProp] = propSchema.default || ''; + } else if (propSchema.type === 'number' || propSchema.type === 'integer') { + filteredArgs[requiredProp] = propSchema.default || 0; + } else if (propSchema.type === 'boolean') { + filteredArgs[requiredProp] = propSchema.default || false; + } else if (propSchema.type === 'array') { + filteredArgs[requiredProp] = propSchema.default || []; + } else if (propSchema.type === 'object') { + filteredArgs[requiredProp] = propSchema.default || {}; + } else { + filteredArgs[requiredProp] = propSchema.default || {}; + } } } + } + + // Include optional properties only if they have actual values + for (const [key, value] of Object.entries(args)) { + // Skip if already included as required + if (requiredProps.includes(key)) { + continue; + } - // Return the unwrapped input and let the tool handle validation - console.log('Returning unwrapped input value'); - return inputValue; + // Skip if property is not in schema + if (!(key in schemaProps)) { + continue; + } + + // Include only if value is meaningful (not empty string, null, undefined, or empty object/array) + if (this.hasActualValue(value)) { + filteredArgs[key] = value; + } } + + console.log('Filtered MCP arguments:', { original: args, filtered: filteredArgs, required: requiredProps }); + return filteredArgs; + } - // If args structure matches or is already properly formatted, return as-is - console.log('Args appear to be in correct format, returning as-is'); - return args; + /** + * Check if a value is meaningful (not empty/null/undefined) + */ + private hasActualValue(value: any): boolean { + // Null, undefined, or empty string are not actual values + if (value === null || value === undefined || value === '') { + return false; + } + + // Empty arrays are not actual values + if (Array.isArray(value)) { + return value.length > 0; + } + + // Empty objects are not actual values + if (typeof value === 'object') { + return Object.keys(value).length > 0; + } + + // Numbers (including 0), booleans, and non-empty strings are actual values + return true; + } + + /** + * Create default parameter structure based on schema + * For required parameters that are missing, provide appropriate defaults + */ + private createDefaultParameterStructure(inputSchema: any): any { + if (!inputSchema || !inputSchema.properties) { + return {}; + } + + const defaultParams: any = {}; + const requiredProps = inputSchema.required || []; + const schemaProps = inputSchema.properties; + + for (const requiredProp of requiredProps) { + if (requiredProp in schemaProps) { + const propSchema = schemaProps[requiredProp]; + + // Create appropriate default values based on type + if (propSchema.type === 'string') { + defaultParams[requiredProp] = propSchema.default || ''; + } else if (propSchema.type === 'number' || propSchema.type === 'integer') { + defaultParams[requiredProp] = propSchema.default || 0; + } else if (propSchema.type === 'boolean') { + defaultParams[requiredProp] = propSchema.default || false; + } else if (propSchema.type === 'array') { + defaultParams[requiredProp] = propSchema.default || []; + } else if (propSchema.type === 'object') { + defaultParams[requiredProp] = propSchema.default || {}; + } else { + // For unknown types, try to use the default or provide an empty object + defaultParams[requiredProp] = propSchema.default || {}; + } + } + } + + console.log('Created default parameter structure:', defaultParams); + return defaultParams; } /** @@ -287,23 +468,111 @@ export class ToolManager { const mcpTool = this.mcpTools.find(tool => tool.name === toolName); if (mcpTool) { try { - const result = await mcpTool.func(args); + const rawResult = await mcpTool.func(args); + + // Check if the raw result indicates an error + const isError = this.detectMCPError(rawResult); + + // Format the MCP output using AI if formatter is available + let formattedContent = rawResult; + if (this.mcpFormatter) { + try { + const formatted = await this.mcpFormatter.formatMCPOutput(rawResult, toolName); + formattedContent = JSON.stringify({ + formatted: true, + mcpOutput: formatted, + raw: rawResult, + isError: isError || formatted.type === 'error', + originalArgs: args // Include original arguments in the formatted content for retry functionality + }); + } catch (formatError) { + console.warn(`Failed to format MCP output for ${toolName}:`, formatError); + // Fall back to simple formatting + const simpleFormatted = this.mcpFormatter.formatSimple(rawResult, toolName); + formattedContent = JSON.stringify({ + formatted: true, + mcpOutput: simpleFormatted, + raw: rawResult, + isError: isError || simpleFormatted.type === 'error', + originalArgs: args // Include original arguments in the fallback formatting too + }); + } + } else { + // No formatter available, but still detect errors + if (isError) { + formattedContent = JSON.stringify({ + error: true, + message: this.extractErrorMessage(rawResult), + toolName, + raw: rawResult, + originalArgs: args // Include original arguments for error cases too + }); + } + } + return { - content: result, + content: formattedContent, shouldAddToHistory: true, shouldProcessFollowUp: false, - metadata: { toolName, source: 'mcp' }, + metadata: { + toolName, + source: 'mcp', + formatted: !!this.mcpFormatter, + isError: isError || (this.mcpFormatter && JSON.parse(formattedContent).mcpOutput?.type === 'error'), + originalArgs: args, // Store original arguments for retry functionality + }, }; } catch (error) { console.error(`Error executing MCP tool ${toolName}:`, error); - return { - content: JSON.stringify({ + + // Format execution errors properly if formatter is available + let errorContent; + if (this.mcpFormatter) { + const errorFormatted = { + type: 'error' as const, + title: `Tool Execution Failed: ${toolName}`, + summary: 'The MCP tool failed to execute due to an internal error.', + data: { + message: 'Tool execution failed', + details: error instanceof Error ? error.message : 'Unknown execution error', + suggestions: [ + 'Check if the tool is properly configured and accessible', + 'Verify the input parameters match the tool requirements', + 'Try again in a few moments as this may be a temporary issue' + ] + }, + insights: ['Tool execution errors may indicate configuration or connectivity issues'], + warnings: ['This tool is currently unavailable'], + actionable_items: ['Review tool configuration and try again'], + metadata: { + toolName, + responseSize: 0, + processingTime: 0, + dataPoints: 0 + } + }; + + errorContent = JSON.stringify({ + formatted: true, + mcpOutput: errorFormatted, + raw: error instanceof Error ? error.message : 'Unknown error', + isError: true, + originalArgs: args // Include original arguments in error formatting too + }); + } else { + errorContent = JSON.stringify({ error: true, message: `Error executing MCP tool: ${error instanceof Error ? error.message : 'Unknown error'}`, - }), + toolName, + originalArgs: args // Include original arguments in simple error format too + }); + } + + return { + content: errorContent, shouldAddToHistory: true, shouldProcessFollowUp: false, - metadata: { error: 'mcp_execution_error', toolName }, + metadata: { error: 'mcp_execution_error', toolName, isError: true }, }; } } @@ -316,26 +585,117 @@ export class ToolManager { */ bindToModel(model: BaseChatModel, providerId: string): BaseChatModel { try { + // Store for potential rebinding when MCP tools are loaded + this.providerId = providerId; + + // Initialize MCP formatter with the model + if (!this.mcpFormatter) { + this.mcpFormatter = new MCPOutputFormatter(model); + console.log('🎨 MCP output formatter initialized'); + } + const langChainTools = this.getLangChainTools(); if (langChainTools.length === 0) { console.warn('No tools configured for binding'); + this.boundModel = model; return model; } - console.log(`Binding ${langChainTools.length} tools to ${providerId} model:`, + console.log(`Binding ${langChainTools.length} tools to ${providerId} model:`, langChainTools.map(t => t.name)); - return model.bindTools(langChainTools); + this.boundModel = model.bindTools(langChainTools); + return this.boundModel; } catch (error) { console.error(`Error binding tools to ${providerId} model:`, error); + this.boundModel = model; return model; } } /** - * Get list of all configured tool names + * Wait for MCP tools to be initialized + */ + async waitForMCPToolsInitialization(): Promise { + if (this.mcpInitializationPromise) { + await this.mcpInitializationPromise; + } + } + + /** + * Check if MCP tools are initialized + */ + areMCPToolsInitialized(): boolean { + return this.mcpToolsInitialized; + } + + /** + * Get list of all configured tool names (including MCP tools) */ getToolNames(): string[] { - return this.tools.map(tool => tool.config.name); + const regularToolNames = this.tools.map(tool => tool.config.name); + const mcpToolNames = this.mcpTools.map(tool => tool.name); + return [...regularToolNames, ...mcpToolNames]; + } + + /** + * Detect if an MCP tool result indicates an error + */ + private detectMCPError(result: string): boolean { + try { + const parsed = JSON.parse(result); + + // Check for explicit error indicators + if (parsed.success === false || parsed.error === true) { + return true; + } + + // Check for error messages + if (parsed.error || parsed.message?.toLowerCase().includes('error')) { + return true; + } + + // Check for schema mismatch or other common error patterns + if (typeof parsed.error === 'string' && parsed.error.length > 0) { + return true; + } + + return false; + } catch { + // If not JSON, check for common error patterns in the string + const lowerResult = result.toLowerCase(); + return lowerResult.includes('error') || + lowerResult.includes('failed') || + lowerResult.includes('exception') || + lowerResult.includes('invalid') || + lowerResult.includes('schema mismatch'); + } + } + + /** + * Extract error message from MCP tool result + */ + private extractErrorMessage(result: string): string { + try { + const parsed = JSON.parse(result); + + // Try various fields that might contain the error message + if (parsed.error && typeof parsed.error === 'string') { + return parsed.error; + } + + if (parsed.message) { + return parsed.message; + } + + if (parsed.details) { + return parsed.details; + } + + return 'Tool execution failed with an unspecified error'; + } catch { + // Not JSON, return the raw result or a cleaned version + return result.length > 200 ? result.substring(0, 200) + '...' : result; + } } } From 047cbe74a92ea052cbea93692a0f215beb94e939 Mon Sep 17 00:00:00 2001 From: ashu8912 Date: Sun, 21 Sep 2025 16:28:05 +0530 Subject: [PATCH 11/39] ai-plugin: Integrate MCP components with content rendering system - Add MCPFormattedMessage integration to ContentRenderer - Add onRetryTool prop propagation through component hierarchy - Integrate prompt width context usage in components - Add MCP tool retry functionality to chat interface - Update TextStreamContainer and AIChatContent with retry support Signed-off-by: ashu8912 --- ai-assistant/src/ContentRenderer.tsx | 27 +++++-- .../components/assistant/AIChatContent.tsx | 8 +++ ai-assistant/src/textstream.tsx | 70 ++++++++++++++++--- 3 files changed, 89 insertions(+), 16 deletions(-) diff --git a/ai-assistant/src/ContentRenderer.tsx b/ai-assistant/src/ContentRenderer.tsx index 92427e1fc5..3dcba2b184 100644 --- a/ai-assistant/src/ContentRenderer.tsx +++ b/ai-assistant/src/ContentRenderer.tsx @@ -6,6 +6,7 @@ import { Link as RouterLink, useHistory } from 'react-router-dom'; import remarkGfm from 'remark-gfm'; import YAML from 'yaml'; import { LogsButton, YamlDisplay } from './components'; +import MCPFormattedMessage from './components/chat/MCPFormattedMessage'; import { getHeadlampLink } from './utils/promptLinkHelper'; import { parseKubernetesYAML } from './utils/SampleYamlLibrary'; @@ -87,6 +88,8 @@ const parseLogsButtonData = (content: string, logsButtonIndex: number): ParseRes interface ContentRendererProps { content: string; onYamlDetected?: (yaml: string, resourceType: string) => void; + promptWidth?: string; // Add width prop + onRetryTool?: (toolName: string, args: Record) => void; } // Table wrapper component with show more functionality - moved outside to preserve state @@ -276,7 +279,7 @@ markdownComponents.li.displayName = 'MarkdownLi'; markdownComponents.blockquote.displayName = 'MarkdownBlockquote'; const ContentRenderer: React.FC = React.memo( - ({ content, onYamlDetected }) => { + ({ content, onYamlDetected, onRetryTool }) => { const history = useHistory(); // Create code component that has access to onYamlDetected const CodeComponent = React.useMemo(() => { @@ -524,7 +527,18 @@ const ContentRenderer: React.FC = React.memo( const processedContent = useMemo(() => { if (!content) return null; - // First, check if content is a JSON response with error or success keys + // First, check if content is a formatted MCP output (pure JSON) + try { + const parsed = JSON.parse(content.trim()); + if (parsed.formatted && parsed.mcpOutput) { + // This is a formatted MCP output, use our specialized component + return ; + } + } catch (error) { + // Not JSON or not formatted MCP output, continue with normal processing + } + + // Second, check if content is a JSON response with error or success keys const jsonParseResult = parseJsonContent(content.trim()); if (jsonParseResult.success) { const parsedContent = jsonParseResult.data; @@ -556,8 +570,8 @@ const ContentRenderer: React.FC = React.memo( theme.palette.grey[100], - color: theme => theme.palette.grey[900], + backgroundColor: (theme: any) => theme.palette.grey[100], + color: (theme: any) => theme.palette.grey[900], padding: 2, borderRadius: 1, overflowX: 'auto', @@ -633,7 +647,7 @@ const ContentRenderer: React.FC = React.memo( {content} ); - }, [content, onYamlDetected, processUnformattedYaml]); + }, [content, onYamlDetected, onRetryTool, processUnformattedYaml]); return ( @@ -645,7 +659,8 @@ const ContentRenderer: React.FC = React.memo( // Only re-render if content or onYamlDetected actually changed return ( prevProps.content === nextProps.content && - prevProps.onYamlDetected === nextProps.onYamlDetected + prevProps.onYamlDetected === nextProps.onYamlDetected && + prevProps.onRetryTool === nextProps.onRetryTool ); } ); diff --git a/ai-assistant/src/components/assistant/AIChatContent.tsx b/ai-assistant/src/components/assistant/AIChatContent.tsx index 6e418bc853..f927321452 100644 --- a/ai-assistant/src/components/assistant/AIChatContent.tsx +++ b/ai-assistant/src/components/assistant/AIChatContent.tsx @@ -11,6 +11,7 @@ interface AIChatContentProps { onOperationSuccess: (response: any) => void; onOperationFailure: (error: any, operationType: string, resourceInfo?: any) => void; onYamlAction: (yaml: string, title: string, type: string, isDeleteOp: boolean) => void; + onRetryTool?: (toolName: string, args: Record) => void; } export default function AIChatContent({ @@ -20,12 +21,18 @@ export default function AIChatContent({ onOperationSuccess, onOperationFailure, onYamlAction, + onRetryTool, }: AIChatContentProps) { return ( {apiError && ( @@ -56,6 +63,7 @@ export default function AIChatContent({ onOperationSuccess={onOperationSuccess} onOperationFailure={onOperationFailure} onYamlAction={onYamlAction} + onRetryTool={onRetryTool} /> ); diff --git a/ai-assistant/src/textstream.tsx b/ai-assistant/src/textstream.tsx index c32ec957cd..b20156a2d6 100644 --- a/ai-assistant/src/textstream.tsx +++ b/ai-assistant/src/textstream.tsx @@ -4,6 +4,7 @@ import { useTheme } from '@mui/material'; import { alpha } from '@mui/material/styles'; import React, { useCallback, useEffect, useRef, useState } from 'react'; import { Prompt } from './ai/manager'; +import { InlineToolConfirmation } from './components'; import ContentRenderer from './ContentRenderer'; import EditorDialog from './editordialog'; @@ -14,6 +15,7 @@ const TextStreamContainer = React.memo(function TextStreamContainer({ onOperationSuccess, onOperationFailure, onYamlAction, + onRetryTool, }: { history: Prompt[]; isLoading: boolean; @@ -21,6 +23,8 @@ const TextStreamContainer = React.memo(function TextStreamContainer({ onOperationSuccess?: (response: any) => void; onOperationFailure?: (error: any, operationType: string, resourceInfo?: any) => void; onYamlAction?: (yaml: string, title: string, resourceType: string, isDelete: boolean) => void; + onRetryTool?: (toolName: string, args: Record) => void; + promptWidth?: string; }) { const [showEditor, setShowEditor] = useState(false); const [editorContent, setEditorContent] = useState(''); @@ -236,7 +240,7 @@ const TextStreamContainer = React.memo(function TextStreamContainer({ const isJsonSuccess = prompt.success; if (prompt.content === '' && prompt.role === 'user') return null; - if (prompt.content === '' && prompt.role === 'assistant') return null; + if (prompt.content === '' && prompt.role === 'assistant' && !prompt.toolConfirmation) return null; return ( {prompt.role === 'user' ? 'You' : 'AI Assistant'} - + {prompt.role === 'user' ? ( prompt.content ) : ( <> {isContentFilterError || hasError ? ( - + {prompt.content} {isContentFilterError && ( @@ -285,11 +314,23 @@ const TextStreamContainer = React.memo(function TextStreamContainer({ ) : ( <> - {/* Use ContentRenderer for all assistant content */} - + {/* Check if this is a tool confirmation message */} + {prompt.toolConfirmation ? ( + + ) : ( + /* Use ContentRenderer for all assistant content */ + + )} )} @@ -302,16 +343,25 @@ const TextStreamContainer = React.memo(function TextStreamContainer({ ); return ( - + {/* Content filter guidance when errors are detected */} From 22231c69b95523ede3aab59ea247b56a78525d10 Mon Sep 17 00:00:00 2001 From: ashu8912 Date: Sun, 21 Sep 2025 16:28:20 +0530 Subject: [PATCH 12/39] ai-plugin: Enhance AI modal with retry functionality and tool management - Add handleRetryTool function for re-executing failed tools - Integrate inline tool approval manager for seamless confirmations - Add comprehensive tool response processing and error handling - Implement suggestion parsing and display from AI responses - Add retry capability propagation to chat components - Improve user experience with better tool interaction workflow Signed-off-by: ashu8912 --- ai-assistant/src/modal.tsx | 179 ++++++++++++++++++++++++++++--------- 1 file changed, 138 insertions(+), 41 deletions(-) diff --git a/ai-assistant/src/modal.tsx b/ai-assistant/src/modal.tsx index 85a5eff154..bc49ac1567 100644 --- a/ai-assistant/src/modal.tsx +++ b/ai-assistant/src/modal.tsx @@ -2,7 +2,6 @@ import { Icon } from '@iconify/react'; import { useClustersConf, useSelectedClusters } from '@kinvolk/headlamp-plugin/lib/k8s'; import { getCluster, getClusterGroup } from '@kinvolk/headlamp-plugin/lib/Utils'; import { Box, Button, Grid, Typography } from '@mui/material'; -import { isEqual } from 'lodash'; import React, { useEffect, useMemo, useState } from 'react'; import { useHistory, useLocation } from 'react-router-dom'; import AIManager, { Prompt } from './ai/manager'; @@ -12,18 +11,16 @@ import { AIInputSection, ApiConfirmationDialog, PromptSuggestions, - ToolApprovalDialog, } from './components'; import { getProviderById } from './config/modelConfig'; import EditorDialog from './editordialog'; import { isTestModeCheck } from './helper'; import { useClusterWarnings } from './hooks/useClusterWarnings'; import { useKubernetesToolUI } from './hooks/useKubernetesToolUI'; -import { useToolApproval } from './hooks/useToolApproval'; -import { toolApprovalManager } from './utils/ToolApprovalManager'; import LangChainManager from './langchain/LangChainManager'; import { getSettingsURL, useGlobalState } from './utils'; import { generateContextDescription } from './utils/contextGenerator'; +import { inlineToolApprovalManager } from './utils/InlineToolApprovalManager'; import { getProviderModels, parseSuggestionsFromResponse } from './utils/modalUtils'; import { useDynamicPrompts } from './utils/promptGenerator'; @@ -34,19 +31,20 @@ const OPERATION_TYPES = { DELETION: 'deletion', GENERIC: 'operation', } as const; +import { usePromptWidth } from './contexts/PromptWidthContext'; import { getActiveConfig, getSavedConfigurations, StoredProviderConfig, } from './utils/ProviderConfigManager'; -import { getEnabledToolIds } from './utils/ToolConfigManager'; export default function AIPrompt(props: { openPopup: boolean; setOpenPopup: (...args) => void; pluginSettings: any; + width: string }) { - const { openPopup, setOpenPopup, pluginSettings } = props; + const { openPopup, setOpenPopup, pluginSettings, width } = props; const history = useHistory(); const location = useLocation(); const rootRef = React.useRef(null); @@ -55,11 +53,17 @@ export default function AIPrompt(props: { const [apiError, setApiError] = React.useState(null); const [aiManager, setAiManager] = React.useState(null); const _pluginSetting = useGlobalState(); + const { enabledTools, setEnabledTools } = _pluginSetting; const [promptHistory, setPromptHistory] = React.useState([]); const [suggestions, setSuggestions] = React.useState([]); const selectedClusters = useSelectedClusters(); const clusters = useClustersConf() || {}; const dynamicPrompts = useDynamicPrompts(); + const prompWidthContext = usePromptWidth(); + + useEffect(() => { + prompWidthContext.setPromptWidth(width) + }, [width]) // Get cluster names for warning lookup - use selected clusters or current cluster only const clusterNames = useMemo(() => { const currentCluster = getCluster(); @@ -80,16 +84,9 @@ export default function AIPrompt(props: { // Use the custom hook to get warnings for clusters const clusterWarnings = useClusterWarnings(clusterNames); - // Tool approval management - const toolApproval = useToolApproval(); - const [activeConfig, setActiveConfig] = useState(null); const [availableConfigs, setAvailableConfigs] = useState([]); - const [enabledTools, setEnabledTools] = React.useState( - getEnabledToolIds(pluginSettings) - ); - // Test mode detection const isTestMode = isTestModeCheck(); @@ -296,23 +293,20 @@ export default function AIPrompt(props: { } }, [enabledTools, activeConfig, selectedModel]); - React.useEffect(() => { - // Only set if different - setEnabledTools(currentlyEnabledTools => { - const newEnabledTools = getEnabledToolIds(pluginSettings); - if (isEqual(currentlyEnabledTools, newEnabledTools)) { - return currentlyEnabledTools; - } - return newEnabledTools; - }); - }, [pluginSettings]); - const updateHistory = React.useCallback(() => { if (!aiManager?.history) { setPromptHistory([]); return; } + console.log('🔄 UpdateHistory called, aiManager.history length:', aiManager.history.length); + console.log('📋 Current aiManager.history:', aiManager.history.map(h => ({ + role: h.role, + hasToolConfirmation: !!h.toolConfirmation, + content: h.content?.substring(0, 50), + isDisplayOnly: h.isDisplayOnly + }))); + // Process the history to extract suggestions and clean content const processedHistory = aiManager.history.map((prompt, index) => { if (prompt.role === 'assistant' && prompt.content && !prompt.error) { @@ -332,6 +326,13 @@ export default function AIPrompt(props: { return prompt; }); + console.log('✅ ProcessedHistory length:', processedHistory.length); + console.log('📝 ProcessedHistory items:', processedHistory.map(h => ({ + role: h.role, + hasToolConfirmation: !!h.toolConfirmation, + isDisplayOnly: h.isDisplayOnly, + content: h.content?.substring(0, 50) + }))); setPromptHistory(processedHistory); }, [aiManager?.history]); @@ -339,6 +340,41 @@ export default function AIPrompt(props: { const { state: kubernetesUI, callbacks: kubernetesCallbacks } = useKubernetesToolUI(updateHistory); + // Set up event listeners for tool confirmation events + React.useEffect(() => { + const handleRequestConfirmation = (data: any) => { + console.log('🎯 Request confirmation event received:', data); + // Clear loading state when tool approval is requested + setLoading(false); + // Force an immediate update of the history from the AI manager + updateHistory(); + // Also force a re-render by updating the state + setPromptHistory(prev => [...prev]); + }; + + const handleUpdateConfirmation = (data: any) => { + console.log('🔄 Update confirmation event received:', data); + updateHistory(); + setPromptHistory(prev => [...prev]); + }; + + const handleMessageUpdated = (data: any) => { + console.log('📝 Message updated event received:', data); + updateHistory(); + setPromptHistory(prev => [...prev]); + }; + + inlineToolApprovalManager.on('request-confirmation', handleRequestConfirmation); + inlineToolApprovalManager.on('update-confirmation', handleUpdateConfirmation); + inlineToolApprovalManager.on('message-updated', handleMessageUpdated); + + return () => { + inlineToolApprovalManager.removeListener('request-confirmation', handleRequestConfirmation); + inlineToolApprovalManager.removeListener('update-confirmation', handleUpdateConfirmation); + inlineToolApprovalManager.removeListener('message-updated', handleMessageUpdated); + }; + }, [updateHistory]); + const handleOperationSuccess = React.useCallback( (response: any) => { // Add the response to the conversation @@ -500,6 +536,67 @@ export default function AIPrompt(props: { } } + // Function to handle tool retry + const handleRetryTool = React.useCallback( + async (toolName: string, args: Record) => { + if (!aiManager) { + console.error('Cannot retry tool: aiManager not available'); + return; + } + + try { + console.log(`Retrying tool ${toolName} with args:`, args); + + // Get the tool manager from the LangChain manager + const toolManager = (aiManager as any).toolManager; + if (!toolManager) { + console.error('Cannot retry tool: toolManager not available'); + return; + } + + // Execute the tool directly + const toolResponse = await toolManager.executeTool(toolName, args); + + // Add the retry result to the conversation history + const retryPrompt: Prompt = { + role: 'tool', + content: toolResponse.content, + toolCallId: `retry-${Date.now()}`, + name: toolName, + }; + + aiManager.history.push(retryPrompt); + updateHistory(); + + // If the tool should process follow-up, trigger that + if (toolResponse.shouldProcessFollowUp) { + await aiManager.processToolResponses(); + updateHistory(); + } + + } catch (error) { + console.error(`Error retrying tool ${toolName}:`, error); + + // Add error to conversation + const errorPrompt: Prompt = { + role: 'tool', + content: JSON.stringify({ + error: true, + message: `Failed to retry tool: ${error instanceof Error ? error.message : 'Unknown error'}`, + toolName, + }), + toolCallId: `retry-error-${Date.now()}`, + name: toolName, + error: true, + }; + + aiManager.history.push(errorPrompt); + updateHistory(); + } + }, + [aiManager, updateHistory] + ); + // Function to stop the current request const handleStopRequest = () => { if (aiManager && loading) { @@ -711,6 +808,9 @@ export default function AIPrompt(props: { height: '100vh', display: 'flex', flexDirection: 'column', + overflow: 'hidden', // Prevent horizontal overflow + maxWidth: '100%', + minWidth: 0, }} > { AnalyzeResourceBasedOnPrompt(prompt).catch(error => { setApiError(error.message); @@ -786,7 +894,7 @@ export default function AIPrompt(props: { updateHistory(); } // Clear tool approval session when history is cleared - toolApprovalManager.clearSession(); + inlineToolApprovalManager.clearSession(); }} onConfigChange={(config, model) => { setActiveConfig(config); @@ -794,27 +902,16 @@ export default function AIPrompt(props: { handleChangeConfig(config, model); }} onTestModeResponse={handleTestModeResponse} + onToolsChange={(newEnabledTools) => { + setEnabledTools(newEnabledTools); + // Recreate AI manager with new tools + handleChangeConfig(activeConfig, selectedModel); + }} /> - {/* Tool Approval Dialog */} - ({ - id: tool.id, - name: tool.name, - description: tool.description, - arguments: tool.arguments, - type: tool.type - })) || []} - onApprove={toolApproval.handleApprove} - onDeny={toolApproval.handleDeny} - onClose={toolApproval.handleClose} - loading={toolApproval.isProcessing} - /> - {/* Editor Dialog */} {!isDelete && showEditor && ( Date: Sun, 21 Sep 2025 16:28:39 +0530 Subject: [PATCH 13/39] ai-plugin: Enhance LangChain manager with advanced tool processing - Add comprehensive tool response processing and validation - Implement intelligent message preparation for tool contexts - Add specialized system prompts for tool response handling - Improve tool call alignment validation and error handling - Add content size management to prevent memory issues - Enhance user context building with conversation history and tool results - Support multiple provider compatibility (Azure, Anthropic, etc.) Signed-off-by: ashu8912 --- .../src/langchain/LangChainManager.ts | 370 +++++++++++++++++- 1 file changed, 354 insertions(+), 16 deletions(-) diff --git a/ai-assistant/src/langchain/LangChainManager.ts b/ai-assistant/src/langchain/LangChainManager.ts index ba139d19bc..d488c2d91c 100644 --- a/ai-assistant/src/langchain/LangChainManager.ts +++ b/ai-assistant/src/langchain/LangChainManager.ts @@ -18,9 +18,11 @@ import { AzureChatOpenAI, ChatOpenAI } from '@langchain/openai'; import sanitizeHtml from 'sanitize-html'; import AIManager, { Prompt } from '../ai/manager'; import { basePrompt } from '../ai/prompts'; +import { MCPArgumentProcessor, UserContext } from '../components/mcpOutput/MCPArgumentProcessor'; +import { inlineToolApprovalManager } from '../utils/InlineToolApprovalManager'; +import { ToolCall } from '../utils/ToolApprovalManager'; import { apiErrorPromptTemplate, toolFailurePromptTemplate } from './PromptTemplates'; import { KubernetesToolContext, ToolManager } from './tools'; -import { toolApprovalManager, ToolCall } from '../utils/ToolApprovalManager'; export default class LangChainManager extends AIManager { private model: BaseChatModel; @@ -40,12 +42,31 @@ export default class LangChainManager extends AIManager { 'AI Assistant: Initializing with enabled tools:', enabledToolIds || 'all tools enabled' ); - this.toolManager = new ToolManager(enabledToolIds); // Only enabled tools + this.toolManager = new ToolManager(undefined, enabledToolIds); // Only enabled tools this.model = this.createModel(providerId, config); // Initialize prompt template and output parser this.promptTemplate = this.createPromptTemplate(); this.outputParser = new StringOutputParser(); + + // Set up event listeners for inline tool confirmations + this.setupToolConfirmationListeners(); + } + + // Set up event listeners for tool confirmation events + private setupToolConfirmationListeners() { + inlineToolApprovalManager.on('request-confirmation', (data: any) => { + // Add the tool confirmation message to chat history + console.log('🔔 LangChainManager: Adding tool confirmation message to history', data); + this.addToolConfirmationMessage('', data.toolConfirmation); + console.log('📝 LangChainManager: History length after adding confirmation:', this.history.length); + }); + + inlineToolApprovalManager.on('update-confirmation', (data: any) => { + // Update the specific tool confirmation message with new state (e.g., loading) + console.log('🔄 Tool confirmation update:', data.requestId, 'loading:', data.toolConfirmation.loading); + this.updateToolConfirmationMessage(data.requestId, data.toolConfirmation); + }); } // Helper method to extract text content from different response formats @@ -187,6 +208,11 @@ export default class LangChainManager extends AIManager { providerId: this.providerId, }); + // Wait for MCP tools to be initialized + console.log('⏳ Waiting for MCP tools initialization...'); + await this.toolManager.waitForMCPToolsInitialization(); + console.log('✅ MCP tools initialization complete'); + // Configure the Kubernetes context for the KubernetesTool this.toolManager.configureKubernetesContext(kubernetesContext); @@ -218,6 +244,52 @@ export default class LangChainManager extends AIManager { return ['openai', 'azure', 'anthropic', 'mistral', 'gemini'].includes(this.providerId); } + /** + * Build user context from current conversation and state + */ + private buildUserContext(): UserContext { + // Get the most recent user message + const recentUserMessages = this.history + .filter(prompt => prompt.role === 'user') + .slice(-3); // Last 3 user messages for context + + const userMessage = recentUserMessages.length > 0 + ? recentUserMessages[recentUserMessages.length - 1].content + : ''; + + // Build conversation history + const conversationHistory = this.history + .slice(-10) // Last 10 messages + .map(prompt => ({ + role: prompt.role, + content: prompt.content + })); + + // Get recent tool results + const lastToolResults: Record = {}; + const recentToolResponses = this.history + .filter(prompt => prompt.role === 'tool') + .slice(-5); // Last 5 tool responses + + recentToolResponses.forEach(response => { + if (response.name) { + try { + const parsed = JSON.parse(response.content); + lastToolResults[response.name] = parsed; + } catch { + lastToolResults[response.name] = response.content; + } + } + }); + + return { + userMessage, + conversationHistory, + lastToolResults, + timeContext: new Date() + }; + } + /** * Get description for a tool (for approval dialog) */ @@ -244,6 +316,50 @@ export default class LangChainManager extends AIManager { } } + /** + * Add a tool confirmation message to the history + */ + public addToolConfirmationMessage(content: string, toolConfirmation: any, updateHistoryCallback?: () => void): void { + console.log('➕ LangChainManager: Adding tool confirmation message', { content, toolConfirmation }); + const confirmationPrompt: Prompt = { + role: 'assistant', + content: content, + toolConfirmation: toolConfirmation, + isDisplayOnly: true, // Don't send to LLM + requestId: toolConfirmation.requestId // Add requestId for tracking + }; + this.history.push(confirmationPrompt); + console.log('📚 LangChainManager: History after adding confirmation:', this.history.length, 'items'); + + // Call the update callback if provided to trigger UI re-render + if (updateHistoryCallback) { + updateHistoryCallback(); + } + } + + public updateToolConfirmationMessage(requestId: string, updatedToolConfirmation: any): void { + console.log('🔄 LangChainManager: Updating tool confirmation message', { requestId, updatedToolConfirmation }); + + // Find the message with matching requestId + const messageIndex = this.history.findIndex( + prompt => prompt.requestId === requestId && prompt.toolConfirmation + ); + + if (messageIndex !== -1) { + // Update the tool confirmation in the existing message + this.history[messageIndex] = { + ...this.history[messageIndex], + toolConfirmation: updatedToolConfirmation + }; + console.log('✅ LangChainManager: Tool confirmation message updated'); + + // Use the inline tool approval manager to emit update event + inlineToolApprovalManager.emit('message-updated', { requestId, updatedToolConfirmation }); + } else { + console.warn('⚠️ LangChainManager: Could not find tool confirmation message to update'); + } + } + // Helper method to prepare chat history for prompt template private prepareChatHistory(): BaseMessage[] { // Filter out system messages and display-only messages to avoid conflicts with the system message in the prompt template @@ -256,6 +372,57 @@ export default class LangChainManager extends AIManager { // Helper method to create system prompt with context private createSystemPrompt(): string { let systemPromptContent = basePrompt; + + // Add MCP tool guidance if we have MCP tools available + const mcpTools = this.toolManager.getMCPTools(); + if (mcpTools.length > 0) { + systemPromptContent += ` + +MCP TOOL GUIDANCE: +You have access to advanced debugging and monitoring tools through MCP (Model Context Protocol). When users request system analysis, monitoring, or debugging: + +INTELLIGENT PARAMETER SETTING: +- When calling MCP tools, intelligently populate parameters based on the user's request and the current context +- For duration parameters: Use reasonable defaults (30 seconds for quick checks, 60-300 seconds for monitoring, 0 for continuous) +- For namespace parameters: Use the current namespace from context, or "default" if not specified +- For filtering parameters: Extract relevant filters from the user's request (pod names, labels, etc.) +- For params objects: Populate with relevant Kubernetes selectors based on context + +COMMON MCP TOOL PATTERNS: +- Gadget tools with "snapshot" are for one-time data collection +- Gadget tools with "trace" are for monitoring over time +- Duration 0 means continuous monitoring (use sparingly) +- Always populate the required "params" object, even if empty: {"params": {}} + +PARAMETER EXAMPLES: +- For namespace-specific requests: {"params": {"operator.KubeManager.namespace": "target-namespace"}} +- For pod-specific requests: {"params": {"operator.KubeManager.podname": "pod-name"}} +- For monitoring duration: {"duration": 30, "params": {"operator.KubeManager.namespace": "default"}} +- For continuous monitoring: {"duration": 0, "params": {...}} + +CONTEXT-AWARE PARAMETER EXTRACTION: +- Extract pod names, namespaces, and labels from the user's request +- Use current cluster context when available +- Default to "default" namespace if not specified +- Apply appropriate filters based on the user's intent + +RESULT INTERPRETATION AND PRESENTATION: +When MCP tools return data, you MUST: +1. **Analyze and summarize** - Don't just show raw JSON data +2. **Identify patterns** - Group similar items, highlight anomalies +3. **Format clearly** - Use tables, lists, or structured presentation +4. **Focus on insights** - Explain what the data means, not just what it contains +5. **Highlight issues** - Point out potential security or performance problems + +EXAMPLE result processing: +- Socket data → "Found X active connections, Y listening ports, Z external connections" +- Process data → "Identified N processes, M high CPU consumers" +- Network traces → "Detected traffic patterns: internal vs external, protocols used" +- Performance data → "Key metrics: CPU usage X%, memory Y%, network Z Mbps" + +NEVER just dump raw JSON - always interpret and present meaningfully.`; + } + if (this.currentContext) { systemPromptContent += `\n\nCURRENT CONTEXT:\n${this.currentContext}`; } @@ -520,30 +687,74 @@ The user is waiting for you to explain what the tools discovered. Provide a dire }; this.history.push(assistantPrompt); - // Prepare tool calls for approval - const toolCallsForApproval: ToolCall[] = toolCalls.map(tc => { + // Prepare tool calls for approval with intelligent argument processing + const toolCallsForApproval: ToolCall[] = await Promise.all(toolCalls.map(async tc => { const toolName = tc.function.name; const mcpTools = this.toolManager.getMCPTools(); const isMCPTool = mcpTools.some(tool => tool.name === toolName); + let processedArguments = JSON.parse(tc.function.arguments); + + // Use AI to enhance arguments for MCP tools + if (isMCPTool) { + try { + const toolSchema = await MCPArgumentProcessor.getToolSchema(toolName); + if (toolSchema) { + // Build user context from current conversation + const userContext = this.buildUserContext(); + + // Use AI to intelligently prepare arguments + processedArguments = await this.enhanceArgumentsWithAI( + toolName, + toolSchema, + userContext, + processedArguments + ); + + console.log(`🧠 AI-enhanced arguments for ${toolName}:`, { + original: JSON.parse(tc.function.arguments), + enhanced: processedArguments + }); + } + } catch (error) { + console.warn(`Failed to enhance arguments for ${toolName}:`, error); + // Fall back to original arguments + } + } return { id: tc.id, name: toolName, description: this.getToolDescription(toolName, isMCPTool), - arguments: JSON.parse(tc.function.arguments), + arguments: processedArguments, type: isMCPTool ? 'mcp' : 'regular' }; - }); + })); try { - // Request approval for tool execution + // Request approval for tool execution using inline approval system console.log('🔐 Requesting approval for', toolCallsForApproval.length, 'tools'); - const approvedToolIds = await toolApprovalManager.requestApproval(toolCallsForApproval); + const approvedToolIds = await inlineToolApprovalManager.requestApproval( + toolCallsForApproval, + this // Pass the AI manager instance + ); console.log('✅ Tools approved:', approvedToolIds.length, 'of', toolCallsForApproval.length); - // Filter tool calls to only execute approved ones - const approvedToolCalls = toolCalls.filter(tc => approvedToolIds.includes(tc.id)); + // Filter tool calls to only execute approved ones and update with processed arguments + const approvedToolCalls = toolCalls.filter(tc => approvedToolIds.includes(tc.id)).map(tc => { + // Find the processed arguments from the approval data + const approvalData = toolCallsForApproval.find(approval => approval.id === tc.id); + if (approvalData) { + return { + ...tc, + function: { + ...tc.function, + arguments: JSON.stringify(approvalData.arguments) + } + }; + } + return tc; + }); const deniedToolCalls = toolCalls.filter(tc => !approvedToolIds.includes(tc.id)); // Add denied tool responses to history @@ -1047,7 +1258,7 @@ Format your response to make the errors prominent and actionable.`, }); } - let content = prompt.content; + const content = prompt.content; // Check response size after optimization handling const responseSize = content.length; @@ -1100,16 +1311,47 @@ Format your response to make the errors prominent and actionable.`, })); // Create a fallback response based on tool results - let fallbackContent = 'I executed the requested tools and here are the results:\n\n'; - + // For MCP tools with formatted output, return them directly without prefix + if (recentToolResponses.length === 1) { + const singleResponse = recentToolResponses[0]; + try { + const parsed = JSON.parse(singleResponse.content); + if (parsed.formatted && parsed.mcpOutput) { + // This is a formatted MCP output, return it directly + const assistantPrompt: Prompt = { + role: 'assistant', + content: singleResponse.content, + toolCalls: [], + }; + + // Clean up history to prevent message order issues + const lastAssistantWithToolsIndex = this.findLastAssistantWithTools(); + if (lastAssistantWithToolsIndex >= 0) { + this.history = this.history.slice(0, lastAssistantWithToolsIndex + 1); + } + + this.history.push(assistantPrompt); + return assistantPrompt; + } + } catch (e) { + // Not formatted MCP output, continue with fallback + } + } + + // Standard fallback for multiple tools or non-MCP tools + let fallbackContent = ''; + recentToolResponses.forEach((toolResponse, index) => { const toolName = toolResponse.name || 'tool'; let content = toolResponse.content; - + // Try to parse and clean up the content try { const parsed = JSON.parse(content); - if (parsed.error) { + if (parsed.formatted && parsed.mcpOutput) { + // For formatted MCP outputs, return the JSON directly + content = toolResponse.content; + } else if (parsed.error) { content = `Error: ${parsed.message || 'Tool execution failed'}`; } else if (parsed.userFriendlyMessage) { content = parsed.userFriendlyMessage; @@ -1121,7 +1363,13 @@ Format your response to make the errors prominent and actionable.`, content = content.toString().trim(); } - fallbackContent += `**${toolName}:**\n${content}\n\n`; + // For single formatted MCP output, return just the content + if (recentToolResponses.length === 1) { + fallbackContent = content; + } else { + // For multiple tools, use the tool name format + fallbackContent += `${toolName}: ${content}${index < recentToolResponses.length - 1 ? '\n\n' : ''}`; + } }); const assistantPrompt: Prompt = { @@ -1283,4 +1531,94 @@ Format your response to make the errors prominent and actionable.`, : JSON.stringify({ error: true, message: 'Content could not be sanitized' }); } } + + /** + * Enhance arguments using AI-like intelligence + */ + private async enhanceArgumentsWithAI( + _toolName: string, + toolSchema: any, + userContext: UserContext, + originalArgs: Record + ): Promise> { + const enhanced = { ...originalArgs }; + + if (!toolSchema.inputSchema?.properties) { + return enhanced; + } + + const properties = toolSchema.inputSchema.properties; + const required = toolSchema.inputSchema.required || []; + + // Fill in required fields that are missing or empty + for (const [fieldName, fieldSchema] of Object.entries(properties)) { + const isRequired = required.includes(fieldName); + const currentValue = enhanced[fieldName]; + + if (isRequired && (currentValue === undefined || currentValue === null || currentValue === '')) { + // Provide intelligent defaults based on field type and context + enhanced[fieldName] = this.getIntelligentDefault(fieldName, fieldSchema, userContext); + } + } + + return enhanced; + } + + /** + * Get intelligent default value for a field based on context + */ + private getIntelligentDefault(fieldName: string, fieldSchema: any, userContext: UserContext): any { + const fieldType = fieldSchema.type; + const fieldNameLower = fieldName.toLowerCase(); + + // Try to extract from user context first + if (userContext.userMessage) { + const userMessage = userContext.userMessage.toLowerCase(); + + // Extract namespace + if (fieldNameLower.includes('namespace')) { + const namespaceMatch = userMessage.match(/namespace[\s:]+([a-zA-Z0-9-_.]+)/i); + if (namespaceMatch) { + return namespaceMatch[1]; + } + return 'default'; // Default Kubernetes namespace + } + + // Extract container/pod names + if (fieldNameLower.includes('container') || fieldNameLower.includes('pod')) { + const containerMatch = userMessage.match(/(?:container|pod)[\s:]+([a-zA-Z0-9-_.]+)/i); + if (containerMatch) { + return containerMatch[1]; + } + } + + // Extract commands + if (fieldNameLower.includes('command') || fieldNameLower.includes('cmd')) { + const commandMatch = userMessage.match(/(?:run|execute|command)[\s:]+["']([^"']+)["']/i); + if (commandMatch) { + return commandMatch[1]; + } + } + } + + // Fallback to type-based defaults + switch (fieldType) { + case 'object': + return {}; + case 'array': + return []; + case 'string': + if (fieldSchema.enum) { + return fieldSchema.enum[0]; + } + return fieldSchema.default || ''; + case 'number': + case 'integer': + return fieldSchema.default || fieldSchema.minimum || 0; + case 'boolean': + return fieldSchema.default !== undefined ? fieldSchema.default : false; + default: + return null; + } + } } From f062894ba4ba5c83ae643f6f3354ff397410ddda Mon Sep 17 00:00:00 2001 From: ashu8912 Date: Sun, 21 Sep 2025 16:28:54 +0530 Subject: [PATCH 14/39] ai-plugin: Add advanced MCP configuration editor dialog - Add MCPConfigEditorDialog with Monaco Editor integration - Provide syntax highlighting and auto-formatting for JSON configurations - Include tabbed interface with Configuration Editor and Schema Documentation - Add real-time validation with clear error messages - Support for environment variables in server configuration - Include comprehensive schema documentation with field descriptions - Add Load Example and Reset functionality for easy configuration management Signed-off-by: ashu8912 --- .../settings/MCPConfigEditorDialog.tsx | 341 ++++++++++++++++++ 1 file changed, 341 insertions(+) create mode 100644 ai-assistant/src/components/settings/MCPConfigEditorDialog.tsx diff --git a/ai-assistant/src/components/settings/MCPConfigEditorDialog.tsx b/ai-assistant/src/components/settings/MCPConfigEditorDialog.tsx new file mode 100644 index 0000000000..26f822abe2 --- /dev/null +++ b/ai-assistant/src/components/settings/MCPConfigEditorDialog.tsx @@ -0,0 +1,341 @@ +import { Icon } from '@iconify/react'; +import { Dialog } from '@kinvolk/headlamp-plugin/lib/CommonComponents'; +import Editor from '@monaco-editor/react'; +import { + Alert, + Box, + Button, + DialogActions, + DialogContent, + DialogTitle, + Paper, + Tab, + Tabs, + Typography, +} from '@mui/material'; +import React, { useEffect, useState } from 'react'; + +export interface MCPServer { + name: string; + command: string; + args: string[]; + env?: Record; + enabled: boolean; +} + +export interface MCPConfig { + enabled: boolean; + servers: MCPServer[]; +} + +interface MCPConfigEditorDialogProps { + open: boolean; + onClose: () => void; + config: MCPConfig; + onSave: (config: MCPConfig) => void; +} + +export default function MCPConfigEditorDialog({ + open, + onClose, + config, + onSave, +}: MCPConfigEditorDialogProps) { + const [content, setContent] = useState(''); + const [validationError, setValidationError] = useState(''); + const [tabValue, setTabValue] = useState(0); + const themeName = localStorage.getItem('headlampThemePreference'); + + useEffect(() => { + if (open) { + setContent(JSON.stringify(config, null, 2)); + setValidationError(''); + setTabValue(0); + } + }, [config, open]); + + const handleEditorChange = (value: string | undefined) => { + if (value !== undefined) { + setContent(value); + setValidationError(''); + } + }; + + const validateConfig = (configToValidate: any): string | null => { + if (typeof configToValidate.enabled !== 'boolean') { + return 'enabled field must be a boolean'; + } + + if (!Array.isArray(configToValidate.servers)) { + return 'servers field must be an array'; + } + + for (let i = 0; i < configToValidate.servers.length; i++) { + const server = configToValidate.servers[i]; + + if (typeof server.name !== 'string' || !server.name.trim()) { + return `Server ${i + 1}: name must be a non-empty string`; + } + + if (typeof server.command !== 'string' || !server.command.trim()) { + return `Server ${i + 1}: command must be a non-empty string`; + } + + if (!Array.isArray(server.args)) { + return `Server ${i + 1}: args must be an array`; + } + + if (server.env !== undefined) { + if (typeof server.env !== 'object' || server.env === null || Array.isArray(server.env)) { + return `Server ${i + 1}: env must be an object with string key-value pairs`; + } + + for (const [key, value] of Object.entries(server.env)) { + if (typeof key !== 'string' || typeof value !== 'string') { + return `Server ${i + 1}: env must contain only string key-value pairs`; + } + } + } + + if (typeof server.enabled !== 'boolean') { + return `Server ${i + 1}: enabled must be a boolean`; + } + } + + return null; + }; + + const handleSave = () => { + try { + const parsedConfig = JSON.parse(content); + + const error = validateConfig(parsedConfig); + if (error) { + setValidationError(error); + return; + } + + onSave(parsedConfig); + onClose(); + } catch (error) { + setValidationError(error instanceof Error ? error.message : 'Invalid JSON configuration'); + } + }; + + const handleLoadExample = () => { + const exampleConfig: MCPConfig = { + enabled: true, + servers: [ + { + name: "inspektor-gadget", + command: "docker", + args: [ + "mcp", + "gateway", + "run" + ], + enabled: true + }, + { + name: "flux-mcp", + command: "flux-operator-mcp", + args: [ + "serve" + ], + env: { + "KUBECONFIG": "/Users/ashughildiyal/.kube/config" + }, + enabled: true + } + ] + }; + + setContent(JSON.stringify(exampleConfig, null, 2)); + setValidationError(''); + }; + + const handleReset = () => { + setContent(JSON.stringify(config, null, 2)); + setValidationError(''); + }; + + const handleTabChange = (_event: React.SyntheticEvent, newValue: number) => { + setTabValue(newValue); + }; + + const getSchemaDocumentation = () => { + return { + enabled: "boolean - Enable/disable all MCP servers", + servers: [ + { + name: "string - Unique server name", + command: "string - Executable command or path", + args: ["array of strings - Command arguments"], + env: { + "KEY": "string value - Environment variables (optional)" + }, + enabled: "boolean - Enable/disable this specific server" + } + ] + }; + }; + + return ( + + + + Edit MCP Configuration + + + + + + + + + + + + + + {tabValue === 0 && ( + + {validationError && ( + + {validationError} + + )} + + + + + Edit the JSON configuration above. The editor will automatically format and validate your configuration. + + + )} + + {tabValue === 1 && ( + + + + Configuration Schema + + + + + The MCP configuration defines how your AI assistant connects to external tools and services. + Each server represents a separate MCP server that provides specific capabilities. + + + + +
+                  {JSON.stringify(getSchemaDocumentation(), null, 2)}
+                
+
+ + + + Field Descriptions: + + +
  • + + enabled: Master switch to enable/disable all MCP servers + +
  • +
  • + + servers: Array of MCP server configurations + +
  • +
  • + + name: Unique identifier for the server + +
  • +
  • + + command: The executable to run (e.g., "docker", "npx", "python") + +
  • +
  • + + args: Command-line arguments passed to the executable + +
  • +
  • + + env: Optional environment variables for the server process + +
  • +
  • + + enabled: Toggle individual server on/off without removing configuration + +
  • +
    +
    +
    +
    + )} +
    + + + + + +
    + ); +} \ No newline at end of file From 303e2a9dd63346a2d893c1ea8f0f28a585fe11ef Mon Sep 17 00:00:00 2001 From: ashu8912 Date: Sun, 21 Sep 2025 16:29:09 +0530 Subject: [PATCH 15/39] ai-plugin: Enhance MCP settings with editor dialog integration - Replace plain textarea with professional editor dialog - Add MCPServer interface support for environment variables - Update example configuration with modern MCP server setups - Add configuration summary with server count and status display - Integrate MCPConfigEditorDialog for better user experience - Show detailed server list with enabled/disabled status - Remove complex JSON validation in favor of editor dialog approach Signed-off-by: ashu8912 --- .../src/components/settings/MCPSettings.tsx | 241 +++++------------- 1 file changed, 66 insertions(+), 175 deletions(-) diff --git a/ai-assistant/src/components/settings/MCPSettings.tsx b/ai-assistant/src/components/settings/MCPSettings.tsx index e375c84b56..24b6292158 100644 --- a/ai-assistant/src/components/settings/MCPSettings.tsx +++ b/ai-assistant/src/components/settings/MCPSettings.tsx @@ -1,16 +1,15 @@ +import { Icon } from '@iconify/react'; import { SectionBox } from '@kinvolk/headlamp-plugin/lib/components/common'; import { Box, Button, FormControlLabel, Switch, - TextField, Typography, - Alert, } from '@mui/material'; -import { Icon } from '@iconify/react'; import React, { useEffect, useState } from 'react'; import { pluginStore } from '../../utils'; +import MCPConfigEditorDialog from './MCPConfigEditorDialog'; // Helper function to check if running in Electron const isElectron = (): boolean => { @@ -23,6 +22,7 @@ export interface MCPServer { name: string; command: string; args: string[]; + env?: Record; enabled: boolean; } @@ -36,13 +36,6 @@ interface MCPSettingsProps { onConfigChange?: (config: MCPConfig) => void; } -const defaultMCPServer: MCPServer = { - name: '', - command: 'npx', - args: [], - enabled: true, -}; - export function MCPSettings({ config, onConfigChange }: MCPSettingsProps) { const [mcpConfig, setMCPConfig] = useState( config || { @@ -51,8 +44,7 @@ export function MCPSettings({ config, onConfigChange }: MCPSettingsProps) { } ); - const [jsonConfig, setJsonConfig] = useState(''); - const [jsonError, setJsonError] = useState(''); + const [editorDialogOpen, setEditorDialogOpen] = useState(false); useEffect(() => { // Load MCP config from Electron if available @@ -67,10 +59,6 @@ export function MCPSettings({ config, onConfigChange }: MCPSettingsProps) { } }, []); - // Update JSON config when mcpConfig changes - useEffect(() => { - setJsonConfig(JSON.stringify(mcpConfig, null, 2)); - }, [mcpConfig]); const loadMCPConfigFromElectron = async () => { if (!isElectron()) return; @@ -140,25 +128,25 @@ export function MCPSettings({ config, onConfigChange }: MCPSettingsProps) { name: 'inspektor-gadget', command: 'docker', args: [ - 'run', '-i', '--rm', - '--mount', 'type=bind,src=%USERPROFILE%\\.kube\\config,dst=/root/.kube/config,readonly', - '--mount', 'type=bind,src=%USERPROFILE%\\.minikube,dst=/root/.minikube,readonly', - 'ghcr.io/inspektor-gadget/ig-mcp-server:latest', - '-gadget-discoverer=artifacthub' + 'mcp', + 'gateway', + 'run' ], - enabled: false, // Disabled by default to avoid errors + enabled: true, }, { - name: 'filesystem', - command: 'npx', + name: 'flux-mcp', + command: 'flux-operator-mcp', args: [ - '-y', '@danielsuguimoto/readonly-server-filesystem', - 'C:\\Users\\' + (process.env.USERNAME || 'username') + '\\Desktop' + 'serve' ], + env: { + 'KUBECONFIG': '/Users/ashughildiyal/.kube/config' + }, enabled: true, } ]; - + newConfig.servers = defaultServers; } @@ -167,87 +155,16 @@ export function MCPSettings({ config, onConfigChange }: MCPSettingsProps) { - const handleJsonConfigChange = (value: string) => { - setJsonConfig(value); - setJsonError(''); - }; - - const validateAndApplyJsonConfig = () => { - try { - const parsedConfig = JSON.parse(jsonConfig) as MCPConfig; - - // Validate the structure - if (typeof parsedConfig.enabled !== 'boolean') { - throw new Error('enabled field must be a boolean'); - } - - if (!Array.isArray(parsedConfig.servers)) { - throw new Error('servers field must be an array'); - } - - // Validate each server - parsedConfig.servers.forEach((server, index) => { - if (typeof server.name !== 'string') { - throw new Error(`Server ${index}: name must be a string`); - } - if (typeof server.command !== 'string') { - throw new Error(`Server ${index}: command must be a string`); - } - if (!Array.isArray(server.args)) { - throw new Error(`Server ${index}: args must be an array`); - } - if (typeof server.enabled !== 'boolean') { - throw new Error(`Server ${index}: enabled must be a boolean`); - } - }); - - // Apply the config - handleConfigChange(parsedConfig); - setJsonError(''); - } catch (error) { - setJsonError(error instanceof Error ? error.message : 'Invalid JSON configuration'); - } - }; - - const resetJsonToCurrentConfig = () => { - setJsonConfig(JSON.stringify(mcpConfig, null, 2)); - setJsonError(''); + const handleOpenEditorDialog = () => { + setEditorDialogOpen(true); }; - const getExampleConfig = (): MCPConfig => { - return { - enabled: true, - servers: [ - { - name: "inspektor-gadget", - command: "docker", - args: [ - "run", "-i", "--rm", - "--mount", "type=bind,src=%USERPROFILE%\\.kube\\config,dst=/root/.kube/config,readonly", - "--mount", "type=bind,src=%USERPROFILE%\\.minikube,dst=/root/.minikube,readonly", - "ghcr.io/inspektor-gadget/ig-mcp-server:latest", - "-gadget-discoverer=artifacthub" - ], - enabled: false - }, - { - name: "filesystem", - command: "npx", - args: [ - "-y", "@danielsuguimoto/readonly-server-filesystem", - "C:\\Users\\username\\Desktop", - "C:\\Users\\username\\Documents" - ], - enabled: true - } - ] - }; + const handleCloseEditorDialog = () => { + setEditorDialogOpen(false); }; - const loadExampleConfig = () => { - const exampleConfig = getExampleConfig(); - setJsonConfig(JSON.stringify(exampleConfig, null, 2)); - setJsonError(''); + const handleSaveConfig = (newConfig: MCPConfig) => { + handleConfigChange(newConfig); }; // Only show MCP settings in Electron @@ -282,86 +199,60 @@ export function MCPSettings({ config, onConfigChange }: MCPSettingsProps) { {mcpConfig.enabled && ( <> - {/* JSON Configuration */} + {/* Configuration Summary */} - JSON Configuration Editor + Server Configuration - Edit your MCP servers configuration as JSON. This allows you to easily add, remove, - and modify multiple servers at once. + You have {mcpConfig.servers.length} server(s) configured. + {mcpConfig.servers.filter(s => s.enabled).length} server(s) are currently enabled. - - {jsonError && ( - - {jsonError} - - )} - - handleJsonConfigChange(e.target.value)} - multiline - rows={15} - fullWidth - variant="outlined" - sx={{ - mb: 2, - '& .MuiInputBase-input': { - fontFamily: 'monospace', - fontSize: '0.875rem', - } - }} - helperText="Edit the JSON configuration above. Make sure to keep the proper structure." - /> - - - - - - - {/* Schema Documentation */} - - - 📝 Configuration Schema: - - - {JSON.stringify({ - "enabled": "boolean - Enable/disable MCP servers", - "servers": [ - { - "name": "string - Unique server name", - "command": "string - Executable command", - "args": ["array of strings - Command arguments"], - "enabled": "boolean - Enable/disable this server" - } - ] - }, null, 2)} + + + + {/* Server List Summary */} + {mcpConfig.servers.length > 0 && ( + + + Configured Servers: + + {mcpConfig.servers.map((server, index) => ( +
  • + + {server.name} ({server.command}) - + + {server.enabled ? 'Enabled' : 'Disabled'} + + {server.env && ( + + (with env variables) + + )} + +
  • + ))} +
    + )} + + )} -
    - )} + {/* Editor Dialog */} + ); } From 0776921fea348199713c253ee0ef13dc399f5a2a Mon Sep 17 00:00:00 2001 From: ashu8912 Date: Sun, 21 Sep 2025 16:29:32 +0530 Subject: [PATCH 16/39] ai-plugin: Add utility enhancements and helper functions - Add isElectron utility function for environment detection - Enhance ToolConfigManager with configuration validation - Update AIInputSection with improved tool integration - Add electron client improvements for MCP communication - Include helper functions for better component integration Signed-off-by: ashu8912 --- ai-assistant/src/ai/mcp/electron-client.ts | 1 + .../components/assistant/AIInputSection.tsx | 28 +++++++++++++++++++ ai-assistant/src/utils.tsx | 16 +++++++++++ ai-assistant/src/utils/ToolConfigManager.ts | 16 +++++++++++ 4 files changed, 61 insertions(+) diff --git a/ai-assistant/src/ai/mcp/electron-client.ts b/ai-assistant/src/ai/mcp/electron-client.ts index e09f8510cb..d7394100e2 100644 --- a/ai-assistant/src/ai/mcp/electron-client.ts +++ b/ai-assistant/src/ai/mcp/electron-client.ts @@ -90,6 +90,7 @@ class ElectronMCPClient { } try { + console.log("args for tool executed is ", args) const response = await window.desktopApi!.mcp.executeTool(toolName, args, toolCallId); if (response.success) { diff --git a/ai-assistant/src/components/assistant/AIInputSection.tsx b/ai-assistant/src/components/assistant/AIInputSection.tsx index 2743eb1023..9f1c2ac79f 100644 --- a/ai-assistant/src/components/assistant/AIInputSection.tsx +++ b/ai-assistant/src/components/assistant/AIInputSection.tsx @@ -15,6 +15,7 @@ import { getProviderById } from '../../config/modelConfig'; import { getModelDisplayName, getProviderModelsForChat } from '../../utils/modalUtils'; import { StoredProviderConfig } from '../../utils/ProviderConfigManager'; import TestModeInput from './TestModeInput'; +import { ToolsDialog } from './ToolsDialog'; interface AIInputSectionProps { promptVal: string; @@ -24,11 +25,13 @@ interface AIInputSectionProps { activeConfig: StoredProviderConfig | null; availableConfigs: StoredProviderConfig[]; selectedModel: string; + enabledTools: string[]; onSend: (prompt: string) => void; onStop: () => void; onClearHistory: () => void; onConfigChange: (config: StoredProviderConfig, model: string) => void; onTestModeResponse: (content: string, type: 'assistant' | 'user', hasError?: boolean) => void; + onToolsChange: (enabledTools: string[]) => void; } export const AIInputSection: React.FC = ({ @@ -39,12 +42,15 @@ export const AIInputSection: React.FC = ({ activeConfig, availableConfigs, selectedModel, + enabledTools, onSend, onStop, onClearHistory, onConfigChange, onTestModeResponse, + onToolsChange, }) => { + const [showToolsDialog, setShowToolsDialog] = React.useState(false); const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); @@ -188,6 +194,20 @@ export const AIInputSection: React.FC = ({
    )} + + {/* Tools Button */} + {!isTestMode && ( + + setShowToolsDialog(true)} + icon="mdi:tools" + iconButtonProps={{ + size: 'small', + }} + /> + + )} @@ -214,6 +234,14 @@ export const AIInputSection: React.FC = ({ )} + + {/* Tools Dialog */} + setShowToolsDialog(false)} + enabledTools={enabledTools} + onToolsChange={onToolsChange} + />
    ); }; diff --git a/ai-assistant/src/utils.tsx b/ai-assistant/src/utils.tsx index bf5e242d51..1c1218616b 100644 --- a/ai-assistant/src/utils.tsx +++ b/ai-assistant/src/utils.tsx @@ -21,6 +21,9 @@ function usePluginSettings() { // Add state to control UI panel visibility - initialize from stored settings const [isUIPanelOpen, setIsUIPanelOpenState] = React.useState(conf?.isUIPanelOpen ?? false); + // Add state for enabled tools - initialize from stored settings + const [enabledTools, setEnabledToolsState] = React.useState(conf?.enabledTools ?? []); + // Wrap setIsUIPanelOpen to also update the stored configuration const setIsUIPanelOpen = (isOpen: boolean) => { setIsUIPanelOpenState(isOpen); @@ -32,6 +35,17 @@ function usePluginSettings() { }); }; + // Wrap setEnabledTools to also update the stored configuration + const setEnabledTools = (tools: string[]) => { + setEnabledToolsState(tools); + // Save the tools configuration + const currentConf = pluginStore.get() || {}; + pluginStore.update({ + ...currentConf, + enabledTools: tools, + }); + }; + return { event, setEvent, @@ -41,6 +55,8 @@ function usePluginSettings() { setActiveProvider, isUIPanelOpen, setIsUIPanelOpen, + enabledTools, + setEnabledTools, }; } diff --git a/ai-assistant/src/utils/ToolConfigManager.ts b/ai-assistant/src/utils/ToolConfigManager.ts index c5c189d2bd..2da84a63c2 100644 --- a/ai-assistant/src/utils/ToolConfigManager.ts +++ b/ai-assistant/src/utils/ToolConfigManager.ts @@ -52,3 +52,19 @@ export function getEnabledToolIds(pluginSettings: any): string[] { const allTools = getAllAvailableTools(); return allTools.map(tool => tool.id).filter(toolId => isToolEnabled(pluginSettings, toolId)); } + +// Sets the enabled tools list in plugin settings +export function setEnabledTools(pluginSettings: any, enabledToolIds: string[]): any { + const enabledTools: Record = {}; + + // Get all available tools and set their enabled state + const allTools = getAllAvailableTools(); + allTools.forEach(tool => { + enabledTools[tool.id] = enabledToolIds.includes(tool.id); + }); + + return { + ...pluginSettings, + enabledTools, + }; +} From e1a016f05c2e3092fe28c69d1b83806379f9b190 Mon Sep 17 00:00:00 2001 From: ashu8912 Date: Sun, 21 Sep 2025 16:33:32 +0530 Subject: [PATCH 17/39] ai-plugin: Run formatter Signed-off-by: ashu8912 --- ai-assistant/src/ai/mcp/client.ts | 32 +- ai-assistant/src/ai/mcp/electron-client.ts | 32 +- .../src/components/assistant/ToolsDialog.tsx | 104 +-- .../components/chat/MCPFormattedMessage.tsx | 155 ++-- .../common/InlineToolConfirmation.tsx | 165 ++-- .../components/common/ToolApprovalDialog.tsx | 118 ++- .../mcpOutput/MCPArgumentProcessor.ts | 49 +- .../components/mcpOutput/MCPOutputDisplay.tsx | 728 ++++++++++-------- .../src/components/mcpOutput/index.ts | 2 +- .../settings/MCPConfigEditorDialog.tsx | 81 +- .../src/components/settings/MCPSettings.tsx | 54 +- .../src/contexts/PromptWidthContext.tsx | 10 +- ai-assistant/src/hooks/useToolApproval.ts | 27 +- ai-assistant/src/index.tsx | 13 +- .../src/langchain/LangChainManager.ts | 235 +++--- .../formatters/MCPOutputFormatter.ts | 77 +- .../src/langchain/tools/ToolManager.ts | 221 +++--- ai-assistant/src/modal.tsx | 49 +- ai-assistant/src/textstream.tsx | 60 +- ai-assistant/src/types/electron.d.ts | 6 +- .../src/utils/InlineToolApprovalManager.ts | 39 +- ai-assistant/src/utils/ToolApprovalManager.ts | 14 +- ai-assistant/src/utils/ToolConfigManager.ts | 2 +- 23 files changed, 1217 insertions(+), 1056 deletions(-) diff --git a/ai-assistant/src/ai/mcp/client.ts b/ai-assistant/src/ai/mcp/client.ts index 8838b3af12..a8c298a0e4 100644 --- a/ai-assistant/src/ai/mcp/client.ts +++ b/ai-assistant/src/ai/mcp/client.ts @@ -1,4 +1,4 @@ -import { MultiServerMCPClient } from "@langchain/mcp-adapters"; +import { MultiServerMCPClient } from '@langchain/mcp-adapters'; const client = new MultiServerMCPClient({ // Global tool configuration options @@ -7,7 +7,7 @@ const client = new MultiServerMCPClient({ // Whether to prefix tool names with the server name (optional, default: false) prefixToolNameWithServerName: false, // Optional additional prefix for tool names (optional, default: "") - additionalToolNamePrefix: "", + additionalToolNamePrefix: '', // Use standardized content block format in tool outputs useStandardContentBlocks: true, @@ -15,17 +15,17 @@ const client = new MultiServerMCPClient({ // Server configuration mcpServers: { // adds the Inspektor Gadget MCP server - "inspektor-gadget": { - transport: "stdio", - command: "docker", + 'inspektor-gadget': { + transport: 'stdio', + command: 'docker', args: [ - "run", - "-i", - "--rm", - "--mount", - "type=bind,src=${env:HOME}/.kube/config,dst=/kubeconfig", - "ghcr.io/inspektor-gadget/ig-mcp-server:latest", - "-gadget-discoverer=artifacthub" + 'run', + '-i', + '--rm', + '--mount', + 'type=bind,src=${env:HOME}/.kube/config,dst=/kubeconfig', + 'ghcr.io/inspektor-gadget/ig-mcp-server:latest', + '-gadget-discoverer=artifacthub', ], // Restart configuration for stdio transport restart: { @@ -34,11 +34,11 @@ const client = new MultiServerMCPClient({ delayMs: 2000, // Slightly longer delay for Docker container startup }, }, - } + }, }); -const tools = async function() { - return await client.getTools(); +const tools = async function () { + return await client.getTools(); }; -export default tools; \ No newline at end of file +export default tools; diff --git a/ai-assistant/src/ai/mcp/electron-client.ts b/ai-assistant/src/ai/mcp/electron-client.ts index d7394100e2..1c741a5cda 100644 --- a/ai-assistant/src/ai/mcp/electron-client.ts +++ b/ai-assistant/src/ai/mcp/electron-client.ts @@ -17,7 +17,11 @@ interface MCPResponse { interface ElectronMCPApi { getTools: () => Promise; - executeTool: (toolName: string, args: Record, toolCallId?: string) => Promise; + executeTool: ( + toolName: string, + args: Record, + toolCallId?: string + ) => Promise; getStatus: () => Promise<{ isInitialized: boolean; hasClient: boolean }>; resetClient: () => Promise; getConfig: () => Promise<{ success: boolean; config?: any; error?: string }>; @@ -36,9 +40,10 @@ class ElectronMCPClient { private isElectron: boolean; constructor() { - this.isElectron = typeof window !== 'undefined' && - typeof window.desktopApi !== 'undefined' && - typeof window.desktopApi.mcp !== 'undefined'; + this.isElectron = + typeof window !== 'undefined' && + typeof window.desktopApi !== 'undefined' && + typeof window.desktopApi.mcp !== 'undefined'; } /** @@ -59,11 +64,8 @@ class ElectronMCPClient { try { const response = await window.desktopApi!.mcp.getTools(); - console.log( - "mcp response from getting tools is", - response - ); - console.log("mcp window desktop api", window.desktopApi!.mcp.getTools) + console.log('mcp response from getting tools is', response); + console.log('mcp window desktop api', window.desktopApi!.mcp.getTools); if (response.success && response.tools) { console.log('Retrieved MCP tools from Electron:', response.tools.length, 'tools'); return response.tools; @@ -81,8 +83,8 @@ class ElectronMCPClient { * Execute an MCP tool via Electron main process */ async executeTool( - toolName: string, - args: Record, + toolName: string, + args: Record, toolCallId?: string ): Promise { if (!this.isElectron) { @@ -90,9 +92,9 @@ class ElectronMCPClient { } try { - console.log("args for tool executed is ", args) + console.log('args for tool executed is ', args); const response = await window.desktopApi!.mcp.executeTool(toolName, args, toolCallId); - + if (response.success) { return response.result; } else { @@ -139,9 +141,9 @@ class ElectronMCPClient { } // Export a function that returns tools (compatible with existing interface) -const tools = async function(): Promise { +const tools = async function (): Promise { const client = new ElectronMCPClient(); - console.log("mcp electron client is ", client); + console.log('mcp electron client is ', client); return await client.getTools(); }; diff --git a/ai-assistant/src/components/assistant/ToolsDialog.tsx b/ai-assistant/src/components/assistant/ToolsDialog.tsx index 3c35dc3a92..0cfad14e9f 100644 --- a/ai-assistant/src/components/assistant/ToolsDialog.tsx +++ b/ai-assistant/src/components/assistant/ToolsDialog.tsx @@ -16,7 +16,7 @@ import { Switch, Typography, } from '@mui/material'; -import React, { useEffect,useState } from 'react'; +import React, { useEffect, useState } from 'react'; import tools from '../../ai/mcp/electron-client'; import { AVAILABLE_TOOLS } from '../../langchain/tools/registry'; @@ -69,10 +69,8 @@ export const ToolsDialog: React.FC = ({ }; const handleToggleTool = (toolName: string) => { - setLocalEnabledTools(prev => - prev.includes(toolName) - ? prev.filter(name => name !== toolName) - : [...prev, toolName] + setLocalEnabledTools(prev => + prev.includes(toolName) ? prev.filter(name => name !== toolName) : [...prev, toolName] ); }; @@ -106,7 +104,7 @@ export const ToolsDialog: React.FC = ({ External Model Context Protocol tools (always enabled)
    - + {loadingMcpTools ? ( @@ -119,29 +117,24 @@ export const ToolsDialog: React.FC = ({ {mcpTools.length > 0 ? ( mcpTools.map((tool, index) => ( - - + - - {tool.name} - - + {tool.name} + } secondary={tool.description || 'External MCP tool'} @@ -195,7 +188,7 @@ export const ToolsDialog: React.FC = ({ )}
    - + {tools.map(ToolClass => { const tempTool = new ToolClass(); @@ -205,10 +198,7 @@ export const ToolsDialog: React.FC = ({ return ( - + = ({ {tempTool.config.displayName || toolName} {tempTool.config.category && ( - + )}
    } @@ -243,67 +229,57 @@ export const ToolsDialog: React.FC = ({ ); return ( - - - Manage Tools - - + Manage Tools + - + - Enable or disable tools that the AI can use. Changes will take effect immediately and will be saved to your settings. + Enable or disable tools that the AI can use. Changes will take effect immediately and will + be saved to your settings. {/* Kubernetes Tools */} {kubernetesTools.length > 0 && ( <> {renderToolList( - kubernetesTools, - "Kubernetes Tools", - "Tools for interacting with Kubernetes clusters" + kubernetesTools, + 'Kubernetes Tools', + 'Tools for interacting with Kubernetes clusters' )} )} {/* Other Tools */} - {otherTools.length > 0 && ( + {otherTools.length > 0 && renderToolList( - otherTools, - "System Tools", - "General purpose tools for various operations" - ) - )} + otherTools, + 'System Tools', + 'General purpose tools for various operations' + )} {/* MCP Tools */} {renderMcpToolList()} - - {(kubernetesTools.length > 0 || otherTools.length > 0) && ( - - )} + + {(kubernetesTools.length > 0 || otherTools.length > 0) && } - + @@ -312,4 +288,4 @@ export const ToolsDialog: React.FC = ({ ); }; -export default ToolsDialog; \ No newline at end of file +export default ToolsDialog; diff --git a/ai-assistant/src/components/chat/MCPFormattedMessage.tsx b/ai-assistant/src/components/chat/MCPFormattedMessage.tsx index d65e5626e2..233478590b 100644 --- a/ai-assistant/src/components/chat/MCPFormattedMessage.tsx +++ b/ai-assistant/src/components/chat/MCPFormattedMessage.tsx @@ -24,7 +24,7 @@ const MCPFormattedMessage: React.FC = ({ onRetryTool, }) => { const widthContext = usePromptWidth(); - console.log("From context width ", widthContext.promptWidth) + console.log('From context width ', widthContext.promptWidth); // Try to parse the content as formatted MCP output const parseContent = (): ParsedMCPContent | null => { try { @@ -63,7 +63,7 @@ const MCPFormattedMessage: React.FC = ({ mcpOutput.data.headers.join(','), ...mcpOutput.data.rows.map((row: any[]) => row.map(cell => `"${String(cell).replace(/"/g, '""')}"`).join(',') - ) + ), ].join('\n'); exportData = csvContent; } else { @@ -100,10 +100,10 @@ const MCPFormattedMessage: React.FC = ({ try { // Parse the content to extract originalArgs and tool information const parsedContent = JSON.parse(content); - + // Look for originalArgs in the parsed content const originalArgs = parsedContent.originalArgs; - + if (!originalArgs) { console.error('No originalArgs found in content for retry'); return; @@ -128,49 +128,51 @@ const MCPFormattedMessage: React.FC = ({ }, [content, onRetryTool]); return ( - + wordBreak: 'break-word', + overflowX: 'auto', // Add horizontal scroll as fallback + '& *': { + maxWidth: '100%', + boxSizing: 'border-box', + overflowWrap: 'break-word', + wordWrap: 'break-word', + }, + '& pre': { + whiteSpace: 'pre-wrap', + wordBreak: 'break-all', + maxWidth: '100%', + overflowWrap: 'break-word', + overflowX: 'auto', // Horizontal scroll for code blocks + }, + }} + > {isAssistant && ( {mcpContent.isError || mcpContent.mcpOutput.type === 'error' ? 'Tool Error - AI Analysis' - : 'AI-Formatted Tool Output' - } + : 'AI-Formatted Tool Output'} {(mcpContent.isError || mcpContent.mcpOutput.type === 'error') && ( - + Tool Execution Failed )} @@ -179,54 +181,59 @@ const MCPFormattedMessage: React.FC = ({ {/* Show processing info if available and not an error */} - {mcpContent.mcpOutput.metadata && !(mcpContent.isError || mcpContent.mcpOutput.type === 'error') && ( - - - - Processed by AI in {mcpContent.mcpOutput.metadata.processingTime}ms - {mcpContent.mcpOutput.insights && mcpContent.mcpOutput.insights.length > 0 && ( - <> • {mcpContent.mcpOutput.insights.length} insights generated - )} - {mcpContent.mcpOutput.actionable_items && mcpContent.mcpOutput.actionable_items.length > 0 && ( - <> • {mcpContent.mcpOutput.actionable_items.length} action items - )} - - - )} + {mcpContent.mcpOutput.metadata && + !(mcpContent.isError || mcpContent.mcpOutput.type === 'error') && ( + + + + Processed by AI in {mcpContent.mcpOutput.metadata.processingTime}ms + {mcpContent.mcpOutput.insights && mcpContent.mcpOutput.insights.length > 0 && ( + <> • {mcpContent.mcpOutput.insights.length} insights generated + )} + {mcpContent.mcpOutput.actionable_items && + mcpContent.mcpOutput.actionable_items.length > 0 && ( + <> • {mcpContent.mcpOutput.actionable_items.length} action items + )} + + + )} {/* Show error-specific info */} - {(mcpContent.isError || mcpContent.mcpOutput.type === 'error') && mcpContent.mcpOutput.metadata && ( - - - - Error analyzed by AI in {mcpContent.mcpOutput.metadata.processingTime}ms - • Tool: {mcpContent.mcpOutput.metadata.toolName} - - - )} + {(mcpContent.isError || mcpContent.mcpOutput.type === 'error') && + mcpContent.mcpOutput.metadata && ( + + + + Error analyzed by AI in {mcpContent.mcpOutput.metadata.processingTime}ms • Tool:{' '} + {mcpContent.mcpOutput.metadata.toolName} + + + )} ); }; @@ -252,4 +259,4 @@ export const withMCPFormatting =

    ( }; }; -export default MCPFormattedMessage; \ No newline at end of file +export default MCPFormattedMessage; diff --git a/ai-assistant/src/components/common/InlineToolConfirmation.tsx b/ai-assistant/src/components/common/InlineToolConfirmation.tsx index e9e8e68436..938b10c233 100644 --- a/ai-assistant/src/components/common/InlineToolConfirmation.tsx +++ b/ai-assistant/src/components/common/InlineToolConfirmation.tsx @@ -25,8 +25,12 @@ import { Typography, useTheme, } from '@mui/material'; -import React, { useEffect,useState } from 'react'; -import { MCPArgumentProcessor, type ProcessedArguments, type UserContext } from '../mcpOutput/MCPArgumentProcessor'; +import React, { useEffect, useState } from 'react'; +import { + MCPArgumentProcessor, + type ProcessedArguments, + type UserContext, +} from '../mcpOutput/MCPArgumentProcessor'; interface ToolCall { id: string; @@ -54,13 +58,13 @@ const InlineToolConfirmation: React.FC = ({ userContext, }) => { const theme = useTheme(); - const [selectedToolIds] = useState( - toolCalls.map(tool => tool.id) - ); + const [selectedToolIds] = useState(toolCalls.map(tool => tool.id)); const [showDetails, setShowDetails] = useState(!compact); // State to track processed arguments for each tool - const [processedArguments, setProcessedArguments] = useState>({}); + const [processedArguments, setProcessedArguments] = useState>( + {} + ); const [editedArguments, setEditedArguments] = useState>>({}); const [argumentsInitialized, setArgumentsInitialized] = useState(false); @@ -153,12 +157,11 @@ const InlineToolConfirmation: React.FC = ({ } }; - const getToolIcon = (toolName: string, toolType: 'mcp' | 'regular') => { if (toolType === 'mcp') { return 'mdi:connection'; // Use connection icon for MCP tools } - + if (toolName.includes('kubernetes') || toolName.includes('k8s')) { return 'mdi:kubernetes'; } @@ -184,8 +187,8 @@ const InlineToolConfirmation: React.FC = ({ ...prev, [toolId]: { ...prev[toolId], - [fieldName]: newValue - } + [fieldName]: newValue, + }, })); }; @@ -221,9 +224,7 @@ const InlineToolConfirmation: React.FC = ({ ))} - {fieldDescription && ( - {fieldDescription} - )} + {fieldDescription && {fieldDescription}} ); } @@ -235,32 +236,39 @@ const InlineToolConfirmation: React.FC = ({ type="number" label={fieldName} value={currentValue ?? ''} - onChange={(e: any) => handleFieldChange( - fieldType === 'integer' ? parseInt(e.target.value) || 0 : parseFloat(e.target.value) || 0 - )} + onChange={(e: any) => + handleFieldChange( + fieldType === 'integer' + ? parseInt(e.target.value) || 0 + : parseFloat(e.target.value) || 0 + ) + } helperText={fieldDescription} error={hasError} sx={{ minWidth: 200 }} inputProps={{ min: fieldSchema?.minimum, max: fieldSchema?.maximum, - step: fieldType === 'integer' ? 1 : 'any' + step: fieldType === 'integer' ? 1 : 'any', }} /> ); } // Default to text field for strings and other types - const isMultiline = fieldType === 'object' || fieldType === 'array' || - (typeof currentValue === 'string' && currentValue.length > 50); + const isMultiline = + fieldType === 'object' || + fieldType === 'array' || + (typeof currentValue === 'string' && currentValue.length > 50); return ( { let newValue = e.target.value; @@ -292,7 +300,11 @@ const InlineToolConfirmation: React.FC = ({ return Object.entries(tool.arguments).map(([key, value]) => ( {key}:} + primary={ + + {key}: + + } secondary={ {typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value)} @@ -323,17 +335,17 @@ const InlineToolConfirmation: React.FC = ({ {fieldName} - {isRequired && ( - - )} - {!isRequired && ( - - )} + {isRequired && } + {!isRequired && } {/* Show intelligent fill indicator */} {processedArgs.intelligentFills[fieldName] && ( = ({ AI Analysis: {processedArgs.intelligentFills[fieldName].reason} {processedArgs.intelligentFills[fieldName].confidence < 0.8 && ( - <> • Please verify this value + <> + {' '} + • Please verify this value + )} @@ -408,7 +423,7 @@ const InlineToolConfirmation: React.FC = ({ variant="outlined" sx={{ maxWidth: 600, - backgroundColor: theme.palette.background.paper + backgroundColor: theme.palette.background.paper, }} > @@ -429,7 +444,7 @@ const InlineToolConfirmation: React.FC = ({ sx={{ maxWidth: 600, backgroundColor: theme.palette.background.paper, - borderColor: theme.palette.error.main + borderColor: theme.palette.error.main, }} > @@ -443,12 +458,12 @@ const InlineToolConfirmation: React.FC = ({ } return ( - @@ -458,9 +473,9 @@ const InlineToolConfirmation: React.FC = ({ Tool Execution Required - 1 ? 's' : ''}`} - size="small" + 1 ? 's' : ''}`} + size="small" variant="outlined" color="primary" /> @@ -468,16 +483,17 @@ const InlineToolConfirmation: React.FC = ({ {/* Summary */} - {compact + {compact ? `Allow execution of ${toolCalls.length} tool${toolCalls.length > 1 ? 's' : ''}?` - : `The following tool${toolCalls.length > 1 ? 's' : ''} need${toolCalls.length > 1 ? '' : 's'} permission to execute:` - } + : `The following tool${toolCalls.length > 1 ? 's' : ''} need${ + toolCalls.length > 1 ? '' : 's' + } permission to execute:`} {/* Tool summary when compact */} {compact && ( - {toolCalls.map((tool) => ( + {toolCalls.map(tool => ( = ({ )} - + {tool.description && ( - + {tool.description} )} {(Object.keys(tool.arguments).length > 0 || tool.type === 'mcp') && ( - + Arguments {tool.type === 'mcp' ? '(editable):' : ':'} {tool.type === 'mcp' ? ( - - {renderArgumentsForTool(tool, tool.id)} - + {renderArgumentsForTool(tool, tool.id)} ) : ( {Object.entries(tool.arguments).map(([key, value]) => ( {key}:} + primary={ + + {key}: + + } secondary={ - - {typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value)} + + {typeof value === 'object' + ? JSON.stringify(value, null, 2) + : String(value)} } /> @@ -619,8 +649,7 @@ const InlineToolConfirmation: React.FC = ({ {mcpTools.length > 0 ? 'MCP tool arguments have been intelligently analyzed and pre-filled based on your request. Arguments marked "AI-filled" were extracted from your message or context. Review and modify as needed.' - : 'These tools will access external systems. Review the details before approving.' - } + : 'These tools will access external systems. Review the details before approving.'} @@ -640,12 +669,14 @@ const InlineToolConfirmation: React.FC = ({ variant="outlined" size="small" onClick={handleDeny} - startIcon={isDenying ? : } + startIcon={ + isDenying ? : + } disabled={isActionInProgress} - color={isDenying ? "error" : "inherit"} + color={isDenying ? 'error' : 'inherit'} sx={{ opacity: isDenying ? 0.7 : 1, - cursor: isActionInProgress ? 'not-allowed' : 'pointer' + cursor: isActionInProgress ? 'not-allowed' : 'pointer', }} > {isDenying ? 'Denying...' : 'Deny'} @@ -654,12 +685,22 @@ const InlineToolConfirmation: React.FC = ({ variant="contained" size="small" onClick={handleApprove} - startIcon={isApproving ? : } - disabled={isActionInProgress || selectedToolIds.length === 0 || (!argumentsInitialized && mcpTools.length > 0)} + startIcon={ + isApproving ? ( + + ) : ( + + ) + } + disabled={ + isActionInProgress || + selectedToolIds.length === 0 || + (!argumentsInitialized && mcpTools.length > 0) + } color="primary" sx={{ opacity: isApproving ? 0.7 : 1, - cursor: isActionInProgress ? 'not-allowed' : 'pointer' + cursor: isActionInProgress ? 'not-allowed' : 'pointer', }} > {isApproving ? 'Approving...' : `Allow (${selectedToolIds.length})`} @@ -670,4 +711,4 @@ const InlineToolConfirmation: React.FC = ({ ); }; -export default InlineToolConfirmation; \ No newline at end of file +export default InlineToolConfirmation; diff --git a/ai-assistant/src/components/common/ToolApprovalDialog.tsx b/ai-assistant/src/components/common/ToolApprovalDialog.tsx index 325cdaed13..b1538bcf9c 100644 --- a/ai-assistant/src/components/common/ToolApprovalDialog.tsx +++ b/ai-assistant/src/components/common/ToolApprovalDialog.tsx @@ -47,9 +47,7 @@ const ToolApprovalDialog: React.FC = ({ onClose, loading = false, }) => { - const [selectedToolIds, setSelectedToolIds] = useState( - toolCalls.map(tool => tool.id) - ); + const [selectedToolIds, setSelectedToolIds] = useState(toolCalls.map(tool => tool.id)); const [rememberChoice, setRememberChoice] = useState(false); // Reset selection when toolCalls change @@ -69,10 +67,8 @@ const ToolApprovalDialog: React.FC = ({ }; const handleToolToggle = (toolId: string) => { - setSelectedToolIds(prev => - prev.includes(toolId) - ? prev.filter(id => id !== toolId) - : [...prev, toolId] + setSelectedToolIds(prev => + prev.includes(toolId) ? prev.filter(id => id !== toolId) : [...prev, toolId] ); }; @@ -87,7 +83,7 @@ const ToolApprovalDialog: React.FC = ({ if (toolType === 'mcp') { return 'mdi:docker'; // Inspektor Gadget runs in Docker } - + // Regular Kubernetes tools if (toolName.includes('kubernetes') || toolName.includes('k8s')) { return 'mdi:kubernetes'; @@ -100,13 +96,9 @@ const ToolApprovalDialog: React.FC = ({ .filter(([, value]) => value !== undefined && value !== null && value !== '') .map(([key, value]) => ( - )); @@ -119,21 +111,16 @@ const ToolApprovalDialog: React.FC = ({ {title} - + {tools.map(tool => ( } - sx={{ + sx={{ '& .MuiAccordionSummary-content': { - alignItems: 'center' - } + alignItems: 'center', + }, }} > = ({ handleToolToggle(tool.id)} - onClick={(e) => e.stopPropagation()} + onClick={e => e.stopPropagation()} /> } label="" sx={{ mr: 1 }} - onClick={(e) => e.stopPropagation()} - /> - e.stopPropagation()} /> + {tool.name} @@ -163,10 +147,10 @@ const ToolApprovalDialog: React.FC = ({ )} {tool.type === 'mcp' && ( - @@ -176,9 +160,7 @@ const ToolApprovalDialog: React.FC = ({ Arguments to be passed: - - {formatArguments(tool.arguments)} - + {formatArguments(tool.arguments)} ))} @@ -187,20 +169,22 @@ const ToolApprovalDialog: React.FC = ({ }; return ( -

    - + Tool Execution Approval Required @@ -214,7 +198,7 @@ const ToolApprovalDialog: React.FC = ({ - The AI Assistant wants to execute {toolCalls.length} tool{toolCalls.length > 1 ? 's' : ''} + The AI Assistant wants to execute {toolCalls.length} tool{toolCalls.length > 1 ? 's' : ''} to complete your request. Please review and approve the tools you want to allow. @@ -224,8 +208,8 @@ const ToolApprovalDialog: React.FC = ({ MCP Tools (Inspektor Gadget) - These tools will execute debugging commands in your Kubernetes clusters through - Inspektor Gadget containers. They provide deep system-level insights but require + These tools will execute debugging commands in your Kubernetes clusters through + Inspektor Gadget containers. They provide deep system-level insights but require elevated permissions. @@ -258,7 +242,7 @@ const ToolApprovalDialog: React.FC = ({ control={ setRememberChoice(e.target.checked)} + onChange={e => setRememberChoice(e.target.checked)} /> } label={ @@ -271,30 +255,36 @@ const ToolApprovalDialog: React.FC = ({ - + {selectedToolIds.length} of {toolCalls.length} tools selected - - diff --git a/ai-assistant/src/components/mcpOutput/MCPArgumentProcessor.ts b/ai-assistant/src/components/mcpOutput/MCPArgumentProcessor.ts index 06b8a1df1a..fe7cc07d2f 100644 --- a/ai-assistant/src/components/mcpOutput/MCPArgumentProcessor.ts +++ b/ai-assistant/src/components/mcpOutput/MCPArgumentProcessor.ts @@ -48,7 +48,7 @@ export class MCPArgumentProcessor { this.toolSchemas.set(tool.name, { name: tool.name, description: tool.description, - inputSchema: tool.inputSchema + inputSchema: tool.inputSchema, }); }); this.schemasLoaded = true; @@ -83,7 +83,7 @@ export class MCPArgumentProcessor { schema: null, suggestions: {}, errors, - intelligentFills + intelligentFills, }; } @@ -98,7 +98,7 @@ export class MCPArgumentProcessor { intelligentFills[requiredField] = { value: processed[requiredField], reason: `Required field provided with empty ${fieldSchema.type}`, - confidence: 0.8 + confidence: 0.8, }; } } @@ -118,7 +118,7 @@ export class MCPArgumentProcessor { schema, suggestions, errors, - intelligentFills + intelligentFills, }; } @@ -197,8 +197,10 @@ export class MCPArgumentProcessor { if (userContext.lastToolResults) { const lowerPropName = propertyName.toLowerCase(); for (const [contextKey, contextValue] of Object.entries(userContext.lastToolResults)) { - if (contextKey.toLowerCase().includes(lowerPropName) || - lowerPropName.includes(contextKey.toLowerCase())) { + if ( + contextKey.toLowerCase().includes(lowerPropName) || + lowerPropName.includes(contextKey.toLowerCase()) + ) { return contextValue; } } @@ -240,7 +242,11 @@ export class MCPArgumentProcessor { } // Path-related suggestions - if (lowerName.includes('path') || lowerName.includes('directory') || lowerName.includes('dir')) { + if ( + lowerName.includes('path') || + lowerName.includes('directory') || + lowerName.includes('dir') + ) { if (lowerDesc.includes('current') || lowerDesc.includes('working')) { return '.'; } @@ -313,9 +319,7 @@ export class MCPArgumentProcessor { /** * Suggest boolean values */ - private static suggestBooleanValue( - propertyName: string - ): boolean | undefined { + private static suggestBooleanValue(propertyName: string): boolean | undefined { const lowerName = propertyName.toLowerCase(); // Note: description analysis could be added here for more intelligent suggestions @@ -355,24 +359,10 @@ export class MCPArgumentProcessor { return {}; } - - - - - - - - - - - /** * Clean up arguments by removing empty non-required fields */ - static cleanupArguments( - args: Record, - schema: MCPToolSchema - ): Record { + static cleanupArguments(args: Record, schema: MCPToolSchema): Record { if (!schema.inputSchema) return args; const cleaned: Record = {}; @@ -431,7 +421,11 @@ export class MCPArgumentProcessor { // Check required fields (allow empty objects/arrays for required fields) for (const requiredField of required) { - if (!(requiredField in args) || args[requiredField] === undefined || args[requiredField] === null) { + if ( + !(requiredField in args) || + args[requiredField] === undefined || + args[requiredField] === null + ) { errors.push(`Required field '${requiredField}' is missing`); } } @@ -451,7 +445,6 @@ export class MCPArgumentProcessor { return errors; } - /** * Get tool schema */ @@ -467,4 +460,4 @@ export class MCPArgumentProcessor { await this.loadSchemas(); return Array.from(this.toolSchemas.keys()); } -} \ No newline at end of file +} diff --git a/ai-assistant/src/components/mcpOutput/MCPOutputDisplay.tsx b/ai-assistant/src/components/mcpOutput/MCPOutputDisplay.tsx index c7ab0437d4..3ddca3b43a 100644 --- a/ai-assistant/src/components/mcpOutput/MCPOutputDisplay.tsx +++ b/ai-assistant/src/components/mcpOutput/MCPOutputDisplay.tsx @@ -37,7 +37,7 @@ interface MCPOutputDisplayProps { function calculateWidth(width: string): string { if (!width) return '800px'; // fallback - + // Handle viewport width (vw) units if (width.includes('vw')) { const vwValue = parseFloat(width.replace('vw', '')); @@ -45,21 +45,21 @@ function calculateWidth(width: string): string { const adjustedWidth = Math.max(300, pixelWidth - 30); // Subtract 40px, minimum 300px return `${adjustedWidth}px`; } - + // Handle pixel (px) units if (width.includes('px')) { const pixelValue = parseInt(width.replace('px', ''), 10); const adjustedWidth = Math.max(300, pixelValue - 30); // Subtract 40px, minimum 300px return `${adjustedWidth}px`; } - + // Handle numeric values (assume pixels) const numericValue = parseInt(width, 10); if (!isNaN(numericValue)) { const adjustedWidth = Math.max(300, numericValue - 30); // Subtract 40px, minimum 300px return `${adjustedWidth}px`; } - + // Fallback for any other format return '780px'; // 800px - 40px } @@ -81,7 +81,7 @@ const MCPOutputDisplay: React.FC = ({ useEffect(() => { const calculatedWidth = calculateWidth(promptWidth?.toString() || '800px'); setWidth(calculatedWidth); - }, [promptWidth]) + }, [promptWidth]); // Get status color based on type or warnings const getStatusColor = () => { if (output.type === 'error') return 'error'; @@ -92,13 +92,20 @@ const MCPOutputDisplay: React.FC = ({ // Get icon based on output type const getTypeIcon = () => { switch (output.type) { - case 'table': return 'mdi:table'; - case 'metrics': return 'mdi:chart-line'; - case 'list': return 'mdi:format-list-bulleted'; - case 'graph': return 'mdi:chart-bar'; - case 'text': return 'mdi:text'; - case 'error': return 'mdi:alert-circle'; - default: return 'mdi:file-document'; + case 'table': + return 'mdi:table'; + case 'metrics': + return 'mdi:chart-line'; + case 'list': + return 'mdi:format-list-bulleted'; + case 'graph': + return 'mdi:chart-bar'; + case 'text': + return 'mdi:text'; + case 'error': + return 'mdi:alert-circle'; + default: + return 'mdi:file-document'; } }; @@ -122,7 +129,7 @@ const MCPOutputDisplay: React.FC = ({ }; return ( - = ({ wordBreak: 'break-word', }} > - - } - title={ - - - {output.title} - - - {output.metadata && ( - - - - )} - - } - subheader={output.summary} - action={ - - {onExport && ( - + } + title={ + + + {output.title} + + + {output.metadata && ( + + + + )} + + } + subheader={output.summary} + action={ + + {onExport && ( + + setShowExportMenu(!showExportMenu)} + sx={{ mr: 2 }} + > + + + + )} + setShowExportMenu(!showExportMenu)} - sx={{mr:2}} + onClick={() => setShowRawData(!showRawData)} + color={showRawData ? 'primary' : 'default'} + sx={{ mr: 2 }} > - + - )} - - setShowRawData(!showRawData)} - color={showRawData ? 'primary' : 'default'} - sx={{mr:2}} - > - - - - {compact && ( - setExpanded(!expanded)} - > - - - )} - - } - sx={{ pb: 1 }} - /> - - - *': { - width: '100%', - minWidth: 0, - overflowWrap: 'break-word', - wordWrap: 'break-word', + {compact && ( + setExpanded(!expanded)}> + + + )} + } - }}> - {/* Warnings */} - {output.warnings && output.warnings.length > 0 && ( - - - Warnings: - - {output.warnings.map((warning, index) => ( - - • {warning} + sx={{ pb: 1 }} + /> + + + *': { + width: '100%', + minWidth: 0, + overflowWrap: 'break-word', + wordWrap: 'break-word', + }, + }} + > + {/* Warnings */} + {output.warnings && output.warnings.length > 0 && ( + + + Warnings: - ))} - - )} + {output.warnings.map((warning, index) => ( + + • {warning} + + ))} + + )} - {/* Main Content */} - {renderContent()} + {/* Main Content */} + {renderContent()} - {/* Insights */} - {output.insights && output.insights.length > 0 && ( - - - - Key Insights: - - {output.insights.map((insight, index) => ( - - • {insight} + {/* Insights */} + {output.insights && output.insights.length > 0 && ( + + + + Key Insights: - ))} - - )} + {output.insights.map((insight, index) => ( + + • {insight} + + ))} + + )} - {/* Actionable Items */} - {output.actionable_items && output.actionable_items.length > 0 && ( - - - - Recommended Actions: - - {output.actionable_items.map((item, index) => ( - - • {item} + {/* Actionable Items */} + {output.actionable_items && output.actionable_items.length > 0 && ( + + + + Recommended Actions: - ))} - - )} + {output.actionable_items.map((item, index) => ( + + • {item} + + ))} + + )} - {/* Raw Data Collapse */} - - - - Raw Data: - - - {JSON.stringify(output.data, null, 2)} - - + {/* Raw Data Collapse */} + + + + Raw Data: + + + {JSON.stringify(output.data, null, 2)} + + - {/* Metadata */} - {output.metadata && ( - - - - - - )} - - - + {/* Metadata */} + {output.metadata && ( + + + + + + )} + + + ); }; @@ -378,16 +382,19 @@ const TableDisplay: React.FC<{ data: any; width: string }> = ({ data, width }) = }, }} > - +
    {data.headers.map((header: string, index: number) => ( @@ -447,53 +454,74 @@ const TableDisplay: React.FC<{ data: any; width: string }> = ({ data, width }) = const MetricsDisplay: React.FC<{ data: any; width: string }> = ({ data, width }) => { const getStatusColor = (status: string) => { switch (status) { - case 'error': return 'error'; - case 'warning': return 'warning'; - case 'info': return 'info'; - default: return 'primary'; + case 'error': + return 'error'; + case 'warning': + return 'warning'; + case 'info': + return 'info'; + default: + return 'primary'; } }; return ( - + {/* Primary Metrics */} {data.primary && ( - + {data.primary.map((metric: any, index: number) => ( - - + }} + > + {metric.value} - + {metric.label} @@ -504,35 +532,49 @@ const MetricsDisplay: React.FC<{ data: any; width: string }> = ({ data, width }) {/* Secondary Metrics */} {data.secondary && ( - + {data.secondary.map((metric: any, index: number) => ( - - + }} + > + {metric.value} - + {metric.label} @@ -543,32 +585,41 @@ const MetricsDisplay: React.FC<{ data: any; width: string }> = ({ data, width }) {/* Trends */} {data.trends && ( - - - Trends: - - + }} + > + + Trends: + + {data.trends.map((trend: any, index: number) => ( = ({ data, width }) overflowWrap: 'break-word', wordWrap: 'break-word', wordBreak: 'break-word', - } + }, }} /> @@ -592,21 +643,27 @@ const MetricsDisplay: React.FC<{ data: any; width: string }> = ({ data, width }) const ListDisplay: React.FC<{ data: any; width: string }> = ({ data, width }) => { const getStatusColor = (status: string) => { switch (status) { - case 'error': return 'error.main'; - case 'warning': return 'warning.main'; - case 'info': return 'info.main'; - default: return 'text.primary'; + case 'error': + return 'error.main'; + case 'warning': + return 'warning.main'; + case 'info': + return 'info.main'; + default: + return 'text.primary'; } }; return ( - + {data.items?.map((item: any, index: number) => ( = ({ data, width }) = }; // Text Display Component -const TextDisplay: React.FC<{ data: any; theme: any; width: string }> = ({ data, theme, width }) => { +const TextDisplay: React.FC<{ data: any; theme: any; width: string }> = ({ + data, + theme, + width, +}) => { return ( - + {data.highlights && data.highlights.length > 0 && ( - + {data.highlights.map((highlight: string, index: number) => ( - = ({ data, wordWrap: 'break-word', wordBreak: 'break-word', whiteSpace: 'normal', - } + }, }} /> ))} )} - + = ({ data, }; // Error Display Component -const ErrorDisplay: React.FC<{ data: any; onRetry?: () => void; width: string }> = ({ data, onRetry, width }) => { - +const ErrorDisplay: React.FC<{ data: any; onRetry?: () => void; width: string }> = ({ + data, + onRetry, + width, +}) => { // Extract concise error message for common error types const getDisplayMessage = (errorData: any) => { const message = errorData.message || 'Tool Execution Error'; @@ -751,14 +821,16 @@ const ErrorDisplay: React.FC<{ data: any; onRetry?: () => void; width: string }> }; return ( - + void; width: string }> size="small" onClick={() => { // Copy error details to clipboard - const errorText = `Tool Error: ${data.message}\nDetails: ${data.details || 'N/A'}\nSuggestions:\n${data.suggestions?.map((s: string) => `- ${s}`).join('\n') || 'None'}`; + const errorText = `Tool Error: ${data.message}\nDetails: ${ + data.details || 'N/A' + }\nSuggestions:\n${ + data.suggestions?.map((s: string) => `- ${s}`).join('\n') || 'None' + }`; navigator.clipboard.writeText(errorText); }} startIcon={} - sx={ - { - mr: 2 - } - } + sx={{ + mr: 2, + }} > Copy Error @@ -878,14 +952,16 @@ const ErrorDisplay: React.FC<{ data: any; onRetry?: () => void; width: string }> // Raw Display Component const RawDisplay: React.FC<{ data: any; theme: any; width: string }> = ({ data, theme, width }) => { return ( - + = ({ data, ); }; -export default MCPOutputDisplay; \ No newline at end of file +export default MCPOutputDisplay; diff --git a/ai-assistant/src/components/mcpOutput/index.ts b/ai-assistant/src/components/mcpOutput/index.ts index b587f0be96..c05de17eb3 100644 --- a/ai-assistant/src/components/mcpOutput/index.ts +++ b/ai-assistant/src/components/mcpOutput/index.ts @@ -1,2 +1,2 @@ export { default as MCPOutputDisplay } from './MCPOutputDisplay'; -export type { FormattedMCPOutput } from '../../langchain/formatters/MCPOutputFormatter'; \ No newline at end of file +export type { FormattedMCPOutput } from '../../langchain/formatters/MCPOutputFormatter'; diff --git a/ai-assistant/src/components/settings/MCPConfigEditorDialog.tsx b/ai-assistant/src/components/settings/MCPConfigEditorDialog.tsx index 26f822abe2..6825b43937 100644 --- a/ai-assistant/src/components/settings/MCPConfigEditorDialog.tsx +++ b/ai-assistant/src/components/settings/MCPConfigEditorDialog.tsx @@ -127,27 +127,21 @@ export default function MCPConfigEditorDialog({ enabled: true, servers: [ { - name: "inspektor-gadget", - command: "docker", - args: [ - "mcp", - "gateway", - "run" - ], - enabled: true + name: 'inspektor-gadget', + command: 'docker', + args: ['mcp', 'gateway', 'run'], + enabled: true, }, { - name: "flux-mcp", - command: "flux-operator-mcp", - args: [ - "serve" - ], + name: 'flux-mcp', + command: 'flux-operator-mcp', + args: ['serve'], env: { - "KUBECONFIG": "/Users/ashughildiyal/.kube/config" + KUBECONFIG: '/Users/ashughildiyal/.kube/config', }, - enabled: true - } - ] + enabled: true, + }, + ], }; setContent(JSON.stringify(exampleConfig, null, 2)); @@ -165,18 +159,18 @@ export default function MCPConfigEditorDialog({ const getSchemaDocumentation = () => { return { - enabled: "boolean - Enable/disable all MCP servers", + enabled: 'boolean - Enable/disable all MCP servers', servers: [ { - name: "string - Unique server name", - command: "string - Executable command or path", - args: ["array of strings - Command arguments"], + name: 'string - Unique server name', + command: 'string - Executable command or path', + args: ['array of strings - Command arguments'], env: { - "KEY": "string value - Environment variables (optional)" + KEY: 'string value - Environment variables (optional)', }, - enabled: "boolean - Enable/disable this specific server" - } - ] + enabled: 'boolean - Enable/disable this specific server', + }, + ], }; }; @@ -246,7 +240,8 @@ export default function MCPConfigEditorDialog({ /> - Edit the JSON configuration above. The editor will automatically format and validate your configuration. + Edit the JSON configuration above. The editor will automatically format and validate + your configuration. )} @@ -260,18 +255,21 @@ export default function MCPConfigEditorDialog({ - The MCP configuration defines how your AI assistant connects to external tools and services. - Each server represents a separate MCP server that provides specific capabilities. + The MCP configuration defines how your AI assistant connects to external tools and + services. Each server represents a separate MCP server that provides specific + capabilities. -
    +                
                       {JSON.stringify(getSchemaDocumentation(), null, 2)}
                     
    @@ -298,7 +296,8 @@ export default function MCPConfigEditorDialog({
  • - command: The executable to run (e.g., "docker", "npx", "python") + command: The executable to run (e.g., "docker", "npx", + "python")
  • @@ -313,7 +312,8 @@ export default function MCPConfigEditorDialog({
  • - enabled: Toggle individual server on/off without removing configuration + enabled: Toggle individual server on/off without removing + configuration
  • @@ -327,15 +327,10 @@ export default function MCPConfigEditorDialog({ - ); -} \ No newline at end of file +} diff --git a/ai-assistant/src/components/settings/MCPSettings.tsx b/ai-assistant/src/components/settings/MCPSettings.tsx index 24b6292158..2fdafed97a 100644 --- a/ai-assistant/src/components/settings/MCPSettings.tsx +++ b/ai-assistant/src/components/settings/MCPSettings.tsx @@ -1,21 +1,17 @@ import { Icon } from '@iconify/react'; import { SectionBox } from '@kinvolk/headlamp-plugin/lib/components/common'; -import { - Box, - Button, - FormControlLabel, - Switch, - Typography, -} from '@mui/material'; +import { Box, Button, FormControlLabel, Switch, Typography } from '@mui/material'; import React, { useEffect, useState } from 'react'; import { pluginStore } from '../../utils'; import MCPConfigEditorDialog from './MCPConfigEditorDialog'; // Helper function to check if running in Electron const isElectron = (): boolean => { - return typeof window !== 'undefined' && - typeof window.desktopApi !== 'undefined' && - typeof window.desktopApi.mcp !== 'undefined'; + return ( + typeof window !== 'undefined' && + typeof window.desktopApi !== 'undefined' && + typeof window.desktopApi.mcp !== 'undefined' + ); }; export interface MCPServer { @@ -59,10 +55,9 @@ export function MCPSettings({ config, onConfigChange }: MCPSettingsProps) { } }, []); - const loadMCPConfigFromElectron = async () => { if (!isElectron()) return; - + try { const response = await window.desktopApi!.mcp.getConfig(); if (response.success && response.config) { @@ -80,7 +75,7 @@ export function MCPSettings({ config, onConfigChange }: MCPSettingsProps) { const handleConfigChange = async (newConfig: MCPConfig) => { setMCPConfig(newConfig); - + if (isElectron()) { // Save to Electron settings and restart MCP client try { @@ -120,41 +115,33 @@ export function MCPSettings({ config, onConfigChange }: MCPSettingsProps) { const handleToggleEnabled = async () => { const newConfig = { ...mcpConfig, enabled: !mcpConfig.enabled }; - + // If enabling MCP for the first time and no servers exist, add default servers if (newConfig.enabled && mcpConfig.servers.length === 0) { const defaultServers: MCPServer[] = [ { name: 'inspektor-gadget', command: 'docker', - args: [ - 'mcp', - 'gateway', - 'run' - ], + args: ['mcp', 'gateway', 'run'], enabled: true, }, { name: 'flux-mcp', command: 'flux-operator-mcp', - args: [ - 'serve' - ], + args: ['serve'], env: { - 'KUBECONFIG': '/Users/ashughildiyal/.kube/config' + KUBECONFIG: '/Users/ashughildiyal/.kube/config', }, enabled: true, - } + }, ]; newConfig.servers = defaultServers; } - + await handleConfigChange(newConfig); }; - - const handleOpenEditorDialog = () => { setEditorDialogOpen(true); }; @@ -182,17 +169,12 @@ export function MCPSettings({ config, onConfigChange }: MCPSettingsProps) { - Model Context Protocol (MCP) allows AI assistants to connect to external tools and data sources. - Configure MCP servers here to extend the AI assistant's capabilities. + Model Context Protocol (MCP) allows AI assistants to connect to external tools and data + sources. Configure MCP servers here to extend the AI assistant's capabilities. - + - } + control={} label="Enable MCP Servers" /> diff --git a/ai-assistant/src/contexts/PromptWidthContext.tsx b/ai-assistant/src/contexts/PromptWidthContext.tsx index 8f3ab64410..eed0b10d90 100644 --- a/ai-assistant/src/contexts/PromptWidthContext.tsx +++ b/ai-assistant/src/contexts/PromptWidthContext.tsx @@ -1,4 +1,4 @@ -import React, { createContext, ReactNode,useContext, useState } from 'react'; +import React, { createContext, ReactNode, useContext, useState } from 'react'; interface PromptWidthContextType { promptWidth: string; @@ -12,9 +12,9 @@ interface PromptWidthProviderProps { initialWidth?: string; } -export const PromptWidthProvider: React.FC = ({ - children, - initialWidth = '400px' +export const PromptWidthProvider: React.FC = ({ + children, + initialWidth = '400px', }) => { const [promptWidth, setPromptWidth] = useState(initialWidth); @@ -31,4 +31,4 @@ export const usePromptWidth = (): PromptWidthContextType => { throw new Error('usePromptWidth must be used within a PromptWidthProvider'); } return context; -}; \ No newline at end of file +}; diff --git a/ai-assistant/src/hooks/useToolApproval.ts b/ai-assistant/src/hooks/useToolApproval.ts index ef8c64ec5a..0b74337ccd 100644 --- a/ai-assistant/src/hooks/useToolApproval.ts +++ b/ai-assistant/src/hooks/useToolApproval.ts @@ -30,19 +30,22 @@ export const useToolApproval = (): UseToolApprovalResult => { }; }, []); - const handleApprove = useCallback((approvedToolIds: string[], rememberChoice = false) => { - if (!pendingRequest) return; + const handleApprove = useCallback( + (approvedToolIds: string[], rememberChoice = false) => { + if (!pendingRequest) return; - setIsProcessing(true); - toolApprovalManager.approveTools(pendingRequest.requestId, approvedToolIds, rememberChoice); - - // Close dialog after a brief delay to show processing state - setTimeout(() => { - setShowApprovalDialog(false); - setPendingRequest(null); - setIsProcessing(false); - }, 500); - }, [pendingRequest]); + setIsProcessing(true); + toolApprovalManager.approveTools(pendingRequest.requestId, approvedToolIds, rememberChoice); + + // Close dialog after a brief delay to show processing state + setTimeout(() => { + setShowApprovalDialog(false); + setPendingRequest(null); + setIsProcessing(false); + }, 500); + }, + [pendingRequest] + ); const handleDeny = useCallback(() => { if (!pendingRequest) return; diff --git a/ai-assistant/src/index.tsx b/ai-assistant/src/index.tsx index a49feb3600..702a3907ca 100644 --- a/ai-assistant/src/index.tsx +++ b/ai-assistant/src/index.tsx @@ -110,18 +110,17 @@ const AIPanelComponent = React.memo(() => { }} /> - + ); }); - AIPanelComponent.displayName = 'AIPanelComponent'; // Register UI Panel component that uses the shared state to show/hide diff --git a/ai-assistant/src/langchain/LangChainManager.ts b/ai-assistant/src/langchain/LangChainManager.ts index d488c2d91c..039be487d7 100644 --- a/ai-assistant/src/langchain/LangChainManager.ts +++ b/ai-assistant/src/langchain/LangChainManager.ts @@ -59,12 +59,20 @@ export default class LangChainManager extends AIManager { // Add the tool confirmation message to chat history console.log('🔔 LangChainManager: Adding tool confirmation message to history', data); this.addToolConfirmationMessage('', data.toolConfirmation); - console.log('📝 LangChainManager: History length after adding confirmation:', this.history.length); + console.log( + '📝 LangChainManager: History length after adding confirmation:', + this.history.length + ); }); inlineToolApprovalManager.on('update-confirmation', (data: any) => { // Update the specific tool confirmation message with new state (e.g., loading) - console.log('🔄 Tool confirmation update:', data.requestId, 'loading:', data.toolConfirmation.loading); + console.log( + '🔄 Tool confirmation update:', + data.requestId, + 'loading:', + data.toolConfirmation.loading + ); this.updateToolConfirmationMessage(data.requestId, data.toolConfirmation); }); } @@ -218,7 +226,11 @@ export default class LangChainManager extends AIManager { // Get all tools (including MCP tools) const allTools = this.toolManager.getLangChainTools(); - console.log(`🔧 Total tools available: ${allTools.length} (Regular: ${tools.length}, MCP: ${this.toolManager.getMCPTools().length})`); + console.log( + `🔧 Total tools available: ${allTools.length} (Regular: ${tools.length}, MCP: ${ + this.toolManager.getMCPTools().length + })` + ); // Bind all tools to the model for compatible providers (OpenAI, Azure, etc.) this.boundModel = this.toolManager.bindToModel(this.model, this.providerId); @@ -232,7 +244,7 @@ export default class LangChainManager extends AIManager { console.log('🔧 Tools configured:', { boundModel: !!this.boundModel, directToolCalling: this.useDirectToolCalling, - toolCount: allTools.length + toolCount: allTools.length, }); } @@ -249,27 +261,24 @@ export default class LangChainManager extends AIManager { */ private buildUserContext(): UserContext { // Get the most recent user message - const recentUserMessages = this.history - .filter(prompt => prompt.role === 'user') - .slice(-3); // Last 3 user messages for context + const recentUserMessages = this.history.filter(prompt => prompt.role === 'user').slice(-3); // Last 3 user messages for context - const userMessage = recentUserMessages.length > 0 - ? recentUserMessages[recentUserMessages.length - 1].content - : ''; + const userMessage = + recentUserMessages.length > 0 + ? recentUserMessages[recentUserMessages.length - 1].content + : ''; // Build conversation history const conversationHistory = this.history .slice(-10) // Last 10 messages .map(prompt => ({ role: prompt.role, - content: prompt.content + content: prompt.content, })); // Get recent tool results const lastToolResults: Record = {}; - const recentToolResponses = this.history - .filter(prompt => prompt.role === 'tool') - .slice(-5); // Last 5 tool responses + const recentToolResponses = this.history.filter(prompt => prompt.role === 'tool').slice(-5); // Last 5 tool responses recentToolResponses.forEach(response => { if (response.name) { @@ -286,7 +295,7 @@ export default class LangChainManager extends AIManager { userMessage, conversationHistory, lastToolResults, - timeContext: new Date() + timeContext: new Date(), }; } @@ -319,18 +328,29 @@ export default class LangChainManager extends AIManager { /** * Add a tool confirmation message to the history */ - public addToolConfirmationMessage(content: string, toolConfirmation: any, updateHistoryCallback?: () => void): void { - console.log('➕ LangChainManager: Adding tool confirmation message', { content, toolConfirmation }); + public addToolConfirmationMessage( + content: string, + toolConfirmation: any, + updateHistoryCallback?: () => void + ): void { + console.log('➕ LangChainManager: Adding tool confirmation message', { + content, + toolConfirmation, + }); const confirmationPrompt: Prompt = { role: 'assistant', content: content, toolConfirmation: toolConfirmation, isDisplayOnly: true, // Don't send to LLM - requestId: toolConfirmation.requestId // Add requestId for tracking + requestId: toolConfirmation.requestId, // Add requestId for tracking }; this.history.push(confirmationPrompt); - console.log('📚 LangChainManager: History after adding confirmation:', this.history.length, 'items'); - + console.log( + '📚 LangChainManager: History after adding confirmation:', + this.history.length, + 'items' + ); + // Call the update callback if provided to trigger UI re-render if (updateHistoryCallback) { updateHistoryCallback(); @@ -338,21 +358,24 @@ export default class LangChainManager extends AIManager { } public updateToolConfirmationMessage(requestId: string, updatedToolConfirmation: any): void { - console.log('🔄 LangChainManager: Updating tool confirmation message', { requestId, updatedToolConfirmation }); - + console.log('🔄 LangChainManager: Updating tool confirmation message', { + requestId, + updatedToolConfirmation, + }); + // Find the message with matching requestId const messageIndex = this.history.findIndex( prompt => prompt.requestId === requestId && prompt.toolConfirmation ); - + if (messageIndex !== -1) { // Update the tool confirmation in the existing message this.history[messageIndex] = { ...this.history[messageIndex], - toolConfirmation: updatedToolConfirmation + toolConfirmation: updatedToolConfirmation, }; console.log('✅ LangChainManager: Tool confirmation message updated'); - + // Use the inline tool approval manager to emit update event inlineToolApprovalManager.emit('message-updated', { requestId, updatedToolConfirmation }); } else { @@ -372,7 +395,7 @@ export default class LangChainManager extends AIManager { // Helper method to create system prompt with context private createSystemPrompt(): string { let systemPromptContent = basePrompt; - + // Add MCP tool guidance if we have MCP tools available const mcpTools = this.toolManager.getMCPTools(); if (mcpTools.length > 0) { @@ -422,7 +445,7 @@ EXAMPLE result processing: NEVER just dump raw JSON - always interpret and present meaningfully.`; } - + if (this.currentContext) { systemPromptContent += `\n\nCURRENT CONTEXT:\n${this.currentContext}`; } @@ -432,7 +455,7 @@ NEVER just dump raw JSON - always interpret and present meaningfully.`; // Helper method to create system prompt specifically for tool response processing private createToolResponseSystemPrompt(): string { const baseSystemPrompt = this.createSystemPrompt(); - + // Add specific instructions for tool response processing const toolResponseInstructions = ` @@ -501,9 +524,9 @@ The user is waiting for you to explain what the tools discovered. Provide a dire private async handleDirectToolCallingRequest(message: string): Promise { try { console.log('🔧 Using direct tool calling for request:', message); - + const modelToUse = this.boundModel || this.model; - + // Prepare input for the model with tools const chainInput = { systemPrompt: this.createSystemPrompt(), @@ -539,14 +562,13 @@ The user is waiting for you to explain what the tools discovered. Provide a dire this.history.push(assistantPrompt); return assistantPrompt; } - } catch (error) { console.error('Error in direct tool calling request:', error); - + // If direct tool calling fails, fall back to regular approach console.log('🔄 Falling back to chain-based approach'); this.useDirectToolCalling = false; - + const modelToUse = this.boundModel || this.model; return await this.handleChainBasedRequest(message, modelToUse); } @@ -688,47 +710,49 @@ The user is waiting for you to explain what the tools discovered. Provide a dire this.history.push(assistantPrompt); // Prepare tool calls for approval with intelligent argument processing - const toolCallsForApproval: ToolCall[] = await Promise.all(toolCalls.map(async tc => { - const toolName = tc.function.name; - const mcpTools = this.toolManager.getMCPTools(); - const isMCPTool = mcpTools.some(tool => tool.name === toolName); - let processedArguments = JSON.parse(tc.function.arguments); - - // Use AI to enhance arguments for MCP tools - if (isMCPTool) { - try { - const toolSchema = await MCPArgumentProcessor.getToolSchema(toolName); - if (toolSchema) { - // Build user context from current conversation - const userContext = this.buildUserContext(); - - // Use AI to intelligently prepare arguments - processedArguments = await this.enhanceArgumentsWithAI( - toolName, - toolSchema, - userContext, - processedArguments - ); - - console.log(`🧠 AI-enhanced arguments for ${toolName}:`, { - original: JSON.parse(tc.function.arguments), - enhanced: processedArguments - }); + const toolCallsForApproval: ToolCall[] = await Promise.all( + toolCalls.map(async tc => { + const toolName = tc.function.name; + const mcpTools = this.toolManager.getMCPTools(); + const isMCPTool = mcpTools.some(tool => tool.name === toolName); + let processedArguments = JSON.parse(tc.function.arguments); + + // Use AI to enhance arguments for MCP tools + if (isMCPTool) { + try { + const toolSchema = await MCPArgumentProcessor.getToolSchema(toolName); + if (toolSchema) { + // Build user context from current conversation + const userContext = this.buildUserContext(); + + // Use AI to intelligently prepare arguments + processedArguments = await this.enhanceArgumentsWithAI( + toolName, + toolSchema, + userContext, + processedArguments + ); + + console.log(`🧠 AI-enhanced arguments for ${toolName}:`, { + original: JSON.parse(tc.function.arguments), + enhanced: processedArguments, + }); + } + } catch (error) { + console.warn(`Failed to enhance arguments for ${toolName}:`, error); + // Fall back to original arguments } - } catch (error) { - console.warn(`Failed to enhance arguments for ${toolName}:`, error); - // Fall back to original arguments } - } - return { - id: tc.id, - name: toolName, - description: this.getToolDescription(toolName, isMCPTool), - arguments: processedArguments, - type: isMCPTool ? 'mcp' : 'regular' - }; - })); + return { + id: tc.id, + name: toolName, + description: this.getToolDescription(toolName, isMCPTool), + arguments: processedArguments, + type: isMCPTool ? 'mcp' : 'regular', + }; + }) + ); try { // Request approval for tool execution using inline approval system @@ -737,24 +761,26 @@ The user is waiting for you to explain what the tools discovered. Provide a dire toolCallsForApproval, this // Pass the AI manager instance ); - + console.log('✅ Tools approved:', approvedToolIds.length, 'of', toolCallsForApproval.length); // Filter tool calls to only execute approved ones and update with processed arguments - const approvedToolCalls = toolCalls.filter(tc => approvedToolIds.includes(tc.id)).map(tc => { - // Find the processed arguments from the approval data - const approvalData = toolCallsForApproval.find(approval => approval.id === tc.id); - if (approvalData) { - return { - ...tc, - function: { - ...tc.function, - arguments: JSON.stringify(approvalData.arguments) - } - }; - } - return tc; - }); + const approvedToolCalls = toolCalls + .filter(tc => approvedToolIds.includes(tc.id)) + .map(tc => { + // Find the processed arguments from the approval data + const approvalData = toolCallsForApproval.find(approval => approval.id === tc.id); + if (approvalData) { + return { + ...tc, + function: { + ...tc.function, + arguments: JSON.stringify(approvalData.arguments), + }, + }; + } + return tc; + }); const deniedToolCalls = toolCalls.filter(tc => !approvedToolIds.includes(tc.id)); // Add denied tool responses to history @@ -764,7 +790,7 @@ The user is waiting for you to explain what the tools discovered. Provide a dire content: JSON.stringify({ error: true, message: 'Tool execution denied by user', - userFriendlyMessage: `The execution of ${deniedTool.function.name} was denied by the user.` + userFriendlyMessage: `The execution of ${deniedTool.function.name} was denied by the user.`, }), toolCallId: deniedTool.id, name: deniedTool.function.name, @@ -775,10 +801,9 @@ The user is waiting for you to explain what the tools discovered. Provide a dire if (approvedToolCalls.length > 0) { await this.processToolCalls(approvedToolCalls, assistantPrompt); } - } catch (error) { console.log('❌ Tool approval denied or failed:', error.message); - + // Add denial responses for all tools for (const toolCall of toolCalls) { this.history.push({ @@ -786,7 +811,9 @@ The user is waiting for you to explain what the tools discovered. Provide a dire content: JSON.stringify({ error: true, message: error.message || 'Tool execution denied', - userFriendlyMessage: `Tool execution was denied: ${error.message || 'User chose not to proceed'}` + userFriendlyMessage: `Tool execution was denied: ${ + error.message || 'User chose not to proceed' + }`, }), toolCallId: toolCall.id, name: toolCall.function.name, @@ -1106,7 +1133,9 @@ Format your response to make the errors prominent and actionable.`, // Helper method to check if there are tool responses private hasToolResponses(): boolean { - const toolResponses = this.history.filter(prompt => prompt.role === 'tool' && prompt.toolCallId); + const toolResponses = this.history.filter( + prompt => prompt.role === 'tool' && prompt.toolCallId + ); return toolResponses.length > 0; } @@ -1299,15 +1328,17 @@ Format your response to make the errors prominent and actionable.`, // If the model returned empty content but has tool calls, it's trying to call more tools // Instead of allowing this, we should provide a fallback response based on the tool results - if ((!extractedContent || extractedContent.trim().length === 0) && response.tool_calls?.length > 0) { - + if ( + (!extractedContent || extractedContent.trim().length === 0) && + response.tool_calls?.length > 0 + ) { // Get the most recent tool responses from history const recentToolResponses = this.history .filter(prompt => prompt.role === 'tool' && prompt.toolCallId) .slice(-3) // Get last 3 tool responses .map(response => ({ name: response.name, - content: response.content + content: response.content, })); // Create a fallback response based on tool results @@ -1368,7 +1399,9 @@ Format your response to make the errors prominent and actionable.`, fallbackContent = content; } else { // For multiple tools, use the tool name format - fallbackContent += `${toolName}: ${content}${index < recentToolResponses.length - 1 ? '\n\n' : ''}`; + fallbackContent += `${toolName}: ${content}${ + index < recentToolResponses.length - 1 ? '\n\n' : '' + }`; } }); @@ -1402,7 +1435,6 @@ Format your response to make the errors prominent and actionable.`, })) || [], }; - // Clean up history to prevent message order issues const lastAssistantWithToolsIndex = this.findLastAssistantWithTools(); if (lastAssistantWithToolsIndex >= 0) { @@ -1555,7 +1587,10 @@ Format your response to make the errors prominent and actionable.`, const isRequired = required.includes(fieldName); const currentValue = enhanced[fieldName]; - if (isRequired && (currentValue === undefined || currentValue === null || currentValue === '')) { + if ( + isRequired && + (currentValue === undefined || currentValue === null || currentValue === '') + ) { // Provide intelligent defaults based on field type and context enhanced[fieldName] = this.getIntelligentDefault(fieldName, fieldSchema, userContext); } @@ -1567,7 +1602,11 @@ Format your response to make the errors prominent and actionable.`, /** * Get intelligent default value for a field based on context */ - private getIntelligentDefault(fieldName: string, fieldSchema: any, userContext: UserContext): any { + private getIntelligentDefault( + fieldName: string, + fieldSchema: any, + userContext: UserContext + ): any { const fieldType = fieldSchema.type; const fieldNameLower = fieldName.toLowerCase(); diff --git a/ai-assistant/src/langchain/formatters/MCPOutputFormatter.ts b/ai-assistant/src/langchain/formatters/MCPOutputFormatter.ts index 34be62834b..c116353c23 100644 --- a/ai-assistant/src/langchain/formatters/MCPOutputFormatter.ts +++ b/ai-assistant/src/langchain/formatters/MCPOutputFormatter.ts @@ -156,20 +156,17 @@ Remember: Focus on making complex data accessible and actionable for Kubernetes includeInsights: true, includeActionableItems: true, formatStyle: 'detailed', - ...options + ...options, } as Required; // Prepare the analysis prompt const analysisPrompt = this.buildAnalysisPrompt(rawOutput, toolName, opts); // Send to AI for analysis and formatting - const messages = [ - new SystemMessage(this.SYSTEM_PROMPT), - new HumanMessage(analysisPrompt) - ]; + const messages = [new SystemMessage(this.SYSTEM_PROMPT), new HumanMessage(analysisPrompt)]; const response = await this.model.invoke(messages, { - max_tokens: opts.maxTokens + max_tokens: opts.maxTokens, }); const processingTime = Date.now() - startTime; @@ -178,11 +175,10 @@ Remember: Focus on making complex data accessible and actionable for Kubernetes const formattedOutput = this.parseAIResponse(response.content as string, { toolName, responseSize: rawOutput.length, - processingTime + processingTime, }); return formattedOutput; - } catch (error) { console.error('Error formatting MCP output:', error); @@ -203,7 +199,9 @@ Remember: Focus on making complex data accessible and actionable for Kubernetes // Detect if this is likely an error const isError = this.detectError(rawOutput); - const errorHint = isError ? '\n\nIMPORTANT: This appears to be an error response. Use "error" type and provide helpful troubleshooting guidance.' : ''; + const errorHint = isError + ? '\n\nIMPORTANT: This appears to be an error response. Use "error" type and provide helpful troubleshooting guidance.' + : ''; return `Analyze and format this ${toolName} tool output: @@ -225,9 +223,10 @@ Pay special attention to: ${errorHint} -${truncatedOutput.length < rawOutput.length ? - `\n[Note: Output was truncated from ${rawOutput.length} to ${truncatedOutput.length} characters for analysis]` : - '' +${ + truncatedOutput.length < rawOutput.length + ? `\n[Note: Output was truncated from ${rawOutput.length} to ${truncatedOutput.length} characters for analysis]` + : '' }`; } @@ -237,16 +236,20 @@ ${truncatedOutput.length < rawOutput.length ? private detectError(rawOutput: string): boolean { try { const parsed = JSON.parse(rawOutput); - return parsed.success === false || - parsed.error === true || - (typeof parsed.error === 'string' && parsed.error.length > 0) || - rawOutput.toLowerCase().includes('schema mismatch'); + return ( + parsed.success === false || + parsed.error === true || + (typeof parsed.error === 'string' && parsed.error.length > 0) || + rawOutput.toLowerCase().includes('schema mismatch') + ); } catch { const lower = rawOutput.toLowerCase(); - return lower.includes('error') || - lower.includes('failed') || - lower.includes('exception') || - lower.includes('schema mismatch'); + return ( + lower.includes('error') || + lower.includes('failed') || + lower.includes('exception') || + lower.includes('schema mismatch') + ); } } @@ -259,8 +262,8 @@ ${truncatedOutput.length < rawOutput.length ? ): FormattedMCPOutput { try { // Extract JSON from response (handle potential markdown wrapping) - const jsonMatch = aiResponse.match(/```json\n?([\s\S]*?)\n?```/) || - aiResponse.match(/\{[\s\S]*\}/); + const jsonMatch = + aiResponse.match(/```json\n?([\s\S]*?)\n?```/) || aiResponse.match(/\{[\s\S]*\}/); if (!jsonMatch) { throw new Error('No JSON found in AI response'); @@ -279,12 +282,11 @@ ${truncatedOutput.length < rawOutput.length ? actionable_items: parsed.actionable_items || [], metadata: { ...metadata, - dataPoints: this.estimateDataPoints(parsed.data) - } + dataPoints: this.estimateDataPoints(parsed.data), + }, }; return formatted; - } catch (error) { console.error('Error parsing AI response:', error); throw error; @@ -312,14 +314,14 @@ ${truncatedOutput.length < rawOutput.length ? items: parsed.slice(0, 100).map((item, index) => ({ text: typeof item === 'string' ? item : JSON.stringify(item), status: 'normal', - metadata: `Item ${index + 1}` - })) + metadata: `Item ${index + 1}`, + })), }; } else if (typeof parsed === 'object') { type = 'text'; data = { content: JSON.stringify(parsed, null, 2), - language: 'json' + language: 'json', }; } else { type = 'text'; @@ -329,10 +331,11 @@ ${truncatedOutput.length < rawOutput.length ? // Not JSON, treat as text type = 'text'; data = { - content: rawOutput.length > 5000 ? - rawOutput.substring(0, 5000) + '\n\n[Output truncated...]' : - rawOutput, - language: 'text' + content: + rawOutput.length > 5000 + ? rawOutput.substring(0, 5000) + '\n\n[Output truncated...]' + : rawOutput, + language: 'text', }; } @@ -348,8 +351,8 @@ ${truncatedOutput.length < rawOutput.length ? toolName, responseSize: rawOutput.length, processingTime, - dataPoints: this.estimateDataPoints(data) - } + dataPoints: this.estimateDataPoints(data), + }, }; } @@ -370,9 +373,7 @@ ${truncatedOutput.length < rawOutput.length ? // Use the best boundary we can find const cutPoint = Math.max(lastNewline, lastBrace, lastBracket); - return cutPoint > maxLength * 0.8 ? - output.substring(0, cutPoint) : - truncated; + return cutPoint > maxLength * 0.8 ? output.substring(0, cutPoint) : truncated; } /** @@ -410,4 +411,4 @@ ${truncatedOutput.length < rawOutput.length ? formatSimple(rawOutput: string, toolName: string): FormattedMCPOutput { return this.createFallbackFormat(rawOutput, toolName, 0); } -} \ No newline at end of file +} diff --git a/ai-assistant/src/langchain/tools/ToolManager.ts b/ai-assistant/src/langchain/tools/ToolManager.ts index 9313674d7e..3e25f01391 100644 --- a/ai-assistant/src/langchain/tools/ToolManager.ts +++ b/ai-assistant/src/langchain/tools/ToolManager.ts @@ -2,7 +2,7 @@ import { BaseChatModel } from '@langchain/core/language_models/chat_models'; import { DynamicTool } from '@langchain/core/tools'; import { Prompt } from '../../ai/manager'; import tools from '../../ai/mcp/electron-client'; -import {MCPOutputFormatter } from '../formatters/MCPOutputFormatter'; +import { MCPOutputFormatter } from '../formatters/MCPOutputFormatter'; import { KubernetesTool, KubernetesToolContext } from './kubernetes'; import { AVAILABLE_TOOLS, getToolByName } from './registry'; import { ToolBase, ToolResponse } from './ToolBase'; @@ -52,60 +52,76 @@ export class ToolManager { try { console.log('Initializing MCP tools from Electron...'); const mcpToolsData = await tools(); - + if (mcpToolsData && mcpToolsData.length > 0) { console.log(`Successfully loaded ${mcpToolsData.length} MCP tools from Electron`); - + // Filter MCP tools by enabled status - const filteredMcpTools = enabledToolIds + const filteredMcpTools = enabledToolIds ? mcpToolsData.filter(toolData => enabledToolIds.includes(toolData.name)) : mcpToolsData; - + if (enabledToolIds) { - console.log(`Filtered to ${filteredMcpTools.length} enabled MCP tools out of ${mcpToolsData.length} total`); + console.log( + `Filtered to ${filteredMcpTools.length} enabled MCP tools out of ${mcpToolsData.length} total` + ); } - console.log("Filtered MCP tools:", filteredMcpTools); + console.log('Filtered MCP tools:', filteredMcpTools); // Convert MCP tools to LangChain DynamicTool format - this.mcpTools = filteredMcpTools.map(toolData => - new DynamicTool({ - name: toolData.name, - description: toolData.description || `MCP tool: ${toolData.name}`, - schema: toolData.inputSchema, - func: async (args: any) => { - try { - // Handle argument mapping for MCP tools - // LangChain may wrap args in different formats, need to handle properly - const mappedArgs = this.mapMCPToolArguments(args, toolData.inputSchema); - - console.log(`MCP tool ${toolData.name} called with original args:`, JSON.stringify(args)); - console.log(`MCP tool ${toolData.name} input schema:`, JSON.stringify(toolData.inputSchema)); - console.log(`MCP tool ${toolData.name} calling with mapped args:`, JSON.stringify(mappedArgs)); - - // Execute MCP tool through Electron API - const result = await window.desktopApi?.mcp.executeTool(toolData.name, mappedArgs); - console.log(`MCP tool ${toolData.name} returned result:`, result); - - // Extract actual result from MCP response - const actualResult = result?.result || result; - console.log(`MCP tool ${toolData.name} actual result:`, actualResult); - console.log(`MCP tool ${toolData.name} result type:`, typeof actualResult); - - // Ensure we return a string response - const response = typeof actualResult === 'string' ? actualResult : JSON.stringify(actualResult); - console.log(`MCP tool ${toolData.name} final response:`, response); - - return response; - } catch (error) { - console.error(`Error executing MCP tool ${toolData.name}:`, error); - throw error; - } - } - }) + this.mcpTools = filteredMcpTools.map( + toolData => + new DynamicTool({ + name: toolData.name, + description: toolData.description || `MCP tool: ${toolData.name}`, + schema: toolData.inputSchema, + func: async (args: any) => { + try { + // Handle argument mapping for MCP tools + // LangChain may wrap args in different formats, need to handle properly + const mappedArgs = this.mapMCPToolArguments(args, toolData.inputSchema); + + console.log( + `MCP tool ${toolData.name} called with original args:`, + JSON.stringify(args) + ); + console.log( + `MCP tool ${toolData.name} input schema:`, + JSON.stringify(toolData.inputSchema) + ); + console.log( + `MCP tool ${toolData.name} calling with mapped args:`, + JSON.stringify(mappedArgs) + ); + + // Execute MCP tool through Electron API + const result = await window.desktopApi?.mcp.executeTool( + toolData.name, + mappedArgs + ); + console.log(`MCP tool ${toolData.name} returned result:`, result); + + // Extract actual result from MCP response + const actualResult = result?.result || result; + console.log(`MCP tool ${toolData.name} actual result:`, actualResult); + console.log(`MCP tool ${toolData.name} result type:`, typeof actualResult); + + // Ensure we return a string response + const response = + typeof actualResult === 'string' ? actualResult : JSON.stringify(actualResult); + console.log(`MCP tool ${toolData.name} final response:`, response); + + return response; + } catch (error) { + console.error(`Error executing MCP tool ${toolData.name}:`, error); + throw error; + } + }, + }) ); - + console.log(`Converted ${this.mcpTools.length} MCP tools to LangChain format`); this.mcpToolsInitialized = true; - + // If we have a bound model, rebind with the new MCP tools if (this.boundModel && this.providerId) { console.log('🔄 Rebinding model with newly loaded MCP tools'); @@ -116,7 +132,10 @@ export class ToolManager { this.mcpToolsInitialized = true; } } catch (error) { - console.warn('Failed to initialize MCP tools from Electron:', error instanceof Error ? error.message : 'Unknown error'); + console.warn( + 'Failed to initialize MCP tools from Electron:', + error instanceof Error ? error.message : 'Unknown error' + ); this.mcpToolsInitialized = true; // Continue without MCP tools - this is not a fatal error } @@ -128,14 +147,14 @@ export class ToolManager { */ private mapMCPToolArguments(args: any, inputSchema?: any): any { console.log('Mapping MCP tool arguments:', { args, inputSchema }); - + if (!inputSchema) { // If no schema, return args as-is return args; } const schemaProps = inputSchema?.properties; - + // Handle tools that expect no parameters if (!schemaProps || Object.keys(schemaProps).length === 0) { // Return empty object for tools that expect no parameters @@ -144,7 +163,12 @@ export class ToolManager { } // Handle empty/null/undefined args - if (!args || args === '' || args === '""' || (typeof args === 'object' && Object.keys(args).length === 0)) { + if ( + !args || + args === '' || + args === '""' || + (typeof args === 'object' && Object.keys(args).length === 0) + ) { // Check if the tool has required parameters const requiredProps = inputSchema?.required || []; if (requiredProps.length === 0) { @@ -153,7 +177,9 @@ export class ToolManager { return {}; } else { // Tool has required parameters but got empty args - create default structure - console.log('Tool has required parameters but got empty args, creating default parameter structure'); + console.log( + 'Tool has required parameters but got empty args, creating default parameter structure' + ); return this.createDefaultParameterStructure(inputSchema); } } @@ -163,24 +189,24 @@ export class ToolManager { // Remove any non-schema fields like 'input' that shouldn't be there const schemaPropertyNames = Object.keys(schemaProps); const cleanArgs: any = {}; - + // Copy only fields that exist in the schema for (const [key, value] of Object.entries(args)) { if (schemaPropertyNames.includes(key)) { cleanArgs[key] = value; } } - + // If we found valid schema fields, use the cleaned args if (Object.keys(cleanArgs).length > 0) { console.log('Found valid schema fields, using cleaned args:', cleanArgs); return this.filterMCPArguments(cleanArgs, inputSchema); } - + // If no valid schema fields found, check for 'input' wrapper if ('input' in args && !schemaProps.input) { const inputValue = args.input; - + // Handle empty input if (!inputValue || inputValue === '' || inputValue === '""') { const requiredProps = inputSchema?.required || []; @@ -189,20 +215,25 @@ export class ToolManager { return {}; } } - + // If the input is a primitive value and the schema has only one property - if (schemaPropertyNames.length === 1 && (typeof inputValue === 'string' || typeof inputValue === 'number' || typeof inputValue === 'boolean')) { + if ( + schemaPropertyNames.length === 1 && + (typeof inputValue === 'string' || + typeof inputValue === 'number' || + typeof inputValue === 'boolean') + ) { // Map the primitive value to the single expected property console.log(`Mapping primitive input to single property: ${schemaPropertyNames[0]}`); return { [schemaPropertyNames[0]]: inputValue }; } - + // If the input is an object, try to unwrap it if (typeof inputValue === 'object' && inputValue !== null) { console.log('Unwrapping object input'); return this.filterMCPArguments(inputValue, inputSchema); } - + // For primitive values with multiple schema properties, try common mappings if (typeof inputValue === 'string') { // Try common parameter names @@ -226,7 +257,7 @@ export class ToolManager { console.log('Mapping string input to name parameter'); return { name: inputValue }; } - + // If no common mapping found, map to first required property, then first property const requiredProps = inputSchema?.required || []; const targetProp = requiredProps.length > 0 ? requiredProps[0] : schemaPropertyNames[0]; @@ -235,7 +266,7 @@ export class ToolManager { return { [targetProp]: inputValue }; } } - + // Return the unwrapped input and let the tool handle validation console.log('Returning unwrapped input value'); return inputValue; @@ -257,13 +288,13 @@ export class ToolManager { const schemaProps = inputSchema?.properties; const requiredProps = inputSchema?.required || []; - + if (!schemaProps) { return args; } const filteredArgs: any = {}; - + // Always include all required properties, even if they have empty/default values for (const requiredProp of requiredProps) { if (requiredProp in args) { @@ -288,26 +319,30 @@ export class ToolManager { } } } - + // Include optional properties only if they have actual values for (const [key, value] of Object.entries(args)) { // Skip if already included as required if (requiredProps.includes(key)) { continue; } - + // Skip if property is not in schema if (!(key in schemaProps)) { continue; } - + // Include only if value is meaningful (not empty string, null, undefined, or empty object/array) if (this.hasActualValue(value)) { filteredArgs[key] = value; } } - - console.log('Filtered MCP arguments:', { original: args, filtered: filteredArgs, required: requiredProps }); + + console.log('Filtered MCP arguments:', { + original: args, + filtered: filteredArgs, + required: requiredProps, + }); return filteredArgs; } @@ -319,17 +354,17 @@ export class ToolManager { if (value === null || value === undefined || value === '') { return false; } - + // Empty arrays are not actual values if (Array.isArray(value)) { return value.length > 0; } - + // Empty objects are not actual values if (typeof value === 'object') { return Object.keys(value).length > 0; } - + // Numbers (including 0), booleans, and non-empty strings are actual values return true; } @@ -350,7 +385,7 @@ export class ToolManager { for (const requiredProp of requiredProps) { if (requiredProp in schemaProps) { const propSchema = schemaProps[requiredProp]; - + // Create appropriate default values based on type if (propSchema.type === 'string') { defaultParams[requiredProp] = propSchema.default || ''; @@ -432,7 +467,7 @@ export class ToolManager { if (this.toolHandlers.has(toolName)) { return true; } - + // Check MCP tools return this.mcpTools.some(tool => tool.name === toolName); } @@ -483,7 +518,7 @@ export class ToolManager { mcpOutput: formatted, raw: rawResult, isError: isError || formatted.type === 'error', - originalArgs: args // Include original arguments in the formatted content for retry functionality + originalArgs: args, // Include original arguments in the formatted content for retry functionality }); } catch (formatError) { console.warn(`Failed to format MCP output for ${toolName}:`, formatError); @@ -494,7 +529,7 @@ export class ToolManager { mcpOutput: simpleFormatted, raw: rawResult, isError: isError || simpleFormatted.type === 'error', - originalArgs: args // Include original arguments in the fallback formatting too + originalArgs: args, // Include original arguments in the fallback formatting too }); } } else { @@ -505,7 +540,7 @@ export class ToolManager { message: this.extractErrorMessage(rawResult), toolName, raw: rawResult, - originalArgs: args // Include original arguments for error cases too + originalArgs: args, // Include original arguments for error cases too }); } } @@ -518,7 +553,9 @@ export class ToolManager { toolName, source: 'mcp', formatted: !!this.mcpFormatter, - isError: isError || (this.mcpFormatter && JSON.parse(formattedContent).mcpOutput?.type === 'error'), + isError: + isError || + (this.mcpFormatter && JSON.parse(formattedContent).mcpOutput?.type === 'error'), originalArgs: args, // Store original arguments for retry functionality }, }; @@ -538,8 +575,8 @@ export class ToolManager { suggestions: [ 'Check if the tool is properly configured and accessible', 'Verify the input parameters match the tool requirements', - 'Try again in a few moments as this may be a temporary issue' - ] + 'Try again in a few moments as this may be a temporary issue', + ], }, insights: ['Tool execution errors may indicate configuration or connectivity issues'], warnings: ['This tool is currently unavailable'], @@ -548,8 +585,8 @@ export class ToolManager { toolName, responseSize: 0, processingTime: 0, - dataPoints: 0 - } + dataPoints: 0, + }, }; errorContent = JSON.stringify({ @@ -557,14 +594,16 @@ export class ToolManager { mcpOutput: errorFormatted, raw: error instanceof Error ? error.message : 'Unknown error', isError: true, - originalArgs: args // Include original arguments in error formatting too + originalArgs: args, // Include original arguments in error formatting too }); } else { errorContent = JSON.stringify({ error: true, - message: `Error executing MCP tool: ${error instanceof Error ? error.message : 'Unknown error'}`, + message: `Error executing MCP tool: ${ + error instanceof Error ? error.message : 'Unknown error' + }`, toolName, - originalArgs: args // Include original arguments in simple error format too + originalArgs: args, // Include original arguments in simple error format too }); } @@ -601,8 +640,10 @@ export class ToolManager { return model; } - console.log(`Binding ${langChainTools.length} tools to ${providerId} model:`, - langChainTools.map(t => t.name)); + console.log( + `Binding ${langChainTools.length} tools to ${providerId} model:`, + langChainTools.map(t => t.name) + ); this.boundModel = model.bindTools(langChainTools); return this.boundModel; @@ -664,11 +705,13 @@ export class ToolManager { } catch { // If not JSON, check for common error patterns in the string const lowerResult = result.toLowerCase(); - return lowerResult.includes('error') || - lowerResult.includes('failed') || - lowerResult.includes('exception') || - lowerResult.includes('invalid') || - lowerResult.includes('schema mismatch'); + return ( + lowerResult.includes('error') || + lowerResult.includes('failed') || + lowerResult.includes('exception') || + lowerResult.includes('invalid') || + lowerResult.includes('schema mismatch') + ); } } diff --git a/ai-assistant/src/modal.tsx b/ai-assistant/src/modal.tsx index bc49ac1567..ebb8715068 100644 --- a/ai-assistant/src/modal.tsx +++ b/ai-assistant/src/modal.tsx @@ -42,7 +42,7 @@ export default function AIPrompt(props: { openPopup: boolean; setOpenPopup: (...args) => void; pluginSettings: any; - width: string + width: string; }) { const { openPopup, setOpenPopup, pluginSettings, width } = props; const history = useHistory(); @@ -62,8 +62,8 @@ export default function AIPrompt(props: { const prompWidthContext = usePromptWidth(); useEffect(() => { - prompWidthContext.setPromptWidth(width) - }, [width]) + prompWidthContext.setPromptWidth(width); + }, [width]); // Get cluster names for warning lookup - use selected clusters or current cluster only const clusterNames = useMemo(() => { const currentCluster = getCluster(); @@ -300,12 +300,15 @@ export default function AIPrompt(props: { } console.log('🔄 UpdateHistory called, aiManager.history length:', aiManager.history.length); - console.log('📋 Current aiManager.history:', aiManager.history.map(h => ({ - role: h.role, - hasToolConfirmation: !!h.toolConfirmation, - content: h.content?.substring(0, 50), - isDisplayOnly: h.isDisplayOnly - }))); + console.log( + '📋 Current aiManager.history:', + aiManager.history.map(h => ({ + role: h.role, + hasToolConfirmation: !!h.toolConfirmation, + content: h.content?.substring(0, 50), + isDisplayOnly: h.isDisplayOnly, + })) + ); // Process the history to extract suggestions and clean content const processedHistory = aiManager.history.map((prompt, index) => { @@ -327,12 +330,15 @@ export default function AIPrompt(props: { }); console.log('✅ ProcessedHistory length:', processedHistory.length); - console.log('📝 ProcessedHistory items:', processedHistory.map(h => ({ - role: h.role, - hasToolConfirmation: !!h.toolConfirmation, - isDisplayOnly: h.isDisplayOnly, - content: h.content?.substring(0, 50) - }))); + console.log( + '📝 ProcessedHistory items:', + processedHistory.map(h => ({ + role: h.role, + hasToolConfirmation: !!h.toolConfirmation, + isDisplayOnly: h.isDisplayOnly, + content: h.content?.substring(0, 50), + })) + ); setPromptHistory(processedHistory); }, [aiManager?.history]); @@ -546,7 +552,7 @@ export default function AIPrompt(props: { try { console.log(`Retrying tool ${toolName} with args:`, args); - + // Get the tool manager from the LangChain manager const toolManager = (aiManager as any).toolManager; if (!toolManager) { @@ -556,7 +562,7 @@ export default function AIPrompt(props: { // Execute the tool directly const toolResponse = await toolManager.executeTool(toolName, args); - + // Add the retry result to the conversation history const retryPrompt: Prompt = { role: 'tool', @@ -573,16 +579,17 @@ export default function AIPrompt(props: { await aiManager.processToolResponses(); updateHistory(); } - } catch (error) { console.error(`Error retrying tool ${toolName}:`, error); - + // Add error to conversation const errorPrompt: Prompt = { role: 'tool', content: JSON.stringify({ error: true, - message: `Failed to retry tool: ${error instanceof Error ? error.message : 'Unknown error'}`, + message: `Failed to retry tool: ${ + error instanceof Error ? error.message : 'Unknown error' + }`, toolName, }), toolCallId: `retry-error-${Date.now()}`, @@ -902,7 +909,7 @@ export default function AIPrompt(props: { handleChangeConfig(config, model); }} onTestModeResponse={handleTestModeResponse} - onToolsChange={(newEnabledTools) => { + onToolsChange={newEnabledTools => { setEnabledTools(newEnabledTools); // Recreate AI manager with new tools handleChangeConfig(activeConfig, selectedModel); diff --git a/ai-assistant/src/textstream.tsx b/ai-assistant/src/textstream.tsx index b20156a2d6..a492c1f8e0 100644 --- a/ai-assistant/src/textstream.tsx +++ b/ai-assistant/src/textstream.tsx @@ -240,7 +240,8 @@ const TextStreamContainer = React.memo(function TextStreamContainer({ const isJsonSuccess = prompt.success; if (prompt.content === '' && prompt.role === 'user') return null; - if (prompt.content === '' && prompt.role === 'assistant' && !prompt.toolConfirmation) return null; + if (prompt.content === '' && prompt.role === 'assistant' && !prompt.toolConfirmation) + return null; return ( {prompt.role === 'user' ? 'You' : 'AI Assistant'} - + {prompt.role === 'user' ? ( prompt.content ) : ( <> {isContentFilterError || hasError ? ( - + {prompt.content} {isContentFilterError && ( @@ -343,13 +349,15 @@ const TextStreamContainer = React.memo(function TextStreamContainer({ ); return ( - + Promise; - executeTool: (toolName: string, args: Record, toolCallId?: string) => Promise; + executeTool: ( + toolName: string, + args: Record, + toolCallId?: string + ) => Promise; getStatus: () => Promise<{ isInitialized: boolean; hasClient: boolean }>; resetClient: () => Promise; } diff --git a/ai-assistant/src/utils/InlineToolApprovalManager.ts b/ai-assistant/src/utils/InlineToolApprovalManager.ts index e8f1f84e8c..a53ada3cb2 100644 --- a/ai-assistant/src/utils/InlineToolApprovalManager.ts +++ b/ai-assistant/src/utils/InlineToolApprovalManager.ts @@ -35,41 +35,35 @@ export class InlineToolApprovalManager extends EventEmitter { */ private extractUserContext(aiManager: any): UserContext { const userContext: UserContext = { - timeContext: new Date() + timeContext: new Date(), }; try { // Extract user message from history const history = aiManager.history || []; - const lastUserMessage = history - .filter((msg: any) => msg.role === 'user') - .pop(); + const lastUserMessage = history.filter((msg: any) => msg.role === 'user').pop(); if (lastUserMessage) { userContext.userMessage = lastUserMessage.content; } // Extract conversation history (last 5 messages for context) - userContext.conversationHistory = history - .slice(-5) - .map((msg: any) => ({ - role: msg.role, - content: msg.content - })); + userContext.conversationHistory = history.slice(-5).map((msg: any) => ({ + role: msg.role, + content: msg.content, + })); // Extract kubernetes context if available if (aiManager.toolManager?.kubernetesContext) { userContext.kubernetesContext = { selectedClusters: aiManager.toolManager.kubernetesContext.selectedClusters, namespace: aiManager.toolManager.kubernetesContext.namespace, - currentResource: aiManager.toolManager.kubernetesContext.currentResource + currentResource: aiManager.toolManager.kubernetesContext.currentResource, }; } // Extract last tool results - const lastToolResults = history - .filter((msg: any) => msg.role === 'tool') - .slice(-3); + const lastToolResults = history.filter((msg: any) => msg.role === 'tool').slice(-3); if (lastToolResults.length > 0) { userContext.lastToolResults = {}; @@ -82,7 +76,6 @@ export class InlineToolApprovalManager extends EventEmitter { } }); } - } catch (error) { console.warn('Failed to extract user context:', error); } @@ -132,12 +125,12 @@ export class InlineToolApprovalManager extends EventEmitter { const handleApprove = (approvedToolIds: string[]) => { // Combine auto-approved and manually approved tools const allApprovedIds = [...autoApprovedTools, ...approvedToolIds]; - + // Update the message to show loading state if (this.pendingRequest?.updateMessage) { this.pendingRequest.updateMessage(true); } - + this.pendingRequest = null; resolve(allApprovedIds); }; @@ -158,8 +151,8 @@ export class InlineToolApprovalManager extends EventEmitter { onDeny: handleDeny, loading: loading, requestId: this.pendingRequest.requestId, // Include requestId - userContext: userContext // Include user context - } + userContext: userContext, // Include user context + }, }); } }; @@ -170,7 +163,7 @@ export class InlineToolApprovalManager extends EventEmitter { resolve: handleApprove, reject: handleDeny, aiManager, - updateMessage + updateMessage, }; // Emit event to add the tool confirmation message to chat history @@ -182,9 +175,9 @@ export class InlineToolApprovalManager extends EventEmitter { onDeny: handleDeny, loading: false, requestId: requestId, // Include requestId in the tool confirmation - userContext: userContext // Pass user context for intelligent argument processing + userContext: userContext, // Pass user context for intelligent argument processing }, - aiManager + aiManager, }); }); } @@ -283,4 +276,4 @@ export class InlineToolApprovalManager extends EventEmitter { } // Export singleton instance -export const inlineToolApprovalManager = InlineToolApprovalManager.getInstance(); \ No newline at end of file +export const inlineToolApprovalManager = InlineToolApprovalManager.getInstance(); diff --git a/ai-assistant/src/utils/ToolApprovalManager.ts b/ai-assistant/src/utils/ToolApprovalManager.ts index 58d49222cf..95c9766a88 100644 --- a/ai-assistant/src/utils/ToolApprovalManager.ts +++ b/ai-assistant/src/utils/ToolApprovalManager.ts @@ -67,7 +67,7 @@ export class ToolApprovalManager extends EventEmitter { return new Promise((resolve, reject) => { const requestId = `tool-approval-${Date.now()}-${Math.random()}`; - + this.pendingRequest = { requestId, toolCalls: needsApprovalTools, @@ -80,7 +80,7 @@ export class ToolApprovalManager extends EventEmitter { reject: (error: Error) => { this.pendingRequest = null; reject(error); - } + }, }; // Emit event for UI components to listen to @@ -169,10 +169,12 @@ export class ToolApprovalManager extends EventEmitter { } { return { sessionAutoApproval: this.sessionAutoApproval, - toolSettings: Array.from(this.autoApproveSettings.entries()).map(([toolName, autoApprove]) => ({ - toolName, - autoApprove, - })), + toolSettings: Array.from(this.autoApproveSettings.entries()).map( + ([toolName, autoApprove]) => ({ + toolName, + autoApprove, + }) + ), }; } } diff --git a/ai-assistant/src/utils/ToolConfigManager.ts b/ai-assistant/src/utils/ToolConfigManager.ts index 2da84a63c2..8f21340479 100644 --- a/ai-assistant/src/utils/ToolConfigManager.ts +++ b/ai-assistant/src/utils/ToolConfigManager.ts @@ -56,7 +56,7 @@ export function getEnabledToolIds(pluginSettings: any): string[] { // Sets the enabled tools list in plugin settings export function setEnabledTools(pluginSettings: any, enabledToolIds: string[]): any { const enabledTools: Record = {}; - + // Get all available tools and set their enabled state const allTools = getAllAvailableTools(); allTools.forEach(tool => { From 9ab77586c84196aa8329ece0b3ca816ce9dec7dc Mon Sep 17 00:00:00 2001 From: ashu8912 Date: Tue, 23 Sep 2025 10:38:55 +0530 Subject: [PATCH 18/39] ai-plugin: Markdown rendering for document responses and UI fixes Signed-off-by: ashu8912 --- .../components/chat/MCPFormattedMessage.tsx | 1 - .../components/mcpOutput/MCPOutputDisplay.tsx | 477 ++++++++++++++++-- .../formatters/MCPOutputFormatter.ts | 121 ++++- 3 files changed, 550 insertions(+), 49 deletions(-) diff --git a/ai-assistant/src/components/chat/MCPFormattedMessage.tsx b/ai-assistant/src/components/chat/MCPFormattedMessage.tsx index 233478590b..7b3575ebef 100644 --- a/ai-assistant/src/components/chat/MCPFormattedMessage.tsx +++ b/ai-assistant/src/components/chat/MCPFormattedMessage.tsx @@ -131,7 +131,6 @@ const MCPFormattedMessage: React.FC = ({ \s+/m, // Blockquotes + /^\s*\|.*\|/m, // Tables + ]; + + // Count how many patterns match + const matchCount = markdownPatterns.filter(pattern => pattern.test(content)).length; + + // If we find 2 or more markdown patterns, consider it markdown + return matchCount >= 2; + } + + return false; +} + +// Markdown Renderer Component +const MarkdownRenderer: React.FC<{ data: any; width: string; syntaxTheme: any }> = ({ + data, + width, + syntaxTheme, +}) => { + const theme = useTheme(); + const [showFullContent, setShowFullContent] = useState(false); + + // Check if content appears to be truncated + const isTruncated = + data.content && + (data.content.includes('[Content truncated for display') || + data.content.includes('[Output truncated...]') || + data.content.endsWith('...')); + + const displayContent = showFullContent ? data.fullContent || data.content : data.content; + + return ( + + {data.highlights && data.highlights.length > 0 && ( + + {data.highlights.map((highlight: string, index: number) => ( + + ))} + + )} + + {/* Truncation notification */} + {isTruncated && !showFullContent && ( + setShowFullContent(true)} + startIcon={} + > + Show Full Content + + } + > + + Content has been truncated for display. Click "Show Full Content" to view the complete + documentation. + + + )} + + + + {String(children).replace(/\n$/, '')} + + ); + } + + return ( + + {children} + + ); + }, + }} + > + {displayContent} + + + + {/* Collapse button for expanded content */} + {showFullContent && isTruncated && ( + + + + )} + + ); +}; + const MCPOutputDisplay: React.FC = ({ output, onRetry, @@ -144,7 +436,8 @@ const MCPOutputDisplay: React.FC = ({ variant="outlined" sx={{ mb: 2, - borderColor: theme.palette[getStatusColor()].main, + backgroundColor: theme.palette.background.paper, + borderColor: output.type === 'error' ? theme.palette.error.main : theme.palette.divider, borderWidth: output.type === 'error' ? 2 : 1, width: '100%', // Use 100% of parent container (which has padding) maxWidth: 'none', // Override any inherited maxWidth @@ -257,13 +550,41 @@ const MCPOutputDisplay: React.FC = ({ {/* Insights */} {output.insights && output.insights.length > 0 && ( - - - + + + Key Insights: {output.insights.map((insight, index) => ( - + • {insight} ))} @@ -272,13 +593,41 @@ const MCPOutputDisplay: React.FC = ({ {/* Actionable Items */} {output.actionable_items && output.actionable_items.length > 0 && ( - - - + + + Recommended Actions: {output.actionable_items.map((item, index) => ( - + • {item} ))} @@ -338,6 +687,7 @@ const MCPOutputDisplay: React.FC = ({ // Table Display Component const TableDisplay: React.FC<{ data: any; width: string }> = ({ data, width }) => { + const theme = useTheme(); const [sortBy, setSortBy] = useState(data.sortBy || null); const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('asc'); @@ -370,14 +720,15 @@ const TableDisplay: React.FC<{ data: any; width: string }> = ({ data, width }) = component={Paper} variant="outlined" sx={{ - width: width, // Use fixed pixel width + width: width, // Use fixed pixel width for container maxWidth: 'none', minWidth: 0, - // Only use horizontal scroll as absolute last resort - overflowX: 'auto', + overflowX: 'auto', // Enable horizontal scrolling + overflowY: 'visible', + backgroundColor: theme.palette.background.paper, + borderColor: theme.palette.divider, '& .MuiTable-root': { - width: width, // Use fixed pixel width - minWidth: 'auto', + minWidth: 'max-content', // Allow table to be wider than container tableLayout: 'auto', // Let table size itself naturally }, }} @@ -385,13 +736,30 @@ const TableDisplay: React.FC<{ data: any; width: string }> = ({ data, width }) =
    @@ -401,14 +769,30 @@ const TableDisplay: React.FC<{ data: any; width: string }> = ({ data, width }) = handleSort(header)} - sx={{ cursor: 'pointer', fontWeight: 'bold' }} + sx={{ + cursor: 'pointer', + fontWeight: 'bold', + minWidth: '120px', + maxWidth: '300px', + whiteSpace: 'nowrap', + overflow: 'hidden', + textOverflow: 'ellipsis', + paddingX: 1, + '&:first-of-type': { + paddingLeft: 2, + }, + '&:last-of-type': { + paddingRight: 2, + }, + }} + title={header} // Show full header on hover > - {header} + {header} {sortBy === header && ( )} @@ -422,22 +806,31 @@ const TableDisplay: React.FC<{ data: any; width: string }> = ({ data, width }) = key={rowIndex} sx={{ backgroundColor: data.highlightRows?.includes(rowIndex) - ? 'warning.light' - : undefined, + ? theme.palette.warning.light + : 'transparent', + '&:hover': { + backgroundColor: theme.palette.action.hover, + }, }} > {row.map((cell: any, cellIndex: number) => ( {typeof cell === 'object' ? JSON.stringify(cell) : String(cell)} @@ -452,6 +845,8 @@ const TableDisplay: React.FC<{ data: any; width: string }> = ({ data, width }) = // Metrics Display Component const MetricsDisplay: React.FC<{ data: any; width: string }> = ({ data, width }) => { + const theme = useTheme(); + const getStatusColor = (status: string) => { switch (status) { case 'error': @@ -500,6 +895,8 @@ const MetricsDisplay: React.FC<{ data: any; width: string }> = ({ data, width }) overflowWrap: 'break-word', wordWrap: 'break-word', wordBreak: 'break-word', + backgroundColor: theme.palette.background.paper, + borderColor: theme.palette.divider, }} > = ({ data, width }) // List Display Component const ListDisplay: React.FC<{ data: any; width: string }> = ({ data, width }) => { + const theme = useTheme(); + const getStatusColor = (status: string) => { switch (status) { case 'error': - return 'error.main'; + return theme.palette.error.main; case 'warning': - return 'warning.main'; + return theme.palette.warning.main; case 'info': - return 'info.main'; + return theme.palette.info.main; default: - return 'text.primary'; + return theme.palette.text.primary; } }; @@ -679,6 +1078,8 @@ const ListDisplay: React.FC<{ data: any; width: string }> = ({ data, width }) => overflowWrap: 'break-word', wordWrap: 'break-word', wordBreak: 'break-word', + backgroundColor: theme.palette.background.paper, + borderColor: theme.palette.divider, }} > @@ -723,6 +1124,12 @@ const TextDisplay: React.FC<{ data: any; theme: any; width: string }> = ({ theme, width, }) => { + // Check if the content should be rendered as markdown + if (isMarkdownContent(data)) { + return ; + } + + // Default to syntax highlighting for non-markdown content return ( ): string { - const truncatedOutput = this.truncateIfNeeded(rawOutput, 10000); // Limit to ~10k chars + // Detect if this is documentation content + const isDocumentation = this.isDocumentationContent(rawOutput, toolName); + + // Use higher limits for documentation content + const maxLength = isDocumentation ? 25000 : 10000; + const truncatedOutput = this.truncateIfNeeded(rawOutput, maxLength); // Detect if this is likely an error const isError = this.detectError(rawOutput); @@ -203,6 +208,10 @@ Remember: Focus on making complex data accessible and actionable for Kubernetes ? '\n\nIMPORTANT: This appears to be an error response. Use "error" type and provide helpful troubleshooting guidance.' : ''; + const docHint = isDocumentation + ? '\n\nIMPORTANT: This appears to be documentation content. Use "text" type with language="markdown" to enable proper markdown rendering.' + : ''; + return `Analyze and format this ${toolName} tool output: TOOL: ${toolName} @@ -218,14 +227,15 @@ Pay special attention to: 1. Identifying the most appropriate visualization type (or "error" if this is an error) 2. For errors: Provide clear, actionable troubleshooting steps 3. For data: Extract key metrics and patterns -4. Highlighting any security or performance issues -5. Providing actionable recommendations +4. For documentation: Use markdown formatting and preserve structure +5. Highlighting any security or performance issues +6. Providing actionable recommendations -${errorHint} +${errorHint}${docHint} ${ truncatedOutput.length < rawOutput.length - ? `\n[Note: Output was truncated from ${rawOutput.length} to ${truncatedOutput.length} characters for analysis]` + ? `\n[Note: Output was truncated from ${rawOutput.length} to ${truncatedOutput.length} characters for analysis. Original content size: ${rawOutput.length} characters]` : '' }`; } @@ -253,6 +263,72 @@ ${ } } + /** + * Detect if content is documentation + */ + private isDocumentationContent(rawOutput: string, toolName: string): boolean { + // Check tool name patterns + const docToolPatterns = [ + 'documentation', + 'docs', + 'fetch', + 'microsoft', + 'azure', + 'guide', + 'tutorial', + 'manual', + 'readme', + ]; + + const toolNameLower = toolName.toLowerCase(); + const isDocTool = docToolPatterns.some(pattern => toolNameLower.includes(pattern)); + + // Check content patterns + const docContentPatterns = [ + /^#{1,6}\s+/m, // Markdown headers + /```[\s\S]*?```/, // Code blocks + /\[.*?\]\(.*?\)/, // Markdown links + /^\s*[-*+]\s+/m, // Lists + /^\s*\d+\.\s+/m, // Numbered lists + /^\s*>\s+/m, // Blockquotes + /\*\*[^*]+\*\*/, // Bold text + /\*[^*]+\*/, // Italic text + /`[^`]+`/, // Inline code + ]; + + const contentMatches = docContentPatterns.filter(pattern => pattern.test(rawOutput)).length; + + // Check for common documentation keywords + const docKeywords = [ + 'prerequisites', + 'installation', + 'configuration', + 'getting started', + 'tutorial', + 'example', + 'usage', + 'overview', + 'introduction', + 'documentation', + 'azure', + 'microsoft', + 'learn.microsoft.com', + ]; + + const contentLower = rawOutput.toLowerCase(); + const keywordMatches = docKeywords.filter(keyword => contentLower.includes(keyword)).length; + + // Consider it documentation if: + // 1. Tool name suggests documentation OR + // 2. Multiple markdown patterns + documentation keywords OR + // 3. Very large content with some doc patterns (likely fetched docs) + return ( + isDocTool || + (contentMatches >= 3 && keywordMatches >= 2) || + (rawOutput.length > 20000 && contentMatches >= 2) + ); + } + /** * Parse AI response and validate structure */ @@ -304,6 +380,7 @@ ${ // Try to detect if it's JSON let data: any; let type: FormattedMCPOutput['type'] = 'text'; + const warnings: string[] = ['AI formatting failed - showing raw output']; try { const parsed = JSON.parse(rawOutput); @@ -330,13 +407,31 @@ ${ } catch { // Not JSON, treat as text type = 'text'; - data = { - content: - rawOutput.length > 5000 - ? rawOutput.substring(0, 5000) + '\n\n[Output truncated...]' - : rawOutput, - language: 'text', - }; + + // Check if this is documentation content + const isDocumentation = this.isDocumentationContent(rawOutput, toolName); + + // Use higher limits for documentation + const maxLength = isDocumentation ? 15000 : 5000; + + if (rawOutput.length > maxLength) { + const truncatedContent = rawOutput.substring(0, maxLength); + warnings.push(`Content truncated from ${rawOutput.length} to ${maxLength} characters`); + + data = { + content: + truncatedContent + + '\n\n[Content truncated for display. Original size: ' + + rawOutput.length + + ' characters]', + language: isDocumentation ? 'markdown' : 'text', + }; + } else { + data = { + content: rawOutput, + language: isDocumentation ? 'markdown' : 'text', + }; + } } return { @@ -345,7 +440,7 @@ ${ summary: `Raw output from ${toolName}. AI formatting was not available.`, data, insights: [], - warnings: ['AI formatting failed - showing raw output'], + warnings, actionable_items: ['Consider checking the AI service connection'], metadata: { toolName, From e136bae558f90c43119854b62b1a9161667611bb Mon Sep 17 00:00:00 2001 From: ashu8912 Date: Tue, 23 Sep 2025 10:39:32 +0530 Subject: [PATCH 19/39] ai-plugin: Enhance tool arguments through ai calls Signed-off-by: ashu8912 --- .../mcpOutput/MCPArgumentProcessor.ts | 36 ++- .../src/langchain/LangChainManager.ts | 213 +++++++++++++++++- 2 files changed, 232 insertions(+), 17 deletions(-) diff --git a/ai-assistant/src/components/mcpOutput/MCPArgumentProcessor.ts b/ai-assistant/src/components/mcpOutput/MCPArgumentProcessor.ts index fe7cc07d2f..2e01631bbc 100644 --- a/ai-assistant/src/components/mcpOutput/MCPArgumentProcessor.ts +++ b/ai-assistant/src/components/mcpOutput/MCPArgumentProcessor.ts @@ -75,6 +75,25 @@ export class MCPArgumentProcessor { const processed = { ...aiProcessedArgs }; const intelligentFills: Record = {}; + // Check if arguments were enhanced by LLM + const llmEnhanced = aiProcessedArgs._llmEnhanced; + if (llmEnhanced) { + // Remove metadata before processing + delete processed._llmEnhanced; + + // Mark LLM-enhanced fields in intelligentFills + for (const fieldName of llmEnhanced.enhancedFields || []) { + if (fieldName in processed) { + intelligentFills[fieldName] = { + value: processed[fieldName], + reason: `AI-enhanced based on user request analysis`, + confidence: 0.9, // High confidence for LLM-enhanced fields + }; + } + } + } + + console.log('schema for this tool is ', toolName, schema); if (!schema) { errors.push(`No schema found for tool: ${toolName}`); return { @@ -95,11 +114,15 @@ export class MCPArgumentProcessor { if (fieldSchema) { // Provide appropriate empty value based on type processed[requiredField] = this.getEmptyValueForRequiredField(fieldSchema); - intelligentFills[requiredField] = { - value: processed[requiredField], - reason: `Required field provided with empty ${fieldSchema.type}`, - confidence: 0.8, - }; + + // Only mark as intelligent fill if not already enhanced by LLM + if (!intelligentFills[requiredField]) { + intelligentFills[requiredField] = { + value: processed[requiredField], + reason: `Required field provided with empty ${fieldSchema.type}`, + confidence: 0.8, + }; + } } } } @@ -370,6 +393,9 @@ export class MCPArgumentProcessor { const properties = schema.inputSchema.properties || {}; for (const [key, value] of Object.entries(args)) { + // Skip LLM metadata + if (key === '_llmEnhanced') continue; + const isRequired = required.includes(key); const propertySchema = properties[key]; const hasDefault = propertySchema?.default !== undefined; diff --git a/ai-assistant/src/langchain/LangChainManager.ts b/ai-assistant/src/langchain/LangChainManager.ts index 039be487d7..0a2c0d898c 100644 --- a/ai-assistant/src/langchain/LangChainManager.ts +++ b/ai-assistant/src/langchain/LangChainManager.ts @@ -725,6 +725,9 @@ The user is waiting for you to explain what the tools discovered. Provide a dire // Build user context from current conversation const userContext = this.buildUserContext(); + // Store original arguments for comparison + const originalArguments = { ...processedArguments }; + // Use AI to intelligently prepare arguments processedArguments = await this.enhanceArgumentsWithAI( toolName, @@ -737,6 +740,13 @@ The user is waiting for you to explain what the tools discovered. Provide a dire original: JSON.parse(tc.function.arguments), enhanced: processedArguments, }); + + // Mark which fields were enhanced by LLM for UI display + processedArguments._llmEnhanced = { + enhanced: true, + originalArgs: originalArguments, + enhancedFields: this.identifyEnhancedFields(originalArguments, processedArguments), + }; } } catch (error) { console.warn(`Failed to enhance arguments for ${toolName}:`, error); @@ -1568,7 +1578,7 @@ Format your response to make the errors prominent and actionable.`, * Enhance arguments using AI-like intelligence */ private async enhanceArgumentsWithAI( - _toolName: string, + toolName: string, toolSchema: any, userContext: UserContext, originalArgs: Record @@ -1579,24 +1589,203 @@ Format your response to make the errors prominent and actionable.`, return enhanced; } - const properties = toolSchema.inputSchema.properties; - const required = toolSchema.inputSchema.required || []; + try { + // Use LLM to intelligently prepare arguments based on user context and tool schema + const llmEnhancedArgs = await this.prepareLLMArguments( + toolName, + toolSchema, + userContext, + originalArgs + ); + + // Merge LLM suggestions with original arguments, preferring LLM suggestions + Object.assign(enhanced, llmEnhancedArgs); + } catch (error) { + console.warn(`Failed to get LLM enhancement for ${toolName}:`, error); + // Fall back to basic enhancement + const properties = toolSchema.inputSchema.properties; + const required = toolSchema.inputSchema.required || []; + + // Fill in required fields that are missing or empty + for (const [fieldName, fieldSchema] of Object.entries(properties)) { + const isRequired = required.includes(fieldName); + const currentValue = enhanced[fieldName]; + + if ( + isRequired && + (currentValue === undefined || currentValue === null || currentValue === '') + ) { + // Provide intelligent defaults based on field type and context + enhanced[fieldName] = this.getIntelligentDefault(fieldName, fieldSchema, userContext); + } + } + } + + return enhanced; + } + + /** + * Use LLM to prepare intelligent arguments based on user request and tool schema + */ + private async prepareLLMArguments( + toolName: string, + toolSchema: any, + userContext: UserContext, + originalArgs: Record + ): Promise> { + // Build prompt for argument preparation + const argumentPreparationPrompt = this.createArgumentPreparationPrompt( + toolName, + toolSchema, + userContext, + originalArgs + ); + + try { + // Use the existing model instance but without tools to avoid recursive tool calls + const response = await this.model.invoke([ + { role: 'system', content: argumentPreparationPrompt.system }, + { role: 'user', content: argumentPreparationPrompt.user }, + ]); + + // Parse the LLM response to extract arguments + const responseText = this.extractTextContent(response.content); + const parsedArgs = this.parseArgumentsFromLLMResponse(responseText); + + return parsedArgs; + } catch (error) { + console.warn('Failed to prepare arguments with LLM:', error); + return {}; + } + } + + /** + * Create a prompt for the LLM to prepare tool arguments + */ + private createArgumentPreparationPrompt( + toolName: string, + toolSchema: any, + userContext: UserContext, + originalArgs: Record + ): { system: string; user: string } { + const properties = toolSchema.inputSchema?.properties || {}; + const required = toolSchema.inputSchema?.required || []; + + // Create a description of the tool schema + const schemaDescription = Object.entries(properties) + .map(([fieldName, fieldSchema]: [string, any]) => { + const isReq = required.includes(fieldName) ? ' (REQUIRED)' : ' (optional)'; + const type = fieldSchema.type || 'any'; + const desc = fieldSchema.description || 'No description'; + + // Handle nested properties for complex objects + let nestedProps = ''; + if (fieldSchema.properties) { + nestedProps = + '\n Nested properties:\n' + + Object.entries(fieldSchema.properties) + .map( + ([nestedName, nestedSchema]: [string, any]) => + ` - ${nestedName} (${nestedSchema.type || 'any'}): ${ + nestedSchema.description || 'No description' + }` + ) + .join('\n'); + } + + return `- ${fieldName}${isReq} (${type}): ${desc}${nestedProps}`; + }) + .join('\n'); + + const system = `You are an expert at preparing tool arguments based on user requests. Your task is to analyze the user's request and generate appropriate arguments for the "${toolName}" tool. + +TOOL SCHEMA: +${schemaDescription} + +INSTRUCTIONS: +1. Analyze the user's request to understand their intent +2. Map their natural language request to the appropriate tool arguments +3. For complex objects (like params), fill in the nested properties based on the user's requirements +4. Use the conversation context to infer missing details +5. Return ONLY a valid JSON object with the tool arguments +6. If a required field cannot be determined from the user's request, provide a sensible default + +RESPONSE FORMAT: +Return only valid JSON with the tool arguments. No explanations, no markdown, just the JSON.`; + + const conversationContext = + userContext.conversationHistory + ?.slice(-5) + .map(msg => `${msg.role}: ${msg.content}`) + .join('\n') || ''; + + const user = `USER REQUEST: "${userContext.userMessage}" + +CONVERSATION CONTEXT: +${conversationContext} + +CURRENT ARGUMENTS: ${JSON.stringify(originalArgs, null, 2)} + +Based on the user's request and the tool schema above, generate the appropriate arguments for the "${toolName}" tool. Focus on mapping the user's intent to the correct parameter values. - // Fill in required fields that are missing or empty - for (const [fieldName, fieldSchema] of Object.entries(properties)) { - const isRequired = required.includes(fieldName); - const currentValue = enhanced[fieldName]; +For example, if the user says "get me info only from gadget namespace", the params object should include: +{"operator.KubeManager.namespace": "gadget"} +Return the complete arguments object:`; + + return { system, user }; + } + + /** + * Parse arguments from LLM response + */ + private parseArgumentsFromLLMResponse(response: string): Record { + try { + // Try to extract JSON from the response + const jsonMatch = response.match(/\{[\s\S]*\}/); + if (jsonMatch) { + return JSON.parse(jsonMatch[0]); + } + + // If no JSON found, try to parse the entire response + return JSON.parse(response.trim()); + } catch (error) { + console.warn('Failed to parse LLM response for arguments:', error, response); + return {}; + } + } + + /** + * Identify which fields were enhanced by comparing original and enhanced arguments + */ + private identifyEnhancedFields( + original: Record, + enhanced: Record + ): string[] { + const enhancedFields: string[] = []; + + // Compare each field to see what was added or modified + for (const [key, enhancedValue] of Object.entries(enhanced)) { + if (key === '_llmEnhanced') continue; // Skip metadata + + const originalValue = original[key]; + + // Field is enhanced if: + // 1. It didn't exist in original + // 2. It was null/undefined/empty in original but has value now + // 3. The value is different if ( - isRequired && - (currentValue === undefined || currentValue === null || currentValue === '') + !(key in original) || + originalValue === null || + originalValue === undefined || + originalValue === '' || + JSON.stringify(originalValue) !== JSON.stringify(enhancedValue) ) { - // Provide intelligent defaults based on field type and context - enhanced[fieldName] = this.getIntelligentDefault(fieldName, fieldSchema, userContext); + enhancedFields.push(key); } } - return enhanced; + return enhancedFields; } /** From 0ccdf6a5402aba5ad8be27f87db2449ad12c2403 Mon Sep 17 00:00:00 2001 From: ashu8912 Date: Mon, 29 Sep 2025 19:41:42 +0530 Subject: [PATCH 20/39] ai-plugin: Add search to mcp tools Signed-off-by: ashu8912 --- ai-assistant/src/ai/mcp/electron-client.ts | 1 + .../src/components/assistant/ToolsDialog.tsx | 206 ++++++++++++------ 2 files changed, 144 insertions(+), 63 deletions(-) diff --git a/ai-assistant/src/ai/mcp/electron-client.ts b/ai-assistant/src/ai/mcp/electron-client.ts index 1c741a5cda..78ac165643 100644 --- a/ai-assistant/src/ai/mcp/electron-client.ts +++ b/ai-assistant/src/ai/mcp/electron-client.ts @@ -5,6 +5,7 @@ interface MCPTool { name: string; description?: string; inputSchema?: any; + server?: string; // Add server information } interface MCPResponse { diff --git a/ai-assistant/src/components/assistant/ToolsDialog.tsx b/ai-assistant/src/components/assistant/ToolsDialog.tsx index 0cfad14e9f..9ed149cb0d 100644 --- a/ai-assistant/src/components/assistant/ToolsDialog.tsx +++ b/ai-assistant/src/components/assistant/ToolsDialog.tsx @@ -1,23 +1,28 @@ import { Icon } from '@iconify/react'; +import { Dialog } from '@kinvolk/headlamp-plugin/lib/CommonComponents'; import { + Accordion, + AccordionDetails, + AccordionSummary, Box, Button, Chip, CircularProgress, - Dialog, DialogActions, DialogContent, DialogTitle, Divider, + InputAdornment, List, ListItem, ListItemSecondaryAction, ListItemText, Switch, + TextField, Typography, } from '@mui/material'; import React, { useEffect, useState } from 'react'; -import tools from '../../ai/mcp/electron-client'; +import { ElectronMCPClient } from '../../ai/mcp/electron-client'; import { AVAILABLE_TOOLS } from '../../langchain/tools/registry'; interface MCPTool { @@ -41,7 +46,9 @@ export const ToolsDialog: React.FC = ({ }) => { const [localEnabledTools, setLocalEnabledTools] = useState(enabledTools); const [mcpTools, setMcpTools] = useState([]); - const [loadingMcpTools, setLoadingMcpTools] = useState(false); + const [isLoadingMcp, setIsLoadingMcp] = useState(true); + const [searchQuery, setSearchQuery] = useState(''); + const [expandedServers, setExpandedServers] = useState>(new Set(['MCP Tools'])); // Load MCP tools when dialog opens useEffect(() => { @@ -56,22 +63,48 @@ export const ToolsDialog: React.FC = ({ }, [enabledTools]); const loadMcpTools = async () => { - setLoadingMcpTools(true); + console.log('ToolsDialog: Starting to load MCP tools...'); + setIsLoadingMcp(true); try { - const mcpToolsData = await tools(); - setMcpTools(mcpToolsData || []); + const mcpClient = new ElectronMCPClient(); + console.log('ToolsDialog: Created MCP client, isAvailable:', mcpClient.isAvailable()); + const tools = await mcpClient.getTools(); + console.log('ToolsDialog: Received tools from client:', tools.length, 'tools'); + console.log('ToolsDialog: Tools:', tools); + setMcpTools(tools); } catch (error) { - console.warn('Failed to load MCP tools:', error); + console.error('ToolsDialog: Failed to load MCP tools:', error); setMcpTools([]); } finally { - setLoadingMcpTools(false); + setIsLoadingMcp(false); } }; const handleToggleTool = (toolName: string) => { - setLocalEnabledTools(prev => - prev.includes(toolName) ? prev.filter(name => name !== toolName) : [...prev, toolName] - ); + const newEnabledTools = localEnabledTools.includes(toolName) + ? localEnabledTools.filter(name => name !== toolName) + : [...localEnabledTools, toolName]; + setLocalEnabledTools(newEnabledTools); + }; + + // Filter tools based on search query + const filteredMcpTools = mcpTools.filter( + tool => + tool.name.toLowerCase().includes(searchQuery.toLowerCase()) || + (tool.description && tool.description.toLowerCase().includes(searchQuery.toLowerCase())) + ); + + // Group tools by server (simplified version for now) + const groupedTools = { 'MCP Tools': filteredMcpTools }; + + const handleToggleServer = (serverName: string) => { + const newExpanded = new Set(expandedServers); + if (newExpanded.has(serverName)) { + newExpanded.delete(serverName); + } else { + newExpanded.add(serverName); + } + setExpandedServers(newExpanded); }; const handleSave = () => { @@ -101,11 +134,29 @@ export const ToolsDialog: React.FC = ({ MCP Tools - External Model Context Protocol tools (always enabled) + These are Model Context Protocol tools that provide additional capabilities to the + assistant. + + {/* Search Bar */} + setSearchQuery(e.target.value)} + size="small" + sx={{ mt: 2 }} + InputProps={{ + startAdornment: ( + + + + ), + }} + /> - {loadingMcpTools ? ( + {isLoadingMcp ? ( @@ -113,59 +164,88 @@ export const ToolsDialog: React.FC = ({ ) : ( - - {mcpTools.length > 0 ? ( - mcpTools.map((tool, index) => ( - - - - - - {tool.name} - - - } - secondary={tool.description || 'External MCP tool'} - /> - - handleToggleTool(tool.name)} - checked={localEnabledTools.includes(tool.name)} - color="primary" - /> - - - )) - ) : ( - - - No MCP tools available. Configure MCP servers to see tools here. - - } - /> - + <> + {Object.entries(groupedTools).map(([serverName, tools]) => ( + handleToggleServer(serverName)} + sx={{ mb: 1 }} + > + }> + + {serverName} ({tools.length}) + + + + + {tools.map((tool, index) => ( + <> + + + + + + + {tool.name} + + + } + secondary={tool.description} + /> + + + handleToggleTool(tool.name)} + checked={localEnabledTools.includes(tool.name)} + /> + + + {index < tools.length - 1 && } + + ))} + + + + ))} + + {filteredMcpTools.length === 0 && mcpTools.length > 0 && ( + + No tools match your search query. + + )} + + {mcpTools.length === 0 && ( + + No MCP tools available. Connect to MCP servers to see available tools. + )} - + )} - ); - - // Get tool categories + ); // Get tool categories const kubernetesTools = AVAILABLE_TOOLS.filter(ToolClass => { const tempTool = new ToolClass(); return tempTool.config.name.includes('kubernetes') || tempTool.config.name.includes('k8s'); From 69ece1426db1031c1a96607fea914ea29e7a33c4 Mon Sep 17 00:00:00 2001 From: Joaquim Rocha Date: Tue, 30 Sep 2025 15:38:22 +0100 Subject: [PATCH 21/39] ai-assistant: Fix rendering of messages especially with bullet points The "white-space: pre-wrap" left lots of spacing around the text. Signed-off-by: Joaquim Rocha Signed-off-by: ashu8912 --- ai-assistant/src/textstream.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/ai-assistant/src/textstream.tsx b/ai-assistant/src/textstream.tsx index a492c1f8e0..641ae75fc8 100644 --- a/ai-assistant/src/textstream.tsx +++ b/ai-assistant/src/textstream.tsx @@ -285,7 +285,6 @@ const TextStreamContainer = React.memo(function TextStreamContainer({ Date: Tue, 30 Sep 2025 18:44:37 +0100 Subject: [PATCH 22/39] ai-assistant: List MCP tools per MCP server in the tools dialog Signed-off-by: Joaquim Rocha Signed-off-by: ashu8912 --- ai-assistant/src/ai/mcp/electron-client.ts | 17 ++ .../src/components/assistant/ToolsDialog.tsx | 154 ++++++++++++++++-- 2 files changed, 155 insertions(+), 16 deletions(-) diff --git a/ai-assistant/src/ai/mcp/electron-client.ts b/ai-assistant/src/ai/mcp/electron-client.ts index 78ac165643..7b21db2836 100644 --- a/ai-assistant/src/ai/mcp/electron-client.ts +++ b/ai-assistant/src/ai/mcp/electron-client.ts @@ -139,6 +139,23 @@ class ElectronMCPClient { return false; } } + + /** + * Get MCP configuration from Electron main process + */ + async getConfig(): Promise<{ success: boolean; config?: any; error?: string }> { + if (!this.isElectron) { + return { success: false, error: 'MCP client not available - not running in Electron environment' }; + } + + try { + const response = await window.desktopApi!.mcp.getConfig(); + return response; + } catch (error) { + console.error('Error getting MCP config:', error); + return { success: false, error: String(error) }; + } + } } // Export a function that returns tools (compatible with existing interface) diff --git a/ai-assistant/src/components/assistant/ToolsDialog.tsx b/ai-assistant/src/components/assistant/ToolsDialog.tsx index 9ed149cb0d..7011fb111b 100644 --- a/ai-assistant/src/components/assistant/ToolsDialog.tsx +++ b/ai-assistant/src/components/assistant/ToolsDialog.tsx @@ -29,6 +29,7 @@ interface MCPTool { name: string; description?: string; inputSchema?: any; + server?: string; } interface ToolsDialogProps { @@ -48,7 +49,8 @@ export const ToolsDialog: React.FC = ({ const [mcpTools, setMcpTools] = useState([]); const [isLoadingMcp, setIsLoadingMcp] = useState(true); const [searchQuery, setSearchQuery] = useState(''); - const [expandedServers, setExpandedServers] = useState>(new Set(['MCP Tools'])); + const [expandedServers, setExpandedServers] = useState>(new Set()); + const [mcpServers, setMcpServers] = useState<{[key: string]: any}>({}); // Load MCP tools when dialog opens useEffect(() => { @@ -68,13 +70,67 @@ export const ToolsDialog: React.FC = ({ try { const mcpClient = new ElectronMCPClient(); console.log('ToolsDialog: Created MCP client, isAvailable:', mcpClient.isAvailable()); - const tools = await mcpClient.getTools(); + + // Load both tools and server configuration + const [tools, configResponse] = await Promise.all([ + mcpClient.getTools(), + mcpClient.getConfig() + ]); + console.log('ToolsDialog: Received tools from client:', tools.length, 'tools'); console.log('ToolsDialog: Tools:', tools); - setMcpTools(tools); + console.log('ToolsDialog: Config response:', configResponse); + + // Extract server names from config + let servers: {[key: string]: any} = {}; + if (configResponse.success && configResponse.config && configResponse.config.servers) { + servers = configResponse.config.servers.reduce((acc: {[key: string]: any}, server: any) => { + acc[server.name] = server; + return acc; + }, {}); + setMcpServers(servers); + } + + // Assign server names to tools based on available servers + // If we have server config, try to match tools to servers + // For now, if there's only one enabled server, assign all tools to it + const enabledServers = Object.values(servers).filter((server: any) => server.enabled); + const toolsWithServer = tools.map(tool => { + if (tool.server) { + return tool; // Tool already has server info + } + + // If we have exactly one enabled server, assign all tools to it + if (enabledServers.length === 1) { + return { ...tool, server: enabledServers[0].name }; + } + + // Otherwise, try to infer from tool name or keep as unknown + const serverNames = Object.keys(servers); + for (const serverName of serverNames) { + if (tool.name.toLowerCase().includes(serverName.toLowerCase().replace('-mcp', ''))) { + return { ...tool, server: serverName }; + } + } + + return { ...tool, server: 'Unknown Server' }; + }); + + setMcpTools(toolsWithServer); + + // Auto-expand servers that have tools + const serversWithTools = new Set(); + toolsWithServer.forEach(tool => { + if (tool.server) { + serversWithTools.add(tool.server); + } + }); + setExpandedServers(serversWithTools); + } catch (error) { console.error('ToolsDialog: Failed to load MCP tools:', error); setMcpTools([]); + setMcpServers({}); } finally { setIsLoadingMcp(false); } @@ -87,6 +143,38 @@ export const ToolsDialog: React.FC = ({ setLocalEnabledTools(newEnabledTools); }; + const handleToggleServer = (serverName: string) => { + const serverTools = mcpTools.filter(tool => tool.server === serverName); + const serverToolNames = serverTools.map(tool => tool.name); + + // Check if all tools from this server are currently enabled + const allEnabled = serverToolNames.every(toolName => localEnabledTools.includes(toolName)); + + let newEnabledTools: string[]; + if (allEnabled) { + // Disable all tools from this server + newEnabledTools = localEnabledTools.filter(toolName => !serverToolNames.includes(toolName)); + } else { + // Enable all tools from this server + newEnabledTools = [...new Set([...localEnabledTools, ...serverToolNames])]; + } + + setLocalEnabledTools(newEnabledTools); + }; + + const isServerEnabled = (serverName: string) => { + const serverTools = mcpTools.filter(tool => tool.server === serverName); + const serverToolNames = serverTools.map(tool => tool.name); + return serverToolNames.length > 0 && serverToolNames.every(toolName => localEnabledTools.includes(toolName)); + }; + + const isServerPartiallyEnabled = (serverName: string) => { + const serverTools = mcpTools.filter(tool => tool.server === serverName); + const serverToolNames = serverTools.map(tool => tool.name); + const enabledCount = serverToolNames.filter(toolName => localEnabledTools.includes(toolName)).length; + return enabledCount > 0 && enabledCount < serverToolNames.length; + }; + // Filter tools based on search query const filteredMcpTools = mcpTools.filter( tool => @@ -94,10 +182,17 @@ export const ToolsDialog: React.FC = ({ (tool.description && tool.description.toLowerCase().includes(searchQuery.toLowerCase())) ); - // Group tools by server (simplified version for now) - const groupedTools = { 'MCP Tools': filteredMcpTools }; + // Group tools by server + const groupedToolsByServer = filteredMcpTools.reduce((acc, tool) => { + const serverName = tool.server || 'Unknown Server'; + if (!acc[serverName]) { + acc[serverName] = []; + } + acc[serverName].push(tool); + return acc; + }, {} as {[key: string]: MCPTool[]}); - const handleToggleServer = (serverName: string) => { + const handleToggleServerExpansion = (serverName: string) => { const newExpanded = new Set(expandedServers); if (newExpanded.has(serverName)) { newExpanded.delete(serverName); @@ -165,23 +260,49 @@ export const ToolsDialog: React.FC = ({ ) : ( <> - {Object.entries(groupedTools).map(([serverName, tools]) => ( + {Object.entries(groupedToolsByServer).map(([serverName, tools]) => ( handleToggleServer(serverName)} + onChange={() => handleToggleServerExpansion(serverName)} sx={{ mb: 1 }} > }> - - {serverName} ({tools.length}) - + + + + + {serverName} ({tools.length} tools) + + + + { + e.stopPropagation(); + handleToggleServer(serverName); + }} + onClick={(e) => e.stopPropagation()} + sx={{ + ...(isServerPartiallyEnabled(serverName) && { + '& .MuiSwitch-thumb': { + backgroundColor: 'orange', + }, + '& .MuiSwitch-track': { + backgroundColor: 'rgba(255, 165, 0, 0.3)', + } + }) + }} + /> + + - + {tools.map((tool, index) => ( - <> - + + = ({ > @@ -209,6 +330,7 @@ export const ToolsDialog: React.FC = ({ handleToggleTool(tool.name)} checked={localEnabledTools.includes(tool.name)} @@ -216,7 +338,7 @@ export const ToolsDialog: React.FC = ({ {index < tools.length - 1 && } - + ))} From ef0be7aab8c31a113209272fa2edc63b50441d1e Mon Sep 17 00:00:00 2001 From: Joaquim Rocha Date: Tue, 30 Sep 2025 18:45:05 +0100 Subject: [PATCH 23/39] ai-assistant: Add shortDescription to the Tool class So we don't show very long descriptions in the tools dialog. Signed-off-by: Joaquim Rocha Signed-off-by: ashu8912 --- ai-assistant/src/components/assistant/ToolsDialog.tsx | 2 +- ai-assistant/src/langchain/tools/ToolBase.ts | 1 + ai-assistant/src/langchain/tools/kubernetes/KubernetesTool.ts | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/ai-assistant/src/components/assistant/ToolsDialog.tsx b/ai-assistant/src/components/assistant/ToolsDialog.tsx index 7011fb111b..e27d601d98 100644 --- a/ai-assistant/src/components/assistant/ToolsDialog.tsx +++ b/ai-assistant/src/components/assistant/ToolsDialog.tsx @@ -413,7 +413,7 @@ export const ToolsDialog: React.FC = ({ )} } - secondary={tempTool.config.description} + secondary={tempTool.config.shortDescription || tempTool.config.description} /> Date: Tue, 30 Sep 2025 18:45:43 +0100 Subject: [PATCH 24/39] ai-assistant: Properly look for whether we are running in-desktop Signed-off-by: Joaquim Rocha Signed-off-by: ashu8912 --- .../src/components/settings/MCPSettings.tsx | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/ai-assistant/src/components/settings/MCPSettings.tsx b/ai-assistant/src/components/settings/MCPSettings.tsx index 2fdafed97a..a6565b0d43 100644 --- a/ai-assistant/src/components/settings/MCPSettings.tsx +++ b/ai-assistant/src/components/settings/MCPSettings.tsx @@ -1,19 +1,11 @@ import { Icon } from '@iconify/react'; +import { Headlamp } from '@kinvolk/headlamp-plugin/lib'; import { SectionBox } from '@kinvolk/headlamp-plugin/lib/components/common'; import { Box, Button, FormControlLabel, Switch, Typography } from '@mui/material'; import React, { useEffect, useState } from 'react'; import { pluginStore } from '../../utils'; import MCPConfigEditorDialog from './MCPConfigEditorDialog'; -// Helper function to check if running in Electron -const isElectron = (): boolean => { - return ( - typeof window !== 'undefined' && - typeof window.desktopApi !== 'undefined' && - typeof window.desktopApi.mcp !== 'undefined' - ); -}; - export interface MCPServer { name: string; command: string; @@ -44,7 +36,7 @@ export function MCPSettings({ config, onConfigChange }: MCPSettingsProps) { useEffect(() => { // Load MCP config from Electron if available - if (isElectron()) { + if (Headlamp.isRunningAsApp()) { loadMCPConfigFromElectron(); } else { // Fallback to plugin store for non-Electron environments @@ -56,7 +48,7 @@ export function MCPSettings({ config, onConfigChange }: MCPSettingsProps) { }, []); const loadMCPConfigFromElectron = async () => { - if (!isElectron()) return; + if (!Headlamp.isRunningAsApp()) return; try { const response = await window.desktopApi!.mcp.getConfig(); @@ -76,7 +68,7 @@ export function MCPSettings({ config, onConfigChange }: MCPSettingsProps) { const handleConfigChange = async (newConfig: MCPConfig) => { setMCPConfig(newConfig); - if (isElectron()) { + if (Headlamp.isRunningAsApp()) { // Save to Electron settings and restart MCP client try { const response = await window.desktopApi!.mcp.updateConfig(newConfig); @@ -155,7 +147,7 @@ export function MCPSettings({ config, onConfigChange }: MCPSettingsProps) { }; // Only show MCP settings in Electron - if (!isElectron()) { + if (!Headlamp.isRunningAsApp()) { return ( From ea87815527f734b14bc9293682eb06fa22184e45 Mon Sep 17 00:00:00 2001 From: Joaquim Rocha Date: Tue, 30 Sep 2025 22:04:40 +0100 Subject: [PATCH 25/39] ai-assistant: Add tool test prompts Signed-off-by: Joaquim Rocha Signed-off-by: ashu8912 --- .../components/assistant/AIInputSection.tsx | 2 +- .../components/assistant/TestModeInput.tsx | 159 +++++++++++++++++- ai-assistant/src/modal.tsx | 30 +++- 3 files changed, 177 insertions(+), 14 deletions(-) diff --git a/ai-assistant/src/components/assistant/AIInputSection.tsx b/ai-assistant/src/components/assistant/AIInputSection.tsx index 9f1c2ac79f..eb69838d59 100644 --- a/ai-assistant/src/components/assistant/AIInputSection.tsx +++ b/ai-assistant/src/components/assistant/AIInputSection.tsx @@ -30,7 +30,7 @@ interface AIInputSectionProps { onStop: () => void; onClearHistory: () => void; onConfigChange: (config: StoredProviderConfig, model: string) => void; - onTestModeResponse: (content: string, type: 'assistant' | 'user', hasError?: boolean) => void; + onTestModeResponse: (content: string | object, type: 'assistant' | 'user', hasError?: boolean) => void; onToolsChange: (enabledTools: string[]) => void; } diff --git a/ai-assistant/src/components/assistant/TestModeInput.tsx b/ai-assistant/src/components/assistant/TestModeInput.tsx index 67ced89307..37073548de 100644 --- a/ai-assistant/src/components/assistant/TestModeInput.tsx +++ b/ai-assistant/src/components/assistant/TestModeInput.tsx @@ -20,7 +20,7 @@ import { import React, { useState } from 'react'; interface TestModeInputProps { - onAddTestResponse: (content: string, type: 'assistant' | 'user', hasError?: boolean) => void; + onAddTestResponse: (content: string | object, type: 'assistant' | 'user', hasError?: boolean) => void; isTestMode: boolean; } @@ -31,7 +31,12 @@ const TestModeInput: React.FC = ({ onAddTestResponse, isTest const [hasError, setHasError] = useState(false); // Sample test responses for quick testing - const sampleResponses = [ + const sampleResponses: Array<{ + label: string; + content: string | object; + type: 'assistant' | 'user'; + hasError?: boolean; + }> = [ { label: 'Simple Markdown Text', content: `Here's how you can create a simple deployment: @@ -183,11 +188,154 @@ All deployments are currently active in your cluster.`, content: `How can I create a deployment with 3 replicas of nginx?`, type: 'user' as const, }, + { + label: 'Tool Confirmation - Kubernetes API', + content: { + role: "assistant", + content: "", + toolConfirmation: { + tools: [ + { + id: "call_O7EYtgCzt5RmchxdZDJihMEF", + name: "kubernetes_api_request", + description: "Executes Kubernetes API operations", + arguments: { + url: "/api/v1/namespaces/default/pods", + method: "GET" + }, + type: "regular" + } + ], + loading: false, + requestId: "tool-approval-1759265356521-0.1868110264399998", + userContext: { + timeContext: "2025-09-30T20:49:16.521Z", + userMessage: "List me the pods here.", + conversationHistory: [ + { + role: "user", + content: "List me the pods here." + }, + { + role: "assistant", + content: "" + } + ] + } + }, + isDisplayOnly: true, + requestId: "tool-approval-1759265356521-0.1868110264399998" + }, + type: 'assistant' as const, + }, + { + label: 'Tool Confirmation - MCP Tool', + content: { + role: "assistant", + content: "", + toolConfirmation: { + tools: [ + { + id: "call_MCP_example", + name: "flux_get_resources", + description: "Get Flux resources from the cluster", + arguments: { + namespace: "flux-system", + resourceType: "helmreleases", + name: "" + }, + type: "mcp" + } + ], + loading: false, + requestId: "tool-approval-mcp-test", + userContext: { + timeContext: "2025-09-30T20:49:16.521Z", + userMessage: "Show me the Flux Helm releases.", + conversationHistory: [ + { + role: "user", + content: "Show me the Flux Helm releases." + }, + { + role: "assistant", + content: "" + } + ] + } + }, + isDisplayOnly: true, + requestId: "tool-approval-mcp-test" + }, + type: 'assistant' as const, + }, + { + label: 'Tool Confirmation - Multiple Tools', + content: { + role: "assistant", + content: "", + toolConfirmation: { + tools: [ + { + id: "call_k8s_get_pods", + name: "kubernetes_api_request", + description: "Get pods from Kubernetes API", + arguments: { + url: "/api/v1/namespaces/default/pods", + method: "GET" + }, + type: "regular" + }, + { + id: "call_flux_check", + name: "flux_get_helmreleases", + description: "Check Flux Helm releases", + arguments: { + namespace: "flux-system", + name: "", + output: "json" + }, + type: "mcp" + } + ], + loading: false, + requestId: "tool-approval-multi-test", + userContext: { + timeContext: "2025-09-30T20:49:16.521Z", + userMessage: "Show me pods and Flux releases.", + conversationHistory: [ + { + role: "user", + content: "Show me pods and Flux releases." + }, + { + role: "assistant", + content: "" + } + ] + } + }, + isDisplayOnly: true, + requestId: "tool-approval-multi-test" + }, + type: 'assistant' as const, + } ]; const handleSubmit = () => { if (testContent.trim()) { - onAddTestResponse(testContent, responseType, hasError); + let content: string | object = testContent; + + // Try to parse as JSON if it looks like a tool confirmation object + if (testContent.trim().startsWith('{') && testContent.includes('toolConfirmation')) { + try { + content = JSON.parse(testContent); + } catch (error) { + console.warn('Failed to parse JSON content, using as string:', error); + } + } + + onAddTestResponse(content, responseType, hasError); setTestContent(''); setOpen(false); } @@ -266,13 +414,12 @@ All deployments are currently active in your cluster.`, fullWidth value={testContent} onChange={e => setTestContent(e.target.value)} - placeholder="Enter your test response here. You can use markdown, YAML code blocks, etc." + placeholder="Enter your test response here. You can use markdown, YAML code blocks, or JSON objects for tool confirmations." variant="outlined" /> - Tip: Use ```yaml code blocks to test YAML rendering, or include markdown for - formatting tests. + Tip: Use ```yaml code blocks to test YAML rendering, markdown for formatting tests, or JSON objects starting with {'{toolConfirmation: ...}'} to test tool confirmation dialogs. diff --git a/ai-assistant/src/modal.tsx b/ai-assistant/src/modal.tsx index ebb8715068..7dd553501b 100644 --- a/ai-assistant/src/modal.tsx +++ b/ai-assistant/src/modal.tsx @@ -472,16 +472,32 @@ export default function AIPrompt(props: { // Function to handle test mode responses const handleTestModeResponse = ( - content: string, + content: string | object, type: 'assistant' | 'user', hasError?: boolean ) => { - const newPrompt: Prompt = { - role: type, - content, - error: hasError || false, - ...(hasError && { contentFilterError: true }), - }; + let newPrompt: Prompt; + + // Handle tool confirmation objects + if (typeof content === 'object' && content && 'toolConfirmation' in content) { + newPrompt = { + role: type, + content: (content as any).content || '', + error: hasError || false, + toolConfirmation: (content as any).toolConfirmation, + isDisplayOnly: (content as any).isDisplayOnly, + requestId: (content as any).requestId, + ...(hasError && { contentFilterError: true }), + }; + } else { + // Handle regular string content + newPrompt = { + role: type, + content: typeof content === 'string' ? content : JSON.stringify(content), + error: hasError || false, + ...(hasError && { contentFilterError: true }), + }; + } setPromptHistory(prev => [...prev, newPrompt]); setOpenPopup(true); From ef32472eb2e202a1db600259e3b2b5805e4935a4 Mon Sep 17 00:00:00 2001 From: Joaquim Rocha Date: Tue, 30 Sep 2025 22:34:58 +0100 Subject: [PATCH 26/39] ai-assistant: Improve the tool UI and UX Signed-off-by: Joaquim Rocha Signed-off-by: ashu8912 --- .../common/InlineToolConfirmation.tsx | 267 ++++++++---------- 1 file changed, 110 insertions(+), 157 deletions(-) diff --git a/ai-assistant/src/components/common/InlineToolConfirmation.tsx b/ai-assistant/src/components/common/InlineToolConfirmation.tsx index 938b10c233..0e8e41d701 100644 --- a/ai-assistant/src/components/common/InlineToolConfirmation.tsx +++ b/ai-assistant/src/components/common/InlineToolConfirmation.tsx @@ -59,7 +59,7 @@ const InlineToolConfirmation: React.FC = ({ }) => { const theme = useTheme(); const [selectedToolIds] = useState(toolCalls.map(tool => tool.id)); - const [showDetails, setShowDetails] = useState(!compact); + const [expandedTools, setExpandedTools] = useState>(new Set()); // Track which tools are expanded // State to track processed arguments for each tool const [processedArguments, setProcessedArguments] = useState>( @@ -168,6 +168,18 @@ const InlineToolConfirmation: React.FC = ({ return 'mdi:tool'; }; + const toggleToolExpansion = (toolId: string) => { + setExpandedTools(prev => { + const newSet = new Set(prev); + if (newSet.has(toolId)) { + newSet.delete(toolId); + } else { + newSet.add(toolId); + } + return newSet; + }); + }; + const renderArgumentField = ( toolId: string, fieldName: string, @@ -412,7 +424,6 @@ const InlineToolConfirmation: React.FC = ({ }; const mcpTools = toolCalls.filter(tool => tool.type === 'mcp'); - const regularTools = toolCalls.filter(tool => tool.type === 'regular'); // Check if any action is in progress const isActionInProgress = loading || isApproving || isDenying; @@ -483,173 +494,115 @@ const InlineToolConfirmation: React.FC = ({ {/* Summary */} - {compact - ? `Allow execution of ${toolCalls.length} tool${toolCalls.length > 1 ? 's' : ''}?` - : `The following tool${toolCalls.length > 1 ? 's' : ''} need${ - toolCalls.length > 1 ? '' : 's' - } permission to execute:`} + The assistant wants to execute {toolCalls.length} tool{toolCalls.length > 1 ? 's' : ''}: - {/* Tool summary when compact */} - {compact && ( - - {toolCalls.map(tool => ( - } - /> - ))} - - )} - - {/* Expandable details */} - {compact && !showDetails && ( - - {/* MCP Tools */} - {mcpTools.length > 0 && ( - - - MCP Tools: + {/* Collapsible tool list */} + + {toolCalls.map((tool, index) => ( + + {/* Tool header - always visible and clickable */} + toggleToolExpansion(tool.id)} + > + + + + + + {tool.name} - {mcpTools.map(tool => ( - - - - - {tool.name} - - - - {tool.description && ( - - {tool.description} - - )} - - ))} + {tool.type === 'mcp' && ( + + )} + {tool.description && ( + + {tool.description.length > 50 + ? `${tool.description.substring(0, 50)}...` + : tool.description + } + + )} - )} - {/* Regular Tools */} - {regularTools.length > 0 && ( - - - System Tools: - - {regularTools.map(tool => ( - - - - - {tool.name} + {/* Tool details - collapsible */} + + + {/* Tool description */} + {tool.description && ( + + + Description - - {tool.description && ( - + {tool.description} - )} - - ))} - - )} - - )} - - {/* Toggle details button for compact mode */} - {compact && ( - - - - )} + + )} - {/* Detailed view */} - - - {toolCalls.map((tool, index) => ( - - {index > 0 && } - - - - {tool.name} - - {tool.type === 'mcp' && ( - + {/* Tool arguments */} + {(Object.keys(tool.arguments).length > 0 || tool.type === 'mcp') && ( + + + Arguments {tool.type === 'mcp' ? '(editable)' : ''} + + {tool.type === 'mcp' ? ( + {renderArgumentsForTool(tool, tool.id)} + ) : ( + + {Object.entries(tool.arguments).map(([key, value]) => ( + + + {key}:{' '} + + + {typeof value === 'object' + ? JSON.stringify(value, null, 2) + : String(value) + } + + + ))} + + )} + )} + + + ))} + - {tool.description && ( - - {tool.description} - - )} - - {(Object.keys(tool.arguments).length > 0 || tool.type === 'mcp') && ( - - - Arguments {tool.type === 'mcp' ? '(editable):' : ':'} - - {tool.type === 'mcp' ? ( - {renderArgumentsForTool(tool, tool.id)} - ) : ( - - {Object.entries(tool.arguments).map(([key, value]) => ( - - - {key}: - - } - secondary={ - - {typeof value === 'object' - ? JSON.stringify(value, null, 2) - : String(value)} - - } - /> - - ))} - - )} - - )} - - ))} - - - - {/* Warning */} + {/* Contextual info */} {mcpTools.length > 0 - ? 'MCP tool arguments have been intelligently analyzed and pre-filled based on your request. Arguments marked "AI-filled" were extracted from your message or context. Review and modify as needed.' - : 'These tools will access external systems. Review the details before approving.'} + ? `${mcpTools.length > 1 ? 'These MCP tools have' : 'This MCP tool has'} been configured with AI-analyzed arguments from your request. Click on any tool above to view details and edit arguments.` + : 'These tools will access your Kubernetes cluster and other systems. Click on any tool above to view details.'} @@ -664,7 +617,7 @@ const InlineToolConfirmation: React.FC = ({ )} {/* Action buttons */} - + From 5232baef5f92a6992aa8068375619a248eb91651 Mon Sep 17 00:00:00 2001 From: ashu8912 Date: Thu, 9 Oct 2025 15:54:17 +0530 Subject: [PATCH 27/39] ai-plugin: Fix k8s tool call working Signed-off-by: ashu8912 --- ai-assistant/src/ai/mcp/electron-client.ts | 5 +- .../components/assistant/AIInputSection.tsx | 6 +- .../components/assistant/TestModeInput.tsx | 148 +++++++------- .../src/components/assistant/ToolsDialog.tsx | 37 ++-- .../common/InlineToolConfirmation.tsx | 39 ++-- .../src/langchain/LangChainManager.ts | 190 +++++++++++++++--- .../src/langchain/tools/ToolManager.ts | 12 +- ai-assistant/src/modal.tsx | 1 + ai-assistant/src/utils/ToolConfigManager.ts | 5 + 9 files changed, 313 insertions(+), 130 deletions(-) diff --git a/ai-assistant/src/ai/mcp/electron-client.ts b/ai-assistant/src/ai/mcp/electron-client.ts index 7b21db2836..cb12aa91e2 100644 --- a/ai-assistant/src/ai/mcp/electron-client.ts +++ b/ai-assistant/src/ai/mcp/electron-client.ts @@ -145,7 +145,10 @@ class ElectronMCPClient { */ async getConfig(): Promise<{ success: boolean; config?: any; error?: string }> { if (!this.isElectron) { - return { success: false, error: 'MCP client not available - not running in Electron environment' }; + return { + success: false, + error: 'MCP client not available - not running in Electron environment', + }; } try { diff --git a/ai-assistant/src/components/assistant/AIInputSection.tsx b/ai-assistant/src/components/assistant/AIInputSection.tsx index eb69838d59..00937b19e1 100644 --- a/ai-assistant/src/components/assistant/AIInputSection.tsx +++ b/ai-assistant/src/components/assistant/AIInputSection.tsx @@ -30,7 +30,11 @@ interface AIInputSectionProps { onStop: () => void; onClearHistory: () => void; onConfigChange: (config: StoredProviderConfig, model: string) => void; - onTestModeResponse: (content: string | object, type: 'assistant' | 'user', hasError?: boolean) => void; + onTestModeResponse: ( + content: string | object, + type: 'assistant' | 'user', + hasError?: boolean + ) => void; onToolsChange: (enabledTools: string[]) => void; } diff --git a/ai-assistant/src/components/assistant/TestModeInput.tsx b/ai-assistant/src/components/assistant/TestModeInput.tsx index 37073548de..43a1eb4b54 100644 --- a/ai-assistant/src/components/assistant/TestModeInput.tsx +++ b/ai-assistant/src/components/assistant/TestModeInput.tsx @@ -20,7 +20,11 @@ import { import React, { useState } from 'react'; interface TestModeInputProps { - onAddTestResponse: (content: string | object, type: 'assistant' | 'user', hasError?: boolean) => void; + onAddTestResponse: ( + content: string | object, + type: 'assistant' | 'user', + hasError?: boolean + ) => void; isTestMode: boolean; } @@ -191,135 +195,135 @@ All deployments are currently active in your cluster.`, { label: 'Tool Confirmation - Kubernetes API', content: { - role: "assistant", - content: "", + role: 'assistant', + content: '', toolConfirmation: { tools: [ { - id: "call_O7EYtgCzt5RmchxdZDJihMEF", - name: "kubernetes_api_request", - description: "Executes Kubernetes API operations", + id: 'call_O7EYtgCzt5RmchxdZDJihMEF', + name: 'kubernetes_api_request', + description: 'Executes Kubernetes API operations', arguments: { - url: "/api/v1/namespaces/default/pods", - method: "GET" + url: '/api/v1/namespaces/default/pods', + method: 'GET', }, - type: "regular" - } + type: 'regular', + }, ], loading: false, - requestId: "tool-approval-1759265356521-0.1868110264399998", + requestId: 'tool-approval-1759265356521-0.1868110264399998', userContext: { - timeContext: "2025-09-30T20:49:16.521Z", - userMessage: "List me the pods here.", + timeContext: '2025-09-30T20:49:16.521Z', + userMessage: 'List me the pods here.', conversationHistory: [ { - role: "user", - content: "List me the pods here." + role: 'user', + content: 'List me the pods here.', }, { - role: "assistant", - content: "" - } - ] - } + role: 'assistant', + content: '', + }, + ], + }, }, isDisplayOnly: true, - requestId: "tool-approval-1759265356521-0.1868110264399998" + requestId: 'tool-approval-1759265356521-0.1868110264399998', }, type: 'assistant' as const, }, { label: 'Tool Confirmation - MCP Tool', content: { - role: "assistant", - content: "", + role: 'assistant', + content: '', toolConfirmation: { tools: [ { - id: "call_MCP_example", - name: "flux_get_resources", - description: "Get Flux resources from the cluster", + id: 'call_MCP_example', + name: 'flux_get_resources', + description: 'Get Flux resources from the cluster', arguments: { - namespace: "flux-system", - resourceType: "helmreleases", - name: "" + namespace: 'flux-system', + resourceType: 'helmreleases', + name: '', }, - type: "mcp" - } + type: 'mcp', + }, ], loading: false, - requestId: "tool-approval-mcp-test", + requestId: 'tool-approval-mcp-test', userContext: { - timeContext: "2025-09-30T20:49:16.521Z", - userMessage: "Show me the Flux Helm releases.", + timeContext: '2025-09-30T20:49:16.521Z', + userMessage: 'Show me the Flux Helm releases.', conversationHistory: [ { - role: "user", - content: "Show me the Flux Helm releases." + role: 'user', + content: 'Show me the Flux Helm releases.', }, { - role: "assistant", - content: "" - } - ] - } + role: 'assistant', + content: '', + }, + ], + }, }, isDisplayOnly: true, - requestId: "tool-approval-mcp-test" + requestId: 'tool-approval-mcp-test', }, type: 'assistant' as const, }, { label: 'Tool Confirmation - Multiple Tools', content: { - role: "assistant", - content: "", + role: 'assistant', + content: '', toolConfirmation: { tools: [ { - id: "call_k8s_get_pods", - name: "kubernetes_api_request", - description: "Get pods from Kubernetes API", + id: 'call_k8s_get_pods', + name: 'kubernetes_api_request', + description: 'Get pods from Kubernetes API', arguments: { - url: "/api/v1/namespaces/default/pods", - method: "GET" + url: '/api/v1/namespaces/default/pods', + method: 'GET', }, - type: "regular" + type: 'regular', }, { - id: "call_flux_check", - name: "flux_get_helmreleases", - description: "Check Flux Helm releases", + id: 'call_flux_check', + name: 'flux_get_helmreleases', + description: 'Check Flux Helm releases', arguments: { - namespace: "flux-system", - name: "", - output: "json" + namespace: 'flux-system', + name: '', + output: 'json', }, - type: "mcp" - } + type: 'mcp', + }, ], loading: false, - requestId: "tool-approval-multi-test", + requestId: 'tool-approval-multi-test', userContext: { - timeContext: "2025-09-30T20:49:16.521Z", - userMessage: "Show me pods and Flux releases.", + timeContext: '2025-09-30T20:49:16.521Z', + userMessage: 'Show me pods and Flux releases.', conversationHistory: [ { - role: "user", - content: "Show me pods and Flux releases." + role: 'user', + content: 'Show me pods and Flux releases.', }, { - role: "assistant", - content: "" - } - ] - } + role: 'assistant', + content: '', + }, + ], + }, }, isDisplayOnly: true, - requestId: "tool-approval-multi-test" + requestId: 'tool-approval-multi-test', }, type: 'assistant' as const, - } + }, ]; const handleSubmit = () => { @@ -419,7 +423,9 @@ All deployments are currently active in your cluster.`, /> - Tip: Use ```yaml code blocks to test YAML rendering, markdown for formatting tests, or JSON objects starting with {'{toolConfirmation: ...}'} to test tool confirmation dialogs. + Tip: Use ```yaml code blocks to test YAML rendering, markdown for formatting tests, or + JSON objects starting with {'{toolConfirmation: ...}'} to test tool confirmation + dialogs. diff --git a/ai-assistant/src/components/assistant/ToolsDialog.tsx b/ai-assistant/src/components/assistant/ToolsDialog.tsx index e27d601d98..bbb8d0169a 100644 --- a/ai-assistant/src/components/assistant/ToolsDialog.tsx +++ b/ai-assistant/src/components/assistant/ToolsDialog.tsx @@ -50,7 +50,7 @@ export const ToolsDialog: React.FC = ({ const [isLoadingMcp, setIsLoadingMcp] = useState(true); const [searchQuery, setSearchQuery] = useState(''); const [expandedServers, setExpandedServers] = useState>(new Set()); - const [mcpServers, setMcpServers] = useState<{[key: string]: any}>({}); + const [, setMcpServers] = useState<{ [key: string]: any }>({}); // Load MCP tools when dialog opens useEffect(() => { @@ -74,7 +74,7 @@ export const ToolsDialog: React.FC = ({ // Load both tools and server configuration const [tools, configResponse] = await Promise.all([ mcpClient.getTools(), - mcpClient.getConfig() + mcpClient.getConfig(), ]); console.log('ToolsDialog: Received tools from client:', tools.length, 'tools'); @@ -82,12 +82,15 @@ export const ToolsDialog: React.FC = ({ console.log('ToolsDialog: Config response:', configResponse); // Extract server names from config - let servers: {[key: string]: any} = {}; + let servers: { [key: string]: any } = {}; if (configResponse.success && configResponse.config && configResponse.config.servers) { - servers = configResponse.config.servers.reduce((acc: {[key: string]: any}, server: any) => { - acc[server.name] = server; - return acc; - }, {}); + servers = configResponse.config.servers.reduce( + (acc: { [key: string]: any }, server: any) => { + acc[server.name] = server; + return acc; + }, + {} + ); setMcpServers(servers); } @@ -126,7 +129,6 @@ export const ToolsDialog: React.FC = ({ } }); setExpandedServers(serversWithTools); - } catch (error) { console.error('ToolsDialog: Failed to load MCP tools:', error); setMcpTools([]); @@ -165,13 +167,18 @@ export const ToolsDialog: React.FC = ({ const isServerEnabled = (serverName: string) => { const serverTools = mcpTools.filter(tool => tool.server === serverName); const serverToolNames = serverTools.map(tool => tool.name); - return serverToolNames.length > 0 && serverToolNames.every(toolName => localEnabledTools.includes(toolName)); + return ( + serverToolNames.length > 0 && + serverToolNames.every(toolName => localEnabledTools.includes(toolName)) + ); }; const isServerPartiallyEnabled = (serverName: string) => { const serverTools = mcpTools.filter(tool => tool.server === serverName); const serverToolNames = serverTools.map(tool => tool.name); - const enabledCount = serverToolNames.filter(toolName => localEnabledTools.includes(toolName)).length; + const enabledCount = serverToolNames.filter(toolName => + localEnabledTools.includes(toolName) + ).length; return enabledCount > 0 && enabledCount < serverToolNames.length; }; @@ -190,7 +197,7 @@ export const ToolsDialog: React.FC = ({ } acc[serverName].push(tool); return acc; - }, {} as {[key: string]: MCPTool[]}); + }, {} as { [key: string]: MCPTool[] }); const handleToggleServerExpansion = (serverName: string) => { const newExpanded = new Set(expandedServers); @@ -279,11 +286,11 @@ export const ToolsDialog: React.FC = ({ { + onChange={e => { e.stopPropagation(); handleToggleServer(serverName); }} - onClick={(e) => e.stopPropagation()} + onClick={e => e.stopPropagation()} sx={{ ...(isServerPartiallyEnabled(serverName) && { '& .MuiSwitch-thumb': { @@ -291,8 +298,8 @@ export const ToolsDialog: React.FC = ({ }, '& .MuiSwitch-track': { backgroundColor: 'rgba(255, 165, 0, 0.3)', - } - }) + }, + }), }} /> diff --git a/ai-assistant/src/components/common/InlineToolConfirmation.tsx b/ai-assistant/src/components/common/InlineToolConfirmation.tsx index 0e8e41d701..4b05bb700d 100644 --- a/ai-assistant/src/components/common/InlineToolConfirmation.tsx +++ b/ai-assistant/src/components/common/InlineToolConfirmation.tsx @@ -8,13 +8,11 @@ import { Chip, CircularProgress, Collapse, - Divider, FormControl, FormControlLabel, FormHelperText, IconButton, InputLabel, - List, ListItem, ListItemText, MenuItem, @@ -54,7 +52,6 @@ const InlineToolConfirmation: React.FC = ({ onApprove, onDeny, loading = false, - compact = false, userContext, }) => { const theme = useTheme(); @@ -537,21 +534,22 @@ const InlineToolConfirmation: React.FC = ({ {tool.description.length > 50 ? `${tool.description.substring(0, 50)}...` - : tool.description - } + : tool.description} )} {/* Tool details - collapsible */} - + {/* Tool description */} {tool.description && ( @@ -576,14 +574,17 @@ const InlineToolConfirmation: React.FC = ({ {Object.entries(tool.arguments).map(([key, value]) => ( - + {key}:{' '} {typeof value === 'object' ? JSON.stringify(value, null, 2) - : String(value) - } + : String(value)} ))} @@ -601,7 +602,9 @@ const InlineToolConfirmation: React.FC = ({ {mcpTools.length > 0 - ? `${mcpTools.length > 1 ? 'These MCP tools have' : 'This MCP tool has'} been configured with AI-analyzed arguments from your request. Click on any tool above to view details and edit arguments.` + ? `${ + mcpTools.length > 1 ? 'These MCP tools have' : 'This MCP tool has' + } been configured with AI-analyzed arguments from your request. Click on any tool above to view details and edit arguments.` : 'These tools will access your Kubernetes cluster and other systems. Click on any tool above to view details.'} @@ -656,7 +659,9 @@ const InlineToolConfirmation: React.FC = ({ cursor: isActionInProgress ? 'not-allowed' : 'pointer', }} > - {isApproving ? 'Executing...' : `Execute ${toolCalls.length} Tool${toolCalls.length > 1 ? 's' : ''}`} + {isApproving + ? 'Executing...' + : `Execute ${toolCalls.length} Tool${toolCalls.length > 1 ? 's' : ''}`} diff --git a/ai-assistant/src/langchain/LangChainManager.ts b/ai-assistant/src/langchain/LangChainManager.ts index 0a2c0d898c..bfc061cb00 100644 --- a/ai-assistant/src/langchain/LangChainManager.ts +++ b/ai-assistant/src/langchain/LangChainManager.ts @@ -21,6 +21,7 @@ import { basePrompt } from '../ai/prompts'; import { MCPArgumentProcessor, UserContext } from '../components/mcpOutput/MCPArgumentProcessor'; import { inlineToolApprovalManager } from '../utils/InlineToolApprovalManager'; import { ToolCall } from '../utils/ToolApprovalManager'; +import { isBuiltInTool } from '../utils/ToolConfigManager'; import { apiErrorPromptTemplate, toolFailurePromptTemplate } from './PromptTemplates'; import { KubernetesToolContext, ToolManager } from './tools'; @@ -210,27 +211,13 @@ export default class LangChainManager extends AIManager { } async configureTools(tools: any[], kubernetesContext: KubernetesToolContext): Promise { - console.log('🔧 Configuring tools for LangChain with context:', { - toolCount: tools.length, - selectedClusters: kubernetesContext.selectedClusters, - providerId: this.providerId, - }); - - // Wait for MCP tools to be initialized - console.log('⏳ Waiting for MCP tools initialization...'); await this.toolManager.waitForMCPToolsInitialization(); - console.log('✅ MCP tools initialization complete'); // Configure the Kubernetes context for the KubernetesTool this.toolManager.configureKubernetesContext(kubernetesContext); // Get all tools (including MCP tools) const allTools = this.toolManager.getLangChainTools(); - console.log( - `🔧 Total tools available: ${allTools.length} (Regular: ${tools.length}, MCP: ${ - this.toolManager.getMCPTools().length - })` - ); // Bind all tools to the model for compatible providers (OpenAI, Azure, etc.) this.boundModel = this.toolManager.bindToModel(this.model, this.providerId); @@ -394,7 +381,65 @@ export default class LangChainManager extends AIManager { // Helper method to create system prompt with context private createSystemPrompt(): string { - let systemPromptContent = basePrompt; + const availableTools = this.toolManager.getToolNames(); + const hasKubernetesTool = availableTools.includes('kubernetes_api_request'); + + let systemPromptContent; + + if (!hasKubernetesTool) { + // Modified prompt when Kubernetes tools are disabled + systemPromptContent = `You are an AI assistant for the Headlamp Kubernetes UI. You help users understand and manage their Kubernetes resources through a web interface. + +IMPORTANT: Kubernetes API access tools are currently DISABLED in your settings. + +CRITICAL LIMITATIONS: +- You CANNOT access live cluster data (pods, deployments, services, etc.) +- You CANNOT fetch current resource information from the cluster +- You CANNOT retrieve logs, events, or real-time status information +- DO NOT promise to fetch, retrieve, or access any live cluster data + +WHAT YOU CAN DO: +- Provide general Kubernetes guidance and explanations +- Generate YAML examples for resource creation +- Explain Kubernetes concepts and best practices +- Help troubleshoot based on information the user provides +- Direct users to enable tools if they need live data access + +WHEN USERS ASK FOR LIVE DATA: +- Clearly explain that you cannot access live cluster information +- Inform them that Kubernetes API tools are disabled +- Provide instructions to enable tools in AI Assistant settings +- Offer to help with general guidance instead + +YAML FORMATTING: +When providing Kubernetes YAML examples, use this format: + +## [Resource Type] Example: + +Brief explanation of the resource. + +\`\`\`yaml +apiVersion: [version] +kind: [kind] +metadata: + name: [name] + namespace: default +spec: + # Configuration here +\`\`\` + +Note: The YAML you provide will be displayed in a preview editor with an "Edit" button that allows users to modify the configuration before applying it to their cluster. + +RESPONSES: +- Format responses in markdown +- Be honest about limitations +- Always suggest enabling tools for live data access +- Provide helpful general guidance when possible +- If asked non-Kubernetes questions, politely redirect and include a light Kubernetes joke`; + } else { + // Original prompt when tools are available + systemPromptContent = basePrompt; + } // Add MCP tool guidance if we have MCP tools available const mcpTools = this.toolManager.getMCPTools(); @@ -675,6 +720,12 @@ The user is waiting for you to explain what the tools discovered. Provide a dire private async handleToolCalls(response: any): Promise { const enabledToolIds = this.toolManager.getToolNames(); + console.log('🔧 Tool calls detected, processing...', { + requestedTools: response.tool_calls?.map(tc => tc.name) || [], + enabledTools: enabledToolIds, + enabledToolsCount: enabledToolIds.length, + }); + // If no tools are enabled but LLM is returning tool calls, this indicates a bug if (enabledToolIds.length === 0) { console.warn('LLM returned tool calls but no tools are enabled. This should not happen.', { @@ -693,7 +744,8 @@ The user is waiting for you to explain what the tools discovered. Provide a dire return assistantPrompt; } - const toolCalls = response.tool_calls.map(tc => ({ + // Filter out disabled tools from tool calls + const allToolCalls = response.tool_calls.map(tc => ({ type: 'function', id: tc.id, function: { @@ -702,6 +754,37 @@ The user is waiting for you to explain what the tools discovered. Provide a dire }, })); + // Only keep tool calls for enabled tools + const toolCalls = allToolCalls.filter(tc => enabledToolIds.includes(tc.function.name)); + + // Log detailed filtering information + console.log('🔍 Tool filtering details:', { + allRequestedTools: allToolCalls.map(tc => tc.function.name), + enabledTools: enabledToolIds, + allowedTools: toolCalls.map(tc => tc.function.name), + }); + + // Log if any tools were filtered out + const filteredOutTools = allToolCalls.filter(tc => !enabledToolIds.includes(tc.function.name)); + if (filteredOutTools.length > 0) { + console.log( + '🚫 Filtered out disabled tool calls:', + filteredOutTools.map(tc => tc.function.name) + ); + console.log('✅ Enabled tools:', enabledToolIds); + console.log( + '🔄 Total tool calls requested:', + allToolCalls.length, + 'Allowed:', + toolCalls.length + ); + } else if (allToolCalls.length > 0) { + console.log( + '✅ All requested tools are enabled, proceeding with:', + toolCalls.map(tc => tc.function.name) + ); + } + const assistantPrompt: Prompt = { role: 'assistant', content: this.extractTextContent(response.content), @@ -709,6 +792,39 @@ The user is waiting for you to explain what the tools discovered. Provide a dire }; this.history.push(assistantPrompt); + // If all tool calls were filtered out (all requested tools are disabled), handle gracefully + if (toolCalls.length === 0) { + console.log('ℹ️ All requested tools are disabled, providing alternative response'); + + // Add informational message about disabled tools if any were filtered + if (filteredOutTools.length > 0) { + const disabledToolNames = filteredOutTools.map(tc => tc.function.name).join(', '); + + // Replace the AI's response with a clear explanation instead of calling tools again + const clarifiedResponse = `I understand you're asking for cluster data, but I cannot access live Kubernetes information because the required tools (${disabledToolNames}) are currently disabled in your settings. + +To get real-time cluster data, you'll need to: +1. Go to AI Assistant settings +2. Enable the "${disabledToolNames}" tool +3. Ask your question again + +Without access to the Kubernetes API, I cannot fetch current pod, deployment, service, or other resource information from your cluster.`; + + // Update the assistant prompt in history with the clarified response + const updatedPrompt: Prompt = { + role: 'assistant', + content: clarifiedResponse, + }; + + // Replace the last history entry with the updated prompt + this.history[this.history.length - 1] = updatedPrompt; + + return updatedPrompt; + } + + return assistantPrompt; + } + // Prepare tool calls for approval with intelligent argument processing const toolCallsForApproval: ToolCall[] = await Promise.all( toolCalls.map(async tc => { @@ -765,14 +881,42 @@ The user is waiting for you to explain what the tools discovered. Provide a dire ); try { - // Request approval for tool execution using inline approval system - console.log('🔐 Requesting approval for', toolCallsForApproval.length, 'tools'); - const approvedToolIds = await inlineToolApprovalManager.requestApproval( - toolCallsForApproval, - this // Pass the AI manager instance - ); + // Separate built-in tools from MCP tools + const builtInTools = toolCallsForApproval.filter(tool => isBuiltInTool(tool.name)); + const mcpTools = toolCallsForApproval.filter(tool => !isBuiltInTool(tool.name)); + + const approvedToolIds: string[] = []; + + // Auto-approve all built-in tools (no user interaction needed) + const builtInToolIds = builtInTools.map(tool => tool.id); + approvedToolIds.push(...builtInToolIds); + + // Only request approval for MCP tools + if (mcpTools.length > 0) { + console.log('🔐 Requesting approval for', mcpTools.length, 'MCP tools'); + const approvedMCPToolIds = await inlineToolApprovalManager.requestApproval( + mcpTools, + this // Pass the AI manager instance + ); + approvedToolIds.push(...approvedMCPToolIds); + console.log('✅ MCP tools approved:', approvedMCPToolIds.length, 'of', mcpTools.length); + } - console.log('✅ Tools approved:', approvedToolIds.length, 'of', toolCallsForApproval.length); + // Log built-in tools that were auto-approved + if (builtInToolIds.length > 0) { + console.log( + '✅ Built-in tools auto-executed (no approval needed):', + builtInToolIds.length, + builtInTools.map(tool => tool.name) + ); + } + + console.log( + '✅ Total tools approved:', + approvedToolIds.length, + 'of', + toolCallsForApproval.length + ); // Filter tool calls to only execute approved ones and update with processed arguments const approvedToolCalls = toolCalls diff --git a/ai-assistant/src/langchain/tools/ToolManager.ts b/ai-assistant/src/langchain/tools/ToolManager.ts index 3e25f01391..8c022af1f1 100644 --- a/ai-assistant/src/langchain/tools/ToolManager.ts +++ b/ai-assistant/src/langchain/tools/ToolManager.ts @@ -18,7 +18,8 @@ export class ToolManager { private mcpFormatter: MCPOutputFormatter | null = null; constructor(private kubernetesContext?: KubernetesToolContext, enabledToolIds?: string[]) { - this.initializeTools(); + console.log('🔧 ToolManager: Initializing with enabledToolIds:', enabledToolIds); + this.initializeTools(enabledToolIds); this.mcpInitializationPromise = this.initializeMCPTools(enabledToolIds); } @@ -30,11 +31,12 @@ export class ToolManager { for (const ToolClass of AVAILABLE_TOOLS) { const tempTool = new ToolClass(); if (enabledToolIds && !enabledToolIds.includes(tempTool.config.name)) { - console.log('AI Assistant: Skipping tool (disabled)', tempTool.config.name); + console.log('🚫 ToolManager: Skipping tool (disabled):', tempTool.config.name); continue; // Skip tools not enabled } try { const tool = tempTool; + console.log('✅ ToolManager: Initializing tool:', tempTool.config.name); this.addTool(tool); } catch (error) { console.error(`Failed to load tool ${ToolClass.name}:`, error); @@ -43,6 +45,12 @@ export class ToolManager { // Initialize MCP tools asynchronously but start immediately this.initializeMCPTools(enabledToolIds); + + // Log final initialized tools + console.log( + '🔧 ToolManager: Regular tools initialized:', + this.tools.map(t => t.config.name) + ); } /** diff --git a/ai-assistant/src/modal.tsx b/ai-assistant/src/modal.tsx index 7dd553501b..8a0cf22bf6 100644 --- a/ai-assistant/src/modal.tsx +++ b/ai-assistant/src/modal.tsx @@ -280,6 +280,7 @@ export default function AIPrompt(props: { ...activeConfig.config, model: selectedModel, }; + console.log('🔄 Creating new LangChainManager with enabledTools:', enabledTools); const newManager = new LangChainManager( activeConfig.providerId, configWithModel, diff --git a/ai-assistant/src/utils/ToolConfigManager.ts b/ai-assistant/src/utils/ToolConfigManager.ts index 8f21340479..d9e47f0b23 100644 --- a/ai-assistant/src/utils/ToolConfigManager.ts +++ b/ai-assistant/src/utils/ToolConfigManager.ts @@ -68,3 +68,8 @@ export function setEnabledTools(pluginSettings: any, enabledToolIds: string[]): enabledTools, }; } + +// Check if a tool is a built-in tool (from AVAILABLE_TOOLS registry) +export function isBuiltInTool(toolName: string): boolean { + return AVAILABLE_TOOLS.some(tool => tool.id === toolName); +} From 64b4ec20d9b923110a5eb82358d275dd87167da3 Mon Sep 17 00:00:00 2001 From: ashu8912 Date: Thu, 9 Oct 2025 17:19:35 +0530 Subject: [PATCH 28/39] ai-plugin: fix log fetch failures Signed-off-by: ashu8912 --- ai-assistant/src/helper/apihelper.tsx | 31 +++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/ai-assistant/src/helper/apihelper.tsx b/ai-assistant/src/helper/apihelper.tsx index 0e06db34ca..672d1fcb69 100644 --- a/ai-assistant/src/helper/apihelper.tsx +++ b/ai-assistant/src/helper/apihelper.tsx @@ -393,14 +393,16 @@ export const handleActualApiRequest = async ( ...requestOptions, isJSON: !isLogRequest(cleanedUrl), }); + console.log('API response received:', response); } catch (apiError) { - console.log('Error in clusterRequest:', apiError); - // Handle specific multi-container pod logs error if ( isLogRequest(cleanedUrl) && apiError.message && - apiError.message?.includes('a container name must be specified') + (apiError.message?.includes('a container name must be specified') || + apiError.message?.includes('container name must be specified') || + (apiError.message?.includes('Bad Request') && cleanedUrl.includes('/log')) || + apiError.message?.includes('choose one of')) ) { // Extract pod name and available containers from error message const podMatch = apiError.message.match(/for pod ([^,]+)/); @@ -425,12 +427,33 @@ export const handleActualApiRequest = async ( role: 'assistant', content: errorContent, }); + } else { + // If we can't parse the specific error but know it's a log request with Bad Request + // Extract pod name from URL and suggest getting pod details + const podNameFromUrl = cleanedUrl.match(/\/pods\/([^\/]+)\/log/); + if (podNameFromUrl) { + const podName = podNameFromUrl[1]; + + const errorContent = `Failed to get logs from pod "${podName}". This is likely because it has multiple containers.\n\nTo see the containers in this pod, I need to get the pod details first. Would you like me to check the pod details to see available containers?`; + + aiManager.history.push({ + error: false, + role: 'assistant', + content: errorContent, + }); + + return JSON.stringify({ + error: false, + role: 'assistant', + content: errorContent, + }); + } } } // Handle general API errors if (onFailure) { - onFailure(apiError, 'GET', { type: 'api_error' }); + // onFailure(apiError, 'GET', { type: 'api_error' }); } aiManager.history.push({ error: true, From aacf512c00ad584a3d9b09a3fda21a359524e0a0 Mon Sep 17 00:00:00 2001 From: ashu8912 Date: Fri, 10 Oct 2025 17:03:27 +0530 Subject: [PATCH 29/39] ai-plugin: add missing tool response Signed-off-by: ashu8912 --- .../src/langchain/LangChainManager.ts | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/ai-assistant/src/langchain/LangChainManager.ts b/ai-assistant/src/langchain/LangChainManager.ts index bfc061cb00..c8ebbb3336 100644 --- a/ai-assistant/src/langchain/LangChainManager.ts +++ b/ai-assistant/src/langchain/LangChainManager.ts @@ -1349,24 +1349,7 @@ Format your response to make the errors prominent and actionable.`, }); // Add missing tool responses - this.addMissingToolResponses(expectedToolCallIds, actualToolResponses); - } - } - } - - // Add missing tool responses - private addMissingToolResponses(expectedIds: string[], actualResponses: any[]): void { - for (const expectedId of expectedIds) { - if (!actualResponses.find(r => r.toolCallId === expectedId)) { - this.history.push({ - role: 'tool', - content: JSON.stringify({ - error: true, - message: 'Tool execution failed - no response recorded', - }), - toolCallId: expectedId, - name: 'kubernetes_api_request', - }); + // this.addMissingToolResponses(expectedToolCallIds, actualToolResponses); } } } From d20293db424398eb5ae683d48a3209b8f754b8e4 Mon Sep 17 00:00:00 2001 From: ashu8912 Date: Mon, 13 Oct 2025 09:55:50 +0530 Subject: [PATCH 30/39] ai-plugin: Use mcp tools config update from electron main process Signed-off-by: ashu8912 --- ai-assistant/src/ai/mcp/client.ts | 44 -- ai-assistant/src/ai/mcp/electron-client.ts | 148 ++++++ .../src/components/assistant/ToolsDialog.tsx | 180 +++++-- .../components/settings/MCPToolSettings.tsx | 479 ++++++++++++++++++ ai-assistant/src/index.tsx | 17 +- .../src/langchain/tools/ToolManager.ts | 139 ++++- ai-assistant/src/utils.tsx | 44 +- ai-assistant/src/utils/ToolConfigManager.ts | 79 +++ 8 files changed, 1027 insertions(+), 103 deletions(-) delete mode 100644 ai-assistant/src/ai/mcp/client.ts create mode 100644 ai-assistant/src/components/settings/MCPToolSettings.tsx diff --git a/ai-assistant/src/ai/mcp/client.ts b/ai-assistant/src/ai/mcp/client.ts deleted file mode 100644 index a8c298a0e4..0000000000 --- a/ai-assistant/src/ai/mcp/client.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { MultiServerMCPClient } from '@langchain/mcp-adapters'; - -const client = new MultiServerMCPClient({ - // Global tool configuration options - // Whether to throw on errors if a tool fails to load (optional, default: true) - throwOnLoadError: true, - // Whether to prefix tool names with the server name (optional, default: false) - prefixToolNameWithServerName: false, - // Optional additional prefix for tool names (optional, default: "") - additionalToolNamePrefix: '', - - // Use standardized content block format in tool outputs - useStandardContentBlocks: true, - - // Server configuration - mcpServers: { - // adds the Inspektor Gadget MCP server - 'inspektor-gadget': { - transport: 'stdio', - command: 'docker', - args: [ - 'run', - '-i', - '--rm', - '--mount', - 'type=bind,src=${env:HOME}/.kube/config,dst=/kubeconfig', - 'ghcr.io/inspektor-gadget/ig-mcp-server:latest', - '-gadget-discoverer=artifacthub', - ], - // Restart configuration for stdio transport - restart: { - enabled: true, - maxAttempts: 3, - delayMs: 2000, // Slightly longer delay for Docker container startup - }, - }, - }, -}); - -const tools = async function () { - return await client.getTools(); -}; - -export default tools; diff --git a/ai-assistant/src/ai/mcp/electron-client.ts b/ai-assistant/src/ai/mcp/electron-client.ts index cb12aa91e2..e4c966314b 100644 --- a/ai-assistant/src/ai/mcp/electron-client.ts +++ b/ai-assistant/src/ai/mcp/electron-client.ts @@ -27,6 +27,10 @@ interface ElectronMCPApi { resetClient: () => Promise; getConfig: () => Promise<{ success: boolean; config?: any; error?: string }>; updateConfig: (config: any) => Promise; + getToolsConfig: () => Promise<{ success: boolean; config?: any; error?: string }>; + updateToolsConfig: (config: any) => Promise; + setToolEnabled: (serverName: string, toolName: string, enabled: boolean) => Promise; + getToolStats: (serverName: string, toolName: string) => Promise<{ success: boolean; stats?: any; error?: string }>; } declare global { @@ -159,6 +163,150 @@ class ElectronMCPClient { return { success: false, error: String(error) }; } } + + /** + * Get MCP tools configuration from Electron main process + */ + async getToolsConfig(): Promise<{ success: boolean; config?: any; error?: string }> { + if (!this.isElectron) { + return { + success: false, + error: 'MCP client not available - not running in Electron environment', + }; + } + + try { + const response = await window.desktopApi!.mcp.getToolsConfig(); + return response; + } catch (error) { + console.error('Error getting MCP tools config:', error); + return { success: false, error: String(error) }; + } + } + + /** + * Update MCP tools configuration in Electron main process + */ + async updateToolsConfig(config: any): Promise { + if (!this.isElectron) { + return false; + } + + try { + const response = await window.desktopApi!.mcp.updateToolsConfig(config); + console.log("response from updating mcp tools config is ", response); + return response.success; + } catch (error) { + console.error('Error updating MCP tools config:', error); + return false; + } + } + + /** + * Enable or disable a specific MCP tool + */ + async setToolEnabled(serverName: string, toolName: string, enabled: boolean): Promise { + if (!this.isElectron) { + return false; + } + + try { + const response = await window.desktopApi!.mcp.setToolEnabled(serverName, toolName, enabled); + return response.success; + } catch (error) { + console.error('Error setting tool enabled state:', error); + return false; + } + } + + /** + * Get tool statistics for a specific MCP tool + */ + async getToolStats(serverName: string, toolName: string): Promise { + if (!this.isElectron) { + return null; + } + + try { + const response = await window.desktopApi!.mcp.getToolStats(serverName, toolName); + return response.success ? response.stats : null; + } catch (error) { + console.error('Error getting tool stats:', error); + return null; + } + } + + /** + * Parse MCP tool name to extract server and tool components + * Format: "serverName__toolName" + */ + parseToolName(fullToolName: string): { serverName: string; toolName: string } { + const parts = fullToolName.split('__'); + if (parts.length >= 2) { + return { + serverName: parts[0], + toolName: parts.slice(1).join('__'), + }; + } + return { + serverName: 'default', + toolName: fullToolName, + }; + } + + /** + * Check if a specific tool is enabled + */ + async isToolEnabled(fullToolName: string): Promise { + if (!this.isElectron) { + return true; // Default to enabled if not in Electron + } + + try { + const { serverName, toolName } = this.parseToolName(fullToolName); + const toolsConfig = await this.getToolsConfig(); + + if (!toolsConfig.success || !toolsConfig.config) { + return true; // Default to enabled if config not available + } + + const serverConfig = toolsConfig.config[serverName]; + if (!serverConfig || !serverConfig[toolName]) { + return true; // Default to enabled for new tools + } + + return serverConfig[toolName].enabled; + } catch (error) { + console.error('Error checking tool enabled state:', error); + return true; // Default to enabled on error + } + } + + /** + * Get all enabled MCP tools + */ + async getEnabledTools(): Promise { + if (!this.isElectron) { + return []; + } + + try { + const allTools = await this.getTools(); + const enabledTools: MCPTool[] = []; + + for (const tool of allTools) { + const isEnabled = await this.isToolEnabled(tool.name); + if (isEnabled) { + enabledTools.push(tool); + } + } + + return enabledTools; + } catch (error) { + console.error('Error getting enabled tools:', error); + return []; + } + } } // Export a function that returns tools (compatible with existing interface) diff --git a/ai-assistant/src/components/assistant/ToolsDialog.tsx b/ai-assistant/src/components/assistant/ToolsDialog.tsx index bbb8d0169a..20f8dbbb2f 100644 --- a/ai-assistant/src/components/assistant/ToolsDialog.tsx +++ b/ai-assistant/src/components/assistant/ToolsDialog.tsx @@ -46,7 +46,9 @@ export const ToolsDialog: React.FC = ({ onToolsChange, }) => { const [localEnabledTools, setLocalEnabledTools] = useState(enabledTools); - const [mcpTools, setMcpTools] = useState([]); + const [allKnownMcpTools, setAllKnownMcpTools] = useState([]); // Track all tools ever seen + const [mcpToolsConfig, setMcpToolsConfig] = useState({}); + const [originalMcpConfig, setOriginalMcpConfig] = useState({}); const [isLoadingMcp, setIsLoadingMcp] = useState(true); const [searchQuery, setSearchQuery] = useState(''); const [expandedServers, setExpandedServers] = useState>(new Set()); @@ -64,6 +66,31 @@ export const ToolsDialog: React.FC = ({ setLocalEnabledTools(enabledTools); }, [enabledTools]); + // Parse MCP tool name to extract server and tool components + const parseMcpToolName = (fullToolName: string): { serverName: string; toolName: string } => { + const parts = fullToolName.split('__'); + if (parts.length >= 2) { + return { + serverName: parts[0], + toolName: parts.slice(1).join('__'), + }; + } + return { + serverName: 'default', + toolName: fullToolName, + }; + }; + + // Check if an MCP tool is enabled in the configuration + const isMcpToolEnabled = (toolName: string): boolean => { + const { serverName, toolName: actualToolName } = parseMcpToolName(toolName); + const serverConfig = mcpToolsConfig[serverName]; + if (!serverConfig || !serverConfig[actualToolName]) { + return true; // Default to enabled for new tools + } + return serverConfig[actualToolName].enabled !== false; + }; + const loadMcpTools = async () => { console.log('ToolsDialog: Starting to load MCP tools...'); setIsLoadingMcp(true); @@ -71,15 +98,23 @@ export const ToolsDialog: React.FC = ({ const mcpClient = new ElectronMCPClient(); console.log('ToolsDialog: Created MCP client, isAvailable:', mcpClient.isAvailable()); - // Load both tools and server configuration - const [tools, configResponse] = await Promise.all([ + // Load tools, server configuration, and tools configuration + const [tools, configResponse, toolsConfigResponse] = await Promise.all([ mcpClient.getTools(), mcpClient.getConfig(), + mcpClient.getToolsConfig(), ]); console.log('ToolsDialog: Received tools from client:', tools.length, 'tools'); console.log('ToolsDialog: Tools:', tools); console.log('ToolsDialog: Config response:', configResponse); + console.log('ToolsDialog: Tools config response:', toolsConfigResponse); + + // Store MCP tools configuration + if (toolsConfigResponse.success && toolsConfigResponse.config) { + setMcpToolsConfig(toolsConfigResponse.config); + setOriginalMcpConfig(JSON.parse(JSON.stringify(toolsConfigResponse.config))); + } // Extract server names from config let servers: { [key: string]: any } = {}; @@ -119,7 +154,12 @@ export const ToolsDialog: React.FC = ({ return { ...tool, server: 'Unknown Server' }; }); - setMcpTools(toolsWithServer); + // Update allKnownMcpTools by merging new tools with existing ones + setAllKnownMcpTools(prevKnown => { + const knownToolNames = new Set(prevKnown.map(t => t.name)); + const newTools = toolsWithServer.filter(t => !knownToolNames.has(t.name)); + return [...prevKnown, ...newTools]; + }); // Auto-expand servers that have tools const serversWithTools = new Set(); @@ -131,59 +171,86 @@ export const ToolsDialog: React.FC = ({ setExpandedServers(serversWithTools); } catch (error) { console.error('ToolsDialog: Failed to load MCP tools:', error); - setMcpTools([]); setMcpServers({}); } finally { setIsLoadingMcp(false); } }; - const handleToggleTool = (toolName: string) => { - const newEnabledTools = localEnabledTools.includes(toolName) - ? localEnabledTools.filter(name => name !== toolName) - : [...localEnabledTools, toolName]; - setLocalEnabledTools(newEnabledTools); + const handleToggleRegularTool = (toolName: string) => { + setLocalEnabledTools(prevTools => { + if (prevTools.includes(toolName)) { + return prevTools.filter(tool => tool !== toolName); + } else { + return [...prevTools, toolName]; + } + }); }; - const handleToggleServer = (serverName: string) => { - const serverTools = mcpTools.filter(tool => tool.server === serverName); - const serverToolNames = serverTools.map(tool => tool.name); + const handleToggleMcpTool = (toolName: string) => { + const { serverName, toolName: actualToolName } = parseMcpToolName(toolName); + const currentlyEnabled = isMcpToolEnabled(toolName); + + setMcpToolsConfig((prevConfig: any) => { + const newConfig = { ...prevConfig }; + if (!newConfig[serverName]) { + newConfig[serverName] = {}; + } + if (!newConfig[serverName][actualToolName]) { + newConfig[serverName][actualToolName] = { + enabled: true, + usageCount: 0, + }; + } + newConfig[serverName][actualToolName].enabled = !currentlyEnabled; + return newConfig; + }); + }; + const handleToggleServer = (serverName: string) => { + const serverTools = allKnownMcpTools.filter(tool => tool.server === serverName); + // Check if all tools from this server are currently enabled - const allEnabled = serverToolNames.every(toolName => localEnabledTools.includes(toolName)); + const allEnabled = serverTools.every(tool => isMcpToolEnabled(tool.name)); - let newEnabledTools: string[]; - if (allEnabled) { - // Disable all tools from this server - newEnabledTools = localEnabledTools.filter(toolName => !serverToolNames.includes(toolName)); - } else { - // Enable all tools from this server - newEnabledTools = [...new Set([...localEnabledTools, ...serverToolNames])]; - } + // Update MCP configuration for all tools in this server + setMcpToolsConfig((prevConfig: any) => { + const newConfig = { ...prevConfig }; + if (!newConfig[serverName]) { + newConfig[serverName] = {}; + } - setLocalEnabledTools(newEnabledTools); + serverTools.forEach(tool => { + const { toolName: actualToolName } = parseMcpToolName(tool.name); + if (!newConfig[serverName][actualToolName]) { + newConfig[serverName][actualToolName] = { + enabled: true, + usageCount: 0, + }; + } + newConfig[serverName][actualToolName].enabled = !allEnabled; + }); + + return newConfig; + }); }; const isServerEnabled = (serverName: string) => { - const serverTools = mcpTools.filter(tool => tool.server === serverName); - const serverToolNames = serverTools.map(tool => tool.name); + const serverTools = allKnownMcpTools.filter(tool => tool.server === serverName); return ( - serverToolNames.length > 0 && - serverToolNames.every(toolName => localEnabledTools.includes(toolName)) + serverTools.length > 0 && + serverTools.every(tool => isMcpToolEnabled(tool.name)) ); }; const isServerPartiallyEnabled = (serverName: string) => { - const serverTools = mcpTools.filter(tool => tool.server === serverName); - const serverToolNames = serverTools.map(tool => tool.name); - const enabledCount = serverToolNames.filter(toolName => - localEnabledTools.includes(toolName) - ).length; - return enabledCount > 0 && enabledCount < serverToolNames.length; + const serverTools = allKnownMcpTools.filter(tool => tool.server === serverName); + const enabledCount = serverTools.filter(tool => isMcpToolEnabled(tool.name)).length; + return enabledCount > 0 && enabledCount < serverTools.length; }; - // Filter tools based on search query - const filteredMcpTools = mcpTools.filter( + // Filter tools based on search query - use allKnownMcpTools to show all tools (including disabled ones) + const filteredMcpTools = allKnownMcpTools.filter( tool => tool.name.toLowerCase().includes(searchQuery.toLowerCase()) || (tool.description && tool.description.toLowerCase().includes(searchQuery.toLowerCase())) @@ -209,13 +276,40 @@ export const ToolsDialog: React.FC = ({ setExpandedServers(newExpanded); }; - const handleSave = () => { - onToolsChange(localEnabledTools); - onClose(); + const handleSave = async () => { + try { + // Save regular tools configuration + onToolsChange(localEnabledTools); + + // Save MCP tools configuration if it has changed + const mcpConfigChanged = JSON.stringify(mcpToolsConfig) !== JSON.stringify(originalMcpConfig); + + if (mcpConfigChanged) { + const mcpClient = new ElectronMCPClient(); + console.log('Saving MCP tools configuration:', mcpToolsConfig); + if (mcpClient.isAvailable()) { + const response = await mcpClient.updateToolsConfig(mcpToolsConfig); + if (!response) { + console.error('Failed to save MCP tools configuration'); + // Still continue to close dialog, but log the error + } else { + console.log('MCP tools configuration saved successfully'); + } + } + } + + onClose(); + } catch (error) { + console.error('Error saving configuration:', error); + // Still close the dialog even if there was an error + onClose(); + } }; const handleCancel = () => { + // Restore original state for both regular and MCP tools setLocalEnabledTools(enabledTools); + setMcpToolsConfig(JSON.parse(JSON.stringify(originalMcpConfig))); onClose(); }; @@ -339,8 +433,8 @@ export const ToolsDialog: React.FC = ({ handleToggleTool(tool.name)} - checked={localEnabledTools.includes(tool.name)} + onChange={() => handleToggleMcpTool(tool.name)} + checked={isMcpToolEnabled(tool.name)} /> @@ -352,7 +446,7 @@ export const ToolsDialog: React.FC = ({ ))} - {filteredMcpTools.length === 0 && mcpTools.length > 0 && ( + {filteredMcpTools.length === 0 && allKnownMcpTools.length > 0 && ( = ({ )} - {mcpTools.length === 0 && ( + {allKnownMcpTools.length === 0 && ( = ({ handleToggleTool(toolName)} + onChange={() => handleToggleRegularTool(toolName)} checked={isEnabled} color="primary" /> diff --git a/ai-assistant/src/components/settings/MCPToolSettings.tsx b/ai-assistant/src/components/settings/MCPToolSettings.tsx new file mode 100644 index 0000000000..0d89fb2221 --- /dev/null +++ b/ai-assistant/src/components/settings/MCPToolSettings.tsx @@ -0,0 +1,479 @@ +import { Icon } from '@iconify/react'; +import { Headlamp } from '@kinvolk/headlamp-plugin/lib'; +import { SectionBox } from '@kinvolk/headlamp-plugin/lib/components/common'; +import { + Box, + Button, + Chip, + FormControlLabel, + IconButton, + Paper, + Switch, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Typography, + useTheme, +} from '@mui/material'; +import React, { useCallback, useEffect, useState } from 'react'; +import { parseMCPToolName } from '../../utils/ToolConfigManager'; + +interface MCPToolState { + enabled: boolean; + lastUsed?: Date; + usageCount?: number; +} + +interface MCPServerToolState { + [toolName: string]: MCPToolState; +} + +interface MCPToolsConfig { + [serverName: string]: MCPServerToolState; +} + +interface MCPToolInfo { + name: string; + description?: string; + server: string; + actualToolName: string; + enabled: boolean; + stats: MCPToolState | null; +} + +interface MCPToolSettingsProps { + onConfigChange?: (hasChanges: boolean) => void; +} + +export function MCPToolSettings({ onConfigChange }: MCPToolSettingsProps) { + const theme = useTheme(); + const [loading, setLoading] = useState(true); + const [mcpTools, setMCPTools] = useState([]); + const [toolsConfig, setToolsConfig] = useState({}); + const [originalConfig, setOriginalConfig] = useState({}); + const [hasChanges, setHasChanges] = useState(false); + const [error, setError] = useState(null); + + // Load MCP tools and configuration + const loadMCPToolsAndConfig = useCallback(async () => { + if (!Headlamp.isRunningAsApp() || !window.desktopApi?.mcp) { + setError('MCP tool management is only available in the Headlamp desktop application.'); + setLoading(false); + return; + } + + try { + setLoading(true); + setError(null); + + // Get available MCP tools + const toolsResponse = await window.desktopApi.mcp.getTools(); + if (!toolsResponse.success) { + throw new Error(toolsResponse.error || 'Failed to get MCP tools'); + } + + // Get tools configuration + const configResponse = await window.desktopApi.mcp.getToolsConfig(); + if (!configResponse.success) { + throw new Error(configResponse.error || 'Failed to get MCP tools configuration'); + } + + const config = configResponse.config || {}; + setToolsConfig(config); + setOriginalConfig(JSON.parse(JSON.stringify(config))); // Deep copy for comparison + + // Process tools data + const toolsData: MCPToolInfo[] = []; + + for (const tool of toolsResponse.tools || []) { + const { serverName, toolName: actualToolName } = parseMCPToolName(tool.name); + + // Get tool state from configuration + const serverConfig = config[serverName]; + const toolState = serverConfig?.[actualToolName]; + const enabled = toolState?.enabled !== false; // Default to true if not configured + + // Get tool statistics + let stats: MCPToolState | null = null; + try { + const statsResponse = await window.desktopApi.mcp.getToolStats(serverName, actualToolName); + if (statsResponse.success) { + stats = statsResponse.stats; + } + } catch (error) { + console.warn(`Failed to get stats for tool ${tool.name}:`, error); + } + + toolsData.push({ + name: tool.name, + description: tool.description, + server: serverName, + actualToolName, + enabled, + stats, + }); + } + + // Sort tools by server name and tool name + toolsData.sort((a, b) => { + if (a.server !== b.server) { + return a.server.localeCompare(b.server); + } + return a.actualToolName.localeCompare(b.actualToolName); + }); + + setMCPTools(toolsData); + } catch (error) { + console.error('Error loading MCP tools and configuration:', error); + setError(error instanceof Error ? error.message : 'Unknown error'); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + loadMCPToolsAndConfig(); + }, [loadMCPToolsAndConfig]); + + // Handle tool enable/disable toggle (local state only) + const handleToolToggle = (toolInfo: MCPToolInfo, enabled: boolean) => { + // Update local tool state + setMCPTools(prevTools => + prevTools.map(tool => + tool.name === toolInfo.name ? { ...tool, enabled } : tool + ) + ); + + // Update configuration state + setToolsConfig(prevConfig => { + console.log('Previous config:', prevConfig); + const newConfig = { ...prevConfig }; + if (!newConfig[toolInfo.server]) { + newConfig[toolInfo.server] = {}; + } + if (!newConfig[toolInfo.server][toolInfo.actualToolName]) { + newConfig[toolInfo.server][toolInfo.actualToolName] = { + enabled: true, + usageCount: 0, + }; + } + newConfig[toolInfo.server][toolInfo.actualToolName].enabled = enabled; + return newConfig; + }); + + // Mark as having changes + setHasChanges(true); + onConfigChange?.(true); + }; + + // Refresh tools and configuration + const handleRefresh = () => { + setHasChanges(false); + onConfigChange?.(false); + loadMCPToolsAndConfig(); + }; + + // Save configuration changes + const handleSaveChanges = async () => { + console.log('Saving changes:', toolsConfig); + if (!window.desktopApi?.mcp) { + return; + } + + try { + console.log("Saving MCP tools configuration:", toolsConfig); + const response = await window.desktopApi.mcp.updateToolsConfig(toolsConfig); + if (response.success) { + setHasChanges(false); + onConfigChange?.(false); + setOriginalConfig(JSON.parse(JSON.stringify(toolsConfig))); // Update original config + console.log('MCP tools configuration saved successfully'); + } else { + throw new Error(response.error || 'Failed to save configuration'); + } + } catch (error) { + console.error('Error saving MCP tools configuration:', error); + setError(`Failed to save configuration: ${error instanceof Error ? error.message : 'Unknown error'}`); + } + }; + + // Discard changes and revert to original configuration + const handleDiscardChanges = () => { + setToolsConfig(JSON.parse(JSON.stringify(originalConfig))); // Restore original config + + // Update tools to reflect original configuration + setMCPTools(prevTools => + prevTools.map(tool => { + const serverConfig = originalConfig[tool.server]; + const toolState = serverConfig?.[tool.actualToolName]; + const enabled = toolState?.enabled !== false; // Default to true if not configured + return { ...tool, enabled }; + }) + ); + + setHasChanges(false); + onConfigChange?.(false); + }; + + // Enable all tools (local state only) + const handleEnableAll = () => { + // Update all tools to enabled in local state + setMCPTools(prevTools => + prevTools.map(tool => ({ ...tool, enabled: true })) + ); + + // Update configuration state + setToolsConfig(prevConfig => { + const newConfig = { ...prevConfig }; + for (const tool of mcpTools) { + if (!newConfig[tool.server]) { + newConfig[tool.server] = {}; + } + if (!newConfig[tool.server][tool.actualToolName]) { + newConfig[tool.server][tool.actualToolName] = { + enabled: true, + usageCount: 0, + }; + } + newConfig[tool.server][tool.actualToolName].enabled = true; + } + return newConfig; + }); + + setHasChanges(true); + onConfigChange?.(true); + }; + + // Disable all tools (local state only) + const handleDisableAll = () => { + // Update all tools to disabled in local state + setMCPTools(prevTools => + prevTools.map(tool => ({ ...tool, enabled: false })) + ); + + // Update configuration state + setToolsConfig(prevConfig => { + const newConfig = { ...prevConfig }; + for (const tool of mcpTools) { + if (!newConfig[tool.server]) { + newConfig[tool.server] = {}; + } + if (!newConfig[tool.server][tool.actualToolName]) { + newConfig[tool.server][tool.actualToolName] = { + enabled: true, + usageCount: 0, + }; + } + newConfig[tool.server][tool.actualToolName].enabled = false; + } + return newConfig; + }); + + setHasChanges(true); + onConfigChange?.(true); + }; + + // Format usage count for display + const formatUsageCount = (count?: number): string => { + if (count === undefined || count === 0) return 'Never used'; + return `Used ${count} time${count === 1 ? '' : 's'}`; + }; + + // Format last used date for display + const formatLastUsed = (lastUsed?: Date): string => { + if (!lastUsed) return 'Never'; + const date = new Date(lastUsed); + return date.toLocaleString(); + }; + + // Group tools by server + const groupedTools = mcpTools.reduce((groups, tool) => { + if (!groups[tool.server]) { + groups[tool.server] = []; + } + groups[tool.server].push(tool); + return groups; + }, {} as Record); + + const enabledCount = mcpTools.filter(tool => tool.enabled).length; + const totalCount = mcpTools.length; + + if (loading) { + return ( + + + Loading MCP tools... + + + ); + } + + if (error) { + return ( + + + + {error} + + + + + ); + } + + return ( + + + + Configure individual MCP (Model Context Protocol) tools. You can enable or disable specific tools + to control which capabilities are available to the AI assistant. + + + + + + {hasChanges && ( + } + /> + )} + + + + + {hasChanges && ( + <> + + + + )} + + + + + + + + + + {totalCount === 0 ? ( + + + + No MCP Tools Available + + + No MCP servers are configured or running. Configure MCP servers in the MCP Settings section + to see available tools here. + + + ) : ( + Object.entries(groupedTools).map(([serverName, serverTools]) => ( + + + + {serverName} ({serverTools.length} tools) + + + +
    + + + Tool Name + Description + Usage Statistics + Enabled + + + + {serverTools.map((tool) => ( + + + + {tool.actualToolName} + + + {tool.name} + + + + + {tool.description || 'No description available'} + + + + + + {formatUsageCount(tool.stats?.usageCount)} + + + Last used: {formatLastUsed(tool.stats?.lastUsed)} + + + + + handleToolToggle(tool, e.target.checked)} + size="small" + /> + } + label="" + /> + + + ))} + +
    + +
    + )) + )} + + ); +} \ No newline at end of file diff --git a/ai-assistant/src/index.tsx b/ai-assistant/src/index.tsx index 702a3907ca..97f96fd9fb 100644 --- a/ai-assistant/src/index.tsx +++ b/ai-assistant/src/index.tsx @@ -22,18 +22,26 @@ import { import React from 'react'; import { useHistory } from 'react-router-dom'; import { ModelSelector } from './components'; -import { MCPSettings } from './components/settings/MCPSettings'; +import { MCPSettings } from './components/settings'; import { getDefaultConfig } from './config/modelConfig'; import { PromptWidthProvider } from './contexts/PromptWidthContext'; import { isTestModeCheck } from './helper'; import AIPrompt from './modal'; -import { getSettingsURL, PLUGIN_NAME, pluginStore, useGlobalState, usePluginConfig } from './utils'; +import { + getSettingsURL, + PLUGIN_NAME, + pluginStore, + useGlobalState, + usePluginConfig, + getAllAvailableTools, + isToolEnabled, + toggleTool +} from './utils'; import { getActiveConfig, getSavedConfigurations, SavedConfigurations, } from './utils/ProviderConfigManager'; -import { getAllAvailableTools, isToolEnabled, toggleTool } from './utils/ToolConfigManager'; // Memoized UI Panel component to prevent unnecessary re-renders const AIPanelComponent = React.memo(() => { @@ -469,6 +477,9 @@ function Settings() { {/* MCP Servers Section */} + + {/* MCP Tool Configuration Section */} +
    ); } diff --git a/ai-assistant/src/langchain/tools/ToolManager.ts b/ai-assistant/src/langchain/tools/ToolManager.ts index 8c022af1f1..969238925b 100644 --- a/ai-assistant/src/langchain/tools/ToolManager.ts +++ b/ai-assistant/src/langchain/tools/ToolManager.ts @@ -1,7 +1,7 @@ import { BaseChatModel } from '@langchain/core/language_models/chat_models'; import { DynamicTool } from '@langchain/core/tools'; import { Prompt } from '../../ai/manager'; -import tools from '../../ai/mcp/electron-client'; +import { ElectronMCPClient } from '../../ai/mcp/electron-client'; import { MCPOutputFormatter } from '../formatters/MCPOutputFormatter'; import { KubernetesTool, KubernetesToolContext } from './kubernetes'; import { AVAILABLE_TOOLS, getToolByName } from './registry'; @@ -16,9 +16,11 @@ export class ToolManager { private boundModel: BaseChatModel | null = null; private providerId: string | null = null; private mcpFormatter: MCPOutputFormatter | null = null; + private mcpClient: ElectronMCPClient; constructor(private kubernetesContext?: KubernetesToolContext, enabledToolIds?: string[]) { console.log('🔧 ToolManager: Initializing with enabledToolIds:', enabledToolIds); + this.mcpClient = new ElectronMCPClient(); this.initializeTools(enabledToolIds); this.mcpInitializationPromise = this.initializeMCPTools(enabledToolIds); } @@ -59,22 +61,44 @@ export class ToolManager { private async initializeMCPTools(enabledToolIds?: string[]): Promise { try { console.log('Initializing MCP tools from Electron...'); - const mcpToolsData = await tools(); + + if (!this.mcpClient.isAvailable()) { + console.log('MCP client not available - not running in Electron environment'); + this.mcpToolsInitialized = true; + return; + } + + // Get all available MCP tools + const mcpToolsData = await this.mcpClient.getTools(); if (mcpToolsData && mcpToolsData.length > 0) { console.log(`Successfully loaded ${mcpToolsData.length} MCP tools from Electron`); - // Filter MCP tools by enabled status - const filteredMcpTools = enabledToolIds - ? mcpToolsData.filter(toolData => enabledToolIds.includes(toolData.name)) - : mcpToolsData; + // Filter MCP tools using the new configuration system + const filteredMcpTools: typeof mcpToolsData = []; + + for (const toolData of mcpToolsData) { + // First check legacy enabledToolIds for backward compatibility + if (enabledToolIds && !enabledToolIds.includes(toolData.name)) { + console.log('🚫 ToolManager: Skipping MCP tool (disabled by legacy config):', toolData.name); + continue; + } - if (enabledToolIds) { - console.log( - `Filtered to ${filteredMcpTools.length} enabled MCP tools out of ${mcpToolsData.length} total` - ); + // Then check the new MCP-specific configuration + const isEnabled = await this.mcpClient.isToolEnabled(toolData.name); + if (!isEnabled) { + console.log('🚫 ToolManager: Skipping MCP tool (disabled by MCP config):', toolData.name); + continue; + } + + console.log('✅ ToolManager: Including MCP tool:', toolData.name); + filteredMcpTools.push(toolData); } - console.log('Filtered MCP tools:', filteredMcpTools); + + console.log( + `Filtered to ${filteredMcpTools.length} enabled MCP tools out of ${mcpToolsData.length} total` + ); + console.log('Filtered MCP tools:', filteredMcpTools.map(t => t.name)); // Convert MCP tools to LangChain DynamicTool format this.mcpTools = filteredMcpTools.map( toolData => @@ -687,6 +711,99 @@ export class ToolManager { return [...regularToolNames, ...mcpToolNames]; } + /** + * Get MCP client instance for configuration management + */ + getMCPClient(): ElectronMCPClient { + return this.mcpClient; + } + + /** + * Enable or disable an MCP tool + */ + async setMCPToolEnabled(toolName: string, enabled: boolean): Promise { + if (!this.mcpClient.isAvailable()) { + return false; + } + + const { serverName, toolName: actualToolName } = this.mcpClient.parseToolName(toolName); + const result = await this.mcpClient.setToolEnabled(serverName, actualToolName, enabled); + + if (result) { + // Reinitialize MCP tools to reflect the change + this.mcpToolsInitialized = false; + this.mcpInitializationPromise = this.initializeMCPTools(); + await this.mcpInitializationPromise; + + // If we have a bound model, rebind with the updated tools + if (this.boundModel && this.providerId) { + this.boundModel = this.bindToModel(this.boundModel, this.providerId); + } + } + + return result; + } + + /** + * Check if an MCP tool is enabled + */ + async isMCPToolEnabled(toolName: string): Promise { + if (!this.mcpClient.isAvailable()) { + return true; + } + return await this.mcpClient.isToolEnabled(toolName); + } + + /** + * Get MCP tool statistics + */ + async getMCPToolStats(toolName: string): Promise { + if (!this.mcpClient.isAvailable()) { + return null; + } + + const { serverName, toolName: actualToolName } = this.mcpClient.parseToolName(toolName); + return await this.mcpClient.getToolStats(serverName, actualToolName); + } + + /** + * Get MCP tools configuration + */ + async getMCPToolsConfig(): Promise<{ success: boolean; config?: any; error?: string }> { + if (!this.mcpClient.isAvailable()) { + return { + success: false, + error: 'MCP client not available - not running in Electron environment', + }; + } + return await this.mcpClient.getToolsConfig(); + } + + /** + * Update MCP tools configuration + */ + async updateMCPToolsConfig(config: any): Promise { + if (!this.mcpClient.isAvailable()) { + return false; + } + + const result = await this.mcpClient.updateToolsConfig(config); + + if (result) { + // Reinitialize MCP tools to reflect the changes + this.mcpToolsInitialized = false; + this.mcpInitializationPromise = this.initializeMCPTools(); + await this.mcpInitializationPromise; + + // If we have a bound model, rebind with the updated tools + if (this.boundModel && this.providerId) { + this.boundModel = this.bindToModel(this.boundModel, this.providerId); + } + } + + return result; + } + /** * Detect if an MCP tool result indicates an error */ diff --git a/ai-assistant/src/utils.tsx b/ai-assistant/src/utils.tsx index 1c1218616b..3ecdabb2e3 100644 --- a/ai-assistant/src/utils.tsx +++ b/ai-assistant/src/utils.tsx @@ -2,6 +2,7 @@ import { ConfigStore } from '@kinvolk/headlamp-plugin/lib'; import React from 'react'; import { useBetween } from 'use-between'; import { StoredProviderConfig } from './utils/ProviderConfigManager'; +import { initializeToolsState } from './utils/ToolConfigManager'; export const PLUGIN_NAME = '@headlamp-k8s/ai-assistant'; export const getSettingsURL = () => `/settings/plugins/${encodeURIComponent(PLUGIN_NAME)}`; @@ -21,8 +22,33 @@ function usePluginSettings() { // Add state to control UI panel visibility - initialize from stored settings const [isUIPanelOpen, setIsUIPanelOpenState] = React.useState(conf?.isUIPanelOpen ?? false); - // Add state for enabled tools - initialize from stored settings - const [enabledTools, setEnabledToolsState] = React.useState(conf?.enabledTools ?? []); + // Add state for enabled tools - will be initialized properly using initializeToolsState + const [enabledTools, setEnabledToolsState] = React.useState([]); + const [toolsInitialized, setToolsInitialized] = React.useState(false); + + // Initialize tools state properly on first load + React.useEffect(() => { + if (!toolsInitialized) { + initializeToolsState(conf).then(initializedTools => { + setEnabledToolsState(initializedTools); + setToolsInitialized(true); + + // If this is the first time and we have tools to save, save them + if (!conf?.enabledTools && initializedTools.length > 0) { + const currentConf = pluginStore.get() || {}; + pluginStore.update({ + ...currentConf, + enabledTools: initializedTools, + }); + } + }).catch(error => { + console.error('Failed to initialize tools state:', error); + // Fallback to existing behavior + setEnabledToolsState(conf?.enabledTools ?? []); + setToolsInitialized(true); + }); + } + }, [conf, toolsInitialized]); // Wrap setIsUIPanelOpen to also update the stored configuration const setIsUIPanelOpen = (isOpen: boolean) => { @@ -57,7 +83,21 @@ function usePluginSettings() { setIsUIPanelOpen, enabledTools, setEnabledTools, + toolsInitialized, }; } export const useGlobalState = () => useBetween(usePluginSettings); + +// Export tool configuration utilities +export { + getAllAvailableTools, + isToolEnabled, + toggleTool, + getAllAvailableToolsIncludingMCP, + getEnabledToolIdsIncludingMCP, + isMCPTool, + parseMCPToolName, + isBuiltInTool, + initializeToolsState +} from './utils/ToolConfigManager'; diff --git a/ai-assistant/src/utils/ToolConfigManager.ts b/ai-assistant/src/utils/ToolConfigManager.ts index d9e47f0b23..5029d166fe 100644 --- a/ai-assistant/src/utils/ToolConfigManager.ts +++ b/ai-assistant/src/utils/ToolConfigManager.ts @@ -5,6 +5,7 @@ export type ToolInfo = { id: string; name: string; description: string; + source: 'built-in' | 'mcp'; }; // List of all available tools (add more here as needed) @@ -14,6 +15,7 @@ const AVAILABLE_TOOLS: ToolInfo[] = [ name: 'Kubernetes API Request', description: 'Make requests to the Kubernetes API server to fetch, create, update or delete resources.', + source: 'built-in', }, // Add more tools here as needed ]; @@ -73,3 +75,80 @@ export function setEnabledTools(pluginSettings: any, enabledToolIds: string[]): export function isBuiltInTool(toolName: string): boolean { return AVAILABLE_TOOLS.some(tool => tool.id === toolName); } + +// Check if a tool is an MCP tool by consulting the tool registry +// This is async because we need to fetch MCP tools to check +export async function isMCPTool(toolName: string): Promise { + const allTools = await getAllAvailableToolsIncludingMCP(); + const tool = allTools.find(t => t.id === toolName); + return tool?.source === 'mcp'; +} + +// Get the source of a tool (built-in or MCP) +export async function getToolSource(toolName: string): Promise<'built-in' | 'mcp' | 'unknown'> { + const allTools = await getAllAvailableToolsIncludingMCP(); + const tool = allTools.find(t => t.id === toolName); + return tool?.source || 'unknown'; +} + +// Parse MCP tool name to extract server and tool components +export function parseMCPToolName(fullToolName: string): { serverName: string; toolName: string } { + const parts = fullToolName.split('__'); + if (parts.length >= 2) { + return { + serverName: parts[0], + toolName: parts.slice(1).join('__'), + }; + } + return { + serverName: 'default', + toolName: fullToolName, + }; +} + +// Get all available tools (both built-in and MCP tools) +// This function needs to be async to fetch MCP tools +export async function getAllAvailableToolsIncludingMCP(): Promise { + const builtInTools = getAllAvailableTools(); + + // Try to get MCP tools if running in Electron environment + try { + if (typeof window !== 'undefined' && window.desktopApi?.mcp) { + const mcpResponse = await window.desktopApi.mcp.getTools(); + if (mcpResponse.success && mcpResponse.tools) { + const mcpTools: ToolInfo[] = mcpResponse.tools.map(tool => ({ + id: tool.name, + name: tool.name, + description: tool.description || `MCP tool: ${tool.name}`, + source: 'mcp' as const, + })); + return [...builtInTools, ...mcpTools]; + } + } + } catch (error) { + console.warn('Failed to fetch MCP tools for tool config management:', error); + } + + return builtInTools; +} + +// Get enabled tool IDs including MCP tools +export async function getEnabledToolIdsIncludingMCP(pluginSettings: any): Promise { + const allTools = await getAllAvailableToolsIncludingMCP(); + return allTools.map(tool => tool.id).filter(toolId => isToolEnabled(pluginSettings, toolId)); +} + +// Initialize tools state properly on app load +// This ensures that on first load, all tools are enabled (default behavior) +// but respects any saved configuration if it exists +export async function initializeToolsState(pluginSettings: any): Promise { + const allTools = await getAllAvailableToolsIncludingMCP(); + + // If we have no enabledTools config at all, enable all tools by default + if (!pluginSettings || !pluginSettings.enabledTools) { + return allTools.map(tool => tool.id); + } + + // If we have partial config, use the isToolEnabled logic which defaults to true + return allTools.map(tool => tool.id).filter(toolId => isToolEnabled(pluginSettings, toolId)); +} From 8fbe0d24c24c31a2eebf0cd7960ff52a27b883ab Mon Sep 17 00:00:00 2001 From: ashu8912 Date: Sat, 18 Oct 2025 14:26:05 +0530 Subject: [PATCH 31/39] ai-plugin: Remove logs and cleanup Signed-off-by: ashu8912 --- ai-assistant/src/ai/mcp/electron-client.ts | 34 +--- .../src/components/assistant/ToolsDialog.tsx | 89 +++------- .../components/chat/MCPFormattedMessage.tsx | 6 +- .../mcpOutput/MCPArgumentProcessor.ts | 5 +- .../components/settings/MCPToolSettings.tsx | 7 +- ai-assistant/src/helper/apihelper.tsx | 1 - .../src/langchain/LangChainManager.ts | 157 +----------------- .../src/langchain/tools/ToolManager.ts | 146 ++++++---------- .../tools/kubernetes/KubernetesTool.ts | 7 - ai-assistant/src/modal.tsx | 67 +------- ai-assistant/src/textstream.tsx | 1 + .../src/utils/InlineToolApprovalManager.ts | 7 - ai-assistant/src/utils/ToolApprovalManager.ts | 5 - 13 files changed, 87 insertions(+), 445 deletions(-) diff --git a/ai-assistant/src/ai/mcp/electron-client.ts b/ai-assistant/src/ai/mcp/electron-client.ts index e4c966314b..9a5a67fff2 100644 --- a/ai-assistant/src/ai/mcp/electron-client.ts +++ b/ai-assistant/src/ai/mcp/electron-client.ts @@ -17,7 +17,6 @@ interface MCPResponse { } interface ElectronMCPApi { - getTools: () => Promise; executeTool: ( toolName: string, args: Record, @@ -58,32 +57,6 @@ class ElectronMCPClient { return this.isElectron; } - /** - * Get available MCP tools from Electron main process - */ - async getTools(): Promise { - if (!this.isElectron) { - console.warn('MCP client not available - not running in Electron environment'); - return []; - } - - try { - const response = await window.desktopApi!.mcp.getTools(); - console.log('mcp response from getting tools is', response); - console.log('mcp window desktop api', window.desktopApi!.mcp.getTools); - if (response.success && response.tools) { - console.log('Retrieved MCP tools from Electron:', response.tools.length, 'tools'); - return response.tools; - } else { - console.warn('Failed to get MCP tools:', response.error); - return []; - } - } catch (error) { - console.error('Error getting MCP tools from Electron:', error); - return []; - } - } - /** * Execute an MCP tool via Electron main process */ @@ -97,7 +70,6 @@ class ElectronMCPClient { } try { - console.log('args for tool executed is ', args); const response = await window.desktopApi!.mcp.executeTool(toolName, args, toolCallId); if (response.success) { @@ -194,7 +166,6 @@ class ElectronMCPClient { try { const response = await window.desktopApi!.mcp.updateToolsConfig(config); - console.log("response from updating mcp tools config is ", response); return response.success; } catch (error) { console.error('Error updating MCP tools config:', error); @@ -291,7 +262,7 @@ class ElectronMCPClient { } try { - const allTools = await this.getTools(); + const allTools = await this.getEnabledTools(); const enabledTools: MCPTool[] = []; for (const tool of allTools) { @@ -312,8 +283,7 @@ class ElectronMCPClient { // Export a function that returns tools (compatible with existing interface) const tools = async function (): Promise { const client = new ElectronMCPClient(); - console.log('mcp electron client is ', client); - return await client.getTools(); + return (await client.getToolsConfig()).config; }; // Export both the client class and the tools function for flexibility diff --git a/ai-assistant/src/components/assistant/ToolsDialog.tsx b/ai-assistant/src/components/assistant/ToolsDialog.tsx index 20f8dbbb2f..fd3b3f04bd 100644 --- a/ai-assistant/src/components/assistant/ToolsDialog.tsx +++ b/ai-assistant/src/components/assistant/ToolsDialog.tsx @@ -54,6 +54,7 @@ export const ToolsDialog: React.FC = ({ const [expandedServers, setExpandedServers] = useState>(new Set()); const [, setMcpServers] = useState<{ [key: string]: any }>({}); + console.log("all known MCP tools ", allKnownMcpTools) // Load MCP tools when dialog opens useEffect(() => { if (open) { @@ -92,24 +93,17 @@ export const ToolsDialog: React.FC = ({ }; const loadMcpTools = async () => { - console.log('ToolsDialog: Starting to load MCP tools...'); setIsLoadingMcp(true); try { const mcpClient = new ElectronMCPClient(); - console.log('ToolsDialog: Created MCP client, isAvailable:', mcpClient.isAvailable()); - // Load tools, server configuration, and tools configuration - const [tools, configResponse, toolsConfigResponse] = await Promise.all([ - mcpClient.getTools(), + // Load server configuration and tools configuration - these are our source of truth + const [configResponse, toolsConfigResponse] = await Promise.all([ mcpClient.getConfig(), mcpClient.getToolsConfig(), ]); - console.log('ToolsDialog: Received tools from client:', tools.length, 'tools'); - console.log('ToolsDialog: Tools:', tools); - console.log('ToolsDialog: Config response:', configResponse); - console.log('ToolsDialog: Tools config response:', toolsConfigResponse); - + console.log("config response is ", configResponse) // Store MCP tools configuration if (toolsConfigResponse.success && toolsConfigResponse.config) { setMcpToolsConfig(toolsConfigResponse.config); @@ -129,48 +123,37 @@ export const ToolsDialog: React.FC = ({ setMcpServers(servers); } - // Assign server names to tools based on available servers - // If we have server config, try to match tools to servers - // For now, if there's only one enabled server, assign all tools to it - const enabledServers = Object.values(servers).filter((server: any) => server.enabled); - const toolsWithServer = tools.map(tool => { - if (tool.server) { - return tool; // Tool already has server info - } - - // If we have exactly one enabled server, assign all tools to it - if (enabledServers.length === 1) { - return { ...tool, server: enabledServers[0].name }; - } - - // Otherwise, try to infer from tool name or keep as unknown - const serverNames = Object.keys(servers); - for (const serverName of serverNames) { - if (tool.name.toLowerCase().includes(serverName.toLowerCase().replace('-mcp', ''))) { - return { ...tool, server: serverName }; - } - } - - return { ...tool, server: 'Unknown Server' }; - }); + // Create tools from configuration (this is our source of truth) + const toolsFromConfig: MCPTool[] = []; + if (toolsConfigResponse.success && toolsConfigResponse.config) { + Object.entries(toolsConfigResponse.config).forEach(([serverName, serverTools]: [string, any]) => { + Object.keys(serverTools).forEach((toolName: string) => { + const fullToolName = `${serverName}__${toolName}`; + toolsFromConfig.push({ + name: fullToolName, + description: `Tool: ${toolName}`, + server: serverName + }); + }); + }); + } - // Update allKnownMcpTools by merging new tools with existing ones + // Update allKnownMcpTools with tools from configuration setAllKnownMcpTools(prevKnown => { const knownToolNames = new Set(prevKnown.map(t => t.name)); - const newTools = toolsWithServer.filter(t => !knownToolNames.has(t.name)); - return [...prevKnown, ...newTools]; + const newToolsFromConfig = toolsFromConfig.filter(t => !knownToolNames.has(t.name)); + return [...prevKnown, ...newToolsFromConfig]; }); - // Auto-expand servers that have tools + // Auto-expand servers that have tools in configuration const serversWithTools = new Set(); - toolsWithServer.forEach(tool => { + toolsFromConfig.forEach(tool => { if (tool.server) { serversWithTools.add(tool.server); } }); setExpandedServers(serversWithTools); } catch (error) { - console.error('ToolsDialog: Failed to load MCP tools:', error); setMcpServers({}); } finally { setIsLoadingMcp(false); @@ -236,6 +219,7 @@ export const ToolsDialog: React.FC = ({ }; const isServerEnabled = (serverName: string) => { + console.log("Server name", serverName) const serverTools = allKnownMcpTools.filter(tool => tool.server === serverName); return ( serverTools.length > 0 && @@ -243,12 +227,6 @@ export const ToolsDialog: React.FC = ({ ); }; - const isServerPartiallyEnabled = (serverName: string) => { - const serverTools = allKnownMcpTools.filter(tool => tool.server === serverName); - const enabledCount = serverTools.filter(tool => isMcpToolEnabled(tool.name)).length; - return enabledCount > 0 && enabledCount < serverTools.length; - }; - // Filter tools based on search query - use allKnownMcpTools to show all tools (including disabled ones) const filteredMcpTools = allKnownMcpTools.filter( tool => @@ -286,15 +264,8 @@ export const ToolsDialog: React.FC = ({ if (mcpConfigChanged) { const mcpClient = new ElectronMCPClient(); - console.log('Saving MCP tools configuration:', mcpToolsConfig); if (mcpClient.isAvailable()) { - const response = await mcpClient.updateToolsConfig(mcpToolsConfig); - if (!response) { - console.error('Failed to save MCP tools configuration'); - // Still continue to close dialog, but log the error - } else { - console.log('MCP tools configuration saved successfully'); - } + await mcpClient.updateToolsConfig(mcpToolsConfig); } } @@ -385,16 +356,6 @@ export const ToolsDialog: React.FC = ({ handleToggleServer(serverName); }} onClick={e => e.stopPropagation()} - sx={{ - ...(isServerPartiallyEnabled(serverName) && { - '& .MuiSwitch-thumb': { - backgroundColor: 'orange', - }, - '& .MuiSwitch-track': { - backgroundColor: 'rgba(255, 165, 0, 0.3)', - }, - }), - }} />
    diff --git a/ai-assistant/src/components/chat/MCPFormattedMessage.tsx b/ai-assistant/src/components/chat/MCPFormattedMessage.tsx index 7b3575ebef..08dd3f7eef 100644 --- a/ai-assistant/src/components/chat/MCPFormattedMessage.tsx +++ b/ai-assistant/src/components/chat/MCPFormattedMessage.tsx @@ -23,8 +23,6 @@ const MCPFormattedMessage: React.FC = ({ isAssistant = true, onRetryTool, }) => { - const widthContext = usePromptWidth(); - console.log('From context width ', widthContext.promptWidth); // Try to parse the content as formatted MCP output const parseContent = (): ParsedMCPContent | null => { try { @@ -93,7 +91,6 @@ const MCPFormattedMessage: React.FC = ({ const handleRetry = useCallback(() => { if (!onRetryTool) { - console.log('Retry requested but no retry handler available'); return; } @@ -120,8 +117,7 @@ const MCPFormattedMessage: React.FC = ({ return; } - console.log('Retrying tool:', toolName, 'with args:', originalArgs); - onRetryTool(toolName, originalArgs); + onRetryTool(toolName, originalArgs); } catch (error) { console.error('Failed to parse content for retry:', error); } diff --git a/ai-assistant/src/components/mcpOutput/MCPArgumentProcessor.ts b/ai-assistant/src/components/mcpOutput/MCPArgumentProcessor.ts index 2e01631bbc..92826ec86c 100644 --- a/ai-assistant/src/components/mcpOutput/MCPArgumentProcessor.ts +++ b/ai-assistant/src/components/mcpOutput/MCPArgumentProcessor.ts @@ -43,6 +43,7 @@ export class MCPArgumentProcessor { try { const mcpTools = await tools(); + console.log("mcp tools are ", mcpTools) if (mcpTools && Array.isArray(mcpTools)) { mcpTools.forEach(tool => { this.toolSchemas.set(tool.name, { @@ -52,7 +53,6 @@ export class MCPArgumentProcessor { }); }); this.schemasLoaded = true; - console.log('MCPArgumentProcessor: Loaded schemas for', this.toolSchemas.size, 'tools'); } } catch (error) { console.error('Failed to load MCP tool schemas:', error); @@ -69,7 +69,7 @@ export class MCPArgumentProcessor { userContext?: UserContext ): Promise { await this.loadSchemas(); - + console.log("tools") const schema = this.toolSchemas.get(toolName); const errors: string[] = []; const processed = { ...aiProcessedArgs }; @@ -93,7 +93,6 @@ export class MCPArgumentProcessor { } } - console.log('schema for this tool is ', toolName, schema); if (!schema) { errors.push(`No schema found for tool: ${toolName}`); return { diff --git a/ai-assistant/src/components/settings/MCPToolSettings.tsx b/ai-assistant/src/components/settings/MCPToolSettings.tsx index 0d89fb2221..41002436bf 100644 --- a/ai-assistant/src/components/settings/MCPToolSettings.tsx +++ b/ai-assistant/src/components/settings/MCPToolSettings.tsx @@ -149,7 +149,6 @@ export function MCPToolSettings({ onConfigChange }: MCPToolSettingsProps) { // Update configuration state setToolsConfig(prevConfig => { - console.log('Previous config:', prevConfig); const newConfig = { ...prevConfig }; if (!newConfig[toolInfo.server]) { newConfig[toolInfo.server] = {}; @@ -178,24 +177,20 @@ export function MCPToolSettings({ onConfigChange }: MCPToolSettingsProps) { // Save configuration changes const handleSaveChanges = async () => { - console.log('Saving changes:', toolsConfig); if (!window.desktopApi?.mcp) { return; } try { - console.log("Saving MCP tools configuration:", toolsConfig); - const response = await window.desktopApi.mcp.updateToolsConfig(toolsConfig); + const response = await window.desktopApi.mcp.updateToolsConfig(toolsConfig); if (response.success) { setHasChanges(false); onConfigChange?.(false); setOriginalConfig(JSON.parse(JSON.stringify(toolsConfig))); // Update original config - console.log('MCP tools configuration saved successfully'); } else { throw new Error(response.error || 'Failed to save configuration'); } } catch (error) { - console.error('Error saving MCP tools configuration:', error); setError(`Failed to save configuration: ${error instanceof Error ? error.message : 'Unknown error'}`); } }; diff --git a/ai-assistant/src/helper/apihelper.tsx b/ai-assistant/src/helper/apihelper.tsx index 672d1fcb69..279bf58463 100644 --- a/ai-assistant/src/helper/apihelper.tsx +++ b/ai-assistant/src/helper/apihelper.tsx @@ -393,7 +393,6 @@ export const handleActualApiRequest = async ( ...requestOptions, isJSON: !isLogRequest(cleanedUrl), }); - console.log('API response received:', response); } catch (apiError) { // Handle specific multi-container pod logs error if ( diff --git a/ai-assistant/src/langchain/LangChainManager.ts b/ai-assistant/src/langchain/LangChainManager.ts index c8ebbb3336..cda174cacd 100644 --- a/ai-assistant/src/langchain/LangChainManager.ts +++ b/ai-assistant/src/langchain/LangChainManager.ts @@ -39,10 +39,6 @@ export default class LangChainManager extends AIManager { super(); this.providerId = providerId; const enabledToolIds = enabledTools ?? []; - console.log( - 'AI Assistant: Initializing with enabled tools:', - enabledToolIds || 'all tools enabled' - ); this.toolManager = new ToolManager(undefined, enabledToolIds); // Only enabled tools this.model = this.createModel(providerId, config); @@ -58,22 +54,11 @@ export default class LangChainManager extends AIManager { private setupToolConfirmationListeners() { inlineToolApprovalManager.on('request-confirmation', (data: any) => { // Add the tool confirmation message to chat history - console.log('🔔 LangChainManager: Adding tool confirmation message to history', data); this.addToolConfirmationMessage('', data.toolConfirmation); - console.log( - '📝 LangChainManager: History length after adding confirmation:', - this.history.length - ); }); inlineToolApprovalManager.on('update-confirmation', (data: any) => { // Update the specific tool confirmation message with new state (e.g., loading) - console.log( - '🔄 Tool confirmation update:', - data.requestId, - 'loading:', - data.toolConfirmation.loading - ); this.updateToolConfirmationMessage(data.requestId, data.toolConfirmation); }); } @@ -220,19 +205,13 @@ export default class LangChainManager extends AIManager { const allTools = this.toolManager.getLangChainTools(); // Bind all tools to the model for compatible providers (OpenAI, Azure, etc.) - this.boundModel = this.toolManager.bindToModel(this.model, this.providerId); + // Use the async version to ensure MCP tools are properly included + this.boundModel = await this.toolManager.bindToModelAsync(this.model, this.providerId); // Enable direct tool calling for better performance if (allTools.length > 0 && this.canUseDirectToolCalling()) { this.useDirectToolCalling = true; - console.log('🔧 Direct tool calling enabled for', allTools.length, 'tools'); } - - console.log('🔧 Tools configured:', { - boundModel: !!this.boundModel, - directToolCalling: this.useDirectToolCalling, - toolCount: allTools.length, - }); } /** @@ -320,10 +299,6 @@ export default class LangChainManager extends AIManager { toolConfirmation: any, updateHistoryCallback?: () => void ): void { - console.log('➕ LangChainManager: Adding tool confirmation message', { - content, - toolConfirmation, - }); const confirmationPrompt: Prompt = { role: 'assistant', content: content, @@ -332,11 +307,6 @@ export default class LangChainManager extends AIManager { requestId: toolConfirmation.requestId, // Add requestId for tracking }; this.history.push(confirmationPrompt); - console.log( - '📚 LangChainManager: History after adding confirmation:', - this.history.length, - 'items' - ); // Call the update callback if provided to trigger UI re-render if (updateHistoryCallback) { @@ -345,11 +315,6 @@ export default class LangChainManager extends AIManager { } public updateToolConfirmationMessage(requestId: string, updatedToolConfirmation: any): void { - console.log('🔄 LangChainManager: Updating tool confirmation message', { - requestId, - updatedToolConfirmation, - }); - // Find the message with matching requestId const messageIndex = this.history.findIndex( prompt => prompt.requestId === requestId && prompt.toolConfirmation @@ -361,7 +326,6 @@ export default class LangChainManager extends AIManager { ...this.history[messageIndex], toolConfirmation: updatedToolConfirmation, }; - console.log('✅ LangChainManager: Tool confirmation message updated'); // Use the inline tool approval manager to emit update event inlineToolApprovalManager.emit('message-updated', { requestId, updatedToolConfirmation }); @@ -568,8 +532,6 @@ The user is waiting for you to explain what the tools discovered. Provide a dire // Handle requests using direct tool calling (single LLM call) private async handleDirectToolCallingRequest(message: string): Promise { try { - console.log('🔧 Using direct tool calling for request:', message); - const modelToUse = this.boundModel || this.model; // Prepare input for the model with tools @@ -595,10 +557,8 @@ The user is waiting for you to explain what the tools discovered. Provide a dire // Handle tool calls if present if (response.tool_calls?.length) { - console.log('🔧 Tool calls detected, processing...'); return await this.handleToolCalls(response); } else { - console.log('💬 No tool calls detected, treating as regular message'); // Handle regular response const assistantPrompt: Prompt = { role: 'assistant', @@ -611,8 +571,7 @@ The user is waiting for you to explain what the tools discovered. Provide a dire console.error('Error in direct tool calling request:', error); // If direct tool calling fails, fall back to regular approach - console.log('🔄 Falling back to chain-based approach'); - this.useDirectToolCalling = false; + this.useDirectToolCalling = false; const modelToUse = this.boundModel || this.model; return await this.handleChainBasedRequest(message, modelToUse); @@ -680,12 +639,6 @@ The user is waiting for you to explain what the tools discovered. Provide a dire // IMPORTANT: Use the boundModel (which has tools) instead of the original model const modelToUse = this.boundModel || model; - console.log('🔧 Using model for tool-enabled request:', { - usingBoundModel: !!this.boundModel, - modelHasBindTools: typeof modelToUse.bindTools === 'function', - toolsAvailable: this.toolManager.getToolNames(), - }); - const response = await modelToUse.invoke(messages, { signal: this.currentAbortController.signal, }); @@ -694,17 +647,7 @@ The user is waiting for you to explain what the tools discovered. Provide a dire // Handle tool calls if present if (response.tool_calls?.length) { - console.log( - '🔧 Tool calls detected:', - response.tool_calls.length, - response.tool_calls.map(tc => ({ - name: tc.name, - args: tc.args, - })) - ); return await this.handleToolCalls(response); - } else { - console.log('💬 No tool calls detected in response, treating as regular message'); } // Handle regular response @@ -720,18 +663,8 @@ The user is waiting for you to explain what the tools discovered. Provide a dire private async handleToolCalls(response: any): Promise { const enabledToolIds = this.toolManager.getToolNames(); - console.log('🔧 Tool calls detected, processing...', { - requestedTools: response.tool_calls?.map(tc => tc.name) || [], - enabledTools: enabledToolIds, - enabledToolsCount: enabledToolIds.length, - }); - // If no tools are enabled but LLM is returning tool calls, this indicates a bug if (enabledToolIds.length === 0) { - console.warn('LLM returned tool calls but no tools are enabled. This should not happen.', { - toolCalls: response.tool_calls, - modelUsed: this.boundModel === this.model ? 'original' : 'bound', - }); // Treat as regular response since no tools should be available const assistantPrompt: Prompt = { @@ -757,33 +690,8 @@ The user is waiting for you to explain what the tools discovered. Provide a dire // Only keep tool calls for enabled tools const toolCalls = allToolCalls.filter(tc => enabledToolIds.includes(tc.function.name)); - // Log detailed filtering information - console.log('🔍 Tool filtering details:', { - allRequestedTools: allToolCalls.map(tc => tc.function.name), - enabledTools: enabledToolIds, - allowedTools: toolCalls.map(tc => tc.function.name), - }); - // Log if any tools were filtered out const filteredOutTools = allToolCalls.filter(tc => !enabledToolIds.includes(tc.function.name)); - if (filteredOutTools.length > 0) { - console.log( - '🚫 Filtered out disabled tool calls:', - filteredOutTools.map(tc => tc.function.name) - ); - console.log('✅ Enabled tools:', enabledToolIds); - console.log( - '🔄 Total tool calls requested:', - allToolCalls.length, - 'Allowed:', - toolCalls.length - ); - } else if (allToolCalls.length > 0) { - console.log( - '✅ All requested tools are enabled, proceeding with:', - toolCalls.map(tc => tc.function.name) - ); - } const assistantPrompt: Prompt = { role: 'assistant', @@ -794,8 +702,6 @@ The user is waiting for you to explain what the tools discovered. Provide a dire // If all tool calls were filtered out (all requested tools are disabled), handle gracefully if (toolCalls.length === 0) { - console.log('ℹ️ All requested tools are disabled, providing alternative response'); - // Add informational message about disabled tools if any were filtered if (filteredOutTools.length > 0) { const disabledToolNames = filteredOutTools.map(tc => tc.function.name).join(', '); @@ -852,11 +758,6 @@ Without access to the Kubernetes API, I cannot fetch current pod, deployment, se processedArguments ); - console.log(`🧠 AI-enhanced arguments for ${toolName}:`, { - original: JSON.parse(tc.function.arguments), - enhanced: processedArguments, - }); - // Mark which fields were enhanced by LLM for UI display processedArguments._llmEnhanced = { enhanced: true, @@ -893,31 +794,13 @@ Without access to the Kubernetes API, I cannot fetch current pod, deployment, se // Only request approval for MCP tools if (mcpTools.length > 0) { - console.log('🔐 Requesting approval for', mcpTools.length, 'MCP tools'); const approvedMCPToolIds = await inlineToolApprovalManager.requestApproval( mcpTools, this // Pass the AI manager instance ); approvedToolIds.push(...approvedMCPToolIds); - console.log('✅ MCP tools approved:', approvedMCPToolIds.length, 'of', mcpTools.length); } - // Log built-in tools that were auto-approved - if (builtInToolIds.length > 0) { - console.log( - '✅ Built-in tools auto-executed (no approval needed):', - builtInToolIds.length, - builtInTools.map(tool => tool.name) - ); - } - - console.log( - '✅ Total tools approved:', - approvedToolIds.length, - 'of', - toolCallsForApproval.length - ); - // Filter tool calls to only execute approved ones and update with processed arguments const approvedToolCalls = toolCalls .filter(tc => approvedToolIds.includes(tc.id)) @@ -956,8 +839,6 @@ Without access to the Kubernetes API, I cannot fetch current pod, deployment, se await this.processToolCalls(approvedToolCalls, assistantPrompt); } } catch (error) { - console.log('❌ Tool approval denied or failed:', error.message); - // Add denial responses for all tools for (const toolCall of toolCalls) { this.history.push({ @@ -1257,12 +1138,9 @@ Format your response to make the errors prominent and actionable.`, public async processToolResponses(): Promise { // Check if there are any tool responses in the history if (!this.hasToolResponses()) { - console.log('🔍 No tool responses found in history'); return this.getLastAssistantMessage(); } - console.log('🔍 Processing tool responses from history'); - // Validate tool call/response alignment this.validateToolCallAlignment(); @@ -1455,9 +1333,6 @@ Format your response to make the errors prominent and actionable.`, // Handle the result of tool response processing private async handleToolResponseResult(response: any): Promise { - // Track usage after tool processing - this.logUsageInfo(response); - // Analyze and potentially correct kubectl suggestions const correctedResponse = await this.analyzeAndCorrectResponse(response); @@ -1582,32 +1457,6 @@ Format your response to make the errors prominent and actionable.`, return assistantPrompt; } - // Log usage information - private logUsageInfo(response: any): void { - let providerName = 'AI Service'; - let estimatedTokens = 0; - - // Estimate tokens - const outputLength = this.extractTextContent(response.content).length || 0; - estimatedTokens = Math.ceil(outputLength / 4); - - switch (this.providerId) { - case 'openai': - providerName = 'OpenAI'; - break; - case 'azure': - providerName = 'Azure OpenAI'; - break; - case 'anthropic': - providerName = 'Anthropic'; - break; - case 'local': - providerName = 'Local Model'; - break; - } - - console.log(`${providerName} - Estimated tokens: ${estimatedTokens}`); - } // Analyze response and correct kubectl suggestions private async analyzeAndCorrectResponse(response: any): Promise { diff --git a/ai-assistant/src/langchain/tools/ToolManager.ts b/ai-assistant/src/langchain/tools/ToolManager.ts index 969238925b..6cce5c81c5 100644 --- a/ai-assistant/src/langchain/tools/ToolManager.ts +++ b/ai-assistant/src/langchain/tools/ToolManager.ts @@ -19,10 +19,9 @@ export class ToolManager { private mcpClient: ElectronMCPClient; constructor(private kubernetesContext?: KubernetesToolContext, enabledToolIds?: string[]) { - console.log('🔧 ToolManager: Initializing with enabledToolIds:', enabledToolIds); this.mcpClient = new ElectronMCPClient(); this.initializeTools(enabledToolIds); - this.mcpInitializationPromise = this.initializeMCPTools(enabledToolIds); + this.mcpInitializationPromise = this.initializeMCPTools(); } /** @@ -33,12 +32,10 @@ export class ToolManager { for (const ToolClass of AVAILABLE_TOOLS) { const tempTool = new ToolClass(); if (enabledToolIds && !enabledToolIds.includes(tempTool.config.name)) { - console.log('🚫 ToolManager: Skipping tool (disabled):', tempTool.config.name); continue; // Skip tools not enabled } try { const tool = tempTool; - console.log('✅ ToolManager: Initializing tool:', tempTool.config.name); this.addTool(tool); } catch (error) { console.error(`Failed to load tool ${ToolClass.name}:`, error); @@ -46,59 +43,57 @@ export class ToolManager { } // Initialize MCP tools asynchronously but start immediately - this.initializeMCPTools(enabledToolIds); - - // Log final initialized tools - console.log( - '🔧 ToolManager: Regular tools initialized:', - this.tools.map(t => t.config.name) - ); + this.initializeMCPTools(); } /** * Initialize MCP tools from Electron main process */ - private async initializeMCPTools(enabledToolIds?: string[]): Promise { + private async initializeMCPTools(): Promise { try { - console.log('Initializing MCP tools from Electron...'); if (!this.mcpClient.isAvailable()) { - console.log('MCP client not available - not running in Electron environment'); this.mcpToolsInitialized = true; return; } - // Get all available MCP tools - const mcpToolsData = await this.mcpClient.getTools(); - - if (mcpToolsData && mcpToolsData.length > 0) { - console.log(`Successfully loaded ${mcpToolsData.length} MCP tools from Electron`); + // Get tools configuration (source of truth) + const toolsConfigResponse = await this.mcpClient.getToolsConfig(); - // Filter MCP tools using the new configuration system - const filteredMcpTools: typeof mcpToolsData = []; - - for (const toolData of mcpToolsData) { - // First check legacy enabledToolIds for backward compatibility - if (enabledToolIds && !enabledToolIds.includes(toolData.name)) { - console.log('🚫 ToolManager: Skipping MCP tool (disabled by legacy config):', toolData.name); - continue; - } + if (!toolsConfigResponse.success || !toolsConfigResponse.config) { + this.mcpToolsInitialized = true; + return; + } - // Then check the new MCP-specific configuration - const isEnabled = await this.mcpClient.isToolEnabled(toolData.name); - if (!isEnabled) { - console.log('🚫 ToolManager: Skipping MCP tool (disabled by MCP config):', toolData.name); - continue; - } + // Create tools from configuration + const mcpToolsData: any[] = []; + Object.entries(toolsConfigResponse.config).forEach(([serverName, serverTools]: [string, any]) => { + Object.entries(serverTools).forEach(([toolName, toolConfig]: [string, any]) => { + const fullToolName = `${serverName}__${toolName}`; + mcpToolsData.push({ + name: fullToolName, + description: `Tool: ${toolName} from ${serverName} server`, + inputSchema: {}, // We'll get this later if needed + server: serverName, + enabled: toolConfig.enabled !== false + }); + }); + }); - console.log('✅ ToolManager: Including MCP tool:', toolData.name); - filteredMcpTools.push(toolData); + // Filter MCP tools using the MCP configuration (not legacy enabledToolIds) + const filteredMcpTools: typeof mcpToolsData = []; + + for (const toolData of mcpToolsData) { + // MCP tools are controlled by their own configuration system, not legacy enabledToolIds + // Check if tool is enabled in MCP configuration + if (!toolData.enabled) { + continue; } - console.log( - `Filtered to ${filteredMcpTools.length} enabled MCP tools out of ${mcpToolsData.length} total` - ); - console.log('Filtered MCP tools:', filteredMcpTools.map(t => t.name)); + filteredMcpTools.push(toolData); + } + + if (filteredMcpTools.length > 0) { // Convert MCP tools to LangChain DynamicTool format this.mcpTools = filteredMcpTools.map( toolData => @@ -112,36 +107,17 @@ export class ToolManager { // LangChain may wrap args in different formats, need to handle properly const mappedArgs = this.mapMCPToolArguments(args, toolData.inputSchema); - console.log( - `MCP tool ${toolData.name} called with original args:`, - JSON.stringify(args) - ); - console.log( - `MCP tool ${toolData.name} input schema:`, - JSON.stringify(toolData.inputSchema) - ); - console.log( - `MCP tool ${toolData.name} calling with mapped args:`, - JSON.stringify(mappedArgs) - ); - // Execute MCP tool through Electron API const result = await window.desktopApi?.mcp.executeTool( toolData.name, mappedArgs ); - console.log(`MCP tool ${toolData.name} returned result:`, result); - // Extract actual result from MCP response const actualResult = result?.result || result; - console.log(`MCP tool ${toolData.name} actual result:`, actualResult); - console.log(`MCP tool ${toolData.name} result type:`, typeof actualResult); // Ensure we return a string response const response = typeof actualResult === 'string' ? actualResult : JSON.stringify(actualResult); - console.log(`MCP tool ${toolData.name} final response:`, response); - return response; } catch (error) { console.error(`Error executing MCP tool ${toolData.name}:`, error); @@ -151,23 +127,16 @@ export class ToolManager { }) ); - console.log(`Converted ${this.mcpTools.length} MCP tools to LangChain format`); this.mcpToolsInitialized = true; // If we have a bound model, rebind with the new MCP tools if (this.boundModel && this.providerId) { - console.log('🔄 Rebinding model with newly loaded MCP tools'); this.boundModel = this.bindToModel(this.boundModel, this.providerId); } } else { - console.log('No MCP tools available or MCP client not initialized'); this.mcpToolsInitialized = true; } } catch (error) { - console.warn( - 'Failed to initialize MCP tools from Electron:', - error instanceof Error ? error.message : 'Unknown error' - ); this.mcpToolsInitialized = true; // Continue without MCP tools - this is not a fatal error } @@ -178,8 +147,6 @@ export class ToolManager { * Handles common argument wrapping patterns and schema mismatches */ private mapMCPToolArguments(args: any, inputSchema?: any): any { - console.log('Mapping MCP tool arguments:', { args, inputSchema }); - if (!inputSchema) { // If no schema, return args as-is return args; @@ -190,7 +157,6 @@ export class ToolManager { // Handle tools that expect no parameters if (!schemaProps || Object.keys(schemaProps).length === 0) { // Return empty object for tools that expect no parameters - console.log('Tool expects no parameters, returning empty object'); return {}; } @@ -205,13 +171,9 @@ export class ToolManager { const requiredProps = inputSchema?.required || []; if (requiredProps.length === 0) { // Tool has no required parameters, safe to return empty object - console.log('Tool has no required parameters, returning empty object'); return {}; } else { // Tool has required parameters but got empty args - create default structure - console.log( - 'Tool has required parameters but got empty args, creating default parameter structure' - ); return this.createDefaultParameterStructure(inputSchema); } } @@ -231,7 +193,6 @@ export class ToolManager { // If we found valid schema fields, use the cleaned args if (Object.keys(cleanArgs).length > 0) { - console.log('Found valid schema fields, using cleaned args:', cleanArgs); return this.filterMCPArguments(cleanArgs, inputSchema); } @@ -243,7 +204,6 @@ export class ToolManager { if (!inputValue || inputValue === '' || inputValue === '""') { const requiredProps = inputSchema?.required || []; if (requiredProps.length === 0) { - console.log('Unwrapped input is empty and no required params, returning empty object'); return {}; } } @@ -256,13 +216,12 @@ export class ToolManager { typeof inputValue === 'boolean') ) { // Map the primitive value to the single expected property - console.log(`Mapping primitive input to single property: ${schemaPropertyNames[0]}`); return { [schemaPropertyNames[0]]: inputValue }; } // If the input is an object, try to unwrap it if (typeof inputValue === 'object' && inputValue !== null) { - console.log('Unwrapping object input'); + return this.filterMCPArguments(inputValue, inputSchema); return this.filterMCPArguments(inputValue, inputSchema); } @@ -270,23 +229,18 @@ export class ToolManager { if (typeof inputValue === 'string') { // Try common parameter names if (schemaProps.query) { - console.log('Mapping string input to query parameter'); return { query: inputValue }; } if (schemaProps.path) { - console.log('Mapping string input to path parameter'); return { path: inputValue }; } if (schemaProps.directory) { - console.log('Mapping string input to directory parameter'); return { directory: inputValue }; } if (schemaProps.file) { - console.log('Mapping string input to file parameter'); return { file: inputValue }; } if (schemaProps.name) { - console.log('Mapping string input to name parameter'); return { name: inputValue }; } @@ -294,19 +248,17 @@ export class ToolManager { const requiredProps = inputSchema?.required || []; const targetProp = requiredProps.length > 0 ? requiredProps[0] : schemaPropertyNames[0]; if (targetProp) { - console.log(`Mapping string input to target property: ${targetProp}`); return { [targetProp]: inputValue }; } } // Return the unwrapped input and let the tool handle validation - console.log('Returning unwrapped input value'); + return inputValue; return inputValue; } } // If args structure matches or is already properly formatted, filter and return - console.log('Args appear to be in correct format, filtering empty values'); return this.filterMCPArguments(args, inputSchema); } @@ -369,12 +321,6 @@ export class ToolManager { filteredArgs[key] = value; } } - - console.log('Filtered MCP arguments:', { - original: args, - filtered: filteredArgs, - required: requiredProps, - }); return filteredArgs; } @@ -436,7 +382,6 @@ export class ToolManager { } } - console.log('Created default parameter structure:', defaultParams); return defaultParams; } @@ -662,7 +607,6 @@ export class ToolManager { // Initialize MCP formatter with the model if (!this.mcpFormatter) { this.mcpFormatter = new MCPOutputFormatter(model); - console.log('🎨 MCP output formatter initialized'); } const langChainTools = this.getLangChainTools(); @@ -671,12 +615,6 @@ export class ToolManager { this.boundModel = model; return model; } - - console.log( - `Binding ${langChainTools.length} tools to ${providerId} model:`, - langChainTools.map(t => t.name) - ); - this.boundModel = model.bindTools(langChainTools); return this.boundModel; } catch (error) { @@ -686,6 +624,16 @@ export class ToolManager { } } + /** + * Bind all tools to a LangChain model, waiting for MCP tools to initialize first + */ + async bindToModelAsync(model: BaseChatModel, providerId: string): Promise { + // Wait for MCP tools to initialize before binding + await this.waitForMCPToolsInitialization(); + + return this.bindToModel(model, providerId); + } + /** * Wait for MCP tools to be initialized */ diff --git a/ai-assistant/src/langchain/tools/kubernetes/KubernetesTool.ts b/ai-assistant/src/langchain/tools/kubernetes/KubernetesTool.ts index 2a10f6188c..ad190de8ff 100644 --- a/ai-assistant/src/langchain/tools/kubernetes/KubernetesTool.ts +++ b/ai-assistant/src/langchain/tools/kubernetes/KubernetesTool.ts @@ -95,16 +95,9 @@ LOG HANDLING FOR MULTI-CONTAINER PODS: pendingPrompt ): Promise => { if (!this.context) { - console.error('Kubernetes tool context not configured'); throw new Error('Kubernetes tool context not configured'); } - console.log(`Processing kubernetes_api_request tool: ${method} ${url}`, { - hasContext: !!this.context, - toolCallId, - selectedClusters: this.context?.selectedClusters, - }); - // For GET requests, we can execute them immediately using the API helper if (method.toUpperCase() === 'GET') { try { diff --git a/ai-assistant/src/modal.tsx b/ai-assistant/src/modal.tsx index 8a0cf22bf6..aa25a434c7 100644 --- a/ai-assistant/src/modal.tsx +++ b/ai-assistant/src/modal.tsx @@ -280,7 +280,6 @@ export default function AIPrompt(props: { ...activeConfig.config, model: selectedModel, }; - console.log('🔄 Creating new LangChainManager with enabledTools:', enabledTools); const newManager = new LangChainManager( activeConfig.providerId, configWithModel, @@ -299,18 +298,6 @@ export default function AIPrompt(props: { setPromptHistory([]); return; } - - console.log('🔄 UpdateHistory called, aiManager.history length:', aiManager.history.length); - console.log( - '📋 Current aiManager.history:', - aiManager.history.map(h => ({ - role: h.role, - hasToolConfirmation: !!h.toolConfirmation, - content: h.content?.substring(0, 50), - isDisplayOnly: h.isDisplayOnly, - })) - ); - // Process the history to extract suggestions and clean content const processedHistory = aiManager.history.map((prompt, index) => { if (prompt.role === 'assistant' && prompt.content && !prompt.error) { @@ -330,16 +317,6 @@ export default function AIPrompt(props: { return prompt; }); - console.log('✅ ProcessedHistory length:', processedHistory.length); - console.log( - '📝 ProcessedHistory items:', - processedHistory.map(h => ({ - role: h.role, - hasToolConfirmation: !!h.toolConfirmation, - isDisplayOnly: h.isDisplayOnly, - content: h.content?.substring(0, 50), - })) - ); setPromptHistory(processedHistory); }, [aiManager?.history]); @@ -349,8 +326,7 @@ export default function AIPrompt(props: { // Set up event listeners for tool confirmation events React.useEffect(() => { - const handleRequestConfirmation = (data: any) => { - console.log('🎯 Request confirmation event received:', data); + const handleRequestConfirmation = () => { // Clear loading state when tool approval is requested setLoading(false); // Force an immediate update of the history from the AI manager @@ -359,14 +335,12 @@ export default function AIPrompt(props: { setPromptHistory(prev => [...prev]); }; - const handleUpdateConfirmation = (data: any) => { - console.log('🔄 Update confirmation event received:', data); + const handleUpdateConfirmation = () => { updateHistory(); setPromptHistory(prev => [...prev]); }; - const handleMessageUpdated = (data: any) => { - console.log('📝 Message updated event received:', data); + const handleMessageUpdated = () => { updateHistory(); setPromptHistory(prev => [...prev]); }; @@ -382,35 +356,6 @@ export default function AIPrompt(props: { }; }, [updateHistory]); - const handleOperationSuccess = React.useCallback( - (response: any) => { - // Add the response to the conversation - const operationType = response.metadata?.deletionTimestamp ? 'deletion' : 'application'; - - const toolPrompt: Prompt = { - role: 'tool', - content: `Resource ${operationType} completed successfully: ${JSON.stringify( - { - kind: response.kind, - name: response.metadata.name, - namespace: response.metadata.namespace, - status: 'Success', - }, - null, - 2 - )}`, - name: 'kubernetes_api_request', - toolCallId: `${operationType}-${Date.now()}`, - }; - - if (aiManager) { - aiManager.history.push(toolPrompt); - updateHistory(); - } - }, - [aiManager, updateHistory] - ); - const handleOperationFailure = React.useCallback( (error: any, operationType: string, resourceInfo?: any) => { // Determine the operation type from the error or method @@ -568,8 +513,6 @@ export default function AIPrompt(props: { } try { - console.log(`Retrying tool ${toolName} with args:`, args); - // Get the tool manager from the LangChain manager const toolManager = (aiManager as any).toolManager; if (!toolManager) { @@ -871,7 +814,7 @@ export default function AIPrompt(props: { history={memoizedHistory} isLoading={loading} apiError={apiError} - onOperationSuccess={handleOperationSuccess} + onOperationSuccess={() => {}} onOperationFailure={handleOperationFailure} onYamlAction={handleYamlAction} onRetryTool={handleRetryTool} @@ -944,7 +887,7 @@ export default function AIPrompt(props: { yamlContent={editorContent} title={editorTitle} resourceType={resourceType} - onSuccess={handleOperationSuccess} + onSuccess={() => {}} onFailure={handleOperationFailure} /> )} diff --git a/ai-assistant/src/textstream.tsx b/ai-assistant/src/textstream.tsx index 641ae75fc8..b5eaa37971 100644 --- a/ai-assistant/src/textstream.tsx +++ b/ai-assistant/src/textstream.tsx @@ -326,6 +326,7 @@ const TextStreamContainer = React.memo(function TextStreamContainer({ onApprove={prompt.toolConfirmation.onApprove} onDeny={prompt.toolConfirmation.onDeny} loading={prompt.toolConfirmation.loading} + userContext={prompt.toolConfirmation.userContext} compact={false} /> ) : ( diff --git a/ai-assistant/src/utils/InlineToolApprovalManager.ts b/ai-assistant/src/utils/InlineToolApprovalManager.ts index a53ada3cb2..01a5c5ed12 100644 --- a/ai-assistant/src/utils/InlineToolApprovalManager.ts +++ b/ai-assistant/src/utils/InlineToolApprovalManager.ts @@ -89,7 +89,6 @@ export class InlineToolApprovalManager extends EventEmitter { async requestApproval(toolCalls: any[], aiManager: any): Promise { // Check if session auto-approval is enabled if (this.sessionAutoApproval) { - console.log('Auto-approving tools due to session setting'); return toolCalls.map(tool => tool.id); } @@ -107,7 +106,6 @@ export class InlineToolApprovalManager extends EventEmitter { // If all tools are auto-approved, return them if (needsApprovalTools.length === 0) { - console.log('All tools auto-approved:', autoApprovedTools); return autoApprovedTools; } @@ -197,7 +195,6 @@ export class InlineToolApprovalManager extends EventEmitter { const allToolIds = this.pendingRequest.toolCalls.map(tool => tool.id); if (approvedToolIds.length === allToolIds.length) { this.sessionAutoApproval = true; - console.log('Session auto-approval enabled'); } else { // Remember individual tool approvals for (const toolCall of this.pendingRequest.toolCalls) { @@ -205,7 +202,6 @@ export class InlineToolApprovalManager extends EventEmitter { this.autoApproveSettings.set(toolCall.name, true); } } - console.log('Individual tool approvals saved'); } } @@ -237,7 +233,6 @@ export class InlineToolApprovalManager extends EventEmitter { public clearSession(): void { this.sessionAutoApproval = false; this.autoApproveSettings.clear(); - console.log('Tool approval session settings cleared'); } /** @@ -245,7 +240,6 @@ export class InlineToolApprovalManager extends EventEmitter { */ public setSessionAutoApproval(enabled: boolean): void { this.sessionAutoApproval = enabled; - console.log(`Session auto-approval ${enabled ? 'enabled' : 'disabled'}`); } /** @@ -264,7 +258,6 @@ export class InlineToolApprovalManager extends EventEmitter { } else { this.autoApproveSettings.delete(toolName); } - console.log(`Tool "${toolName}" auto-approval ${enabled ? 'enabled' : 'disabled'}`); } /** diff --git a/ai-assistant/src/utils/ToolApprovalManager.ts b/ai-assistant/src/utils/ToolApprovalManager.ts index 95c9766a88..843e82ddca 100644 --- a/ai-assistant/src/utils/ToolApprovalManager.ts +++ b/ai-assistant/src/utils/ToolApprovalManager.ts @@ -38,7 +38,6 @@ export class ToolApprovalManager extends EventEmitter { public async requestApproval(toolCalls: ToolCall[]): Promise { // Check if session auto-approval is enabled if (this.sessionAutoApproval) { - console.log('Auto-approving tools due to session setting'); return toolCalls.map(tool => tool.id); } @@ -56,7 +55,6 @@ export class ToolApprovalManager extends EventEmitter { // If all tools are auto-approved, return them if (needsApprovalTools.length === 0) { - console.log('All tools auto-approved:', autoApprovedTools); return autoApprovedTools; } @@ -103,7 +101,6 @@ export class ToolApprovalManager extends EventEmitter { const allToolIds = this.pendingRequest.toolCalls.map(tool => tool.id); if (approvedToolIds.length === allToolIds.length) { this.sessionAutoApproval = true; - console.log('Session auto-approval enabled'); } else { // Remember individual tool approvals for (const toolCall of this.pendingRequest.toolCalls) { @@ -111,7 +108,6 @@ export class ToolApprovalManager extends EventEmitter { this.autoApproveSettings.set(toolCall.name, true); } } - console.log('Individual tool approvals saved'); } } @@ -143,7 +139,6 @@ export class ToolApprovalManager extends EventEmitter { public clearSession(): void { this.sessionAutoApproval = false; this.autoApproveSettings.clear(); - console.log('Tool approval session settings cleared'); } /** From bc6bb84e0626b266059cbe21cb0e3ec66f36bfca Mon Sep 17 00:00:00 2001 From: ashu8912 Date: Sun, 19 Oct 2025 12:53:12 +0530 Subject: [PATCH 32/39] ai-plugin: Fix tool args passing Signed-off-by: ashu8912 --- .../src/components/assistant/ToolsDialog.tsx | 2 -- .../common/InlineToolConfirmation.tsx | 6 ++--- .../mcpOutput/MCPArgumentProcessor.ts | 23 +++++++++++-------- .../src/langchain/LangChainManager.ts | 1 + .../src/langchain/tools/ToolManager.ts | 4 ++-- 5 files changed, 20 insertions(+), 16 deletions(-) diff --git a/ai-assistant/src/components/assistant/ToolsDialog.tsx b/ai-assistant/src/components/assistant/ToolsDialog.tsx index fd3b3f04bd..20b332c301 100644 --- a/ai-assistant/src/components/assistant/ToolsDialog.tsx +++ b/ai-assistant/src/components/assistant/ToolsDialog.tsx @@ -54,7 +54,6 @@ export const ToolsDialog: React.FC = ({ const [expandedServers, setExpandedServers] = useState>(new Set()); const [, setMcpServers] = useState<{ [key: string]: any }>({}); - console.log("all known MCP tools ", allKnownMcpTools) // Load MCP tools when dialog opens useEffect(() => { if (open) { @@ -219,7 +218,6 @@ export const ToolsDialog: React.FC = ({ }; const isServerEnabled = (serverName: string) => { - console.log("Server name", serverName) const serverTools = allKnownMcpTools.filter(tool => tool.server === serverName); return ( serverTools.length > 0 && diff --git a/ai-assistant/src/components/common/InlineToolConfirmation.tsx b/ai-assistant/src/components/common/InlineToolConfirmation.tsx index 4b05bb700d..313ed41fea 100644 --- a/ai-assistant/src/components/common/InlineToolConfirmation.tsx +++ b/ai-assistant/src/components/common/InlineToolConfirmation.tsx @@ -114,7 +114,7 @@ const InlineToolConfirmation: React.FC = ({ try { // Update the original toolCalls with edited arguments toolCalls.forEach(tool => { - if (editedArguments[tool.id]) { + if (selectedToolIds.includes(tool.id) && editedArguments[tool.id]) { const edited = editedArguments[tool.id]; if (tool.type === 'mcp') { @@ -131,10 +131,10 @@ const InlineToolConfirmation: React.FC = ({ // For regular tools, use edited arguments as-is tool.arguments = edited; } - } + } }); - onApprove(selectedToolIds); + onApprove(selectedToolIds); } catch (error) { console.error('Error during tool approval:', error); setIsApproving(false); diff --git a/ai-assistant/src/components/mcpOutput/MCPArgumentProcessor.ts b/ai-assistant/src/components/mcpOutput/MCPArgumentProcessor.ts index 92826ec86c..2b93df6939 100644 --- a/ai-assistant/src/components/mcpOutput/MCPArgumentProcessor.ts +++ b/ai-assistant/src/components/mcpOutput/MCPArgumentProcessor.ts @@ -1,4 +1,4 @@ -import tools from '../../ai/mcp/electron-client'; +import { ElectronMCPClient } from '../../ai/mcp/electron-client'; export interface MCPToolSchema { name: string; @@ -42,14 +42,19 @@ export class MCPArgumentProcessor { if (this.schemasLoaded) return; try { - const mcpTools = await tools(); - console.log("mcp tools are ", mcpTools) - if (mcpTools && Array.isArray(mcpTools)) { - mcpTools.forEach(tool => { - this.toolSchemas.set(tool.name, { - name: tool.name, - description: tool.description, - inputSchema: tool.inputSchema, + const mcpClient = new ElectronMCPClient(); + const toolsConfigResponse = await mcpClient.getToolsConfig(); + + if (toolsConfigResponse && toolsConfigResponse.config) { + // Parse the new structure: { "serverName": { "toolName": { enabled, inputSchema, ... } } } + Object.entries(toolsConfigResponse.config).forEach(([serverName, serverTools]: [string, any]) => { + Object.entries(serverTools).forEach(([toolName, toolConfig]: [string, any]) => { + const fullToolName = `${serverName}__${toolName}`; + this.toolSchemas.set(fullToolName, { + name: fullToolName, + description: toolConfig.description, + inputSchema: toolConfig.inputSchema, + }); }); }); this.schemasLoaded = true; diff --git a/ai-assistant/src/langchain/LangChainManager.ts b/ai-assistant/src/langchain/LangChainManager.ts index cda174cacd..55a0f0172e 100644 --- a/ai-assistant/src/langchain/LangChainManager.ts +++ b/ai-assistant/src/langchain/LangChainManager.ts @@ -883,6 +883,7 @@ Without access to the Kubernetes API, I cannot fetch current pod, deployment, se for (const toolCall of toolCalls) { const args = JSON.parse(toolCall.function.arguments); + console.log('🔧 LangChainManager: Executing tool', toolCall.function.name, 'with parsed args:', args); try { // Execute the tool call using ToolManager diff --git a/ai-assistant/src/langchain/tools/ToolManager.ts b/ai-assistant/src/langchain/tools/ToolManager.ts index 6cce5c81c5..25b3d31892 100644 --- a/ai-assistant/src/langchain/tools/ToolManager.ts +++ b/ai-assistant/src/langchain/tools/ToolManager.ts @@ -72,8 +72,8 @@ export class ToolManager { const fullToolName = `${serverName}__${toolName}`; mcpToolsData.push({ name: fullToolName, - description: `Tool: ${toolName} from ${serverName} server`, - inputSchema: {}, // We'll get this later if needed + description: toolConfig.description || `Tool: ${toolName} from ${serverName} server`, + inputSchema: toolConfig.inputSchema || {}, // Use the actual schema from MCP config server: serverName, enabled: toolConfig.enabled !== false }); From df53824e282dfc18fc2f9d9dbd6b070eb7bdb032 Mon Sep 17 00:00:00 2001 From: ashu8912 Date: Sun, 19 Oct 2025 13:27:10 +0530 Subject: [PATCH 33/39] ai-plugin: UI fixes Signed-off-by: ashu8912 --- .../src/components/assistant/AIInputSection.tsx | 6 +++++- .../src/components/chat/MCPFormattedMessage.tsx | 1 - .../src/components/settings/MCPToolSettings.tsx | 4 ++-- ai-assistant/src/langchain/LangChainManager.ts | 2 ++ ai-assistant/src/modal.tsx | 11 +++++++++++ ai-assistant/src/textstream.tsx | 3 ++- 6 files changed, 22 insertions(+), 5 deletions(-) diff --git a/ai-assistant/src/components/assistant/AIInputSection.tsx b/ai-assistant/src/components/assistant/AIInputSection.tsx index 00937b19e1..e801a00f51 100644 --- a/ai-assistant/src/components/assistant/AIInputSection.tsx +++ b/ai-assistant/src/components/assistant/AIInputSection.tsx @@ -133,7 +133,11 @@ export const AIInputSection: React.FC = ({ }} /> - + .MuiGrid-item': { + maxWidth: '100% !important' + } + }}> diff --git a/ai-assistant/src/components/chat/MCPFormattedMessage.tsx b/ai-assistant/src/components/chat/MCPFormattedMessage.tsx index 08dd3f7eef..66341c3844 100644 --- a/ai-assistant/src/components/chat/MCPFormattedMessage.tsx +++ b/ai-assistant/src/components/chat/MCPFormattedMessage.tsx @@ -1,7 +1,6 @@ import { Icon } from '@iconify/react'; import { Alert, Box, Paper, Typography } from '@mui/material'; import React, { useCallback } from 'react'; -import { usePromptWidth } from '../../contexts/PromptWidthContext'; import { FormattedMCPOutput } from '../../langchain/formatters/MCPOutputFormatter'; import MCPOutputDisplay from '../mcpOutput/MCPOutputDisplay'; diff --git a/ai-assistant/src/components/settings/MCPToolSettings.tsx b/ai-assistant/src/components/settings/MCPToolSettings.tsx index 41002436bf..34a1286af1 100644 --- a/ai-assistant/src/components/settings/MCPToolSettings.tsx +++ b/ai-assistant/src/components/settings/MCPToolSettings.tsx @@ -70,7 +70,7 @@ export function MCPToolSettings({ onConfigChange }: MCPToolSettingsProps) { setError(null); // Get available MCP tools - const toolsResponse = await window.desktopApi.mcp.getTools(); + const toolsResponse = await window.desktopApi.mcp.getToolsConfig(); if (!toolsResponse.success) { throw new Error(toolsResponse.error || 'Failed to get MCP tools'); } @@ -88,7 +88,7 @@ export function MCPToolSettings({ onConfigChange }: MCPToolSettingsProps) { // Process tools data const toolsData: MCPToolInfo[] = []; - for (const tool of toolsResponse.tools || []) { + for (const tool of toolsResponse.config || []) { const { serverName, toolName: actualToolName } = parseMCPToolName(tool.name); // Get tool state from configuration diff --git a/ai-assistant/src/langchain/LangChainManager.ts b/ai-assistant/src/langchain/LangChainManager.ts index 55a0f0172e..5fc49f4808 100644 --- a/ai-assistant/src/langchain/LangChainManager.ts +++ b/ai-assistant/src/langchain/LangChainManager.ts @@ -1409,8 +1409,10 @@ Format your response to make the errors prominent and actionable.`, // For single formatted MCP output, return just the content if (recentToolResponses.length === 1) { + console.log('🔧 Taking single tool response path'); fallbackContent = content; } else { + console.log('🔧 Taking multiple tool responses path'); // For multiple tools, use the tool name format fallbackContent += `${toolName}: ${content}${ index < recentToolResponses.length - 1 ? '\n\n' : '' diff --git a/ai-assistant/src/modal.tsx b/ai-assistant/src/modal.tsx index aa25a434c7..f075b3bf3f 100644 --- a/ai-assistant/src/modal.tsx +++ b/ai-assistant/src/modal.tsx @@ -778,6 +778,10 @@ export default function AIPrompt(props: { overflow: 'hidden', // Prevent horizontal overflow maxWidth: '100%', minWidth: 0, + // Global override for all MUI Grid items to prevent max-width: none + '& .MuiGrid-root > .MuiGrid-item': { + maxWidth: '100% !important' + }, }} > .MuiGrid-item': { + maxWidth: '100% !important' + } }} > ) : ( From 6a48fa594246a51d29b1ee0e06d1685491f6e15b Mon Sep 17 00:00:00 2001 From: ashu8912 Date: Sun, 19 Oct 2025 13:43:29 +0530 Subject: [PATCH 34/39] ai-plugin: Format and lint fixes Signed-off-by: ashu8912 --- ai-assistant/src/ai/mcp/electron-client.ts | 7 ++- .../components/assistant/AIInputSection.tsx | 15 +++-- .../src/components/assistant/ToolsDialog.tsx | 33 +++++------ .../components/chat/MCPFormattedMessage.tsx | 2 +- .../common/InlineToolConfirmation.tsx | 4 +- .../mcpOutput/MCPArgumentProcessor.ts | 24 ++++---- .../components/settings/MCPToolSettings.tsx | 59 ++++++++++--------- ai-assistant/src/helper/apihelper.tsx | 8 +-- ai-assistant/src/index.tsx | 16 ++--- .../src/langchain/LangChainManager.ts | 11 ++-- .../src/langchain/tools/ToolManager.ts | 45 +++++++------- ai-assistant/src/modal.tsx | 12 ++-- ai-assistant/src/utils.tsx | 46 ++++++++------- ai-assistant/src/utils/ToolConfigManager.ts | 8 +-- 14 files changed, 153 insertions(+), 137 deletions(-) diff --git a/ai-assistant/src/ai/mcp/electron-client.ts b/ai-assistant/src/ai/mcp/electron-client.ts index 9a5a67fff2..720d67753e 100644 --- a/ai-assistant/src/ai/mcp/electron-client.ts +++ b/ai-assistant/src/ai/mcp/electron-client.ts @@ -29,7 +29,10 @@ interface ElectronMCPApi { getToolsConfig: () => Promise<{ success: boolean; config?: any; error?: string }>; updateToolsConfig: (config: any) => Promise; setToolEnabled: (serverName: string, toolName: string, enabled: boolean) => Promise; - getToolStats: (serverName: string, toolName: string) => Promise<{ success: boolean; stats?: any; error?: string }>; + getToolStats: ( + serverName: string, + toolName: string + ) => Promise<{ success: boolean; stats?: any; error?: string }>; } declare global { @@ -236,7 +239,7 @@ class ElectronMCPClient { try { const { serverName, toolName } = this.parseToolName(fullToolName); const toolsConfig = await this.getToolsConfig(); - + if (!toolsConfig.success || !toolsConfig.config) { return true; // Default to enabled if config not available } diff --git a/ai-assistant/src/components/assistant/AIInputSection.tsx b/ai-assistant/src/components/assistant/AIInputSection.tsx index e801a00f51..677508cc33 100644 --- a/ai-assistant/src/components/assistant/AIInputSection.tsx +++ b/ai-assistant/src/components/assistant/AIInputSection.tsx @@ -133,11 +133,16 @@ export const AIInputSection: React.FC = ({ }} /> - .MuiGrid-item': { - maxWidth: '100% !important' - } - }}> + .MuiGrid-item': { + maxWidth: '100% !important', + }, + }} + > diff --git a/ai-assistant/src/components/assistant/ToolsDialog.tsx b/ai-assistant/src/components/assistant/ToolsDialog.tsx index 20b332c301..379377924c 100644 --- a/ai-assistant/src/components/assistant/ToolsDialog.tsx +++ b/ai-assistant/src/components/assistant/ToolsDialog.tsx @@ -102,7 +102,7 @@ export const ToolsDialog: React.FC = ({ mcpClient.getToolsConfig(), ]); - console.log("config response is ", configResponse) + console.log('config response is ', configResponse); // Store MCP tools configuration if (toolsConfigResponse.success && toolsConfigResponse.config) { setMcpToolsConfig(toolsConfigResponse.config); @@ -125,16 +125,18 @@ export const ToolsDialog: React.FC = ({ // Create tools from configuration (this is our source of truth) const toolsFromConfig: MCPTool[] = []; if (toolsConfigResponse.success && toolsConfigResponse.config) { - Object.entries(toolsConfigResponse.config).forEach(([serverName, serverTools]: [string, any]) => { - Object.keys(serverTools).forEach((toolName: string) => { - const fullToolName = `${serverName}__${toolName}`; - toolsFromConfig.push({ - name: fullToolName, - description: `Tool: ${toolName}`, - server: serverName + Object.entries(toolsConfigResponse.config).forEach( + ([serverName, serverTools]: [string, any]) => { + Object.keys(serverTools).forEach((toolName: string) => { + const fullToolName = `${serverName}__${toolName}`; + toolsFromConfig.push({ + name: fullToolName, + description: `Tool: ${toolName}`, + server: serverName, + }); }); - }); - }); + } + ); } // Update allKnownMcpTools with tools from configuration @@ -172,7 +174,7 @@ export const ToolsDialog: React.FC = ({ const handleToggleMcpTool = (toolName: string) => { const { serverName, toolName: actualToolName } = parseMcpToolName(toolName); const currentlyEnabled = isMcpToolEnabled(toolName); - + setMcpToolsConfig((prevConfig: any) => { const newConfig = { ...prevConfig }; if (!newConfig[serverName]) { @@ -191,7 +193,7 @@ export const ToolsDialog: React.FC = ({ const handleToggleServer = (serverName: string) => { const serverTools = allKnownMcpTools.filter(tool => tool.server === serverName); - + // Check if all tools from this server are currently enabled const allEnabled = serverTools.every(tool => isMcpToolEnabled(tool.name)); @@ -219,10 +221,7 @@ export const ToolsDialog: React.FC = ({ const isServerEnabled = (serverName: string) => { const serverTools = allKnownMcpTools.filter(tool => tool.server === serverName); - return ( - serverTools.length > 0 && - serverTools.every(tool => isMcpToolEnabled(tool.name)) - ); + return serverTools.length > 0 && serverTools.every(tool => isMcpToolEnabled(tool.name)); }; // Filter tools based on search query - use allKnownMcpTools to show all tools (including disabled ones) @@ -259,7 +258,7 @@ export const ToolsDialog: React.FC = ({ // Save MCP tools configuration if it has changed const mcpConfigChanged = JSON.stringify(mcpToolsConfig) !== JSON.stringify(originalMcpConfig); - + if (mcpConfigChanged) { const mcpClient = new ElectronMCPClient(); if (mcpClient.isAvailable()) { diff --git a/ai-assistant/src/components/chat/MCPFormattedMessage.tsx b/ai-assistant/src/components/chat/MCPFormattedMessage.tsx index 66341c3844..b8d4b6e4e6 100644 --- a/ai-assistant/src/components/chat/MCPFormattedMessage.tsx +++ b/ai-assistant/src/components/chat/MCPFormattedMessage.tsx @@ -116,7 +116,7 @@ const MCPFormattedMessage: React.FC = ({ return; } - onRetryTool(toolName, originalArgs); + onRetryTool(toolName, originalArgs); } catch (error) { console.error('Failed to parse content for retry:', error); } diff --git a/ai-assistant/src/components/common/InlineToolConfirmation.tsx b/ai-assistant/src/components/common/InlineToolConfirmation.tsx index 313ed41fea..09cfeb3fe7 100644 --- a/ai-assistant/src/components/common/InlineToolConfirmation.tsx +++ b/ai-assistant/src/components/common/InlineToolConfirmation.tsx @@ -131,10 +131,10 @@ const InlineToolConfirmation: React.FC = ({ // For regular tools, use edited arguments as-is tool.arguments = edited; } - } + } }); - onApprove(selectedToolIds); + onApprove(selectedToolIds); } catch (error) { console.error('Error during tool approval:', error); setIsApproving(false); diff --git a/ai-assistant/src/components/mcpOutput/MCPArgumentProcessor.ts b/ai-assistant/src/components/mcpOutput/MCPArgumentProcessor.ts index 2b93df6939..168be4a5c7 100644 --- a/ai-assistant/src/components/mcpOutput/MCPArgumentProcessor.ts +++ b/ai-assistant/src/components/mcpOutput/MCPArgumentProcessor.ts @@ -44,19 +44,21 @@ export class MCPArgumentProcessor { try { const mcpClient = new ElectronMCPClient(); const toolsConfigResponse = await mcpClient.getToolsConfig(); - + if (toolsConfigResponse && toolsConfigResponse.config) { // Parse the new structure: { "serverName": { "toolName": { enabled, inputSchema, ... } } } - Object.entries(toolsConfigResponse.config).forEach(([serverName, serverTools]: [string, any]) => { - Object.entries(serverTools).forEach(([toolName, toolConfig]: [string, any]) => { - const fullToolName = `${serverName}__${toolName}`; - this.toolSchemas.set(fullToolName, { - name: fullToolName, - description: toolConfig.description, - inputSchema: toolConfig.inputSchema, + Object.entries(toolsConfigResponse.config).forEach( + ([serverName, serverTools]: [string, any]) => { + Object.entries(serverTools).forEach(([toolName, toolConfig]: [string, any]) => { + const fullToolName = `${serverName}__${toolName}`; + this.toolSchemas.set(fullToolName, { + name: fullToolName, + description: toolConfig.description, + inputSchema: toolConfig.inputSchema, + }); }); - }); - }); + } + ); this.schemasLoaded = true; } } catch (error) { @@ -74,7 +76,7 @@ export class MCPArgumentProcessor { userContext?: UserContext ): Promise { await this.loadSchemas(); - console.log("tools") + console.log('tools'); const schema = this.toolSchemas.get(toolName); const errors: string[] = []; const processed = { ...aiProcessedArgs }; diff --git a/ai-assistant/src/components/settings/MCPToolSettings.tsx b/ai-assistant/src/components/settings/MCPToolSettings.tsx index 34a1286af1..14abc6c73a 100644 --- a/ai-assistant/src/components/settings/MCPToolSettings.tsx +++ b/ai-assistant/src/components/settings/MCPToolSettings.tsx @@ -87,19 +87,22 @@ export function MCPToolSettings({ onConfigChange }: MCPToolSettingsProps) { // Process tools data const toolsData: MCPToolInfo[] = []; - + for (const tool of toolsResponse.config || []) { const { serverName, toolName: actualToolName } = parseMCPToolName(tool.name); - + // Get tool state from configuration const serverConfig = config[serverName]; const toolState = serverConfig?.[actualToolName]; const enabled = toolState?.enabled !== false; // Default to true if not configured - + // Get tool statistics let stats: MCPToolState | null = null; try { - const statsResponse = await window.desktopApi.mcp.getToolStats(serverName, actualToolName); + const statsResponse = await window.desktopApi.mcp.getToolStats( + serverName, + actualToolName + ); if (statsResponse.success) { stats = statsResponse.stats; } @@ -142,9 +145,7 @@ export function MCPToolSettings({ onConfigChange }: MCPToolSettingsProps) { const handleToolToggle = (toolInfo: MCPToolInfo, enabled: boolean) => { // Update local tool state setMCPTools(prevTools => - prevTools.map(tool => - tool.name === toolInfo.name ? { ...tool, enabled } : tool - ) + prevTools.map(tool => (tool.name === toolInfo.name ? { ...tool, enabled } : tool)) ); // Update configuration state @@ -182,7 +183,7 @@ export function MCPToolSettings({ onConfigChange }: MCPToolSettingsProps) { } try { - const response = await window.desktopApi.mcp.updateToolsConfig(toolsConfig); + const response = await window.desktopApi.mcp.updateToolsConfig(toolsConfig); if (response.success) { setHasChanges(false); onConfigChange?.(false); @@ -191,14 +192,16 @@ export function MCPToolSettings({ onConfigChange }: MCPToolSettingsProps) { throw new Error(response.error || 'Failed to save configuration'); } } catch (error) { - setError(`Failed to save configuration: ${error instanceof Error ? error.message : 'Unknown error'}`); + setError( + `Failed to save configuration: ${error instanceof Error ? error.message : 'Unknown error'}` + ); } }; // Discard changes and revert to original configuration const handleDiscardChanges = () => { setToolsConfig(JSON.parse(JSON.stringify(originalConfig))); // Restore original config - + // Update tools to reflect original configuration setMCPTools(prevTools => prevTools.map(tool => { @@ -208,7 +211,7 @@ export function MCPToolSettings({ onConfigChange }: MCPToolSettingsProps) { return { ...tool, enabled }; }) ); - + setHasChanges(false); onConfigChange?.(false); }; @@ -216,9 +219,7 @@ export function MCPToolSettings({ onConfigChange }: MCPToolSettingsProps) { // Enable all tools (local state only) const handleEnableAll = () => { // Update all tools to enabled in local state - setMCPTools(prevTools => - prevTools.map(tool => ({ ...tool, enabled: true })) - ); + setMCPTools(prevTools => prevTools.map(tool => ({ ...tool, enabled: true }))); // Update configuration state setToolsConfig(prevConfig => { @@ -245,9 +246,7 @@ export function MCPToolSettings({ onConfigChange }: MCPToolSettingsProps) { // Disable all tools (local state only) const handleDisableAll = () => { // Update all tools to disabled in local state - setMCPTools(prevTools => - prevTools.map(tool => ({ ...tool, enabled: false })) - ); + setMCPTools(prevTools => prevTools.map(tool => ({ ...tool, enabled: false }))); // Update configuration state setToolsConfig(prevConfig => { @@ -325,10 +324,10 @@ export function MCPToolSettings({ onConfigChange }: MCPToolSettingsProps) { - Configure individual MCP (Model Context Protocol) tools. You can enable or disable specific tools - to control which capabilities are available to the AI assistant. + Configure individual MCP (Model Context Protocol) tools. You can enable or disable + specific tools to control which capabilities are available to the AI assistant. - + } /> )} - - + {hasChanges && ( <> @@ -396,13 +394,16 @@ export function MCPToolSettings({ onConfigChange }: MCPToolSettingsProps) { {totalCount === 0 ? ( - + No MCP Tools Available - No MCP servers are configured or running. Configure MCP servers in the MCP Settings section - to see available tools here. + No MCP servers are configured or running. Configure MCP servers in the MCP Settings + section to see available tools here. ) : ( @@ -412,7 +413,7 @@ export function MCPToolSettings({ onConfigChange }: MCPToolSettingsProps) { {serverName} ({serverTools.length} tools) - + @@ -424,7 +425,7 @@ export function MCPToolSettings({ onConfigChange }: MCPToolSettingsProps) { - {serverTools.map((tool) => ( + {serverTools.map(tool => ( @@ -454,7 +455,7 @@ export function MCPToolSettings({ onConfigChange }: MCPToolSettingsProps) { control={ handleToolToggle(tool, e.target.checked)} + onChange={e => handleToolToggle(tool, e.target.checked)} size="small" /> } @@ -471,4 +472,4 @@ export function MCPToolSettings({ onConfigChange }: MCPToolSettingsProps) { )} ); -} \ No newline at end of file +} diff --git a/ai-assistant/src/helper/apihelper.tsx b/ai-assistant/src/helper/apihelper.tsx index 279bf58463..abe8c4c261 100644 --- a/ai-assistant/src/helper/apihelper.tsx +++ b/ai-assistant/src/helper/apihelper.tsx @@ -399,9 +399,9 @@ export const handleActualApiRequest = async ( isLogRequest(cleanedUrl) && apiError.message && (apiError.message?.includes('a container name must be specified') || - apiError.message?.includes('container name must be specified') || - (apiError.message?.includes('Bad Request') && cleanedUrl.includes('/log')) || - apiError.message?.includes('choose one of')) + apiError.message?.includes('container name must be specified') || + (apiError.message?.includes('Bad Request') && cleanedUrl.includes('/log')) || + apiError.message?.includes('choose one of')) ) { // Extract pod name and available containers from error message const podMatch = apiError.message.match(/for pod ([^,]+)/); @@ -432,7 +432,7 @@ export const handleActualApiRequest = async ( const podNameFromUrl = cleanedUrl.match(/\/pods\/([^\/]+)\/log/); if (podNameFromUrl) { const podName = podNameFromUrl[1]; - + const errorContent = `Failed to get logs from pod "${podName}". This is likely because it has multiple containers.\n\nTo see the containers in this pod, I need to get the pod details first. Would you like me to check the pod details to see available containers?`; aiManager.history.push({ diff --git a/ai-assistant/src/index.tsx b/ai-assistant/src/index.tsx index 97f96fd9fb..2e69f196b4 100644 --- a/ai-assistant/src/index.tsx +++ b/ai-assistant/src/index.tsx @@ -27,15 +27,15 @@ import { getDefaultConfig } from './config/modelConfig'; import { PromptWidthProvider } from './contexts/PromptWidthContext'; import { isTestModeCheck } from './helper'; import AIPrompt from './modal'; -import { - getSettingsURL, - PLUGIN_NAME, - pluginStore, - useGlobalState, - usePluginConfig, +import { getAllAvailableTools, + getSettingsURL, isToolEnabled, - toggleTool + PLUGIN_NAME, + pluginStore, + toggleTool, + useGlobalState, + usePluginConfig, } from './utils'; import { getActiveConfig, @@ -477,7 +477,7 @@ function Settings() { {/* MCP Servers Section */} - + {/* MCP Tool Configuration Section */} diff --git a/ai-assistant/src/langchain/LangChainManager.ts b/ai-assistant/src/langchain/LangChainManager.ts index 5fc49f4808..5fcfd26878 100644 --- a/ai-assistant/src/langchain/LangChainManager.ts +++ b/ai-assistant/src/langchain/LangChainManager.ts @@ -571,7 +571,7 @@ The user is waiting for you to explain what the tools discovered. Provide a dire console.error('Error in direct tool calling request:', error); // If direct tool calling fails, fall back to regular approach - this.useDirectToolCalling = false; + this.useDirectToolCalling = false; const modelToUse = this.boundModel || this.model; return await this.handleChainBasedRequest(message, modelToUse); @@ -665,7 +665,6 @@ The user is waiting for you to explain what the tools discovered. Provide a dire // If no tools are enabled but LLM is returning tool calls, this indicates a bug if (enabledToolIds.length === 0) { - // Treat as regular response since no tools should be available const assistantPrompt: Prompt = { role: 'assistant', @@ -883,7 +882,12 @@ Without access to the Kubernetes API, I cannot fetch current pod, deployment, se for (const toolCall of toolCalls) { const args = JSON.parse(toolCall.function.arguments); - console.log('🔧 LangChainManager: Executing tool', toolCall.function.name, 'with parsed args:', args); + console.log( + '🔧 LangChainManager: Executing tool', + toolCall.function.name, + 'with parsed args:', + args + ); try { // Execute the tool call using ToolManager @@ -1460,7 +1464,6 @@ Format your response to make the errors prominent and actionable.`, return assistantPrompt; } - // Analyze response and correct kubectl suggestions private async analyzeAndCorrectResponse(response: any): Promise { const responseContent = this.extractTextContent(response.content); diff --git a/ai-assistant/src/langchain/tools/ToolManager.ts b/ai-assistant/src/langchain/tools/ToolManager.ts index 25b3d31892..50a6432553 100644 --- a/ai-assistant/src/langchain/tools/ToolManager.ts +++ b/ai-assistant/src/langchain/tools/ToolManager.ts @@ -51,7 +51,6 @@ export class ToolManager { */ private async initializeMCPTools(): Promise { try { - if (!this.mcpClient.isAvailable()) { this.mcpToolsInitialized = true; return; @@ -67,22 +66,24 @@ export class ToolManager { // Create tools from configuration const mcpToolsData: any[] = []; - Object.entries(toolsConfigResponse.config).forEach(([serverName, serverTools]: [string, any]) => { - Object.entries(serverTools).forEach(([toolName, toolConfig]: [string, any]) => { - const fullToolName = `${serverName}__${toolName}`; - mcpToolsData.push({ - name: fullToolName, - description: toolConfig.description || `Tool: ${toolName} from ${serverName} server`, - inputSchema: toolConfig.inputSchema || {}, // Use the actual schema from MCP config - server: serverName, - enabled: toolConfig.enabled !== false + Object.entries(toolsConfigResponse.config).forEach( + ([serverName, serverTools]: [string, any]) => { + Object.entries(serverTools).forEach(([toolName, toolConfig]: [string, any]) => { + const fullToolName = `${serverName}__${toolName}`; + mcpToolsData.push({ + name: fullToolName, + description: toolConfig.description || `Tool: ${toolName} from ${serverName} server`, + inputSchema: toolConfig.inputSchema || {}, // Use the actual schema from MCP config + server: serverName, + enabled: toolConfig.enabled !== false, + }); }); - }); - }); + } + ); // Filter MCP tools using the MCP configuration (not legacy enabledToolIds) const filteredMcpTools: typeof mcpToolsData = []; - + for (const toolData of mcpToolsData) { // MCP tools are controlled by their own configuration system, not legacy enabledToolIds // Check if tool is enabled in MCP configuration @@ -630,7 +631,7 @@ export class ToolManager { async bindToModelAsync(model: BaseChatModel, providerId: string): Promise { // Wait for MCP tools to initialize before binding await this.waitForMCPToolsInitialization(); - + return this.bindToModel(model, providerId); } @@ -676,19 +677,19 @@ export class ToolManager { const { serverName, toolName: actualToolName } = this.mcpClient.parseToolName(toolName); const result = await this.mcpClient.setToolEnabled(serverName, actualToolName, enabled); - + if (result) { // Reinitialize MCP tools to reflect the change this.mcpToolsInitialized = false; this.mcpInitializationPromise = this.initializeMCPTools(); await this.mcpInitializationPromise; - + // If we have a bound model, rebind with the updated tools if (this.boundModel && this.providerId) { this.boundModel = this.bindToModel(this.boundModel, this.providerId); } } - + return result; } @@ -709,7 +710,7 @@ export class ToolManager { if (!this.mcpClient.isAvailable()) { return null; } - + const { serverName, toolName: actualToolName } = this.mcpClient.parseToolName(toolName); return await this.mcpClient.getToolStats(serverName, actualToolName); } @@ -734,21 +735,21 @@ export class ToolManager { if (!this.mcpClient.isAvailable()) { return false; } - + const result = await this.mcpClient.updateToolsConfig(config); - + if (result) { // Reinitialize MCP tools to reflect the changes this.mcpToolsInitialized = false; this.mcpInitializationPromise = this.initializeMCPTools(); await this.mcpInitializationPromise; - + // If we have a bound model, rebind with the updated tools if (this.boundModel && this.providerId) { this.boundModel = this.bindToModel(this.boundModel, this.providerId); } } - + return result; } diff --git a/ai-assistant/src/modal.tsx b/ai-assistant/src/modal.tsx index f075b3bf3f..ca9eebb758 100644 --- a/ai-assistant/src/modal.tsx +++ b/ai-assistant/src/modal.tsx @@ -780,7 +780,7 @@ export default function AIPrompt(props: { minWidth: 0, // Global override for all MUI Grid items to prevent max-width: none '& .MuiGrid-root > .MuiGrid-item': { - maxWidth: '100% !important' + maxWidth: '100% !important', }, }} > @@ -802,8 +802,8 @@ export default function AIPrompt(props: { maxWidth: '100%', minWidth: 0, '& > .MuiGrid-item': { - maxWidth: '100% !important' - } + maxWidth: '100% !important', + }, }} > { if (!toolsInitialized) { - initializeToolsState(conf).then(initializedTools => { - setEnabledToolsState(initializedTools); - setToolsInitialized(true); - - // If this is the first time and we have tools to save, save them - if (!conf?.enabledTools && initializedTools.length > 0) { - const currentConf = pluginStore.get() || {}; - pluginStore.update({ - ...currentConf, - enabledTools: initializedTools, - }); - } - }).catch(error => { - console.error('Failed to initialize tools state:', error); - // Fallback to existing behavior - setEnabledToolsState(conf?.enabledTools ?? []); - setToolsInitialized(true); - }); + initializeToolsState(conf) + .then(initializedTools => { + setEnabledToolsState(initializedTools); + setToolsInitialized(true); + + // If this is the first time and we have tools to save, save them + if (!conf?.enabledTools && initializedTools.length > 0) { + const currentConf = pluginStore.get() || {}; + pluginStore.update({ + ...currentConf, + enabledTools: initializedTools, + }); + } + }) + .catch(error => { + console.error('Failed to initialize tools state:', error); + // Fallback to existing behavior + setEnabledToolsState(conf?.enabledTools ?? []); + setToolsInitialized(true); + }); } }, [conf, toolsInitialized]); @@ -90,14 +92,14 @@ function usePluginSettings() { export const useGlobalState = () => useBetween(usePluginSettings); // Export tool configuration utilities -export { - getAllAvailableTools, - isToolEnabled, +export { + getAllAvailableTools, + isToolEnabled, toggleTool, getAllAvailableToolsIncludingMCP, getEnabledToolIdsIncludingMCP, isMCPTool, parseMCPToolName, isBuiltInTool, - initializeToolsState + initializeToolsState, } from './utils/ToolConfigManager'; diff --git a/ai-assistant/src/utils/ToolConfigManager.ts b/ai-assistant/src/utils/ToolConfigManager.ts index 5029d166fe..501a5f1ba5 100644 --- a/ai-assistant/src/utils/ToolConfigManager.ts +++ b/ai-assistant/src/utils/ToolConfigManager.ts @@ -110,7 +110,7 @@ export function parseMCPToolName(fullToolName: string): { serverName: string; to // This function needs to be async to fetch MCP tools export async function getAllAvailableToolsIncludingMCP(): Promise { const builtInTools = getAllAvailableTools(); - + // Try to get MCP tools if running in Electron environment try { if (typeof window !== 'undefined' && window.desktopApi?.mcp) { @@ -128,7 +128,7 @@ export async function getAllAvailableToolsIncludingMCP(): Promise { } catch (error) { console.warn('Failed to fetch MCP tools for tool config management:', error); } - + return builtInTools; } @@ -143,12 +143,12 @@ export async function getEnabledToolIdsIncludingMCP(pluginSettings: any): Promis // but respects any saved configuration if it exists export async function initializeToolsState(pluginSettings: any): Promise { const allTools = await getAllAvailableToolsIncludingMCP(); - + // If we have no enabledTools config at all, enable all tools by default if (!pluginSettings || !pluginSettings.enabledTools) { return allTools.map(tool => tool.id); } - + // If we have partial config, use the isToolEnabled logic which defaults to true return allTools.map(tool => tool.id).filter(toolId => isToolEnabled(pluginSettings, toolId)); } From e741d31f19c04c4f7d060486b8c762bf9243c1bf Mon Sep 17 00:00:00 2001 From: ashu8912 Date: Mon, 27 Oct 2025 13:49:46 +0530 Subject: [PATCH 35/39] ai-plugin: React to cluster change Signed-off-by: ashu8912 --- .../settings/MCPConfigEditorDialog.tsx | 10 +-- .../src/hooks/useClusterChangeNotifier.ts | 68 +++++++++++++++++++ ai-assistant/src/index.tsx | 13 ++++ 3 files changed, 83 insertions(+), 8 deletions(-) create mode 100644 ai-assistant/src/hooks/useClusterChangeNotifier.ts diff --git a/ai-assistant/src/components/settings/MCPConfigEditorDialog.tsx b/ai-assistant/src/components/settings/MCPConfigEditorDialog.tsx index 6825b43937..b0f64a74d4 100644 --- a/ai-assistant/src/components/settings/MCPConfigEditorDialog.tsx +++ b/ai-assistant/src/components/settings/MCPConfigEditorDialog.tsx @@ -126,18 +126,12 @@ export default function MCPConfigEditorDialog({ const exampleConfig: MCPConfig = { enabled: true, servers: [ - { - name: 'inspektor-gadget', - command: 'docker', - args: ['mcp', 'gateway', 'run'], - enabled: true, - }, { name: 'flux-mcp', command: 'flux-operator-mcp', - args: ['serve'], + args: ['serve', '--kube-context', 'HEADLAMP_CURRENT_CLUSTER'], env: { - KUBECONFIG: '/Users/ashughildiyal/.kube/config', + KUBECONFIG: 'PATH_TO_KUBECONFIG', }, enabled: true, }, diff --git a/ai-assistant/src/hooks/useClusterChangeNotifier.ts b/ai-assistant/src/hooks/useClusterChangeNotifier.ts new file mode 100644 index 0000000000..673137fb07 --- /dev/null +++ b/ai-assistant/src/hooks/useClusterChangeNotifier.ts @@ -0,0 +1,68 @@ +import { getCluster } from '@kinvolk/headlamp-plugin/lib/Utils'; +import React from 'react'; + +// Check if we're running in Electron +const isElectron = !!(window as any)?.desktopApi; + +/** + * Hook that monitors cluster changes and notifies the Electron main process + * This enables MCP servers to restart when cluster context changes + */ +export function useClusterChangeNotifier() { + const [currentCluster, setCurrentCluster] = React.useState(null); + const previousClusterRef = React.useRef(null); + + React.useEffect(() => { + // Function to check and update cluster + const checkClusterChange = () => { + const cluster = getCluster() || null; + + // Update state if cluster changed + if (cluster !== currentCluster) { + setCurrentCluster(cluster); + } + }; + + // Check initially + checkClusterChange(); + + // Set up interval to check for cluster changes + const interval = setInterval(checkClusterChange, 1000); // Check every second + + return () => clearInterval(interval); + }, [currentCluster]); + + React.useEffect(() => { + // Only notify if running in Electron + if (!isElectron || !(window as any)?.desktopApi?.notifyClusterChange) { + return; + } + + const previousCluster = previousClusterRef.current; + + // Only notify if cluster actually changed and it's not the initial load + if (currentCluster !== previousCluster && previousClusterRef.current !== undefined) { + console.log('Cluster change detected, notifying electron:', { + from: previousCluster, + to: currentCluster, + }); + + // Notify the electron main process + (window as any).desktopApi.notifyClusterChange(currentCluster); + } + + // Update the ref for next comparison + previousClusterRef.current = currentCluster; + }, [currentCluster]); + + return currentCluster; +} + +/** + * Component that automatically monitors cluster changes and notifies Electron + * This component should be included once in the app root to enable MCP server restart functionality + */ +export function ClusterChangeNotifier(): null { + useClusterChangeNotifier(); + return null; +} diff --git a/ai-assistant/src/index.tsx b/ai-assistant/src/index.tsx index 2e69f196b4..452d692c0e 100644 --- a/ai-assistant/src/index.tsx +++ b/ai-assistant/src/index.tsx @@ -26,6 +26,7 @@ import { MCPSettings } from './components/settings'; import { getDefaultConfig } from './config/modelConfig'; import { PromptWidthProvider } from './contexts/PromptWidthContext'; import { isTestModeCheck } from './helper'; +import { ClusterChangeNotifier } from './hooks/useClusterChangeNotifier'; import AIPrompt from './modal'; import { getAllAvailableTools, @@ -50,6 +51,13 @@ const AIPanelComponent = React.memo(() => { const [width, setWidth] = React.useState('35vw'); const [isResizing, setIsResizing] = React.useState(false); + // Check if models are configured + const savedConfigData = React.useMemo(() => { + return getSavedConfigurations(conf); + }, [conf]); + + const hasAnyValidConfig = savedConfigData.providers && savedConfigData.providers.length > 0; + const handleMouseDown = React.useCallback((e: React.MouseEvent) => { e.preventDefault(); setIsResizing(true); @@ -105,6 +113,8 @@ const AIPanelComponent = React.memo(() => { }, }} > + {/* Monitor cluster changes and notify electron - only when models are configured */} + {hasAnyValidConfig && } Date: Mon, 27 Oct 2025 23:14:32 +0530 Subject: [PATCH 36/39] ai-plugin: Fix rendering issues Signed-off-by: ashu8912 --- ai-assistant/src/ContentRenderer.tsx | 14 +- .../src/components/assistant/ToolsDialog.tsx | 1 - .../mcpOutput/MCPArgumentProcessor.ts | 1 - .../components/mcpOutput/MCPOutputDisplay.tsx | 444 ++++++++---------- .../src/hooks/useClusterChangeNotifier.ts | 5 - .../src/langchain/LangChainManager.ts | 8 - ai-assistant/src/textstream.tsx | 22 +- 7 files changed, 216 insertions(+), 279 deletions(-) diff --git a/ai-assistant/src/ContentRenderer.tsx b/ai-assistant/src/ContentRenderer.tsx index 3dcba2b184..6fd40def44 100644 --- a/ai-assistant/src/ContentRenderer.tsx +++ b/ai-assistant/src/ContentRenderer.tsx @@ -99,7 +99,14 @@ const TableWrapper: React.FC<{ children: React.ReactNode }> = React.memo(({ chil // Extract table rows from children const tableElement = React.Children.only(children) as React.ReactElement; - const tbody = React.Children.toArray(tableElement.props.children).find( + const tableChildren = tableElement.props.children; + + if (!tableChildren) { + // No children found, return table as is + return {children}; + } + + const tbody = React.Children.toArray(tableChildren).find( (child: any) => child?.type === 'tbody' || child?.props?.component === 'tbody' ); @@ -109,7 +116,8 @@ const TableWrapper: React.FC<{ children: React.ReactNode }> = React.memo(({ chil } const tbodyElement = tbody as React.ReactElement; - const rows = React.Children.toArray(tbodyElement.props.children); + const tbodyChildren = tbodyElement.props.children; + const rows = tbodyChildren ? React.Children.toArray(tbodyChildren) : []; const hasMoreRows = rows.length > maxRows; const visibleRows = showAll ? rows : rows.slice(0, maxRows); @@ -120,7 +128,7 @@ const TableWrapper: React.FC<{ children: React.ReactNode }> = React.memo(({ chil // Clone the table with the limited tbody const limitedTable = React.cloneElement(tableElement, { - children: React.Children.map(tableElement.props.children, (child: any) => { + children: React.Children.map(tableChildren, (child: any) => { if (child?.type === 'tbody' || child?.props?.component === 'tbody') { return limitedTbody; } diff --git a/ai-assistant/src/components/assistant/ToolsDialog.tsx b/ai-assistant/src/components/assistant/ToolsDialog.tsx index 379377924c..84e7b63882 100644 --- a/ai-assistant/src/components/assistant/ToolsDialog.tsx +++ b/ai-assistant/src/components/assistant/ToolsDialog.tsx @@ -102,7 +102,6 @@ export const ToolsDialog: React.FC = ({ mcpClient.getToolsConfig(), ]); - console.log('config response is ', configResponse); // Store MCP tools configuration if (toolsConfigResponse.success && toolsConfigResponse.config) { setMcpToolsConfig(toolsConfigResponse.config); diff --git a/ai-assistant/src/components/mcpOutput/MCPArgumentProcessor.ts b/ai-assistant/src/components/mcpOutput/MCPArgumentProcessor.ts index 168be4a5c7..f8d72d07db 100644 --- a/ai-assistant/src/components/mcpOutput/MCPArgumentProcessor.ts +++ b/ai-assistant/src/components/mcpOutput/MCPArgumentProcessor.ts @@ -76,7 +76,6 @@ export class MCPArgumentProcessor { userContext?: UserContext ): Promise { await this.loadSchemas(); - console.log('tools'); const schema = this.toolSchemas.get(toolName); const errors: string[] = []; const processed = { ...aiProcessedArgs }; diff --git a/ai-assistant/src/components/mcpOutput/MCPOutputDisplay.tsx b/ai-assistant/src/components/mcpOutput/MCPOutputDisplay.tsx index 7ca55a686a..db94f4bdfd 100644 --- a/ai-assistant/src/components/mcpOutput/MCPOutputDisplay.tsx +++ b/ai-assistant/src/components/mcpOutput/MCPOutputDisplay.tsx @@ -447,75 +447,78 @@ const MCPOutputDisplay: React.FC = ({ wordBreak: 'break-word', }} > - - } - title={ - - - {output.title} - - - {output.metadata && ( - - - - )} - - } - subheader={output.summary} - action={ - - {onExport && ( - + } + title={ + + + {output.title} + + + {output.metadata && ( + + + + )} + + } + subheader={output.summary} + action={ + + {onExport && ( + + setShowExportMenu(!showExportMenu)} + sx={{ mr: 2 }} + > + + + + )} + setShowExportMenu(!showExportMenu)} + onClick={() => setShowRawData(!showRawData)} + color={showRawData ? 'primary' : 'default'} sx={{ mr: 2 }} > - + - )} - - setShowRawData(!showRawData)} - color={showRawData ? 'primary' : 'default'} - sx={{ mr: 2 }} - > - - - - {compact && ( - setExpanded(!expanded)}> - - - )} - - } - sx={{ pb: 1 }} - /> + {compact && ( + setExpanded(!expanded)}> + + + )} + + } + sx={{ pb: 1 }} + /> + )} - + = ({ }, }} > - {/* Warnings */} - {output.warnings && output.warnings.length > 0 && ( - - - Warnings: - - {output.warnings.map((warning, index) => ( - - • {warning} - - ))} - + {/* Skip warnings, insights, and actionable items for errors */} + {output.type !== 'error' && ( + <> + {/* Warnings */} + {output.warnings && output.warnings.length > 0 && ( + + + Warnings: + + {output.warnings.map((warning, index) => ( + + • {warning} + + ))} + + )} + )} {/* Main Content */} {renderContent()} - {/* Insights */} - {output.insights && output.insights.length > 0 && ( - - - - Key Insights: - - {output.insights.map((insight, index) => ( - + {/* Insights */} + {output.insights && output.insights.length > 0 && ( + - • {insight} - - ))} - - )} + + + Key Insights: + + {output.insights.map((insight, index) => ( + + • {insight} + + ))} + + )} - {/* Actionable Items */} - {output.actionable_items && output.actionable_items.length > 0 && ( - - - - Recommended Actions: - - {output.actionable_items.map((item, index) => ( - 0 && ( + - • {item} - - ))} - + + + Recommended Actions: + + {output.actionable_items.map((item, index) => ( + + • {item} + + ))} + + )} + )} {/* Raw Data Collapse */} @@ -1210,23 +1223,6 @@ const ErrorDisplay: React.FC<{ data: any; onRetry?: () => void; width: string }> onRetry, width, }) => { - // Extract concise error message for common error types - const getDisplayMessage = (errorData: any) => { - const message = errorData.message || 'Tool Execution Error'; - - // Handle file not found errors specifically - if (message.includes('ENOENT') || message.includes('no such file')) { - return 'File Not Found Error'; - } - - // Handle schema mismatch errors - if (message.includes('schema mismatch')) { - return 'Tool Configuration Error'; - } - - return message; - }; - return ( void; width: string }> wordWrap: 'break-word', }} > - - - - - - {getDisplayMessage(data)} - - {data.details && ( - - - Error Details: - - - {data.details} - - - )} - - - - - {data.suggestions && data.suggestions.length > 0 && ( - - - - Troubleshooting Suggestions: - - - {data.suggestions.map((suggestion: string, index: number) => ( - - {suggestion} - - ))} - - - )} + {data.message || data.details || 'Tool Execution Error'} + + - + {onRetry && ( )} - ); diff --git a/ai-assistant/src/hooks/useClusterChangeNotifier.ts b/ai-assistant/src/hooks/useClusterChangeNotifier.ts index 673137fb07..77d1773b8a 100644 --- a/ai-assistant/src/hooks/useClusterChangeNotifier.ts +++ b/ai-assistant/src/hooks/useClusterChangeNotifier.ts @@ -42,11 +42,6 @@ export function useClusterChangeNotifier() { // Only notify if cluster actually changed and it's not the initial load if (currentCluster !== previousCluster && previousClusterRef.current !== undefined) { - console.log('Cluster change detected, notifying electron:', { - from: previousCluster, - to: currentCluster, - }); - // Notify the electron main process (window as any).desktopApi.notifyClusterChange(currentCluster); } diff --git a/ai-assistant/src/langchain/LangChainManager.ts b/ai-assistant/src/langchain/LangChainManager.ts index 5fcfd26878..c509a5e010 100644 --- a/ai-assistant/src/langchain/LangChainManager.ts +++ b/ai-assistant/src/langchain/LangChainManager.ts @@ -882,12 +882,6 @@ Without access to the Kubernetes API, I cannot fetch current pod, deployment, se for (const toolCall of toolCalls) { const args = JSON.parse(toolCall.function.arguments); - console.log( - '🔧 LangChainManager: Executing tool', - toolCall.function.name, - 'with parsed args:', - args - ); try { // Execute the tool call using ToolManager @@ -1413,10 +1407,8 @@ Format your response to make the errors prominent and actionable.`, // For single formatted MCP output, return just the content if (recentToolResponses.length === 1) { - console.log('🔧 Taking single tool response path'); fallbackContent = content; } else { - console.log('🔧 Taking multiple tool responses path'); // For multiple tools, use the tool name format fallbackContent += `${toolName}: ${content}${ index < recentToolResponses.length - 1 ? '\n\n' : '' diff --git a/ai-assistant/src/textstream.tsx b/ai-assistant/src/textstream.tsx index 9c176a0044..9319a0ac46 100644 --- a/ai-assistant/src/textstream.tsx +++ b/ai-assistant/src/textstream.tsx @@ -233,6 +233,20 @@ const TextStreamContainer = React.memo(function TextStreamContainer({ return null; } + // Extract message from tool response if it's JSON with shouldProcessFollowUp + let displayContent = prompt.content; + if (prompt.role === 'tool' && typeof prompt.content === 'string') { + try { + const parsed = JSON.parse(prompt.content); + if (parsed.shouldProcessFollowUp && parsed.message) { + // Use the message field for display + displayContent = parsed.message; + } + } catch (e) { + // Not JSON, use original content + } + } + // Check if this is a content filter error or if the prompt has its own error const isContentFilterError = prompt.role === 'assistant' && prompt.contentFilterError; const hasError = prompt.error === true; @@ -240,7 +254,7 @@ const TextStreamContainer = React.memo(function TextStreamContainer({ const isJsonSuccess = prompt.success; if (prompt.content === '' && prompt.role === 'user') return null; - if (prompt.content === '' && prompt.role === 'assistant' && !prompt.toolConfirmation) + if (displayContent === '' && prompt.role === 'assistant' && !prompt.toolConfirmation) return null; return ( {prompt.role === 'user' ? ( - prompt.content + displayContent ) : ( <> {isContentFilterError || hasError ? ( @@ -310,7 +324,7 @@ const TextStreamContainer = React.memo(function TextStreamContainer({ wordBreak: 'break-word', }} > - {prompt.content} + {displayContent} {isContentFilterError && ( Tip: Focus your question specifically on Kubernetes administration tasks. @@ -333,7 +347,7 @@ const TextStreamContainer = React.memo(function TextStreamContainer({ ) : ( /* Use ContentRenderer for all assistant content */ From ef4dc011982fdb3932beb55e374cd1a1bd0ed6e4 Mon Sep 17 00:00:00 2001 From: ashu8912 Date: Tue, 28 Oct 2025 00:55:41 +0530 Subject: [PATCH 37/39] ai-plugin: Remove unused code Signed-off-by: ashu8912 --- ai-assistant/src/utils/SampleYamlLibrary.ts | 184 -------------------- 1 file changed, 184 deletions(-) diff --git a/ai-assistant/src/utils/SampleYamlLibrary.ts b/ai-assistant/src/utils/SampleYamlLibrary.ts index 4e1ec6a76b..175a371e3d 100644 --- a/ai-assistant/src/utils/SampleYamlLibrary.ts +++ b/ai-assistant/src/utils/SampleYamlLibrary.ts @@ -7,190 +7,6 @@ export interface YamlExample { resourceType: string; } -export const yamlExamples: YamlExample[] = [ - { - title: 'Sample Pod', - filename: 'sample-pod.yaml', - resourceType: 'Pod', - yaml: `apiVersion: v1 -kind: Pod -metadata: - name: sample-pod - namespace: default -spec: - containers: - - name: nginx - image: nginx:alpine - ports: - - containerPort: 80`, - }, - { - title: 'Sample Deployment', - filename: 'sample-deployment.yaml', - resourceType: 'Deployment', - yaml: `apiVersion: apps/v1 -kind: Deployment -metadata: - name: sample-deployment - namespace: default -spec: - replicas: 3 - selector: - matchLabels: - app: sample-app - template: - metadata: - labels: - app: sample-app - spec: - containers: - - name: sample-container - image: nginx:stable - ports: - - containerPort: 80`, - }, - { - title: 'Sample Service', - filename: 'sample-service.yaml', - resourceType: 'Service', - yaml: `apiVersion: v1 -kind: Service -metadata: - name: sample-service - namespace: default -spec: - selector: - app: sample-app - ports: - - protocol: TCP - port: 80 - targetPort: 80 - type: ClusterIP`, - }, - { - title: 'Sample Ingress', - filename: 'sample-ingress.yaml', - resourceType: 'Ingress', - yaml: `apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: sample-ingress - namespace: default - annotations: - nginx.ingress.kubernetes.io/rewrite-target: / -spec: - rules: - - host: example.local - http: - paths: - - path: / - pathType: Prefix - backend: - service: - name: sample-service - port: - number: 80`, - }, - { - title: 'Sample ConfigMap', - filename: 'sample-configmap.yaml', - resourceType: 'ConfigMap', - yaml: `apiVersion: v1 -kind: ConfigMap -metadata: - name: sample-config - namespace: default -data: - sample.property: "Hello, Kubernetes!" - another.property: "This is a sample value."`, - }, - { - title: 'Sample Secret', - filename: 'sample-secret.yaml', - resourceType: 'Secret', - yaml: `apiVersion: v1 -kind: Secret -metadata: - name: sample-secret - namespace: default -type: Opaque -data: - # These values are base64-encoded. For example, "admin" becomes "YWRtaW4=" - username: YWRtaW4= - password: c2VjcmV0`, - }, - { - title: 'Sample Namespace', - filename: 'sample-namespace.yaml', - resourceType: 'Namespace', - yaml: `apiVersion: v1 -kind: Namespace -metadata: - name: sample-namespace`, - }, - { - title: 'Sample Job', - filename: 'sample-job.yaml', - resourceType: 'Job', - yaml: `apiVersion: batch/v1 -kind: Job -metadata: - name: sample-job - namespace: default -spec: - template: - metadata: - name: sample-job - spec: - containers: - - name: job - image: busybox - command: ["echo", "Hello from the Job!"] - restartPolicy: Never - backoffLimit: 4`, - }, - { - title: 'Sample CronJob', - filename: 'sample-cronjob.yaml', - resourceType: 'CronJob', - yaml: `apiVersion: batch/v1 -kind: CronJob -metadata: - name: sample-cronjob - namespace: default -spec: - schedule: "*/5 * * * *" - jobTemplate: - spec: - template: - spec: - containers: - - name: cronjob - image: busybox - args: - - /bin/sh - - -c - - date; echo "Hello from the CronJob!" - restartPolicy: OnFailure`, - }, - { - title: 'Sample PersistentVolumeClaim', - filename: 'sample-pvc.yaml', - resourceType: 'PersistentVolumeClaim', - yaml: `apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - name: sample-pvc - namespace: default -spec: - accessModes: - - ReadWriteOnce - resources: - requests: - storage: 1Gi`, - }, -]; - // Function to validate and parse YAML to extract resource type and metadata export function parseKubernetesYAML(yamlStr: string): { isValid: boolean; From d9bb4cb5be461e6dc278c3ac58080c26d866d5e0 Mon Sep 17 00:00:00 2001 From: ashu8912 Date: Wed, 29 Oct 2025 13:26:33 +0530 Subject: [PATCH 38/39] ai-plugin: Allow multiple tool calls to happen Signed-off-by: ashu8912 --- ai-assistant/src/ContentRenderer.tsx | 4 +- .../common/InlineToolConfirmation.tsx | 10 +- .../components/mcpOutput/MCPOutputDisplay.tsx | 2 +- .../src/langchain/LangChainManager.ts | 391 +++++++++++++++++- .../src/langchain/tools/ToolOrchestrator.ts | 314 ++++++++++++++ 5 files changed, 715 insertions(+), 6 deletions(-) create mode 100644 ai-assistant/src/langchain/tools/ToolOrchestrator.ts diff --git a/ai-assistant/src/ContentRenderer.tsx b/ai-assistant/src/ContentRenderer.tsx index 6fd40def44..8127332f60 100644 --- a/ai-assistant/src/ContentRenderer.tsx +++ b/ai-assistant/src/ContentRenderer.tsx @@ -100,12 +100,12 @@ const TableWrapper: React.FC<{ children: React.ReactNode }> = React.memo(({ chil // Extract table rows from children const tableElement = React.Children.only(children) as React.ReactElement; const tableChildren = tableElement.props.children; - + if (!tableChildren) { // No children found, return table as is return {children}; } - + const tbody = React.Children.toArray(tableChildren).find( (child: any) => child?.type === 'tbody' || child?.props?.component === 'tbody' ); diff --git a/ai-assistant/src/components/common/InlineToolConfirmation.tsx b/ai-assistant/src/components/common/InlineToolConfirmation.tsx index 09cfeb3fe7..1799d8ab4e 100644 --- a/ai-assistant/src/components/common/InlineToolConfirmation.tsx +++ b/ai-assistant/src/components/common/InlineToolConfirmation.tsx @@ -134,9 +134,12 @@ const InlineToolConfirmation: React.FC = ({ } }); - onApprove(selectedToolIds); + // Call onApprove and wait for it to complete + await onApprove(selectedToolIds); } catch (error) { console.error('Error during tool approval:', error); + } finally { + // Always reset the approving state, whether success or error setIsApproving(false); } }; @@ -147,9 +150,12 @@ const InlineToolConfirmation: React.FC = ({ setIsDenying(true); try { - onDeny(); + // Call onDeny and wait for it to complete + await onDeny(); } catch (error) { console.error('Error during tool denial:', error); + } finally { + // Always reset the denying state setIsDenying(false); } }; diff --git a/ai-assistant/src/components/mcpOutput/MCPOutputDisplay.tsx b/ai-assistant/src/components/mcpOutput/MCPOutputDisplay.tsx index db94f4bdfd..1ea0c6e18e 100644 --- a/ai-assistant/src/components/mcpOutput/MCPOutputDisplay.tsx +++ b/ai-assistant/src/components/mcpOutput/MCPOutputDisplay.tsx @@ -1276,7 +1276,7 @@ const ErrorDisplay: React.FC<{ data: any; onRetry?: () => void; width: string }> size="small" onClick={onRetry} startIcon={} - sx={{mr: 0.5}} + sx={{ mr: 0.5 }} > Retry Tool diff --git a/ai-assistant/src/langchain/LangChainManager.ts b/ai-assistant/src/langchain/LangChainManager.ts index c509a5e010..e5f05e0103 100644 --- a/ai-assistant/src/langchain/LangChainManager.ts +++ b/ai-assistant/src/langchain/LangChainManager.ts @@ -24,6 +24,7 @@ import { ToolCall } from '../utils/ToolApprovalManager'; import { isBuiltInTool } from '../utils/ToolConfigManager'; import { apiErrorPromptTemplate, toolFailurePromptTemplate } from './PromptTemplates'; import { KubernetesToolContext, ToolManager } from './tools'; +import { RecommendedTool, ToolOrchestrator } from './tools/ToolOrchestrator'; export default class LangChainManager extends AIManager { private model: BaseChatModel; @@ -334,6 +335,21 @@ export default class LangChainManager extends AIManager { } } + /** + * Clear the most recent tool confirmation message from history + * Called after tool execution completes to hide the loading dialog + */ + public clearToolConfirmation(): void { + // Find the most recent tool confirmation message (from the end) + for (let i = this.history.length - 1; i >= 0; i--) { + if (this.history[i].toolConfirmation) { + // Remove this message from history + this.history.splice(i, 1); + return; + } + } + } + // Helper method to prepare chat history for prompt template private prepareChatHistory(): BaseMessage[] { // Filter out system messages and display-only messages to avoid conflicts with the system message in the prompt template @@ -510,7 +526,16 @@ The user is waiting for you to explain what the tools discovered. Provide a dire this.currentAbortController = new AbortController(); try { - // Use direct tool calling if enabled + // FIRST: Try to orchestrate multiple relevant tools before making LLM call + // This enables multi-tool execution for comprehensive responses + const recommendedTools = await this.orchestrateToolsForRequest(message); + + if (recommendedTools && recommendedTools.length > 0) { + // Execute multiple tools together for a comprehensive response + return await this.handleMultipleToolExecution(message, recommendedTools); + } + + // FALLBACK: Use direct tool calling if enabled if (this.useDirectToolCalling) { return await this.handleDirectToolCallingRequest(message); } @@ -659,6 +684,370 @@ The user is waiting for you to explain what the tools discovered. Provide a dire return assistantPrompt; } + /** + * Analyze user request to determine ALL relevant tools that should be executed together + * This enables multi-tool orchestration for comprehensive responses + */ + private async orchestrateToolsForRequest(userMessage: string): Promise { + try { + const enabledToolIds = this.toolManager.getToolNames(); + + if (enabledToolIds.length === 0) { + // No tools available + return null; + } + + // Get available tools with descriptions + const allTools = this.toolManager.getLangChainTools(); + const availableTools = allTools.map(tool => ({ + name: tool.name, + description: tool.description || '', + })); + + // Use ToolOrchestrator to analyze and recommend tools + const recommendation = await ToolOrchestrator.analyzeAndRecommendTools( + userMessage, + availableTools, + this.boundModel || this.model, + this.history.slice(-10), // Pass last 10 messages for context + this.currentAbortController?.signal + ); + + // Return tools if recommendation suggests executing all together + if (recommendation.shouldExecuteAll && recommendation.tools.length > 0) { + return recommendation.tools; + } + + // Return null if only single tool is needed + return null; + } catch (error) { + return null; + } + } + + /** + * Execute multiple tools together based on orchestration recommendation + * Requests approval before executing each batch of tools + * Collects results and provides a comprehensive response + */ + private async handleMultipleToolExecution( + userMessage: string, + recommendedTools: RecommendedTool[] + ): Promise { + try { + // Prepare tools with enhanced arguments (using same pattern as regular tool execution) + const toolsForApproval = await Promise.all( + recommendedTools.map(async tool => { + const isMCPTool = !isBuiltInTool(tool.name); + let processedArguments = tool.arguments || {}; + + // Use AI to enhance arguments for MCP tools (same as regular flow) + if (isMCPTool) { + try { + const toolSchema = await MCPArgumentProcessor.getToolSchema(tool.name); + if (toolSchema) { + // Build user context from current conversation + const userContext = this.buildUserContext(); + + // Store original arguments for comparison + const originalArguments = { ...processedArguments }; + + // Use AI to intelligently prepare arguments + processedArguments = await this.enhanceArgumentsWithAI( + tool.name, + toolSchema, + userContext, + processedArguments + ); + + // Mark which fields were enhanced by LLM for UI display + processedArguments._llmEnhanced = { + enhanced: true, + originalArgs: originalArguments, + enhancedFields: this.identifyEnhancedFields( + originalArguments, + processedArguments + ), + }; + } + } catch (error) { + console.warn(`Failed to enhance arguments for ${tool.name}:`, error); + // Fall back to original arguments + } + } + + return { + id: `orchestrated-${tool.name}-${Date.now()}`, + name: tool.name, + description: tool.description, + arguments: processedArguments, + type: isMCPTool ? 'mcp' : 'regular', + priority: tool.priority, + reason: tool.reason, + }; + }) + ); + + let approvedToolIds: string[] = []; + try { + approvedToolIds = await inlineToolApprovalManager.requestApproval(toolsForApproval, this); + } catch (approvalError) { + const denialPrompt: Prompt = { + role: 'assistant', + content: "I understand. I won't execute those tools. Feel free to ask me something else.", + }; + this.history.push(denialPrompt); + return denialPrompt; + } + + // Filter approved tools and get their processed arguments + const approvedTools = recommendedTools.filter(tool => + approvedToolIds.some(id => id.includes(tool.name)) + ); + + // Group tools by execution strategy (parallel vs sequential) + const { parallel, sequential } = + ToolOrchestrator.groupToolsByExecutionStrategy(approvedTools); + + // Execute parallel tools first + const toolResults: Record = {}; + const toolExecutionIds: Record = {}; + + if (parallel.length > 0) { + const parallelPromises = parallel.map(async tool => { + const approvalData = toolsForApproval.find(t => t.name === tool.name); + const toolCallId = approvalData?.id || `orchestrated-${tool.name}-${Date.now()}`; + toolExecutionIds[tool.name] = toolCallId; + + try { + const result = await this.toolManager.executeTool( + tool.name, + approvalData?.arguments || tool.arguments || {} + ); + toolResults[tool.name] = result; + return result; + } catch (error) { + toolResults[tool.name] = { + error: true, + message: `Failed to execute ${tool.name}: ${error?.message || 'Unknown error'}`, + }; + } + }); + + try { + await Promise.all(parallelPromises); + } catch (error) { + console.error('Error executing parallel tools:', error); + // Continue with sequential tools even if some parallel tools fail + } + } + + // Execute sequential tools one by one + for (const tool of sequential) { + const approvalData = toolsForApproval.find(t => t.name === tool.name); + const toolCallId = approvalData?.id || `orchestrated-${tool.name}-${Date.now()}`; + toolExecutionIds[tool.name] = toolCallId; + + try { + const result = await this.toolManager.executeTool( + tool.name, + approvalData?.arguments || tool.arguments || {} + ); + toolResults[tool.name] = result; + } catch (error) { + toolResults[tool.name] = { + error: true, + message: `Failed to execute ${tool.name}: ${error?.message || 'Unknown error'}`, + }; + } + } + + // DO NOT add tool results to history - we'll let the LLM response handle rendering + // This prevents duplicate JSON rendering in the UI + // The results are kept in memory for the response generation below + + // Use LLM to generate a comprehensive response based on tool results + // Pass the FULL tool results with all data for the LLM to analyze + const response = await this.generateResponseFromToolResults(userMessage, toolResults); + + // Clear the tool confirmation message from history after execution completes + // This ensures the "Executing tools..." loading dialog is hidden + this.clearToolConfirmation(); + + return response; + } catch (error) { + // Fall back to regular LLM response + const errorPrompt: Prompt = { + role: 'assistant', + content: `I encountered an error coordinating multiple tools: ${ + error?.message || 'Unknown error' + }. + +Please try your request again or ask a simpler question.`, + error: true, + }; + this.history.push(errorPrompt); + return errorPrompt; + } + } + + /** + * Execute a single tool and return its result + */ + private async executeSingleTool(tool: RecommendedTool): Promise { + try { + // Create a tool call object compatible with existing tool execution logic + const toolCall = { + id: `tool-${tool.name}-${Date.now()}`, + function: { + name: tool.name, + arguments: JSON.stringify(tool.arguments || {}), + }, + }; + + // Use the existing tool manager to execute + const toolResponse = await this.toolManager.executeTool( + tool.name, + tool.arguments || {}, + toolCall.id, + { role: 'assistant', content: '' } // Placeholder prompt + ); + + return { + success: true, + toolName: tool.name, + data: JSON.parse(toolResponse.content), + }; + } catch (error) { + return { + success: false, + toolName: tool.name, + error: true, + message: error?.message || 'Unknown error occurred', + }; + } + } + + /** + * Aggregate results from multiple tools into a structured format + */ + private aggregateToolResults(results: Record): string { + let aggregation = '## Tool Execution Results\n\n'; + + for (const [toolName, result] of Object.entries(results)) { + aggregation += `### ${toolName}\n`; + + if (result.error) { + aggregation += `**Error**: ${result.message}\n\n`; + } else if (result.success) { + aggregation += `**Status**: Successfully executed\n`; + aggregation += `**Data**:\n\`\`\`json\n${JSON.stringify(result.data, null, 2)}\n\`\`\`\n\n`; + } else { + aggregation += `**Result**:\n\`\`\`json\n${JSON.stringify(result, null, 2)}\n\`\`\`\n\n`; + } + } + + return aggregation; + } + + /** + * Generate a comprehensive response based on aggregated tool results + * Now accepts full tool results object to provide complete context to LLM + */ + private async generateResponseFromToolResults( + userMessage: string, + toolResults: Record + ): Promise { + try { + const modelToUse = this.boundModel || this.model; + + // Format tool results with full data for LLM analysis + let formattedResults = '## Tool Execution Results\n\n'; + + for (const [toolName, result] of Object.entries(toolResults)) { + formattedResults += `### ${toolName}\n`; + + if (result.error || result.isError) { + formattedResults += `**Status**: ❌ Error\n`; + formattedResults += `**Error Message**: ${result.message || 'Unknown error'}\n\n`; + } else { + formattedResults += `**Status**: ✅ Success\n`; + + // Include the raw data (this is what was missing before!) + if (result.data) { + formattedResults += `**Data**:\n`; + formattedResults += '```json\n'; + formattedResults += JSON.stringify(result.data, null, 2); + formattedResults += '\n```\n\n'; + } else if (result.content) { + // Handle case where tool returns content directly + formattedResults += `**Data**:\n${result.content}\n\n`; + } else { + formattedResults += `**Result**:\n`; + formattedResults += '```json\n'; + formattedResults += JSON.stringify(result, null, 2); + formattedResults += '\n```\n\n'; + } + } + } + + const systemPrompt = `You are an AI assistant analyzing results from multiple tools that were executed together. + +Based on the ACTUAL tool response data provided below, generate a clear, comprehensive response to the user's request. + +CRITICAL GUIDELINES: +1. Analyze and discuss the ACTUAL data returned by the tools - don't be generic +2. Reference specific values, numbers, status, and findings from the results +3. Synthesize information across all tools to provide a complete picture +4. Use clear formatting (lists, tables, sections) for readability +5. Explain what the data means and why it matters +6. If any tools failed, mention that but focus on successful results +7. Provide actionable insights or next steps based on the actual data +8. Do NOT give generic responses - engage with the specific results`; + + const userPrompt = `Original user request: "${userMessage}" + +Tool Results: +${formattedResults} + +Please analyze this data and provide a specific, detailed response that directly addresses the user's request using the information provided above. Reference specific findings from the results.`; + + const messages = [ + new SystemMessage(systemPrompt), + ...this.prepareChatHistory().slice(-4), // Include recent context + new HumanMessage(userPrompt), + ]; + + const response = await modelToUse.invoke(messages, { + signal: this.currentAbortController?.signal, + }); + + this.currentAbortController = null; + + const assistantPrompt: Prompt = { + role: 'assistant', + content: this.extractTextContent(response.content), + }; + + this.history.push(assistantPrompt); + return assistantPrompt; + } catch (error) { + console.error('Error generating response from tool results:', error); + + const fallbackPrompt: Prompt = { + role: 'assistant', + content: `I executed the requested tools and gathered information. There was an error generating a comprehensive summary, but here are the raw results: + +${Object.entries(toolResults) + .map(([name, result]) => `**${name}**: ${JSON.stringify(result, null, 2)}`) + .join('\n\n')}`, + }; + + this.history.push(fallbackPrompt); + return fallbackPrompt; + } + } + // Extract tool call handling into separate method private async handleToolCalls(response: any): Promise { const enabledToolIds = this.toolManager.getToolNames(); diff --git a/ai-assistant/src/langchain/tools/ToolOrchestrator.ts b/ai-assistant/src/langchain/tools/ToolOrchestrator.ts new file mode 100644 index 0000000000..4c897c746a --- /dev/null +++ b/ai-assistant/src/langchain/tools/ToolOrchestrator.ts @@ -0,0 +1,314 @@ +import { BaseChatModel } from '@langchain/core/language_models/chat_models'; +import { HumanMessage, SystemMessage } from '@langchain/core/messages'; +import { z } from 'zod'; + +/** + * ToolOrchestrator - Analyzes user requests and determines all relevant tools + * that should be executed together to provide a comprehensive response. + * + * This enables multi-tool execution in a single interaction rather than + * requiring users to make multiple requests. + * + * Works with ANY type of tools: + * - MCP (Model Context Protocol) tools + * - Kubernetes tools + * - GitHub/GitOps tools + * - Cloud provider tools + * - Custom business tools + * - Any tool that can be executed and returns results + */ + +export interface RecommendedTool { + name: string; + description: string; + arguments: Record; + priority: 'high' | 'medium' | 'low'; + reason: string; +} + +export interface ToolRecommendation { + tools: RecommendedTool[]; + analysis: string; + shouldExecuteAll: boolean; +} + +const ToolRecommendationSchema = z.object({ + analysis: z.string().describe('Analysis of what information the user needs'), + tools: z + .array( + z + .object({ + name: z.string().optional().describe('Exact name of the tool to execute'), + tool_name: z.string().optional().describe('Alternative field for tool name'), + description: z.string().describe('What this tool will do'), + arguments: z + .union([ + z.record(z.any()), + z.string().transform(v => { + try { + return typeof v === 'string' ? JSON.parse(v) : v; + } catch { + return v; + } + }), + ]) + .describe('Arguments needed for this tool'), + priority: z + .enum(['high', 'medium', 'low']) + .default('medium') + .describe('Execution priority - high priority tools run first'), + reason: z.string().describe('Why this tool is needed to answer the user question'), + }) + .transform(tool => ({ + ...tool, + name: tool.name || tool.tool_name, + arguments: + typeof tool.arguments === 'string' ? JSON.parse(tool.arguments) : tool.arguments, + })) + .refine(tool => !!tool.name, { message: 'Tool must have either name or tool_name field' }) + ) + .describe('List of tools to execute'), + shouldExecuteAll: z + .boolean() + .default(true) + .describe('Whether all tools should be executed together for a complete answer'), +}); + +export class ToolOrchestrator { + /** + * Analyzes a user request and determines all relevant tools to execute together + * @param userMessage The user's question/request + * @param availableTools List of available tool names and descriptions (from any domain) + * @param model The language model to use for analysis + * @param conversationHistory Previous messages for context + * @param signal Optional AbortSignal for cancellation + * @returns Recommended tools with arguments, analysis, and execution strategy + * + * This method is domain-agnostic and works with: + * - Infrastructure/Kubernetes tools + * - GitOps/GitHub tools + * - MCP (Model Context Protocol) tools + * - Database tools + * - API tools + * - Any other tools that return results + */ + static async analyzeAndRecommendTools( + userMessage: string, + availableTools: Array<{ name: string; description: string }>, + model: BaseChatModel, + conversationHistory: any[] = [], + signal?: AbortSignal + ): Promise { + const toolsList = availableTools.map(tool => `- ${tool.name}: ${tool.description}`).join('\n'); + + const systemPrompt = `You are an intelligent tool orchestrator that analyzes user requests and recommends the best combination of tools to execute together. +Your task is to determine ALL relevant tools that should be executed to provide a comprehensive answer to the user's request. + +IMPORTANT RULES: +1. Recommend MULTIPLE tools when they provide complementary information to answer the user's question completely +2. Order tools by execution priority (high priority first) +3. Provide specific, concrete arguments for each tool based on the user's request and context +4. Tools that read/fetch data can execute in parallel for better performance +5. Tools that modify state should execute sequentially to maintain consistency +6. Always think about what information the user really needs to answer their question completely +7. Consider dependencies: if one tool's output is needed for another tool's input, put dependent tool last +8. Be inclusive: recommend all tools that could be helpful, not just the minimum needed + +TOOL CATEGORIES: +- READ tools: get_*, list_*, search_*, read, fetch, describe, show, display +- WRITE tools: create, apply, update, patch, delete, remove, modify, reconcile, merge +- QUERY tools: search, query, find, lookup, retrieve + +Available tools: +${toolsList} + +GENERIC EXAMPLES OF MULTI-TOOL SCENARIOS: + +Example 1: Information Gathering +- User: "Give me a complete picture of the system" +- Recommended tools: All read/query tools that provide different perspectives +- Pattern: Execute all reads in parallel, synthesize results + +Example 2: Creation + Verification +- User: "Create something and verify it worked" +- Recommended tools: Write tool first, then read tools to verify +- Pattern: Sequential write, then parallel reads + +Example 3: Query + Related Data +- User: "Show me status, metrics, and logs" +- Recommended tools: get_status, get_metrics, get_logs +- Pattern: All parallel (all read operations) + +Example 4: Multi-source Data +- User: "Compare configurations from multiple sources" +- Recommended tools: get_file_contents, list_resources, query_database +- Pattern: Parallel execution of different data sources + +Example 5: Workflow Orchestration +- User: "Deploy this and handle any failures" +- Recommended tools: apply_manifest, trigger_reconciliation, get_status, get_logs +- Pattern: Apply (sequential), then reconcile (sequential), then verify (parallel) + +DECISION LOGIC: +- Can tools run in parallel? If yes, and both help answer the question, recommend both +- Are there dependencies? If tool B needs output from tool A, note this in priority +- Does the user need complete information? If yes, recommend complementary tools +- Are there failure cases? Include tools that help diagnose problems + +RESPONSE FORMAT: +Return a JSON object with EXACTLY this structure: +{ + "analysis": "Your explanation of what the user needs", + "tools": [ + { + "name": "exact_tool_name", + "description": "What this tool does", + "arguments": { "key": "value" }, + "priority": "high|medium|low", + "reason": "Why this tool is needed" + } + ], + "shouldExecuteAll": true +} + +IMPORTANT: Use "name" (not "tool_name") for the tool field. Each tool object must have: name, description, arguments (as object), priority, and reason.`; + + const userPrompt = `User request: "${userMessage}" + +Analyze this request and recommend ALL tools needed to answer it completely. +For each tool, provide: +1. Exact tool name (must match available tools) - use the "name" field +2. Complete arguments needed as a JSON object +3. Why it's needed + +Return your response as a valid JSON object. Include ONLY the JSON, no other text.`; + + try { + // Build messages with conversation history if available + const messages = [ + new SystemMessage(systemPrompt), + ...conversationHistory.filter( + msg => msg.role === 'system' || msg.role === 'assistant' || msg.role === 'user' + ), + new HumanMessage(userPrompt), + ]; + + // Call the model to get tool recommendations + const response = await model.invoke(messages, { signal }); + + // Extract and parse the response + const responseText = this.extractTextContent(response.content); + + // Try to extract JSON from the response - handle nested braces + let parsedResponse; + const jsonMatch = responseText.match(/\{[\s\S]*\}/); + if (!jsonMatch) { + console.warn('Could not extract JSON from tool orchestrator response:', responseText); + return { + tools: [], + analysis: responseText, + shouldExecuteAll: false, + }; + } + + try { + parsedResponse = JSON.parse(jsonMatch[0]); + } catch (parseError) { + console.warn('Failed to parse JSON from response:', jsonMatch[0], parseError); + return { + tools: [], + analysis: responseText, + shouldExecuteAll: false, + }; + } + + // Validate the response against schema with better error handling + let validated; + try { + validated = ToolRecommendationSchema.parse(parsedResponse); + } catch (validationError) { + // Try to extract what we can from the response + const fallbackAnalysis = parsedResponse.analysis || responseText; + + return { + tools: [], + analysis: fallbackAnalysis, + shouldExecuteAll: false, + }; + } + + // Filter tools to ensure they exist in availableTools + const validatedTools = validated.tools.filter(tool => + availableTools.some(available => available.name === tool.name) + ); + + return { + tools: validatedTools, + analysis: validated.analysis, + shouldExecuteAll: validated.shouldExecuteAll && validatedTools.length > 0, + }; + } catch (error) { + console.error('Error in tool orchestration analysis:', error); + // Return empty recommendations on error - fall back to single tool + return { + tools: [], + analysis: 'Unable to analyze tool requirements', + shouldExecuteAll: false, + }; + } + } + + /** + * Groups tools by execution strategy (parallel vs sequential) + */ + static groupToolsByExecutionStrategy(tools: RecommendedTool[]): { + parallel: RecommendedTool[]; + sequential: RecommendedTool[]; + } { + // Read-only tools (get_*) can execute in parallel + // Modification tools should execute sequentially + const parallelPatterns = ['get_', 'list_', 'search_', 'read']; + + const parallel = tools.filter(tool => parallelPatterns.some(p => tool.name.includes(p))); + + const sequential = tools.filter(tool => !parallel.includes(tool)); + + // Sort by priority within each group + const priorityOrder = { high: 0, medium: 1, low: 2 }; + parallel.sort((a, b) => priorityOrder[a.priority] - priorityOrder[b.priority]); + sequential.sort((a, b) => priorityOrder[a.priority] - priorityOrder[b.priority]); + + return { parallel, sequential }; + } + + /** + * Helper to extract text content from different response formats + */ + private static extractTextContent(content: any): string { + if (typeof content === 'string') { + return content; + } + + if (Array.isArray(content)) { + return content + .filter(item => item && typeof item === 'object' && item.type === 'text') + .map(item => item.text || '') + .join(''); + } + + if (content && typeof content === 'object') { + if (content.text) { + return content.text; + } + if (content.content) { + return this.extractTextContent(content.content); + } + } + + try { + return String(content || ''); + } catch { + return ''; + } + } +} From 8debdd565e828ae38169dd9b742635fc97ec658b Mon Sep 17 00:00:00 2001 From: Joaquim Rocha Date: Fri, 31 Oct 2025 23:43:34 +0000 Subject: [PATCH 39/39] ai-assistant: Add a GUI for managing MCP servers Instead of just relying on editing JSON. --- .../settings/MCPConfigEditorDialog.tsx | 5 +- .../components/settings/MCPServerEditor.tsx | 268 ++++++++++ .../settings/MCPServerFormDialog.tsx | 495 ++++++++++++++++++ .../src/components/settings/MCPSettings.tsx | 338 +++++++++--- ai-assistant/src/index.tsx | 2 +- 5 files changed, 1028 insertions(+), 80 deletions(-) create mode 100644 ai-assistant/src/components/settings/MCPServerEditor.tsx create mode 100644 ai-assistant/src/components/settings/MCPServerFormDialog.tsx diff --git a/ai-assistant/src/components/settings/MCPConfigEditorDialog.tsx b/ai-assistant/src/components/settings/MCPConfigEditorDialog.tsx index b0f64a74d4..e3d1275920 100644 --- a/ai-assistant/src/components/settings/MCPConfigEditorDialog.tsx +++ b/ai-assistant/src/components/settings/MCPConfigEditorDialog.tsx @@ -140,6 +140,7 @@ export default function MCPConfigEditorDialog({ setContent(JSON.stringify(exampleConfig, null, 2)); setValidationError(''); + setTabValue(0); // Switch to editor tab }; const handleReset = () => { @@ -296,7 +297,9 @@ export default function MCPConfigEditorDialog({
  • - args: Command-line arguments passed to the executable + args: Command-line arguments passed to the executable. You + can use HEADLAMP_CURRENT_CLUSTER as a placeholder that will be + replaced with the current cluster context at runtime.
  • diff --git a/ai-assistant/src/components/settings/MCPServerEditor.tsx b/ai-assistant/src/components/settings/MCPServerEditor.tsx new file mode 100644 index 0000000000..d5e7c5b9d2 --- /dev/null +++ b/ai-assistant/src/components/settings/MCPServerEditor.tsx @@ -0,0 +1,268 @@ +import { Icon } from '@iconify/react'; +import { Dialog } from '@kinvolk/headlamp-plugin/lib/CommonComponents'; +import { + Alert, + Box, + Button, + DialogActions, + DialogContent, + DialogTitle, + FormControlLabel, + IconButton, + Paper, + Switch, + TextField, + Typography, +} from '@mui/material'; +import React, { useEffect, useState } from 'react'; + +export interface MCPServer { + name: string; + command: string; + args: string[]; + env?: Record; + enabled: boolean; +} + +interface MCPServerEditorProps { + open: boolean; + onClose: () => void; + server?: MCPServer; + onSave: (server: MCPServer) => void; + existingServerNames: string[]; +} + +export default function MCPServerEditor({ + open, + onClose, + server, + onSave, + existingServerNames, +}: MCPServerEditorProps) { + const [name, setName] = useState(''); + const [command, setCommand] = useState(''); + const [args, setArgs] = useState(''); + const [env, setEnv] = useState>([]); + const [enabled, setEnabled] = useState(true); + const [validationError, setValidationError] = useState(''); + + const isEditing = !!server; + + useEffect(() => { + if (open) { + if (server) { + setName(server.name); + setCommand(server.command); + setArgs(server.args.join(' ')); + setEnv( + server.env ? Object.entries(server.env).map(([key, value]) => ({ key, value })) : [] + ); + setEnabled(server.enabled); + } else { + setName(''); + setCommand(''); + setArgs(''); + setEnv([]); + setEnabled(true); + } + setValidationError(''); + } + }, [open, server]); + + const handleAddEnvVar = () => { + setEnv([...env, { key: '', value: '' }]); + }; + + const handleRemoveEnvVar = (index: number) => { + setEnv(env.filter((_, i) => i !== index)); + }; + + const handleEnvVarChange = (index: number, field: 'key' | 'value', value: string) => { + const newEnv = [...env]; + newEnv[index][field] = value; + setEnv(newEnv); + }; + + const handleLoadExample = () => { + setName('flux-mcp'); + setCommand('flux-operator-mcp'); + setArgs('serve --kube-context HEADLAMP_CURRENT_CLUSTER'); + setEnv([{ key: 'KUBECONFIG', value: 'PATH_TO_KUBECONFIG' }]); + setEnabled(true); + setValidationError(''); + }; + + const validateServer = (): string | null => { + if (!name.trim()) { + return 'Server name is required'; + } + + if (!command.trim()) { + return 'Command is required'; + } + + // Check for duplicate names (excluding current server if editing) + const namesToCheck = existingServerNames.filter(n => !isEditing || n !== server?.name); + if (namesToCheck.map(n => n.toLowerCase()).includes(name.toLowerCase())) { + return 'A server with this name already exists'; + } + + // Validate env variables + for (const envVar of env) { + if (!envVar.key.trim()) { + return 'Environment variable keys cannot be empty'; + } + } + + return null; + }; + + const handleSave = () => { + const error = validateServer(); + if (error) { + setValidationError(error); + return; + } + + const mcpServer: MCPServer = { + name, + command, + args: args + .split(/\s+/) + .filter(arg => arg.trim()) + .map(arg => arg.trim()), + enabled, + }; + + if (env.length > 0) { + mcpServer.env = env.reduce((acc, { key, value }) => { + if (key.trim()) { + acc[key] = value; + } + return acc; + }, {} as Record); + } + + onSave(mcpServer); + onClose(); + }; + + return ( + + + + {isEditing ? 'Edit Server' : 'Add MCP Server'} + {!isEditing && ( + + )} + + + + + {validationError && ( + + {validationError} + + )} + + + + setName(e.target.value)} + fullWidth + required + helperText="Unique identifier for this MCP server" + /> + setEnabled(e.target.checked)} />} + label="Enabled" + sx={{ mt: 1, minWidth: '120px' }} + /> + + + setCommand(e.target.value)} + fullWidth + required + helperText="Executable command (e.g., 'docker', 'npx', 'python')" + /> + + setArgs(e.target.value)} + fullWidth + helperText="Command-line arguments separated by spaces. Use HEADLAMP_CURRENT_CLUSTER as a placeholder for the current cluster context." + /> + + {/* Environment Variables */} + + + + Environment Variables + + Use HEADLAMP_CURRENT_CLUSTER as a placeholder for the current cluster + context + + + + + + {env.length === 0 ? ( + + No environment variables configured + + ) : ( + + {env.map((envVar, index) => ( + + handleEnvVarChange(index, 'key', e.target.value)} + size="small" + sx={{ flex: 1 }} + /> + handleEnvVarChange(index, 'value', e.target.value)} + size="small" + sx={{ flex: 2 }} + placeholder="e.g., HEADLAMP_CURRENT_CLUSTER" + /> + handleRemoveEnvVar(index)} color="error"> + + + + ))} + + )} + + + + + + + + + + ); +} diff --git a/ai-assistant/src/components/settings/MCPServerFormDialog.tsx b/ai-assistant/src/components/settings/MCPServerFormDialog.tsx new file mode 100644 index 0000000000..32651d5b22 --- /dev/null +++ b/ai-assistant/src/components/settings/MCPServerFormDialog.tsx @@ -0,0 +1,495 @@ +import { Icon } from '@iconify/react'; +import { Dialog } from '@kinvolk/headlamp-plugin/lib/CommonComponents'; +import { + Alert, + Box, + Button, + Chip, + DialogActions, + DialogContent, + DialogTitle, + FormControlLabel, + IconButton, + Paper, + Switch, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + TextField, + Tooltip, + Typography, +} from '@mui/material'; +import React, { useState } from 'react'; + +export interface MCPServer { + name: string; + command: string; + args: string[]; + env?: Record; + enabled: boolean; +} + +export interface MCPConfig { + enabled: boolean; + servers: MCPServer[]; +} + +interface MCPServerFormDialogProps { + open: boolean; + onClose: () => void; + config: MCPConfig; + onSave: (config: MCPConfig) => void; +} + +interface ServerFormData { + name: string; + command: string; + args: string; + env: Array<{ key: string; value: string }>; + enabled: boolean; +} + +export default function MCPServerFormDialog({ + open, + onClose, + config, + onSave, +}: MCPServerFormDialogProps) { + const [servers, setServers] = useState([]); + const [editingIndex, setEditingIndex] = useState(null); + const [currentServer, setCurrentServer] = useState({ + name: '', + command: '', + args: '', + env: [], + enabled: true, + }); + const [validationError, setValidationError] = useState(''); + + React.useEffect(() => { + if (open) { + // Convert config to form data + const formData: ServerFormData[] = config.servers.map(server => ({ + name: server.name, + command: server.command, + args: server.args.join(' '), + env: server.env + ? Object.entries(server.env).map(([key, value]) => ({ key, value })) + : [], + enabled: server.enabled, + })); + setServers(formData); + setEditingIndex(null); + setCurrentServer({ + name: '', + command: '', + args: '', + env: [], + enabled: true, + }); + setValidationError(''); + } + }, [config, open]); + + const handleAddServer = () => { + setEditingIndex(null); + setCurrentServer({ + name: '', + command: '', + args: '', + env: [], + enabled: true, + }); + setValidationError(''); + }; + + const handleEditServer = (index: number) => { + setEditingIndex(index); + setCurrentServer(servers[index]); + setValidationError(''); + }; + + const handleDeleteServer = (index: number) => { + const newServers = servers.filter((_, i) => i !== index); + setServers(newServers); + if (editingIndex === index) { + setEditingIndex(null); + setCurrentServer({ + name: '', + command: '', + args: '', + env: [], + enabled: true, + }); + } + }; + + const handleToggleServerEnabled = (index: number) => { + const newServers = [...servers]; + newServers[index].enabled = !newServers[index].enabled; + setServers(newServers); + }; + + const handleAddEnvVar = () => { + setCurrentServer({ + ...currentServer, + env: [...currentServer.env, { key: '', value: '' }], + }); + }; + + const handleRemoveEnvVar = (index: number) => { + setCurrentServer({ + ...currentServer, + env: currentServer.env.filter((_, i) => i !== index), + }); + }; + + const handleEnvVarChange = (index: number, field: 'key' | 'value', value: string) => { + const newEnv = [...currentServer.env]; + newEnv[index][field] = value; + setCurrentServer({ + ...currentServer, + env: newEnv, + }); + }; + + const validateServer = (server: ServerFormData): string | null => { + if (!server.name.trim()) { + return 'Server name is required'; + } + + if (!server.command.trim()) { + return 'Command is required'; + } + + // Check for duplicate names (excluding current editing server) + const existingNames = servers + .filter((_, i) => i !== editingIndex) + .map(s => s.name.toLowerCase()); + if (existingNames.includes(server.name.toLowerCase())) { + return 'A server with this name already exists'; + } + + // Validate env variables + for (const envVar of server.env) { + if (!envVar.key.trim()) { + return 'Environment variable keys cannot be empty'; + } + } + + return null; + }; + + const handleSaveServer = () => { + const error = validateServer(currentServer); + if (error) { + setValidationError(error); + return; + } + + const newServers = [...servers]; + if (editingIndex !== null) { + newServers[editingIndex] = currentServer; + } else { + newServers.push(currentServer); + } + + setServers(newServers); + setEditingIndex(null); + setCurrentServer({ + name: '', + command: '', + args: '', + env: [], + enabled: true, + }); + setValidationError(''); + }; + + const handleCancelEdit = () => { + setEditingIndex(null); + setCurrentServer({ + name: '', + command: '', + args: '', + env: [], + enabled: true, + }); + setValidationError(''); + }; + + const handleSaveConfig = () => { + // Convert form data back to config format + const newConfig: MCPConfig = { + enabled: config.enabled, + servers: servers.map(server => { + const mcpServer: MCPServer = { + name: server.name, + command: server.command, + args: server.args + .split(/\s+/) + .filter(arg => arg.trim()) + .map(arg => arg.trim()), + enabled: server.enabled, + }; + + if (server.env.length > 0) { + mcpServer.env = server.env.reduce((acc, { key, value }) => { + if (key.trim()) { + acc[key] = value; + } + return acc; + }, {} as Record); + } + + return mcpServer; + }), + }; + + onSave(newConfig); + onClose(); + }; + + const handleLoadExample = () => { + const exampleServer: ServerFormData = { + name: 'flux-mcp', + command: 'flux-operator-mcp', + args: 'serve --kube-context HEADLAMP_CURRENT_CLUSTER', + env: [{ key: 'KUBECONFIG', value: 'PATH_TO_KUBECONFIG' }], + enabled: true, + }; + + setCurrentServer(exampleServer); + setEditingIndex(null); + setValidationError(''); + }; + + const isFormVisible = editingIndex !== null || currentServer.name || currentServer.command; + + return ( + + + + Configure MCP Servers + + + + + + + + Configure MCP (Model Context Protocol) servers to extend the AI assistant's + capabilities. Each server provides specific tools and functionality. + + + + {/* Existing Servers List */} + {servers.length > 0 && ( + + + Configured Servers ({servers.length}) + + +
  • + + + Name + Enabled + Actions + + + + {servers.map((server, index) => ( + + + + {server.name} + + {server.env.length > 0 && ( + + {server.env.length} env var(s) + + )} + + + handleToggleServerEnabled(index)} + size="small" + /> + + + + handleEditServer(index)}> + + + + + handleDeleteServer(index)} + color="error" + > + + + + + + ))} + +
    +
    +
    + )} + + {/* Server Form */} + {isFormVisible && ( + + + + {editingIndex !== null ? 'Edit Server' : 'Add New Server'} + + + + + {validationError && ( + + {validationError} + + )} + + + setCurrentServer({ ...currentServer, name: e.target.value })} + fullWidth + required + helperText="Unique identifier for this MCP server" + /> + + setCurrentServer({ ...currentServer, command: e.target.value })} + fullWidth + required + helperText="Executable command (e.g., 'docker', 'npx', 'python')" + /> + + setCurrentServer({ ...currentServer, args: e.target.value })} + fullWidth + multiline + rows={2} + helperText="Command-line arguments separated by spaces" + /> + + setCurrentServer({ ...currentServer, enabled: e.target.checked })} + /> + } + label="Enable this server" + /> + + {/* Environment Variables */} + + + Environment Variables + + + + {currentServer.env.length === 0 ? ( + + No environment variables configured + + ) : ( + + {currentServer.env.map((envVar, index) => ( + + handleEnvVarChange(index, 'key', e.target.value)} + size="small" + sx={{ flex: 1 }} + /> + handleEnvVarChange(index, 'value', e.target.value)} + size="small" + sx={{ flex: 2 }} + /> + handleRemoveEnvVar(index)} + color="error" + > + + + + ))} + + )} + + + + + + + + + )} + + + + + + + + ); +} diff --git a/ai-assistant/src/components/settings/MCPSettings.tsx b/ai-assistant/src/components/settings/MCPSettings.tsx index a6565b0d43..0a2ab71345 100644 --- a/ai-assistant/src/components/settings/MCPSettings.tsx +++ b/ai-assistant/src/components/settings/MCPSettings.tsx @@ -1,10 +1,26 @@ import { Icon } from '@iconify/react'; import { Headlamp } from '@kinvolk/headlamp-plugin/lib'; import { SectionBox } from '@kinvolk/headlamp-plugin/lib/components/common'; -import { Box, Button, FormControlLabel, Switch, Typography } from '@mui/material'; +import { + Box, + Button, + FormControlLabel, + IconButton, + Paper, + Switch, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Tooltip, + Typography, +} from '@mui/material'; import React, { useEffect, useState } from 'react'; import { pluginStore } from '../../utils'; import MCPConfigEditorDialog from './MCPConfigEditorDialog'; +import MCPServerEditor from './MCPServerEditor'; export interface MCPServer { name: string; @@ -25,14 +41,15 @@ interface MCPSettingsProps { } export function MCPSettings({ config, onConfigChange }: MCPSettingsProps) { - const [mcpConfig, setMCPConfig] = useState( - config || { - enabled: false, - servers: [], - } - ); - - const [editorDialogOpen, setEditorDialogOpen] = useState(false); + const [jsonEditorOpen, setJsonEditorOpen] = useState(false); + const [serverEditorOpen, setServerEditorOpen] = useState(false); + const [editingServer, setEditingServer] = useState(undefined); + const [mcpConfig, setMCPConfig] = useState({ + enabled: false, + servers: [], + }); + const [pendingConfig, setPendingConfig] = useState(null); + const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false); useEffect(() => { // Load MCP config from Electron if available @@ -54,6 +71,8 @@ export function MCPSettings({ config, onConfigChange }: MCPSettingsProps) { const response = await window.desktopApi!.mcp.getConfig(); if (response.success && response.config) { setMCPConfig(response.config); + setPendingConfig(response.config); + setHasUnsavedChanges(false); } } catch (error) { console.error('Error loading MCP config from Electron:', error); @@ -61,51 +80,62 @@ export function MCPSettings({ config, onConfigChange }: MCPSettingsProps) { const savedConfig = pluginStore.get(); if (savedConfig?.mcpConfig) { setMCPConfig(savedConfig.mcpConfig); + setPendingConfig(savedConfig.mcpConfig); + setHasUnsavedChanges(false); } } }; - const handleConfigChange = async (newConfig: MCPConfig) => { - setMCPConfig(newConfig); + const handleSaveChanges = async () => { + if (!pendingConfig) return; if (Headlamp.isRunningAsApp()) { // Save to Electron settings and restart MCP client try { - const response = await window.desktopApi!.mcp.updateConfig(newConfig); - if (!response.success) { + const response = await window.desktopApi!.mcp.updateConfig(pendingConfig); + if (response.success) { + // Reload config from Electron after successful update + await loadMCPConfigFromElectron(); + } else { console.error('Error updating MCP config in Electron:', response.error); - // Still save to plugin store as fallback - const currentConfig = pluginStore.get() || {}; - pluginStore.update({ - ...currentConfig, - mcpConfig: newConfig, - }); } } catch (error) { console.error('Error updating MCP config:', error); - // Fallback to plugin store - const currentConfig = pluginStore.get() || {}; - pluginStore.update({ - ...currentConfig, - mcpConfig: newConfig, - }); } } else { // Save to plugin store for non-Electron environments const currentConfig = pluginStore.get() || {}; pluginStore.update({ ...currentConfig, - mcpConfig: newConfig, + mcpConfig: pendingConfig, }); + // Update local state from plugin store + const updatedConfig = pluginStore.get(); + if (updatedConfig?.mcpConfig) { + setMCPConfig(updatedConfig.mcpConfig); + setPendingConfig(updatedConfig.mcpConfig); + setHasUnsavedChanges(false); + } } - // Also notify parent if callback provided + // Notify parent if callback provided if (onConfigChange) { - onConfigChange(newConfig); + onConfigChange(pendingConfig); } }; + const handleDiscardChanges = () => { + setPendingConfig(mcpConfig); + setHasUnsavedChanges(false); + }; + + const updatePendingConfig = (newConfig: MCPConfig) => { + setPendingConfig(newConfig); + setHasUnsavedChanges(true); + }; + const handleToggleEnabled = async () => { + if (!pendingConfig) return; const newConfig = { ...mcpConfig, enabled: !mcpConfig.enabled }; // If enabling MCP for the first time and no servers exist, add default servers @@ -131,19 +161,91 @@ export function MCPSettings({ config, onConfigChange }: MCPSettingsProps) { newConfig.servers = defaultServers; } - await handleConfigChange(newConfig); + // Immediately save this change (bypass pending state) + if (Headlamp.isRunningAsApp()) { + try { + const response = await window.desktopApi!.mcp.updateConfig(newConfig); + if (response.success) { + await loadMCPConfigFromElectron(); + } else { + console.error('Error updating MCP config in Electron:', response.error); + } + } catch (error) { + console.error('Error updating MCP config:', error); + } + } else { + const currentConfig = pluginStore.get() || {}; + pluginStore.update({ + ...currentConfig, + mcpConfig: newConfig, + }); + const updatedConfig = pluginStore.get(); + if (updatedConfig?.mcpConfig) { + setMCPConfig(updatedConfig.mcpConfig); + setPendingConfig(updatedConfig.mcpConfig); + setHasUnsavedChanges(false); + } + } + + if (onConfigChange) { + onConfigChange(newConfig); + } + }; + + const handleOpenServerEditor = (server?: MCPServer) => { + setEditingServer(server); + setServerEditorOpen(true); + }; + + const handleCloseServerEditor = () => { + setServerEditorOpen(false); + setEditingServer(undefined); + }; + + const handleSaveServer = (server: MCPServer) => { + if (!pendingConfig) return; + + let newServers: MCPServer[]; + + if (editingServer) { + // Check if the server actually changed + const originalServer = pendingConfig.servers.find(s => s.name === editingServer.name); + const serverChanged = !originalServer || JSON.stringify(originalServer) !== JSON.stringify(server); + + if (!serverChanged) { + // No changes, just close the dialog + return; + } + + // Update existing server + newServers = pendingConfig.servers.map(s => (s.name === editingServer.name ? server : s)); + } else { + // Add new server + newServers = [...pendingConfig.servers, server]; + } + + const newConfig = { ...pendingConfig, servers: newServers }; + updatePendingConfig(newConfig); }; - const handleOpenEditorDialog = () => { - setEditorDialogOpen(true); + const handleDeleteServer = (serverName: string) => { + if (!pendingConfig) return; + const newServers = pendingConfig.servers.filter(s => s.name !== serverName); + const newConfig = { ...pendingConfig, servers: newServers }; + updatePendingConfig(newConfig); }; - const handleCloseEditorDialog = () => { - setEditorDialogOpen(false); + const handleToggleServerEnabled = (serverName: string) => { + if (!pendingConfig) return; + const newServers = pendingConfig.servers.map(s => + s.name === serverName ? { ...s, enabled: !s.enabled } : s + ); + const newConfig = { ...pendingConfig, servers: newServers }; + updatePendingConfig(newConfig); }; const handleSaveConfig = (newConfig: MCPConfig) => { - handleConfigChange(newConfig); + updatePendingConfig(newConfig); }; // Only show MCP settings in Electron @@ -157,6 +259,8 @@ export function MCPSettings({ config, onConfigChange }: MCPSettingsProps) { ); } + const displayConfig = pendingConfig || mcpConfig; + return ( @@ -171,62 +275,140 @@ export function MCPSettings({ config, onConfigChange }: MCPSettingsProps) { /> - {mcpConfig.enabled && ( + {displayConfig.enabled && ( <> - {/* Configuration Summary */} + {/* Configuration Header */} - - Server Configuration - - - You have {mcpConfig.servers.length} server(s) configured. - {mcpConfig.servers.filter(s => s.enabled).length} server(s) are currently enabled. - - - - + + Configured Servers + + + + + - {/* Server List Summary */} - {mcpConfig.servers.length > 0 && ( - - - Configured Servers: + + + {displayConfig.servers.length} server(s) configured,{' '} + {displayConfig.servers.filter(s => s.enabled).length} enabled. + {hasUnsavedChanges && ( + + (Unsaved changes) + + )} - - {mcpConfig.servers.map((server, index) => ( -
  • - - {server.name} ({server.command}) - - - {server.enabled ? 'Enabled' : 'Disabled'} - - {server.env && ( - - (with env variables) - - )} - -
  • - ))} -
    + {hasUnsavedChanges && ( + + + + + )}
    +
    + + {/* Server List Table */} + {displayConfig.servers.length > 0 ? ( + + + + + Name + Enabled + Actions + + + + {displayConfig.servers.map((server, index) => ( + + + + {server.name} + + + + handleToggleServerEnabled(server.name)} + size="small" + /> + + + + handleOpenServerEditor(server)} + > + + + + + handleDeleteServer(server.name)} + color="error" + > + + + + + + ))} + +
    +
    + ) : ( + + + No MCP servers configured. Click "Add Server" to get started. + + )} )} - {/* Editor Dialog */} + {/* JSON Editor Dialog */} setJsonEditorOpen(false)} + config={displayConfig} onSave={handleSaveConfig} /> + + {/* Server Editor Dialog */} + s.name)} + />
    ); } diff --git a/ai-assistant/src/index.tsx b/ai-assistant/src/index.tsx index 452d692c0e..ee25c9e236 100644 --- a/ai-assistant/src/index.tsx +++ b/ai-assistant/src/index.tsx @@ -394,7 +394,7 @@ function Settings() { }; return ( - + This plugin is in early development and is not yet ready for production use. Using it may incur in costs from the AI provider! Use at your own risk.