Skip to content

Commit

Permalink
feat: add support for agentic plugin documentation (#5)
Browse files Browse the repository at this point in the history
  • Loading branch information
Ed-Marcavage authored Dec 31, 2024
1 parent 0b3e2a2 commit 4318caf
Show file tree
Hide file tree
Showing 8 changed files with 703 additions and 9 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/jsdoc-automation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ on:
root_directory:
description: 'Only scans files in this directory (relative to repository root, e.g., packages/core/src)'
required: true
default: 'packages/core/src/test_resources'
default: 'packages/plugin-near/'
type: string
excluded_directories:
description: 'Directories to exclude from scanning (comma-separated, relative to root_directory)'
Expand Down
1 change: 1 addition & 0 deletions packages/plugin-near/src/actions/swap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ async function checkStorageBalance(
}
}

// TODO: add functionality to support multiple networks
async function swapToken(
runtime: IAgentRuntime,
inputTokenId: string,
Expand Down
226 changes: 224 additions & 2 deletions scripts/jsdoc-automation/src/AIService.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ChatOpenAI } from "@langchain/openai";
import dotenv from 'dotenv';
import { ASTQueueItem, EnvUsage, OrganizedDocs, PluginDocumentation, TodoItem, TodoSection } from "./types/index.js";

dotenv.config();

