From 467d279cdf8b2cd2939990a5670c3d56e0e82e66 Mon Sep 17 00:00:00 2001 From: Ian He Date: Thu, 15 May 2025 12:14:05 +1200 Subject: [PATCH 1/6] introduce chat api to the project supported by langchain's sqlagent * remove project service to drop support for legacy version of subql node --- packages/query/README.md | 27 + packages/query/package.json | 6 + packages/query/src/app.module.ts | 5 +- .../query/src/configure/configure.module.ts | 18 +- packages/query/src/graphql/graphql.module.ts | 8 +- packages/query/src/graphql/project.service.ts | 39 -- packages/query/src/llm/chat.module.ts | 186 ++++++ packages/query/src/llm/createLLM.ts | 35 ++ packages/query/src/yargs.ts | 12 + yarn.lock | 593 +++++++++++++++++- 10 files changed, 871 insertions(+), 58 deletions(-) delete mode 100644 packages/query/src/graphql/project.service.ts create mode 100644 packages/query/src/llm/chat.module.ts create mode 100644 packages/query/src/llm/createLLM.ts diff --git a/packages/query/README.md b/packages/query/README.md index c330ca2af4..bea858b148 100644 --- a/packages/query/README.md +++ b/packages/query/README.md @@ -17,3 +17,30 @@ then run the following command ```sh NODE_OPTIONS="-r dotenv/config" yarn start:dev -- --name --playground ``` + +## LLM Configuration + +Suggest to use reasoning models to archive better quality of results. + +### Choose one of the following providers: + +### Option 1: OpenAI + +LLM_PROVIDER=openai +LLM_MODEL=o1 # Optional: defaults to '4o-mini' +OPENAI_API_KEY=your_openai_api_key + +### Option 2: Anthropic Claude + +LLM_PROVIDER=anthropic +LLM_MODEL=claude-3-7-sonnet-latest # Optional: defaults to 'claude-3-7-sonnet-latest' +ANTHROPIC_API_KEY=your_anthropic_api_key + +### Option 3: Custom OpenAI-compatible endpoint + +LLM_PROVIDER=openai +LLM_BASE_URL=http://your-llm-endpoint/v1 # Required for custom provider +LLM_MODEL=your-model-name # Required: model name for custom endpoint +OPENAI_API_KEY=your_api_key # Optional: API key for custom endpoint + +### Common LLM Settings diff --git a/packages/query/package.json b/packages/query/package.json index 96f7744593..27e548959b 100644 --- a/packages/query/package.json +++ b/packages/query/package.json @@ -35,6 +35,10 @@ "@graphile-contrib/pg-simplify-inflector": "^6.1.0", "@graphile/pg-aggregates": "^0.1.1", "@graphile/pg-pubsub": "^4.13.0", + "@langchain/anthropic": "^0.3.20", + "@langchain/core": "^0.3.55", + "@langchain/langgraph": "^0.2.71", + "@langchain/openai": "^0.5.10", "@nestjs/common": "^9.4.0", "@nestjs/core": "^9.4.0", "@nestjs/platform-express": "^9.4.0", @@ -50,12 +54,14 @@ "graphql": "^15.8.0", "graphql-query-complexity": "^0.11.0", "graphql-ws": "^5.16.0", + "langchain": "^0.3.24", "lodash": "^4.17.21", "pg": "^8.12.0", "pg-tsquery": "^8.4.2", "postgraphile": "^4.13.0", "postgraphile-plugin-connection-filter": "^2.2.2", "rxjs": "^7.1.0", + "typeorm": "^0.3.23", "ws": "^8.18.0", "yargs": "^16.2.0" }, diff --git a/packages/query/src/app.module.ts b/packages/query/src/app.module.ts index 91306f6ead..49548e1173 100644 --- a/packages/query/src/app.module.ts +++ b/packages/query/src/app.module.ts @@ -4,9 +4,12 @@ import {Module} from '@nestjs/common'; import {ConfigureModule} from './configure/configure.module'; import {GraphqlModule} from './graphql/graphql.module'; +import {ChatModule} from './llm/chat.module'; @Module({ - imports: [ConfigureModule.register(), GraphqlModule], + // the order is essential, the ChatModule must be before the GraphqlModule so /v1/chat/completions + // can be handled without interference from the GraphqlModule + imports: [ConfigureModule.register(), ChatModule, GraphqlModule], controllers: [], }) export class AppModule {} diff --git a/packages/query/src/configure/configure.module.ts b/packages/query/src/configure/configure.module.ts index 9ab093a424..578898cfe3 100644 --- a/packages/query/src/configure/configure.module.ts +++ b/packages/query/src/configure/configure.module.ts @@ -5,6 +5,7 @@ import {ConnectionOptions} from 'tls'; import {DynamicModule, Global, Module} from '@nestjs/common'; import {getFileContent, CONNECTION_SSL_ERROR_REGEX} from '@subql/common'; import {Pool, PoolConfig} from 'pg'; +import {DataSource} from 'typeorm'; import {getLogger} from '../utils/logger'; import {getYargsOption} from '../yargs'; import {Config} from './config'; @@ -85,6 +86,17 @@ export class ConfigureModule { pgClient._explainResults = []; }); } + // todo: support ssl + const dataSource = new DataSource({ + type: 'postgres', + host: config.get('DB_HOST_READ'), + port: config.get('DB_PORT'), + username: config.get('DB_USER'), + password: config.get('DB_PASS'), + database: config.get('DB_DATABASE'), + schema: config.get('name'), + }); + await dataSource.initialize(); return { module: ConfigureModule, providers: [ @@ -96,8 +108,12 @@ export class ConfigureModule { provide: Pool, useValue: pgPool, }, + { + provide: DataSource, + useValue: dataSource, + }, ], - exports: [Config, Pool], + exports: [Config, Pool, DataSource], }; } } diff --git a/packages/query/src/graphql/graphql.module.ts b/packages/query/src/graphql/graphql.module.ts index 4a21da0d5b..816c1cd12b 100644 --- a/packages/query/src/graphql/graphql.module.ts +++ b/packages/query/src/graphql/graphql.module.ts @@ -30,7 +30,6 @@ import {playgroundPlugin} from './plugins/PlaygroundPlugin'; import {queryAliasLimit} from './plugins/QueryAliasLimitPlugin'; import {queryComplexityPlugin} from './plugins/QueryComplexityPlugin'; import {queryDepthLimitPlugin} from './plugins/QueryDepthLimitPlugin'; -import {ProjectService} from './project.service'; const {argv} = getYargsOption(); const logger = getLogger('graphql-module'); @@ -45,7 +44,7 @@ class NoInitError extends Error { } } @Module({ - providers: [ProjectService], + providers: [], }) export class GraphqlModule implements OnModuleInit, OnModuleDestroy { private _apolloServer?: ApolloServer; @@ -53,8 +52,7 @@ export class GraphqlModule implements OnModuleInit, OnModuleDestroy { constructor( private readonly httpAdapterHost: HttpAdapterHost, private readonly config: Config, - private readonly pgPool: Pool, - private readonly projectService: ProjectService + private readonly pgPool: Pool ) {} private get apolloServer(): ApolloServer { @@ -138,7 +136,7 @@ export class GraphqlModule implements OnModuleInit, OnModuleDestroy { const schemaName = this.config.get('name'); if (!schemaName) throw new Error('Unable to get schema name from config'); - const dbSchema = await this.projectService.getProjectSchema(schemaName); + const dbSchema = schemaName; let options: PostGraphileCoreOptions = { replaceAllPlugins: plugins, subscriptions: true, diff --git a/packages/query/src/graphql/project.service.ts b/packages/query/src/graphql/project.service.ts deleted file mode 100644 index 6656153aea..0000000000 --- a/packages/query/src/graphql/project.service.ts +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright 2020-2025 SubQuery Pte Ltd authors & contributors -// SPDX-License-Identifier: GPL-3.0 - -import {Injectable} from '@nestjs/common'; -import {Pool} from 'pg'; -import {Config} from '../configure'; - -@Injectable() -export class ProjectService { - constructor( - private readonly pool: Pool, - private readonly config: Config - ) {} - - async getProjectSchema(name: string): Promise { - // After subqueries table has been deprecated, project may not be present in subqueries table - const result = await this.pool - .query<{schema_name: string}, any[]>(`SELECT schema_name FROM information_schema.schemata`) - .then((obj) => obj.rows.map((x) => x.schema_name)) - .catch((e) => { - throw new Error(`Unable to fetch all database schemas: ${e}`); - }); - if (result.includes(name)) { - return name; - } else { - // fallback to subqueries table - const {rows} = await this.pool.query( - `SELECT * - FROM public.subqueries - WHERE name = $1`, - [name] - ); - if (rows.length === 0) { - throw new Error(`unknown project name ${this.config.get('name')}`); - } - return rows[0].db_schema; - } - } -} diff --git a/packages/query/src/llm/chat.module.ts b/packages/query/src/llm/chat.module.ts new file mode 100644 index 0000000000..6164af7e1a --- /dev/null +++ b/packages/query/src/llm/chat.module.ts @@ -0,0 +1,186 @@ +// Copyright 2020-2025 SubQuery Pte Ltd authors & contributors +// SPDX-License-Identifier: GPL-3.0 + +import { BaseMessage, SystemMessage } from '@langchain/core/messages'; +import { createReactAgent } from '@langchain/langgraph/prebuilt'; +import { Module, OnModuleInit } from '@nestjs/common'; +import { HttpAdapterHost } from '@nestjs/core'; +import { SqlToolkit } from 'langchain/agents/toolkits/sql'; +import { SqlDatabase } from 'langchain/sql_db'; +import { DataSource } from 'typeorm'; +import { Config } from '../configure'; +import { getLogger } from '../utils/logger'; +import { getYargsOption } from '../yargs'; +import { createLLM } from './createLLM'; + +const { argv } = getYargsOption(); +const logger = getLogger('chat-module'); + +@Module({ + providers: [], +}) +export class ChatModule implements OnModuleInit { + agent?: ReturnType; + + constructor( + private readonly httpAdapterHost: HttpAdapterHost, + private readonly dataSource: DataSource, + private readonly config: Config + ) {} + + onModuleInit(): void { + if (!this.httpAdapterHost) { + return; + } + try { + this.createServer(); + } catch (e: any) { + throw new Error(`create apollo server failed, ${e.message}`); + } + } + + private async initializeAgent() { + const db = await SqlDatabase.fromDataSourceParams({ + appDataSource: this.dataSource, + }); + + const llm = createLLM(); + + const toolkit = new SqlToolkit(db, llm); + + this.agent = createReactAgent({ + llm, + tools: toolkit.getTools(), + prompt: new SystemMessage( + `You are an AI assistant that helps users query their PostgreSQL database using natural language. + +When generating SQL queries: +* Always use the correct schema (${this.config.get('name') || 'public'}) +* Only query tables that are available to you +* Never mutate database, including add/update/remove record from any table, run any DLL statements, only read. +* Always limit the query with maximum 100 rows +* Format your responses in a clear, readable way +* If you're unsure about the schema or table structure, ask for clarification +* If a table has column _block_range, it is a versioned table, You MUST always add \`_block_range @> 9223372036854775807\` to the where clause for all queries +* If it is a join query, \`_block_range @> 9223372036854775807\` is needed for all tables in the join` + ), + }); + } + + private createServer() { + const app = this.httpAdapterHost.httpAdapter.getInstance(); + + if (argv.chat) { + app.post('/v1/chat/completions', async (req, res) => { + try { + if (!this.agent) { + await this.initializeAgent(); + } + + const { messages, stream = false } = req.body; + + if (!messages || !Array.isArray(messages) || messages.length === 0) { + return res.status(400).json({ + error: { + message: 'messages is required and must be a non-empty array', + type: 'invalid_request_error', + code: 'invalid_messages', + }, + }); + } + + // Convert OpenAI format messages to LangChain format + const lastMessage = messages[messages.length - 1]; + const question = lastMessage.content; + + res.setHeader('Content-Type', 'text/event-stream'); + res.setHeader('Cache-Control', 'no-cache'); + res.setHeader('Connection', 'keep-alive'); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const result = await this.agent!.stream({ messages: [['user', question]] }, { streamMode: 'values' }); + + let fullResponse = ''; + for await (const event of result) { + const lastMsg: BaseMessage = event.messages[event.messages.length - 1]; + if (lastMsg.content) { + fullResponse = lastMsg.content as string; + logger.info(`Streaming response: ${JSON.stringify(lastMsg)}`); + if (argv['llm-debug'] && stream) { + // todo: send them as thinking details + res.write( + `data: ${JSON.stringify({ + id: `chatcmpl-${Date.now()}`, + object: 'chat.completion.chunk', + created: Math.floor(Date.now() / 1000), + model: process.env.OPENAI_MODEL, + choices: [ + { + index: 0, + delta: { content: lastMsg.content }, + finish_reason: null, + }, + ], + })}\n\n` + ); + } + } + } + + // Send final message + if (stream) { + res.write( + `data: ${JSON.stringify({ + id: `chatcmpl-${Date.now()}`, + object: 'chat.completion.chunk', + created: Math.floor(Date.now() / 1000), + model: process.env.OPENAI_MODEL, + choices: [ + { + index: 0, + message: { role: 'assistant', content: fullResponse }, + finish_reason: 'stop', + }, + ], + })}\n\n` + ); + } else { + res.write( + `data: ${JSON.stringify({ + id: `chatcmpl-${Date.now()}`, + object: 'chat.completion', + created: Math.floor(Date.now() / 1000), + model: process.env.OPENAI_MODEL, + choices: [ + { + index: 0, + message: { role: 'assistant', content: fullResponse }, + finish_reason: 'stop', + }, + ], + })}\n\n` + ); + } + res.end(); + } catch (error) { + logger.error('Error processing request:', error); + res.status(500).json({ + error: { + message: (error as any).message, + type: 'internal_server_error', + }, + }); + } + }); + } else { + app.post('/v1/chat/completions', (req, res) => { + res.status(404).json({ + error: { + message: 'Chat completions API is not enabled', + type: 'invalid_request_error', + code: 'chat_api_not_enabled', + }, + }); + }); + } + } +} diff --git a/packages/query/src/llm/createLLM.ts b/packages/query/src/llm/createLLM.ts new file mode 100644 index 0000000000..22be4a8082 --- /dev/null +++ b/packages/query/src/llm/createLLM.ts @@ -0,0 +1,35 @@ +// Copyright 2020-2025 SubQuery Pte Ltd authors & contributors +// SPDX-License-Identifier: GPL-3.0 + +import {ChatAnthropic} from '@langchain/anthropic'; +import {ChatOpenAI} from '@langchain/openai'; + +export function createLLM() { + const baseConfig = { + model: process.env.LLM_MODEL || 'gpt-4o', + }; + + // Determine which provider to use + const provider = process.env.LLM_PROVIDER?.toLowerCase() || 'openai'; + + switch (provider) { + case 'anthropic': + return new ChatAnthropic({ + ...baseConfig, + anthropicApiKey: process.env.ANTHROPIC_API_KEY, + // Claude models have different names + model: process.env.LLM_MODEL || 'claude-3-7-sonnet-latest', + }); + case 'openai': + default: + return new ChatOpenAI({ + ...baseConfig, + openAIApiKey: process.env.OPENAI_API_KEY || 'not-needed', + configuration: process.env.LLM_BASE_URL + ? { + baseURL: process.env.LLM_BASE_URL, + } + : undefined, + }); + } +} diff --git a/packages/query/src/yargs.ts b/packages/query/src/yargs.ts index 7267433ff5..84947d8dbd 100644 --- a/packages/query/src/yargs.ts +++ b/packages/query/src/yargs.ts @@ -16,6 +16,18 @@ export function getYargsOption() { describe: 'Enable aggregate feature', type: 'boolean', }, + chat: { + demandOption: false, + describe: 'enable openai compatible chat api, /v1/chat/completions', + type: 'boolean', + default: true, + }, + 'chat-debug': { + demandOption: false, + default: false, + describe: 'Enable debug for llm chat feature', + type: 'boolean', + }, 'disable-hot-schema': { demandOption: false, describe: 'Hot reload schema on schema-changes', diff --git a/yarn.lock b/yarn.lock index 789e5f470a..9f5653e036 100644 --- a/yarn.lock +++ b/yarn.lock @@ -129,6 +129,21 @@ __metadata: languageName: node linkType: hard +"@anthropic-ai/sdk@npm:^0.39.0": + version: 0.39.0 + resolution: "@anthropic-ai/sdk@npm:0.39.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 + checksum: 47f00c9d3ca2d497b25c688cdfd923474e2f16e774f67a3e2ef3f0c24cd80412687e3a2caee3024dd9a5d7a95e0717d85e3ffc9c4ba75ff6f1641d5820394a76 + languageName: node + linkType: hard + "@apollo/client@npm:^3.11.2": version: 3.11.2 resolution: "@apollo/client@npm:3.11.2" @@ -3234,6 +3249,13 @@ __metadata: languageName: node linkType: hard +"@cfworker/json-schema@npm:^4.0.2": + version: 4.1.1 + resolution: "@cfworker/json-schema@npm:4.1.1" + checksum: 35b5b246eff7bc75a17befb6e6d56475ab9261279c5d727610dc6827cce557d11db353cca3c06b8272f3974eb2ac508a7bbae3accd3d6c8402dfe0aafbfea0aa + languageName: node + linkType: hard + "@codama/errors@npm:1.3.0": version: 1.3.0 resolution: "@codama/errors@npm:1.3.0" @@ -5248,6 +5270,114 @@ __metadata: languageName: node linkType: hard +"@langchain/anthropic@npm:^0.3.20": + version: 0.3.20 + resolution: "@langchain/anthropic@npm:0.3.20" + dependencies: + "@anthropic-ai/sdk": ^0.39.0 + fast-xml-parser: ^4.4.1 + zod: ^3.22.4 + zod-to-json-schema: ^3.22.4 + peerDependencies: + "@langchain/core": ">=0.3.48 <0.4.0" + checksum: a7dcf60cc089696a7c9ff72ba89a2ceb79f902831d1e5d1e747473bfa20c870c16d30e25370252bf78d3dc812e49b3133463432d0792777a81bc6616d95aff0d + languageName: node + linkType: hard + +"@langchain/core@npm:^0.3.55": + version: 0.3.55 + resolution: "@langchain/core@npm:0.3.55" + 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.16 + mustache: ^4.2.0 + p-queue: ^6.6.2 + p-retry: 4 + uuid: ^10.0.0 + zod: ^3.22.4 + zod-to-json-schema: ^3.22.3 + checksum: 8a433f6f3118c0abfb9209cb31843e35a73f1f38aa8565383ec8ea5bd12e217be9369c88f4fb60215730f3248a069bcfbb9c036ea30fdbf2827c50c844ed8620 + languageName: node + linkType: hard + +"@langchain/langgraph-checkpoint@npm:~0.0.17": + version: 0.0.17 + resolution: "@langchain/langgraph-checkpoint@npm:0.0.17" + dependencies: + uuid: ^10.0.0 + peerDependencies: + "@langchain/core": ">=0.2.31 <0.4.0" + checksum: c8dc429e66ff5b0f31037c493c4f8687196f1800d52b48e8ff04829742cd7200af66ce46dee93ff3c0f6b6e6c2af36336f09626a5ce296ec610383a3bd7131ef + languageName: node + linkType: hard + +"@langchain/langgraph-sdk@npm:~0.0.32": + version: 0.0.74 + resolution: "@langchain/langgraph-sdk@npm:0.0.74" + 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 + peerDependenciesMeta: + "@langchain/core": + optional: true + react: + optional: true + checksum: 816a680333c96ddeda1aca722f11fc4e9e21687b9faa908b4a340cb0107310ea71996681fc8379842e1a990dbc7d9cda1d8dc35ed2ff9ba2f6ba8323c1c57058 + languageName: node + linkType: hard + +"@langchain/langgraph@npm:^0.2.71": + version: 0.2.71 + resolution: "@langchain/langgraph@npm:0.2.71" + dependencies: + "@langchain/langgraph-checkpoint": ~0.0.17 + "@langchain/langgraph-sdk": ~0.0.32 + uuid: ^10.0.0 + zod: ^3.23.8 + 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 + checksum: c8d3d9b381509a4b82731990dbc4134cf4725e64ead82c2bd2e1aac17e742aeda0e45cc0ae5748a21baa63d0c80095a6ea5cbd2361145dccba9f4f62932f89bf + languageName: node + linkType: hard + +"@langchain/openai@npm:>=0.1.0 <0.6.0, @langchain/openai@npm:^0.5.10": + version: 0.5.10 + resolution: "@langchain/openai@npm:0.5.10" + dependencies: + js-tiktoken: ^1.0.12 + openai: ^4.96.0 + zod: ^3.22.4 + zod-to-json-schema: ^3.22.3 + peerDependencies: + "@langchain/core": ">=0.3.48 <0.4.0" + checksum: 0fbd0f6781b2069b8276df5837ea4c98f2344485622cc0f44633c8401cc7b9f1fdd0c53cef4ba860fc95bf1d37e905dc3a8d138c938735a0df24d4ecdf3137f1 + languageName: node + linkType: hard + +"@langchain/textsplitters@npm:>=0.0.0 <0.2.0": + version: 0.1.0 + resolution: "@langchain/textsplitters@npm:0.1.0" + dependencies: + js-tiktoken: ^1.0.12 + peerDependencies: + "@langchain/core": ">=0.2.21 <0.4.0" + checksum: f179113423b003968127abc0b41c23e9ba21bed9336745841bf54b95e283fc3cbfcd67e40d1b58922b89b35953caeb38d8d5d7a3a49c1c5b202f1542bf76dc3a + languageName: node + linkType: hard + "@lukeed/csprng@npm:^1.0.0": version: 1.1.0 resolution: "@lukeed/csprng@npm:1.1.0" @@ -7636,6 +7766,13 @@ __metadata: languageName: node linkType: hard +"@sqltools/formatter@npm:^1.2.5": + version: 1.2.5 + resolution: "@sqltools/formatter@npm:1.2.5" + checksum: 9b8354e715467d660daa5afe044860b5686bbb1a5cb67a60866b932effafbf5e8b429f19a8ae67cd412065a4f067161f227e182f3664a0245339d5eb1e26e355 + languageName: node + linkType: hard + "@stellar/js-xdr@npm:^3.1.2": version: 3.1.2 resolution: "@stellar/js-xdr@npm:3.1.2" @@ -8032,6 +8169,10 @@ __metadata: "@graphile-contrib/pg-simplify-inflector": ^6.1.0 "@graphile/pg-aggregates": ^0.1.1 "@graphile/pg-pubsub": ^4.13.0 + "@langchain/anthropic": ^0.3.20 + "@langchain/core": ^0.3.55 + "@langchain/langgraph": ^0.2.71 + "@langchain/openai": ^0.5.10 "@nestjs/common": ^9.4.0 "@nestjs/core": ^9.4.0 "@nestjs/platform-express": ^9.4.0 @@ -8056,6 +8197,7 @@ __metadata: graphql: ^15.8.0 graphql-query-complexity: ^0.11.0 graphql-ws: ^5.16.0 + langchain: ^0.3.24 lodash: ^4.17.21 nodemon: ^3.1.4 pg: ^8.12.0 @@ -8063,6 +8205,7 @@ __metadata: postgraphile: ^4.13.0 postgraphile-plugin-connection-filter: ^2.2.2 rxjs: ^7.1.0 + typeorm: ^0.3.23 ws: ^8.18.0 yargs: ^16.2.0 bin: @@ -8849,6 +8992,13 @@ __metadata: languageName: node linkType: hard +"@types/json-schema@npm:^7.0.15": + version: 7.0.15 + resolution: "@types/json-schema@npm:7.0.15" + checksum: 97ed0cb44d4070aecea772b7b2e2ed971e10c81ec87dd4ecc160322ffa55ff330dace1793489540e3e318d90942064bb697cc0f8989391797792d919737b3b98 + languageName: node + linkType: hard + "@types/json-schema@npm:^7.0.7, @types/json-schema@npm:^7.0.8, @types/json-schema@npm:^7.0.9": version: 7.0.11 resolution: "@types/json-schema@npm:7.0.11" @@ -8964,6 +9114,16 @@ __metadata: languageName: node linkType: hard +"@types/node-fetch@npm:^2.6.4": + version: 2.6.12 + resolution: "@types/node-fetch@npm:2.6.12" + dependencies: + "@types/node": "*" + form-data: ^4.0.0 + checksum: 9647e68f9a125a090220c38d77b3c8e669c488658ae7506f1b4f9568214beba087624b1705bba1dc76649a65281ce3fd5b400e15266cbef8088027fb88777557 + languageName: node + linkType: hard + "@types/node@npm:*, @types/node@npm:>=13.7.0": version: 17.0.31 resolution: "@types/node@npm:17.0.31" @@ -8985,6 +9145,15 @@ __metadata: languageName: node linkType: hard +"@types/node@npm:^18.11.18": + version: 18.19.100 + resolution: "@types/node@npm:18.19.100" + dependencies: + undici-types: ~5.26.4 + checksum: b84c705cd723526aa10f08dce2f7df69d824f6702e728efd73a2819c0c618a4e0be4eedbca40454827625b00c0f5e1fd5ac4aedc81cb8b9222643f7d78dae473 + languageName: node + linkType: hard + "@types/node@npm:^18.19.42": version: 18.19.42 resolution: "@types/node@npm:18.19.42" @@ -9114,6 +9283,13 @@ __metadata: languageName: node linkType: hard +"@types/retry@npm:0.12.0": + version: 0.12.0 + resolution: "@types/retry@npm:0.12.0" + checksum: 61a072c7639f6e8126588bf1eb1ce8835f2cb9c2aba795c4491cf6310e013267b0c8488039857c261c387e9728c1b43205099223f160bb6a76b4374f741b5603 + languageName: node + linkType: hard + "@types/rimraf@npm:3.0.2": version: 3.0.2 resolution: "@types/rimraf@npm:3.0.2" @@ -9199,6 +9375,13 @@ __metadata: languageName: node linkType: hard +"@types/uuid@npm:^10.0.0": + version: 10.0.0 + resolution: "@types/uuid@npm:10.0.0" + checksum: e3958f8b0fe551c86c14431f5940c3470127293280830684154b91dc7eb3514aeb79fe3216968833cf79d4d1c67f580f054b5be2cd562bebf4f728913e73e944 + languageName: node + linkType: hard + "@types/validator@npm:^13.11.8": version: 13.11.9 resolution: "@types/validator@npm:13.11.9" @@ -10122,6 +10305,13 @@ __metadata: languageName: node linkType: hard +"ansis@npm:^3.17.0": + version: 3.17.0 + resolution: "ansis@npm:3.17.0" + checksum: 6fd6bc4d1187b894d9706f4c141c81b788e90766426617385486dae38f8b2f5a1726d8cc754939e44265f92a9db4647d5136cb1425435c39ac42b35e3acf4f3d + languageName: node + linkType: hard + "any-promise@npm:^1.0.0": version: 1.3.0 resolution: "any-promise@npm:1.3.0" @@ -10256,6 +10446,13 @@ __metadata: languageName: node linkType: hard +"app-root-path@npm:^3.1.0": + version: 3.1.0 + resolution: "app-root-path@npm:3.1.0" + checksum: e3db3957aee197143a0f6c75e39fe89b19e7244f28b4f2944f7276a9c526d2a7ab2d115b4b2d70a51a65a9a3ca17506690e5b36f75a068a7e5a13f8c092389ba + languageName: node + linkType: hard + "append-field@npm:^1.0.0": version: 1.0.0 resolution: "append-field@npm:1.0.0" @@ -10760,7 +10957,7 @@ __metadata: languageName: node linkType: hard -"base64-js@npm:^1.0.2, base64-js@npm:^1.3.0, base64-js@npm:^1.3.1": +"base64-js@npm:^1.0.2, base64-js@npm:^1.3.0, base64-js@npm:^1.3.1, base64-js@npm:^1.5.1": version: 1.5.1 resolution: "base64-js@npm:1.5.1" checksum: 669632eb3745404c2f822a18fc3a0122d2f9a7a13f7fb8b5823ee19d1d2ff9ee5b52c53367176ea4ad093c332fd5ab4bd0ebae5a8e27917a4105a4cfc86b1005 @@ -11446,6 +11643,13 @@ __metadata: languageName: node linkType: hard +"camelcase@npm:6, camelcase@npm:^6.2.0": + version: 6.3.0 + resolution: "camelcase@npm:6.3.0" + checksum: 8c96818a9076434998511251dcb2761a94817ea17dbdc37f47ac080bd088fc62c7369429a19e2178b993497132c8cbcf5cc1f44ba963e76782ba469c0474938d + languageName: node + linkType: hard + "camelcase@npm:^5.0.0, camelcase@npm:^5.3.1": version: 5.3.1 resolution: "camelcase@npm:5.3.1" @@ -11453,13 +11657,6 @@ __metadata: languageName: node linkType: hard -"camelcase@npm:^6.2.0": - version: 6.3.0 - resolution: "camelcase@npm:6.3.0" - checksum: 8c96818a9076434998511251dcb2761a94817ea17dbdc37f47ac080bd088fc62c7369429a19e2178b993497132c8cbcf5cc1f44ba963e76782ba469c0474938d - languageName: node - linkType: hard - "camelcase@npm:^7.0.1": version: 7.0.1 resolution: "camelcase@npm:7.0.1" @@ -12220,6 +12417,15 @@ __metadata: languageName: node linkType: hard +"console-table-printer@npm:^2.12.1": + version: 2.12.1 + resolution: "console-table-printer@npm:2.12.1" + dependencies: + simple-wcswidth: ^1.0.1 + checksum: 4d58fd4f18d3a69f421c9b0ffd44e5b0542677423491199e92c3e5ca0c023a06304a94bcc60f0d2b480a06cdf0720d2f228807f2af127abc8c905e73fcf64363 + languageName: node + linkType: hard + "content-disposition@npm:0.5.4": version: 0.5.4 resolution: "content-disposition@npm:0.5.4" @@ -12514,6 +12720,13 @@ __metadata: languageName: node linkType: hard +"dayjs@npm:^1.11.13": + version: 1.11.13 + resolution: "dayjs@npm:1.11.13" + checksum: f388db88a6aa93956c1f6121644e783391c7b738b73dbc54485578736565c8931bdfba4bb94e9b1535c6e509c97d5deb918bbe1ae6b34358d994de735055cca9 + languageName: node + linkType: hard + "debug@npm:2.6.9, debug@npm:^2.2.0, debug@npm:^2.6.9": version: 2.6.9 resolution: "debug@npm:2.6.9" @@ -12565,6 +12778,18 @@ __metadata: languageName: node linkType: hard +"debug@npm:^4.4.0": + version: 4.4.0 + resolution: "debug@npm:4.4.0" + dependencies: + ms: ^2.1.3 + peerDependenciesMeta: + supports-color: + optional: true + checksum: fb42df878dd0e22816fc56e1fdca9da73caa85212fbe40c868b1295a6878f9101ae684f4eeef516c13acfc700f5ea07f1136954f43d4cd2d477a811144136479 + languageName: node + linkType: hard + "debuglog@npm:^1.0.1": version: 1.0.1 resolution: "debuglog@npm:1.0.1" @@ -12572,6 +12797,13 @@ __metadata: languageName: node linkType: hard +"decamelize@npm:1.2.0": + version: 1.2.0 + resolution: "decamelize@npm:1.2.0" + checksum: ad8c51a7e7e0720c70ec2eeb1163b66da03e7616d7b98c9ef43cce2416395e84c1e9548dd94f5f6ffecfee9f8b94251fc57121a8b021f2ff2469b2bae247b8aa + languageName: node + linkType: hard + "decode-uri-component@npm:^0.2.0": version: 0.2.2 resolution: "decode-uri-component@npm:0.2.2" @@ -12826,6 +13058,13 @@ __metadata: languageName: node linkType: hard +"dotenv@npm:^16.4.7": + version: 16.5.0 + resolution: "dotenv@npm:16.5.0" + checksum: 6543fe87b5ddf2d60dd42df6616eec99148a5fc150cb4530fef5bda655db5204a3afa0e6f25f7cd64b20657ace4d79c0ef974bec32fdb462cad18754191e7a90 + languageName: node + linkType: hard + "dottie@npm:^2.0.2": version: 2.0.6 resolution: "dottie@npm:2.0.6" @@ -14099,6 +14338,17 @@ __metadata: languageName: node linkType: hard +"fast-xml-parser@npm:^4.4.1": + version: 4.5.3 + resolution: "fast-xml-parser@npm:4.5.3" + dependencies: + strnum: ^1.1.1 + bin: + fxparser: src/cli/cli.js + checksum: cd6a184941ec6c23f9e6b514421a3f396cfdff5f4a8c7c27bd0eff896edb4a2b55c27da16f09b789663613dfc4933602b9b71ac3e9d1d2ddcc0492fc46c8fa52 + languageName: node + linkType: hard + "fastest-levenshtein@npm:^1.0.7": version: 1.0.16 resolution: "fastest-levenshtein@npm:1.0.16" @@ -14375,6 +14625,13 @@ __metadata: languageName: node linkType: hard +"form-data-encoder@npm:1.7.2": + version: 1.7.2 + resolution: "form-data-encoder@npm:1.7.2" + checksum: aeebd87a1cb009e13cbb5e4e4008e6202ed5f6551eb6d9582ba8a062005178907b90f4887899d3c993de879159b6c0c940af8196725b428b4248cec5af3acf5f + languageName: node + linkType: hard + "form-data@npm:^4.0.0": version: 4.0.0 resolution: "form-data@npm:4.0.0" @@ -14397,6 +14654,16 @@ __metadata: languageName: node linkType: hard +"formdata-node@npm:^4.3.2": + version: 4.4.1 + resolution: "formdata-node@npm:4.4.1" + dependencies: + node-domexception: 1.0.0 + web-streams-polyfill: 4.0.0-beta.3 + checksum: d91d4f667cfed74827fc281594102c0dabddd03c9f8b426fc97123eedbf73f5060ee43205d89284d6854e2fc5827e030cd352ef68b93beda8decc2d72128c576 + languageName: node + linkType: hard + "formdata-polyfill@npm:^4.0.10": version: 4.0.10 resolution: "formdata-polyfill@npm:4.0.10" @@ -14889,7 +15156,7 @@ __metadata: languageName: node linkType: hard -"glob@npm:^10.3.7, glob@npm:^10.4": +"glob@npm:^10.3.7, glob@npm:^10.4, glob@npm:^10.4.5": version: 10.4.5 resolution: "glob@npm:10.4.5" dependencies: @@ -17127,6 +17394,15 @@ __metadata: languageName: node linkType: hard +"js-tiktoken@npm:^1.0.12": + version: 1.0.20 + resolution: "js-tiktoken@npm:1.0.20" + dependencies: + base64-js: ^1.5.1 + checksum: 29106a6faa65c85d13ead291ce17b007ef7c16918fdd570d4636b74da33e54b72af66f9d5dd963f2979733f70d4221e4a9655d236395719c5cc04620d9f1e2cd + languageName: node + linkType: hard + "js-tokens@npm:^3.0.0 || ^4.0.0, js-tokens@npm:^4.0.0": version: 4.0.0 resolution: "js-tokens@npm:4.0.0" @@ -17349,6 +17625,13 @@ __metadata: languageName: node linkType: hard +"jsonpointer@npm:^5.0.1": + version: 5.0.1 + resolution: "jsonpointer@npm:5.0.1" + checksum: 0b40f712900ad0c846681ea2db23b6684b9d5eedf55807b4708c656f5894b63507d0e28ae10aa1bddbea551241035afe62b6df0800fc94c2e2806a7f3adecd7c + languageName: node + linkType: hard + "jsonwebtoken@npm:^9.0.0": version: 9.0.0 resolution: "jsonwebtoken@npm:9.0.0" @@ -17428,6 +17711,100 @@ __metadata: languageName: node linkType: hard +"langchain@npm:^0.3.24": + version: 0.3.24 + resolution: "langchain@npm:0.3.24" + dependencies: + "@langchain/openai": ">=0.1.0 <0.6.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.16 + openapi-types: ^12.1.3 + p-retry: 4 + uuid: ^10.0.0 + yaml: ^2.2.1 + zod: ^3.22.4 + zod-to-json-schema: ^3.22.3 + peerDependencies: + "@langchain/anthropic": "*" + "@langchain/aws": "*" + "@langchain/cerebras": "*" + "@langchain/cohere": "*" + "@langchain/core": ">=0.2.21 <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 + checksum: 6d8426a89caab334ac90f3aaef83df870f5c2a875d7fb3b9d3741fcbe16bfba6856f82db157db158a541858bbab464554ad95d08c70aa4ff1dbc591225493462 + languageName: node + linkType: hard + +"langsmith@npm:^0.3.16": + version: 0.3.28 + resolution: "langsmith@npm:0.3.28" + dependencies: + "@types/uuid": ^10.0.0 + chalk: ^4.1.2 + console-table-printer: ^2.12.1 + p-queue: ^6.6.2 + p-retry: 4 + semver: ^7.6.3 + uuid: ^10.0.0 + peerDependencies: + openai: "*" + peerDependenciesMeta: + openai: + optional: true + checksum: 0057aa8bc40d6b22e7be6f294276b04e0d2842348d0b7e6f0b1e4ec3987572f68fd5e577559fde3341bb206c7504ce462c846b67e9f114488fbe30295ef0d4c9 + languageName: node + linkType: hard + "latest-version@npm:^5.1.0": version: 5.1.0 resolution: "latest-version@npm:5.1.0" @@ -18623,7 +19000,7 @@ __metadata: languageName: node linkType: hard -"mustache@npm:^4.0.0": +"mustache@npm:^4.0.0, mustache@npm:^4.2.0": version: 4.2.0 resolution: "mustache@npm:4.2.0" bin: @@ -18780,7 +19157,7 @@ __metadata: languageName: node linkType: hard -"node-domexception@npm:^1.0.0": +"node-domexception@npm:1.0.0, node-domexception@npm:^1.0.0": version: 1.0.0 resolution: "node-domexception@npm:1.0.0" checksum: ee1d37dd2a4eb26a8a92cd6b64dfc29caec72bff5e1ed9aba80c294f57a31ba4895a60fd48347cf17dd6e766da0ae87d75657dfd1f384ebfa60462c2283f5c7f @@ -19446,6 +19823,38 @@ __metadata: languageName: node linkType: hard +"openai@npm:^4.96.0": + version: 4.98.0 + resolution: "openai@npm:4.98.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 + peerDependencies: + ws: ^8.18.0 + zod: ^3.23.8 + peerDependenciesMeta: + ws: + optional: true + zod: + optional: true + bin: + openai: bin/cli + checksum: c16a64589ae6b917c6d1c5703047e6baa1f3186d37eb82299ceb6d3ba96222708758cedf9483b87b7cba3253fdfdd23e32899478c8a3f0154b4ef5e8b0cf437c + languageName: node + linkType: hard + +"openapi-types@npm:^12.1.3": + version: 12.1.3 + resolution: "openapi-types@npm:12.1.3" + checksum: 7fa5547f87a58d2aa0eba6e91d396f42d7d31bc3ae140e61b5d60b47d2fd068b48776f42407d5a8da7280cf31195aa128c2fc285e8bb871d1105edee5647a0bb + languageName: node + linkType: hard + "optimism@npm:^0.18.0": version: 0.18.0 resolution: "optimism@npm:0.18.0" @@ -19590,6 +19999,16 @@ __metadata: languageName: node linkType: hard +"p-retry@npm:4": + version: 4.6.2 + resolution: "p-retry@npm:4.6.2" + dependencies: + "@types/retry": 0.12.0 + retry: ^0.13.1 + checksum: 45c270bfddaffb4a895cea16cb760dcc72bdecb6cb45fef1971fa6ea2e91ddeafddefe01e444ac73e33b1b3d5d29fb0dd18a7effb294262437221ddc03ce0f2e + languageName: node + linkType: hard + "p-timeout@npm:^3.2.0": version: 3.2.0 resolution: "p-timeout@npm:3.2.0" @@ -21413,7 +21832,7 @@ __metadata: languageName: node linkType: hard -"retry@npm:0.13.1": +"retry@npm:0.13.1, retry@npm:^0.13.1": version: 0.13.1 resolution: "retry@npm:0.13.1" checksum: 47c4d5be674f7c13eee4cfe927345023972197dbbdfba5d3af7e461d13b44de1bfd663bfc80d2f601f8ef3fc8164c16dd99655a221921954a65d044a2fc1233b @@ -22034,6 +22453,13 @@ __metadata: languageName: node linkType: hard +"simple-wcswidth@npm:^1.0.1": + version: 1.0.1 + resolution: "simple-wcswidth@npm:1.0.1" + checksum: dc5bf4cb131d9c386825d1355add2b1ecc408b37dc2c2334edd7a1a4c9f527e6b594dedcdbf6d949bce2740c3a332e39af1183072a2d068e40d9e9146067a37f + languageName: node + linkType: hard + "sisteransi@npm:^1.0.5": version: 1.0.5 resolution: "sisteransi@npm:1.0.5" @@ -22271,6 +22697,13 @@ __metadata: languageName: node linkType: hard +"sql-highlight@npm:^6.0.0": + version: 6.0.0 + resolution: "sql-highlight@npm:6.0.0" + checksum: 34bfba3ada8e8f1ff9843f1dfa1386db4cf62be212676c300c9f455a6be202fced784098b16488c780198e109024ecb3efafef6ad5527fca46ebbf363080f31d + languageName: node + linkType: hard + "ssri@npm:^10.0.0": version: 10.0.5 resolution: "ssri@npm:10.0.5" @@ -22566,6 +22999,13 @@ __metadata: languageName: node linkType: hard +"strnum@npm:^1.1.1": + version: 1.1.2 + resolution: "strnum@npm:1.1.2" + checksum: a85219eda13e97151c95e343a9e5960eacfb0a0ff98104b4c9cb7a212e3008bddf0c9714c9c37c2e508be78e741a04afc80027c2dc18509d1b5ffd4c37191fc2 + languageName: node + linkType: hard + "subql-mono@workspace:.": version: 0.0.0-use.local resolution: "subql-mono@workspace:." @@ -23460,6 +23900,85 @@ __metadata: languageName: node linkType: hard +"typeorm@npm:^0.3.23": + version: 0.3.23 + resolution: "typeorm@npm:0.3.23" + dependencies: + "@sqltools/formatter": ^1.2.5 + ansis: ^3.17.0 + app-root-path: ^3.1.0 + buffer: ^6.0.3 + dayjs: ^1.11.13 + debug: ^4.4.0 + dotenv: ^16.4.7 + glob: ^10.4.5 + sha.js: ^2.4.11 + sql-highlight: ^6.0.0 + tslib: ^2.8.1 + uuid: ^11.1.0 + yargs: ^17.7.2 + peerDependencies: + "@google-cloud/spanner": ^5.18.0 || ^6.0.0 || ^7.0.0 + "@sap/hana-client": ^2.12.25 + better-sqlite3: ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 + hdb-pool: ^0.1.6 + ioredis: ^5.0.4 + mongodb: ^5.8.0 || ^6.0.0 + mssql: ^9.1.1 || ^10.0.1 || ^11.0.1 + mysql2: ^2.2.5 || ^3.0.1 + oracledb: ^6.3.0 + pg: ^8.5.1 + pg-native: ^3.0.0 + pg-query-stream: ^4.0.0 + redis: ^3.1.1 || ^4.0.0 + reflect-metadata: ^0.1.14 || ^0.2.0 + sql.js: ^1.4.0 + sqlite3: ^5.0.3 + ts-node: ^10.7.0 + typeorm-aurora-data-api-driver: ^2.0.0 || ^3.0.0 + peerDependenciesMeta: + "@google-cloud/spanner": + optional: true + "@sap/hana-client": + optional: true + better-sqlite3: + optional: true + hdb-pool: + optional: true + ioredis: + optional: true + mongodb: + optional: true + mssql: + optional: true + mysql2: + optional: true + oracledb: + optional: true + pg: + optional: true + pg-native: + optional: true + pg-query-stream: + optional: true + redis: + optional: true + sql.js: + optional: true + sqlite3: + optional: true + ts-node: + optional: true + typeorm-aurora-data-api-driver: + optional: true + bin: + typeorm: cli.js + typeorm-ts-node-commonjs: cli-ts-node-commonjs.js + typeorm-ts-node-esm: cli-ts-node-esm.js + checksum: f42770241d784354b0b868de8f2bf2761eff21e76650f6634f1a9fa5e6788dcef26c7d2b95b1a150f205c4f934431291cdac0cb5a4320c1a3fa52450f51813e6 + languageName: node + linkType: hard + "typescript@npm:^5.7.3": version: 5.7.3 resolution: "typescript@npm:5.7.3" @@ -23986,6 +24505,24 @@ __metadata: languageName: node linkType: hard +"uuid@npm:^10.0.0": + version: 10.0.0 + resolution: "uuid@npm:10.0.0" + bin: + uuid: dist/bin/uuid + checksum: 4b81611ade2885d2313ddd8dc865d93d8dccc13ddf901745edca8f86d99bc46d7a330d678e7532e7ebf93ce616679fb19b2e3568873ac0c14c999032acb25869 + languageName: node + linkType: hard + +"uuid@npm:^11.1.0": + version: 11.1.0 + resolution: "uuid@npm:11.1.0" + bin: + uuid: dist/esm/bin/uuid + checksum: 840f19758543c4631e58a29439e51b5b669d5f34b4dd2700b6a1d15c5708c7a6e0c3e2c8c4a2eae761a3a7caa7e9884d00c86c02622ba91137bd3deade6b4b4a + languageName: node + linkType: hard + "uuid@npm:^9.0.0": version: 9.0.0 resolution: "uuid@npm:9.0.0" @@ -24186,6 +24723,13 @@ __metadata: languageName: node linkType: hard +"web-streams-polyfill@npm:4.0.0-beta.3": + version: 4.0.0-beta.3 + resolution: "web-streams-polyfill@npm:4.0.0-beta.3" + checksum: dfec1fbf52b9140e4183a941e380487b6c3d5d3838dd1259be81506c1c9f2abfcf5aeb670aeeecfd9dff4271a6d8fef931b193c7bedfb42542a3b05ff36c0d16 + languageName: node + linkType: hard + "web-streams-polyfill@npm:^3.0.3": version: 3.3.3 resolution: "web-streams-polyfill@npm:3.3.3" @@ -24684,6 +25228,15 @@ __metadata: languageName: node linkType: hard +"yaml@npm:^2.2.1": + version: 2.7.1 + resolution: "yaml@npm:2.7.1" + bin: + yaml: bin.mjs + checksum: 385f8115ddfafdf8e599813cca8b2bf4e3f6a01b919fff5ae7da277e164df684d7dfe558b4085172094792b5a04786d3c55fa8b74abb0ee029873f031150bb80 + languageName: node + linkType: hard + "yaml@npm:~2.4.2": version: 2.4.5 resolution: "yaml@npm:2.4.5" @@ -24849,6 +25402,22 @@ __metadata: languageName: node linkType: hard +"zod-to-json-schema@npm:^3.22.3, zod-to-json-schema@npm:^3.22.4": + version: 3.24.5 + resolution: "zod-to-json-schema@npm:3.24.5" + peerDependencies: + zod: ^3.24.1 + checksum: dc4e5e4c06e9a5494e4b1d8c8363ac907f9d488f36c8e4923e1e5ac4f91f737722f99200cd92a409551e7456d960734d4cabd37935234ca95e290572468ffc08 + languageName: node + linkType: hard + +"zod@npm:^3.22.4, zod@npm:^3.23.8": + version: 3.24.4 + resolution: "zod@npm:3.24.4" + checksum: 62829789765a9187bd72bed3972a7c1a39fdfe6c59bc752eedabec5f99af701658471b8577d22e0fee2081e6e35d4efc93c02c90e13350755a36feadbf72bbbc + languageName: node + linkType: hard + "zwitch@npm:^1.0.0": version: 1.0.5 resolution: "zwitch@npm:1.0.5" From 866d50f2cc513cd675d78007e5662cff37341f8d Mon Sep 17 00:00:00 2001 From: Ian He Date: Fri, 16 May 2025 12:12:23 +1200 Subject: [PATCH 2/6] return tool calling as reasoning content --- packages/query/src/llm/chat.module.ts | 58 ++++++++++++++++++++++----- 1 file changed, 49 insertions(+), 9 deletions(-) diff --git a/packages/query/src/llm/chat.module.ts b/packages/query/src/llm/chat.module.ts index 6164af7e1a..74595a5860 100644 --- a/packages/query/src/llm/chat.module.ts +++ b/packages/query/src/llm/chat.module.ts @@ -90,23 +90,45 @@ When generating SQL queries: } // Convert OpenAI format messages to LangChain format - const lastMessage = messages[messages.length - 1]; - const question = lastMessage.content; + // const lastMessage = messages[messages.length - 1]; + // const question = lastMessage.content; res.setHeader('Content-Type', 'text/event-stream'); res.setHeader('Cache-Control', 'no-cache'); res.setHeader('Connection', 'keep-alive'); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const result = await this.agent!.stream({ messages: [['user', question]] }, { streamMode: 'values' }); + + const result = await this.agent!.stream({ messages }, { streamMode: 'values' }); let fullResponse = ''; + let first = true; + let thinking = false; for await (const event of result) { const lastMsg: BaseMessage = event.messages[event.messages.length - 1]; - if (lastMsg.content) { - fullResponse = lastMsg.content as string; - logger.info(`Streaming response: ${JSON.stringify(lastMsg)}`); - if (argv['llm-debug'] && stream) { + fullResponse = lastMsg.content as string; + if (lastMsg.content && lastMsg.getType() === 'tool') { + if (argv['chat-debug'] && stream && lastMsg.response_metadata?.finish_reason !== 'stop') { + if (first) { + res.write( + `data: ${JSON.stringify({ + id: `chatcmpl-${Date.now()}`, + object: 'chat.completion.chunk', + created: Math.floor(Date.now() / 1000), + model: process.env.OPENAI_MODEL, + choices: [ + { + index: 0, + delta: { role: 'assistant', content: '\n\n' }, + finish_reason: null, + }, + ], + })}\n\n` + ); + first = false; + thinking = true; + } // todo: send them as thinking details + logger.info(`Streaming response: ${JSON.stringify(lastMsg)}`); res.write( `data: ${JSON.stringify({ id: `chatcmpl-${Date.now()}`, @@ -116,7 +138,7 @@ When generating SQL queries: choices: [ { index: 0, - delta: { content: lastMsg.content }, + delta: { content: `${lastMsg.name}: ${lastMsg.content} \n\n` }, finish_reason: null, }, ], @@ -124,10 +146,28 @@ When generating SQL queries: ); } } + if (lastMsg.response_metadata?.finish_reason === 'stop' && thinking) { + res.write( + `data: ${JSON.stringify({ + id: `chatcmpl-${Date.now()}`, + object: 'chat.completion.chunk', + created: Math.floor(Date.now() / 1000), + model: process.env.OPENAI_MODEL, + choices: [ + { + index: 0, + delta: { content: '\n\n' }, + finish_reason: null, + }, + ], + })}\n\n` + ); + } } // Send final message if (stream) { + logger.info(`Final response: ${JSON.stringify(fullResponse)}`); res.write( `data: ${JSON.stringify({ id: `chatcmpl-${Date.now()}`, @@ -137,7 +177,7 @@ When generating SQL queries: choices: [ { index: 0, - message: { role: 'assistant', content: fullResponse }, + delta: { role: 'assistant', content: fullResponse }, finish_reason: 'stop', }, ], From c131f35052e7d0e93a88c94f5bb1554d057704b5 Mon Sep 17 00:00:00 2001 From: Scott Twiname Date: Thu, 22 May 2025 15:16:12 +1200 Subject: [PATCH 3/6] Improve code reuse and improve error handling, compatibility --- packages/query/src/llm/chat.module.ts | 302 +++++++++++++------------- 1 file changed, 149 insertions(+), 153 deletions(-) diff --git a/packages/query/src/llm/chat.module.ts b/packages/query/src/llm/chat.module.ts index 74595a5860..a7d89b9b3b 100644 --- a/packages/query/src/llm/chat.module.ts +++ b/packages/query/src/llm/chat.module.ts @@ -1,26 +1,28 @@ // Copyright 2020-2025 SubQuery Pte Ltd authors & contributors // SPDX-License-Identifier: GPL-3.0 -import { BaseMessage, SystemMessage } from '@langchain/core/messages'; -import { createReactAgent } from '@langchain/langgraph/prebuilt'; -import { Module, OnModuleInit } from '@nestjs/common'; -import { HttpAdapterHost } from '@nestjs/core'; -import { SqlToolkit } from 'langchain/agents/toolkits/sql'; -import { SqlDatabase } from 'langchain/sql_db'; -import { DataSource } from 'typeorm'; -import { Config } from '../configure'; -import { getLogger } from '../utils/logger'; -import { getYargsOption } from '../yargs'; -import { createLLM } from './createLLM'; - -const { argv } = getYargsOption(); +import {BaseMessage, SystemMessage} from '@langchain/core/messages'; +import {createReactAgent} from '@langchain/langgraph/prebuilt'; +import {Module, OnModuleInit} from '@nestjs/common'; +import {HttpAdapterHost} from '@nestjs/core'; +import {SqlToolkit} from 'langchain/agents/toolkits/sql'; +import {SqlDatabase} from 'langchain/sql_db'; +import {DataSource} from 'typeorm'; +import {Config} from '../configure'; +import {getLogger} from '../utils/logger'; +import {getYargsOption} from '../yargs'; +import {createLLM} from './createLLM'; + +const {argv} = getYargsOption(); const logger = getLogger('chat-module'); +type Agent = ReturnType; + @Module({ providers: [], }) export class ChatModule implements OnModuleInit { - agent?: ReturnType; + agent?: Agent; constructor( private readonly httpAdapterHost: HttpAdapterHost, @@ -39,7 +41,8 @@ export class ChatModule implements OnModuleInit { } } - private async initializeAgent() { + private async initializeAgent(): Promise { + if (this.agent) return this.agent; const db = await SqlDatabase.fromDataSourceParams({ appDataSource: this.dataSource, }); @@ -62,165 +65,158 @@ When generating SQL queries: * Format your responses in a clear, readable way * If you're unsure about the schema or table structure, ask for clarification * If a table has column _block_range, it is a versioned table, You MUST always add \`_block_range @> 9223372036854775807\` to the where clause for all queries -* If it is a join query, \`_block_range @> 9223372036854775807\` is needed for all tables in the join` +* If it is a join query, \`_block_range @> 9223372036854775807\` is needed for all tables in the join +* If the query has an error consider the error and try again` ), }); + + return this.agent; } private createServer() { const app = this.httpAdapterHost.httpAdapter.getInstance(); - if (argv.chat) { - app.post('/v1/chat/completions', async (req, res) => { - try { - if (!this.agent) { - await this.initializeAgent(); - } + if (!argv.chat) { + app.post('/v1/chat/completions', (req, res) => { + res.status(404).json({ + error: { + message: 'Chat completions API is not enabled', + type: 'invalid_request_error', + code: 'chat_api_not_enabled', + }, + }); + }); - const { messages, stream = false } = req.body; + return; + } - if (!messages || !Array.isArray(messages) || messages.length === 0) { - return res.status(400).json({ - error: { - message: 'messages is required and must be a non-empty array', - type: 'invalid_request_error', - code: 'invalid_messages', - }, - }); - } + // Needed for some web UIs. eg. ghcr.io/open-webui/open-webui + app.get('/v1/models', (req, res) => { + return res.json({ + object: 'list', + data: [ + { + id: 'subql-ai', + object: 'model', + created: new Date().getTime(), + owner: 'SubQuery', + }, + ], + }); + }); - // Convert OpenAI format messages to LangChain format - // const lastMessage = messages[messages.length - 1]; - // const question = lastMessage.content; - - res.setHeader('Content-Type', 'text/event-stream'); - res.setHeader('Cache-Control', 'no-cache'); - res.setHeader('Connection', 'keep-alive'); - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - - const result = await this.agent!.stream({ messages }, { streamMode: 'values' }); - - let fullResponse = ''; - let first = true; - let thinking = false; - for await (const event of result) { - const lastMsg: BaseMessage = event.messages[event.messages.length - 1]; - fullResponse = lastMsg.content as string; - if (lastMsg.content && lastMsg.getType() === 'tool') { - if (argv['chat-debug'] && stream && lastMsg.response_metadata?.finish_reason !== 'stop') { - if (first) { - res.write( - `data: ${JSON.stringify({ - id: `chatcmpl-${Date.now()}`, - object: 'chat.completion.chunk', - created: Math.floor(Date.now() / 1000), - model: process.env.OPENAI_MODEL, - choices: [ - { - index: 0, - delta: { role: 'assistant', content: '\n\n' }, - finish_reason: null, - }, - ], - })}\n\n` - ); - first = false; - thinking = true; - } - // todo: send them as thinking details - logger.info(`Streaming response: ${JSON.stringify(lastMsg)}`); - res.write( - `data: ${JSON.stringify({ - id: `chatcmpl-${Date.now()}`, - object: 'chat.completion.chunk', - created: Math.floor(Date.now() / 1000), - model: process.env.OPENAI_MODEL, - choices: [ - { - index: 0, - delta: { content: `${lastMsg.name}: ${lastMsg.content} \n\n` }, - finish_reason: null, - }, - ], - })}\n\n` - ); - } - } - if (lastMsg.response_metadata?.finish_reason === 'stop' && thinking) { - res.write( - `data: ${JSON.stringify({ - id: `chatcmpl-${Date.now()}`, - object: 'chat.completion.chunk', - created: Math.floor(Date.now() / 1000), - model: process.env.OPENAI_MODEL, - choices: [ - { - index: 0, - delta: { content: '\n\n' }, - finish_reason: null, - }, - ], - })}\n\n` - ); - } - } + app.post('/v1/chat/completions', async (req, res) => { + try { + const {messages, stream = false} = req.body; - // Send final message - if (stream) { - logger.info(`Final response: ${JSON.stringify(fullResponse)}`); - res.write( - `data: ${JSON.stringify({ - id: `chatcmpl-${Date.now()}`, - object: 'chat.completion.chunk', - created: Math.floor(Date.now() / 1000), - model: process.env.OPENAI_MODEL, - choices: [ - { - index: 0, - delta: { role: 'assistant', content: fullResponse }, - finish_reason: 'stop', - }, - ], - })}\n\n` - ); - } else { - res.write( - `data: ${JSON.stringify({ - id: `chatcmpl-${Date.now()}`, - object: 'chat.completion', - created: Math.floor(Date.now() / 1000), - model: process.env.OPENAI_MODEL, - choices: [ + if (!messages || !Array.isArray(messages) || messages.length === 0) { + return res.status(400).json({ + error: { + message: 'messages is required and must be a non-empty array', + type: 'invalid_request_error', + code: 'invalid_messages', + }, + }); + } + + const sendMessage = (object: string, choices: any[]) => { + return res.write( + `data: ${JSON.stringify({ + id: `chatcmpl-${Date.now()}`, + object, + created: Math.floor(Date.now() / 1000), + model: process.env.OPENAI_MODEL, + choices, + })}\n\n` + ); + }; + + // Convert OpenAI format messages to LangChain format + // const lastMessage = messages[messages.length - 1]; + // const question = lastMessage.content; + + res.setHeader('Content-Type', 'text/event-stream'); + res.setHeader('Cache-Control', 'no-cache'); + res.setHeader('Connection', 'keep-alive'); + + const agent = await this.initializeAgent(); + const result = await agent.stream({messages}, {streamMode: 'values'}); + + let fullResponse = ''; + let first = true; + let thinking = false; + for await (const event of result) { + const lastMsg: BaseMessage = event.messages[event.messages.length - 1]; + fullResponse = lastMsg.content as string; + if (lastMsg.content && lastMsg.getType() === 'tool') { + if (argv['chat-debug'] && stream && lastMsg.response_metadata?.finish_reason !== 'stop') { + if (first) { + sendMessage('chat.completion.chunk', [ { index: 0, - message: { role: 'assistant', content: fullResponse }, - finish_reason: 'stop', + delta: {role: 'assistant', content: '\n\n'}, + finish_reason: null, }, - ], - })}\n\n` - ); + ]); + first = false; + thinking = true; + } + // todo: send them as thinking details + logger.info(`Streaming response: ${JSON.stringify(lastMsg)}`); + sendMessage('chat.completion.chunk', [ + { + index: 0, + delta: {content: `${lastMsg.name}: ${lastMsg.content} \n\n`}, + finish_reason: null, + }, + ]); + } + } + if (lastMsg.response_metadata?.finish_reason === 'stop' && thinking) { + sendMessage('chat.completion.chunk', [ + { + index: 0, + delta: {content: '\n\n'}, + finish_reason: null, + }, + ]); } - res.end(); - } catch (error) { - logger.error('Error processing request:', error); + } + + // Send final message + if (stream) { + logger.info(`Final response: ${JSON.stringify(fullResponse)}`); + sendMessage('chat.completion.chunk', [ + { + index: 0, + delta: {role: 'assistant', content: fullResponse}, + finish_reason: 'stop', + }, + ]); + } else { + sendMessage('chat.completion', [ + { + index: 0, + message: {role: 'assistant', content: fullResponse}, + finish_reason: 'stop', + }, + ]); + } + res.end(); + } catch (error) { + logger.error('Error processing request:', error); + + try { res.status(500).json({ error: { message: (error as any).message, type: 'internal_server_error', }, }); + } catch (e) { + logger.error('Failed to send error response', e); } - }); - } else { - app.post('/v1/chat/completions', (req, res) => { - res.status(404).json({ - error: { - message: 'Chat completions API is not enabled', - type: 'invalid_request_error', - code: 'chat_api_not_enabled', - }, - }); - }); - } + } + }); } } From 29ce5044122c3c5fba3750f60491f876f50aa0ab Mon Sep 17 00:00:00 2001 From: Scott Twiname Date: Thu, 22 May 2025 16:11:44 +1200 Subject: [PATCH 4/6] Support SSL with chat db connection, only create connection if chat enabled --- .../query/src/configure/configure.module.ts | 34 ++++++++++++------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/packages/query/src/configure/configure.module.ts b/packages/query/src/configure/configure.module.ts index 578898cfe3..5b9e6d37c6 100644 --- a/packages/query/src/configure/configure.module.ts +++ b/packages/query/src/configure/configure.module.ts @@ -1,7 +1,7 @@ // Copyright 2020-2025 SubQuery Pte Ltd authors & contributors // SPDX-License-Identifier: GPL-3.0 -import {ConnectionOptions} from 'tls'; +import {CommonConnectionOptions, SecureContextOptions} from 'tls'; import {DynamicModule, Global, Module} from '@nestjs/common'; import {getFileContent, CONNECTION_SSL_ERROR_REGEX} from '@subql/common'; import {Pool, PoolConfig} from 'pg'; @@ -41,7 +41,8 @@ export class ConfigureModule { }); const dbSslOption = () => { - const sslConfig: ConnectionOptions = {rejectUnauthorized: false}; + // This is a subset of ConnectionOptions and TlsOptions to satisfy PgPool and DataSource + const sslConfig: SecureContextOptions & CommonConnectionOptions = {rejectUnauthorized: false}; if (opts['pg-ca']) { try { sslConfig.ca = getFileContent(opts['pg-ca'], 'postgres ca cert'); @@ -86,17 +87,24 @@ export class ConfigureModule { pgClient._explainResults = []; }); } - // todo: support ssl - const dataSource = new DataSource({ - type: 'postgres', - host: config.get('DB_HOST_READ'), - port: config.get('DB_PORT'), - username: config.get('DB_USER'), - password: config.get('DB_PASS'), - database: config.get('DB_DATABASE'), - schema: config.get('name'), - }); - await dataSource.initialize(); + + // Only establish connection if chat is enabled + let dataSource: DataSource | undefined; + console.log('sslConfig', dbSslOption()); + if (opts.chat) { + dataSource = new DataSource({ + type: 'postgres', + host: config.get('DB_HOST_READ'), + port: config.get('DB_PORT'), + username: config.get('DB_USER'), + password: config.get('DB_PASS'), + database: config.get('DB_DATABASE'), + schema: config.get('name'), + ssl: opts['pg-ca'] ? dbSslOption() : undefined, // Cannot be an empty object + }); + await dataSource.initialize(); + } + return { module: ConfigureModule, providers: [ From db7c21c7fc8c5062c3458773586ddffecd94ade9 Mon Sep 17 00:00:00 2001 From: Scott Twiname Date: Mon, 26 May 2025 10:54:45 +1200 Subject: [PATCH 5/6] Add support for MCP --- packages/query/package.json | 2 + packages/query/src/app.module.ts | 3 +- packages/query/src/llm/chat.module.ts | 55 +----- packages/query/src/llm/chat.service.ts | 67 +++++++ packages/query/src/mcp/mcp.module.ts | 19 ++ packages/query/src/mcp/query.tool.ts | 32 ++++ yarn.lock | 254 ++++++++++++++++++++++++- 7 files changed, 373 insertions(+), 59 deletions(-) create mode 100644 packages/query/src/llm/chat.service.ts create mode 100644 packages/query/src/mcp/mcp.module.ts create mode 100644 packages/query/src/mcp/query.tool.ts diff --git a/packages/query/package.json b/packages/query/package.json index 27e548959b..88058fe064 100644 --- a/packages/query/package.json +++ b/packages/query/package.json @@ -39,9 +39,11 @@ "@langchain/core": "^0.3.55", "@langchain/langgraph": "^0.2.71", "@langchain/openai": "^0.5.10", + "@modelcontextprotocol/sdk": "^1.11.5", "@nestjs/common": "^9.4.0", "@nestjs/core": "^9.4.0", "@nestjs/platform-express": "^9.4.0", + "@rekog/mcp-nest": "^1.5.2", "@subql/common": "workspace:*", "@subql/utils": "workspace:*", "@subql/x-graphile-build-pg": "4.13.0-0.2.5", diff --git a/packages/query/src/app.module.ts b/packages/query/src/app.module.ts index 49548e1173..f5d407a224 100644 --- a/packages/query/src/app.module.ts +++ b/packages/query/src/app.module.ts @@ -5,11 +5,12 @@ import {Module} from '@nestjs/common'; import {ConfigureModule} from './configure/configure.module'; import {GraphqlModule} from './graphql/graphql.module'; import {ChatModule} from './llm/chat.module'; +import {MCPModule} from './mcp/mcp.module'; @Module({ // the order is essential, the ChatModule must be before the GraphqlModule so /v1/chat/completions // can be handled without interference from the GraphqlModule - imports: [ConfigureModule.register(), ChatModule, GraphqlModule], + imports: [ConfigureModule.register(), ChatModule, MCPModule, GraphqlModule], controllers: [], }) export class AppModule {} diff --git a/packages/query/src/llm/chat.module.ts b/packages/query/src/llm/chat.module.ts index a7d89b9b3b..f06f58ea85 100644 --- a/packages/query/src/llm/chat.module.ts +++ b/packages/query/src/llm/chat.module.ts @@ -1,17 +1,13 @@ // Copyright 2020-2025 SubQuery Pte Ltd authors & contributors // SPDX-License-Identifier: GPL-3.0 -import {BaseMessage, SystemMessage} from '@langchain/core/messages'; +import {BaseMessage} from '@langchain/core/messages'; import {createReactAgent} from '@langchain/langgraph/prebuilt'; import {Module, OnModuleInit} from '@nestjs/common'; import {HttpAdapterHost} from '@nestjs/core'; -import {SqlToolkit} from 'langchain/agents/toolkits/sql'; -import {SqlDatabase} from 'langchain/sql_db'; -import {DataSource} from 'typeorm'; -import {Config} from '../configure'; import {getLogger} from '../utils/logger'; import {getYargsOption} from '../yargs'; -import {createLLM} from './createLLM'; +import {ChatService} from './chat.service'; const {argv} = getYargsOption(); const logger = getLogger('chat-module'); @@ -19,15 +15,14 @@ const logger = getLogger('chat-module'); type Agent = ReturnType; @Module({ - providers: [], + providers: [ChatService], + + exports: [ChatService], }) export class ChatModule implements OnModuleInit { - agent?: Agent; - constructor( private readonly httpAdapterHost: HttpAdapterHost, - private readonly dataSource: DataSource, - private readonly config: Config + private readonly chatService: ChatService ) {} onModuleInit(): void { @@ -41,38 +36,6 @@ export class ChatModule implements OnModuleInit { } } - private async initializeAgent(): Promise { - if (this.agent) return this.agent; - const db = await SqlDatabase.fromDataSourceParams({ - appDataSource: this.dataSource, - }); - - const llm = createLLM(); - - const toolkit = new SqlToolkit(db, llm); - - this.agent = createReactAgent({ - llm, - tools: toolkit.getTools(), - prompt: new SystemMessage( - `You are an AI assistant that helps users query their PostgreSQL database using natural language. - -When generating SQL queries: -* Always use the correct schema (${this.config.get('name') || 'public'}) -* Only query tables that are available to you -* Never mutate database, including add/update/remove record from any table, run any DLL statements, only read. -* Always limit the query with maximum 100 rows -* Format your responses in a clear, readable way -* If you're unsure about the schema or table structure, ask for clarification -* If a table has column _block_range, it is a versioned table, You MUST always add \`_block_range @> 9223372036854775807\` to the where clause for all queries -* If it is a join query, \`_block_range @> 9223372036854775807\` is needed for all tables in the join -* If the query has an error consider the error and try again` - ), - }); - - return this.agent; - } - private createServer() { const app = this.httpAdapterHost.httpAdapter.getInstance(); @@ -131,15 +94,11 @@ When generating SQL queries: ); }; - // Convert OpenAI format messages to LangChain format - // const lastMessage = messages[messages.length - 1]; - // const question = lastMessage.content; - res.setHeader('Content-Type', 'text/event-stream'); res.setHeader('Cache-Control', 'no-cache'); res.setHeader('Connection', 'keep-alive'); - const agent = await this.initializeAgent(); + const agent = await this.chatService.getAgent(); const result = await agent.stream({messages}, {streamMode: 'values'}); let fullResponse = ''; diff --git a/packages/query/src/llm/chat.service.ts b/packages/query/src/llm/chat.service.ts new file mode 100644 index 0000000000..c617999238 --- /dev/null +++ b/packages/query/src/llm/chat.service.ts @@ -0,0 +1,67 @@ +// Copyright 2020-2025 SubQuery Pte Ltd authors & contributors +// SPDX-License-Identifier: GPL-3.0 + +import {SystemMessage} from '@langchain/core/messages'; +import {createReactAgent} from '@langchain/langgraph/prebuilt'; +import {Injectable} from '@nestjs/common'; +import {SqlToolkit} from 'langchain/agents/toolkits/sql'; +import {SqlDatabase} from 'langchain/sql_db'; +import {DataSource} from 'typeorm'; +import {Config} from '../configure'; +import {createLLM} from './createLLM'; + +type Agent = ReturnType; + +@Injectable() +export class ChatService { + constructor( + private readonly dataSource: DataSource, + private readonly config: Config + ) {} + + private agent?: Agent; + + async getAgent(): Promise { + if (this.agent) return this.agent; + const db = await SqlDatabase.fromDataSourceParams({ + appDataSource: this.dataSource, + }); + + const llm = createLLM(); + + const toolkit = new SqlToolkit(db, llm); + + this.agent = createReactAgent({ + llm, + tools: toolkit.getTools(), + prompt: new SystemMessage( + `You are an AI assistant that helps users query their PostgreSQL database using natural language. + +When generating SQL queries: +* Always use the correct schema (${this.config.get('name') || 'public'}) +* Only query tables that are available to you +* Never mutate database, including add/update/remove record from any table, run any DLL statements, only read. +* Always limit the query with maximum 100 rows +* Format your responses in a clear, readable way +* If you're unsure about the schema or table structure, ask for clarification +* If a table has column _block_range, it is a versioned table, You MUST always add \`_block_range @> 9223372036854775807\` to the where clause for all queries +* If it is a join query, \`_block_range @> 9223372036854775807\` is needed for all tables in the join +* If the query has an error consider the error and try again` + ), + }); + + return this.agent; + } + + async prompt(messages: string[]): Promise { + const agent = await this.getAgent(); + + const result = await agent.stream({messages}, {streamMode: 'values'}); + let finalRes = ''; + for await (const event of result) { + finalRes = event.messages[event.messages.length - 1].content as string; + } + + return finalRes; + } +} diff --git a/packages/query/src/mcp/mcp.module.ts b/packages/query/src/mcp/mcp.module.ts new file mode 100644 index 0000000000..66cce89ff1 --- /dev/null +++ b/packages/query/src/mcp/mcp.module.ts @@ -0,0 +1,19 @@ +// Copyright 2020-2025 SubQuery Pte Ltd authors & contributors +// SPDX-License-Identifier: GPL-3.0 + +import {Module} from '@nestjs/common'; +import {McpModule} from '@rekog/mcp-nest'; +import {ChatModule} from '../llm/chat.module'; +import {QueryTool} from './query.tool'; + +@Module({ + imports: [ + McpModule.forRoot({ + name: 'subquery-mcp-server', + version: '1.0.0', + }), + ChatModule, + ], + providers: [QueryTool], +}) +export class MCPModule {} diff --git a/packages/query/src/mcp/query.tool.ts b/packages/query/src/mcp/query.tool.ts new file mode 100644 index 0000000000..9304314dec --- /dev/null +++ b/packages/query/src/mcp/query.tool.ts @@ -0,0 +1,32 @@ +// Copyright 2020-2025 SubQuery Pte Ltd authors & contributors +// SPDX-License-Identifier: GPL-3.0 + +import {Inject, Injectable} from '@nestjs/common'; +import {Tool, Context} from '@rekog/mcp-nest'; +import type {Request} from 'express'; +import {z} from 'zod'; +import {ChatService} from '../llm/chat.service'; + +@Injectable() +export class QueryTool { + constructor(@Inject(ChatService) private readonly chatSevice: ChatService) {} + + @Tool({ + name: 'natural-query-subquery', + description: 'Make a natural language query to the SubQuery Project', + parameters: z.object({ + input: z.array(z.string({description: 'The messages to be input to the chat service'})), + }), + }) + async naturalQuery( + {input}: {input: string[]}, + context: Context, + request: Request + ): Promise<{content: {type: 'text'; text: string}[]}> { + const result = await this.chatSevice.prompt(input); + + return { + content: [{type: 'text', text: result}], + }; + } +} diff --git a/yarn.lock b/yarn.lock index 9f5653e036..c7046e457b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5385,6 +5385,25 @@ __metadata: languageName: node linkType: hard +"@modelcontextprotocol/sdk@npm:^1.11.5": + version: 1.11.5 + resolution: "@modelcontextprotocol/sdk@npm:1.11.5" + dependencies: + ajv: ^8.17.1 + content-type: ^1.0.5 + cors: ^2.8.5 + cross-spawn: ^7.0.5 + eventsource: ^3.0.2 + express: ^5.0.1 + express-rate-limit: ^7.5.0 + pkce-challenge: ^5.0.0 + raw-body: ^3.0.0 + zod: ^3.23.8 + zod-to-json-schema: ^3.24.1 + checksum: d99b1a346e9d7501a6351ac7c223ab5b84fa34d9dc1dec9b73366d01abeb37aa3815bc30ffd61206a233257118496cfe7a5e7814e2c951806564cb40d3f7ad40 + languageName: node + linkType: hard + "@nestjs/common@npm:^11.0.16": version: 11.0.16 resolution: "@nestjs/common@npm:11.0.16" @@ -6975,6 +6994,23 @@ __metadata: languageName: node linkType: hard +"@rekog/mcp-nest@npm:^1.5.2": + version: 1.5.2 + resolution: "@rekog/mcp-nest@npm:1.5.2" + dependencies: + path-to-regexp: ^8.2.0 + peerDependencies: + "@modelcontextprotocol/sdk": ">=1.10.0" + "@nestjs/common": ">=9.0.0" + "@nestjs/core": ">=9.0.0" + express: ">=4.0.0" + reflect-metadata: ">=0.1.14" + zod: ">=3.0.0" + zod-to-json-schema: ">=3.23.0" + checksum: b43c5f06f5dd907c8d0f795adb5c059de36588069ddce7d59f745ccdde1bab66a8ffcb186d2ca85e12c5e2322ee342e6eebe8ad0ee39fe5c6121bfa7c153d695 + languageName: node + linkType: hard + "@scure/base@npm:^1.1.1": version: 1.1.6 resolution: "@scure/base@npm:1.1.6" @@ -8173,11 +8209,13 @@ __metadata: "@langchain/core": ^0.3.55 "@langchain/langgraph": ^0.2.71 "@langchain/openai": ^0.5.10 + "@modelcontextprotocol/sdk": ^1.11.5 "@nestjs/common": ^9.4.0 "@nestjs/core": ^9.4.0 "@nestjs/platform-express": ^9.4.0 "@nestjs/schematics": ^9.2.0 "@nestjs/testing": ^9.4.0 + "@rekog/mcp-nest": ^1.5.2 "@subql/common": "workspace:*" "@subql/utils": "workspace:*" "@subql/x-graphile-build-pg": 4.13.0-0.2.5 @@ -10133,7 +10171,7 @@ __metadata: languageName: node linkType: hard -"ajv@npm:8.17.1": +"ajv@npm:8.17.1, ajv@npm:^8.17.1": version: 8.17.1 resolution: "ajv@npm:8.17.1" dependencies: @@ -11144,6 +11182,23 @@ __metadata: languageName: node linkType: hard +"body-parser@npm:^2.2.0": + version: 2.2.0 + resolution: "body-parser@npm:2.2.0" + dependencies: + bytes: ^3.1.2 + content-type: ^1.0.5 + debug: ^4.4.0 + http-errors: ^2.0.0 + iconv-lite: ^0.6.3 + on-finished: ^2.4.1 + qs: ^6.14.0 + raw-body: ^3.0.0 + type-is: ^2.0.0 + checksum: 7fe3a2d288f0b632528d6ccb90052d1a9492c5b79d5716d32c8de1f5fb8237b0d31ee5050e1d0b7ff143a492ff151804612c6e2686a222a1d4c9e2e6531b8fb2 + languageName: node + linkType: hard + "boolbase@npm:^1.0.0": version: 1.0.0 resolution: "boolbase@npm:1.0.0" @@ -11418,7 +11473,7 @@ __metadata: languageName: node linkType: hard -"bytes@npm:3.1.2": +"bytes@npm:3.1.2, bytes@npm:^3.1.2": version: 3.1.2 resolution: "bytes@npm:3.1.2" checksum: e4bcd3948d289c5127591fbedf10c0b639ccbf00243504e4e127374a15c3bc8eed0d28d4aaab08ff6f1cf2abc0cce6ba3085ed32f4f90e82a5683ce0014e1b6e @@ -12509,6 +12564,13 @@ __metadata: languageName: node linkType: hard +"cookie@npm:^0.7.1": + version: 0.7.2 + resolution: "cookie@npm:0.7.2" + checksum: 9bf8555e33530affd571ea37b615ccad9b9a34febbf2c950c86787088eb00a8973690833b0f8ebd6b69b753c62669ea60cec89178c1fb007bf0749abed74f93e + languageName: node + linkType: hard + "core-js-compat@npm:^3.21.0, core-js-compat@npm:^3.22.1": version: 3.22.4 resolution: "core-js-compat@npm:3.22.4" @@ -12638,6 +12700,17 @@ __metadata: languageName: node linkType: hard +"cross-spawn@npm:^7.0.5": + version: 7.0.6 + resolution: "cross-spawn@npm:7.0.6" + dependencies: + path-key: ^3.1.0 + shebang-command: ^2.0.0 + which: ^2.0.1 + checksum: 8d306efacaf6f3f60e0224c287664093fa9185680b2d195852ba9a863f85d02dcc737094c6e512175f8ee0161f9b87c73c6826034c2422e39de7d6569cf4503b + languageName: node + linkType: hard + "crypto-js@npm:^4.2.0": version: 4.2.0 resolution: "crypto-js@npm:4.2.0" @@ -14064,6 +14137,13 @@ __metadata: languageName: node linkType: hard +"eventsource-parser@npm:^3.0.1": + version: 3.0.2 + resolution: "eventsource-parser@npm:3.0.2" + checksum: 8032ffe62f00f22bf1c6e9a8b34c93ee425ca58b6ec246758bb6781e1aa7aa10523a1bcb283b379e2965373c1d54d4df0eb645ebcd9126bf7da5cb2b36273cb6 + languageName: node + linkType: hard + "eventsource@npm:^2.0.2": version: 2.0.2 resolution: "eventsource@npm:2.0.2" @@ -14071,6 +14151,15 @@ __metadata: languageName: node linkType: hard +"eventsource@npm:^3.0.2": + version: 3.0.7 + resolution: "eventsource@npm:3.0.7" + dependencies: + eventsource-parser: ^3.0.1 + checksum: cd8cbc3418238b9d751b6652edf442d4b869829fbc3b73444abca1816fe3d23dc707130dd9a990360bc27c281d986f2f62059d870921173425c3ac28d20a8414 + languageName: node + linkType: hard + "execa@npm:^5.0.0, execa@npm:^5.1.1": version: 5.1.1 resolution: "execa@npm:5.1.1" @@ -14141,6 +14230,15 @@ __metadata: languageName: node linkType: hard +"express-rate-limit@npm:^7.5.0": + version: 7.5.0 + resolution: "express-rate-limit@npm:7.5.0" + peerDependencies: + express: ^4.11 || 5 || ^5.0.0-beta.1 + checksum: 2807341039c111eed292e28768aff3c69515cb96ff15799976a44ead776c41931d6947fe3da3cea021fa0490700b1ab468b4832bbed7d231bed63c195d22b959 + languageName: node + linkType: hard + "express@npm:4.18.2": version: 4.18.2 resolution: "express@npm:4.18.2" @@ -14220,6 +14318,41 @@ __metadata: languageName: node linkType: hard +"express@npm:^5.0.1": + version: 5.1.0 + resolution: "express@npm:5.1.0" + dependencies: + accepts: ^2.0.0 + body-parser: ^2.2.0 + content-disposition: ^1.0.0 + content-type: ^1.0.5 + cookie: ^0.7.1 + cookie-signature: ^1.2.1 + debug: ^4.4.0 + encodeurl: ^2.0.0 + escape-html: ^1.0.3 + etag: ^1.8.1 + finalhandler: ^2.1.0 + fresh: ^2.0.0 + http-errors: ^2.0.0 + merge-descriptors: ^2.0.0 + mime-types: ^3.0.0 + on-finished: ^2.4.1 + once: ^1.4.0 + parseurl: ^1.3.3 + proxy-addr: ^2.0.7 + qs: ^6.14.0 + range-parser: ^1.2.1 + router: ^2.2.0 + send: ^1.1.0 + serve-static: ^2.2.0 + statuses: ^2.0.1 + type-is: ^2.0.1 + vary: ^1.1.2 + checksum: 06e6141780c6c4780111f971ce062c83d4cf4862c40b43caf1d95afcbb58d7422c560503b8c9d04c7271511525d09cbdbe940bcaad63970fd4c1b9f6fd713bdb + languageName: node + linkType: hard + "ext@npm:^1.1.2": version: 1.6.0 resolution: "ext@npm:1.6.0" @@ -14490,6 +14623,20 @@ __metadata: languageName: node linkType: hard +"finalhandler@npm:^2.1.0": + version: 2.1.0 + resolution: "finalhandler@npm:2.1.0" + dependencies: + debug: ^4.4.0 + encodeurl: ^2.0.0 + escape-html: ^1.0.3 + on-finished: ^2.4.1 + parseurl: ^1.3.3 + statuses: ^2.0.1 + checksum: 27ca9cc83b1384ba37959eb95bc7e62bc0bf4d6f6af63f6d38821cf7499b113e34b23f96a2a031616817f73986f94deea67c2f558de9daf406790c181a2501df + languageName: node + linkType: hard + "find-replace@npm:^3.0.0": version: 3.0.0 resolution: "find-replace@npm:3.0.0" @@ -14687,7 +14834,7 @@ __metadata: languageName: node linkType: hard -"fresh@npm:2.0.0": +"fresh@npm:2.0.0, fresh@npm:^2.0.0": version: 2.0.0 resolution: "fresh@npm:2.0.0" checksum: 38b9828352c6271e2a0dd8bdd985d0100dbbc4eb8b6a03286071dd6f7d96cfaacd06d7735701ad9a95870eb3f4555e67c08db1dcfe24c2e7bb87383c72fae1d2 @@ -15877,7 +16024,7 @@ __metadata: languageName: node linkType: hard -"iconv-lite@npm:0.6.3, iconv-lite@npm:^0.6.2": +"iconv-lite@npm:0.6.3, iconv-lite@npm:^0.6.2, iconv-lite@npm:^0.6.3": version: 0.6.3 resolution: "iconv-lite@npm:0.6.3" dependencies: @@ -16455,7 +16602,7 @@ __metadata: languageName: node linkType: hard -"is-promise@npm:4.0.0": +"is-promise@npm:4.0.0, is-promise@npm:^4.0.0": version: 4.0.0 resolution: "is-promise@npm:4.0.0" checksum: 0b46517ad47b00b6358fd6553c83ec1f6ba9acd7ffb3d30a0bf519c5c69e7147c132430452351b8a9fc198f8dd6c4f76f8e6f5a7f100f8c77d57d9e0f4261a8a @@ -18549,6 +18696,13 @@ __metadata: languageName: node linkType: hard +"mime-db@npm:^1.54.0": + version: 1.54.0 + resolution: "mime-db@npm:1.54.0" + checksum: e99aaf2f23f5bd607deb08c83faba5dd25cf2fec90a7cc5b92d8260867ee08dab65312e1a589e60093dc7796d41e5fae013268418482f1db4c7d52d0a0960ac9 + languageName: node + linkType: hard + "mime-types@npm:^2.1.12, mime-types@npm:^2.1.27, mime-types@npm:^2.1.35, mime-types@npm:~2.1.24, mime-types@npm:~2.1.34": version: 2.1.35 resolution: "mime-types@npm:2.1.35" @@ -18567,6 +18721,15 @@ __metadata: languageName: node linkType: hard +"mime-types@npm:^3.0.1": + version: 3.0.1 + resolution: "mime-types@npm:3.0.1" + dependencies: + mime-db: ^1.54.0 + checksum: 8d497ad5cb2dd1210ac7d049b5de94af0b24b45a314961e145b44389344604d54752f03bc00bf880c0da60a214be6fb6d423d318104f02c28d95dd8ebeea4fb4 + languageName: node + linkType: hard + "mime@npm:1.6.0": version: 1.6.0 resolution: "mime@npm:1.6.0" @@ -20337,7 +20500,7 @@ __metadata: languageName: node linkType: hard -"path-to-regexp@npm:8.2.0, path-to-regexp@npm:^8.0.0": +"path-to-regexp@npm:8.2.0, path-to-regexp@npm:^8.0.0, path-to-regexp@npm:^8.2.0": version: 8.2.0 resolution: "path-to-regexp@npm:8.2.0" checksum: 56e13e45962e776e9e7cd72e87a441cfe41f33fd539d097237ceb16adc922281136ca12f5a742962e33d8dda9569f630ba594de56d8b7b6e49adf31803c5e771 @@ -20623,6 +20786,13 @@ __metadata: languageName: node linkType: hard +"pkce-challenge@npm:^5.0.0": + version: 5.0.0 + resolution: "pkce-challenge@npm:5.0.0" + checksum: b5cc239f67ed525b49a23a86fdb8f49e3cdb9fd8f5e8612a15f35b553a18e5a43c99db474ffc6232e084c8328d4f2da51557e51ee4e7f8be42f710215df36f3f + languageName: node + linkType: hard + "pkg-conf@npm:^2.1.0": version: 2.1.0 resolution: "pkg-conf@npm:2.1.0" @@ -21019,7 +21189,7 @@ __metadata: languageName: node linkType: hard -"proxy-addr@npm:~2.0.7": +"proxy-addr@npm:^2.0.7, proxy-addr@npm:~2.0.7": version: 2.0.7 resolution: "proxy-addr@npm:2.0.7" dependencies: @@ -21131,6 +21301,15 @@ __metadata: languageName: node linkType: hard +"qs@npm:^6.14.0": + version: 6.14.0 + resolution: "qs@npm:6.14.0" + dependencies: + side-channel: ^1.1.0 + checksum: 189b52ad4e9a0da1a16aff4c58b2a554a8dad9bd7e287c7da7446059b49ca2e33a49e570480e8be406b87fccebf134f51c373cbce36c8c83859efa0c9b71d635 + languageName: node + linkType: hard + "qs@npm:^6.9.4": version: 6.11.2 resolution: "qs@npm:6.11.2" @@ -21925,6 +22104,19 @@ __metadata: languageName: node linkType: hard +"router@npm:^2.2.0": + version: 2.2.0 + resolution: "router@npm:2.2.0" + dependencies: + debug: ^4.4.0 + depd: ^2.0.0 + is-promise: ^4.0.0 + parseurl: ^1.3.3 + path-to-regexp: ^8.0.0 + checksum: 4c3bec8011ed10bb07d1ee860bc715f245fff0fdff991d8319741d2932d89c3fe0a56766b4fa78e95444bc323fd2538e09c8e43bfbd442c2a7fab67456df7fa5 + languageName: node + linkType: hard + "run-async@npm:^2.0.0, run-async@npm:^2.2.0, run-async@npm:^2.3.0, run-async@npm:^2.4.0": version: 2.4.1 resolution: "run-async@npm:2.4.1" @@ -22175,6 +22367,25 @@ __metadata: languageName: node linkType: hard +"send@npm:^1.2.0": + version: 1.2.0 + resolution: "send@npm:1.2.0" + dependencies: + debug: ^4.3.5 + encodeurl: ^2.0.0 + escape-html: ^1.0.3 + etag: ^1.8.1 + fresh: ^2.0.0 + http-errors: ^2.0.0 + mime-types: ^3.0.1 + ms: ^2.1.3 + on-finished: ^2.4.1 + range-parser: ^1.2.1 + statuses: ^2.0.1 + checksum: 7557ee6c1c257a1c53b402b4fba8ed88c95800b08abe085fc79e0824869274f213491be2efb2df3de228c70e4d40ce2019e5f77b58c42adb97149135420c3f34 + languageName: node + linkType: hard + "sequelize-pool@npm:^7.1.0": version: 7.1.0 resolution: "sequelize-pool@npm:7.1.0" @@ -22215,6 +22426,18 @@ __metadata: languageName: node linkType: hard +"serve-static@npm:^2.2.0": + version: 2.2.0 + resolution: "serve-static@npm:2.2.0" + dependencies: + encodeurl: ^2.0.0 + escape-html: ^1.0.3 + parseurl: ^1.3.3 + send: ^1.2.0 + checksum: 74f39e88f0444aa6732aae3b9597739c47552adecdc83fa32aa42555e76f1daad480d791af73894655c27a2d378275a461e691cead33fb35d8b976f1e2d24665 + languageName: node + linkType: hard + "set-blocking@npm:^2.0.0": version: 2.0.0 resolution: "set-blocking@npm:2.0.0" @@ -22362,7 +22585,7 @@ __metadata: languageName: node linkType: hard -"side-channel@npm:^1.0.6": +"side-channel@npm:^1.0.6, side-channel@npm:^1.1.0": version: 1.1.0 resolution: "side-channel@npm:1.1.0" dependencies: @@ -23841,6 +24064,17 @@ __metadata: languageName: node linkType: hard +"type-is@npm:^2.0.1": + version: 2.0.1 + resolution: "type-is@npm:2.0.1" + dependencies: + content-type: ^1.0.5 + media-typer: ^1.1.0 + mime-types: ^3.0.0 + checksum: 0266e7c782238128292e8c45e60037174d48c6366bb2d45e6bd6422b611c193f83409a8341518b6b5f33f8e4d5a959f38658cacfea77f0a3505b9f7ac1ddec8f + languageName: node + linkType: hard + "type@npm:^1.0.1": version: 1.2.0 resolution: "type@npm:1.2.0" @@ -24606,7 +24840,7 @@ __metadata: languageName: node linkType: hard -"vary@npm:^1, vary@npm:~1.1.2": +"vary@npm:^1, vary@npm:^1.1.2, vary@npm:~1.1.2": version: 1.1.2 resolution: "vary@npm:1.1.2" checksum: ae0123222c6df65b437669d63dfa8c36cee20a504101b2fcd97b8bf76f91259c17f9f2b4d70a1e3c6bbcee7f51b28392833adb6b2770b23b01abec84e369660b @@ -25402,7 +25636,7 @@ __metadata: languageName: node linkType: hard -"zod-to-json-schema@npm:^3.22.3, zod-to-json-schema@npm:^3.22.4": +"zod-to-json-schema@npm:^3.22.3, zod-to-json-schema@npm:^3.22.4, zod-to-json-schema@npm:^3.24.1": version: 3.24.5 resolution: "zod-to-json-schema@npm:3.24.5" peerDependencies: From 1913bcee1a975965e890fb7cd6af6bb50221bef0 Mon Sep 17 00:00:00 2001 From: Scott Twiname Date: Mon, 26 May 2025 12:10:28 +1200 Subject: [PATCH 6/6] Update changelog --- packages/query/CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/query/CHANGELOG.md b/packages/query/CHANGELOG.md index 449da43d6b..d3f9aa7693 100644 --- a/packages/query/CHANGELOG.md +++ b/packages/query/CHANGELOG.md @@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Changed +- Add LLM and MCP support (#2788) ## [2.22.1] - 2025-05-21 ### Changed