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
|
package agentic
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"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/io"
|
||||||
"github.com/host-uk/core/pkg/log"
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Config holds the configuration for connecting to the core-agentic service.
|
// Config holds the configuration for connecting to the core-agentic service.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
// BaseURL is the URL of the core-agentic API server.
|
// 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 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 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 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.
|
// configFileName is the name of the YAML config file.
|
||||||
|
|
@ -33,9 +32,10 @@ const envFileName = ".env"
|
||||||
const DefaultBaseURL = "https://api.core-agentic.dev"
|
const DefaultBaseURL = "https://api.core-agentic.dev"
|
||||||
|
|
||||||
// LoadConfig loads the agentic configuration from the specified directory.
|
// 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_BASE_URL: API base URL
|
||||||
// - AGENTIC_TOKEN: Authentication token
|
// - AGENTIC_TOKEN: Authentication token
|
||||||
// - AGENTIC_PROJECT: Default project
|
// - AGENTIC_PROJECT: Default project
|
||||||
|
|
@ -58,13 +58,15 @@ func LoadConfig(dir string) (*Config, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try loading from current directory .env
|
// Try loading from current directory .env
|
||||||
cwd, err := os.Getwd()
|
if dir == "" {
|
||||||
if err == nil {
|
cwd, err := os.Getwd()
|
||||||
envPath := filepath.Join(cwd, envFileName)
|
if err == nil {
|
||||||
if err := loadEnvFile(envPath, cfg); err == nil {
|
envPath := filepath.Join(cwd, envFileName)
|
||||||
applyEnvOverrides(cfg)
|
if err := loadEnvFile(envPath, cfg); err == nil {
|
||||||
if cfg.Token != "" {
|
applyEnvOverrides(cfg)
|
||||||
return cfg, nil
|
if cfg.Token != "" {
|
||||||
|
return cfg, nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -72,19 +74,12 @@ func LoadConfig(dir string) (*Config, error) {
|
||||||
// Try loading from ~/.core/agentic.yaml
|
// Try loading from ~/.core/agentic.yaml
|
||||||
homeDir, err := os.UserHomeDir()
|
homeDir, err := os.UserHomeDir()
|
||||||
if err != nil {
|
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)
|
configPath := filepath.Join(homeDir, ".core", configFileName)
|
||||||
if io.Local.IsFile(configPath) {
|
if err := loadYAMLConfig(configPath, cfg); err != nil && !os.IsNotExist(err) {
|
||||||
// Use centralized config service to load the YAML file
|
return nil, errors.E("agentic.LoadConfig", "failed to load config", err)
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply environment variable overrides
|
// Apply environment variable overrides
|
||||||
|
|
@ -92,25 +87,21 @@ func LoadConfig(dir string) (*Config, error) {
|
||||||
|
|
||||||
// Validate configuration
|
// Validate configuration
|
||||||
if cfg.Token == "" {
|
if cfg.Token == "" {
|
||||||
log.Security("agentic authentication failed: no token configured", "user", log.Username())
|
return nil, errors.E("agentic.LoadConfig", "no authentication token configured", nil)
|
||||||
return nil, log.E("agentic.LoadConfig", "no authentication token configured", nil)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Security("agentic configuration loaded", "user", log.Username(), "baseURL", cfg.BaseURL)
|
|
||||||
return cfg, nil
|
return cfg, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// loadEnvFile reads a .env file and extracts agentic configuration.
|
// loadEnvFile reads a .env file and extracts agentic configuration.
|
||||||
func loadEnvFile(path string, cfg *Config) error {
|
func loadEnvFile(path string, cfg *Config) error {
|
||||||
file, err := os.Open(path)
|
content, err := io.Local.Read(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer func() { _ = file.Close() }()
|
|
||||||
|
|
||||||
scanner := bufio.NewScanner(file)
|
for _, line := range strings.Split(content, "\n") {
|
||||||
for scanner.Scan() {
|
line = strings.TrimSpace(line)
|
||||||
line := strings.TrimSpace(scanner.Text())
|
|
||||||
|
|
||||||
// Skip empty lines and comments
|
// Skip empty lines and comments
|
||||||
if line == "" || strings.HasPrefix(line, "#") {
|
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.
|
// applyEnvOverrides applies environment variable overrides to the config.
|
||||||
|
|
@ -162,25 +163,35 @@ func applyEnvOverrides(cfg *Config) {
|
||||||
|
|
||||||
// SaveConfig saves the configuration to ~/.core/agentic.yaml.
|
// SaveConfig saves the configuration to ~/.core/agentic.yaml.
|
||||||
func SaveConfig(cfg *Config) error {
|
func SaveConfig(cfg *Config) error {
|
||||||
path, err := ConfigPath()
|
homeDir, err := os.UserHomeDir()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return errors.E("agentic.SaveConfig", "failed to get home directory", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
data := make(map[string]any)
|
configDir := filepath.Join(homeDir, ".core")
|
||||||
data["base_url"] = cfg.BaseURL
|
if err := io.Local.EnsureDir(configDir); err != nil {
|
||||||
data["token"] = cfg.Token
|
return errors.E("agentic.SaveConfig", "failed to create config directory", err)
|
||||||
data["default_project"] = cfg.DefaultProject
|
}
|
||||||
data["agent_id"] = cfg.AgentID
|
|
||||||
|
|
||||||
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.
|
// ConfigPath returns the path to the config file in the user's home directory.
|
||||||
func ConfigPath() (string, error) {
|
func ConfigPath() (string, error) {
|
||||||
homeDir, err := os.UserHomeDir()
|
homeDir, err := os.UserHomeDir()
|
||||||
if err != nil {
|
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
|
return filepath.Join(homeDir, ".core", configFileName), nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,20 +3,16 @@ package agentic
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
goio "io"
|
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"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/io"
|
||||||
"github.com/host-uk/core/pkg/log"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const maxContextBytes = 5000
|
|
||||||
|
|
||||||
// FileContent represents the content of a file for AI context.
|
// FileContent represents the content of a file for AI context.
|
||||||
type FileContent struct {
|
type FileContent struct {
|
||||||
// Path is the relative path to the file.
|
// Path is the relative path to the file.
|
||||||
|
|
@ -39,8 +35,6 @@ type TaskContext struct {
|
||||||
RecentCommits string `json:"recent_commits"`
|
RecentCommits string `json:"recent_commits"`
|
||||||
// RelatedCode contains code snippets related to the task.
|
// RelatedCode contains code snippets related to the task.
|
||||||
RelatedCode []FileContent `json:"related_code"`
|
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.
|
// 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"
|
const op = "agentic.BuildTaskContext"
|
||||||
|
|
||||||
if task == nil {
|
if task == nil {
|
||||||
return nil, log.E(op, "task is required", nil)
|
return nil, errors.E(op, "task is required", nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
if dir == "" {
|
if dir == "" {
|
||||||
cwd, err := os.Getwd()
|
cwd, err := os.Getwd()
|
||||||
if err != nil {
|
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
|
dir = cwd
|
||||||
}
|
}
|
||||||
|
|
@ -86,13 +80,6 @@ func BuildTaskContext(task *Task, dir string) (*TaskContext, error) {
|
||||||
}
|
}
|
||||||
ctx.RelatedCode = relatedCode
|
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
|
return ctx, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -101,31 +88,24 @@ func GatherRelatedFiles(task *Task, dir string) ([]FileContent, error) {
|
||||||
const op = "agentic.GatherRelatedFiles"
|
const op = "agentic.GatherRelatedFiles"
|
||||||
|
|
||||||
if task == nil {
|
if task == nil {
|
||||||
return nil, log.E(op, "task is required", nil)
|
return nil, errors.E(op, "task is required", nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
var files []FileContent
|
var files []FileContent
|
||||||
|
|
||||||
// Read files explicitly mentioned in the task
|
// Read files explicitly mentioned in the task
|
||||||
for _, relPath := range task.Files {
|
for _, relPath := range task.Files {
|
||||||
fullPath := relPath
|
fullPath := filepath.Join(dir, relPath)
|
||||||
if !filepath.IsAbs(relPath) {
|
|
||||||
fullPath = filepath.Join(dir, relPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
content, truncated, err := readAndTruncate(fullPath)
|
content, err := io.Local.Read(fullPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
// Skip files that don't exist
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
contentStr := string(content)
|
|
||||||
if truncated {
|
|
||||||
contentStr += "\n... (truncated)"
|
|
||||||
}
|
|
||||||
|
|
||||||
files = append(files, FileContent{
|
files = append(files, FileContent{
|
||||||
Path: relPath,
|
Path: relPath,
|
||||||
Content: contentStr,
|
Content: content,
|
||||||
Language: detectLanguage(relPath),
|
Language: detectLanguage(relPath),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -138,7 +118,7 @@ func findRelatedCode(task *Task, dir string) ([]FileContent, error) {
|
||||||
const op = "agentic.findRelatedCode"
|
const op = "agentic.findRelatedCode"
|
||||||
|
|
||||||
if task == nil {
|
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
|
// Extract keywords from title and description
|
||||||
|
|
@ -174,24 +154,20 @@ func findRelatedCode(task *Task, dir string) ([]FileContent, error) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
fullPath := line
|
fullPath := filepath.Join(dir, line)
|
||||||
if !filepath.IsAbs(line) {
|
content, err := io.Local.Read(fullPath)
|
||||||
fullPath = filepath.Join(dir, line)
|
|
||||||
}
|
|
||||||
|
|
||||||
content, truncated, err := readAndTruncate(fullPath)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
contentStr := string(content)
|
// Truncate large files
|
||||||
if truncated {
|
if len(content) > 5000 {
|
||||||
contentStr += "\n... (truncated)"
|
content = content[:5000] + "\n... (truncated)"
|
||||||
}
|
}
|
||||||
|
|
||||||
files = append(files, FileContent{
|
files = append(files, FileContent{
|
||||||
Path: line,
|
Path: line,
|
||||||
Content: contentStr,
|
Content: content,
|
||||||
Language: detectLanguage(line),
|
Language: detectLanguage(line),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -286,30 +262,6 @@ func detectLanguage(path string) string {
|
||||||
return "text"
|
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.
|
// runGitCommand runs a git command and returns the output.
|
||||||
func runGitCommand(dir string, args ...string) (string, error) {
|
func runGitCommand(dir string, args ...string) (string, error) {
|
||||||
cmd := exec.Command("git", args...)
|
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()
|
return sb.String()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue