docs-agent 1.1.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.
@@ -0,0 +1,728 @@
1
+ import ReviewPrompt from './config/prompt.review.js';
2
+ import PrioritizePrompt from './config/prompt.prioritize.js';
3
+ import EditPrompt from './config/prompt.edit.js';
4
+ import DisruptiveEditPrompt from './config/prompt.edit.disruptive.js';
5
+ import LinkingPrompt from './config/prompt.linking.js';
6
+ import GenerateReferenceDocsPrompt from './config/prompt.gen.referencedocs.js';
7
+ import DocsVsCodePrompt from './config/prompt.docs.vs.code.js';
8
+ import ExtractCodeAndSymbolsPrompt from './config/prompt.extract.code.js';
9
+ import LLM from './LLM.js';
10
+ import * as FileUtility from './FileUtility.js';
11
+ import CodeSearch from './CodeSearch.js';
12
+ import { z } from "zod";
13
+
14
+ class DocsAgent {
15
+ constructor(accessMode = 'mcp') {
16
+ this.name = 'Docs Agent';
17
+ this.accessMode = accessMode;
18
+ }
19
+
20
+ /**
21
+ * Plan and execute the docs improvement process for the given docs page content
22
+ * @param {string} content - The content of the docs page to improve
23
+ * @param {object} context - The context of the docs page
24
+ * @param {string} context.filepath - The absolute filepath of the docs page to improve
25
+ * @param {string} context.filename - The filename of the docs page
26
+ * @param {string} context.projectStructure - The project structure as folders/files hierarchy
27
+ * @param {string} customInstructions - Custom instructions for the docs improvement process
28
+ * @param {string} context.glossaryFile - The filepath of the glossary file, containing a list of project-specific terms and their definitions, used for consistent and technically accurate documentation
29
+ * @param {string[]} context.relatedFiles - The filepaths of the docs pages that are related to the current page being reviewed/edited
30
+ * @param {boolean} isFileWriteAllowed - Whether to write the edited content to the file system
31
+ * @returns {Promise<string>} - The edited docs content
32
+ */
33
+ async reviewPrioritizeAndEdit(content, context={}, customInstructions, isFileWriteAllowed){
34
+ console.log("Starting the docs improvement process now");
35
+ if(!content){
36
+ if(!context?.filepath){
37
+ throw new Error("Content or filepath is required");
38
+ }
39
+ content = await FileUtility.readFile(context.filepath, this.accessMode);
40
+ }
41
+ console.log("Received content to review, prioritize and edit");
42
+ console.log(context?.filename, context?.projectStructure, context?.glossaryFile, context?.relatedFiles);
43
+ if(!context?.filename){
44
+ context.filename = "docs.md";
45
+ }
46
+ if(context?.glossaryFile){
47
+ context.glossary = await FileUtility.readFile(context.glossaryFile, this.accessMode);
48
+ }
49
+ if(context?.relatedFiles){
50
+ // TODO: Remove duplicates
51
+ context.relatedFilesContent = await Promise.all(context.relatedFiles.map(async (file) => {
52
+ return FileUtility.readFile(file, this.accessMode);
53
+ }));
54
+ }
55
+ // Save the original content
56
+ if(isFileWriteAllowed){
57
+ await FileUtility.writeFile(path.join(process.cwd(), "public", "original", context.filename), content, this.accessMode);
58
+ }
59
+ // Review the content
60
+ const review = await this.review(content, context, customInstructions);
61
+
62
+ // Prioritize the editing tasks
63
+ const prioritize = await this.prioritize(content, review, context, customInstructions);
64
+
65
+ // Edit the content as per the prioritized tasks
66
+ const editedContent = await this.edit(content, prioritize, context, customInstructions);
67
+
68
+ // Save the edited content
69
+ if(context.filename && isFileWriteAllowed){
70
+ await FileUtility.writeFile(path.join(process.cwd(), "public", context.filename), editedContent, this.accessMode);
71
+ }
72
+ return editedContent;
73
+ }
74
+
75
+ /**
76
+ * Review the documentation and suggest improvements.
77
+ * @param {string} content - The documentation to review.
78
+ * @param {string} context - The context of the documentation.
79
+ * @param {string} customInstructions - Custom instructions for the docs improvement process
80
+ * @returns {Promise<string>} - The review of the documentation.
81
+ */
82
+ async review(content, context, customInstructions) {
83
+ console.log("Reviewing the docs", content?.slice(0, 300)+"...");
84
+ if(!content && context?.filepath){
85
+ content = await FileUtility.readFile(context.filepath, this.accessMode);
86
+ }
87
+ if(!content){
88
+ throw new Error("Content is required");
89
+ }
90
+ const llm = new LLM({
91
+ aiService: process.env.REVIEW_AI_SERVICE || "gemini",
92
+ model: process.env.REVIEW_AI_MODEL || "gemini-2.5-pro-exp-03-25"
93
+ });
94
+ let userMessage = "Content:\n"+content;
95
+ if(context?.projectStructure){
96
+ userMessage = userMessage + "\n\n\n\n\n\nProject Structure:\n"+context.projectStructure;
97
+ }
98
+ if(context?.filepath){
99
+ userMessage = userMessage + "\n\n\n\n\n\nPath of the file being reviewed (the content is already provided): \n"+context.filepath + " [Ensure to mention the relative path (from the root of the github repository project only) to this file in the output (the review result)]";
100
+ }
101
+ const review = await llm.chat([
102
+ {
103
+ role: "system",
104
+ content: ReviewPrompt + "\n\n\n\n\n\nCustom Instructions:\n" + customInstructions
105
+ },
106
+ {
107
+ role: "user",
108
+ content: [
109
+ {
110
+ type: "text",
111
+ text: userMessage
112
+ }
113
+ ]
114
+ }], null, {
115
+ isEnabled: true,
116
+ functionId: "reviewDocs",
117
+ metadata: {
118
+ customInstructions: customInstructions,
119
+ }
120
+ });
121
+ console.log("Review:\n"+review?.slice(0, 600)+"...");
122
+ return review;
123
+ }
124
+
125
+ /**
126
+ * Break down review into a documentation editing tasks and prioritize them based on their impact vs disruption..
127
+ * @param {string} review - The review of the documentation.
128
+ * @param {string} content - The documentation to review.
129
+ * @param {string} context - The context of the documentation.
130
+ * @param {string} customInstructions - Custom instructions for the docs improvement process
131
+ * @returns {Promise<string>} - The prioritized documentation editing plan.
132
+ */
133
+ async prioritize(content, review, context = {}, customInstructions) {
134
+ console.log("Prioritizing the docs improvement plan", review);
135
+ if(!content && context?.filepath){
136
+ content = await FileUtility.readFile(context.filepath, this.accessMode);
137
+ }
138
+ if(!content || !review){
139
+ throw new Error("Content and review are required");
140
+ }
141
+ const llm = new LLM({
142
+ aiService: "anthropic",
143
+ model: "claude-3-5-sonnet-20240620",
144
+ temperature: 0,
145
+ topP: 0.95
146
+ });
147
+ // System prompt
148
+ const ALLOW_DISRUPTIVE_CHANGES = (process.env.ALLOW_DISRUPTIVE_CHANGES === "true") || context?.allowDisruptiveChanges;
149
+ let systemPrompt = PrioritizePrompt;
150
+ systemPrompt += `\n\nRestructuring the project files/folder structure is ${ALLOW_DISRUPTIVE_CHANGES ? "allowed" : "NOT ALLOWED"}.`;
151
+ if(customInstructions){
152
+ systemPrompt += "\n\n\n\n\n\nCustom Instructions:\n" + customInstructions;
153
+ }
154
+ // User message
155
+ let userMessage = "Content:\n"+content;
156
+ if(context?.projectStructure){
157
+ userMessage = userMessage + "\n\n\n\n\n\nProject Structure:\n"+context.projectStructure;
158
+ }
159
+ if(review){
160
+ userMessage = userMessage + "\n\n\n\n\n\nContent Review By The Documentation Expert:\n"+review;
161
+ }
162
+ const prioritized = await llm.chat([
163
+ {
164
+ role: "system",
165
+ content: systemPrompt
166
+ },
167
+ {
168
+ role: "user",
169
+ content: [
170
+ {
171
+ type: "text",
172
+ text: userMessage
173
+ }
174
+ ]
175
+ }
176
+ ], null, {
177
+ isEnabled: true,
178
+ functionId: "prioritizeDocsEditTasks",
179
+ metadata: {
180
+ customInstructions: customInstructions,
181
+ }
182
+ });
183
+ console.log(prioritized);
184
+ return prioritized;
185
+ }
186
+
187
+ /**
188
+ * Edit the documentation.
189
+ * @param {string} content - The documentation to edit.
190
+ * @param {string} editPlan - The edit plan. Specific changes to be made to the documentation.
191
+ * @param {string} context - The context of the documentation.
192
+ * @param {string} context.filepath - The absolute filepath of the docs page to edit
193
+ * @param {string} context.filename - The filename of the docs page
194
+ * @param {string} context.projectStructure - The project structure as folders/files hierarchy
195
+ * @param {string} context.glossaryFile - The filepath of the glossary file, containing a list of project-specific terms and their definitions, used for consistent and technically accurate documentation
196
+ * @param {string[]} context.relatedFiles - The filepaths of the docs pages that are related to the current page being reviewed/edited
197
+ * @param {string} customInstructions - Custom instructions for the docs editing process
198
+ * @returns {Promise<string>} - The edited documentation.
199
+ */
200
+ async edit(content, editPlan, context = {}, customInstructions) {
201
+ console.log("Editing the docs according to the edit plan", editPlan);
202
+ if(!editPlan){
203
+ throw new Error("Edit plan is required");
204
+ }
205
+ if(!content && context?.filepath){
206
+ content = await FileUtility.readFile(context.filepath, this.accessMode);
207
+ }
208
+ if(!content){
209
+ throw new Error("Content is required");
210
+ }
211
+ const llm = new LLM({
212
+ aiService: "anthropic",
213
+ model: "claude-3-5-sonnet-20240620",
214
+ temperature: 0,
215
+ topP: 0.95,
216
+ maxInputTokens: 50000
217
+ });
218
+ // System prompt
219
+ const ALLOW_DISRUPTIVE_CHANGES = (process.env.ALLOW_DISRUPTIVE_CHANGES === "true") || context?.allowDisruptiveChanges;
220
+ let systemPrompt = ALLOW_DISRUPTIVE_CHANGES ? DisruptiveEditPrompt : EditPrompt;
221
+ if(customInstructions){
222
+ systemPrompt = systemPrompt + "\n\n\n\n\n\nCustom Instructions:\n" + customInstructions;
223
+ }
224
+ // User message
225
+ let userMessage = "Content:\n"+content;
226
+ if(context?.projectStructure){
227
+ userMessage = userMessage + "\n\n\n\n\n\nProject Structure:\n"+context.projectStructure;
228
+ }
229
+ if(editPlan){
230
+ userMessage = userMessage + "\n\n\n\n\n\nEdit Plan:\n"+editPlan;
231
+ }
232
+ // Assistant reassurance
233
+ let assistantReassurancePrompt = "I will edit the documentation strictly following the given principles. I will not add any other text or comments other than the main file content. I won't add the text 'Here's the edited version...' either. I will only return the main file content.";
234
+ // Optional context
235
+ let tokenEstimate = llm.estimateTokens(userMessage);
236
+ if(tokenEstimate < llm.maxInputTokens){
237
+ if(!context?.glossary && context?.glossaryFile){
238
+ try{
239
+ context.glossary = await FileUtility.readFile(context.glossaryFile, this.accessMode);
240
+ } catch(error){
241
+ console.error("Error reading glossary file", context.glossaryFile, error);
242
+ }
243
+ }
244
+ if(context?.glossary){
245
+ tokenEstimate += llm.estimateTokens(context.glossary);
246
+ if(tokenEstimate < llm.maxInputTokens){
247
+ assistantReassurancePrompt += "\n\nI will use following glossary to make the writing more consistent and professional:\n" + context.glossary;
248
+ }
249
+ }
250
+ if(!context?.relatedFilesContent && context?.relatedFiles){
251
+ context.relatedFilesContent = "";
252
+ for(const file of context.relatedFiles){
253
+ try{
254
+ const fileContent = await FileUtility.readFile(file, this.accessMode);
255
+ if(fileContent){
256
+ context.relatedFilesContent += "// "+file+"\n\n"+fileContent+"\n\n\n\n\n\n";
257
+ }
258
+ } catch(error){
259
+ console.error("Error reading related file", file, error);
260
+ }
261
+ }
262
+ }
263
+ if(context?.relatedFilesContent){
264
+ tokenEstimate += llm.estimateTokens(context.relatedFilesContent);
265
+ if(tokenEstimate < llm.maxInputTokens){
266
+ assistantReassurancePrompt += "\n\nI will use following related files content for proper context and linking:\n" + context.relatedFilesContent;
267
+ }
268
+ }
269
+ }
270
+ const edited = await llm.chat([
271
+ {
272
+ role: "system",
273
+ content: systemPrompt
274
+ },
275
+ {
276
+ role: "assistant",
277
+ content: assistantReassurancePrompt
278
+ },
279
+ {
280
+ role: "user",
281
+ content: [
282
+ {
283
+ type: "text",
284
+ text: userMessage
285
+ }
286
+ ]
287
+ }
288
+ ], null, {
289
+ isEnabled: true,
290
+ functionId: "editDocs",
291
+ metadata: {
292
+ customInstructions: customInstructions,
293
+ editPlan: editPlan,
294
+ allowDisruptiveChanges: ALLOW_DISRUPTIVE_CHANGES
295
+ }
296
+ });
297
+ return edited;
298
+ }
299
+
300
+ /**
301
+ * Improves internal linking of the docs page to the related files and glossary concepts.
302
+ * @param {string} content - The content of the docs page to linkify
303
+ * @param {object} context - The context of the docs page
304
+ * @param {string} context.filepath - The absolute filepath of the docs page to linkify
305
+ * @param {string} context.filename - The filename of the docs page
306
+ * @param {string} context.projectStructure - The project structure as folders/files hierarchy
307
+ * @param {string} customInstructions - Custom instructions for the docs improvement process
308
+ * @param {string} context.glossaryFile - The filepath of the glossary file, containing a list of project-specific terms and their definitions, used for consistent and technically accurate documentation
309
+ * @param {string[]} context.relatedFiles - The filepaths of the docs pages that are related to the current page being reviewed/edited
310
+ * @param {string[]} context.referenceSourceCodeFiles - The remote URLs of the reference source code files for which the docs are being generated. Use this to verify and generate technically accurate documentation
311
+ * @returns {Promise<string>} - The edited docs content with improved internal linking
312
+ */
313
+ async linkify(content, context, customInstructions){
314
+ console.log("Linkifying the docs", content);
315
+ if(!content && context?.filepath){
316
+ content = await FileUtility.readFile(context.filepath, this.accessMode);
317
+ }
318
+ const llm = new LLM({
319
+ aiService: "anthropic",
320
+ model: "claude-3-5-sonnet-20240620",
321
+ temperature: 0,
322
+ topP: 0.95,
323
+ maxInputTokens: 80000
324
+ });
325
+ // System prompt
326
+ let systemPrompt = LinkingPrompt;
327
+ if(customInstructions){
328
+ systemPrompt = systemPrompt + "\n\n\n\n\n\nCustom Instructions:\n" + customInstructions;
329
+ }
330
+ // User message
331
+ let userMessage = "Content:\n"+content;
332
+ if(context?.projectStructure){
333
+ userMessage = userMessage + "\n\n\n\n\n\nProject Structure:\n"+context.projectStructure;
334
+ }
335
+ if(context?.relatedFilesContent){
336
+ if(context?.relatedFiles){
337
+ userMessage = userMessage + "\n\n\n\n\n\nRelated Files:\n"+context.relatedFiles.join("\n");
338
+ }
339
+ if(context?.relatedFilesContent){
340
+ userMessage = userMessage + "\n\n\n\n\n\nRelated Files Content:\n"+context.relatedFilesContent.join("\n\n");
341
+ }
342
+ }
343
+ if(context?.referenceSourceCodeFiles){
344
+ let referenceSourceCodeContent = "\n\n\n\n\n\nReference Source Code:\n";
345
+ for(const fileUrl of context.referenceSourceCodeFiles){
346
+ const fileContent = await FileUtility.readFile(fileUrl, this.accessMode);
347
+ userMessage += "// "+fileUrl+"\n\n"+fileContent+"\n\n\n\n\n\n";
348
+ }
349
+ userMessage += referenceSourceCodeContent;
350
+ }
351
+ if(context?.glossary){
352
+ if(context?.glossaryFile){
353
+ userMessage = userMessage + "\n\n\n\n\n\nGlossary File:\n"+context.glossaryFile;
354
+ }
355
+ if(context?.glossary){
356
+ userMessage = userMessage + "\n\n\n\n\n\nGlossary:\n"+context.glossary;
357
+ }
358
+ }
359
+ // Assistant reassurance
360
+ let assistantReassurancePrompt = "I will improve internal linking of the documentation strictly following the given principles. I will not add any other text or comments in the final output. I won't add the text 'Here's the edited content...' in the final output. I will only return the edited file content.";
361
+ const contentWithProperLinking = await llm.chat([
362
+ {
363
+ role: "system",
364
+ content: systemPrompt
365
+ },
366
+ {
367
+ role: "assistant",
368
+ content: assistantReassurancePrompt
369
+ },
370
+ {
371
+ role: "user",
372
+ content: [
373
+ {
374
+ type: "text",
375
+ text: userMessage
376
+ }
377
+ ]
378
+ }
379
+ ], null, {
380
+ isEnabled: true,
381
+ functionId: "linkifyDocs",
382
+ metadata: {
383
+ customInstructions: customInstructions,
384
+ }
385
+ });
386
+ return contentWithProperLinking;
387
+ }
388
+
389
+ /**
390
+ * Extract code and related symbols from docs
391
+ * @param {string} content - The content of the docs page to extract code and symbols from
392
+ * @param {object} context - The context of the docs page
393
+ * @param {string} customInstructions - Custom instructions to extract code and symbols
394
+ * @returns {Promise<{symbols: string[], codeBlocks: string[]}>} - The extracted code and related symbols
395
+ */
396
+ async extractCodeAndSymbols(content, context, customInstructions){
397
+ console.log("Extracting code and symbols from the docs", content);
398
+ const llm = new LLM({
399
+ aiService: "anthropic",
400
+ model: "claude-3-5-sonnet-20240620",
401
+ temperature: 0,
402
+ topP: 0.95,
403
+ maxInputTokens: 30000
404
+ });
405
+ let systemPrompt = ExtractCodeAndSymbolsPrompt;
406
+ if(customInstructions){
407
+ systemPrompt = systemPrompt + "\n\n\n\n\n\nCustom Instructions:\n" + customInstructions;
408
+ }
409
+ // User message
410
+ let userMessage = "Docs Page Content:\n"+content;
411
+ const { symbols, codeBlocks } = await llm.chat([
412
+ {
413
+ role: "system",
414
+ content: systemPrompt
415
+ },
416
+ {
417
+ role: "assistant",
418
+ content: "I will extract the code and related symbols from the docs page content. I will return the code and related symbols in a JSON format. I will not add any other text or comments in the final output. I won't add the text 'Here's the extracted code and symbols...' in the final output. I will only return the JSON object."
419
+ },
420
+ {
421
+ role: "user",
422
+ content: [
423
+ {
424
+ type: "text",
425
+ text: userMessage
426
+ }
427
+ ]
428
+ }
429
+ ], {
430
+ responseFormat: "json",
431
+ schema: z.object({
432
+ symbols: z.array(z.string()).describe("The symbols extracted from the docs page content"),
433
+ codeBlocks: z.array(z.string()).describe("The code blocks extracted from the docs page content")
434
+ })
435
+ }, {
436
+ isEnabled: true,
437
+ functionId: "extractCodeAndSymbols",
438
+ metadata: {
439
+ customInstructions: customInstructions,
440
+ }
441
+ });
442
+ console.log("Extracted "+symbols?.length+" symbols and "+codeBlocks?.length+" code blocks");
443
+ console.log("Symbols:", "\n "+symbols?.join("\n "));
444
+ console.log("Code Blocks:", "\n "+codeBlocks?.join("\n "));
445
+ return { symbols, codeBlocks };
446
+ }
447
+
448
+ /**
449
+ * Find the reference source code files for the given docs page content
450
+ * @param {string} content
451
+ * @param {object} context
452
+ * @param {string} context.repoUrl - GitHub repository URL
453
+ * @param {string} customInstructions - Custom instructions to extract symbols
454
+ * @returns {Promise<string>} - The edited docs content with improved internal linking
455
+ */
456
+ async findReferenceSourceCodeFiles(content, context, customInstructions){
457
+ // Extract symbols and code blocks from the docs page
458
+ let symbols = [];
459
+ let codeBlocks = [];
460
+
461
+ try {
462
+ const extractionResult = await this.extractCodeAndSymbols(content, context, customInstructions);
463
+ symbols = extractionResult.symbols || [];
464
+ codeBlocks = extractionResult.codeBlocks || [];
465
+ } catch (error) {
466
+ console.error('Failed to extract code and symbols:', error);
467
+ console.log('Continuing with empty symbols and code blocks due to extraction failure');
468
+ }
469
+
470
+ // Search for the symbols in the reference codebase
471
+ const codeSearch = new CodeSearch({
472
+ searchSpace: "github",
473
+ repository: context.repoUrl,
474
+ fetchContent: false,
475
+ maxResults: 5,
476
+ scoreThreshold: 0
477
+ });
478
+ // Sanitize search results
479
+ let referenceSourceCodeFilesMap = new Map();
480
+ const setOfSymbols = new Set(symbols);
481
+ let notFoundSymbols = new Set();
482
+ let foundSymbols = new Set();
483
+ for(const symbol of setOfSymbols){
484
+ const searchResults = await codeSearch.execute(symbol);
485
+ for(const result of searchResults){
486
+ let mapItemValue = referenceSourceCodeFilesMap.get(result.path) || {};
487
+ mapItemValue.results = result.results;
488
+ mapItemValue.symbols = [...(mapItemValue.symbols || []), symbol];
489
+ mapItemValue.totalScore = (mapItemValue.totalScore || 0) + result.score;
490
+ mapItemValue.path = result.path;
491
+ mapItemValue.filepath = result.filepath;
492
+ referenceSourceCodeFilesMap.set(result.path, mapItemValue);
493
+ foundSymbols.add(symbol);
494
+ }
495
+ if(!foundSymbols.has(symbol)){
496
+ notFoundSymbols.add(symbol);
497
+ }
498
+ }
499
+ // Sort reference files by aggregated score
500
+ const sortedReferenceSourceCodeFiles = Array.from(referenceSourceCodeFilesMap.values()).sort((a, b) => b.totalScore - a.totalScore);
501
+ console.log("Sorted "+sortedReferenceSourceCodeFiles?.length+" reference source code files:", "\n "+sortedReferenceSourceCodeFiles?.map(result => (result.filepath || result.path) + " (Score: " + result.totalScore + ")").join("\n "));
502
+ // Draft a summary
503
+ let summary = ""; // Initialize as empty string
504
+ if(notFoundSymbols.size > 0){
505
+ summary += "\n\n"+notFoundSymbols.size+" symbols not found in the codebase: "+Array.from(notFoundSymbols).join(", ");
506
+ }
507
+ if(foundSymbols.size > 0){
508
+ summary += "\n\n"+foundSymbols.size+" symbols found in the codebase: "+Array.from(foundSymbols).join(", ");
509
+ if(sortedReferenceSourceCodeFiles?.length > 0){
510
+ summary += "\n\nReference source code files where these symbols are found:\n " +
511
+ sortedReferenceSourceCodeFiles?.map(result => {
512
+ const symbolsList = result.symbols ? result.symbols.join(", ") : "";
513
+ return (result.filepath || result.path) + " (Score: " + result.totalScore + "; Symbols: " + symbolsList + ")";
514
+ }).join("\n ");
515
+ }
516
+ } else {
517
+ console.error("No symbols found in the codebase. Check if the repo url is correct and the repository is accessible.");
518
+ }
519
+ // TODO: Code blocks check (critical in case the docs does not have any symbols)
520
+ console.log("Reference File Search Summary:\n", summary);
521
+ return {
522
+ files: sortedReferenceSourceCodeFiles?.map(result => result.filepath || result.path),
523
+ fileScoreMap: referenceSourceCodeFilesMap,
524
+ summary
525
+ };
526
+ }
527
+
528
+ /**
529
+ * Verify if the docs are accurate and up to date with the source code
530
+ * @param {string} docsContentFilePath - The absolute filepath of the docs page to audit
531
+ * @param {object} context - The context of the docs page
532
+ * @param {string} context.repoUrl - GitHub repository URL
533
+ * @param {string} context.projectStructure - A nested markdown list of the project structure showing the folders/files hierarchy of the repository
534
+ * @param {string} customInstructions - Custom instructions for the audit process
535
+ * @returns {Promise<string>} - The audit report
536
+ */
537
+ async auditDocsAgainstCode(docsContentFilePath, context, customInstructions){
538
+ console.log("Comparing the docs with the source code to find the incorrect information", docsContentFilePath);
539
+ if(!docsContentFilePath){
540
+ throw new Error("docsContentFilePath is required");
541
+ }
542
+ // Docs content
543
+ const docsContent = await FileUtility.readFile(docsContentFilePath, this.accessMode);
544
+ if(!docsContent){
545
+ throw new Error("Couldn't read the docs content from the file: "+docsContentFilePath);
546
+ }
547
+ const { files: referenceSourceCodeFiles, fileScoreMap: referenceSourceCodeFilesMap, summary: referenceFilesSummary } = await this.findReferenceSourceCodeFiles(docsContent, context, customInstructions);
548
+ const llm = new LLM({
549
+ aiService: process.env.REVIEW_AI_SERVICE || "gemini",
550
+ model: process.env.REVIEW_AI_MODEL || "gemini-2.5-pro-preview-06-05"
551
+ });
552
+ // System prompt
553
+ let systemPrompt = DocsVsCodePrompt;
554
+ if(customInstructions){
555
+ systemPrompt = systemPrompt + "\n\n\n\n\n\nCustom Instructions:\n" + customInstructions;
556
+ }
557
+ // User message
558
+ let userMessage = "Documentation Content:\n"+docsContent;
559
+ // Optional context
560
+ let tokenEstimate = llm.estimateTokens(userMessage);
561
+ let consideredReferenceSourceCodeFiles = [];
562
+ if(referenceSourceCodeFiles?.length > 0){
563
+ userMessage += "\n\n\n\n\n\nReference Source Code:\n"+referenceFilesSummary+"\n\n";
564
+ for(const fileUrl of referenceSourceCodeFiles){
565
+ const fileContent = await FileUtility.readFile(fileUrl, this.accessMode);
566
+ tokenEstimate += llm.estimateTokens(fileContent);
567
+ if(tokenEstimate < llm.maxInputTokens){
568
+ consideredReferenceSourceCodeFiles.push(fileUrl);
569
+ userMessage += "// "+fileUrl+"\n\n"+fileContent+"\n\n\n\n\n\n";
570
+ }
571
+ }
572
+ }
573
+ if(context?.projectStructure){
574
+ tokenEstimate += llm.estimateTokens(context.projectStructure);
575
+ if(tokenEstimate < llm.maxInputTokens){
576
+ userMessage = userMessage + "\n\n\n\n\n\nProject Structure:\n"+context.projectStructure;
577
+ }
578
+ }
579
+ if(context?.relatedFileUrls){
580
+ // TODO: Remove duplicates
581
+ const relatedFilesContent = await Promise.all(context.relatedFileUrls.map(async (fileUrl) => {
582
+ return FileUtility.readFile(fileUrl, this.accessMode);
583
+ }));
584
+ tokenEstimate += llm.estimateTokens(relatedFilesContent.join("\n\n"));
585
+ if(tokenEstimate < llm.maxInputTokens){
586
+ userMessage = userMessage + "\n\n\n\n\n\nRelated Files Content:\n"+relatedFilesContent.join("\n\n");
587
+ }
588
+ }
589
+ if(context?.knowledgeBase){
590
+ tokenEstimate += llm.estimateTokens(context.knowledgeBase);
591
+ if(tokenEstimate < llm.maxInputTokens){
592
+ userMessage = userMessage + "\n\n\n\n\n\nKnowledge Base (Use this only for understanding. Do not include this in the reference docs, this is already published on the docs website.):\n"+context.knowledgeBase;
593
+ }
594
+ }
595
+ console.log("Auditing docs against code using", llm.model, "with temperature", llm.temperature, "and topP", llm.topP);
596
+ console.log("Total tokens estimated:", tokenEstimate);
597
+ console.log("Considered reference source code files:", consideredReferenceSourceCodeFiles?.length);
598
+ console.log(" ", consideredReferenceSourceCodeFiles?.map(fileUrl => fileUrl).join("\n "));
599
+ if(!referenceSourceCodeFiles?.length){
600
+ return "Couldn't audit the docs against code because the reference source code files couldn't be found. Please make sure the repository URL is correct and the repository is accessible.";
601
+ } else if(!consideredReferenceSourceCodeFiles?.length){
602
+ return "Couldn't audit the docs against code because the reference source code files are too large to process. Please specify which specific code or class or configuration you want to audit.";
603
+ }
604
+ const auditReport = await llm.chat([
605
+ {
606
+ role: "system",
607
+ content: systemPrompt
608
+ },
609
+ {
610
+ role: "user",
611
+ content: [
612
+ {
613
+ type: "text",
614
+ text: userMessage
615
+ }
616
+ ]
617
+ }
618
+ ], null, {
619
+ isEnabled: true,
620
+ functionId: "auditDocsAgainstCode",
621
+ metadata: {
622
+ docsContentFilePath: docsContentFilePath,
623
+ referenceSourceCodeFiles: consideredReferenceSourceCodeFiles,
624
+ repoUrl: context?.repoUrl,
625
+ customInstructions: customInstructions,
626
+ }
627
+ });
628
+ console.log("Audit Report:", auditReport);
629
+
630
+ // Add validation to ensure we always return a string
631
+ if (!auditReport || typeof auditReport !== 'string') {
632
+ console.error('LLM returned invalid response:', auditReport);
633
+ return "Error: Could not generate audit report. Please try again.";
634
+ }
635
+
636
+ return auditReport + (auditReport?.length > 200 ? "\n\nNext Step: You may call editDocs tool to edit the docs to fix the issues found in the audit report" : "");
637
+ }
638
+
639
+ /**
640
+ * Generate reference docs for the given public interface and config files or GitHub repository
641
+ * @experimental This tool is experimental and might be dropped in the future.
642
+ * @param {string[]} referenceSourceCodeFiles - Remote URLs of the necessary source code files to generate reference docs e.g. [\"https://raw.githubusercontent.com/rudderlabs/analytics-go/refs/heads/master/analytics.go\", \"https://raw.githubusercontent.com/rudderlabs/analytics-go/refs/heads/master/config.go\"]
643
+ * @param {object} context - The context of the reference docs generation
644
+ * @param {string} context.interfaceType - Type of reference docs, how the code exposes its interfaces to the outside world
645
+ * @param {string} context.projectStructure - A nested markdown list of the project structure showing the folders/files hierarchy of the repository
646
+ * @param {string[]} context.relatedFileUrls - Remote URLs of the code files that are related to the given main public interface file
647
+ * @param {string} context.repoUrl - GitHub repository URL
648
+ * @param {string} context.knowledgeBase - Knowledge base such as higher level architecture of the parent project, this is only to develop understanding of the context of the current codebase. Do not include this in the reference docs, this is already published on docs website
649
+ * @param {string} customInstructions - Custom instructions for the reference docs generation process
650
+ * @returns {Promise<string>} - The generated reference docs
651
+ */
652
+ async generateReferenceDocs(referenceSourceCodeFiles, context, customInstructions){
653
+ console.log("Generating the docs", referenceSourceCodeFiles);
654
+ if(!referenceSourceCodeFiles?.length){
655
+ throw new Error("referenceSourceCodeFiles is required");
656
+ }
657
+ const llm = new LLM({
658
+ aiService: process.env.REVIEW_AI_SERVICE || "gemini",
659
+ model: process.env.REVIEW_AI_MODEL || "gemini-2.5-pro-preview-06-05"
660
+ });
661
+ // System prompt
662
+ let systemPrompt = GenerateReferenceDocsPrompt;
663
+ if(customInstructions){
664
+ systemPrompt = systemPrompt + "\n\n\n\n\n\nCustom Instructions:\n" + customInstructions;
665
+ }
666
+ let referenceSourceCodeFilesContent = "";
667
+ for(const fileUrl of referenceSourceCodeFiles){
668
+ const fileContent = await FileUtility.readFile(fileUrl, this.accessMode);
669
+ referenceSourceCodeFilesContent += "// "+fileUrl+"\n\n"+fileContent+"\n\n\n\n\n\n";
670
+ }
671
+ // User message
672
+ let userMessage = "Reference Source Code Files:\n"+referenceSourceCodeFilesContent;
673
+ if(context?.projectStructure){
674
+ userMessage = userMessage + "\n\n\n\n\n\nProject Structure:\n"+context.projectStructure;
675
+ }
676
+ // Optional context
677
+ let tokenEstimate = llm.estimateTokens(userMessage);
678
+ if(context?.relatedFileUrls){
679
+ // TODO: Remove duplicates
680
+ const relatedFilesContent = await Promise.all(context.relatedFileUrls.map(async (fileUrl) => {
681
+ return FileUtility.readFile(fileUrl, this.accessMode);
682
+ }));
683
+ tokenEstimate += llm.estimateTokens(relatedFilesContent.join("\n\n"));
684
+ if(tokenEstimate < llm.maxInputTokens){
685
+ userMessage = userMessage + "\n\n\n\n\n\nRelated Files Content:\n"+relatedFilesContent.join("\n\n");
686
+ }
687
+ }
688
+ if(context?.knowledgeBase){
689
+ tokenEstimate += llm.estimateTokens(context.knowledgeBase);
690
+ if(tokenEstimate < llm.maxInputTokens){
691
+ userMessage = userMessage + "\n\n\n\n\n\nKnowledge Base (Use this only for understanding. Do not include this in the reference docs, this is already published on the docs website.):\n"+context.knowledgeBase;
692
+ }
693
+ }
694
+ if(context?.repoUrl){
695
+ userMessage = userMessage + "\n\n\n\n\n\nSource Code Repository URL:\n"+context.repoUrl;
696
+ }
697
+ if(context?.interfaceType){
698
+ userMessage = userMessage + "\n\n\n\n\n\nInterface Type:\n"+context.interfaceType;
699
+ }
700
+ const generated = await llm.chat([
701
+ {
702
+ role: "system",
703
+ content: systemPrompt
704
+ },
705
+ {
706
+ role: "user",
707
+ content: [
708
+ {
709
+ type: "text",
710
+ text: userMessage
711
+ }
712
+ ]
713
+ }
714
+ ], null, {
715
+ isEnabled: true,
716
+ functionId: "generateReferenceDocs",
717
+ metadata: {
718
+ referenceSourceCodeFiles: referenceSourceCodeFiles,
719
+ repoUrl: context?.repoUrl,
720
+ interfaceType: context?.interfaceType,
721
+ customInstructions: customInstructions,
722
+ }
723
+ });
724
+ return generated;
725
+ }
726
+ }
727
+
728
+ export default DocsAgent;