@neural-technologies-indonesia/nl2sql 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +110 -0
- package/dist/adapters/mcpExecutor.d.ts +22 -0
- package/dist/adapters/mcpExecutor.d.ts.map +1 -0
- package/dist/adapters/mcpExecutor.js +30 -0
- package/dist/adapters/mcpRetriever.d.ts +20 -0
- package/dist/adapters/mcpRetriever.d.ts.map +1 -0
- package/dist/adapters/mcpRetriever.js +116 -0
- package/dist/adapters/openaiLLM.d.ts +17 -0
- package/dist/adapters/openaiLLM.d.ts.map +1 -0
- package/dist/adapters/openaiLLM.js +57 -0
- package/dist/clients/mcpUseClient.d.ts +39 -0
- package/dist/clients/mcpUseClient.d.ts.map +1 -0
- package/dist/clients/mcpUseClient.js +182 -0
- package/dist/defaults/promptComposer.d.ts +7 -0
- package/dist/defaults/promptComposer.d.ts.map +1 -0
- package/dist/defaults/promptComposer.js +33 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/pipeline.d.ts +29 -0
- package/dist/pipeline.d.ts.map +1 -0
- package/dist/pipeline.js +84 -0
- package/dist/types.d.ts +68 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/package.json +40 -0
package/README.md
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# text-to-sql-reasoner
|
|
2
|
+
|
|
3
|
+
Core reasoning flow for text-to-SQL that can be exposed by any transport (Streamlit, REST API, MCP, etc.).
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
[Input Port]
|
|
7
|
+
↓
|
|
8
|
+
[Prompt Composer]
|
|
9
|
+
↓
|
|
10
|
+
[Retrieval (RAG)]
|
|
11
|
+
↓
|
|
12
|
+
[Reasoning LLM]
|
|
13
|
+
↓
|
|
14
|
+
[Query Candidate]
|
|
15
|
+
↓
|
|
16
|
+
[MCP: run_query]
|
|
17
|
+
↓
|
|
18
|
+
[Execution Feedback]
|
|
19
|
+
├─ slow / failed ─► retry loop
|
|
20
|
+
└─ success & fast ─► output port
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## What lives here (and how it works)
|
|
24
|
+
- Pure orchestrator in TypeScript (Bun-friendly) with clean interfaces for prompt composing, retrieval, LLM reasoning, query execution, and feedback policy.
|
|
25
|
+
- Transport-agnostic: callers plug in their own input/output (UI, REST, MCP, etc.).
|
|
26
|
+
- Defaults: prompt composer (`DefaultPromptComposer`), feedback policy (`SimpleFeedbackPolicy`), MCP executor (`MCPQueryExecutor`), MCP retriever (`MCPMilvusRetriever`), and OpenAI LLM wrapper (`OpenAILLM`).
|
|
27
|
+
- Flow: retrieve context → compose prompt → ask LLM for SQL candidates → execute via MCP `run_query` → accept/retry via feedback policy.
|
|
28
|
+
|
|
29
|
+
## Example wiring
|
|
30
|
+
```ts
|
|
31
|
+
import {
|
|
32
|
+
ReasoningPipeline,
|
|
33
|
+
SimpleFeedbackPolicy,
|
|
34
|
+
DefaultPromptComposer,
|
|
35
|
+
OpenAILLM,
|
|
36
|
+
MCPQueryExecutor,
|
|
37
|
+
} from "@neural-technologies-indonesia/nl2sql";
|
|
38
|
+
|
|
39
|
+
const pipeline = new ReasoningPipeline({
|
|
40
|
+
composer: new DefaultPromptComposer(),
|
|
41
|
+
retriever: /* plug a retriever implementation here */,
|
|
42
|
+
llm: new OpenAILLM({ model: "gpt-4o-mini" }),
|
|
43
|
+
executor: new MCPQueryExecutor({
|
|
44
|
+
runQuery: async (sql) => {
|
|
45
|
+
// call MCP run_query tool here
|
|
46
|
+
return { durationMs: 120, rows: 1 };
|
|
47
|
+
},
|
|
48
|
+
}),
|
|
49
|
+
policy: new SimpleFeedbackPolicy({ durationBudgetMs: 5000 }),
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const result = await pipeline.run({ question: "How many users signed up today?" });
|
|
53
|
+
console.log(result);
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Port it anywhere
|
|
57
|
+
- REST / gRPC / WebSocket: parse request → build `PromptInput` → call `pipeline.run` → return `FlowOutput`.
|
|
58
|
+
- Streamlit or CLI: same core call; render `logs` for debugging.
|
|
59
|
+
- MCP: wrap `pipeline.run` behind a tool; reuse the included `MCPQueryExecutor` to hit `run_query`.
|
|
60
|
+
|
|
61
|
+
## Inference backends
|
|
62
|
+
- Implemented: OpenAI (default focus).
|
|
63
|
+
- Future slots: MLX LM (Apple), Ollama, vLLM — add new adapters implementing `ReasoningLLM` without changing the pipeline.
|
|
64
|
+
|
|
65
|
+
## Env
|
|
66
|
+
- Copy `.env.example` and set `OPENAI_API_KEY` (and optionally `OPENAI_BASE_URL`, `OPENAI_MODEL`).
|
|
67
|
+
|
|
68
|
+
## Examples
|
|
69
|
+
- `example/bun-server.ts`: starts a Bun HTTP server exposing POST `/query` that runs the pipeline (OpenAI + MCP-backed executor/retriever). Run with `bun run example/bun-server.ts`.
|
|
70
|
+
- `example/streamlit_app.py`: minimal Streamlit UI that POSTs to the Bun server. Configure `API_URL` in `.streamlit/secrets.toml` or edit the default (`http://localhost:8787/query`).
|
|
71
|
+
|
|
72
|
+
## Use the sibling MCP server (`../mcp-server`) via `mcp-use`
|
|
73
|
+
- That folder ships a FastMCP server exposing `run_query` (Postgres latency) and `milvus_retrieve` (RAG search). Start it with your env: `DATABASE_URL=... OPENAI_API_KEY=... bun run start` from `../mcp-server`.
|
|
74
|
+
- Install the client dependency here: `bun add mcp-use` (or `npm install mcp-use`).
|
|
75
|
+
- If your MCP server lives somewhere else, point the helper there with env vars: `MCP_SERVER_CWD=/path/to/server`, optionally `MCP_SERVER_CMD=bun` and `MCP_SERVER_ARGS="run start"`. The example Bun server picks these up automatically.
|
|
76
|
+
- Wire the pipeline to that server with the provided helper:
|
|
77
|
+
```ts
|
|
78
|
+
import {
|
|
79
|
+
ReasoningPipeline,
|
|
80
|
+
DefaultPromptComposer,
|
|
81
|
+
SimpleFeedbackPolicy,
|
|
82
|
+
OpenAILLM,
|
|
83
|
+
createMcpUseAdapters,
|
|
84
|
+
} from "@neural-technologies-indonesia/nl2sql";
|
|
85
|
+
|
|
86
|
+
const { executor, retriever, close } = await createMcpUseAdapters({
|
|
87
|
+
// optional overrides:
|
|
88
|
+
// serverCwd: path.resolve("../mcp-server"),
|
|
89
|
+
// command: "bun",
|
|
90
|
+
// args: ["run", "start"],
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const pipeline = new ReasoningPipeline({
|
|
94
|
+
composer: new DefaultPromptComposer(),
|
|
95
|
+
retriever,
|
|
96
|
+
llm: new OpenAILLM({ model: process.env.OPENAI_MODEL ?? "gpt-4o-mini" }),
|
|
97
|
+
executor,
|
|
98
|
+
policy: new SimpleFeedbackPolicy({ durationBudgetMs: 5000 }),
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const result = await pipeline.run({ question: "How many users signed up today?" });
|
|
102
|
+
console.log(result);
|
|
103
|
+
await close();
|
|
104
|
+
```
|
|
105
|
+
- `createMcpUseAdapters` calls the MCP tools (no duplicated logic here) and returns the MCP-backed executor/retriever pair.
|
|
106
|
+
|
|
107
|
+
## Next steps
|
|
108
|
+
- Swap in real retriever and LLM clients.
|
|
109
|
+
- Add schema guards to prompt composer (e.g., restrict tables/columns).
|
|
110
|
+
- Extend feedback policy with statistical latency bounds or result validation.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { QueryCandidate, QueryExecutor, ExecutionResult } from "../types.js";
|
|
2
|
+
type MCPClient = {
|
|
3
|
+
runQuery: (sql: string) => Promise<{
|
|
4
|
+
durationMs: number;
|
|
5
|
+
rows?: number;
|
|
6
|
+
}>;
|
|
7
|
+
runQueryExplain?: (sql: string) => Promise<{
|
|
8
|
+
durationMs: number;
|
|
9
|
+
explain?: string;
|
|
10
|
+
raw?: string;
|
|
11
|
+
explainLines?: string[];
|
|
12
|
+
planningMs?: number;
|
|
13
|
+
executionMs?: number;
|
|
14
|
+
}>;
|
|
15
|
+
};
|
|
16
|
+
export declare class MCPQueryExecutor implements QueryExecutor {
|
|
17
|
+
private readonly client;
|
|
18
|
+
constructor(client: MCPClient);
|
|
19
|
+
run(candidate: QueryCandidate): Promise<ExecutionResult>;
|
|
20
|
+
}
|
|
21
|
+
export {};
|
|
22
|
+
//# sourceMappingURL=mcpExecutor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mcpExecutor.d.ts","sourceRoot":"","sources":["../../src/adapters/mcpExecutor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,cAAc,EACd,aAAa,EACb,eAAe,EAChB,MAAM,aAAa,CAAC;AAErB,KAAK,SAAS,GAAG;IACf,QAAQ,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC1E,eAAe,CAAC,EAAE,CAChB,GAAG,EAAE,MAAM,KACR,OAAO,CAAC;QACX,UAAU,EAAE,MAAM,CAAC;QACnB,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,GAAG,CAAC,EAAE,MAAM,CAAC;QACb,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;QACxB,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,WAAW,CAAC,EAAE,MAAM,CAAC;KACtB,CAAC,CAAC;CACJ,CAAC;AAGF,qBAAa,gBAAiB,YAAW,aAAa;IACpD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAY;gBAEvB,MAAM,EAAE,SAAS;IAIvB,GAAG,CAAC,SAAS,EAAE,cAAc,GAAG,OAAO,CAAC,eAAe,CAAC;CAuB/D"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Thin executor adapter that forwards SQL to an MCP client's run_query tool.
|
|
2
|
+
export class MCPQueryExecutor {
|
|
3
|
+
client;
|
|
4
|
+
constructor(client) {
|
|
5
|
+
this.client = client;
|
|
6
|
+
}
|
|
7
|
+
async run(candidate) {
|
|
8
|
+
try {
|
|
9
|
+
if (this.client.runQueryExplain) {
|
|
10
|
+
console.log(candidate.sql, "-> run_query_explain via MCP");
|
|
11
|
+
const res = await this.client.runQueryExplain(candidate.sql);
|
|
12
|
+
return {
|
|
13
|
+
ok: true,
|
|
14
|
+
durationMs: res.durationMs,
|
|
15
|
+
explain: res.explain,
|
|
16
|
+
rawExplain: res.raw,
|
|
17
|
+
explainLines: res.explainLines,
|
|
18
|
+
planningMs: res.planningMs,
|
|
19
|
+
executionMs: res.executionMs,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
const res = await this.client.runQuery(candidate.sql);
|
|
23
|
+
return { ok: true, durationMs: res.durationMs, rows: res.rows };
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
const message = error instanceof Error ? error.message : "Unknown MCP error";
|
|
27
|
+
return { ok: false, error: message };
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ContextChunk, PromptInput, Retriever } from "../types.js";
|
|
2
|
+
export type MCPMilvusResult = {
|
|
3
|
+
id?: string;
|
|
4
|
+
score?: number;
|
|
5
|
+
rag_text?: string;
|
|
6
|
+
metadata?: Record<string, unknown>;
|
|
7
|
+
};
|
|
8
|
+
type MCPMilvusClient = {
|
|
9
|
+
milvusRetrieve: (query: string, topK?: number) => Promise<MCPMilvusResult[] | string | Record<string, unknown>>;
|
|
10
|
+
};
|
|
11
|
+
export declare class MCPMilvusRetriever implements Retriever {
|
|
12
|
+
private readonly client;
|
|
13
|
+
private readonly topK;
|
|
14
|
+
constructor(client: MCPMilvusClient, options?: {
|
|
15
|
+
topK?: number;
|
|
16
|
+
});
|
|
17
|
+
retrieve(input: PromptInput): Promise<ContextChunk[]>;
|
|
18
|
+
}
|
|
19
|
+
export {};
|
|
20
|
+
//# sourceMappingURL=mcpRetriever.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mcpRetriever.d.ts","sourceRoot":"","sources":["../../src/adapters/mcpRetriever.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAExE,MAAM,MAAM,eAAe,GAAG;IAC5B,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC,CAAC;AAEF,KAAK,eAAe,GAAG;IACrB,cAAc,EAAE,CACd,KAAK,EAAE,MAAM,EACb,IAAI,CAAC,EAAE,MAAM,KACV,OAAO,CAAC,eAAe,EAAE,GAAG,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;CACpE,CAAC;AAGF,qBAAa,kBAAmB,YAAW,SAAS;IAClD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAkB;IACzC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAS;gBAElB,MAAM,EAAE,eAAe,EAAE,OAAO,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE;IAK1D,QAAQ,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC;CAY5D"}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// Retriever that calls the MCP milvus_retrieve tool and shapes results into context chunks.
|
|
2
|
+
export class MCPMilvusRetriever {
|
|
3
|
+
client;
|
|
4
|
+
topK;
|
|
5
|
+
constructor(client, options) {
|
|
6
|
+
this.client = client;
|
|
7
|
+
this.topK = options?.topK ?? 5;
|
|
8
|
+
}
|
|
9
|
+
async retrieve(input) {
|
|
10
|
+
const query = buildSearchQuery(input);
|
|
11
|
+
if (!query)
|
|
12
|
+
return [];
|
|
13
|
+
try {
|
|
14
|
+
const results = await this.client.milvusRetrieve(query, this.topK);
|
|
15
|
+
return normalizeResults(results);
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
// Retrieval failures should not break the flow; return empty context instead.
|
|
19
|
+
return [];
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
// Combine user input fields into a single search string.
|
|
24
|
+
const buildSearchQuery = (input) => {
|
|
25
|
+
const queryParts = [input.question.trim()];
|
|
26
|
+
if (input.userContext?.trim()) {
|
|
27
|
+
queryParts.push(`Context: ${input.userContext.trim()}`);
|
|
28
|
+
}
|
|
29
|
+
if (input.constraints?.length) {
|
|
30
|
+
queryParts.push(`Constraints: ${input.constraints.filter(Boolean).join(" | ")}`);
|
|
31
|
+
}
|
|
32
|
+
return queryParts.filter(Boolean).join("\n").trim();
|
|
33
|
+
};
|
|
34
|
+
// Accepts any tool payload shape and converts it into ContextChunks.
|
|
35
|
+
const normalizeResults = (results) => {
|
|
36
|
+
if (typeof results === "string") {
|
|
37
|
+
return [{ source: "milvus", snippet: results }];
|
|
38
|
+
}
|
|
39
|
+
if (!Array.isArray(results)) {
|
|
40
|
+
return [{ source: "milvus", snippet: contentToSnippet(results) }];
|
|
41
|
+
}
|
|
42
|
+
return results.map((item, index) => {
|
|
43
|
+
const snippetParts = [item.rag_text ?? "(no rag_text)"];
|
|
44
|
+
if (item.metadata) {
|
|
45
|
+
snippetParts.push(buildMetadataSnippet(item.metadata));
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
source: item.id ? String(item.id) : `milvus-${index + 1}`,
|
|
49
|
+
snippet: snippetParts.join("\n"),
|
|
50
|
+
score: item.score,
|
|
51
|
+
};
|
|
52
|
+
});
|
|
53
|
+
};
|
|
54
|
+
const contentToSnippet = (content) => {
|
|
55
|
+
if (typeof content === "string")
|
|
56
|
+
return content;
|
|
57
|
+
if (Array.isArray(content))
|
|
58
|
+
return content.map((item) => JSON.stringify(item)).join("\n");
|
|
59
|
+
if (content && typeof content === "object")
|
|
60
|
+
return JSON.stringify(content);
|
|
61
|
+
return String(content ?? "");
|
|
62
|
+
};
|
|
63
|
+
const normalizeTableReference = (schema, table) => {
|
|
64
|
+
if (!table.includes(".")) {
|
|
65
|
+
return { schema, table };
|
|
66
|
+
}
|
|
67
|
+
const parts = table.split(".").filter(Boolean);
|
|
68
|
+
if (!parts.length) {
|
|
69
|
+
return { schema, table };
|
|
70
|
+
}
|
|
71
|
+
if (parts.length >= 2 && !schema) {
|
|
72
|
+
return {
|
|
73
|
+
schema: parts[parts.length - 2],
|
|
74
|
+
table: parts[parts.length - 1],
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
return { schema, table: parts[parts.length - 1] };
|
|
78
|
+
};
|
|
79
|
+
// Formats metadata for the prompt while avoiding fully qualified names that can confuse the model.
|
|
80
|
+
const buildMetadataSnippet = (metadata) => {
|
|
81
|
+
const parts = [];
|
|
82
|
+
const schema = typeof metadata.schema === "string" ? metadata.schema : "";
|
|
83
|
+
const tableRaw = metadata.table ?? metadata.table_name ?? metadata.name;
|
|
84
|
+
const table = typeof tableRaw === "string" ? tableRaw : "";
|
|
85
|
+
const normalized = normalizeTableReference(schema, table);
|
|
86
|
+
const qualified = [normalized.schema, normalized.table].filter(Boolean).join(".");
|
|
87
|
+
if (qualified)
|
|
88
|
+
parts.push(`table=${qualified}`);
|
|
89
|
+
const tableType = typeof metadata.table_type === "string"
|
|
90
|
+
? metadata.table_type
|
|
91
|
+
: typeof metadata.tableType === "string"
|
|
92
|
+
? metadata.tableType
|
|
93
|
+
: "";
|
|
94
|
+
if (tableType)
|
|
95
|
+
parts.push(`type=${tableType}`);
|
|
96
|
+
const primaryKeys = Array.isArray(metadata.primary_keys) ? metadata.primary_keys : [];
|
|
97
|
+
if (primaryKeys.length)
|
|
98
|
+
parts.push(`primary_keys=${primaryKeys.map(String).join(",")}`);
|
|
99
|
+
const columns = Array.isArray(metadata.columns)
|
|
100
|
+
? metadata.columns
|
|
101
|
+
.map((col) => (col && typeof col === "object" && "name" in col ? String(col.name) : null))
|
|
102
|
+
.filter((name) => Boolean(name))
|
|
103
|
+
: [];
|
|
104
|
+
if (columns.length)
|
|
105
|
+
parts.push(`columns=${columns.join(", ")}`);
|
|
106
|
+
const description = typeof metadata.description === "string" ? metadata.description : "";
|
|
107
|
+
if (description)
|
|
108
|
+
parts.push(`desc=${description}`);
|
|
109
|
+
if (!parts.length) {
|
|
110
|
+
const sanitized = { ...metadata };
|
|
111
|
+
delete sanitized.fully_qualified_name;
|
|
112
|
+
delete sanitized.fullyQualifiedName;
|
|
113
|
+
return `metadata=${JSON.stringify(sanitized)}`;
|
|
114
|
+
}
|
|
115
|
+
return parts.join("; ");
|
|
116
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import OpenAI from "openai";
|
|
2
|
+
import type { PromptPayload, QueryCandidate, ReasoningLLM } from "../types.js";
|
|
3
|
+
export type OpenAIOptions = {
|
|
4
|
+
client?: OpenAI;
|
|
5
|
+
apiKey?: string;
|
|
6
|
+
baseURL?: string;
|
|
7
|
+
model?: string;
|
|
8
|
+
temperature?: number;
|
|
9
|
+
};
|
|
10
|
+
export declare class OpenAILLM implements ReasoningLLM {
|
|
11
|
+
private readonly client;
|
|
12
|
+
private readonly model;
|
|
13
|
+
private readonly temperature;
|
|
14
|
+
constructor(options?: OpenAIOptions);
|
|
15
|
+
generate(payload: PromptPayload): Promise<QueryCandidate[]>;
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=openaiLLM.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"openaiLLM.d.ts","sourceRoot":"","sources":["../../src/adapters/openaiLLM.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,QAAQ,CAAC;AAC5B,OAAO,KAAK,EAAE,aAAa,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE/E,MAAM,MAAM,aAAa,GAAG;IAC1B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AAGF,qBAAa,SAAU,YAAW,YAAY;IAC5C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAS;IAC/B,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;gBAEzB,OAAO,GAAE,aAAkB;IAWjC,QAAQ,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC;CAwBlE"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import OpenAI from "openai";
|
|
2
|
+
// OpenAI-backed LLM that returns a single SQL candidate from a prompt.
|
|
3
|
+
export class OpenAILLM {
|
|
4
|
+
client;
|
|
5
|
+
model;
|
|
6
|
+
temperature;
|
|
7
|
+
constructor(options = {}) {
|
|
8
|
+
this.client =
|
|
9
|
+
options.client ??
|
|
10
|
+
new OpenAI({
|
|
11
|
+
apiKey: options.apiKey ?? process.env.OPENAI_API_KEY,
|
|
12
|
+
baseURL: options.baseURL ?? process.env.OPENAI_BASE_URL,
|
|
13
|
+
});
|
|
14
|
+
this.model = options.model ?? process.env.OPENAI_MODEL ?? "gpt-4o-mini";
|
|
15
|
+
this.temperature = options.temperature ?? 0;
|
|
16
|
+
}
|
|
17
|
+
async generate(payload) {
|
|
18
|
+
const completion = await this.client.responses.create({
|
|
19
|
+
model: this.model,
|
|
20
|
+
input: [
|
|
21
|
+
{
|
|
22
|
+
role: "user",
|
|
23
|
+
content: [{ type: "input_text", text: payload.prompt }],
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
temperature: this.temperature,
|
|
27
|
+
});
|
|
28
|
+
const text = extractText(completion);
|
|
29
|
+
const sql = text?.trim();
|
|
30
|
+
return sql
|
|
31
|
+
? [
|
|
32
|
+
{
|
|
33
|
+
sql,
|
|
34
|
+
rationale: "OpenAI single-shot",
|
|
35
|
+
},
|
|
36
|
+
]
|
|
37
|
+
: [];
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
// Pulls output text from the OpenAI Responses payload regardless of shape.
|
|
41
|
+
const extractText = (completion) => {
|
|
42
|
+
if (!completion || typeof completion !== "object")
|
|
43
|
+
return "";
|
|
44
|
+
const typed = completion;
|
|
45
|
+
if (typeof typed.output_text === "string")
|
|
46
|
+
return typed.output_text;
|
|
47
|
+
if (Array.isArray(typed.output)) {
|
|
48
|
+
for (const item of typed.output) {
|
|
49
|
+
if (item && item.type === "message" && Array.isArray(item.content)) {
|
|
50
|
+
const textPart = item.content.find((c) => c?.type === "output_text");
|
|
51
|
+
if (textPart && textPart.type === "output_text")
|
|
52
|
+
return textPart.text;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return "";
|
|
57
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { MCPClient } from "mcp-use";
|
|
2
|
+
import { MCPQueryExecutor } from "../adapters/mcpExecutor.js";
|
|
3
|
+
import { MCPMilvusRetriever } from "../adapters/mcpRetriever.js";
|
|
4
|
+
import type { MCPMilvusResult } from "../adapters/mcpRetriever.js";
|
|
5
|
+
export type MCPUseClientOptions = {
|
|
6
|
+
serverName?: string;
|
|
7
|
+
serverCwd?: string;
|
|
8
|
+
command?: string;
|
|
9
|
+
args?: string[];
|
|
10
|
+
env?: Record<string, string>;
|
|
11
|
+
};
|
|
12
|
+
export declare const createMcpUseAdapters: (options?: MCPUseClientOptions) => Promise<{
|
|
13
|
+
executor: MCPQueryExecutor;
|
|
14
|
+
retriever: MCPMilvusRetriever;
|
|
15
|
+
callTool: (name: "run_query" | "run_query_explain" | "list_indexes" | "milvus_retrieve", args: Record<string, unknown>) => Promise<string | MCPMilvusResult[] | Record<string, unknown>>;
|
|
16
|
+
runQueryExplain: (sql: string) => Promise<{
|
|
17
|
+
durationMs: number;
|
|
18
|
+
explainLines: string[] | undefined;
|
|
19
|
+
planningMs: number | undefined;
|
|
20
|
+
executionMs: number | undefined;
|
|
21
|
+
raw: string;
|
|
22
|
+
explain?: undefined;
|
|
23
|
+
} | {
|
|
24
|
+
durationMs: number;
|
|
25
|
+
explain: string;
|
|
26
|
+
raw: string;
|
|
27
|
+
explainLines?: undefined;
|
|
28
|
+
planningMs?: undefined;
|
|
29
|
+
executionMs?: undefined;
|
|
30
|
+
}>;
|
|
31
|
+
listIndexes: (options?: {
|
|
32
|
+
schemaname?: string;
|
|
33
|
+
tablename?: string;
|
|
34
|
+
}) => Promise<string>;
|
|
35
|
+
close: () => Promise<void>;
|
|
36
|
+
client: MCPClient;
|
|
37
|
+
session: import("mcp-use").MCPSession;
|
|
38
|
+
}>;
|
|
39
|
+
//# sourceMappingURL=mcpUseClient.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mcpUseClient.d.ts","sourceRoot":"","sources":["../../src/clients/mcpUseClient.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AACpC,OAAO,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAC;AAC9D,OAAO,EAAE,kBAAkB,EAAE,MAAM,6BAA6B,CAAC;AACjE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AAGnE,MAAM,MAAM,mBAAmB,GAAG;IAChC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC9B,CAAC;AAwEF,eAAO,MAAM,oBAAoB,GAAU,UAAU,mBAAmB;;;qBA2C9D,WAAW,GAAG,mBAAmB,GAAG,cAAc,GAAG,iBAAiB,QACtE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAC5B,OAAO,CAAC,MAAM,GAAG,eAAe,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;2BAuB5B,MAAM;;;;;;;;;;;;;;;4BAoDL;QACnC,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,SAAS,CAAC,EAAE,MAAM,CAAC;KACpB;;;;EA2BF,CAAC"}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { MCPClient } from "mcp-use";
|
|
4
|
+
import { MCPQueryExecutor } from "../adapters/mcpExecutor.js";
|
|
5
|
+
import { MCPMilvusRetriever } from "../adapters/mcpRetriever.js";
|
|
6
|
+
// Accepts a whitespace-delimited env var and turns it into an args array.
|
|
7
|
+
const parseArgs = (value) => {
|
|
8
|
+
if (!value)
|
|
9
|
+
return undefined;
|
|
10
|
+
return value
|
|
11
|
+
.split(" ")
|
|
12
|
+
.map((part) => part.trim())
|
|
13
|
+
.filter(Boolean);
|
|
14
|
+
};
|
|
15
|
+
// Convert relative file-like args to absolute paths so Bun can locate the entrypoint
|
|
16
|
+
// even if the MCP client spawns from a different working directory.
|
|
17
|
+
const absolutizeArgs = (args, baseDir) => {
|
|
18
|
+
return args.map((arg) => {
|
|
19
|
+
if (path.isAbsolute(arg))
|
|
20
|
+
return arg;
|
|
21
|
+
const candidate = path.join(baseDir, arg);
|
|
22
|
+
return fs.existsSync(candidate) ? candidate : arg;
|
|
23
|
+
});
|
|
24
|
+
};
|
|
25
|
+
// Extracts the textual part from any MCP tool result content.
|
|
26
|
+
const contentToText = (content) => {
|
|
27
|
+
if (typeof content === "string")
|
|
28
|
+
return content;
|
|
29
|
+
if (!content)
|
|
30
|
+
return "";
|
|
31
|
+
if (Array.isArray(content)) {
|
|
32
|
+
return content
|
|
33
|
+
.map((item) => {
|
|
34
|
+
if (typeof item === "string")
|
|
35
|
+
return item;
|
|
36
|
+
if (item && typeof item === "object" && "text" in item) {
|
|
37
|
+
return String(item.text);
|
|
38
|
+
}
|
|
39
|
+
return JSON.stringify(item);
|
|
40
|
+
})
|
|
41
|
+
.join("\n");
|
|
42
|
+
}
|
|
43
|
+
if (typeof content === "object" &&
|
|
44
|
+
"text" in content) {
|
|
45
|
+
return String(content.text);
|
|
46
|
+
}
|
|
47
|
+
return JSON.stringify(content);
|
|
48
|
+
};
|
|
49
|
+
const toolResultPayload = (result) => {
|
|
50
|
+
if (!result)
|
|
51
|
+
return "";
|
|
52
|
+
if (typeof result === "object" && result !== null) {
|
|
53
|
+
const typed = result;
|
|
54
|
+
if (typed.isError) {
|
|
55
|
+
const message = contentToText(typed.content ?? result) || "MCP tool returned an error";
|
|
56
|
+
throw new Error(message);
|
|
57
|
+
}
|
|
58
|
+
if (typed.structuredContent)
|
|
59
|
+
return typed.structuredContent;
|
|
60
|
+
if (typed.content)
|
|
61
|
+
return contentToText(typed.content);
|
|
62
|
+
}
|
|
63
|
+
return contentToText(result);
|
|
64
|
+
};
|
|
65
|
+
const parseDurationMs = (text) => {
|
|
66
|
+
const match = text.match(/([0-9]+(?:\.[0-9]+)?)\s*ms/i);
|
|
67
|
+
return match ? Number(match[1]) : undefined;
|
|
68
|
+
};
|
|
69
|
+
// Starts mcp-use against the sibling MCP server and returns MCP-backed adapters.
|
|
70
|
+
export const createMcpUseAdapters = async (options) => {
|
|
71
|
+
const serverName = options?.serverName ?? "query-runner";
|
|
72
|
+
console.log("mcp server cwd", process.env.MCP_SERVER_CWD);
|
|
73
|
+
const serverCwd = options?.serverCwd ??
|
|
74
|
+
process.env.MCP_SERVER_CWD ??
|
|
75
|
+
path.resolve(process.cwd(), "..", "mcp-server");
|
|
76
|
+
const command = options?.command ?? process.env.MCP_SERVER_CMD ?? "bun";
|
|
77
|
+
const rawArgs = options?.args ??
|
|
78
|
+
parseArgs(process.env.MCP_SERVER_ARGS) ?? ["run", "start"];
|
|
79
|
+
const args = absolutizeArgs(rawArgs, serverCwd);
|
|
80
|
+
if (!fs.existsSync(serverCwd)) {
|
|
81
|
+
throw new Error(`MCP server directory not found at ${serverCwd}. Set MCP_SERVER_CWD or pass options.serverCwd to point at your MCP server (see README).`);
|
|
82
|
+
}
|
|
83
|
+
const client = new MCPClient({
|
|
84
|
+
mcpServers: {
|
|
85
|
+
[serverName]: {
|
|
86
|
+
command,
|
|
87
|
+
args,
|
|
88
|
+
cwd: serverCwd,
|
|
89
|
+
env: options?.env,
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
console.log(`Starting MCP server "${serverName}" via ${command} ${args.join(" ")} (cwd: ${serverCwd})`);
|
|
94
|
+
await client.createAllSessions();
|
|
95
|
+
const session = client.getSession(serverName);
|
|
96
|
+
if (!session) {
|
|
97
|
+
throw new Error(`Failed to start MCP session for ${serverName}`);
|
|
98
|
+
}
|
|
99
|
+
const callTool = async (name, args) => {
|
|
100
|
+
const result = await session.callTool(name, args);
|
|
101
|
+
return toolResultPayload(result);
|
|
102
|
+
};
|
|
103
|
+
const executor = new MCPQueryExecutor({
|
|
104
|
+
runQuery: async (sql) => {
|
|
105
|
+
const content = await callTool("run_query", { raw_query: sql });
|
|
106
|
+
const text = contentToText(content);
|
|
107
|
+
const durationMs = parseDurationMs(text) ?? Number.NaN; // Keep numeric for typing even if parse fails.
|
|
108
|
+
return { durationMs };
|
|
109
|
+
},
|
|
110
|
+
runQueryExplain: async (sql) => runQueryExplain(sql),
|
|
111
|
+
});
|
|
112
|
+
const retriever = new MCPMilvusRetriever({
|
|
113
|
+
milvusRetrieve: async (query, topK) => callTool("milvus_retrieve", { query, top_k: topK ?? 5 }),
|
|
114
|
+
});
|
|
115
|
+
const runQueryExplain = async (sql) => {
|
|
116
|
+
const content = await callTool("run_query_explain", { raw_query: sql });
|
|
117
|
+
const text = contentToText(content);
|
|
118
|
+
const durationMs = parseDurationMs(text) ?? Number.NaN;
|
|
119
|
+
const jsonStart = text.search(/JSON:\s*/i);
|
|
120
|
+
const explainMarker = text.search(/EXPLAIN ANALYZE:\s*/i);
|
|
121
|
+
let parsed = null;
|
|
122
|
+
if (jsonStart !== -1) {
|
|
123
|
+
const jsonBlock = text
|
|
124
|
+
.slice(jsonStart)
|
|
125
|
+
.replace(/^JSON:\s*/i, "");
|
|
126
|
+
const jsonText = explainMarker !== -1
|
|
127
|
+
? jsonBlock.slice(0, explainMarker - jsonStart).trim()
|
|
128
|
+
: jsonBlock.trim();
|
|
129
|
+
try {
|
|
130
|
+
parsed = JSON.parse(jsonText);
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
parsed = null;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (parsed) {
|
|
137
|
+
const explainLines = Array.isArray(parsed.explainLines)
|
|
138
|
+
? parsed.explainLines.map((line) => String(line))
|
|
139
|
+
: undefined;
|
|
140
|
+
return {
|
|
141
|
+
durationMs: typeof parsed.durationMs === "number"
|
|
142
|
+
? parsed.durationMs
|
|
143
|
+
: durationMs,
|
|
144
|
+
explainLines,
|
|
145
|
+
planningMs: typeof parsed.planningMs === "number"
|
|
146
|
+
? parsed.planningMs
|
|
147
|
+
: undefined,
|
|
148
|
+
executionMs: typeof parsed.executionMs === "number"
|
|
149
|
+
? parsed.executionMs
|
|
150
|
+
: undefined,
|
|
151
|
+
raw: text,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
const explainSplit = text.split(/EXPLAIN ANALYZE:\s*/i)[1] ??
|
|
155
|
+
text.split(/EXPLAIN:\s*/i)[1] ??
|
|
156
|
+
"";
|
|
157
|
+
return { durationMs, explain: explainSplit.trim(), raw: text };
|
|
158
|
+
};
|
|
159
|
+
const listIndexes = async (options) => {
|
|
160
|
+
const content = await callTool("list_indexes", {
|
|
161
|
+
schemaname: options?.schemaname,
|
|
162
|
+
tablename: options?.tablename,
|
|
163
|
+
});
|
|
164
|
+
return contentToText(content);
|
|
165
|
+
};
|
|
166
|
+
const close = async () => {
|
|
167
|
+
if (typeof client
|
|
168
|
+
.closeAllSessions === "function") {
|
|
169
|
+
await client.closeAllSessions();
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
return {
|
|
173
|
+
executor,
|
|
174
|
+
retriever,
|
|
175
|
+
callTool,
|
|
176
|
+
runQueryExplain,
|
|
177
|
+
listIndexes,
|
|
178
|
+
close,
|
|
179
|
+
client,
|
|
180
|
+
session,
|
|
181
|
+
};
|
|
182
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ContextChunk, PromptComposer, PromptInput, PromptPayload } from "../types.js";
|
|
2
|
+
export declare class DefaultPromptComposer implements PromptComposer {
|
|
3
|
+
private readonly systemPrompt;
|
|
4
|
+
constructor(systemPrompt?: string);
|
|
5
|
+
compose(input: PromptInput, retrieved: ContextChunk[]): Promise<PromptPayload>;
|
|
6
|
+
}
|
|
7
|
+
//# sourceMappingURL=promptComposer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"promptComposer.d.ts","sourceRoot":"","sources":["../../src/defaults/promptComposer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,cAAc,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAW5F,qBAAa,qBAAsB,YAAW,cAAc;IAC1D,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;gBAE1B,YAAY,SAAqB;IAIvC,OAAO,CAAC,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,YAAY,EAAE,GAAG,OAAO,CAAC,aAAa,CAAC;CAsBrF"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
const defaultInstruction = [
|
|
2
|
+
"You are a text-to-SQL planner.",
|
|
3
|
+
"Use the provided context to stay within available schema; only use table and column names that appear in the context.",
|
|
4
|
+
"Return only SQL and keep it safe for read-only execution.",
|
|
5
|
+
"Output a single SELECT/CTE statement with no trailing semicolon, no markdown/code fences, and no extra commentary.",
|
|
6
|
+
"If the context is empty or does not mention any relevant tables/columns for the question, do not guess; instead return: SELECT 'NO_RELEVANT_TABLE'::text AS reason",
|
|
7
|
+
].join(" ");
|
|
8
|
+
// Builds a simple text prompt that threads user input and retrieved context.
|
|
9
|
+
export class DefaultPromptComposer {
|
|
10
|
+
systemPrompt;
|
|
11
|
+
constructor(systemPrompt = defaultInstruction) {
|
|
12
|
+
this.systemPrompt = systemPrompt;
|
|
13
|
+
}
|
|
14
|
+
async compose(input, retrieved) {
|
|
15
|
+
const contextBlock = retrieved
|
|
16
|
+
.map((c, idx) => `#${idx + 1} [${c.source}] (score=${c.score ?? "n/a"})\n${c.snippet}`)
|
|
17
|
+
.join("\n\n");
|
|
18
|
+
const constraints = input.constraints?.length ? `Constraints:\n- ${input.constraints.join("\n- ")}` : "";
|
|
19
|
+
const prompt = [
|
|
20
|
+
this.systemPrompt,
|
|
21
|
+
constraints,
|
|
22
|
+
"User question:",
|
|
23
|
+
input.question,
|
|
24
|
+
input.userContext ? `User context: ${input.userContext}` : "",
|
|
25
|
+
"Context:",
|
|
26
|
+
contextBlock || "(none)",
|
|
27
|
+
"Return a best-effort SQL statement.",
|
|
28
|
+
]
|
|
29
|
+
.filter(Boolean)
|
|
30
|
+
.join("\n\n");
|
|
31
|
+
return { prompt, context: retrieved };
|
|
32
|
+
}
|
|
33
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export * from "./types.js";
|
|
2
|
+
export * from "./pipeline.js";
|
|
3
|
+
export * from "./defaults/promptComposer.js";
|
|
4
|
+
export * from "./adapters/mcpExecutor.js";
|
|
5
|
+
export * from "./adapters/mcpRetriever.js";
|
|
6
|
+
export * from "./adapters/openaiLLM.js";
|
|
7
|
+
export * from "./clients/mcpUseClient.js";
|
|
8
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,eAAe,CAAC;AAC9B,cAAc,8BAA8B,CAAC;AAC7C,cAAc,2BAA2B,CAAC;AAC1C,cAAc,4BAA4B,CAAC;AAC3C,cAAc,yBAAyB,CAAC;AACxC,cAAc,2BAA2B,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export * from "./types.js";
|
|
2
|
+
export * from "./pipeline.js";
|
|
3
|
+
export * from "./defaults/promptComposer.js";
|
|
4
|
+
export * from "./adapters/mcpExecutor.js";
|
|
5
|
+
export * from "./adapters/mcpRetriever.js";
|
|
6
|
+
export * from "./adapters/openaiLLM.js";
|
|
7
|
+
export * from "./clients/mcpUseClient.js";
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { ExecutionResult, FeedbackPolicy, FlowConfig, FlowOutput, IndexLookup, PromptComposer, PromptInput, QueryExecutor, ReasoningLLM, Retriever } from "./types.js";
|
|
2
|
+
export declare class ReasoningPipeline {
|
|
3
|
+
private readonly composer;
|
|
4
|
+
private readonly retriever;
|
|
5
|
+
private readonly llm;
|
|
6
|
+
private readonly executor;
|
|
7
|
+
private readonly policy;
|
|
8
|
+
private readonly indexLookup?;
|
|
9
|
+
private readonly config;
|
|
10
|
+
constructor(options: {
|
|
11
|
+
composer: PromptComposer;
|
|
12
|
+
retriever: Retriever;
|
|
13
|
+
llm: ReasoningLLM;
|
|
14
|
+
executor: QueryExecutor;
|
|
15
|
+
policy: FeedbackPolicy;
|
|
16
|
+
indexLookup?: IndexLookup;
|
|
17
|
+
config?: FlowConfig;
|
|
18
|
+
});
|
|
19
|
+
run(input: PromptInput): Promise<FlowOutput>;
|
|
20
|
+
}
|
|
21
|
+
export declare class SimpleFeedbackPolicy implements FeedbackPolicy {
|
|
22
|
+
private readonly durationBudgetMs;
|
|
23
|
+
constructor(options?: {
|
|
24
|
+
durationBudgetMs?: number;
|
|
25
|
+
});
|
|
26
|
+
accept(result: ExecutionResult): boolean;
|
|
27
|
+
shouldRetry(result: ExecutionResult, attempt: number): boolean;
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=pipeline.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pipeline.d.ts","sourceRoot":"","sources":["../src/pipeline.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,eAAe,EACf,cAAc,EACd,UAAU,EAEV,UAAU,EACV,WAAW,EACX,cAAc,EACd,WAAW,EAEX,aAAa,EACb,YAAY,EACZ,SAAS,EACV,MAAM,YAAY,CAAC;AAQpB,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAiB;IAC1C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAY;IACtC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;IACnC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAgB;IACzC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAiB;IACxC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAc;IAC3C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAuB;gBAElC,OAAO,EAAE;QACnB,QAAQ,EAAE,cAAc,CAAC;QACzB,SAAS,EAAE,SAAS,CAAC;QACrB,GAAG,EAAE,YAAY,CAAC;QAClB,QAAQ,EAAE,aAAa,CAAC;QACxB,MAAM,EAAE,cAAc,CAAC;QACvB,WAAW,CAAC,EAAE,WAAW,CAAC;QAC1B,MAAM,CAAC,EAAE,UAAU,CAAC;KACrB;IAWK,GAAG,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,UAAU,CAAC;CAkDnD;AAGD,qBAAa,oBAAqB,YAAW,cAAc;IACzD,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAS;gBAE9B,OAAO,CAAC,EAAE;QAAE,gBAAgB,CAAC,EAAE,MAAM,CAAA;KAAE;IAInD,MAAM,CAAC,MAAM,EAAE,eAAe,GAAG,OAAO;IAIxC,WAAW,CAAC,MAAM,EAAE,eAAe,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO;CAK/D"}
|
package/dist/pipeline.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// Default config values used when a field is not provided.
|
|
2
|
+
const defaultConfig = {
|
|
3
|
+
maxAttempts: 3,
|
|
4
|
+
};
|
|
5
|
+
// Orchestrates the reasoning flow: retrieve → compose → LLM → execute with feedback-driven retries.
|
|
6
|
+
export class ReasoningPipeline {
|
|
7
|
+
composer;
|
|
8
|
+
retriever;
|
|
9
|
+
llm;
|
|
10
|
+
executor;
|
|
11
|
+
policy;
|
|
12
|
+
indexLookup;
|
|
13
|
+
config;
|
|
14
|
+
constructor(options) {
|
|
15
|
+
this.composer = options.composer;
|
|
16
|
+
this.retriever = options.retriever;
|
|
17
|
+
this.llm = options.llm;
|
|
18
|
+
this.executor = options.executor;
|
|
19
|
+
this.policy = options.policy;
|
|
20
|
+
this.indexLookup = options.indexLookup;
|
|
21
|
+
this.config = { ...defaultConfig, ...options.config };
|
|
22
|
+
}
|
|
23
|
+
// Runs a single question through the pipeline and returns the outcome plus attempt logs.
|
|
24
|
+
async run(input) {
|
|
25
|
+
const baseConstraints = [...(input.constraints ?? [])];
|
|
26
|
+
if (this.indexLookup && input.indexFilters) {
|
|
27
|
+
const indexInfo = await this.indexLookup.listIndexes(input.indexFilters);
|
|
28
|
+
if (indexInfo.trim()) {
|
|
29
|
+
baseConstraints.push(`Available indexes:\n${indexInfo}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
const logs = [];
|
|
33
|
+
const attemptsLimit = this.config.maxAttempts;
|
|
34
|
+
let lastError;
|
|
35
|
+
let lastCandidate;
|
|
36
|
+
for (let attempt = 0; attempt < attemptsLimit; attempt++) {
|
|
37
|
+
const attemptNumber = attempt + 1;
|
|
38
|
+
const constraints = [...baseConstraints];
|
|
39
|
+
if (lastError) {
|
|
40
|
+
constraints.push(`Previous attempt error:\n${lastError}`);
|
|
41
|
+
}
|
|
42
|
+
if (lastCandidate?.sql) {
|
|
43
|
+
constraints.push(`Previous attempt SQL:\n${lastCandidate.sql}`);
|
|
44
|
+
}
|
|
45
|
+
const attemptInput = { ...input, constraints };
|
|
46
|
+
const context = await this.retriever.retrieve(attemptInput);
|
|
47
|
+
const prompt = await this.composer.compose(attemptInput, context);
|
|
48
|
+
const candidates = await this.llm.generate(prompt);
|
|
49
|
+
const candidate = candidates[0];
|
|
50
|
+
if (!candidate)
|
|
51
|
+
break;
|
|
52
|
+
const execution = await this.executor.run(candidate);
|
|
53
|
+
logs.push({ attempt: attemptNumber, candidate, execution });
|
|
54
|
+
if (this.policy.accept(execution)) {
|
|
55
|
+
return { accepted: true, candidate, result: execution, logs };
|
|
56
|
+
}
|
|
57
|
+
const shouldRetry = this.policy.shouldRetry(execution, attemptNumber);
|
|
58
|
+
const hasAttemptsLeft = attemptNumber < attemptsLimit;
|
|
59
|
+
if (!shouldRetry || !hasAttemptsLeft) {
|
|
60
|
+
return { accepted: false, candidate, result: execution, logs };
|
|
61
|
+
}
|
|
62
|
+
lastError = execution.error;
|
|
63
|
+
lastCandidate = candidate;
|
|
64
|
+
}
|
|
65
|
+
return { accepted: false, logs };
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// Simple policy: succeed if ok and under a latency budget; otherwise allow a few retries.
|
|
69
|
+
export class SimpleFeedbackPolicy {
|
|
70
|
+
durationBudgetMs;
|
|
71
|
+
constructor(options) {
|
|
72
|
+
this.durationBudgetMs = options?.durationBudgetMs ?? 5000;
|
|
73
|
+
}
|
|
74
|
+
accept(result) {
|
|
75
|
+
return result.ok && (result.durationMs ?? Infinity) <= this.durationBudgetMs;
|
|
76
|
+
}
|
|
77
|
+
shouldRetry(result, attempt) {
|
|
78
|
+
if (result.ok)
|
|
79
|
+
return false;
|
|
80
|
+
if ((result.durationMs ?? Infinity) > this.durationBudgetMs)
|
|
81
|
+
return attempt < 2;
|
|
82
|
+
return attempt < 3;
|
|
83
|
+
}
|
|
84
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
export type ContextChunk = {
|
|
2
|
+
source: string;
|
|
3
|
+
snippet: string;
|
|
4
|
+
score?: number;
|
|
5
|
+
};
|
|
6
|
+
export type PromptInput = {
|
|
7
|
+
question: string;
|
|
8
|
+
userContext?: string;
|
|
9
|
+
constraints?: string[];
|
|
10
|
+
indexFilters?: IndexFilters;
|
|
11
|
+
};
|
|
12
|
+
export type PromptPayload = {
|
|
13
|
+
prompt: string;
|
|
14
|
+
context: ContextChunk[];
|
|
15
|
+
};
|
|
16
|
+
export type QueryCandidate = {
|
|
17
|
+
sql: string;
|
|
18
|
+
rationale?: string;
|
|
19
|
+
};
|
|
20
|
+
export type ExecutionResult = {
|
|
21
|
+
ok: boolean;
|
|
22
|
+
durationMs?: number;
|
|
23
|
+
rows?: number;
|
|
24
|
+
error?: string;
|
|
25
|
+
explain?: string;
|
|
26
|
+
rawExplain?: string;
|
|
27
|
+
explainLines?: string[];
|
|
28
|
+
planningMs?: number;
|
|
29
|
+
executionMs?: number;
|
|
30
|
+
};
|
|
31
|
+
export interface PromptComposer {
|
|
32
|
+
compose(input: PromptInput, retrieved: ContextChunk[]): Promise<PromptPayload>;
|
|
33
|
+
}
|
|
34
|
+
export interface Retriever {
|
|
35
|
+
retrieve(input: PromptInput): Promise<ContextChunk[]>;
|
|
36
|
+
}
|
|
37
|
+
export interface ReasoningLLM {
|
|
38
|
+
generate(prompt: PromptPayload): Promise<QueryCandidate[]>;
|
|
39
|
+
}
|
|
40
|
+
export interface QueryExecutor {
|
|
41
|
+
run(candidate: QueryCandidate): Promise<ExecutionResult>;
|
|
42
|
+
}
|
|
43
|
+
export type IndexFilters = {
|
|
44
|
+
schemaname?: string;
|
|
45
|
+
tablename?: string;
|
|
46
|
+
};
|
|
47
|
+
export interface IndexLookup {
|
|
48
|
+
listIndexes(filters?: IndexFilters): Promise<string>;
|
|
49
|
+
}
|
|
50
|
+
export interface FeedbackPolicy {
|
|
51
|
+
accept(result: ExecutionResult): boolean;
|
|
52
|
+
shouldRetry(result: ExecutionResult, attempt: number): boolean;
|
|
53
|
+
}
|
|
54
|
+
export type FlowConfig = {
|
|
55
|
+
maxAttempts?: number;
|
|
56
|
+
};
|
|
57
|
+
export type FlowLog = {
|
|
58
|
+
attempt: number;
|
|
59
|
+
candidate: QueryCandidate;
|
|
60
|
+
execution: ExecutionResult;
|
|
61
|
+
};
|
|
62
|
+
export type FlowOutput = {
|
|
63
|
+
accepted: boolean;
|
|
64
|
+
candidate?: QueryCandidate;
|
|
65
|
+
result?: ExecutionResult;
|
|
66
|
+
logs: FlowLog[];
|
|
67
|
+
};
|
|
68
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AACA,MAAM,MAAM,YAAY,GAAG;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,YAAY,CAAC,EAAE,YAAY,CAAC;CAC7B,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,YAAY,EAAE,CAAC;CACzB,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG;IAC3B,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAGF,MAAM,MAAM,eAAe,GAAG;IAC5B,EAAE,EAAE,OAAO,CAAC;IACZ,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AAGF,MAAM,WAAW,cAAc;IAC7B,OAAO,CACL,KAAK,EAAE,WAAW,EAClB,SAAS,EAAE,YAAY,EAAE,GACxB,OAAO,CAAC,aAAa,CAAC,CAAC;CAC3B;AAED,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,CAAC;CACvD;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC,CAAC;CAC5D;AAED,MAAM,WAAW,aAAa;IAC5B,GAAG,CAAC,SAAS,EAAE,cAAc,GAAG,OAAO,CAAC,eAAe,CAAC,CAAC;CAC1D;AAED,MAAM,MAAM,YAAY,GAAG;IACzB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,MAAM,WAAW,WAAW;IAC1B,WAAW,CAAC,OAAO,CAAC,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CACtD;AAED,MAAM,WAAW,cAAc;IAC7B,MAAM,CAAC,MAAM,EAAE,eAAe,GAAG,OAAO,CAAC;IACzC,WAAW,CAAC,MAAM,EAAE,eAAe,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC;CAChE;AAED,MAAM,MAAM,UAAU,GAAG;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AAGF,MAAM,MAAM,OAAO,GAAG;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,cAAc,CAAC;IAC1B,SAAS,EAAE,eAAe,CAAC;CAC5B,CAAC;AAGF,MAAM,MAAM,UAAU,GAAG;IACvB,QAAQ,EAAE,OAAO,CAAC;IAClB,SAAS,CAAC,EAAE,cAAc,CAAC;IAC3B,MAAM,CAAC,EAAE,eAAe,CAAC;IACzB,IAAI,EAAE,OAAO,EAAE,CAAC;CACjB,CAAC"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@neural-technologies-indonesia/nl2sql",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Core reasoning pipeline for text-to-SQL.",
|
|
5
|
+
"private": false,
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"module": "./dist/index.js",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"import": "./dist/index.js"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist",
|
|
18
|
+
"README.md"
|
|
19
|
+
],
|
|
20
|
+
"sideEffects": false,
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsc -p tsconfig.build.json",
|
|
23
|
+
"lint": "echo \"add linter\"",
|
|
24
|
+
"test": "echo \"no tests yet\""
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"langchain": "^1.2.1",
|
|
28
|
+
"mcp-use": "*",
|
|
29
|
+
"openai": "^6.14.0"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@types/bun": "latest"
|
|
33
|
+
},
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"typescript": "^5"
|
|
36
|
+
},
|
|
37
|
+
"publishConfig": {
|
|
38
|
+
"access": "public"
|
|
39
|
+
}
|
|
40
|
+
}
|