Skip to content

Commit

Permalink
feat(tools): Add GitHub Issues tool (#150)
Browse files Browse the repository at this point in the history
# 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
darielnoel authored Nov 15, 2024
2 parents 663b5aa + 519e7f2 commit 59383e5
Show file tree
Hide file tree
Showing 10 changed files with 415 additions and 3 deletions.
6 changes: 6 additions & 0 deletions packages/tools/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,12 @@ Key features:

Learn more: https://www.wolframalpha.com/

### 6. Github Issues

Github Issues is a tool that allows agents to interact with the Github API, enabling them to fetch issues from a repository.

Learn more: https://docs.github.com/en/rest/issues/issues

## Development

### Local Setup
Expand Down
9 changes: 8 additions & 1 deletion packages/tools/rollup.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,14 @@ import json from '@rollup/plugin-json';
import nodePolyfills from 'rollup-plugin-node-polyfills'; // Correct plugin name

// Array of tool folder names
const toolFolders = ['firecrawl', 'tavily', 'serper', 'exa', 'wolfram-alpha']; // Add more folder names as needed
const toolFolders = [
'firecrawl',
'tavily',
'serper',
'exa',
'wolfram-alpha',
'github-issues',
]; // Add more folder names as needed

const toolConfigs = toolFolders.map((tool) => {
const inputPath = `src/${tool}/index.js`; // Adjusted for plain JavaScript
Expand Down
2 changes: 0 additions & 2 deletions packages/tools/src/_utils/ToolPreviewer.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,6 @@ export const ToolPreviewer = ({ toolInstance, callParams }) => {
const handleToolCall = async () => {
setIsLoading(true);
try {
// Use the params state instead of callParams
console.log('params', params);
const result = await toolInstance._call(params);
// setOutput(result);
setOutput(
Expand Down
File renamed without changes.
160 changes: 160 additions & 0 deletions packages/tools/src/github-issues/index.js
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');
}
}
}
66 changes: 66 additions & 0 deletions packages/tools/src/github-issues/tool.stories.jsx
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,
},
};
Loading

0 comments on commit 59383e5

Please sign in to comment.