Skip to content
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

Add ability to manually specify github token #104

Merged
merged 17 commits into from
Oct 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
e349612
Add GitHub token management to RepoState in repo.ts for enhanced auth…
AtlantisPleb Oct 24, 2024
f89cd88
Create file components/GitHubTokenInput.tsx
AtlantisPleb Oct 24, 2024
238e024
Added GitHubTokenInput component import to layout.tsx for enhanced Gi…
AtlantisPleb Oct 24, 2024
de6e2ca
Add `useRepoStore` import to `githubUtils.ts` for enhanced state mana…
AtlantisPleb Oct 24, 2024
6b6a8a4
Refactor `useChatCore.ts` to improve code readability and maintainabi…
AtlantisPleb Oct 24, 2024
c223e69
Refactor `route.ts` to remove unused imports and streamline module de…
AtlantisPleb Oct 24, 2024
71b6477
Removed unused `getGitHubToken` import and added `createFileTool` to …
AtlantisPleb Oct 24, 2024
5712b7e
useclient on input component
AtlantisPleb Oct 24, 2024
95119c9
Refactor `githubUtils.ts` by removing unused `useRepoStore` import to…
AtlantisPleb Oct 24, 2024
77c1b77
Add `getGitHubToken` import to handle GitHub authentication in `index…
AtlantisPleb Oct 24, 2024
459477d
Create file docs/github_token.md
AtlantisPleb Oct 24, 2024
2ab2308
Create file components/GitHubTokenDialog.tsx
AtlantisPleb Oct 24, 2024
351439c
Add GitHubTokenDialog component to authenticated section in Page.
AtlantisPleb Oct 24, 2024
b768f3e
Remove GitHubTokenInput component and update metadata in layout.tsx f…
AtlantisPleb Oct 24, 2024
8e88b4e
Refactor GitHubTokenInput to use custom Button and Input components f…
AtlantisPleb Oct 24, 2024
5c4e686
Refactor GitHubTokenDialog for clarity and maintainability, no functi…
AtlantisPleb Oct 24, 2024
afed37a
token
AtlantisPleb Oct 24, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion app/[[...rest]]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Authenticated, Unauthenticated } from "convex/react"
import { HomeAuthed } from "@/components/home"
import { Lander } from "@/components/landing/Lander"
import ResetHUDButton from "@/components/ResetHUDButton"
import { GitHubTokenDialog } from "@/components/GitHubTokenDialog"

export default function Page() {
return (
Expand All @@ -12,8 +13,9 @@ export default function Page() {
</Unauthenticated>
<Authenticated>
<ResetHUDButton />
<GitHubTokenDialog />
<HomeAuthed />
</Authenticated>
</>
)
}
}
8 changes: 6 additions & 2 deletions app/api/chat/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,18 @@ const convex = new ConvexHttpClient(process.env.NEXT_PUBLIC_CONVEX_URL!);
export async function POST(req: Request) {
const body = await req.json();
const threadId = body.threadId as Id<"threads">;
const githubToken = body.githubToken as string | undefined;

if (!threadId) {
console.error("Invalid threadId");
return new Response('Invalid threadId', { status: 400 });
}

const toolContext = await getToolContext(body);
const tools = getTools(toolContext, body.tools);

// Pass the GitHub token to getTools
const tools = getTools(toolContext, body.tools, githubToken);

const { userId } = auth();
if (!userId) {
console.error("Unauthorized: No userId found");
Expand Down Expand Up @@ -80,4 +84,4 @@ export async function POST(req: Request) {
// }

return result.toAIStreamResponse();
}
}
2 changes: 1 addition & 1 deletion app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,4 @@ export default function RootLayout({
</body>
</html>
)
}
}
29 changes: 29 additions & 0 deletions components/GitHubTokenDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
"use client"

import React from 'react';
import { Button } from '@/components/ui/button';
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
import { GitHubTokenInput } from '@/components/GitHubTokenInput';
import { Github } from 'lucide-react';

export const GitHubTokenDialog: React.FC = () => {
return (
<Dialog>
<DialogTrigger asChild>
<Button variant="outline" size="icon" className="fixed bottom-4 left-20 z-50">
<Github className="size-4" />
<span className="sr-only">Set GitHub Token</span>
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Set GitHub Token</DialogTitle>
<DialogDescription>
If you have a GitHub account connected, we&apos;ll use your GitHub token from that. You can also manually specify one here.
</DialogDescription>
</DialogHeader>
<GitHubTokenInput />
</DialogContent>
</Dialog>
);
};
32 changes: 32 additions & 0 deletions components/GitHubTokenInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
"use client"

import React, { useState } from "react"
import { useRepoStore } from "../store/repo"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"

export function GitHubTokenInput() {
const [token, setToken] = useState('');
const setGithubToken = useRepoStore((state) => state.setGithubToken);

const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
setGithubToken(token);
setToken(''); // Clear the input field after submission
};

return (
<form onSubmit={handleSubmit} className="flex flex-col space-y-2">
<Input
type="text"
value={token}
onChange={(e) => setToken(e.target.value)}
placeholder="Enter your GitHub token"
className="w-full"
/>
<Button type="submit" className="w-full">
Set GitHub Token
</Button>
</form>
);
}
97 changes: 97 additions & 0 deletions docs/github_token.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# GitHub Token Handling in OpenAgents

This document explains how GitHub tokens are handled in the OpenAgents application, including the process of manual token setting and fallback mechanisms.

## Overview

OpenAgents uses GitHub tokens to authenticate API requests to GitHub, allowing users to interact with repositories, issues, and pull requests. The application supports two methods of obtaining a GitHub token:

1. Manually set by the user
2. Automatically retrieved from the user's Clerk account (if connected to GitHub)

## Token Retrieval Process

The token retrieval process follows this order:

1. Check for a manually set token
2. If no manual token is found, attempt to retrieve the token from the user's Clerk account
3. If no token is available from either source, the application will handle the absence of a token gracefully

## Implementation Details

### Manual Token Setting

Users can manually set their GitHub token through the `GitHubTokenInput` component. This token is stored in the application's state using the `useRepoStore` from `store/repo.ts`.

### Token Retrieval in `getToolContext`

The `getToolContext` function in `tools/index.ts` is responsible for assembling the context used by various tools in the application. It handles GitHub token retrieval as follows:

```typescript
export const getToolContext = async (body: ToolContextBody): Promise<ToolContext> => {
// ... other context setup ...

// Use the manually set token if available, otherwise fall back to the Clerk user's token
let finalGitHubToken = githubToken;
if (!finalGitHubToken && user) {
finalGitHubToken = await getGitHubToken(user);
}

return {
// ... other context properties ...
gitHubToken: finalGitHubToken,
};
};
```

### Clerk Token Retrieval

If no manual token is set, the application attempts to retrieve the GitHub token from the user's Clerk account. This is done in the `getGitHubToken` function in `lib/github/isGitHubUser.ts`:

```typescript
export async function getGitHubToken(user: User) {
const tokenResponse = await clerkClient().users.getUserOauthAccessToken(user.id, 'oauth_github');
if (tokenResponse.data.length > 0) {
return tokenResponse.data[0].token;
} else {
console.log("No GitHub token found for user:", user.id);
return undefined;
}
}
```

## Usage in Tools

Individual tools that require GitHub authentication use the `gitHubToken` from the provided context. For example, in `tools/view-file.ts`:

```typescript
if (!context.gitHubToken) {
return {
success: false,
error: "Missing GitHub token",
summary: "Failed to view file due to missing GitHub token",
details: "The GitHub token is missing. Please ensure it is provided in the context."
};
}
```

## Handling Missing Tokens

If no GitHub token is available (either manually set or from Clerk), the tools are designed to handle this gracefully, typically by returning an error message indicating that the GitHub token is missing.

## Best Practices

1. Always check for the presence of a GitHub token before attempting to make GitHub API requests.
2. Provide clear feedback to the user if a GitHub token is required but not available.
3. Encourage users to either manually set their GitHub token or connect their GitHub account to their Clerk profile for seamless authentication.

## Troubleshooting

If you encounter issues with GitHub authentication:

1. Check if the user has manually set a GitHub token.
2. Verify if the user's Clerk account is connected to GitHub.
3. Ensure that the GitHub token has the necessary permissions for the required operations.
4. Check the console logs for any error messages related to token retrieval or GitHub API requests.

By following this token handling process, OpenAgents ensures a flexible and robust approach to GitHub authentication, accommodating both manual token entry and automated token retrieval through Clerk.
13 changes: 11 additions & 2 deletions hooks/chat/useChatCore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,22 @@ export function useChat({ propsId, onTitleUpdate }: { propsId?: Id<"threads">, o

const model = useModelStore((state) => state.model)
const repo = useRepoStore((state) => state.repo)
const githubToken = useRepoStore((state) => state.githubToken)
const tools = useToolStore((state) => state.tools)
const setBalance = useBalanceStore((state) => state.setBalance)

const vercelChatProps = useVercelChat({
id: threadId?.toString(),
initialMessages: threadData.messages as VercelMessage[],
body: { model: model.id, tools, threadId, repoOwner: repo?.owner, repoName: repo?.name, repoBranch: repo?.branch },
body: {
model: model.id,
tools,
threadId,
repoOwner: repo?.owner,
repoName: repo?.name,
repoBranch: repo?.branch,
githubToken: githubToken // Add the GitHub token to the body
},
maxToolRoundtrips: 20,
onFinish: async (message, options) => {
if (threadId && user) {
Expand Down Expand Up @@ -165,4 +174,4 @@ export function useChat({ propsId, onTitleUpdate }: { propsId?: Id<"threads">, o
error,
stop: vercelChatProps.stop,
}
}
}
3 changes: 1 addition & 2 deletions lib/githubUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ const githubListUserReposArgsSchema = z.object({
});

export async function githubListUserRepos(args: z.infer<typeof githubListUserReposArgsSchema>): Promise<any[]> {
console.log("are we here")
const { token, perPage, sort, direction } = githubListUserReposArgsSchema.parse(args);
const params = new URLSearchParams();

Expand Down Expand Up @@ -125,4 +124,4 @@ export async function githubDeleteFile(args: {

// Send DELETE request
await githubApiRequest(fileUrl, token, 'DELETE', body);
}
}
4 changes: 4 additions & 0 deletions panes/changelog/ChangelogPane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ const ChangelogPane: React.FC = () => {
<Card className="w-full h-full bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
<CardContent>
<ScrollArea className="py-4 h-[calc(100%-2rem)] pr-4">
<h2 className="mb-2 font-semibold">Oct 24</h2>
<ul className="space-y-2 list-disc list-inside">
<li className="text-sm text-foreground/80">Added ability to manually specify GitHub token</li>
</ul>
<h2 className="mb-2 font-semibold">Aug 29</h2>
<ul className="space-y-2 list-disc list-inside">
<li className="text-sm text-foreground/80">Added tool to create GitHub issue</li>
Expand Down
8 changes: 6 additions & 2 deletions store/repo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,21 @@ interface Repo {
interface RepoState {
repo: Repo | null;
setRepo: (repo: Repo | null) => void;
githubToken: string | null;
setGithubToken: (token: string | null) => void;
}

export const useRepoStore = create<RepoState>()(
persist(
(set) => ({
repo: null,
setRepo: (repo) => set({ repo }),
githubToken: null,
setGithubToken: (token) => set({ githubToken: token }),
}),
{
name: 'openagents-repo-storage-10',
partialize: (state) => ({ repo: state.repo }),
name: 'openagents-repo-storage-11',
partialize: (state) => ({ repo: state.repo, githubToken: state.githubToken }),
}
)
)
27 changes: 17 additions & 10 deletions tools/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { getGitHubToken } from "@/lib/github/isGitHubUser"
import { models } from "@/lib/models"
import { Model, Repo, ToolContext } from "@/types"
import { bedrock } from "@ai-sdk/amazon-bedrock"
import { anthropic } from "@ai-sdk/anthropic"
import { openai } from "@ai-sdk/openai"
import { currentUser, User } from "@clerk/nextjs/server"
import { getGitHubToken } from "@/lib/github/isGitHubUser"
import { closeIssueTool } from "./close-issue"
import { closePullRequestTool } from "./close-pull-request"
import { createBranchTool } from "./create-branch"
Expand All @@ -15,7 +15,6 @@ import { fetchGitHubIssueTool } from "./fetch-github-issue"
import { listOpenIssuesTool } from "./list-open-issues"
import { listPullRequestsTool } from "./list-pull-requests"
import { openIssueTool } from "./open-issue"
// import { listReposTool } from "./list-repos"
import { postGitHubCommentTool } from "./post-github-comment"
import { rewriteFileTool } from "./rewrite-file"
import { scrapeWebpageTool } from "./scrape-webpage"
Expand All @@ -26,7 +25,6 @@ import { viewPullRequestTool } from "./view-pull-request"

export const allTools = {
create_file: { tool: createFileTool, description: "Create a new file at path with content" },
// list_repos: { tool: listReposTool, description: "List all repositories for the authenticated user" },
rewrite_file: { tool: rewriteFileTool, description: "Rewrite file at path with new content" },
scrape_webpage: { tool: scrapeWebpageTool, description: "Scrape webpage for information" },
view_file: { tool: viewFileTool, description: "View file contents at path" },
Expand All @@ -47,11 +45,14 @@ export const allTools = {

type ToolName = keyof typeof allTools;

export const getTools = (context: ToolContext, toolNames: ToolName[]) => {
export const getTools = (context: ToolContext, toolNames: ToolName[], githubToken?: string) => {
const tools: Partial<Record<ToolName, ReturnType<typeof allTools[ToolName]["tool"]>>> = {};
toolNames.forEach(toolName => {
if (allTools[toolName]) {
tools[toolName] = allTools[toolName].tool(context);
tools[toolName] = allTools[toolName].tool({
...context,
gitHubToken: githubToken || context.gitHubToken
});
}
});
return tools;
Expand All @@ -61,18 +62,18 @@ interface ToolContextBody {
repoOwner: string;
repoName: string;
repoBranch: string;
model: string; // Change this to expect just the model ID
model: string;
githubToken?: string;
}

export const getToolContext = async (body: ToolContextBody): Promise<ToolContext> => {
const { repoOwner, repoName, repoBranch, model: modelId } = body;
const { repoOwner, repoName, repoBranch, model: modelId, githubToken } = body;
const repo: Repo = {
owner: repoOwner,
name: repoName,
branch: repoBranch
};
const user = await currentUser();
const gitHubToken = user ? await getGitHubToken(user) : undefined;
const firecrawlToken = process.env.FIRECRAWL_API_KEY;

// Find the full model object based on the provided model ID
Expand All @@ -96,11 +97,17 @@ export const getToolContext = async (body: ToolContextBody): Promise<ToolContext
throw new Error(`Unsupported model provider: ${modelObj.provider}`);
}

// Use the manually set token if available, otherwise fall back to the Clerk user's token
let finalGitHubToken = githubToken;
if (!finalGitHubToken && user) {
finalGitHubToken = await getGitHubToken(user);
}

return {
repo,
user: user as User | null,
gitHubToken,
gitHubToken: finalGitHubToken,
firecrawlToken,
model
};
};
};
Loading