-
Notifications
You must be signed in to change notification settings - Fork 88
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(tools): Add GitHub Issues tool (#150)
# GitHub Issues Tool This PR adds a new tool to fetch and analyze issues from GitHub repositories. ## Features - Fetch issues from any public GitHub repository - Support for both authenticated and unauthenticated requests - Configurable issue limit - Structured response format with repository metadata - Built-in error handling for API limits and invalid requests ## Usage Example ```js const tool = new GithubIssues({ token: 'github_pat_...', // optional limit: 10 // optional, defaults to 10 }); const result = await tool.call({ repoUrl: 'https://github.com/owner/repo' }); ``` ## Rate Limits - Authenticated: 5,000 requests/hour - Unauthenticated: 60 requests/hour ## Testing - Unit tests cover success and error cases - Storybook preview available for manual testing - CI checks for linting and formatting
- Loading branch information
Showing
10 changed files
with
415 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
File renamed without changes.
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,160 @@ | ||
/** | ||
* GitHub Issues | ||
* | ||
* This tool integrates with GitHub's API to fetch issues from specified repositories. | ||
* It provides a clean, structured way to retrieve open issues, making it ideal for | ||
* monitoring and analysis purposes. | ||
* | ||
* Key features: | ||
* - Fetches open issues from any public GitHub repository | ||
* - Handles pagination automatically | ||
* - Returns structured data with issue details | ||
* - Includes metadata like issue numbers, titles, labels, and descriptions | ||
* | ||
* Authentication: | ||
* - Works without authentication for public repositories (60 requests/hour limit) | ||
* - Optional GitHub token for higher rate limits (5,000 requests/hour) | ||
* | ||
* Usage: | ||
* const tool = new GithubIssues({ | ||
* token: 'github_pat_...', // optional: GitHub personal access token | ||
* limit: 20 // optional: number of issues to fetch (default: 10) | ||
* }); | ||
* const result = await tool._call({ | ||
* repoUrl: 'https://github.com/owner/repo' | ||
* }); | ||
* | ||
* Rate Limits: | ||
* - Authenticated: 5,000 requests per hour | ||
* - Unauthenticated: 60 requests per hour | ||
* | ||
* For more information about GitHub's API, visit: https://docs.github.com/en/rest | ||
*/ | ||
|
||
import { Tool } from '@langchain/core/tools'; | ||
import { z } from 'zod'; | ||
import ky from 'ky'; | ||
import { HTTPError } from 'ky'; | ||
|
||
export class GithubIssues extends Tool { | ||
constructor(fields = {}) { | ||
super(fields); | ||
this.name = 'github-issues'; | ||
this.token = fields.token; | ||
this.limit = fields.limit || 10; | ||
this.description = | ||
'Fetches open issues from a specified GitHub repository. Input should include the repository URL.'; | ||
|
||
this.schema = z.object({ | ||
repoUrl: z | ||
.string() | ||
.url() | ||
.describe( | ||
'The GitHub repository URL (e.g., https://github.com/owner/repo)' | ||
), | ||
}); | ||
|
||
this.httpClient = ky; | ||
} | ||
|
||
async _call(input) { | ||
try { | ||
const { owner, repo } = this._parseRepoUrl(input.repoUrl); | ||
const issues = await this._fetchIssues({ owner, repo }); | ||
return this._formatResponse(issues, { owner, repo }); | ||
} catch (error) { | ||
if (error instanceof HTTPError) { | ||
const statusCode = error.response.status; | ||
let errorType = 'Unknown'; | ||
if (statusCode >= 400 && statusCode < 500) { | ||
errorType = 'Client Error'; | ||
} else if (statusCode >= 500) { | ||
errorType = 'Server Error'; | ||
} | ||
return `API request failed: ${errorType} (${statusCode})`; | ||
} else { | ||
return `An unexpected error occurred: ${error.message}`; | ||
} | ||
} | ||
} | ||
|
||
async _fetchIssues({ owner, repo }) { | ||
let page = 1; | ||
let allIssues = []; | ||
const headers = { | ||
Accept: 'application/vnd.github.v3+json', | ||
}; | ||
|
||
if (this.token) { | ||
headers.Authorization = `Bearer ${this.token}`; | ||
} | ||
|
||
let hasMorePages = true; | ||
while (hasMorePages) { | ||
const issues = await this.httpClient | ||
.get(`https://api.github.com/repos/${owner}/${repo}/issues`, { | ||
searchParams: { | ||
page: String(page), | ||
per_page: '100', | ||
state: 'open', | ||
}, | ||
headers, | ||
timeout: 10000, | ||
}) | ||
.json(); | ||
|
||
if (!Array.isArray(issues) || issues.length === 0) { | ||
hasMorePages = false; | ||
break; | ||
} | ||
|
||
allIssues = allIssues.concat(issues); | ||
if (allIssues.length >= this.limit) { | ||
allIssues = allIssues.slice(0, this.limit); | ||
hasMorePages = false; | ||
break; | ||
} | ||
page++; | ||
} | ||
|
||
return allIssues; | ||
} | ||
|
||
_formatResponse(issues, input) { | ||
const { owner, repo } = input; | ||
const repoUrl = `https://github.com/${owner}/${repo}`; | ||
const today = new Date().toISOString().split('T')[0]; | ||
|
||
return { | ||
repository: { | ||
name: repo, | ||
url: repoUrl, | ||
owner: owner, | ||
}, | ||
metadata: { | ||
totalIssues: issues.length, | ||
lastUpdated: today, | ||
limit: this.limit, | ||
}, | ||
issues: issues.map((issue) => ({ | ||
number: issue.number, | ||
title: issue.title, | ||
url: issue.html_url, | ||
labels: issue.labels.map((label) => label.name), | ||
description: issue.body || 'No description provided', | ||
})), | ||
}; | ||
} | ||
|
||
_parseRepoUrl(url) { | ||
try { | ||
const path = new URL(url).pathname.split('/').filter(Boolean); | ||
if (path.length < 2) { | ||
throw new Error('Invalid GitHub repository URL'); | ||
} | ||
return { owner: path[0], repo: path[1] }; | ||
} catch (_error) { | ||
throw new Error('Invalid GitHub repository URL'); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
import { ToolPreviewer } from '../_utils/ToolPreviewer.jsx'; | ||
import { AgentWithToolPreviewer } from '../_utils/AgentWithToolPreviewer.jsx'; | ||
import { GithubIssues } from './index.js'; | ||
import { Agent, Task, Team } from '../../../../src/index'; | ||
import React from 'react'; | ||
|
||
export default { | ||
title: 'Tools/Github Issues', | ||
parameters: { | ||
layout: 'centered', | ||
}, | ||
tags: ['autodocs'], | ||
argTypes: {}, | ||
}; | ||
|
||
const githubTool = new GithubIssues({ | ||
token: import.meta.env.VITE_GITHUB_TOKEN, | ||
limit: 5, | ||
}); | ||
|
||
export const Default = { | ||
render: (args) => <ToolPreviewer {...args} />, | ||
args: { | ||
toolInstance: githubTool, | ||
callParams: { | ||
repoUrl: 'https://github.com/facebook/react', | ||
}, | ||
}, | ||
}; | ||
|
||
// Create an agent with the GitHub tool | ||
const issueAnalyzer = new Agent({ | ||
name: 'Issue Analyzer', | ||
role: 'GitHub Repository Inspector', | ||
goal: 'Analyze and summarize GitHub repository issues', | ||
tools: [githubTool], | ||
}); | ||
|
||
// Create an analysis task | ||
const issueAnalysisTask = new Task({ | ||
description: | ||
'Fetch and analyze issues from the following repository: {repoUrl}', | ||
agent: issueAnalyzer, | ||
expectedOutput: 'A structured summary of repository issues', | ||
}); | ||
|
||
// Create the team | ||
const team = new Team({ | ||
name: 'Repository Analysis Unit', | ||
description: 'Specialized team for GitHub repository issue analysis', | ||
agents: [issueAnalyzer], | ||
tasks: [issueAnalysisTask], | ||
inputs: { | ||
repoUrl: 'https://github.com/facebook/react', | ||
}, | ||
env: { | ||
OPENAI_API_KEY: import.meta.env.VITE_OPENAI_API_KEY, | ||
}, | ||
}); | ||
|
||
export const withAgent = { | ||
render: (args) => <AgentWithToolPreviewer {...args} />, | ||
args: { | ||
team: team, | ||
}, | ||
}; |
Oops, something went wrong.