@proteinjs/conversation 2.1.3 → 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.
Files changed (31) hide show
  1. package/dist/index.d.ts +8 -1
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +8 -0
  4. package/dist/index.js.map +1 -1
  5. package/dist/src/Conversation.d.ts +36 -1
  6. package/dist/src/Conversation.d.ts.map +1 -1
  7. package/dist/src/Conversation.js +163 -5
  8. package/dist/src/Conversation.js.map +1 -1
  9. package/dist/src/OpenAi.d.ts +29 -0
  10. package/dist/src/OpenAi.d.ts.map +1 -1
  11. package/dist/src/OpenAi.js +77 -30
  12. package/dist/src/OpenAi.js.map +1 -1
  13. package/dist/src/fs/conversation_fs/ConversationFsModule.d.ts +1 -0
  14. package/dist/src/fs/conversation_fs/ConversationFsModule.d.ts.map +1 -1
  15. package/dist/src/fs/conversation_fs/ConversationFsModule.js +6 -2
  16. package/dist/src/fs/conversation_fs/ConversationFsModule.js.map +1 -1
  17. package/dist/src/fs/conversation_fs/FsFunctions.d.ts +36 -3
  18. package/dist/src/fs/conversation_fs/FsFunctions.d.ts.map +1 -1
  19. package/dist/src/fs/conversation_fs/FsFunctions.js +142 -20
  20. package/dist/src/fs/conversation_fs/FsFunctions.js.map +1 -1
  21. package/dist/src/fs/keyword_to_files_index/KeywordToFilesIndexModule.d.ts +4 -1
  22. package/dist/src/fs/keyword_to_files_index/KeywordToFilesIndexModule.d.ts.map +1 -1
  23. package/dist/src/fs/keyword_to_files_index/KeywordToFilesIndexModule.js +13 -9
  24. package/dist/src/fs/keyword_to_files_index/KeywordToFilesIndexModule.js.map +1 -1
  25. package/index.ts +10 -1
  26. package/package.json +4 -3
  27. package/src/Conversation.ts +213 -5
  28. package/src/OpenAi.ts +123 -13
  29. package/src/fs/conversation_fs/ConversationFsModule.ts +8 -2
  30. package/src/fs/conversation_fs/FsFunctions.ts +97 -17
  31. 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-3.5-turbo';
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({ model: model ?? this.model, stream: true, ...rest })) as Readable;
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(async (toolCall) => await this.callFunction(toolCall.function, toolCall.id, usageDataAccumulator))
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((f) => f.definition.name === functionCall.name);
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
- getRecentlyAccessedFilePathsFunctionName,
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 ${searchLibrariesFunctionName} function first, then fall back to functions: ${searchPackagesFunctionName}, ${searchFilesFunctionName}`,
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
- return await Fs.readFiles(params.filePaths);
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
- path: {
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((file) => file.path));
65
- return await Fs.writeFiles(params.files);
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
- keywordFilesIndex: { [keyword: string]: string[] /** file paths */ };
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.info({ message: `Searching for file, keyword: ${params.keyword}` });
27
- const filePaths = this.params.keywordFilesIndex[params.keyword];
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 fileName = path.parse(filePath).name; // Get file name without extension
84
+ const fileNameLower = path.parse(filePath).name.toLowerCase(); // Get file name without extension
80
85
 
81
- if (!keywordFilesIndex[fileName]) {
82
- keywordFilesIndex[fileName] = [];
86
+ if (!keywordFilesIndex[fileNameLower]) {
87
+ keywordFilesIndex[fileNameLower] = [];
83
88
  }
84
89
 
85
- this.logger.debug({ message: `fileName: ${fileName}, filePath: ${filePath}` });
86
- keywordFilesIndex[fileName].push(filePath);
90
+ this.logger.debug({ message: `fileName: ${fileNameLower}, filePath: ${filePath}` });
91
+ keywordFilesIndex[fileNameLower].push(filePath);
87
92
  }
88
93
 
89
94
  return keywordFilesIndex;