Merge branch 'fix/io-migration-agentic' into new
# Conflicts: # pkg/agentic/config.go # pkg/agentic/context.go
This commit is contained in:
commit
0a553dcf6e
2 changed files with 70 additions and 114 deletions
|
|
@ -1,26 +1,25 @@
|
|||
package agentic
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/host-uk/core/pkg/config"
|
||||
"github.com/host-uk/core/pkg/errors"
|
||||
"github.com/host-uk/core/pkg/io"
|
||||
"github.com/host-uk/core/pkg/log"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// Config holds the configuration for connecting to the core-agentic service.
|
||||
type Config struct {
|
||||
// BaseURL is the URL of the core-agentic API server.
|
||||
BaseURL string `yaml:"base_url" json:"base_url" mapstructure:"base_url"`
|
||||
BaseURL string `yaml:"base_url" json:"base_url"`
|
||||
// Token is the authentication token for API requests.
|
||||
Token string `yaml:"token" json:"token" mapstructure:"token"`
|
||||
Token string `yaml:"token" json:"token"`
|
||||
// DefaultProject is the project to use when none is specified.
|
||||
DefaultProject string `yaml:"default_project" json:"default_project" mapstructure:"default_project"`
|
||||
DefaultProject string `yaml:"default_project" json:"default_project"`
|
||||
// AgentID is the identifier for this agent (optional, used for claiming tasks).
|
||||
AgentID string `yaml:"agent_id" json:"agent_id" mapstructure:"agent_id"`
|
||||
AgentID string `yaml:"agent_id" json:"agent_id"`
|
||||
}
|
||||
|
||||
// configFileName is the name of the YAML config file.
|
||||
|
|
@ -33,9 +32,10 @@ const envFileName = ".env"
|
|||
const DefaultBaseURL = "https://api.core-agentic.dev"
|
||||
|
||||
// LoadConfig loads the agentic configuration from the specified directory.
|
||||
// It uses the centralized config service.
|
||||
// It first checks for a .env file, then falls back to ~/.core/agentic.yaml.
|
||||
// If dir is empty, it checks the current directory first.
|
||||
//
|
||||
// Environment variables take precedence (prefix: AGENTIC_):
|
||||
// Environment variables take precedence:
|
||||
// - AGENTIC_BASE_URL: API base URL
|
||||
// - AGENTIC_TOKEN: Authentication token
|
||||
// - AGENTIC_PROJECT: Default project
|
||||
|
|
@ -58,6 +58,7 @@ func LoadConfig(dir string) (*Config, error) {
|
|||
}
|
||||
|
||||
// Try loading from current directory .env
|
||||
if dir == "" {
|
||||
cwd, err := os.Getwd()
|
||||
if err == nil {
|
||||
envPath := filepath.Join(cwd, envFileName)
|
||||
|
|
@ -68,23 +69,17 @@ func LoadConfig(dir string) (*Config, error) {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try loading from ~/.core/agentic.yaml
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return nil, log.E("agentic.LoadConfig", "failed to get home directory", err)
|
||||
return nil, errors.E("agentic.LoadConfig", "failed to get home directory", err)
|
||||
}
|
||||
|
||||
configPath := filepath.Join(homeDir, ".core", configFileName)
|
||||
if io.Local.IsFile(configPath) {
|
||||
// Use centralized config service to load the YAML file
|
||||
c, err := config.New(config.WithPath(configPath))
|
||||
if err != nil {
|
||||
return nil, log.E("agentic.LoadConfig", "failed to initialize config", err)
|
||||
}
|
||||
if err := c.Get("", cfg); err != nil {
|
||||
return nil, log.E("agentic.LoadConfig", "failed to load config", err)
|
||||
}
|
||||
if err := loadYAMLConfig(configPath, cfg); err != nil && !os.IsNotExist(err) {
|
||||
return nil, errors.E("agentic.LoadConfig", "failed to load config", err)
|
||||
}
|
||||
|
||||
// Apply environment variable overrides
|
||||
|
|
@ -92,25 +87,21 @@ func LoadConfig(dir string) (*Config, error) {
|
|||
|
||||
// Validate configuration
|
||||
if cfg.Token == "" {
|
||||
log.Security("agentic authentication failed: no token configured", "user", log.Username())
|
||||
return nil, log.E("agentic.LoadConfig", "no authentication token configured", nil)
|
||||
return nil, errors.E("agentic.LoadConfig", "no authentication token configured", nil)
|
||||
}
|
||||
|
||||
log.Security("agentic configuration loaded", "user", log.Username(), "baseURL", cfg.BaseURL)
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
// loadEnvFile reads a .env file and extracts agentic configuration.
|
||||
func loadEnvFile(path string, cfg *Config) error {
|
||||
file, err := os.Open(path)
|
||||
content, err := io.Local.Read(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { _ = file.Close() }()
|
||||
|
||||
scanner := bufio.NewScanner(file)
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
for _, line := range strings.Split(content, "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
|
||||
// Skip empty lines and comments
|
||||
if line == "" || strings.HasPrefix(line, "#") {
|
||||
|
|
@ -141,7 +132,17 @@ func loadEnvFile(path string, cfg *Config) error {
|
|||
}
|
||||
}
|
||||
|
||||
return scanner.Err()
|
||||
return nil
|
||||
}
|
||||
|
||||
// loadYAMLConfig reads configuration from a YAML file.
|
||||
func loadYAMLConfig(path string, cfg *Config) error {
|
||||
content, err := io.Local.Read(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return yaml.Unmarshal([]byte(content), cfg)
|
||||
}
|
||||
|
||||
// applyEnvOverrides applies environment variable overrides to the config.
|
||||
|
|
@ -162,25 +163,35 @@ func applyEnvOverrides(cfg *Config) {
|
|||
|
||||
// SaveConfig saves the configuration to ~/.core/agentic.yaml.
|
||||
func SaveConfig(cfg *Config) error {
|
||||
path, err := ConfigPath()
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return err
|
||||
return errors.E("agentic.SaveConfig", "failed to get home directory", err)
|
||||
}
|
||||
|
||||
data := make(map[string]any)
|
||||
data["base_url"] = cfg.BaseURL
|
||||
data["token"] = cfg.Token
|
||||
data["default_project"] = cfg.DefaultProject
|
||||
data["agent_id"] = cfg.AgentID
|
||||
configDir := filepath.Join(homeDir, ".core")
|
||||
if err := io.Local.EnsureDir(configDir); err != nil {
|
||||
return errors.E("agentic.SaveConfig", "failed to create config directory", err)
|
||||
}
|
||||
|
||||
return config.Save(io.Local, path, data)
|
||||
configPath := filepath.Join(configDir, configFileName)
|
||||
|
||||
data, err := yaml.Marshal(cfg)
|
||||
if err != nil {
|
||||
return errors.E("agentic.SaveConfig", "failed to marshal config", err)
|
||||
}
|
||||
|
||||
if err := io.Local.Write(configPath, string(data)); err != nil {
|
||||
return errors.E("agentic.SaveConfig", "failed to write config file", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ConfigPath returns the path to the config file in the user's home directory.
|
||||
func ConfigPath() (string, error) {
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return "", log.E("agentic.ConfigPath", "failed to get home directory", err)
|
||||
return "", errors.E("agentic.ConfigPath", "failed to get home directory", err)
|
||||
}
|
||||
return filepath.Join(homeDir, ".core", configFileName), nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,20 +3,16 @@ package agentic
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
goio "io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/host-uk/core/pkg/ai"
|
||||
"github.com/host-uk/core/pkg/errors"
|
||||
"github.com/host-uk/core/pkg/io"
|
||||
"github.com/host-uk/core/pkg/log"
|
||||
)
|
||||
|
||||
const maxContextBytes = 5000
|
||||
|
||||
// FileContent represents the content of a file for AI context.
|
||||
type FileContent struct {
|
||||
// Path is the relative path to the file.
|
||||
|
|
@ -39,8 +35,6 @@ type TaskContext struct {
|
|||
RecentCommits string `json:"recent_commits"`
|
||||
// RelatedCode contains code snippets related to the task.
|
||||
RelatedCode []FileContent `json:"related_code"`
|
||||
// RAGContext contains relevant documentation from the vector database.
|
||||
RAGContext string `json:"rag_context,omitempty"`
|
||||
}
|
||||
|
||||
// BuildTaskContext gathers context for AI collaboration on a task.
|
||||
|
|
@ -48,13 +42,13 @@ func BuildTaskContext(task *Task, dir string) (*TaskContext, error) {
|
|||
const op = "agentic.BuildTaskContext"
|
||||
|
||||
if task == nil {
|
||||
return nil, log.E(op, "task is required", nil)
|
||||
return nil, errors.E(op, "task is required", nil)
|
||||
}
|
||||
|
||||
if dir == "" {
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return nil, log.E(op, "failed to get working directory", err)
|
||||
return nil, errors.E(op, "failed to get working directory", err)
|
||||
}
|
||||
dir = cwd
|
||||
}
|
||||
|
|
@ -86,13 +80,6 @@ func BuildTaskContext(task *Task, dir string) (*TaskContext, error) {
|
|||
}
|
||||
ctx.RelatedCode = relatedCode
|
||||
|
||||
// Query RAG for relevant documentation (graceful degradation)
|
||||
ragCtx := ai.QueryRAGForTask(ai.TaskInfo{
|
||||
Title: task.Title,
|
||||
Description: task.Description,
|
||||
})
|
||||
ctx.RAGContext = ragCtx
|
||||
|
||||
return ctx, nil
|
||||
}
|
||||
|
||||
|
|
@ -101,31 +88,24 @@ func GatherRelatedFiles(task *Task, dir string) ([]FileContent, error) {
|
|||
const op = "agentic.GatherRelatedFiles"
|
||||
|
||||
if task == nil {
|
||||
return nil, log.E(op, "task is required", nil)
|
||||
return nil, errors.E(op, "task is required", nil)
|
||||
}
|
||||
|
||||
var files []FileContent
|
||||
|
||||
// Read files explicitly mentioned in the task
|
||||
for _, relPath := range task.Files {
|
||||
fullPath := relPath
|
||||
if !filepath.IsAbs(relPath) {
|
||||
fullPath = filepath.Join(dir, relPath)
|
||||
}
|
||||
fullPath := filepath.Join(dir, relPath)
|
||||
|
||||
content, truncated, err := readAndTruncate(fullPath)
|
||||
content, err := io.Local.Read(fullPath)
|
||||
if err != nil {
|
||||
// Skip files that don't exist
|
||||
continue
|
||||
}
|
||||
|
||||
contentStr := string(content)
|
||||
if truncated {
|
||||
contentStr += "\n... (truncated)"
|
||||
}
|
||||
|
||||
files = append(files, FileContent{
|
||||
Path: relPath,
|
||||
Content: contentStr,
|
||||
Content: content,
|
||||
Language: detectLanguage(relPath),
|
||||
})
|
||||
}
|
||||
|
|
@ -138,7 +118,7 @@ func findRelatedCode(task *Task, dir string) ([]FileContent, error) {
|
|||
const op = "agentic.findRelatedCode"
|
||||
|
||||
if task == nil {
|
||||
return nil, log.E(op, "task is required", nil)
|
||||
return nil, errors.E(op, "task is required", nil)
|
||||
}
|
||||
|
||||
// Extract keywords from title and description
|
||||
|
|
@ -174,24 +154,20 @@ func findRelatedCode(task *Task, dir string) ([]FileContent, error) {
|
|||
break
|
||||
}
|
||||
|
||||
fullPath := line
|
||||
if !filepath.IsAbs(line) {
|
||||
fullPath = filepath.Join(dir, line)
|
||||
}
|
||||
|
||||
content, truncated, err := readAndTruncate(fullPath)
|
||||
fullPath := filepath.Join(dir, line)
|
||||
content, err := io.Local.Read(fullPath)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
contentStr := string(content)
|
||||
if truncated {
|
||||
contentStr += "\n... (truncated)"
|
||||
// Truncate large files
|
||||
if len(content) > 5000 {
|
||||
content = content[:5000] + "\n... (truncated)"
|
||||
}
|
||||
|
||||
files = append(files, FileContent{
|
||||
Path: line,
|
||||
Content: contentStr,
|
||||
Content: content,
|
||||
Language: detectLanguage(line),
|
||||
})
|
||||
}
|
||||
|
|
@ -286,30 +262,6 @@ func detectLanguage(path string) string {
|
|||
return "text"
|
||||
}
|
||||
|
||||
// readAndTruncate reads up to maxContextBytes from a file.
|
||||
func readAndTruncate(path string) ([]byte, bool, error) {
|
||||
f, err := io.Local.ReadStream(path)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
defer func() { _ = f.Close() }()
|
||||
|
||||
// Read up to maxContextBytes + 1 to detect truncation
|
||||
reader := goio.LimitReader(f, maxContextBytes+1)
|
||||
content, err := goio.ReadAll(reader)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
truncated := false
|
||||
if len(content) > maxContextBytes {
|
||||
content = content[:maxContextBytes]
|
||||
truncated = true
|
||||
}
|
||||
|
||||
return content, truncated, nil
|
||||
}
|
||||
|
||||
// runGitCommand runs a git command and returns the output.
|
||||
func runGitCommand(dir string, args ...string) (string, error) {
|
||||
cmd := exec.Command("git", args...)
|
||||
|
|
@ -379,12 +331,5 @@ func (tc *TaskContext) FormatContext() string {
|
|||
}
|
||||
}
|
||||
|
||||
// Relevant documentation from RAG
|
||||
if tc.RAGContext != "" {
|
||||
sb.WriteString("## Relevant Documentation\n")
|
||||
sb.WriteString(tc.RAGContext)
|
||||
sb.WriteString("\n\n")
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue