- Project infrastructure: go.mod, Makefile, .gitignore, README - Configuration system with YAML parsing and env overrides - Structured logging with slog - AI provider interface with Ollama, OpenAI, Anthropic implementations - Provider manager with runtime switching - Agent orchestrator with ReAct loop (reason-act-observe) - Tool registry with JSON Schema descriptions - Terminal tool: shell command execution with safety controls - Filesystem tools: read, write, list with path access control - Conversation memory with session management - Web UI server with WebSocket streaming - Modern dark theme UI with glassmorphism, animations - Frontend: WebSocket client, markdown rendering, tool call display
142 lines
3.5 KiB
Go
142 lines
3.5 KiB
Go
package agent
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log/slog"
|
|
"time"
|
|
|
|
"git.bebrik.xyz/Dan4ick/ZovOS_AI/internal/ai"
|
|
"git.bebrik.xyz/Dan4ick/ZovOS_AI/internal/config"
|
|
"git.bebrik.xyz/Dan4ick/ZovOS_AI/internal/tools"
|
|
)
|
|
|
|
// StreamCallback is called for each chunk of streamed output.
|
|
type StreamCallback func(chunk string, toolCall *ai.ToolCall, toolResult *string, done bool)
|
|
|
|
// Agent orchestrates the AI reasoning loop.
|
|
type Agent struct {
|
|
provider ai.Provider
|
|
tools *tools.Registry
|
|
cfg config.AgentConfig
|
|
log *slog.Logger
|
|
}
|
|
|
|
// New creates a new Agent.
|
|
func New(provider ai.Provider, toolsRegistry *tools.Registry, cfg config.AgentConfig, log *slog.Logger) *Agent {
|
|
return &Agent{
|
|
provider: provider,
|
|
tools: toolsRegistry,
|
|
cfg: cfg,
|
|
log: log,
|
|
}
|
|
}
|
|
|
|
// SetProvider updates the active AI provider.
|
|
func (a *Agent) SetProvider(p ai.Provider) {
|
|
a.provider = p
|
|
}
|
|
|
|
// Run executes the agent loop: send messages → get response → execute tools → repeat.
|
|
func (a *Agent) Run(ctx context.Context, userMessage string, history []ai.Message, callback StreamCallback) ([]ai.Message, error) {
|
|
timeout := time.Duration(a.cfg.TimeoutSeconds) * time.Second
|
|
ctx, cancel := context.WithTimeout(ctx, timeout)
|
|
defer cancel()
|
|
|
|
messages := make([]ai.Message, 0, len(history)+2)
|
|
|
|
// Add system prompt
|
|
if a.cfg.SystemPrompt != "" {
|
|
messages = append(messages, ai.Message{
|
|
Role: "system",
|
|
Content: a.cfg.SystemPrompt,
|
|
})
|
|
}
|
|
|
|
// Add conversation history
|
|
messages = append(messages, history...)
|
|
|
|
// Add new user message
|
|
messages = append(messages, ai.Message{
|
|
Role: "user",
|
|
Content: userMessage,
|
|
})
|
|
|
|
availableTools := a.tools.List()
|
|
|
|
for iteration := 0; iteration < a.cfg.MaxIterations; iteration++ {
|
|
a.log.Info("agent iteration", "iteration", iteration+1, "messages", len(messages))
|
|
|
|
// Try streaming first
|
|
streamCh, err := a.provider.StreamChat(ctx, messages, availableTools)
|
|
if err != nil {
|
|
return messages, fmt.Errorf("AI provider error: %w", err)
|
|
}
|
|
|
|
// Collect full response from stream
|
|
var fullContent string
|
|
var allToolCalls []ai.ToolCall
|
|
|
|
for chunk := range streamCh {
|
|
if chunk.Error != nil {
|
|
return messages, fmt.Errorf("stream error: %w", chunk.Error)
|
|
}
|
|
if chunk.Content != "" {
|
|
fullContent += chunk.Content
|
|
if callback != nil {
|
|
callback(chunk.Content, nil, nil, false)
|
|
}
|
|
}
|
|
if len(chunk.ToolCalls) > 0 {
|
|
allToolCalls = append(allToolCalls, chunk.ToolCalls...)
|
|
}
|
|
}
|
|
|
|
// Add assistant response to messages
|
|
assistantMsg := ai.Message{
|
|
Role: "assistant",
|
|
Content: fullContent,
|
|
ToolCalls: allToolCalls,
|
|
}
|
|
messages = append(messages, assistantMsg)
|
|
|
|
// If no tool calls, we're done
|
|
if len(allToolCalls) == 0 {
|
|
if callback != nil {
|
|
callback("", nil, nil, true)
|
|
}
|
|
return messages, nil
|
|
}
|
|
|
|
// Execute tool calls
|
|
for _, tc := range allToolCalls {
|
|
a.log.Info("executing tool call", "tool", tc.Function.Name, "id", tc.ID)
|
|
|
|
if callback != nil {
|
|
callback("", &tc, nil, false)
|
|
}
|
|
|
|
result, err := a.tools.Execute(ctx, tc.Function.Name, tc.Function.Arguments)
|
|
if err != nil {
|
|
result = fmt.Sprintf("Tool execution error: %v", err)
|
|
}
|
|
|
|
if callback != nil {
|
|
callback("", &tc, &result, false)
|
|
}
|
|
|
|
messages = append(messages, ai.Message{
|
|
Role: "tool",
|
|
Content: result,
|
|
ToolCallID: tc.ID,
|
|
Name: tc.Function.Name,
|
|
})
|
|
}
|
|
}
|
|
|
|
if callback != nil {
|
|
callback("", nil, nil, true)
|
|
}
|
|
|
|
return messages, fmt.Errorf("max iterations (%d) reached", a.cfg.MaxIterations)
|
|
}
|