Building a Code-Editing Agent with DSPy and Gemini: A Modern Approach

Building a Code-Editing Agent with DSPy and Gemini: A Modern Approach

This comprehensive guide demonstrates how to transform the traditional approach to building code-editing agents by leveraging DSPy's declarative framework. While the original implementation by Thorsten Ball (How to Build an Agent) showcased building an agent with Go and raw API calls, this tutorial reimagines the same functionality using DSPy's powerful modular architecture and automatic optimization capabilities.

Why DSPy?

DSPy represents a paradigm shift from traditional prompt engineering to declarative AI programming. Rather than crafting specific prompts manually, DSPy allows developers to define what they want their AI to accomplish through signatures and modules. The framework automatically optimizes prompts and handles conversation management, reducing the original 400-line Go implementation to significantly less code with built-in modules like dspy.ReAct.

Key advantages include automatic prompt optimization, type safety through signatures, and powerful compilation processes that can improve agent behavior through techniques like few-shot learning without manual intervention.

Setting Up Your Python Environment

Before you begin, it's recommended to use a Python virtual environment to keep dependencies isolated. Run the following commands in your project directory:

python3 -m venv venv
source venv/bin/activate
pip install dspy-ai

Now you can proceed with the DSPy and Gemini setup:

Creating Your main.py File

Let's start building our code-editing agent by creating a main.py file in your project directory. We'll build this file step by step as we go through each section of the tutorial.

Create a new file called main.py and we'll add the code pieces as we explain each component:

Setting Up the DSPy Environment

Before implementing our code-editing agent, we need to establish the proper DSPy environment and configuration. Add the following setup code to your main.py file:

# Install DSPy
# pip install dspy-ai

import dspy
import os
from typing import List, Dict, Any
from pathlib import Path
import json

# Configure the language model
# Using Google Gemini (similar to original implementation)
gemini = dspy.LM(
    model="gemini/gemini-2.5-flash-preview-04-17",
    api_key=os.getenv("GOOGLE_API_KEY"),
    max_tokens=1024
)

# Set as default language model
dspy.configure(lm=gemini)

This configuration establishes Gemini as our primary language model, maintaining consistency with the original implementation's use of Google's API. The DSPy framework automatically handles the conversation management and API communication that required explicit implementation in the Go version.

Environment Variables and Dependencies

Add the environment variable handling to your main.py file:

# Required environment variables
GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY")
if not GOOGLE_API_KEY:
    raise ValueError("GOOGLE_API_KEY environment variable is required")

# Optional: Configure working directory
WORKING_DIR = Path.cwd()

This simplified setup eliminates the need for Go-specific tooling while maintaining the same core functionality. The DSPy framework handles the underlying complexity of model communication and conversation state management automatically.

Gemini-Specific Configuration

Google's Gemini 2.0 Flash model offers excellent performance for code-editing tasks, providing strong reasoning capabilities and fast response times. You can enhance the configuration with additional parameters:

# Enhanced Gemini configuration
gemini_enhanced = dspy.LM(
    model="gemini/gemini-2.5-flash-preview-04-17",
    api_key=os.getenv("GOOGLE_API_KEY"),
    max_tokens=2048,  # Gemini supports larger outputs
    temperature=0.1,  # Lower temperature for more consistent code generation
)

Gemini's multimodal capabilities also enable future extensions to handle code screenshots or diagrams, though this tutorial focuses on text-based interactions.

Implementing Tools with DSPy.Tool

The original implementation defined tools through custom Go structs with name, description, input schema, and execution functions. DSPy provides a more elegant approach through its Tool class, which automatically infers tool schemas from Python function signatures and docstrings.

Let's add our tool functions to main.py:

The read_file Tool

Add the read_file function to your main.py file. This tool demonstrates DSPy's ability to automatically generate tool schemas from function definitions:

