trailofbits / trailofbits/buttercup

Migrate Patcher from Manual Prompts to DSPy Framework

Open
#322 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Python
Stars
1.7k
Forks
185
Avg merge
51m
Merged PRs (30d)
1

Description

Migrate Patcher from Manual Prompts to DSPy Framework

Background

The Buttercup patcher currently uses a sophisticated multi-agent system built with LangGraph to automatically generate patches for security vulnerabilities. While effective, the system relies heavily on manually crafted prompts that are:

  1. Brittle and hard to maintain - All prompts are hardcoded strings requiring manual updates
  2. Difficult to optimize - No systematic way to improve prompt effectiveness
  3. Inconsistent in output handling - Regex-based parsing prone to failures
  4. Limited in reliability - Simple fallback mechanisms without adaptive optimization

DSPy (Declarative Self-improving Python) offers a paradigm shift from manual prompt engineering to programmatic, optimizable LLM applications. Instead of writing brittle prompt strings, DSPy allows us to:

  • Define task signatures declaratively
  • Automatically optimize prompts using algorithms like MIPROv2
  • Compose modular, reusable components
  • Systematically improve performance with data-driven optimization

Current Architecture Analysis

Agent Structure

The patcher consists of 7 specialized agents orchestrated through LangGraph:

  1. InputProcessingAgent - Initial vulnerability data processing
  2. ContextRetrieverAgent - Code context extraction (694-1357 lines!)
  3. RootCauseAgent - Vulnerability root cause analysis
  4. SWEAgent - Patch strategy and creation
  5. QEAgent - Quality engineering and validation
  6. ReflectionAgent - Failure analysis and recovery
  7. PatcherLeaderAgent - Overall orchestration
Key Pain Points
1. Manual Prompt Templates
  • RootCauseAgent: 60-line hardcoded system prompt (rootcause.py:41-100)
  • ReflectionAgent: 169-line complex reflection prompt (reflection.py:47-216)
  • SWEAgent: Separate 60-line strategy + 70-line creation prompts (swe.py:49-190)
  • ContextRetrieverAgent: Multiple specialized prompts for different tasks
2. Complex Output Parsing
  • Regex-based extraction throughout (common.py:491-503, reflection.py:744-750)
  • Manual XML tag parsing with frequent failures
  • No structured output guarantees
3. Limited Optimization
  • No systematic prompt tuning capability
  • Manual trial-and-error for improvements
  • No data-driven optimization feedback loop

DSPy Migration Opportunity

High-Impact Transformations
1. Root Cause Analysis Module

Current: 60-line manual prompt with complex instructions

# Before: rootcause.py:41-100
ROOT_CAUSE_SYSTEM_MSG = """You are an expert..."""  # 60 lines

DSPy Approach:

class RootCauseSignature(dspy.Signature):
    """Analyze vulnerability to identify root cause."""
    stack_trace: str = dspy.InputField(desc="crash stack trace")
    code_context: str = dspy.InputField(desc="relevant code snippets")
    diff_context: str = dspy.InputField(desc="recent code changes")
    root_cause: str = dspy.OutputField(desc="root cause analysis")
    vulnerable_lines: list[int] = dspy.OutputField(desc="vulnerable line numbers")

class RootCauseAnalyzer(dspy.Module):
    def __init__(self):
        self.analyze = dspy.ChainOfThought(RootCauseSignature)
    
    def forward(self, stack_trace, code_context, diff_context):
        return self.analyze(stack_trace=stack_trace, 
                           code_context=code_context,
                           diff_context=diff_context)
2. Patch Generation Pipeline

Current: Separate strategy + creation with manual chaining

# Before: swe.py - manual prompt construction across 200+ lines

DSPy Approach:

class PatchStrategySignature(dspy.Signature):
    """Determine optimal patching strategy."""
    root_cause: str = dspy.InputField()
    code_context: str = dspy.InputField()
    strategy: str = dspy.OutputField(desc="patch strategy")
    confidence: float = dspy.OutputField(desc="confidence score 0-1")

class PatchCreationSignature(dspy.Signature):
    """Generate concrete patch code."""
    strategy: str = dspy.InputField()
    vulnerable_code: str = dspy.InputField()
    patch_diff: str = dspy.OutputField(desc="unified diff format patch")

