mirror of
https://github.com/sourcebot-dev/sourcebot.git
synced 2025-12-11 20:05:25 +00:00
Some checks are pending
Publish to ghcr / build (linux/amd64, blacksmith-4vcpu-ubuntu-2404) (push) Waiting to run
Publish to ghcr / build (linux/arm64, blacksmith-8vcpu-ubuntu-2204-arm) (push) Waiting to run
Publish to ghcr / merge (push) Blocked by required conditions
Update Roadmap Released / update (push) Waiting to run
275 lines
11 KiB
TypeScript
275 lines
11 KiB
TypeScript
import { z } from "zod"
|
|
import { search } from "@/features/search"
|
|
import { InferToolInput, InferToolOutput, InferUITool, tool, ToolUIPart } from "ai";
|
|
import { isServiceError } from "@/lib/utils";
|
|
import { getFileSource } from "../search/fileSourceApi";
|
|
import { findSearchBasedSymbolDefinitions, findSearchBasedSymbolReferences } from "../codeNav/api";
|
|
import { FileSourceResponse } from "../search/types";
|
|
import { addLineNumbers, buildSearchQuery } from "./utils";
|
|
import { toolNames } from "./constants";
|
|
import { getRepos } from "@/actions";
|
|
import Fuse from "fuse.js";
|
|
|
|
// @NOTE: When adding a new tool, follow these steps:
|
|
// 1. Add the tool to the `toolNames` constant in `constants.ts`.
|
|
// 2. Add the tool to the `SBChatMessageToolTypes` type in `types.ts`.
|
|
// 3. Add the tool to the `tools` prop in `agent.ts`.
|
|
// 4. If the tool is meant to be rendered in the UI:
|
|
// - Add the tool to the `uiVisiblePartTypes` constant in `constants.ts`.
|
|
// - Add the tool's component to the `DetailsCard` switch statement in `detailsCard.tsx`.
|
|
//
|
|
// - bk, 2025-07-25
|
|
|
|
|
|
export const findSymbolReferencesTool = tool({
|
|
description: `Finds references to a symbol in the codebase.`,
|
|
inputSchema: z.object({
|
|
symbol: z.string().describe("The symbol to find references to"),
|
|
language: z.string().describe("The programming language of the symbol"),
|
|
repository: z.string().describe("The repository to scope the search to").optional(),
|
|
}),
|
|
execute: async ({ symbol, language, repository }) => {
|
|
// @todo: make revision configurable.
|
|
const revision = "HEAD";
|
|
|
|
const response = await findSearchBasedSymbolReferences({
|
|
symbolName: symbol,
|
|
language,
|
|
revisionName: "HEAD",
|
|
repoName: repository,
|
|
});
|
|
|
|
if (isServiceError(response)) {
|
|
return response;
|
|
}
|
|
|
|
return response.files.map((file) => ({
|
|
fileName: file.fileName,
|
|
repository: file.repository,
|
|
language: file.language,
|
|
matches: file.matches.map(({ lineContent, range }) => {
|
|
return addLineNumbers(lineContent, range.start.lineNumber);
|
|
}),
|
|
revision,
|
|
}));
|
|
},
|
|
});
|
|
|
|
export type FindSymbolReferencesTool = InferUITool<typeof findSymbolReferencesTool>;
|
|
export type FindSymbolReferencesToolInput = InferToolInput<typeof findSymbolReferencesTool>;
|
|
export type FindSymbolReferencesToolOutput = InferToolOutput<typeof findSymbolReferencesTool>;
|
|
export type FindSymbolReferencesToolUIPart = ToolUIPart<{ [toolNames.findSymbolReferences]: FindSymbolReferencesTool }>
|
|
|
|
export const findSymbolDefinitionsTool = tool({
|
|
description: `Finds definitions of a symbol in the codebase.`,
|
|
inputSchema: z.object({
|
|
symbol: z.string().describe("The symbol to find definitions of"),
|
|
language: z.string().describe("The programming language of the symbol"),
|
|
repository: z.string().describe("The repository to scope the search to").optional(),
|
|
}),
|
|
execute: async ({ symbol, language, repository }) => {
|
|
// @todo: make revision configurable.
|
|
const revision = "HEAD";
|
|
|
|
const response = await findSearchBasedSymbolDefinitions({
|
|
symbolName: symbol,
|
|
language,
|
|
revisionName: revision,
|
|
repoName: repository,
|
|
});
|
|
|
|
if (isServiceError(response)) {
|
|
return response;
|
|
}
|
|
|
|
return response.files.map((file) => ({
|
|
fileName: file.fileName,
|
|
repository: file.repository,
|
|
language: file.language,
|
|
matches: file.matches.map(({ lineContent, range }) => {
|
|
return addLineNumbers(lineContent, range.start.lineNumber);
|
|
}),
|
|
revision,
|
|
}));
|
|
}
|
|
});
|
|
|
|
export type FindSymbolDefinitionsTool = InferUITool<typeof findSymbolDefinitionsTool>;
|
|
export type FindSymbolDefinitionsToolInput = InferToolInput<typeof findSymbolDefinitionsTool>;
|
|
export type FindSymbolDefinitionsToolOutput = InferToolOutput<typeof findSymbolDefinitionsTool>;
|
|
export type FindSymbolDefinitionsToolUIPart = ToolUIPart<{ [toolNames.findSymbolDefinitions]: FindSymbolDefinitionsTool }>
|
|
|
|
export const readFilesTool = tool({
|
|
description: `Reads the contents of multiple files at the given paths.`,
|
|
inputSchema: z.object({
|
|
paths: z.array(z.string()).describe("The paths to the files to read"),
|
|
repository: z.string().describe("The repository to read the files from"),
|
|
}),
|
|
execute: async ({ paths, repository }) => {
|
|
// @todo: make revision configurable.
|
|
const revision = "HEAD";
|
|
|
|
const responses = await Promise.all(paths.map(async (path) => {
|
|
return getFileSource({
|
|
fileName: path,
|
|
repository,
|
|
branch: revision,
|
|
// @todo(mt): handle multi-tenancy.
|
|
});
|
|
}));
|
|
|
|
if (responses.some(isServiceError)) {
|
|
const firstError = responses.find(isServiceError);
|
|
return firstError!;
|
|
}
|
|
|
|
return (responses as FileSourceResponse[]).map((response) => ({
|
|
path: response.path,
|
|
repository: response.repository,
|
|
language: response.language,
|
|
source: addLineNumbers(response.source),
|
|
revision,
|
|
}));
|
|
}
|
|
});
|
|
|
|
export type ReadFilesTool = InferUITool<typeof readFilesTool>;
|
|
export type ReadFilesToolInput = InferToolInput<typeof readFilesTool>;
|
|
export type ReadFilesToolOutput = InferToolOutput<typeof readFilesTool>;
|
|
export type ReadFilesToolUIPart = ToolUIPart<{ [toolNames.readFiles]: ReadFilesTool }>
|
|
|
|
export const createCodeSearchTool = (selectedRepos: string[]) => tool({
|
|
description: `Fetches code that matches the provided regex pattern in \`query\`. This is NOT a semantic search.
|
|
Results are returned as an array of matching files, with the file's URL, repository, and language.`,
|
|
inputSchema: z.object({
|
|
queryRegexp: z
|
|
.string()
|
|
.describe(`The regex pattern to search for in the code.
|
|
|
|
Queries consist of space-seperated regular expressions. Wrapping expressions in "" combines them. By default, a file must have at least one match for each expression to be included. Examples:
|
|
|
|
\`foo\` - Match files with regex /foo/
|
|
\`foo bar\` - Match files with regex /foo/ and /bar/
|
|
\`"foo bar"\` - Match files with regex /foo bar/
|
|
\`console.log\` - Match files with regex /console.log/
|
|
|
|
Multiple expressions can be or'd together with or, negated with -, or grouped with (). Examples:
|
|
\`foo or bar\` - Match files with regex /foo/ or /bar/
|
|
\`foo -bar\` - Match files with regex /foo/ but not /bar/
|
|
\`foo (bar or baz)\` - Match files with regex /foo/ and either /bar/ or /baz/
|
|
`),
|
|
repoNamesFilterRegexp: z
|
|
.array(z.string())
|
|
.describe(`Filter results from repos that match the regex. By default all repos are searched.`)
|
|
.optional(),
|
|
languageNamesFilter: z
|
|
.array(z.string())
|
|
.describe(`Scope the search to the provided languages. The language MUST be formatted as a GitHub linguist language. Examples: Python, JavaScript, TypeScript, Java, C#, C++, PHP, Go, Rust, Ruby, Swift, Kotlin, Shell, C, Dart, HTML, CSS, PowerShell, SQL, R`)
|
|
.optional(),
|
|
fileNamesFilterRegexp: z
|
|
.array(z.string())
|
|
.describe(`Filter results from filepaths that match the regex. When this option is not specified, all files are searched.`)
|
|
.optional(),
|
|
limit: z.number().default(10).describe("Maximum number of matches to return (default: 100)"),
|
|
}),
|
|
execute: async ({ queryRegexp: _query, repoNamesFilterRegexp, languageNamesFilter, fileNamesFilterRegexp, limit }) => {
|
|
const query = buildSearchQuery({
|
|
query: _query,
|
|
repoNamesFilter: selectedRepos,
|
|
repoNamesFilterRegexp,
|
|
languageNamesFilter,
|
|
fileNamesFilterRegexp,
|
|
});
|
|
|
|
const response = await search({
|
|
queryType: 'string',
|
|
query,
|
|
options: {
|
|
matches: limit ?? 100,
|
|
contextLines: 3,
|
|
whole: false,
|
|
isCaseSensitivityEnabled: true,
|
|
isRegexEnabled: true,
|
|
}
|
|
});
|
|
|
|
if (isServiceError(response)) {
|
|
return response;
|
|
}
|
|
|
|
return {
|
|
files: response.files.map((file) => ({
|
|
fileName: file.fileName.text,
|
|
repository: file.repository,
|
|
language: file.language,
|
|
matches: file.chunks.map(({ content, contentStart }) => {
|
|
return addLineNumbers(content, contentStart.lineNumber);
|
|
}),
|
|
// @todo: make revision configurable.
|
|
revision: 'HEAD',
|
|
})),
|
|
query,
|
|
}
|
|
},
|
|
});
|
|
|
|
export type SearchCodeTool = InferUITool<ReturnType<typeof createCodeSearchTool>>;
|
|
export type SearchCodeToolInput = InferToolInput<ReturnType<typeof createCodeSearchTool>>;
|
|
export type SearchCodeToolOutput = InferToolOutput<ReturnType<typeof createCodeSearchTool>>;
|
|
export type SearchCodeToolUIPart = ToolUIPart<{ [toolNames.searchCode]: SearchCodeTool }>;
|
|
|
|
export const searchReposTool = tool({
|
|
description: `Search for repositories by name using fuzzy search. This helps find repositories in the codebase when you know part of their name.`,
|
|
inputSchema: z.object({
|
|
query: z.string().describe("The search query to find repositories by name (supports fuzzy matching)"),
|
|
limit: z.number().default(10).describe("Maximum number of repositories to return (default: 10)")
|
|
}),
|
|
execute: async ({ query, limit }) => {
|
|
const reposResponse = await getRepos();
|
|
|
|
if (isServiceError(reposResponse)) {
|
|
return reposResponse;
|
|
}
|
|
|
|
// Configure Fuse.js for fuzzy searching
|
|
const fuse = new Fuse(reposResponse, {
|
|
keys: [
|
|
{ name: 'repoName', weight: 0.7 },
|
|
{ name: 'repoDisplayName', weight: 0.3 }
|
|
],
|
|
threshold: 0.4, // Lower threshold = more strict matching
|
|
includeScore: true,
|
|
minMatchCharLength: 1,
|
|
});
|
|
|
|
const searchResults = fuse.search(query, { limit: limit ?? 10 });
|
|
|
|
searchResults.sort((a, b) => (a.score ?? 0) - (b.score ?? 0));
|
|
|
|
return searchResults.map(({ item }) => item.repoName);
|
|
}
|
|
});
|
|
|
|
export type SearchReposTool = InferUITool<typeof searchReposTool>;
|
|
export type SearchReposToolInput = InferToolInput<typeof searchReposTool>;
|
|
export type SearchReposToolOutput = InferToolOutput<typeof searchReposTool>;
|
|
export type SearchReposToolUIPart = ToolUIPart<{ [toolNames.searchRepos]: SearchReposTool }>;
|
|
|
|
export const listAllReposTool = tool({
|
|
description: `Lists all repositories in the codebase. This provides a complete overview of all available repositories.`,
|
|
inputSchema: z.object({}),
|
|
execute: async () => {
|
|
const reposResponse = await getRepos();
|
|
|
|
if (isServiceError(reposResponse)) {
|
|
return reposResponse;
|
|
}
|
|
|
|
return reposResponse.map((repo) => repo.repoName);
|
|
}
|
|
});
|
|
|
|
export type ListAllReposTool = InferUITool<typeof listAllReposTool>;
|
|
export type ListAllReposToolInput = InferToolInput<typeof listAllReposTool>;
|
|
export type ListAllReposToolOutput = InferToolOutput<typeof listAllReposTool>;
|
|
export type ListAllReposToolUIPart = ToolUIPart<{ [toolNames.listAllRepos]: ListAllReposTool }>;
|