feat: import agents_and_robots platform as unibots (Matrix-out, unibus transport)
Reemplaza el scaffold del echobot por la plataforma completa de bots traida desde ~/DataProyects/Github/agents_and_robots tras la operacion Matrix-out: los bots ya no hablan por Matrix sino por el bus unibus (modelo todo-rooms + E2E via shell/transportunibus sobre github.com/enmanuel/unibus/pkg/client). - go.mod: replace de unibus -> ../unibus y de fn-registry -> ../../../.. (paths relativos reajustados a la nueva ubicacion dentro de fn_registry). - app.md: bump a 0.2.0, descripcion + arquitectura + comandos + gotchas reales. - modulo Go conservado como github.com/enmanuel/agents (sin reescribir imports). agents_and_robots queda archivado como museo de la era Matrix.
This commit is contained in:
@@ -0,0 +1,242 @@
|
||||
// Package llm contains impure LLM provider implementations.
|
||||
package llm
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"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, log *slog.Logger) coretypes.CompleteFunc {
|
||||
if baseURL == "" {
|
||||
baseURL = anthropicAPIBase
|
||||
}
|
||||
|
||||
return func(ctx context.Context, req coretypes.CompletionRequest) (coretypes.CompletionResponse, error) {
|
||||
apiKey := os.Getenv(apiKeyEnv)
|
||||
if apiKey == "" {
|
||||
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 {
|
||||
return coretypes.CompletionResponse{}, fmt.Errorf("marshal request: %w", err)
|
||||
}
|
||||
|
||||
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, baseURL+"/messages", bytes.NewReader(raw))
|
||||
if err != nil {
|
||||
return coretypes.CompletionResponse{}, err
|
||||
}
|
||||
httpReq.Header.Set("x-api-key", apiKey)
|
||||
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()
|
||||
|
||||
respBytes, err := io.ReadAll(resp.Body)
|
||||
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)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// ── private conversion helpers ────────────────────────────────────────────
|
||||
|
||||
type anthropicRequest struct {
|
||||
Model string `json:"model"`
|
||||
MaxTokens int `json:"max_tokens"`
|
||||
System string `json:"system,omitempty"`
|
||||
Messages []anthropicMessage `json:"messages"`
|
||||
Tools []anthropicTool `json:"tools,omitempty"`
|
||||
}
|
||||
|
||||
type anthropicMessage struct {
|
||||
Role string `json:"role"`
|
||||
Content json.RawMessage `json:"content"`
|
||||
}
|
||||
|
||||
type anthropicTool struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
InputSchema map[string]any `json:"input_schema"`
|
||||
}
|
||||
|
||||
// anthropicContentBlock represents a block in a content array.
|
||||
type anthropicContentBlock struct {
|
||||
Type string `json:"type"`
|
||||
|
||||
// text block
|
||||
Text string `json:"text,omitempty"`
|
||||
|
||||
// tool_use block (in assistant responses)
|
||||
ID string `json:"id,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Input map[string]any `json:"input,omitempty"`
|
||||
|
||||
// tool_result block (in user messages)
|
||||
ToolUseID string `json:"tool_use_id,omitempty"`
|
||||
Content string `json:"content,omitempty"`
|
||||
}
|
||||
|
||||
type anthropicResponse struct {
|
||||
Content []anthropicContentBlock `json:"content"`
|
||||
Usage struct {
|
||||
InputTokens int `json:"input_tokens"`
|
||||
OutputTokens int `json:"output_tokens"`
|
||||
} `json:"usage"`
|
||||
StopReason string `json:"stop_reason"`
|
||||
}
|
||||
|
||||
func toAnthropicRequest(req coretypes.CompletionRequest) anthropicRequest {
|
||||
msgs := make([]anthropicMessage, 0, len(req.Messages))
|
||||
for _, m := range req.Messages {
|
||||
if m.Role == coretypes.RoleSystem {
|
||||
continue
|
||||
}
|
||||
msgs = append(msgs, toAnthropicMessage(m))
|
||||
}
|
||||
|
||||
tools := make([]anthropicTool, len(req.Tools))
|
||||
for i, t := range req.Tools {
|
||||
tools[i] = anthropicTool{
|
||||
Name: t.Name,
|
||||
Description: t.Description,
|
||||
InputSchema: t.InputSchema,
|
||||
}
|
||||
}
|
||||
|
||||
return anthropicRequest{
|
||||
Model: req.Model,
|
||||
MaxTokens: req.MaxTokens,
|
||||
System: req.SystemPrompt,
|
||||
Messages: msgs,
|
||||
Tools: tools,
|
||||
}
|
||||
}
|
||||
|
||||
// toAnthropicMessage converts a core Message to the Anthropic format.
|
||||
// Handles plain text, assistant messages with tool calls, and tool result messages.
|
||||
func toAnthropicMessage(m coretypes.Message) anthropicMessage {
|
||||
// Assistant message with tool calls → content array with text + tool_use blocks
|
||||
if m.Role == coretypes.RoleAssistant && len(m.ToolCalls) > 0 {
|
||||
blocks := make([]anthropicContentBlock, 0, len(m.ToolCalls)+1)
|
||||
if m.Content != "" {
|
||||
blocks = append(blocks, anthropicContentBlock{Type: "text", Text: m.Content})
|
||||
}
|
||||
for _, tc := range m.ToolCalls {
|
||||
var input map[string]any
|
||||
_ = json.Unmarshal([]byte(tc.Arguments), &input)
|
||||
blocks = append(blocks, anthropicContentBlock{
|
||||
Type: "tool_use",
|
||||
ID: tc.ID,
|
||||
Name: tc.Name,
|
||||
Input: input,
|
||||
})
|
||||
}
|
||||
raw, _ := json.Marshal(blocks)
|
||||
return anthropicMessage{Role: "assistant", Content: raw}
|
||||
}
|
||||
|
||||
// Tool result message → user message with tool_result content array
|
||||
if m.Role == coretypes.RoleTool {
|
||||
blocks := []anthropicContentBlock{{
|
||||
Type: "tool_result",
|
||||
ToolUseID: m.ToolCallID,
|
||||
Content: m.Content,
|
||||
}}
|
||||
raw, _ := json.Marshal(blocks)
|
||||
return anthropicMessage{Role: "user", Content: raw}
|
||||
}
|
||||
|
||||
// Plain text message
|
||||
raw, _ := json.Marshal(m.Content)
|
||||
return anthropicMessage{Role: string(m.Role), Content: raw}
|
||||
}
|
||||
|
||||
func fromAnthropicResponse(raw []byte) (coretypes.CompletionResponse, error) {
|
||||
var ar anthropicResponse
|
||||
if err := json.Unmarshal(raw, &ar); err != nil {
|
||||
return coretypes.CompletionResponse{}, fmt.Errorf("unmarshal response: %w", err)
|
||||
}
|
||||
|
||||
var content string
|
||||
var toolCalls []coretypes.ToolCall
|
||||
|
||||
for _, c := range ar.Content {
|
||||
switch c.Type {
|
||||
case "text":
|
||||
content += c.Text
|
||||
case "tool_use":
|
||||
argsJSON, _ := json.Marshal(c.Input)
|
||||
toolCalls = append(toolCalls, coretypes.ToolCall{
|
||||
ID: c.ID,
|
||||
Name: c.Name,
|
||||
Arguments: string(argsJSON),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return coretypes.CompletionResponse{
|
||||
Content: content,
|
||||
ToolCalls: toolCalls,
|
||||
FinishReason: ar.StopReason,
|
||||
Usage: coretypes.TokenUsage{
|
||||
InputTokens: ar.Usage.InputTokens,
|
||||
OutputTokens: ar.Usage.OutputTokens,
|
||||
TotalTokens: ar.Usage.InputTokens + ar.Usage.OutputTokens,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
Reference in New Issue
Block a user