def read_file(path: str) -> str:
    """
    Read the contents of a given relative file path.
    
    Args:
        path: The relative path of a file in the working directory
        
    Returns:
        The contents of the file as a string
        
    Raises:
        FileNotFoundError: If the file doesn't exist
    """
    try:
        file_path = WORKING_DIR / path
        with open(file_path, 'r', encoding='utf-8') as f:
            return f.read()
    except FileNotFoundError:
        return f"Error: File '{path}' not found"
    except Exception as e:
        return f"Error reading file '{path}': {str(e)}"

# Create DSPy tool
read_file_tool = dspy.Tool(read_file)

This implementation is significantly more concise than the original Go version, which required explicit JSON schema generation and type marshaling. DSPy automatically extracts the function signature, parameter types, and documentation to create the tool definition that gets sent to the language model.

The list_files Tool

Add the directory listing functionality to your main.py file, following the same pattern and leveraging Python's built-in path handling capabilities:

def list_files(path: str = ".") -> str:
    """
    List files and directories at a given path.
    
    Args:
        path: Optional relative path to list files from. Defaults to current directory.
        
    Returns:
        JSON string containing list of files and directories
    """
    try:
        dir_path = WORKING_DIR / path
        if not dir_path.exists():
            return f"Error: Directory '{path}' does not exist"
        
        files = []
        for item in dir_path.iterdir():
            if item.is_dir():
                files.append(f"{item.name}/")
            else:
                files.append(item.name)
        
        return json.dumps(sorted(files))
    except Exception as e:
        return f"Error listing directory '{path}': {str(e)}"

list_files_tool = dspy.Tool(list_files)

The DSPy tool creation process eliminates the boilerplate code required in the original Go implementation for JSON schema generation and input validation. The framework handles these concerns automatically based on the function's type annotations and docstring.

The edit_file Tool

Add the file editing capability to your main.py file. This maintains the same string replacement approach used in the original implementation, but with improved error handling and Python's native file operations:

def edit_file(path: str, old_str: str, new_str: str) -> str:
    """
    Make edits to a text file by replacing old_str with new_str.
    
    Args:
        path: The path to the file
        old_str: Text to search for - must match exactly
        new_str: Text to replace old_str with
        
    Returns:
        Success message or error description
    """
    if old_str == new_str:
        return "Error: old_str and new_str must be different"
    
    file_path = WORKING_DIR / path
    
    try:
        # Handle file creation for new files
        if not file_path.exists() and old_str == "":
            file_path.parent.mkdir(parents=True, exist_ok=True)
            with open(file_path, 'w', encoding='utf-8') as f:
                f.write(new_str)
            return f"Successfully created file {path}"
        
        # Read existing file
        with open(file_path, 'r', encoding='utf-8') as f:
            content = f.read()
        
        # Perform replacement
        new_content = content.replace(old_str, new_str)
        
        if content == new_content and old_str != "":
            return f"Error: '{old_str}' not found in file"
        
        # Write updated content
        with open(file_path, 'w', encoding='utf-8') as f:
            f.write(new_content)
        
        return "OK"
    
    except Exception as e:
        return f"Error editing file '{path}': {str(e)}"

edit_file_tool = dspy.Tool(edit_file)

This implementation maintains the core functionality of the original while benefiting from Python's more intuitive file handling and DSPy's automatic tool integration.

Creating the ReAct Agent

Now let's add the agent class and interactive functionality to complete our main.py file. DSPy's ReAct module provides a sophisticated implementation of the Reasoning and Acting paradigm, eliminating the need for manual conversation loop management.

Add the following agent class and interactive function to your main.py file:

# Now create the agent using the tools we defined earlier
class CodeEditingAgent(dspy.Module):
    def __init__(self):
        super().__init__()
        self.agent = dspy.ReAct(
            signature="question -> answer",
            tools=[read_file_tool, list_files_tool, edit_file_tool],
            max_iters=10
        )
    
    def forward(self, question: str):
        return self.agent(question=question)

# Instantiate the agent
code_agent = CodeEditingAgent()