Expand All @@ -11,7 +12,7 @@ export class AIService {

/**
* Constructor for initializing the ChatOpenAI instance.
*
*
* @throws {Error} If OPENAI_API_KEY environment variable is not set.
*/
constructor() {
Expand All @@ -36,9 +37,230 @@ export class AIService {
}
}

public async generatePluginDocumentation({
existingDocs,
packageJson,
readmeContent,
todoItems,
envUsages
}: {
existingDocs: ASTQueueItem[];
packageJson: any;
readmeContent?: string;
todoItems: TodoItem[];
envUsages: EnvUsage[];
}): Promise<PluginDocumentation & { todos: string }> {
const organizedDocs = this.organizeDocumentation(existingDocs);

const [overview, installation, configuration, usage, apiRef, troubleshooting, todoSection] = await Promise.all([
this.generateOverview(organizedDocs, packageJson),
this.generateInstallation(packageJson),
this.generateConfiguration(envUsages),
this.generateUsage(organizedDocs, packageJson),
this.generateApiReference(organizedDocs),
this.generateTroubleshooting(organizedDocs, packageJson),
this.generateTodoSection(todoItems)
]);

return {
overview,
installation,
configuration,
usage,
apiReference: apiRef,
troubleshooting,
todos: todoSection.todos
};
}

// should be moved to utils
private organizeDocumentation(docs: ASTQueueItem[]): OrganizedDocs {
return docs.reduce((acc: OrganizedDocs, doc) => {
// Use nodeType to determine the category
switch (doc.nodeType) {
case 'ClassDeclaration':
acc.classes.push(doc);
break;
case 'MethodDefinition':
case 'TSMethodSignature':
acc.methods.push(doc);
break;
case 'TSInterfaceDeclaration':
acc.interfaces.push(doc);
break;
case 'TSTypeAliasDeclaration':
acc.types.push(doc);
break;
}
return acc;
}, { classes: [], methods: [], interfaces: [], types: [] });
}

private async generateOverview(docs: OrganizedDocs, packageJson: any): Promise<string> {
const prompt = `Generate a comprehensive overview for a plugin/package based on the following information:
Package name: ${packageJson.name}
Package description: ${packageJson.description}
Main classes:
${docs.classes.map(c => `${c.name}: ${c.jsDoc}`).join('\n')}
Key interfaces:
${docs.interfaces.map(i => `${i.name}: ${i.jsDoc}`).join('\n')}
Generate a clear, concise overview that explains:
1. The purpose of this plugin
2. Its main features and capabilities
3. When and why someone would use it
4. Any key dependencies or requirements
Format the response in markdown.`;

return await this.generateComment(prompt);
}

private async generateInstallation(packageJson: any): Promise<string> {
const prompt = `Generate installation instructions for the following package:
Package name: ${packageJson.name}
Dependencies: ${JSON.stringify(packageJson.dependencies || {}, null, 2)}
Peer dependencies: ${JSON.stringify(packageJson.peerDependencies || {}, null, 2)}
Include:
1. Package manager commands - we are using pnpm
2. Any prerequisite installations
4. Verification steps to ensure successful installation
Format the response in markdown.`;

return await this.generateComment(prompt);
}

private async generateConfiguration(envUsages: EnvUsage[]): Promise<string> {
const prompt = `Generate configuration documentation based on these environment variable usages:
${envUsages.map(item => `
Environment Variable: ${item.code}
Full Context: ${item.fullContext}
`).join('\n')}
Create comprehensive configuration documentation that:
1. Lists all required environment variables
2. Explains the purpose of each variable
3. Provides example values where possible
Inform the user that the configuration is done in the .env file.
And to ensure the .env is set in the .gitignore file so it is not committed to the repository.
Format the response in markdown with proper headings and code blocks.`;

return await this.generateComment(prompt);
}

private async generateUsage(docs: OrganizedDocs, packageJson: any): Promise<string> {
const prompt = `Generate usage examples based on the following API documentation:
Classes:
${docs.classes.map(c => `${c.className}: ${c.jsDoc}`).join('\n')}
Methods:
${docs.methods.map(m => `${m.methodName}: ${m.jsDoc}`).join('\n')}
Create:
1. Basic usage example
2. Common use cases with Code snippets demonstrating key features
Format the response in markdown with code examples.`;

return await this.generateComment(prompt);
}

private async generateApiReference(docs: OrganizedDocs): Promise<string> {
const prompt = `Generate API reference documentation based on:
Classes:
${docs.classes.map(c => `${c.name}: ${c.jsDoc}`).join('\n')}
Methods:
${docs.methods.map(m => `${m.name}: ${m.jsDoc}`).join('\n')}
Interfaces:
${docs.interfaces.map(i => `${i.name}: ${i.jsDoc}`).join('\n')}
Types:
${docs.types.map(t => `${t.name}: ${t.jsDoc}`).join('\n')}
Create a comprehensive API reference including:
1. Class descriptions and methods
2. Method signatures and parameters
3. Return types and values
4. Interface definitions
5. Type definitions
6. Examples for complex APIs
Format the response in markdown with proper headings and code blocks.`;

return await this.generateComment(prompt);
}

/**
* Generates troubleshooting guide based on documentation and common patterns
*/
private async generateTroubleshooting(docs: OrganizedDocs, packageJson: any): Promise<string> {
const prompt = `Generate a troubleshooting guide based on:
Package dependencies: ${JSON.stringify(packageJson.dependencies || {}, null, 2)}
Error handling in methods:
${docs.methods
.filter(m => m.jsDoc?.toLowerCase().includes('error') || m.jsDoc?.toLowerCase().includes('throw'))
.map(m => `${m.methodName}: ${m.jsDoc}`)
.join('\n')}
Create a troubleshooting guide including:
1. Common issues and their solutions
2. Error messages and their meaning
3. Debugging tips
4. Configuration problems
5. Compatibility issues
6. Performance optimization
7. FAQ section
Format the response in markdown with clear headings and code examples where relevant.`;

return await this.generateComment(prompt);
}

/**
* Generates TODO section documentation from found TODO comments
*/
private async generateTodoSection(todoItems: TodoItem[]): Promise<TodoSection> {
if (todoItems.length === 0) {
return {
todos: "No TODOs found in the codebase.",
todoCount: 0
};
}

const prompt = `Generate a TODO section for documentation based on these TODO items:
${todoItems.map(item => `
TODO Comment: ${item.comment}
Code Context: ${item.fullContext}
`).join('\n')}
Create a section that:
1. Lists all TODOs in a clear, organized way
2. Groups related TODOs if any
3. Provides context about what needs to be done
4. Suggests priority based on the code context
5. Includes the file location for reference
Format the response in markdown with proper headings and code blocks.`;

const todos = await this.generateComment(prompt);
return {
todos,
todoCount: todoItems.length
};
}





/**
* Handle API errors by logging the error message and throwing the error.
*
*
* @param {Error} error The error object to handle
* @returns {void}
*/
Expand Down
48 changes: 43 additions & 5 deletions scripts/jsdoc-automation/src/DocumentationGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@ import { TypeScriptParser } from './TypeScriptParser.js';
import { JsDocAnalyzer } from './JsDocAnalyzer.js';
import { JsDocGenerator } from './JsDocGenerator.js';
import type { TSESTree } from '@typescript-eslint/types';
import { ASTQueueItem, FullModeFileChange, PrModeFileChange } from './types/index.js';
import { ASTQueueItem, EnvUsage, FullModeFileChange, PrModeFileChange, TodoItem } from './types/index.js';
import { GitManager } from './GitManager.js';
import fs from 'fs';
import { Configuration } from './Configuration.js';
import path from 'path';
import { AIService } from './AIService.js';
import { PluginDocumentationGenerator } from './PluginDocumentationGenerator.js';

/**
* Class representing a Documentation Generator.
Expand All @@ -19,8 +20,9 @@ export class DocumentationGenerator {
public existingJsDocQueue: ASTQueueItem[] = [];
private hasChanges: boolean = false;
private fileContents: Map<string, string> = new Map();
private branchName: string = '';
public branchName: string = '';
private fileOffsets: Map<string, number> = new Map();
private typeScriptFiles: string[] = [];

/**
* Constructor for initializing the object with necessary dependencies.
Expand All @@ -41,16 +43,18 @@ export class DocumentationGenerator {
public jsDocGenerator: JsDocGenerator,
public gitManager: GitManager,
public configuration: Configuration,
public aiService: AIService
) { }
public aiService: AIService,
) {
this.typeScriptFiles = this.directoryTraversal.traverse();
}

/**
* Asynchronously generates JSDoc comments for the TypeScript files based on the given pull request number or full mode.
*
* @param pullNumber - Optional. The pull request number to generate JSDoc comments for.
* @returns A promise that resolves once the JSDoc generation process is completed.
*/
public async generate(pullNumber?: number): Promise<void> {
public async generate(pullNumber?: number): Promise<{ documentedItems: ASTQueueItem[], branchName: string | undefined }> {
let fileChanges: PrModeFileChange[] | FullModeFileChange[] = [];
this.fileOffsets.clear();

Expand Down Expand Up @@ -137,6 +141,10 @@ export class DocumentationGenerator {
comment = await this.jsDocGenerator.generateComment(queueItem);
}
await this.updateFileWithJSDoc(queueItem.filePath, comment, queueItem.startLine);

queueItem.jsDoc = comment;
this.existingJsDocQueue.push(queueItem);

this.hasChanges = true;
}

Expand All @@ -162,6 +170,10 @@ export class DocumentationGenerator {
});
}
}
return {
documentedItems: this.existingJsDocQueue,
branchName: this.branchName
};
}

/**
Expand Down Expand Up @@ -316,4 +328,30 @@ export class DocumentationGenerator {
### 🤖 Generated by Documentation Bot
This is an automated PR created by the documentation generator tool.`;
}

/**
* Analyzes TODOs and environment variables in the code
*/
public async analyzeCodebase(): Promise<{ todoItems: TodoItem[], envUsages: EnvUsage[] }> {
const todoItems: TodoItem[] = [];
const envUsages: EnvUsage[] = [];

for (const filePath of this.typeScriptFiles) {
const ast = this.typeScriptParser.parse(filePath);
if (!ast) continue;

const sourceCode = fs.readFileSync(filePath, 'utf-8');

// Find TODOs
this.jsDocAnalyzer.findTodoComments(ast, ast.comments || [], sourceCode);
todoItems.push(...this.jsDocAnalyzer.todoItems);

// Find env usages
this.jsDocAnalyzer.findEnvUsages(ast, sourceCode);
envUsages.push(...this.jsDocAnalyzer.envUsages);
}

return { todoItems, envUsages };
}

}
Loading

0 comments on commit 4318caf

Please sign in to comment.