feat: ask ai discord bot (#6842)

Signed-off-by: The-Best-Codes <bestcodes.official@gmail.com>
This commit is contained in:
BestCodes
2026-02-02 16:11:28 -06:00
committed by GitHub
parent fafda07dd0
commit 849cc60fbc
23 changed files with 1262 additions and 0 deletions
@@ -0,0 +1,158 @@
import fs from "fs";
import MiniSearch from "minisearch";
import path from "path";
import { logger } from "../../logger";
export interface SearchResult {
filePath: string;
fileName: string;
score: number;
preview: string;
lineCount: number;
webUrl: string;
}
interface DocFile {
id: string;
path: string;
fileName: string;
content: string;
lineCount: number;
}
let miniSearch: MiniSearch<DocFile> | null = null;
function getDocsDir(): string {
return process.env.DOCS_PATH || path.join(process.cwd(), "docs");
}
function initializeSearch(): MiniSearch<DocFile> {
if (miniSearch) {
return miniSearch;
}
const docsDir = path.resolve(getDocsDir());
if (!fs.existsSync(docsDir)) {
logger.warn(`Docs directory not found at ${docsDir}`);
miniSearch = new MiniSearch({
fields: ["content", "fileName", "path"],
storeFields: ["path", "fileName", "content", "lineCount"],
});
return miniSearch;
}
const docs: DocFile[] = [];
function walkDir(dir: string) {
try {
const files = fs.readdirSync(dir);
for (const file of files) {
const filePath = path.join(dir, file);
const stat = fs.statSync(filePath);
if (stat.isDirectory()) {
if (file === "assets" || file === "docker") {
continue;
}
walkDir(filePath);
} else {
try {
const content = fs.readFileSync(filePath, "utf-8");
const relativePath = path.relative(docsDir, filePath);
const docFile: DocFile = {
id: relativePath,
path: relativePath,
fileName: file,
content,
lineCount: content.split("\n").length,
};
docs.push(docFile);
} catch (error) {
logger.error(`Error reading file ${filePath}:`, error);
}
}
}
} catch (error) {
logger.error(`Error walking directory ${dir}:`, error);
}
}
walkDir(docsDir);
miniSearch = new MiniSearch({
fields: ["content", "fileName", "path"],
storeFields: ["path", "fileName", "content", "lineCount"],
});
miniSearch.addAll(docs);
logger.verbose(`Loaded ${docs.length} documentation files`);
return miniSearch;
}
function generateWebUrl(filePath: string): string {
const baseUrl = "https://block.github.io/goose/docs";
// Remove file extension for the URL path
const urlPath = filePath.replace(/\.[^/.]+$/, "");
return `${baseUrl}/${urlPath}`;
}
function getPreview(content: string, maxLength: number = 200): string {
const withoutFrontmatter = content.replace(/^---[\s\S]*?---\n/, "");
const lines = withoutFrontmatter.split("\n");
let preview = "";
for (const line of lines) {
const cleanLine = line
.replace(/^#+\s+/, "")
.replace(/\[([^\]]+)\]\([^\)]+\)/g, "$1")
.replace(/[*_]/g, "")
.trim();
if (
cleanLine &&
!cleanLine.startsWith("import") &&
!cleanLine.startsWith("export")
) {
preview = cleanLine;
break;
}
}
if (preview.length > maxLength) {
preview = preview.substring(0, maxLength) + "...";
}
return preview || "(No preview available)";
}
export function searchDocs(query: string, limit: number = 5): SearchResult[] {
const search = initializeSearch();
const results = search.search(query).slice(0, limit);
if (results.length === 0) {
logger.verbose(`Search for "${query}" returned no results`);
return [];
}
const searchResults: SearchResult[] = results.map((result) => ({
filePath: result.path,
fileName: result.fileName,
score: result.score,
preview: getPreview(result.content),
lineCount: result.lineCount,
webUrl: generateWebUrl(result.path),
}));
logger.verbose(
`Search for "${query}" returned ${searchResults.length} results`,
);
return searchResults;
}
export function reloadDocsCache(): void {
miniSearch = null;
initializeSearch();
}
@@ -0,0 +1,119 @@
import fs from "fs";
import path from "path";
function getDocsDir(): string {
return process.env.DOCS_PATH || path.join(process.cwd(), "docs");
}
function generateWebUrl(filePath: string): string {
const baseUrl = "https://block.github.io/goose/docs";
// Remove file extension for the URL path
const urlPath = filePath.replace(/\.[^/.]+$/, "");
return `${baseUrl}/${urlPath}`;
}
function findDocFile(partialPath: string): string | null {
const docsDir = getDocsDir();
if (!fs.existsSync(docsDir)) {
return null;
}
const searchTerm = partialPath.toLowerCase().replace(/\.md$/, "");
let foundPath: string | null = null;
function walkDir(dir: string) {
if (foundPath) return;
const files = fs.readdirSync(dir);
for (const file of files) {
const filePath = path.join(dir, file);
const stat = fs.statSync(filePath);
if (stat.isDirectory()) {
if (file === "assets" || file === "docker") {
continue;
}
walkDir(filePath);
} else {
const relativePath = path.relative(docsDir, filePath);
if (relativePath.toLowerCase().includes(searchTerm)) {
foundPath = relativePath;
return;
}
}
}
}
walkDir(docsDir);
return foundPath;
}
function getDocChunk(
filePath: string,
startLine: number = 0,
lineCount: number = 100,
): { fileName: string; content: string; webUrl: string } {
const docsDir = path.resolve(getDocsDir());
const fullPath = path.join(docsDir, filePath);
const normalizedPath = path.resolve(fullPath);
if (!normalizedPath.startsWith(docsDir)) {
throw new Error("Invalid file path - directory traversal not allowed");
}
if (!fs.existsSync(fullPath)) {
throw new Error(`Documentation file not found: ${filePath}`);
}
try {
const content = fs.readFileSync(fullPath, "utf-8");
const lines = content.split("\n");
const actualStartLine = Math.max(0, Math.min(startLine, lines.length - 1));
const actualEndLine = Math.min(actualStartLine + lineCount, lines.length);
const chunkLines = lines.slice(actualStartLine, actualEndLine);
const chunkContent = chunkLines.join("\n");
const fileName = path.basename(fullPath);
return {
content: chunkContent,
fileName,
webUrl: generateWebUrl(filePath),
};
} catch (error) {
if (error instanceof Error && error.message.includes("ENOENT")) {
throw new Error(`Documentation file not found: ${filePath}`);
}
throw error;
}
}
export function viewDocs(
filePaths: string | string[],
startLine: number = 0,
lineCount: number = 100,
): string {
const paths = Array.isArray(filePaths) ? filePaths : [filePaths];
const docs = paths.map((filePath) => {
let resolvedPath = filePath;
// Check if file has an extension; if not, search for it
if (!path.extname(filePath)) {
const found = findDocFile(filePath);
if (found) {
resolvedPath = found;
}
}
return getDocChunk(resolvedPath, startLine, lineCount);
});
return docs
.map(
(doc) =>
`**${doc.fileName}**\nWeb URL: <${doc.webUrl}>\n\`\`\`\n${doc.content}\n\`\`\``,
)
.join("\n\n---\n\n");
}
@@ -0,0 +1,70 @@
import { tool } from "ai";
import { z } from "zod";
import { logger } from "../../logger";
import { searchDocs } from "./docs-search";
import { viewDocs } from "./docs-viewer";
export const aiTools = {
search_docs: tool({
description: "Search the goose documentation for relevant information",
inputSchema: z.object({
query: z
.string()
.describe(
"Search query for the documentation (example: 'sessions', 'tool management')",
),
limit: z
.number()
.optional()
.describe("Maximum number of results to return (default 5)"),
}),
execute: async ({ query, limit = 5 }) => {
const results = searchDocs(query, limit);
logger.verbose(
`Searched docs for "${query}", found ${results.length} results`,
);
if (results.length === 0) {
return "No relevant documentation found for your query. Try different keywords.";
}
return results
.map(
(r) =>
`**${r.fileName}** (${r.filePath})\nPreview: ${r.preview}\nWeb URL: <${r.webUrl}>`,
)
.join("\n\n");
},
}),
view_docs: tool({
description: "View documentation file(s)",
inputSchema: z.object({
filePaths: z
.union([z.string(), z.array(z.string())])
.describe(
"Path or array of paths to documentation files (example: 'quickstart.md' or ['guides/managing-projects.md', 'api/overview.md'])",
),
startLine: z
.number()
.optional()
.describe("Starting line number (0-indexed, default 0)"),
lineCount: z
.number()
.optional()
.describe("Number of lines to show (default 100)"),
}),
execute: async ({ filePaths, startLine = 0, lineCount = 100 }) => {
try {
const result = viewDocs(filePaths, startLine, lineCount);
const count = Array.isArray(filePaths) ? filePaths.length : 1;
logger.verbose(`Viewed ${count} documentation file(s)`);
return result;
} catch (error) {
const errorMsg =
error instanceof Error ? error.message : "Unknown error";
logger.error(`Error viewing docs: ${errorMsg}`);
return `Error viewing documentation: ${errorMsg}`;
}
},
}),
};