@proteinjs/conversation 2.1.2 → 2.1.4
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/index.d.ts +8 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -1
- package/dist/src/Conversation.d.ts +36 -1
- package/dist/src/Conversation.d.ts.map +1 -1
- package/dist/src/Conversation.js +163 -5
- package/dist/src/Conversation.js.map +1 -1
- package/dist/src/OpenAi.d.ts +29 -0
- package/dist/src/OpenAi.d.ts.map +1 -1
- package/dist/src/OpenAi.js +77 -30
- package/dist/src/OpenAi.js.map +1 -1
- package/dist/src/fs/conversation_fs/ConversationFsModule.d.ts +1 -0
- package/dist/src/fs/conversation_fs/ConversationFsModule.d.ts.map +1 -1
- package/dist/src/fs/conversation_fs/ConversationFsModule.js +6 -2
- package/dist/src/fs/conversation_fs/ConversationFsModule.js.map +1 -1
- package/dist/src/fs/conversation_fs/FsFunctions.d.ts +36 -3
- package/dist/src/fs/conversation_fs/FsFunctions.d.ts.map +1 -1
- package/dist/src/fs/conversation_fs/FsFunctions.js +142 -20
- package/dist/src/fs/conversation_fs/FsFunctions.js.map +1 -1
- package/dist/src/fs/keyword_to_files_index/KeywordToFilesIndexModule.d.ts +4 -1
- package/dist/src/fs/keyword_to_files_index/KeywordToFilesIndexModule.d.ts.map +1 -1
- package/dist/src/fs/keyword_to_files_index/KeywordToFilesIndexModule.js +13 -9
- package/dist/src/fs/keyword_to_files_index/KeywordToFilesIndexModule.js.map +1 -1
- package/index.ts +10 -1
- package/package.json +4 -3
- package/src/Conversation.ts +213 -5
- package/src/OpenAi.ts +123 -13
- package/src/fs/conversation_fs/ConversationFsModule.ts +8 -2
- package/src/fs/conversation_fs/FsFunctions.ts +97 -17
- package/src/fs/keyword_to_files_index/KeywordToFilesIndexModule.ts +14 -9
package/src/OpenAi.ts
CHANGED
|
@@ -22,14 +22,44 @@ function delay(ms: number) {
|
|
|
22
22
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
/** Structured capture of each tool call during a single generateResponse loop. */
|
|
26
|
+
export type ToolInvocationResult = {
|
|
27
|
+
id: string; // tool_call_id from the model
|
|
28
|
+
name: string; // function name invoked
|
|
29
|
+
startedAt: Date;
|
|
30
|
+
finishedAt: Date;
|
|
31
|
+
input: unknown; // parsed JSON args (or raw string if parse failed)
|
|
32
|
+
ok: boolean;
|
|
33
|
+
data?: unknown; // tool return value (JSON-serializable)
|
|
34
|
+
error?: { message: string; stack?: string };
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/** Realtime progress hook for tool calls. */
|
|
38
|
+
export type ToolInvocationProgressEvent =
|
|
39
|
+
| {
|
|
40
|
+
type: 'started';
|
|
41
|
+
id: string;
|
|
42
|
+
name: string;
|
|
43
|
+
startedAt: Date;
|
|
44
|
+
input: unknown;
|
|
45
|
+
}
|
|
46
|
+
| {
|
|
47
|
+
type: 'finished';
|
|
48
|
+
result: ToolInvocationResult;
|
|
49
|
+
};
|
|
50
|
+
|
|
25
51
|
export type GenerateResponseParams = {
|
|
26
52
|
messages: (string | ChatCompletionMessageParam)[];
|
|
27
53
|
model?: TiktokenModel;
|
|
54
|
+
/** Optional realtime hook for tool-call lifecycle (started/finished). */
|
|
55
|
+
onToolInvocation?: (evt: ToolInvocationProgressEvent) => void;
|
|
28
56
|
};
|
|
29
57
|
|
|
30
58
|
export type GenerateResponseReturn = {
|
|
31
59
|
message: string;
|
|
32
60
|
usagedata: UsageData;
|
|
61
|
+
/** Structured ledger of tool calls executed while producing this message. */
|
|
62
|
+
toolInvocations: ToolInvocationResult[];
|
|
33
63
|
};
|
|
34
64
|
|
|
35
65
|
export type GenerateStreamingResponseParams = GenerateResponseParams & {
|
|
@@ -42,6 +72,8 @@ type GenerateResponseHelperParams = GenerateStreamingResponseParams & {
|
|
|
42
72
|
stream: boolean;
|
|
43
73
|
currentFunctionCalls?: number;
|
|
44
74
|
usageDataAccumulator?: UsageDataAccumulator;
|
|
75
|
+
/** Accumulated across recursive tool loops. */
|
|
76
|
+
toolInvocations?: ToolInvocationResult[];
|
|
45
77
|
};
|
|
46
78
|
|
|
47
79
|
export type OpenAiParams = {
|
|
@@ -53,7 +85,7 @@ export type OpenAiParams = {
|
|
|
53
85
|
logLevel?: LogLevel;
|
|
54
86
|
};
|
|
55
87
|
|
|
56
|
-
export const DEFAULT_MODEL: TiktokenModel = 'gpt-
|
|
88
|
+
export const DEFAULT_MODEL: TiktokenModel = 'gpt-4o';
|
|
57
89
|
export const DEFAULT_MAX_FUNCTION_CALLS = 50;
|
|
58
90
|
|
|
59
91
|
export class OpenAi {
|
|
@@ -77,7 +109,7 @@ export class OpenAi {
|
|
|
77
109
|
this.functions = functions;
|
|
78
110
|
this.messageModerators = messageModerators;
|
|
79
111
|
this.maxFunctionCalls = maxFunctionCalls;
|
|
80
|
-
this.logLevel = logLevel;
|
|
112
|
+
this.logLevel = (process.env.PROTEINJS_CONVERSATION_OPENAI_LOG_LEVEL as LogLevel | undefined) ?? logLevel;
|
|
81
113
|
}
|
|
82
114
|
|
|
83
115
|
async generateResponse({ model, ...rest }: GenerateResponseParams): Promise<GenerateResponseReturn> {
|
|
@@ -85,11 +117,17 @@ export class OpenAi {
|
|
|
85
117
|
model: model ?? this.model,
|
|
86
118
|
stream: false,
|
|
87
119
|
...rest,
|
|
120
|
+
toolInvocations: [],
|
|
88
121
|
})) as GenerateResponseReturn;
|
|
89
122
|
}
|
|
90
123
|
|
|
91
124
|
async generateStreamingResponse({ model, ...rest }: GenerateStreamingResponseParams): Promise<Readable> {
|
|
92
|
-
return (await this.generateResponseHelper({
|
|
125
|
+
return (await this.generateResponseHelper({
|
|
126
|
+
model: model ?? this.model,
|
|
127
|
+
stream: true,
|
|
128
|
+
...rest,
|
|
129
|
+
toolInvocations: [],
|
|
130
|
+
})) as Readable;
|
|
93
131
|
}
|
|
94
132
|
|
|
95
133
|
private async generateResponseHelper({
|
|
@@ -98,8 +136,10 @@ export class OpenAi {
|
|
|
98
136
|
stream,
|
|
99
137
|
abortSignal,
|
|
100
138
|
onUsageData,
|
|
139
|
+
onToolInvocation,
|
|
101
140
|
usageDataAccumulator,
|
|
102
141
|
currentFunctionCalls = 0,
|
|
142
|
+
toolInvocations = [],
|
|
103
143
|
}: GenerateResponseHelperParams): Promise<GenerateResponseReturn | Readable> {
|
|
104
144
|
const logger = new Logger({ name: 'OpenAi.generateResponseHelper', logLevel: this.logLevel });
|
|
105
145
|
this.updateMessageHistory(messages);
|
|
@@ -130,7 +170,9 @@ export class OpenAi {
|
|
|
130
170
|
currentFunctionCalls,
|
|
131
171
|
resolvedUsageDataAccumulator,
|
|
132
172
|
abortSignal,
|
|
133
|
-
onUsageData
|
|
173
|
+
onUsageData,
|
|
174
|
+
toolInvocations,
|
|
175
|
+
onToolInvocation
|
|
134
176
|
)) as (toolCalls: ChatCompletionMessageToolCall[], currentFunctionCalls: number) => Promise<Readable>;
|
|
135
177
|
const streamProcessor = new OpenAiStreamProcessor(
|
|
136
178
|
inputStream,
|
|
@@ -152,7 +194,9 @@ export class OpenAi {
|
|
|
152
194
|
currentFunctionCalls,
|
|
153
195
|
resolvedUsageDataAccumulator,
|
|
154
196
|
abortSignal,
|
|
155
|
-
onUsageData
|
|
197
|
+
onUsageData,
|
|
198
|
+
toolInvocations,
|
|
199
|
+
onToolInvocation
|
|
156
200
|
);
|
|
157
201
|
}
|
|
158
202
|
|
|
@@ -162,7 +206,7 @@ export class OpenAi {
|
|
|
162
206
|
}
|
|
163
207
|
|
|
164
208
|
this.history.push([responseMessage]);
|
|
165
|
-
return { message: responseText, usagedata: resolvedUsageDataAccumulator.usageData };
|
|
209
|
+
return { message: responseText, usagedata: resolvedUsageDataAccumulator.usageData, toolInvocations };
|
|
166
210
|
};
|
|
167
211
|
|
|
168
212
|
// Only wrap in context if this is the first call
|
|
@@ -208,7 +252,6 @@ export class OpenAi {
|
|
|
208
252
|
const response = await openaiApi.chat.completions.create(
|
|
209
253
|
{
|
|
210
254
|
model,
|
|
211
|
-
temperature: 0,
|
|
212
255
|
messages: this.history.getMessages(),
|
|
213
256
|
...(this.functions &&
|
|
214
257
|
this.functions.length > 0 && {
|
|
@@ -310,7 +353,9 @@ export class OpenAi {
|
|
|
310
353
|
currentFunctionCalls: number,
|
|
311
354
|
usageDataAccumulator: UsageDataAccumulator,
|
|
312
355
|
abortSignal?: AbortSignal,
|
|
313
|
-
onUsageData?: (usageData: UsageData) => Promise<void
|
|
356
|
+
onUsageData?: (usageData: UsageData) => Promise<void>,
|
|
357
|
+
toolInvocations: ToolInvocationResult[] = [],
|
|
358
|
+
onToolInvocation?: (evt: ToolInvocationProgressEvent) => void
|
|
314
359
|
): Promise<GenerateResponseReturn | Readable> {
|
|
315
360
|
if (currentFunctionCalls >= this.maxFunctionCalls) {
|
|
316
361
|
throw new Error(`Max function calls (${this.maxFunctionCalls}) reached. Stopping execution.`);
|
|
@@ -327,7 +372,7 @@ export class OpenAi {
|
|
|
327
372
|
this.history.push([toolCallMessage]);
|
|
328
373
|
|
|
329
374
|
// Call the tools and get the responses
|
|
330
|
-
const toolMessageParams = await this.callTools(toolCalls, usageDataAccumulator);
|
|
375
|
+
const toolMessageParams = await this.callTools(toolCalls, usageDataAccumulator, toolInvocations, onToolInvocation);
|
|
331
376
|
|
|
332
377
|
// Add the tool responses to the history
|
|
333
378
|
this.history.push(toolMessageParams);
|
|
@@ -339,18 +384,31 @@ export class OpenAi {
|
|
|
339
384
|
stream,
|
|
340
385
|
abortSignal,
|
|
341
386
|
onUsageData,
|
|
387
|
+
onToolInvocation,
|
|
342
388
|
usageDataAccumulator,
|
|
343
389
|
currentFunctionCalls: currentFunctionCalls + toolCalls.length,
|
|
390
|
+
toolInvocations,
|
|
344
391
|
});
|
|
345
392
|
}
|
|
346
393
|
|
|
347
394
|
private async callTools(
|
|
348
395
|
toolCalls: ChatCompletionMessageToolCall[],
|
|
349
|
-
usageDataAccumulator: UsageDataAccumulator
|
|
396
|
+
usageDataAccumulator: UsageDataAccumulator,
|
|
397
|
+
toolInvocations: ToolInvocationResult[],
|
|
398
|
+
onToolInvocation?: (evt: ToolInvocationProgressEvent) => void
|
|
350
399
|
): Promise<ChatCompletionMessageParam[]> {
|
|
351
400
|
const toolMessageParams: ChatCompletionMessageParam[] = (
|
|
352
401
|
await Promise.all(
|
|
353
|
-
toolCalls.map(
|
|
402
|
+
toolCalls.map(
|
|
403
|
+
async (toolCall) =>
|
|
404
|
+
await this.callFunction(
|
|
405
|
+
toolCall.function,
|
|
406
|
+
toolCall.id,
|
|
407
|
+
usageDataAccumulator,
|
|
408
|
+
toolInvocations,
|
|
409
|
+
onToolInvocation
|
|
410
|
+
)
|
|
411
|
+
)
|
|
354
412
|
)
|
|
355
413
|
).reduce((acc, val) => acc.concat(val), []);
|
|
356
414
|
|
|
@@ -360,7 +418,9 @@ export class OpenAi {
|
|
|
360
418
|
private async callFunction(
|
|
361
419
|
functionCall: ChatCompletionMessageToolCall.Function,
|
|
362
420
|
toolCallId: string,
|
|
363
|
-
usageDataAccumulator: UsageDataAccumulator
|
|
421
|
+
usageDataAccumulator: UsageDataAccumulator,
|
|
422
|
+
toolInvocations: ToolInvocationResult[],
|
|
423
|
+
onToolInvocation?: (evt: ToolInvocationProgressEvent) => void
|
|
364
424
|
): Promise<ChatCompletionMessageParam[]> {
|
|
365
425
|
const logger = new Logger({ name: 'OpenAi.callFunction', logLevel: this.logLevel });
|
|
366
426
|
if (!this.functions) {
|
|
@@ -370,7 +430,7 @@ export class OpenAi {
|
|
|
370
430
|
}
|
|
371
431
|
|
|
372
432
|
functionCall.name = functionCall.name.split('.').pop() as string;
|
|
373
|
-
const f = this.functions.find((
|
|
433
|
+
const f = this.functions.find((fx) => fx.definition.name === functionCall.name);
|
|
374
434
|
if (!f) {
|
|
375
435
|
const errorMessage = `Assistant attempted to call nonexistent function`;
|
|
376
436
|
logger.error({ message: errorMessage, obj: { functionName: functionCall.name } });
|
|
@@ -390,7 +450,33 @@ export class OpenAi {
|
|
|
390
450
|
obj: { toolCallId, functionName: f.definition.name, args: parsedArguments },
|
|
391
451
|
});
|
|
392
452
|
usageDataAccumulator.recordToolCall(f.definition.name);
|
|
453
|
+
|
|
454
|
+
const startedAt = new Date();
|
|
455
|
+
|
|
456
|
+
onToolInvocation?.({
|
|
457
|
+
type: 'started',
|
|
458
|
+
id: toolCallId,
|
|
459
|
+
name: f.definition.name,
|
|
460
|
+
startedAt,
|
|
461
|
+
input: parsedArguments,
|
|
462
|
+
});
|
|
463
|
+
|
|
393
464
|
const returnObject = await f.call(parsedArguments);
|
|
465
|
+
const finishedAt = new Date();
|
|
466
|
+
|
|
467
|
+
// Record success
|
|
468
|
+
const rec: ToolInvocationResult = {
|
|
469
|
+
id: toolCallId,
|
|
470
|
+
name: f.definition.name,
|
|
471
|
+
startedAt,
|
|
472
|
+
finishedAt,
|
|
473
|
+
input: parsedArguments,
|
|
474
|
+
ok: true,
|
|
475
|
+
data: returnObject,
|
|
476
|
+
};
|
|
477
|
+
toolInvocations.push(rec);
|
|
478
|
+
|
|
479
|
+
onToolInvocation?.({ type: 'finished', result: rec });
|
|
394
480
|
|
|
395
481
|
const returnObjectCompletionParams: ChatCompletionMessageParam[] = [];
|
|
396
482
|
if (isInstanceOf(returnObject, ChatCompletionMessageParamFactory)) {
|
|
@@ -433,11 +519,35 @@ export class OpenAi {
|
|
|
433
519
|
|
|
434
520
|
return returnObjectCompletionParams;
|
|
435
521
|
} catch (error: any) {
|
|
522
|
+
const now = new Date();
|
|
523
|
+
const attemptedArgs = (() => {
|
|
524
|
+
try {
|
|
525
|
+
return JSON.parse(functionCall.arguments);
|
|
526
|
+
} catch {
|
|
527
|
+
return functionCall.arguments;
|
|
528
|
+
}
|
|
529
|
+
})();
|
|
530
|
+
|
|
531
|
+
// Record failure
|
|
532
|
+
const rec: ToolInvocationResult = {
|
|
533
|
+
id: toolCallId,
|
|
534
|
+
name: functionCall.name,
|
|
535
|
+
startedAt: now,
|
|
536
|
+
finishedAt: now,
|
|
537
|
+
input: attemptedArgs,
|
|
538
|
+
ok: false,
|
|
539
|
+
error: { message: String(error?.message ?? error), stack: (error as any)?.stack },
|
|
540
|
+
};
|
|
541
|
+
toolInvocations.push(rec);
|
|
542
|
+
|
|
543
|
+
onToolInvocation?.({ type: 'finished', result: rec });
|
|
544
|
+
|
|
436
545
|
logger.error({
|
|
437
546
|
message: `An error occurred while executing function`,
|
|
438
547
|
error,
|
|
439
548
|
obj: { toolCallId, functionName: f.definition.name },
|
|
440
549
|
});
|
|
550
|
+
|
|
441
551
|
throw error;
|
|
442
552
|
}
|
|
443
553
|
}
|
|
@@ -6,7 +6,8 @@ import { ConversationFsModerator } from './ConversationFsModerator';
|
|
|
6
6
|
import {
|
|
7
7
|
fsFunctions,
|
|
8
8
|
getRecentlyAccessedFilePathsFunction,
|
|
9
|
-
|
|
9
|
+
grepFunction,
|
|
10
|
+
grepFunctionName,
|
|
10
11
|
readFilesFunction,
|
|
11
12
|
readFilesFunctionName,
|
|
12
13
|
writeFilesFunction,
|
|
@@ -36,7 +37,7 @@ export class ConversationFsModule implements ConversationModule {
|
|
|
36
37
|
`When reading/writing a file in a specified package, join the package directory with the relative path to form the file path`,
|
|
37
38
|
`When searching for source files, do not look in the dist or node_modules directories`,
|
|
38
39
|
`If you don't know a file path, don't try to guess it, use the ${searchFilesFunctionName} function to find it`,
|
|
39
|
-
`When searching for something (ie. a file to work with/in), unless more context is specified, use the ${
|
|
40
|
+
`When searching for something (ie. a file to work with/in), unless more context is specified, use the ${grepFunctionName} function first, then fall back to functions: ${searchPackagesFunctionName}, ${searchFilesFunctionName}`,
|
|
40
41
|
`After finding a file to work with, assume the user's following question pertains to that file and use ${readFilesFunctionName} to read the file if needed`,
|
|
41
42
|
];
|
|
42
43
|
}
|
|
@@ -46,6 +47,7 @@ export class ConversationFsModule implements ConversationModule {
|
|
|
46
47
|
readFilesFunction(this),
|
|
47
48
|
writeFilesFunction(this),
|
|
48
49
|
getRecentlyAccessedFilePathsFunction(this),
|
|
50
|
+
grepFunction(this),
|
|
49
51
|
...fsFunctions,
|
|
50
52
|
];
|
|
51
53
|
}
|
|
@@ -61,6 +63,10 @@ export class ConversationFsModule implements ConversationModule {
|
|
|
61
63
|
getRecentlyAccessedFilePaths() {
|
|
62
64
|
return this.recentlyAccessedFilePaths;
|
|
63
65
|
}
|
|
66
|
+
|
|
67
|
+
getRepoPath(): string {
|
|
68
|
+
return this.repoPath;
|
|
69
|
+
}
|
|
64
70
|
}
|
|
65
71
|
|
|
66
72
|
export class ConversationFsModuleFactory implements ConversationModuleFactory {
|
|
@@ -1,6 +1,50 @@
|
|
|
1
1
|
import { File, Fs } from '@proteinjs/util-node';
|
|
2
2
|
import { Function } from '../../Function';
|
|
3
3
|
import { ConversationFsModule } from './ConversationFsModule';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
|
|
6
|
+
const toRepoAbs = (mod: ConversationFsModule, p: string) => (path.isAbsolute(p) ? p : path.join(mod.getRepoPath(), p));
|
|
7
|
+
|
|
8
|
+
// If path doesn’t exist, try to resolve "<repo>/<basename>" to the actual file under repo
|
|
9
|
+
async function canonicalizePaths(mod: ConversationFsModule, paths: string[]): Promise<string[]> {
|
|
10
|
+
const repo = mod.getRepoPath();
|
|
11
|
+
const ignore = ['**/node_modules/**', '**/dist/**', '**/.git/**'];
|
|
12
|
+
|
|
13
|
+
const out: string[] = [];
|
|
14
|
+
for (const p of paths) {
|
|
15
|
+
const abs = toRepoAbs(mod, p);
|
|
16
|
+
if (await Fs.exists(abs)) {
|
|
17
|
+
out.push(abs);
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
const base = path.basename(p);
|
|
21
|
+
if (!base) {
|
|
22
|
+
out.push(abs);
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
const parsed = path.parse(base); // { name, ext }
|
|
26
|
+
const pattern = parsed.ext ? `**/${parsed.name}${parsed.ext}` : `**/${parsed.name}.*`;
|
|
27
|
+
|
|
28
|
+
let matches: string[] = [];
|
|
29
|
+
try {
|
|
30
|
+
matches = await (Fs as any).getFilePathsMatchingGlob(repo, pattern, ignore);
|
|
31
|
+
} catch {
|
|
32
|
+
// fall through
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (matches.length === 1) {
|
|
36
|
+
out.push(matches[0]);
|
|
37
|
+
} else if (matches.length > 1) {
|
|
38
|
+
// Prefer the shortest match (usually “src/...” beats deeper/duplicate locations)
|
|
39
|
+
matches.sort((a, b) => a.length - b.length);
|
|
40
|
+
out.push(matches[0]);
|
|
41
|
+
} else {
|
|
42
|
+
// No luck; keep the original absolute (will throw with a clear error)
|
|
43
|
+
out.push(abs);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return out;
|
|
47
|
+
}
|
|
4
48
|
|
|
5
49
|
export const readFilesFunctionName = 'readFiles';
|
|
6
50
|
export function readFilesFunction(fsModule: ConversationFsModule) {
|
|
@@ -14,9 +58,7 @@ export function readFilesFunction(fsModule: ConversationFsModule) {
|
|
|
14
58
|
filePaths: {
|
|
15
59
|
type: 'array',
|
|
16
60
|
description: 'Paths to the files',
|
|
17
|
-
items: {
|
|
18
|
-
type: 'string',
|
|
19
|
-
},
|
|
61
|
+
items: { type: 'string' },
|
|
20
62
|
},
|
|
21
63
|
},
|
|
22
64
|
required: ['filePaths'],
|
|
@@ -24,7 +66,8 @@ export function readFilesFunction(fsModule: ConversationFsModule) {
|
|
|
24
66
|
},
|
|
25
67
|
call: async (params: { filePaths: string[] }) => {
|
|
26
68
|
fsModule.pushRecentlyAccessedFilePath(params.filePaths);
|
|
27
|
-
|
|
69
|
+
const absPaths = await canonicalizePaths(fsModule, params.filePaths);
|
|
70
|
+
return await Fs.readFiles(absPaths);
|
|
28
71
|
},
|
|
29
72
|
instructions: [`To read files from the local file system, use the ${readFilesFunctionName} function`],
|
|
30
73
|
};
|
|
@@ -41,19 +84,10 @@ export function writeFilesFunction(fsModule: ConversationFsModule) {
|
|
|
41
84
|
properties: {
|
|
42
85
|
files: {
|
|
43
86
|
type: 'array',
|
|
44
|
-
description: 'Files to write',
|
|
45
87
|
items: {
|
|
46
88
|
type: 'object',
|
|
47
|
-
properties: {
|
|
48
|
-
|
|
49
|
-
type: 'string',
|
|
50
|
-
description: 'the file path',
|
|
51
|
-
},
|
|
52
|
-
content: {
|
|
53
|
-
type: 'string',
|
|
54
|
-
description: 'the content to write to the file',
|
|
55
|
-
},
|
|
56
|
-
},
|
|
89
|
+
properties: { path: { type: 'string' }, content: { type: 'string' } },
|
|
90
|
+
required: ['path', 'content'],
|
|
57
91
|
},
|
|
58
92
|
},
|
|
59
93
|
},
|
|
@@ -61,8 +95,13 @@ export function writeFilesFunction(fsModule: ConversationFsModule) {
|
|
|
61
95
|
},
|
|
62
96
|
},
|
|
63
97
|
call: async (params: { files: File[] }) => {
|
|
64
|
-
fsModule.pushRecentlyAccessedFilePath(params.files.map((
|
|
65
|
-
|
|
98
|
+
fsModule.pushRecentlyAccessedFilePath(params.files.map((f) => f.path));
|
|
99
|
+
const canon = await canonicalizePaths(
|
|
100
|
+
fsModule,
|
|
101
|
+
params.files.map((f) => f.path)
|
|
102
|
+
);
|
|
103
|
+
const absFiles = params.files.map((f, i) => ({ ...f, path: canon[i] }));
|
|
104
|
+
return await Fs.writeFiles(absFiles);
|
|
66
105
|
},
|
|
67
106
|
instructions: [`To write files to the local file system, use the ${writeFilesFunctionName} function`],
|
|
68
107
|
};
|
|
@@ -232,6 +271,47 @@ const moveFunction: Function = {
|
|
|
232
271
|
instructions: [`To move a file or directory, use the ${moveFunctionName} function`],
|
|
233
272
|
};
|
|
234
273
|
|
|
274
|
+
export const grepFunctionName = 'grep';
|
|
275
|
+
export function grepFunction(fsModule: ConversationFsModule) {
|
|
276
|
+
return {
|
|
277
|
+
definition: {
|
|
278
|
+
name: grepFunctionName,
|
|
279
|
+
description:
|
|
280
|
+
"Run system grep recursively (-F literal) within the repository and return raw stdout/stderr/code. Excludes node_modules, dist, and .git. Use 'maxResults' to cap output.",
|
|
281
|
+
parameters: {
|
|
282
|
+
type: 'object',
|
|
283
|
+
properties: {
|
|
284
|
+
pattern: {
|
|
285
|
+
type: 'string',
|
|
286
|
+
description:
|
|
287
|
+
'Literal text to search for (grep -F). For parentheses or special characters, pass them as-is; no regex needed.',
|
|
288
|
+
},
|
|
289
|
+
dir: {
|
|
290
|
+
type: 'string',
|
|
291
|
+
description:
|
|
292
|
+
'Directory to search under. If relative, it is resolved against the repo root. Defaults to the repo root.',
|
|
293
|
+
},
|
|
294
|
+
maxResults: {
|
|
295
|
+
type: 'number',
|
|
296
|
+
description: 'Maximum number of matching lines to return (uses grep -m N).',
|
|
297
|
+
},
|
|
298
|
+
},
|
|
299
|
+
required: ['pattern'],
|
|
300
|
+
},
|
|
301
|
+
},
|
|
302
|
+
call: async (params: { pattern: string; dir?: string; maxResults?: number }) => {
|
|
303
|
+
const repo = fsModule.getRepoPath();
|
|
304
|
+
const cwd = params.dir ? toRepoAbs(fsModule, params.dir) : repo;
|
|
305
|
+
return await Fs.grep({ pattern: params.pattern, dir: cwd, maxResults: params.maxResults });
|
|
306
|
+
},
|
|
307
|
+
instructions: [
|
|
308
|
+
`Use ${grepFunctionName} to search for literal text across the repo.`,
|
|
309
|
+
`Prefer small 'maxResults' (e.g., 5-20) to avoid flooding the context.`,
|
|
310
|
+
`Parse the returned stdout yourself (format: "<path>:<line>:<text>") to pick files to read.`,
|
|
311
|
+
],
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
|
|
235
315
|
export const fsFunctions: Function[] = [
|
|
236
316
|
createFolderFunction,
|
|
237
317
|
fileOrDirectoryExistsFunction,
|
|
@@ -7,7 +7,8 @@ import { searchFilesFunction, searchFilesFunctionName } from './KeywordToFilesIn
|
|
|
7
7
|
|
|
8
8
|
export type KeywordToFilesIndexModuleParams = {
|
|
9
9
|
dir: string;
|
|
10
|
-
|
|
10
|
+
// Map from lowercase filename *stem* (no extension) → file paths
|
|
11
|
+
keywordFilesIndex: { [keyword: string]: string[] };
|
|
11
12
|
};
|
|
12
13
|
|
|
13
14
|
export class KeywordToFilesIndexModule implements ConversationModule {
|
|
@@ -22,15 +23,19 @@ export class KeywordToFilesIndexModule implements ConversationModule {
|
|
|
22
23
|
return 'Keyword to files index';
|
|
23
24
|
}
|
|
24
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Case-insensitive file name search that ignores extension.
|
|
28
|
+
*/
|
|
25
29
|
searchFiles(params: { keyword: string }) {
|
|
26
|
-
this.logger.
|
|
27
|
-
const
|
|
30
|
+
this.logger.debug({ message: `Searching for file, keyword: ${params.keyword}` });
|
|
31
|
+
const keywordLowerNoExtension = path.parse(params.keyword).name.toLowerCase();
|
|
32
|
+
const filePaths = this.params.keywordFilesIndex[keywordLowerNoExtension];
|
|
28
33
|
return filePaths || [];
|
|
29
34
|
}
|
|
30
35
|
|
|
31
36
|
getSystemMessages(): string[] {
|
|
32
37
|
return [
|
|
33
|
-
`If you're searching for something, use the ${searchFilesFunctionName} function to find a file matching the search string`,
|
|
38
|
+
`If you're searching for something, use the ${searchFilesFunctionName} function to find a file (by name) matching the search string`,
|
|
34
39
|
];
|
|
35
40
|
}
|
|
36
41
|
|
|
@@ -76,14 +81,14 @@ export class KeywordToFilesIndexModuleFactory implements ConversationModuleFacto
|
|
|
76
81
|
|
|
77
82
|
// Process each file path
|
|
78
83
|
for (const filePath of filePaths) {
|
|
79
|
-
const
|
|
84
|
+
const fileNameLower = path.parse(filePath).name.toLowerCase(); // Get file name without extension
|
|
80
85
|
|
|
81
|
-
if (!keywordFilesIndex[
|
|
82
|
-
keywordFilesIndex[
|
|
86
|
+
if (!keywordFilesIndex[fileNameLower]) {
|
|
87
|
+
keywordFilesIndex[fileNameLower] = [];
|
|
83
88
|
}
|
|
84
89
|
|
|
85
|
-
this.logger.debug({ message: `fileName: ${
|
|
86
|
-
keywordFilesIndex[
|
|
90
|
+
this.logger.debug({ message: `fileName: ${fileNameLower}, filePath: ${filePath}` });
|
|
91
|
+
keywordFilesIndex[fileNameLower].push(filePath);
|
|
87
92
|
}
|
|
88
93
|
|
|
89
94
|
return keywordFilesIndex;
|