Future of Coding — Multi-Agent LLM Framework using LangGraph
Artificial intelligence (AI) is rapidly transforming the way we live and work, and the domain of software engineering is undergoing profound changes. Software development holds a pivotal role in shaping our future, particularly with the advent of AI and automation. Today, software development stands at the vanguard of this technological revolution.
LLM-based agents are increasingly employed to automate numerous tasks integral to software development, spanning from code composition to testing and debugging. Leveraging a multi-agent framework empowers these agents to operate autonomously, reducing the necessity for extensive human intervention. This article delves into the process of developing a Multi-agent Coder utilizing LangGraph, exploring its capabilities and the integration of agent and chain-based LLM solutions.
Recent papers and tools such as AgentCoder, AlphaCodium, and GPTEngineer underscore the significance of this approach. To commence, let’s delve into the capabilities afforded by LangGraph for crafting these agents and deploying chain-based LLM solutions.
Introduction to LangGraph
LangGraph enhances the LangChain ecosystem by enabling the development of sophisticated agent runtimes that leverage the capabilities of Large Language Models for reasoning and decision-making tasks within cyclical graph structures.
Key Components in LangGraph
- State Graph: The primary type of graph in LangGraph is the StatefulGraph, which is parameterized by a state object passed to each node. Nodes within the graph return operations to update this state, either by setting specific attributes or adding to existing attributes
- Nodes: Nodes in LangGraph represent agent components responsible for specific tasks within the application. Each node interacts with the state object and returns operations to update it based on its function in the agent architecture
- Edges: Edges connect nodes within the graph, defining the flow of information and operations between different components of the application. They facilitate communication and coordination between nodes to achieve the desired functionality
Implementation using LangGraph
In this we first define the architecture flow and different Agents. We specialize these agents in specific tasks and assign the role.
Agent Node “Programmer”
Task: Specialized in Writing Program based on Requirements
class Code(BaseModel):
"""Plan to follow in future"""
code: str = Field(
description="Detailed optmized error-free Python code on the provided requirements"
)
from langchain.chains.openai_functions import create_structured_output_runnable
from langchain_core.prompts import ChatPromptTemplate
code_gen_prompt = ChatPromptTemplate.from_template(
'''**Role**: You are a expert software python programmer. You need to develop python code
**Task**: As a programmer, you are required to complete the function. Use a Chain-of-Thought approach to break
down the problem, create pseudocode, and then write the code in Python language. Ensure that your code is
efficient, readable, and well-commented.
**Instructions**:
1. **Understand and Clarify**: Make sure you understand the task.
2. **Algorithm/Method Selection**: Decide on the most efficient way.
3. **Pseudocode Creation**: Write down the steps you will follow in pseudocode.
4. **Code Generation**: Translate your pseudocode into executable Python code
*REQURIEMENT*
{requirement}'''
)
coder = create_structured_output_runnable(
Code, llm, code_gen_prompt
)
Agent Node “Tester”
Task: Generates input test cases and expected output
class Tester(BaseModel):
"""Plan to follow in future"""
Input: List[List] = Field(
description="Input for Test cases to evaluate the provided code"
)
Output: List[List] = Field(
description="Expected Output for Test cases to evaluate the provided code"
)
from langchain.chains.openai_functions import create_structured_output_runnable
from langchain_core.prompts import ChatPromptTemplate
test_gen_prompt = ChatPromptTemplate.from_template(
'''**Role**: As a tester, your task is to create Basic and Simple test cases based on provided Requirement and Python Code.
These test cases should encompass Basic, Edge scenarios to ensure the code's robustness, reliability, and scalability.
**1. Basic Test Cases**:
- **Objective**: Basic and Small scale test cases to validate basic functioning
**2. Edge Test Cases**:
- **Objective**: To evaluate the function's behavior under extreme or unusual conditions.
**Instructions**:
- Implement a comprehensive set of test cases based on requirements.
- Pay special attention to edge cases as they often reveal hidden bugs.
- Only Generate Basics and Edge cases which are small
- Avoid generating Large scale and Medium scale test case. Focus only small, basic test-cases
*REQURIEMENT*
{requirement}
**Code**
{code}
'''
)
tester_agent = create_structured_output_runnable(
Test, llm, test_gen_prompt
)
Agent Node “Executor”
Task: Executes code in a Python environment on provided test cases
class ExecutableCode(BaseModel):
"""Plan to follow in future"""
code: str = Field(
description="Detailed optmized error-free Python code with test cases assertion"
)
python_execution_gen = ChatPromptTemplate.from_template(
"""You have to add testing layer in the *Python Code* that can help to execute the code. You need to pass only Provided Input as argument and validate if the Given Expected Output is matched.
*Instruction*:
- Make sure to return the error if the assertion fails
- Generate the code that can be execute
Python Code to excecute:
*Python Code*:{code}
Input and Output For Code:
*Input*:{input}
*Expected Output*:{output}"""
)
execution = create_structured_output_runnable(
ExecutableCode, llm, python_execution_gen
)
Agent Node “Debugger”
Task: Debugs code using LLM knowledge and sends it back to the ‘Executer’ Agent in case of errors.
class RefineCode(BaseModel):
code: str = Field(
description="Optimized and Refined Python code to resolve the error"
)
python_refine_gen = ChatPromptTemplate.from_template(
"""You are expert in Python Debugging. You have to analysis Given Code and Error and generate code that handles the error
*Instructions*:
- Make sure to generate error free code
- Generated code is able to handle the error
*Code*: {code}
*Error*: {error}
"""
)
refine_code = create_structured_output_runnable(
RefineCode, llm, python_refine_gen
)
Decision Edge “Decision_To_End”
Task: This is a conditional edge, based on the “Execution”. If it encounters errors, then it would make decision to whether to end the execution or send to “Debugger” to resolve the errors.
Graph State and Define Graph Node
In LangGraph, State object helps to keep track of graph state.
class AgentCoder(TypedDict):
requirement: str #Keep Track of the requirements
code: str #Latest Code
tests: Dict[str, any] #Generated Test Cases
errors: Optional[str] #If encountered, Error Keep track of it
Define functionality and execution for each agent flow
def programmer(state):
print(f'Entering in Programmer')
requirement = state['requirement']
code_ = coder.invoke({'requirement':requirement})
return {'code':code_.code}
def debugger(state):
print(f'Entering in Debugger')
errors = state['errors']
code = state['code']
refine_code_ = refine_code.invoke({'code':code,'error':errors})
return {'code':refine_code_.code,'errors':None}
def executer(state):
print(f'Entering in Executer')
tests = state['tests']
input_ = tests['input']
output_ = tests['output']
code = state['code']
executable_code = execution.invoke({"code":code,"input":input_,'output':output_})
#print(f"Executable Code - {executable_code.code}")
error = None
try:
exec(executable_code.code)
print("Code Execution Successful")
except Exception as e:
print('Found Error While Running')
error = f"Execution Error : {e}"
return {'code':executable_code.code,'errors':error}
def tester(state):
print(f'Entering in Tester')
requirement = state['requirement']
code = state['code']
tests = tester_agent.invoke({'requirement':requirement,'code':code})
#tester.invoke({'requirement':'Generate fibbinaco series','code':code_.code})
return {'tests':{'input':tests.Input,'output':tests.Output}}
def decide_to_end(state):
print(f'Entering in Decide to End')
if state['errors']:
return 'debugger'
else:
return 'end'
Integrating different node to each other using edges and add conditional edge ‘Decision_To_End”
from langgraph.graph import END, StateGraph
workflow = StateGraph(AgentCoder)
# Define the nodes
workflow.add_node("programmer", programmer)
workflow.add_node("debugger", debugger)
workflow.add_node("executer", executer)
workflow.add_node("tester", tester)
#workflow.add_node('decide_to_end',decide_to_end)
# Build graph
workflow.set_entry_point("programmer")
workflow.add_edge("programmer", "tester")
workflow.add_edge("debugger", "executer")
workflow.add_edge("tester", "executer")
#workflow.add_edge("executer", "decide_to_end")
workflow.add_conditional_edges(
"executer",
decide_to_end,
{
"end": END,
"debugger": "debugger",
},
)
# Compile
app = workflow.compile()
In this case, I have taken LeetCode Problem (https://leetcode.com/problems/two-sum/description/) and pass the problem statement to the Solution. It generated the Code and Test cases.
config = {"recursion_limit": 50}
inputs = {"requirement": requirement}
running_dict = {}
async for event in app.astream(inputs, config=config):
for k, v in event.items():
running_dict[k] = v
if k != "__end__":
print(v)
Conclusion
LLM based agents are high evolving field and still requires a lot of checks to enable it to production ready. This is a prototype of using Multi-agent based flow. I have future plan to integrate tools in Agent Nodes (like allowing to refer RAG-based Library Documentation and Web tool by “Debugger” agent to provide latest and refined code, Use internal tools to execute the code by “Executor” agent)
As this is highly changing area so I will keep updating this article for new advancement. I frequently write about developments in Generative AI and Machine learning, so feel free to follow me on LinkedIn (https://www.linkedin.com/in/anurag-mishra-660961b7/)