feat: implement tool registry and add various tools for HTTP, file operations, SSH, and Matrix messaging
This commit is contained in:
+76
-13
@@ -71,8 +71,8 @@ type anthropicRequest struct {
|
||||
}
|
||||
|
||||
type anthropicMessage struct {
|
||||
Role string `json:"role"`
|
||||
Content string `json:"content"`
|
||||
Role string `json:"role"`
|
||||
Content json.RawMessage `json:"content"`
|
||||
}
|
||||
|
||||
type anthropicTool struct {
|
||||
@@ -81,12 +81,26 @@ type anthropicTool struct {
|
||||
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 []struct {
|
||||
Type string `json:"type"`
|
||||
Text string `json:"text"`
|
||||
} `json:"content"`
|
||||
Usage struct {
|
||||
Content []anthropicContentBlock `json:"content"`
|
||||
Usage struct {
|
||||
InputTokens int `json:"input_tokens"`
|
||||
OutputTokens int `json:"output_tokens"`
|
||||
} `json:"usage"`
|
||||
@@ -97,12 +111,9 @@ func toAnthropicRequest(req coretypes.CompletionRequest) anthropicRequest {
|
||||
msgs := make([]anthropicMessage, 0, len(req.Messages))
|
||||
for _, m := range req.Messages {
|
||||
if m.Role == coretypes.RoleSystem {
|
||||
continue // handled as top-level system param
|
||||
continue
|
||||
}
|
||||
msgs = append(msgs, anthropicMessage{
|
||||
Role: string(m.Role),
|
||||
Content: m.Content,
|
||||
})
|
||||
msgs = append(msgs, toAnthropicMessage(m))
|
||||
}
|
||||
|
||||
tools := make([]anthropicTool, len(req.Tools))
|
||||
@@ -123,19 +134,71 @@ func toAnthropicRequest(req coretypes.CompletionRequest) anthropicRequest {
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
if c.Type == "text" {
|
||||
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,
|
||||
|
||||
+81
-15
@@ -2,6 +2,7 @@ package llm
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
@@ -33,19 +34,7 @@ func NewOpenAIComplete(apiKeyEnv, baseURL string) coretypes.CompleteFunc {
|
||||
})
|
||||
}
|
||||
for _, m := range req.Messages {
|
||||
role := openai.ChatMessageRoleUser
|
||||
switch m.Role {
|
||||
case coretypes.RoleAssistant:
|
||||
role = openai.ChatMessageRoleAssistant
|
||||
case coretypes.RoleSystem:
|
||||
role = openai.ChatMessageRoleSystem
|
||||
case coretypes.RoleTool:
|
||||
role = openai.ChatMessageRoleTool
|
||||
}
|
||||
msgs = append(msgs, openai.ChatCompletionMessage{
|
||||
Role: role,
|
||||
Content: m.Content,
|
||||
})
|
||||
msgs = append(msgs, toOpenAIMessage(m))
|
||||
}
|
||||
|
||||
openReq := openai.ChatCompletionRequest{
|
||||
@@ -55,6 +44,11 @@ func NewOpenAIComplete(apiKeyEnv, baseURL string) coretypes.CompleteFunc {
|
||||
Temperature: float32(req.Temperature),
|
||||
}
|
||||
|
||||
// Add tools if present
|
||||
if len(req.Tools) > 0 {
|
||||
openReq.Tools = toOpenAITools(req.Tools)
|
||||
}
|
||||
|
||||
resp, err := client.CreateChatCompletion(ctx, openReq)
|
||||
if err != nil {
|
||||
return coretypes.CompletionResponse{}, fmt.Errorf("openai completion: %w", err)
|
||||
@@ -63,9 +57,20 @@ func NewOpenAIComplete(apiKeyEnv, baseURL string) coretypes.CompleteFunc {
|
||||
return coretypes.CompletionResponse{}, fmt.Errorf("openai: empty choices")
|
||||
}
|
||||
|
||||
choice := resp.Choices[0]
|
||||
var toolCalls []coretypes.ToolCall
|
||||
for _, tc := range choice.Message.ToolCalls {
|
||||
toolCalls = append(toolCalls, coretypes.ToolCall{
|
||||
ID: tc.ID,
|
||||
Name: tc.Function.Name,
|
||||
Arguments: tc.Function.Arguments,
|
||||
})
|
||||
}
|
||||
|
||||
return coretypes.CompletionResponse{
|
||||
Content: resp.Choices[0].Message.Content,
|
||||
FinishReason: string(resp.Choices[0].FinishReason),
|
||||
Content: choice.Message.Content,
|
||||
ToolCalls: toolCalls,
|
||||
FinishReason: string(choice.FinishReason),
|
||||
Usage: coretypes.TokenUsage{
|
||||
InputTokens: resp.Usage.PromptTokens,
|
||||
OutputTokens: resp.Usage.CompletionTokens,
|
||||
@@ -74,3 +79,64 @@ func NewOpenAIComplete(apiKeyEnv, baseURL string) coretypes.CompleteFunc {
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
// toOpenAIMessage converts a core Message to an OpenAI ChatCompletionMessage.
|
||||
func toOpenAIMessage(m coretypes.Message) openai.ChatCompletionMessage {
|
||||
role := openai.ChatMessageRoleUser
|
||||
switch m.Role {
|
||||
case coretypes.RoleAssistant:
|
||||
role = openai.ChatMessageRoleAssistant
|
||||
case coretypes.RoleSystem:
|
||||
role = openai.ChatMessageRoleSystem
|
||||
case coretypes.RoleTool:
|
||||
role = openai.ChatMessageRoleTool
|
||||
}
|
||||
|
||||
msg := openai.ChatCompletionMessage{
|
||||
Role: role,
|
||||
Content: m.Content,
|
||||
ToolCallID: m.ToolCallID,
|
||||
}
|
||||
|
||||
// Assistant messages with tool calls
|
||||
if m.Role == coretypes.RoleAssistant && len(m.ToolCalls) > 0 {
|
||||
msg.ToolCalls = make([]openai.ToolCall, len(m.ToolCalls))
|
||||
for i, tc := range m.ToolCalls {
|
||||
msg.ToolCalls[i] = openai.ToolCall{
|
||||
ID: tc.ID,
|
||||
Type: openai.ToolTypeFunction,
|
||||
Function: openai.FunctionCall{
|
||||
Name: tc.Name,
|
||||
Arguments: tc.Arguments,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return msg
|
||||
}
|
||||
|
||||
// toOpenAITools converts core ToolSpecs to OpenAI Tool format.
|
||||
func toOpenAITools(specs []coretypes.ToolSpec) []openai.Tool {
|
||||
tools := make([]openai.Tool, len(specs))
|
||||
for i, s := range specs {
|
||||
tools[i] = openai.Tool{
|
||||
Type: openai.ToolTypeFunction,
|
||||
Function: &openai.FunctionDefinition{
|
||||
Name: s.Name,
|
||||
Description: s.Description,
|
||||
Parameters: json.RawMessage(marshalSchema(s.InputSchema)),
|
||||
},
|
||||
}
|
||||
}
|
||||
return tools
|
||||
}
|
||||
|
||||
// marshalSchema marshals a JSON schema map to bytes. Falls back to empty object.
|
||||
func marshalSchema(schema map[string]any) []byte {
|
||||
b, err := json.Marshal(schema)
|
||||
if err != nil {
|
||||
return []byte("{}")
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user