Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions services/ask-ai-bot/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,8 @@ AI_MODEL=claude-sonnet-4-6
# Path to documentation directory (default: ./docs in Docker, for local dev use ../../documentation/docs)
DOCS_PATH=../../documentation/docs

# GitHub personal access token (optional, for higher API rate limits when reading issues)
GITHUB_TOKEN=ghp_1234

Comment on lines +17 to +19

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is optional and it works perfectly fine without it

# Path to codebase root (default: ../.. relative to this service, /app/codebase in Docker)
CODEBASE_PATH=../..
33 changes: 33 additions & 0 deletions services/ask-ai-bot/bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion services/ask-ai-bot/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "goose-ask-ai-bot",
"private": true,
"version": "1.0.3",
"version": "1.0.4",
"description": "Bot created with discraft",
"module": "index.ts",
"type": "module",
Expand All @@ -16,6 +16,7 @@
},
"dependencies": {
"@ai-sdk/anthropic": "^4.0.10",
"@octokit/rest": "^22.0.1",
"ai": "^7.0.18",
"consola": "^3.4.2",
"dedent": "^1.7.2",
Expand Down
8 changes: 8 additions & 0 deletions services/ask-ai-bot/utils/ai/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,10 @@ export async function answerQuestion({
);
} else if (event.toolName === "list_codebase_files") {
await statusMessage.edit("Exploring project structure...");
} else if (event.toolName === "search_github") {
await statusMessage.edit("Searching GitHub...");
} else if (event.toolName === "get_github_issue_or_pr") {
await statusMessage.edit("Reading GitHub issues and PRs...");
}
} catch (error) {
logger.verbose("Failed to update status message:", error);
Expand Down Expand Up @@ -123,6 +127,10 @@ export async function answerQuestion({
}
} else if (event.toolName === "list_codebase_files") {
tracker.recordListDir();
} else if (event.toolName === "search_github") {
tracker.recordGitHubSearch();
} else if (event.toolName === "get_github_issue_or_pr") {
tracker.recordGitHubRead();
}
}
}
Expand Down
10 changes: 8 additions & 2 deletions services/ask-ai-bot/utils/ai/system-prompt.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import dedent from "dedent";

export const MAX_STEPS = 25;
export const MAX_STEPS = 35;