# Interactive function
def run_interactive_agent():
    """
    Run interactive agent session similar to original implementation
    """
    print("Chat with Gemini (use 'quit' to exit)")
    
    while True:
        try:
            user_input = input("\033[94mYou\033[0m: ").strip()
            
            if user_input.lower() in ['quit', 'exit', 'bye']:
                break
            
            if not user_input:
                continue
            
            print("\033[92mThinking...\033[0m")
            response = code_agent(question=user_input)
            
            print(f"\033[93mGemini\033[0m: {response.answer}")
            
        except KeyboardInterrupt:
            print("\nGoodbye!")
            break
        except Exception as e:
            print(f"\033[91mError\033[0m: {str(e)}")

# Run the interactive session
if __name__ == "__main__":
    run_interactive_agent()

Running Your Agent

Your main.py file now has all the core components (tools, agent class, and agent instantiation), but you need to add the interactive function to actually use it.

The interactive function will be shown in the next section. Once you add it, you'll be able to run your code-editing agent:

  1. Make sure you have set your GOOGLE_API_KEY environment variable
  2. Ensure you're in your virtual environment with dspy-ai installed
  3. Add the interactive function from the next section

Then execute:

export GOOGLE_API_KEY="your-api-key-here"
python main.py

You should see a prompt like "Chat with Gemini (use 'quit' to exit)" and you can start interacting with your agent!

Interactive Agent Usage

The DSPy agent provides the same interactive capabilities as the original implementation but with improved error handling and more robust conversation management. Here's how to create an interactive session:

def run_interactive_agent():
    """
    Run interactive agent session similar to original implementation
    """
    print("Chat with Gemini (use 'quit' to exit)")
    
    while True:
        try:
            user_input = input("\033[94mYou\033[0m: ").strip()
            
            if user_input.lower() in ['quit', 'exit', 'bye']:
                break
            
            if not user_input:
                continue
            
            print("\033[92mThinking...\033[0m")
            response = code_agent(question=user_input)
            
            print(f"\033[93mGemini\033[0m: {response.answer}")
            
        except KeyboardInterrupt:
            print("\nGoodbye!")
            break
        except Exception as e:
            print(f"\033[91mError\033[0m: {str(e)}")

# Run the interactive session
if __name__ == "__main__":
    run_interactive_agent()

This interactive interface replicates the functionality of the original Go implementation while benefiting from DSPy's built-in conversation management and tool integration.

Example Usage Scenarios

The DSPy agent can handle the same tasks demonstrated in the original article. For example, creating a FizzBuzz implementation:

# Example: Create FizzBuzz program
response = code_agent(
    question="Create a FizzBuzz function in JavaScript that prints numbers 1-100, "
             "replacing multiples of 3 with 'Fizz', multiples of 5 with 'Buzz', "
             "and multiples of both with 'FizzBuzz'"
)
print(response.answer)

The agent will automatically use the edit_file tool to create the JavaScript file, demonstrating the same intelligent tool selection shown in the original implementation.

Agent Optimization with DSPy

One of DSPy's most powerful features is its ability to automatically optimize agent behavior through compilation and training processes. This capability goes far beyond what was possible with the original manual implementation.

Few-Shot Optimization

DSPy can optimize the agent's performance using example tasks and expected outcomes:

# Define training examples
training_examples = [
    dspy.Example(
        question="List all Python files in the current directory",
        answer="I'll use the list_files tool to show Python files."
    ),
    dspy.Example(
        question="Read the contents of main.py",
        answer="I'll use the read_file tool to show the file contents."
    ),
    dspy.Example(
        question="Create a simple Hello World program in Python",
        answer="I'll create hello.py with a basic print statement."
    )
]

# Define evaluation metric
def task_success_metric(example, prediction, trace=None):
    """
    Evaluate if the agent successfully completed the task
    """
    # This would be more sophisticated in practice
    return "error" not in prediction.answer.lower()

