266 lines
8.1 KiB
Python
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())
|