nexus-ai-pro 2.2.0 → 2.3.0
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/API_STABILITY.md +39 -0
- package/CHANGELOG.md +142 -1
- package/README.md +10 -5
- package/dist/agent/capabilities.d.ts +39 -0
- package/dist/agent/capabilities.js +36 -0
- package/dist/agent/create-agent.d.ts +61 -11
- package/dist/agent/create-agent.js +136 -22
- package/dist/agent/index.d.ts +3 -2
- package/dist/agent/index.js +1 -1
- package/dist/agent/middleware/approval.d.ts +26 -0
- package/dist/agent/middleware/approval.js +23 -0
- package/dist/agent/{middleware.d.ts → middleware/basic.d.ts} +5 -4
- package/dist/agent/{middleware.js → middleware/basic.js} +3 -5
- package/dist/agent/middleware/context-editor.d.ts +52 -0
- package/dist/agent/middleware/context-editor.js +66 -0
- package/dist/agent/middleware/filesystem.d.ts +44 -0
- package/dist/agent/middleware/filesystem.js +83 -0
- package/dist/agent/middleware/index.d.ts +19 -0
- package/dist/agent/middleware/index.js +9 -0
- package/dist/agent/middleware/limits.d.ts +42 -0
- package/dist/agent/middleware/limits.js +52 -0
- package/dist/agent/middleware/models.d.ts +48 -0
- package/dist/agent/middleware/models.js +44 -0
- package/dist/agent/middleware/pii.d.ts +67 -0
- package/dist/agent/middleware/pii.js +173 -0
- package/dist/agent/middleware/retry.d.ts +66 -0
- package/dist/agent/middleware/retry.js +59 -0
- package/dist/agent/middleware/shared.d.ts +41 -0
- package/dist/agent/middleware/shared.js +73 -0
- package/dist/agent/middleware/tool-selector.d.ts +64 -0
- package/dist/agent/middleware/tool-selector.js +106 -0
- package/dist/agent/permissions.d.ts +113 -0
- package/dist/agent/permissions.js +180 -0
- package/dist/agent/sandbox.d.ts +151 -0
- package/dist/agent/sandbox.js +257 -0
- package/dist/agent/tool.d.ts +8 -3
- package/dist/agent/tool.js +14 -3
- package/dist/deep-agent/index.d.ts +109 -0
- package/dist/deep-agent/index.js +216 -0
- package/dist/graph/functional.d.ts +9 -0
- package/dist/graph/functional.js +20 -3
- package/dist/graph/graph.js +25 -4
- package/dist/graph/lint.d.ts +4 -1
- package/dist/graph/lint.js +35 -0
- package/dist/postgres/fulltext.d.ts +67 -0
- package/dist/postgres/fulltext.js +91 -0
- package/dist/rag/elasticsearch.d.ts +71 -0
- package/dist/rag/elasticsearch.js +130 -0
- package/dist/rag/pipeline.d.ts +140 -0
- package/dist/rag/pipeline.js +187 -0
- package/dist/rag/rerankers.d.ts +92 -0
- package/dist/rag/rerankers.js +97 -0
- package/dist/rag/retrievers.d.ts +12 -1
- package/dist/server/assistant.d.ts +5 -1
- package/dist/server/assistant.js +4 -2
- package/dist/server/auth.d.ts +250 -0
- package/dist/server/auth.js +392 -0
- package/dist/store/memory.d.ts +10 -0
- package/dist/store/memory.js +10 -0
- package/dist/types/graph.d.ts +38 -0
- package/dist/types/messages.d.ts +36 -2
- package/dist/types/principal.d.ts +21 -0
- package/dist/types/principal.js +1 -0
- package/dist/types/server.d.ts +2 -9
- package/dist/utils/tenant.d.ts +11 -0
- package/dist/utils/tenant.js +14 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist-cjs/agent/capabilities.d.ts +39 -0
- package/dist-cjs/agent/capabilities.js +41 -0
- package/dist-cjs/agent/create-agent.d.ts +61 -11
- package/dist-cjs/agent/create-agent.js +136 -22
- package/dist-cjs/agent/index.d.ts +3 -2
- package/dist-cjs/agent/index.js +4 -4
- package/dist-cjs/agent/middleware/approval.d.ts +26 -0
- package/dist-cjs/agent/middleware/approval.js +26 -0
- package/dist-cjs/agent/{middleware.d.ts → middleware/basic.d.ts} +5 -4
- package/dist-cjs/agent/{middleware.js → middleware/basic.js} +3 -5
- package/dist-cjs/agent/middleware/context-editor.d.ts +52 -0
- package/dist-cjs/agent/middleware/context-editor.js +70 -0
- package/dist-cjs/agent/middleware/filesystem.d.ts +44 -0
- package/dist-cjs/agent/middleware/filesystem.js +86 -0
- package/dist-cjs/agent/middleware/index.d.ts +19 -0
- package/dist-cjs/agent/middleware/index.js +28 -0
- package/dist-cjs/agent/middleware/limits.d.ts +42 -0
- package/dist-cjs/agent/middleware/limits.js +57 -0
- package/dist-cjs/agent/middleware/models.d.ts +48 -0
- package/dist-cjs/agent/middleware/models.js +48 -0
- package/dist-cjs/agent/middleware/pii.d.ts +67 -0
- package/dist-cjs/agent/middleware/pii.js +178 -0
- package/dist-cjs/agent/middleware/retry.d.ts +66 -0
- package/dist-cjs/agent/middleware/retry.js +63 -0
- package/dist-cjs/agent/middleware/shared.d.ts +41 -0
- package/dist-cjs/agent/middleware/shared.js +83 -0
- package/dist-cjs/agent/middleware/tool-selector.d.ts +64 -0
- package/dist-cjs/agent/middleware/tool-selector.js +109 -0
- package/dist-cjs/agent/permissions.d.ts +113 -0
- package/dist-cjs/agent/permissions.js +189 -0
- package/dist-cjs/agent/sandbox.d.ts +151 -0
- package/dist-cjs/agent/sandbox.js +297 -0
- package/dist-cjs/agent/tool.d.ts +8 -3
- package/dist-cjs/agent/tool.js +14 -3
- package/dist-cjs/deep-agent/index.d.ts +109 -0
- package/dist-cjs/deep-agent/index.js +219 -0
- package/dist-cjs/graph/functional.d.ts +9 -0
- package/dist-cjs/graph/functional.js +20 -3
- package/dist-cjs/graph/graph.js +25 -4
- package/dist-cjs/graph/lint.d.ts +4 -1
- package/dist-cjs/graph/lint.js +35 -0
- package/dist-cjs/postgres/fulltext.d.ts +67 -0
- package/dist-cjs/postgres/fulltext.js +129 -0
- package/dist-cjs/rag/elasticsearch.d.ts +71 -0
- package/dist-cjs/rag/elasticsearch.js +135 -0
- package/dist-cjs/rag/pipeline.d.ts +140 -0
- package/dist-cjs/rag/pipeline.js +191 -0
- package/dist-cjs/rag/rerankers.d.ts +92 -0
- package/dist-cjs/rag/rerankers.js +107 -0
- package/dist-cjs/rag/retrievers.d.ts +12 -1
- package/dist-cjs/server/assistant.d.ts +5 -1
- package/dist-cjs/server/assistant.js +4 -2
- package/dist-cjs/server/auth.d.ts +250 -0
- package/dist-cjs/server/auth.js +402 -0
- package/dist-cjs/store/memory.d.ts +10 -0
- package/dist-cjs/store/memory.js +10 -0
- package/dist-cjs/types/graph.d.ts +38 -0
- package/dist-cjs/types/messages.d.ts +36 -2
- package/dist-cjs/types/principal.d.ts +21 -0
- package/dist-cjs/types/principal.js +2 -0
- package/dist-cjs/types/server.d.ts +2 -9
- package/dist-cjs/utils/tenant.d.ts +11 -0
- package/dist-cjs/utils/tenant.js +15 -0
- package/dist-cjs/version.d.ts +1 -1
- package/dist-cjs/version.js +1 -1
- package/llms.txt +4 -3
- package/package.json +118 -1
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createDeepAgent = createDeepAgent;
|
|
4
|
+
const create_agent_js_1 = require("../agent/create-agent.js");
|
|
5
|
+
const context_editor_js_1 = require("../agent/middleware/context-editor.js");
|
|
6
|
+
const filesystem_js_1 = require("../agent/middleware/filesystem.js");
|
|
7
|
+
const tool_selector_js_1 = require("../agent/middleware/tool-selector.js");
|
|
8
|
+
const permissions_js_1 = require("../agent/permissions.js");
|
|
9
|
+
const sandbox_js_1 = require("../agent/sandbox.js");
|
|
10
|
+
const tool_js_1 = require("../agent/tool.js");
|
|
11
|
+
const MOUNT = '/workspace';
|
|
12
|
+
const CORE_TOOLS = [
|
|
13
|
+
'write_todos',
|
|
14
|
+
'delegate',
|
|
15
|
+
'load_skill',
|
|
16
|
+
'read_file',
|
|
17
|
+
'write_file',
|
|
18
|
+
'edit_file',
|
|
19
|
+
'list_files',
|
|
20
|
+
'run_command',
|
|
21
|
+
];
|
|
22
|
+
function createDeepAgent(options) {
|
|
23
|
+
const { instructions, sandbox, commands = [], planning = true, subagents = [], skills = [], include = ['AGENTS.md'], offload, contextEditing, toolSelection, permissions, middleware = [], tools: extraTools = [], ...agentOptions } = options;
|
|
24
|
+
const workspace = sandbox
|
|
25
|
+
? [...(0, sandbox_js_1.sandboxTools)(sandbox, { shell: commands.length > 0, mount: MOUNT }), editFileTool(sandbox)]
|
|
26
|
+
: [];
|
|
27
|
+
const own = [
|
|
28
|
+
...(planning ? [writeTodosTool()] : []),
|
|
29
|
+
...(skills.length > 0 ? [loadSkillTool(skills, sandbox)] : []),
|
|
30
|
+
...workspace,
|
|
31
|
+
...extraTools,
|
|
32
|
+
];
|
|
33
|
+
const helpers = new Map();
|
|
34
|
+
const approved = new Set(Object.keys(agentOptions.interruptOn ?? {}));
|
|
35
|
+
const { interruptOn: _approvals, ...helperOptions } = agentOptions;
|
|
36
|
+
for (const helper of subagents) {
|
|
37
|
+
const allowed = helper.tools ? new Set(helper.tools) : undefined;
|
|
38
|
+
helpers.set(helper.name, (0, create_agent_js_1.createAgent)({
|
|
39
|
+
...helperOptions,
|
|
40
|
+
name: `${options.name ?? 'deep-agent'}.${helper.name}`,
|
|
41
|
+
...(helper.model ? { model: helper.model } : {}),
|
|
42
|
+
maxIterations: helper.maxIterations ?? 20,
|
|
43
|
+
systemPrompt: `${helper.instructions}\n\n${WORKSPACE_NOTE}`,
|
|
44
|
+
tools: own.filter((candidate) => !approved.has(candidate.name) && (allowed ? allowed.has(candidate.name) : true)),
|
|
45
|
+
...(permissions === false ? {} : { permissions: permissions ?? defaultPolicy(commands) }),
|
|
46
|
+
checkpointer: false,
|
|
47
|
+
}));
|
|
48
|
+
}
|
|
49
|
+
const tools = [...own, ...(helpers.size > 0 ? [delegateTool(subagents, helpers)] : [])];
|
|
50
|
+
const kit = [
|
|
51
|
+
...(sandbox && (include.length > 0 || offload !== false)
|
|
52
|
+
? [
|
|
53
|
+
(0, filesystem_js_1.filesystemContext)({
|
|
54
|
+
source: sandbox,
|
|
55
|
+
include,
|
|
56
|
+
...(offload === false ? {} : { offload: { overChars: 20_000, ...offload } }),
|
|
57
|
+
}),
|
|
58
|
+
]
|
|
59
|
+
: []),
|
|
60
|
+
...(contextEditing === false
|
|
61
|
+
? []
|
|
62
|
+
: [
|
|
63
|
+
(0, context_editor_js_1.contextEditor)({
|
|
64
|
+
triggerTokens: 60_000,
|
|
65
|
+
...contextEditing,
|
|
66
|
+
exclude: ['write_todos', ...(contextEditing?.exclude ?? [])],
|
|
67
|
+
}),
|
|
68
|
+
]),
|
|
69
|
+
...(toolSelection
|
|
70
|
+
? [(0, tool_selector_js_1.toolSelector)({ ...toolSelection, always: [...CORE_TOOLS, ...(toolSelection.always ?? [])] })]
|
|
71
|
+
: []),
|
|
72
|
+
];
|
|
73
|
+
return (0, create_agent_js_1.createAgent)({
|
|
74
|
+
maxIterations: 40,
|
|
75
|
+
...agentOptions,
|
|
76
|
+
systemPrompt: systemPrompt(instructions, { planning, sandbox: Boolean(sandbox), commands, subagents, skills }),
|
|
77
|
+
tools,
|
|
78
|
+
middleware: [...kit, ...middleware],
|
|
79
|
+
...(permissions === false ? {} : { permissions: permissions ?? defaultPolicy(commands) }),
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
function defaultPolicy(commands) {
|
|
83
|
+
return (0, permissions_js_1.permissionPolicy)({
|
|
84
|
+
filesystem: { write: [`${MOUNT}/**`] },
|
|
85
|
+
shell: { allow: commands },
|
|
86
|
+
tools: { write_todos: 'allow', delegate: 'allow', load_skill: 'allow' },
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
const WORKSPACE_NOTE = `Files live in ${MOUNT}; paths are relative to it.`;
|
|
90
|
+
function systemPrompt(instructions, parts) {
|
|
91
|
+
const lines = [];
|
|
92
|
+
if (instructions)
|
|
93
|
+
lines.push(instructions, '');
|
|
94
|
+
lines.push('Work through the task step by step, checking each step before the next.');
|
|
95
|
+
if (parts.planning) {
|
|
96
|
+
lines.push('For any task of more than two steps, first record a plan with write_todos, then keep it current: mark a step in_progress when you start it and completed when it is done.');
|
|
97
|
+
}
|
|
98
|
+
if (parts.sandbox) {
|
|
99
|
+
lines.push(`${WORKSPACE_NOTE} Read before you change a file; prefer edit_file to rewriting a whole file. A large result may be saved to a file and shown as a preview: read the file when you need the rest.`);
|
|
100
|
+
if (parts.commands.length > 0)
|
|
101
|
+
lines.push(`You may run these commands: ${parts.commands.join(', ')}.`);
|
|
102
|
+
}
|
|
103
|
+
if (parts.subagents.length > 0) {
|
|
104
|
+
lines.push('', 'Delegate a self-contained piece of work with delegate, to one of:');
|
|
105
|
+
for (const helper of parts.subagents)
|
|
106
|
+
lines.push(`- ${helper.name}: ${helper.description}`);
|
|
107
|
+
}
|
|
108
|
+
if (parts.skills.length > 0) {
|
|
109
|
+
lines.push('', 'Skills you can load with load_skill when a task calls for one:');
|
|
110
|
+
for (const skill of parts.skills)
|
|
111
|
+
lines.push(`- ${skill.name}: ${skill.description}`);
|
|
112
|
+
}
|
|
113
|
+
return lines.join('\n');
|
|
114
|
+
}
|
|
115
|
+
function writeTodosTool() {
|
|
116
|
+
return (0, tool_js_1.tool)({
|
|
117
|
+
name: 'write_todos',
|
|
118
|
+
description: 'Records the plan for this task as a list of steps, each pending, in_progress, or completed. Send the whole list each time.',
|
|
119
|
+
parameters: {
|
|
120
|
+
type: 'object',
|
|
121
|
+
properties: {
|
|
122
|
+
todos: {
|
|
123
|
+
type: 'array',
|
|
124
|
+
items: {
|
|
125
|
+
type: 'object',
|
|
126
|
+
properties: {
|
|
127
|
+
content: { type: 'string' },
|
|
128
|
+
status: { type: 'string', enum: ['pending', 'in_progress', 'completed'] },
|
|
129
|
+
},
|
|
130
|
+
required: ['content', 'status'],
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
required: ['todos'],
|
|
135
|
+
},
|
|
136
|
+
capabilities: [],
|
|
137
|
+
execute: ({ todos }) => {
|
|
138
|
+
const list = Array.isArray(todos) ? todos : [];
|
|
139
|
+
const mark = { pending: '[ ]', in_progress: '[~]', completed: '[x]' };
|
|
140
|
+
const done = list.filter((todo) => todo.status === 'completed').length;
|
|
141
|
+
return `Plan (${done}/${list.length} done):\n${list.map((todo) => `${mark[todo.status] ?? '[ ]'} ${todo.content}`).join('\n')}`;
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
function editFileTool(sandbox) {
|
|
146
|
+
const local = (file) => String(file ?? '')
|
|
147
|
+
.replace(/\\/g, '/')
|
|
148
|
+
.replace(new RegExp(`^${MOUNT}/`), '')
|
|
149
|
+
.replace(/^\/+/, '');
|
|
150
|
+
return (0, tool_js_1.tool)({
|
|
151
|
+
name: 'edit_file',
|
|
152
|
+
description: `Replaces one exact occurrence of old_text with new_text in a file. Paths are relative to ${MOUNT}.`,
|
|
153
|
+
parameters: {
|
|
154
|
+
type: 'object',
|
|
155
|
+
properties: { path: { type: 'string' }, old_text: { type: 'string' }, new_text: { type: 'string' } },
|
|
156
|
+
required: ['path', 'old_text', 'new_text'],
|
|
157
|
+
},
|
|
158
|
+
capabilities: (args) => [`filesystem:write:${MOUNT}/${local(args.path)}`],
|
|
159
|
+
execute: async ({ path, old_text: oldText, new_text: newText }) => {
|
|
160
|
+
const file = local(path);
|
|
161
|
+
const text = await sandbox.readFile(file);
|
|
162
|
+
const count = text.split(String(oldText)).length - 1;
|
|
163
|
+
if (count === 0)
|
|
164
|
+
throw new Error(`old_text was not found in ${file}`);
|
|
165
|
+
if (count > 1)
|
|
166
|
+
throw new Error(`old_text appears ${count} times in ${file}; include more context so it appears once`);
|
|
167
|
+
await sandbox.writeFile(file, text.replace(String(oldText), () => String(newText)));
|
|
168
|
+
return { edited: file };
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
function loadSkillTool(skills, sandbox) {
|
|
173
|
+
const byName = new Map(skills.map((skill) => [skill.name, skill]));
|
|
174
|
+
return (0, tool_js_1.tool)({
|
|
175
|
+
name: 'load_skill',
|
|
176
|
+
description: 'Loads the instructions of a skill by name.',
|
|
177
|
+
parameters: {
|
|
178
|
+
type: 'object',
|
|
179
|
+
properties: { name: { type: 'string', enum: skills.map((skill) => skill.name) } },
|
|
180
|
+
required: ['name'],
|
|
181
|
+
},
|
|
182
|
+
capabilities: [],
|
|
183
|
+
execute: async ({ name }) => {
|
|
184
|
+
const skill = byName.get(String(name));
|
|
185
|
+
if (!skill)
|
|
186
|
+
throw new Error(`No skill named "${String(name)}"`);
|
|
187
|
+
if (skill.instructions !== undefined)
|
|
188
|
+
return skill.instructions;
|
|
189
|
+
if (skill.file && sandbox)
|
|
190
|
+
return sandbox.readFile(skill.file);
|
|
191
|
+
throw new Error(`Skill "${skill.name}" has no instructions`);
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
function delegateTool(subagents, helpers) {
|
|
196
|
+
return (0, tool_js_1.tool)({
|
|
197
|
+
name: 'delegate',
|
|
198
|
+
description: 'Hands a self-contained task to a helper, which works in a fresh context and returns its answer.',
|
|
199
|
+
parameters: {
|
|
200
|
+
type: 'object',
|
|
201
|
+
properties: {
|
|
202
|
+
agent: { type: 'string', enum: subagents.map((helper) => helper.name) },
|
|
203
|
+
task: { type: 'string', description: 'Everything the helper needs to know: it sees nothing else.' },
|
|
204
|
+
},
|
|
205
|
+
required: ['agent', 'task'],
|
|
206
|
+
},
|
|
207
|
+
capabilities: [],
|
|
208
|
+
execute: async ({ agent, task }, context) => {
|
|
209
|
+
const helper = helpers.get(String(agent));
|
|
210
|
+
if (!helper)
|
|
211
|
+
throw new Error(`No helper named "${String(agent)}"`);
|
|
212
|
+
const result = await helper.invoke((0, create_agent_js_1.agentInput)(String(task)), {
|
|
213
|
+
...(context.principal ? { principal: context.principal } : {}),
|
|
214
|
+
...(context.signal ? { signal: context.signal } : {}),
|
|
215
|
+
});
|
|
216
|
+
return (result.state.answer || `The ${String(agent)} helper finished without an answer (${result.state.stopReason}).`);
|
|
217
|
+
},
|
|
218
|
+
});
|
|
219
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { OperationLifecycleLike } from '../types/lifecycle.js';
|
|
2
|
+
import type { Principal } from '../types/principal.js';
|
|
2
3
|
import { GraphError } from './errors.js';
|
|
3
4
|
import type { GraphCheckpoint, GraphCheckpointer, GraphDescription, GraphEvent, GraphStatus, InterruptRequest, PendingInterrupt, RetryPolicy, RunControlLike } from '../types/graph.js';
|
|
4
5
|
import type { Store } from '../types/store.js';
|
|
@@ -86,6 +87,8 @@ export interface WorkflowContext {
|
|
|
86
87
|
readonly threadId: string;
|
|
87
88
|
/** The tenant the run is for, when it has one. */
|
|
88
89
|
readonly tenantId?: string;
|
|
90
|
+
/** Who the run is for, when it was started with a principal: the caller the server authenticated. */
|
|
91
|
+
readonly principal?: Readonly<Principal>;
|
|
89
92
|
/** Aborted when the run is cancelled. */
|
|
90
93
|
readonly signal: AbortSignal;
|
|
91
94
|
/** Long-term memory, when the workflow was created with a store. In a tenant's run, the tenant's view of it. */
|
|
@@ -162,6 +165,12 @@ export interface WorkflowRunOptions {
|
|
|
162
165
|
* without one carries on the thread's.
|
|
163
166
|
*/
|
|
164
167
|
tenantId?: string;
|
|
168
|
+
/**
|
|
169
|
+
* Who the run is for, read as `context.principal`. Its `tenantId` is the run's tenant when
|
|
170
|
+
* `tenantId` is not given, and a run whose two tenants differ is refused; its `userId` is recorded
|
|
171
|
+
* on every checkpoint as `metadata.userId`.
|
|
172
|
+
*/
|
|
173
|
+
principal?: Principal;
|
|
165
174
|
}
|
|
166
175
|
/** One event of a streamed workflow run. */
|
|
167
176
|
export type WorkflowEvent<O = unknown> = {
|
|
@@ -6,6 +6,7 @@ const checkpointer_js_1 = require("./checkpointer.js");
|
|
|
6
6
|
const run_js_1 = require("../lifecycle/run.js");
|
|
7
7
|
const checkpoint_migration_js_1 = require("./checkpoint-migration.js");
|
|
8
8
|
const tenant_js_1 = require("../store/tenant.js");
|
|
9
|
+
const tenant_js_2 = require("../utils/tenant.js");
|
|
9
10
|
const errors_js_1 = require("./errors.js");
|
|
10
11
|
class WorkflowStepTimeoutError extends errors_js_1.GraphError {
|
|
11
12
|
stepName;
|
|
@@ -57,9 +58,10 @@ class Workflow {
|
|
|
57
58
|
const threadId = runOptions.threadId ?? generatedId();
|
|
58
59
|
return this.execute(threadId, runOptions, async () => {
|
|
59
60
|
const latest = await this.latest(threadId);
|
|
60
|
-
|
|
61
|
+
const tenantId = runOptions.tenantId ?? runOptions.principal?.tenantId;
|
|
62
|
+
if (latest && tenantId !== undefined) {
|
|
61
63
|
const owner = latest.metadata?.tenantId;
|
|
62
|
-
if (typeof owner === 'string' && owner !==
|
|
64
|
+
if (typeof owner === 'string' && owner !== tenantId) {
|
|
63
65
|
throw new errors_js_1.GraphError(`Thread "${threadId}" cannot be used by this tenant; choose another id`, 'GRAPH_VALIDATION_ERROR');
|
|
64
66
|
}
|
|
65
67
|
}
|
|
@@ -153,7 +155,21 @@ class Workflow {
|
|
|
153
155
|
}
|
|
154
156
|
execute(threadId, runOptions, start) {
|
|
155
157
|
const queue = new EventQueue();
|
|
156
|
-
|
|
158
|
+
let options;
|
|
159
|
+
try {
|
|
160
|
+
options = (0, tenant_js_2.withPrincipal)(runOptions);
|
|
161
|
+
}
|
|
162
|
+
catch (error) {
|
|
163
|
+
queue.fail(error);
|
|
164
|
+
return queue;
|
|
165
|
+
}
|
|
166
|
+
const metadata = {
|
|
167
|
+
threadId,
|
|
168
|
+
graph: this.name,
|
|
169
|
+
...(options.tenantId !== undefined ? { tenantId: options.tenantId } : {}),
|
|
170
|
+
...(options.principal?.userId !== undefined ? { userId: options.principal.userId } : {}),
|
|
171
|
+
};
|
|
172
|
+
void (0, run_js_1.withLifecycle)(this.options.lifecycle, { family: 'graph', operation: `workflow.${this.name}`, metadata }, options.signal ? { signal: options.signal } : {}, () => this.run(threadId, options, start, queue)).then(() => queue.close(), (error) => queue.fail(error));
|
|
157
173
|
return queue;
|
|
158
174
|
}
|
|
159
175
|
async run(threadId, runOptions, start, queue) {
|
|
@@ -208,6 +224,7 @@ class Workflow {
|
|
|
208
224
|
const context = {
|
|
209
225
|
threadId,
|
|
210
226
|
...(tenantId === undefined ? {} : { tenantId }),
|
|
227
|
+
...(runOptions.principal ? { principal: runOptions.principal } : {}),
|
|
211
228
|
signal: controller.signal,
|
|
212
229
|
...(this.options.store
|
|
213
230
|
? { store: tenantId === undefined ? this.options.store : (0, tenant_js_1.tenantStore)(this.options.store, tenantId) }
|
package/dist-cjs/graph/graph.js
CHANGED
|
@@ -38,6 +38,7 @@ exports.createGraph = createGraph;
|
|
|
38
38
|
const node_crypto_1 = require("node:crypto");
|
|
39
39
|
const graph_js_1 = require("../types/graph.js");
|
|
40
40
|
const tenant_js_1 = require("../store/tenant.js");
|
|
41
|
+
const tenant_js_2 = require("../utils/tenant.js");
|
|
41
42
|
const signals_js_1 = require("../utils/signals.js");
|
|
42
43
|
const checkpointer_js_1 = require("./checkpointer.js");
|
|
43
44
|
const checkpoint_migration_js_1 = require("./checkpoint-migration.js");
|
|
@@ -190,9 +191,10 @@ class CompiledGraph {
|
|
|
190
191
|
const threadId = resolveThreadId(runOptions.threadId);
|
|
191
192
|
return this.run(threadId, runOptions, async () => {
|
|
192
193
|
this.assertInput(input);
|
|
193
|
-
|
|
194
|
+
const tenantId = runOptions.tenantId ?? runOptions.principal?.tenantId;
|
|
195
|
+
if (tenantId !== undefined) {
|
|
194
196
|
const existing = await this.checkpointer()?.get(threadId);
|
|
195
|
-
if (existing && !ownedBy(existing,
|
|
197
|
+
if (existing && !ownedBy(existing, tenantId)) {
|
|
196
198
|
throw new errors_js_1.GraphValidationError(`Thread "${threadId}" cannot be used by this tenant; choose another id`);
|
|
197
199
|
}
|
|
198
200
|
}
|
|
@@ -304,6 +306,8 @@ class CompiledGraph {
|
|
|
304
306
|
const runOptions = {
|
|
305
307
|
threadId: `${context.threadId}:${context.taskId}`,
|
|
306
308
|
signal: context.signal,
|
|
309
|
+
...(context.tenantId !== undefined ? { tenantId: context.tenantId } : {}),
|
|
310
|
+
...(context.principal ? { principal: context.principal } : {}),
|
|
307
311
|
...(forward ? { onEvent: forward, subgraphEvents: true } : {}),
|
|
308
312
|
};
|
|
309
313
|
const paused = await this.state(runOptions.threadId);
|
|
@@ -347,7 +351,7 @@ class CompiledGraph {
|
|
|
347
351
|
describe() {
|
|
348
352
|
const nodes = [...this.nodes.entries()].map(([id, definition]) => {
|
|
349
353
|
const subgraph = definition.fn[SUBGRAPH];
|
|
350
|
-
const { ends, defer, timeoutMs, cache, idempotent } = definition.options;
|
|
354
|
+
const { ends, defer, timeoutMs, cache, idempotent, effects, interrupts } = definition.options;
|
|
351
355
|
const maxAttempts = this.retryPolicy(definition).maxAttempts;
|
|
352
356
|
const timeout = this.timeoutOf(definition);
|
|
353
357
|
return {
|
|
@@ -360,6 +364,8 @@ class CompiledGraph {
|
|
|
360
364
|
...(this.errorHandler(definition) ? { onError: true } : {}),
|
|
361
365
|
...(idempotent ? { idempotent: true } : {}),
|
|
362
366
|
...(cache ? { cache: true } : {}),
|
|
367
|
+
...(effects?.length ? { effects: [...effects] } : {}),
|
|
368
|
+
...(interrupts ? { interrupts: true } : {}),
|
|
363
369
|
...(subgraph ? { subgraph: subgraph.describe() } : {}),
|
|
364
370
|
};
|
|
365
371
|
});
|
|
@@ -396,6 +402,14 @@ class CompiledGraph {
|
|
|
396
402
|
durability: this.options.durability ?? 'sync',
|
|
397
403
|
checkpointer: this.options.checkpointer === false ? 'none' : this.options.checkpointer === undefined ? 'memory' : 'custom',
|
|
398
404
|
maxConcurrency: this.options.maxConcurrency ?? DEFAULT_MAX_CONCURRENCY,
|
|
405
|
+
...(this.options.tools?.length
|
|
406
|
+
? {
|
|
407
|
+
tools: this.options.tools.map((tool) => ({
|
|
408
|
+
...tool,
|
|
409
|
+
...(tool.capabilities ? { capabilities: [...tool.capabilities] } : {}),
|
|
410
|
+
})),
|
|
411
|
+
}
|
|
412
|
+
: {}),
|
|
399
413
|
};
|
|
400
414
|
}
|
|
401
415
|
async updateState(threadId, update, options = {}) {
|
|
@@ -456,6 +470,7 @@ class CompiledGraph {
|
|
|
456
470
|
return forkId;
|
|
457
471
|
}
|
|
458
472
|
async *run(threadId, runOptions, seed) {
|
|
473
|
+
runOptions = (0, tenant_js_2.withPrincipal)(runOptions);
|
|
459
474
|
const lifecycle = this.options.lifecycle;
|
|
460
475
|
if (!lifecycle) {
|
|
461
476
|
yield* this.runSteps(threadId, runOptions, seed);
|
|
@@ -464,7 +479,12 @@ class CompiledGraph {
|
|
|
464
479
|
const ticket = await lifecycle.start({
|
|
465
480
|
family: this.options.lifecycleFamily ?? 'graph',
|
|
466
481
|
operation: this.options.name ? `${this.options.lifecycleFamily ?? 'graph'}.${this.options.name}` : 'graph.run',
|
|
467
|
-
metadata: {
|
|
482
|
+
metadata: {
|
|
483
|
+
threadId,
|
|
484
|
+
...(this.options.name ? { graph: this.options.name } : {}),
|
|
485
|
+
...(runOptions.tenantId !== undefined ? { tenantId: runOptions.tenantId } : {}),
|
|
486
|
+
...(runOptions.principal?.userId !== undefined ? { userId: runOptions.principal.userId } : {}),
|
|
487
|
+
},
|
|
468
488
|
}, runOptions.signal ? { signal: runOptions.signal } : {});
|
|
469
489
|
let last;
|
|
470
490
|
let settled = false;
|
|
@@ -888,6 +908,7 @@ class CompiledGraph {
|
|
|
888
908
|
step,
|
|
889
909
|
threadId,
|
|
890
910
|
...(runOptions.tenantId !== undefined ? { tenantId: runOptions.tenantId } : {}),
|
|
911
|
+
...(runOptions.principal ? { principal: runOptions.principal } : {}),
|
|
891
912
|
taskId: task.id,
|
|
892
913
|
input: task.input,
|
|
893
914
|
attempt,
|
package/dist-cjs/graph/lint.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { GraphDescription } from '../types/graph.js';
|
|
|
2
2
|
/** How much a lint finding matters: `error` fails `nexus graph lint`, the others only report. */
|
|
3
3
|
export type GraphLintSeverity = 'error' | 'warning' | 'info';
|
|
4
4
|
/** The rules `lintGraph()` checks. */
|
|
5
|
-
export type GraphLintCode = 'UNBOUNDED_CYCLE' | 'RETRY_WITHOUT_IDEMPOTENCY' | 'RETRY_WITHOUT_TIMEOUT' | 'DEFERRED_DURABILITY_WITH_SIDE_EFFECTS' | 'MEMORY_CHECKPOINTER' | 'NO_CHECKPOINTER' | 'UNBOUNDED_CONCURRENCY' | 'DYNAMIC_ROUTE';
|
|
5
|
+
export type GraphLintCode = 'UNBOUNDED_CYCLE' | 'RETRY_WITHOUT_IDEMPOTENCY' | 'RETRY_WITHOUT_TIMEOUT' | 'DEFERRED_DURABILITY_WITH_SIDE_EFFECTS' | 'MEMORY_CHECKPOINTER' | 'NO_CHECKPOINTER' | 'UNBOUNDED_CONCURRENCY' | 'DYNAMIC_ROUTE' | 'SENSITIVE_TOOL_WITHOUT_APPROVAL' | 'UNDECLARED_TOOL_CAPABILITIES' | 'SIDE_EFFECT_BEFORE_INTERRUPT';
|
|
6
6
|
/** One design problem `lintGraph()` found. */
|
|
7
7
|
export interface GraphLintFinding {
|
|
8
8
|
/** Which rule found it. */
|
|
@@ -43,6 +43,9 @@ export interface GraphLintOptions {
|
|
|
43
43
|
* | `NO_CHECKPOINTER` | info | No checkpoints, so no interrupt, resume, or recovery |
|
|
44
44
|
* | `UNBOUNDED_CONCURRENCY` | warning | A fan-out with no limit on tasks at once |
|
|
45
45
|
* | `DYNAMIC_ROUTE` | info | A router with no mapping or `ends`, so routes cannot be checked |
|
|
46
|
+
* | `SENSITIVE_TOOL_WITHOUT_APPROVAL` | warning, or error when `deployed` | A tool that writes, runs commands or code, or reaches the network, with no approval and no permission policy |
|
|
47
|
+
* | `UNDECLARED_TOOL_CAPABILITIES` | info | A tool with no capabilities and no approval, whose effects cannot be judged |
|
|
48
|
+
* | `SIDE_EFFECT_BEFORE_INTERRUPT` | warning | A node that declares effects and interrupts, so its effects repeat when it runs again on the answer |
|
|
46
49
|
*
|
|
47
50
|
* @example
|
|
48
51
|
* ```ts
|
package/dist-cjs/graph/lint.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.lintGraph = lintGraph;
|
|
4
|
+
const capabilities_js_1 = require("../agent/capabilities.js");
|
|
4
5
|
function lintGraph(graph, options = {}) {
|
|
5
6
|
const description = 'describe' in graph && typeof graph.describe === 'function' ? graph.describe() : graph;
|
|
6
7
|
const findings = [];
|
|
@@ -42,9 +43,43 @@ function lintDescription(description, prefix, options, findings, top) {
|
|
|
42
43
|
fix: 'Set `timeout: { runMs }`, or `idleMs` with `context.heartbeat()` for long work.',
|
|
43
44
|
});
|
|
44
45
|
}
|
|
46
|
+
if (node.effects?.length && node.interrupts && !node.idempotent) {
|
|
47
|
+
findings.push({
|
|
48
|
+
code: 'SIDE_EFFECT_BEFORE_INTERRUPT',
|
|
49
|
+
severity: 'warning',
|
|
50
|
+
node: at(node.id),
|
|
51
|
+
message: `"${at(node.id)}" declares effects (${node.effects.join(', ')}) and may interrupt, so when the answer comes the node runs again from the top and repeats whatever it did before asking.`,
|
|
52
|
+
fix: 'Ask in a node of its own before the effect, or give the effect an idempotency key and declare the node idempotent.',
|
|
53
|
+
});
|
|
54
|
+
}
|
|
45
55
|
if (node.subgraph)
|
|
46
56
|
lintDescription(node.subgraph, `${at(node.id)}/`, options, findings, false);
|
|
47
57
|
}
|
|
58
|
+
for (const tool of description.tools ?? []) {
|
|
59
|
+
if (tool.approval !== 'none')
|
|
60
|
+
continue;
|
|
61
|
+
const sensitive = (tool.capabilities ?? []).filter((capability) => (0, capabilities_js_1.isSensitiveCapability)(capability));
|
|
62
|
+
if (sensitive.length > 0 || tool.dynamic) {
|
|
63
|
+
findings.push({
|
|
64
|
+
code: 'SENSITIVE_TOOL_WITHOUT_APPROVAL',
|
|
65
|
+
severity: options.deployed ? 'error' : 'warning',
|
|
66
|
+
node: `tool:${tool.name}`,
|
|
67
|
+
message: sensitive.length
|
|
68
|
+
? `Tool "${tool.name}" can ${sensitive.join(', ')}, and nothing approves or limits its calls.`
|
|
69
|
+
: `Tool "${tool.name}" computes its capabilities per call, and nothing approves or limits its calls.`,
|
|
70
|
+
fix: 'Give the agent a permission policy that grants what the tool may do, or approve its calls with interruptOn.',
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
else if (tool.capabilities === undefined) {
|
|
74
|
+
findings.push({
|
|
75
|
+
code: 'UNDECLARED_TOOL_CAPABILITIES',
|
|
76
|
+
severity: 'info',
|
|
77
|
+
node: `tool:${tool.name}`,
|
|
78
|
+
message: `Tool "${tool.name}" declares no capabilities, so what it does cannot be judged.`,
|
|
79
|
+
fix: 'Declare its capabilities, or `capabilities: []` for a tool that changes nothing.',
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
48
83
|
if (!top)
|
|
49
84
|
return;
|
|
50
85
|
if ((description.durability === 'async' || description.durability === 'exit') && sideEffecting.length > 0) {
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { RagChunk } from '../hallucination/rag.js';
|
|
2
|
+
import type { VectorSearchResult } from '../hallucination/retrieval.js';
|
|
3
|
+
import type { RetrieveOptions, SparseRetriever } from '../rag/retrievers.js';
|
|
4
|
+
import type { MigrationResult, SchemaMigration } from '../utils/schema-migrations.js';
|
|
5
|
+
import { type PostgresLikeClient } from './client.js';
|
|
6
|
+
import type { PostgresMigrateOptions } from './migrations.js';
|
|
7
|
+
/** Options for `PostgresKeywordIndex`. */
|
|
8
|
+
export interface PostgresKeywordIndexOptions {
|
|
9
|
+
/** Table name, optionally schema-qualified. Defaults to `nexus_keywords`. */
|
|
10
|
+
table?: string;
|
|
11
|
+
/**
|
|
12
|
+
* Searches the table of a `PostgresVectorStore` instead of keeping chunks of its own. `migrate()`
|
|
13
|
+
* adds a generated search column and its index to that table, the store's writes keep it current,
|
|
14
|
+
* and `add()` and `delete()` leave writing to the store. One copy of every chunk, and keyword and
|
|
15
|
+
* vector search can never disagree about what exists.
|
|
16
|
+
*/
|
|
17
|
+
shared?: boolean;
|
|
18
|
+
/** The text search configuration: `english`, `simple`, `german`, and so on. Defaults to `english`. */
|
|
19
|
+
language?: string;
|
|
20
|
+
/**
|
|
21
|
+
* `any` (the default) finds chunks with any of the query's terms, ranked by how many and how close,
|
|
22
|
+
* as keyword search for retrieval should. `all` needs every term, read as a web search query, with
|
|
23
|
+
* quoted phrases and `-` for exclusion.
|
|
24
|
+
*/
|
|
25
|
+
match?: 'any' | 'all';
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* The versioned schema, which `migrate()` applies. Version 1 creates the table with a generated
|
|
29
|
+
* `tsvector` column and a GIN index, or, `shared`, adds the column and the index to a vector table.
|
|
30
|
+
*/
|
|
31
|
+
export declare function keywordIndexMigrations(options?: PostgresKeywordIndexOptions): SchemaMigration[];
|
|
32
|
+
/**
|
|
33
|
+
* Keyword search in Postgres, with its full-text search: stemmed terms, a GIN index, and
|
|
34
|
+
* `ts_rank_cd` ranking, so only the best matches leave the database however large the table.
|
|
35
|
+
*
|
|
36
|
+
* A `SparseRetriever`, so `hybridRetriever()` fuses it with `PostgresVectorStore`. With `shared`, it
|
|
37
|
+
* searches the vector store's own table, and the hybrid search runs over one copy of the data. A
|
|
38
|
+
* metadata filter runs in SQL, with the same meaning as the vector store's.
|
|
39
|
+
*
|
|
40
|
+
* ```ts
|
|
41
|
+
* const vectors = new PostgresVectorStore(client, { dimensions: 1536, embed });
|
|
42
|
+
* const keywords = new PostgresKeywordIndex(client, { table: 'nexus_vectors', shared: true });
|
|
43
|
+
* await vectors.migrate();
|
|
44
|
+
* await keywords.migrate();
|
|
45
|
+
* const retriever = hybridRetriever([vectorRetriever(vectors), keywords]);
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
export declare class PostgresKeywordIndex implements SparseRetriever {
|
|
49
|
+
private readonly client;
|
|
50
|
+
private readonly options;
|
|
51
|
+
private readonly table;
|
|
52
|
+
private readonly language;
|
|
53
|
+
constructor(client: PostgresLikeClient, options?: PostgresKeywordIndexOptions);
|
|
54
|
+
/**
|
|
55
|
+
* Applies this index's pending migrations, recorded in `nexus_schema_migrations`, under a lock.
|
|
56
|
+
* Never runs implicitly. With `shared`, run the vector store's `migrate()` first.
|
|
57
|
+
*/
|
|
58
|
+
migrate(options?: PostgresMigrateOptions): Promise<MigrationResult>;
|
|
59
|
+
/** Adds chunks, or replaces those whose id exists. With `shared`, does nothing: the vector store writes. */
|
|
60
|
+
add(chunks: readonly RagChunk[]): Promise<void>;
|
|
61
|
+
/** Removes chunks by id. With `shared`, does nothing: the vector store deletes. */
|
|
62
|
+
delete(ids: readonly string[]): Promise<void>;
|
|
63
|
+
/** The chunks that best match a query's terms, best first, scored from 0 to 1. */
|
|
64
|
+
retrieve(query: string, options?: RetrieveOptions): Promise<VectorSearchResult[]>;
|
|
65
|
+
/** Chunks held. */
|
|
66
|
+
size(): Promise<number>;
|
|
67
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.PostgresKeywordIndex = void 0;
|
|
37
|
+
exports.keywordIndexMigrations = keywordIndexMigrations;
|
|
38
|
+
const client_js_1 = require("./client.js");
|
|
39
|
+
const sql_js_1 = require("./sql.js");
|
|
40
|
+
const DEFAULT_TABLE = 'nexus_keywords';
|
|
41
|
+
function languageOf(options) {
|
|
42
|
+
const language = options.language ?? 'english';
|
|
43
|
+
if (!/^[a-z_]{1,63}$/.test(language))
|
|
44
|
+
throw new RangeError(`"${language}" is not a text search configuration name`);
|
|
45
|
+
return language;
|
|
46
|
+
}
|
|
47
|
+
function keywordIndexMigrations(options = {}) {
|
|
48
|
+
const name = options.table ?? DEFAULT_TABLE;
|
|
49
|
+
const table = (0, client_js_1.quoteTable)(name);
|
|
50
|
+
const generated = `fts tsvector GENERATED ALWAYS AS (to_tsvector('${languageOf(options)}'::regconfig, content)) STORED`;
|
|
51
|
+
return [
|
|
52
|
+
{
|
|
53
|
+
component: `keywords:${name}`,
|
|
54
|
+
version: 1,
|
|
55
|
+
name: options.shared ? 'add full-text search to a vector table' : 'create the keyword table',
|
|
56
|
+
statements: [
|
|
57
|
+
options.shared
|
|
58
|
+
? `ALTER TABLE ${table} ADD COLUMN IF NOT EXISTS ${generated}`
|
|
59
|
+
: `CREATE TABLE IF NOT EXISTS ${table} (
|
|
60
|
+
id text PRIMARY KEY,
|
|
61
|
+
content text NOT NULL,
|
|
62
|
+
source text,
|
|
63
|
+
metadata jsonb,
|
|
64
|
+
${generated}
|
|
65
|
+
)`,
|
|
66
|
+
`CREATE INDEX IF NOT EXISTS ${(0, client_js_1.quoteDerived)(name, 'fts')} ON ${table} USING gin (fts)`,
|
|
67
|
+
],
|
|
68
|
+
},
|
|
69
|
+
];
|
|
70
|
+
}
|
|
71
|
+
class PostgresKeywordIndex {
|
|
72
|
+
client;
|
|
73
|
+
options;
|
|
74
|
+
table;
|
|
75
|
+
language;
|
|
76
|
+
constructor(client, options = {}) {
|
|
77
|
+
this.client = client;
|
|
78
|
+
this.options = options;
|
|
79
|
+
this.table = (0, client_js_1.quoteTable)(options.table ?? DEFAULT_TABLE);
|
|
80
|
+
this.language = languageOf(options);
|
|
81
|
+
}
|
|
82
|
+
async migrate(options = {}) {
|
|
83
|
+
const { applyPostgresMigrations } = await Promise.resolve().then(() => __importStar(require('./migrations.js')));
|
|
84
|
+
return applyPostgresMigrations(this.client, keywordIndexMigrations(this.options), options);
|
|
85
|
+
}
|
|
86
|
+
async add(chunks) {
|
|
87
|
+
if (this.options.shared || chunks.length === 0)
|
|
88
|
+
return;
|
|
89
|
+
const params = new sql_js_1.SqlParams();
|
|
90
|
+
const rows = chunks.map((chunk) => `(${params.add(chunk.id)}, ${params.add(chunk.content)}, ${params.add(chunk.source ?? null)}, ${params.add(chunk.metadata === undefined ? null : JSON.stringify(chunk.metadata))}::jsonb)`);
|
|
91
|
+
await this.client.query(`INSERT INTO ${this.table} (id, content, source, metadata) VALUES ${rows.join(', ')}
|
|
92
|
+
ON CONFLICT (id) DO UPDATE SET content = EXCLUDED.content, source = EXCLUDED.source, metadata = EXCLUDED.metadata`, params.values);
|
|
93
|
+
}
|
|
94
|
+
async delete(ids) {
|
|
95
|
+
if (this.options.shared || ids.length === 0)
|
|
96
|
+
return;
|
|
97
|
+
await this.client.query(`DELETE FROM ${this.table} WHERE id = ANY($1::text[])`, [(0, sql_js_1.textArray)(ids)]);
|
|
98
|
+
}
|
|
99
|
+
async retrieve(query, options = {}) {
|
|
100
|
+
options.signal?.throwIfAborted();
|
|
101
|
+
const params = new sql_js_1.SqlParams();
|
|
102
|
+
const text = params.add(query);
|
|
103
|
+
const language = `'${this.language}'::regconfig`;
|
|
104
|
+
const tsquery = this.options.match === 'all'
|
|
105
|
+
? `websearch_to_tsquery(${language}, ${text})`
|
|
106
|
+
: `NULLIF(replace(plainto_tsquery(${language}, ${text})::text, ' & ', ' | '), '')::tsquery`;
|
|
107
|
+
const conditions = ['fts @@ q.query'];
|
|
108
|
+
if (options.filter && Object.keys(options.filter).length > 0) {
|
|
109
|
+
conditions.push(`metadata @> ${params.add(JSON.stringify(options.filter))}::jsonb`);
|
|
110
|
+
}
|
|
111
|
+
const limit = params.add(options.topK ?? 5);
|
|
112
|
+
const { rows } = await this.client.query(`SELECT id, content, source, metadata::text AS metadata, ts_rank_cd(fts, q.query, 32) AS score
|
|
113
|
+
FROM ${this.table}, (SELECT ${tsquery} AS query) AS q
|
|
114
|
+
WHERE ${conditions.join(' AND ')}
|
|
115
|
+
ORDER BY score DESC, id LIMIT ${limit}`, params.values);
|
|
116
|
+
return rows.map((row) => ({
|
|
117
|
+
id: row.id,
|
|
118
|
+
content: row.content,
|
|
119
|
+
...(row.source === null ? {} : { source: row.source }),
|
|
120
|
+
...(row.metadata === null ? {} : { metadata: (0, client_js_1.fromJson)(row.metadata) }),
|
|
121
|
+
score: Number(row.score),
|
|
122
|
+
}));
|
|
123
|
+
}
|
|
124
|
+
async size() {
|
|
125
|
+
const { rows } = await this.client.query(`SELECT count(*)::int AS count FROM ${this.table}`);
|
|
126
|
+
return Number(rows[0]?.count ?? 0);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
exports.PostgresKeywordIndex = PostgresKeywordIndex;
|