-
Notifications
You must be signed in to change notification settings - Fork 264
experiment(ask_gh): Support on demand indexing of GitHub repositories #785
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 3 commits
fd9973a
9f9ce5f
db8adf5
f8e5a84
a8dc2ae
82a6616
c10e10d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,6 +7,8 @@ import z from 'zod'; | |
| import { ConnectionManager } from './connectionManager.js'; | ||
| import { PromClient } from './promClient.js'; | ||
| import { RepoIndexManager } from './repoIndexManager.js'; | ||
| import { createGitHubRepoRecord } from './repoCompileUtils.js'; | ||
| import { Octokit } from '@octokit/rest'; | ||
|
|
||
| const logger = createLogger('api'); | ||
| const PORT = 3060; | ||
|
|
@@ -33,6 +35,7 @@ export class Api { | |
|
|
||
| app.post('/api/sync-connection', this.syncConnection.bind(this)); | ||
| app.post('/api/index-repo', this.indexRepo.bind(this)); | ||
| app.post(`/api/experimental/add-github-repo`, this.addGithubRepo.bind(this)); | ||
|
|
||
| this.server = app.listen(PORT, () => { | ||
| logger.info(`API server is running on port ${PORT}`); | ||
|
|
@@ -92,6 +95,39 @@ export class Api { | |
| res.status(200).json({ jobId }); | ||
| } | ||
|
|
||
| private async addGithubRepo(req: Request, res: Response) { | ||
| const schema = z.object({ | ||
| owner: z.string(), | ||
| repo: z.string(), | ||
| }).strict(); | ||
|
|
||
| const parsed = schema.safeParse(req.body); | ||
| if (!parsed.success) { | ||
| res.status(400).json({ error: parsed.error.message }); | ||
| return; | ||
| } | ||
|
|
||
| const octokit = new Octokit(); | ||
| const response = await octokit.rest.repos.get({ | ||
| owner: parsed.data.owner, | ||
| repo: parsed.data.repo, | ||
| }); | ||
|
|
||
| const record = createGitHubRepoRecord({ | ||
| repo: response.data, | ||
| hostUrl: 'https://github.com', | ||
| isAutoCleanupDisabled: true, | ||
| }); | ||
|
|
||
| const repo = await this.prisma.repo.create({ | ||
| data: record, | ||
| }); | ||
|
|
||
| const [jobId ] = await this.repoIndexManager.createJobs([repo], RepoIndexingJobType.INDEX); | ||
|
|
||
| res.status(200).json({ jobId, repoId: repo.id }); | ||
|
Comment on lines
+111
to
+137
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: # Check if file exists and view the content around lines 111-137
if [ -f "packages/backend/src/api.ts" ]; then
echo "File found. Checking lines 111-137:"
sed -n '111,137p' packages/backend/src/api.ts
echo ""
echo "Broader context (lines 100-150):"
wc -l packages/backend/src/api.ts
else
echo "File not found. Searching for api.ts files:"
find . -name "api.ts" -type f 2>/dev/null | head -20
fiRepository: sourcebot-dev/sourcebot Length of output: 1094 🏁 Script executed: # Check for imports and logger availability
if [ -f "packages/backend/src/api.ts" ]; then
echo "=== Imports and class context ==="
head -50 packages/backend/src/api.ts
fiRepository: sourcebot-dev/sourcebot Length of output: 1925 🏁 Script executed: # Search for existing error handling patterns in the codebase
echo "=== Searching for octokit usage patterns ==="
rg -A 5 "new Octokit" --type ts packages/backend/src/Repository: sourcebot-dev/sourcebot Length of output: 805 🌐 Web query:
💡 Result: In Octokit JS, GitHub API failures are typically thrown as a
How to interpret / handle the status codes404 Not Found
403 Forbidden
429 Too Many Requests
Rate limit signals to check (403/429)GitHub’s guidance when rate-limited: (docs.github.com)
Practical Octokit patterntry {
await octokit.request("GET /repos/{owner}/{repo}", { owner, repo });
} catch (error) {
if (!error.status) throw error; // not an Octokit HTTP error
const status = error.status;
const headers = error.response?.headers || {};
const message = error.response?.data?.message;
if (status === 404) {
// not found OR not accessible
} else if (status === 403 || status === 429) {
const remaining = headers["x-ratelimit-remaining"];
const reset = headers["x-ratelimit-reset"];
const retryAfter = headers["retry-after"];
// decide whether this is rate limit vs permissions by checking remaining/message
} else {
// other HTTP errors
}
}Best practice: let Octokit throttle for youIf you want automatic handling/retries for rate limits, use Citations:
Handle GitHub API failures explicitly and prefer authenticated Octokit. The 🛠️ Proposed handling- const octokit = new Octokit();
- const response = await octokit.rest.repos.get({
- owner: parsed.data.owner,
- repo: parsed.data.repo,
- });
+ const octokit = process.env.GITHUB_TOKEN
+ ? new Octokit({ auth: process.env.GITHUB_TOKEN })
+ : new Octokit();
+
+ let response;
+ try {
+ response = await octokit.rest.repos.get({
+ owner: parsed.data.owner,
+ repo: parsed.data.repo,
+ });
+ } catch (err: any) {
+ const status = err?.status;
+ if (status === 404) {
+ res.status(404).json({ error: 'GitHub repo not found' });
+ return;
+ }
+ if (status === 401 || status === 403) {
+ res.status(403).json({ error: 'GitHub access denied' });
+ return;
+ }
+ if (status === 429) {
+ res.status(429).json({ error: 'GitHub rate limit exceeded' });
+ return;
+ }
+ logger.error({ err }, 'GitHub API request failed');
+ res.status(502).json({ error: 'GitHub API error' });
+ return;
+ }🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| public async dispose() { | ||
| return new Promise<void>((resolve, reject) => { | ||
| this.server.close((err) => { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| -- AlterTable | ||
| ALTER TABLE "Repo" ADD COLUMN "isAutoCleanupDisabled" BOOLEAN NOT NULL DEFAULT false; | ||
|
brendan-kellam marked this conversation as resolved.
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| import 'server-only'; | ||
|
|
||
| import { sew } from '@/actions'; | ||
| import { notFound, ServiceError } from '@/lib/serviceError'; | ||
| import { withOptionalAuthV2 } from '@/withAuthV2'; | ||
| import { RepoInfo } from './types'; | ||
|
|
||
| export const getRepoInfo = async (repoId: number): Promise<RepoInfo | ServiceError> => sew(() => | ||
| withOptionalAuthV2(async ({ prisma }) => { | ||
| const repo = await prisma.repo.findUnique({ | ||
| where: { id: repoId }, | ||
| include: { | ||
| jobs: { | ||
| orderBy: { | ||
| createdAt: 'desc', | ||
| }, | ||
| take: 1, | ||
| }, | ||
| }, | ||
| }); | ||
|
|
||
| if (!repo) { | ||
| return notFound(); | ||
| } | ||
|
|
||
| return { | ||
| id: repo.id, | ||
| name: repo.name, | ||
| displayName: repo.displayName, | ||
| imageUrl: repo.imageUrl, | ||
| isIndexed: repo.indexedAt !== null, | ||
| }; | ||
| }) | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,107 @@ | ||
| 'use client'; | ||
|
|
||
| import Image from 'next/image'; | ||
| import { SearchModeSelector } from "@/app/[domain]/components/searchModeSelector"; | ||
| import { Separator } from "@/components/ui/separator"; | ||
| import { ChatBox } from "@/features/chat/components/chatBox"; | ||
| import { ChatBoxToolbar } from "@/features/chat/components/chatBox/chatBoxToolbar"; | ||
| import { NotConfiguredErrorBanner } from "@/features/chat/components/notConfiguredErrorBanner"; | ||
| import { LanguageModelInfo, RepoSearchScope } from "@/features/chat/types"; | ||
| import { useCreateNewChatThread } from "@/features/chat/useCreateNewChatThread"; | ||
| import { getRepoImageSrc } from '@/lib/utils'; | ||
| import { useMemo, useState } from "react"; | ||
|
|
||
| interface LandingPageProps { | ||
| languageModels: LanguageModelInfo[]; | ||
| repoName: string; | ||
| repoDisplayName?: string; | ||
| imageUrl?: string | null; | ||
| repoId: number; | ||
| } | ||
|
|
||
| export const LandingPage = ({ | ||
| languageModels, | ||
| repoName, | ||
| repoDisplayName, | ||
| imageUrl, | ||
| repoId, | ||
| }: LandingPageProps) => { | ||
| const { createNewChatThread, isLoading } = useCreateNewChatThread(); | ||
| const [isContextSelectorOpen, setIsContextSelectorOpen] = useState(false); | ||
| const isChatBoxDisabled = languageModels.length === 0; | ||
|
|
||
| const selectedSearchScopes = useMemo(() => [ | ||
| { | ||
| type: 'repo', | ||
| name: repoDisplayName ?? repoName, | ||
| value: repoName, | ||
| codeHostType: 'github' as const, | ||
| } satisfies RepoSearchScope, | ||
| ], [repoDisplayName, repoName]); | ||
|
|
||
| const imageSrc = imageUrl ? getRepoImageSrc(imageUrl, repoId) : undefined; | ||
| const displayName = repoDisplayName ?? repoName; | ||
|
|
||
| return ( | ||
| <div className="min-h-screen flex flex-col justify-between p-4"> | ||
| {/* Center Section - Repository Info */} | ||
| <div className="flex-1 flex items-center justify-center"> | ||
| <div className="flex items-center gap-4"> | ||
| {imageSrc && ( | ||
| <Image | ||
| src={imageSrc} | ||
| alt={`${displayName} avatar`} | ||
| width={32} | ||
| height={32} | ||
| className="rounded-lg" | ||
| unoptimized={imageSrc.startsWith('/api/')} | ||
| /> | ||
| )} | ||
| <h1 className="text-2xl font-bold">{displayName}</h1> | ||
| </div> | ||
| </div> | ||
|
|
||
| {/* Bottom Section - ChatBox */} | ||
| <div className="flex justify-center pb-8"> | ||
| <div className="w-full max-w-[800px]"> | ||
| <div className="border rounded-md w-full shadow-sm"> | ||
| <ChatBox | ||
| onSubmit={(children) => { | ||
| createNewChatThread(children, selectedSearchScopes); | ||
| }} | ||
| className="min-h-[50px]" | ||
| isRedirecting={isLoading} | ||
| languageModels={languageModels} | ||
| selectedSearchScopes={selectedSearchScopes} | ||
| searchContexts={[]} | ||
| onContextSelectorOpenChanged={setIsContextSelectorOpen} | ||
| isDisabled={isChatBoxDisabled} | ||
| /> | ||
| <Separator /> | ||
| <div className="relative"> | ||
| <div className="w-full flex flex-row items-center bg-accent rounded-b-md px-2"> | ||
| <ChatBoxToolbar | ||
| languageModels={languageModels} | ||
| repos={[]} | ||
| searchContexts={[]} | ||
| selectedSearchScopes={selectedSearchScopes} | ||
| onSelectedSearchScopesChange={() => {}} | ||
| isContextSelectorOpen={isContextSelectorOpen} | ||
| onContextSelectorOpenChanged={setIsContextSelectorOpen} | ||
| /> | ||
| <SearchModeSelector | ||
| searchMode="agentic" | ||
| className="ml-auto" | ||
| /> | ||
| </div> | ||
| </div> | ||
| </div> | ||
|
|
||
| {isChatBoxDisabled && ( | ||
| <NotConfiguredErrorBanner className="mt-4" /> | ||
| )} | ||
| </div> | ||
| </div> | ||
| </div> | ||
| ) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: sourcebot-dev/sourcebot
Length of output: 698
🏁 Script executed:
Repository: sourcebot-dev/sourcebot
Length of output: 1265
🌐 Web query:
Zod string trim() min() validation API documentation💡 Result:
In Zod, both
.trim()and.min()are chainable methods onz.string()schemas:.trim(): a transformation that trims leading/trailing whitespace. Example shown in the official “Strings” guide:z.string().trim();(commented “trim whitespace”). [1].min(length, options?): a validation that enforces a minimum string length. Example:z.string().min(5);and with a custom message:z.string().min(5, { message: "Must be 5 or more characters long" });. [1]Typical usage to require a non-empty string after trimming:
(Chaining is the standard Zod API style—e.g.,
z.string().min(5).max(10).toLowerCase()). [2]Sources: Zod strings guide [1], Zod v4 package docs [2].
Tighten owner/repo validation.
Empty strings (including whitespace-only strings) currently pass validation and would fail when passed to the GitHub API. Add
.trim().min(1)to both fields to validate early and prevent unnecessary API calls.🔧 Suggested fix
📝 Committable suggestion
🤖 Prompt for AI Agents