Add intelligent image selection for development environments

Integrate chorus-dev-images repository with automatic language detection
and appropriate development container selection.

New features:
- ImageSelector for automatic language-to-image mapping
- Language detection from task context, description, and repository
- Standardized workspace environment variables
- Support for 7 development environments (Rust, Go, Python, Node, Java, C++)

Changes:
- pkg/execution/images.go (new): Image selection and language detection logic
- pkg/execution/engine.go: Modified createSandboxConfig to use ImageSelector

This ensures agents automatically get the right tools for their tasks without
manual configuration.

Related: https://gitea.chorus.services/tony/chorus-dev-images

🤖 Generated with Claude Code (https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
anthonyrawlins
2025-09-30 11:11:03 +10:00
parent ae021b47b9
commit 7469b9c4c1
2 changed files with 268 additions and 2 deletions

View File

@@ -395,15 +395,25 @@ func (e *DefaultTaskExecutionEngine) executeSandboxCommands(ctx context.Context,
// createSandboxConfig creates a sandbox configuration from task requirements // createSandboxConfig creates a sandbox configuration from task requirements
func (e *DefaultTaskExecutionEngine) createSandboxConfig(request *TaskExecutionRequest) *SandboxConfig { func (e *DefaultTaskExecutionEngine) createSandboxConfig(request *TaskExecutionRequest) *SandboxConfig {
// Use image selector to choose appropriate development environment
imageSelector := NewImageSelector()
selectedImage := imageSelector.SelectImageForTask(request)
config := &SandboxConfig{ config := &SandboxConfig{
Type: "docker", Type: "docker",
Image: "alpine:latest", Image: selectedImage, // Auto-selected based on task language
Architecture: "amd64", Architecture: "amd64",
WorkingDir: "/workspace", WorkingDir: "/workspace/data", // Use standardized workspace structure
Timeout: 5 * time.Minute, Timeout: 5 * time.Minute,
Environment: make(map[string]string), Environment: make(map[string]string),
} }
// Add standardized workspace environment variables
config.Environment["WORKSPACE_ROOT"] = "/workspace"
config.Environment["WORKSPACE_INPUT"] = "/workspace/input"
config.Environment["WORKSPACE_DATA"] = "/workspace/data"
config.Environment["WORKSPACE_OUTPUT"] = "/workspace/output"
// Apply defaults from engine config // Apply defaults from engine config
if e.config.SandboxDefaults != nil { if e.config.SandboxDefaults != nil {
if e.config.SandboxDefaults.Image != "" { if e.config.SandboxDefaults.Image != "" {

256
pkg/execution/images.go Normal file
View File

@@ -0,0 +1,256 @@
package execution
import (
"fmt"
"strings"
)
const (
// ImageRegistry is the default registry for CHORUS development images
ImageRegistry = "registry.home.deepblack.cloud/chorus"
// ImageVersion is the default version tag to use
ImageVersion = "latest"
)
// ImageSelector maps task languages and contexts to appropriate development images
type ImageSelector struct {
registry string
version string
}
// NewImageSelector creates a new image selector with default settings
func NewImageSelector() *ImageSelector {
return &ImageSelector{
registry: ImageRegistry,
version: ImageVersion,
}
}
// NewImageSelectorWithConfig creates an image selector with custom registry and version
func NewImageSelectorWithConfig(registry, version string) *ImageSelector {
if registry == "" {
registry = ImageRegistry
}
if version == "" {
version = ImageVersion
}
return &ImageSelector{
registry: registry,
version: version,
}
}
// SelectImage returns the appropriate image name for a given language
func (s *ImageSelector) SelectImage(language string) string {
imageMap := map[string]string{
"rust": "rust-dev",
"go": "go-dev",
"golang": "go-dev",
"python": "python-dev",
"py": "python-dev",
"javascript": "node-dev",
"js": "node-dev",
"typescript": "node-dev",
"ts": "node-dev",
"node": "node-dev",
"nodejs": "node-dev",
"java": "java-dev",
"cpp": "cpp-dev",
"c++": "cpp-dev",
"c": "cpp-dev",
}
normalizedLang := strings.ToLower(strings.TrimSpace(language))
if img, ok := imageMap[normalizedLang]; ok {
return fmt.Sprintf("%s/%s:%s", s.registry, img, s.version)
}
// Default to base image if language not recognized
return fmt.Sprintf("%s/base:%s", s.registry, s.version)
}
// DetectLanguage analyzes task context to determine primary programming language
func (s *ImageSelector) DetectLanguage(task *TaskExecutionRequest) string {
// Priority 1: Explicit language specification
if lang, ok := task.Context["language"].(string); ok && lang != "" {
return strings.ToLower(strings.TrimSpace(lang))
}
// Priority 2: Language hint in requirements
if task.Requirements != nil && task.Requirements.AIModel != "" {
// Some models might hint at language in their name
modelLang := extractLanguageFromModel(task.Requirements.AIModel)
if modelLang != "" {
return modelLang
}
}
// Priority 3: Repository URL analysis
if repoURL, ok := task.Context["repository_url"].(string); ok && repoURL != "" {
return detectLanguageFromRepo(repoURL)
}
// Priority 4: Description keyword analysis
return detectLanguageFromDescription(task.Description)
}
// SelectImageForTask is a convenience method that detects language and returns appropriate image
func (s *ImageSelector) SelectImageForTask(task *TaskExecutionRequest) string {
language := s.DetectLanguage(task)
return s.SelectImage(language)
}
// detectLanguageFromDescription analyzes task description for language keywords
func detectLanguageFromDescription(description string) string {
desc := strings.ToLower(description)
// Keyword map with priority (specific keywords beat generic ones)
keywords := []struct {
language string
patterns []string
priority int
}{
// High priority - specific language indicators
{"rust", []string{"rust", "cargo.toml", ".rs file", "rustc", "cargo build"}, 3},
{"go", []string{"golang", "go.mod", "go.sum", ".go file", "go build"}, 3},
{"python", []string{"python3", "pip install", ".py file", "pytest", "requirements.txt", "pyproject.toml"}, 3},
{"typescript", []string{"typescript", ".ts file", "tsconfig.json"}, 3},
{"javascript", []string{"node.js", "npm install", "package.json", ".js file"}, 2},
{"java", []string{"java", "maven", "gradle", "pom.xml", ".java file"}, 3},
{"cpp", []string{"c++", "cmake", ".cpp file", ".cc file", "makefile"}, 3},
// Medium priority - generic mentions
{"rust", []string{"rust"}, 2},
{"go", []string{"go "}, 2},
{"python", []string{"python"}, 2},
{"node", []string{"node ", "npm ", "yarn "}, 2},
{"java", []string{"java "}, 2},
{"cpp", []string{"c++ ", "cpp "}, 2},
{"c", []string{" c "}, 1},
}
bestMatch := ""
bestPriority := 0
for _, kw := range keywords {
for _, pattern := range kw.patterns {
if strings.Contains(desc, pattern) {
if kw.priority > bestPriority {
bestMatch = kw.language
bestPriority = kw.priority
}
break
}
}
}
if bestMatch != "" {
return bestMatch
}
return "base"
}
// detectLanguageFromRepo attempts to detect language from repository URL or name
func detectLanguageFromRepo(repoURL string) string {
repo := strings.ToLower(repoURL)
// Check for language-specific repository naming patterns
patterns := map[string][]string{
"rust": {"-rs", ".rs", "rust-"},
"go": {"-go", ".go", "go-"},
"python": {"-py", ".py", "python-"},
"javascript": {"-js", ".js", "node-"},
"typescript": {"-ts", ".ts"},
"java": {"-java", ".java"},
"cpp": {"-cpp", ".cpp", "-cxx"},
}
for lang, pats := range patterns {
for _, pat := range pats {
if strings.Contains(repo, pat) {
return lang
}
}
}
return "base"
}
// extractLanguageFromModel tries to extract language hints from model name
func extractLanguageFromModel(modelName string) string {
model := strings.ToLower(modelName)
// Some models are language-specific
if strings.Contains(model, "codellama") {
return "base" // CodeLlama is multi-language
}
if strings.Contains(model, "go") && strings.Contains(model, "coder") {
return "go"
}
if strings.Contains(model, "rust") {
return "rust"
}
if strings.Contains(model, "python") {
return "python"
}
return ""
}
// GetAvailableImages returns a list of all available development images
func (s *ImageSelector) GetAvailableImages() []string {
images := []string{"base", "rust-dev", "go-dev", "python-dev", "node-dev", "java-dev", "cpp-dev"}
result := make([]string, len(images))
for i, img := range images {
result[i] = fmt.Sprintf("%s/%s:%s", s.registry, img, s.version)
}
return result
}
// GetImageInfo returns metadata about a specific image
func (s *ImageSelector) GetImageInfo(imageName string) map[string]string {
infoMap := map[string]map[string]string{
"base": {
"description": "Base Debian development environment with common tools",
"size": "~200MB",
"tools": "git, curl, build-essential, vim, jq",
},
"rust-dev": {
"description": "Rust development environment with cargo and tooling",
"size": "~1.2GB",
"tools": "rustc, cargo, clippy, rustfmt, ripgrep, fd-find",
},
"go-dev": {
"description": "Go development environment with standard tooling",
"size": "~600MB",
"tools": "go1.22, gopls, delve, staticcheck, golangci-lint",
},
"python-dev": {
"description": "Python development environment with modern tooling",
"size": "~800MB",
"tools": "python3.11, uv, ruff, black, pytest, mypy",
},
"node-dev": {
"description": "Node.js development environment with package managers",
"size": "~700MB",
"tools": "node20, pnpm, yarn, typescript, eslint, prettier",
},
"java-dev": {
"description": "Java development environment with build tools",
"size": "~1.5GB",
"tools": "openjdk-17, maven, gradle",
},
"cpp-dev": {
"description": "C/C++ development environment with compilers and tools",
"size": "~900MB",
"tools": "gcc, g++, clang, cmake, ninja, gdb, valgrind",
},
}
return infoMap[imageName]
}