From 017a012074b333d4d73ef30cb39feff31558d218 Mon Sep 17 00:00:00 2001 From: BestCodes <106822363+The-Best-Codes@users.noreply.github.com> Date: Sat, 11 Jul 2026 17:07:01 -0500 Subject: [PATCH 1/6] feat: allow bot to search and read github issues and prs Signed-off-by: BestCodes <106822363+The-Best-Codes@users.noreply.github.com> --- services/ask-ai-bot/.env.example | 3 + services/ask-ai-bot/bun.lock | 33 +++++ services/ask-ai-bot/package.json | 1 + services/ask-ai-bot/utils/ai/index.ts | 4 + services/ask-ai-bot/utils/ai/system-prompt.ts | 10 +- services/ask-ai-bot/utils/ai/tools/github.ts | 116 ++++++++++++++++ services/ask-ai-bot/utils/ai/tools/index.ts | 126 ++++++++++++++++++ 7 files changed, 291 insertions(+), 2 deletions(-) create mode 100644 services/ask-ai-bot/utils/ai/tools/github.ts diff --git a/services/ask-ai-bot/.env.example b/services/ask-ai-bot/.env.example index bd6490f05f09..6a56465928f7 100644 --- a/services/ask-ai-bot/.env.example +++ b/services/ask-ai-bot/.env.example @@ -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=../.. diff --git a/services/ask-ai-bot/bun.lock b/services/ask-ai-bot/bun.lock index 1ad1463c1ac6..aa4e4e80209c 100644 --- a/services/ask-ai-bot/bun.lock +++ b/services/ask-ai-bot/bun.lock @@ -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=="], diff --git a/services/ask-ai-bot/package.json b/services/ask-ai-bot/package.json index 96af25ce6233..c5c152479637 100644 --- a/services/ask-ai-bot/package.json +++ b/services/ask-ai-bot/package.json @@ -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", diff --git a/services/ask-ai-bot/utils/ai/index.ts b/services/ask-ai-bot/utils/ai/index.ts index ab5c458f3745..1a54dc577956 100644 --- a/services/ask-ai-bot/utils/ai/index.ts +++ b/services/ask-ai-bot/utils/ai/index.ts @@ -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); diff --git a/services/ask-ai-bot/utils/ai/system-prompt.ts b/services/ask-ai-bot/utils/ai/system-prompt.ts index 35eeffafd440..82bcbebd7d80 100644 --- a/services/ask-ai-bot/utils/ai/system-prompt.ts +++ b/services/ask-ai-bot/utils/ai/system-prompt.ts @@ -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., \`\` or \`[Example]()\`) to prevent excessive link previews. Do not use backtick characters around the URL.`; diff --git a/services/ask-ai-bot/utils/ai/tools/github.ts b/services/ask-ai-bot/utils/ai/tools/github.ts new file mode 100644 index 000000000000..0da8cfe119ac --- /dev/null +++ b/services/ask-ai-bot/utils/ai/tools/github.ts @@ -0,0 +1,116 @@ +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; + 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 { + const { sort, order = "desc", state = "all", limit = 10 } = options; + const api = getOctokit(); + + const q = `repo:${REPO_OWNER}/${REPO_NAME} ${query}${state !== "all" ? ` state:${state}` : ""}`; + + const response = await api.rest.search.issuesAndPullRequests({ + q, + ...(sort ? { sort, order } : {}), + per_page: limit, + }); + + return response.data.items.map((item) => ({ + number: item.number, + title: item.title, + state: item.state, + 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 { + const api = getOctokit(); + + const response = await api.rest.issues.get({ + owner: REPO_OWNER, + repo: REPO_NAME, + issue_number: number, + }); + + const item = response.data; + return { + number: item.number, + title: item.title, + state: item.state, + 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 { + const api = getOctokit(); + + const response = await api.rest.issues.listComments({ + owner: REPO_OWNER, + repo: REPO_NAME, + issue_number: number, + per_page: limit, + }); + + return response.data.map((comment) => ({ + author: comment.user?.login ?? "unknown", + createdAt: comment.created_at, + body: comment.body ?? "", + })); +} diff --git a/services/ask-ai-bot/utils/ai/tools/index.ts b/services/ask-ai-bot/utils/ai/tools/index.ts index 79b46e24d0fd..35a045db6274 100644 --- a/services/ask-ai-bot/utils/ai/tools/index.ts +++ b/services/ask-ai-bot/utils/ai/tools/index.ts @@ -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 = 3000): string { + return body.length > maxLen ? body.slice(0, maxLen) + "..." : body; +} export const aiTools = { search_docs: tool({ @@ -173,4 +178,125 @@ 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) => + `**#${r.number}** (${r.state}) - ${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); + + let result = + `## #${item.number}: ${item.title}\n` + + `**State:** ${item.state} | **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, 20) + .map( + (c) => + `\n**${c.author}** (${c.createdAt.slice(0, 10)}):\n${c.body.slice(0, 1500)}`, + ) + .join("\n---"); + if (comments.length > 20) { + result += `\n\n... and ${comments.length - 20} more comments. Use get_github_issue_or_pr with includeComments=true and 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}`; + } + }, + }), }; From 7d967641c2c2f4c90935cb2602a8bf350eaf728a Mon Sep 17 00:00:00 2001 From: BestCodes <106822363+The-Best-Codes@users.noreply.github.com> Date: Sat, 11 Jul 2026 17:07:46 -0500 Subject: [PATCH 2/6] reduce body preview length Signed-off-by: BestCodes <106822363+The-Best-Codes@users.noreply.github.com> --- services/ask-ai-bot/utils/ai/tools/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/ask-ai-bot/utils/ai/tools/index.ts b/services/ask-ai-bot/utils/ai/tools/index.ts index 35a045db6274..c6840ec9125b 100644 --- a/services/ask-ai-bot/utils/ai/tools/index.ts +++ b/services/ask-ai-bot/utils/ai/tools/index.ts @@ -7,7 +7,7 @@ import { searchDocs } from "./docs-search"; import { viewDocs } from "./docs-viewer"; import { getGitHubItem, getGitHubItemComments, searchGitHub } from "./github"; -function truncateBody(body: string, maxLen: number = 3000): string { +function truncateBody(body: string, maxLen: number = 500): string { return body.length > maxLen ? body.slice(0, maxLen) + "..." : body; } From d196f0cb1f5c25e8b09235f7d32fe0ea180724fb Mon Sep 17 00:00:00 2001 From: BestCodes <106822363+The-Best-Codes@users.noreply.github.com> Date: Sat, 11 Jul 2026 17:30:02 -0500 Subject: [PATCH 3/6] address security concerns Signed-off-by: BestCodes <106822363+The-Best-Codes@users.noreply.github.com> --- services/ask-ai-bot/utils/ai/tools/github.ts | 37 +++++++++++++------- services/ask-ai-bot/utils/ai/tools/index.ts | 6 ++-- 2 files changed, 28 insertions(+), 15 deletions(-) diff --git a/services/ask-ai-bot/utils/ai/tools/github.ts b/services/ask-ai-bot/utils/ai/tools/github.ts index 0da8cfe119ac..60f9413bede1 100644 --- a/services/ask-ai-bot/utils/ai/tools/github.ts +++ b/services/ask-ai-bot/utils/ai/tools/github.ts @@ -45,7 +45,8 @@ export async function searchGitHub( const { sort, order = "desc", state = "all", limit = 10 } = options; const api = getOctokit(); - const q = `repo:${REPO_OWNER}/${REPO_NAME} ${query}${state !== "all" ? ` state:${state}` : ""}`; + 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, @@ -100,17 +101,29 @@ export async function getGitHubItemComments( limit: number = 30, ): Promise { const api = getOctokit(); + const perPage = Math.min(limit, 100); + const comments: GitHubComment[] = []; - const response = await api.rest.issues.listComments({ - owner: REPO_OWNER, - repo: REPO_NAME, - issue_number: number, - per_page: limit, - }); + 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, + }); - return response.data.map((comment) => ({ - author: comment.user?.login ?? "unknown", - createdAt: comment.created_at, - body: comment.body ?? "", - })); + 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; } diff --git a/services/ask-ai-bot/utils/ai/tools/index.ts b/services/ask-ai-bot/utils/ai/tools/index.ts index c6840ec9125b..c38fc5c51a40 100644 --- a/services/ask-ai-bot/utils/ai/tools/index.ts +++ b/services/ask-ai-bot/utils/ai/tools/index.ts @@ -279,14 +279,14 @@ export const aiTools = { ); result += `\n**Comments (${comments.length}):**\n`; result += comments - .slice(0, 20) + .slice(0, commentLimit) .map( (c) => `\n**${c.author}** (${c.createdAt.slice(0, 10)}):\n${c.body.slice(0, 1500)}`, ) .join("\n---"); - if (comments.length > 20) { - result += `\n\n... and ${comments.length - 20} more comments. Use get_github_issue_or_pr with includeComments=true and commentLimit to fetch more.`; + 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.`; } } From c1ac7fd602e334e6d12fb2cde880a1a3e5afb35a Mon Sep 17 00:00:00 2001 From: BestCodes <106822363+The-Best-Codes@users.noreply.github.com> Date: Sat, 11 Jul 2026 17:34:45 -0500 Subject: [PATCH 4/6] add github tools to tooltracker Signed-off-by: BestCodes <106822363+The-Best-Codes@users.noreply.github.com> --- services/ask-ai-bot/utils/ai/index.ts | 4 ++++ services/ask-ai-bot/utils/ai/tool-tracker.ts | 25 ++++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/services/ask-ai-bot/utils/ai/index.ts b/services/ask-ai-bot/utils/ai/index.ts index 1a54dc577956..79de16693432 100644 --- a/services/ask-ai-bot/utils/ai/index.ts +++ b/services/ask-ai-bot/utils/ai/index.ts @@ -127,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(); } } } diff --git a/services/ask-ai-bot/utils/ai/tool-tracker.ts b/services/ask-ai-bot/utils/ai/tool-tracker.ts index 1fd6c9426d70..35ca2bc7be91 100644 --- a/services/ask-ai-bot/utils/ai/tool-tracker.ts +++ b/services/ask-ai-bot/utils/ai/tool-tracker.ts @@ -6,6 +6,8 @@ export class ToolTracker { private codeSearchResults: number = 0; private viewedCodePaths: Set = 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); From a0231ca26ab54975b9c0c542caee4dc681291db4 Mon Sep 17 00:00:00 2001 From: BestCodes <106822363+The-Best-Codes@users.noreply.github.com> Date: Sat, 11 Jul 2026 17:38:23 -0500 Subject: [PATCH 5/6] distinguish between closed and merged prs Signed-off-by: BestCodes <106822363+The-Best-Codes@users.noreply.github.com> --- services/ask-ai-bot/utils/ai/tools/github.ts | 7 ++++-- services/ask-ai-bot/utils/ai/tools/index.ts | 25 +++++++++++++++----- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/services/ask-ai-bot/utils/ai/tools/github.ts b/services/ask-ai-bot/utils/ai/tools/github.ts index 60f9413bede1..329d32809df0 100644 --- a/services/ask-ai-bot/utils/ai/tools/github.ts +++ b/services/ask-ai-bot/utils/ai/tools/github.ts @@ -18,6 +18,7 @@ export interface GitHubItem { number: number; title: string; state: string; + isMerged: boolean; author: string; createdAt: string; updatedAt: string; @@ -54,10 +55,11 @@ export async function searchGitHub( per_page: limit, }); - return response.data.items.map((item) => ({ + 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, @@ -79,11 +81,12 @@ export async function getGitHubItem(number: number): Promise { issue_number: number, }); - const item = response.data; + 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, diff --git a/services/ask-ai-bot/utils/ai/tools/index.ts b/services/ask-ai-bot/utils/ai/tools/index.ts index c38fc5c51a40..491cce0fd8fd 100644 --- a/services/ask-ai-bot/utils/ai/tools/index.ts +++ b/services/ask-ai-bot/utils/ai/tools/index.ts @@ -220,14 +220,21 @@ export const aiTools = { } return results - .map( - (r) => - `**#${r.number}** (${r.state}) - ${r.title}\n` + + .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}>`, - ) + `URL: <${r.url}>` + ); + }) .join("\n\n"); } catch (error) { const errorMsg = @@ -261,9 +268,15 @@ export const aiTools = { 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:** ${item.state} | **Author:** ${item.author}\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`; From 05248152787e81067f47f187308fdd0e4b92f09b Mon Sep 17 00:00:00 2001 From: BestCodes <106822363+The-Best-Codes@users.noreply.github.com> Date: Sat, 11 Jul 2026 17:50:19 -0500 Subject: [PATCH 6/6] bump version Signed-off-by: BestCodes <106822363+The-Best-Codes@users.noreply.github.com> --- services/ask-ai-bot/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/ask-ai-bot/package.json b/services/ask-ai-bot/package.json index c5c152479637..3faf9a33101d 100644 --- a/services/ask-ai-bot/package.json +++ b/services/ask-ai-bot/package.json @@ -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",