export function buildSystemPrompt(serverContext?: string): string {
let prompt = dedent`You are a helpful assistant in the goose Discord server.
Expand All @@ -22,7 +22,13 @@ When answering questions about how goose works internally, its architecture, imp
3. Use \`view_codebase\` to read the actual source code files
4. Cite the source file in your response (using its GitHub URL)

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.
## GitHub tools
When answering questions about specific issues, bug reports, feature requests, or the development history of goose:
1. Use \`search_github\` to find relevant issues and PRs - you can use GitHub qualifiers (e.g., \`label:bug\`, \`is:pr\`, \`author:username\`) and sort by recency (\`sort: "updated"\`) or other criteria
2. Use \`get_github_issue_or_pr\` to read the full description and comments of a specific issue or PR
3. Cite the issue URL in your response

You can combine documentation, codebase, and GitHub 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, search the codebase for the implementation, and read related GitHub issues or PRs. Be thorough!

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.`;

Expand Down
25 changes: 25 additions & 0 deletions services/ask-ai-bot/utils/ai/tool-tracker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ export class ToolTracker {
private codeSearchResults: number = 0;
private viewedCodePaths: Set<string> = new Set();
private listedDirs: number = 0;
private gitHubSearchCalls: number = 0;
private gitHubReadCalls: number = 0;

recordSearchCall(results: string[]): void {
this.docSearchCalls++;
Expand All @@ -31,6 +33,14 @@ export class ToolTracker {
this.listedDirs++;
}

recordGitHubSearch(): void {
this.gitHubSearchCalls++;
}

recordGitHubRead(): void {
this.gitHubReadCalls++;
}

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

Expand Down Expand Up @@ -63,6 +73,21 @@ export class ToolTracker {
parts.push(`viewed ${fileCount} source ${filesText}`);
}

if (this.listedDirs > 0) {
const timesText = this.listedDirs === 1 ? "time" : "times";
parts.push(`listed directories ${this.listedDirs} ${timesText}`);
}

if (this.gitHubSearchCalls > 0) {
const timesText = this.gitHubSearchCalls === 1 ? "time" : "times";
parts.push(`searched GitHub ${this.gitHubSearchCalls} ${timesText}`);
}

if (this.gitHubReadCalls > 0) {
const itemText = this.gitHubReadCalls === 1 ? "item" : "items";
parts.push(`read ${this.gitHubReadCalls} GitHub ${itemText}`);
}

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

const firstPart = parts[0].charAt(0).toUpperCase() + parts[0].slice(1);
Expand Down
132 changes: 132 additions & 0 deletions services/ask-ai-bot/utils/ai/tools/github.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import { Octokit } from "@octokit/rest";

const REPO_OWNER = "aaif-goose";
const REPO_NAME = "goose";

let octokit: Octokit | null = null;

function getOctokit(): Octokit {
if (!octokit) {
octokit = new Octokit({
auth: process.env.GITHUB_TOKEN,
});
}
return octokit;
}

export interface GitHubItem {
number: number;
title: string;
state: string;
isMerged: boolean;
author: string;
createdAt: string;
updatedAt: string;
labels: string[];
body: string;
comments: number;
url: string;
}

export interface GitHubComment {
author: string;
createdAt: string;
body: string;
}

export async function searchGitHub(
query: string,
options: {
sort?: "created" | "updated" | "comments";
order?: "asc" | "desc";
state?: "open" | "closed" | "all";
limit?: number;
} = {},
): Promise<GitHubItem[]> {
const { sort, order = "desc", state = "all", limit = 10 } = options;
const api = getOctokit();

const sanitized = query.replace(/\b(?:repo|org|user):\S+/gi, "").trim();
const q = `repo:${REPO_OWNER}/${REPO_NAME} ${sanitized}${state !== "all" ? ` state:${state}` : ""}`;

const response = await api.rest.search.issuesAndPullRequests({
q,
...(sort ? { sort, order } : {}),
per_page: limit,
});

return response.data.items.map((item: any) => ({
number: item.number,
title: item.title,
state: item.state,
isMerged: !!item.pull_request?.merged_at,
author: item.user?.login ?? "unknown",
createdAt: item.created_at,
updatedAt: item.updated_at,
labels: item.labels.map((l: any) =>
typeof l === "string" ? l : (l.name ?? ""),
),
body: item.body ?? "",
comments: item.comments,
url: item.html_url,
}));
}

export async function getGitHubItem(number: number): Promise<GitHubItem> {
const api = getOctokit();

const response = await api.rest.issues.get({
owner: REPO_OWNER,
repo: REPO_NAME,
issue_number: number,
});

const item: any = response.data;
return {
number: item.number,
title: item.title,
state: item.state,
isMerged: !!item.pull_request?.merged_at,
Comment thread
The-Best-Codes marked this conversation as resolved.
author: item.user?.login ?? "unknown",
createdAt: item.created_at,
updatedAt: item.updated_at,
labels: item.labels.map((l: any) =>
typeof l === "string" ? l : (l.name ?? ""),
),
body: item.body ?? "",
comments: item.comments,
url: item.html_url,
};
}

export async function getGitHubItemComments(
number: number,
limit: number = 30,
): Promise<GitHubComment[]> {
const api = getOctokit();
const perPage = Math.min(limit, 100);
const comments: GitHubComment[] = [];

for (let page = 1; comments.length < limit; page++) {
const response = await api.rest.issues.listComments({
owner: REPO_OWNER,
repo: REPO_NAME,
issue_number: number,
per_page: perPage,
page,
Comment thread
The-Best-Codes marked this conversation as resolved.
});

if (response.data.length === 0) break;

for (const comment of response.data) {
comments.push({
author: comment.user?.login ?? "unknown",
createdAt: comment.created_at,
body: comment.body ?? "",
});
if (comments.length >= limit) break;
}
}

return comments;
}
Loading
Loading