mvp-factory-openhands/.github/scripts/agent_build.py

266 lines
8.1 KiB
Python

#!/usr/bin/env python3
"""
OpenHands Build Agent Script
Executes build and test tasks using OpenHands SDK.
Designed for GitHub Actions integration.
Usage:
python agent_build.py
Environment Variables (all required):
LLM_API_KEY: API key for the LLM
TASK: Build task to execute
REPO_NAME: Name of the repository
COMMIT_SHA: Commit SHA being built
Environment Variables (optional):
LLM_MODEL: Language model to use (default: anthropic/claude-sonnet-4-5-20250929)
LLM_BASE_URL: Optional base URL for LLM API
RETRY_COUNT: Attempt number (for retry logic)
PREVIOUS_ERRORS: Error messages from previous attempts
Example:
export LLM_API_KEY="sk-..."
export TASK="Build and test the project"
export REPO_NAME="my-project"
export COMMIT_SHA="abc123def456"
python agent_build.py
"""
import os
import sys
import logging
import json
from pathlib import Path
from datetime import datetime
from openhands.sdk import LLM, Conversation, get_logger
from openhands.tools.preset.default import get_default_agent
def setup_logging():
"""Configure logging for the build process."""
# Create logs directory
logs_dir = Path("build_logs")
logs_dir.mkdir(exist_ok=True)
# Setup logger
logger = get_logger(__name__)
# Create file handler
log_file = logs_dir / f"build_{datetime.now().strftime('%Y%m%d_%H%M%S')}.log"
file_handler = logging.FileHandler(log_file)
file_handler.setLevel(logging.INFO)
# Create console handler
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.INFO)
# Format
formatter = logging.Formatter(
'%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
file_handler.setFormatter(formatter)
console_handler.setFormatter(formatter)
# Get root logger
root_logger = logging.getLogger()
root_logger.setLevel(logging.INFO)
root_logger.addHandler(file_handler)
root_logger.addHandler(console_handler)
return logger, log_file
def load_context():
"""Load build context from environment variables."""
required_vars = ['LLM_API_KEY', 'TASK', 'REPO_NAME', 'COMMIT_SHA']
missing = [var for var in required_vars if not os.getenv(var)]
if missing:
raise ValueError(f"Missing required environment variables: {missing}")
return {
'api_key': os.getenv('LLM_API_KEY'),
'task': os.getenv('TASK'),
'repo_name': os.getenv('REPO_NAME'),
'commit_sha': os.getenv('COMMIT_SHA'),
'retry_count': os.getenv('RETRY_COUNT', '0'),
'previous_errors': os.getenv('PREVIOUS_ERRORS', ''),
'model': os.getenv('LLM_MODEL', 'anthropic/claude-sonnet-4-5-20250929'),
'base_url': os.getenv('LLM_BASE_URL', ''),
}
def create_llm(config):
"""Create and configure LLM instance."""
llm_config = {
'model': config['model'],
'api_key': config['api_key'],
'usage_id': f"build-{config['repo_name']}-{config['commit_sha'][:8]}",
'drop_params': True,
}
if config['base_url']:
llm_config['base_url'] = config['base_url']
logger.info(f"Initializing LLM: {config['model']}")
return LLM(**llm_config)
def build_enhanced_task(config):
"""Build enhanced task prompt with context."""
task_parts = []
# Header
task_parts.append("=" * 80)
task_parts.append(f"BUILD TASK: {config['repo_name']}")
task_parts.append(f"Commit: {config['commit_sha']}")
if int(config['retry_count']) > 0:
task_parts.append(f"Retry Attempt: {config['retry_count']}")
task_parts.append("=" * 80)
task_parts.append("")
# Previous errors (for retry)
if config['previous_errors']:
task_parts.append("PREVIOUS BUILD ERRORS:")
task_parts.append("-" * 80)
task_parts.append(config['previous_errors'])
task_parts.append("-" * 80)
task_parts.append("")
task_parts.append("Please analyze these errors and fix them in this retry attempt.")
task_parts.append("")
# Main task
task_parts.append("TASK:")
task_parts.append(config['task'])
task_parts.append("")
# Instructions
task_parts.append("INSTRUCTIONS:")
task_parts.append("1. Analyze the project structure and identify build system")
task_parts.append("2. Install dependencies (npm install, pip install, etc.)")
task_parts.append("3. Run build commands (npm run build, ./build.sh, etc.)")
task_parts.append("4. Execute tests (npm test, pytest, etc.)")
task_parts.append("5. Report detailed results:")
task_parts.append(" - Dependencies installed: YES/NO")
task_parts.append(" - Build completed: YES/NO")
task_parts.append(" - Tests passed: YES/NO")
task_parts.append(" - All errors must be documented")
task_parts.append("")
task_parts.append("6. If errors occur:")
task_parts.append(" - Analyze the error messages")
task_parts.append(" - Attempt to fix them")
task_parts.append(" - Retry build process")
task_parts.append(" - Document what was fixed")
task_parts.append("")
task_parts.append("SUCCESS CRITERIA:")
task_parts.append("- All dependencies installed successfully")
task_parts.append("- Build completes without errors")
task_parts.append("- All tests pass")
task_parts.append("")
task_parts.append("=" * 80)
return "\n".join(task_parts)
def save_build_metadata(config, status, duration):
"""Save build metadata to JSON file."""
metadata = {
'repo_name': config['repo_name'],
'commit_sha': config['commit_sha'],
'retry_count': int(config['retry_count']),
'status': status,
'duration_seconds': duration,
'timestamp': datetime.now().isoformat(),
'model': config['model'],
}
metadata_file = Path("build_metadata.json")
with open(metadata_file, 'w') as f:
json.dump(metadata, f, indent=2)
logger.info(f"Build metadata saved to {metadata_file}")
def main():
"""Execute build task with OpenHands SDK."""
start_time = datetime.now()
status = 'unknown'
try:
# Setup
logger, log_file = setup_logging()
logger.info("=" * 80)
logger.info("Starting OpenHands Build")
logger.info("=" * 80)
# Load context
config = load_context()
logger.info(f"Loaded configuration: {config['repo_name']}@{config['commit_sha'][:8]}")
# Create LLM
llm = create_llm(config)
# Create agent with default tools
logger.info("Initializing OpenHands agent with default tools")
agent = get_default_agent(llm=llm, cli_mode=True)
# Create conversation with workspace
cwd = Path.cwd()
logger.info(f"Workspace: {cwd}")
conversation = Conversation(agent=agent, workspace=cwd)
# Build enhanced task
task = build_enhanced_task(config)
logger.info(f"Task prepared ({len(task)} characters)")
logger.debug(f"Task:\n{task}")
# Execute task
logger.info("Sending task to OpenHands agent...")
conversation.send_message(task)
logger.info("Running agent conversation...")
result = conversation.run()
# Calculate duration
end_time = datetime.now()
duration = (end_time - start_time).total_seconds()
# Save metadata
status = 'success'
save_build_metadata(config, status, duration)
logger.info("=" * 80)
logger.info(f"Build completed successfully in {duration:.2f} seconds")
logger.info(f"Logs saved to: {log_file}")
logger.info("=" * 80)
return 0
except Exception as e:
# Calculate duration
end_time = datetime.now()
duration = (end_time - start_time).total_seconds()
# Log error
logger.error("=" * 80)
logger.error(f"Build failed after {duration:.2f} seconds")
logger.error(f"Error: {str(e)}")
logger.error("=" * 80)
# Try to save metadata even on failure
try:
config = load_context()
status = 'failure'
save_build_metadata(config, status, duration)
except Exception:
pass
return 1
if __name__ == "__main__":
sys.exit(main())