Skip to content

Commit 85c7f97

Browse files
feat: allow goose askai bot to search goose codebase (#7508)
Signed-off-by: The-Best-Codes <106822363+The-Best-Codes@users.noreply.github.com>
1 parent 8b79e81 commit 85c7f97

10 files changed

Lines changed: 540 additions & 31 deletions

File tree

services/ask-ai-bot/.dockerignore

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@
1010
# Include documentation (only docs subfolder needed)
1111
!documentation/docs/
1212

13+
# Include codebase source for code browsing
14+
!ui/
15+
!crates/
16+
1317
# Exclude unnecessary files from included directories
1418
services/ask-ai-bot/node_modules
1519
services/ask-ai-bot/dist
@@ -19,3 +23,6 @@ services/ask-ai-bot/.discraft
1923

2024
# Exclude large assets from docs that aren't needed for search
2125
documentation/docs/assets
26+
27+
# Exclude build artifacts from crates
28+
crates/**/target

services/ask-ai-bot/.env.example

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,6 @@ AI_MODEL=claude-sonnet-4-6
1313

1414
# Path to documentation directory (default: ./docs in Docker, for local dev use ../../documentation/docs)
1515
DOCS_PATH=../../documentation/docs
16+
17+
# Path to codebase root (default: ../.. relative to this service, /app/codebase in Docker)
18+
CODEBASE_PATH=../..

services/ask-ai-bot/Dockerfile

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ RUN bun run build
1616
FROM base AS production
1717
ENV NODE_ENV=production
1818
ENV DOCS_PATH=/app/docs
19+
ENV CODEBASE_PATH=/app/codebase
1920

2021
COPY --from=build /app/dist ./dist
2122
COPY --from=build /app/node_modules ./node_modules
@@ -24,6 +25,10 @@ COPY --from=build /app/package.json ./
2425
# Copy documentation (only docs/ subdirectory with markdown files)
2526
COPY documentation/docs ./docs
2627

28+
# Copy codebase source for code browsing
29+
COPY ui/ ./codebase/ui/
30+
COPY crates ./codebase/crates
31+
2732
# Empty index.ts for discraft start to detect
2833
RUN touch index.ts
2934

services/ask-ai-bot/utils/ai/index.ts

Lines changed: 46 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -56,21 +56,35 @@ export async function answerQuestion({
5656

5757
for await (const event of result.fullStream) {
5858
if (event.type === "tool-call") {
59-
if (event.toolName === "search_docs" && statusMessage) {
59+
if (statusMessage) {
6060
try {
61-
await statusMessage.edit("Searching the docs...");
62-
} catch (error) {
63-
logger.verbose("Failed to update status message:", error);
64-
}
65-
} else if (event.toolName === "view_docs" && statusMessage) {
66-
const input = event.input as { filePaths?: string | string[] };
67-
const filePaths = input.filePaths;
68-
const pathArray = Array.isArray(filePaths) ? filePaths : [filePaths];
69-
const pagesText = pathArray.length === 1 ? "page" : "pages";
70-
try {
71-
await statusMessage.edit(
72-
`Viewing ${pathArray.length} ${pagesText}...`,
73-
);
61+
if (event.toolName === "search_docs") {
62+
await statusMessage.edit("Searching the docs...");
63+
} else if (event.toolName === "view_docs") {
64+
const input = event.input as { filePaths?: string | string[] };
65+
const filePaths = input.filePaths;
66+
const pathArray = Array.isArray(filePaths)
67+
? filePaths
68+
: [filePaths];
69+
const pagesText = pathArray.length === 1 ? "page" : "pages";
70+
await statusMessage.edit(
71+
`Viewing ${pathArray.length} ${pagesText}...`,
72+
);
73+
} else if (event.toolName === "search_codebase") {
74+
await statusMessage.edit("Searching the codebase...");
75+
} else if (event.toolName === "view_codebase") {
76+
const input = event.input as { filePaths?: string | string[] };
77+
const filePaths = input.filePaths;
78+
const pathArray = Array.isArray(filePaths)
79+
? filePaths
80+
: [filePaths];
81+
const filesText = pathArray.length === 1 ? "file" : "files";
82+
await statusMessage.edit(
83+
`Reading ${pathArray.length} source ${filesText}...`,
84+
);
85+
} else if (event.toolName === "list_codebase_files") {
86+
await statusMessage.edit("Exploring project structure...");
87+
}
7488
} catch (error) {
7589
logger.verbose("Failed to update status message:", error);
7690
}
@@ -91,6 +105,24 @@ export async function answerQuestion({
91105
if (pathArray.length > 0) {
92106
tracker.recordViewCall(pathArray);
93107
}
108+
} else if (event.toolName === "search_codebase") {
109+
const resultText = String(event.output);
110+
const matchCount = (resultText.match(/\*\*[^*]+:\d+\*\*/g) || [])
111+
.length;
112+
tracker.recordCodeSearchCall(matchCount);
113+
} else if (event.toolName === "view_codebase") {
114+
const input = event.input as { filePaths?: string | string[] };
115+
const filePaths = input.filePaths;
116+
const pathArray = Array.isArray(filePaths)
117+
? filePaths
118+
: filePaths
119+
? [filePaths]
120+
: [];
121+
if (pathArray.length > 0) {
122+
tracker.recordCodeViewCall(pathArray);
123+
}
124+
} else if (event.toolName === "list_codebase_files") {
125+
tracker.recordListDir();
94126
}
95127
}
96128
}

services/ask-ai-bot/utils/ai/system-prompt.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,32 @@
11
import dedent from "dedent";
22

3-
export const MAX_STEPS = 10;
3+
export const MAX_STEPS = 15;
44

55
export function buildSystemPrompt(serverContext?: string): string {
66
let prompt = dedent`You are a helpful assistant in the goose Discord server.
77
Your role is to provide assistance and answer questions about codename goose, an open-source AI agent developed by Block. codename goose's website is \`https://block.github.io/goose\`. Your answers should be short and to the point. Always assume that a user's question is related to codename goose unless they specifically state otherwise. DO NOT capitalize "goose" or "codename goose".
88
99
You can perform a maximum of ${MAX_STEPS} steps (tool calls, text outputs, etc.). If you exceed this limit, no response will be provided to the user. BEFORE you reach the limit, STOP calling tools, respond to the user, and don't call any tools after your final response until the user asks another question.
1010
11-
When answering questions about goose:
11+
## Documentation tools
12+
When answering questions about how to use goose, configuration, setup, etc.:
1213
1. Use the \`search_docs\` tool to find relevant documentation
1314
2. Use the \`view_docs\` tool to read documentation (read multiple relevant files to get the full picture)
1415
3. Iterate on steps 1 and 2 (not necessarily in order) until you have a deep understanding of the question and relevant documentation
1516
4. Cite the documentation source in your response (using its Web URL)
1617
18+
## Codebase tools
19+
When answering questions about how goose works internally, its architecture, implementation details, or when users ask about specific code:
20+
1. Use \`search_codebase\` to grep for relevant code patterns (function names, struct names, error messages, etc.)
21+
2. Use \`list_codebase_files\` to explore the project structure and find relevant directories
22+
3. Use \`view_codebase\` to read the actual source code files
23+
4. The codebase is split into two main areas:
24+
- \`crates/\` - Rust backend code (core agent logic, CLI, server, MCP extensions)
25+
- \`ui/\` - Electron/TypeScript desktop application and other UIs
26+
5. Cite the source file in your response (using its GitHub URL)
27+
28+
You can combine documentation and codebase tools in a single response when needed. For example, if a user asks how a feature works, you might search the docs for usage instructions AND search the codebase for the implementation.
29+
1730
When providing links, wrap the URL in angle brackets (e.g., \`<https://example.com>\` or \`[Example](<https://example.com>)\`) to prevent excessive link previews. Do not use backtick characters around the URL.`;
1831

1932
if (serverContext) {
Lines changed: 47 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,72 @@
11
export class ToolTracker {
2-
private searchCalls: number = 0;
3-
private searchResults: Set<string> = new Set();
4-
private viewedPaths: Set<string> = new Set();
2+
private docSearchCalls: number = 0;
3+
private docSearchResults: Set<string> = new Set();
4+
private viewedDocPaths: Set<string> = new Set();
5+
private codeSearchCalls: number = 0;
6+
private codeSearchResults: number = 0;
7+
private viewedCodePaths: Set<string> = new Set();
8+
private listedDirs: number = 0;
59

610
recordSearchCall(results: string[]): void {
7-
this.searchCalls++;
8-
results.forEach((result) => this.searchResults.add(result));
11+
this.docSearchCalls++;
12+
results.forEach((result) => this.docSearchResults.add(result));
913
}
1014

1115
recordViewCall(filePaths: string | string[]): void {
1216
const paths = Array.isArray(filePaths) ? filePaths : [filePaths];
13-
paths.forEach((path) => this.viewedPaths.add(path));
17+
paths.forEach((path) => this.viewedDocPaths.add(path));
18+
}
19+
20+
recordCodeSearchCall(resultCount: number): void {
21+
this.codeSearchCalls++;
22+
this.codeSearchResults += resultCount;
23+
}
24+
25+
recordCodeViewCall(filePaths: string | string[]): void {
26+
const paths = Array.isArray(filePaths) ? filePaths : [filePaths];
27+
paths.forEach((path) => this.viewedCodePaths.add(path));
28+
}
29+
30+
recordListDir(): void {
31+
this.listedDirs++;
1432
}
1533

1634
getSummary(): string {
1735
const parts: string[] = [];
1836

19-
if (this.searchCalls > 0) {
20-
const resultCount = this.searchResults.size;
21-
const timesText = this.searchCalls === 1 ? "time" : "times";
37+
if (this.docSearchCalls > 0) {
38+
const resultCount = this.docSearchResults.size;
39+
const timesText = this.docSearchCalls === 1 ? "time" : "times";
2240
const resultsText = resultCount === 1 ? "result" : "results";
2341
parts.push(
24-
`searched ${this.searchCalls} ${timesText} with ${resultCount} ${resultsText}`,
42+
`searched docs ${this.docSearchCalls} ${timesText} with ${resultCount} ${resultsText}`,
2543
);
2644
}
2745

28-
if (this.viewedPaths.size > 0) {
29-
const pageCount = this.viewedPaths.size;
46+
if (this.viewedDocPaths.size > 0) {
47+
const pageCount = this.viewedDocPaths.size;
3048
const pagesText = pageCount === 1 ? "page" : "pages";
31-
parts.push(`viewed ${pageCount} ${pagesText}`);
49+
parts.push(`viewed ${pageCount} doc ${pagesText}`);
50+
}
51+
52+
if (this.codeSearchCalls > 0) {
53+
const timesText = this.codeSearchCalls === 1 ? "time" : "times";
54+
const matchText = this.codeSearchResults === 1 ? "match" : "matches";
55+
parts.push(
56+
`searched code ${this.codeSearchCalls} ${timesText} with ${this.codeSearchResults} ${matchText}`,
57+
);
58+
}
59+
60+
if (this.viewedCodePaths.size > 0) {
61+
const fileCount = this.viewedCodePaths.size;
62+
const filesText = fileCount === 1 ? "file" : "files";
63+
parts.push(`viewed ${fileCount} source ${filesText}`);
3264
}
3365

3466
if (parts.length === 0) return "";
3567

3668
const firstPart = parts[0].charAt(0).toUpperCase() + parts[0].slice(1);
37-
return parts.length === 1 ? firstPart : firstPart + ", " + parts[1];
69+
if (parts.length === 1) return firstPart;
70+
return firstPart + ", " + parts.slice(1).join(", ");
3871
}
3972
}

0 commit comments

Comments
 (0)