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
|
// 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
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