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.
- package/CHANGELOG.md +47 -0
- package/LICENSE +21 -0
- package/README.md +239 -0
- package/docs/DEPLOYMENT.md +142 -0
- package/docs/mcp-client-prompt.md +26 -0
- package/docs/reference.md +258 -0
- package/env.example +47 -0
- package/package.json +67 -0
- package/src/CodeSearch.js +125 -0
- package/src/DocsAgent.js +728 -0
- package/src/FileUtility.js +130 -0
- package/src/GitHubApi.js +337 -0
- package/src/LLM.js +463 -0
- package/src/UrlValidator.js +190 -0
- package/src/api.js +107 -0
- package/src/cli.js +0 -0
- package/src/config/principles.diataxis.js +28 -0
- package/src/config/principles.first.js +11 -0
- package/src/config/prompt.docs.vs.code.js +52 -0
- package/src/config/prompt.edit.disruptive.js +9 -0
- package/src/config/prompt.edit.js +40 -0
- package/src/config/prompt.extract.code.js +45 -0
- package/src/config/prompt.formatting.js +38 -0
- package/src/config/prompt.gen.referencedocs.js +181 -0
- package/src/config/prompt.linking.js +14 -0
- package/src/config/prompt.prioritize.js +24 -0
- package/src/config/prompt.relatedfiles.js +14 -0
- package/src/config/prompt.review.js +23 -0
- package/src/config/prompt.scoring.js +9 -0
- package/src/config/prompt.writing.js +13 -0
- package/src/config/rules.linking.js +10 -0
- package/src/index.js +49 -0
- package/src/lib.js +4 -0
- package/src/mcp.js +268 -0
package/src/DocsAgent.js
ADDED
|
@@ -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;
|