feat: allow askai bot to read issues and PRs (#10398)

Signed-off-by: BestCodes <106822363+The-Best-Codes@users.noreply.github.com>
This commit is contained in:
BestCodes
2026-07-11 16:07:17 -07:00
committed by GitHub
parent 3c1fdd692c
commit 858e8de359
8 changed files with 350 additions and 3 deletions
+3
View File
@@ -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
# Path to codebase root (default: ../.. relative to this service, /app/codebase in Docker)
CODEBASE_PATH=../..
+33
View File
@@ -6,6 +6,7 @@
"name": "discraft-bot",
"dependencies": {
"@ai-sdk/anthropic": "^4.0.10",
"@octokit/rest": "^22.0.1",
"ai": "^7.0.18",
"consola": "^3.4.2",
"dedent": "^1.7.2",
@@ -102,6 +103,30 @@
"@isaacs/brace-expansion": ["@isaacs/brace-expansion@5.0.0", "", { "dependencies": { "@isaacs/balanced-match": "^4.0.1" } }, "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA=="],
"@octokit/auth-token": ["@octokit/auth-token@6.0.0", "", {}, "sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w=="],
"@octokit/core": ["@octokit/core@7.0.6", "", { "dependencies": { "@octokit/auth-token": "^6.0.0", "@octokit/graphql": "^9.0.3", "@octokit/request": "^10.0.6", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", "before-after-hook": "^4.0.0", "universal-user-agent": "^7.0.0" } }, "sha512-DhGl4xMVFGVIyMwswXeyzdL4uXD5OGILGX5N8Y+f6W7LhC1Ze2poSNrkF/fedpVDHEEZ+PHFW0vL14I+mm8K3Q=="],
"@octokit/endpoint": ["@octokit/endpoint@11.0.3", "", { "dependencies": { "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-FWFlNxghg4HrXkD3ifYbS/IdL/mDHjh9QcsNyhQjN8dplUoZbejsdpmuqdA76nxj2xoWPs7p8uX2SNr9rYu0Ag=="],
"@octokit/graphql": ["@octokit/graphql@9.0.3", "", { "dependencies": { "@octokit/request": "^10.0.6", "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.0" } }, "sha512-grAEuupr/C1rALFnXTv6ZQhFuL1D8G5y8CN04RgrO4FIPMrtm+mcZzFG7dcBm+nq+1ppNixu+Jd78aeJOYxlGA=="],
"@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="],
"@octokit/plugin-paginate-rest": ["@octokit/plugin-paginate-rest@14.0.0", "", { "dependencies": { "@octokit/types": "^16.0.0" }, "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-fNVRE7ufJiAA3XUrha2omTA39M6IXIc6GIZLvlbsm8QOQCYvpq/LkMNGyFlB1d8hTDzsAXa3OKtybdMAYsV/fw=="],
"@octokit/plugin-request-log": ["@octokit/plugin-request-log@6.0.0", "", { "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-UkOzeEN3W91/eBq9sPZNQ7sUBvYCqYbrrD8gTbBuGtHEuycE4/awMXcYvx6sVYo7LypPhmQwwpUe4Yyu4QZN5Q=="],
"@octokit/plugin-rest-endpoint-methods": ["@octokit/plugin-rest-endpoint-methods@17.0.0", "", { "dependencies": { "@octokit/types": "^16.0.0" }, "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-B5yCyIlOJFPqUUeiD0cnBJwWJO8lkJs5d8+ze9QDP6SvfiXSz1BF+91+0MeI1d2yxgOhU/O+CvtiZ9jSkHhFAw=="],
"@octokit/request": ["@octokit/request@10.0.11", "", { "dependencies": { "@octokit/endpoint": "^11.0.3", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", "content-type": "^2.0.0", "json-with-bigint": "^3.5.3", "universal-user-agent": "^7.0.2" } }, "sha512-+s7HUxjfFqOMS9VlIwDffq0MikjSAK0gSpG73W+meAvVAvX4MBrHYTK5Bj3Uot55qFT4gzUtfzE4mGWY4Br8/Q=="],
"@octokit/request-error": ["@octokit/request-error@7.1.0", "", { "dependencies": { "@octokit/types": "^16.0.0" } }, "sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw=="],
"@octokit/rest": ["@octokit/rest@22.0.1", "", { "dependencies": { "@octokit/core": "^7.0.6", "@octokit/plugin-paginate-rest": "^14.0.0", "@octokit/plugin-request-log": "^6.0.0", "@octokit/plugin-rest-endpoint-methods": "^17.0.0" } }, "sha512-Jzbhzl3CEexhnivb1iQ0KJ7s5vvjMWcmRtq5aUsKmKDrRW6z3r84ngmiFKFvpZjpiU/9/S6ITPFRpn5s/3uQJw=="],
"@octokit/types": ["@octokit/types@16.0.0", "", { "dependencies": { "@octokit/openapi-types": "^27.0.0" } }, "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg=="],
"@sapphire/async-queue": ["@sapphire/async-queue@1.5.5", "", {}, "sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg=="],
"@sapphire/shapeshift": ["@sapphire/shapeshift@4.0.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "lodash": "^4.17.21" } }, "sha512-d9dUmWVA7MMiKobL3VpLF8P2aeanRTu6ypG2OIaEv/ZHH/SUQ2iHOVyi5wAPjQ+HmnMuL0whK9ez8I/raWbtIg=="],
@@ -162,12 +187,16 @@
"ai": ["ai@7.0.18", "", { "dependencies": { "@ai-sdk/gateway": "4.0.14", "@ai-sdk/provider": "4.0.2", "@ai-sdk/provider-utils": "5.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-/DsiRfbLPtoHf9C47Hy+LK+HkjNowX1DQo3id4zWegfU6z1WjVDRAHYJAiX7SXhGp0mWX4tcenUhfIFessDWnw=="],
"before-after-hook": ["before-after-hook@4.0.0", "", {}, "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ=="],
"chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="],
"commander": ["commander@14.0.2", "", {}, "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ=="],
"consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="],
"content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="],
"dedent": ["dedent@1.7.2", "", { "peerDependencies": { "babel-plugin-macros": "^3.1.0" }, "optionalPeers": ["babel-plugin-macros"] }, "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA=="],
"discord-api-types": ["discord-api-types@0.38.49", "", {}, "sha512-XnqcWmnFZFAE8ZM8SHAw9DIV8D3Or00rMQ8iQLotrEA2PmXhl+ykaf6L6q4l474hrSUH1JaYcv+iOMRWp2p6Tg=="],
@@ -196,6 +225,8 @@
"json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="],
"json-with-bigint": ["json-with-bigint@3.5.10", "", {}, "sha512-Vcx+JVNEBts/xfcoCS69sKrOhOk/3TVlvlT+XzUOefVKnnrbYSCKpDCm10pohsJFtsJVYnwa/cXRZ4eElzaM6w=="],
"jsonfile": ["jsonfile@6.2.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg=="],
"kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="],
@@ -242,6 +273,8 @@
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
"universal-user-agent": ["universal-user-agent@7.0.3", "", {}, "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A=="],
"universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="],
"ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="],
+2 -1
View File
@@ -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",
@@ -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",
+8
View File
@@ -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);
@@ -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();
}
}
}
@@ -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.
@@ -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.`;
@@ -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++;
@@ -31,6 +33,14 @@ export class ToolTracker {
this.listedDirs++;
}
recordGitHubSearch(): void {
this.gitHubSearchCalls++;
}
recordGitHubRead(): void {
this.gitHubReadCalls++;
}
getSummary(): string {
const parts: string[] = [];
@@ -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);
@@ -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,
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,
});
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;
}
+139
View File
@@ -5,6 +5,11 @@ import { listCodebaseFiles, searchCodebase } from "./codebase-search";
import { viewCodebaseFiles } from "./codebase-viewer";
import { searchDocs } from "./docs-search";
import { viewDocs } from "./docs-viewer";
import { getGitHubItem, getGitHubItemComments, searchGitHub } from "./github";
function truncateBody(body: string, maxLen: number = 500): string {
return body.length > maxLen ? body.slice(0, maxLen) + "..." : body;
}
export const aiTools = {
search_docs: tool({
@@ -173,4 +178,138 @@ export const aiTools = {
}
},
}),
search_github: tool({
description:
"Search GitHub issues and pull requests in the aaif-goose/goose repository. Use this to find bugs, feature requests, or discussions. Results can be sorted by recency, relevance, or comment count.",
inputSchema: z.object({
query: z
.string()
.describe(
"Search query (supports GitHub qualifiers like 'label:bug', 'is:issue', 'is:pr', 'author:username')",
),
sort: z
.enum(["created", "updated", "comments"])
.optional()
.describe(
"Sort by created date, last updated, or comment count. Omit for relevance-based sorting (default).",
),
order: z
.enum(["asc", "desc"])
.optional()
.describe("Sort order (default: desc)"),
state: z
.enum(["open", "closed", "all"])
.optional()
.describe("Filter by issue state (default: all)"),
limit: z
.number()
.optional()
.describe("Maximum number of results (default 10)"),
}),
execute: async ({ query, sort, order, state, limit }) => {
try {
const results = await searchGitHub(query, {
sort,
order,
state,
limit,
});
if (results.length === 0) {
return "Nothing found on GitHub matching your query. Try different keywords.";
}
return results
.map((r) => {
const status =
r.state === "closed"
? r.isMerged
? "merged"
: "closed"
: r.state;
return (
`**#${r.number}** (${status}) - ${r.title}\n` +
`Author: ${r.author} | Created: ${r.createdAt.slice(0, 10)} | Comments: ${r.comments}\n` +
`Labels: ${r.labels.join(", ") || "none"}\n` +
`${r.body ? truncateBody(r.body) + "\n" : ""}` +
`URL: <${r.url}>`
);
})
.join("\n\n");
} catch (error) {
const errorMsg =
error instanceof Error ? error.message : "Unknown error";
logger.error(`Error searching GitHub: ${errorMsg}`);
return `Error searching GitHub: ${errorMsg}`;
}
},
}),
get_github_issue_or_pr: tool({
description:
"Get detailed information about a specific GitHub issue or pull request in the aaif-goose/goose repository, including its description and comments.",
inputSchema: z.object({
issueNumber: z.number().describe("The issue or pull request number"),
includeComments: z
.boolean()
.optional()
.describe(
"Whether to include comments in the response (default: true)",
),
commentLimit: z
.number()
.optional()
.describe("Maximum number of comments to fetch (default: 30)"),
}),
execute: async ({
issueNumber,
includeComments = true,
commentLimit = 30,
}) => {
try {
const item = await getGitHubItem(issueNumber);
const status =
item.state === "closed"
? item.isMerged
? "merged"
: "closed"
: item.state;
let result =
`## #${item.number}: ${item.title}\n` +
`**State:** ${status} | **Author:** ${item.author}\n` +
`**Created:** ${item.createdAt} | **Updated:** ${item.updatedAt}\n` +
`**Labels:** ${item.labels.join(", ") || "none"}\n` +
`**URL:** <${item.url}>\n\n`;
if (item.body) {
result += `**Description:**\n${item.body.slice(0, 4000)}\n`;
}
if (includeComments && item.comments > 0) {
const comments = await getGitHubItemComments(
issueNumber,
commentLimit,
);
result += `\n**Comments (${comments.length}):**\n`;
result += comments
.slice(0, commentLimit)
.map(
(c) =>
`\n**${c.author}** (${c.createdAt.slice(0, 10)}):\n${c.body.slice(0, 1500)}`,
)
.join("\n---");
if (item.comments > comments.length) {
result += `\n\n... and ${item.comments - comments.length} more comments. Use get_github_issue_or_pr with includeComments=true and a larger commentLimit to fetch more.`;
}
}
return result;
} catch (error) {
const errorMsg =
error instanceof Error ? error.message : "Unknown error";
logger.error(`Error getting GitHub item: ${errorMsg}`);
return `Error getting item: ${errorMsg}`;
}
},
}),
};