class PatchGenerator(dspy.Module):
    def __init__(self):
        self.strategize = dspy.ChainOfThought(PatchStrategySignature)
        self.create = dspy.Predict(PatchCreationSignature)
        
    def forward(self, root_cause, code_context, vulnerable_code):
        strategy = self.strategize(root_cause=root_cause, 
                                  code_context=code_context)
        patch = self.create(strategy=strategy.strategy,
                           vulnerable_code=vulnerable_code)
        return patch
3. Reflection with Adaptive Learning

Current: 169-line hardcoded 12-step process

# Before: reflection.py:47-216 - massive manual prompt

DSPy Approach:

class ReflectionSignature(dspy.Signature):
    """Analyze patch failure and suggest improvements."""
    failure_info: str = dspy.InputField(desc="build/test failure details")
    previous_attempts: list[str] = dspy.InputField(desc="prior patch attempts")
    analysis: str = dspy.OutputField(desc="failure analysis")
    next_action: str = dspy.OutputField(desc="recommended action")
    should_retry: bool = dspy.OutputField(desc="whether to retry")

class AdaptiveReflection(dspy.Module):
    def __init__(self):
        self.reflect = dspy.ReAct(ReflectionSignature, 
                                  tools=[analyze_build_log, 
                                        check_test_results,
                                        examine_patch_diff])
    
    def forward(self, failure_info, previous_attempts):
        return self.reflect(failure_info=failure_info,
                           previous_attempts=previous_attempts)
Optimization Strategy with MIPROv2

DSPy's MIPROv2 optimizer can automatically tune our modules using historical patch success data:

# Optimization configuration
from dspy.teleprompt import MIPROv2

# Prepare training data from successful patches
train_data = load_successful_patches()  # Historical success cases
val_data = load_validation_patches()    # Hold-out validation set

# Define metric
def patch_success_metric(gold, pred, trace=None):
    """Evaluate patch quality."""
    builds = check_build_success(pred.patch_diff)
    fixes_pov = check_pov_fixed(pred.patch_diff)
    tests_pass = check_tests_pass(pred.patch_diff)
    return (builds * 0.3 + fixes_pov * 0.5 + tests_pass * 0.2)

# Optimize the pipeline
optimizer = MIPROv2(
    metric=patch_success_metric,
    num_candidates=20,  # Generate 20 prompt variations
    init_temperature=1.0
)

optimized_patcher = optimizer.compile(
    PatchGenerator(),
    trainset=train_data,
    valset=val_data,
    num_trials=50,  # Run 50 optimization trials
    max_bootstrapped_demos=3,
    max_labeled_demos=5
)

Implementation Plan

Phase 1: Foundation (Week 1-2)
  1. Set up DSPy infrastructure

    • Add DSPy to patcher dependencies
    • Create DSPy configuration module
    • Set up telemetry for optimization metrics
  2. Create base signatures

    • Define signatures for each agent's core functionality
    • Implement type-safe input/output fields
    • Add descriptions for all fields
  3. Build evaluation framework

    • Implement success metrics (build, PoV, tests)
    • Create training data loader from historical patches
    • Set up validation pipeline
Phase 2: Core Module Migration (Week 3-4)
  1. Migrate RootCauseAgent

    • Convert hardcoded prompt to RootCauseSignature
    • Implement dspy.ChainOfThought for analysis
    • Add tool integration for code understanding
  2. Migrate SWEAgent

    • Split into PatchStrategyModule and PatchCreationModule
    • Chain modules using DSPy composition
    • Preserve existing tool usage patterns
  3. Migrate ReflectionAgent

    • Convert 12-step process to dspy.ReAct agent
    • Integrate existing analysis tools
    • Add adaptive decision making
Phase 3: Advanced Features (Week 5-6)
  1. Implement ContextRetriever as DSPy Module

    • Create retrieval signatures for different context types
    • Use dspy.Retrieve for semantic search
    • Optimize context selection
  2. Build ensemble capabilities

    • Create dspy.Ensemble of top-performing modules
    • Implement voting mechanisms for patch selection
    • Add confidence scoring
  3. Add optimization pipeline

    • Implement MIPROv2 optimization
    • Create automated retraining pipeline
    • Set up A/B testing framework
