Skip to content

Commit 85e3bde

Browse files
committed
introduce chat api to the project supported by langchain's sqlagent
* remove project service to drop support for legacy version of subql node
1 parent ae3de86 commit 85e3bde

File tree

10 files changed

+877
-57
lines changed

10 files changed

+877
-57
lines changed

packages/query/README.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,30 @@ then run the following command
1717
```sh
1818
NODE_OPTIONS="-r dotenv/config" yarn start:dev -- --name <subquery_name> --playground
1919
```
20+
21+
## LLM Configuration
22+
23+
Suggest to use reasoning models to archive better quality of results.
24+
25+
### Choose one of the following providers:
26+
27+
### Option 1: OpenAI
28+
29+
LLM_PROVIDER=openai
30+
LLM_MODEL=o1 # Optional: defaults to '4o-mini'
31+
OPENAI_API_KEY=your_openai_api_key
32+
33+
### Option 2: Anthropic Claude
34+
35+
LLM_PROVIDER=anthropic
36+
LLM_MODEL=claude-3-7-sonnet-latest # Optional: defaults to 'claude-3-7-sonnet-latest'
37+
ANTHROPIC_API_KEY=your_anthropic_api_key
38+
39+
### Option 3: Custom OpenAI-compatible endpoint
40+
41+
LLM_PROVIDER=openai
42+
LLM_BASE_URL=http://your-llm-endpoint/v1 # Required for custom provider
43+
LLM_MODEL=your-model-name # Required: model name for custom endpoint
44+
OPENAI_API_KEY=your_api_key # Optional: API key for custom endpoint
45+
46+
### Common LLM Settings

packages/query/package.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@
3535
"@graphile-contrib/pg-simplify-inflector": "^6.1.0",
3636
"@graphile/pg-aggregates": "^0.1.1",
3737
"@graphile/pg-pubsub": "^4.13.0",
38+
"@langchain/anthropic": "^0.3.20",
39+
"@langchain/core": "^0.3.55",
40+
"@langchain/langgraph": "^0.2.71",
41+
"@langchain/openai": "^0.5.10",
3842
"@nestjs/common": "^9.4.0",
3943
"@nestjs/core": "^9.4.0",
4044
"@nestjs/platform-express": "^9.4.0",
@@ -50,12 +54,14 @@
5054
"graphql": "^15.8.0",
5155
"graphql-query-complexity": "^0.11.0",
5256
"graphql-ws": "^5.16.0",
57+
"langchain": "^0.3.24",
5358
"lodash": "^4.17.21",
5459
"pg": "^8.12.0",
5560
"pg-tsquery": "^8.4.2",
5661
"postgraphile": "^4.13.0",
5762
"postgraphile-plugin-connection-filter": "^2.2.2",
5863
"rxjs": "^7.1.0",
64+
"typeorm": "^0.3.23",
5965
"ws": "^8.18.0",
6066
"yargs": "^16.2.0"
6167
},

packages/query/src/app.module.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,12 @@
44
import {Module} from '@nestjs/common';
55
import {ConfigureModule} from './configure/configure.module';
66
import {GraphqlModule} from './graphql/graphql.module';
7+
import {ChatModule} from './llm/chat.module';
78

89
@Module({
9-
imports: [ConfigureModule.register(), GraphqlModule],
10+
// the order is essential, the ChatModule must be before the GraphqlModule so /v1/chat/completions
11+
// can be handled without interference from the GraphqlModule
12+
imports: [ConfigureModule.register(), ChatModule, GraphqlModule],
1013
controllers: [],
1114
})
1215
export class AppModule {}

packages/query/src/configure/configure.module.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {ConnectionOptions} from 'tls';
55
import {DynamicModule, Global, Module} from '@nestjs/common';
66
import {getFileContent, CONNECTION_SSL_ERROR_REGEX} from '@subql/common';
77
import {Pool, PoolConfig} from 'pg';
8+
import {DataSource} from 'typeorm';
89
import {getLogger} from '../utils/logger';
910
import {getYargsOption} from '../yargs';
1011
import {Config} from './config';
@@ -85,6 +86,17 @@ export class ConfigureModule {
8586
pgClient._explainResults = [];
8687
});
8788
}
89+
// todo: support ssl
90+
const dataSource = new DataSource({
91+
type: 'postgres',
92+
host: config.get('DB_HOST_READ'),
93+
port: config.get('DB_PORT'),
94+
username: config.get('DB_USER'),
95+
password: config.get('DB_PASS'),
96+
database: config.get('DB_DATABASE'),
97+
schema: config.get<string>('name'),
98+
});
99+
await dataSource.initialize();
88100
return {
89101
module: ConfigureModule,
90102
providers: [
@@ -96,8 +108,12 @@ export class ConfigureModule {
96108
provide: Pool,
97109
useValue: pgPool,
98110
},
111+
{
112+
provide: DataSource,
113+
useValue: dataSource,
114+
},
99115
],
100-
exports: [Config, Pool],
116+
exports: [Config, Pool, DataSource],
101117
};
102118
}
103119
}

