@illuma-ai/agents 1.4.0-alpha.2 → 1.4.0-alpha.3
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/dist/cjs/main.cjs +37 -27
- package/dist/cjs/main.cjs.map +1 -1
- package/dist/cjs/tools/artifacts/schema.cjs +63 -0
- package/dist/cjs/tools/artifacts/schema.cjs.map +1 -0
- package/dist/cjs/tools/artifacts/tool.cjs +213 -0
- package/dist/cjs/tools/artifacts/tool.cjs.map +1 -0
- package/dist/cjs/tools/fileSearch/formatter.cjs +2 -4
- package/dist/cjs/tools/fileSearch/formatter.cjs.map +1 -1
- package/dist/cjs/tools/fileSearch/ragClient.cjs +4 -6
- package/dist/cjs/tools/fileSearch/ragClient.cjs.map +1 -1
- package/dist/cjs/tools/fileSearch/schema.cjs.map +1 -1
- package/dist/cjs/tools/fileSearch/tool.cjs.map +1 -1
- package/dist/esm/main.mjs +2 -0
- package/dist/esm/main.mjs.map +1 -1
- package/dist/esm/tools/artifacts/schema.mjs +56 -0
- package/dist/esm/tools/artifacts/schema.mjs.map +1 -0
- package/dist/esm/tools/artifacts/tool.mjs +207 -0
- package/dist/esm/tools/artifacts/tool.mjs.map +1 -0
- package/dist/esm/tools/fileSearch/formatter.mjs +2 -4
- package/dist/esm/tools/fileSearch/formatter.mjs.map +1 -1
- package/dist/esm/tools/fileSearch/ragClient.mjs +4 -6
- package/dist/esm/tools/fileSearch/ragClient.mjs.map +1 -1
- package/dist/esm/tools/fileSearch/schema.mjs.map +1 -1
- package/dist/esm/tools/fileSearch/tool.mjs.map +1 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/tools/artifacts/index.d.ts +3 -0
- package/dist/types/tools/artifacts/schema.d.ts +63 -0
- package/dist/types/tools/artifacts/tool.d.ts +16 -0
- package/dist/types/tools/artifacts/types.d.ts +127 -0
- package/package.json +1 -1
- package/src/index.ts +1 -0
- package/src/tools/artifacts/__tests__/tool.test.ts +243 -0
- package/src/tools/artifacts/index.ts +33 -0
- package/src/tools/artifacts/schema.ts +76 -0
- package/src/tools/artifacts/tool.ts +277 -0
- package/src/tools/artifacts/types.ts +149 -0
- package/src/tools/fileSearch/__tests__/tool.test.ts +20 -10
- package/src/tools/fileSearch/formatter.ts +5 -7
- package/src/tools/fileSearch/ragClient.ts +6 -10
- package/src/tools/fileSearch/schema.ts +2 -2
- package/src/tools/fileSearch/tool.ts +6 -6
- package/src/tools/fileSearch/types.ts +4 -2
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* artifact_tool + content_reader library factories.
|
|
3
|
+
*
|
|
4
|
+
* The library owns the LangChain wiring (schema, description, response
|
|
5
|
+
* shape) and the action dispatch; the runtime supplies a handler bundle
|
|
6
|
+
* matching the `ArtifactWriteHandlers` / `ContentReadHandlers` interface.
|
|
7
|
+
*
|
|
8
|
+
* This keeps 800+ LOC of host-specific handler logic (S3 adapters, file
|
|
9
|
+
* model CRUD, syntax checkers, line utils) out of the library while
|
|
10
|
+
* still centralizing the tool surface every runtime shares.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { tool, DynamicStructuredTool } from '@langchain/core/tools';
|
|
14
|
+
import {
|
|
15
|
+
artifactToolSchema,
|
|
16
|
+
contentReaderSchema,
|
|
17
|
+
ARTIFACT_TOOL_NAME,
|
|
18
|
+
CONTENT_READER_NAME,
|
|
19
|
+
} from './schema';
|
|
20
|
+
import type {
|
|
21
|
+
ArtifactToolConfig,
|
|
22
|
+
ContentReaderToolConfig,
|
|
23
|
+
ArtifactToolScope,
|
|
24
|
+
ArtifactToolResult,
|
|
25
|
+
WriteArgs,
|
|
26
|
+
EditArgs,
|
|
27
|
+
VerifyArgs,
|
|
28
|
+
DeleteArgs,
|
|
29
|
+
ReadArgs,
|
|
30
|
+
SearchArgs,
|
|
31
|
+
ListArgs,
|
|
32
|
+
InfoArgs,
|
|
33
|
+
ContentIdResolver,
|
|
34
|
+
} from './types';
|
|
35
|
+
|
|
36
|
+
const DEFAULT_ARTIFACT_DESCRIPTION = `Author content artifacts that render live in the host's preview panel — this tool does NOT produce downloadable files (use execute_code for those).
|
|
37
|
+
|
|
38
|
+
Actions:
|
|
39
|
+
- write: Create a new artifact. Write a COMPLETE file in one call. The \`name\` field MUST include the file extension — it routes the preview (.tsx, .html, .mmd, .svg, .csv, .json, .dot, .md, .drawio).
|
|
40
|
+
- verify: Check an artifact for syntax errors. REQUIRED as the next step after every write/edit on code files — do not render until verify passes.
|
|
41
|
+
- edit: Surgical string replacement — provide old_str (exact match) and new_str. Works on all file types.
|
|
42
|
+
- delete: Remove an artifact and its backing file.
|
|
43
|
+
|
|
44
|
+
Artifacts are persisted by the host. No manual save needed.`;
|
|
45
|
+
|
|
46
|
+
const DEFAULT_CONTENT_READER_DESCRIPTION = `Read and navigate stored content — artifacts authored by artifact_tool, large tool results auto-cached by the host, uploaded file attachments, and code blocks.
|
|
47
|
+
|
|
48
|
+
Read-only surface. Use write/edit tools (artifact_tool, execute_code) to mutate.
|
|
49
|
+
|
|
50
|
+
Actions:
|
|
51
|
+
- read: Return line ranges from a specific content_id.
|
|
52
|
+
- search: Regex search across a specific content_id with paginated matches.
|
|
53
|
+
- list: Enumerate every content entry currently stored.
|
|
54
|
+
- info: Metadata (size, kind, creation time) for a specific content_id.`;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Optional content_id self-healing — if the runtime supplies a resolver,
|
|
58
|
+
* we pre-resolve the ID on every action that takes one so nicknames
|
|
59
|
+
* (e.g., "Dashboard") map to canonical IDs.
|
|
60
|
+
*/
|
|
61
|
+
async function resolveContentIdIfPresent(
|
|
62
|
+
resolver: ContentIdResolver | undefined,
|
|
63
|
+
id: string | undefined,
|
|
64
|
+
scope: ArtifactToolScope,
|
|
65
|
+
logger?: { debug: (msg: string) => void },
|
|
66
|
+
): Promise<string | undefined> {
|
|
67
|
+
if (!resolver || !id) return id;
|
|
68
|
+
const out = await resolver.resolve(id, scope);
|
|
69
|
+
if (!out) return id;
|
|
70
|
+
if (out.resolvedId !== id) {
|
|
71
|
+
logger?.debug(
|
|
72
|
+
`[artifact] resolved "${id}" → "${out.resolvedId}"${out.resolvedName ? ` ("${out.resolvedName}")` : ''}`,
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
return out.resolvedId;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ─── Writer tool (artifact_tool) ─────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
export function createArtifactTool(
|
|
81
|
+
config: ArtifactToolConfig,
|
|
82
|
+
): DynamicStructuredTool {
|
|
83
|
+
const { handlers, getScope, resolver, logger, descriptionOverride } = config;
|
|
84
|
+
|
|
85
|
+
return tool(
|
|
86
|
+
async (rawInput, runnableConfig): Promise<ArtifactToolResult> => {
|
|
87
|
+
const scope = getScope(runnableConfig);
|
|
88
|
+
if (!scope) {
|
|
89
|
+
logger?.warn('[artifact_tool] no scope resolved from runnableConfig');
|
|
90
|
+
return ['Error: No conversation context available', {}];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const input = rawInput as {
|
|
94
|
+
action: 'write' | 'edit' | 'verify' | 'delete';
|
|
95
|
+
content_id?: string;
|
|
96
|
+
content?: string;
|
|
97
|
+
name?: string;
|
|
98
|
+
old_str?: string;
|
|
99
|
+
new_str?: string;
|
|
100
|
+
replace_all?: boolean;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const resolvedContentId = await resolveContentIdIfPresent(
|
|
104
|
+
resolver,
|
|
105
|
+
input.content_id,
|
|
106
|
+
scope,
|
|
107
|
+
logger,
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
const started = Date.now();
|
|
111
|
+
try {
|
|
112
|
+
switch (input.action) {
|
|
113
|
+
case 'write': {
|
|
114
|
+
const args: WriteArgs = {
|
|
115
|
+
action: 'write',
|
|
116
|
+
content_id: resolvedContentId,
|
|
117
|
+
content: input.content ?? '',
|
|
118
|
+
name: input.name,
|
|
119
|
+
};
|
|
120
|
+
if (!args.content) return ['Error: write requires content', {}];
|
|
121
|
+
return await handlers.write(args, scope);
|
|
122
|
+
}
|
|
123
|
+
case 'edit': {
|
|
124
|
+
if (!resolvedContentId) return ['Error: edit requires content_id', {}];
|
|
125
|
+
const args: EditArgs = {
|
|
126
|
+
action: 'edit',
|
|
127
|
+
content_id: resolvedContentId,
|
|
128
|
+
old_str: input.old_str ?? '',
|
|
129
|
+
new_str: input.new_str ?? '',
|
|
130
|
+
replace_all: input.replace_all,
|
|
131
|
+
};
|
|
132
|
+
if (!args.old_str) return ['Error: edit requires old_str', {}];
|
|
133
|
+
return await handlers.edit(args, scope);
|
|
134
|
+
}
|
|
135
|
+
case 'verify': {
|
|
136
|
+
if (!resolvedContentId) return ['Error: verify requires content_id', {}];
|
|
137
|
+
const args: VerifyArgs = {
|
|
138
|
+
action: 'verify',
|
|
139
|
+
content_id: resolvedContentId,
|
|
140
|
+
};
|
|
141
|
+
return await handlers.verify(args, scope);
|
|
142
|
+
}
|
|
143
|
+
case 'delete': {
|
|
144
|
+
if (!resolvedContentId) return ['Error: delete requires content_id', {}];
|
|
145
|
+
const args: DeleteArgs = {
|
|
146
|
+
action: 'delete',
|
|
147
|
+
content_id: resolvedContentId,
|
|
148
|
+
};
|
|
149
|
+
return await handlers.delete(args, scope);
|
|
150
|
+
}
|
|
151
|
+
default:
|
|
152
|
+
return [`Unknown action: ${(input as { action: string }).action}`, {}];
|
|
153
|
+
}
|
|
154
|
+
} catch (err) {
|
|
155
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
156
|
+
logger?.error('[artifact_tool] handler threw', {
|
|
157
|
+
action: input.action,
|
|
158
|
+
contentId: resolvedContentId,
|
|
159
|
+
error: e.message,
|
|
160
|
+
elapsed: `${Date.now() - started}ms`,
|
|
161
|
+
});
|
|
162
|
+
return [`Error: ${e.message}`, {}];
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
name: ARTIFACT_TOOL_NAME,
|
|
167
|
+
responseFormat: 'content_and_artifact',
|
|
168
|
+
description: descriptionOverride ?? DEFAULT_ARTIFACT_DESCRIPTION,
|
|
169
|
+
schema: artifactToolSchema,
|
|
170
|
+
},
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ─── Reader tool (content_reader) ────────────────────────────────────────
|
|
175
|
+
|
|
176
|
+
export function createContentReaderTool(
|
|
177
|
+
config: ContentReaderToolConfig,
|
|
178
|
+
): DynamicStructuredTool {
|
|
179
|
+
const { handlers, getScope, resolver, logger, descriptionOverride } = config;
|
|
180
|
+
|
|
181
|
+
return tool(
|
|
182
|
+
async (rawInput, runnableConfig): Promise<ArtifactToolResult> => {
|
|
183
|
+
const scope = getScope(runnableConfig);
|
|
184
|
+
if (!scope) {
|
|
185
|
+
logger?.warn('[content_reader] no scope resolved from runnableConfig');
|
|
186
|
+
return ['Error: No conversation context available', {}];
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const input = rawInput as {
|
|
190
|
+
action: 'read' | 'search' | 'list' | 'info';
|
|
191
|
+
content_id?: string;
|
|
192
|
+
start_line?: number;
|
|
193
|
+
end_line?: number;
|
|
194
|
+
pattern?: string;
|
|
195
|
+
flags?: string;
|
|
196
|
+
context?: number;
|
|
197
|
+
offset?: number;
|
|
198
|
+
limit?: number;
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
const resolvedContentId = await resolveContentIdIfPresent(
|
|
202
|
+
resolver,
|
|
203
|
+
input.content_id,
|
|
204
|
+
scope,
|
|
205
|
+
logger,
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
const started = Date.now();
|
|
209
|
+
try {
|
|
210
|
+
switch (input.action) {
|
|
211
|
+
case 'read': {
|
|
212
|
+
if (!resolvedContentId) return ['Error: read requires content_id', {}];
|
|
213
|
+
const args: ReadArgs = {
|
|
214
|
+
action: 'read',
|
|
215
|
+
content_id: resolvedContentId,
|
|
216
|
+
start_line: input.start_line,
|
|
217
|
+
end_line: input.end_line,
|
|
218
|
+
offset: input.offset,
|
|
219
|
+
limit: input.limit,
|
|
220
|
+
};
|
|
221
|
+
return await handlers.read(args, scope);
|
|
222
|
+
}
|
|
223
|
+
case 'search': {
|
|
224
|
+
if (!resolvedContentId) return ['Error: search requires content_id', {}];
|
|
225
|
+
if (!input.pattern) return ['Error: search requires pattern', {}];
|
|
226
|
+
const args: SearchArgs = {
|
|
227
|
+
action: 'search',
|
|
228
|
+
content_id: resolvedContentId,
|
|
229
|
+
pattern: input.pattern,
|
|
230
|
+
flags: input.flags,
|
|
231
|
+
context: input.context,
|
|
232
|
+
offset: input.offset,
|
|
233
|
+
limit: input.limit,
|
|
234
|
+
};
|
|
235
|
+
return await handlers.search(args, scope);
|
|
236
|
+
}
|
|
237
|
+
case 'list': {
|
|
238
|
+
const args: ListArgs = { action: 'list' };
|
|
239
|
+
return await handlers.list(args, scope);
|
|
240
|
+
}
|
|
241
|
+
case 'info': {
|
|
242
|
+
if (!resolvedContentId) return ['Error: info requires content_id', {}];
|
|
243
|
+
const args: InfoArgs = {
|
|
244
|
+
action: 'info',
|
|
245
|
+
content_id: resolvedContentId,
|
|
246
|
+
};
|
|
247
|
+
return await handlers.info(args, scope);
|
|
248
|
+
}
|
|
249
|
+
default:
|
|
250
|
+
return [`Unknown action: ${(input as { action: string }).action}`, {}];
|
|
251
|
+
}
|
|
252
|
+
} catch (err) {
|
|
253
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
254
|
+
logger?.error('[content_reader] handler threw', {
|
|
255
|
+
action: input.action,
|
|
256
|
+
contentId: resolvedContentId,
|
|
257
|
+
error: e.message,
|
|
258
|
+
elapsed: `${Date.now() - started}ms`,
|
|
259
|
+
});
|
|
260
|
+
return [`Error: ${e.message}`, {}];
|
|
261
|
+
}
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
name: CONTENT_READER_NAME,
|
|
265
|
+
responseFormat: 'content_and_artifact',
|
|
266
|
+
description: descriptionOverride ?? DEFAULT_CONTENT_READER_DESCRIPTION,
|
|
267
|
+
schema: contentReaderSchema,
|
|
268
|
+
},
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export {
|
|
273
|
+
ARTIFACT_TOOL_NAME,
|
|
274
|
+
CONTENT_READER_NAME,
|
|
275
|
+
ARTIFACT_WRITE_ACTIONS,
|
|
276
|
+
CONTENT_READ_ACTIONS,
|
|
277
|
+
} from './schema';
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Artifact-tool types. The library owns the LangChain wiring, schema, and
|
|
3
|
+
* action dispatch. Runtime supplies an `ArtifactHandlers` bundle — one
|
|
4
|
+
* function per action — so ranger reuses its existing handlers, CLI
|
|
5
|
+
* supplies disk-backed ones, and A2A server supplies buffer-backed ones.
|
|
6
|
+
*
|
|
7
|
+
* Each handler receives:
|
|
8
|
+
* - `args`: the parsed input (typed per action)
|
|
9
|
+
* - `scope`: runtime-resolved scope identity (conversationId + userId
|
|
10
|
+
* for ranger, agent-run-id for CLI, a2a-task-id for A2A)
|
|
11
|
+
*
|
|
12
|
+
* Each handler returns a `[llmText, toolArtifact]` tuple matching
|
|
13
|
+
* LangChain's `content_and_artifact` response format.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export interface ArtifactToolScope {
|
|
17
|
+
/**
|
|
18
|
+
* Primary scope — whatever the runtime uses to silo artifacts per
|
|
19
|
+
* session/run/conversation. Ranger uses `conversationId`; CLI uses
|
|
20
|
+
* `agent-run-id` or `agent-id`; A2A uses `a2a-task-id`.
|
|
21
|
+
*/
|
|
22
|
+
conversationId: string;
|
|
23
|
+
/** Optional — present when the runtime has an authenticated user. */
|
|
24
|
+
userId?: string;
|
|
25
|
+
/** Free-form extension — runtimes can add their own fields. */
|
|
26
|
+
[key: string]: unknown;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type ArtifactToolResult = [llmText: string, artifact?: unknown];
|
|
30
|
+
|
|
31
|
+
// ─── Action arg shapes ────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
export interface WriteArgs {
|
|
34
|
+
action: 'write';
|
|
35
|
+
content_id?: string;
|
|
36
|
+
content: string;
|
|
37
|
+
name?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface EditArgs {
|
|
41
|
+
action: 'edit';
|
|
42
|
+
content_id: string;
|
|
43
|
+
old_str: string;
|
|
44
|
+
new_str: string;
|
|
45
|
+
replace_all?: boolean;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface VerifyArgs {
|
|
49
|
+
action: 'verify';
|
|
50
|
+
content_id: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface DeleteArgs {
|
|
54
|
+
action: 'delete';
|
|
55
|
+
content_id: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface ReadArgs {
|
|
59
|
+
action: 'read';
|
|
60
|
+
content_id: string;
|
|
61
|
+
start_line?: number;
|
|
62
|
+
end_line?: number;
|
|
63
|
+
offset?: number;
|
|
64
|
+
limit?: number;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface SearchArgs {
|
|
68
|
+
action: 'search';
|
|
69
|
+
content_id: string;
|
|
70
|
+
pattern: string;
|
|
71
|
+
flags?: string;
|
|
72
|
+
context?: number;
|
|
73
|
+
offset?: number;
|
|
74
|
+
limit?: number;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface ListArgs {
|
|
78
|
+
action: 'list';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface InfoArgs {
|
|
82
|
+
action: 'info';
|
|
83
|
+
content_id: string;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export type ArtifactWriteAction = WriteArgs | EditArgs | VerifyArgs | DeleteArgs;
|
|
87
|
+
export type ArtifactReadAction = ReadArgs | SearchArgs | ListArgs | InfoArgs;
|
|
88
|
+
|
|
89
|
+
// ─── Handler bundles ──────────────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
/** Writer-surface handlers — invoked by `artifact_tool`. */
|
|
92
|
+
export interface ArtifactWriteHandlers {
|
|
93
|
+
write(args: WriteArgs, scope: ArtifactToolScope): Promise<ArtifactToolResult>;
|
|
94
|
+
edit(args: EditArgs, scope: ArtifactToolScope): Promise<ArtifactToolResult>;
|
|
95
|
+
verify(args: VerifyArgs, scope: ArtifactToolScope): Promise<ArtifactToolResult>;
|
|
96
|
+
delete(args: DeleteArgs, scope: ArtifactToolScope): Promise<ArtifactToolResult>;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Reader-surface handlers — invoked by `content_reader`. */
|
|
100
|
+
export interface ContentReadHandlers {
|
|
101
|
+
read(args: ReadArgs, scope: ArtifactToolScope): Promise<ArtifactToolResult>;
|
|
102
|
+
search(args: SearchArgs, scope: ArtifactToolScope): Promise<ArtifactToolResult>;
|
|
103
|
+
list(args: ListArgs, scope: ArtifactToolScope): Promise<ArtifactToolResult>;
|
|
104
|
+
info(args: InfoArgs, scope: ArtifactToolScope): Promise<ArtifactToolResult>;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Optional content_id self-healing: runtimes that want to let the LLM
|
|
109
|
+
* pass nicknames (e.g., "Dashboard") instead of canonical IDs implement
|
|
110
|
+
* this. Library falls through when unset.
|
|
111
|
+
*/
|
|
112
|
+
export interface ContentIdResolver {
|
|
113
|
+
resolve(
|
|
114
|
+
id: string,
|
|
115
|
+
scope: ArtifactToolScope,
|
|
116
|
+
): Promise<{ resolvedId: string; resolvedName?: string } | null>;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export interface ArtifactToolLogger {
|
|
120
|
+
debug: (msg: string, ...args: unknown[]) => void;
|
|
121
|
+
info: (msg: string, ...args: unknown[]) => void;
|
|
122
|
+
warn: (msg: string, ...args: unknown[]) => void;
|
|
123
|
+
error: (msg: string, ...args: unknown[]) => void;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export interface ArtifactToolBaseConfig {
|
|
127
|
+
/**
|
|
128
|
+
* Resolves the runtime scope from the LangChain `config` object on each
|
|
129
|
+
* invocation. Ranger pulls `conversationId` + `userId` from request
|
|
130
|
+
* metadata; CLI pulls `agent-run-id` from its runner context.
|
|
131
|
+
*/
|
|
132
|
+
getScope: (config: unknown) => ArtifactToolScope | null;
|
|
133
|
+
resolver?: ContentIdResolver;
|
|
134
|
+
logger?: ArtifactToolLogger;
|
|
135
|
+
/**
|
|
136
|
+
* Description override. Host can inject brand-specific guidance
|
|
137
|
+
* (e.g., ranger's HTML branding mandate, TSX style rules). Defaults
|
|
138
|
+
* to a generic description appropriate for any runtime.
|
|
139
|
+
*/
|
|
140
|
+
descriptionOverride?: string;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export interface ArtifactToolConfig extends ArtifactToolBaseConfig {
|
|
144
|
+
handlers: ArtifactWriteHandlers;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export interface ContentReaderToolConfig extends ArtifactToolBaseConfig {
|
|
148
|
+
handlers: ContentReadHandlers;
|
|
149
|
+
}
|
|
@@ -8,16 +8,26 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { createFileSearchTool } from '../tool';
|
|
11
|
-
import {
|
|
12
|
-
|
|
11
|
+
import {
|
|
12
|
+
plainTextFormatter,
|
|
13
|
+
createCitationAnchorFormatter,
|
|
14
|
+
} from '../formatter';
|
|
15
|
+
import type {
|
|
16
|
+
RagClient,
|
|
17
|
+
RagQueryParams,
|
|
18
|
+
RagChunk,
|
|
19
|
+
FileSearchFile,
|
|
20
|
+
} from '../types';
|
|
13
21
|
|
|
14
22
|
// Build a mock RagClient that records every query it receives and returns
|
|
15
23
|
// a deterministic chunk set per file.
|
|
16
|
-
function makeRagClient(
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
24
|
+
function makeRagClient(
|
|
25
|
+
opts: {
|
|
26
|
+
chunksByFile?: Record<string, RagChunk[]>;
|
|
27
|
+
failFileIds?: Set<string>;
|
|
28
|
+
hang?: boolean;
|
|
29
|
+
} = {}
|
|
30
|
+
) {
|
|
21
31
|
const calls: RagQueryParams[] = [];
|
|
22
32
|
const client: RagClient = {
|
|
23
33
|
async query(params) {
|
|
@@ -119,7 +129,7 @@ describe('createFileSearchTool', () => {
|
|
|
119
129
|
expect(calls.length).toBe(3);
|
|
120
130
|
expect(onFileError).toHaveBeenCalledWith(
|
|
121
131
|
expect.objectContaining({ file_id: 'f-beta' }),
|
|
122
|
-
expect.any(Error)
|
|
132
|
+
expect.any(Error)
|
|
123
133
|
);
|
|
124
134
|
});
|
|
125
135
|
|
|
@@ -142,7 +152,7 @@ describe('createFileSearchTool', () => {
|
|
|
142
152
|
entity_id: 'tenant-42',
|
|
143
153
|
scope: 'user:alice',
|
|
144
154
|
authHeaders: { Authorization: 'Bearer TOKEN' },
|
|
145
|
-
})
|
|
155
|
+
})
|
|
146
156
|
);
|
|
147
157
|
});
|
|
148
158
|
|
|
@@ -236,7 +246,7 @@ function chunk(
|
|
|
236
246
|
file_id: string,
|
|
237
247
|
text: string,
|
|
238
248
|
distance: number,
|
|
239
|
-
metadata?: Record<string, unknown
|
|
249
|
+
metadata?: Record<string, unknown>
|
|
240
250
|
): RagChunk {
|
|
241
251
|
return { file_id, page_content: text, distance, metadata };
|
|
242
252
|
}
|
|
@@ -63,14 +63,14 @@ export interface CitationAnchorFormatterOptions {
|
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
export function createCitationAnchorFormatter(
|
|
66
|
-
opts: CitationAnchorFormatterOptions = {}
|
|
66
|
+
opts: CitationAnchorFormatterOptions = {}
|
|
67
67
|
): FileSearchResultFormatter {
|
|
68
68
|
const toolName = opts.toolName ?? 'file_search';
|
|
69
|
-
const getOffset = opts.getSourceOffset ?? (() => 0);
|
|
70
|
-
const advance = opts.advanceSourceOffset ?? (() => {});
|
|
69
|
+
const getOffset = opts.getSourceOffset ?? ((): number => 0);
|
|
70
|
+
const advance = opts.advanceSourceOffset ?? ((_by: number): void => {});
|
|
71
71
|
|
|
72
72
|
return {
|
|
73
|
-
format(chunks) {
|
|
73
|
+
format(chunks): { message: string; artifact?: unknown } {
|
|
74
74
|
if (chunks.length === 0) {
|
|
75
75
|
return {
|
|
76
76
|
message:
|
|
@@ -100,9 +100,7 @@ export function createCitationAnchorFormatter(
|
|
|
100
100
|
relevance: 1 - c.distance,
|
|
101
101
|
pages: getPage(c) != null ? [getPage(c) as number] : [],
|
|
102
102
|
pageRelevance:
|
|
103
|
-
getPage(c) != null
|
|
104
|
-
? { [getPage(c) as number]: 1 - c.distance }
|
|
105
|
-
: {},
|
|
103
|
+
getPage(c) != null ? { [getPage(c) as number]: 1 - c.distance } : {},
|
|
106
104
|
}));
|
|
107
105
|
|
|
108
106
|
advance(chunks.length);
|
|
@@ -21,14 +21,11 @@ export const RAG_API_URL_ENV = 'RAG_API_URL';
|
|
|
21
21
|
|
|
22
22
|
/** Resolve base URL at call time so env-var changes propagate. */
|
|
23
23
|
export function getRagBaseUrl(override?: string): string {
|
|
24
|
-
const url =
|
|
25
|
-
override ??
|
|
26
|
-
getEnvironmentVariable(RAG_API_URL_ENV) ??
|
|
27
|
-
'';
|
|
24
|
+
const url = override ?? getEnvironmentVariable(RAG_API_URL_ENV) ?? '';
|
|
28
25
|
if (!url) {
|
|
29
26
|
throw new Error(
|
|
30
27
|
`file_search: ${RAG_API_URL_ENV} is not configured. ` +
|
|
31
|
-
`Set the env var or pass baseUrl to HttpRagClient
|
|
28
|
+
`Set the env var or pass baseUrl to HttpRagClient.`
|
|
32
29
|
);
|
|
33
30
|
}
|
|
34
31
|
return url.replace(/\/$/, '');
|
|
@@ -112,7 +109,7 @@ export class HttpRagClient implements RagClient {
|
|
|
112
109
|
if (!res.ok) {
|
|
113
110
|
const text = await res.text().catch(() => '');
|
|
114
111
|
throw new Error(
|
|
115
|
-
`RAG query failed: ${res.status} ${res.statusText} — ${text.slice(0, 200)}
|
|
112
|
+
`RAG query failed: ${res.status} ${res.statusText} — ${text.slice(0, 200)}`
|
|
116
113
|
);
|
|
117
114
|
}
|
|
118
115
|
const json = (await res.json()) as RagApiResponse;
|
|
@@ -131,11 +128,10 @@ export class HttpRagClient implements RagClient {
|
|
|
131
128
|
return resp
|
|
132
129
|
.filter((row) => Array.isArray(row) && row.length === 2)
|
|
133
130
|
.map(([doc, distance]) => ({
|
|
134
|
-
file_id:
|
|
135
|
-
|
|
136
|
-
page_content: doc?.page_content ?? '',
|
|
131
|
+
file_id: (doc.metadata?.file_id as string | undefined) ?? file_id,
|
|
132
|
+
page_content: doc.page_content ?? '',
|
|
137
133
|
distance: typeof distance === 'number' ? distance : 1,
|
|
138
|
-
metadata: doc
|
|
134
|
+
metadata: doc.metadata,
|
|
139
135
|
}));
|
|
140
136
|
}
|
|
141
137
|
}
|
|
@@ -4,13 +4,13 @@ export const fileSearchInputSchema = z.object({
|
|
|
4
4
|
query: z
|
|
5
5
|
.string()
|
|
6
6
|
.describe(
|
|
7
|
-
"A natural language query to search for relevant information in the files. Be SPECIFIC and TARGETED — use keywords for the specific section or topic you need. For comprehensive tasks (summaries, overviews), call this tool multiple times with different targeted queries (e.g., 'introduction', 'methodology', 'results', 'conclusions') rather than one broad query."
|
|
7
|
+
"A natural language query to search for relevant information in the files. Be SPECIFIC and TARGETED — use keywords for the specific section or topic you need. For comprehensive tasks (summaries, overviews), call this tool multiple times with different targeted queries (e.g., 'introduction', 'methodology', 'results', 'conclusions') rather than one broad query."
|
|
8
8
|
),
|
|
9
9
|
target_files: z
|
|
10
10
|
.array(z.string())
|
|
11
11
|
.optional()
|
|
12
12
|
.describe(
|
|
13
|
-
'Optional list of filenames (or partial names) to limit the search to. When provided, only files whose name contains one of these strings will be searched. Use this to avoid searching irrelevant files. Omit to search all available files.'
|
|
13
|
+
'Optional list of filenames (or partial names) to limit the search to. When provided, only files whose name contains one of these strings will be searched. Use this to avoid searching irrelevant files. Omit to search all available files.'
|
|
14
14
|
),
|
|
15
15
|
});
|
|
16
16
|
|
|
@@ -54,7 +54,7 @@ Cite EVERY statement derived from file content. Place the citation anchor IMMEDI
|
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
export function createFileSearchTool(
|
|
57
|
-
config: FileSearchToolConfig
|
|
57
|
+
config: FileSearchToolConfig
|
|
58
58
|
): DynamicStructuredTool {
|
|
59
59
|
const {
|
|
60
60
|
ragClient,
|
|
@@ -97,16 +97,16 @@ export function createFileSearchTool(
|
|
|
97
97
|
if (target_files && target_files.length > 0) {
|
|
98
98
|
const lowerTargets = target_files.map((t) => t.toLowerCase());
|
|
99
99
|
const matched = files.filter((f) =>
|
|
100
|
-
lowerTargets.some((t) => f.filename.toLowerCase().includes(t))
|
|
100
|
+
lowerTargets.some((t) => f.filename.toLowerCase().includes(t))
|
|
101
101
|
);
|
|
102
102
|
if (matched.length === 0) {
|
|
103
103
|
logger?.warn(
|
|
104
|
-
`[file_search] No files matched target_files ${target_files.join(', ')}; falling back to all files
|
|
104
|
+
`[file_search] No files matched target_files ${target_files.join(', ')}; falling back to all files`
|
|
105
105
|
);
|
|
106
106
|
filesToQuery = files;
|
|
107
107
|
} else {
|
|
108
108
|
logger?.info(
|
|
109
|
-
`[file_search] Filtered to ${matched.length}/${files.length} via target_files
|
|
109
|
+
`[file_search] Filtered to ${matched.length}/${files.length} via target_files`
|
|
110
110
|
);
|
|
111
111
|
filesToQuery = matched;
|
|
112
112
|
}
|
|
@@ -131,7 +131,7 @@ export function createFileSearchTool(
|
|
|
131
131
|
} catch (err) {
|
|
132
132
|
const e = err instanceof Error ? err : new Error(String(err));
|
|
133
133
|
logger?.error(
|
|
134
|
-
`[file_search] Query failed for ${file.filename}: ${e.message}
|
|
134
|
+
`[file_search] Query failed for ${file.filename}: ${e.message}`
|
|
135
135
|
);
|
|
136
136
|
callbacks?.onFileError?.(file, e);
|
|
137
137
|
return [];
|
|
@@ -200,7 +200,7 @@ export function createFileSearchTool(
|
|
|
200
200
|
responseFormat: 'content_and_artifact',
|
|
201
201
|
description: buildDescription({ fileCitations }),
|
|
202
202
|
schema: fileSearchInputSchema,
|
|
203
|
-
}
|
|
203
|
+
}
|
|
204
204
|
);
|
|
205
205
|
}
|
|
206
206
|
|
|
@@ -79,7 +79,7 @@ export interface FileSearchResultFormatter {
|
|
|
79
79
|
callIndex: number;
|
|
80
80
|
/** Same files list the factory was seeded with (for lookups). */
|
|
81
81
|
files: FileSearchFile[];
|
|
82
|
-
}
|
|
82
|
+
}
|
|
83
83
|
): {
|
|
84
84
|
/** Message returned to the LLM (content). */
|
|
85
85
|
message: string;
|
|
@@ -122,7 +122,9 @@ export interface FileSearchToolConfig {
|
|
|
122
122
|
* the host can mint fresh short-lived tokens (ranger's JWT pattern).
|
|
123
123
|
* When omitted, no auth headers are sent.
|
|
124
124
|
*/
|
|
125
|
-
getAuthHeaders?: () =>
|
|
125
|
+
getAuthHeaders?: () =>
|
|
126
|
+
| Record<string, string>
|
|
127
|
+
| Promise<Record<string, string>>;
|
|
126
128
|
/**
|
|
127
129
|
* Result formatter. When omitted, the default plain-text formatter is
|
|
128
130
|
* used (suitable for CLI/A2A runtimes that don't need citation anchors).
|