Phase 4: Integration & Testing (Week 7-8)
  1. LangGraph integration

    • Wrap DSPy modules as LangGraph nodes
    • Preserve existing state management
    • Maintain backward compatibility
  2. Comprehensive testing

    • Unit tests for each DSPy module
    • Integration tests with full pipeline
    • Performance benchmarking vs current system
  3. Gradual rollout

    • Feature flag for DSPy vs legacy mode
    • Shadow mode operation for comparison
    • Monitoring and alerting setup

Success Metrics

Primary Metrics
  • Patch Success Rate: Target 20% improvement over baseline
  • Mean Time to Patch: Reduce by 30% through optimized prompts
  • PoV Fix Rate: Increase from current baseline by 25%
Secondary Metrics
  • LLM Token Usage: Reduce by 40% through optimized prompts
  • Parsing Failure Rate: Reduce from current to <1%
  • Agent Retry Count: Decrease average retries by 50%
Quality Metrics
  • Code Quality: Maintain or improve linting/formatting compliance
  • Test Coverage: Ensure patches don't reduce test coverage
  • Security: Validate patches don't introduce new vulnerabilities

Technical Requirements

Dependencies
[tool.poetry.dependencies]
dspy-ai = "^2.6.0"  # Latest DSPy version
pydantic = "^2.0"   # For structured outputs
mlflow = "^2.0"     # For experiment tracking
Infrastructure
  • GPU Access: Optional but recommended for optimization phase
  • Storage: ~10GB for training data and optimized models
  • Compute: MIPROv2 optimization requires ~20 hours of LLM calls
Backward Compatibility
  • Maintain LangGraph state interface
  • Support existing Redis queue integration
  • Preserve current monitoring/telemetry

Risk Mitigation

Technical Risks
  1. DSPy Learning Curve

    • Mitigation: Start with simple modules, gradually increase complexity
    • Provide team training on DSPy concepts
  2. Integration Complexity

    • Mitigation: Maintain parallel implementations initially
    • Extensive testing before full migration
  3. Performance Regression

    • Mitigation: Shadow mode operation for comparison
    • Rollback capability via feature flags
Operational Risks
  1. Optimization Cost

    • Mitigation: Use cheaper models for initial optimization
    • Implement caching for repeated evaluations
  2. Model Drift

    • Mitigation: Regular reoptimization schedule
    • Monitoring for performance degradation

Resources & References

DSPy Documentation
Relevant Papers
  • "DSPy: Compiling Declarative Language Model Calls into Self-Improving Pipelines" (2023)
  • "Optimizing Instructions and Demonstrations for Multi-Stage Language Model Programs" (2024)
Internal Resources
  • Current patcher codebase: /patcher/src/buttercup/patcher/
  • Historical patch data: Available in Redis/telemetry
  • Team expertise: LangGraph, LLM integration experience

Expected Outcomes

By migrating to DSPy, we expect to:

  1. Eliminate manual prompt engineering - Replace 500+ lines of hardcoded prompts with optimizable signatures
  2. Improve reliability - Reduce parsing failures and increase success rates through structured outputs
  3. Enable continuous improvement - Automatic optimization based on real-world performance data
  4. Accelerate development - Faster iteration on agent capabilities without manual prompt tuning
  5. Reduce operational costs - More efficient token usage through optimized prompts

Next Steps

  1. Team Review: Schedule architecture review meeting
  2. Proof of Concept: Build DSPy version of RootCauseAgent
  3. Benchmark: Compare PoC performance against current implementation
  4. Resource Allocation: Assign team members and timeline
  5. Kickoff: Begin Phase 1 implementation

This migration represents a significant architectural improvement that will make our patcher more reliable, maintainable, and effective at automatically fixing security vulnerabilities. The investment in DSPy will pay dividends through improved success rates and reduced maintenance burden.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by reading the current agent implementation in /patcher/src/buttercup/patcher/, especially rootcause.py, reflection.py, swe.py, and common.py, along with the LangGraph orchestration. Review the proposed DSPy signatures, optimization metrics, dependency changes, and phased migration plan before estimating work. Done would require a coordinated migration, integration and regression testing, optimization, and rollout rather than a single localized change.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
ai, backend
Issue type
Refactor
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Needs clarification
Newbie friendliness
15/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.