mirror of
https://github.com/sourcebot-dev/sourcebot.git
synced 2025-12-11 20:05:25 +00:00
Compare commits
5 commits
b5b01d9589
...
8075ce0d1b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8075ce0d1b | ||
|
|
3d85a0595c | ||
|
|
84cf524d84 | ||
|
|
fa833b3574 | ||
|
|
5cdb8fefc5 |
4 changed files with 171 additions and 121 deletions
|
|
@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
### Fixed
|
||||
- Fixed review agent so that it works with GHES instances [#611](https://github.com/sourcebot-dev/sourcebot/pull/611)
|
||||
|
||||
### Added
|
||||
- Added support for arbitrary user IDs required for OpenShift. [#658](https://github.com/sourcebot-dev/sourcebot/pull/658)
|
||||
|
||||
## [4.10.2] - 2025-12-04
|
||||
|
||||
### Fixed
|
||||
|
|
|
|||
|
|
@ -195,6 +195,7 @@ RUN addgroup -g $GID sourcebot && \
|
|||
adduser -D -u $UID -h /app -S sourcebot && \
|
||||
adduser sourcebot postgres && \
|
||||
adduser sourcebot redis && \
|
||||
chown -R sourcebot /app && \
|
||||
adduser sourcebot node && \
|
||||
mkdir /var/log/sourcebot && \
|
||||
chown sourcebot /var/log/sourcebot
|
||||
|
|
@ -244,7 +245,12 @@ RUN mkdir -p /run/postgresql && \
|
|||
chown -R postgres:postgres /run/postgresql && \
|
||||
chmod 775 /run/postgresql
|
||||
|
||||
RUN chown -R sourcebot:sourcebot /data
|
||||
# Make app directory accessible to both root and sourcebot user
|
||||
RUN chown -R sourcebot /app \
|
||||
&& chgrp -R 0 /app \
|
||||
&& chmod -R g=u /app
|
||||
# Make data directory accessible to both root and sourcebot user
|
||||
RUN chown -R sourcebot /data
|
||||
|
||||
COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf
|
||||
COPY prefix-output.sh ./prefix-output.sh
|
||||
|
|
|
|||
|
|
@ -6,28 +6,32 @@ import { WebhookEventDefinition} from "@octokit/webhooks/types";
|
|||
import { EndpointDefaults } from "@octokit/types";
|
||||
import { env } from "@sourcebot/shared";
|
||||
import { processGitHubPullRequest } from "@/features/agents/review-agent/app";
|
||||
import { throttling } from "@octokit/plugin-throttling";
|
||||
import { throttling, type ThrottlingOptions } from "@octokit/plugin-throttling";
|
||||
import fs from "fs";
|
||||
import { GitHubPullRequest } from "@/features/agents/review-agent/types";
|
||||
import { createLogger } from "@sourcebot/shared";
|
||||
|
||||
const logger = createLogger('github-webhook');
|
||||
|
||||
let githubApp: App | undefined;
|
||||
const DEFAULT_GITHUB_API_BASE_URL = "https://api.github.com";
|
||||
type GitHubAppBaseOptions = Omit<ConstructorParameters<typeof App>[0], "Octokit"> & { throttle: ThrottlingOptions };
|
||||
|
||||
let githubAppBaseOptions: GitHubAppBaseOptions | undefined;
|
||||
const githubAppCache = new Map<string, App>();
|
||||
|
||||
if (env.GITHUB_REVIEW_AGENT_APP_ID && env.GITHUB_REVIEW_AGENT_APP_WEBHOOK_SECRET && env.GITHUB_REVIEW_AGENT_APP_PRIVATE_KEY_PATH) {
|
||||
try {
|
||||
const privateKey = fs.readFileSync(env.GITHUB_REVIEW_AGENT_APP_PRIVATE_KEY_PATH, "utf8");
|
||||
|
||||
const throttledOctokit = Octokit.plugin(throttling);
|
||||
githubApp = new App({
|
||||
githubAppBaseOptions = {
|
||||
appId: env.GITHUB_REVIEW_AGENT_APP_ID,
|
||||
privateKey: privateKey,
|
||||
privateKey,
|
||||
webhooks: {
|
||||
secret: env.GITHUB_REVIEW_AGENT_APP_WEBHOOK_SECRET,
|
||||
},
|
||||
Octokit: throttledOctokit,
|
||||
throttle: {
|
||||
onRateLimit: (retryAfter: number, options: Required<EndpointDefaults>, octokit: Octokit, retryCount: number) => {
|
||||
enabled: true,
|
||||
onRateLimit: (retryAfter, _options, _octokit, retryCount) => {
|
||||
if (retryCount > 3) {
|
||||
logger.warn(`Rate limit exceeded: ${retryAfter} seconds`);
|
||||
return false;
|
||||
|
|
@ -35,13 +39,55 @@ if (env.GITHUB_REVIEW_AGENT_APP_ID && env.GITHUB_REVIEW_AGENT_APP_WEBHOOK_SECRET
|
|||
|
||||
return true;
|
||||
},
|
||||
}
|
||||
});
|
||||
onSecondaryRateLimit: (_retryAfter, options) => {
|
||||
// no retries on secondary rate limits
|
||||
logger.warn(`SecondaryRateLimit detected for ${options.method} ${options.url}`);
|
||||
}
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error(`Error initializing GitHub app: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
const normalizeGithubApiBaseUrl = (baseUrl?: string) => {
|
||||
if (!baseUrl) {
|
||||
return DEFAULT_GITHUB_API_BASE_URL;
|
||||
}
|
||||
|
||||
return baseUrl.replace(/\/+$/, "");
|
||||
};
|
||||
|
||||
const resolveGithubApiBaseUrl = (headers: Record<string, string>) => {
|
||||
const enterpriseHost = headers["x-github-enterprise-host"];
|
||||
if (enterpriseHost) {
|
||||
return normalizeGithubApiBaseUrl(`https://${enterpriseHost}/api/v3`);
|
||||
}
|
||||
|
||||
return DEFAULT_GITHUB_API_BASE_URL;
|
||||
};
|
||||
|
||||
const getGithubAppForBaseUrl = (baseUrl: string) => {
|
||||
if (!githubAppBaseOptions) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const normalizedBaseUrl = normalizeGithubApiBaseUrl(baseUrl);
|
||||
const cachedApp = githubAppCache.get(normalizedBaseUrl);
|
||||
if (cachedApp) {
|
||||
return cachedApp;
|
||||
}
|
||||
|
||||
const OctokitWithBaseUrl = Octokit.plugin(throttling).defaults({ baseUrl: normalizedBaseUrl });
|
||||
const app = new App({
|
||||
...githubAppBaseOptions,
|
||||
Octokit: OctokitWithBaseUrl,
|
||||
});
|
||||
|
||||
githubAppCache.set(normalizedBaseUrl, app);
|
||||
return app;
|
||||
};
|
||||
|
||||
function isPullRequestEvent(eventHeader: string, payload: unknown): payload is WebhookEventDefinition<"pull-request-opened"> | WebhookEventDefinition<"pull-request-synchronize"> {
|
||||
return eventHeader === "pull_request" && typeof payload === "object" && payload !== null && "action" in payload && typeof payload.action === "string" && (payload.action === "opened" || payload.action === "synchronize");
|
||||
}
|
||||
|
|
@ -52,12 +98,16 @@ function isIssueCommentEvent(eventHeader: string, payload: unknown): payload is
|
|||
|
||||
export const POST = async (request: NextRequest) => {
|
||||
const body = await request.json();
|
||||
const headers = Object.fromEntries(request.headers.entries());
|
||||
const headers = Object.fromEntries(Array.from(request.headers.entries(), ([key, value]) => [key.toLowerCase(), value]));
|
||||
|
||||
const githubEvent = headers['x-github-event'] || headers['X-GitHub-Event'];
|
||||
const githubEvent = headers['x-github-event'];
|
||||
if (githubEvent) {
|
||||
logger.info('GitHub event received:', githubEvent);
|
||||
|
||||
const githubApiBaseUrl = resolveGithubApiBaseUrl(headers);
|
||||
logger.debug('Using GitHub API base URL for event', { githubApiBaseUrl });
|
||||
const githubApp = getGithubAppForBaseUrl(githubApiBaseUrl);
|
||||
|
||||
if (!githubApp) {
|
||||
logger.warn('Received GitHub webhook event but GitHub app env vars are not set');
|
||||
return Response.json({ status: 'ok' });
|
||||
|
|
@ -113,4 +163,4 @@ export const POST = async (request: NextRequest) => {
|
|||
}
|
||||
|
||||
return Response.json({ status: 'ok' });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,115 +42,103 @@ export const createAgentStream = async ({
|
|||
searchScopeRepoNames,
|
||||
});
|
||||
|
||||
const stream = streamText({
|
||||
model,
|
||||
providerOptions,
|
||||
system: baseSystemPrompt,
|
||||
messages: inputMessages,
|
||||
tools: {
|
||||
[toolNames.searchCode]: createCodeSearchTool(searchScopeRepoNames),
|
||||
[toolNames.readFiles]: readFilesTool,
|
||||
[toolNames.findSymbolReferences]: findSymbolReferencesTool,
|
||||
[toolNames.findSymbolDefinitions]: findSymbolDefinitionsTool,
|
||||
[toolNames.searchRepos]: searchReposTool,
|
||||
[toolNames.listAllRepos]: listAllReposTool,
|
||||
},
|
||||
prepareStep: async ({ stepNumber }) => {
|
||||
// The first step attaches any mentioned sources to the system prompt.
|
||||
if (stepNumber === 0 && inputSources.length > 0) {
|
||||
const fileSources = inputSources.filter((source) => source.type === 'file');
|
||||
|
||||
const resolvedFileSources = (
|
||||
await Promise.all(fileSources.map(resolveFileSource)))
|
||||
.filter((source) => source !== undefined)
|
||||
|
||||
const fileSourcesSystemPrompt = await createFileSourcesSystemPrompt({
|
||||
files: resolvedFileSources
|
||||
});
|
||||
|
||||
return {
|
||||
system: `${baseSystemPrompt}\n\n${fileSourcesSystemPrompt}`
|
||||
}
|
||||
}
|
||||
|
||||
if (stepNumber === env.SOURCEBOT_CHAT_MAX_STEP_COUNT - 1) {
|
||||
return {
|
||||
system: `**CRITICAL**: You have reached the maximum number of steps!! YOU MUST PROVIDE YOUR FINAL ANSWER NOW. DO NOT KEEP RESEARCHING.\n\n${answerInstructions}`,
|
||||
activeTools: [],
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
},
|
||||
temperature: env.SOURCEBOT_CHAT_MODEL_TEMPERATURE,
|
||||
stopWhen: [
|
||||
stepCountIsGTE(env.SOURCEBOT_CHAT_MAX_STEP_COUNT),
|
||||
],
|
||||
toolChoice: "auto", // Let the model decide when to use tools
|
||||
onStepFinish: ({ toolResults }) => {
|
||||
// This takes care of extracting any sources that the LLM has seen as part of
|
||||
// the tool calls it made.
|
||||
toolResults.forEach(({ toolName, output, dynamic }) => {
|
||||
// we don't care about dynamic tool results here.
|
||||
if (dynamic) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isServiceError(output)) {
|
||||
// is there something we want to do here?
|
||||
return;
|
||||
}
|
||||
|
||||
if (toolName === toolNames.readFiles) {
|
||||
output.forEach((file) => {
|
||||
onWriteSource({
|
||||
type: 'file',
|
||||
language: file.language,
|
||||
repo: file.repository,
|
||||
path: file.path,
|
||||
revision: file.revision,
|
||||
name: file.path.split('/').pop() ?? file.path,
|
||||
})
|
||||
})
|
||||
}
|
||||
else if (toolName === toolNames.searchCode) {
|
||||
output.files.forEach((file) => {
|
||||
onWriteSource({
|
||||
type: 'file',
|
||||
language: file.language,
|
||||
repo: file.repository,
|
||||
path: file.fileName,
|
||||
revision: file.revision,
|
||||
name: file.fileName.split('/').pop() ?? file.fileName,
|
||||
})
|
||||
})
|
||||
}
|
||||
else if (toolName === toolNames.findSymbolDefinitions || toolName === toolNames.findSymbolReferences) {
|
||||
output.forEach((file) => {
|
||||
onWriteSource({
|
||||
type: 'file',
|
||||
language: file.language,
|
||||
repo: file.repository,
|
||||
path: file.fileName,
|
||||
revision: file.revision,
|
||||
name: file.fileName.split('/').pop() ?? file.fileName,
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
// Only enable langfuse traces in cloud environments.
|
||||
experimental_telemetry: {
|
||||
isEnabled: clientEnv.NEXT_PUBLIC_SOURCEBOT_CLOUD_ENVIRONMENT !== undefined,
|
||||
metadata: {
|
||||
langfuseTraceId: traceId,
|
||||
let stream;
|
||||
try {
|
||||
stream = streamText({
|
||||
model,
|
||||
providerOptions,
|
||||
system: baseSystemPrompt,
|
||||
messages: inputMessages,
|
||||
tools: {
|
||||
[toolNames.searchCode]: createCodeSearchTool(searchScopeRepoNames),
|
||||
[toolNames.readFiles]: readFilesTool,
|
||||
[toolNames.findSymbolReferences]: findSymbolReferencesTool,
|
||||
[toolNames.findSymbolDefinitions]: findSymbolDefinitionsTool,
|
||||
[toolNames.searchRepos]: searchReposTool,
|
||||
[toolNames.listAllRepos]: listAllReposTool,
|
||||
},
|
||||
},
|
||||
onError: (error) => {
|
||||
logger.error(error);
|
||||
},
|
||||
});
|
||||
|
||||
prepareStep: async ({ stepNumber }) => {
|
||||
if (stepNumber === 0 && inputSources.length > 0) {
|
||||
const fileSources = inputSources.filter((source) => source.type === 'file');
|
||||
const resolvedFileSources = (
|
||||
await Promise.all(fileSources.map(resolveFileSource)))
|
||||
.filter((source) => source !== undefined)
|
||||
const fileSourcesSystemPrompt = await createFileSourcesSystemPrompt({
|
||||
files: resolvedFileSources
|
||||
});
|
||||
return {
|
||||
system: `${baseSystemPrompt}\n\n${fileSourcesSystemPrompt}`
|
||||
}
|
||||
}
|
||||
if (stepNumber === env.SOURCEBOT_CHAT_MAX_STEP_COUNT - 1) {
|
||||
return {
|
||||
system: `**CRITICAL**: You have reached the maximum number of steps!! YOU MUST PROVIDE YOUR FINAL ANSWER NOW. DO NOT KEEP RESEARCHING.\n\n${answerInstructions}`,
|
||||
activeTools: [],
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
temperature: env.SOURCEBOT_CHAT_MODEL_TEMPERATURE,
|
||||
stopWhen: [
|
||||
stepCountIsGTE(env.SOURCEBOT_CHAT_MAX_STEP_COUNT),
|
||||
],
|
||||
toolChoice: "auto",
|
||||
onStepFinish: ({ toolResults }) => {
|
||||
toolResults.forEach(({ toolName, output, dynamic }) => {
|
||||
if (dynamic) return;
|
||||
if (isServiceError(output)) return;
|
||||
if (toolName === toolNames.readFiles) {
|
||||
output.forEach((file) => {
|
||||
onWriteSource({
|
||||
type: 'file',
|
||||
language: file.language,
|
||||
repo: file.repository,
|
||||
path: file.path,
|
||||
revision: file.revision,
|
||||
name: file.path.split('/').pop() ?? file.path,
|
||||
})
|
||||
})
|
||||
} else if (toolName === toolNames.searchCode) {
|
||||
output.files.forEach((file) => {
|
||||
onWriteSource({
|
||||
type: 'file',
|
||||
language: file.language,
|
||||
repo: file.repository,
|
||||
path: file.fileName,
|
||||
revision: file.revision,
|
||||
name: file.fileName.split('/').pop() ?? file.fileName,
|
||||
})
|
||||
})
|
||||
} else if (toolName === toolNames.findSymbolDefinitions || toolName === toolNames.findSymbolReferences) {
|
||||
output.forEach((file) => {
|
||||
onWriteSource({
|
||||
type: 'file',
|
||||
language: file.language,
|
||||
repo: file.repository,
|
||||
path: file.fileName,
|
||||
revision: file.revision,
|
||||
name: file.fileName.split('/').pop() ?? file.fileName,
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
experimental_telemetry: {
|
||||
isEnabled: clientEnv.NEXT_PUBLIC_SOURCEBOT_CLOUD_ENVIRONMENT !== undefined,
|
||||
metadata: {
|
||||
langfuseTraceId: traceId,
|
||||
},
|
||||
},
|
||||
onError: (error) => {
|
||||
logger.error(error);
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
if (model?.providerId === 'openai-compatible') {
|
||||
throw new Error('The selected AI provider does not support codebase tool calls. Please use a provider that supports function/tool calls for codebase-related questions.');
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
return stream;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue