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:
@@ -395,15 +395,25 @@ func (e *DefaultTaskExecutionEngine) executeSandboxCommands(ctx context.Context,
|
||||
|
||||
// createSandboxConfig creates a sandbox configuration from task requirements
|
||||
func (e *DefaultTaskExecutionEngine) createSandboxConfig(request *TaskExecutionRequest) *SandboxConfig {
|
||||
// Use image selector to choose appropriate development environment
|
||||
imageSelector := NewImageSelector()
|
||||
selectedImage := imageSelector.SelectImageForTask(request)
|
||||
|
||||
config := &SandboxConfig{
|
||||
Type: "docker",
|
||||
Image: "alpine:latest",
|
||||
Image: selectedImage, // Auto-selected based on task language
|
||||
Architecture: "amd64",
|
||||
WorkingDir: "/workspace",
|
||||
WorkingDir: "/workspace/data", // Use standardized workspace structure
|
||||
Timeout: 5 * time.Minute,
|
||||
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
|
||||
if e.config.SandboxDefaults != nil {
|
||||
if e.config.SandboxDefaults.Image != "" {
|
||||
|
||||
256
pkg/execution/images.go
Normal file
256
pkg/execution/images.go
Normal 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]
|
||||
}
|
||||
Reference in New Issue
Block a user