packages/query/src/graphql/graphql.module.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ import {playgroundPlugin} from './plugins/PlaygroundPlugin';
3030
import {queryAliasLimit} from './plugins/QueryAliasLimitPlugin';
3131
import {queryComplexityPlugin} from './plugins/QueryComplexityPlugin';
3232
import {queryDepthLimitPlugin} from './plugins/QueryDepthLimitPlugin';
33-
import {ProjectService} from './project.service';
3433

3534
const {argv} = getYargsOption();
3635
const logger = getLogger('graphql-module');
@@ -45,16 +44,15 @@ class NoInitError extends Error {
4544
}
4645
}
4746
@Module({
48-
providers: [ProjectService],
47+
providers: [],
4948
})
5049
export class GraphqlModule implements OnModuleInit, OnModuleDestroy {
5150
private _apolloServer?: ApolloServer;
5251
private wsCleanup?: ReturnType<typeof useServer>;
5352
constructor(
5453
private readonly httpAdapterHost: HttpAdapterHost,
5554
private readonly config: Config,
56-
private readonly pgPool: Pool,
57-
private readonly projectService: ProjectService
55+
private readonly pgPool: Pool
5856
) {}
5957

6058
private get apolloServer(): ApolloServer {
@@ -138,7 +136,7 @@ export class GraphqlModule implements OnModuleInit, OnModuleDestroy {
138136
const schemaName = this.config.get<string>('name');
139137
if (!schemaName) throw new Error('Unable to get schema name from config');
140138

141-
const dbSchema = await this.projectService.getProjectSchema(schemaName);
139+
const dbSchema = schemaName;
142140
let options: PostGraphileCoreOptions = {
143141
replaceAllPlugins: plugins,
144142
subscriptions: true,

packages/query/src/graphql/project.service.ts

Lines changed: 0 additions & 39 deletions
This file was deleted.
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
// Copyright 2020-2025 SubQuery Pte Ltd authors & contributors
2+
// SPDX-License-Identifier: GPL-3.0
3+
4+
import {BaseMessage, SystemMessage} from '@langchain/core/messages';
5+
import {createReactAgent} from '@langchain/langgraph/prebuilt';
6+
import {Module, OnModuleInit} from '@nestjs/common';
7+
import {HttpAdapterHost} from '@nestjs/core';
8+
import {SqlToolkit} from 'langchain/agents/toolkits/sql';
9+
import {SqlDatabase} from 'langchain/sql_db';
10+
import {DataSource} from 'typeorm';
11+
import {Config} from '../configure';
12+
import {getLogger} from '../utils/logger';
13+
import {getYargsOption} from '../yargs';
14+
import {createLLM} from './createLLM';
15+
16+
const {argv} = getYargsOption();
17+
const logger = getLogger('chat-module');
18+
19+
@Module({
20+
providers: [],
21+
})
22+
export class ChatModule implements OnModuleInit {
23+
agent?: ReturnType<typeof createReactAgent>;
24+
25+
constructor(
26+
private readonly httpAdapterHost: HttpAdapterHost,
27+
private readonly dataSource: DataSource,
28+
private readonly config: Config
29+
) {}
30+
31+
onModuleInit(): void {
32+
if (!this.httpAdapterHost) {
33+
return;
34+
}
35+
try {
36+
this.createServer();
37+
} catch (e: any) {
38+
throw new Error(`create apollo server failed, ${e.message}`);
39+
}
40+
}
41+
42+
private async initializeAgent() {
43+
const db = await SqlDatabase.fromDataSourceParams({
44+
appDataSource: this.dataSource,
45+
});
46+
47+
const llm = createLLM();
48+
49+
const toolkit = new SqlToolkit(db, llm);
50+
51+
this.agent = createReactAgent({
52+
llm,
53+
tools: toolkit.getTools(),
54+
prompt: new SystemMessage(
55+
`You are an AI assistant that helps users query their PostgreSQL database using natural language.
56+
57+
When generating SQL queries:
58+
* Always use the correct schema (${this.config.get<string>('name') || 'public'})
59+
* Only query tables that are available to you
60+
* Never mutate database, including add/update/remove record from any table, run any DLL statements, only read.
61+
* Always limit the query with maximum 100 rows
62+
* Format your responses in a clear, readable way
63+
* If you're unsure about the schema or table structure, ask for clarification
64+
* 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
65+
* If it is a join query, \`_block_range @> 9223372036854775807\` is needed for all tables in the join`
66+
),
67+
});
68+
}
69+
70+
private createServer() {
71+
const app = this.httpAdapterHost.httpAdapter.getInstance();
72+
73+
if (argv.chat) {
74+
app.post('/v1/chat/completions', async (req, res) => {
75+
try {
76+
if (!this.agent) {
77+
await this.initializeAgent();
78+
}
79+
80+
const {messages, stream = false} = req.body;
81+
82+
if (!messages || !Array.isArray(messages) || messages.length === 0) {
83+
return res.status(400).json({
84+
error: {
85+
message: 'messages is required and must be a non-empty array',
86+
type: 'invalid_request_error',
87+
code: 'invalid_messages',
88+
},
89+
});
90+
}
91+
92+
// Convert OpenAI format messages to LangChain format
93+
const lastMessage = messages[messages.length - 1];
94+
const question = lastMessage.content;
95+
96+
res.setHeader('Content-Type', 'text/event-stream');
97+
res.setHeader('Cache-Control', 'no-cache');
98+
res.setHeader('Connection', 'keep-alive');
99+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
100+
const result = await this.agent!.stream({messages: [['user', question]]}, {streamMode: 'values'});
101+
102+
let fullResponse = '';
103+
for await (const event of result) {
104+
const lastMsg: BaseMessage = event.messages[event.messages.length - 1];
105+
if (lastMsg.content) {
106+
fullResponse = lastMsg.content as string;
107+
logger.info(`Streaming response: ${JSON.stringify(lastMsg)}`);
108+
if (argv['llm-debug'] && stream) {
109+
// todo: send them as thinking details
110+
res.write(
111+
`data: ${JSON.stringify({
112+
id: `chatcmpl-${Date.now()}`,
113+
object: 'chat.completion.chunk',
114+
created: Math.floor(Date.now() / 1000),
115+
model: process.env.OPENAI_MODEL,
116+
choices: [
117+
{
118+
index: 0,
119+
delta: {content: lastMsg.content},
120+
finish_reason: null,
121+
},
122+
],
123+
})}\n\n`
124+
);
125+
}
126+
}
127+
}
128+
129+
// Send final message
130+
if (stream) {
131+
res.write(
132+
`data: ${JSON.stringify({
133+
id: `chatcmpl-${Date.now()}`,
134+
object: 'chat.completion.chunk',
135+
created: Math.floor(Date.now() / 1000),
136+
model: process.env.OPENAI_MODEL,
137+
choices: [
138+
{
139+
index: 0,
140+
message: {role: 'assistant', content: fullResponse},
141+
finish_reason: 'stop',
142+
},
143+
],
144+
})}\n\n`
145+
);
146+
} else {
147+
res.write(
148+
`data: ${JSON.stringify({
149+
id: `chatcmpl-${Date.now()}`,
150+
object: 'chat.completion',
151+
created: Math.floor(Date.now() / 1000),
152+
model: process.env.OPENAI_MODEL,
153+
choices: [
154+
{
155+
index: 0,
156+
message: {role: 'assistant', content: fullResponse},
157+
finish_reason: 'stop',
158+
},
159+
],
160+
})}\n\n`
161+
);
162+
}
163+
res.end();
164+
} catch (error) {
165+
logger.error('Error processing request:', error);
166+
res.status(500).json({
167+
error: {
168+
message: (error as any).message,
169+
type: 'internal_server_error',
170+
},
171+
});
172+
}
173+
});
174+
} else {
175+
app.post('/v1/chat/completions', (req, res) => {
176+
res.status(404).json({
177+
error: {
178+
message: 'Chat completions API is not enabled',
179+
type: 'invalid_request_error',
180+
code: 'chat_api_not_enabled',
181+
},
182+
});
183+
});
184+
}
185+
}
186+
}

0 commit comments

Comments
 (0)