This example shows how you can use the Github API to automatically create issues for the latest trending repositories.

Keywords: vault, agent, github

Overview

Let’s first break down the problem into smaller parts. To solve this, we basically need two agents:

  1. An agent that can scrape the latest trending repositories from Github.
  2. An agent that can create an issue on Github for a given repository.

Furthermore, we need to remember which issues we have already created to avoid duplicates. We will use a .csv file as a simple database to store the issues we have already created.

Setup environment variables & python environment

First, you need to set up the following environment variables:

# Notte credentials.
NOTTE_API_KEY=<your-notte-api-key>

# Github credentials: make sure to set up MFA secret to use this.
GITHUB_COM_EMAIL=<your-github-email>
GITHUB_COM_PASSWORD=<your-github-password>
GITHUB_COM_MFA_SECRET=<your-github-mfa-secret>

Make sure to also create a new python environment and install the following packages:

pip install notte-sdk pandas halo

Let’s start by defining the types required to scrape the latest trending repositories from Github. Trending repositories are defined by their:

  • Organization name
  • Repository name
  • URL
  • Description
  • Number of stars
  • Number of forks

For issues, we simply need the issue URL and a boolean indicating if the issue has been created.

types.py
from pydantic import BaseModel
from typing import Annotated

class TrendingRepo(BaseModel):
    org: Annotated[str, "The organization name of the repository. E.g. 'example_org'"]
    repo: Annotated[str, "The repository name. E.g. 'example_repo'"]
    url: Annotated[str, "The URL of the repository. E.g. 'https://github.com/example_org/example_repo'"]
    desc: Annotated[str, "The description of the repository. E.g. 'This is an example repository'"]
    n_stars: Annotated[int | None, "The number of stars of the repository. E.g. 100"]
    n_forks: Annotated[int | None, "The number of forks of the repository. E.g. 100"]


class TrendingRepos(BaseModel):
    trending: list[TrendingRepo]

Our trending repo scraping agent, doesn’t need to be a multi-step agent. We can simply scrape the latest trending repositories from Github in one go. Github already reports the trending repos at https://github.com/trending. We just need to scrape the page and use structured output to get the trending repos:

trending_repo_scraper.py
from notte_sdk import NotteClient, retry
from dotenv import load_dotenv

_ = load_dotenv()

client = NotteClient()

@retry(max_tries=3, delay_seconds=5, error_message="Failed to fetch trending repos. Try again later...")
def fetch_trending_repos() -> list[TrendingRepo]:
    data = client.scrape(
        url="https://github.com/trending",
        response_format=TrendingRepos,
        instructions="Retrieve the top 3 trending repositories",
        use_llm=True,
    )
    trending_repos: TrendingRepos = data.structured.get()  # type: ignore
    return trending_repos.trending

Step 2: Create Github Issue Agent

Safely store your Github credentials

To post an issue on Github, we need to be logged in with a valid Github account. Notte allows you to safely store your Github credentials in a vault.

vault.py
from notte_sdk import NotteClient
from notte_sdk.endpoints.vaults import NotteVault
from halo import Halo  # type: ignore
import os
from loguru import logger

def get_or_create_vault() -> NotteVault:
    vault_id = os.getenv("NOTTE_VAULT_ID")
    if vault_id is not None and len(vault_id) > 0:
        return client.vaults.get(vault_id)
    # create a new vault and save it the `.env` file
    with Halo(text="Creating a new vault ", spinner="dots"):
        vault = client.vaults.create()
        vault_id = vault.vault_id
        logger.info(f"Vault created with id: {vault_id}. Storing it in .env file...")
        # store vault id in .env file
        with open(".env", "a") as f:
            _ = f.write(f"NOTTE_VAULT_ID={vault_id}\n")
        # get vault
        logger.info(f"Loading vault with id: {vault_id}...")

        logger.info("Added github credentials to vault...")
        _ = vault.add_credentials_from_env(url="https://github.com")
        return vault

Create Github Issue Agent

This is the final step. We will notte agents to create a Github issue for all the trending repos fetching in the previous step.

github_issues.py
from notte_sdk import NotteClient, retry
from notte_sdk.endpoints.vaults import NotteVault
from pydantic import BaseModel

