feat: integrar structured logging en todos los componentes del shell
Se propaga *slog.Logger a todos los componentes impuros del shell: - shell/bus/ — logs de subscribe, send, reply, timeout, unsubscribe - shell/effects/ — duración y resultado de cada action ejecutada - shell/llm/ (anthropic, openai, factory) — request/response con tokens, duración, fallback - shell/memory/sqlite — open, save, recall, close con detalles - shell/ssh/ — inicio, fin, errores y duración de comandos SSH - tools/registry — registro, ejecución y errores de herramientas Se usa el paquete shell/logger para field names consistentes (FieldDurationMS, FieldTokensUsed, etc.). Cada componente recibe el logger por inyección de dependencias, sin globals. Las firmas de New/FromConfig se actualizan para aceptar *slog.Logger. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+35
-2
@@ -7,17 +7,20 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
coretypes "github.com/enmanuel/agents/pkg/llm"
|
||||
"github.com/enmanuel/agents/shell/logger"
|
||||
)
|
||||
|
||||
const anthropicAPIBase = "https://api.anthropic.com/v1"
|
||||
const anthropicVersion = "2023-06-01"
|
||||
|
||||
// NewAnthropicComplete returns a CompleteFunc backed by the Anthropic API.
|
||||
func NewAnthropicComplete(apiKeyEnv, baseURL string) coretypes.CompleteFunc {
|
||||
func NewAnthropicComplete(apiKeyEnv, baseURL string, log *slog.Logger) coretypes.CompleteFunc {
|
||||
if baseURL == "" {
|
||||
baseURL = anthropicAPIBase
|
||||
}
|
||||
@@ -28,6 +31,13 @@ func NewAnthropicComplete(apiKeyEnv, baseURL string) coretypes.CompleteFunc {
|
||||
return coretypes.CompletionResponse{}, fmt.Errorf("env var %s is not set", apiKeyEnv)
|
||||
}
|
||||
|
||||
log.Info("llm_request",
|
||||
"provider", "anthropic",
|
||||
"model", req.Model,
|
||||
"messages", len(req.Messages),
|
||||
"tools", len(req.Tools),
|
||||
)
|
||||
|
||||
body := toAnthropicRequest(req)
|
||||
raw, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
@@ -42,8 +52,11 @@ func NewAnthropicComplete(apiKeyEnv, baseURL string) coretypes.CompleteFunc {
|
||||
httpReq.Header.Set("anthropic-version", anthropicVersion)
|
||||
httpReq.Header.Set("content-type", "application/json")
|
||||
|
||||
start := time.Now()
|
||||
resp, err := http.DefaultClient.Do(httpReq)
|
||||
if err != nil {
|
||||
ms := time.Since(start).Milliseconds()
|
||||
log.Error("llm_error", "provider", "anthropic", logger.FieldDurationMS, ms, "err", err)
|
||||
return coretypes.CompletionResponse{}, fmt.Errorf("anthropic request: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
@@ -52,11 +65,31 @@ func NewAnthropicComplete(apiKeyEnv, baseURL string) coretypes.CompleteFunc {
|
||||
if err != nil {
|
||||
return coretypes.CompletionResponse{}, fmt.Errorf("read response: %w", err)
|
||||
}
|
||||
|
||||
ms := time.Since(start).Milliseconds()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
log.Error("llm_error", "provider", "anthropic", logger.FieldDurationMS, ms, "status", resp.StatusCode)
|
||||
return coretypes.CompletionResponse{}, fmt.Errorf("anthropic error %d: %s", resp.StatusCode, respBytes)
|
||||
}
|
||||
|
||||
return fromAnthropicResponse(respBytes)
|
||||
result, err := fromAnthropicResponse(respBytes)
|
||||
if err != nil {
|
||||
log.Error("llm_error", "provider", "anthropic", logger.FieldDurationMS, ms, "err", err)
|
||||
return result, err
|
||||
}
|
||||
|
||||
log.Info("llm_response",
|
||||
"provider", "anthropic",
|
||||
"model", req.Model,
|
||||
logger.FieldDurationMS, ms,
|
||||
logger.FieldTokensUsed, result.Usage.TotalTokens,
|
||||
"input_tokens", result.Usage.InputTokens,
|
||||
"output_tokens", result.Usage.OutputTokens,
|
||||
"tool_calls", len(result.ToolCalls),
|
||||
"finish_reason", result.FinishReason,
|
||||
)
|
||||
|
||||
return result, nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,24 +3,26 @@ package llm
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
|
||||
"github.com/enmanuel/agents/internal/config"
|
||||
coretypes "github.com/enmanuel/agents/pkg/llm"
|
||||
)
|
||||
|
||||
// FromConfig builds a CompleteFunc from an LLMProviderCfg.
|
||||
func FromConfig(cfg config.LLMProviderCfg) (coretypes.CompleteFunc, error) {
|
||||
func FromConfig(cfg config.LLMProviderCfg, log *slog.Logger) (coretypes.CompleteFunc, error) {
|
||||
log.Info("llm_provider_init", "provider", cfg.Provider, "model", cfg.Model)
|
||||
switch cfg.Provider {
|
||||
case "anthropic":
|
||||
return NewAnthropicComplete(cfg.APIKeyEnv, cfg.BaseURL), nil
|
||||
return NewAnthropicComplete(cfg.APIKeyEnv, cfg.BaseURL, log), nil
|
||||
case "openai":
|
||||
return NewOpenAIComplete(cfg.APIKeyEnv, cfg.BaseURL), nil
|
||||
return NewOpenAIComplete(cfg.APIKeyEnv, cfg.BaseURL, log), nil
|
||||
case "ollama":
|
||||
base := cfg.BaseURL
|
||||
if base == "" {
|
||||
base = "http://localhost:11434/v1"
|
||||
}
|
||||
return NewOpenAIComplete("OLLAMA_API_KEY", base), nil
|
||||
return NewOpenAIComplete("OLLAMA_API_KEY", base, log), nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown LLM provider: %s", cfg.Provider)
|
||||
}
|
||||
@@ -28,10 +30,11 @@ func FromConfig(cfg config.LLMProviderCfg) (coretypes.CompleteFunc, error) {
|
||||
|
||||
// WithFallback wraps primary with a fallback CompleteFunc.
|
||||
// If primary returns an error, fallback is tried.
|
||||
func WithFallback(primary, fallback coretypes.CompleteFunc) coretypes.CompleteFunc {
|
||||
func WithFallback(primary, fallback coretypes.CompleteFunc, log *slog.Logger) coretypes.CompleteFunc {
|
||||
return func(ctx context.Context, req coretypes.CompletionRequest) (coretypes.CompletionResponse, error) {
|
||||
resp, err := primary(ctx, req)
|
||||
if err != nil {
|
||||
log.Warn("llm_fallback_triggered", "primary_err", err)
|
||||
return fallback(ctx, req)
|
||||
}
|
||||
return resp, nil
|
||||
|
||||
+28
-1
@@ -4,16 +4,19 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
openai "github.com/sashabaranov/go-openai"
|
||||
|
||||
coretypes "github.com/enmanuel/agents/pkg/llm"
|
||||
"github.com/enmanuel/agents/shell/logger"
|
||||
)
|
||||
|
||||
// NewOpenAIComplete returns a CompleteFunc backed by the OpenAI-compatible API.
|
||||
// Works with OpenAI, Ollama, vLLM, LMStudio — just change baseURL.
|
||||
func NewOpenAIComplete(apiKeyEnv, baseURL string) coretypes.CompleteFunc {
|
||||
func NewOpenAIComplete(apiKeyEnv, baseURL string, log *slog.Logger) coretypes.CompleteFunc {
|
||||
return func(ctx context.Context, req coretypes.CompletionRequest) (coretypes.CompletionResponse, error) {
|
||||
apiKey := os.Getenv(apiKeyEnv)
|
||||
if apiKey == "" {
|
||||
@@ -49,11 +52,24 @@ func NewOpenAIComplete(apiKeyEnv, baseURL string) coretypes.CompleteFunc {
|
||||
openReq.Tools = toOpenAITools(req.Tools)
|
||||
}
|
||||
|
||||
log.Info("llm_request",
|
||||
"provider", "openai",
|
||||
"model", req.Model,
|
||||
"messages", len(req.Messages),
|
||||
"tools", len(req.Tools),
|
||||
)
|
||||
|
||||
start := time.Now()
|
||||
resp, err := client.CreateChatCompletion(ctx, openReq)
|
||||
if err != nil {
|
||||
ms := time.Since(start).Milliseconds()
|
||||
log.Error("llm_error", "provider", "openai", logger.FieldDurationMS, ms, "err", err)
|
||||
return coretypes.CompletionResponse{}, fmt.Errorf("openai completion: %w", err)
|
||||
}
|
||||
ms := time.Since(start).Milliseconds()
|
||||
|
||||
if len(resp.Choices) == 0 {
|
||||
log.Error("llm_error", "provider", "openai", logger.FieldDurationMS, ms, "err", "empty choices")
|
||||
return coretypes.CompletionResponse{}, fmt.Errorf("openai: empty choices")
|
||||
}
|
||||
|
||||
@@ -67,6 +83,17 @@ func NewOpenAIComplete(apiKeyEnv, baseURL string) coretypes.CompleteFunc {
|
||||
})
|
||||
}
|
||||
|
||||
log.Info("llm_response",
|
||||
"provider", "openai",
|
||||
"model", req.Model,
|
||||
logger.FieldDurationMS, ms,
|
||||
logger.FieldTokensUsed, resp.Usage.TotalTokens,
|
||||
"input_tokens", resp.Usage.PromptTokens,
|
||||
"output_tokens", resp.Usage.CompletionTokens,
|
||||
"tool_calls", len(toolCalls),
|
||||
"finish_reason", string(choice.FinishReason),
|
||||
)
|
||||
|
||||
return coretypes.CompletionResponse{
|
||||
Content: choice.Message.Content,
|
||||
ToolCalls: toolCalls,
|
||||
|
||||
Reference in New Issue
Block a user