The integration of AI with external systems has revolutionized how we interact with complex workflows. Rather than only generating text, modern Large Language Models (LLMs) can now perform real-world actions like creating pull requests, managing databases, sending emails, and orchestrating entire development workflows. This capability transforms LLMs from passive text generators into active agents that can execute tasks across multiple platforms and services.
In this post, we'll explore how AI tools work, demonstrate a practical example of creating GitHub pull requests through natural language commands, and show you how to build this functionality using Python, LangGraph and GitHub.
What are Tools for LLMs?
Tools for LLMs are external functions or APIs that language models can invoke to perform actions beyond text generation. They act as bridges between the AI's reasoning capabilities and real-world systems, enabling the model to:
- Interact with APIs and web services
- Execute code and scripts
- Access databases and file systems
- Perform calculations and data processing
- Integrate with third-party platforms
These tools are typically defined with schemas that describe their parameters, expected inputs, and outputs, allowing the LLM to understand when and how to use them appropriately.
How They Work
The tool integration process follows a structured workflow:
- Tool Definition: Developers define available tools with clear schemas, including function signatures, parameter types, and descriptions.
- Context Awareness: The LLM receives information about available tools along with the user's request.
- Decision Making: Based on the user's intent, the LLM decides which tools to use and determines the appropriate parameters.
- Function Calling: The LLM generates structured function calls with the required parameters.
- Execution: The system executes the tool functions and returns results to the LLM.
- Response Generation: The LLM processes the results and generates a human-readable response.
This process enables seamless integration between natural language interfaces and complex system operations.
Example: GitHub Pull Request Creation
Now, let's build a practical example where you can tell an LLM: "Create a pull request in GitHub from branch feature-auth to main, and add a description explaining the new authentication system implementation."
Setup and Installation
If you are lazy (like me), you can just clone this repo: https://github.com/liblaber/ai-github-agent-example
Otherwise, you can setup the project step by step using python:
# Create a new directory
mkdir ai-github-agent
# Add dependencies
pip install langgraph langchain-core langchain-openai requests python-dotenv
# Create .env
touch .env
# Add environment variables
OPENAI_API_KEY=your_openai_api_key_here
# Ensure your GITHUB_TOKEN has 'repo' scope for the example to work.
GITHUB_TOKEN=your_github_personal_access_token_here
Configuration Notes
To run this example successfully:
- Get a GitHub Personal Access Token:
- Go to GitHub Settings → Developer settings → Personal access tokens
- Generate a token with
repo
scope permissions - Add it to your
.env
file
- OpenAI API Key:
- Get your API key from OpenAI's platform
- Add it to your
.env
file
- Test Repository:
- Use a test repository you own or have write access to
- Replace
'myuser/my-awesome-project'
with your actual repository
GitHub Tools Implementation
This Python code defines the specific tools the AI agent will use to interact with GitHub. Each @tool
decorated function is an action the LLM can call, handling the actual API requests to create pull requests or list branches.
# github_tools.py
"""
This module provides the actual GitHub API integration tools that our agent can use.
It implements two main functions:
1. create_github_pull_request: Creates a new pull request
2. list_github_branches: Lists repository branches
"""
import os
from typing import Dict, Any
import requests
from dotenv import load_dotenv
from langchain_core.tools import tool
# Load environment variables from .env file
load_dotenv()
# GitHub API configuration
GITHUB_TOKEN = os.getenv("GITHUB_TOKEN")
GITHUB_API_BASE = "https://api.github.com"
@tool
def create_github_pull_request(
repo_owner: str,
repo_name: str,
title: str,
body: str,
head_branch: str,
base_branch: str = "main"
) -> Dict[str, Any]:
"""
Create a pull request in a GitHub repository.
Args:
repo_owner: GitHub username or organization name
repo_name: Repository name
title: Pull request title
body: Pull request description/body
head_branch: Source branch (the branch you want to merge FROM)
base_branch: Target branch (the branch you want to merge INTO, defaults to 'main')
Returns:
Dictionary containing pull request details or error information
"""
# Check if GitHub token is configured
if not GITHUB_TOKEN:
return {"error": "GitHub token not configured"}
# Set up headers for GitHub API request
headers = {
"Authorization": f"token {GITHUB_TOKEN}",
"Accept": "application/vnd.github.v3+json",
"Content-Type": "application/json"
}
# Prepare the pull request data
pr_data = {
"title": title,
"body": body,
"head": head_branch,
"base": base_branch
}
# Construct the API endpoint URL
url = f"{GITHUB_API_BASE}/repos/{repo_owner}/{repo_name}/pulls"
try:
# Make the API request to create the pull request
response = requests.post(url, headers=headers, json=pr_data)
if response.status_code == 201: # 201 means Created
pr_info = response.json()
return {
"success": True,
"pull_request": {
"number": pr_info["number"],
"title": pr_info["title"],
"html_url": pr_info["html_url"],
"head": pr_info["head"]["ref"],
"base": pr_info["base"]["ref"],
"state": pr_info["state"]
}
}
else:
# Handle error response
error_detail = response.json() if response.content else {"message": "Unknown error"}
return {
"success": False,
"error": f"Failed to create PR: {response.status_code}",
"details": error_detail.get("message", "No additional details")
}
except requests.RequestException as e:
# Handle network or request errors
return {
"success": False,
"error": f"Request failed: {str(e)}"
}
@tool
def list_github_branches(repo_owner: str, repo_name: str, open_only: bool = False) -> Dict[str, Any]:
"""
List branches in a GitHub repository.
Args:
repo_owner: GitHub username or organization name
repo_name: Repository name
open_only: If True, only return branches that have open pull requests
Returns:
Dictionary containing branch information
"""
# Check if GitHub token is configured
if not GITHUB_TOKEN:
return {"error": "GitHub token not configured"}
# Set up headers for GitHub API request
headers = {
"Authorization": f"token {GITHUB_TOKEN}",
"Accept": "application/vnd.github.v3+json"
}
# First get all branches from the repository
url = f"{GITHUB_API_BASE}/repos/{repo_owner}/{repo_name}/branches"
try:
# Make the API request to list branches
response = requests.get(url, headers=headers)
if response.status_code != 200:
return {
"success": False,
"error": f"Failed to list branches: {response.status_code}"
}
# Extract branch names from the response
branches = response.json()
branch_names = [branch["name"] for branch in branches]
# If we don't need to filter for open branches, return all branches
if not open_only:
return {
"success": True,
"branches": branch_names
}
# If open_only is True, we need to check which branches have open PRs
open_branches = set()
page = 1
while True:
# Get open pull requests page by page
prs_url = f"{GITHUB_API_BASE}/repos/{repo_owner}/{repo_name}/pulls?state=open&page={page}"
prs_response = requests.get(prs_url, headers=headers)
if prs_response.status_code != 200:
return {
"success": False,
"error": f"Failed to list pull requests: {prs_response.status_code}"
}
prs = prs_response.json()
if not prs: # No more PRs to process
break
# Add branch names from open PRs to our set
for pr in prs:
open_branches.add(pr["head"]["ref"])
page += 1
return {
"success": True,
"branches": list(open_branches)
}
except requests.RequestException as e:
# Handle network or request errors
return {
"success": False,
"error": f"Request failed: {str(e)}"
}
AI Agent Implementation
This GitHubAgent
class orchestrates the entire process. It initializes the LLM, binds the GitHub tools to it, and sets up a LangGraph
workflow. This workflow defines how the agent processes user requests, decides which tools to use, executes them, and generates a coherent response, maintaining context across interactions.
# github_agent.py
"""
This module implements a GitHub agent using LangChain and LangGraph.
It creates an AI agent that can understand natural language requests and perform GitHub operations
using a combination of LLM (Language Model) and tools.
"""
from typing import Dict, List
from langchain_core.messages import SystemMessage, HumanMessage
from langchain_openai import ChatOpenAI
from langgraph.graph import MessagesState, StateGraph
from langgraph.prebuilt import ToolNode
from github_tools import create_github_pull_request, list_github_branches
class GitHubAgent:
def __init__(self):
# Initialize the language model (GPT-4) with zero temperature for consistent results
self.llm = ChatOpenAI(
model="gpt-4",
temperature=0
)
# Define the tools our agent can use
self.tools = [create_github_pull_request, list_github_branches]
# Bind the tools to our language model so it knows what operations it can perform
self.llm_with_tools = self.llm.bind_tools(self.tools)
# Create the workflow graph that defines how our agent processes requests
self.graph = self._create_graph()
# We want to store the messages so the agent can maintain context across different requests
# For example, this allows the following behaviour:
# - User: "List branches in my-org/my-repo"
# - Agent: "Here are the branches: ..."
# - User: "Create a PR from branch feature-x to main"
# - Agent: "Creating PR from feature-x to main"
# Notice that we didn't have to provide the repository information again,
# since the agent was aware of our previous interaction
self._messages = []
def _create_graph(self):
"""Create the LangGraph workflow that defines how our agent processes requests."""
def should_continue(state: MessagesState) -> str:
"""
Decision function that determines whether to continue with tool calls or end.
Returns 'tools' if the last message contains tool calls, otherwise 'end'.
"""
last_message = state["messages"][-1]
if hasattr(last_message, 'tool_calls') and last_message.tool_calls:
return "tools"
return "end"
def call_model(state: MessagesState) -> Dict[str, List]:
"""
Function that calls the LLM with the current state of messages.
Returns the LLM's response.
"""
messages = state["messages"]
response = self.llm_with_tools.invoke(messages)
return {"messages": [response]}
# Initialize the workflow graph with a state that tracks messages
workflow = StateGraph(MessagesState)
# Add nodes to our graph:
# - 'agent': Handles LLM interactions
# - 'tools': Executes GitHub operations
workflow.add_node("agent", call_model)
workflow.add_node("tools", ToolNode(self.tools))
# Set the entry point of our workflow
workflow.set_entry_point("agent")
# Define the flow of our workflow:
# - After agent, check if we need to use tools
# - If tools are needed, execute them and return to agent
# - If no tools needed, end the workflow
workflow.add_conditional_edges(
"agent",
should_continue,
{
"tools": "tools",
"end": "__end__"
}
)
# After tools are executed, return to the agent for further processing
workflow.add_edge("tools", "agent")
return workflow.compile()
def run(self, user_input: str) -> str:
"""
Process user input and return response.
This is the main entry point for interacting with our GitHub agent.
"""
# Define the system message that sets the context for our agent
system_message = SystemMessage(content="""
You are a helpful GitHub assistant that can create pull requests and manage repositories.
When users ask you to list branches:
- Look for phrases like "list branches", "show branches", "get branches"
- Extract the repository owner and name from the format "owner/repo" or phrases like "in repo owner/repo"
- If the request mentions "open branches" or "open only", set open_only to True
- Use the list_github_branches tool with the appropriate parameters
When users ask you to create a pull request:
- Look for phrases like "create PR", "create pull request", "make a PR"
- Extract the repository owner and name from the format "owner/repo" or phrases like "in repo owner/repo"
- If the user has not specified the repository owner and name, try to extract it from the previous messages
- Extract the source branch (head) and target branch (base) from phrases like "from branch X to Y"
- If the target branch isn't specified, assume it's "main"
- Use the create_github_pull_request tool with the appropriate parameters
If any required information is missing, ask the user to provide it in a friendly way.
Always be clear about what actions you're taking and provide helpful feedback.
Format your responses in a clear, readable way.
""")
# Create the message list with system context, previous messages, and new user input
messages = [system_message] + self._messages + [HumanMessage(content=user_input)]
# Run the workflow and get the result
result = self.graph.invoke({"messages": messages})
# Store the updated state for next time
self._messages = result["messages"]
# Return the content of the last message (the final response)
return result["messages"][-1].content
Main Application
This main.py
file serves as the entry point for our conversational with the application. It initializes the GitHubAgent
and provides a simple command-line interface for users to interact with the AI, allowing them to make requests like creating pull requests or listing branches using natural language.
# main.py
"""
This is the main entry point for our GitHub repository management tool.
It provides a conversational interface for interacting with GitHub operations.
"""
from github_agent import GitHubAgent
def main():
"""GitHub repository management tool with conversational interface."""
# Initialize our GitHub agent
agent = GitHubAgent()
# Welcome message
print("Hey! How can I help you today?")
print("You can ask me to:")
print(" - Create a PR (e.g., 'create me a PR in the repo my-org/my-repo from branch feature-x to main')")
print(" - List branches (e.g., 'list all open branches in my-org/my-repo')")
print("Type 'exit' or 'quit' to end the conversation.\n")
while True:
try:
# Get user input
user_input = input("> ").strip()
# Check for exit command
if user_input.lower() in ['exit', 'quit']:
print("Goodbye! Have a great day!")
break
# Skip empty input
if not user_input:
continue
# Process the request through our agent
response = agent.run(user_input)
print("\nResponse:", response, "\n")
except KeyboardInterrupt:
print("\nGoodbye! Have a great day!")
break
except Exception as e:
print(f"\nOops! Something went wrong: {str(e)}\n")
if __name__ == "__main__":
main()
Running the Example
To test the example, run the main.py
script to initialize the interaction.
python main.py
Hey! How can I help you today?
You can ask me to:
- Create a PR (e.g., 'create me a PR in the repo my-org/my-repo from branch feature-x to main')
- List branches (e.g., 'list all open branches in my-org/my-repo')
Type 'exit' or 'quit' to end the conversation.
> list all open branches in my-org/my-repo
Response: I'll list all the open branches in my-org/my-repo...
> create me a PR from branch feature-x to main
Response: I'll create a pull request from feature-x to main... Done! Here is the link: https://github.com/my-org/my-repo/pull/123 (Note: This is an example, the agent provides the actual PR link)
> exit
Goodbye! Have a great day!
The agent will demonstrate the following tool usage:
- Creating a pull request with detailed specifications
- Listing repository branches
How does this work?
- The agent takes the user input and based on it acts like an LLM but with capabilities to actually perform action.
- It will read tool names, descriptions, arguments and other meta information and figure out how to use them.
- Based on the information above, if it decides that it needs to call a tool, it will construct the tool arguments from the user input (which is plain, human readable text) and actually execute the code.
- It will handle the response of the tool, understand it, and respond to the user with human-readable, well-explained results.
Key Benefits and Use Cases
This LLM-powered GitHub agent shows what’s possible when you bridge AI with real APIs.
Natural Language Interface: Users can describe what they want in plain English rather than remembering specific API syntax or commands.
Parameter Flexibility: The agent can handle various combinations of input parameters, which come in the form of free text, providing the ability to handle a potentially infinite number of different inputs.
Extensibility: Additional tools can be easily added for other GitHub operations like issue creation, repository management, or deployment workflows.
Conclusion
LLM tools represent a fundamental shift in how we interact with external systems and APIs. By providing natural language interfaces to complex technical operations, they give access to powerful integrations and workflows to everyday people. Instead of code, now humans can give instructions to computers in natural language. The GitHub example demonstrates how developers can bridge the gap between conversational AI and real-world system integration.
As AI capabilities continue to evolve, we can expect to see increasingly sophisticated tool ecosystems that enable seamless interaction with virtually any API or service. The combination of natural language understanding and structured tool execution opens up possibilities for automated workflows, intelligent system management, and more intuitive developer experiences.
Whether you're building internal tools, customer-facing applications, or automated workflows, LLM tools provide a powerful foundation for creating more accessible and intelligent systems. The key is to start with clear use cases, implement robust error handling, and design tools that enhance rather than replace human decision-making in critical operations.
If you're an API provider looking to simplify making your services AI-native, explore liblab's Model Context Protocol (MCP) server generator today. You can start by checking the guide Generate MCP Server for Your API .
Before you go, are you ready to transform your API strategy with automated SDK generation? Explore how liblab can help you generate consistent, high-quality SDKs across 6 programming languages from your OpenAPI specification. Your developers—and your budget—will thank you.Build an SDK For Any API