class RepoIssue(BaseModel):
    issue_url: str
    created_issue: bool

_ = load_dotenv()

client = NotteClient()

# TODO: update the prompt based on your needs
ISSUE_TASK_PROMPT = r"""
Look for github issues on the repo {repo_url} with the following details:
- Title: "{repo}: a great repo"
- Body: "This has to be the best issue I have ever posted in my life"

If the issue doesn't exist, create it. If it does exist, your job is done.
CRITICAL: Your output has to be a valid JSON object with the following structure:

{{
    "url": "url_of_the_issue",
    "existed": bool
}}
"""

@retry(max_tries=3, delay_seconds=5, error_message="Failed to create issue. Try again later...")
def create_github_issue(repo: TrendingRepo, vault: NotteVault) -> RepoIssue | None:
    with client.Session(
        proxies=True,
        timeout_minutes=3,
        chrome_args=[],
    ) as session:
        agent = client.Agent(session=session, vault=vault)
        response = agent.run(
            task=ISSUE_TASK_PROMPT.format(repo_url=repo.url, repo=repo.repo),
            url="https://github.com",
        )
    if not response.success:
        error_msg = f"Agent {agent.agent_id} failed to create issue for {repo.url}: {response.answer}"
        logger.error(error_msg)
        raise Exception(error_msg)

    if response.answer:
        issue_data = json.loads(response.answer)
        issue_url = issue_data.get("url")
        if issue_data and issue_data.get("existed"):
            print(f"Issue already exists at: {issue_data.get('url')}")
            return RepoIssue(issue_url=issue_url, created_issue=False)
        elif issue_data:
            print(f"Successfully created issue: {issue_data.get('url')}")
            return RepoIssue(issue_url=issue_url, created_issue=True)
    return None

Put all parts together

In previous steps, we have defined both create_github_issue and fetch_trending_repos agents. Now, we can put all parts together.

But before that, as discussed in the overview, we need to store the trending repos in a csv file to avoid duplicates. We will define a CsvLogger class to do this:

csv_logger.py
import pandas as pd
import os
from pathlib import Path
from typing import Any

class TrendingRepoWithIssue(TrendingRepo, RepoIssue):
    pass


class CsvLogger:
    csv_path: Path = Path("trending.csv")
    trending: pd.DataFrame

    def __init__(self):
        if not self.csv_path.exists():
            df = pd.DataFrame(
                [],
                columns=list(TrendingRepoWithIssue.model_fields.keys()),
            )
            df.to_csv(self.csv_path, index=False)

        self.trending = pd.read_csv(self.csv_path)  # type: ignore

    def log(self, data: list[TrendingRepoWithIssue]):
        to_add: list[dict[str, Any]] = []

        for issue in data:
            if self.check_if_issue_exists(issue):
                logger.info(f"Issue already exists at: {issue.issue_url}. Skipping...")
                continue

            to_add.append(issue.model_dump())

        self.trending = pd.concat((self.trending, pd.DataFrame(to_add)))
        self.trending.to_csv(self.csv_path, index=False)

    def check_if_issue_exists(self, repo: TrendingRepo) -> bool:
        return any(repo.url == self.trending.url)  # type: ignore

Finally, we can put all parts together:

main.py


def create_new_issues():
    csv_logger = CsvLogger()
    issues_to_add: list[TrendingRepoWithIssue] = []
    vault = get_or_create_vault()

    with Halo(text="Fetching the trending repos ", spinner="dots"):
        trending_repos = fetch_trending_repos()

    for repo in trending_repos:
        if csv_logger.check_if_issue_exists(repo):
            continue
        with Halo(text=f"Creating issue for {repo.repo} ", spinner="dots"):
            issue = create_github_issue(repo, vault)

        if issue is not None:
            issues_to_add.append(TrendingRepoWithIssue(**repo.model_dump(), **issue.model_dump()))

    csv_logger.log(issues_to_add)

if __name__ == "__main__":
    create_new_issues()

Conclusion

In this example, we have seen how to use Notte to create a Github issue for all the trending repos. We have also seen how to safely store your Github credentials in a vault.

In a few lines of code, we have been able to create a Github issue for all the trending repos which automatically logs the issues created in a csv file.

You can find the full code in the Github repo along with many other examples.