From b154d6c2ef02c54c38797ff139d7c65a89842ea3 Mon Sep 17 00:00:00 2001 From: abhigyanpatwari Date: Tue, 6 Jan 2026 04:10:38 +0530 Subject: [PATCH 1/3] implemented azureopenai and gemini provider. currently struggling with graph query tool for agent --- package-lock.json | 785 ++++++++++++++++++++++++++++++- package.json | 7 +- src/App.tsx | 19 + src/components/DropZone.tsx | 3 - src/components/RightPanel.tsx | 152 ++++-- src/components/SettingsPanel.tsx | 489 +++++++++++++++++++ src/core/llm/agent.ts | 235 +++++++++ src/core/llm/index.ts | 34 ++ src/core/llm/settings-service.ts | 189 ++++++++ src/core/llm/tools.ts | 267 +++++++++++ src/core/llm/types.ts | 188 ++++++++ src/hooks/useAppState.tsx | 222 +++++++++ src/workers/ingestion.worker.ts | 111 +++++ 13 files changed, 2651 insertions(+), 50 deletions(-) create mode 100644 src/components/SettingsPanel.tsx create mode 100644 src/core/llm/agent.ts create mode 100644 src/core/llm/index.ts create mode 100644 src/core/llm/settings-service.ts create mode 100644 src/core/llm/tools.ts create mode 100644 src/core/llm/types.ts diff --git a/package-lock.json b/package-lock.json index 5cdccdc15..ea72b7cd1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,10 @@ "dependencies": { "@huggingface/transformers": "^3.0.0", "@isomorphic-git/lightning-fs": "^4.6.2", + "@langchain/core": "^0.3.0", + "@langchain/google-genai": "^0.1.0", + "@langchain/langgraph": "^0.2.74", + "@langchain/openai": "^0.3.0", "@sigma/edge-curve": "^3.1.0", "@tailwindcss/vite": "^4.1.18", "axios": "^1.13.2", @@ -23,6 +27,7 @@ "isomorphic-git": "^1.36.1", "jszip": "^3.10.1", "kuzu-wasm": "^0.11.1", + "langchain": "^0.3.37", "lru-cache": "^11.2.4", "lucide-react": "^0.562.0", "react": "^18.3.1", @@ -35,7 +40,7 @@ "vite-plugin-top-level-await": "^1.6.0", "vite-plugin-wasm": "^3.5.0", "web-tree-sitter": "^0.20.8", - "zod": "^4.1.13" + "zod": "^3.25.76" }, "devDependencies": { "@babel/types": "^7.28.5", @@ -352,6 +357,12 @@ "node": ">=6.9.0" } }, + "node_modules/@cfworker/json-schema": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@cfworker/json-schema/-/json-schema-4.1.1.tgz", + "integrity": "sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og==", + "license": "MIT" + }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", @@ -817,6 +828,15 @@ "node": ">=14" } }, + "node_modules/@google/generative-ai": { + "version": "0.24.1", + "resolved": "https://registry.npmjs.org/@google/generative-ai/-/generative-ai-0.24.1.tgz", + "integrity": "sha512-MqO+MLfM6kjxcKoy0p1wRzG3b4ZZXtPI+z2IE26UogS2Cm/XHO+7gGRBh6gcJsOiIVoH93UwKvW4HdgiOZCy9Q==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@huggingface/jinja": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/@huggingface/jinja/-/jinja-0.5.3.tgz", @@ -1404,6 +1424,242 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@langchain/core": { + "version": "0.3.80", + "resolved": "https://registry.npmjs.org/@langchain/core/-/core-0.3.80.tgz", + "integrity": "sha512-vcJDV2vk1AlCwSh3aBm/urQ1ZrlXFFBocv11bz/NBUfLWD5/UDNMzwPdaAd2dKvNmTWa9FM2lirLU3+JCf4cRA==", + "license": "MIT", + "dependencies": { + "@cfworker/json-schema": "^4.0.2", + "ansi-styles": "^5.0.0", + "camelcase": "6", + "decamelize": "1.2.0", + "js-tiktoken": "^1.0.12", + "langsmith": "^0.3.67", + "mustache": "^4.2.0", + "p-queue": "^6.6.2", + "p-retry": "4", + "uuid": "^10.0.0", + "zod": "^3.25.32", + "zod-to-json-schema": "^3.22.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@langchain/core/node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@langchain/google-genai": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/@langchain/google-genai/-/google-genai-0.1.12.tgz", + "integrity": "sha512-0Ea0E2g63ejCuormVxbuoyJQ5BYN53i2/fb6WP8bMKzyh+y43R13V8JqOtr3e/GmgNyv3ou/VeaZjx7KAvu/0g==", + "license": "MIT", + "dependencies": { + "@google/generative-ai": "^0.24.0", + "zod-to-json-schema": "^3.22.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@langchain/core": ">=0.3.17 <0.4.0" + } + }, + "node_modules/@langchain/langgraph": { + "version": "0.2.74", + "resolved": "https://registry.npmjs.org/@langchain/langgraph/-/langgraph-0.2.74.tgz", + "integrity": "sha512-oHpEi5sTZTPaeZX1UnzfM2OAJ21QGQrwReTV6+QnX7h8nDCBzhtipAw1cK616S+X8zpcVOjgOtJuaJhXa4mN8w==", + "license": "MIT", + "dependencies": { + "@langchain/langgraph-checkpoint": "~0.0.17", + "@langchain/langgraph-sdk": "~0.0.32", + "uuid": "^10.0.0", + "zod": "^3.23.8" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@langchain/core": ">=0.2.36 <0.3.0 || >=0.3.40 < 0.4.0", + "zod-to-json-schema": "^3.x" + }, + "peerDependenciesMeta": { + "zod-to-json-schema": { + "optional": true + } + } + }, + "node_modules/@langchain/langgraph-checkpoint": { + "version": "0.0.18", + "resolved": "https://registry.npmjs.org/@langchain/langgraph-checkpoint/-/langgraph-checkpoint-0.0.18.tgz", + "integrity": "sha512-IS7zJj36VgY+4pf8ZjsVuUWef7oTwt1y9ylvwu0aLuOn1d0fg05Om9DLm3v2GZ2Df6bhLV1kfWAM0IAl9O5rQQ==", + "license": "MIT", + "dependencies": { + "uuid": "^10.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@langchain/core": ">=0.2.31 <0.4.0" + } + }, + "node_modules/@langchain/langgraph-checkpoint/node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@langchain/langgraph-sdk": { + "version": "0.0.112", + "resolved": "https://registry.npmjs.org/@langchain/langgraph-sdk/-/langgraph-sdk-0.0.112.tgz", + "integrity": "sha512-/9W5HSWCqYgwma6EoOspL4BGYxGxeJP6lIquPSF4FA0JlKopaUv58ucZC3vAgdJyCgg6sorCIV/qg7SGpEcCLw==", + "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/langgraph/node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@langchain/openai": { + "version": "0.3.17", + "resolved": "https://registry.npmjs.org/@langchain/openai/-/openai-0.3.17.tgz", + "integrity": "sha512-uw4po32OKptVjq+CYHrumgbfh4NuD7LqyE+ZgqY9I/LrLc6bHLMc+sisHmI17vgek0K/yqtarI0alPJbzrwyag==", + "license": "MIT", + "dependencies": { + "js-tiktoken": "^1.0.12", + "openai": "^4.77.0", + "zod": "^3.22.4", + "zod-to-json-schema": "^3.22.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@langchain/core": ">=0.3.29 <0.4.0" + } + }, + "node_modules/@langchain/openai/node_modules/@types/node": { + "version": "18.19.130", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz", + "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==", + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@langchain/openai/node_modules/openai": { + "version": "4.104.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-4.104.0.tgz", + "integrity": "sha512-p99EFNsA/yX6UhVO93f5kJsDRLAg+CTA2RBqdHK4RtK8u5IJw32Hyb2dTGKbnnFmnuoBv5r7Z2CURI9sGZpSuA==", + "license": "Apache-2.0", + "dependencies": { + "@types/node": "^18.11.18", + "@types/node-fetch": "^2.6.4", + "abort-controller": "^3.0.0", + "agentkeepalive": "^4.2.1", + "form-data-encoder": "1.7.2", + "formdata-node": "^4.3.2", + "node-fetch": "^2.6.7" + }, + "bin": { + "openai": "bin/cli" + }, + "peerDependencies": { + "ws": "^8.18.0", + "zod": "^3.23.8" + }, + "peerDependenciesMeta": { + "ws": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/@langchain/openai/node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "license": "MIT" + }, + "node_modules/@langchain/textsplitters": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@langchain/textsplitters/-/textsplitters-0.1.0.tgz", + "integrity": "sha512-djI4uw9rlkAb5iMhtLED+xJebDdAG935AdP4eRTB02R7OB/act55Bj9wsskhZsvuyQRpO4O1wQOp85s6T6GWmw==", + "license": "MIT", + "dependencies": { + "js-tiktoken": "^1.0.12" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@langchain/core": ">=0.2.21 <0.4.0" + } + }, "node_modules/@mapbox/node-pre-gyp": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-2.0.3.tgz", @@ -2519,7 +2775,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/jszip": { @@ -2556,6 +2811,16 @@ "undici-types": "~7.16.0" } }, + "node_modules/@types/node-fetch": { + "version": "2.6.13", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.13.tgz", + "integrity": "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "form-data": "^4.0.4" + } + }, "node_modules/@types/prismjs": { "version": "1.26.5", "resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.5.tgz", @@ -2598,12 +2863,24 @@ "@types/react": "*" } }, + "node_modules/@types/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", + "license": "MIT" + }, "node_modules/@types/unist": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", "license": "MIT" }, + "node_modules/@types/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", + "license": "MIT" + }, "node_modules/@ungap/structured-clone": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", @@ -2854,6 +3131,18 @@ "node": ">= 14" } }, + "node_modules/agentkeepalive": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "license": "MIT", + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, "node_modules/ajv": { "version": "8.6.3", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.6.3.tgz", @@ -2871,6 +3160,18 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", @@ -2892,6 +3193,12 @@ "dev": true, "license": "MIT" }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, "node_modules/async-listen": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/async-listen/-/async-listen-3.0.0.tgz", @@ -3162,6 +3469,18 @@ "node": ">=6" } }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001762", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001762.tgz", @@ -3193,6 +3512,37 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/character-entities": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", @@ -3287,6 +3637,24 @@ "dev": true, "license": "MIT" }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -3341,6 +3709,15 @@ "node": "^14.18.0 || >=16.10.0" } }, + "node_modules/console-table-printer": { + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/console-table-printer/-/console-table-printer-2.15.0.tgz", + "integrity": "sha512-SrhBq4hYVjLCkBVOWaTzceJalvn5K1Zq5aQA6wXC/cYjI3frKWNPEMK3sZsJfNNQApvCQmgBcc13ZKmFj8qExw==", + "license": "MIT", + "dependencies": { + "simple-wcswidth": "^1.1.2" + } + }, "node_modules/convert-hrtime": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/convert-hrtime/-/convert-hrtime-3.0.0.tgz", @@ -3807,6 +4184,15 @@ } } }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/decode-named-character-reference": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.2.0.tgz", @@ -4518,6 +4904,12 @@ "node": ">=6" } }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "license": "MIT" + }, "node_modules/events": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", @@ -4663,6 +5055,12 @@ "node": ">= 6" } }, + "node_modules/form-data-encoder": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz", + "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==", + "license": "MIT" + }, "node_modules/format": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz", @@ -4671,6 +5069,19 @@ "node": ">=0.4.x" } }, + "node_modules/formdata-node": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz", + "integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==", + "license": "MIT", + "dependencies": { + "node-domexception": "1.0.0", + "web-streams-polyfill": "4.0.0-beta.3" + }, + "engines": { + "node": ">= 12.20" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -4905,6 +5316,15 @@ "integrity": "sha512-Y8T4vYhEfwJOTbouREvG+3XDsjr8E3kIr7uf+JZ0BYloFsttiHU0WfvANVsR7TxNUJa/WpCnw/Ino/p+DeBhBQ==", "license": "ISC" }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/has-property-descriptors": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", @@ -5065,6 +5485,15 @@ "node": ">= 14" } }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.0.0" + } + }, "node_modules/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", @@ -5368,12 +5797,33 @@ "jiti": "lib/jiti-cli.mjs" } }, + "node_modules/js-tiktoken": { + "version": "1.0.21", + "resolved": "https://registry.npmjs.org/js-tiktoken/-/js-tiktoken-1.0.21.tgz", + "integrity": "sha512-biOj/6M5qdgx5TKjDnFT1ymSpM5tbd3ylwDtrQvFQSu0Z7bBYko2dF+W/aUkXUPuk6IVpRxk/3Q2sHOzGlS36g==", + "license": "MIT", + "dependencies": { + "base64-js": "^1.5.1" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "license": "MIT" }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -5424,6 +5874,15 @@ "node": ">=6" } }, + "node_modules/jsonpointer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz", + "integrity": "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/jszip": { "version": "3.10.1", "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", @@ -5473,6 +5932,173 @@ "uuid": "dist/esm/bin/uuid" } }, + "node_modules/langchain": { + "version": "0.3.37", + "resolved": "https://registry.npmjs.org/langchain/-/langchain-0.3.37.tgz", + "integrity": "sha512-1jPsZ6xsxkcQPUvqRjvfuOLwZLLyt49hzcOK7OYAJovIkkOxd5gzK4Yw6giPUQ8g4XHyvULNlWBz+subdkcokw==", + "license": "MIT", + "dependencies": { + "@langchain/openai": ">=0.1.0 <0.7.0", + "@langchain/textsplitters": ">=0.0.0 <0.2.0", + "js-tiktoken": "^1.0.12", + "js-yaml": "^4.1.0", + "jsonpointer": "^5.0.1", + "langsmith": "^0.3.67", + "openapi-types": "^12.1.3", + "p-retry": "4", + "uuid": "^10.0.0", + "yaml": "^2.2.1", + "zod": "^3.25.32" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@langchain/anthropic": "*", + "@langchain/aws": "*", + "@langchain/cerebras": "*", + "@langchain/cohere": "*", + "@langchain/core": ">=0.3.58 <0.4.0", + "@langchain/deepseek": "*", + "@langchain/google-genai": "*", + "@langchain/google-vertexai": "*", + "@langchain/google-vertexai-web": "*", + "@langchain/groq": "*", + "@langchain/mistralai": "*", + "@langchain/ollama": "*", + "@langchain/xai": "*", + "axios": "*", + "cheerio": "*", + "handlebars": "^4.7.8", + "peggy": "^3.0.2", + "typeorm": "*" + }, + "peerDependenciesMeta": { + "@langchain/anthropic": { + "optional": true + }, + "@langchain/aws": { + "optional": true + }, + "@langchain/cerebras": { + "optional": true + }, + "@langchain/cohere": { + "optional": true + }, + "@langchain/deepseek": { + "optional": true + }, + "@langchain/google-genai": { + "optional": true + }, + "@langchain/google-vertexai": { + "optional": true + }, + "@langchain/google-vertexai-web": { + "optional": true + }, + "@langchain/groq": { + "optional": true + }, + "@langchain/mistralai": { + "optional": true + }, + "@langchain/ollama": { + "optional": true + }, + "@langchain/xai": { + "optional": true + }, + "axios": { + "optional": true + }, + "cheerio": { + "optional": true + }, + "handlebars": { + "optional": true + }, + "peggy": { + "optional": true + }, + "typeorm": { + "optional": true + } + } + }, + "node_modules/langchain/node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/langsmith": { + "version": "0.3.87", + "resolved": "https://registry.npmjs.org/langsmith/-/langsmith-0.3.87.tgz", + "integrity": "sha512-XXR1+9INH8YX96FKWc5tie0QixWz6tOqAsAKfcJyPkE0xPep+NDz0IQLR32q4bn10QK3LqD2HN6T3n6z1YLW7Q==", + "license": "MIT", + "dependencies": { + "@types/uuid": "^10.0.0", + "chalk": "^4.1.2", + "console-table-printer": "^2.12.1", + "p-queue": "^6.6.2", + "semver": "^7.6.3", + "uuid": "^10.0.0" + }, + "peerDependencies": { + "@opentelemetry/api": "*", + "@opentelemetry/exporter-trace-otlp-proto": "*", + "@opentelemetry/sdk-trace-base": "*", + "openai": "*" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@opentelemetry/exporter-trace-otlp-proto": { + "optional": true + }, + "@opentelemetry/sdk-trace-base": { + "optional": true + }, + "openai": { + "optional": true + } + } + }, + "node_modules/langsmith/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/langsmith/node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/lie": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", @@ -6564,6 +7190,15 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/mustache": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz", + "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", + "license": "MIT", + "bin": { + "mustache": "bin/mustache" + } + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -6582,11 +7217,30 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, "node_modules/node-fetch": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.9.tgz", "integrity": "sha512-DJm/CJkZkRjKKj4Zi4BsKVZh3ValV5IR5s7LVZnW+6YMh0W1BfNA8XSs6DLMGYlId5F3KnA70uu2qepcR08Qqg==", - "dev": true, "license": "MIT", "dependencies": { "whatwg-url": "^5.0.0" @@ -6715,6 +7369,21 @@ "integrity": "sha512-vDJMkfCfb0b1A836rgHj+ORuZf4B4+cc2bASQtpeoJLueuFc5DuYwjIZUBrSvx/fO5IrLjLz+oTrB3pcGlhovQ==", "license": "MIT" }, + "node_modules/openapi-types": { + "version": "12.1.3", + "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz", + "integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==", + "license": "MIT" + }, + "node_modules/p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/p-map": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.4.tgz", @@ -6728,6 +7397,47 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-queue": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-6.6.2.tgz", + "integrity": "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==", + "license": "MIT", + "dependencies": { + "eventemitter3": "^4.0.4", + "p-timeout": "^3.2.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-retry": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", + "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", + "license": "MIT", + "dependencies": { + "@types/retry": "0.12.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-timeout": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz", + "integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==", + "license": "MIT", + "dependencies": { + "p-finally": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/pako": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", @@ -7169,6 +7879,15 @@ "node": ">=8" } }, + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/reusify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", @@ -7513,6 +8232,12 @@ "simple-concat": "^1.0.0" } }, + "node_modules/simple-wcswidth": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/simple-wcswidth/-/simple-wcswidth-1.1.2.tgz", + "integrity": "sha512-j7piyCjAeTDSjzTSQ7DokZtMNwNlEAyxqSZeCS+CXH7fJ4jx3FuJ/mTW3mE+6JLs4VJBbcll0Kjn+KXI5t21Iw==", + "license": "MIT" + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -7579,6 +8304,18 @@ "inline-style-parser": "0.2.7" } }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/tailwindcss": { "version": "4.1.18", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz", @@ -7771,7 +8508,6 @@ "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "dev": true, "license": "MIT" }, "node_modules/trim-lines": { @@ -8234,6 +8970,15 @@ "vite": "^2 || ^3 || ^4 || ^5 || ^6 || ^7" } }, + "node_modules/web-streams-polyfill": { + "version": "4.0.0-beta.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz", + "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/web-tree-sitter": { "version": "0.20.8", "resolved": "https://registry.npmjs.org/web-tree-sitter/-/web-tree-sitter-0.20.8.tgz", @@ -8244,14 +8989,12 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "dev": true, "license": "BSD-2-Clause" }, "node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "dev": true, "license": "MIT", "dependencies": { "tr46": "~0.0.3", @@ -8292,6 +9035,21 @@ "dev": true, "license": "ISC" }, + "node_modules/yaml": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", + "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, "node_modules/yn": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", @@ -8303,14 +9061,23 @@ } }, "node_modules/zod": { - "version": "4.1.13", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.13.tgz", - "integrity": "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==", + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" } }, + "node_modules/zod-to-json-schema": { + "version": "3.25.1", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.1.tgz", + "integrity": "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.25 || ^4" + } + }, "node_modules/zwitch": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", diff --git a/package.json b/package.json index 79910530e..389cb37c2 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,10 @@ "dependencies": { "@huggingface/transformers": "^3.0.0", "@isomorphic-git/lightning-fs": "^4.6.2", + "@langchain/core": "^0.3.0", + "@langchain/google-genai": "^0.1.0", + "@langchain/langgraph": "^0.2.74", + "@langchain/openai": "^0.3.0", "@sigma/edge-curve": "^3.1.0", "@tailwindcss/vite": "^4.1.18", "axios": "^1.13.2", @@ -24,6 +28,7 @@ "isomorphic-git": "^1.36.1", "jszip": "^3.10.1", "kuzu-wasm": "^0.11.1", + "langchain": "^0.3.37", "lru-cache": "^11.2.4", "lucide-react": "^0.562.0", "react": "^18.3.1", @@ -36,7 +41,7 @@ "vite-plugin-top-level-await": "^1.6.0", "vite-plugin-wasm": "^3.5.0", "web-tree-sitter": "^0.20.8", - "zod": "^4.1.13" + "zod": "^3.25.76" }, "devDependencies": { "@babel/types": "^7.28.5", diff --git a/src/App.tsx b/src/App.tsx index 7e75d40f4..b88a0fec0 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -5,6 +5,7 @@ import { LoadingOverlay } from './components/LoadingOverlay'; import { Header } from './components/Header'; import { GraphCanvas, GraphCanvasHandle } from './components/GraphCanvas'; import { RightPanel } from './components/RightPanel'; +import { SettingsPanel } from './components/SettingsPanel'; import { StatusBar } from './components/StatusBar'; import { FileTreePanel } from './components/FileTreePanel'; import { FileEntry } from './services/zip'; @@ -21,6 +22,10 @@ const AppContent = () => { isRightPanelOpen, runPipeline, runPipelineFromFiles, + isSettingsPanelOpen, + setSettingsPanelOpen, + refreshLLMSettings, + initializeAgent, } = useAppState(); const graphCanvasRef = useRef(null); @@ -88,6 +93,13 @@ const AppContent = () => { graphCanvasRef.current?.focusNode(nodeId); }, []); + // Handle settings saved - refresh and reinitialize agent + // NOTE: Must be defined BEFORE any conditional returns (React hooks rule) + const handleSettingsSaved = useCallback(() => { + refreshLLMSettings(); + initializeAgent(); + }, [refreshLLMSettings, initializeAgent]); + // Render based on view mode if (viewMode === 'onboarding') { return ; @@ -116,6 +128,13 @@ const AppContent = () => { + + {/* Settings Panel (modal) */} + setSettingsPanelOpen(false)} + onSettingsSaved={handleSettingsSaved} + /> ); }; diff --git a/src/components/DropZone.tsx b/src/components/DropZone.tsx index b89dbdfbe..c2141f61a 100644 --- a/src/components/DropZone.tsx +++ b/src/components/DropZone.tsx @@ -209,9 +209,6 @@ export const DropZone = ({ onFileSelect, onGitClone }: DropZoneProps) => { .zip - - up to 50MB - )} diff --git a/src/components/RightPanel.tsx b/src/components/RightPanel.tsx index 61539dd9d..4c0181319 100644 --- a/src/components/RightPanel.tsx +++ b/src/components/RightPanel.tsx @@ -1,7 +1,11 @@ import { useState, useMemo } from 'react'; -import { X, Send, Sparkles, User, FileCode, Hash, GitBranch, Code, MessageSquare, PanelRightClose } from 'lucide-react'; +import { + X, Send, Sparkles, User, FileCode, Hash, GitBranch, Code, MessageSquare, + PanelRightClose, Loader2, Settings, AlertTriangle +} from 'lucide-react'; import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism'; +import ReactMarkdown from 'react-markdown'; import { useAppState } from '../hooks/useAppState'; import { NODE_COLORS } from '../lib/constants'; @@ -23,13 +27,6 @@ const customTheme = { }, }; -// Chat message interface -interface Message { - id: string; - role: 'user' | 'assistant'; - content: string; -} - export const RightPanel = () => { const { selectedNode, @@ -40,10 +37,18 @@ export const RightPanel = () => { setRightPanelOpen, rightPanelTab, setRightPanelTab, + // LLM / chat state + chatMessages, + isChatLoading, + currentToolCalls, + agentError, + isAgentReady, + isAgentInitializing, + setSettingsPanelOpen, + sendChatMessage, + clearChat, } = useAppState(); - // Chat state - const [messages, setMessages] = useState([]); const [chatInput, setChatInput] = useState(''); // Get source code for selected node @@ -90,27 +95,11 @@ export const RightPanel = () => { }, [selectedNode, graph]); // Chat handlers - const handleSendMessage = () => { + const handleSendMessage = async () => { if (!chatInput.trim()) return; - - const userMessage: Message = { - id: Date.now().toString(), - role: 'user', - content: chatInput.trim(), - }; - - setMessages(prev => [...prev, userMessage]); + const text = chatInput.trim(); setChatInput(''); - - // Simulate AI response - setTimeout(() => { - const aiMessage: Message = { - id: (Date.now() + 1).toString(), - role: 'assistant', - content: 'This is a placeholder response. AI integration coming soon! I will be able to help you understand the codebase, find specific functions, and explain how different parts connect.', - }; - setMessages(prev => [...prev, aiMessage]); - }, 500); + await sendChatMessage(text); }; const handleKeyDown = (e: React.KeyboardEvent) => { @@ -289,11 +278,54 @@ export const RightPanel = () => { Nexus AI • Ask about the codebase +
+ {!isAgentReady && ( + + Configure AI + + )} + {isAgentInitializing && ( + + Connecting + + )} + +
+ {/* Status / errors */} + {agentError && ( +
+ + {agentError} +
+ )} + + {/* Tool call indicator */} + {currentToolCalls.length > 0 && ( +
+ {currentToolCalls.map(tc => ( + + + {tc.name} + ({tc.status}) + + ))} +
+ )} + {/* Messages */}
- {messages.length === 0 ? ( + {chatMessages.length === 0 ? (
🧠 @@ -318,7 +350,7 @@ export const RightPanel = () => {
) : (
- {messages.map((message) => ( + {chatMessages.map((message) => (
{
- {message.content} + {message.role === 'assistant' ? ( + { + const isInline = !className; + return isInline ? ( + + {children} + + ) : ( + + {children} + + ); + }, + pre: ({ children }) => <>{children}, + }} + > + {message.content} + + ) : ( + message.content + )} + {message.toolCalls && message.toolCalls.length > 0 && ( +
+ {message.toolCalls.map(tc => ( +
+ + {tc.name} + {tc.result && → {tc.result}} +
+ ))} +
+ )}
))} @@ -363,13 +428,26 @@ export const RightPanel = () => { className="flex-1 bg-transparent border-none outline-none text-sm text-text-primary placeholder:text-text-muted resize-none max-h-24" /> +
+ {!isAgentReady && !isAgentInitializing && ( +
+ + Configure an LLM provider to enable chat. +
+ )}
)} diff --git a/src/components/SettingsPanel.tsx b/src/components/SettingsPanel.tsx new file mode 100644 index 000000000..7663dcb01 --- /dev/null +++ b/src/components/SettingsPanel.tsx @@ -0,0 +1,489 @@ +import { useState, useEffect, useCallback } from 'react'; +import { X, Key, Server, Brain, Check, AlertCircle, Eye, EyeOff, RefreshCw, Loader2 } from 'lucide-react'; +import { + loadSettings, + saveSettings, + getProviderDisplayName, + getAvailableModels, +} from '../core/llm/settings-service'; +import type { LLMSettings, LLMProvider } from '../core/llm/types'; + +interface SettingsPanelProps { + isOpen: boolean; + onClose: () => void; + onSettingsSaved?: () => void; +} + +/** + * Fetch available Gemini models from the API + */ +const fetchGeminiModels = async (apiKey: string): Promise => { + try { + const response = await fetch( + `https://generativelanguage.googleapis.com/v1beta/models?key=${apiKey}` + ); + + if (!response.ok) { + throw new Error(`API error: ${response.status}`); + } + + const data = await response.json(); + + // Filter for chat-capable models and extract model names + const models = (data.models || []) + .filter((model: any) => { + const methods = model.supportedGenerationMethods || []; + return methods.includes('generateContent'); + }) + .map((model: any) => { + const name = model.name || ''; + return name.replace('models/', ''); + }) + .filter((name: string) => name.length > 0) + .sort((a: string, b: string) => { + const score = (n: string) => { + if (n.includes('gemini-2')) return 0; + if (n.includes('gemini-1.5')) return 1; + if (n.includes('gemini-1')) return 2; + return 3; + }; + return score(a) - score(b) || a.localeCompare(b); + }); + + return models; + } catch (error) { + console.warn('Failed to fetch Gemini models:', error); + return []; + } +}; + +export const SettingsPanel = ({ isOpen, onClose, onSettingsSaved }: SettingsPanelProps) => { + const [settings, setSettings] = useState(loadSettings); + const [showApiKey, setShowApiKey] = useState>({}); + const [saveStatus, setSaveStatus] = useState<'idle' | 'saved' | 'error'>('idle'); + + // Gemini model fetching state + const [geminiModels, setGeminiModels] = useState([]); + const [isLoadingModels, setIsLoadingModels] = useState(false); + const [modelFetchError, setModelFetchError] = useState(null); + const [useCustomModel, setUseCustomModel] = useState(false); + + // Load settings when panel opens + useEffect(() => { + if (isOpen) { + setSettings(loadSettings()); + setSaveStatus('idle'); + setGeminiModels([]); + setModelFetchError(null); + setUseCustomModel(false); + } + }, [isOpen]); + + // Auto-fetch models when Gemini API key changes + const fetchModels = useCallback(async (apiKey: string) => { + if (!apiKey || apiKey.length < 10) { + setGeminiModels([]); + return; + } + + setIsLoadingModels(true); + setModelFetchError(null); + + const models = await fetchGeminiModels(apiKey); + + setIsLoadingModels(false); + + if (models.length > 0) { + setGeminiModels(models); + setModelFetchError(null); + } else { + setGeminiModels([]); + setModelFetchError('Could not fetch models. Check your API key or enter model manually.'); + } + }, []); + + // Fetch models when API key is entered (debounced) + useEffect(() => { + if (settings.activeProvider === 'gemini' && settings.gemini?.apiKey) { + const timer = setTimeout(() => { + fetchModels(settings.gemini!.apiKey); + }, 500); + return () => clearTimeout(timer); + } + }, [settings.gemini?.apiKey, settings.activeProvider, fetchModels]); + + const handleProviderChange = (provider: LLMProvider) => { + setSettings(prev => ({ ...prev, activeProvider: provider })); + }; + + const handleSave = () => { + try { + saveSettings(settings); + setSaveStatus('saved'); + onSettingsSaved?.(); + setTimeout(() => setSaveStatus('idle'), 2000); + } catch { + setSaveStatus('error'); + } + }; + + const toggleApiKeyVisibility = (key: string) => { + setShowApiKey(prev => ({ ...prev, [key]: !prev[key] })); + }; + + if (!isOpen) return null; + + const providers: LLMProvider[] = ['gemini', 'azure-openai']; + + const availableGeminiModels = geminiModels.length > 0 + ? geminiModels + : getAvailableModels('gemini'); + const currentGeminiModel = settings.gemini?.model ?? 'gemini-2.0-flash'; + const isCustomModelSelected = !availableGeminiModels.includes(currentGeminiModel); + + return ( +
+ {/* Backdrop */} +
+ + {/* Panel */} +
+ {/* Header */} +
+
+
+ +
+
+

AI Settings

+

Configure your LLM provider

+
+
+ +
+ + {/* Content */} +
+ {/* Provider Selection */} +
+ +
+ {providers.map(provider => ( + + ))} +
+
+ + {/* Gemini Settings */} + {settings.activeProvider === 'gemini' && ( +
+
+ +
+ setSettings(prev => ({ + ...prev, + gemini: { ...prev.gemini!, apiKey: e.target.value } + }))} + placeholder="Enter your Google AI API key" + className="w-full px-4 py-3 pr-12 bg-elevated border border-border-subtle rounded-xl text-text-primary placeholder:text-text-muted focus:border-accent focus:ring-2 focus:ring-accent/20 outline-none transition-all" + /> + +
+

+ Get your API key from{' '} + + Google AI Studio + +

+
+ +
+
+ +
+ {isLoadingModels && ( + + + Fetching models... + + )} + {geminiModels.length > 0 && ( + + {geminiModels.length} models available + + )} + {settings.gemini?.apiKey && !isLoadingModels && ( + + )} +
+
+ + {/* Model selector or manual input */} + {(useCustomModel || isCustomModelSelected) ? ( +
+ setSettings(prev => ({ + ...prev, + gemini: { ...prev.gemini!, model: e.target.value } + }))} + placeholder="e.g., gemini-2.0-flash-exp" + className="w-full px-4 py-3 bg-elevated border border-border-subtle rounded-xl text-text-primary placeholder:text-text-muted focus:border-accent focus:ring-2 focus:ring-accent/20 outline-none transition-all font-mono text-sm" + /> + +
+ ) : ( +
+ + +
+ )} + + {modelFetchError && !useCustomModel && ( +

+ + {modelFetchError} +

+ )} +
+
+ )} + + {/* Azure OpenAI Settings */} + {settings.activeProvider === 'azure-openai' && ( +
+
+ +
+ setSettings(prev => ({ + ...prev, + azureOpenAI: { ...prev.azureOpenAI!, apiKey: e.target.value } + }))} + placeholder="Enter your Azure OpenAI API key" + className="w-full px-4 py-3 pr-12 bg-elevated border border-border-subtle rounded-xl text-text-primary placeholder:text-text-muted focus:border-accent focus:ring-2 focus:ring-accent/20 outline-none transition-all" + /> + +
+
+ +
+ + setSettings(prev => ({ + ...prev, + azureOpenAI: { ...prev.azureOpenAI!, endpoint: e.target.value } + }))} + placeholder="https://your-resource.openai.azure.com" + className="w-full px-4 py-3 bg-elevated border border-border-subtle rounded-xl text-text-primary placeholder:text-text-muted focus:border-accent focus:ring-2 focus:ring-accent/20 outline-none transition-all" + /> +
+ +
+ + setSettings(prev => ({ + ...prev, + azureOpenAI: { ...prev.azureOpenAI!, deploymentName: e.target.value } + }))} + placeholder="e.g., gpt-4o-deployment" + className="w-full px-4 py-3 bg-elevated border border-border-subtle rounded-xl text-text-primary placeholder:text-text-muted focus:border-accent focus:ring-2 focus:ring-accent/20 outline-none transition-all" + /> +
+ +
+
+ + setSettings(prev => ({ + ...prev, + azureOpenAI: { ...prev.azureOpenAI!, model: e.target.value } + }))} + placeholder="gpt-4o" + className="w-full px-4 py-3 bg-elevated border border-border-subtle rounded-xl text-text-primary placeholder:text-text-muted focus:border-accent focus:ring-2 focus:ring-accent/20 outline-none transition-all" + /> +
+ +
+ + setSettings(prev => ({ + ...prev, + azureOpenAI: { ...prev.azureOpenAI!, apiVersion: e.target.value } + }))} + placeholder="2024-08-01-preview" + className="w-full px-4 py-3 bg-elevated border border-border-subtle rounded-xl text-text-primary placeholder:text-text-muted focus:border-accent focus:ring-2 focus:ring-accent/20 outline-none transition-all" + /> +
+
+ +

+ Configure your Azure OpenAI service in the{' '} + + Azure Portal + +

+
+ )} + + {/* Privacy Note */} +
+
+
+ 🔒 +
+
+ Privacy: Your API keys are stored only in your browser's local storage. + They're sent directly to the LLM provider when you chat. Your code never leaves your machine. +
+
+
+
+ + {/* Footer */} +
+
+ {saveStatus === 'saved' && ( + + + Settings saved + + )} + {saveStatus === 'error' && ( + + + Failed to save + + )} +
+
+ + +
+
+
+
+ ); +}; + diff --git a/src/core/llm/agent.ts b/src/core/llm/agent.ts new file mode 100644 index 000000000..12cfbf8be --- /dev/null +++ b/src/core/llm/agent.ts @@ -0,0 +1,235 @@ +/** + * Graph RAG Agent Factory + * + * Creates a LangChain agent configured for code graph analysis. + * Supports Azure OpenAI and Google Gemini providers. + */ + +import { createReactAgent } from '@langchain/langgraph/prebuilt'; +import { SystemMessage } from '@langchain/core/messages'; +import { AzureChatOpenAI } from '@langchain/openai'; +import { ChatGoogleGenerativeAI } from '@langchain/google-genai'; +import type { BaseChatModel } from '@langchain/core/language_models/chat_models'; +import { createGraphRAGTools } from './tools'; +import type { + ProviderConfig, + AzureOpenAIConfig, + GeminiConfig, + AgentStreamChunk, +} from './types'; + +/** + * System prompt for the Graph RAG agent + */ +const SYSTEM_PROMPT = `You are Nexus AI, an intelligent code analysis assistant. You help developers understand codebases by querying a knowledge graph that contains code structure, relationships, and semantic embeddings. + +CAPABILITIES: +- Execute Cypher queries to explore code structure (functions, classes, files, imports, call graphs) +- Perform semantic search to find code by meaning (when embeddings are available) +- Trace dependencies and relationships between code elements +- Explain code architecture and patterns + +APPROACH: +1. Start by understanding what the user wants to know +2. Choose the right tool(s) for the task: + - Use 'get_codebase_stats' first if you need an overview + - Use 'semantic_search' for concept-based queries ("find authentication logic") + - Use 'execute_cypher' for structural queries ("what functions does X call?") + - Use 'get_code_content' to show actual source code +3. Interpret results and explain them clearly +4. Suggest follow-up explorations when relevant + +STYLE: +- Be concise but thorough +- Use code formatting when showing results +- Explain technical concepts when helpful +- If a query fails, explain why and suggest alternatives + +LIMITATIONS: +- You can only see code that's been indexed in the knowledge graph +- Semantic search requires embeddings to be generated first +- Large codebases may require more specific queries + +When showing code or query results, format them nicely using markdown.`; + +/** + * Create a chat model instance from provider configuration + */ +export const createChatModel = (config: ProviderConfig): BaseChatModel => { + switch (config.provider) { + case 'azure-openai': { + const azureConfig = config as AzureOpenAIConfig; + return new AzureChatOpenAI({ + azureOpenAIApiKey: azureConfig.apiKey, + azureOpenAIApiInstanceName: extractInstanceName(azureConfig.endpoint), + azureOpenAIApiDeploymentName: azureConfig.deploymentName, + azureOpenAIApiVersion: azureConfig.apiVersion ?? '2024-08-01-preview', + temperature: azureConfig.temperature ?? 0.1, + maxTokens: azureConfig.maxTokens, + streaming: true, + }); + } + + case 'gemini': { + const geminiConfig = config as GeminiConfig; + return new ChatGoogleGenerativeAI({ + apiKey: geminiConfig.apiKey, + model: geminiConfig.model, + temperature: geminiConfig.temperature ?? 0.1, + maxOutputTokens: geminiConfig.maxTokens, + streaming: true, + }); + } + + default: + throw new Error(`Unsupported provider: ${(config as any).provider}`); + } +}; + +/** + * Extract instance name from Azure endpoint URL + * e.g., "https://my-resource.openai.azure.com" -> "my-resource" + */ +const extractInstanceName = (endpoint: string): string => { + try { + const url = new URL(endpoint); + const hostname = url.hostname; + // Extract the first part before .openai.azure.com + const match = hostname.match(/^([^.]+)\.openai\.azure\.com/); + if (match) { + return match[1]; + } + // Fallback: just use the first part of hostname + return hostname.split('.')[0]; + } catch { + return endpoint; + } +}; + +/** + * Create a Graph RAG agent + */ +export const createGraphRAGAgent = ( + config: ProviderConfig, + executeQuery: (cypher: string) => Promise, + semanticSearch: (query: string, k?: number, maxDistance?: number) => Promise, + semanticSearchWithContext: (query: string, k?: number, hops?: number) => Promise, + isEmbeddingReady: () => boolean +) => { + const model = createChatModel(config); + const tools = createGraphRAGTools( + executeQuery, + semanticSearch, + semanticSearchWithContext, + isEmbeddingReady + ); + + const agent = createReactAgent({ + llm: model as any, + tools: tools as any, + messageModifier: new SystemMessage(SYSTEM_PROMPT) as any, + }); + + return agent; +}; + +/** + * Message type for agent conversation + */ +export interface AgentMessage { + role: 'user' | 'assistant'; + content: string; +} + +/** + * Stream a response from the agent + * Yields chunks for real-time UI updates + */ +export async function* streamAgentResponse( + agent: ReturnType, + messages: AgentMessage[] +): AsyncGenerator { + try { + const formattedMessages = messages.map(m => ({ + role: m.role, + content: m.content, + })); + + // In LangGraph, we stream events to get granular updates + const stream = await agent.streamEvents( + { messages: formattedMessages }, + { version: 'v2' } + ); + + for await (const event of stream) { + const { event: eventType, data } = event; + + // Handle tool calls start + if (eventType === 'on_tool_start') { + yield { + type: 'tool_call', + toolCall: { + id: data.tool_call_id || Date.now().toString(), // fallback if ID missing + name: event.name, + args: data.input, + status: 'running', + }, + }; + } + + // Handle tool output + if (eventType === 'on_tool_end') { + yield { + type: 'tool_result', + toolCall: { + id: data.tool_call_id || '', // we might need to match by name if ID missing + name: event.name, + args: {}, + result: typeof data.output === 'string' ? data.output : JSON.stringify(data.output), + status: 'completed', + }, + }; + } + + // Handle streamed LLM content + if (eventType === 'on_chat_model_stream') { + const content = data.chunk?.content; + if (content && typeof content === 'string') { + yield { + type: 'content', + content: content, + }; + } + } + } + + yield { type: 'done' }; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + yield { + type: 'error', + error: message, + }; + } +} + +/** + * Get a non-streaming response from the agent + * Simpler for cases where streaming isn't needed + */ +export const invokeAgent = async ( + agent: ReturnType, + messages: AgentMessage[] +): Promise => { + const formattedMessages = messages.map(m => ({ + role: m.role, + content: m.content, + })); + + const result = await agent.invoke({ messages: formattedMessages }); + + // result.messages is the full conversation state + const lastMessage = result.messages[result.messages.length - 1]; + return lastMessage?.content?.toString() ?? 'No response generated.'; +}; + diff --git a/src/core/llm/index.ts b/src/core/llm/index.ts new file mode 100644 index 000000000..ed1de4b87 --- /dev/null +++ b/src/core/llm/index.ts @@ -0,0 +1,34 @@ +/** + * LLM Module Exports + * + * Provides Graph RAG agent capabilities for code analysis. + */ + +// Types +export * from './types'; + +// Settings management +export { + loadSettings, + saveSettings, + updateProviderSettings, + setActiveProvider, + getActiveProviderConfig, + isProviderConfigured, + clearSettings, + getProviderDisplayName, + getAvailableModels, +} from './settings-service'; + +// Tools +export { createGraphRAGTools } from './tools'; + +// Agent +export { + createChatModel, + createGraphRAGAgent, + streamAgentResponse, + invokeAgent, + type AgentMessage, +} from './agent'; + diff --git a/src/core/llm/settings-service.ts b/src/core/llm/settings-service.ts new file mode 100644 index 000000000..aba674890 --- /dev/null +++ b/src/core/llm/settings-service.ts @@ -0,0 +1,189 @@ +/** + * Settings Service + * + * Handles localStorage persistence for LLM provider settings. + * All API keys are stored locally - never sent to any server except the LLM provider. + */ + +import { + LLMSettings, + DEFAULT_LLM_SETTINGS, + LLMProvider, + AzureOpenAIConfig, + GeminiConfig, + OllamaConfig, + ProviderConfig, +} from './types'; + +const STORAGE_KEY = 'gitnexus-llm-settings'; + +/** + * Load settings from localStorage + */ +export const loadSettings = (): LLMSettings => { + try { + const stored = localStorage.getItem(STORAGE_KEY); + if (!stored) { + return DEFAULT_LLM_SETTINGS; + } + + const parsed = JSON.parse(stored) as Partial; + + // Merge with defaults to handle new fields + return { + ...DEFAULT_LLM_SETTINGS, + ...parsed, + azureOpenAI: { + ...DEFAULT_LLM_SETTINGS.azureOpenAI, + ...parsed.azureOpenAI, + }, + gemini: { + ...DEFAULT_LLM_SETTINGS.gemini, + ...parsed.gemini, + }, + ollama: { + ...DEFAULT_LLM_SETTINGS.ollama, + ...parsed.ollama, + }, + }; + } catch (error) { + console.warn('Failed to load LLM settings:', error); + return DEFAULT_LLM_SETTINGS; + } +}; + +/** + * Save settings to localStorage + */ +export const saveSettings = (settings: LLMSettings): void => { + try { + localStorage.setItem(STORAGE_KEY, JSON.stringify(settings)); + } catch (error) { + console.error('Failed to save LLM settings:', error); + } +}; + +/** + * Update a specific provider's settings + */ +export const updateProviderSettings = ( + provider: T, + updates: Partial< + T extends 'azure-openai' ? Omit : + T extends 'gemini' ? Omit : + T extends 'ollama' ? Omit : + never + > +): LLMSettings => { + const current = loadSettings(); + + const providerKey = provider === 'azure-openai' ? 'azureOpenAI' : provider; + + const updated: LLMSettings = { + ...current, + [providerKey]: { + ...current[providerKey as keyof LLMSettings], + ...updates, + }, + }; + + saveSettings(updated); + return updated; +}; + +/** + * Set the active provider + */ +export const setActiveProvider = (provider: LLMProvider): LLMSettings => { + const current = loadSettings(); + const updated: LLMSettings = { + ...current, + activeProvider: provider, + }; + saveSettings(updated); + return updated; +}; + +/** + * Get the current provider configuration + */ +export const getActiveProviderConfig = (): ProviderConfig | null => { + const settings = loadSettings(); + + switch (settings.activeProvider) { + case 'azure-openai': + if (!settings.azureOpenAI?.apiKey || !settings.azureOpenAI?.endpoint) { + return null; + } + return { + provider: 'azure-openai', + ...settings.azureOpenAI, + } as AzureOpenAIConfig; + + case 'gemini': + if (!settings.gemini?.apiKey) { + return null; + } + return { + provider: 'gemini', + ...settings.gemini, + } as GeminiConfig; + + case 'ollama': + return { + provider: 'ollama', + ...settings.ollama, + } as OllamaConfig; + + default: + return null; + } +}; + +/** + * Check if the active provider is properly configured + */ +export const isProviderConfigured = (): boolean => { + return getActiveProviderConfig() !== null; +}; + +/** + * Clear all settings (reset to defaults) + */ +export const clearSettings = (): void => { + localStorage.removeItem(STORAGE_KEY); +}; + +/** + * Get display name for a provider + */ +export const getProviderDisplayName = (provider: LLMProvider): string => { + switch (provider) { + case 'azure-openai': + return 'Azure OpenAI'; + case 'gemini': + return 'Google Gemini'; + case 'ollama': + return 'Ollama (Local)'; + default: + return provider; + } +}; + +/** + * Get available models for a provider + */ +export const getAvailableModels = (provider: LLMProvider): string[] => { + switch (provider) { + case 'azure-openai': + // Azure models depend on deployment, so we show common ones + return ['gpt-4o', 'gpt-4o-mini', 'gpt-4-turbo', 'gpt-4', 'gpt-35-turbo']; + case 'gemini': + return ['gemini-2.0-flash', 'gemini-1.5-pro', 'gemini-1.5-flash', 'gemini-1.0-pro']; + case 'ollama': + return ['llama3.2', 'llama3.1', 'mistral', 'codellama', 'deepseek-coder']; + default: + return []; + } +}; + diff --git a/src/core/llm/tools.ts b/src/core/llm/tools.ts new file mode 100644 index 000000000..c72d5fc88 --- /dev/null +++ b/src/core/llm/tools.ts @@ -0,0 +1,267 @@ +/** + * Graph RAG Tools for LangChain Agent + * + * Custom tools that allow the agent to interact with the KuzuDB graph database + * for code analysis, semantic search, and graph traversal. + */ + +import { tool } from '@langchain/core/tools'; +import { z } from 'zod'; +import { GRAPH_SCHEMA_DESCRIPTION } from './types'; + +/** + * Tool factory - creates tools bound to the KuzuDB query functions + * This is needed because the tools run in the worker and need access to the adapter + */ +export const createGraphRAGTools = ( + executeQuery: (cypher: string) => Promise, + semanticSearch: (query: string, k?: number, maxDistance?: number) => Promise, + semanticSearchWithContext: (query: string, k?: number, hops?: number) => Promise, + isEmbeddingReady: () => boolean +) => { + /** + * Tool: Execute Cypher Query + * Allows the agent to run arbitrary Cypher queries against the graph + */ + const executeCypherTool = tool( + async ({ query }: { query: string }) => { + try { + const results = await executeQuery(query); + + if (results.length === 0) { + return 'Query returned no results.'; + } + + // Format results nicely for the LLM + const formatted = results.slice(0, 50).map((row, i) => { + // Handle both object and array results + if (Array.isArray(row)) { + return `[${i + 1}] ${row.join(', ')}`; + } + return `[${i + 1}] ${JSON.stringify(row)}`; + }); + + const resultText = formatted.join('\n'); + const truncated = results.length > 50 ? `\n... (${results.length - 50} more results truncated)` : ''; + + return `Query returned ${results.length} results:\n${resultText}${truncated}`; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + return `Cypher query error: ${message}\n\nPlease check your query syntax and try again.`; + } + }, + { + name: 'execute_cypher', + description: 'Execute a Cypher query against the code knowledge graph. Use this for structural queries like finding functions, tracing call graphs, or analyzing imports. Call get_graph_schema first if you need to see the database schema.', + schema: z.object({ + query: z.string().describe('The Cypher query to execute. Must be valid KuzuDB Cypher syntax.'), + }), + } + ); + + /** + * Tool: Semantic Code Search + * Find code by meaning using vector embeddings + */ + const semanticSearchTool = tool( + async ({ query, limit }: { query: string; limit?: number }) => { + if (!isEmbeddingReady()) { + return 'Semantic search is not available. Embeddings have not been generated yet. Please use execute_cypher tool for structured queries instead.'; + } + + try { + const results = await semanticSearch(query, limit ?? 10, 0.5); + + if (results.length === 0) { + return `No code found matching "${query}". Try a different search term or use execute_cypher for structured queries.`; + } + + const formatted = results.map((r, i) => { + const location = r.startLine ? ` (lines ${r.startLine}-${r.endLine})` : ''; + return `[${i + 1}] ${r.label}: ${r.name}\n File: ${r.filePath}${location}\n Relevance: ${(1 - r.distance).toFixed(2)}`; + }); + + return `Found ${results.length} semantically similar code elements:\n\n${formatted.join('\n\n')}`; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + return `Semantic search error: ${message}`; + } + }, + { + name: 'semantic_search', + description: 'Search for code by meaning using semantic similarity. Good for finding code related to a concept even if exact terms are not used.', + schema: z.object({ + query: z.string().describe('Natural language description of what you are looking for'), + limit: z.number().optional().describe('Maximum number of results to return (default: 10)'), + }), + } + ); + + /** + * Tool: Semantic Search with Graph Context + * Find similar code AND expand to connected nodes + */ + const semanticSearchWithContextTool = tool( + async ({ query, limit, hops }: { query: string; limit?: number; hops?: number }) => { + if (!isEmbeddingReady()) { + return 'Semantic search is not available. Embeddings have not been generated yet. Please use execute_cypher tool for structured queries instead.'; + } + + try { + const results = await semanticSearchWithContext(query, limit ?? 5, hops ?? 2); + + if (results.length === 0) { + return `No code found matching "${query}". Try a different search term.`; + } + + const formatted = results.map((r, i) => { + const matchName = r.matchName ?? r[1]; + const matchLabel = r.matchLabel ?? r[2]; + const matchPath = r.matchPath ?? r[3]; + const distance = r.distance ?? r[4]; + const connections = r.connections ?? r[5] ?? []; + + const connectedNames = Array.isArray(connections) + ? connections.slice(0, 10).map((c: any) => c.name || c).join(', ') + : 'none'; + + return `[${i + 1}] ${matchLabel}: ${matchName}\n File: ${matchPath}\n Relevance: ${(1 - distance).toFixed(2)}\n Connected to: ${connectedNames}`; + }); + + return `Found ${results.length} code elements with context:\n\n${formatted.join('\n\n')}`; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + return `Search with context error: ${message}`; + } + }, + { + name: 'semantic_search_with_context', + description: 'Search for code semantically AND expand to show connected code elements (callers, callees, imports). Use this to understand how code fits into the broader architecture.', + schema: z.object({ + query: z.string().describe('Natural language description of what you are looking for'), + limit: z.number().optional().describe('Number of initial matches to find (default: 5)'), + hops: z.number().optional().describe('Number of graph hops to expand (default: 2, max: 3)'), + }), + } + ); + + /** + * Tool: Get Graph Schema + * Returns the schema for reference - LLM should call this before writing Cypher queries + */ + const getSchemaTool = tool( + async ({ includeExamples }: { includeExamples?: boolean }) => { + return GRAPH_SCHEMA_DESCRIPTION; + }, + { + name: 'get_graph_schema', + description: 'Get the graph database schema including node types, relationships, and Cypher query patterns. Call this before writing Cypher queries.', + schema: z.object({ + includeExamples: z.boolean().optional().describe('Whether to include query examples (default: true)'), + }), + } + ); + + /** + * Tool: Get Code Content + * Retrieve the source code for a specific node + */ + const getCodeContentTool = tool( + async ({ nodeId }: { nodeId: string }) => { + try { + const results = await executeQuery( + `MATCH (n:CodeNode {id: '${nodeId.replace(/'/g, "''")}'}) + RETURN n.name AS name, n.label AS label, n.filePath AS filePath, + n.content AS content, n.startLine AS startLine, n.endLine AS endLine` + ); + + if (results.length === 0) { + return `No node found with ID: ${nodeId}`; + } + + const node = results[0]; + const name = node.name ?? node[0]; + const label = node.label ?? node[1]; + const filePath = node.filePath ?? node[2]; + const content = node.content ?? node[3]; + const startLine = node.startLine ?? node[4]; + const endLine = node.endLine ?? node[5]; + + if (!content) { + return `${label} "${name}" in ${filePath} (no source code available)`; + } + + return `${label}: ${name}\nFile: ${filePath}\nLines: ${startLine}-${endLine}\n\n\`\`\`\n${content}\n\`\`\``; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + return `Error retrieving code: ${message}`; + } + }, + { + name: 'get_code_content', + description: 'Retrieve the source code content for a specific node by its ID. Use this after finding relevant nodes to see the actual implementation.', + schema: z.object({ + nodeId: z.string().describe('The ID of the node to retrieve code for'), + }), + } + ); + + /** + * Tool: Get Codebase Statistics + * Quick overview of what's in the graph + */ + const getStatsTool = tool( + async ({ verbose }: { verbose?: boolean }) => { + try { + const labelCounts = await executeQuery(` + MATCH (n:CodeNode) + RETURN n.label AS label, count(*) AS count + ORDER BY count DESC + `); + + const relCounts = await executeQuery(` + MATCH ()-[r:CodeRelation]->() + RETURN r.type AS type, count(*) AS count + ORDER BY count DESC + `); + + const nodeStats = labelCounts.map(r => { + const label = r.label ?? r[0]; + const count = r.count ?? r[1]; + return ` ${label}: ${count}`; + }).join('\n'); + + const relStats = relCounts.map(r => { + const type = r.type ?? r[0]; + const count = r.count ?? r[1]; + return ` ${type}: ${count}`; + }).join('\n'); + + const embeddingStatus = isEmbeddingReady() + ? 'Ready (semantic search available)' + : 'Not generated (use execute_cypher for queries)'; + + return `Codebase Statistics:\n\nNodes by type:\n${nodeStats}\n\nRelationships by type:\n${relStats}\n\nEmbeddings: ${embeddingStatus}`; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + return `Error getting stats: ${message}`; + } + }, + { + name: 'get_codebase_stats', + description: 'Get an overview of the codebase including counts of different element types (files, functions, classes) and relationship types.', + schema: z.object({ + verbose: z.boolean().optional().describe('Include detailed breakdown (default: false)'), + }), + } + ); + + return [ + executeCypherTool, + semanticSearchTool, + semanticSearchWithContextTool, + getSchemaTool, + getCodeContentTool, + getStatsTool, + ]; +}; diff --git a/src/core/llm/types.ts b/src/core/llm/types.ts new file mode 100644 index 000000000..37515dbe0 --- /dev/null +++ b/src/core/llm/types.ts @@ -0,0 +1,188 @@ +/** + * LLM Provider Types + * + * Type definitions for multi-provider LLM support. + * Supports Azure OpenAI and Google Gemini (with extensibility for others). + */ + +/** + * Supported LLM providers + */ +export type LLMProvider = 'azure-openai' | 'gemini' | 'ollama'; + +/** + * Base configuration shared by all providers + */ +export interface BaseProviderConfig { + provider: LLMProvider; + model: string; + temperature?: number; + maxTokens?: number; +} + +/** + * Azure OpenAI specific configuration + */ +export interface AzureOpenAIConfig extends BaseProviderConfig { + provider: 'azure-openai'; + apiKey: string; + endpoint: string; // e.g., https://your-resource.openai.azure.com + deploymentName: string; + apiVersion?: string; // defaults to '2024-08-01-preview' +} + +/** + * Google Gemini specific configuration + */ +export interface GeminiConfig extends BaseProviderConfig { + provider: 'gemini'; + apiKey: string; + model: string; // e.g., 'gemini-2.0-flash', 'gemini-1.5-pro' +} + +/** + * Ollama configuration (for future use) + */ +export interface OllamaConfig extends BaseProviderConfig { + provider: 'ollama'; + baseUrl?: string; // defaults to http://localhost:11434 + model: string; +} + +/** + * Union type for all provider configurations + */ +export type ProviderConfig = AzureOpenAIConfig | GeminiConfig | OllamaConfig; + +/** + * Stored settings (what goes to localStorage) + */ +export interface LLMSettings { + activeProvider: LLMProvider; + azureOpenAI?: Omit; + gemini?: Omit; + ollama?: Omit; +} + +/** + * Default LLM settings + */ +export const DEFAULT_LLM_SETTINGS: LLMSettings = { + activeProvider: 'gemini', + gemini: { + apiKey: '', + model: 'gemini-2.0-flash', + temperature: 0.1, + }, + azureOpenAI: { + apiKey: '', + endpoint: '', + deploymentName: '', + model: 'gpt-4o', + apiVersion: '2024-08-01-preview', + temperature: 0.1, + }, + ollama: { + baseUrl: 'http://localhost:11434', + model: 'llama3.2', + temperature: 0.1, + }, +}; + +/** + * Chat message for agent interaction + */ +export interface ChatMessage { + id: string; + role: 'user' | 'assistant' | 'tool'; + content: string; + toolCalls?: ToolCallInfo[]; + toolCallId?: string; + timestamp: number; +} + +/** + * Tool call information for UI display + */ +export interface ToolCallInfo { + id: string; + name: string; + args: Record; + result?: string; + status: 'pending' | 'running' | 'completed' | 'error'; +} + +/** + * Streaming chunk from agent + */ +export interface AgentStreamChunk { + type: 'content' | 'tool_call' | 'tool_result' | 'error' | 'done'; + content?: string; + toolCall?: ToolCallInfo; + error?: string; +} + +/** + * Graph schema information for LLM context + */ +export const GRAPH_SCHEMA_DESCRIPTION = ` +KUZU GRAPH DATABASE SCHEMA: + +NODE TABLES: +1. CodeNode - All code elements (polymorphic) + - id: STRING (primary key) + - label: STRING (one of: File, Folder, Function, Class, Method, Interface) + - name: STRING (element name) + - filePath: STRING (path in project) + - startLine: INT64 (line number where element starts) + - endLine: INT64 (line number where element ends) + - content: STRING (source code snippet) + +2. CodeEmbedding - Vector embeddings (SEPARATE TABLE for memory efficiency) + - nodeId: STRING (primary key, references CodeNode.id) + - embedding: FLOAT[384] (semantic vector) + +RELATIONSHIP TABLE: +- CodeRelation (FROM CodeNode TO CodeNode) + - type: STRING (one of: CALLS, IMPORTS, CONTAINS, DEFINES) + +IMPORTANT QUERY PATTERNS: + +1. Basic node queries: + MATCH (n:CodeNode {label: 'Function'}) RETURN n.name, n.filePath LIMIT 10 + +2. Relationship traversal: + MATCH (f:CodeNode {label: 'File'})-[r:CodeRelation {type: 'DEFINES'}]->(fn:CodeNode {label: 'Function'}) + RETURN f.name AS file, fn.name AS function + +3. SEMANTIC SEARCH (embeddings in separate table - MUST JOIN): + CALL QUERY_VECTOR_INDEX('CodeEmbedding', 'code_embedding_idx', $queryVector, 10) + YIELD node AS emb, distance + WHERE distance < 0.4 + MATCH (n:CodeNode {id: emb.nodeId}) -- JOIN required! + RETURN n.name, n.label, n.filePath, distance + ORDER BY distance + +4. Semantic search + graph expansion: + CALL QUERY_VECTOR_INDEX('CodeEmbedding', 'code_embedding_idx', $queryVector, 5) + YIELD node AS emb, distance + WHERE distance < 0.5 + MATCH (match:CodeNode {id: emb.nodeId}) + MATCH (match)-[r:CodeRelation*1..2]-(connected:CodeNode) + RETURN match.name, distance, collect(DISTINCT connected.name) AS related + +5. Find callers of a function: + MATCH (caller:CodeNode)-[r:CodeRelation {type: 'CALLS'}]->(fn:CodeNode {name: $functionName}) + RETURN caller.name, caller.label, caller.filePath + +6. Import chain analysis: + MATCH (f:CodeNode {name: $fileName})-[r:CodeRelation {type: 'IMPORTS'}]->(imported:CodeNode) + RETURN imported.name AS imports + +NOTES: +- Always use WHERE clauses to filter by label when possible for performance +- Use LIMIT to avoid returning too many results +- For semantic search, the vector index is on CodeEmbedding table, not CodeNode +- distance in vector search is cosine distance (0 = identical, 1 = orthogonal) +`; + diff --git a/src/hooks/useAppState.tsx b/src/hooks/useAppState.tsx index 6336e7a47..5c546488f 100644 --- a/src/hooks/useAppState.tsx +++ b/src/hooks/useAppState.tsx @@ -7,6 +7,9 @@ import { DEFAULT_VISIBLE_LABELS } from '../lib/constants'; import type { IngestionWorkerApi } from '../workers/ingestion.worker'; import type { FileEntry } from '../services/zip'; import type { EmbeddingProgress, SemanticSearchResult } from '../core/embeddings/types'; +import type { LLMSettings, ProviderConfig, AgentStreamChunk, ChatMessage, ToolCallInfo } from '../core/llm/types'; +import { loadSettings, getActiveProviderConfig } from '../core/llm/settings-service'; +import type { AgentMessage } from '../core/llm/agent'; export type ViewMode = 'onboarding' | 'loading' | 'exploring'; export type RightPanelTab = 'code' | 'chat'; @@ -82,6 +85,25 @@ interface AppState { // Debug/test methods testArrayParams: () => Promise<{ success: boolean; error?: string }>; + + // LLM/Agent state + llmSettings: LLMSettings; + isSettingsPanelOpen: boolean; + setSettingsPanelOpen: (open: boolean) => void; + isAgentReady: boolean; + isAgentInitializing: boolean; + agentError: string | null; + + // Chat state + chatMessages: ChatMessage[]; + isChatLoading: boolean; + currentToolCalls: ToolCallInfo[]; + + // LLM methods + refreshLLMSettings: () => void; + initializeAgent: () => Promise; + sendChatMessage: (message: string) => Promise; + clearChat: () => void; } const AppStateContext = createContext(null); @@ -135,6 +157,18 @@ export const AppStateProvider = ({ children }: { children: ReactNode }) => { // Embedding state const [embeddingStatus, setEmbeddingStatus] = useState('idle'); const [embeddingProgress, setEmbeddingProgress] = useState(null); + + // LLM/Agent state + const [llmSettings, setLLMSettings] = useState(loadSettings); + const [isSettingsPanelOpen, setSettingsPanelOpen] = useState(false); + const [isAgentReady, setIsAgentReady] = useState(false); + const [isAgentInitializing, setIsAgentInitializing] = useState(false); + const [agentError, setAgentError] = useState(null); + + // Chat state + const [chatMessages, setChatMessages] = useState([]); + const [isChatLoading, setIsChatLoading] = useState(false); + const [currentToolCalls, setCurrentToolCalls] = useState([]); // Worker (single instance shared across app) const workerRef = useRef(null); @@ -266,6 +300,178 @@ export const AppStateProvider = ({ children }: { children: ReactNode }) => { return api.testArrayParams(); }, []); + // LLM methods + const refreshLLMSettings = useCallback(() => { + setLLMSettings(loadSettings()); + }, []); + + const initializeAgent = useCallback(async (): Promise => { + const api = apiRef.current; + if (!api) { + setAgentError('Worker not initialized'); + return; + } + + const config = getActiveProviderConfig(); + if (!config) { + setAgentError('Please configure an LLM provider in settings'); + return; + } + + setIsAgentInitializing(true); + setAgentError(null); + + try { + const result = await api.initializeAgent(config); + if (result.success) { + setIsAgentReady(true); + setAgentError(null); + if (import.meta.env.DEV) { + console.log('✅ Agent initialized successfully'); + } + } else { + setAgentError(result.error ?? 'Failed to initialize agent'); + setIsAgentReady(false); + } + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + setAgentError(message); + setIsAgentReady(false); + } finally { + setIsAgentInitializing(false); + } + }, []); + + const sendChatMessage = useCallback(async (message: string): Promise => { + const api = apiRef.current; + if (!api) { + setAgentError('Worker not initialized'); + return; + } + + if (!isAgentReady) { + // Try to initialize first + await initializeAgent(); + if (!apiRef.current) return; + } + + // Add user message + const userMessage: ChatMessage = { + id: `user-${Date.now()}`, + role: 'user', + content: message, + timestamp: Date.now(), + }; + setChatMessages(prev => [...prev, userMessage]); + setIsChatLoading(true); + setCurrentToolCalls([]); + + // Prepare message history for agent (convert our format to AgentMessage format) + const history: AgentMessage[] = [...chatMessages, userMessage].map(m => ({ + role: m.role === 'tool' ? 'assistant' : m.role, + content: m.content, + })); + + // Create placeholder for assistant response + const assistantMessageId = `assistant-${Date.now()}`; + let assistantContent = ''; + const toolCallsForMessage: ToolCallInfo[] = []; + + try { + const onChunk = Comlink.proxy((chunk: AgentStreamChunk) => { + switch (chunk.type) { + case 'content': + if (chunk.content) { + assistantContent += chunk.content; + // Update the assistant message in place + setChatMessages(prev => { + const existing = prev.find(m => m.id === assistantMessageId); + if (existing) { + return prev.map(m => + m.id === assistantMessageId + ? { ...m, content: assistantContent } + : m + ); + } else { + return [...prev, { + id: assistantMessageId, + role: 'assistant' as const, + content: assistantContent, + timestamp: Date.now(), + toolCalls: toolCallsForMessage, + }]; + } + }); + } + break; + + case 'tool_call': + if (chunk.toolCall) { + const tc = chunk.toolCall; + toolCallsForMessage.push(tc); + setCurrentToolCalls(prev => [...prev, tc]); + } + break; + + case 'tool_result': + if (chunk.toolCall) { + const tc = chunk.toolCall; + setCurrentToolCalls(prev => + prev.map(t => t.id === tc.id ? { ...t, result: tc.result, status: 'completed' } : t) + ); + // Update the tool call in the message too + const idx = toolCallsForMessage.findIndex(t => t.id === tc.id); + if (idx >= 0) { + toolCallsForMessage[idx] = { ...toolCallsForMessage[idx], result: tc.result, status: 'completed' }; + } + } + break; + + case 'error': + setAgentError(chunk.error ?? 'Unknown error'); + break; + + case 'done': + // Finalize the assistant message with all tool calls + setChatMessages(prev => { + const existing = prev.find(m => m.id === assistantMessageId); + if (existing) { + return prev.map(m => + m.id === assistantMessageId + ? { ...m, content: assistantContent, toolCalls: toolCallsForMessage } + : m + ); + } else if (assistantContent) { + return [...prev, { + id: assistantMessageId, + role: 'assistant' as const, + content: assistantContent, + timestamp: Date.now(), + toolCalls: toolCallsForMessage, + }]; + } + return prev; + }); + break; + } + }); + + await api.chatStream(history, onChunk); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + setAgentError(message); + } finally { + setIsChatLoading(false); + setCurrentToolCalls([]); + } + }, [chatMessages, isAgentReady, initializeAgent]); + + const clearChat = useCallback(() => { + setChatMessages([]); + setCurrentToolCalls([]); + setAgentError(null); + }, []); + const toggleLabelVisibility = useCallback((label: NodeLabel) => { setVisibleLabels(prev => { if (prev.includes(label)) { @@ -317,6 +523,22 @@ export const AppStateProvider = ({ children }: { children: ReactNode }) => { isEmbeddingReady: embeddingStatus === 'ready', // Debug testArrayParams, + // LLM/Agent state + llmSettings, + isSettingsPanelOpen, + setSettingsPanelOpen, + isAgentReady, + isAgentInitializing, + agentError, + // Chat state + chatMessages, + isChatLoading, + currentToolCalls, + // LLM methods + refreshLLMSettings, + initializeAgent, + sendChatMessage, + clearChat, }; return ( diff --git a/src/workers/ingestion.worker.ts b/src/workers/ingestion.worker.ts index 1c6ccb86c..e6823a5c5 100644 --- a/src/workers/ingestion.worker.ts +++ b/src/workers/ingestion.worker.ts @@ -10,6 +10,8 @@ import { } from '../core/embeddings/embedding-pipeline'; import { isEmbedderReady, disposeEmbedder } from '../core/embeddings/embedder'; import type { EmbeddingProgress, SemanticSearchResult } from '../core/embeddings/types'; +import type { ProviderConfig, AgentStreamChunk } from '../core/llm/types'; +import { createGraphRAGAgent, streamAgentResponse, type AgentMessage } from '../core/llm/agent'; // Lazy import for Kuzu to avoid breaking worker if SharedArrayBuffer unavailable let kuzuAdapter: typeof import('../core/kuzu/kuzu-adapter') | null = null; @@ -24,6 +26,10 @@ const getKuzuAdapter = async () => { let embeddingProgress: EmbeddingProgress | null = null; let isEmbeddingComplete = false; +// Agent state +let currentAgent: ReturnType | null = null; +let currentProviderConfig: ProviderConfig | null = null; + /** * Worker API exposed via Comlink * @@ -285,6 +291,111 @@ const workerApi = { } return kuzu.testArrayParams(); }, + + // ============================================================ + // Graph RAG Agent Methods + // ============================================================ + + /** + * Initialize the Graph RAG agent with a provider configuration + * Must be called before using chat methods + * @param config - Provider configuration (Azure OpenAI or Gemini) + */ + async initializeAgent(config: ProviderConfig): Promise<{ success: boolean; error?: string }> { + try { + const kuzu = await getKuzuAdapter(); + if (!kuzu.isKuzuReady()) { + return { success: false, error: 'Database not ready. Please load a repository first.' }; + } + + // Create semantic search wrappers that handle embedding state + const semanticSearchWrapper = async (query: string, k?: number, maxDistance?: number) => { + if (!isEmbeddingComplete) { + throw new Error('Embeddings not ready'); + } + return doSemanticSearch(kuzu.executeQuery, query, k, maxDistance); + }; + + const semanticSearchWithContextWrapper = async (query: string, k?: number, hops?: number) => { + if (!isEmbeddingComplete) { + throw new Error('Embeddings not ready'); + } + return doSemanticSearchWithContext(kuzu.executeQuery, query, k, hops); + }; + + currentAgent = createGraphRAGAgent( + config, + kuzu.executeQuery, + semanticSearchWrapper, + semanticSearchWithContextWrapper, + () => isEmbeddingComplete + ); + currentProviderConfig = config; + + if (import.meta.env.DEV) { + console.log('🤖 Graph RAG Agent initialized with provider:', config.provider); + } + + return { success: true }; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + if (import.meta.env.DEV) { + console.error('❌ Agent initialization failed:', error); + } + return { success: false, error: message }; + } + }, + + /** + * Check if the agent is initialized + */ + isAgentReady(): boolean { + return currentAgent !== null; + }, + + /** + * Get current provider info + */ + getAgentProvider(): { provider: string; model: string } | null { + if (!currentProviderConfig) return null; + return { + provider: currentProviderConfig.provider, + model: currentProviderConfig.model, + }; + }, + + /** + * Chat with the Graph RAG agent (streaming) + * Sends response chunks via the onChunk callback + * @param messages - Conversation history + * @param onChunk - Proxied callback for streaming chunks (runs on main thread) + */ + async chatStream( + messages: AgentMessage[], + onChunk: (chunk: AgentStreamChunk) => void + ): Promise { + if (!currentAgent) { + onChunk({ type: 'error', error: 'Agent not initialized. Please configure an LLM provider first.' }); + return; + } + + try { + for await (const chunk of streamAgentResponse(currentAgent, messages)) { + onChunk(chunk); + } + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + onChunk({ type: 'error', error: message }); + } + }, + + /** + * Dispose of the current agent + */ + disposeAgent(): void { + currentAgent = null; + currentProviderConfig = null; + }, }; // Expose the worker API to the main thread From 4bcb9fbc740ec9d73205ffaf23fd91b599e8acf7 Mon Sep 17 00:00:00 2001 From: abhigyanpatwari Date: Tue, 6 Jan 2026 05:12:11 +0530 Subject: [PATCH 2/3] Graph RAG agent is able to execute cyfers, but needs more testing, something is wrong ;-; --- src/components/SettingsPanel.tsx | 5 +- src/core/embeddings/embedding-pipeline.ts | 35 +++--- src/core/llm/agent.ts | 47 +++++-- src/core/llm/settings-service.ts | 62 ++++++--- src/core/llm/tools.ts | 147 ++++++++++++++++++++-- src/core/llm/types.ts | 27 +++- 6 files changed, 263 insertions(+), 60 deletions(-) diff --git a/src/components/SettingsPanel.tsx b/src/components/SettingsPanel.tsx index 7663dcb01..632a321bf 100644 --- a/src/components/SettingsPanel.tsx +++ b/src/components/SettingsPanel.tsx @@ -105,8 +105,9 @@ export const SettingsPanel = ({ isOpen, onClose, onSettingsSaved }: SettingsPane // Fetch models when API key is entered (debounced) useEffect(() => { if (settings.activeProvider === 'gemini' && settings.gemini?.apiKey) { + const apiKey = settings.gemini.apiKey ?? ''; const timer = setTimeout(() => { - fetchModels(settings.gemini!.apiKey); + fetchModels(apiKey); }, 500); return () => clearTimeout(timer); } @@ -260,7 +261,7 @@ export const SettingsPanel = ({ isOpen, onClose, onSettingsSaved }: SettingsPane {settings.gemini?.apiKey && !isLoadingModels && ( + + {/* Expanded content */} + {isExpanded && ( +
+ {/* Arguments/Query */} + {formattedArgs && ( +
+
+ {toolCall.name.includes('cypher') ? 'Query' : 'Input'} +
+
+                {formattedArgs}
+              
+
+ )} + + {/* Result */} + {toolCall.result && ( +
+
+ Result +
+
+                {toolCall.result.length > 2000 
+                  ? toolCall.result.slice(0, 2000) + '\n\n... (truncated)'
+                  : toolCall.result
+                }
+              
+
+ )} + + {/* Loading state for in-progress */} + {toolCall.status === 'running' && !toolCall.result && ( +
+ + Executing... +
+ )} +
+ )} +
+ ); +}; + +export default ToolCallCard; + diff --git a/src/core/llm/agent.ts b/src/core/llm/agent.ts index 78033d205..73ca5ec70 100644 --- a/src/core/llm/agent.ts +++ b/src/core/llm/agent.ts @@ -27,6 +27,8 @@ CAPABILITIES: - Execute Cypher queries to explore code structure (functions, classes, files, imports, call graphs) - Perform semantic search to find code by meaning (when embeddings are available) - Combine semantic search + graph traversal in a SINGLE Cypher query via the vector index (when embeddings are available) +- Search for exact text patterns using grep (regex) across all files +- Read full file contents directly - Trace dependencies and relationships between code elements - Explain code architecture and patterns @@ -34,21 +36,33 @@ APPROACH: 1. Start by understanding what the user wants to know 2. Choose the right tool(s) for the task: - Use 'get_codebase_stats' first if you need an overview - - Use 'semantic_search' for simple concept-based lookup (find relevant nodes) - - Use 'semantic_search_with_context' for simple semantic + neighborhood expansion (prebuilt 1-3 hop expansion) - - Use 'execute_vector_cypher' when you need semantic search + CUSTOM traversal/filters/returns in ONE query - - Your Cypher MUST include the placeholder {{QUERY_VECTOR}} where a FLOAT[384] vector belongs - - The vector index is on CodeEmbedding (code_embedding_idx). You must JOIN back to CodeNode via emb.nodeId - - Use 'execute_cypher' for pure structural queries (no vector search) - - Use 'get_code_content' to show actual source code + - Use 'grep_code' to find EXACT text patterns (strings, error messages, TODOs, variable names) + - Use 'read_file' to see the FULL content of any file + - Use 'semantic_search' for CONCEPT-based lookup (find code by meaning, not exact text) + - Use 'semantic_search_with_context' for semantic + neighborhood expansion + - Use 'execute_vector_cypher' when you need semantic search + CUSTOM traversal in ONE query + - Use 'execute_cypher' for pure structural queries (relationships, call graphs) + - Use 'get_code_content' to show source code for a specific node ID 3. Interpret results and explain them clearly 4. Suggest follow-up explorations when relevant +TOOL SELECTION GUIDE: +| Task | Best Tool | +|------|-----------| +| Find exact string "API_KEY" | grep_code | +| Find all TODO comments | grep_code | +| Find code related to "authentication" | semantic_search | +| What functions call X? | execute_cypher | +| Show me file utils.ts | read_file | +| Show code for a found node | get_code_content | +| Find auth code AND its callers | semantic_search_with_context | + IMPORTANT NOTES ABOUT THE DATABASE: - Nodes are stored in CodeNode(id, label, name, filePath, startLine, endLine, content) - Edges are stored in CodeRelation(FROM CodeNode TO CodeNode, type) where type ∈ {CALLS, IMPORTS, CONTAINS, DEFINES} - Embeddings are stored separately in CodeEmbedding(nodeId, embedding) for memory efficiency - Vector index: code_embedding_idx on CodeEmbedding.embedding (cosine distance; smaller distance = more similar) +- Full file contents are available via grep_code and read_file (not truncated) UNIFIED VECTOR + GRAPH QUERY PATTERN (ONE QUERY): 1) Vector search to get closest embeddings @@ -72,8 +86,7 @@ STYLE: - If a query fails, explain why and suggest alternatives LIMITATIONS: -- You can only see code that's been indexed in the knowledge graph -- Semantic search requires embeddings to be generated first +- Semantic search requires embeddings to be generated first (but grep_code always works) - Large codebases may require more specific queries When showing code or query results, format them nicely using markdown.`; @@ -140,14 +153,16 @@ export const createGraphRAGAgent = ( executeQuery: (cypher: string) => Promise, semanticSearch: (query: string, k?: number, maxDistance?: number) => Promise, semanticSearchWithContext: (query: string, k?: number, hops?: number) => Promise, - isEmbeddingReady: () => boolean + isEmbeddingReady: () => boolean, + fileContents: Map ) => { const model = createChatModel(config); const tools = createGraphRAGTools( executeQuery, semanticSearch, semanticSearchWithContext, - isEmbeddingReady + isEmbeddingReady, + fileContents ); const agent = createReactAgent({ @@ -169,7 +184,13 @@ export interface AgentMessage { /** * Stream a response from the agent - * Yields chunks for real-time UI updates + * Uses streamMode: "values" to get step-by-step updates including reasoning + * + * Each step shows: + * - AI reasoning/thinking (content before tool calls) + * - Tool calls with arguments + * - Tool results + * - Final answer */ export async function* streamAgentResponse( agent: ReturnType, @@ -181,53 +202,70 @@ export async function* streamAgentResponse( content: m.content, })); - // In LangGraph, we stream events to get granular updates - const stream = await agent.streamEvents( + // Use stream with "values" mode to get each step as a complete state + // This lets us see reasoning, tool calls, and results separately + const stream = await agent.stream( { messages: formattedMessages }, - { version: 'v2' } + { streamMode: 'values' } ); - for await (const event of stream) { - const { event: eventType, data } = event; - const dataAny = data as any; - - // Handle tool calls start - if (eventType === 'on_tool_start') { - yield { - type: 'tool_call', - toolCall: { - id: dataAny?.tool_call_id || dataAny?.toolCallId || Date.now().toString(), // fallback if ID missing - name: (event as any).name, - args: dataAny?.input ?? {}, - status: 'running', - }, - }; - } + let lastMessageCount = formattedMessages.length; + + for await (const step of stream) { + const stepMessages = step.messages || []; - // Handle tool output - if (eventType === 'on_tool_end') { - yield { - type: 'tool_result', - toolCall: { - id: dataAny?.tool_call_id || dataAny?.toolCallId || '', // we might need to match by name if ID missing - name: (event as any).name, - args: {}, - result: typeof dataAny?.output === 'string' ? dataAny.output : JSON.stringify(dataAny?.output), - status: 'completed', - }, - }; - } - - // Handle streamed LLM content - if (eventType === 'on_chat_model_stream') { - const content = dataAny?.chunk?.content; - if (content && typeof content === 'string') { + // Process only new messages since last step + for (let i = lastMessageCount; i < stepMessages.length; i++) { + const msg = stepMessages[i]; + const msgType = msg._getType?.() || msg.type || 'unknown'; + + // AI message with content (reasoning or final answer) + if (msgType === 'ai' || msgType === 'AIMessage') { + const content = msg.content; + const toolCalls = msg.tool_calls || msg.additional_kwargs?.tool_calls || []; + + // If has content, yield it (reasoning or answer) + if (content && typeof content === 'string' && content.trim()) { + yield { + type: toolCalls.length > 0 ? 'reasoning' : 'content', + reasoning: toolCalls.length > 0 ? content : undefined, + content: toolCalls.length === 0 ? content : undefined, + }; + } + + // If has tool calls, yield each one + for (const tc of toolCalls) { + yield { + type: 'tool_call', + toolCall: { + id: tc.id || `tool-${Date.now()}-${Math.random().toString(36).slice(2)}`, + name: tc.name || tc.function?.name || 'unknown', + args: tc.args || (tc.function?.arguments ? JSON.parse(tc.function.arguments) : {}), + status: 'running', + }, + }; + } + } + + // Tool message (result from a tool) + if (msgType === 'tool' || msgType === 'ToolMessage') { + const toolCallId = msg.tool_call_id || msg.additional_kwargs?.tool_call_id || ''; + const result = typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content); + yield { - type: 'content', - content: content, + type: 'tool_result', + toolCall: { + id: toolCallId, + name: msg.name || 'tool', + args: {}, + result: result, + status: 'completed', + }, }; } } + + lastMessageCount = stepMessages.length; } yield { type: 'done' }; diff --git a/src/core/llm/tools.ts b/src/core/llm/tools.ts index 988b0331d..c94fc76ce 100644 --- a/src/core/llm/tools.ts +++ b/src/core/llm/tools.ts @@ -18,7 +18,8 @@ export const createGraphRAGTools = ( executeQuery: (cypher: string) => Promise, semanticSearch: (query: string, k?: number, maxDistance?: number) => Promise, semanticSearchWithContext: (query: string, k?: number, hops?: number) => Promise, - isEmbeddingReady: () => boolean + isEmbeddingReady: () => boolean, + fileContents: Map ) => { /** * Tool: Execute Cypher Query @@ -165,10 +166,10 @@ export const createGraphRAGTools = ( const formatted = results.map((r, i) => { const location = r.startLine ? ` (lines ${r.startLine}-${r.endLine})` : ''; - return `[${i + 1}] ${r.label}: ${r.name}\n File: ${r.filePath}${location}\n Relevance: ${(1 - r.distance).toFixed(2)}`; + return `[${i + 1}] ${r.label}: ${r.name}\n ID: ${r.nodeId}\n File: ${r.filePath}${location}\n Relevance: ${(1 - r.distance).toFixed(2)}`; }); - return `Found ${results.length} semantically similar code elements:\n\n${formatted.join('\n\n')}`; + return `Found ${results.length} semantically similar code elements (use ID with get_code_content to see source):\n\n${formatted.join('\n\n')}`; } catch (error) { const message = error instanceof Error ? error.message : String(error); return `Semantic search error: ${message}`; @@ -179,7 +180,7 @@ export const createGraphRAGTools = ( description: 'Search for code by meaning using semantic similarity. Good for finding code related to a concept even if exact terms are not used.', schema: z.object({ query: z.string().describe('Natural language description of what you are looking for'), - limit: z.number().optional().describe('Maximum number of results to return (default: 10)'), + limit: z.number().optional().nullable().describe('Maximum number of results to return (default: 10)'), }), } ); @@ -204,6 +205,7 @@ export const createGraphRAGTools = ( // Results are flattened: one row per (match → connected) pair // Group by match for cleaner output const grouped = new Map 15 ? `\n ... and ${g.connections.length - 15} more` : ''; - return `[${i + 1}] ${g.matchLabel}: ${g.matchName}\n File: ${g.matchPath}\n Relevance: ${(1 - g.distance).toFixed(2)}\n Connections:\n ${connectionsList}${more}`; + return `[${i + 1}] ${g.matchLabel}: ${g.matchName}\n ID: ${g.matchId}\n File: ${g.matchPath}\n Relevance: ${(1 - g.distance).toFixed(2)}\n Connections:\n ${connectionsList}${more}`; }); - return `Found ${grouped.size} code elements with ${results.length} total connections:\n\n${formatted.join('\n\n')}`; + return `Found ${grouped.size} code elements with ${results.length} total connections (use ID with get_code_content to see source):\n\n${formatted.join('\n\n')}`; } catch (error) { const message = error instanceof Error ? error.message : String(error); return `Search with context error: ${message}`; @@ -260,7 +263,7 @@ export const createGraphRAGTools = ( description: 'Search for code semantically AND show directly connected code elements with relationship types (CALLS, IMPORTS, DEFINES, CONTAINS). Shows what each match is connected to and how.', schema: z.object({ query: z.string().describe('Natural language description of what you are looking for'), - limit: z.number().optional().describe('Number of semantic matches to find (default: 5)'), + limit: z.number().optional().nullable().describe('Number of semantic matches to find (default: 5)'), }), } ); @@ -277,7 +280,7 @@ export const createGraphRAGTools = ( name: 'get_graph_schema', description: 'Get the graph database schema including node types, relationships, and Cypher query patterns. Call this before writing Cypher queries.', schema: z.object({ - includeExamples: z.boolean().optional().describe('Whether to include query examples (default: true)'), + includeExamples: z.boolean().optional().nullable().describe('Whether to include query examples (default: true)'), }), } ); @@ -285,14 +288,16 @@ export const createGraphRAGTools = ( /** * Tool: Get Code Content * Retrieve the source code for a specific node + * Uses fileContents Map for full content (not truncated DB content) */ const getCodeContentTool = tool( async ({ nodeId }: { nodeId: string }) => { try { + // Query graph for node metadata (fast, small data) const results = await executeQuery( `MATCH (n:CodeNode {id: '${nodeId.replace(/'/g, "''")}'}) RETURN n.name AS name, n.label AS label, n.filePath AS filePath, - n.content AS content, n.startLine AS startLine, n.endLine AS endLine` + n.startLine AS startLine, n.endLine AS endLine` ); if (results.length === 0) { @@ -303,15 +308,36 @@ export const createGraphRAGTools = ( const name = node.name ?? node[0]; const label = node.label ?? node[1]; const filePath = node.filePath ?? node[2]; - const content = node.content ?? node[3]; - const startLine = node.startLine ?? node[4]; - const endLine = node.endLine ?? node[5]; + const startLine = node.startLine ?? node[3]; + const endLine = node.endLine ?? node[4]; - if (!content) { - return `${label} "${name}" in ${filePath} (no source code available)`; + // Get FULL content from fileContents Map (not truncated DB) + const fileContent = fileContents.get(filePath); + + if (!fileContent) { + return `${label}: ${name}\nFile: ${filePath}\n(File content not available in memory)`; } - return `${label}: ${name}\nFile: ${filePath}\nLines: ${startLine}-${endLine}\n\n\`\`\`\n${content}\n\`\`\``; + // For File nodes, return full content (limited for very large files) + if (label === 'File' || label === 'Folder') { + const MAX_FILE_CONTENT = 30000; + if (fileContent.length > MAX_FILE_CONTENT) { + return `${label}: ${name}\nFile: ${filePath}\nTotal size: ${fileContent.length} characters\n\n\`\`\`\n${fileContent.slice(0, MAX_FILE_CONTENT)}\n\`\`\`\n\n... [truncated, use read_file for full content]`; + } + return `${label}: ${name}\nFile: ${filePath}\n\n\`\`\`\n${fileContent}\n\`\`\``; + } + + // For Function/Class/Method nodes, extract specific lines with context + const lines = fileContent.split('\n'); + const contextBefore = 3; + const contextAfter = 20; // Show more after to capture full function body + + const start = Math.max(0, (startLine ?? 0) - contextBefore); + const end = Math.min(lines.length - 1, (endLine ?? startLine ?? 0) + contextAfter); + + const snippet = lines.slice(start, end + 1).join('\n'); + + return `${label}: ${name}\nFile: ${filePath}\nLines: ${startLine + 1}-${endLine + 1}\n\n\`\`\`\n${snippet}\n\`\`\``; } catch (error) { const message = error instanceof Error ? error.message : String(error); return `Error retrieving code: ${message}`; @@ -361,7 +387,7 @@ export const createGraphRAGTools = ( ? 'Ready (semantic search available)' : 'Not generated (use execute_cypher for queries)'; - return `Codebase Statistics:\n\nNodes by type:\n${nodeStats}\n\nRelationships by type:\n${relStats}\n\nEmbeddings: ${embeddingStatus}`; + return `Codebase Statistics:\n\nNodes by type:\n${nodeStats}\n\nRelationships by type:\n${relStats}\n\nEmbeddings: ${embeddingStatus}\n\nFiles in memory: ${fileContents.size}`; } catch (error) { const message = error instanceof Error ? error.message : String(error); return `Error getting stats: ${message}`; @@ -371,7 +397,185 @@ export const createGraphRAGTools = ( name: 'get_codebase_stats', description: 'Get an overview of the codebase including counts of different element types (files, functions, classes) and relationship types.', schema: z.object({ - verbose: z.boolean().optional().describe('Include detailed breakdown (default: false)'), + verbose: z.boolean().optional().nullable().describe('Include detailed breakdown (default: false)'), + }), + } + ); + + /** + * Tool: Grep Code + * Search for patterns across all file contents using regex + */ + const grepCodeTool = tool( + async ({ pattern, filePattern, caseSensitive, maxResults }: { + pattern: string; + filePattern?: string; + caseSensitive?: boolean; + maxResults?: number; + }) => { + try { + const flags = caseSensitive ? 'g' : 'gi'; + let regex: RegExp; + try { + regex = new RegExp(pattern, flags); + } catch (e) { + return `Invalid regex pattern: ${pattern}. Error: ${e instanceof Error ? e.message : String(e)}`; + } + + const results: Array<{ + file: string; + line: number; + content: string; + }> = []; + + const limit = maxResults ?? 100; + + for (const [filePath, content] of fileContents.entries()) { + // Optional file pattern filter + if (filePattern && !filePath.toLowerCase().includes(filePattern.toLowerCase())) { + continue; + } + + const lines = content.split('\n'); + for (let i = 0; i < lines.length; i++) { + if (regex.test(lines[i])) { + results.push({ + file: filePath, + line: i + 1, + content: lines[i].trim().slice(0, 200), // Limit line length + }); + + if (results.length >= limit) break; + } + // Reset regex lastIndex for global flag + regex.lastIndex = 0; + } + + if (results.length >= limit) break; + } + + if (results.length === 0) { + return `No matches found for pattern: "${pattern}"${filePattern ? ` in files matching "${filePattern}"` : ''}`; + } + + const formatted = results.map(r => + `${r.file}:${r.line}: ${r.content}` + ).join('\n'); + + const truncatedMsg = results.length >= limit + ? `\n\n(Showing first ${limit} results. Use filePattern to narrow search.)` + : ''; + + return `Found ${results.length} matches for "${pattern}":\n\n${formatted}${truncatedMsg}`; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + return `Grep error: ${message}`; + } + }, + { + name: 'grep_code', + description: 'Search for text patterns across all files in the codebase using regex. Use this to find exact strings, error messages, TODOs, specific variable names, or any text pattern. Returns file paths and line numbers of matches.', + schema: z.object({ + pattern: z.string().describe('Regex pattern to search for (e.g., "TODO", "console\\.log", "API_KEY")'), + filePattern: z.string().optional().nullable().describe('Optional filter - only search files whose path contains this string (e.g., ".ts", "src/api")'), + caseSensitive: z.boolean().optional().nullable().describe('Whether search is case-sensitive (default: false)'), + maxResults: z.number().optional().nullable().describe('Maximum number of results to return (default: 100)'), + }), + } + ); + + /** + * Tool: Read File + * Read the full content of a file by its path + */ + const readFileTool = tool( + async ({ filePath }: { filePath: string }) => { + // Normalize the requested path (handle Windows-style paths) + const normalizedRequest = filePath.replace(/\\/g, '/').toLowerCase(); + + // Try exact match first + let content = fileContents.get(filePath); + let actualPath = filePath; + + // If not found, try smarter matching + if (!content) { + // Score each file path by how well it matches the request + const candidates: Array<{ path: string; score: number }> = []; + + for (const [path] of fileContents.entries()) { + const normalizedPath = path.toLowerCase(); + + // Exact match (case-insensitive) + if (normalizedPath === normalizedRequest) { + candidates.push({ path, score: 1000 }); + continue; + } + + // Ends with the requested path (e.g., "README.md" matches "src/agent_service/README.md") + if (normalizedPath.endsWith(normalizedRequest)) { + // Score higher for shorter paths (more specific match) + candidates.push({ path, score: 100 + (200 - path.length) }); + continue; + } + + // Path contains all segments of the request in order + const requestSegments = normalizedRequest.split('/').filter(Boolean); + const pathSegments = normalizedPath.split('/'); + let matchScore = 0; + let lastMatchIdx = -1; + + for (const seg of requestSegments) { + const idx = pathSegments.findIndex((s, i) => i > lastMatchIdx && s.includes(seg)); + if (idx > lastMatchIdx) { + matchScore += 10; + lastMatchIdx = idx; + } + } + + // Only include if we matched more than half the segments + if (matchScore >= requestSegments.length * 5) { + candidates.push({ path, score: matchScore }); + } + } + + // Sort by score descending, pick best match + candidates.sort((a, b) => b.score - a.score); + + if (candidates.length > 0) { + actualPath = candidates[0].path; + content = fileContents.get(actualPath); + } + } + + if (!content) { + // List similar files to help the user + const fileName = filePath.split('/').pop()?.toLowerCase() || ''; + const similarFiles = Array.from(fileContents.keys()) + .filter(p => p.toLowerCase().includes(fileName)) + .slice(0, 5); + + if (similarFiles.length > 0) { + return `File not found: "${filePath}"\n\nDid you mean one of these?\n${similarFiles.map(f => ` - ${f}`).join('\n')}`; + } + return `File not found: "${filePath}"`; + } + + // For very large files, truncate with a warning + const MAX_CONTENT = 50000; // ~50KB + if (content.length > MAX_CONTENT) { + const truncated = content.slice(0, MAX_CONTENT); + const lines = content.split('\n').length; + return `File: ${actualPath}\nTotal lines: ${lines}\n\n(Showing first ${MAX_CONTENT} characters, file is ${content.length} characters total)\n\n${truncated}\n\n... [truncated]`; + } + + const lines = content.split('\n').length; + return `File: ${actualPath}\nLines: ${lines}\n\n${content}`; + }, + { + name: 'read_file', + description: 'Read the full content of a file by its path. Use this to see the complete source code of any file in the codebase.', + schema: z.object({ + filePath: z.string().describe('The file path to read (can be partial path like "src/utils.ts")'), }), } ); @@ -384,5 +588,7 @@ export const createGraphRAGTools = ( getSchemaTool, getCodeContentTool, getStatsTool, + grepCodeTool, + readFileTool, ]; }; diff --git a/src/core/llm/types.ts b/src/core/llm/types.ts index b4b14f719..282cab508 100644 --- a/src/core/llm/types.ts +++ b/src/core/llm/types.ts @@ -118,14 +118,35 @@ export interface ToolCallInfo { /** * Streaming chunk from agent + * Now supports step-based streaming where each step is a distinct message */ export interface AgentStreamChunk { - type: 'content' | 'tool_call' | 'tool_result' | 'error' | 'done'; + type: 'reasoning' | 'tool_call' | 'tool_result' | 'content' | 'error' | 'done'; + /** LLM's reasoning/thinking text (shown as a step) */ + reasoning?: string; + /** Final answer content (streamed token by token) */ content?: string; + /** Tool call information */ toolCall?: ToolCallInfo; + /** Error message */ error?: string; } +/** + * A single step in the agent's execution + * Used for displaying the agent's thought process + */ +export interface AgentStep { + id: string; + type: 'reasoning' | 'tool_call' | 'answer'; + /** For reasoning steps */ + content?: string; + /** For tool_call steps */ + toolCall?: ToolCallInfo; + /** Timestamp */ + timestamp: number; +} + /** * Graph schema information for LLM context */ diff --git a/src/hooks/useAppState.tsx b/src/hooks/useAppState.tsx index 5c546488f..88c63cb6e 100644 --- a/src/hooks/useAppState.tsx +++ b/src/hooks/useAppState.tsx @@ -376,29 +376,63 @@ export const AppStateProvider = ({ children }: { children: ReactNode }) => { const assistantMessageId = `assistant-${Date.now()}`; let assistantContent = ''; const toolCallsForMessage: ToolCallInfo[] = []; + let reasoningSteps: string[] = []; // Collect reasoning steps try { const onChunk = Comlink.proxy((chunk: AgentStreamChunk) => { switch (chunk.type) { - case 'content': - if (chunk.content) { - assistantContent += chunk.content; - // Update the assistant message in place + case 'reasoning': + // LLM's thinking/reasoning before tool calls + if (chunk.reasoning) { + reasoningSteps.push(chunk.reasoning); + // Update message to show reasoning setChatMessages(prev => { const existing = prev.find(m => m.id === assistantMessageId); + // Build content with reasoning steps + const reasoningText = reasoningSteps.join('\n\n'); if (existing) { return prev.map(m => m.id === assistantMessageId - ? { ...m, content: assistantContent } + ? { ...m, content: reasoningText, toolCalls: [...toolCallsForMessage] } : m ); } else { return [...prev, { id: assistantMessageId, role: 'assistant' as const, - content: assistantContent, + content: reasoningText, timestamp: Date.now(), - toolCalls: toolCallsForMessage, + toolCalls: [...toolCallsForMessage], + }]; + } + }); + } + break; + + case 'content': + // Final answer content + if (chunk.content) { + assistantContent = chunk.content; // Replace, don't append (it's a full message) + // Update the assistant message + setChatMessages(prev => { + const existing = prev.find(m => m.id === assistantMessageId); + // Combine reasoning + final answer + const fullContent = reasoningSteps.length > 0 + ? reasoningSteps.join('\n\n') + '\n\n---\n\n' + assistantContent + : assistantContent; + if (existing) { + return prev.map(m => + m.id === assistantMessageId + ? { ...m, content: fullContent, toolCalls: [...toolCallsForMessage] } + : m + ); + } else { + return [...prev, { + id: assistantMessageId, + role: 'assistant' as const, + content: fullContent, + timestamp: Date.now(), + toolCalls: [...toolCallsForMessage], }]; } }); @@ -410,20 +444,83 @@ export const AppStateProvider = ({ children }: { children: ReactNode }) => { const tc = chunk.toolCall; toolCallsForMessage.push(tc); setCurrentToolCalls(prev => [...prev, tc]); + // Update message to include this tool call + setChatMessages(prev => { + const existing = prev.find(m => m.id === assistantMessageId); + const currentContent = reasoningSteps.length > 0 + ? reasoningSteps.join('\n\n') + : assistantContent || ''; + if (existing) { + return prev.map(m => + m.id === assistantMessageId + ? { ...m, content: currentContent, toolCalls: [...toolCallsForMessage] } + : m + ); + } else { + return [...prev, { + id: assistantMessageId, + role: 'assistant' as const, + content: currentContent, + timestamp: Date.now(), + toolCalls: [...toolCallsForMessage], + }]; + } + }); } break; case 'tool_result': if (chunk.toolCall) { const tc = chunk.toolCall; - setCurrentToolCalls(prev => - prev.map(t => t.id === tc.id ? { ...t, result: tc.result, status: 'completed' } : t) - ); - // Update the tool call in the message too - const idx = toolCallsForMessage.findIndex(t => t.id === tc.id); - if (idx >= 0) { - toolCallsForMessage[idx] = { ...toolCallsForMessage[idx], result: tc.result, status: 'completed' }; + // Update the tool call status - try ID first, then find first running tool with same name + let idx = toolCallsForMessage.findIndex(t => t.id === tc.id); + if (idx < 0) { + // ID didn't match - find the first RUNNING tool with same name (not already completed) + idx = toolCallsForMessage.findIndex(t => t.name === tc.name && t.status === 'running'); } + if (idx < 0) { + // Still no match - find any tool with same name + idx = toolCallsForMessage.findIndex(t => t.name === tc.name && !t.result); + } + if (idx >= 0) { + toolCallsForMessage[idx] = { + ...toolCallsForMessage[idx], + result: tc.result, + status: 'completed' + }; + } + // Same logic for currentToolCalls + setCurrentToolCalls(prev => { + // Find by ID first + let targetIdx = prev.findIndex(t => t.id === tc.id); + if (targetIdx < 0) { + // Find first running tool with same name + targetIdx = prev.findIndex(t => t.name === tc.name && t.status === 'running'); + } + if (targetIdx < 0) { + // Find any incomplete tool with same name + targetIdx = prev.findIndex(t => t.name === tc.name && !t.result); + } + if (targetIdx >= 0) { + return prev.map((t, i) => i === targetIdx + ? { ...t, result: tc.result, status: 'completed' } + : t + ); + } + return prev; + }); + // Update message with completed tool call + setChatMessages(prev => { + const existing = prev.find(m => m.id === assistantMessageId); + if (existing) { + return prev.map(m => + m.id === assistantMessageId + ? { ...m, toolCalls: [...toolCallsForMessage] } + : m + ); + } + return prev; + }); } break; @@ -432,22 +529,23 @@ export const AppStateProvider = ({ children }: { children: ReactNode }) => { break; case 'done': - // Finalize the assistant message with all tool calls + // Finalize the assistant message setChatMessages(prev => { const existing = prev.find(m => m.id === assistantMessageId); + const finalContent = assistantContent || (reasoningSteps.length > 0 ? reasoningSteps.join('\n\n') : ''); if (existing) { return prev.map(m => m.id === assistantMessageId - ? { ...m, content: assistantContent, toolCalls: toolCallsForMessage } + ? { ...m, content: finalContent, toolCalls: [...toolCallsForMessage] } : m ); - } else if (assistantContent) { + } else if (finalContent || toolCallsForMessage.length > 0) { return [...prev, { id: assistantMessageId, role: 'assistant' as const, - content: assistantContent, + content: finalContent, timestamp: Date.now(), - toolCalls: toolCallsForMessage, + toolCalls: [...toolCallsForMessage], }]; } return prev; diff --git a/src/workers/ingestion.worker.ts b/src/workers/ingestion.worker.ts index e6823a5c5..f70a7b2e4 100644 --- a/src/workers/ingestion.worker.ts +++ b/src/workers/ingestion.worker.ts @@ -26,6 +26,9 @@ const getKuzuAdapter = async () => { let embeddingProgress: EmbeddingProgress | null = null; let isEmbeddingComplete = false; +// File contents state - stores full file contents for grep/read tools +let storedFileContents: Map = new Map(); + // Agent state let currentAgent: ReturnType | null = null; let currentProviderConfig: ProviderConfig | null = null; @@ -50,6 +53,9 @@ const workerApi = { // Run the actual pipeline const result = await runIngestionPipeline(file, onProgress); + // Store file contents for grep/read tools (full content, not truncated) + storedFileContents = result.fileContents; + // Load graph into KuzuDB for querying (optional - gracefully degrades) try { onProgress({ @@ -67,8 +73,9 @@ const workerApi = { await kuzu.loadGraphToKuzu(result.graph, result.fileContents); if (import.meta.env.DEV) { - const stats = await kuzu.getKuzuStats(); - console.log('KuzuDB loaded:', stats); + const stats = await kuzu.getKuzuStats(); + console.log('KuzuDB loaded:', stats); + console.log('📁 Stored', storedFileContents.size, 'files for grep/read tools'); } } catch { // KuzuDB is optional - silently continue without it @@ -136,6 +143,9 @@ const workerApi = { // Run the pipeline const result = await runPipelineFromFiles(files, onProgress); + // Store file contents for grep/read tools (full content, not truncated) + storedFileContents = result.fileContents; + // Load graph into KuzuDB for querying (optional - gracefully degrades) try { onProgress({ @@ -155,6 +165,7 @@ const workerApi = { if (import.meta.env.DEV) { const stats = await kuzu.getKuzuStats(); console.log('KuzuDB loaded:', stats); + console.log('📁 Stored', storedFileContents.size, 'files for grep/read tools'); } } catch { // KuzuDB is optional - silently continue without it @@ -328,7 +339,8 @@ const workerApi = { kuzu.executeQuery, semanticSearchWrapper, semanticSearchWithContextWrapper, - () => isEmbeddingComplete + () => isEmbeddingComplete, + storedFileContents ); currentProviderConfig = config;