# Optimize the agent
from dspy.teleprompt import BootstrapFewShot

optimizer = BootstrapFewShot(
    metric=task_success_metric,
    max_bootstrapped_demos=4,
    max_labeled_demos=2
)

# Compile optimized agent
optimized_agent = optimizer.compile(
    code_agent,
    trainset=training_examples[:2],
    valset=training_examples[2:]
)

This optimization process automatically improves the agent's prompt and behavior based on successful examples, eliminating the manual prompt tuning that would be required in traditional implementations.

Advanced Optimization Strategies

DSPy supports more sophisticated optimization approaches for complex agent scenarios:

# Advanced optimization with multiple metrics
class AgentEvaluator(dspy.Module):
    def __init__(self):
        super().__init__()
        self.evaluator = dspy.ChainOfThought(
            "task, agent_response -> success_score, explanation"
        )
    
    def forward(self, task, response):
        return self.evaluator(task=task, agent_response=response)

# Multi-objective optimization
from dspy.teleprompt import BootstrapFewShotWithRandomSearch

evaluator = AgentEvaluator()

def comprehensive_metric(example, prediction, trace=None):
    """Evaluate agent performance on multiple dimensions"""
    eval_result = evaluator(
        task=example.question,
        response=prediction.answer
    )
    return float(eval_result.success_score) / 10.0

advanced_optimizer = BootstrapFewShotWithRandomSearch(
    metric=comprehensive_metric,
    max_bootstrapped_demos=8,
    num_candidate_programs=10,
    num_threads=4
)

highly_optimized_agent = advanced_optimizer.compile(
    code_agent,
    trainset=training_examples
)

This advanced optimization demonstrates capabilities that would be extremely difficult to implement in the original manual approach.

Deployment and Production Considerations

DSPy agents offer significant advantages for production deployment compared to traditional implementations. The framework's modular architecture and automatic optimization capabilities make it easier to maintain and scale agent systems.

Containerization and Scaling

The Python-based DSPy implementation is more straightforward to containerize and deploy than the original Go version:

FROM python:3.11-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install -r requirements.txt

COPY . .

ENV GOOGLE_API_KEY=""
ENV PYTHONPATH="/app"

CMD ["python", "agent_server.py"]

DSPy's architecture naturally supports microservice deployments and cloud-native scaling patterns. The framework's optimization capabilities also enable better resource utilization in production environments.

Monitoring and Observability

DSPy integrates well with modern observability tools for monitoring agent performance:

# Integration with MLflow for tracking
import mlflow
import mlflow.dspy

# Enable automatic logging
mlflow.dspy.autolog()

# Set up experiment tracking
mlflow.set_experiment("Code-Editing-Agent")

with mlflow.start_run():
    response = optimized_agent(question="Create a Python calculator")
    mlflow.log_param("question", "Create a Python calculator")
    mlflow.log_metric("response_length", len(response.answer))

This observability integration provides insights into agent performance and optimization opportunities that were not readily available in the original implementation.

Conclusion

The transformation from a traditional agent implementation to DSPy demonstrates the framework's power in simplifying complex AI system development. While the original Go implementation required approximately 400 lines of code to achieve basic agent functionality, the DSPy version accomplishes the same goals with significantly less code while adding powerful optimization capabilities.

DSPy's declarative approach eliminates much of the boilerplate code associated with conversation management, tool integration, and prompt formatting. The framework's automatic optimization features provide capabilities that would require substantial additional development in traditional implementations. Most importantly, DSPy's modular architecture makes agent systems more maintainable, testable, and scalable for production use.

The ReAct module's sophisticated reasoning and acting capabilities, combined with DSPy's tool integration system, create agent implementations that are both more capable and more reliable than manually crafted alternatives. As the field of AI agents continues to evolve, frameworks like DSPy represent the next generation of development tools that enable developers to focus on agent behavior and capabilities rather than infrastructure and prompt engineering details.