feat(fmt): Run formatters
This commit is contained in:
parent
b1e3b11a5d
commit
9e120f90ea
96 changed files with 1239 additions and 976 deletions
20
.github/workflows/pr.yml
vendored
20
.github/workflows/pr.yml
vendored
|
|
@ -24,29 +24,10 @@ jobs:
|
||||||
with:
|
with:
|
||||||
version: v2.10.1
|
version: v2.10.1
|
||||||
|
|
||||||
# TODO: Remove once linter is properly configured
|
|
||||||
fmt-check:
|
|
||||||
name: Formatting
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v6
|
|
||||||
|
|
||||||
- name: Setup Go
|
|
||||||
uses: actions/setup-go@v6
|
|
||||||
with:
|
|
||||||
go-version-file: go.mod
|
|
||||||
|
|
||||||
- name: Check formatting
|
|
||||||
run: |
|
|
||||||
make fmt
|
|
||||||
git diff --exit-code || (echo "::error::Code is not formatted. Run 'make fmt' and commit the changes." && exit 1)
|
|
||||||
|
|
||||||
# TODO: Remove once linter is properly configured
|
# TODO: Remove once linter is properly configured
|
||||||
vet:
|
vet:
|
||||||
name: Vet
|
name: Vet
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: fmt-check
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
|
|
@ -65,7 +46,6 @@ jobs:
|
||||||
test:
|
test:
|
||||||
name: Tests
|
name: Tests
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: fmt-check
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
|
|
|
||||||
|
|
@ -160,12 +160,11 @@ issues:
|
||||||
|
|
||||||
formatters:
|
formatters:
|
||||||
enable:
|
enable:
|
||||||
|
- gci
|
||||||
|
- gofmt
|
||||||
|
- gofumpt
|
||||||
- goimports
|
- goimports
|
||||||
# TODO: Disabled, because they are failing at the moment, we should fix them and enable (step by step)
|
- golines
|
||||||
# - gci
|
|
||||||
# - gofmt
|
|
||||||
# - gofumpt
|
|
||||||
# - golines
|
|
||||||
settings:
|
settings:
|
||||||
gci:
|
gci:
|
||||||
sections:
|
sections:
|
||||||
|
|
|
||||||
11
Makefile
11
Makefile
|
|
@ -17,6 +17,9 @@ LDFLAGS=-ldflags "-X main.version=$(VERSION) -X main.gitCommit=$(GIT_COMMIT) -X
|
||||||
GO?=go
|
GO?=go
|
||||||
GOFLAGS?=-v -tags stdjson
|
GOFLAGS?=-v -tags stdjson
|
||||||
|
|
||||||
|
# Golangci-lint
|
||||||
|
GOLANGCI_LINT?=golangci-lint
|
||||||
|
|
||||||
# Installation
|
# Installation
|
||||||
INSTALL_PREFIX?=$(HOME)/.local
|
INSTALL_PREFIX?=$(HOME)/.local
|
||||||
INSTALL_BIN_DIR=$(INSTALL_PREFIX)/bin
|
INSTALL_BIN_DIR=$(INSTALL_PREFIX)/bin
|
||||||
|
|
@ -126,13 +129,17 @@ clean:
|
||||||
vet:
|
vet:
|
||||||
@$(GO) vet ./...
|
@$(GO) vet ./...
|
||||||
|
|
||||||
## fmt: Format Go code
|
## test: Test Go code
|
||||||
test:
|
test:
|
||||||
@$(GO) test ./...
|
@$(GO) test ./...
|
||||||
|
|
||||||
## fmt: Format Go code
|
## fmt: Format Go code
|
||||||
fmt:
|
fmt:
|
||||||
@$(GO) fmt ./...
|
@$(GOLANGCI_LINT) fmt
|
||||||
|
|
||||||
|
## lint: Run linters
|
||||||
|
lint:
|
||||||
|
@$(GOLANGCI_LINT) run
|
||||||
|
|
||||||
## deps: Download dependencies
|
## deps: Download dependencies
|
||||||
deps:
|
deps:
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/chzyer/readline"
|
"github.com/chzyer/readline"
|
||||||
|
|
||||||
"github.com/sipeed/picoclaw/pkg/agent"
|
"github.com/sipeed/picoclaw/pkg/agent"
|
||||||
"github.com/sipeed/picoclaw/pkg/auth"
|
"github.com/sipeed/picoclaw/pkg/auth"
|
||||||
"github.com/sipeed/picoclaw/pkg/bus"
|
"github.com/sipeed/picoclaw/pkg/bus"
|
||||||
|
|
@ -248,7 +249,7 @@ func onboard() {
|
||||||
|
|
||||||
func copyEmbeddedToTarget(targetDir string) error {
|
func copyEmbeddedToTarget(targetDir string) error {
|
||||||
// Ensure target directory exists
|
// Ensure target directory exists
|
||||||
if err := os.MkdirAll(targetDir, 0755); err != nil {
|
if err := os.MkdirAll(targetDir, 0o755); err != nil {
|
||||||
return fmt.Errorf("Failed to create target directory: %w", err)
|
return fmt.Errorf("Failed to create target directory: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -278,12 +279,12 @@ func copyEmbeddedToTarget(targetDir string) error {
|
||||||
targetPath := filepath.Join(targetDir, new_path)
|
targetPath := filepath.Join(targetDir, new_path)
|
||||||
|
|
||||||
// Ensure target file's directory exists
|
// Ensure target file's directory exists
|
||||||
if err := os.MkdirAll(filepath.Dir(targetPath), 0755); err != nil {
|
if err := os.MkdirAll(filepath.Dir(targetPath), 0o755); err != nil {
|
||||||
return fmt.Errorf("Failed to create directory %s: %w", filepath.Dir(targetPath), err)
|
return fmt.Errorf("Failed to create directory %s: %w", filepath.Dir(targetPath), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write file
|
// Write file
|
||||||
if err := os.WriteFile(targetPath, data, 0644); err != nil {
|
if err := os.WriteFile(targetPath, data, 0o644); err != nil {
|
||||||
return fmt.Errorf("Failed to write file %s: %w", targetPath, err)
|
return fmt.Errorf("Failed to write file %s: %w", targetPath, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -411,10 +412,10 @@ func agentCmd() {
|
||||||
// Print agent startup info (only for interactive mode)
|
// Print agent startup info (only for interactive mode)
|
||||||
startupInfo := agentLoop.GetStartupInfo()
|
startupInfo := agentLoop.GetStartupInfo()
|
||||||
logger.InfoCF("agent", "Agent initialized",
|
logger.InfoCF("agent", "Agent initialized",
|
||||||
map[string]interface{}{
|
map[string]any{
|
||||||
"tools_count": startupInfo["tools"].(map[string]interface{})["count"],
|
"tools_count": startupInfo["tools"].(map[string]any)["count"],
|
||||||
"skills_total": startupInfo["skills"].(map[string]interface{})["total"],
|
"skills_total": startupInfo["skills"].(map[string]any)["total"],
|
||||||
"skills_available": startupInfo["skills"].(map[string]interface{})["available"],
|
"skills_available": startupInfo["skills"].(map[string]any)["available"],
|
||||||
})
|
})
|
||||||
|
|
||||||
if message != "" {
|
if message != "" {
|
||||||
|
|
@ -441,7 +442,6 @@ func interactiveMode(agentLoop *agent.AgentLoop, sessionKey string) {
|
||||||
InterruptPrompt: "^C",
|
InterruptPrompt: "^C",
|
||||||
EOFPrompt: "exit",
|
EOFPrompt: "exit",
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Error initializing readline: %v\n", err)
|
fmt.Printf("Error initializing readline: %v\n", err)
|
||||||
fmt.Println("Falling back to simple input mode...")
|
fmt.Println("Falling back to simple input mode...")
|
||||||
|
|
@ -546,8 +546,8 @@ func gatewayCmd() {
|
||||||
// Print agent startup info
|
// Print agent startup info
|
||||||
fmt.Println("\n📦 Agent Status:")
|
fmt.Println("\n📦 Agent Status:")
|
||||||
startupInfo := agentLoop.GetStartupInfo()
|
startupInfo := agentLoop.GetStartupInfo()
|
||||||
toolsInfo := startupInfo["tools"].(map[string]interface{})
|
toolsInfo := startupInfo["tools"].(map[string]any)
|
||||||
skillsInfo := startupInfo["skills"].(map[string]interface{})
|
skillsInfo := startupInfo["skills"].(map[string]any)
|
||||||
fmt.Printf(" • Tools: %d loaded\n", toolsInfo["count"])
|
fmt.Printf(" • Tools: %d loaded\n", toolsInfo["count"])
|
||||||
fmt.Printf(" • Skills: %d/%d available\n",
|
fmt.Printf(" • Skills: %d/%d available\n",
|
||||||
skillsInfo["available"],
|
skillsInfo["available"],
|
||||||
|
|
@ -555,7 +555,7 @@ func gatewayCmd() {
|
||||||
|
|
||||||
// Log to file as well
|
// Log to file as well
|
||||||
logger.InfoCF("agent", "Agent initialized",
|
logger.InfoCF("agent", "Agent initialized",
|
||||||
map[string]interface{}{
|
map[string]any{
|
||||||
"tools_count": toolsInfo["count"],
|
"tools_count": toolsInfo["count"],
|
||||||
"skills_total": skillsInfo["total"],
|
"skills_total": skillsInfo["total"],
|
||||||
"skills_available": skillsInfo["available"],
|
"skills_available": skillsInfo["available"],
|
||||||
|
|
@ -563,7 +563,14 @@ func gatewayCmd() {
|
||||||
|
|
||||||
// Setup cron tool and service
|
// Setup cron tool and service
|
||||||
execTimeout := time.Duration(cfg.Tools.Cron.ExecTimeoutMinutes) * time.Minute
|
execTimeout := time.Duration(cfg.Tools.Cron.ExecTimeoutMinutes) * time.Minute
|
||||||
cronService := setupCronTool(agentLoop, msgBus, cfg.WorkspacePath(), cfg.Agents.Defaults.RestrictToWorkspace, execTimeout, cfg)
|
cronService := setupCronTool(
|
||||||
|
agentLoop,
|
||||||
|
msgBus,
|
||||||
|
cfg.WorkspacePath(),
|
||||||
|
cfg.Agents.Defaults.RestrictToWorkspace,
|
||||||
|
execTimeout,
|
||||||
|
cfg,
|
||||||
|
)
|
||||||
|
|
||||||
heartbeatService := heartbeat.NewHeartbeatService(
|
heartbeatService := heartbeat.NewHeartbeatService(
|
||||||
cfg.WorkspacePath(),
|
cfg.WorkspacePath(),
|
||||||
|
|
@ -667,7 +674,7 @@ func gatewayCmd() {
|
||||||
healthServer := health.NewServer(cfg.Gateway.Host, cfg.Gateway.Port)
|
healthServer := health.NewServer(cfg.Gateway.Host, cfg.Gateway.Port)
|
||||||
go func() {
|
go func() {
|
||||||
if err := healthServer.Start(); err != nil && err != http.ErrServerClosed {
|
if err := healthServer.Start(); err != nil && err != http.ErrServerClosed {
|
||||||
logger.ErrorCF("health", "Health server error", map[string]interface{}{"error": err.Error()})
|
logger.ErrorCF("health", "Health server error", map[string]any{"error": err.Error()})
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
fmt.Printf("✓ Health endpoints available at http://%s:%d/health and /ready\n", cfg.Gateway.Host, cfg.Gateway.Port)
|
fmt.Printf("✓ Health endpoints available at http://%s:%d/health and /ready\n", cfg.Gateway.Host, cfg.Gateway.Port)
|
||||||
|
|
@ -988,7 +995,10 @@ func getConfigPath() string {
|
||||||
return filepath.Join(home, ".picoclaw", "config.json")
|
return filepath.Join(home, ".picoclaw", "config.json")
|
||||||
}
|
}
|
||||||
|
|
||||||
func setupCronTool(agentLoop *agent.AgentLoop, msgBus *bus.MessageBus, workspace string, restrict bool, execTimeout time.Duration, config *config.Config) *cron.CronService {
|
func setupCronTool(
|
||||||
|
agentLoop *agent.AgentLoop, msgBus *bus.MessageBus, workspace string, restrict bool, execTimeout time.Duration,
|
||||||
|
config *config.Config,
|
||||||
|
) *cron.CronService {
|
||||||
cronStorePath := filepath.Join(workspace, "cron", "jobs.json")
|
cronStorePath := filepath.Join(workspace, "cron", "jobs.json")
|
||||||
|
|
||||||
// Create cron service
|
// Create cron service
|
||||||
|
|
@ -1315,7 +1325,7 @@ func skillsInstallBuiltinCmd(workspace string) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := os.MkdirAll(workspacePath, 0755); err != nil {
|
if err := os.MkdirAll(workspacePath, 0o755); err != nil {
|
||||||
fmt.Printf("✗ Failed to create directory for %s: %v\n", skillName, err)
|
fmt.Printf("✗ Failed to create directory for %s: %v\n", skillName, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -96,7 +96,9 @@ func (cb *ContextBuilder) buildToolsSection() string {
|
||||||
|
|
||||||
var sb strings.Builder
|
var sb strings.Builder
|
||||||
sb.WriteString("## Available Tools\n\n")
|
sb.WriteString("## Available Tools\n\n")
|
||||||
sb.WriteString("**CRITICAL**: You MUST use tools to perform actions. Do NOT pretend to execute commands or schedule tasks.\n\n")
|
sb.WriteString(
|
||||||
|
"**CRITICAL**: You MUST use tools to perform actions. Do NOT pretend to execute commands or schedule tasks.\n\n",
|
||||||
|
)
|
||||||
sb.WriteString("You have access to the following tools:\n\n")
|
sb.WriteString("You have access to the following tools:\n\n")
|
||||||
for _, s := range summaries {
|
for _, s := range summaries {
|
||||||
sb.WriteString(s)
|
sb.WriteString(s)
|
||||||
|
|
@ -157,7 +159,9 @@ func (cb *ContextBuilder) LoadBootstrapFiles() string {
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cb *ContextBuilder) BuildMessages(history []providers.Message, summary string, currentMessage string, media []string, channel, chatID string) []providers.Message {
|
func (cb *ContextBuilder) BuildMessages(
|
||||||
|
history []providers.Message, summary string, currentMessage string, media []string, channel, chatID string,
|
||||||
|
) []providers.Message {
|
||||||
messages := []providers.Message{}
|
messages := []providers.Message{}
|
||||||
|
|
||||||
systemPrompt := cb.BuildSystemPrompt()
|
systemPrompt := cb.BuildSystemPrompt()
|
||||||
|
|
@ -169,7 +173,7 @@ func (cb *ContextBuilder) BuildMessages(history []providers.Message, summary str
|
||||||
|
|
||||||
// Log system prompt summary for debugging (debug mode only)
|
// Log system prompt summary for debugging (debug mode only)
|
||||||
logger.DebugCF("agent", "System prompt built",
|
logger.DebugCF("agent", "System prompt built",
|
||||||
map[string]interface{}{
|
map[string]any{
|
||||||
"total_chars": len(systemPrompt),
|
"total_chars": len(systemPrompt),
|
||||||
"total_lines": strings.Count(systemPrompt, "\n") + 1,
|
"total_lines": strings.Count(systemPrompt, "\n") + 1,
|
||||||
"section_count": strings.Count(systemPrompt, "\n\n---\n\n") + 1,
|
"section_count": strings.Count(systemPrompt, "\n\n---\n\n") + 1,
|
||||||
|
|
@ -181,7 +185,7 @@ func (cb *ContextBuilder) BuildMessages(history []providers.Message, summary str
|
||||||
preview = preview[:500] + "... (truncated)"
|
preview = preview[:500] + "... (truncated)"
|
||||||
}
|
}
|
||||||
logger.DebugCF("agent", "System prompt preview",
|
logger.DebugCF("agent", "System prompt preview",
|
||||||
map[string]interface{}{
|
map[string]any{
|
||||||
"preview": preview,
|
"preview": preview,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -189,15 +193,15 @@ func (cb *ContextBuilder) BuildMessages(history []providers.Message, summary str
|
||||||
systemPrompt += "\n\n## Summary of Previous Conversation\n\n" + summary
|
systemPrompt += "\n\n## Summary of Previous Conversation\n\n" + summary
|
||||||
}
|
}
|
||||||
|
|
||||||
//This fix prevents the session memory from LLM failure due to elimination of toolu_IDs required from LLM
|
// This fix prevents the session memory from LLM failure due to elimination of toolu_IDs required from LLM
|
||||||
// --- INICIO DEL FIX ---
|
// --- INICIO DEL FIX ---
|
||||||
//Diegox-17
|
// Diegox-17
|
||||||
for len(history) > 0 && (history[0].Role == "tool") {
|
for len(history) > 0 && (history[0].Role == "tool") {
|
||||||
logger.DebugCF("agent", "Removing orphaned tool message from history to prevent LLM error",
|
logger.DebugCF("agent", "Removing orphaned tool message from history to prevent LLM error",
|
||||||
map[string]interface{}{"role": history[0].Role})
|
map[string]any{"role": history[0].Role})
|
||||||
history = history[1:]
|
history = history[1:]
|
||||||
}
|
}
|
||||||
//Diegox-17
|
// Diegox-17
|
||||||
// --- FIN DEL FIX ---
|
// --- FIN DEL FIX ---
|
||||||
|
|
||||||
messages = append(messages, providers.Message{
|
messages = append(messages, providers.Message{
|
||||||
|
|
@ -215,7 +219,9 @@ func (cb *ContextBuilder) BuildMessages(history []providers.Message, summary str
|
||||||
return messages
|
return messages
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cb *ContextBuilder) AddToolResult(messages []providers.Message, toolCallID, toolName, result string) []providers.Message {
|
func (cb *ContextBuilder) AddToolResult(
|
||||||
|
messages []providers.Message, toolCallID, toolName, result string,
|
||||||
|
) []providers.Message {
|
||||||
messages = append(messages, providers.Message{
|
messages = append(messages, providers.Message{
|
||||||
Role: "tool",
|
Role: "tool",
|
||||||
Content: result,
|
Content: result,
|
||||||
|
|
@ -224,7 +230,9 @@ func (cb *ContextBuilder) AddToolResult(messages []providers.Message, toolCallID
|
||||||
return messages
|
return messages
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cb *ContextBuilder) AddAssistantMessage(messages []providers.Message, content string, toolCalls []map[string]interface{}) []providers.Message {
|
func (cb *ContextBuilder) AddAssistantMessage(
|
||||||
|
messages []providers.Message, content string, toolCalls []map[string]any,
|
||||||
|
) []providers.Message {
|
||||||
msg := providers.Message{
|
msg := providers.Message{
|
||||||
Role: "assistant",
|
Role: "assistant",
|
||||||
Content: content,
|
Content: content,
|
||||||
|
|
@ -254,13 +262,13 @@ func (cb *ContextBuilder) loadSkills() string {
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetSkillsInfo returns information about loaded skills.
|
// GetSkillsInfo returns information about loaded skills.
|
||||||
func (cb *ContextBuilder) GetSkillsInfo() map[string]interface{} {
|
func (cb *ContextBuilder) GetSkillsInfo() map[string]any {
|
||||||
allSkills := cb.skillsLoader.ListSkills()
|
allSkills := cb.skillsLoader.ListSkills()
|
||||||
skillNames := make([]string, 0, len(allSkills))
|
skillNames := make([]string, 0, len(allSkills))
|
||||||
for _, s := range allSkills {
|
for _, s := range allSkills {
|
||||||
skillNames = append(skillNames, s.Name)
|
skillNames = append(skillNames, s.Name)
|
||||||
}
|
}
|
||||||
return map[string]interface{}{
|
return map[string]any{
|
||||||
"total": len(allSkills),
|
"total": len(allSkills),
|
||||||
"available": len(allSkills),
|
"available": len(allSkills),
|
||||||
"names": skillNames,
|
"names": skillNames,
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ func NewAgentInstance(
|
||||||
provider providers.LLMProvider,
|
provider providers.LLMProvider,
|
||||||
) *AgentInstance {
|
) *AgentInstance {
|
||||||
workspace := resolveAgentWorkspace(agentCfg, defaults)
|
workspace := resolveAgentWorkspace(agentCfg, defaults)
|
||||||
os.MkdirAll(workspace, 0755)
|
os.MkdirAll(workspace, 0o755)
|
||||||
|
|
||||||
model := resolveAgentModel(agentCfg, defaults)
|
model := resolveAgentModel(agentCfg, defaults)
|
||||||
fallbacks := resolveAgentFallbacks(agentCfg, defaults)
|
fallbacks := resolveAgentFallbacks(agentCfg, defaults)
|
||||||
|
|
|
||||||
|
|
@ -79,7 +79,9 @@ func NewAgentLoop(cfg *config.Config, msgBus *bus.MessageBus, provider providers
|
||||||
}
|
}
|
||||||
|
|
||||||
// registerSharedTools registers tools that are shared across all agents (web, message, spawn).
|
// registerSharedTools registers tools that are shared across all agents (web, message, spawn).
|
||||||
func registerSharedTools(cfg *config.Config, msgBus *bus.MessageBus, registry *AgentRegistry, provider providers.LLMProvider) {
|
func registerSharedTools(
|
||||||
|
cfg *config.Config, msgBus *bus.MessageBus, registry *AgentRegistry, provider providers.LLMProvider,
|
||||||
|
) {
|
||||||
for _, agentID := range registry.ListAgentIDs() {
|
for _, agentID := range registry.ListAgentIDs() {
|
||||||
agent, ok := registry.GetAgent(agentID)
|
agent, ok := registry.GetAgent(agentID)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
|
@ -215,7 +217,9 @@ func (al *AgentLoop) ProcessDirect(ctx context.Context, content, sessionKey stri
|
||||||
return al.ProcessDirectWithChannel(ctx, content, sessionKey, "cli", "direct")
|
return al.ProcessDirectWithChannel(ctx, content, sessionKey, "cli", "direct")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (al *AgentLoop) ProcessDirectWithChannel(ctx context.Context, content, sessionKey, channel, chatID string) (string, error) {
|
func (al *AgentLoop) ProcessDirectWithChannel(
|
||||||
|
ctx context.Context, content, sessionKey, channel, chatID string,
|
||||||
|
) (string, error) {
|
||||||
msg := bus.InboundMessage{
|
msg := bus.InboundMessage{
|
||||||
Channel: channel,
|
Channel: channel,
|
||||||
SenderID: "cron",
|
SenderID: "cron",
|
||||||
|
|
@ -252,7 +256,7 @@ func (al *AgentLoop) processMessage(ctx context.Context, msg bus.InboundMessage)
|
||||||
logContent = utils.Truncate(msg.Content, 80)
|
logContent = utils.Truncate(msg.Content, 80)
|
||||||
}
|
}
|
||||||
logger.InfoCF("agent", fmt.Sprintf("Processing message from %s:%s: %s", msg.Channel, msg.SenderID, logContent),
|
logger.InfoCF("agent", fmt.Sprintf("Processing message from %s:%s: %s", msg.Channel, msg.SenderID, logContent),
|
||||||
map[string]interface{}{
|
map[string]any{
|
||||||
"channel": msg.Channel,
|
"channel": msg.Channel,
|
||||||
"chat_id": msg.ChatID,
|
"chat_id": msg.ChatID,
|
||||||
"sender_id": msg.SenderID,
|
"sender_id": msg.SenderID,
|
||||||
|
|
@ -291,7 +295,7 @@ func (al *AgentLoop) processMessage(ctx context.Context, msg bus.InboundMessage)
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.InfoCF("agent", "Routed message",
|
logger.InfoCF("agent", "Routed message",
|
||||||
map[string]interface{}{
|
map[string]any{
|
||||||
"agent_id": agent.ID,
|
"agent_id": agent.ID,
|
||||||
"session_key": sessionKey,
|
"session_key": sessionKey,
|
||||||
"matched_by": route.MatchedBy,
|
"matched_by": route.MatchedBy,
|
||||||
|
|
@ -314,7 +318,7 @@ func (al *AgentLoop) processSystemMessage(ctx context.Context, msg bus.InboundMe
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.InfoCF("agent", "Processing system message",
|
logger.InfoCF("agent", "Processing system message",
|
||||||
map[string]interface{}{
|
map[string]any{
|
||||||
"sender_id": msg.SenderID,
|
"sender_id": msg.SenderID,
|
||||||
"chat_id": msg.ChatID,
|
"chat_id": msg.ChatID,
|
||||||
})
|
})
|
||||||
|
|
@ -339,7 +343,7 @@ func (al *AgentLoop) processSystemMessage(ctx context.Context, msg bus.InboundMe
|
||||||
// Skip internal channels - only log, don't send to user
|
// Skip internal channels - only log, don't send to user
|
||||||
if constants.IsInternalChannel(originChannel) {
|
if constants.IsInternalChannel(originChannel) {
|
||||||
logger.InfoCF("agent", "Subagent completed (internal channel)",
|
logger.InfoCF("agent", "Subagent completed (internal channel)",
|
||||||
map[string]interface{}{
|
map[string]any{
|
||||||
"sender_id": msg.SenderID,
|
"sender_id": msg.SenderID,
|
||||||
"content_len": len(content),
|
"content_len": len(content),
|
||||||
"channel": originChannel,
|
"channel": originChannel,
|
||||||
|
|
@ -372,7 +376,7 @@ func (al *AgentLoop) runAgentLoop(ctx context.Context, agent *AgentInstance, opt
|
||||||
if !constants.IsInternalChannel(opts.Channel) {
|
if !constants.IsInternalChannel(opts.Channel) {
|
||||||
channelKey := fmt.Sprintf("%s:%s", opts.Channel, opts.ChatID)
|
channelKey := fmt.Sprintf("%s:%s", opts.Channel, opts.ChatID)
|
||||||
if err := al.RecordLastChannel(channelKey); err != nil {
|
if err := al.RecordLastChannel(channelKey); err != nil {
|
||||||
logger.WarnCF("agent", "Failed to record last channel", map[string]interface{}{"error": err.Error()})
|
logger.WarnCF("agent", "Failed to record last channel", map[string]any{"error": err.Error()})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -434,7 +438,7 @@ func (al *AgentLoop) runAgentLoop(ctx context.Context, agent *AgentInstance, opt
|
||||||
// 9. Log response
|
// 9. Log response
|
||||||
responsePreview := utils.Truncate(finalContent, 120)
|
responsePreview := utils.Truncate(finalContent, 120)
|
||||||
logger.InfoCF("agent", fmt.Sprintf("Response: %s", responsePreview),
|
logger.InfoCF("agent", fmt.Sprintf("Response: %s", responsePreview),
|
||||||
map[string]interface{}{
|
map[string]any{
|
||||||
"agent_id": agent.ID,
|
"agent_id": agent.ID,
|
||||||
"session_key": opts.SessionKey,
|
"session_key": opts.SessionKey,
|
||||||
"iterations": iteration,
|
"iterations": iteration,
|
||||||
|
|
@ -445,7 +449,9 @@ func (al *AgentLoop) runAgentLoop(ctx context.Context, agent *AgentInstance, opt
|
||||||
}
|
}
|
||||||
|
|
||||||
// runLLMIteration executes the LLM call loop with tool handling.
|
// runLLMIteration executes the LLM call loop with tool handling.
|
||||||
func (al *AgentLoop) runLLMIteration(ctx context.Context, agent *AgentInstance, messages []providers.Message, opts processOptions) (string, int, error) {
|
func (al *AgentLoop) runLLMIteration(
|
||||||
|
ctx context.Context, agent *AgentInstance, messages []providers.Message, opts processOptions,
|
||||||
|
) (string, int, error) {
|
||||||
iteration := 0
|
iteration := 0
|
||||||
var finalContent string
|
var finalContent string
|
||||||
|
|
||||||
|
|
@ -453,7 +459,7 @@ func (al *AgentLoop) runLLMIteration(ctx context.Context, agent *AgentInstance,
|
||||||
iteration++
|
iteration++
|
||||||
|
|
||||||
logger.DebugCF("agent", "LLM iteration",
|
logger.DebugCF("agent", "LLM iteration",
|
||||||
map[string]interface{}{
|
map[string]any{
|
||||||
"agent_id": agent.ID,
|
"agent_id": agent.ID,
|
||||||
"iteration": iteration,
|
"iteration": iteration,
|
||||||
"max": agent.MaxIterations,
|
"max": agent.MaxIterations,
|
||||||
|
|
@ -464,7 +470,7 @@ func (al *AgentLoop) runLLMIteration(ctx context.Context, agent *AgentInstance,
|
||||||
|
|
||||||
// Log LLM request details
|
// Log LLM request details
|
||||||
logger.DebugCF("agent", "LLM request",
|
logger.DebugCF("agent", "LLM request",
|
||||||
map[string]interface{}{
|
map[string]any{
|
||||||
"agent_id": agent.ID,
|
"agent_id": agent.ID,
|
||||||
"iteration": iteration,
|
"iteration": iteration,
|
||||||
"model": agent.Model,
|
"model": agent.Model,
|
||||||
|
|
@ -477,7 +483,7 @@ func (al *AgentLoop) runLLMIteration(ctx context.Context, agent *AgentInstance,
|
||||||
|
|
||||||
// Log full messages (detailed)
|
// Log full messages (detailed)
|
||||||
logger.DebugCF("agent", "Full LLM request",
|
logger.DebugCF("agent", "Full LLM request",
|
||||||
map[string]interface{}{
|
map[string]any{
|
||||||
"iteration": iteration,
|
"iteration": iteration,
|
||||||
"messages_json": formatMessagesForLog(messages),
|
"messages_json": formatMessagesForLog(messages),
|
||||||
"tools_json": formatToolsForLog(providerToolDefs),
|
"tools_json": formatToolsForLog(providerToolDefs),
|
||||||
|
|
@ -491,7 +497,7 @@ func (al *AgentLoop) runLLMIteration(ctx context.Context, agent *AgentInstance,
|
||||||
if len(agent.Candidates) > 1 && al.fallback != nil {
|
if len(agent.Candidates) > 1 && al.fallback != nil {
|
||||||
fbResult, fbErr := al.fallback.Execute(ctx, agent.Candidates,
|
fbResult, fbErr := al.fallback.Execute(ctx, agent.Candidates,
|
||||||
func(ctx context.Context, provider, model string) (*providers.LLMResponse, error) {
|
func(ctx context.Context, provider, model string) (*providers.LLMResponse, error) {
|
||||||
return agent.Provider.Chat(ctx, messages, providerToolDefs, model, map[string]interface{}{
|
return agent.Provider.Chat(ctx, messages, providerToolDefs, model, map[string]any{
|
||||||
"max_tokens": 8192,
|
"max_tokens": 8192,
|
||||||
"temperature": 0.7,
|
"temperature": 0.7,
|
||||||
})
|
})
|
||||||
|
|
@ -503,11 +509,11 @@ func (al *AgentLoop) runLLMIteration(ctx context.Context, agent *AgentInstance,
|
||||||
if fbResult.Provider != "" && len(fbResult.Attempts) > 0 {
|
if fbResult.Provider != "" && len(fbResult.Attempts) > 0 {
|
||||||
logger.InfoCF("agent", fmt.Sprintf("Fallback: succeeded with %s/%s after %d attempts",
|
logger.InfoCF("agent", fmt.Sprintf("Fallback: succeeded with %s/%s after %d attempts",
|
||||||
fbResult.Provider, fbResult.Model, len(fbResult.Attempts)+1),
|
fbResult.Provider, fbResult.Model, len(fbResult.Attempts)+1),
|
||||||
map[string]interface{}{"agent_id": agent.ID, "iteration": iteration})
|
map[string]any{"agent_id": agent.ID, "iteration": iteration})
|
||||||
}
|
}
|
||||||
return fbResult.Response, nil
|
return fbResult.Response, nil
|
||||||
}
|
}
|
||||||
return agent.Provider.Chat(ctx, messages, providerToolDefs, agent.Model, map[string]interface{}{
|
return agent.Provider.Chat(ctx, messages, providerToolDefs, agent.Model, map[string]any{
|
||||||
"max_tokens": 8192,
|
"max_tokens": 8192,
|
||||||
"temperature": 0.7,
|
"temperature": 0.7,
|
||||||
})
|
})
|
||||||
|
|
@ -528,7 +534,7 @@ func (al *AgentLoop) runLLMIteration(ctx context.Context, agent *AgentInstance,
|
||||||
strings.Contains(errMsg, "length")
|
strings.Contains(errMsg, "length")
|
||||||
|
|
||||||
if isContextError && retry < maxRetries {
|
if isContextError && retry < maxRetries {
|
||||||
logger.WarnCF("agent", "Context window error detected, attempting compression", map[string]interface{}{
|
logger.WarnCF("agent", "Context window error detected, attempting compression", map[string]any{
|
||||||
"error": err.Error(),
|
"error": err.Error(),
|
||||||
"retry": retry,
|
"retry": retry,
|
||||||
})
|
})
|
||||||
|
|
@ -555,7 +561,7 @@ func (al *AgentLoop) runLLMIteration(ctx context.Context, agent *AgentInstance,
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.ErrorCF("agent", "LLM call failed",
|
logger.ErrorCF("agent", "LLM call failed",
|
||||||
map[string]interface{}{
|
map[string]any{
|
||||||
"agent_id": agent.ID,
|
"agent_id": agent.ID,
|
||||||
"iteration": iteration,
|
"iteration": iteration,
|
||||||
"error": err.Error(),
|
"error": err.Error(),
|
||||||
|
|
@ -567,7 +573,7 @@ func (al *AgentLoop) runLLMIteration(ctx context.Context, agent *AgentInstance,
|
||||||
if len(response.ToolCalls) == 0 {
|
if len(response.ToolCalls) == 0 {
|
||||||
finalContent = response.Content
|
finalContent = response.Content
|
||||||
logger.InfoCF("agent", "LLM response without tool calls (direct answer)",
|
logger.InfoCF("agent", "LLM response without tool calls (direct answer)",
|
||||||
map[string]interface{}{
|
map[string]any{
|
||||||
"agent_id": agent.ID,
|
"agent_id": agent.ID,
|
||||||
"iteration": iteration,
|
"iteration": iteration,
|
||||||
"content_chars": len(finalContent),
|
"content_chars": len(finalContent),
|
||||||
|
|
@ -581,7 +587,7 @@ func (al *AgentLoop) runLLMIteration(ctx context.Context, agent *AgentInstance,
|
||||||
toolNames = append(toolNames, tc.Name)
|
toolNames = append(toolNames, tc.Name)
|
||||||
}
|
}
|
||||||
logger.InfoCF("agent", "LLM requested tool calls",
|
logger.InfoCF("agent", "LLM requested tool calls",
|
||||||
map[string]interface{}{
|
map[string]any{
|
||||||
"agent_id": agent.ID,
|
"agent_id": agent.ID,
|
||||||
"tools": toolNames,
|
"tools": toolNames,
|
||||||
"count": len(response.ToolCalls),
|
"count": len(response.ToolCalls),
|
||||||
|
|
@ -614,7 +620,7 @@ func (al *AgentLoop) runLLMIteration(ctx context.Context, agent *AgentInstance,
|
||||||
argsJSON, _ := json.Marshal(tc.Arguments)
|
argsJSON, _ := json.Marshal(tc.Arguments)
|
||||||
argsPreview := utils.Truncate(string(argsJSON), 200)
|
argsPreview := utils.Truncate(string(argsJSON), 200)
|
||||||
logger.InfoCF("agent", fmt.Sprintf("Tool call: %s(%s)", tc.Name, argsPreview),
|
logger.InfoCF("agent", fmt.Sprintf("Tool call: %s(%s)", tc.Name, argsPreview),
|
||||||
map[string]interface{}{
|
map[string]any{
|
||||||
"agent_id": agent.ID,
|
"agent_id": agent.ID,
|
||||||
"tool": tc.Name,
|
"tool": tc.Name,
|
||||||
"iteration": iteration,
|
"iteration": iteration,
|
||||||
|
|
@ -629,14 +635,16 @@ func (al *AgentLoop) runLLMIteration(ctx context.Context, agent *AgentInstance,
|
||||||
// The agent will handle user notification via processSystemMessage
|
// The agent will handle user notification via processSystemMessage
|
||||||
if !result.Silent && result.ForUser != "" {
|
if !result.Silent && result.ForUser != "" {
|
||||||
logger.InfoCF("agent", "Async tool completed, agent will handle notification",
|
logger.InfoCF("agent", "Async tool completed, agent will handle notification",
|
||||||
map[string]interface{}{
|
map[string]any{
|
||||||
"tool": tc.Name,
|
"tool": tc.Name,
|
||||||
"content_len": len(result.ForUser),
|
"content_len": len(result.ForUser),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
toolResult := agent.Tools.ExecuteWithContext(ctx, tc.Name, tc.Arguments, opts.Channel, opts.ChatID, asyncCallback)
|
toolResult := agent.Tools.ExecuteWithContext(
|
||||||
|
ctx, tc.Name, tc.Arguments, opts.Channel, opts.ChatID, asyncCallback,
|
||||||
|
)
|
||||||
|
|
||||||
// Send ForUser content to user immediately if not Silent
|
// Send ForUser content to user immediately if not Silent
|
||||||
if !toolResult.Silent && toolResult.ForUser != "" && opts.SendResponse {
|
if !toolResult.Silent && toolResult.ForUser != "" && opts.SendResponse {
|
||||||
|
|
@ -646,7 +654,7 @@ func (al *AgentLoop) runLLMIteration(ctx context.Context, agent *AgentInstance,
|
||||||
Content: toolResult.ForUser,
|
Content: toolResult.ForUser,
|
||||||
})
|
})
|
||||||
logger.DebugCF("agent", "Sent tool result to user",
|
logger.DebugCF("agent", "Sent tool result to user",
|
||||||
map[string]interface{}{
|
map[string]any{
|
||||||
"tool": tc.Name,
|
"tool": tc.Name,
|
||||||
"content_len": len(toolResult.ForUser),
|
"content_len": len(toolResult.ForUser),
|
||||||
})
|
})
|
||||||
|
|
@ -752,7 +760,10 @@ func (al *AgentLoop) forceCompression(agent *AgentInstance, sessionKey string) {
|
||||||
newHistory = append(newHistory, history[0]) // System prompt
|
newHistory = append(newHistory, history[0]) // System prompt
|
||||||
|
|
||||||
// Add a note about compression
|
// Add a note about compression
|
||||||
compressionNote := fmt.Sprintf("[System: Emergency compression dropped %d oldest messages due to context limit]", droppedCount)
|
compressionNote := fmt.Sprintf(
|
||||||
|
"[System: Emergency compression dropped %d oldest messages due to context limit]",
|
||||||
|
droppedCount,
|
||||||
|
)
|
||||||
// If there was an existing summary, we might lose it if it was in the dropped part (which is just messages).
|
// If there was an existing summary, we might lose it if it was in the dropped part (which is just messages).
|
||||||
// The summary is stored separately in session.Summary, so it persists!
|
// The summary is stored separately in session.Summary, so it persists!
|
||||||
// We just need to ensure the user knows there's a gap.
|
// We just need to ensure the user knows there's a gap.
|
||||||
|
|
@ -770,7 +781,7 @@ func (al *AgentLoop) forceCompression(agent *AgentInstance, sessionKey string) {
|
||||||
agent.Sessions.SetHistory(sessionKey, newHistory)
|
agent.Sessions.SetHistory(sessionKey, newHistory)
|
||||||
agent.Sessions.Save(sessionKey)
|
agent.Sessions.Save(sessionKey)
|
||||||
|
|
||||||
logger.WarnCF("agent", "Forced compression executed", map[string]interface{}{
|
logger.WarnCF("agent", "Forced compression executed", map[string]any{
|
||||||
"session_key": sessionKey,
|
"session_key": sessionKey,
|
||||||
"dropped_msgs": droppedCount,
|
"dropped_msgs": droppedCount,
|
||||||
"new_count": len(newHistory),
|
"new_count": len(newHistory),
|
||||||
|
|
@ -778,8 +789,8 @@ func (al *AgentLoop) forceCompression(agent *AgentInstance, sessionKey string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetStartupInfo returns information about loaded tools and skills for logging.
|
// GetStartupInfo returns information about loaded tools and skills for logging.
|
||||||
func (al *AgentLoop) GetStartupInfo() map[string]interface{} {
|
func (al *AgentLoop) GetStartupInfo() map[string]any {
|
||||||
info := make(map[string]interface{})
|
info := make(map[string]any)
|
||||||
|
|
||||||
agent := al.registry.GetDefaultAgent()
|
agent := al.registry.GetDefaultAgent()
|
||||||
if agent == nil {
|
if agent == nil {
|
||||||
|
|
@ -788,7 +799,7 @@ func (al *AgentLoop) GetStartupInfo() map[string]interface{} {
|
||||||
|
|
||||||
// Tools info
|
// Tools info
|
||||||
toolsList := agent.Tools.List()
|
toolsList := agent.Tools.List()
|
||||||
info["tools"] = map[string]interface{}{
|
info["tools"] = map[string]any{
|
||||||
"count": len(toolsList),
|
"count": len(toolsList),
|
||||||
"names": toolsList,
|
"names": toolsList,
|
||||||
}
|
}
|
||||||
|
|
@ -797,7 +808,7 @@ func (al *AgentLoop) GetStartupInfo() map[string]interface{} {
|
||||||
info["skills"] = agent.ContextBuilder.GetSkillsInfo()
|
info["skills"] = agent.ContextBuilder.GetSkillsInfo()
|
||||||
|
|
||||||
// Agents info
|
// Agents info
|
||||||
info["agents"] = map[string]interface{}{
|
info["agents"] = map[string]any{
|
||||||
"count": len(al.registry.ListAgentIDs()),
|
"count": len(al.registry.ListAgentIDs()),
|
||||||
"ids": al.registry.ListAgentIDs(),
|
"ids": al.registry.ListAgentIDs(),
|
||||||
}
|
}
|
||||||
|
|
@ -849,7 +860,10 @@ func formatToolsForLog(tools []providers.ToolDefinition) string {
|
||||||
result += fmt.Sprintf(" [%d] Type: %s, Name: %s\n", i, tool.Type, tool.Function.Name)
|
result += fmt.Sprintf(" [%d] Type: %s, Name: %s\n", i, tool.Type, tool.Function.Name)
|
||||||
result += fmt.Sprintf(" Description: %s\n", tool.Function.Description)
|
result += fmt.Sprintf(" Description: %s\n", tool.Function.Description)
|
||||||
if len(tool.Function.Parameters) > 0 {
|
if len(tool.Function.Parameters) > 0 {
|
||||||
result += fmt.Sprintf(" Parameters: %s\n", utils.Truncate(fmt.Sprintf("%v", tool.Function.Parameters), 200))
|
result += fmt.Sprintf(
|
||||||
|
" Parameters: %s\n",
|
||||||
|
utils.Truncate(fmt.Sprintf("%v", tool.Function.Parameters), 200),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
result += "]"
|
result += "]"
|
||||||
|
|
@ -902,11 +916,21 @@ func (al *AgentLoop) summarizeSession(agent *AgentInstance, sessionKey string) {
|
||||||
s1, _ := al.summarizeBatch(ctx, agent, part1, "")
|
s1, _ := al.summarizeBatch(ctx, agent, part1, "")
|
||||||
s2, _ := al.summarizeBatch(ctx, agent, part2, "")
|
s2, _ := al.summarizeBatch(ctx, agent, part2, "")
|
||||||
|
|
||||||
mergePrompt := fmt.Sprintf("Merge these two conversation summaries into one cohesive summary:\n\n1: %s\n\n2: %s", s1, s2)
|
mergePrompt := fmt.Sprintf(
|
||||||
resp, err := agent.Provider.Chat(ctx, []providers.Message{{Role: "user", Content: mergePrompt}}, nil, agent.Model, map[string]interface{}{
|
"Merge these two conversation summaries into one cohesive summary:\n\n1: %s\n\n2: %s",
|
||||||
"max_tokens": 1024,
|
s1,
|
||||||
"temperature": 0.3,
|
s2,
|
||||||
})
|
)
|
||||||
|
resp, err := agent.Provider.Chat(
|
||||||
|
ctx,
|
||||||
|
[]providers.Message{{Role: "user", Content: mergePrompt}},
|
||||||
|
nil,
|
||||||
|
agent.Model,
|
||||||
|
map[string]any{
|
||||||
|
"max_tokens": 1024,
|
||||||
|
"temperature": 0.3,
|
||||||
|
},
|
||||||
|
)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
finalSummary = resp.Content
|
finalSummary = resp.Content
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -928,7 +952,9 @@ func (al *AgentLoop) summarizeSession(agent *AgentInstance, sessionKey string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// summarizeBatch summarizes a batch of messages.
|
// summarizeBatch summarizes a batch of messages.
|
||||||
func (al *AgentLoop) summarizeBatch(ctx context.Context, agent *AgentInstance, batch []providers.Message, existingSummary string) (string, error) {
|
func (al *AgentLoop) summarizeBatch(
|
||||||
|
ctx context.Context, agent *AgentInstance, batch []providers.Message, existingSummary string,
|
||||||
|
) (string, error) {
|
||||||
prompt := "Provide a concise summary of this conversation segment, preserving core context and key points.\n"
|
prompt := "Provide a concise summary of this conversation segment, preserving core context and key points.\n"
|
||||||
if existingSummary != "" {
|
if existingSummary != "" {
|
||||||
prompt += "Existing context: " + existingSummary + "\n"
|
prompt += "Existing context: " + existingSummary + "\n"
|
||||||
|
|
@ -938,10 +964,16 @@ func (al *AgentLoop) summarizeBatch(ctx context.Context, agent *AgentInstance, b
|
||||||
prompt += fmt.Sprintf("%s: %s\n", m.Role, m.Content)
|
prompt += fmt.Sprintf("%s: %s\n", m.Role, m.Content)
|
||||||
}
|
}
|
||||||
|
|
||||||
response, err := agent.Provider.Chat(ctx, []providers.Message{{Role: "user", Content: prompt}}, nil, agent.Model, map[string]interface{}{
|
response, err := agent.Provider.Chat(
|
||||||
"max_tokens": 1024,
|
ctx,
|
||||||
"temperature": 0.3,
|
[]providers.Message{{Role: "user", Content: prompt}},
|
||||||
})
|
nil,
|
||||||
|
agent.Model,
|
||||||
|
map[string]any{
|
||||||
|
"max_tokens": 1024,
|
||||||
|
"temperature": 0.3,
|
||||||
|
},
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,10 @@ import (
|
||||||
// mockProvider is a simple mock LLM provider for testing
|
// mockProvider is a simple mock LLM provider for testing
|
||||||
type mockProvider struct{}
|
type mockProvider struct{}
|
||||||
|
|
||||||
func (m *mockProvider) Chat(ctx context.Context, messages []providers.Message, tools []providers.ToolDefinition, model string, opts map[string]interface{}) (*providers.LLMResponse, error) {
|
func (m *mockProvider) Chat(
|
||||||
|
ctx context.Context, messages []providers.Message, tools []providers.ToolDefinition, model string,
|
||||||
|
opts map[string]any,
|
||||||
|
) (*providers.LLMResponse, error) {
|
||||||
return &providers.LLMResponse{
|
return &providers.LLMResponse{
|
||||||
Content: "Mock response",
|
Content: "Mock response",
|
||||||
ToolCalls: []providers.ToolCall{},
|
ToolCalls: []providers.ToolCall{},
|
||||||
|
|
@ -185,7 +188,7 @@ func TestToolRegistry_ToolRegistration(t *testing.T) {
|
||||||
// Verify tool is registered by checking it doesn't panic on GetStartupInfo
|
// Verify tool is registered by checking it doesn't panic on GetStartupInfo
|
||||||
// (actual tool retrieval is tested in tools package tests)
|
// (actual tool retrieval is tested in tools package tests)
|
||||||
info := al.GetStartupInfo()
|
info := al.GetStartupInfo()
|
||||||
toolsInfo := info["tools"].(map[string]interface{})
|
toolsInfo := info["tools"].(map[string]any)
|
||||||
toolsList := toolsInfo["names"].([]string)
|
toolsList := toolsInfo["names"].([]string)
|
||||||
|
|
||||||
// Check that our custom tool name is in the list
|
// Check that our custom tool name is in the list
|
||||||
|
|
@ -260,7 +263,7 @@ func TestToolRegistry_GetDefinitions(t *testing.T) {
|
||||||
al.RegisterTool(testTool)
|
al.RegisterTool(testTool)
|
||||||
|
|
||||||
info := al.GetStartupInfo()
|
info := al.GetStartupInfo()
|
||||||
toolsInfo := info["tools"].(map[string]interface{})
|
toolsInfo := info["tools"].(map[string]any)
|
||||||
toolsList := toolsInfo["names"].([]string)
|
toolsList := toolsInfo["names"].([]string)
|
||||||
|
|
||||||
// Check that our custom tool name is in the list
|
// Check that our custom tool name is in the list
|
||||||
|
|
@ -307,7 +310,7 @@ func TestAgentLoop_GetStartupInfo(t *testing.T) {
|
||||||
t.Fatal("Expected 'tools' key in startup info")
|
t.Fatal("Expected 'tools' key in startup info")
|
||||||
}
|
}
|
||||||
|
|
||||||
toolsMap, ok := toolsInfo.(map[string]interface{})
|
toolsMap, ok := toolsInfo.(map[string]any)
|
||||||
if !ok {
|
if !ok {
|
||||||
t.Fatal("Expected 'tools' to be a map")
|
t.Fatal("Expected 'tools' to be a map")
|
||||||
}
|
}
|
||||||
|
|
@ -363,7 +366,10 @@ type simpleMockProvider struct {
|
||||||
response string
|
response string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *simpleMockProvider) Chat(ctx context.Context, messages []providers.Message, tools []providers.ToolDefinition, model string, opts map[string]interface{}) (*providers.LLMResponse, error) {
|
func (m *simpleMockProvider) Chat(
|
||||||
|
ctx context.Context, messages []providers.Message, tools []providers.ToolDefinition, model string,
|
||||||
|
opts map[string]any,
|
||||||
|
) (*providers.LLMResponse, error) {
|
||||||
return &providers.LLMResponse{
|
return &providers.LLMResponse{
|
||||||
Content: m.response,
|
Content: m.response,
|
||||||
ToolCalls: []providers.ToolCall{},
|
ToolCalls: []providers.ToolCall{},
|
||||||
|
|
@ -385,14 +391,14 @@ func (m *mockCustomTool) Description() string {
|
||||||
return "Mock custom tool for testing"
|
return "Mock custom tool for testing"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mockCustomTool) Parameters() map[string]interface{} {
|
func (m *mockCustomTool) Parameters() map[string]any {
|
||||||
return map[string]interface{}{
|
return map[string]any{
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": map[string]interface{}{},
|
"properties": map[string]any{},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mockCustomTool) Execute(ctx context.Context, args map[string]interface{}) *tools.ToolResult {
|
func (m *mockCustomTool) Execute(ctx context.Context, args map[string]any) *tools.ToolResult {
|
||||||
return tools.SilentResult("Custom tool executed")
|
return tools.SilentResult("Custom tool executed")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -410,14 +416,14 @@ func (m *mockContextualTool) Description() string {
|
||||||
return "Mock contextual tool"
|
return "Mock contextual tool"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mockContextualTool) Parameters() map[string]interface{} {
|
func (m *mockContextualTool) Parameters() map[string]any {
|
||||||
return map[string]interface{}{
|
return map[string]any{
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": map[string]interface{}{},
|
"properties": map[string]any{},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mockContextualTool) Execute(ctx context.Context, args map[string]interface{}) *tools.ToolResult {
|
func (m *mockContextualTool) Execute(ctx context.Context, args map[string]any) *tools.ToolResult {
|
||||||
return tools.SilentResult("Contextual tool executed")
|
return tools.SilentResult("Contextual tool executed")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -537,7 +543,10 @@ type failFirstMockProvider struct {
|
||||||
successResp string
|
successResp string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *failFirstMockProvider) Chat(ctx context.Context, messages []providers.Message, tools []providers.ToolDefinition, model string, opts map[string]interface{}) (*providers.LLMResponse, error) {
|
func (m *failFirstMockProvider) Chat(
|
||||||
|
ctx context.Context, messages []providers.Message, tools []providers.ToolDefinition, model string,
|
||||||
|
opts map[string]any,
|
||||||
|
) (*providers.LLMResponse, error) {
|
||||||
m.currentCall++
|
m.currentCall++
|
||||||
if m.currentCall <= m.failures {
|
if m.currentCall <= m.failures {
|
||||||
return nil, m.failError
|
return nil, m.failError
|
||||||
|
|
@ -602,8 +611,13 @@ func TestAgentLoop_ContextExhaustionRetry(t *testing.T) {
|
||||||
|
|
||||||
// Call ProcessDirectWithChannel
|
// Call ProcessDirectWithChannel
|
||||||
// Note: ProcessDirectWithChannel calls processMessage which will execute runLLMIteration
|
// Note: ProcessDirectWithChannel calls processMessage which will execute runLLMIteration
|
||||||
response, err := al.ProcessDirectWithChannel(context.Background(), "Trigger message", sessionKey, "test", "test-chat")
|
response, err := al.ProcessDirectWithChannel(
|
||||||
|
context.Background(),
|
||||||
|
"Trigger message",
|
||||||
|
sessionKey,
|
||||||
|
"test",
|
||||||
|
"test-chat",
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Expected success after retry, got error: %v", err)
|
t.Fatalf("Expected success after retry, got error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ func NewMemoryStore(workspace string) *MemoryStore {
|
||||||
memoryFile := filepath.Join(memoryDir, "MEMORY.md")
|
memoryFile := filepath.Join(memoryDir, "MEMORY.md")
|
||||||
|
|
||||||
// Ensure memory directory exists
|
// Ensure memory directory exists
|
||||||
os.MkdirAll(memoryDir, 0755)
|
os.MkdirAll(memoryDir, 0o755)
|
||||||
|
|
||||||
return &MemoryStore{
|
return &MemoryStore{
|
||||||
workspace: workspace,
|
workspace: workspace,
|
||||||
|
|
@ -57,7 +57,7 @@ func (ms *MemoryStore) ReadLongTerm() string {
|
||||||
|
|
||||||
// WriteLongTerm writes content to the long-term memory file (MEMORY.md).
|
// WriteLongTerm writes content to the long-term memory file (MEMORY.md).
|
||||||
func (ms *MemoryStore) WriteLongTerm(content string) error {
|
func (ms *MemoryStore) WriteLongTerm(content string) error {
|
||||||
return os.WriteFile(ms.memoryFile, []byte(content), 0644)
|
return os.WriteFile(ms.memoryFile, []byte(content), 0o644)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReadToday reads today's daily note.
|
// ReadToday reads today's daily note.
|
||||||
|
|
@ -77,7 +77,7 @@ func (ms *MemoryStore) AppendToday(content string) error {
|
||||||
|
|
||||||
// Ensure month directory exists
|
// Ensure month directory exists
|
||||||
monthDir := filepath.Dir(todayFile)
|
monthDir := filepath.Dir(todayFile)
|
||||||
os.MkdirAll(monthDir, 0755)
|
os.MkdirAll(monthDir, 0o755)
|
||||||
|
|
||||||
var existingContent string
|
var existingContent string
|
||||||
if data, err := os.ReadFile(todayFile); err == nil {
|
if data, err := os.ReadFile(todayFile); err == nil {
|
||||||
|
|
@ -94,7 +94,7 @@ func (ms *MemoryStore) AppendToday(content string) error {
|
||||||
newContent = existingContent + "\n" + content
|
newContent = existingContent + "\n" + content
|
||||||
}
|
}
|
||||||
|
|
||||||
return os.WriteFile(todayFile, []byte(newContent), 0644)
|
return os.WriteFile(todayFile, []byte(newContent), 0o644)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetRecentDailyNotes returns daily notes from the last N days.
|
// GetRecentDailyNotes returns daily notes from the last N days.
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ func NewAgentRegistry(
|
||||||
instance := NewAgentInstance(ac, &cfg.Agents.Defaults, cfg, provider)
|
instance := NewAgentInstance(ac, &cfg.Agents.Defaults, cfg, provider)
|
||||||
registry.agents[id] = instance
|
registry.agents[id] = instance
|
||||||
logger.InfoCF("agent", "Registered agent",
|
logger.InfoCF("agent", "Registered agent",
|
||||||
map[string]interface{}{
|
map[string]any{
|
||||||
"agent_id": id,
|
"agent_id": id,
|
||||||
"name": ac.Name,
|
"name": ac.Name,
|
||||||
"workspace": instance.Workspace,
|
"workspace": instance.Workspace,
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,13 @@ import (
|
||||||
|
|
||||||
type mockRegistryProvider struct{}
|
type mockRegistryProvider struct{}
|
||||||
|
|
||||||
func (m *mockRegistryProvider) Chat(ctx context.Context, messages []providers.Message, tools []providers.ToolDefinition, model string, options map[string]interface{}) (*providers.LLMResponse, error) {
|
func (m *mockRegistryProvider) Chat(
|
||||||
|
ctx context.Context,
|
||||||
|
messages []providers.Message,
|
||||||
|
tools []providers.ToolDefinition,
|
||||||
|
model string,
|
||||||
|
options map[string]any,
|
||||||
|
) (*providers.LLMResponse, error) {
|
||||||
return &providers.LLMResponse{Content: "mock", FinishReason: "stop"}, nil
|
return &providers.LLMResponse{Content: "mock", FinishReason: "stop"}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -200,8 +200,11 @@ func LoginDeviceCode(cfg OAuthProviderConfig) (*AuthCredential, error) {
|
||||||
deviceResp.Interval = 5
|
deviceResp.Interval = 5
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("\nTo authenticate, open this URL in your browser:\n\n %s/codex/device\n\nThen enter this code: %s\n\nWaiting for authentication...\n",
|
fmt.Printf(
|
||||||
cfg.Issuer, deviceResp.UserCode)
|
"\nTo authenticate, open this URL in your browser:\n\n %s/codex/device\n\nThen enter this code: %s\n\nWaiting for authentication...\n",
|
||||||
|
cfg.Issuer,
|
||||||
|
deviceResp.UserCode,
|
||||||
|
)
|
||||||
|
|
||||||
deadline := time.After(15 * time.Minute)
|
deadline := time.After(15 * time.Minute)
|
||||||
ticker := time.NewTicker(time.Duration(deviceResp.Interval) * time.Second)
|
ticker := time.NewTicker(time.Duration(deviceResp.Interval) * time.Second)
|
||||||
|
|
@ -396,15 +399,15 @@ func extractAccountID(token string) string {
|
||||||
return accountID
|
return accountID
|
||||||
}
|
}
|
||||||
|
|
||||||
if authClaim, ok := claims["https://api.openai.com/auth"].(map[string]interface{}); ok {
|
if authClaim, ok := claims["https://api.openai.com/auth"].(map[string]any); ok {
|
||||||
if accountID, ok := authClaim["chatgpt_account_id"].(string); ok && accountID != "" {
|
if accountID, ok := authClaim["chatgpt_account_id"].(string); ok && accountID != "" {
|
||||||
return accountID
|
return accountID
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if orgs, ok := claims["organizations"].([]interface{}); ok {
|
if orgs, ok := claims["organizations"].([]any); ok {
|
||||||
for _, org := range orgs {
|
for _, org := range orgs {
|
||||||
if orgMap, ok := org.(map[string]interface{}); ok {
|
if orgMap, ok := org.(map[string]any); ok {
|
||||||
if accountID, ok := orgMap["id"].(string); ok && accountID != "" {
|
if accountID, ok := orgMap["id"].(string); ok && accountID != "" {
|
||||||
return accountID
|
return accountID
|
||||||
}
|
}
|
||||||
|
|
@ -415,7 +418,7 @@ func extractAccountID(token string) string {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseJWTClaims(token string) (map[string]interface{}, error) {
|
func parseJWTClaims(token string) (map[string]any, error) {
|
||||||
parts := strings.Split(token, ".")
|
parts := strings.Split(token, ".")
|
||||||
if len(parts) < 2 {
|
if len(parts) < 2 {
|
||||||
return nil, fmt.Errorf("token is not a JWT")
|
return nil, fmt.Errorf("token is not a JWT")
|
||||||
|
|
@ -434,7 +437,7 @@ func parseJWTClaims(token string) (map[string]interface{}, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var claims map[string]interface{}
|
var claims map[string]any
|
||||||
if err := json.Unmarshal(decoded, &claims); err != nil {
|
if err := json.Unmarshal(decoded, &claims); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func makeJWTForClaims(t *testing.T, claims map[string]interface{}) string {
|
func makeJWTForClaims(t *testing.T, claims map[string]any) string {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
header := base64.RawURLEncoding.EncodeToString([]byte(`{"alg":"none","typ":"JWT"}`))
|
header := base64.RawURLEncoding.EncodeToString([]byte(`{"alg":"none","typ":"JWT"}`))
|
||||||
|
|
@ -89,7 +89,7 @@ func TestBuildAuthorizeURLOpenAIExtras(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParseTokenResponse(t *testing.T) {
|
func TestParseTokenResponse(t *testing.T) {
|
||||||
resp := map[string]interface{}{
|
resp := map[string]any{
|
||||||
"access_token": "test-access-token",
|
"access_token": "test-access-token",
|
||||||
"refresh_token": "test-refresh-token",
|
"refresh_token": "test-refresh-token",
|
||||||
"expires_in": 3600,
|
"expires_in": 3600,
|
||||||
|
|
@ -120,8 +120,8 @@ func TestParseTokenResponse(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParseTokenResponseExtractsAccountIDFromIDToken(t *testing.T) {
|
func TestParseTokenResponseExtractsAccountIDFromIDToken(t *testing.T) {
|
||||||
idToken := makeJWTForClaims(t, map[string]interface{}{"chatgpt_account_id": "acc-id-from-id-token"})
|
idToken := makeJWTForClaims(t, map[string]any{"chatgpt_account_id": "acc-id-from-id-token"})
|
||||||
resp := map[string]interface{}{
|
resp := map[string]any{
|
||||||
"access_token": "opaque-access-token",
|
"access_token": "opaque-access-token",
|
||||||
"refresh_token": "test-refresh-token",
|
"refresh_token": "test-refresh-token",
|
||||||
"expires_in": 3600,
|
"expires_in": 3600,
|
||||||
|
|
@ -139,9 +139,9 @@ func TestParseTokenResponseExtractsAccountIDFromIDToken(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestExtractAccountIDFromOrganizationsFallback(t *testing.T) {
|
func TestExtractAccountIDFromOrganizationsFallback(t *testing.T) {
|
||||||
token := makeJWTForClaims(t, map[string]interface{}{
|
token := makeJWTForClaims(t, map[string]any{
|
||||||
"organizations": []interface{}{
|
"organizations": []any{
|
||||||
map[string]interface{}{"id": "org_from_orgs"},
|
map[string]any{"id": "org_from_orgs"},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -160,7 +160,7 @@ func TestParseTokenResponseNoAccessToken(t *testing.T) {
|
||||||
|
|
||||||
func TestParseTokenResponseAccountIDFromIDToken(t *testing.T) {
|
func TestParseTokenResponseAccountIDFromIDToken(t *testing.T) {
|
||||||
idToken := makeJWTWithAccountID("acc-from-id")
|
idToken := makeJWTWithAccountID("acc-from-id")
|
||||||
resp := map[string]interface{}{
|
resp := map[string]any{
|
||||||
"access_token": "not-a-jwt",
|
"access_token": "not-a-jwt",
|
||||||
"refresh_token": "test-refresh-token",
|
"refresh_token": "test-refresh-token",
|
||||||
"expires_in": 3600,
|
"expires_in": 3600,
|
||||||
|
|
@ -180,7 +180,9 @@ func TestParseTokenResponseAccountIDFromIDToken(t *testing.T) {
|
||||||
|
|
||||||
func makeJWTWithAccountID(accountID string) string {
|
func makeJWTWithAccountID(accountID string) string {
|
||||||
header := base64.RawURLEncoding.EncodeToString([]byte(`{"alg":"none","typ":"JWT"}`))
|
header := base64.RawURLEncoding.EncodeToString([]byte(`{"alg":"none","typ":"JWT"}`))
|
||||||
payload := base64.RawURLEncoding.EncodeToString([]byte(`{"https://api.openai.com/auth":{"chatgpt_account_id":"` + accountID + `"}}`))
|
payload := base64.RawURLEncoding.EncodeToString(
|
||||||
|
[]byte(`{"https://api.openai.com/auth":{"chatgpt_account_id":"` + accountID + `"}}`),
|
||||||
|
)
|
||||||
return header + "." + payload + ".sig"
|
return header + "." + payload + ".sig"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -201,7 +203,7 @@ func TestExchangeCodeForTokens(t *testing.T) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
resp := map[string]interface{}{
|
resp := map[string]any{
|
||||||
"access_token": "mock-access-token",
|
"access_token": "mock-access-token",
|
||||||
"refresh_token": "mock-refresh-token",
|
"refresh_token": "mock-refresh-token",
|
||||||
"expires_in": 3600,
|
"expires_in": 3600,
|
||||||
|
|
@ -240,7 +242,7 @@ func TestRefreshAccessToken(t *testing.T) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
resp := map[string]interface{}{
|
resp := map[string]any{
|
||||||
"access_token": "refreshed-access-token",
|
"access_token": "refreshed-access-token",
|
||||||
"refresh_token": "refreshed-refresh-token",
|
"refresh_token": "refreshed-refresh-token",
|
||||||
"expires_in": 3600,
|
"expires_in": 3600,
|
||||||
|
|
@ -290,7 +292,7 @@ func TestRefreshAccessTokenNoRefreshToken(t *testing.T) {
|
||||||
|
|
||||||
func TestRefreshAccessTokenPreservesRefreshAndAccountID(t *testing.T) {
|
func TestRefreshAccessTokenPreservesRefreshAndAccountID(t *testing.T) {
|
||||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
resp := map[string]interface{}{
|
resp := map[string]any{
|
||||||
"access_token": "new-access-token-only",
|
"access_token": "new-access-token-only",
|
||||||
"expires_in": 3600,
|
"expires_in": 3600,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,7 @@ func LoadStore() (*AuthStore, error) {
|
||||||
func SaveStore(store *AuthStore) error {
|
func SaveStore(store *AuthStore) error {
|
||||||
path := authFilePath()
|
path := authFilePath()
|
||||||
dir := filepath.Dir(path)
|
dir := filepath.Dir(path)
|
||||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -70,7 +70,7 @@ func SaveStore(store *AuthStore) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return os.WriteFile(path, data, 0600)
|
return os.WriteFile(path, data, 0o600)
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetCredential(provider string) (*AuthCredential, error) {
|
func GetCredential(provider string) (*AuthCredential, error) {
|
||||||
|
|
|
||||||
|
|
@ -108,7 +108,7 @@ func TestStoreFilePermissions(t *testing.T) {
|
||||||
t.Fatalf("Stat() error: %v", err)
|
t.Fatalf("Stat() error: %v", err)
|
||||||
}
|
}
|
||||||
perm := info.Mode().Perm()
|
perm := info.Mode().Perm()
|
||||||
if perm != 0600 {
|
if perm != 0o600 {
|
||||||
t.Errorf("file permissions = %o, want 0600", perm)
|
t.Errorf("file permissions = %o, want 0600", perm)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,14 +17,14 @@ type Channel interface {
|
||||||
}
|
}
|
||||||
|
|
||||||
type BaseChannel struct {
|
type BaseChannel struct {
|
||||||
config interface{}
|
config any
|
||||||
bus *bus.MessageBus
|
bus *bus.MessageBus
|
||||||
running bool
|
running bool
|
||||||
name string
|
name string
|
||||||
allowList []string
|
allowList []string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewBaseChannel(name string, config interface{}, bus *bus.MessageBus, allowList []string) *BaseChannel {
|
func NewBaseChannel(name string, config any, bus *bus.MessageBus, allowList []string) *BaseChannel {
|
||||||
return &BaseChannel{
|
return &BaseChannel{
|
||||||
config: config,
|
config: config,
|
||||||
bus: bus,
|
bus: bus,
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import (
|
||||||
|
|
||||||
"github.com/open-dingtalk/dingtalk-stream-sdk-go/chatbot"
|
"github.com/open-dingtalk/dingtalk-stream-sdk-go/chatbot"
|
||||||
"github.com/open-dingtalk/dingtalk-stream-sdk-go/client"
|
"github.com/open-dingtalk/dingtalk-stream-sdk-go/client"
|
||||||
|
|
||||||
"github.com/sipeed/picoclaw/pkg/bus"
|
"github.com/sipeed/picoclaw/pkg/bus"
|
||||||
"github.com/sipeed/picoclaw/pkg/config"
|
"github.com/sipeed/picoclaw/pkg/config"
|
||||||
"github.com/sipeed/picoclaw/pkg/logger"
|
"github.com/sipeed/picoclaw/pkg/logger"
|
||||||
|
|
@ -108,7 +109,7 @@ func (c *DingTalkChannel) Send(ctx context.Context, msg bus.OutboundMessage) err
|
||||||
return fmt.Errorf("invalid session_webhook type for chat %s", msg.ChatID)
|
return fmt.Errorf("invalid session_webhook type for chat %s", msg.ChatID)
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.DebugCF("dingtalk", "Sending message", map[string]interface{}{
|
logger.DebugCF("dingtalk", "Sending message", map[string]any{
|
||||||
"chat_id": msg.ChatID,
|
"chat_id": msg.ChatID,
|
||||||
"preview": utils.Truncate(msg.Content, 100),
|
"preview": utils.Truncate(msg.Content, 100),
|
||||||
})
|
})
|
||||||
|
|
@ -120,12 +121,14 @@ func (c *DingTalkChannel) Send(ctx context.Context, msg bus.OutboundMessage) err
|
||||||
// onChatBotMessageReceived implements the IChatBotMessageHandler function signature
|
// onChatBotMessageReceived implements the IChatBotMessageHandler function signature
|
||||||
// This is called by the Stream SDK when a new message arrives
|
// This is called by the Stream SDK when a new message arrives
|
||||||
// IChatBotMessageHandler is: func(c context.Context, data *chatbot.BotCallbackDataModel) ([]byte, error)
|
// IChatBotMessageHandler is: func(c context.Context, data *chatbot.BotCallbackDataModel) ([]byte, error)
|
||||||
func (c *DingTalkChannel) onChatBotMessageReceived(ctx context.Context, data *chatbot.BotCallbackDataModel) ([]byte, error) {
|
func (c *DingTalkChannel) onChatBotMessageReceived(
|
||||||
|
ctx context.Context, data *chatbot.BotCallbackDataModel,
|
||||||
|
) ([]byte, error) {
|
||||||
// Extract message content from Text field
|
// Extract message content from Text field
|
||||||
content := data.Text.Content
|
content := data.Text.Content
|
||||||
if content == "" {
|
if content == "" {
|
||||||
// Try to extract from Content interface{} if Text is empty
|
// Try to extract from Content interface{} if Text is empty
|
||||||
if contentMap, ok := data.Content.(map[string]interface{}); ok {
|
if contentMap, ok := data.Content.(map[string]any); ok {
|
||||||
if textContent, ok := contentMap["content"].(string); ok {
|
if textContent, ok := contentMap["content"].(string); ok {
|
||||||
content = textContent
|
content = textContent
|
||||||
}
|
}
|
||||||
|
|
@ -155,7 +158,7 @@ func (c *DingTalkChannel) onChatBotMessageReceived(ctx context.Context, data *ch
|
||||||
"session_webhook": data.SessionWebhook,
|
"session_webhook": data.SessionWebhook,
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.DebugCF("dingtalk", "Received message", map[string]interface{}{
|
logger.DebugCF("dingtalk", "Received message", map[string]any{
|
||||||
"sender_nick": senderNick,
|
"sender_nick": senderNick,
|
||||||
"sender_id": senderID,
|
"sender_id": senderID,
|
||||||
"preview": utils.Truncate(content, 50),
|
"preview": utils.Truncate(content, 50),
|
||||||
|
|
@ -184,7 +187,6 @@ func (c *DingTalkChannel) SendDirectReply(ctx context.Context, sessionWebhook, c
|
||||||
titleBytes,
|
titleBytes,
|
||||||
contentBytes,
|
contentBytes,
|
||||||
)
|
)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to send reply: %w", err)
|
return fmt.Errorf("failed to send reply: %w", err)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/bwmarrin/discordgo"
|
"github.com/bwmarrin/discordgo"
|
||||||
|
|
||||||
"github.com/sipeed/picoclaw/pkg/bus"
|
"github.com/sipeed/picoclaw/pkg/bus"
|
||||||
"github.com/sipeed/picoclaw/pkg/config"
|
"github.com/sipeed/picoclaw/pkg/config"
|
||||||
"github.com/sipeed/picoclaw/pkg/logger"
|
"github.com/sipeed/picoclaw/pkg/logger"
|
||||||
|
|
@ -106,7 +107,9 @@ func (c *DiscordChannel) Send(ctx context.Context, msg bus.OutboundMessage) erro
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
chunks := splitMessage(msg.Content, 1500) // Discord has a limit of 2000 characters per message, leave 500 for natural split e.g. code blocks
|
chunks := splitMessage(
|
||||||
|
msg.Content, 1500,
|
||||||
|
) // Discord has a limit of 2000 characters per message, leave 500 for natural split e.g. code blocks
|
||||||
|
|
||||||
for _, chunk := range chunks {
|
for _, chunk := range chunks {
|
||||||
if err := c.sendChunk(ctx, channelID, chunk); err != nil {
|
if err := c.sendChunk(ctx, channelID, chunk); err != nil {
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,9 @@ type FeishuChannel struct {
|
||||||
|
|
||||||
// NewFeishuChannel returns an error on 32-bit architectures where the Feishu SDK is not supported
|
// NewFeishuChannel returns an error on 32-bit architectures where the Feishu SDK is not supported
|
||||||
func NewFeishuChannel(cfg config.FeishuConfig, bus *bus.MessageBus) (*FeishuChannel, error) {
|
func NewFeishuChannel(cfg config.FeishuConfig, bus *bus.MessageBus) (*FeishuChannel, error) {
|
||||||
return nil, errors.New("feishu channel is not supported on 32-bit architectures (armv7l, 386, etc.). Please use a 64-bit system or disable feishu in your config")
|
return nil, errors.New(
|
||||||
|
"feishu channel is not supported on 32-bit architectures (armv7l, 386, etc.). Please use a 64-bit system or disable feishu in your config",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start is a stub method to satisfy the Channel interface
|
// Start is a stub method to satisfy the Channel interface
|
||||||
|
|
|
||||||
|
|
@ -65,7 +65,7 @@ func (c *FeishuChannel) Start(ctx context.Context) error {
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
if err := wsClient.Start(runCtx); err != nil {
|
if err := wsClient.Start(runCtx); err != nil {
|
||||||
logger.ErrorCF("feishu", "Feishu websocket stopped with error", map[string]interface{}{
|
logger.ErrorCF("feishu", "Feishu websocket stopped with error", map[string]any{
|
||||||
"error": err.Error(),
|
"error": err.Error(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -121,7 +121,7 @@ func (c *FeishuChannel) Send(ctx context.Context, msg bus.OutboundMessage) error
|
||||||
return fmt.Errorf("feishu api error: code=%d msg=%s", resp.Code, resp.Msg)
|
return fmt.Errorf("feishu api error: code=%d msg=%s", resp.Code, resp.Msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.DebugCF("feishu", "Feishu message sent", map[string]interface{}{
|
logger.DebugCF("feishu", "Feishu message sent", map[string]any{
|
||||||
"chat_id": msg.ChatID,
|
"chat_id": msg.ChatID,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -165,7 +165,7 @@ func (c *FeishuChannel) handleMessageReceive(_ context.Context, event *larkim.P2
|
||||||
metadata["tenant_key"] = *sender.TenantKey
|
metadata["tenant_key"] = *sender.TenantKey
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.InfoCF("feishu", "Feishu message received", map[string]interface{}{
|
logger.InfoCF("feishu", "Feishu message received", map[string]any{
|
||||||
"sender_id": senderID,
|
"sender_id": senderID,
|
||||||
"chat_id": chatID,
|
"chat_id": chatID,
|
||||||
"preview": utils.Truncate(content, 80),
|
"preview": utils.Truncate(content, 80),
|
||||||
|
|
|
||||||
|
|
@ -75,11 +75,11 @@ func (c *LINEChannel) Start(ctx context.Context) error {
|
||||||
|
|
||||||
// Fetch bot profile to get bot's userId for mention detection
|
// Fetch bot profile to get bot's userId for mention detection
|
||||||
if err := c.fetchBotInfo(); err != nil {
|
if err := c.fetchBotInfo(); err != nil {
|
||||||
logger.WarnCF("line", "Failed to fetch bot info (mention detection disabled)", map[string]interface{}{
|
logger.WarnCF("line", "Failed to fetch bot info (mention detection disabled)", map[string]any{
|
||||||
"error": err.Error(),
|
"error": err.Error(),
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
logger.InfoCF("line", "Bot info fetched", map[string]interface{}{
|
logger.InfoCF("line", "Bot info fetched", map[string]any{
|
||||||
"bot_user_id": c.botUserID,
|
"bot_user_id": c.botUserID,
|
||||||
"basic_id": c.botBasicID,
|
"basic_id": c.botBasicID,
|
||||||
"display_name": c.botDisplayName,
|
"display_name": c.botDisplayName,
|
||||||
|
|
@ -100,12 +100,12 @@ func (c *LINEChannel) Start(ctx context.Context) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
logger.InfoCF("line", "LINE webhook server listening", map[string]interface{}{
|
logger.InfoCF("line", "LINE webhook server listening", map[string]any{
|
||||||
"addr": addr,
|
"addr": addr,
|
||||||
"path": path,
|
"path": path,
|
||||||
})
|
})
|
||||||
if err := c.httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
if err := c.httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||||
logger.ErrorCF("line", "Webhook server error", map[string]interface{}{
|
logger.ErrorCF("line", "Webhook server error", map[string]any{
|
||||||
"error": err.Error(),
|
"error": err.Error(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -162,7 +162,7 @@ func (c *LINEChannel) Stop(ctx context.Context) error {
|
||||||
shutdownCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
shutdownCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
if err := c.httpServer.Shutdown(shutdownCtx); err != nil {
|
if err := c.httpServer.Shutdown(shutdownCtx); err != nil {
|
||||||
logger.ErrorCF("line", "Webhook server shutdown error", map[string]interface{}{
|
logger.ErrorCF("line", "Webhook server shutdown error", map[string]any{
|
||||||
"error": err.Error(),
|
"error": err.Error(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -182,7 +182,7 @@ func (c *LINEChannel) webhookHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
body, err := io.ReadAll(r.Body)
|
body, err := io.ReadAll(r.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.ErrorCF("line", "Failed to read request body", map[string]interface{}{
|
logger.ErrorCF("line", "Failed to read request body", map[string]any{
|
||||||
"error": err.Error(),
|
"error": err.Error(),
|
||||||
})
|
})
|
||||||
http.Error(w, "Bad request", http.StatusBadRequest)
|
http.Error(w, "Bad request", http.StatusBadRequest)
|
||||||
|
|
@ -200,7 +200,7 @@ func (c *LINEChannel) webhookHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
Events []lineEvent `json:"events"`
|
Events []lineEvent `json:"events"`
|
||||||
}
|
}
|
||||||
if err := json.Unmarshal(body, &payload); err != nil {
|
if err := json.Unmarshal(body, &payload); err != nil {
|
||||||
logger.ErrorCF("line", "Failed to parse webhook payload", map[string]interface{}{
|
logger.ErrorCF("line", "Failed to parse webhook payload", map[string]any{
|
||||||
"error": err.Error(),
|
"error": err.Error(),
|
||||||
})
|
})
|
||||||
http.Error(w, "Bad request", http.StatusBadRequest)
|
http.Error(w, "Bad request", http.StatusBadRequest)
|
||||||
|
|
@ -266,7 +266,7 @@ type lineMentionee struct {
|
||||||
|
|
||||||
func (c *LINEChannel) processEvent(event lineEvent) {
|
func (c *LINEChannel) processEvent(event lineEvent) {
|
||||||
if event.Type != "message" {
|
if event.Type != "message" {
|
||||||
logger.DebugCF("line", "Ignoring non-message event", map[string]interface{}{
|
logger.DebugCF("line", "Ignoring non-message event", map[string]any{
|
||||||
"type": event.Type,
|
"type": event.Type,
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
|
|
@ -278,7 +278,7 @@ func (c *LINEChannel) processEvent(event lineEvent) {
|
||||||
|
|
||||||
var msg lineMessage
|
var msg lineMessage
|
||||||
if err := json.Unmarshal(event.Message, &msg); err != nil {
|
if err := json.Unmarshal(event.Message, &msg); err != nil {
|
||||||
logger.ErrorCF("line", "Failed to parse message", map[string]interface{}{
|
logger.ErrorCF("line", "Failed to parse message", map[string]any{
|
||||||
"error": err.Error(),
|
"error": err.Error(),
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
|
|
@ -286,7 +286,7 @@ func (c *LINEChannel) processEvent(event lineEvent) {
|
||||||
|
|
||||||
// In group chats, only respond when the bot is mentioned
|
// In group chats, only respond when the bot is mentioned
|
||||||
if isGroup && !c.isBotMentioned(msg) {
|
if isGroup && !c.isBotMentioned(msg) {
|
||||||
logger.DebugCF("line", "Ignoring group message without mention", map[string]interface{}{
|
logger.DebugCF("line", "Ignoring group message without mention", map[string]any{
|
||||||
"chat_id": chatID,
|
"chat_id": chatID,
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
|
|
@ -312,7 +312,7 @@ func (c *LINEChannel) processEvent(event lineEvent) {
|
||||||
defer func() {
|
defer func() {
|
||||||
for _, file := range localFiles {
|
for _, file := range localFiles {
|
||||||
if err := os.Remove(file); err != nil {
|
if err := os.Remove(file); err != nil {
|
||||||
logger.DebugCF("line", "Failed to cleanup temp file", map[string]interface{}{
|
logger.DebugCF("line", "Failed to cleanup temp file", map[string]any{
|
||||||
"file": file,
|
"file": file,
|
||||||
"error": err.Error(),
|
"error": err.Error(),
|
||||||
})
|
})
|
||||||
|
|
@ -366,7 +366,7 @@ func (c *LINEChannel) processEvent(event lineEvent) {
|
||||||
"message_id": msg.ID,
|
"message_id": msg.ID,
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.DebugCF("line", "Received message", map[string]interface{}{
|
logger.DebugCF("line", "Received message", map[string]any{
|
||||||
"sender_id": senderID,
|
"sender_id": senderID,
|
||||||
"chat_id": chatID,
|
"chat_id": chatID,
|
||||||
"message_type": msg.Type,
|
"message_type": msg.Type,
|
||||||
|
|
@ -497,7 +497,7 @@ func (c *LINEChannel) Send(ctx context.Context, msg bus.OutboundMessage) error {
|
||||||
tokenEntry := entry.(replyTokenEntry)
|
tokenEntry := entry.(replyTokenEntry)
|
||||||
if time.Since(tokenEntry.timestamp) < lineReplyTokenMaxAge {
|
if time.Since(tokenEntry.timestamp) < lineReplyTokenMaxAge {
|
||||||
if err := c.sendReply(ctx, tokenEntry.token, msg.Content, quoteToken); err == nil {
|
if err := c.sendReply(ctx, tokenEntry.token, msg.Content, quoteToken); err == nil {
|
||||||
logger.DebugCF("line", "Message sent via Reply API", map[string]interface{}{
|
logger.DebugCF("line", "Message sent via Reply API", map[string]any{
|
||||||
"chat_id": msg.ChatID,
|
"chat_id": msg.ChatID,
|
||||||
"quoted": quoteToken != "",
|
"quoted": quoteToken != "",
|
||||||
})
|
})
|
||||||
|
|
@ -525,7 +525,7 @@ func buildTextMessage(content, quoteToken string) map[string]string {
|
||||||
|
|
||||||
// sendReply sends a message using the LINE Reply API.
|
// sendReply sends a message using the LINE Reply API.
|
||||||
func (c *LINEChannel) sendReply(ctx context.Context, replyToken, content, quoteToken string) error {
|
func (c *LINEChannel) sendReply(ctx context.Context, replyToken, content, quoteToken string) error {
|
||||||
payload := map[string]interface{}{
|
payload := map[string]any{
|
||||||
"replyToken": replyToken,
|
"replyToken": replyToken,
|
||||||
"messages": []map[string]string{buildTextMessage(content, quoteToken)},
|
"messages": []map[string]string{buildTextMessage(content, quoteToken)},
|
||||||
}
|
}
|
||||||
|
|
@ -535,7 +535,7 @@ func (c *LINEChannel) sendReply(ctx context.Context, replyToken, content, quoteT
|
||||||
|
|
||||||
// sendPush sends a message using the LINE Push API.
|
// sendPush sends a message using the LINE Push API.
|
||||||
func (c *LINEChannel) sendPush(ctx context.Context, to, content, quoteToken string) error {
|
func (c *LINEChannel) sendPush(ctx context.Context, to, content, quoteToken string) error {
|
||||||
payload := map[string]interface{}{
|
payload := map[string]any{
|
||||||
"to": to,
|
"to": to,
|
||||||
"messages": []map[string]string{buildTextMessage(content, quoteToken)},
|
"messages": []map[string]string{buildTextMessage(content, quoteToken)},
|
||||||
}
|
}
|
||||||
|
|
@ -545,19 +545,19 @@ func (c *LINEChannel) sendPush(ctx context.Context, to, content, quoteToken stri
|
||||||
|
|
||||||
// sendLoading sends a loading animation indicator to the chat.
|
// sendLoading sends a loading animation indicator to the chat.
|
||||||
func (c *LINEChannel) sendLoading(chatID string) {
|
func (c *LINEChannel) sendLoading(chatID string) {
|
||||||
payload := map[string]interface{}{
|
payload := map[string]any{
|
||||||
"chatId": chatID,
|
"chatId": chatID,
|
||||||
"loadingSeconds": 60,
|
"loadingSeconds": 60,
|
||||||
}
|
}
|
||||||
if err := c.callAPI(c.ctx, lineLoadingEndpoint, payload); err != nil {
|
if err := c.callAPI(c.ctx, lineLoadingEndpoint, payload); err != nil {
|
||||||
logger.DebugCF("line", "Failed to send loading indicator", map[string]interface{}{
|
logger.DebugCF("line", "Failed to send loading indicator", map[string]any{
|
||||||
"error": err.Error(),
|
"error": err.Error(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// callAPI makes an authenticated POST request to the LINE API.
|
// callAPI makes an authenticated POST request to the LINE API.
|
||||||
func (c *LINEChannel) callAPI(ctx context.Context, endpoint string, payload interface{}) error {
|
func (c *LINEChannel) callAPI(ctx context.Context, endpoint string, payload any) error {
|
||||||
body, err := json.Marshal(payload)
|
body, err := json.Marshal(payload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to marshal payload: %w", err)
|
return fmt.Errorf("failed to marshal payload: %w", err)
|
||||||
|
|
|
||||||
|
|
@ -21,10 +21,10 @@ type MaixCamChannel struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type MaixCamMessage struct {
|
type MaixCamMessage struct {
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Tips string `json:"tips"`
|
Tips string `json:"tips"`
|
||||||
Timestamp float64 `json:"timestamp"`
|
Timestamp float64 `json:"timestamp"`
|
||||||
Data map[string]interface{} `json:"data"`
|
Data map[string]any `json:"data"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewMaixCamChannel(cfg config.MaixCamConfig, bus *bus.MessageBus) (*MaixCamChannel, error) {
|
func NewMaixCamChannel(cfg config.MaixCamConfig, bus *bus.MessageBus) (*MaixCamChannel, error) {
|
||||||
|
|
@ -49,7 +49,7 @@ func (c *MaixCamChannel) Start(ctx context.Context) error {
|
||||||
c.listener = listener
|
c.listener = listener
|
||||||
c.setRunning(true)
|
c.setRunning(true)
|
||||||
|
|
||||||
logger.InfoCF("maixcam", "MaixCam server listening", map[string]interface{}{
|
logger.InfoCF("maixcam", "MaixCam server listening", map[string]any{
|
||||||
"host": c.config.Host,
|
"host": c.config.Host,
|
||||||
"port": c.config.Port,
|
"port": c.config.Port,
|
||||||
})
|
})
|
||||||
|
|
@ -71,14 +71,14 @@ func (c *MaixCamChannel) acceptConnections(ctx context.Context) {
|
||||||
conn, err := c.listener.Accept()
|
conn, err := c.listener.Accept()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if c.running {
|
if c.running {
|
||||||
logger.ErrorCF("maixcam", "Failed to accept connection", map[string]interface{}{
|
logger.ErrorCF("maixcam", "Failed to accept connection", map[string]any{
|
||||||
"error": err.Error(),
|
"error": err.Error(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.InfoCF("maixcam", "New connection from MaixCam device", map[string]interface{}{
|
logger.InfoCF("maixcam", "New connection from MaixCam device", map[string]any{
|
||||||
"remote_addr": conn.RemoteAddr().String(),
|
"remote_addr": conn.RemoteAddr().String(),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -112,7 +112,7 @@ func (c *MaixCamChannel) handleConnection(conn net.Conn, ctx context.Context) {
|
||||||
var msg MaixCamMessage
|
var msg MaixCamMessage
|
||||||
if err := decoder.Decode(&msg); err != nil {
|
if err := decoder.Decode(&msg); err != nil {
|
||||||
if err.Error() != "EOF" {
|
if err.Error() != "EOF" {
|
||||||
logger.ErrorCF("maixcam", "Failed to decode message", map[string]interface{}{
|
logger.ErrorCF("maixcam", "Failed to decode message", map[string]any{
|
||||||
"error": err.Error(),
|
"error": err.Error(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -133,14 +133,14 @@ func (c *MaixCamChannel) processMessage(msg MaixCamMessage, conn net.Conn) {
|
||||||
case "status":
|
case "status":
|
||||||
c.handleStatusUpdate(msg)
|
c.handleStatusUpdate(msg)
|
||||||
default:
|
default:
|
||||||
logger.WarnCF("maixcam", "Unknown message type", map[string]interface{}{
|
logger.WarnCF("maixcam", "Unknown message type", map[string]any{
|
||||||
"type": msg.Type,
|
"type": msg.Type,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *MaixCamChannel) handlePersonDetection(msg MaixCamMessage) {
|
func (c *MaixCamChannel) handlePersonDetection(msg MaixCamMessage) {
|
||||||
logger.InfoCF("maixcam", "", map[string]interface{}{
|
logger.InfoCF("maixcam", "", map[string]any{
|
||||||
"timestamp": msg.Timestamp,
|
"timestamp": msg.Timestamp,
|
||||||
"data": msg.Data,
|
"data": msg.Data,
|
||||||
})
|
})
|
||||||
|
|
@ -176,7 +176,7 @@ func (c *MaixCamChannel) handlePersonDetection(msg MaixCamMessage) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *MaixCamChannel) handleStatusUpdate(msg MaixCamMessage) {
|
func (c *MaixCamChannel) handleStatusUpdate(msg MaixCamMessage) {
|
||||||
logger.InfoCF("maixcam", "Status update from MaixCam", map[string]interface{}{
|
logger.InfoCF("maixcam", "Status update from MaixCam", map[string]any{
|
||||||
"status": msg.Data,
|
"status": msg.Data,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -214,7 +214,7 @@ func (c *MaixCamChannel) Send(ctx context.Context, msg bus.OutboundMessage) erro
|
||||||
return fmt.Errorf("no connected MaixCam devices")
|
return fmt.Errorf("no connected MaixCam devices")
|
||||||
}
|
}
|
||||||
|
|
||||||
response := map[string]interface{}{
|
response := map[string]any{
|
||||||
"type": "command",
|
"type": "command",
|
||||||
"timestamp": float64(0),
|
"timestamp": float64(0),
|
||||||
"message": msg.Content,
|
"message": msg.Content,
|
||||||
|
|
@ -229,7 +229,7 @@ func (c *MaixCamChannel) Send(ctx context.Context, msg bus.OutboundMessage) erro
|
||||||
var sendErr error
|
var sendErr error
|
||||||
for conn := range c.clients {
|
for conn := range c.clients {
|
||||||
if _, err := conn.Write(data); err != nil {
|
if _, err := conn.Write(data); err != nil {
|
||||||
logger.ErrorCF("maixcam", "Failed to send to client", map[string]interface{}{
|
logger.ErrorCF("maixcam", "Failed to send to client", map[string]any{
|
||||||
"client": conn.RemoteAddr().String(),
|
"client": conn.RemoteAddr().String(),
|
||||||
"error": err.Error(),
|
"error": err.Error(),
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,7 @@ func (m *Manager) initChannels() error {
|
||||||
logger.DebugC("channels", "Attempting to initialize Telegram channel")
|
logger.DebugC("channels", "Attempting to initialize Telegram channel")
|
||||||
telegram, err := NewTelegramChannel(m.config, m.bus)
|
telegram, err := NewTelegramChannel(m.config, m.bus)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.ErrorCF("channels", "Failed to initialize Telegram channel", map[string]interface{}{
|
logger.ErrorCF("channels", "Failed to initialize Telegram channel", map[string]any{
|
||||||
"error": err.Error(),
|
"error": err.Error(),
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -63,7 +63,7 @@ func (m *Manager) initChannels() error {
|
||||||
logger.DebugC("channels", "Attempting to initialize WhatsApp channel")
|
logger.DebugC("channels", "Attempting to initialize WhatsApp channel")
|
||||||
whatsapp, err := NewWhatsAppChannel(m.config.Channels.WhatsApp, m.bus)
|
whatsapp, err := NewWhatsAppChannel(m.config.Channels.WhatsApp, m.bus)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.ErrorCF("channels", "Failed to initialize WhatsApp channel", map[string]interface{}{
|
logger.ErrorCF("channels", "Failed to initialize WhatsApp channel", map[string]any{
|
||||||
"error": err.Error(),
|
"error": err.Error(),
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -76,7 +76,7 @@ func (m *Manager) initChannels() error {
|
||||||
logger.DebugC("channels", "Attempting to initialize Feishu channel")
|
logger.DebugC("channels", "Attempting to initialize Feishu channel")
|
||||||
feishu, err := NewFeishuChannel(m.config.Channels.Feishu, m.bus)
|
feishu, err := NewFeishuChannel(m.config.Channels.Feishu, m.bus)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.ErrorCF("channels", "Failed to initialize Feishu channel", map[string]interface{}{
|
logger.ErrorCF("channels", "Failed to initialize Feishu channel", map[string]any{
|
||||||
"error": err.Error(),
|
"error": err.Error(),
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -89,7 +89,7 @@ func (m *Manager) initChannels() error {
|
||||||
logger.DebugC("channels", "Attempting to initialize Discord channel")
|
logger.DebugC("channels", "Attempting to initialize Discord channel")
|
||||||
discord, err := NewDiscordChannel(m.config.Channels.Discord, m.bus)
|
discord, err := NewDiscordChannel(m.config.Channels.Discord, m.bus)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.ErrorCF("channels", "Failed to initialize Discord channel", map[string]interface{}{
|
logger.ErrorCF("channels", "Failed to initialize Discord channel", map[string]any{
|
||||||
"error": err.Error(),
|
"error": err.Error(),
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -102,7 +102,7 @@ func (m *Manager) initChannels() error {
|
||||||
logger.DebugC("channels", "Attempting to initialize MaixCam channel")
|
logger.DebugC("channels", "Attempting to initialize MaixCam channel")
|
||||||
maixcam, err := NewMaixCamChannel(m.config.Channels.MaixCam, m.bus)
|
maixcam, err := NewMaixCamChannel(m.config.Channels.MaixCam, m.bus)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.ErrorCF("channels", "Failed to initialize MaixCam channel", map[string]interface{}{
|
logger.ErrorCF("channels", "Failed to initialize MaixCam channel", map[string]any{
|
||||||
"error": err.Error(),
|
"error": err.Error(),
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -115,7 +115,7 @@ func (m *Manager) initChannels() error {
|
||||||
logger.DebugC("channels", "Attempting to initialize QQ channel")
|
logger.DebugC("channels", "Attempting to initialize QQ channel")
|
||||||
qq, err := NewQQChannel(m.config.Channels.QQ, m.bus)
|
qq, err := NewQQChannel(m.config.Channels.QQ, m.bus)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.ErrorCF("channels", "Failed to initialize QQ channel", map[string]interface{}{
|
logger.ErrorCF("channels", "Failed to initialize QQ channel", map[string]any{
|
||||||
"error": err.Error(),
|
"error": err.Error(),
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -128,7 +128,7 @@ func (m *Manager) initChannels() error {
|
||||||
logger.DebugC("channels", "Attempting to initialize DingTalk channel")
|
logger.DebugC("channels", "Attempting to initialize DingTalk channel")
|
||||||
dingtalk, err := NewDingTalkChannel(m.config.Channels.DingTalk, m.bus)
|
dingtalk, err := NewDingTalkChannel(m.config.Channels.DingTalk, m.bus)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.ErrorCF("channels", "Failed to initialize DingTalk channel", map[string]interface{}{
|
logger.ErrorCF("channels", "Failed to initialize DingTalk channel", map[string]any{
|
||||||
"error": err.Error(),
|
"error": err.Error(),
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -141,7 +141,7 @@ func (m *Manager) initChannels() error {
|
||||||
logger.DebugC("channels", "Attempting to initialize Slack channel")
|
logger.DebugC("channels", "Attempting to initialize Slack channel")
|
||||||
slackCh, err := NewSlackChannel(m.config.Channels.Slack, m.bus)
|
slackCh, err := NewSlackChannel(m.config.Channels.Slack, m.bus)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.ErrorCF("channels", "Failed to initialize Slack channel", map[string]interface{}{
|
logger.ErrorCF("channels", "Failed to initialize Slack channel", map[string]any{
|
||||||
"error": err.Error(),
|
"error": err.Error(),
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -154,7 +154,7 @@ func (m *Manager) initChannels() error {
|
||||||
logger.DebugC("channels", "Attempting to initialize LINE channel")
|
logger.DebugC("channels", "Attempting to initialize LINE channel")
|
||||||
line, err := NewLINEChannel(m.config.Channels.LINE, m.bus)
|
line, err := NewLINEChannel(m.config.Channels.LINE, m.bus)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.ErrorCF("channels", "Failed to initialize LINE channel", map[string]interface{}{
|
logger.ErrorCF("channels", "Failed to initialize LINE channel", map[string]any{
|
||||||
"error": err.Error(),
|
"error": err.Error(),
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -167,7 +167,7 @@ func (m *Manager) initChannels() error {
|
||||||
logger.DebugC("channels", "Attempting to initialize OneBot channel")
|
logger.DebugC("channels", "Attempting to initialize OneBot channel")
|
||||||
onebot, err := NewOneBotChannel(m.config.Channels.OneBot, m.bus)
|
onebot, err := NewOneBotChannel(m.config.Channels.OneBot, m.bus)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.ErrorCF("channels", "Failed to initialize OneBot channel", map[string]interface{}{
|
logger.ErrorCF("channels", "Failed to initialize OneBot channel", map[string]any{
|
||||||
"error": err.Error(),
|
"error": err.Error(),
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -176,7 +176,7 @@ func (m *Manager) initChannels() error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.InfoCF("channels", "Channel initialization completed", map[string]interface{}{
|
logger.InfoCF("channels", "Channel initialization completed", map[string]any{
|
||||||
"enabled_channels": len(m.channels),
|
"enabled_channels": len(m.channels),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -200,11 +200,11 @@ func (m *Manager) StartAll(ctx context.Context) error {
|
||||||
go m.dispatchOutbound(dispatchCtx)
|
go m.dispatchOutbound(dispatchCtx)
|
||||||
|
|
||||||
for name, channel := range m.channels {
|
for name, channel := range m.channels {
|
||||||
logger.InfoCF("channels", "Starting channel", map[string]interface{}{
|
logger.InfoCF("channels", "Starting channel", map[string]any{
|
||||||
"channel": name,
|
"channel": name,
|
||||||
})
|
})
|
||||||
if err := channel.Start(ctx); err != nil {
|
if err := channel.Start(ctx); err != nil {
|
||||||
logger.ErrorCF("channels", "Failed to start channel", map[string]interface{}{
|
logger.ErrorCF("channels", "Failed to start channel", map[string]any{
|
||||||
"channel": name,
|
"channel": name,
|
||||||
"error": err.Error(),
|
"error": err.Error(),
|
||||||
})
|
})
|
||||||
|
|
@ -227,11 +227,11 @@ func (m *Manager) StopAll(ctx context.Context) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
for name, channel := range m.channels {
|
for name, channel := range m.channels {
|
||||||
logger.InfoCF("channels", "Stopping channel", map[string]interface{}{
|
logger.InfoCF("channels", "Stopping channel", map[string]any{
|
||||||
"channel": name,
|
"channel": name,
|
||||||
})
|
})
|
||||||
if err := channel.Stop(ctx); err != nil {
|
if err := channel.Stop(ctx); err != nil {
|
||||||
logger.ErrorCF("channels", "Error stopping channel", map[string]interface{}{
|
logger.ErrorCF("channels", "Error stopping channel", map[string]any{
|
||||||
"channel": name,
|
"channel": name,
|
||||||
"error": err.Error(),
|
"error": err.Error(),
|
||||||
})
|
})
|
||||||
|
|
@ -266,14 +266,14 @@ func (m *Manager) dispatchOutbound(ctx context.Context) {
|
||||||
m.mu.RUnlock()
|
m.mu.RUnlock()
|
||||||
|
|
||||||
if !exists {
|
if !exists {
|
||||||
logger.WarnCF("channels", "Unknown channel for outbound message", map[string]interface{}{
|
logger.WarnCF("channels", "Unknown channel for outbound message", map[string]any{
|
||||||
"channel": msg.Channel,
|
"channel": msg.Channel,
|
||||||
})
|
})
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := channel.Send(ctx, msg); err != nil {
|
if err := channel.Send(ctx, msg); err != nil {
|
||||||
logger.ErrorCF("channels", "Error sending message to channel", map[string]interface{}{
|
logger.ErrorCF("channels", "Error sending message to channel", map[string]any{
|
||||||
"channel": msg.Channel,
|
"channel": msg.Channel,
|
||||||
"error": err.Error(),
|
"error": err.Error(),
|
||||||
})
|
})
|
||||||
|
|
@ -289,13 +289,13 @@ func (m *Manager) GetChannel(name string) (Channel, bool) {
|
||||||
return channel, ok
|
return channel, ok
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) GetStatus() map[string]interface{} {
|
func (m *Manager) GetStatus() map[string]any {
|
||||||
m.mu.RLock()
|
m.mu.RLock()
|
||||||
defer m.mu.RUnlock()
|
defer m.mu.RUnlock()
|
||||||
|
|
||||||
status := make(map[string]interface{})
|
status := make(map[string]any)
|
||||||
for name, channel := range m.channels {
|
for name, channel := range m.channels {
|
||||||
status[name] = map[string]interface{}{
|
status[name] = map[string]any{
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"running": channel.IsRunning(),
|
"running": channel.IsRunning(),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -76,9 +76,9 @@ type oneBotEvent struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type oneBotAPIRequest struct {
|
type oneBotAPIRequest struct {
|
||||||
Action string `json:"action"`
|
Action string `json:"action"`
|
||||||
Params interface{} `json:"params"`
|
Params any `json:"params"`
|
||||||
Echo string `json:"echo,omitempty"`
|
Echo string `json:"echo,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type oneBotSendPrivateMsgParams struct {
|
type oneBotSendPrivateMsgParams struct {
|
||||||
|
|
@ -109,14 +109,14 @@ func (c *OneBotChannel) Start(ctx context.Context) error {
|
||||||
return fmt.Errorf("OneBot ws_url not configured")
|
return fmt.Errorf("OneBot ws_url not configured")
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.InfoCF("onebot", "Starting OneBot channel", map[string]interface{}{
|
logger.InfoCF("onebot", "Starting OneBot channel", map[string]any{
|
||||||
"ws_url": c.config.WSUrl,
|
"ws_url": c.config.WSUrl,
|
||||||
})
|
})
|
||||||
|
|
||||||
c.ctx, c.cancel = context.WithCancel(ctx)
|
c.ctx, c.cancel = context.WithCancel(ctx)
|
||||||
|
|
||||||
if err := c.connect(); err != nil {
|
if err := c.connect(); err != nil {
|
||||||
logger.WarnCF("onebot", "Initial connection failed, will retry in background", map[string]interface{}{
|
logger.WarnCF("onebot", "Initial connection failed, will retry in background", map[string]any{
|
||||||
"error": err.Error(),
|
"error": err.Error(),
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -178,7 +178,7 @@ func (c *OneBotChannel) reconnectLoop() {
|
||||||
if conn == nil {
|
if conn == nil {
|
||||||
logger.InfoC("onebot", "Attempting to reconnect...")
|
logger.InfoC("onebot", "Attempting to reconnect...")
|
||||||
if err := c.connect(); err != nil {
|
if err := c.connect(); err != nil {
|
||||||
logger.ErrorCF("onebot", "Reconnect failed", map[string]interface{}{
|
logger.ErrorCF("onebot", "Reconnect failed", map[string]any{
|
||||||
"error": err.Error(),
|
"error": err.Error(),
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -246,7 +246,7 @@ func (c *OneBotChannel) Send(ctx context.Context, msg bus.OutboundMessage) error
|
||||||
c.writeMu.Unlock()
|
c.writeMu.Unlock()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.ErrorCF("onebot", "Failed to send message", map[string]interface{}{
|
logger.ErrorCF("onebot", "Failed to send message", map[string]any{
|
||||||
"error": err.Error(),
|
"error": err.Error(),
|
||||||
})
|
})
|
||||||
return err
|
return err
|
||||||
|
|
@ -255,7 +255,7 @@ func (c *OneBotChannel) Send(ctx context.Context, msg bus.OutboundMessage) error
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *OneBotChannel) buildSendRequest(msg bus.OutboundMessage) (string, interface{}, error) {
|
func (c *OneBotChannel) buildSendRequest(msg bus.OutboundMessage) (string, any, error) {
|
||||||
chatID := msg.ChatID
|
chatID := msg.ChatID
|
||||||
|
|
||||||
if len(chatID) > 6 && chatID[:6] == "group:" {
|
if len(chatID) > 6 && chatID[:6] == "group:" {
|
||||||
|
|
@ -308,7 +308,7 @@ func (c *OneBotChannel) listen() {
|
||||||
|
|
||||||
_, message, err := conn.ReadMessage()
|
_, message, err := conn.ReadMessage()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.ErrorCF("onebot", "WebSocket read error", map[string]interface{}{
|
logger.ErrorCF("onebot", "WebSocket read error", map[string]any{
|
||||||
"error": err.Error(),
|
"error": err.Error(),
|
||||||
})
|
})
|
||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
|
|
@ -320,14 +320,14 @@ func (c *OneBotChannel) listen() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.DebugCF("onebot", "Raw WebSocket message received", map[string]interface{}{
|
logger.DebugCF("onebot", "Raw WebSocket message received", map[string]any{
|
||||||
"length": len(message),
|
"length": len(message),
|
||||||
"payload": string(message),
|
"payload": string(message),
|
||||||
})
|
})
|
||||||
|
|
||||||
var raw oneBotRawEvent
|
var raw oneBotRawEvent
|
||||||
if err := json.Unmarshal(message, &raw); err != nil {
|
if err := json.Unmarshal(message, &raw); err != nil {
|
||||||
logger.WarnCF("onebot", "Failed to unmarshal raw event", map[string]interface{}{
|
logger.WarnCF("onebot", "Failed to unmarshal raw event", map[string]any{
|
||||||
"error": err.Error(),
|
"error": err.Error(),
|
||||||
"payload": string(message),
|
"payload": string(message),
|
||||||
})
|
})
|
||||||
|
|
@ -335,14 +335,14 @@ func (c *OneBotChannel) listen() {
|
||||||
}
|
}
|
||||||
|
|
||||||
if raw.Echo != "" || raw.Status.Online || raw.Status.Good {
|
if raw.Echo != "" || raw.Status.Online || raw.Status.Good {
|
||||||
logger.DebugCF("onebot", "Received API response, skipping", map[string]interface{}{
|
logger.DebugCF("onebot", "Received API response, skipping", map[string]any{
|
||||||
"echo": raw.Echo,
|
"echo": raw.Echo,
|
||||||
"status": raw.Status,
|
"status": raw.Status,
|
||||||
})
|
})
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.DebugCF("onebot", "Parsed raw event", map[string]interface{}{
|
logger.DebugCF("onebot", "Parsed raw event", map[string]any{
|
||||||
"post_type": raw.PostType,
|
"post_type": raw.PostType,
|
||||||
"message_type": raw.MessageType,
|
"message_type": raw.MessageType,
|
||||||
"sub_type": raw.SubType,
|
"sub_type": raw.SubType,
|
||||||
|
|
@ -407,14 +407,14 @@ func parseMessageContentEx(raw json.RawMessage, selfID int64) parseMessageResult
|
||||||
return parseMessageResult{Text: s, IsBotMentioned: mentioned}
|
return parseMessageResult{Text: s, IsBotMentioned: mentioned}
|
||||||
}
|
}
|
||||||
|
|
||||||
var segments []map[string]interface{}
|
var segments []map[string]any
|
||||||
if err := json.Unmarshal(raw, &segments); err == nil {
|
if err := json.Unmarshal(raw, &segments); err == nil {
|
||||||
var text string
|
var text string
|
||||||
mentioned := false
|
mentioned := false
|
||||||
selfIDStr := strconv.FormatInt(selfID, 10)
|
selfIDStr := strconv.FormatInt(selfID, 10)
|
||||||
for _, seg := range segments {
|
for _, seg := range segments {
|
||||||
segType, _ := seg["type"].(string)
|
segType, _ := seg["type"].(string)
|
||||||
data, _ := seg["data"].(map[string]interface{})
|
data, _ := seg["data"].(map[string]any)
|
||||||
switch segType {
|
switch segType {
|
||||||
case "text":
|
case "text":
|
||||||
if data != nil {
|
if data != nil {
|
||||||
|
|
@ -441,7 +441,7 @@ func (c *OneBotChannel) handleRawEvent(raw *oneBotRawEvent) {
|
||||||
case "message":
|
case "message":
|
||||||
evt, err := c.normalizeMessageEvent(raw)
|
evt, err := c.normalizeMessageEvent(raw)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.WarnCF("onebot", "Failed to normalize message event", map[string]interface{}{
|
logger.WarnCF("onebot", "Failed to normalize message event", map[string]any{
|
||||||
"error": err.Error(),
|
"error": err.Error(),
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
|
|
@ -450,20 +450,20 @@ func (c *OneBotChannel) handleRawEvent(raw *oneBotRawEvent) {
|
||||||
case "meta_event":
|
case "meta_event":
|
||||||
c.handleMetaEvent(raw)
|
c.handleMetaEvent(raw)
|
||||||
case "notice":
|
case "notice":
|
||||||
logger.DebugCF("onebot", "Notice event received", map[string]interface{}{
|
logger.DebugCF("onebot", "Notice event received", map[string]any{
|
||||||
"sub_type": raw.SubType,
|
"sub_type": raw.SubType,
|
||||||
})
|
})
|
||||||
case "request":
|
case "request":
|
||||||
logger.DebugCF("onebot", "Request event received", map[string]interface{}{
|
logger.DebugCF("onebot", "Request event received", map[string]any{
|
||||||
"sub_type": raw.SubType,
|
"sub_type": raw.SubType,
|
||||||
})
|
})
|
||||||
case "":
|
case "":
|
||||||
logger.DebugCF("onebot", "Event with empty post_type (possibly API response)", map[string]interface{}{
|
logger.DebugCF("onebot", "Event with empty post_type (possibly API response)", map[string]any{
|
||||||
"echo": raw.Echo,
|
"echo": raw.Echo,
|
||||||
"status": raw.Status,
|
"status": raw.Status,
|
||||||
})
|
})
|
||||||
default:
|
default:
|
||||||
logger.DebugCF("onebot", "Unknown post_type", map[string]interface{}{
|
logger.DebugCF("onebot", "Unknown post_type", map[string]any{
|
||||||
"post_type": raw.PostType,
|
"post_type": raw.PostType,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -498,14 +498,14 @@ func (c *OneBotChannel) normalizeMessageEvent(raw *oneBotRawEvent) (*oneBotEvent
|
||||||
var sender oneBotSender
|
var sender oneBotSender
|
||||||
if len(raw.Sender) > 0 {
|
if len(raw.Sender) > 0 {
|
||||||
if err := json.Unmarshal(raw.Sender, &sender); err != nil {
|
if err := json.Unmarshal(raw.Sender, &sender); err != nil {
|
||||||
logger.WarnCF("onebot", "Failed to parse sender", map[string]interface{}{
|
logger.WarnCF("onebot", "Failed to parse sender", map[string]any{
|
||||||
"error": err.Error(),
|
"error": err.Error(),
|
||||||
"sender": string(raw.Sender),
|
"sender": string(raw.Sender),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.DebugCF("onebot", "Normalized message event", map[string]interface{}{
|
logger.DebugCF("onebot", "Normalized message event", map[string]any{
|
||||||
"message_type": raw.MessageType,
|
"message_type": raw.MessageType,
|
||||||
"user_id": userID,
|
"user_id": userID,
|
||||||
"group_id": groupID,
|
"group_id": groupID,
|
||||||
|
|
@ -534,13 +534,13 @@ func (c *OneBotChannel) normalizeMessageEvent(raw *oneBotRawEvent) (*oneBotEvent
|
||||||
func (c *OneBotChannel) handleMetaEvent(raw *oneBotRawEvent) {
|
func (c *OneBotChannel) handleMetaEvent(raw *oneBotRawEvent) {
|
||||||
switch raw.MetaEventType {
|
switch raw.MetaEventType {
|
||||||
case "lifecycle":
|
case "lifecycle":
|
||||||
logger.InfoCF("onebot", "Lifecycle event", map[string]interface{}{
|
logger.InfoCF("onebot", "Lifecycle event", map[string]any{
|
||||||
"sub_type": raw.SubType,
|
"sub_type": raw.SubType,
|
||||||
})
|
})
|
||||||
case "heartbeat":
|
case "heartbeat":
|
||||||
logger.DebugC("onebot", "Heartbeat received")
|
logger.DebugC("onebot", "Heartbeat received")
|
||||||
default:
|
default:
|
||||||
logger.DebugCF("onebot", "Unknown meta_event_type", map[string]interface{}{
|
logger.DebugCF("onebot", "Unknown meta_event_type", map[string]any{
|
||||||
"meta_event_type": raw.MetaEventType,
|
"meta_event_type": raw.MetaEventType,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -548,7 +548,7 @@ func (c *OneBotChannel) handleMetaEvent(raw *oneBotRawEvent) {
|
||||||
|
|
||||||
func (c *OneBotChannel) handleMessage(evt *oneBotEvent) {
|
func (c *OneBotChannel) handleMessage(evt *oneBotEvent) {
|
||||||
if c.isDuplicate(evt.MessageID) {
|
if c.isDuplicate(evt.MessageID) {
|
||||||
logger.DebugCF("onebot", "Duplicate message, skipping", map[string]interface{}{
|
logger.DebugCF("onebot", "Duplicate message, skipping", map[string]any{
|
||||||
"message_id": evt.MessageID,
|
"message_id": evt.MessageID,
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
|
|
@ -556,7 +556,7 @@ func (c *OneBotChannel) handleMessage(evt *oneBotEvent) {
|
||||||
|
|
||||||
content := evt.Content
|
content := evt.Content
|
||||||
if content == "" {
|
if content == "" {
|
||||||
logger.DebugCF("onebot", "Received empty message, ignoring", map[string]interface{}{
|
logger.DebugCF("onebot", "Received empty message, ignoring", map[string]any{
|
||||||
"message_id": evt.MessageID,
|
"message_id": evt.MessageID,
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
|
|
@ -572,7 +572,7 @@ func (c *OneBotChannel) handleMessage(evt *oneBotEvent) {
|
||||||
switch evt.MessageType {
|
switch evt.MessageType {
|
||||||
case "private":
|
case "private":
|
||||||
chatID = "private:" + senderID
|
chatID = "private:" + senderID
|
||||||
logger.InfoCF("onebot", "Received private message", map[string]interface{}{
|
logger.InfoCF("onebot", "Received private message", map[string]any{
|
||||||
"sender": senderID,
|
"sender": senderID,
|
||||||
"message_id": evt.MessageID,
|
"message_id": evt.MessageID,
|
||||||
"length": len(content),
|
"length": len(content),
|
||||||
|
|
@ -597,7 +597,7 @@ func (c *OneBotChannel) handleMessage(evt *oneBotEvent) {
|
||||||
|
|
||||||
triggered, strippedContent := c.checkGroupTrigger(content, evt.IsBotMentioned)
|
triggered, strippedContent := c.checkGroupTrigger(content, evt.IsBotMentioned)
|
||||||
if !triggered {
|
if !triggered {
|
||||||
logger.DebugCF("onebot", "Group message ignored (no trigger)", map[string]interface{}{
|
logger.DebugCF("onebot", "Group message ignored (no trigger)", map[string]any{
|
||||||
"sender": senderID,
|
"sender": senderID,
|
||||||
"group": groupIDStr,
|
"group": groupIDStr,
|
||||||
"is_mentioned": evt.IsBotMentioned,
|
"is_mentioned": evt.IsBotMentioned,
|
||||||
|
|
@ -607,7 +607,7 @@ func (c *OneBotChannel) handleMessage(evt *oneBotEvent) {
|
||||||
}
|
}
|
||||||
content = strippedContent
|
content = strippedContent
|
||||||
|
|
||||||
logger.InfoCF("onebot", "Received group message", map[string]interface{}{
|
logger.InfoCF("onebot", "Received group message", map[string]any{
|
||||||
"sender": senderID,
|
"sender": senderID,
|
||||||
"group": groupIDStr,
|
"group": groupIDStr,
|
||||||
"message_id": evt.MessageID,
|
"message_id": evt.MessageID,
|
||||||
|
|
@ -617,7 +617,7 @@ func (c *OneBotChannel) handleMessage(evt *oneBotEvent) {
|
||||||
})
|
})
|
||||||
|
|
||||||
default:
|
default:
|
||||||
logger.WarnCF("onebot", "Unknown message type, cannot route", map[string]interface{}{
|
logger.WarnCF("onebot", "Unknown message type, cannot route", map[string]any{
|
||||||
"type": evt.MessageType,
|
"type": evt.MessageType,
|
||||||
"message_id": evt.MessageID,
|
"message_id": evt.MessageID,
|
||||||
"user_id": evt.UserID,
|
"user_id": evt.UserID,
|
||||||
|
|
@ -629,7 +629,7 @@ func (c *OneBotChannel) handleMessage(evt *oneBotEvent) {
|
||||||
metadata["nickname"] = evt.Sender.Nickname
|
metadata["nickname"] = evt.Sender.Nickname
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.DebugCF("onebot", "Forwarding message to bus", map[string]interface{}{
|
logger.DebugCF("onebot", "Forwarding message to bus", map[string]any{
|
||||||
"sender_id": senderID,
|
"sender_id": senderID,
|
||||||
"chat_id": chatID,
|
"chat_id": chatID,
|
||||||
"content": truncate(content, 100),
|
"content": truncate(content, 100),
|
||||||
|
|
@ -668,7 +668,10 @@ func truncate(s string, n int) string {
|
||||||
return string(runes[:n]) + "..."
|
return string(runes[:n]) + "..."
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *OneBotChannel) checkGroupTrigger(content string, isBotMentioned bool) (triggered bool, strippedContent string) {
|
func (c *OneBotChannel) checkGroupTrigger(
|
||||||
|
content string,
|
||||||
|
isBotMentioned bool,
|
||||||
|
) (triggered bool, strippedContent string) {
|
||||||
if isBotMentioned {
|
if isBotMentioned {
|
||||||
return true, strings.TrimSpace(content)
|
return true, strings.TrimSpace(content)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -77,7 +77,7 @@ func (c *QQChannel) Start(ctx context.Context) error {
|
||||||
return fmt.Errorf("failed to get websocket info: %w", err)
|
return fmt.Errorf("failed to get websocket info: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.InfoCF("qq", "Got WebSocket info", map[string]interface{}{
|
logger.InfoCF("qq", "Got WebSocket info", map[string]any{
|
||||||
"shards": wsInfo.Shards,
|
"shards": wsInfo.Shards,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -87,7 +87,7 @@ func (c *QQChannel) Start(ctx context.Context) error {
|
||||||
// 在 goroutine 中启动 WebSocket 连接,避免阻塞
|
// 在 goroutine 中启动 WebSocket 连接,避免阻塞
|
||||||
go func() {
|
go func() {
|
||||||
if err := c.sessionManager.Start(wsInfo, c.tokenSource, &intent); err != nil {
|
if err := c.sessionManager.Start(wsInfo, c.tokenSource, &intent); err != nil {
|
||||||
logger.ErrorCF("qq", "WebSocket session error", map[string]interface{}{
|
logger.ErrorCF("qq", "WebSocket session error", map[string]any{
|
||||||
"error": err.Error(),
|
"error": err.Error(),
|
||||||
})
|
})
|
||||||
c.setRunning(false)
|
c.setRunning(false)
|
||||||
|
|
@ -124,7 +124,7 @@ func (c *QQChannel) Send(ctx context.Context, msg bus.OutboundMessage) error {
|
||||||
// C2C 消息发送
|
// C2C 消息发送
|
||||||
_, err := c.api.PostC2CMessage(ctx, msg.ChatID, msgToCreate)
|
_, err := c.api.PostC2CMessage(ctx, msg.ChatID, msgToCreate)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.ErrorCF("qq", "Failed to send C2C message", map[string]interface{}{
|
logger.ErrorCF("qq", "Failed to send C2C message", map[string]any{
|
||||||
"error": err.Error(),
|
"error": err.Error(),
|
||||||
})
|
})
|
||||||
return err
|
return err
|
||||||
|
|
@ -157,7 +157,7 @@ func (c *QQChannel) handleC2CMessage() event.C2CMessageEventHandler {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.InfoCF("qq", "Received C2C message", map[string]interface{}{
|
logger.InfoCF("qq", "Received C2C message", map[string]any{
|
||||||
"sender": senderID,
|
"sender": senderID,
|
||||||
"length": len(content),
|
"length": len(content),
|
||||||
})
|
})
|
||||||
|
|
@ -197,7 +197,7 @@ func (c *QQChannel) handleGroupATMessage() event.GroupATMessageEventHandler {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.InfoCF("qq", "Received group AT message", map[string]interface{}{
|
logger.InfoCF("qq", "Received group AT message", map[string]any{
|
||||||
"sender": senderID,
|
"sender": senderID,
|
||||||
"group": data.GroupID,
|
"group": data.GroupID,
|
||||||
"length": len(content),
|
"length": len(content),
|
||||||
|
|
|
||||||
|
|
@ -75,7 +75,7 @@ func (c *SlackChannel) Start(ctx context.Context) error {
|
||||||
c.botUserID = authResp.UserID
|
c.botUserID = authResp.UserID
|
||||||
c.teamID = authResp.TeamID
|
c.teamID = authResp.TeamID
|
||||||
|
|
||||||
logger.InfoCF("slack", "Slack bot connected", map[string]interface{}{
|
logger.InfoCF("slack", "Slack bot connected", map[string]any{
|
||||||
"bot_user_id": c.botUserID,
|
"bot_user_id": c.botUserID,
|
||||||
"team": authResp.Team,
|
"team": authResp.Team,
|
||||||
})
|
})
|
||||||
|
|
@ -85,7 +85,7 @@ func (c *SlackChannel) Start(ctx context.Context) error {
|
||||||
go func() {
|
go func() {
|
||||||
if err := c.socketClient.RunContext(c.ctx); err != nil {
|
if err := c.socketClient.RunContext(c.ctx); err != nil {
|
||||||
if c.ctx.Err() == nil {
|
if c.ctx.Err() == nil {
|
||||||
logger.ErrorCF("slack", "Socket Mode connection error", map[string]interface{}{
|
logger.ErrorCF("slack", "Socket Mode connection error", map[string]any{
|
||||||
"error": err.Error(),
|
"error": err.Error(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -140,7 +140,7 @@ func (c *SlackChannel) Send(ctx context.Context, msg bus.OutboundMessage) error
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.DebugCF("slack", "Message sent", map[string]interface{}{
|
logger.DebugCF("slack", "Message sent", map[string]any{
|
||||||
"channel_id": channelID,
|
"channel_id": channelID,
|
||||||
"thread_ts": threadTS,
|
"thread_ts": threadTS,
|
||||||
})
|
})
|
||||||
|
|
@ -202,7 +202,7 @@ func (c *SlackChannel) handleMessageEvent(ev *slackevents.MessageEvent) {
|
||||||
|
|
||||||
// 检查白名单,避免为被拒绝的用户下载附件
|
// 检查白名单,避免为被拒绝的用户下载附件
|
||||||
if !c.IsAllowed(ev.User) {
|
if !c.IsAllowed(ev.User) {
|
||||||
logger.DebugCF("slack", "Message rejected by allowlist", map[string]interface{}{
|
logger.DebugCF("slack", "Message rejected by allowlist", map[string]any{
|
||||||
"user_id": ev.User,
|
"user_id": ev.User,
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
|
|
@ -238,7 +238,7 @@ func (c *SlackChannel) handleMessageEvent(ev *slackevents.MessageEvent) {
|
||||||
defer func() {
|
defer func() {
|
||||||
for _, file := range localFiles {
|
for _, file := range localFiles {
|
||||||
if err := os.Remove(file); err != nil {
|
if err := os.Remove(file); err != nil {
|
||||||
logger.DebugCF("slack", "Failed to cleanup temp file", map[string]interface{}{
|
logger.DebugCF("slack", "Failed to cleanup temp file", map[string]any{
|
||||||
"file": file,
|
"file": file,
|
||||||
"error": err.Error(),
|
"error": err.Error(),
|
||||||
})
|
})
|
||||||
|
|
@ -261,7 +261,7 @@ func (c *SlackChannel) handleMessageEvent(ev *slackevents.MessageEvent) {
|
||||||
result, err := c.transcriber.Transcribe(ctx, localPath)
|
result, err := c.transcriber.Transcribe(ctx, localPath)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.ErrorCF("slack", "Voice transcription failed", map[string]interface{}{"error": err.Error()})
|
logger.ErrorCF("slack", "Voice transcription failed", map[string]any{"error": err.Error()})
|
||||||
content += fmt.Sprintf("\n[audio: %s (transcription failed)]", file.Name)
|
content += fmt.Sprintf("\n[audio: %s (transcription failed)]", file.Name)
|
||||||
} else {
|
} else {
|
||||||
content += fmt.Sprintf("\n[voice transcription: %s]", result.Text)
|
content += fmt.Sprintf("\n[voice transcription: %s]", result.Text)
|
||||||
|
|
@ -293,7 +293,7 @@ func (c *SlackChannel) handleMessageEvent(ev *slackevents.MessageEvent) {
|
||||||
"team_id": c.teamID,
|
"team_id": c.teamID,
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.DebugCF("slack", "Received message", map[string]interface{}{
|
logger.DebugCF("slack", "Received message", map[string]any{
|
||||||
"sender_id": senderID,
|
"sender_id": senderID,
|
||||||
"chat_id": chatID,
|
"chat_id": chatID,
|
||||||
"preview": utils.Truncate(content, 50),
|
"preview": utils.Truncate(content, 50),
|
||||||
|
|
@ -309,7 +309,7 @@ func (c *SlackChannel) handleAppMention(ev *slackevents.AppMentionEvent) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if !c.IsAllowed(ev.User) {
|
if !c.IsAllowed(ev.User) {
|
||||||
logger.DebugCF("slack", "Mention rejected by allowlist", map[string]interface{}{
|
logger.DebugCF("slack", "Mention rejected by allowlist", map[string]any{
|
||||||
"user_id": ev.User,
|
"user_id": ev.User,
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
|
|
@ -375,7 +375,7 @@ func (c *SlackChannel) handleSlashCommand(event socketmode.Event) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if !c.IsAllowed(cmd.UserID) {
|
if !c.IsAllowed(cmd.UserID) {
|
||||||
logger.DebugCF("slack", "Slash command rejected by allowlist", map[string]interface{}{
|
logger.DebugCF("slack", "Slash command rejected by allowlist", map[string]any{
|
||||||
"user_id": cmd.UserID,
|
"user_id": cmd.UserID,
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
|
|
@ -400,7 +400,7 @@ func (c *SlackChannel) handleSlashCommand(event socketmode.Event) {
|
||||||
"team_id": c.teamID,
|
"team_id": c.teamID,
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.DebugCF("slack", "Slash command received", map[string]interface{}{
|
logger.DebugCF("slack", "Slash command received", map[string]any{
|
||||||
"sender_id": senderID,
|
"sender_id": senderID,
|
||||||
"command": cmd.Command,
|
"command": cmd.Command,
|
||||||
"text": utils.Truncate(content, 50),
|
"text": utils.Truncate(content, 50),
|
||||||
|
|
@ -415,7 +415,7 @@ func (c *SlackChannel) downloadSlackFile(file slack.File) string {
|
||||||
downloadURL = file.URLPrivate
|
downloadURL = file.URLPrivate
|
||||||
}
|
}
|
||||||
if downloadURL == "" {
|
if downloadURL == "" {
|
||||||
logger.ErrorCF("slack", "No download URL for file", map[string]interface{}{"file_id": file.ID})
|
logger.ErrorCF("slack", "No download URL for file", map[string]any{"file_id": file.ID})
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,10 +11,9 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
th "github.com/mymmrac/telego/telegohandler"
|
|
||||||
|
|
||||||
"github.com/mymmrac/telego"
|
"github.com/mymmrac/telego"
|
||||||
"github.com/mymmrac/telego/telegohandler"
|
"github.com/mymmrac/telego/telegohandler"
|
||||||
|
th "github.com/mymmrac/telego/telegohandler"
|
||||||
tu "github.com/mymmrac/telego/telegoutil"
|
tu "github.com/mymmrac/telego/telegoutil"
|
||||||
|
|
||||||
"github.com/sipeed/picoclaw/pkg/bus"
|
"github.com/sipeed/picoclaw/pkg/bus"
|
||||||
|
|
@ -120,7 +119,7 @@ func (c *TelegramChannel) Start(ctx context.Context) error {
|
||||||
}, th.AnyMessage())
|
}, th.AnyMessage())
|
||||||
|
|
||||||
c.setRunning(true)
|
c.setRunning(true)
|
||||||
logger.InfoCF("telegram", "Telegram bot connected", map[string]interface{}{
|
logger.InfoCF("telegram", "Telegram bot connected", map[string]any{
|
||||||
"username": c.bot.Username(),
|
"username": c.bot.Username(),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -133,6 +132,7 @@ func (c *TelegramChannel) Start(ctx context.Context) error {
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *TelegramChannel) Stop(ctx context.Context) error {
|
func (c *TelegramChannel) Stop(ctx context.Context) error {
|
||||||
logger.InfoC("telegram", "Stopping Telegram bot...")
|
logger.InfoC("telegram", "Stopping Telegram bot...")
|
||||||
c.setRunning(false)
|
c.setRunning(false)
|
||||||
|
|
@ -175,7 +175,7 @@ func (c *TelegramChannel) Send(ctx context.Context, msg bus.OutboundMessage) err
|
||||||
tgMsg.ParseMode = telego.ModeHTML
|
tgMsg.ParseMode = telego.ModeHTML
|
||||||
|
|
||||||
if _, err = c.bot.SendMessage(ctx, tgMsg); err != nil {
|
if _, err = c.bot.SendMessage(ctx, tgMsg); err != nil {
|
||||||
logger.ErrorCF("telegram", "HTML parse failed, falling back to plain text", map[string]interface{}{
|
logger.ErrorCF("telegram", "HTML parse failed, falling back to plain text", map[string]any{
|
||||||
"error": err.Error(),
|
"error": err.Error(),
|
||||||
})
|
})
|
||||||
tgMsg.ParseMode = ""
|
tgMsg.ParseMode = ""
|
||||||
|
|
@ -203,7 +203,7 @@ func (c *TelegramChannel) handleMessage(ctx context.Context, message *telego.Mes
|
||||||
|
|
||||||
// 检查白名单,避免为被拒绝的用户下载附件
|
// 检查白名单,避免为被拒绝的用户下载附件
|
||||||
if !c.IsAllowed(senderID) {
|
if !c.IsAllowed(senderID) {
|
||||||
logger.DebugCF("telegram", "Message rejected by allowlist", map[string]interface{}{
|
logger.DebugCF("telegram", "Message rejected by allowlist", map[string]any{
|
||||||
"user_id": senderID,
|
"user_id": senderID,
|
||||||
})
|
})
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -220,7 +220,7 @@ func (c *TelegramChannel) handleMessage(ctx context.Context, message *telego.Mes
|
||||||
defer func() {
|
defer func() {
|
||||||
for _, file := range localFiles {
|
for _, file := range localFiles {
|
||||||
if err := os.Remove(file); err != nil {
|
if err := os.Remove(file); err != nil {
|
||||||
logger.DebugCF("telegram", "Failed to cleanup temp file", map[string]interface{}{
|
logger.DebugCF("telegram", "Failed to cleanup temp file", map[string]any{
|
||||||
"file": file,
|
"file": file,
|
||||||
"error": err.Error(),
|
"error": err.Error(),
|
||||||
})
|
})
|
||||||
|
|
@ -265,14 +265,14 @@ func (c *TelegramChannel) handleMessage(ctx context.Context, message *telego.Mes
|
||||||
|
|
||||||
result, err := c.transcriber.Transcribe(ctx, voicePath)
|
result, err := c.transcriber.Transcribe(ctx, voicePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.ErrorCF("telegram", "Voice transcription failed", map[string]interface{}{
|
logger.ErrorCF("telegram", "Voice transcription failed", map[string]any{
|
||||||
"error": err.Error(),
|
"error": err.Error(),
|
||||||
"path": voicePath,
|
"path": voicePath,
|
||||||
})
|
})
|
||||||
transcribedText = "[voice (transcription failed)]"
|
transcribedText = "[voice (transcription failed)]"
|
||||||
} else {
|
} else {
|
||||||
transcribedText = fmt.Sprintf("[voice transcription: %s]", result.Text)
|
transcribedText = fmt.Sprintf("[voice transcription: %s]", result.Text)
|
||||||
logger.InfoCF("telegram", "Voice transcribed successfully", map[string]interface{}{
|
logger.InfoCF("telegram", "Voice transcribed successfully", map[string]any{
|
||||||
"text": result.Text,
|
"text": result.Text,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -315,7 +315,7 @@ func (c *TelegramChannel) handleMessage(ctx context.Context, message *telego.Mes
|
||||||
content = "[empty message]"
|
content = "[empty message]"
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.DebugCF("telegram", "Received message", map[string]interface{}{
|
logger.DebugCF("telegram", "Received message", map[string]any{
|
||||||
"sender_id": senderID,
|
"sender_id": senderID,
|
||||||
"chat_id": fmt.Sprintf("%d", chatID),
|
"chat_id": fmt.Sprintf("%d", chatID),
|
||||||
"preview": utils.Truncate(content, 50),
|
"preview": utils.Truncate(content, 50),
|
||||||
|
|
@ -324,7 +324,7 @@ func (c *TelegramChannel) handleMessage(ctx context.Context, message *telego.Mes
|
||||||
// Thinking indicator
|
// Thinking indicator
|
||||||
err := c.bot.SendChatAction(ctx, tu.ChatAction(tu.ID(chatID), telego.ChatActionTyping))
|
err := c.bot.SendChatAction(ctx, tu.ChatAction(tu.ID(chatID), telego.ChatActionTyping))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.ErrorCF("telegram", "Failed to send chat action", map[string]interface{}{
|
logger.ErrorCF("telegram", "Failed to send chat action", map[string]any{
|
||||||
"error": err.Error(),
|
"error": err.Error(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -371,7 +371,7 @@ func (c *TelegramChannel) handleMessage(ctx context.Context, message *telego.Mes
|
||||||
func (c *TelegramChannel) downloadPhoto(ctx context.Context, fileID string) string {
|
func (c *TelegramChannel) downloadPhoto(ctx context.Context, fileID string) string {
|
||||||
file, err := c.bot.GetFile(ctx, &telego.GetFileParams{FileID: fileID})
|
file, err := c.bot.GetFile(ctx, &telego.GetFileParams{FileID: fileID})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.ErrorCF("telegram", "Failed to get photo file", map[string]interface{}{
|
logger.ErrorCF("telegram", "Failed to get photo file", map[string]any{
|
||||||
"error": err.Error(),
|
"error": err.Error(),
|
||||||
})
|
})
|
||||||
return ""
|
return ""
|
||||||
|
|
@ -386,7 +386,7 @@ func (c *TelegramChannel) downloadFileWithInfo(file *telego.File, ext string) st
|
||||||
}
|
}
|
||||||
|
|
||||||
url := c.bot.FileDownloadURL(file.FilePath)
|
url := c.bot.FileDownloadURL(file.FilePath)
|
||||||
logger.DebugCF("telegram", "File URL", map[string]interface{}{"url": url})
|
logger.DebugCF("telegram", "File URL", map[string]any{"url": url})
|
||||||
|
|
||||||
// Use FilePath as filename for better identification
|
// Use FilePath as filename for better identification
|
||||||
filename := file.FilePath + ext
|
filename := file.FilePath + ext
|
||||||
|
|
@ -398,7 +398,7 @@ func (c *TelegramChannel) downloadFileWithInfo(file *telego.File, ext string) st
|
||||||
func (c *TelegramChannel) downloadFile(ctx context.Context, fileID, ext string) string {
|
func (c *TelegramChannel) downloadFile(ctx context.Context, fileID, ext string) string {
|
||||||
file, err := c.bot.GetFile(ctx, &telego.GetFileParams{FileID: fileID})
|
file, err := c.bot.GetFile(ctx, &telego.GetFileParams{FileID: fileID})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.ErrorCF("telegram", "Failed to get file", map[string]interface{}{
|
logger.ErrorCF("telegram", "Failed to get file", map[string]any{
|
||||||
"error": err.Error(),
|
"error": err.Error(),
|
||||||
})
|
})
|
||||||
return ""
|
return ""
|
||||||
|
|
@ -456,7 +456,11 @@ func markdownToTelegramHTML(text string) string {
|
||||||
|
|
||||||
for i, code := range codeBlocks.codes {
|
for i, code := range codeBlocks.codes {
|
||||||
escaped := escapeHTML(code)
|
escaped := escapeHTML(code)
|
||||||
text = strings.ReplaceAll(text, fmt.Sprintf("\x00CB%d\x00", i), fmt.Sprintf("<pre><code>%s</code></pre>", escaped))
|
text = strings.ReplaceAll(
|
||||||
|
text,
|
||||||
|
fmt.Sprintf("\x00CB%d\x00", i),
|
||||||
|
fmt.Sprintf("<pre><code>%s</code></pre>", escaped),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return text
|
return text
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/mymmrac/telego"
|
"github.com/mymmrac/telego"
|
||||||
|
|
||||||
"github.com/sipeed/picoclaw/pkg/config"
|
"github.com/sipeed/picoclaw/pkg/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -35,6 +36,7 @@ func commandArgs(text string) string {
|
||||||
}
|
}
|
||||||
return strings.TrimSpace(parts[1])
|
return strings.TrimSpace(parts[1])
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *cmd) Help(ctx context.Context, message telego.Message) error {
|
func (c *cmd) Help(ctx context.Context, message telego.Message) error {
|
||||||
msg := `/start - Start the bot
|
msg := `/start - Start the bot
|
||||||
/help - Show this help message
|
/help - Show this help message
|
||||||
|
|
@ -96,6 +98,7 @@ func (c *cmd) Show(ctx context.Context, message telego.Message) error {
|
||||||
})
|
})
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *cmd) List(ctx context.Context, message telego.Message) error {
|
func (c *cmd) List(ctx context.Context, message telego.Message) error {
|
||||||
args := commandArgs(message.Text)
|
args := commandArgs(message.Text)
|
||||||
if args == "" {
|
if args == "" {
|
||||||
|
|
|
||||||
|
|
@ -86,7 +86,7 @@ func (c *WhatsAppChannel) Send(ctx context.Context, msg bus.OutboundMessage) err
|
||||||
return fmt.Errorf("whatsapp connection not established")
|
return fmt.Errorf("whatsapp connection not established")
|
||||||
}
|
}
|
||||||
|
|
||||||
payload := map[string]interface{}{
|
payload := map[string]any{
|
||||||
"type": "message",
|
"type": "message",
|
||||||
"to": msg.ChatID,
|
"to": msg.ChatID,
|
||||||
"content": msg.Content,
|
"content": msg.Content,
|
||||||
|
|
@ -126,7 +126,7 @@ func (c *WhatsAppChannel) listen(ctx context.Context) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
var msg map[string]interface{}
|
var msg map[string]any
|
||||||
if err := json.Unmarshal(message, &msg); err != nil {
|
if err := json.Unmarshal(message, &msg); err != nil {
|
||||||
log.Printf("Failed to unmarshal WhatsApp message: %v", err)
|
log.Printf("Failed to unmarshal WhatsApp message: %v", err)
|
||||||
continue
|
continue
|
||||||
|
|
@ -144,7 +144,7 @@ func (c *WhatsAppChannel) listen(ctx context.Context) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *WhatsAppChannel) handleIncomingMessage(msg map[string]interface{}) {
|
func (c *WhatsAppChannel) handleIncomingMessage(msg map[string]any) {
|
||||||
senderID, ok := msg["from"].(string)
|
senderID, ok := msg["from"].(string)
|
||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
|
|
@ -161,7 +161,7 @@ func (c *WhatsAppChannel) handleIncomingMessage(msg map[string]interface{}) {
|
||||||
}
|
}
|
||||||
|
|
||||||
var mediaPaths []string
|
var mediaPaths []string
|
||||||
if mediaData, ok := msg["media"].([]interface{}); ok {
|
if mediaData, ok := msg["media"].([]any); ok {
|
||||||
mediaPaths = make([]string, 0, len(mediaData))
|
mediaPaths = make([]string, 0, len(mediaData))
|
||||||
for _, m := range mediaData {
|
for _, m := range mediaData {
|
||||||
if path, ok := m.(string); ok {
|
if path, ok := m.(string); ok {
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ func (f *FlexibleStringSlice) UnmarshalJSON(data []byte) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try []interface{} to handle mixed types
|
// Try []interface{} to handle mixed types
|
||||||
var raw []interface{}
|
var raw []any
|
||||||
if err := json.Unmarshal(data, &raw); err != nil {
|
if err := json.Unmarshal(data, &raw); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -139,16 +139,16 @@ type SessionConfig struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type AgentDefaults struct {
|
type AgentDefaults struct {
|
||||||
Workspace string `json:"workspace" env:"PICOCLAW_AGENTS_DEFAULTS_WORKSPACE"`
|
Workspace string `json:"workspace" env:"PICOCLAW_AGENTS_DEFAULTS_WORKSPACE"`
|
||||||
RestrictToWorkspace bool `json:"restrict_to_workspace" env:"PICOCLAW_AGENTS_DEFAULTS_RESTRICT_TO_WORKSPACE"`
|
RestrictToWorkspace bool `json:"restrict_to_workspace" env:"PICOCLAW_AGENTS_DEFAULTS_RESTRICT_TO_WORKSPACE"`
|
||||||
Provider string `json:"provider" env:"PICOCLAW_AGENTS_DEFAULTS_PROVIDER"`
|
Provider string `json:"provider" env:"PICOCLAW_AGENTS_DEFAULTS_PROVIDER"`
|
||||||
Model string `json:"model" env:"PICOCLAW_AGENTS_DEFAULTS_MODEL"`
|
Model string `json:"model" env:"PICOCLAW_AGENTS_DEFAULTS_MODEL"`
|
||||||
ModelFallbacks []string `json:"model_fallbacks,omitempty"`
|
ModelFallbacks []string `json:"model_fallbacks,omitempty"`
|
||||||
ImageModel string `json:"image_model,omitempty" env:"PICOCLAW_AGENTS_DEFAULTS_IMAGE_MODEL"`
|
ImageModel string `json:"image_model,omitempty" env:"PICOCLAW_AGENTS_DEFAULTS_IMAGE_MODEL"`
|
||||||
ImageModelFallbacks []string `json:"image_model_fallbacks,omitempty"`
|
ImageModelFallbacks []string `json:"image_model_fallbacks,omitempty"`
|
||||||
MaxTokens int `json:"max_tokens" env:"PICOCLAW_AGENTS_DEFAULTS_MAX_TOKENS"`
|
MaxTokens int `json:"max_tokens" env:"PICOCLAW_AGENTS_DEFAULTS_MAX_TOKENS"`
|
||||||
Temperature float64 `json:"temperature" env:"PICOCLAW_AGENTS_DEFAULTS_TEMPERATURE"`
|
Temperature float64 `json:"temperature" env:"PICOCLAW_AGENTS_DEFAULTS_TEMPERATURE"`
|
||||||
MaxToolIterations int `json:"max_tool_iterations" env:"PICOCLAW_AGENTS_DEFAULTS_MAX_TOOL_ITERATIONS"`
|
MaxToolIterations int `json:"max_tool_iterations" env:"PICOCLAW_AGENTS_DEFAULTS_MAX_TOOL_ITERATIONS"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ChannelsConfig struct {
|
type ChannelsConfig struct {
|
||||||
|
|
@ -165,87 +165,87 @@ type ChannelsConfig struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type WhatsAppConfig struct {
|
type WhatsAppConfig struct {
|
||||||
Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_WHATSAPP_ENABLED"`
|
Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_WHATSAPP_ENABLED"`
|
||||||
BridgeURL string `json:"bridge_url" env:"PICOCLAW_CHANNELS_WHATSAPP_BRIDGE_URL"`
|
BridgeURL string `json:"bridge_url" env:"PICOCLAW_CHANNELS_WHATSAPP_BRIDGE_URL"`
|
||||||
AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_WHATSAPP_ALLOW_FROM"`
|
AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_WHATSAPP_ALLOW_FROM"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type TelegramConfig struct {
|
type TelegramConfig struct {
|
||||||
Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_TELEGRAM_ENABLED"`
|
Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_TELEGRAM_ENABLED"`
|
||||||
Token string `json:"token" env:"PICOCLAW_CHANNELS_TELEGRAM_TOKEN"`
|
Token string `json:"token" env:"PICOCLAW_CHANNELS_TELEGRAM_TOKEN"`
|
||||||
Proxy string `json:"proxy" env:"PICOCLAW_CHANNELS_TELEGRAM_PROXY"`
|
Proxy string `json:"proxy" env:"PICOCLAW_CHANNELS_TELEGRAM_PROXY"`
|
||||||
AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_TELEGRAM_ALLOW_FROM"`
|
AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_TELEGRAM_ALLOW_FROM"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type FeishuConfig struct {
|
type FeishuConfig struct {
|
||||||
Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_FEISHU_ENABLED"`
|
Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_FEISHU_ENABLED"`
|
||||||
AppID string `json:"app_id" env:"PICOCLAW_CHANNELS_FEISHU_APP_ID"`
|
AppID string `json:"app_id" env:"PICOCLAW_CHANNELS_FEISHU_APP_ID"`
|
||||||
AppSecret string `json:"app_secret" env:"PICOCLAW_CHANNELS_FEISHU_APP_SECRET"`
|
AppSecret string `json:"app_secret" env:"PICOCLAW_CHANNELS_FEISHU_APP_SECRET"`
|
||||||
EncryptKey string `json:"encrypt_key" env:"PICOCLAW_CHANNELS_FEISHU_ENCRYPT_KEY"`
|
EncryptKey string `json:"encrypt_key" env:"PICOCLAW_CHANNELS_FEISHU_ENCRYPT_KEY"`
|
||||||
VerificationToken string `json:"verification_token" env:"PICOCLAW_CHANNELS_FEISHU_VERIFICATION_TOKEN"`
|
VerificationToken string `json:"verification_token" env:"PICOCLAW_CHANNELS_FEISHU_VERIFICATION_TOKEN"`
|
||||||
AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_FEISHU_ALLOW_FROM"`
|
AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_FEISHU_ALLOW_FROM"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type DiscordConfig struct {
|
type DiscordConfig struct {
|
||||||
Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_DISCORD_ENABLED"`
|
Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_DISCORD_ENABLED"`
|
||||||
Token string `json:"token" env:"PICOCLAW_CHANNELS_DISCORD_TOKEN"`
|
Token string `json:"token" env:"PICOCLAW_CHANNELS_DISCORD_TOKEN"`
|
||||||
AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_DISCORD_ALLOW_FROM"`
|
AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_DISCORD_ALLOW_FROM"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type MaixCamConfig struct {
|
type MaixCamConfig struct {
|
||||||
Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_MAIXCAM_ENABLED"`
|
Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_MAIXCAM_ENABLED"`
|
||||||
Host string `json:"host" env:"PICOCLAW_CHANNELS_MAIXCAM_HOST"`
|
Host string `json:"host" env:"PICOCLAW_CHANNELS_MAIXCAM_HOST"`
|
||||||
Port int `json:"port" env:"PICOCLAW_CHANNELS_MAIXCAM_PORT"`
|
Port int `json:"port" env:"PICOCLAW_CHANNELS_MAIXCAM_PORT"`
|
||||||
AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_MAIXCAM_ALLOW_FROM"`
|
AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_MAIXCAM_ALLOW_FROM"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type QQConfig struct {
|
type QQConfig struct {
|
||||||
Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_QQ_ENABLED"`
|
Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_QQ_ENABLED"`
|
||||||
AppID string `json:"app_id" env:"PICOCLAW_CHANNELS_QQ_APP_ID"`
|
AppID string `json:"app_id" env:"PICOCLAW_CHANNELS_QQ_APP_ID"`
|
||||||
AppSecret string `json:"app_secret" env:"PICOCLAW_CHANNELS_QQ_APP_SECRET"`
|
AppSecret string `json:"app_secret" env:"PICOCLAW_CHANNELS_QQ_APP_SECRET"`
|
||||||
AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_QQ_ALLOW_FROM"`
|
AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_QQ_ALLOW_FROM"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type DingTalkConfig struct {
|
type DingTalkConfig struct {
|
||||||
Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_DINGTALK_ENABLED"`
|
Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_DINGTALK_ENABLED"`
|
||||||
ClientID string `json:"client_id" env:"PICOCLAW_CHANNELS_DINGTALK_CLIENT_ID"`
|
ClientID string `json:"client_id" env:"PICOCLAW_CHANNELS_DINGTALK_CLIENT_ID"`
|
||||||
ClientSecret string `json:"client_secret" env:"PICOCLAW_CHANNELS_DINGTALK_CLIENT_SECRET"`
|
ClientSecret string `json:"client_secret" env:"PICOCLAW_CHANNELS_DINGTALK_CLIENT_SECRET"`
|
||||||
AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_DINGTALK_ALLOW_FROM"`
|
AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_DINGTALK_ALLOW_FROM"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type SlackConfig struct {
|
type SlackConfig struct {
|
||||||
Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_SLACK_ENABLED"`
|
Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_SLACK_ENABLED"`
|
||||||
BotToken string `json:"bot_token" env:"PICOCLAW_CHANNELS_SLACK_BOT_TOKEN"`
|
BotToken string `json:"bot_token" env:"PICOCLAW_CHANNELS_SLACK_BOT_TOKEN"`
|
||||||
AppToken string `json:"app_token" env:"PICOCLAW_CHANNELS_SLACK_APP_TOKEN"`
|
AppToken string `json:"app_token" env:"PICOCLAW_CHANNELS_SLACK_APP_TOKEN"`
|
||||||
AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_SLACK_ALLOW_FROM"`
|
AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_SLACK_ALLOW_FROM"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type LINEConfig struct {
|
type LINEConfig struct {
|
||||||
Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_LINE_ENABLED"`
|
Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_LINE_ENABLED"`
|
||||||
ChannelSecret string `json:"channel_secret" env:"PICOCLAW_CHANNELS_LINE_CHANNEL_SECRET"`
|
ChannelSecret string `json:"channel_secret" env:"PICOCLAW_CHANNELS_LINE_CHANNEL_SECRET"`
|
||||||
ChannelAccessToken string `json:"channel_access_token" env:"PICOCLAW_CHANNELS_LINE_CHANNEL_ACCESS_TOKEN"`
|
ChannelAccessToken string `json:"channel_access_token" env:"PICOCLAW_CHANNELS_LINE_CHANNEL_ACCESS_TOKEN"`
|
||||||
WebhookHost string `json:"webhook_host" env:"PICOCLAW_CHANNELS_LINE_WEBHOOK_HOST"`
|
WebhookHost string `json:"webhook_host" env:"PICOCLAW_CHANNELS_LINE_WEBHOOK_HOST"`
|
||||||
WebhookPort int `json:"webhook_port" env:"PICOCLAW_CHANNELS_LINE_WEBHOOK_PORT"`
|
WebhookPort int `json:"webhook_port" env:"PICOCLAW_CHANNELS_LINE_WEBHOOK_PORT"`
|
||||||
WebhookPath string `json:"webhook_path" env:"PICOCLAW_CHANNELS_LINE_WEBHOOK_PATH"`
|
WebhookPath string `json:"webhook_path" env:"PICOCLAW_CHANNELS_LINE_WEBHOOK_PATH"`
|
||||||
AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_LINE_ALLOW_FROM"`
|
AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_LINE_ALLOW_FROM"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type OneBotConfig struct {
|
type OneBotConfig struct {
|
||||||
Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_ONEBOT_ENABLED"`
|
Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_ONEBOT_ENABLED"`
|
||||||
WSUrl string `json:"ws_url" env:"PICOCLAW_CHANNELS_ONEBOT_WS_URL"`
|
WSUrl string `json:"ws_url" env:"PICOCLAW_CHANNELS_ONEBOT_WS_URL"`
|
||||||
AccessToken string `json:"access_token" env:"PICOCLAW_CHANNELS_ONEBOT_ACCESS_TOKEN"`
|
AccessToken string `json:"access_token" env:"PICOCLAW_CHANNELS_ONEBOT_ACCESS_TOKEN"`
|
||||||
ReconnectInterval int `json:"reconnect_interval" env:"PICOCLAW_CHANNELS_ONEBOT_RECONNECT_INTERVAL"`
|
ReconnectInterval int `json:"reconnect_interval" env:"PICOCLAW_CHANNELS_ONEBOT_RECONNECT_INTERVAL"`
|
||||||
GroupTriggerPrefix []string `json:"group_trigger_prefix" env:"PICOCLAW_CHANNELS_ONEBOT_GROUP_TRIGGER_PREFIX"`
|
GroupTriggerPrefix []string `json:"group_trigger_prefix" env:"PICOCLAW_CHANNELS_ONEBOT_GROUP_TRIGGER_PREFIX"`
|
||||||
AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_ONEBOT_ALLOW_FROM"`
|
AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_ONEBOT_ALLOW_FROM"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type HeartbeatConfig struct {
|
type HeartbeatConfig struct {
|
||||||
Enabled bool `json:"enabled" env:"PICOCLAW_HEARTBEAT_ENABLED"`
|
Enabled bool `json:"enabled" env:"PICOCLAW_HEARTBEAT_ENABLED"`
|
||||||
Interval int `json:"interval" env:"PICOCLAW_HEARTBEAT_INTERVAL"` // minutes, min 5
|
Interval int `json:"interval" env:"PICOCLAW_HEARTBEAT_INTERVAL"` // minutes, min 5
|
||||||
}
|
}
|
||||||
|
|
||||||
type DevicesConfig struct {
|
type DevicesConfig struct {
|
||||||
Enabled bool `json:"enabled" env:"PICOCLAW_DEVICES_ENABLED"`
|
Enabled bool `json:"enabled" env:"PICOCLAW_DEVICES_ENABLED"`
|
||||||
MonitorUSB bool `json:"monitor_usb" env:"PICOCLAW_DEVICES_MONITOR_USB"`
|
MonitorUSB bool `json:"monitor_usb" env:"PICOCLAW_DEVICES_MONITOR_USB"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -266,11 +266,11 @@ type ProvidersConfig struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProviderConfig struct {
|
type ProviderConfig struct {
|
||||||
APIKey string `json:"api_key" env:"PICOCLAW_PROVIDERS_{{.Name}}_API_KEY"`
|
APIKey string `json:"api_key" env:"PICOCLAW_PROVIDERS_{{.Name}}_API_KEY"`
|
||||||
APIBase string `json:"api_base" env:"PICOCLAW_PROVIDERS_{{.Name}}_API_BASE"`
|
APIBase string `json:"api_base" env:"PICOCLAW_PROVIDERS_{{.Name}}_API_BASE"`
|
||||||
Proxy string `json:"proxy,omitempty" env:"PICOCLAW_PROVIDERS_{{.Name}}_PROXY"`
|
Proxy string `json:"proxy,omitempty" env:"PICOCLAW_PROVIDERS_{{.Name}}_PROXY"`
|
||||||
AuthMethod string `json:"auth_method,omitempty" env:"PICOCLAW_PROVIDERS_{{.Name}}_AUTH_METHOD"`
|
AuthMethod string `json:"auth_method,omitempty" env:"PICOCLAW_PROVIDERS_{{.Name}}_AUTH_METHOD"`
|
||||||
ConnectMode string `json:"connect_mode,omitempty" env:"PICOCLAW_PROVIDERS_{{.Name}}_CONNECT_MODE"` //only for Github Copilot, `stdio` or `grpc`
|
ConnectMode string `json:"connect_mode,omitempty" env:"PICOCLAW_PROVIDERS_{{.Name}}_CONNECT_MODE"` // only for Github Copilot, `stdio` or `grpc`
|
||||||
}
|
}
|
||||||
|
|
||||||
type OpenAIProviderConfig struct {
|
type OpenAIProviderConfig struct {
|
||||||
|
|
@ -284,19 +284,19 @@ type GatewayConfig struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type BraveConfig struct {
|
type BraveConfig struct {
|
||||||
Enabled bool `json:"enabled" env:"PICOCLAW_TOOLS_WEB_BRAVE_ENABLED"`
|
Enabled bool `json:"enabled" env:"PICOCLAW_TOOLS_WEB_BRAVE_ENABLED"`
|
||||||
APIKey string `json:"api_key" env:"PICOCLAW_TOOLS_WEB_BRAVE_API_KEY"`
|
APIKey string `json:"api_key" env:"PICOCLAW_TOOLS_WEB_BRAVE_API_KEY"`
|
||||||
MaxResults int `json:"max_results" env:"PICOCLAW_TOOLS_WEB_BRAVE_MAX_RESULTS"`
|
MaxResults int `json:"max_results" env:"PICOCLAW_TOOLS_WEB_BRAVE_MAX_RESULTS"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type DuckDuckGoConfig struct {
|
type DuckDuckGoConfig struct {
|
||||||
Enabled bool `json:"enabled" env:"PICOCLAW_TOOLS_WEB_DUCKDUCKGO_ENABLED"`
|
Enabled bool `json:"enabled" env:"PICOCLAW_TOOLS_WEB_DUCKDUCKGO_ENABLED"`
|
||||||
MaxResults int `json:"max_results" env:"PICOCLAW_TOOLS_WEB_DUCKDUCKGO_MAX_RESULTS"`
|
MaxResults int `json:"max_results" env:"PICOCLAW_TOOLS_WEB_DUCKDUCKGO_MAX_RESULTS"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type PerplexityConfig struct {
|
type PerplexityConfig struct {
|
||||||
Enabled bool `json:"enabled" env:"PICOCLAW_TOOLS_WEB_PERPLEXITY_ENABLED"`
|
Enabled bool `json:"enabled" env:"PICOCLAW_TOOLS_WEB_PERPLEXITY_ENABLED"`
|
||||||
APIKey string `json:"api_key" env:"PICOCLAW_TOOLS_WEB_PERPLEXITY_API_KEY"`
|
APIKey string `json:"api_key" env:"PICOCLAW_TOOLS_WEB_PERPLEXITY_API_KEY"`
|
||||||
MaxResults int `json:"max_results" env:"PICOCLAW_TOOLS_WEB_PERPLEXITY_MAX_RESULTS"`
|
MaxResults int `json:"max_results" env:"PICOCLAW_TOOLS_WEB_PERPLEXITY_MAX_RESULTS"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -483,11 +483,11 @@ func SaveConfig(path string, cfg *Config) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
dir := filepath.Dir(path)
|
dir := filepath.Dir(path)
|
||||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return os.WriteFile(path, data, 0600)
|
return os.WriteFile(path, data, 0o600)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Config) WorkspacePath() string {
|
func (c *Config) WorkspacePath() string {
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,7 @@ func TestAgentModelConfig_MarshalObject(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("marshal: %v", err)
|
t.Fatalf("marshal: %v", err)
|
||||||
}
|
}
|
||||||
var result map[string]interface{}
|
var result map[string]any
|
||||||
json.Unmarshal(data, &result)
|
json.Unmarshal(data, &result)
|
||||||
if result["primary"] != "claude-opus" {
|
if result["primary"] != "claude-opus" {
|
||||||
t.Errorf("primary = %v", result["primary"])
|
t.Errorf("primary = %v", result["primary"])
|
||||||
|
|
@ -319,7 +319,7 @@ func TestSaveConfig_FilePermissions(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
perm := info.Mode().Perm()
|
perm := info.Mode().Perm()
|
||||||
if perm != 0600 {
|
if perm != 0o600 {
|
||||||
t.Errorf("config file has permission %04o, want 0600", perm)
|
t.Errorf("config file has permission %04o, want 0600", perm)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -331,7 +331,7 @@ func (cs *CronService) loadStore() error {
|
||||||
|
|
||||||
func (cs *CronService) saveStoreUnsafe() error {
|
func (cs *CronService) saveStoreUnsafe() error {
|
||||||
dir := filepath.Dir(cs.storePath)
|
dir := filepath.Dir(cs.storePath)
|
||||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -340,10 +340,16 @@ func (cs *CronService) saveStoreUnsafe() error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return os.WriteFile(cs.storePath, data, 0600)
|
return os.WriteFile(cs.storePath, data, 0o600)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cs *CronService) AddJob(name string, schedule CronSchedule, message string, deliver bool, channel, to string) (*CronJob, error) {
|
func (cs *CronService) AddJob(
|
||||||
|
name string,
|
||||||
|
schedule CronSchedule,
|
||||||
|
message string,
|
||||||
|
deliver bool,
|
||||||
|
channel, to string,
|
||||||
|
) (*CronJob, error) {
|
||||||
cs.mu.Lock()
|
cs.mu.Lock()
|
||||||
defer cs.mu.Unlock()
|
defer cs.mu.Unlock()
|
||||||
|
|
||||||
|
|
@ -465,7 +471,7 @@ func (cs *CronService) ListJobs(includeDisabled bool) []CronJob {
|
||||||
return enabled
|
return enabled
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cs *CronService) Status() map[string]interface{} {
|
func (cs *CronService) Status() map[string]any {
|
||||||
cs.mu.RLock()
|
cs.mu.RLock()
|
||||||
defer cs.mu.RUnlock()
|
defer cs.mu.RUnlock()
|
||||||
|
|
||||||
|
|
@ -476,7 +482,7 @@ func (cs *CronService) Status() map[string]interface{} {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return map[string]interface{}{
|
return map[string]any{
|
||||||
"enabled": cs.running,
|
"enabled": cs.running,
|
||||||
"jobs": len(cs.store.Jobs),
|
"jobs": len(cs.store.Jobs),
|
||||||
"nextWakeAtMS": cs.getNextWakeMS(),
|
"nextWakeAtMS": cs.getNextWakeMS(),
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ func TestSaveStore_FilePermissions(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
perm := info.Mode().Perm()
|
perm := info.Mode().Perm()
|
||||||
if perm != 0600 {
|
if perm != 0o600 {
|
||||||
t.Errorf("cron store has permission %04o, want 0600", perm)
|
t.Errorf("cron store has permission %04o, want 0600", perm)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -63,14 +63,14 @@ func (s *Service) Start(ctx context.Context) error {
|
||||||
for _, src := range s.sources {
|
for _, src := range s.sources {
|
||||||
eventCh, err := src.Start(s.ctx)
|
eventCh, err := src.Start(s.ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.ErrorCF("devices", "Failed to start source", map[string]interface{}{
|
logger.ErrorCF("devices", "Failed to start source", map[string]any{
|
||||||
"kind": src.Kind(),
|
"kind": src.Kind(),
|
||||||
"error": err.Error(),
|
"error": err.Error(),
|
||||||
})
|
})
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
go s.handleEvents(src.Kind(), eventCh)
|
go s.handleEvents(src.Kind(), eventCh)
|
||||||
logger.InfoCF("devices", "Device source started", map[string]interface{}{
|
logger.InfoCF("devices", "Device source started", map[string]any{
|
||||||
"kind": src.Kind(),
|
"kind": src.Kind(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -115,7 +115,7 @@ func (s *Service) sendNotification(ev *events.DeviceEvent) {
|
||||||
|
|
||||||
lastChannel := s.state.GetLastChannel()
|
lastChannel := s.state.GetLastChannel()
|
||||||
if lastChannel == "" {
|
if lastChannel == "" {
|
||||||
logger.DebugCF("devices", "No last channel, skipping notification", map[string]interface{}{
|
logger.DebugCF("devices", "No last channel, skipping notification", map[string]any{
|
||||||
"event": ev.FormatMessage(),
|
"event": ev.FormatMessage(),
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
|
|
@ -133,7 +133,7 @@ func (s *Service) sendNotification(ev *events.DeviceEvent) {
|
||||||
Content: msg,
|
Content: msg,
|
||||||
})
|
})
|
||||||
|
|
||||||
logger.InfoCF("devices", "Device notification sent", map[string]interface{}{
|
logger.InfoCF("devices", "Device notification sent", map[string]any{
|
||||||
"kind": ev.Kind,
|
"kind": ev.Kind,
|
||||||
"action": ev.Action,
|
"action": ev.Action,
|
||||||
"to": platform,
|
"to": platform,
|
||||||
|
|
|
||||||
|
|
@ -115,7 +115,7 @@ func (m *USBMonitor) Start(ctx context.Context) (<-chan *events.DeviceEvent, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := scanner.Err(); err != nil {
|
if err := scanner.Err(); err != nil {
|
||||||
logger.ErrorCF("devices", "udevadm scan error", map[string]interface{}{"error": err.Error()})
|
logger.ErrorCF("devices", "udevadm scan error", map[string]any{"error": err.Error()})
|
||||||
}
|
}
|
||||||
cmd.Wait()
|
cmd.Wait()
|
||||||
}()
|
}()
|
||||||
|
|
|
||||||
|
|
@ -193,7 +193,7 @@ func (hs *HeartbeatService) executeHeartbeat() {
|
||||||
if result.Async {
|
if result.Async {
|
||||||
hs.logInfo("Async task started: %s", result.ForLLM)
|
hs.logInfo("Async task started: %s", result.ForLLM)
|
||||||
logger.InfoCF("heartbeat", "Async heartbeat task started",
|
logger.InfoCF("heartbeat", "Async heartbeat task started",
|
||||||
map[string]interface{}{
|
map[string]any{
|
||||||
"message": result.ForLLM,
|
"message": result.ForLLM,
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
|
|
@ -275,7 +275,7 @@ This file contains tasks for the heartbeat service to check periodically.
|
||||||
Add your heartbeat tasks below this line:
|
Add your heartbeat tasks below this line:
|
||||||
`
|
`
|
||||||
|
|
||||||
if err := os.WriteFile(heartbeatPath, []byte(defaultContent), 0644); err != nil {
|
if err := os.WriteFile(heartbeatPath, []byte(defaultContent), 0o644); err != nil {
|
||||||
hs.logError("Failed to create default HEARTBEAT.md: %v", err)
|
hs.logError("Failed to create default HEARTBEAT.md: %v", err)
|
||||||
} else {
|
} else {
|
||||||
hs.logInfo("Created default HEARTBEAT.md template")
|
hs.logInfo("Created default HEARTBEAT.md template")
|
||||||
|
|
@ -354,7 +354,7 @@ func (hs *HeartbeatService) logError(format string, args ...any) {
|
||||||
// log writes a message to the heartbeat log file
|
// log writes a message to the heartbeat log file
|
||||||
func (hs *HeartbeatService) log(level, format string, args ...any) {
|
func (hs *HeartbeatService) log(level, format string, args ...any) {
|
||||||
logFile := filepath.Join(hs.workspace, "heartbeat.log")
|
logFile := filepath.Join(hs.workspace, "heartbeat.log")
|
||||||
f, err := os.OpenFile(logFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
f, err := os.OpenFile(logFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ func TestExecuteHeartbeat_Async(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
// Create HEARTBEAT.md
|
// Create HEARTBEAT.md
|
||||||
os.WriteFile(filepath.Join(tmpDir, "HEARTBEAT.md"), []byte("Test task"), 0644)
|
os.WriteFile(filepath.Join(tmpDir, "HEARTBEAT.md"), []byte("Test task"), 0o644)
|
||||||
|
|
||||||
// Execute heartbeat directly (internal method for testing)
|
// Execute heartbeat directly (internal method for testing)
|
||||||
hs.executeHeartbeat()
|
hs.executeHeartbeat()
|
||||||
|
|
@ -68,7 +68,7 @@ func TestExecuteHeartbeat_Error(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
// Create HEARTBEAT.md
|
// Create HEARTBEAT.md
|
||||||
os.WriteFile(filepath.Join(tmpDir, "HEARTBEAT.md"), []byte("Test task"), 0644)
|
os.WriteFile(filepath.Join(tmpDir, "HEARTBEAT.md"), []byte("Test task"), 0o644)
|
||||||
|
|
||||||
hs.executeHeartbeat()
|
hs.executeHeartbeat()
|
||||||
|
|
||||||
|
|
@ -106,7 +106,7 @@ func TestExecuteHeartbeat_Silent(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
// Create HEARTBEAT.md
|
// Create HEARTBEAT.md
|
||||||
os.WriteFile(filepath.Join(tmpDir, "HEARTBEAT.md"), []byte("Test task"), 0644)
|
os.WriteFile(filepath.Join(tmpDir, "HEARTBEAT.md"), []byte("Test task"), 0o644)
|
||||||
|
|
||||||
hs.executeHeartbeat()
|
hs.executeHeartbeat()
|
||||||
|
|
||||||
|
|
@ -174,7 +174,7 @@ func TestExecuteHeartbeat_NilResult(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
// Create HEARTBEAT.md
|
// Create HEARTBEAT.md
|
||||||
os.WriteFile(filepath.Join(tmpDir, "HEARTBEAT.md"), []byte("Test task"), 0644)
|
os.WriteFile(filepath.Join(tmpDir, "HEARTBEAT.md"), []byte("Test task"), 0o644)
|
||||||
|
|
||||||
// Should not panic with nil result
|
// Should not panic with nil result
|
||||||
hs.executeHeartbeat()
|
hs.executeHeartbeat()
|
||||||
|
|
|
||||||
|
|
@ -41,12 +41,12 @@ type Logger struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type LogEntry struct {
|
type LogEntry struct {
|
||||||
Level string `json:"level"`
|
Level string `json:"level"`
|
||||||
Timestamp string `json:"timestamp"`
|
Timestamp string `json:"timestamp"`
|
||||||
Component string `json:"component,omitempty"`
|
Component string `json:"component,omitempty"`
|
||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
Fields map[string]interface{} `json:"fields,omitempty"`
|
Fields map[string]any `json:"fields,omitempty"`
|
||||||
Caller string `json:"caller,omitempty"`
|
Caller string `json:"caller,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
|
@ -71,7 +71,7 @@ func EnableFileLogging(filePath string) error {
|
||||||
mu.Lock()
|
mu.Lock()
|
||||||
defer mu.Unlock()
|
defer mu.Unlock()
|
||||||
|
|
||||||
file, err := os.OpenFile(filePath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
|
file, err := os.OpenFile(filePath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to open log file: %w", err)
|
return fmt.Errorf("failed to open log file: %w", err)
|
||||||
}
|
}
|
||||||
|
|
@ -96,7 +96,7 @@ func DisableFileLogging() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func logMessage(level LogLevel, component string, message string, fields map[string]interface{}) {
|
func logMessage(level LogLevel, component string, message string, fields map[string]any) {
|
||||||
if level < currentLevel {
|
if level < currentLevel {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -150,7 +150,7 @@ func formatComponent(component string) string {
|
||||||
return fmt.Sprintf(" %s:", component)
|
return fmt.Sprintf(" %s:", component)
|
||||||
}
|
}
|
||||||
|
|
||||||
func formatFields(fields map[string]interface{}) string {
|
func formatFields(fields map[string]any) string {
|
||||||
var parts []string
|
var parts []string
|
||||||
for k, v := range fields {
|
for k, v := range fields {
|
||||||
parts = append(parts, fmt.Sprintf("%s=%v", k, v))
|
parts = append(parts, fmt.Sprintf("%s=%v", k, v))
|
||||||
|
|
@ -166,11 +166,11 @@ func DebugC(component string, message string) {
|
||||||
logMessage(DEBUG, component, message, nil)
|
logMessage(DEBUG, component, message, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func DebugF(message string, fields map[string]interface{}) {
|
func DebugF(message string, fields map[string]any) {
|
||||||
logMessage(DEBUG, "", message, fields)
|
logMessage(DEBUG, "", message, fields)
|
||||||
}
|
}
|
||||||
|
|
||||||
func DebugCF(component string, message string, fields map[string]interface{}) {
|
func DebugCF(component string, message string, fields map[string]any) {
|
||||||
logMessage(DEBUG, component, message, fields)
|
logMessage(DEBUG, component, message, fields)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -182,11 +182,11 @@ func InfoC(component string, message string) {
|
||||||
logMessage(INFO, component, message, nil)
|
logMessage(INFO, component, message, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func InfoF(message string, fields map[string]interface{}) {
|
func InfoF(message string, fields map[string]any) {
|
||||||
logMessage(INFO, "", message, fields)
|
logMessage(INFO, "", message, fields)
|
||||||
}
|
}
|
||||||
|
|
||||||
func InfoCF(component string, message string, fields map[string]interface{}) {
|
func InfoCF(component string, message string, fields map[string]any) {
|
||||||
logMessage(INFO, component, message, fields)
|
logMessage(INFO, component, message, fields)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -198,11 +198,11 @@ func WarnC(component string, message string) {
|
||||||
logMessage(WARN, component, message, nil)
|
logMessage(WARN, component, message, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func WarnF(message string, fields map[string]interface{}) {
|
func WarnF(message string, fields map[string]any) {
|
||||||
logMessage(WARN, "", message, fields)
|
logMessage(WARN, "", message, fields)
|
||||||
}
|
}
|
||||||
|
|
||||||
func WarnCF(component string, message string, fields map[string]interface{}) {
|
func WarnCF(component string, message string, fields map[string]any) {
|
||||||
logMessage(WARN, component, message, fields)
|
logMessage(WARN, component, message, fields)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -214,11 +214,11 @@ func ErrorC(component string, message string) {
|
||||||
logMessage(ERROR, component, message, nil)
|
logMessage(ERROR, component, message, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func ErrorF(message string, fields map[string]interface{}) {
|
func ErrorF(message string, fields map[string]any) {
|
||||||
logMessage(ERROR, "", message, fields)
|
logMessage(ERROR, "", message, fields)
|
||||||
}
|
}
|
||||||
|
|
||||||
func ErrorCF(component string, message string, fields map[string]interface{}) {
|
func ErrorCF(component string, message string, fields map[string]any) {
|
||||||
logMessage(ERROR, component, message, fields)
|
logMessage(ERROR, component, message, fields)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -230,10 +230,10 @@ func FatalC(component string, message string) {
|
||||||
logMessage(FATAL, component, message, nil)
|
logMessage(FATAL, component, message, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func FatalF(message string, fields map[string]interface{}) {
|
func FatalF(message string, fields map[string]any) {
|
||||||
logMessage(FATAL, "", message, fields)
|
logMessage(FATAL, "", message, fields)
|
||||||
}
|
}
|
||||||
|
|
||||||
func FatalCF(component string, message string, fields map[string]interface{}) {
|
func FatalCF(component string, message string, fields map[string]any) {
|
||||||
logMessage(FATAL, component, message, fields)
|
logMessage(FATAL, component, message, fields)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -54,11 +54,11 @@ func TestLoggerWithComponent(t *testing.T) {
|
||||||
name string
|
name string
|
||||||
component string
|
component string
|
||||||
message string
|
message string
|
||||||
fields map[string]interface{}
|
fields map[string]any
|
||||||
}{
|
}{
|
||||||
{"Simple message", "test", "Hello, world!", nil},
|
{"Simple message", "test", "Hello, world!", nil},
|
||||||
{"Message with component", "discord", "Discord message", nil},
|
{"Message with component", "discord", "Discord message", nil},
|
||||||
{"Message with fields", "telegram", "Telegram message", map[string]interface{}{
|
{"Message with fields", "telegram", "Telegram message", map[string]any{
|
||||||
"user_id": "12345",
|
"user_id": "12345",
|
||||||
"count": 42,
|
"count": 42,
|
||||||
}},
|
}},
|
||||||
|
|
@ -128,12 +128,12 @@ func TestLoggerHelperFunctions(t *testing.T) {
|
||||||
Error("This should log")
|
Error("This should log")
|
||||||
|
|
||||||
InfoC("test", "Component message")
|
InfoC("test", "Component message")
|
||||||
InfoF("Fields message", map[string]interface{}{"key": "value"})
|
InfoF("Fields message", map[string]any{"key": "value"})
|
||||||
|
|
||||||
WarnC("test", "Warning with component")
|
WarnC("test", "Warning with component")
|
||||||
ErrorF("Error with fields", map[string]interface{}{"error": "test"})
|
ErrorF("Error with fields", map[string]any{"error": "test"})
|
||||||
|
|
||||||
SetLevel(DEBUG)
|
SetLevel(DEBUG)
|
||||||
DebugC("test", "Debug with component")
|
DebugC("test", "Debug with component")
|
||||||
WarnF("Warning with fields", map[string]interface{}{"key": "value"})
|
WarnF("Warning with fields", map[string]any{"key": "value"})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -44,26 +44,26 @@ func findOpenClawConfig(openclawHome string) (string, error) {
|
||||||
return "", fmt.Errorf("no config file found in %s (tried openclaw.json, config.json)", openclawHome)
|
return "", fmt.Errorf("no config file found in %s (tried openclaw.json, config.json)", openclawHome)
|
||||||
}
|
}
|
||||||
|
|
||||||
func LoadOpenClawConfig(configPath string) (map[string]interface{}, error) {
|
func LoadOpenClawConfig(configPath string) (map[string]any, error) {
|
||||||
data, err := os.ReadFile(configPath)
|
data, err := os.ReadFile(configPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("reading OpenClaw config: %w", err)
|
return nil, fmt.Errorf("reading OpenClaw config: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var raw map[string]interface{}
|
var raw map[string]any
|
||||||
if err := json.Unmarshal(data, &raw); err != nil {
|
if err := json.Unmarshal(data, &raw); err != nil {
|
||||||
return nil, fmt.Errorf("parsing OpenClaw config: %w", err)
|
return nil, fmt.Errorf("parsing OpenClaw config: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
converted := convertKeysToSnake(raw)
|
converted := convertKeysToSnake(raw)
|
||||||
result, ok := converted.(map[string]interface{})
|
result, ok := converted.(map[string]any)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, fmt.Errorf("unexpected config format")
|
return nil, fmt.Errorf("unexpected config format")
|
||||||
}
|
}
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func ConvertConfig(data map[string]interface{}) (*config.Config, []string, error) {
|
func ConvertConfig(data map[string]any) (*config.Config, []string, error) {
|
||||||
cfg := config.DefaultConfig()
|
cfg := config.DefaultConfig()
|
||||||
var warnings []string
|
var warnings []string
|
||||||
|
|
||||||
|
|
@ -89,7 +89,7 @@ func ConvertConfig(data map[string]interface{}) (*config.Config, []string, error
|
||||||
|
|
||||||
if providers, ok := getMap(data, "providers"); ok {
|
if providers, ok := getMap(data, "providers"); ok {
|
||||||
for name, val := range providers {
|
for name, val := range providers {
|
||||||
pMap, ok := val.(map[string]interface{})
|
pMap, ok := val.(map[string]any)
|
||||||
if !ok {
|
if !ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
@ -128,7 +128,7 @@ func ConvertConfig(data map[string]interface{}) (*config.Config, []string, error
|
||||||
|
|
||||||
if channels, ok := getMap(data, "channels"); ok {
|
if channels, ok := getMap(data, "channels"); ok {
|
||||||
for name, val := range channels {
|
for name, val := range channels {
|
||||||
cMap, ok := val.(map[string]interface{})
|
cMap, ok := val.(map[string]any)
|
||||||
if !ok {
|
if !ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
@ -306,16 +306,16 @@ func camelToSnake(s string) string {
|
||||||
return result.String()
|
return result.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
func convertKeysToSnake(data interface{}) interface{} {
|
func convertKeysToSnake(data any) any {
|
||||||
switch v := data.(type) {
|
switch v := data.(type) {
|
||||||
case map[string]interface{}:
|
case map[string]any:
|
||||||
result := make(map[string]interface{}, len(v))
|
result := make(map[string]any, len(v))
|
||||||
for key, val := range v {
|
for key, val := range v {
|
||||||
result[camelToSnake(key)] = convertKeysToSnake(val)
|
result[camelToSnake(key)] = convertKeysToSnake(val)
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
case []interface{}:
|
case []any:
|
||||||
result := make([]interface{}, len(v))
|
result := make([]any, len(v))
|
||||||
for i, val := range v {
|
for i, val := range v {
|
||||||
result[i] = convertKeysToSnake(val)
|
result[i] = convertKeysToSnake(val)
|
||||||
}
|
}
|
||||||
|
|
@ -330,16 +330,16 @@ func rewriteWorkspacePath(path string) string {
|
||||||
return path
|
return path
|
||||||
}
|
}
|
||||||
|
|
||||||
func getMap(data map[string]interface{}, key string) (map[string]interface{}, bool) {
|
func getMap(data map[string]any, key string) (map[string]any, bool) {
|
||||||
v, ok := data[key]
|
v, ok := data[key]
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
m, ok := v.(map[string]interface{})
|
m, ok := v.(map[string]any)
|
||||||
return m, ok
|
return m, ok
|
||||||
}
|
}
|
||||||
|
|
||||||
func getString(data map[string]interface{}, key string) (string, bool) {
|
func getString(data map[string]any, key string) (string, bool) {
|
||||||
v, ok := data[key]
|
v, ok := data[key]
|
||||||
if !ok {
|
if !ok {
|
||||||
return "", false
|
return "", false
|
||||||
|
|
@ -348,7 +348,7 @@ func getString(data map[string]interface{}, key string) (string, bool) {
|
||||||
return s, ok
|
return s, ok
|
||||||
}
|
}
|
||||||
|
|
||||||
func getFloat(data map[string]interface{}, key string) (float64, bool) {
|
func getFloat(data map[string]any, key string) (float64, bool) {
|
||||||
v, ok := data[key]
|
v, ok := data[key]
|
||||||
if !ok {
|
if !ok {
|
||||||
return 0, false
|
return 0, false
|
||||||
|
|
@ -357,7 +357,7 @@ func getFloat(data map[string]interface{}, key string) (float64, bool) {
|
||||||
return f, ok
|
return f, ok
|
||||||
}
|
}
|
||||||
|
|
||||||
func getBool(data map[string]interface{}, key string) (bool, bool) {
|
func getBool(data map[string]any, key string) (bool, bool) {
|
||||||
v, ok := data[key]
|
v, ok := data[key]
|
||||||
if !ok {
|
if !ok {
|
||||||
return false, false
|
return false, false
|
||||||
|
|
@ -366,19 +366,19 @@ func getBool(data map[string]interface{}, key string) (bool, bool) {
|
||||||
return b, ok
|
return b, ok
|
||||||
}
|
}
|
||||||
|
|
||||||
func getBoolOrDefault(data map[string]interface{}, key string, defaultVal bool) bool {
|
func getBoolOrDefault(data map[string]any, key string, defaultVal bool) bool {
|
||||||
if v, ok := getBool(data, key); ok {
|
if v, ok := getBool(data, key); ok {
|
||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
return defaultVal
|
return defaultVal
|
||||||
}
|
}
|
||||||
|
|
||||||
func getStringSlice(data map[string]interface{}, key string) []string {
|
func getStringSlice(data map[string]any, key string) []string {
|
||||||
v, ok := data[key]
|
v, ok := data[key]
|
||||||
if !ok {
|
if !ok {
|
||||||
return []string{}
|
return []string{}
|
||||||
}
|
}
|
||||||
arr, ok := v.([]interface{})
|
arr, ok := v.([]any)
|
||||||
if !ok {
|
if !ok {
|
||||||
return []string{}
|
return []string{}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -161,7 +161,7 @@ func Execute(actions []Action, openclawHome, picoClawHome string) *Result {
|
||||||
fmt.Printf(" ✓ Converted config: %s\n", action.Destination)
|
fmt.Printf(" ✓ Converted config: %s\n", action.Destination)
|
||||||
}
|
}
|
||||||
case ActionCreateDir:
|
case ActionCreateDir:
|
||||||
if err := os.MkdirAll(action.Destination, 0755); err != nil {
|
if err := os.MkdirAll(action.Destination, 0o755); err != nil {
|
||||||
result.Errors = append(result.Errors, err)
|
result.Errors = append(result.Errors, err)
|
||||||
} else {
|
} else {
|
||||||
result.DirsCreated++
|
result.DirsCreated++
|
||||||
|
|
@ -174,9 +174,13 @@ func Execute(actions []Action, openclawHome, picoClawHome string) *Result {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
result.BackupsCreated++
|
result.BackupsCreated++
|
||||||
fmt.Printf(" ✓ Backed up %s -> %s.bak\n", filepath.Base(action.Destination), filepath.Base(action.Destination))
|
fmt.Printf(
|
||||||
|
" ✓ Backed up %s -> %s.bak\n",
|
||||||
|
filepath.Base(action.Destination),
|
||||||
|
filepath.Base(action.Destination),
|
||||||
|
)
|
||||||
|
|
||||||
if err := os.MkdirAll(filepath.Dir(action.Destination), 0755); err != nil {
|
if err := os.MkdirAll(filepath.Dir(action.Destination), 0o755); err != nil {
|
||||||
result.Errors = append(result.Errors, err)
|
result.Errors = append(result.Errors, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
@ -188,7 +192,7 @@ func Execute(actions []Action, openclawHome, picoClawHome string) *Result {
|
||||||
fmt.Printf(" ✓ Copied %s\n", relPath(action.Source, openclawHome))
|
fmt.Printf(" ✓ Copied %s\n", relPath(action.Source, openclawHome))
|
||||||
}
|
}
|
||||||
case ActionCopy:
|
case ActionCopy:
|
||||||
if err := os.MkdirAll(filepath.Dir(action.Destination), 0755); err != nil {
|
if err := os.MkdirAll(filepath.Dir(action.Destination), 0o755); err != nil {
|
||||||
result.Errors = append(result.Errors, err)
|
result.Errors = append(result.Errors, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
@ -226,7 +230,7 @@ func executeConfigMigration(srcConfigPath, dstConfigPath, picoClawHome string) e
|
||||||
incoming = MergeConfig(existing, incoming)
|
incoming = MergeConfig(existing, incoming)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := os.MkdirAll(filepath.Dir(dstConfigPath), 0755); err != nil {
|
if err := os.MkdirAll(filepath.Dir(dstConfigPath), 0o755); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return config.SaveConfig(dstConfigPath, incoming)
|
return config.SaveConfig(dstConfigPath, incoming)
|
||||||
|
|
|
||||||
|
|
@ -40,20 +40,20 @@ func TestCamelToSnake(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestConvertKeysToSnake(t *testing.T) {
|
func TestConvertKeysToSnake(t *testing.T) {
|
||||||
input := map[string]interface{}{
|
input := map[string]any{
|
||||||
"apiKey": "test-key",
|
"apiKey": "test-key",
|
||||||
"apiBase": "https://example.com",
|
"apiBase": "https://example.com",
|
||||||
"nested": map[string]interface{}{
|
"nested": map[string]any{
|
||||||
"maxTokens": float64(8192),
|
"maxTokens": float64(8192),
|
||||||
"allowFrom": []interface{}{"user1", "user2"},
|
"allowFrom": []any{"user1", "user2"},
|
||||||
"deeperLevel": map[string]interface{}{
|
"deeperLevel": map[string]any{
|
||||||
"clientId": "abc",
|
"clientId": "abc",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
result := convertKeysToSnake(input)
|
result := convertKeysToSnake(input)
|
||||||
m, ok := result.(map[string]interface{})
|
m, ok := result.(map[string]any)
|
||||||
if !ok {
|
if !ok {
|
||||||
t.Fatal("expected map[string]interface{}")
|
t.Fatal("expected map[string]interface{}")
|
||||||
}
|
}
|
||||||
|
|
@ -65,7 +65,7 @@ func TestConvertKeysToSnake(t *testing.T) {
|
||||||
t.Error("expected key 'api_base' after conversion")
|
t.Error("expected key 'api_base' after conversion")
|
||||||
}
|
}
|
||||||
|
|
||||||
nested, ok := m["nested"].(map[string]interface{})
|
nested, ok := m["nested"].(map[string]any)
|
||||||
if !ok {
|
if !ok {
|
||||||
t.Fatal("expected nested map")
|
t.Fatal("expected nested map")
|
||||||
}
|
}
|
||||||
|
|
@ -76,7 +76,7 @@ func TestConvertKeysToSnake(t *testing.T) {
|
||||||
t.Error("expected key 'allow_from' in nested map")
|
t.Error("expected key 'allow_from' in nested map")
|
||||||
}
|
}
|
||||||
|
|
||||||
deeper, ok := nested["deeper_level"].(map[string]interface{})
|
deeper, ok := nested["deeper_level"].(map[string]any)
|
||||||
if !ok {
|
if !ok {
|
||||||
t.Fatal("expected deeper_level map")
|
t.Fatal("expected deeper_level map")
|
||||||
}
|
}
|
||||||
|
|
@ -89,15 +89,15 @@ func TestLoadOpenClawConfig(t *testing.T) {
|
||||||
tmpDir := t.TempDir()
|
tmpDir := t.TempDir()
|
||||||
configPath := filepath.Join(tmpDir, "openclaw.json")
|
configPath := filepath.Join(tmpDir, "openclaw.json")
|
||||||
|
|
||||||
openclawConfig := map[string]interface{}{
|
openclawConfig := map[string]any{
|
||||||
"providers": map[string]interface{}{
|
"providers": map[string]any{
|
||||||
"anthropic": map[string]interface{}{
|
"anthropic": map[string]any{
|
||||||
"apiKey": "sk-ant-test123",
|
"apiKey": "sk-ant-test123",
|
||||||
"apiBase": "https://api.anthropic.com",
|
"apiBase": "https://api.anthropic.com",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"agents": map[string]interface{}{
|
"agents": map[string]any{
|
||||||
"defaults": map[string]interface{}{
|
"defaults": map[string]any{
|
||||||
"maxTokens": float64(4096),
|
"maxTokens": float64(4096),
|
||||||
"model": "claude-3-opus",
|
"model": "claude-3-opus",
|
||||||
},
|
},
|
||||||
|
|
@ -108,7 +108,7 @@ func TestLoadOpenClawConfig(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if err := os.WriteFile(configPath, data, 0644); err != nil {
|
if err := os.WriteFile(configPath, data, 0o644); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -117,11 +117,11 @@ func TestLoadOpenClawConfig(t *testing.T) {
|
||||||
t.Fatalf("LoadOpenClawConfig: %v", err)
|
t.Fatalf("LoadOpenClawConfig: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
providers, ok := result["providers"].(map[string]interface{})
|
providers, ok := result["providers"].(map[string]any)
|
||||||
if !ok {
|
if !ok {
|
||||||
t.Fatal("expected providers map")
|
t.Fatal("expected providers map")
|
||||||
}
|
}
|
||||||
anthropic, ok := providers["anthropic"].(map[string]interface{})
|
anthropic, ok := providers["anthropic"].(map[string]any)
|
||||||
if !ok {
|
if !ok {
|
||||||
t.Fatal("expected anthropic map")
|
t.Fatal("expected anthropic map")
|
||||||
}
|
}
|
||||||
|
|
@ -129,11 +129,11 @@ func TestLoadOpenClawConfig(t *testing.T) {
|
||||||
t.Errorf("api_key = %v, want sk-ant-test123", anthropic["api_key"])
|
t.Errorf("api_key = %v, want sk-ant-test123", anthropic["api_key"])
|
||||||
}
|
}
|
||||||
|
|
||||||
agents, ok := result["agents"].(map[string]interface{})
|
agents, ok := result["agents"].(map[string]any)
|
||||||
if !ok {
|
if !ok {
|
||||||
t.Fatal("expected agents map")
|
t.Fatal("expected agents map")
|
||||||
}
|
}
|
||||||
defaults, ok := agents["defaults"].(map[string]interface{})
|
defaults, ok := agents["defaults"].(map[string]any)
|
||||||
if !ok {
|
if !ok {
|
||||||
t.Fatal("expected defaults map")
|
t.Fatal("expected defaults map")
|
||||||
}
|
}
|
||||||
|
|
@ -144,16 +144,16 @@ func TestLoadOpenClawConfig(t *testing.T) {
|
||||||
|
|
||||||
func TestConvertConfig(t *testing.T) {
|
func TestConvertConfig(t *testing.T) {
|
||||||
t.Run("providers mapping", func(t *testing.T) {
|
t.Run("providers mapping", func(t *testing.T) {
|
||||||
data := map[string]interface{}{
|
data := map[string]any{
|
||||||
"providers": map[string]interface{}{
|
"providers": map[string]any{
|
||||||
"anthropic": map[string]interface{}{
|
"anthropic": map[string]any{
|
||||||
"api_key": "sk-ant-test",
|
"api_key": "sk-ant-test",
|
||||||
"api_base": "https://api.anthropic.com",
|
"api_base": "https://api.anthropic.com",
|
||||||
},
|
},
|
||||||
"openrouter": map[string]interface{}{
|
"openrouter": map[string]any{
|
||||||
"api_key": "sk-or-test",
|
"api_key": "sk-or-test",
|
||||||
},
|
},
|
||||||
"groq": map[string]interface{}{
|
"groq": map[string]any{
|
||||||
"api_key": "gsk-test",
|
"api_key": "gsk-test",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -178,9 +178,9 @@ func TestConvertConfig(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("unsupported provider warning", func(t *testing.T) {
|
t.Run("unsupported provider warning", func(t *testing.T) {
|
||||||
data := map[string]interface{}{
|
data := map[string]any{
|
||||||
"providers": map[string]interface{}{
|
"providers": map[string]any{
|
||||||
"deepseek": map[string]interface{}{
|
"deepseek": map[string]any{
|
||||||
"api_key": "sk-deep-test",
|
"api_key": "sk-deep-test",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -199,14 +199,14 @@ func TestConvertConfig(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("channels mapping", func(t *testing.T) {
|
t.Run("channels mapping", func(t *testing.T) {
|
||||||
data := map[string]interface{}{
|
data := map[string]any{
|
||||||
"channels": map[string]interface{}{
|
"channels": map[string]any{
|
||||||
"telegram": map[string]interface{}{
|
"telegram": map[string]any{
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"token": "tg-token-123",
|
"token": "tg-token-123",
|
||||||
"allow_from": []interface{}{"user1"},
|
"allow_from": []any{"user1"},
|
||||||
},
|
},
|
||||||
"discord": map[string]interface{}{
|
"discord": map[string]any{
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"token": "disc-token-456",
|
"token": "disc-token-456",
|
||||||
},
|
},
|
||||||
|
|
@ -232,9 +232,9 @@ func TestConvertConfig(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("unsupported channel warning", func(t *testing.T) {
|
t.Run("unsupported channel warning", func(t *testing.T) {
|
||||||
data := map[string]interface{}{
|
data := map[string]any{
|
||||||
"channels": map[string]interface{}{
|
"channels": map[string]any{
|
||||||
"email": map[string]interface{}{
|
"email": map[string]any{
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -253,9 +253,9 @@ func TestConvertConfig(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("agent defaults", func(t *testing.T) {
|
t.Run("agent defaults", func(t *testing.T) {
|
||||||
data := map[string]interface{}{
|
data := map[string]any{
|
||||||
"agents": map[string]interface{}{
|
"agents": map[string]any{
|
||||||
"defaults": map[string]interface{}{
|
"defaults": map[string]any{
|
||||||
"model": "claude-3-opus",
|
"model": "claude-3-opus",
|
||||||
"max_tokens": float64(4096),
|
"max_tokens": float64(4096),
|
||||||
"temperature": 0.5,
|
"temperature": 0.5,
|
||||||
|
|
@ -284,7 +284,7 @@ func TestConvertConfig(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("empty config", func(t *testing.T) {
|
t.Run("empty config", func(t *testing.T) {
|
||||||
data := map[string]interface{}{}
|
data := map[string]any{}
|
||||||
|
|
||||||
cfg, warnings, err := ConvertConfig(data)
|
cfg, warnings, err := ConvertConfig(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -386,9 +386,9 @@ func TestPlanWorkspaceMigration(t *testing.T) {
|
||||||
srcDir := t.TempDir()
|
srcDir := t.TempDir()
|
||||||
dstDir := t.TempDir()
|
dstDir := t.TempDir()
|
||||||
|
|
||||||
os.WriteFile(filepath.Join(srcDir, "AGENTS.md"), []byte("# Agents"), 0644)
|
os.WriteFile(filepath.Join(srcDir, "AGENTS.md"), []byte("# Agents"), 0o644)
|
||||||
os.WriteFile(filepath.Join(srcDir, "SOUL.md"), []byte("# Soul"), 0644)
|
os.WriteFile(filepath.Join(srcDir, "SOUL.md"), []byte("# Soul"), 0o644)
|
||||||
os.WriteFile(filepath.Join(srcDir, "USER.md"), []byte("# User"), 0644)
|
os.WriteFile(filepath.Join(srcDir, "USER.md"), []byte("# User"), 0o644)
|
||||||
|
|
||||||
actions, err := PlanWorkspaceMigration(srcDir, dstDir, false)
|
actions, err := PlanWorkspaceMigration(srcDir, dstDir, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -417,8 +417,8 @@ func TestPlanWorkspaceMigration(t *testing.T) {
|
||||||
srcDir := t.TempDir()
|
srcDir := t.TempDir()
|
||||||
dstDir := t.TempDir()
|
dstDir := t.TempDir()
|
||||||
|
|
||||||
os.WriteFile(filepath.Join(srcDir, "AGENTS.md"), []byte("# Agents from OpenClaw"), 0644)
|
os.WriteFile(filepath.Join(srcDir, "AGENTS.md"), []byte("# Agents from OpenClaw"), 0o644)
|
||||||
os.WriteFile(filepath.Join(dstDir, "AGENTS.md"), []byte("# Existing Agents"), 0644)
|
os.WriteFile(filepath.Join(dstDir, "AGENTS.md"), []byte("# Existing Agents"), 0o644)
|
||||||
|
|
||||||
actions, err := PlanWorkspaceMigration(srcDir, dstDir, false)
|
actions, err := PlanWorkspaceMigration(srcDir, dstDir, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -440,8 +440,8 @@ func TestPlanWorkspaceMigration(t *testing.T) {
|
||||||
srcDir := t.TempDir()
|
srcDir := t.TempDir()
|
||||||
dstDir := t.TempDir()
|
dstDir := t.TempDir()
|
||||||
|
|
||||||
os.WriteFile(filepath.Join(srcDir, "AGENTS.md"), []byte("# Agents"), 0644)
|
os.WriteFile(filepath.Join(srcDir, "AGENTS.md"), []byte("# Agents"), 0o644)
|
||||||
os.WriteFile(filepath.Join(dstDir, "AGENTS.md"), []byte("# Existing"), 0644)
|
os.WriteFile(filepath.Join(dstDir, "AGENTS.md"), []byte("# Existing"), 0o644)
|
||||||
|
|
||||||
actions, err := PlanWorkspaceMigration(srcDir, dstDir, true)
|
actions, err := PlanWorkspaceMigration(srcDir, dstDir, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -460,8 +460,8 @@ func TestPlanWorkspaceMigration(t *testing.T) {
|
||||||
dstDir := t.TempDir()
|
dstDir := t.TempDir()
|
||||||
|
|
||||||
memDir := filepath.Join(srcDir, "memory")
|
memDir := filepath.Join(srcDir, "memory")
|
||||||
os.MkdirAll(memDir, 0755)
|
os.MkdirAll(memDir, 0o755)
|
||||||
os.WriteFile(filepath.Join(memDir, "MEMORY.md"), []byte("# Memory"), 0644)
|
os.WriteFile(filepath.Join(memDir, "MEMORY.md"), []byte("# Memory"), 0o644)
|
||||||
|
|
||||||
actions, err := PlanWorkspaceMigration(srcDir, dstDir, false)
|
actions, err := PlanWorkspaceMigration(srcDir, dstDir, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -491,8 +491,8 @@ func TestPlanWorkspaceMigration(t *testing.T) {
|
||||||
dstDir := t.TempDir()
|
dstDir := t.TempDir()
|
||||||
|
|
||||||
skillDir := filepath.Join(srcDir, "skills", "weather")
|
skillDir := filepath.Join(srcDir, "skills", "weather")
|
||||||
os.MkdirAll(skillDir, 0755)
|
os.MkdirAll(skillDir, 0o755)
|
||||||
os.WriteFile(filepath.Join(skillDir, "SKILL.md"), []byte("# Weather"), 0644)
|
os.WriteFile(filepath.Join(skillDir, "SKILL.md"), []byte("# Weather"), 0o644)
|
||||||
|
|
||||||
actions, err := PlanWorkspaceMigration(srcDir, dstDir, false)
|
actions, err := PlanWorkspaceMigration(srcDir, dstDir, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -515,7 +515,7 @@ func TestFindOpenClawConfig(t *testing.T) {
|
||||||
t.Run("finds openclaw.json", func(t *testing.T) {
|
t.Run("finds openclaw.json", func(t *testing.T) {
|
||||||
tmpDir := t.TempDir()
|
tmpDir := t.TempDir()
|
||||||
configPath := filepath.Join(tmpDir, "openclaw.json")
|
configPath := filepath.Join(tmpDir, "openclaw.json")
|
||||||
os.WriteFile(configPath, []byte("{}"), 0644)
|
os.WriteFile(configPath, []byte("{}"), 0o644)
|
||||||
|
|
||||||
found, err := findOpenClawConfig(tmpDir)
|
found, err := findOpenClawConfig(tmpDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -529,7 +529,7 @@ func TestFindOpenClawConfig(t *testing.T) {
|
||||||
t.Run("falls back to config.json", func(t *testing.T) {
|
t.Run("falls back to config.json", func(t *testing.T) {
|
||||||
tmpDir := t.TempDir()
|
tmpDir := t.TempDir()
|
||||||
configPath := filepath.Join(tmpDir, "config.json")
|
configPath := filepath.Join(tmpDir, "config.json")
|
||||||
os.WriteFile(configPath, []byte("{}"), 0644)
|
os.WriteFile(configPath, []byte("{}"), 0o644)
|
||||||
|
|
||||||
found, err := findOpenClawConfig(tmpDir)
|
found, err := findOpenClawConfig(tmpDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -543,8 +543,8 @@ func TestFindOpenClawConfig(t *testing.T) {
|
||||||
t.Run("prefers openclaw.json over config.json", func(t *testing.T) {
|
t.Run("prefers openclaw.json over config.json", func(t *testing.T) {
|
||||||
tmpDir := t.TempDir()
|
tmpDir := t.TempDir()
|
||||||
openclawPath := filepath.Join(tmpDir, "openclaw.json")
|
openclawPath := filepath.Join(tmpDir, "openclaw.json")
|
||||||
os.WriteFile(openclawPath, []byte("{}"), 0644)
|
os.WriteFile(openclawPath, []byte("{}"), 0o644)
|
||||||
os.WriteFile(filepath.Join(tmpDir, "config.json"), []byte("{}"), 0644)
|
os.WriteFile(filepath.Join(tmpDir, "config.json"), []byte("{}"), 0o644)
|
||||||
|
|
||||||
found, err := findOpenClawConfig(tmpDir)
|
found, err := findOpenClawConfig(tmpDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -590,19 +590,19 @@ func TestRunDryRun(t *testing.T) {
|
||||||
picoClawHome := t.TempDir()
|
picoClawHome := t.TempDir()
|
||||||
|
|
||||||
wsDir := filepath.Join(openclawHome, "workspace")
|
wsDir := filepath.Join(openclawHome, "workspace")
|
||||||
os.MkdirAll(wsDir, 0755)
|
os.MkdirAll(wsDir, 0o755)
|
||||||
os.WriteFile(filepath.Join(wsDir, "SOUL.md"), []byte("# Soul"), 0644)
|
os.WriteFile(filepath.Join(wsDir, "SOUL.md"), []byte("# Soul"), 0o644)
|
||||||
os.WriteFile(filepath.Join(wsDir, "AGENTS.md"), []byte("# Agents"), 0644)
|
os.WriteFile(filepath.Join(wsDir, "AGENTS.md"), []byte("# Agents"), 0o644)
|
||||||
|
|
||||||
configData := map[string]interface{}{
|
configData := map[string]any{
|
||||||
"providers": map[string]interface{}{
|
"providers": map[string]any{
|
||||||
"anthropic": map[string]interface{}{
|
"anthropic": map[string]any{
|
||||||
"apiKey": "test-key",
|
"apiKey": "test-key",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
data, _ := json.Marshal(configData)
|
data, _ := json.Marshal(configData)
|
||||||
os.WriteFile(filepath.Join(openclawHome, "openclaw.json"), data, 0644)
|
os.WriteFile(filepath.Join(openclawHome, "openclaw.json"), data, 0o644)
|
||||||
|
|
||||||
opts := Options{
|
opts := Options{
|
||||||
DryRun: true,
|
DryRun: true,
|
||||||
|
|
@ -631,33 +631,33 @@ func TestRunFullMigration(t *testing.T) {
|
||||||
picoClawHome := t.TempDir()
|
picoClawHome := t.TempDir()
|
||||||
|
|
||||||
wsDir := filepath.Join(openclawHome, "workspace")
|
wsDir := filepath.Join(openclawHome, "workspace")
|
||||||
os.MkdirAll(wsDir, 0755)
|
os.MkdirAll(wsDir, 0o755)
|
||||||
os.WriteFile(filepath.Join(wsDir, "SOUL.md"), []byte("# Soul from OpenClaw"), 0644)
|
os.WriteFile(filepath.Join(wsDir, "SOUL.md"), []byte("# Soul from OpenClaw"), 0o644)
|
||||||
os.WriteFile(filepath.Join(wsDir, "AGENTS.md"), []byte("# Agents from OpenClaw"), 0644)
|
os.WriteFile(filepath.Join(wsDir, "AGENTS.md"), []byte("# Agents from OpenClaw"), 0o644)
|
||||||
os.WriteFile(filepath.Join(wsDir, "USER.md"), []byte("# User from OpenClaw"), 0644)
|
os.WriteFile(filepath.Join(wsDir, "USER.md"), []byte("# User from OpenClaw"), 0o644)
|
||||||
|
|
||||||
memDir := filepath.Join(wsDir, "memory")
|
memDir := filepath.Join(wsDir, "memory")
|
||||||
os.MkdirAll(memDir, 0755)
|
os.MkdirAll(memDir, 0o755)
|
||||||
os.WriteFile(filepath.Join(memDir, "MEMORY.md"), []byte("# Memory notes"), 0644)
|
os.WriteFile(filepath.Join(memDir, "MEMORY.md"), []byte("# Memory notes"), 0o644)
|
||||||
|
|
||||||
configData := map[string]interface{}{
|
configData := map[string]any{
|
||||||
"providers": map[string]interface{}{
|
"providers": map[string]any{
|
||||||
"anthropic": map[string]interface{}{
|
"anthropic": map[string]any{
|
||||||
"apiKey": "sk-ant-migrate-test",
|
"apiKey": "sk-ant-migrate-test",
|
||||||
},
|
},
|
||||||
"openrouter": map[string]interface{}{
|
"openrouter": map[string]any{
|
||||||
"apiKey": "sk-or-migrate-test",
|
"apiKey": "sk-or-migrate-test",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"channels": map[string]interface{}{
|
"channels": map[string]any{
|
||||||
"telegram": map[string]interface{}{
|
"telegram": map[string]any{
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"token": "tg-migrate-test",
|
"token": "tg-migrate-test",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
data, _ := json.Marshal(configData)
|
data, _ := json.Marshal(configData)
|
||||||
os.WriteFile(filepath.Join(openclawHome, "openclaw.json"), data, 0644)
|
os.WriteFile(filepath.Join(openclawHome, "openclaw.json"), data, 0o644)
|
||||||
|
|
||||||
opts := Options{
|
opts := Options{
|
||||||
Force: true,
|
Force: true,
|
||||||
|
|
@ -751,7 +751,7 @@ func TestRunMutuallyExclusiveFlags(t *testing.T) {
|
||||||
func TestBackupFile(t *testing.T) {
|
func TestBackupFile(t *testing.T) {
|
||||||
tmpDir := t.TempDir()
|
tmpDir := t.TempDir()
|
||||||
filePath := filepath.Join(tmpDir, "test.md")
|
filePath := filepath.Join(tmpDir, "test.md")
|
||||||
os.WriteFile(filePath, []byte("original content"), 0644)
|
os.WriteFile(filePath, []byte("original content"), 0o644)
|
||||||
|
|
||||||
if err := backupFile(filePath); err != nil {
|
if err := backupFile(filePath); err != nil {
|
||||||
t.Fatalf("backupFile: %v", err)
|
t.Fatalf("backupFile: %v", err)
|
||||||
|
|
@ -772,7 +772,7 @@ func TestCopyFile(t *testing.T) {
|
||||||
srcPath := filepath.Join(tmpDir, "src.md")
|
srcPath := filepath.Join(tmpDir, "src.md")
|
||||||
dstPath := filepath.Join(tmpDir, "dst.md")
|
dstPath := filepath.Join(tmpDir, "dst.md")
|
||||||
|
|
||||||
os.WriteFile(srcPath, []byte("file content"), 0644)
|
os.WriteFile(srcPath, []byte("file content"), 0o644)
|
||||||
|
|
||||||
if err := copyFile(srcPath, dstPath); err != nil {
|
if err := copyFile(srcPath, dstPath); err != nil {
|
||||||
t.Fatalf("copyFile: %v", err)
|
t.Fatalf("copyFile: %v", err)
|
||||||
|
|
@ -792,18 +792,18 @@ func TestRunConfigOnly(t *testing.T) {
|
||||||
picoClawHome := t.TempDir()
|
picoClawHome := t.TempDir()
|
||||||
|
|
||||||
wsDir := filepath.Join(openclawHome, "workspace")
|
wsDir := filepath.Join(openclawHome, "workspace")
|
||||||
os.MkdirAll(wsDir, 0755)
|
os.MkdirAll(wsDir, 0o755)
|
||||||
os.WriteFile(filepath.Join(wsDir, "SOUL.md"), []byte("# Soul"), 0644)
|
os.WriteFile(filepath.Join(wsDir, "SOUL.md"), []byte("# Soul"), 0o644)
|
||||||
|
|
||||||
configData := map[string]interface{}{
|
configData := map[string]any{
|
||||||
"providers": map[string]interface{}{
|
"providers": map[string]any{
|
||||||
"anthropic": map[string]interface{}{
|
"anthropic": map[string]any{
|
||||||
"apiKey": "sk-config-only",
|
"apiKey": "sk-config-only",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
data, _ := json.Marshal(configData)
|
data, _ := json.Marshal(configData)
|
||||||
os.WriteFile(filepath.Join(openclawHome, "openclaw.json"), data, 0644)
|
os.WriteFile(filepath.Join(openclawHome, "openclaw.json"), data, 0o644)
|
||||||
|
|
||||||
opts := Options{
|
opts := Options{
|
||||||
Force: true,
|
Force: true,
|
||||||
|
|
@ -832,18 +832,18 @@ func TestRunWorkspaceOnly(t *testing.T) {
|
||||||
picoClawHome := t.TempDir()
|
picoClawHome := t.TempDir()
|
||||||
|
|
||||||
wsDir := filepath.Join(openclawHome, "workspace")
|
wsDir := filepath.Join(openclawHome, "workspace")
|
||||||
os.MkdirAll(wsDir, 0755)
|
os.MkdirAll(wsDir, 0o755)
|
||||||
os.WriteFile(filepath.Join(wsDir, "SOUL.md"), []byte("# Soul"), 0644)
|
os.WriteFile(filepath.Join(wsDir, "SOUL.md"), []byte("# Soul"), 0o644)
|
||||||
|
|
||||||
configData := map[string]interface{}{
|
configData := map[string]any{
|
||||||
"providers": map[string]interface{}{
|
"providers": map[string]any{
|
||||||
"anthropic": map[string]interface{}{
|
"anthropic": map[string]any{
|
||||||
"apiKey": "sk-ws-only",
|
"apiKey": "sk-ws-only",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
data, _ := json.Marshal(configData)
|
data, _ := json.Marshal(configData)
|
||||||
os.WriteFile(filepath.Join(openclawHome, "openclaw.json"), data, 0644)
|
os.WriteFile(filepath.Join(openclawHome, "openclaw.json"), data, 0o644)
|
||||||
|
|
||||||
opts := Options{
|
opts := Options{
|
||||||
Force: true,
|
Force: true,
|
||||||
|
|
|
||||||
|
|
@ -9,16 +9,19 @@ import (
|
||||||
|
|
||||||
"github.com/anthropics/anthropic-sdk-go"
|
"github.com/anthropics/anthropic-sdk-go"
|
||||||
"github.com/anthropics/anthropic-sdk-go/option"
|
"github.com/anthropics/anthropic-sdk-go/option"
|
||||||
|
|
||||||
"github.com/sipeed/picoclaw/pkg/providers/protocoltypes"
|
"github.com/sipeed/picoclaw/pkg/providers/protocoltypes"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ToolCall = protocoltypes.ToolCall
|
type (
|
||||||
type FunctionCall = protocoltypes.FunctionCall
|
ToolCall = protocoltypes.ToolCall
|
||||||
type LLMResponse = protocoltypes.LLMResponse
|
FunctionCall = protocoltypes.FunctionCall
|
||||||
type UsageInfo = protocoltypes.UsageInfo
|
LLMResponse = protocoltypes.LLMResponse
|
||||||
type Message = protocoltypes.Message
|
UsageInfo = protocoltypes.UsageInfo
|
||||||
type ToolDefinition = protocoltypes.ToolDefinition
|
Message = protocoltypes.Message
|
||||||
type ToolFunctionDefinition = protocoltypes.ToolFunctionDefinition
|
ToolDefinition = protocoltypes.ToolDefinition
|
||||||
|
ToolFunctionDefinition = protocoltypes.ToolFunctionDefinition
|
||||||
|
)
|
||||||
|
|
||||||
const defaultBaseURL = "https://api.anthropic.com"
|
const defaultBaseURL = "https://api.anthropic.com"
|
||||||
|
|
||||||
|
|
@ -61,7 +64,13 @@ func NewProviderWithTokenSourceAndBaseURL(token string, tokenSource func() (stri
|
||||||
return p
|
return p
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Provider) Chat(ctx context.Context, messages []Message, tools []ToolDefinition, model string, options map[string]interface{}) (*LLMResponse, error) {
|
func (p *Provider) Chat(
|
||||||
|
ctx context.Context,
|
||||||
|
messages []Message,
|
||||||
|
tools []ToolDefinition,
|
||||||
|
model string,
|
||||||
|
options map[string]any,
|
||||||
|
) (*LLMResponse, error) {
|
||||||
var opts []option.RequestOption
|
var opts []option.RequestOption
|
||||||
if p.tokenSource != nil {
|
if p.tokenSource != nil {
|
||||||
tok, err := p.tokenSource()
|
tok, err := p.tokenSource()
|
||||||
|
|
@ -92,7 +101,12 @@ func (p *Provider) BaseURL() string {
|
||||||
return p.baseURL
|
return p.baseURL
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildParams(messages []Message, tools []ToolDefinition, model string, options map[string]interface{}) (anthropic.MessageNewParams, error) {
|
func buildParams(
|
||||||
|
messages []Message,
|
||||||
|
tools []ToolDefinition,
|
||||||
|
model string,
|
||||||
|
options map[string]any,
|
||||||
|
) (anthropic.MessageNewParams, error) {
|
||||||
var system []anthropic.TextBlockParam
|
var system []anthropic.TextBlockParam
|
||||||
var anthropicMessages []anthropic.MessageParam
|
var anthropicMessages []anthropic.MessageParam
|
||||||
|
|
||||||
|
|
@ -170,7 +184,7 @@ func translateTools(tools []ToolDefinition) []anthropic.ToolUnionParam {
|
||||||
if desc := t.Function.Description; desc != "" {
|
if desc := t.Function.Description; desc != "" {
|
||||||
tool.Description = anthropic.String(desc)
|
tool.Description = anthropic.String(desc)
|
||||||
}
|
}
|
||||||
if req, ok := t.Function.Parameters["required"].([]interface{}); ok {
|
if req, ok := t.Function.Parameters["required"].([]any); ok {
|
||||||
required := make([]string, 0, len(req))
|
required := make([]string, 0, len(req))
|
||||||
for _, r := range req {
|
for _, r := range req {
|
||||||
if s, ok := r.(string); ok {
|
if s, ok := r.(string); ok {
|
||||||
|
|
@ -195,10 +209,10 @@ func parseResponse(resp *anthropic.Message) *LLMResponse {
|
||||||
content += tb.Text
|
content += tb.Text
|
||||||
case "tool_use":
|
case "tool_use":
|
||||||
tu := block.AsToolUse()
|
tu := block.AsToolUse()
|
||||||
var args map[string]interface{}
|
var args map[string]any
|
||||||
if err := json.Unmarshal(tu.Input, &args); err != nil {
|
if err := json.Unmarshal(tu.Input, &args); err != nil {
|
||||||
log.Printf("anthropic: failed to decode tool call input for %q: %v", tu.Name, err)
|
log.Printf("anthropic: failed to decode tool call input for %q: %v", tu.Name, err)
|
||||||
args = map[string]interface{}{"raw": string(tu.Input)}
|
args = map[string]any{"raw": string(tu.Input)}
|
||||||
}
|
}
|
||||||
toolCalls = append(toolCalls, ToolCall{
|
toolCalls = append(toolCalls, ToolCall{
|
||||||
ID: tu.ID,
|
ID: tu.ID,
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ func TestBuildParams_BasicMessage(t *testing.T) {
|
||||||
messages := []Message{
|
messages := []Message{
|
||||||
{Role: "user", Content: "Hello"},
|
{Role: "user", Content: "Hello"},
|
||||||
}
|
}
|
||||||
params, err := buildParams(messages, nil, "claude-sonnet-4-5-20250929", map[string]interface{}{
|
params, err := buildParams(messages, nil, "claude-sonnet-4-5-20250929", map[string]any{
|
||||||
"max_tokens": 1024,
|
"max_tokens": 1024,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -37,7 +37,7 @@ func TestBuildParams_SystemMessage(t *testing.T) {
|
||||||
{Role: "system", Content: "You are helpful"},
|
{Role: "system", Content: "You are helpful"},
|
||||||
{Role: "user", Content: "Hi"},
|
{Role: "user", Content: "Hi"},
|
||||||
}
|
}
|
||||||
params, err := buildParams(messages, nil, "claude-sonnet-4-5-20250929", map[string]interface{}{})
|
params, err := buildParams(messages, nil, "claude-sonnet-4-5-20250929", map[string]any{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("buildParams() error: %v", err)
|
t.Fatalf("buildParams() error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
@ -62,13 +62,13 @@ func TestBuildParams_ToolCallMessage(t *testing.T) {
|
||||||
{
|
{
|
||||||
ID: "call_1",
|
ID: "call_1",
|
||||||
Name: "get_weather",
|
Name: "get_weather",
|
||||||
Arguments: map[string]interface{}{"city": "SF"},
|
Arguments: map[string]any{"city": "SF"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{Role: "tool", Content: `{"temp": 72}`, ToolCallID: "call_1"},
|
{Role: "tool", Content: `{"temp": 72}`, ToolCallID: "call_1"},
|
||||||
}
|
}
|
||||||
params, err := buildParams(messages, nil, "claude-sonnet-4-5-20250929", map[string]interface{}{})
|
params, err := buildParams(messages, nil, "claude-sonnet-4-5-20250929", map[string]any{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("buildParams() error: %v", err)
|
t.Fatalf("buildParams() error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
@ -84,17 +84,22 @@ func TestBuildParams_WithTools(t *testing.T) {
|
||||||
Function: ToolFunctionDefinition{
|
Function: ToolFunctionDefinition{
|
||||||
Name: "get_weather",
|
Name: "get_weather",
|
||||||
Description: "Get weather for a city",
|
Description: "Get weather for a city",
|
||||||
Parameters: map[string]interface{}{
|
Parameters: map[string]any{
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": map[string]interface{}{
|
"properties": map[string]any{
|
||||||
"city": map[string]interface{}{"type": "string"},
|
"city": map[string]any{"type": "string"},
|
||||||
},
|
},
|
||||||
"required": []interface{}{"city"},
|
"required": []any{"city"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
params, err := buildParams([]Message{{Role: "user", Content: "Hi"}}, tools, "claude-sonnet-4-5-20250929", map[string]interface{}{})
|
params, err := buildParams(
|
||||||
|
[]Message{{Role: "user", Content: "Hi"}},
|
||||||
|
tools,
|
||||||
|
"claude-sonnet-4-5-20250929",
|
||||||
|
map[string]any{},
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("buildParams() error: %v", err)
|
t.Fatalf("buildParams() error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
@ -154,19 +159,19 @@ func TestProvider_ChatRoundTrip(t *testing.T) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var reqBody map[string]interface{}
|
var reqBody map[string]any
|
||||||
json.NewDecoder(r.Body).Decode(&reqBody)
|
json.NewDecoder(r.Body).Decode(&reqBody)
|
||||||
|
|
||||||
resp := map[string]interface{}{
|
resp := map[string]any{
|
||||||
"id": "msg_test",
|
"id": "msg_test",
|
||||||
"type": "message",
|
"type": "message",
|
||||||
"role": "assistant",
|
"role": "assistant",
|
||||||
"model": reqBody["model"],
|
"model": reqBody["model"],
|
||||||
"stop_reason": "end_turn",
|
"stop_reason": "end_turn",
|
||||||
"content": []map[string]interface{}{
|
"content": []map[string]any{
|
||||||
{"type": "text", "text": "Hello! How can I help you?"},
|
{"type": "text", "text": "Hello! How can I help you?"},
|
||||||
},
|
},
|
||||||
"usage": map[string]interface{}{
|
"usage": map[string]any{
|
||||||
"input_tokens": 15,
|
"input_tokens": 15,
|
||||||
"output_tokens": 8,
|
"output_tokens": 8,
|
||||||
},
|
},
|
||||||
|
|
@ -178,7 +183,13 @@ func TestProvider_ChatRoundTrip(t *testing.T) {
|
||||||
|
|
||||||
provider := NewProviderWithClient(createAnthropicTestClient(server.URL, "test-token"))
|
provider := NewProviderWithClient(createAnthropicTestClient(server.URL, "test-token"))
|
||||||
messages := []Message{{Role: "user", Content: "Hello"}}
|
messages := []Message{{Role: "user", Content: "Hello"}}
|
||||||
resp, err := provider.Chat(t.Context(), messages, nil, "claude-sonnet-4-5-20250929", map[string]interface{}{"max_tokens": 1024})
|
resp, err := provider.Chat(
|
||||||
|
t.Context(),
|
||||||
|
messages,
|
||||||
|
nil,
|
||||||
|
"claude-sonnet-4-5-20250929",
|
||||||
|
map[string]any{"max_tokens": 1024},
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Chat() error: %v", err)
|
t.Fatalf("Chat() error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
@ -221,19 +232,19 @@ func TestProvider_ChatUsesTokenSource(t *testing.T) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var reqBody map[string]interface{}
|
var reqBody map[string]any
|
||||||
json.NewDecoder(r.Body).Decode(&reqBody)
|
json.NewDecoder(r.Body).Decode(&reqBody)
|
||||||
|
|
||||||
resp := map[string]interface{}{
|
resp := map[string]any{
|
||||||
"id": "msg_test",
|
"id": "msg_test",
|
||||||
"type": "message",
|
"type": "message",
|
||||||
"role": "assistant",
|
"role": "assistant",
|
||||||
"model": reqBody["model"],
|
"model": reqBody["model"],
|
||||||
"stop_reason": "end_turn",
|
"stop_reason": "end_turn",
|
||||||
"content": []map[string]interface{}{
|
"content": []map[string]any{
|
||||||
{"type": "text", "text": "ok"},
|
{"type": "text", "text": "ok"},
|
||||||
},
|
},
|
||||||
"usage": map[string]interface{}{
|
"usage": map[string]any{
|
||||||
"input_tokens": 1,
|
"input_tokens": 1,
|
||||||
"output_tokens": 1,
|
"output_tokens": 1,
|
||||||
},
|
},
|
||||||
|
|
@ -247,7 +258,13 @@ func TestProvider_ChatUsesTokenSource(t *testing.T) {
|
||||||
return "refreshed-token", nil
|
return "refreshed-token", nil
|
||||||
}, server.URL)
|
}, server.URL)
|
||||||
|
|
||||||
_, err := p.Chat(t.Context(), []Message{{Role: "user", Content: "hello"}}, nil, "claude-sonnet-4-5-20250929", map[string]interface{}{})
|
_, err := p.Chat(
|
||||||
|
t.Context(),
|
||||||
|
[]Message{{Role: "user", Content: "hello"}},
|
||||||
|
nil,
|
||||||
|
"claude-sonnet-4-5-20250929",
|
||||||
|
map[string]any{},
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Chat() error: %v", err)
|
t.Fatalf("Chat() error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,9 @@ func NewClaudeCliProvider(workspace string) *ClaudeCliProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Chat implements LLMProvider.Chat by executing the claude CLI.
|
// Chat implements LLMProvider.Chat by executing the claude CLI.
|
||||||
func (p *ClaudeCliProvider) Chat(ctx context.Context, messages []Message, tools []ToolDefinition, model string, options map[string]interface{}) (*LLMResponse, error) {
|
func (p *ClaudeCliProvider) Chat(
|
||||||
|
ctx context.Context, messages []Message, tools []ToolDefinition, model string, options map[string]any,
|
||||||
|
) (*LLMResponse, error) {
|
||||||
systemPrompt := p.buildSystemPrompt(messages, tools)
|
systemPrompt := p.buildSystemPrompt(messages, tools)
|
||||||
prompt := p.messagesToPrompt(messages)
|
prompt := p.messagesToPrompt(messages)
|
||||||
|
|
||||||
|
|
@ -111,7 +113,9 @@ func (p *ClaudeCliProvider) buildToolsPrompt(tools []ToolDefinition) string {
|
||||||
sb.WriteString("## Available Tools\n\n")
|
sb.WriteString("## Available Tools\n\n")
|
||||||
sb.WriteString("When you need to use a tool, respond with ONLY a JSON object:\n\n")
|
sb.WriteString("When you need to use a tool, respond with ONLY a JSON object:\n\n")
|
||||||
sb.WriteString("```json\n")
|
sb.WriteString("```json\n")
|
||||||
sb.WriteString(`{"tool_calls":[{"id":"call_xxx","type":"function","function":{"name":"tool_name","arguments":"{...}"}}]}`)
|
sb.WriteString(
|
||||||
|
`{"tool_calls":[{"id":"call_xxx","type":"function","function":{"name":"tool_name","arguments":"{...}"}}]}`,
|
||||||
|
)
|
||||||
sb.WriteString("\n```\n\n")
|
sb.WriteString("\n```\n\n")
|
||||||
sb.WriteString("CRITICAL: The 'arguments' field MUST be a JSON-encoded STRING.\n\n")
|
sb.WriteString("CRITICAL: The 'arguments' field MUST be a JSON-encoded STRING.\n\n")
|
||||||
sb.WriteString("### Tool Definitions:\n\n")
|
sb.WriteString("### Tool Definitions:\n\n")
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,6 @@ func TestIntegration_RealClaudeCLI(t *testing.T) {
|
||||||
resp, err := p.Chat(ctx, []Message{
|
resp, err := p.Chat(ctx, []Message{
|
||||||
{Role: "user", Content: "Respond with only the word 'pong'. Nothing else."},
|
{Role: "user", Content: "Respond with only the word 'pong'. Nothing else."},
|
||||||
}, nil, "", nil)
|
}, nil, "", nil)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Chat() with real CLI error = %v", err)
|
t.Fatalf("Chat() with real CLI error = %v", err)
|
||||||
}
|
}
|
||||||
|
|
@ -75,7 +74,6 @@ func TestIntegration_RealClaudeCLI_WithSystemPrompt(t *testing.T) {
|
||||||
{Role: "system", Content: "You are a calculator. Only respond with numbers. No text."},
|
{Role: "system", Content: "You are a calculator. Only respond with numbers. No text."},
|
||||||
{Role: "user", Content: "What is 2+2?"},
|
{Role: "user", Content: "What is 2+2?"},
|
||||||
}, nil, "", nil)
|
}, nil, "", nil)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Chat() error = %v", err)
|
t.Fatalf("Chat() error = %v", err)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,12 +30,12 @@ func createMockCLI(t *testing.T, stdout, stderr string, exitCode int) string {
|
||||||
dir := t.TempDir()
|
dir := t.TempDir()
|
||||||
|
|
||||||
if stdout != "" {
|
if stdout != "" {
|
||||||
if err := os.WriteFile(filepath.Join(dir, "stdout.txt"), []byte(stdout), 0644); err != nil {
|
if err := os.WriteFile(filepath.Join(dir, "stdout.txt"), []byte(stdout), 0o644); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if stderr != "" {
|
if stderr != "" {
|
||||||
if err := os.WriteFile(filepath.Join(dir, "stderr.txt"), []byte(stderr), 0644); err != nil {
|
if err := os.WriteFile(filepath.Join(dir, "stderr.txt"), []byte(stderr), 0o644); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -51,7 +51,7 @@ func createMockCLI(t *testing.T, stdout, stderr string, exitCode int) string {
|
||||||
sb.WriteString(fmt.Sprintf("exit %d\n", exitCode))
|
sb.WriteString(fmt.Sprintf("exit %d\n", exitCode))
|
||||||
|
|
||||||
script := filepath.Join(dir, "claude")
|
script := filepath.Join(dir, "claude")
|
||||||
if err := os.WriteFile(script, []byte(sb.String()), 0755); err != nil {
|
if err := os.WriteFile(script, []byte(sb.String()), 0o755); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
return script
|
return script
|
||||||
|
|
@ -67,7 +67,7 @@ func createSlowMockCLI(t *testing.T, sleepSeconds int) string {
|
||||||
dir := t.TempDir()
|
dir := t.TempDir()
|
||||||
script := filepath.Join(dir, "claude")
|
script := filepath.Join(dir, "claude")
|
||||||
content := fmt.Sprintf("#!/bin/sh\nsleep %d\necho '{\"type\":\"result\",\"result\":\"late\"}'\n", sleepSeconds)
|
content := fmt.Sprintf("#!/bin/sh\nsleep %d\necho '{\"type\":\"result\",\"result\":\"late\"}'\n", sleepSeconds)
|
||||||
if err := os.WriteFile(script, []byte(content), 0755); err != nil {
|
if err := os.WriteFile(script, []byte(content), 0o755); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
return script
|
return script
|
||||||
|
|
@ -88,7 +88,7 @@ cat <<'EOFMOCK'
|
||||||
{"type":"result","result":"ok","session_id":"test"}
|
{"type":"result","result":"ok","session_id":"test"}
|
||||||
EOFMOCK
|
EOFMOCK
|
||||||
`, argsFile)
|
`, argsFile)
|
||||||
if err := os.WriteFile(script, []byte(content), 0755); err != nil {
|
if err := os.WriteFile(script, []byte(content), 0o755); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
return script
|
return script
|
||||||
|
|
@ -137,7 +137,6 @@ func TestChat_Success(t *testing.T) {
|
||||||
resp, err := p.Chat(context.Background(), []Message{
|
resp, err := p.Chat(context.Background(), []Message{
|
||||||
{Role: "user", Content: "Hello"},
|
{Role: "user", Content: "Hello"},
|
||||||
}, nil, "", nil)
|
}, nil, "", nil)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Chat() error = %v", err)
|
t.Fatalf("Chat() error = %v", err)
|
||||||
}
|
}
|
||||||
|
|
@ -193,7 +192,6 @@ func TestChat_WithToolCallsInResponse(t *testing.T) {
|
||||||
resp, err := p.Chat(context.Background(), []Message{
|
resp, err := p.Chat(context.Background(), []Message{
|
||||||
{Role: "user", Content: "What's the weather?"},
|
{Role: "user", Content: "What's the weather?"},
|
||||||
}, nil, "", nil)
|
}, nil, "", nil)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Chat() error = %v", err)
|
t.Fatalf("Chat() error = %v", err)
|
||||||
}
|
}
|
||||||
|
|
@ -403,7 +401,6 @@ func TestChat_EmptyWorkspaceDoesNotSetDir(t *testing.T) {
|
||||||
resp, err := p.Chat(context.Background(), []Message{
|
resp, err := p.Chat(context.Background(), []Message{
|
||||||
{Role: "user", Content: "Hello"},
|
{Role: "user", Content: "Hello"},
|
||||||
}, nil, "", nil)
|
}, nil, "", nil)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Chat() with empty workspace error = %v", err)
|
t.Fatalf("Chat() with empty workspace error = %v", err)
|
||||||
}
|
}
|
||||||
|
|
@ -611,10 +608,10 @@ func TestBuildSystemPrompt_WithTools(t *testing.T) {
|
||||||
Function: ToolFunctionDefinition{
|
Function: ToolFunctionDefinition{
|
||||||
Name: "get_weather",
|
Name: "get_weather",
|
||||||
Description: "Get weather for a location",
|
Description: "Get weather for a location",
|
||||||
Parameters: map[string]interface{}{
|
Parameters: map[string]any{
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": map[string]interface{}{
|
"properties": map[string]any{
|
||||||
"location": map[string]interface{}{"type": "string"},
|
"location": map[string]any{"type": "string"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,9 @@ func NewClaudeProviderWithTokenSource(token string, tokenSource func() (string,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewClaudeProviderWithTokenSourceAndBaseURL(token string, tokenSource func() (string, error), apiBase string) *ClaudeProvider {
|
func NewClaudeProviderWithTokenSourceAndBaseURL(
|
||||||
|
token string, tokenSource func() (string, error), apiBase string,
|
||||||
|
) *ClaudeProvider {
|
||||||
return &ClaudeProvider{
|
return &ClaudeProvider{
|
||||||
delegate: anthropicprovider.NewProviderWithTokenSourceAndBaseURL(token, tokenSource, apiBase),
|
delegate: anthropicprovider.NewProviderWithTokenSourceAndBaseURL(token, tokenSource, apiBase),
|
||||||
}
|
}
|
||||||
|
|
@ -39,7 +41,9 @@ func newClaudeProviderWithDelegate(delegate *anthropicprovider.Provider) *Claude
|
||||||
return &ClaudeProvider{delegate: delegate}
|
return &ClaudeProvider{delegate: delegate}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *ClaudeProvider) Chat(ctx context.Context, messages []Message, tools []ToolDefinition, model string, options map[string]interface{}) (*LLMResponse, error) {
|
func (p *ClaudeProvider) Chat(
|
||||||
|
ctx context.Context, messages []Message, tools []ToolDefinition, model string, options map[string]any,
|
||||||
|
) (*LLMResponse, error) {
|
||||||
resp, err := p.delegate.Chat(ctx, messages, tools, model, options)
|
resp, err := p.delegate.Chat(ctx, messages, tools, model, options)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import (
|
||||||
|
|
||||||
"github.com/anthropics/anthropic-sdk-go"
|
"github.com/anthropics/anthropic-sdk-go"
|
||||||
anthropicoption "github.com/anthropics/anthropic-sdk-go/option"
|
anthropicoption "github.com/anthropics/anthropic-sdk-go/option"
|
||||||
|
|
||||||
anthropicprovider "github.com/sipeed/picoclaw/pkg/providers/anthropic"
|
anthropicprovider "github.com/sipeed/picoclaw/pkg/providers/anthropic"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -22,19 +23,19 @@ func TestClaudeProvider_ChatRoundTrip(t *testing.T) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var reqBody map[string]interface{}
|
var reqBody map[string]any
|
||||||
json.NewDecoder(r.Body).Decode(&reqBody)
|
json.NewDecoder(r.Body).Decode(&reqBody)
|
||||||
|
|
||||||
resp := map[string]interface{}{
|
resp := map[string]any{
|
||||||
"id": "msg_test",
|
"id": "msg_test",
|
||||||
"type": "message",
|
"type": "message",
|
||||||
"role": "assistant",
|
"role": "assistant",
|
||||||
"model": reqBody["model"],
|
"model": reqBody["model"],
|
||||||
"stop_reason": "end_turn",
|
"stop_reason": "end_turn",
|
||||||
"content": []map[string]interface{}{
|
"content": []map[string]any{
|
||||||
{"type": "text", "text": "Hello! How can I help you?"},
|
{"type": "text", "text": "Hello! How can I help you?"},
|
||||||
},
|
},
|
||||||
"usage": map[string]interface{}{
|
"usage": map[string]any{
|
||||||
"input_tokens": 15,
|
"input_tokens": 15,
|
||||||
"output_tokens": 8,
|
"output_tokens": 8,
|
||||||
},
|
},
|
||||||
|
|
@ -48,7 +49,9 @@ func TestClaudeProvider_ChatRoundTrip(t *testing.T) {
|
||||||
provider := newClaudeProviderWithDelegate(delegate)
|
provider := newClaudeProviderWithDelegate(delegate)
|
||||||
|
|
||||||
messages := []Message{{Role: "user", Content: "Hello"}}
|
messages := []Message{{Role: "user", Content: "Hello"}}
|
||||||
resp, err := provider.Chat(t.Context(), messages, nil, "claude-sonnet-4-5-20250929", map[string]interface{}{"max_tokens": 1024})
|
resp, err := provider.Chat(
|
||||||
|
t.Context(), messages, nil, "claude-sonnet-4-5-20250929", map[string]any{"max_tokens": 1024},
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Chat() error: %v", err)
|
t.Fatalf("Chat() error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,9 @@ func CreateCodexCliTokenSource() func() (string, string, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if time.Now().After(expiresAt) {
|
if time.Now().After(expiresAt) {
|
||||||
return "", "", fmt.Errorf("codex cli credentials expired (auth.json last modified > 1h ago). Run: codex login")
|
return "", "", fmt.Errorf(
|
||||||
|
"codex cli credentials expired (auth.json last modified > 1h ago). Run: codex login",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return token, accountID, nil
|
return token, accountID, nil
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ func TestReadCodexCliCredentials_Valid(t *testing.T) {
|
||||||
"account_id": "org-test123"
|
"account_id": "org-test123"
|
||||||
}
|
}
|
||||||
}`
|
}`
|
||||||
if err := os.WriteFile(authPath, []byte(authJSON), 0600); err != nil {
|
if err := os.WriteFile(authPath, []byte(authJSON), 0o600); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -58,7 +58,7 @@ func TestReadCodexCliCredentials_EmptyToken(t *testing.T) {
|
||||||
authPath := filepath.Join(tmpDir, "auth.json")
|
authPath := filepath.Join(tmpDir, "auth.json")
|
||||||
|
|
||||||
authJSON := `{"tokens": {"access_token": "", "refresh_token": "r", "account_id": "a"}}`
|
authJSON := `{"tokens": {"access_token": "", "refresh_token": "r", "account_id": "a"}}`
|
||||||
if err := os.WriteFile(authPath, []byte(authJSON), 0600); err != nil {
|
if err := os.WriteFile(authPath, []byte(authJSON), 0o600); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -74,7 +74,7 @@ func TestReadCodexCliCredentials_InvalidJSON(t *testing.T) {
|
||||||
tmpDir := t.TempDir()
|
tmpDir := t.TempDir()
|
||||||
authPath := filepath.Join(tmpDir, "auth.json")
|
authPath := filepath.Join(tmpDir, "auth.json")
|
||||||
|
|
||||||
if err := os.WriteFile(authPath, []byte("not json"), 0600); err != nil {
|
if err := os.WriteFile(authPath, []byte("not json"), 0o600); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -91,7 +91,7 @@ func TestReadCodexCliCredentials_NoAccountID(t *testing.T) {
|
||||||
authPath := filepath.Join(tmpDir, "auth.json")
|
authPath := filepath.Join(tmpDir, "auth.json")
|
||||||
|
|
||||||
authJSON := `{"tokens": {"access_token": "tok123", "refresh_token": "ref456"}}`
|
authJSON := `{"tokens": {"access_token": "tok123", "refresh_token": "ref456"}}`
|
||||||
if err := os.WriteFile(authPath, []byte(authJSON), 0600); err != nil {
|
if err := os.WriteFile(authPath, []byte(authJSON), 0o600); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -112,12 +112,12 @@ func TestReadCodexCliCredentials_NoAccountID(t *testing.T) {
|
||||||
func TestReadCodexCliCredentials_CodexHomeEnv(t *testing.T) {
|
func TestReadCodexCliCredentials_CodexHomeEnv(t *testing.T) {
|
||||||
tmpDir := t.TempDir()
|
tmpDir := t.TempDir()
|
||||||
customDir := filepath.Join(tmpDir, "custom-codex")
|
customDir := filepath.Join(tmpDir, "custom-codex")
|
||||||
if err := os.MkdirAll(customDir, 0755); err != nil {
|
if err := os.MkdirAll(customDir, 0o755); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
authJSON := `{"tokens": {"access_token": "custom-token", "refresh_token": "r"}}`
|
authJSON := `{"tokens": {"access_token": "custom-token", "refresh_token": "r"}}`
|
||||||
if err := os.WriteFile(filepath.Join(customDir, "auth.json"), []byte(authJSON), 0600); err != nil {
|
if err := os.WriteFile(filepath.Join(customDir, "auth.json"), []byte(authJSON), 0o600); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -137,7 +137,7 @@ func TestCreateCodexCliTokenSource_Valid(t *testing.T) {
|
||||||
authPath := filepath.Join(tmpDir, "auth.json")
|
authPath := filepath.Join(tmpDir, "auth.json")
|
||||||
|
|
||||||
authJSON := `{"tokens": {"access_token": "fresh-token", "refresh_token": "r", "account_id": "acc"}}`
|
authJSON := `{"tokens": {"access_token": "fresh-token", "refresh_token": "r", "account_id": "acc"}}`
|
||||||
if err := os.WriteFile(authPath, []byte(authJSON), 0600); err != nil {
|
if err := os.WriteFile(authPath, []byte(authJSON), 0o600); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -161,7 +161,7 @@ func TestCreateCodexCliTokenSource_Expired(t *testing.T) {
|
||||||
authPath := filepath.Join(tmpDir, "auth.json")
|
authPath := filepath.Join(tmpDir, "auth.json")
|
||||||
|
|
||||||
authJSON := `{"tokens": {"access_token": "old-token", "refresh_token": "r"}}`
|
authJSON := `{"tokens": {"access_token": "old-token", "refresh_token": "r"}}`
|
||||||
if err := os.WriteFile(authPath, []byte(authJSON), 0600); err != nil {
|
if err := os.WriteFile(authPath, []byte(authJSON), 0o600); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,9 @@ func NewCodexCliProvider(workspace string) *CodexCliProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Chat implements LLMProvider.Chat by executing the codex CLI in non-interactive mode.
|
// Chat implements LLMProvider.Chat by executing the codex CLI in non-interactive mode.
|
||||||
func (p *CodexCliProvider) Chat(ctx context.Context, messages []Message, tools []ToolDefinition, model string, options map[string]interface{}) (*LLMResponse, error) {
|
func (p *CodexCliProvider) Chat(
|
||||||
|
ctx context.Context, messages []Message, tools []ToolDefinition, model string, options map[string]any,
|
||||||
|
) (*LLMResponse, error) {
|
||||||
if p.command == "" {
|
if p.command == "" {
|
||||||
return nil, fmt.Errorf("codex command not configured")
|
return nil, fmt.Errorf("codex command not configured")
|
||||||
}
|
}
|
||||||
|
|
@ -133,7 +135,9 @@ func (p *CodexCliProvider) buildToolsPrompt(tools []ToolDefinition) string {
|
||||||
sb.WriteString("## Available Tools\n\n")
|
sb.WriteString("## Available Tools\n\n")
|
||||||
sb.WriteString("When you need to use a tool, respond with ONLY a JSON object:\n\n")
|
sb.WriteString("When you need to use a tool, respond with ONLY a JSON object:\n\n")
|
||||||
sb.WriteString("```json\n")
|
sb.WriteString("```json\n")
|
||||||
sb.WriteString(`{"tool_calls":[{"id":"call_xxx","type":"function","function":{"name":"tool_name","arguments":"{...}"}}]}`)
|
sb.WriteString(
|
||||||
|
`{"tool_calls":[{"id":"call_xxx","type":"function","function":{"name":"tool_name","arguments":"{...}"}}]}`,
|
||||||
|
)
|
||||||
sb.WriteString("\n```\n\n")
|
sb.WriteString("\n```\n\n")
|
||||||
sb.WriteString("CRITICAL: The 'arguments' field MUST be a JSON-encoded STRING.\n\n")
|
sb.WriteString("CRITICAL: The 'arguments' field MUST be a JSON-encoded STRING.\n\n")
|
||||||
sb.WriteString("### Tool Definitions:\n\n")
|
sb.WriteString("### Tool Definitions:\n\n")
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,6 @@ func TestIntegration_RealCodexCLI(t *testing.T) {
|
||||||
resp, err := p.Chat(ctx, []Message{
|
resp, err := p.Chat(ctx, []Message{
|
||||||
{Role: "user", Content: "Respond with only the word 'pong'. Nothing else."},
|
{Role: "user", Content: "Respond with only the word 'pong'. Nothing else."},
|
||||||
}, nil, "", nil)
|
}, nil, "", nil)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Chat() with real CLI error = %v", err)
|
t.Fatalf("Chat() with real CLI error = %v", err)
|
||||||
}
|
}
|
||||||
|
|
@ -64,7 +63,6 @@ func TestIntegration_RealCodexCLI_WithSystemPrompt(t *testing.T) {
|
||||||
{Role: "system", Content: "You are a calculator. Only respond with numbers. No text."},
|
{Role: "system", Content: "You are a calculator. Only respond with numbers. No text."},
|
||||||
{Role: "user", Content: "What is 2+2?"},
|
{Role: "user", Content: "What is 2+2?"},
|
||||||
}, nil, "", nil)
|
}, nil, "", nil)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Chat() error = %v", err)
|
t.Fatalf("Chat() error = %v", err)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -292,10 +292,10 @@ func TestBuildPrompt_WithTools(t *testing.T) {
|
||||||
Function: ToolFunctionDefinition{
|
Function: ToolFunctionDefinition{
|
||||||
Name: "get_weather",
|
Name: "get_weather",
|
||||||
Description: "Get current weather",
|
Description: "Get current weather",
|
||||||
Parameters: map[string]interface{}{
|
Parameters: map[string]any{
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": map[string]interface{}{
|
"properties": map[string]any{
|
||||||
"city": map[string]interface{}{"type": "string"},
|
"city": map[string]any{"type": "string"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -409,7 +409,7 @@ func createMockCodexCLI(t *testing.T, events []string) string {
|
||||||
sb.WriteString(fmt.Sprintf("echo '%s'\n", event))
|
sb.WriteString(fmt.Sprintf("echo '%s'\n", event))
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := os.WriteFile(scriptPath, []byte(sb.String()), 0755); err != nil {
|
if err := os.WriteFile(scriptPath, []byte(sb.String()), 0o755); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
return scriptPath
|
return scriptPath
|
||||||
|
|
@ -480,7 +480,7 @@ echo "$@" > "` + filepath.Join(tmpDir, "args.txt") + `"
|
||||||
echo '{"type":"item.completed","item":{"id":"1","type":"agent_message","text":"ok"}}'
|
echo '{"type":"item.completed","item":{"id":"1","type":"agent_message","text":"ok"}}'
|
||||||
echo '{"type":"turn.completed"}'`
|
echo '{"type":"turn.completed"}'`
|
||||||
|
|
||||||
if err := os.WriteFile(scriptPath, []byte(script), 0755); err != nil {
|
if err := os.WriteFile(scriptPath, []byte(script), 0o755); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -522,7 +522,7 @@ func TestCodexCliProvider_MockCLI_ContextCancel(t *testing.T) {
|
||||||
scriptPath := filepath.Join(tmpDir, "codex")
|
scriptPath := filepath.Join(tmpDir, "codex")
|
||||||
script := "#!/bin/bash\nsleep 60"
|
script := "#!/bin/bash\nsleep 60"
|
||||||
|
|
||||||
if err := os.WriteFile(scriptPath, []byte(script), 0755); err != nil {
|
if err := os.WriteFile(scriptPath, []byte(script), 0o755); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,12 +10,15 @@ import (
|
||||||
"github.com/openai/openai-go/v3"
|
"github.com/openai/openai-go/v3"
|
||||||
"github.com/openai/openai-go/v3/option"
|
"github.com/openai/openai-go/v3/option"
|
||||||
"github.com/openai/openai-go/v3/responses"
|
"github.com/openai/openai-go/v3/responses"
|
||||||
|
|
||||||
"github.com/sipeed/picoclaw/pkg/auth"
|
"github.com/sipeed/picoclaw/pkg/auth"
|
||||||
"github.com/sipeed/picoclaw/pkg/logger"
|
"github.com/sipeed/picoclaw/pkg/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
const codexDefaultModel = "gpt-5.2"
|
const (
|
||||||
const codexDefaultInstructions = "You are Codex, a coding assistant."
|
codexDefaultModel = "gpt-5.2"
|
||||||
|
codexDefaultInstructions = "You are Codex, a coding assistant."
|
||||||
|
)
|
||||||
|
|
||||||
type CodexProvider struct {
|
type CodexProvider struct {
|
||||||
client *openai.Client
|
client *openai.Client
|
||||||
|
|
@ -44,22 +47,30 @@ func NewCodexProvider(token, accountID string) *CodexProvider {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewCodexProviderWithTokenSource(token, accountID string, tokenSource func() (string, string, error)) *CodexProvider {
|
func NewCodexProviderWithTokenSource(
|
||||||
|
token, accountID string, tokenSource func() (string, string, error),
|
||||||
|
) *CodexProvider {
|
||||||
p := NewCodexProvider(token, accountID)
|
p := NewCodexProvider(token, accountID)
|
||||||
p.tokenSource = tokenSource
|
p.tokenSource = tokenSource
|
||||||
return p
|
return p
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *CodexProvider) Chat(ctx context.Context, messages []Message, tools []ToolDefinition, model string, options map[string]interface{}) (*LLMResponse, error) {
|
func (p *CodexProvider) Chat(
|
||||||
|
ctx context.Context, messages []Message, tools []ToolDefinition, model string, options map[string]any,
|
||||||
|
) (*LLMResponse, error) {
|
||||||
var opts []option.RequestOption
|
var opts []option.RequestOption
|
||||||
accountID := p.accountID
|
accountID := p.accountID
|
||||||
resolvedModel, fallbackReason := resolveCodexModel(model)
|
resolvedModel, fallbackReason := resolveCodexModel(model)
|
||||||
if fallbackReason != "" {
|
if fallbackReason != "" {
|
||||||
logger.WarnCF("provider.codex", "Requested model is not compatible with Codex backend, using fallback", map[string]interface{}{
|
logger.WarnCF(
|
||||||
"requested_model": model,
|
"provider.codex",
|
||||||
"resolved_model": resolvedModel,
|
"Requested model is not compatible with Codex backend, using fallback",
|
||||||
"reason": fallbackReason,
|
map[string]any{
|
||||||
})
|
"requested_model": model,
|
||||||
|
"resolved_model": resolvedModel,
|
||||||
|
"reason": fallbackReason,
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
if p.tokenSource != nil {
|
if p.tokenSource != nil {
|
||||||
tok, accID, err := p.tokenSource()
|
tok, accID, err := p.tokenSource()
|
||||||
|
|
@ -74,10 +85,14 @@ func (p *CodexProvider) Chat(ctx context.Context, messages []Message, tools []To
|
||||||
if accountID != "" {
|
if accountID != "" {
|
||||||
opts = append(opts, option.WithHeader("Chatgpt-Account-Id", accountID))
|
opts = append(opts, option.WithHeader("Chatgpt-Account-Id", accountID))
|
||||||
} else {
|
} else {
|
||||||
logger.WarnCF("provider.codex", "No account id found for Codex request; backend may reject with 400", map[string]interface{}{
|
logger.WarnCF(
|
||||||
"requested_model": model,
|
"provider.codex",
|
||||||
"resolved_model": resolvedModel,
|
"No account id found for Codex request; backend may reject with 400",
|
||||||
})
|
map[string]any{
|
||||||
|
"requested_model": model,
|
||||||
|
"resolved_model": resolvedModel,
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
params := buildCodexParams(messages, tools, resolvedModel, options, p.enableWebSearch)
|
params := buildCodexParams(messages, tools, resolvedModel, options, p.enableWebSearch)
|
||||||
|
|
@ -98,7 +113,7 @@ func (p *CodexProvider) Chat(ctx context.Context, messages []Message, tools []To
|
||||||
}
|
}
|
||||||
err := stream.Err()
|
err := stream.Err()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fields := map[string]interface{}{
|
fields := map[string]any{
|
||||||
"requested_model": model,
|
"requested_model": model,
|
||||||
"resolved_model": resolvedModel,
|
"resolved_model": resolvedModel,
|
||||||
"messages_count": len(messages),
|
"messages_count": len(messages),
|
||||||
|
|
@ -124,7 +139,7 @@ func (p *CodexProvider) Chat(ctx context.Context, messages []Message, tools []To
|
||||||
return nil, fmt.Errorf("codex API call: %w", err)
|
return nil, fmt.Errorf("codex API call: %w", err)
|
||||||
}
|
}
|
||||||
if resp == nil {
|
if resp == nil {
|
||||||
fields := map[string]interface{}{
|
fields := map[string]any{
|
||||||
"requested_model": model,
|
"requested_model": model,
|
||||||
"resolved_model": resolvedModel,
|
"resolved_model": resolvedModel,
|
||||||
"messages_count": len(messages),
|
"messages_count": len(messages),
|
||||||
|
|
@ -184,7 +199,9 @@ func resolveCodexModel(model string) (string, string) {
|
||||||
return codexDefaultModel, "unsupported model family"
|
return codexDefaultModel, "unsupported model family"
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildCodexParams(messages []Message, tools []ToolDefinition, model string, options map[string]interface{}, enableWebSearch bool) responses.ResponseNewParams {
|
func buildCodexParams(
|
||||||
|
messages []Message, tools []ToolDefinition, model string, options map[string]any, enableWebSearch bool,
|
||||||
|
) responses.ResponseNewParams {
|
||||||
var inputItems responses.ResponseInputParam
|
var inputItems responses.ResponseInputParam
|
||||||
var instructions string
|
var instructions string
|
||||||
|
|
||||||
|
|
@ -197,7 +214,9 @@ func buildCodexParams(messages []Message, tools []ToolDefinition, model string,
|
||||||
inputItems = append(inputItems, responses.ResponseInputItemUnionParam{
|
inputItems = append(inputItems, responses.ResponseInputItemUnionParam{
|
||||||
OfFunctionCallOutput: &responses.ResponseInputItemFunctionCallOutputParam{
|
OfFunctionCallOutput: &responses.ResponseInputItemFunctionCallOutputParam{
|
||||||
CallID: msg.ToolCallID,
|
CallID: msg.ToolCallID,
|
||||||
Output: responses.ResponseInputItemFunctionCallOutputOutputUnionParam{OfString: openai.Opt(msg.Content)},
|
Output: responses.ResponseInputItemFunctionCallOutputOutputUnionParam{
|
||||||
|
OfString: openai.Opt(msg.Content),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -221,7 +240,7 @@ func buildCodexParams(messages []Message, tools []ToolDefinition, model string,
|
||||||
for _, tc := range msg.ToolCalls {
|
for _, tc := range msg.ToolCalls {
|
||||||
name, args, ok := resolveCodexToolCall(tc)
|
name, args, ok := resolveCodexToolCall(tc)
|
||||||
if !ok {
|
if !ok {
|
||||||
logger.WarnCF("provider.codex", "Skipping invalid tool call in history", map[string]interface{}{
|
logger.WarnCF("provider.codex", "Skipping invalid tool call in history", map[string]any{
|
||||||
"call_id": tc.ID,
|
"call_id": tc.ID,
|
||||||
})
|
})
|
||||||
continue
|
continue
|
||||||
|
|
@ -246,7 +265,9 @@ func buildCodexParams(messages []Message, tools []ToolDefinition, model string,
|
||||||
inputItems = append(inputItems, responses.ResponseInputItemUnionParam{
|
inputItems = append(inputItems, responses.ResponseInputItemUnionParam{
|
||||||
OfFunctionCallOutput: &responses.ResponseInputItemFunctionCallOutputParam{
|
OfFunctionCallOutput: &responses.ResponseInputItemFunctionCallOutputParam{
|
||||||
CallID: msg.ToolCallID,
|
CallID: msg.ToolCallID,
|
||||||
Output: responses.ResponseInputItemFunctionCallOutputOutputUnionParam{OfString: openai.Opt(msg.Content)},
|
Output: responses.ResponseInputItemFunctionCallOutputOutputUnionParam{
|
||||||
|
OfString: openai.Opt(msg.Content),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -341,9 +362,9 @@ func parseCodexResponse(resp *responses.Response) *LLMResponse {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case "function_call":
|
case "function_call":
|
||||||
var args map[string]interface{}
|
var args map[string]any
|
||||||
if err := json.Unmarshal([]byte(item.Arguments), &args); err != nil {
|
if err := json.Unmarshal([]byte(item.Arguments), &args); err != nil {
|
||||||
args = map[string]interface{}{"raw": item.Arguments}
|
args = map[string]any{"raw": item.Arguments}
|
||||||
}
|
}
|
||||||
toolCalls = append(toolCalls, ToolCall{
|
toolCalls = append(toolCalls, ToolCall{
|
||||||
ID: item.CallID,
|
ID: item.CallID,
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ func TestBuildCodexParams_BasicMessage(t *testing.T) {
|
||||||
messages := []Message{
|
messages := []Message{
|
||||||
{Role: "user", Content: "Hello"},
|
{Role: "user", Content: "Hello"},
|
||||||
}
|
}
|
||||||
params := buildCodexParams(messages, nil, "gpt-4o", map[string]interface{}{
|
params := buildCodexParams(messages, nil, "gpt-4o", map[string]any{
|
||||||
"max_tokens": 2048,
|
"max_tokens": 2048,
|
||||||
"temperature": 0.7,
|
"temperature": 0.7,
|
||||||
}, true)
|
}, true)
|
||||||
|
|
@ -39,7 +39,7 @@ func TestBuildCodexParams_SystemAsInstructions(t *testing.T) {
|
||||||
{Role: "system", Content: "You are helpful"},
|
{Role: "system", Content: "You are helpful"},
|
||||||
{Role: "user", Content: "Hi"},
|
{Role: "user", Content: "Hi"},
|
||||||
}
|
}
|
||||||
params := buildCodexParams(messages, nil, "gpt-4o", map[string]interface{}{}, true)
|
params := buildCodexParams(messages, nil, "gpt-4o", map[string]any{}, true)
|
||||||
if !params.Instructions.Valid() {
|
if !params.Instructions.Valid() {
|
||||||
t.Fatal("Instructions should be set")
|
t.Fatal("Instructions should be set")
|
||||||
}
|
}
|
||||||
|
|
@ -54,12 +54,12 @@ func TestBuildCodexParams_ToolCallConversation(t *testing.T) {
|
||||||
{
|
{
|
||||||
Role: "assistant",
|
Role: "assistant",
|
||||||
ToolCalls: []ToolCall{
|
ToolCalls: []ToolCall{
|
||||||
{ID: "call_1", Name: "get_weather", Arguments: map[string]interface{}{"city": "SF"}},
|
{ID: "call_1", Name: "get_weather", Arguments: map[string]any{"city": "SF"}},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{Role: "tool", Content: `{"temp": 72}`, ToolCallID: "call_1"},
|
{Role: "tool", Content: `{"temp": 72}`, ToolCallID: "call_1"},
|
||||||
}
|
}
|
||||||
params := buildCodexParams(messages, nil, "gpt-4o", map[string]interface{}{}, false)
|
params := buildCodexParams(messages, nil, "gpt-4o", map[string]any{}, false)
|
||||||
if params.Input.OfInputItemList == nil {
|
if params.Input.OfInputItemList == nil {
|
||||||
t.Fatal("Input.OfInputItemList should not be nil")
|
t.Fatal("Input.OfInputItemList should not be nil")
|
||||||
}
|
}
|
||||||
|
|
@ -87,7 +87,7 @@ func TestBuildCodexParams_ToolCallFunctionFallback(t *testing.T) {
|
||||||
{Role: "tool", Content: "ok", ToolCallID: "call_1"},
|
{Role: "tool", Content: "ok", ToolCallID: "call_1"},
|
||||||
}
|
}
|
||||||
|
|
||||||
params := buildCodexParams(messages, nil, "gpt-4o", map[string]interface{}{}, false)
|
params := buildCodexParams(messages, nil, "gpt-4o", map[string]any{}, false)
|
||||||
if params.Input.OfInputItemList == nil {
|
if params.Input.OfInputItemList == nil {
|
||||||
t.Fatal("Input.OfInputItemList should not be nil")
|
t.Fatal("Input.OfInputItemList should not be nil")
|
||||||
}
|
}
|
||||||
|
|
@ -114,16 +114,16 @@ func TestBuildCodexParams_WithTools(t *testing.T) {
|
||||||
Function: ToolFunctionDefinition{
|
Function: ToolFunctionDefinition{
|
||||||
Name: "get_weather",
|
Name: "get_weather",
|
||||||
Description: "Get weather",
|
Description: "Get weather",
|
||||||
Parameters: map[string]interface{}{
|
Parameters: map[string]any{
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": map[string]interface{}{
|
"properties": map[string]any{
|
||||||
"city": map[string]interface{}{"type": "string"},
|
"city": map[string]any{"type": "string"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
params := buildCodexParams([]Message{{Role: "user", Content: "Hi"}}, tools, "gpt-4o", map[string]interface{}{}, false)
|
params := buildCodexParams([]Message{{Role: "user", Content: "Hi"}}, tools, "gpt-4o", map[string]any{}, false)
|
||||||
if len(params.Tools) != 1 {
|
if len(params.Tools) != 1 {
|
||||||
t.Fatalf("len(Tools) = %d, want 1", len(params.Tools))
|
t.Fatalf("len(Tools) = %d, want 1", len(params.Tools))
|
||||||
}
|
}
|
||||||
|
|
@ -136,14 +136,14 @@ func TestBuildCodexParams_WithTools(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBuildCodexParams_StoreIsFalse(t *testing.T) {
|
func TestBuildCodexParams_StoreIsFalse(t *testing.T) {
|
||||||
params := buildCodexParams([]Message{{Role: "user", Content: "Hi"}}, nil, "gpt-4o", map[string]interface{}{}, false)
|
params := buildCodexParams([]Message{{Role: "user", Content: "Hi"}}, nil, "gpt-4o", map[string]any{}, false)
|
||||||
if !params.Store.Valid() || params.Store.Or(true) != false {
|
if !params.Store.Valid() || params.Store.Or(true) != false {
|
||||||
t.Error("Store should be explicitly set to false")
|
t.Error("Store should be explicitly set to false")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBuildCodexParams_DefaultWebSearchEnabled(t *testing.T) {
|
func TestBuildCodexParams_DefaultWebSearchEnabled(t *testing.T) {
|
||||||
params := buildCodexParams([]Message{{Role: "user", Content: "Hi"}}, nil, "gpt-4o", map[string]interface{}{}, true)
|
params := buildCodexParams([]Message{{Role: "user", Content: "Hi"}}, nil, "gpt-4o", map[string]any{}, true)
|
||||||
if len(params.Tools) != 1 {
|
if len(params.Tools) != 1 {
|
||||||
t.Fatalf("len(Tools) = %d, want 1", len(params.Tools))
|
t.Fatalf("len(Tools) = %d, want 1", len(params.Tools))
|
||||||
}
|
}
|
||||||
|
|
@ -151,7 +151,11 @@ func TestBuildCodexParams_DefaultWebSearchEnabled(t *testing.T) {
|
||||||
t.Fatal("Tool should include built-in web_search")
|
t.Fatal("Tool should include built-in web_search")
|
||||||
}
|
}
|
||||||
if params.Tools[0].OfWebSearch.Type != responses.WebSearchToolTypeWebSearch {
|
if params.Tools[0].OfWebSearch.Type != responses.WebSearchToolTypeWebSearch {
|
||||||
t.Errorf("Web search tool type = %q, want %q", params.Tools[0].OfWebSearch.Type, responses.WebSearchToolTypeWebSearch)
|
t.Errorf(
|
||||||
|
"Web search tool type = %q, want %q",
|
||||||
|
params.Tools[0].OfWebSearch.Type,
|
||||||
|
responses.WebSearchToolTypeWebSearch,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -162,7 +166,7 @@ func TestBuildCodexParams_WebSearchFunctionReplacedWithBuiltin(t *testing.T) {
|
||||||
Function: ToolFunctionDefinition{
|
Function: ToolFunctionDefinition{
|
||||||
Name: "web_search",
|
Name: "web_search",
|
||||||
Description: "local web search",
|
Description: "local web search",
|
||||||
Parameters: map[string]interface{}{
|
Parameters: map[string]any{
|
||||||
"type": "object",
|
"type": "object",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -172,14 +176,14 @@ func TestBuildCodexParams_WebSearchFunctionReplacedWithBuiltin(t *testing.T) {
|
||||||
Function: ToolFunctionDefinition{
|
Function: ToolFunctionDefinition{
|
||||||
Name: "read_file",
|
Name: "read_file",
|
||||||
Description: "read file",
|
Description: "read file",
|
||||||
Parameters: map[string]interface{}{
|
Parameters: map[string]any{
|
||||||
"type": "object",
|
"type": "object",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
params := buildCodexParams([]Message{{Role: "user", Content: "Hi"}}, tools, "gpt-4o", map[string]interface{}{}, true)
|
params := buildCodexParams([]Message{{Role: "user", Content: "Hi"}}, tools, "gpt-4o", map[string]any{}, true)
|
||||||
if len(params.Tools) != 2 {
|
if len(params.Tools) != 2 {
|
||||||
t.Fatalf("len(Tools) = %d, want 2", len(params.Tools))
|
t.Fatalf("len(Tools) = %d, want 2", len(params.Tools))
|
||||||
}
|
}
|
||||||
|
|
@ -296,7 +300,7 @@ func TestCodexProvider_ChatRoundTrip(t *testing.T) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var reqBody map[string]interface{}
|
var reqBody map[string]any
|
||||||
if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil {
|
||||||
http.Error(w, "invalid json", http.StatusBadRequest)
|
http.Error(w, "invalid json", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
|
|
@ -309,38 +313,38 @@ func TestCodexProvider_ChatRoundTrip(t *testing.T) {
|
||||||
http.Error(w, "max_output_tokens is not supported", http.StatusBadRequest)
|
http.Error(w, "max_output_tokens is not supported", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
toolsAny, ok := reqBody["tools"].([]interface{})
|
toolsAny, ok := reqBody["tools"].([]any)
|
||||||
if !ok || len(toolsAny) != 1 {
|
if !ok || len(toolsAny) != 1 {
|
||||||
http.Error(w, "missing default web search tool", http.StatusBadRequest)
|
http.Error(w, "missing default web search tool", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
toolObj, ok := toolsAny[0].(map[string]interface{})
|
toolObj, ok := toolsAny[0].(map[string]any)
|
||||||
if !ok || toolObj["type"] != "web_search" {
|
if !ok || toolObj["type"] != "web_search" {
|
||||||
http.Error(w, "expected web_search tool", http.StatusBadRequest)
|
http.Error(w, "expected web_search tool", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
resp := map[string]interface{}{
|
resp := map[string]any{
|
||||||
"id": "resp_test",
|
"id": "resp_test",
|
||||||
"object": "response",
|
"object": "response",
|
||||||
"status": "completed",
|
"status": "completed",
|
||||||
"output": []map[string]interface{}{
|
"output": []map[string]any{
|
||||||
{
|
{
|
||||||
"id": "msg_1",
|
"id": "msg_1",
|
||||||
"type": "message",
|
"type": "message",
|
||||||
"role": "assistant",
|
"role": "assistant",
|
||||||
"status": "completed",
|
"status": "completed",
|
||||||
"content": []map[string]interface{}{
|
"content": []map[string]any{
|
||||||
{"type": "output_text", "text": "Hi from Codex!"},
|
{"type": "output_text", "text": "Hi from Codex!"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"usage": map[string]interface{}{
|
"usage": map[string]any{
|
||||||
"input_tokens": 12,
|
"input_tokens": 12,
|
||||||
"output_tokens": 6,
|
"output_tokens": 6,
|
||||||
"total_tokens": 18,
|
"total_tokens": 18,
|
||||||
"input_tokens_details": map[string]interface{}{"cached_tokens": 0},
|
"input_tokens_details": map[string]any{"cached_tokens": 0},
|
||||||
"output_tokens_details": map[string]interface{}{"reasoning_tokens": 0},
|
"output_tokens_details": map[string]any{"reasoning_tokens": 0},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
writeCompletedSSE(w, resp)
|
writeCompletedSSE(w, resp)
|
||||||
|
|
@ -351,7 +355,7 @@ func TestCodexProvider_ChatRoundTrip(t *testing.T) {
|
||||||
provider.client = createOpenAITestClient(server.URL, "test-token", "acc-123")
|
provider.client = createOpenAITestClient(server.URL, "test-token", "acc-123")
|
||||||
|
|
||||||
messages := []Message{{Role: "user", Content: "Hello"}}
|
messages := []Message{{Role: "user", Content: "Hello"}}
|
||||||
resp, err := provider.Chat(t.Context(), messages, nil, "gpt-4o", map[string]interface{}{"max_tokens": 1024})
|
resp, err := provider.Chat(t.Context(), messages, nil, "gpt-4o", map[string]any{"max_tokens": 1024})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Chat() error: %v", err)
|
t.Fatalf("Chat() error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
@ -373,7 +377,7 @@ func TestCodexProvider_ChatRoundTrip_WebSearchDisabled(t *testing.T) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var reqBody map[string]interface{}
|
var reqBody map[string]any
|
||||||
if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil {
|
||||||
http.Error(w, "invalid json", http.StatusBadRequest)
|
http.Error(w, "invalid json", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
|
|
@ -383,27 +387,27 @@ func TestCodexProvider_ChatRoundTrip_WebSearchDisabled(t *testing.T) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
resp := map[string]interface{}{
|
resp := map[string]any{
|
||||||
"id": "resp_test",
|
"id": "resp_test",
|
||||||
"object": "response",
|
"object": "response",
|
||||||
"status": "completed",
|
"status": "completed",
|
||||||
"output": []map[string]interface{}{
|
"output": []map[string]any{
|
||||||
{
|
{
|
||||||
"id": "msg_1",
|
"id": "msg_1",
|
||||||
"type": "message",
|
"type": "message",
|
||||||
"role": "assistant",
|
"role": "assistant",
|
||||||
"status": "completed",
|
"status": "completed",
|
||||||
"content": []map[string]interface{}{
|
"content": []map[string]any{
|
||||||
{"type": "output_text", "text": "Hi from Codex!"},
|
{"type": "output_text", "text": "Hi from Codex!"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"usage": map[string]interface{}{
|
"usage": map[string]any{
|
||||||
"input_tokens": 4,
|
"input_tokens": 4,
|
||||||
"output_tokens": 3,
|
"output_tokens": 3,
|
||||||
"total_tokens": 7,
|
"total_tokens": 7,
|
||||||
"input_tokens_details": map[string]interface{}{"cached_tokens": 0},
|
"input_tokens_details": map[string]any{"cached_tokens": 0},
|
||||||
"output_tokens_details": map[string]interface{}{"reasoning_tokens": 0},
|
"output_tokens_details": map[string]any{"reasoning_tokens": 0},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
writeCompletedSSE(w, resp)
|
writeCompletedSSE(w, resp)
|
||||||
|
|
@ -415,7 +419,7 @@ func TestCodexProvider_ChatRoundTrip_WebSearchDisabled(t *testing.T) {
|
||||||
provider.client = createOpenAITestClient(server.URL, "test-token", "acc-123")
|
provider.client = createOpenAITestClient(server.URL, "test-token", "acc-123")
|
||||||
|
|
||||||
messages := []Message{{Role: "user", Content: "Hello"}}
|
messages := []Message{{Role: "user", Content: "Hello"}}
|
||||||
resp, err := provider.Chat(t.Context(), messages, nil, "gpt-4o", map[string]interface{}{})
|
resp, err := provider.Chat(t.Context(), messages, nil, "gpt-4o", map[string]any{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Chat() error: %v", err)
|
t.Fatalf("Chat() error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
@ -439,7 +443,7 @@ func TestCodexProvider_ChatRoundTrip_TokenSourceFallbackAccountID(t *testing.T)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var reqBody map[string]interface{}
|
var reqBody map[string]any
|
||||||
if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil {
|
||||||
http.Error(w, "invalid json", http.StatusBadRequest)
|
http.Error(w, "invalid json", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
|
|
@ -465,27 +469,27 @@ func TestCodexProvider_ChatRoundTrip_TokenSourceFallbackAccountID(t *testing.T)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
resp := map[string]interface{}{
|
resp := map[string]any{
|
||||||
"id": "resp_test",
|
"id": "resp_test",
|
||||||
"object": "response",
|
"object": "response",
|
||||||
"status": "completed",
|
"status": "completed",
|
||||||
"output": []map[string]interface{}{
|
"output": []map[string]any{
|
||||||
{
|
{
|
||||||
"id": "msg_1",
|
"id": "msg_1",
|
||||||
"type": "message",
|
"type": "message",
|
||||||
"role": "assistant",
|
"role": "assistant",
|
||||||
"status": "completed",
|
"status": "completed",
|
||||||
"content": []map[string]interface{}{
|
"content": []map[string]any{
|
||||||
{"type": "output_text", "text": "Hi from Codex!"},
|
{"type": "output_text", "text": "Hi from Codex!"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"usage": map[string]interface{}{
|
"usage": map[string]any{
|
||||||
"input_tokens": 8,
|
"input_tokens": 8,
|
||||||
"output_tokens": 4,
|
"output_tokens": 4,
|
||||||
"total_tokens": 12,
|
"total_tokens": 12,
|
||||||
"input_tokens_details": map[string]interface{}{"cached_tokens": 0},
|
"input_tokens_details": map[string]any{"cached_tokens": 0},
|
||||||
"output_tokens_details": map[string]interface{}{"reasoning_tokens": 0},
|
"output_tokens_details": map[string]any{"reasoning_tokens": 0},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
writeCompletedSSE(w, resp)
|
writeCompletedSSE(w, resp)
|
||||||
|
|
@ -499,7 +503,7 @@ func TestCodexProvider_ChatRoundTrip_TokenSourceFallbackAccountID(t *testing.T)
|
||||||
}
|
}
|
||||||
|
|
||||||
messages := []Message{{Role: "user", Content: "Hello"}}
|
messages := []Message{{Role: "user", Content: "Hello"}}
|
||||||
resp, err := provider.Chat(t.Context(), messages, nil, "gpt-4o", map[string]interface{}{"temperature": 0.7})
|
resp, err := provider.Chat(t.Context(), messages, nil, "gpt-4o", map[string]any{"temperature": 0.7})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Chat() error: %v", err)
|
t.Fatalf("Chat() error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
@ -515,7 +519,7 @@ func TestCodexProvider_ChatRoundTrip_ModelFallbackFromUnsupported(t *testing.T)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var reqBody map[string]interface{}
|
var reqBody map[string]any
|
||||||
if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil {
|
||||||
http.Error(w, "invalid json", http.StatusBadRequest)
|
http.Error(w, "invalid json", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
|
|
@ -533,27 +537,27 @@ func TestCodexProvider_ChatRoundTrip_ModelFallbackFromUnsupported(t *testing.T)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
resp := map[string]interface{}{
|
resp := map[string]any{
|
||||||
"id": "resp_test",
|
"id": "resp_test",
|
||||||
"object": "response",
|
"object": "response",
|
||||||
"status": "completed",
|
"status": "completed",
|
||||||
"output": []map[string]interface{}{
|
"output": []map[string]any{
|
||||||
{
|
{
|
||||||
"id": "msg_1",
|
"id": "msg_1",
|
||||||
"type": "message",
|
"type": "message",
|
||||||
"role": "assistant",
|
"role": "assistant",
|
||||||
"status": "completed",
|
"status": "completed",
|
||||||
"content": []map[string]interface{}{
|
"content": []map[string]any{
|
||||||
{"type": "output_text", "text": "Hi from Codex!"},
|
{"type": "output_text", "text": "Hi from Codex!"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"usage": map[string]interface{}{
|
"usage": map[string]any{
|
||||||
"input_tokens": 8,
|
"input_tokens": 8,
|
||||||
"output_tokens": 4,
|
"output_tokens": 4,
|
||||||
"total_tokens": 12,
|
"total_tokens": 12,
|
||||||
"input_tokens_details": map[string]interface{}{"cached_tokens": 0},
|
"input_tokens_details": map[string]any{"cached_tokens": 0},
|
||||||
"output_tokens_details": map[string]interface{}{"reasoning_tokens": 0},
|
"output_tokens_details": map[string]any{"reasoning_tokens": 0},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
writeCompletedSSE(w, resp)
|
writeCompletedSSE(w, resp)
|
||||||
|
|
@ -588,7 +592,12 @@ func TestResolveCodexModel(t *testing.T) {
|
||||||
wantFallback bool
|
wantFallback bool
|
||||||
}{
|
}{
|
||||||
{name: "empty", input: "", wantModel: codexDefaultModel, wantFallback: true},
|
{name: "empty", input: "", wantModel: codexDefaultModel, wantFallback: true},
|
||||||
{name: "unsupported namespace", input: "anthropic/claude-3.5", wantModel: codexDefaultModel, wantFallback: true},
|
{
|
||||||
|
name: "unsupported namespace",
|
||||||
|
input: "anthropic/claude-3.5",
|
||||||
|
wantModel: codexDefaultModel,
|
||||||
|
wantFallback: true,
|
||||||
|
},
|
||||||
{name: "non-openai prefixed", input: "glm-4.7", wantModel: codexDefaultModel, wantFallback: true},
|
{name: "non-openai prefixed", input: "glm-4.7", wantModel: codexDefaultModel, wantFallback: true},
|
||||||
{name: "openai prefix", input: "openai/gpt-5.2", wantModel: "gpt-5.2", wantFallback: false},
|
{name: "openai prefix", input: "openai/gpt-5.2", wantModel: "gpt-5.2", wantFallback: false},
|
||||||
{name: "direct gpt", input: "gpt-4o", wantModel: "gpt-4o", wantFallback: false},
|
{name: "direct gpt", input: "gpt-4o", wantModel: "gpt-4o", wantFallback: false},
|
||||||
|
|
@ -622,8 +631,8 @@ func createOpenAITestClient(baseURL, token, accountID string) *openai.Client {
|
||||||
return &c
|
return &c
|
||||||
}
|
}
|
||||||
|
|
||||||
func writeCompletedSSE(w http.ResponseWriter, response map[string]interface{}) {
|
func writeCompletedSSE(w http.ResponseWriter, response map[string]any) {
|
||||||
event := map[string]interface{}{
|
event := map[string]any{
|
||||||
"type": "response.completed",
|
"type": "response.completed",
|
||||||
"sequence_number": 1,
|
"sequence_number": 1,
|
||||||
"response": response,
|
"response": response,
|
||||||
|
|
|
||||||
|
|
@ -110,7 +110,11 @@ func (fc *FallbackChain) Execute(
|
||||||
Model: candidate.Model,
|
Model: candidate.Model,
|
||||||
Skipped: true,
|
Skipped: true,
|
||||||
Reason: FailoverRateLimit,
|
Reason: FailoverRateLimit,
|
||||||
Error: fmt.Errorf("provider %s in cooldown (%s remaining)", candidate.Provider, remaining.Round(time.Second)),
|
Error: fmt.Errorf(
|
||||||
|
"provider %s in cooldown (%s remaining)",
|
||||||
|
candidate.Provider,
|
||||||
|
remaining.Round(time.Second),
|
||||||
|
),
|
||||||
})
|
})
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -462,7 +462,13 @@ func TestResolveCandidates_EmptyPrimary(t *testing.T) {
|
||||||
func TestFallbackExhaustedError_Message(t *testing.T) {
|
func TestFallbackExhaustedError_Message(t *testing.T) {
|
||||||
e := &FallbackExhaustedError{
|
e := &FallbackExhaustedError{
|
||||||
Attempts: []FallbackAttempt{
|
Attempts: []FallbackAttempt{
|
||||||
{Provider: "openai", Model: "gpt-4", Error: errors.New("rate limited"), Reason: FailoverRateLimit, Duration: 500 * time.Millisecond},
|
{
|
||||||
|
Provider: "openai",
|
||||||
|
Model: "gpt-4",
|
||||||
|
Error: errors.New("rate limited"),
|
||||||
|
Reason: FailoverRateLimit,
|
||||||
|
Duration: 500 * time.Millisecond,
|
||||||
|
},
|
||||||
{Provider: "anthropic", Model: "claude", Skipped: true},
|
{Provider: "anthropic", Model: "claude", Skipped: true},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,9 @@ package providers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
json "encoding/json"
|
|
||||||
|
|
||||||
copilot "github.com/github/copilot-sdk/go"
|
copilot "github.com/github/copilot-sdk/go"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -17,7 +16,6 @@ type GitHubCopilotProvider struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewGitHubCopilotProvider(uri string, connectMode string, model string) (*GitHubCopilotProvider, error) {
|
func NewGitHubCopilotProvider(uri string, connectMode string, model string) (*GitHubCopilotProvider, error) {
|
||||||
|
|
||||||
var session *copilot.Session
|
var session *copilot.Session
|
||||||
if connectMode == "" {
|
if connectMode == "" {
|
||||||
connectMode = "grpc"
|
connectMode = "grpc"
|
||||||
|
|
@ -25,13 +23,15 @@ func NewGitHubCopilotProvider(uri string, connectMode string, model string) (*Gi
|
||||||
switch connectMode {
|
switch connectMode {
|
||||||
|
|
||||||
case "stdio":
|
case "stdio":
|
||||||
//todo
|
// todo
|
||||||
case "grpc":
|
case "grpc":
|
||||||
client := copilot.NewClient(&copilot.ClientOptions{
|
client := copilot.NewClient(&copilot.ClientOptions{
|
||||||
CLIUrl: uri,
|
CLIUrl: uri,
|
||||||
})
|
})
|
||||||
if err := client.Start(context.Background()); err != nil {
|
if err := client.Start(context.Background()); err != nil {
|
||||||
return nil, fmt.Errorf("Can't connect to Github Copilot, https://github.com/github/copilot-sdk/blob/main/docs/getting-started.md#connecting-to-an-external-cli-server for details")
|
return nil, fmt.Errorf(
|
||||||
|
"Can't connect to Github Copilot, https://github.com/github/copilot-sdk/blob/main/docs/getting-started.md#connecting-to-an-external-cli-server for details",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
defer client.Stop()
|
defer client.Stop()
|
||||||
session, _ = client.CreateSession(context.Background(), &copilot.SessionConfig{
|
session, _ = client.CreateSession(context.Background(), &copilot.SessionConfig{
|
||||||
|
|
@ -49,7 +49,9 @@ func NewGitHubCopilotProvider(uri string, connectMode string, model string) (*Gi
|
||||||
}
|
}
|
||||||
|
|
||||||
// Chat sends a chat request to GitHub Copilot
|
// Chat sends a chat request to GitHub Copilot
|
||||||
func (p *GitHubCopilotProvider) Chat(ctx context.Context, messages []Message, tools []ToolDefinition, model string, options map[string]interface{}) (*LLMResponse, error) {
|
func (p *GitHubCopilotProvider) Chat(
|
||||||
|
ctx context.Context, messages []Message, tools []ToolDefinition, model string, options map[string]any,
|
||||||
|
) (*LLMResponse, error) {
|
||||||
type tempMessage struct {
|
type tempMessage struct {
|
||||||
Role string `json:"role"`
|
Role string `json:"role"`
|
||||||
Content string `json:"content"`
|
Content string `json:"content"`
|
||||||
|
|
@ -73,10 +75,8 @@ func (p *GitHubCopilotProvider) Chat(ctx context.Context, messages []Message, to
|
||||||
FinishReason: "stop",
|
FinishReason: "stop",
|
||||||
Content: content,
|
Content: content,
|
||||||
}, nil
|
}, nil
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *GitHubCopilotProvider) GetDefaultModel() string {
|
func (p *GitHubCopilotProvider) GetDefaultModel() string {
|
||||||
|
|
||||||
return "gpt-4.1"
|
return "gpt-4.1"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,9 @@ func NewHTTPProvider(apiKey, apiBase, proxy string) *HTTPProvider {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *HTTPProvider) Chat(ctx context.Context, messages []Message, tools []ToolDefinition, model string, options map[string]interface{}) (*LLMResponse, error) {
|
func (p *HTTPProvider) Chat(
|
||||||
|
ctx context.Context, messages []Message, tools []ToolDefinition, model string, options map[string]any,
|
||||||
|
) (*LLMResponse, error) {
|
||||||
return p.delegate.Chat(ctx, messages, tools, model, options)
|
return p.delegate.Chat(ctx, messages, tools, model, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,13 +15,15 @@ import (
|
||||||
"github.com/sipeed/picoclaw/pkg/providers/protocoltypes"
|
"github.com/sipeed/picoclaw/pkg/providers/protocoltypes"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ToolCall = protocoltypes.ToolCall
|
type (
|
||||||
type FunctionCall = protocoltypes.FunctionCall
|
ToolCall = protocoltypes.ToolCall
|
||||||
type LLMResponse = protocoltypes.LLMResponse
|
FunctionCall = protocoltypes.FunctionCall
|
||||||
type UsageInfo = protocoltypes.UsageInfo
|
LLMResponse = protocoltypes.LLMResponse
|
||||||
type Message = protocoltypes.Message
|
UsageInfo = protocoltypes.UsageInfo
|
||||||
type ToolDefinition = protocoltypes.ToolDefinition
|
Message = protocoltypes.Message
|
||||||
type ToolFunctionDefinition = protocoltypes.ToolFunctionDefinition
|
ToolDefinition = protocoltypes.ToolDefinition
|
||||||
|
ToolFunctionDefinition = protocoltypes.ToolFunctionDefinition
|
||||||
|
)
|
||||||
|
|
||||||
type Provider struct {
|
type Provider struct {
|
||||||
apiKey string
|
apiKey string
|
||||||
|
|
@ -52,14 +54,20 @@ func NewProvider(apiKey, apiBase, proxy string) *Provider {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Provider) Chat(ctx context.Context, messages []Message, tools []ToolDefinition, model string, options map[string]interface{}) (*LLMResponse, error) {
|
func (p *Provider) Chat(
|
||||||
|
ctx context.Context,
|
||||||
|
messages []Message,
|
||||||
|
tools []ToolDefinition,
|
||||||
|
model string,
|
||||||
|
options map[string]any,
|
||||||
|
) (*LLMResponse, error) {
|
||||||
if p.apiBase == "" {
|
if p.apiBase == "" {
|
||||||
return nil, fmt.Errorf("API base not configured")
|
return nil, fmt.Errorf("API base not configured")
|
||||||
}
|
}
|
||||||
|
|
||||||
model = normalizeModel(model, p.apiBase)
|
model = normalizeModel(model, p.apiBase)
|
||||||
|
|
||||||
requestBody := map[string]interface{}{
|
requestBody := map[string]any{
|
||||||
"model": model,
|
"model": model,
|
||||||
"messages": messages,
|
"messages": messages,
|
||||||
}
|
}
|
||||||
|
|
@ -154,7 +162,7 @@ func parseResponse(body []byte) (*LLMResponse, error) {
|
||||||
choice := apiResponse.Choices[0]
|
choice := apiResponse.Choices[0]
|
||||||
toolCalls := make([]ToolCall, 0, len(choice.Message.ToolCalls))
|
toolCalls := make([]ToolCall, 0, len(choice.Message.ToolCalls))
|
||||||
for _, tc := range choice.Message.ToolCalls {
|
for _, tc := range choice.Message.ToolCalls {
|
||||||
arguments := make(map[string]interface{})
|
arguments := make(map[string]any)
|
||||||
name := ""
|
name := ""
|
||||||
|
|
||||||
if tc.Function != nil {
|
if tc.Function != nil {
|
||||||
|
|
@ -201,7 +209,7 @@ func normalizeModel(model, apiBase string) string {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func asInt(v interface{}) (int, bool) {
|
func asInt(v any) (int, bool) {
|
||||||
switch val := v.(type) {
|
switch val := v.(type) {
|
||||||
case int:
|
case int:
|
||||||
return val, true
|
return val, true
|
||||||
|
|
@ -216,7 +224,7 @@ func asInt(v interface{}) (int, bool) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func asFloat(v interface{}) (float64, bool) {
|
func asFloat(v any) (float64, bool) {
|
||||||
switch val := v.(type) {
|
switch val := v.(type) {
|
||||||
case float64:
|
case float64:
|
||||||
return val, true
|
return val, true
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestProviderChat_UsesMaxCompletionTokensForGLM(t *testing.T) {
|
func TestProviderChat_UsesMaxCompletionTokensForGLM(t *testing.T) {
|
||||||
var requestBody map[string]interface{}
|
var requestBody map[string]any
|
||||||
|
|
||||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.URL.Path != "/chat/completions" {
|
if r.URL.Path != "/chat/completions" {
|
||||||
|
|
@ -20,10 +20,10 @@ func TestProviderChat_UsesMaxCompletionTokensForGLM(t *testing.T) {
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
resp := map[string]interface{}{
|
resp := map[string]any{
|
||||||
"choices": []map[string]interface{}{
|
"choices": []map[string]any{
|
||||||
{
|
{
|
||||||
"message": map[string]interface{}{"content": "ok"},
|
"message": map[string]any{"content": "ok"},
|
||||||
"finish_reason": "stop",
|
"finish_reason": "stop",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -34,7 +34,13 @@ func TestProviderChat_UsesMaxCompletionTokensForGLM(t *testing.T) {
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
p := NewProvider("key", server.URL, "")
|
p := NewProvider("key", server.URL, "")
|
||||||
_, err := p.Chat(t.Context(), []Message{{Role: "user", Content: "hi"}}, nil, "glm-4.7", map[string]interface{}{"max_tokens": 1234})
|
_, err := p.Chat(
|
||||||
|
t.Context(),
|
||||||
|
[]Message{{Role: "user", Content: "hi"}},
|
||||||
|
nil,
|
||||||
|
"glm-4.7",
|
||||||
|
map[string]any{"max_tokens": 1234},
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Chat() error = %v", err)
|
t.Fatalf("Chat() error = %v", err)
|
||||||
}
|
}
|
||||||
|
|
@ -49,16 +55,16 @@ func TestProviderChat_UsesMaxCompletionTokensForGLM(t *testing.T) {
|
||||||
|
|
||||||
func TestProviderChat_ParsesToolCalls(t *testing.T) {
|
func TestProviderChat_ParsesToolCalls(t *testing.T) {
|
||||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
resp := map[string]interface{}{
|
resp := map[string]any{
|
||||||
"choices": []map[string]interface{}{
|
"choices": []map[string]any{
|
||||||
{
|
{
|
||||||
"message": map[string]interface{}{
|
"message": map[string]any{
|
||||||
"content": "",
|
"content": "",
|
||||||
"tool_calls": []map[string]interface{}{
|
"tool_calls": []map[string]any{
|
||||||
{
|
{
|
||||||
"id": "call_1",
|
"id": "call_1",
|
||||||
"type": "function",
|
"type": "function",
|
||||||
"function": map[string]interface{}{
|
"function": map[string]any{
|
||||||
"name": "get_weather",
|
"name": "get_weather",
|
||||||
"arguments": "{\"city\":\"SF\"}",
|
"arguments": "{\"city\":\"SF\"}",
|
||||||
},
|
},
|
||||||
|
|
@ -68,7 +74,7 @@ func TestProviderChat_ParsesToolCalls(t *testing.T) {
|
||||||
"finish_reason": "tool_calls",
|
"finish_reason": "tool_calls",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"usage": map[string]interface{}{
|
"usage": map[string]any{
|
||||||
"prompt_tokens": 10,
|
"prompt_tokens": 10,
|
||||||
"completion_tokens": 5,
|
"completion_tokens": 5,
|
||||||
"total_tokens": 15,
|
"total_tokens": 15,
|
||||||
|
|
@ -109,17 +115,17 @@ func TestProviderChat_HTTPError(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestProviderChat_StripsMoonshotPrefixAndNormalizesKimiTemperature(t *testing.T) {
|
func TestProviderChat_StripsMoonshotPrefixAndNormalizesKimiTemperature(t *testing.T) {
|
||||||
var requestBody map[string]interface{}
|
var requestBody map[string]any
|
||||||
|
|
||||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
if err := json.NewDecoder(r.Body).Decode(&requestBody); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(&requestBody); err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
resp := map[string]interface{}{
|
resp := map[string]any{
|
||||||
"choices": []map[string]interface{}{
|
"choices": []map[string]any{
|
||||||
{
|
{
|
||||||
"message": map[string]interface{}{"content": "ok"},
|
"message": map[string]any{"content": "ok"},
|
||||||
"finish_reason": "stop",
|
"finish_reason": "stop",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -135,7 +141,7 @@ func TestProviderChat_StripsMoonshotPrefixAndNormalizesKimiTemperature(t *testin
|
||||||
[]Message{{Role: "user", Content: "hi"}},
|
[]Message{{Role: "user", Content: "hi"}},
|
||||||
nil,
|
nil,
|
||||||
"moonshot/kimi-k2.5",
|
"moonshot/kimi-k2.5",
|
||||||
map[string]interface{}{"temperature": 0.3},
|
map[string]any{"temperature": 0.3},
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Chat() error = %v", err)
|
t.Fatalf("Chat() error = %v", err)
|
||||||
|
|
@ -174,17 +180,17 @@ func TestProviderChat_StripsGroqAndOllamaPrefixes(t *testing.T) {
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
var requestBody map[string]interface{}
|
var requestBody map[string]any
|
||||||
|
|
||||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
if err := json.NewDecoder(r.Body).Decode(&requestBody); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(&requestBody); err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
resp := map[string]interface{}{
|
resp := map[string]any{
|
||||||
"choices": []map[string]interface{}{
|
"choices": []map[string]any{
|
||||||
{
|
{
|
||||||
"message": map[string]interface{}{"content": "ok"},
|
"message": map[string]any{"content": "ok"},
|
||||||
"finish_reason": "stop",
|
"finish_reason": "stop",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -227,17 +233,17 @@ func TestProvider_ProxyConfigured(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestProviderChat_AcceptsNumericOptionTypes(t *testing.T) {
|
func TestProviderChat_AcceptsNumericOptionTypes(t *testing.T) {
|
||||||
var requestBody map[string]interface{}
|
var requestBody map[string]any
|
||||||
|
|
||||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
if err := json.NewDecoder(r.Body).Decode(&requestBody); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(&requestBody); err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
resp := map[string]interface{}{
|
resp := map[string]any{
|
||||||
"choices": []map[string]interface{}{
|
"choices": []map[string]any{
|
||||||
{
|
{
|
||||||
"message": map[string]interface{}{"content": "ok"},
|
"message": map[string]any{"content": "ok"},
|
||||||
"finish_reason": "stop",
|
"finish_reason": "stop",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -253,7 +259,7 @@ func TestProviderChat_AcceptsNumericOptionTypes(t *testing.T) {
|
||||||
[]Message{{Role: "user", Content: "hi"}},
|
[]Message{{Role: "user", Content: "hi"}},
|
||||||
nil,
|
nil,
|
||||||
"gpt-4o",
|
"gpt-4o",
|
||||||
map[string]interface{}{"max_tokens": float64(512), "temperature": 1},
|
map[string]any{"max_tokens": float64(512), "temperature": 1},
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Chat() error = %v", err)
|
t.Fatalf("Chat() error = %v", err)
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
package protocoltypes
|
package protocoltypes
|
||||||
|
|
||||||
type ToolCall struct {
|
type ToolCall struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Type string `json:"type,omitempty"`
|
Type string `json:"type,omitempty"`
|
||||||
Function *FunctionCall `json:"function,omitempty"`
|
Function *FunctionCall `json:"function,omitempty"`
|
||||||
Name string `json:"name,omitempty"`
|
Name string `json:"name,omitempty"`
|
||||||
Arguments map[string]interface{} `json:"arguments,omitempty"`
|
Arguments map[string]any `json:"arguments,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type FunctionCall struct {
|
type FunctionCall struct {
|
||||||
|
|
@ -39,7 +39,7 @@ type ToolDefinition struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type ToolFunctionDefinition struct {
|
type ToolFunctionDefinition struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
Parameters map[string]interface{} `json:"parameters"`
|
Parameters map[string]any `json:"parameters"`
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ func extractToolCallsFromText(text string) []ToolCall {
|
||||||
|
|
||||||
var result []ToolCall
|
var result []ToolCall
|
||||||
for _, tc := range wrapper.ToolCalls {
|
for _, tc := range wrapper.ToolCalls {
|
||||||
var args map[string]interface{}
|
var args map[string]any
|
||||||
json.Unmarshal([]byte(tc.Function.Arguments), &args)
|
json.Unmarshal([]byte(tc.Function.Arguments), &args)
|
||||||
|
|
||||||
result = append(result, ToolCall{
|
result = append(result, ToolCall{
|
||||||
|
|
|
||||||
|
|
@ -7,16 +7,24 @@ import (
|
||||||
"github.com/sipeed/picoclaw/pkg/providers/protocoltypes"
|
"github.com/sipeed/picoclaw/pkg/providers/protocoltypes"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ToolCall = protocoltypes.ToolCall
|
type (
|
||||||
type FunctionCall = protocoltypes.FunctionCall
|
ToolCall = protocoltypes.ToolCall
|
||||||
type LLMResponse = protocoltypes.LLMResponse
|
FunctionCall = protocoltypes.FunctionCall
|
||||||
type UsageInfo = protocoltypes.UsageInfo
|
LLMResponse = protocoltypes.LLMResponse
|
||||||
type Message = protocoltypes.Message
|
UsageInfo = protocoltypes.UsageInfo
|
||||||
type ToolDefinition = protocoltypes.ToolDefinition
|
Message = protocoltypes.Message
|
||||||
type ToolFunctionDefinition = protocoltypes.ToolFunctionDefinition
|
ToolDefinition = protocoltypes.ToolDefinition
|
||||||
|
ToolFunctionDefinition = protocoltypes.ToolFunctionDefinition
|
||||||
|
)
|
||||||
|
|
||||||
type LLMProvider interface {
|
type LLMProvider interface {
|
||||||
Chat(ctx context.Context, messages []Message, tools []ToolDefinition, model string, options map[string]interface{}) (*LLMResponse, error)
|
Chat(
|
||||||
|
ctx context.Context,
|
||||||
|
messages []Message,
|
||||||
|
tools []ToolDefinition,
|
||||||
|
model string,
|
||||||
|
options map[string]any,
|
||||||
|
) (*LLMResponse, error)
|
||||||
GetDefaultModel() string
|
GetDefaultModel() string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ func NewSessionManager(storage string) *SessionManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
if storage != "" {
|
if storage != "" {
|
||||||
os.MkdirAll(storage, 0755)
|
os.MkdirAll(storage, 0o755)
|
||||||
sm.loadSessions()
|
sm.loadSessions()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -214,7 +214,7 @@ func (sm *SessionManager) Save(key string) error {
|
||||||
_ = tmpFile.Close()
|
_ = tmpFile.Close()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := tmpFile.Chmod(0644); err != nil {
|
if err := tmpFile.Chmod(0o644); err != nil {
|
||||||
_ = tmpFile.Close()
|
_ = tmpFile.Close()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -66,12 +66,12 @@ func (si *SkillInstaller) InstallFromGitHub(ctx context.Context, repo string) er
|
||||||
return fmt.Errorf("failed to read response: %w", err)
|
return fmt.Errorf("failed to read response: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := os.MkdirAll(skillDir, 0755); err != nil {
|
if err := os.MkdirAll(skillDir, 0o755); err != nil {
|
||||||
return fmt.Errorf("failed to create skill directory: %w", err)
|
return fmt.Errorf("failed to create skill directory: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
skillPath := filepath.Join(skillDir, "SKILL.md")
|
skillPath := filepath.Join(skillDir, "SKILL.md")
|
||||||
if err := os.WriteFile(skillPath, body, 0644); err != nil {
|
if err := os.WriteFile(skillPath, body, 0o644); err != nil {
|
||||||
return fmt.Errorf("failed to write skill file: %w", err)
|
return fmt.Errorf("failed to write skill file: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ func NewManager(workspace string) *Manager {
|
||||||
oldStateFile := filepath.Join(workspace, "state.json")
|
oldStateFile := filepath.Join(workspace, "state.json")
|
||||||
|
|
||||||
// Create state directory if it doesn't exist
|
// Create state directory if it doesn't exist
|
||||||
os.MkdirAll(stateDir, 0755)
|
os.MkdirAll(stateDir, 0o755)
|
||||||
|
|
||||||
sm := &Manager{
|
sm := &Manager{
|
||||||
workspace: workspace,
|
workspace: workspace,
|
||||||
|
|
@ -139,7 +139,7 @@ func (sm *Manager) saveAtomic() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write to temp file
|
// Write to temp file
|
||||||
if err := os.WriteFile(tempFile, data, 0644); err != nil {
|
if err := os.WriteFile(tempFile, data, 0o644); err != nil {
|
||||||
return fmt.Errorf("failed to write temp file: %w", err)
|
return fmt.Errorf("failed to write temp file: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -98,7 +98,7 @@ func TestAtomicity_NoCorruptionOnInterrupt(t *testing.T) {
|
||||||
|
|
||||||
// Simulate a crash scenario by manually creating a corrupted temp file
|
// Simulate a crash scenario by manually creating a corrupted temp file
|
||||||
tempFile := filepath.Join(tmpDir, "state", "state.json.tmp")
|
tempFile := filepath.Join(tmpDir, "state", "state.json.tmp")
|
||||||
err = os.WriteFile(tempFile, []byte("corrupted data"), 0644)
|
err = os.WriteFile(tempFile, []byte("corrupted data"), 0o644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to create temp file: %v", err)
|
t.Fatalf("Failed to create temp file: %v", err)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,8 @@ import "context"
|
||||||
type Tool interface {
|
type Tool interface {
|
||||||
Name() string
|
Name() string
|
||||||
Description() string
|
Description() string
|
||||||
Parameters() map[string]interface{}
|
Parameters() map[string]any
|
||||||
Execute(ctx context.Context, args map[string]interface{}) *ToolResult
|
Execute(ctx context.Context, args map[string]any) *ToolResult
|
||||||
}
|
}
|
||||||
|
|
||||||
// ContextualTool is an optional interface that tools can implement
|
// ContextualTool is an optional interface that tools can implement
|
||||||
|
|
@ -69,10 +69,10 @@ type AsyncTool interface {
|
||||||
SetCallback(cb AsyncCallback)
|
SetCallback(cb AsyncCallback)
|
||||||
}
|
}
|
||||||
|
|
||||||
func ToolToSchema(tool Tool) map[string]interface{} {
|
func ToolToSchema(tool Tool) map[string]any {
|
||||||
return map[string]interface{}{
|
return map[string]any{
|
||||||
"type": "function",
|
"type": "function",
|
||||||
"function": map[string]interface{}{
|
"function": map[string]any{
|
||||||
"name": tool.Name(),
|
"name": tool.Name(),
|
||||||
"description": tool.Description(),
|
"description": tool.Description(),
|
||||||
"parameters": tool.Parameters(),
|
"parameters": tool.Parameters(),
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,10 @@ type CronTool struct {
|
||||||
|
|
||||||
// NewCronTool creates a new CronTool
|
// NewCronTool creates a new CronTool
|
||||||
// execTimeout: 0 means no timeout, >0 sets the timeout duration
|
// execTimeout: 0 means no timeout, >0 sets the timeout duration
|
||||||
func NewCronTool(cronService *cron.CronService, executor JobExecutor, msgBus *bus.MessageBus, workspace string, restrict bool, execTimeout time.Duration, config *config.Config) *CronTool {
|
func NewCronTool(
|
||||||
|
cronService *cron.CronService, executor JobExecutor, msgBus *bus.MessageBus, workspace string, restrict bool,
|
||||||
|
execTimeout time.Duration, config *config.Config,
|
||||||
|
) *CronTool {
|
||||||
execTool := NewExecToolWithConfig(workspace, restrict, config)
|
execTool := NewExecToolWithConfig(workspace, restrict, config)
|
||||||
execTool.SetTimeout(execTimeout)
|
execTool.SetTimeout(execTimeout)
|
||||||
return &CronTool{
|
return &CronTool{
|
||||||
|
|
@ -52,40 +55,40 @@ func (t *CronTool) Description() string {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parameters returns the tool parameters schema
|
// Parameters returns the tool parameters schema
|
||||||
func (t *CronTool) Parameters() map[string]interface{} {
|
func (t *CronTool) Parameters() map[string]any {
|
||||||
return map[string]interface{}{
|
return map[string]any{
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": map[string]interface{}{
|
"properties": map[string]any{
|
||||||
"action": map[string]interface{}{
|
"action": map[string]any{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": []string{"add", "list", "remove", "enable", "disable"},
|
"enum": []string{"add", "list", "remove", "enable", "disable"},
|
||||||
"description": "Action to perform. Use 'add' when user wants to schedule a reminder or task.",
|
"description": "Action to perform. Use 'add' when user wants to schedule a reminder or task.",
|
||||||
},
|
},
|
||||||
"message": map[string]interface{}{
|
"message": map[string]any{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "The reminder/task message to display when triggered. If 'command' is used, this describes what the command does.",
|
"description": "The reminder/task message to display when triggered. If 'command' is used, this describes what the command does.",
|
||||||
},
|
},
|
||||||
"command": map[string]interface{}{
|
"command": map[string]any{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Optional: Shell command to execute directly (e.g., 'df -h'). If set, the agent will run this command and report output instead of just showing the message. 'deliver' will be forced to false for commands.",
|
"description": "Optional: Shell command to execute directly (e.g., 'df -h'). If set, the agent will run this command and report output instead of just showing the message. 'deliver' will be forced to false for commands.",
|
||||||
},
|
},
|
||||||
"at_seconds": map[string]interface{}{
|
"at_seconds": map[string]any{
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"description": "One-time reminder: seconds from now when to trigger (e.g., 600 for 10 minutes later). Use this for one-time reminders like 'remind me in 10 minutes'.",
|
"description": "One-time reminder: seconds from now when to trigger (e.g., 600 for 10 minutes later). Use this for one-time reminders like 'remind me in 10 minutes'.",
|
||||||
},
|
},
|
||||||
"every_seconds": map[string]interface{}{
|
"every_seconds": map[string]any{
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"description": "Recurring interval in seconds (e.g., 3600 for every hour). Use this ONLY for recurring tasks like 'every 2 hours' or 'daily reminder'.",
|
"description": "Recurring interval in seconds (e.g., 3600 for every hour). Use this ONLY for recurring tasks like 'every 2 hours' or 'daily reminder'.",
|
||||||
},
|
},
|
||||||
"cron_expr": map[string]interface{}{
|
"cron_expr": map[string]any{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Cron expression for complex recurring schedules (e.g., '0 9 * * *' for daily at 9am). Use this for complex recurring schedules.",
|
"description": "Cron expression for complex recurring schedules (e.g., '0 9 * * *' for daily at 9am). Use this for complex recurring schedules.",
|
||||||
},
|
},
|
||||||
"job_id": map[string]interface{}{
|
"job_id": map[string]any{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Job ID (for remove/enable/disable)",
|
"description": "Job ID (for remove/enable/disable)",
|
||||||
},
|
},
|
||||||
"deliver": map[string]interface{}{
|
"deliver": map[string]any{
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"description": "If true, send message directly to channel. If false, let agent process message (for complex tasks). Default: true",
|
"description": "If true, send message directly to channel. If false, let agent process message (for complex tasks). Default: true",
|
||||||
},
|
},
|
||||||
|
|
@ -103,7 +106,7 @@ func (t *CronTool) SetContext(channel, chatID string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Execute runs the tool with the given arguments
|
// Execute runs the tool with the given arguments
|
||||||
func (t *CronTool) Execute(ctx context.Context, args map[string]interface{}) *ToolResult {
|
func (t *CronTool) Execute(ctx context.Context, args map[string]any) *ToolResult {
|
||||||
action, ok := args["action"].(string)
|
action, ok := args["action"].(string)
|
||||||
if !ok {
|
if !ok {
|
||||||
return ErrorResult("action is required")
|
return ErrorResult("action is required")
|
||||||
|
|
@ -125,7 +128,7 @@ func (t *CronTool) Execute(ctx context.Context, args map[string]interface{}) *To
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *CronTool) addJob(args map[string]interface{}) *ToolResult {
|
func (t *CronTool) addJob(args map[string]any) *ToolResult {
|
||||||
t.mu.RLock()
|
t.mu.RLock()
|
||||||
channel := t.channel
|
channel := t.channel
|
||||||
chatID := t.chatID
|
chatID := t.chatID
|
||||||
|
|
@ -233,7 +236,7 @@ func (t *CronTool) listJobs() *ToolResult {
|
||||||
return SilentResult(result)
|
return SilentResult(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *CronTool) removeJob(args map[string]interface{}) *ToolResult {
|
func (t *CronTool) removeJob(args map[string]any) *ToolResult {
|
||||||
jobID, ok := args["job_id"].(string)
|
jobID, ok := args["job_id"].(string)
|
||||||
if !ok || jobID == "" {
|
if !ok || jobID == "" {
|
||||||
return ErrorResult("job_id is required for remove")
|
return ErrorResult("job_id is required for remove")
|
||||||
|
|
@ -245,7 +248,7 @@ func (t *CronTool) removeJob(args map[string]interface{}) *ToolResult {
|
||||||
return ErrorResult(fmt.Sprintf("Job %s not found", jobID))
|
return ErrorResult(fmt.Sprintf("Job %s not found", jobID))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *CronTool) enableJob(args map[string]interface{}, enable bool) *ToolResult {
|
func (t *CronTool) enableJob(args map[string]any, enable bool) *ToolResult {
|
||||||
jobID, ok := args["job_id"].(string)
|
jobID, ok := args["job_id"].(string)
|
||||||
if !ok || jobID == "" {
|
if !ok || jobID == "" {
|
||||||
return ErrorResult("job_id is required for enable/disable")
|
return ErrorResult("job_id is required for enable/disable")
|
||||||
|
|
@ -279,7 +282,7 @@ func (t *CronTool) ExecuteJob(ctx context.Context, job *cron.CronJob) string {
|
||||||
|
|
||||||
// Execute command if present
|
// Execute command if present
|
||||||
if job.Payload.Command != "" {
|
if job.Payload.Command != "" {
|
||||||
args := map[string]interface{}{
|
args := map[string]any{
|
||||||
"command": job.Payload.Command,
|
"command": job.Payload.Command,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -320,7 +323,6 @@ func (t *CronTool) ExecuteJob(ctx context.Context, job *cron.CronJob) string {
|
||||||
channel,
|
channel,
|
||||||
chatID,
|
chatID,
|
||||||
)
|
)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Sprintf("Error: %v", err)
|
return fmt.Sprintf("Error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,19 +30,19 @@ func (t *EditFileTool) Description() string {
|
||||||
return "Edit a file by replacing old_text with new_text. The old_text must exist exactly in the file."
|
return "Edit a file by replacing old_text with new_text. The old_text must exist exactly in the file."
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *EditFileTool) Parameters() map[string]interface{} {
|
func (t *EditFileTool) Parameters() map[string]any {
|
||||||
return map[string]interface{}{
|
return map[string]any{
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": map[string]interface{}{
|
"properties": map[string]any{
|
||||||
"path": map[string]interface{}{
|
"path": map[string]any{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "The file path to edit",
|
"description": "The file path to edit",
|
||||||
},
|
},
|
||||||
"old_text": map[string]interface{}{
|
"old_text": map[string]any{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "The exact text to find and replace",
|
"description": "The exact text to find and replace",
|
||||||
},
|
},
|
||||||
"new_text": map[string]interface{}{
|
"new_text": map[string]any{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "The text to replace with",
|
"description": "The text to replace with",
|
||||||
},
|
},
|
||||||
|
|
@ -51,7 +51,7 @@ func (t *EditFileTool) Parameters() map[string]interface{} {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *EditFileTool) Execute(ctx context.Context, args map[string]interface{}) *ToolResult {
|
func (t *EditFileTool) Execute(ctx context.Context, args map[string]any) *ToolResult {
|
||||||
path, ok := args["path"].(string)
|
path, ok := args["path"].(string)
|
||||||
if !ok {
|
if !ok {
|
||||||
return ErrorResult("path is required")
|
return ErrorResult("path is required")
|
||||||
|
|
@ -89,12 +89,14 @@ func (t *EditFileTool) Execute(ctx context.Context, args map[string]interface{})
|
||||||
|
|
||||||
count := strings.Count(contentStr, oldText)
|
count := strings.Count(contentStr, oldText)
|
||||||
if count > 1 {
|
if count > 1 {
|
||||||
return ErrorResult(fmt.Sprintf("old_text appears %d times. Please provide more context to make it unique", count))
|
return ErrorResult(
|
||||||
|
fmt.Sprintf("old_text appears %d times. Please provide more context to make it unique", count),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
newContent := strings.Replace(contentStr, oldText, newText, 1)
|
newContent := strings.Replace(contentStr, oldText, newText, 1)
|
||||||
|
|
||||||
if err := os.WriteFile(resolvedPath, []byte(newContent), 0644); err != nil {
|
if err := os.WriteFile(resolvedPath, []byte(newContent), 0o644); err != nil {
|
||||||
return ErrorResult(fmt.Sprintf("failed to write file: %v", err))
|
return ErrorResult(fmt.Sprintf("failed to write file: %v", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -118,15 +120,15 @@ func (t *AppendFileTool) Description() string {
|
||||||
return "Append content to the end of a file"
|
return "Append content to the end of a file"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *AppendFileTool) Parameters() map[string]interface{} {
|
func (t *AppendFileTool) Parameters() map[string]any {
|
||||||
return map[string]interface{}{
|
return map[string]any{
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": map[string]interface{}{
|
"properties": map[string]any{
|
||||||
"path": map[string]interface{}{
|
"path": map[string]any{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "The file path to append to",
|
"description": "The file path to append to",
|
||||||
},
|
},
|
||||||
"content": map[string]interface{}{
|
"content": map[string]any{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "The content to append",
|
"description": "The content to append",
|
||||||
},
|
},
|
||||||
|
|
@ -135,7 +137,7 @@ func (t *AppendFileTool) Parameters() map[string]interface{} {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *AppendFileTool) Execute(ctx context.Context, args map[string]interface{}) *ToolResult {
|
func (t *AppendFileTool) Execute(ctx context.Context, args map[string]any) *ToolResult {
|
||||||
path, ok := args["path"].(string)
|
path, ok := args["path"].(string)
|
||||||
if !ok {
|
if !ok {
|
||||||
return ErrorResult("path is required")
|
return ErrorResult("path is required")
|
||||||
|
|
@ -151,7 +153,7 @@ func (t *AppendFileTool) Execute(ctx context.Context, args map[string]interface{
|
||||||
return ErrorResult(err.Error())
|
return ErrorResult(err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
f, err := os.OpenFile(resolvedPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
f, err := os.OpenFile(resolvedPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ErrorResult(fmt.Sprintf("failed to open file: %v", err))
|
return ErrorResult(fmt.Sprintf("failed to open file: %v", err))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,11 +12,11 @@ import (
|
||||||
func TestEditTool_EditFile_Success(t *testing.T) {
|
func TestEditTool_EditFile_Success(t *testing.T) {
|
||||||
tmpDir := t.TempDir()
|
tmpDir := t.TempDir()
|
||||||
testFile := filepath.Join(tmpDir, "test.txt")
|
testFile := filepath.Join(tmpDir, "test.txt")
|
||||||
os.WriteFile(testFile, []byte("Hello World\nThis is a test"), 0644)
|
os.WriteFile(testFile, []byte("Hello World\nThis is a test"), 0o644)
|
||||||
|
|
||||||
tool := NewEditFileTool(tmpDir, true)
|
tool := NewEditFileTool(tmpDir, true)
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
args := map[string]interface{}{
|
args := map[string]any{
|
||||||
"path": testFile,
|
"path": testFile,
|
||||||
"old_text": "World",
|
"old_text": "World",
|
||||||
"new_text": "Universe",
|
"new_text": "Universe",
|
||||||
|
|
@ -60,7 +60,7 @@ func TestEditTool_EditFile_NotFound(t *testing.T) {
|
||||||
|
|
||||||
tool := NewEditFileTool(tmpDir, true)
|
tool := NewEditFileTool(tmpDir, true)
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
args := map[string]interface{}{
|
args := map[string]any{
|
||||||
"path": testFile,
|
"path": testFile,
|
||||||
"old_text": "old",
|
"old_text": "old",
|
||||||
"new_text": "new",
|
"new_text": "new",
|
||||||
|
|
@ -83,11 +83,11 @@ func TestEditTool_EditFile_NotFound(t *testing.T) {
|
||||||
func TestEditTool_EditFile_OldTextNotFound(t *testing.T) {
|
func TestEditTool_EditFile_OldTextNotFound(t *testing.T) {
|
||||||
tmpDir := t.TempDir()
|
tmpDir := t.TempDir()
|
||||||
testFile := filepath.Join(tmpDir, "test.txt")
|
testFile := filepath.Join(tmpDir, "test.txt")
|
||||||
os.WriteFile(testFile, []byte("Hello World"), 0644)
|
os.WriteFile(testFile, []byte("Hello World"), 0o644)
|
||||||
|
|
||||||
tool := NewEditFileTool(tmpDir, true)
|
tool := NewEditFileTool(tmpDir, true)
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
args := map[string]interface{}{
|
args := map[string]any{
|
||||||
"path": testFile,
|
"path": testFile,
|
||||||
"old_text": "Goodbye",
|
"old_text": "Goodbye",
|
||||||
"new_text": "Hello",
|
"new_text": "Hello",
|
||||||
|
|
@ -110,11 +110,11 @@ func TestEditTool_EditFile_OldTextNotFound(t *testing.T) {
|
||||||
func TestEditTool_EditFile_MultipleMatches(t *testing.T) {
|
func TestEditTool_EditFile_MultipleMatches(t *testing.T) {
|
||||||
tmpDir := t.TempDir()
|
tmpDir := t.TempDir()
|
||||||
testFile := filepath.Join(tmpDir, "test.txt")
|
testFile := filepath.Join(tmpDir, "test.txt")
|
||||||
os.WriteFile(testFile, []byte("test test test"), 0644)
|
os.WriteFile(testFile, []byte("test test test"), 0o644)
|
||||||
|
|
||||||
tool := NewEditFileTool(tmpDir, true)
|
tool := NewEditFileTool(tmpDir, true)
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
args := map[string]interface{}{
|
args := map[string]any{
|
||||||
"path": testFile,
|
"path": testFile,
|
||||||
"old_text": "test",
|
"old_text": "test",
|
||||||
"new_text": "done",
|
"new_text": "done",
|
||||||
|
|
@ -138,11 +138,11 @@ func TestEditTool_EditFile_OutsideAllowedDir(t *testing.T) {
|
||||||
tmpDir := t.TempDir()
|
tmpDir := t.TempDir()
|
||||||
otherDir := t.TempDir()
|
otherDir := t.TempDir()
|
||||||
testFile := filepath.Join(otherDir, "test.txt")
|
testFile := filepath.Join(otherDir, "test.txt")
|
||||||
os.WriteFile(testFile, []byte("content"), 0644)
|
os.WriteFile(testFile, []byte("content"), 0o644)
|
||||||
|
|
||||||
tool := NewEditFileTool(tmpDir, true) // Restrict to tmpDir
|
tool := NewEditFileTool(tmpDir, true) // Restrict to tmpDir
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
args := map[string]interface{}{
|
args := map[string]any{
|
||||||
"path": testFile,
|
"path": testFile,
|
||||||
"old_text": "content",
|
"old_text": "content",
|
||||||
"new_text": "new",
|
"new_text": "new",
|
||||||
|
|
@ -165,7 +165,7 @@ func TestEditTool_EditFile_OutsideAllowedDir(t *testing.T) {
|
||||||
func TestEditTool_EditFile_MissingPath(t *testing.T) {
|
func TestEditTool_EditFile_MissingPath(t *testing.T) {
|
||||||
tool := NewEditFileTool("", false)
|
tool := NewEditFileTool("", false)
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
args := map[string]interface{}{
|
args := map[string]any{
|
||||||
"old_text": "old",
|
"old_text": "old",
|
||||||
"new_text": "new",
|
"new_text": "new",
|
||||||
}
|
}
|
||||||
|
|
@ -182,7 +182,7 @@ func TestEditTool_EditFile_MissingPath(t *testing.T) {
|
||||||
func TestEditTool_EditFile_MissingOldText(t *testing.T) {
|
func TestEditTool_EditFile_MissingOldText(t *testing.T) {
|
||||||
tool := NewEditFileTool("", false)
|
tool := NewEditFileTool("", false)
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
args := map[string]interface{}{
|
args := map[string]any{
|
||||||
"path": "/tmp/test.txt",
|
"path": "/tmp/test.txt",
|
||||||
"new_text": "new",
|
"new_text": "new",
|
||||||
}
|
}
|
||||||
|
|
@ -199,7 +199,7 @@ func TestEditTool_EditFile_MissingOldText(t *testing.T) {
|
||||||
func TestEditTool_EditFile_MissingNewText(t *testing.T) {
|
func TestEditTool_EditFile_MissingNewText(t *testing.T) {
|
||||||
tool := NewEditFileTool("", false)
|
tool := NewEditFileTool("", false)
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
args := map[string]interface{}{
|
args := map[string]any{
|
||||||
"path": "/tmp/test.txt",
|
"path": "/tmp/test.txt",
|
||||||
"old_text": "old",
|
"old_text": "old",
|
||||||
}
|
}
|
||||||
|
|
@ -216,11 +216,11 @@ func TestEditTool_EditFile_MissingNewText(t *testing.T) {
|
||||||
func TestEditTool_AppendFile_Success(t *testing.T) {
|
func TestEditTool_AppendFile_Success(t *testing.T) {
|
||||||
tmpDir := t.TempDir()
|
tmpDir := t.TempDir()
|
||||||
testFile := filepath.Join(tmpDir, "test.txt")
|
testFile := filepath.Join(tmpDir, "test.txt")
|
||||||
os.WriteFile(testFile, []byte("Initial content"), 0644)
|
os.WriteFile(testFile, []byte("Initial content"), 0o644)
|
||||||
|
|
||||||
tool := NewAppendFileTool("", false)
|
tool := NewAppendFileTool("", false)
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
args := map[string]interface{}{
|
args := map[string]any{
|
||||||
"path": testFile,
|
"path": testFile,
|
||||||
"content": "\nAppended content",
|
"content": "\nAppended content",
|
||||||
}
|
}
|
||||||
|
|
@ -260,7 +260,7 @@ func TestEditTool_AppendFile_Success(t *testing.T) {
|
||||||
func TestEditTool_AppendFile_MissingPath(t *testing.T) {
|
func TestEditTool_AppendFile_MissingPath(t *testing.T) {
|
||||||
tool := NewAppendFileTool("", false)
|
tool := NewAppendFileTool("", false)
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
args := map[string]interface{}{
|
args := map[string]any{
|
||||||
"content": "test",
|
"content": "test",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -276,7 +276,7 @@ func TestEditTool_AppendFile_MissingPath(t *testing.T) {
|
||||||
func TestEditTool_AppendFile_MissingContent(t *testing.T) {
|
func TestEditTool_AppendFile_MissingContent(t *testing.T) {
|
||||||
tool := NewAppendFileTool("", false)
|
tool := NewAppendFileTool("", false)
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
args := map[string]interface{}{
|
args := map[string]any{
|
||||||
"path": "/tmp/test.txt",
|
"path": "/tmp/test.txt",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -94,11 +94,11 @@ func (t *ReadFileTool) Description() string {
|
||||||
return "Read the contents of a file"
|
return "Read the contents of a file"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *ReadFileTool) Parameters() map[string]interface{} {
|
func (t *ReadFileTool) Parameters() map[string]any {
|
||||||
return map[string]interface{}{
|
return map[string]any{
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": map[string]interface{}{
|
"properties": map[string]any{
|
||||||
"path": map[string]interface{}{
|
"path": map[string]any{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Path to the file to read",
|
"description": "Path to the file to read",
|
||||||
},
|
},
|
||||||
|
|
@ -107,7 +107,7 @@ func (t *ReadFileTool) Parameters() map[string]interface{} {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *ReadFileTool) Execute(ctx context.Context, args map[string]interface{}) *ToolResult {
|
func (t *ReadFileTool) Execute(ctx context.Context, args map[string]any) *ToolResult {
|
||||||
path, ok := args["path"].(string)
|
path, ok := args["path"].(string)
|
||||||
if !ok {
|
if !ok {
|
||||||
return ErrorResult("path is required")
|
return ErrorResult("path is required")
|
||||||
|
|
@ -143,15 +143,15 @@ func (t *WriteFileTool) Description() string {
|
||||||
return "Write content to a file"
|
return "Write content to a file"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *WriteFileTool) Parameters() map[string]interface{} {
|
func (t *WriteFileTool) Parameters() map[string]any {
|
||||||
return map[string]interface{}{
|
return map[string]any{
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": map[string]interface{}{
|
"properties": map[string]any{
|
||||||
"path": map[string]interface{}{
|
"path": map[string]any{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Path to the file to write",
|
"description": "Path to the file to write",
|
||||||
},
|
},
|
||||||
"content": map[string]interface{}{
|
"content": map[string]any{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Content to write to the file",
|
"description": "Content to write to the file",
|
||||||
},
|
},
|
||||||
|
|
@ -160,7 +160,7 @@ func (t *WriteFileTool) Parameters() map[string]interface{} {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *WriteFileTool) Execute(ctx context.Context, args map[string]interface{}) *ToolResult {
|
func (t *WriteFileTool) Execute(ctx context.Context, args map[string]any) *ToolResult {
|
||||||
path, ok := args["path"].(string)
|
path, ok := args["path"].(string)
|
||||||
if !ok {
|
if !ok {
|
||||||
return ErrorResult("path is required")
|
return ErrorResult("path is required")
|
||||||
|
|
@ -177,11 +177,11 @@ func (t *WriteFileTool) Execute(ctx context.Context, args map[string]interface{}
|
||||||
}
|
}
|
||||||
|
|
||||||
dir := filepath.Dir(resolvedPath)
|
dir := filepath.Dir(resolvedPath)
|
||||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||||
return ErrorResult(fmt.Sprintf("failed to create directory: %v", err))
|
return ErrorResult(fmt.Sprintf("failed to create directory: %v", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := os.WriteFile(resolvedPath, []byte(content), 0644); err != nil {
|
if err := os.WriteFile(resolvedPath, []byte(content), 0o644); err != nil {
|
||||||
return ErrorResult(fmt.Sprintf("failed to write file: %v", err))
|
return ErrorResult(fmt.Sprintf("failed to write file: %v", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -205,11 +205,11 @@ func (t *ListDirTool) Description() string {
|
||||||
return "List files and directories in a path"
|
return "List files and directories in a path"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *ListDirTool) Parameters() map[string]interface{} {
|
func (t *ListDirTool) Parameters() map[string]any {
|
||||||
return map[string]interface{}{
|
return map[string]any{
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": map[string]interface{}{
|
"properties": map[string]any{
|
||||||
"path": map[string]interface{}{
|
"path": map[string]any{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Path to list",
|
"description": "Path to list",
|
||||||
},
|
},
|
||||||
|
|
@ -218,7 +218,7 @@ func (t *ListDirTool) Parameters() map[string]interface{} {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *ListDirTool) Execute(ctx context.Context, args map[string]interface{}) *ToolResult {
|
func (t *ListDirTool) Execute(ctx context.Context, args map[string]any) *ToolResult {
|
||||||
path, ok := args["path"].(string)
|
path, ok := args["path"].(string)
|
||||||
if !ok {
|
if !ok {
|
||||||
path = "."
|
path = "."
|
||||||
|
|
|
||||||
|
|
@ -12,11 +12,11 @@ import (
|
||||||
func TestFilesystemTool_ReadFile_Success(t *testing.T) {
|
func TestFilesystemTool_ReadFile_Success(t *testing.T) {
|
||||||
tmpDir := t.TempDir()
|
tmpDir := t.TempDir()
|
||||||
testFile := filepath.Join(tmpDir, "test.txt")
|
testFile := filepath.Join(tmpDir, "test.txt")
|
||||||
os.WriteFile(testFile, []byte("test content"), 0644)
|
os.WriteFile(testFile, []byte("test content"), 0o644)
|
||||||
|
|
||||||
tool := &ReadFileTool{}
|
tool := &ReadFileTool{}
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
args := map[string]interface{}{
|
args := map[string]any{
|
||||||
"path": testFile,
|
"path": testFile,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -43,7 +43,7 @@ func TestFilesystemTool_ReadFile_Success(t *testing.T) {
|
||||||
func TestFilesystemTool_ReadFile_NotFound(t *testing.T) {
|
func TestFilesystemTool_ReadFile_NotFound(t *testing.T) {
|
||||||
tool := &ReadFileTool{}
|
tool := &ReadFileTool{}
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
args := map[string]interface{}{
|
args := map[string]any{
|
||||||
"path": "/nonexistent_file_12345.txt",
|
"path": "/nonexistent_file_12345.txt",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -64,7 +64,7 @@ func TestFilesystemTool_ReadFile_NotFound(t *testing.T) {
|
||||||
func TestFilesystemTool_ReadFile_MissingPath(t *testing.T) {
|
func TestFilesystemTool_ReadFile_MissingPath(t *testing.T) {
|
||||||
tool := &ReadFileTool{}
|
tool := &ReadFileTool{}
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
args := map[string]interface{}{}
|
args := map[string]any{}
|
||||||
|
|
||||||
result := tool.Execute(ctx, args)
|
result := tool.Execute(ctx, args)
|
||||||
|
|
||||||
|
|
@ -86,7 +86,7 @@ func TestFilesystemTool_WriteFile_Success(t *testing.T) {
|
||||||
|
|
||||||
tool := &WriteFileTool{}
|
tool := &WriteFileTool{}
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
args := map[string]interface{}{
|
args := map[string]any{
|
||||||
"path": testFile,
|
"path": testFile,
|
||||||
"content": "hello world",
|
"content": "hello world",
|
||||||
}
|
}
|
||||||
|
|
@ -125,7 +125,7 @@ func TestFilesystemTool_WriteFile_CreateDir(t *testing.T) {
|
||||||
|
|
||||||
tool := &WriteFileTool{}
|
tool := &WriteFileTool{}
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
args := map[string]interface{}{
|
args := map[string]any{
|
||||||
"path": testFile,
|
"path": testFile,
|
||||||
"content": "test",
|
"content": "test",
|
||||||
}
|
}
|
||||||
|
|
@ -151,7 +151,7 @@ func TestFilesystemTool_WriteFile_CreateDir(t *testing.T) {
|
||||||
func TestFilesystemTool_WriteFile_MissingPath(t *testing.T) {
|
func TestFilesystemTool_WriteFile_MissingPath(t *testing.T) {
|
||||||
tool := &WriteFileTool{}
|
tool := &WriteFileTool{}
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
args := map[string]interface{}{
|
args := map[string]any{
|
||||||
"content": "test",
|
"content": "test",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -167,7 +167,7 @@ func TestFilesystemTool_WriteFile_MissingPath(t *testing.T) {
|
||||||
func TestFilesystemTool_WriteFile_MissingContent(t *testing.T) {
|
func TestFilesystemTool_WriteFile_MissingContent(t *testing.T) {
|
||||||
tool := &WriteFileTool{}
|
tool := &WriteFileTool{}
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
args := map[string]interface{}{
|
args := map[string]any{
|
||||||
"path": "/tmp/test.txt",
|
"path": "/tmp/test.txt",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -179,7 +179,8 @@ func TestFilesystemTool_WriteFile_MissingContent(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Should mention required parameter
|
// Should mention required parameter
|
||||||
if !strings.Contains(result.ForLLM, "content is required") && !strings.Contains(result.ForUser, "content is required") {
|
if !strings.Contains(result.ForLLM, "content is required") &&
|
||||||
|
!strings.Contains(result.ForUser, "content is required") {
|
||||||
t.Errorf("Expected 'content is required' message, got ForLLM: %s", result.ForLLM)
|
t.Errorf("Expected 'content is required' message, got ForLLM: %s", result.ForLLM)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -187,13 +188,13 @@ func TestFilesystemTool_WriteFile_MissingContent(t *testing.T) {
|
||||||
// TestFilesystemTool_ListDir_Success verifies successful directory listing
|
// TestFilesystemTool_ListDir_Success verifies successful directory listing
|
||||||
func TestFilesystemTool_ListDir_Success(t *testing.T) {
|
func TestFilesystemTool_ListDir_Success(t *testing.T) {
|
||||||
tmpDir := t.TempDir()
|
tmpDir := t.TempDir()
|
||||||
os.WriteFile(filepath.Join(tmpDir, "file1.txt"), []byte("content"), 0644)
|
os.WriteFile(filepath.Join(tmpDir, "file1.txt"), []byte("content"), 0o644)
|
||||||
os.WriteFile(filepath.Join(tmpDir, "file2.txt"), []byte("content"), 0644)
|
os.WriteFile(filepath.Join(tmpDir, "file2.txt"), []byte("content"), 0o644)
|
||||||
os.Mkdir(filepath.Join(tmpDir, "subdir"), 0755)
|
os.Mkdir(filepath.Join(tmpDir, "subdir"), 0o755)
|
||||||
|
|
||||||
tool := &ListDirTool{}
|
tool := &ListDirTool{}
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
args := map[string]interface{}{
|
args := map[string]any{
|
||||||
"path": tmpDir,
|
"path": tmpDir,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -217,7 +218,7 @@ func TestFilesystemTool_ListDir_Success(t *testing.T) {
|
||||||
func TestFilesystemTool_ListDir_NotFound(t *testing.T) {
|
func TestFilesystemTool_ListDir_NotFound(t *testing.T) {
|
||||||
tool := &ListDirTool{}
|
tool := &ListDirTool{}
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
args := map[string]interface{}{
|
args := map[string]any{
|
||||||
"path": "/nonexistent_directory_12345",
|
"path": "/nonexistent_directory_12345",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -238,7 +239,7 @@ func TestFilesystemTool_ListDir_NotFound(t *testing.T) {
|
||||||
func TestFilesystemTool_ListDir_DefaultPath(t *testing.T) {
|
func TestFilesystemTool_ListDir_DefaultPath(t *testing.T) {
|
||||||
tool := &ListDirTool{}
|
tool := &ListDirTool{}
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
args := map[string]interface{}{}
|
args := map[string]any{}
|
||||||
|
|
||||||
result := tool.Execute(ctx, args)
|
result := tool.Execute(ctx, args)
|
||||||
|
|
||||||
|
|
@ -250,15 +251,14 @@ func TestFilesystemTool_ListDir_DefaultPath(t *testing.T) {
|
||||||
|
|
||||||
// Block paths that look inside workspace but point outside via symlink.
|
// Block paths that look inside workspace but point outside via symlink.
|
||||||
func TestFilesystemTool_ReadFile_RejectsSymlinkEscape(t *testing.T) {
|
func TestFilesystemTool_ReadFile_RejectsSymlinkEscape(t *testing.T) {
|
||||||
|
|
||||||
root := t.TempDir()
|
root := t.TempDir()
|
||||||
workspace := filepath.Join(root, "workspace")
|
workspace := filepath.Join(root, "workspace")
|
||||||
if err := os.MkdirAll(workspace, 0755); err != nil {
|
if err := os.MkdirAll(workspace, 0o755); err != nil {
|
||||||
t.Fatalf("failed to create workspace: %v", err)
|
t.Fatalf("failed to create workspace: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
secret := filepath.Join(root, "secret.txt")
|
secret := filepath.Join(root, "secret.txt")
|
||||||
if err := os.WriteFile(secret, []byte("top secret"), 0644); err != nil {
|
if err := os.WriteFile(secret, []byte("top secret"), 0o644); err != nil {
|
||||||
t.Fatalf("failed to write secret file: %v", err)
|
t.Fatalf("failed to write secret file: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -268,7 +268,7 @@ func TestFilesystemTool_ReadFile_RejectsSymlinkEscape(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
tool := NewReadFileTool(workspace, true)
|
tool := NewReadFileTool(workspace, true)
|
||||||
result := tool.Execute(context.Background(), map[string]interface{}{
|
result := tool.Execute(context.Background(), map[string]any{
|
||||||
"path": link,
|
"path": link,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,37 +24,37 @@ func (t *I2CTool) Description() string {
|
||||||
return "Interact with I2C bus devices for reading sensors and controlling peripherals. Actions: detect (list buses), scan (find devices on a bus), read (read bytes from device), write (send bytes to device). Linux only."
|
return "Interact with I2C bus devices for reading sensors and controlling peripherals. Actions: detect (list buses), scan (find devices on a bus), read (read bytes from device), write (send bytes to device). Linux only."
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *I2CTool) Parameters() map[string]interface{} {
|
func (t *I2CTool) Parameters() map[string]any {
|
||||||
return map[string]interface{}{
|
return map[string]any{
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": map[string]interface{}{
|
"properties": map[string]any{
|
||||||
"action": map[string]interface{}{
|
"action": map[string]any{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": []string{"detect", "scan", "read", "write"},
|
"enum": []string{"detect", "scan", "read", "write"},
|
||||||
"description": "Action to perform: detect (list available I2C buses), scan (find devices on a bus), read (read bytes from a device), write (send bytes to a device)",
|
"description": "Action to perform: detect (list available I2C buses), scan (find devices on a bus), read (read bytes from a device), write (send bytes to a device)",
|
||||||
},
|
},
|
||||||
"bus": map[string]interface{}{
|
"bus": map[string]any{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "I2C bus number (e.g. \"1\" for /dev/i2c-1). Required for scan/read/write.",
|
"description": "I2C bus number (e.g. \"1\" for /dev/i2c-1). Required for scan/read/write.",
|
||||||
},
|
},
|
||||||
"address": map[string]interface{}{
|
"address": map[string]any{
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"description": "7-bit I2C device address (0x03-0x77). Required for read/write.",
|
"description": "7-bit I2C device address (0x03-0x77). Required for read/write.",
|
||||||
},
|
},
|
||||||
"register": map[string]interface{}{
|
"register": map[string]any{
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"description": "Register address to read from or write to. If set, sends register byte before read/write.",
|
"description": "Register address to read from or write to. If set, sends register byte before read/write.",
|
||||||
},
|
},
|
||||||
"data": map[string]interface{}{
|
"data": map[string]any{
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": map[string]interface{}{"type": "integer"},
|
"items": map[string]any{"type": "integer"},
|
||||||
"description": "Bytes to write (0-255 each). Required for write action.",
|
"description": "Bytes to write (0-255 each). Required for write action.",
|
||||||
},
|
},
|
||||||
"length": map[string]interface{}{
|
"length": map[string]any{
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"description": "Number of bytes to read (1-256). Default: 1. Used with read action.",
|
"description": "Number of bytes to read (1-256). Default: 1. Used with read action.",
|
||||||
},
|
},
|
||||||
"confirm": map[string]interface{}{
|
"confirm": map[string]any{
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"description": "Must be true for write operations. Safety guard to prevent accidental writes.",
|
"description": "Must be true for write operations. Safety guard to prevent accidental writes.",
|
||||||
},
|
},
|
||||||
|
|
@ -63,7 +63,7 @@ func (t *I2CTool) Parameters() map[string]interface{} {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *I2CTool) Execute(ctx context.Context, args map[string]interface{}) *ToolResult {
|
func (t *I2CTool) Execute(ctx context.Context, args map[string]any) *ToolResult {
|
||||||
if runtime.GOOS != "linux" {
|
if runtime.GOOS != "linux" {
|
||||||
return ErrorResult("I2C is only supported on Linux. This tool requires /dev/i2c-* device files.")
|
return ErrorResult("I2C is only supported on Linux. This tool requires /dev/i2c-* device files.")
|
||||||
}
|
}
|
||||||
|
|
@ -95,7 +95,9 @@ func (t *I2CTool) detect() *ToolResult {
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(matches) == 0 {
|
if len(matches) == 0 {
|
||||||
return SilentResult("No I2C buses found. You may need to:\n1. Load the i2c-dev module: modprobe i2c-dev\n2. Check that I2C is enabled in device tree\n3. Configure pinmux for your board (see hardware skill)")
|
return SilentResult(
|
||||||
|
"No I2C buses found. You may need to:\n1. Load the i2c-dev module: modprobe i2c-dev\n2. Check that I2C is enabled in device tree\n3. Configure pinmux for your board (see hardware skill)",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
type busInfo struct {
|
type busInfo struct {
|
||||||
|
|
@ -122,7 +124,7 @@ func isValidBusID(id string) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseI2CAddress extracts and validates an I2C address from args
|
// parseI2CAddress extracts and validates an I2C address from args
|
||||||
func parseI2CAddress(args map[string]interface{}) (int, *ToolResult) {
|
func parseI2CAddress(args map[string]any) (int, *ToolResult) {
|
||||||
addrFloat, ok := args["address"].(float64)
|
addrFloat, ok := args["address"].(float64)
|
||||||
if !ok {
|
if !ok {
|
||||||
return 0, ErrorResult("address is required (e.g. 0x38 for AHT20)")
|
return 0, ErrorResult("address is required (e.g. 0x38 for AHT20)")
|
||||||
|
|
@ -135,7 +137,7 @@ func parseI2CAddress(args map[string]interface{}) (int, *ToolResult) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseI2CBus extracts and validates an I2C bus from args
|
// parseI2CBus extracts and validates an I2C bus from args
|
||||||
func parseI2CBus(args map[string]interface{}) (string, *ToolResult) {
|
func parseI2CBus(args map[string]any) (string, *ToolResult) {
|
||||||
bus, ok := args["bus"].(string)
|
bus, ok := args["bus"].(string)
|
||||||
if !ok || bus == "" {
|
if !ok || bus == "" {
|
||||||
return "", ErrorResult("bus is required (e.g. \"1\" for /dev/i2c-1)")
|
return "", ErrorResult("bus is required (e.g. \"1\" for /dev/i2c-1)")
|
||||||
|
|
|
||||||
|
|
@ -74,7 +74,7 @@ func smbusProbe(fd int, addr int, hasQuick bool) bool {
|
||||||
// scan probes valid 7-bit addresses on a bus for connected devices.
|
// scan probes valid 7-bit addresses on a bus for connected devices.
|
||||||
// Uses the same hybrid probe strategy as i2cdetect's MODE_AUTO:
|
// Uses the same hybrid probe strategy as i2cdetect's MODE_AUTO:
|
||||||
// SMBus Quick Write for most addresses, SMBus Read Byte for EEPROM ranges.
|
// SMBus Quick Write for most addresses, SMBus Read Byte for EEPROM ranges.
|
||||||
func (t *I2CTool) scan(args map[string]interface{}) *ToolResult {
|
func (t *I2CTool) scan(args map[string]any) *ToolResult {
|
||||||
bus, errResult := parseI2CBus(args)
|
bus, errResult := parseI2CBus(args)
|
||||||
if errResult != nil {
|
if errResult != nil {
|
||||||
return errResult
|
return errResult
|
||||||
|
|
@ -99,7 +99,9 @@ func (t *I2CTool) scan(args map[string]interface{}) *ToolResult {
|
||||||
hasReadByte := funcs&i2cFuncSmbusReadByte != 0
|
hasReadByte := funcs&i2cFuncSmbusReadByte != 0
|
||||||
|
|
||||||
if !hasQuick && !hasReadByte {
|
if !hasQuick && !hasReadByte {
|
||||||
return ErrorResult(fmt.Sprintf("I2C adapter %s supports neither SMBus Quick nor Read Byte — cannot probe safely", devPath))
|
return ErrorResult(
|
||||||
|
fmt.Sprintf("I2C adapter %s supports neither SMBus Quick nor Read Byte — cannot probe safely", devPath),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
type deviceEntry struct {
|
type deviceEntry struct {
|
||||||
|
|
@ -133,7 +135,7 @@ func (t *I2CTool) scan(args map[string]interface{}) *ToolResult {
|
||||||
return SilentResult(fmt.Sprintf("No devices found on %s. Check wiring and pull-up resistors.", devPath))
|
return SilentResult(fmt.Sprintf("No devices found on %s. Check wiring and pull-up resistors.", devPath))
|
||||||
}
|
}
|
||||||
|
|
||||||
result, _ := json.MarshalIndent(map[string]interface{}{
|
result, _ := json.MarshalIndent(map[string]any{
|
||||||
"bus": devPath,
|
"bus": devPath,
|
||||||
"devices": found,
|
"devices": found,
|
||||||
"count": len(found),
|
"count": len(found),
|
||||||
|
|
@ -142,7 +144,7 @@ func (t *I2CTool) scan(args map[string]interface{}) *ToolResult {
|
||||||
}
|
}
|
||||||
|
|
||||||
// readDevice reads bytes from an I2C device, optionally at a specific register
|
// readDevice reads bytes from an I2C device, optionally at a specific register
|
||||||
func (t *I2CTool) readDevice(args map[string]interface{}) *ToolResult {
|
func (t *I2CTool) readDevice(args map[string]any) *ToolResult {
|
||||||
bus, errResult := parseI2CBus(args)
|
bus, errResult := parseI2CBus(args)
|
||||||
if errResult != nil {
|
if errResult != nil {
|
||||||
return errResult
|
return errResult
|
||||||
|
|
@ -201,7 +203,7 @@ func (t *I2CTool) readDevice(args map[string]interface{}) *ToolResult {
|
||||||
intBytes[i] = int(buf[i])
|
intBytes[i] = int(buf[i])
|
||||||
}
|
}
|
||||||
|
|
||||||
result, _ := json.MarshalIndent(map[string]interface{}{
|
result, _ := json.MarshalIndent(map[string]any{
|
||||||
"bus": devPath,
|
"bus": devPath,
|
||||||
"address": fmt.Sprintf("0x%02x", addr),
|
"address": fmt.Sprintf("0x%02x", addr),
|
||||||
"bytes": intBytes,
|
"bytes": intBytes,
|
||||||
|
|
@ -212,10 +214,12 @@ func (t *I2CTool) readDevice(args map[string]interface{}) *ToolResult {
|
||||||
}
|
}
|
||||||
|
|
||||||
// writeDevice writes bytes to an I2C device, optionally at a specific register
|
// writeDevice writes bytes to an I2C device, optionally at a specific register
|
||||||
func (t *I2CTool) writeDevice(args map[string]interface{}) *ToolResult {
|
func (t *I2CTool) writeDevice(args map[string]any) *ToolResult {
|
||||||
confirm, _ := args["confirm"].(bool)
|
confirm, _ := args["confirm"].(bool)
|
||||||
if !confirm {
|
if !confirm {
|
||||||
return ErrorResult("write operations require confirm: true. Please confirm with the user before writing to I2C devices, as incorrect writes can misconfigure hardware.")
|
return ErrorResult(
|
||||||
|
"write operations require confirm: true. Please confirm with the user before writing to I2C devices, as incorrect writes can misconfigure hardware.",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
bus, errResult := parseI2CBus(args)
|
bus, errResult := parseI2CBus(args)
|
||||||
|
|
@ -228,7 +232,7 @@ func (t *I2CTool) writeDevice(args map[string]interface{}) *ToolResult {
|
||||||
return errResult
|
return errResult
|
||||||
}
|
}
|
||||||
|
|
||||||
dataRaw, ok := args["data"].([]interface{})
|
dataRaw, ok := args["data"].([]any)
|
||||||
if !ok || len(dataRaw) == 0 {
|
if !ok || len(dataRaw) == 0 {
|
||||||
return ErrorResult("data is required for write (array of byte values 0-255)")
|
return ErrorResult("data is required for write (array of byte values 0-255)")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,16 +3,16 @@
|
||||||
package tools
|
package tools
|
||||||
|
|
||||||
// scan is a stub for non-Linux platforms.
|
// scan is a stub for non-Linux platforms.
|
||||||
func (t *I2CTool) scan(args map[string]interface{}) *ToolResult {
|
func (t *I2CTool) scan(args map[string]any) *ToolResult {
|
||||||
return ErrorResult("I2C is only supported on Linux")
|
return ErrorResult("I2C is only supported on Linux")
|
||||||
}
|
}
|
||||||
|
|
||||||
// readDevice is a stub for non-Linux platforms.
|
// readDevice is a stub for non-Linux platforms.
|
||||||
func (t *I2CTool) readDevice(args map[string]interface{}) *ToolResult {
|
func (t *I2CTool) readDevice(args map[string]any) *ToolResult {
|
||||||
return ErrorResult("I2C is only supported on Linux")
|
return ErrorResult("I2C is only supported on Linux")
|
||||||
}
|
}
|
||||||
|
|
||||||
// writeDevice is a stub for non-Linux platforms.
|
// writeDevice is a stub for non-Linux platforms.
|
||||||
func (t *I2CTool) writeDevice(args map[string]interface{}) *ToolResult {
|
func (t *I2CTool) writeDevice(args map[string]any) *ToolResult {
|
||||||
return ErrorResult("I2C is only supported on Linux")
|
return ErrorResult("I2C is only supported on Linux")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,19 +26,19 @@ func (t *MessageTool) Description() string {
|
||||||
return "Send a message to user on a chat channel. Use this when you want to communicate something."
|
return "Send a message to user on a chat channel. Use this when you want to communicate something."
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *MessageTool) Parameters() map[string]interface{} {
|
func (t *MessageTool) Parameters() map[string]any {
|
||||||
return map[string]interface{}{
|
return map[string]any{
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": map[string]interface{}{
|
"properties": map[string]any{
|
||||||
"content": map[string]interface{}{
|
"content": map[string]any{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "The message content to send",
|
"description": "The message content to send",
|
||||||
},
|
},
|
||||||
"channel": map[string]interface{}{
|
"channel": map[string]any{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Optional: target channel (telegram, whatsapp, etc.)",
|
"description": "Optional: target channel (telegram, whatsapp, etc.)",
|
||||||
},
|
},
|
||||||
"chat_id": map[string]interface{}{
|
"chat_id": map[string]any{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Optional: target chat/user ID",
|
"description": "Optional: target chat/user ID",
|
||||||
},
|
},
|
||||||
|
|
@ -62,7 +62,7 @@ func (t *MessageTool) SetSendCallback(callback SendCallback) {
|
||||||
t.sendCallback = callback
|
t.sendCallback = callback
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *MessageTool) Execute(ctx context.Context, args map[string]interface{}) *ToolResult {
|
func (t *MessageTool) Execute(ctx context.Context, args map[string]any) *ToolResult {
|
||||||
content, ok := args["content"].(string)
|
content, ok := args["content"].(string)
|
||||||
if !ok {
|
if !ok {
|
||||||
return &ToolResult{ForLLM: "content is required", IsError: true}
|
return &ToolResult{ForLLM: "content is required", IsError: true}
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ func TestMessageTool_Execute_Success(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
args := map[string]interface{}{
|
args := map[string]any{
|
||||||
"content": "Hello, world!",
|
"content": "Hello, world!",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -70,7 +70,7 @@ func TestMessageTool_Execute_WithCustomChannel(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
args := map[string]interface{}{
|
args := map[string]any{
|
||||||
"content": "Test message",
|
"content": "Test message",
|
||||||
"channel": "custom-channel",
|
"channel": "custom-channel",
|
||||||
"chat_id": "custom-chat-id",
|
"chat_id": "custom-chat-id",
|
||||||
|
|
@ -104,7 +104,7 @@ func TestMessageTool_Execute_SendFailure(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
args := map[string]interface{}{
|
args := map[string]any{
|
||||||
"content": "Test message",
|
"content": "Test message",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -136,7 +136,7 @@ func TestMessageTool_Execute_MissingContent(t *testing.T) {
|
||||||
tool.SetContext("test-channel", "test-chat-id")
|
tool.SetContext("test-channel", "test-chat-id")
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
args := map[string]interface{}{} // content missing
|
args := map[string]any{} // content missing
|
||||||
|
|
||||||
result := tool.Execute(ctx, args)
|
result := tool.Execute(ctx, args)
|
||||||
|
|
||||||
|
|
@ -158,7 +158,7 @@ func TestMessageTool_Execute_NoTargetChannel(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
args := map[string]interface{}{
|
args := map[string]any{
|
||||||
"content": "Test message",
|
"content": "Test message",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -179,7 +179,7 @@ func TestMessageTool_Execute_NotConfigured(t *testing.T) {
|
||||||
// No SetSendCallback called
|
// No SetSendCallback called
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
args := map[string]interface{}{
|
args := map[string]any{
|
||||||
"content": "Test message",
|
"content": "Test message",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -219,7 +219,7 @@ func TestMessageTool_Parameters(t *testing.T) {
|
||||||
t.Error("Expected type 'object'")
|
t.Error("Expected type 'object'")
|
||||||
}
|
}
|
||||||
|
|
||||||
props, ok := params["properties"].(map[string]interface{})
|
props, ok := params["properties"].(map[string]any)
|
||||||
if !ok {
|
if !ok {
|
||||||
t.Fatal("Expected properties to be a map")
|
t.Fatal("Expected properties to be a map")
|
||||||
}
|
}
|
||||||
|
|
@ -231,7 +231,7 @@ func TestMessageTool_Parameters(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check content property
|
// Check content property
|
||||||
contentProp, ok := props["content"].(map[string]interface{})
|
contentProp, ok := props["content"].(map[string]any)
|
||||||
if !ok {
|
if !ok {
|
||||||
t.Error("Expected 'content' property")
|
t.Error("Expected 'content' property")
|
||||||
}
|
}
|
||||||
|
|
@ -240,7 +240,7 @@ func TestMessageTool_Parameters(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check channel property (optional)
|
// Check channel property (optional)
|
||||||
channelProp, ok := props["channel"].(map[string]interface{})
|
channelProp, ok := props["channel"].(map[string]any)
|
||||||
if !ok {
|
if !ok {
|
||||||
t.Error("Expected 'channel' property")
|
t.Error("Expected 'channel' property")
|
||||||
}
|
}
|
||||||
|
|
@ -249,7 +249,7 @@ func TestMessageTool_Parameters(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check chat_id property (optional)
|
// Check chat_id property (optional)
|
||||||
chatIDProp, ok := props["chat_id"].(map[string]interface{})
|
chatIDProp, ok := props["chat_id"].(map[string]any)
|
||||||
if !ok {
|
if !ok {
|
||||||
t.Error("Expected 'chat_id' property")
|
t.Error("Expected 'chat_id' property")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -34,16 +34,22 @@ func (r *ToolRegistry) Get(name string) (Tool, bool) {
|
||||||
return tool, ok
|
return tool, ok
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *ToolRegistry) Execute(ctx context.Context, name string, args map[string]interface{}) *ToolResult {
|
func (r *ToolRegistry) Execute(ctx context.Context, name string, args map[string]any) *ToolResult {
|
||||||
return r.ExecuteWithContext(ctx, name, args, "", "", nil)
|
return r.ExecuteWithContext(ctx, name, args, "", "", nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExecuteWithContext executes a tool with channel/chatID context and optional async callback.
|
// ExecuteWithContext executes a tool with channel/chatID context and optional async callback.
|
||||||
// If the tool implements AsyncTool and a non-nil callback is provided,
|
// If the tool implements AsyncTool and a non-nil callback is provided,
|
||||||
// the callback will be set on the tool before execution.
|
// the callback will be set on the tool before execution.
|
||||||
func (r *ToolRegistry) ExecuteWithContext(ctx context.Context, name string, args map[string]interface{}, channel, chatID string, asyncCallback AsyncCallback) *ToolResult {
|
func (r *ToolRegistry) ExecuteWithContext(
|
||||||
|
ctx context.Context,
|
||||||
|
name string,
|
||||||
|
args map[string]any,
|
||||||
|
channel, chatID string,
|
||||||
|
asyncCallback AsyncCallback,
|
||||||
|
) *ToolResult {
|
||||||
logger.InfoCF("tool", "Tool execution started",
|
logger.InfoCF("tool", "Tool execution started",
|
||||||
map[string]interface{}{
|
map[string]any{
|
||||||
"tool": name,
|
"tool": name,
|
||||||
"args": args,
|
"args": args,
|
||||||
})
|
})
|
||||||
|
|
@ -51,7 +57,7 @@ func (r *ToolRegistry) ExecuteWithContext(ctx context.Context, name string, args
|
||||||
tool, ok := r.Get(name)
|
tool, ok := r.Get(name)
|
||||||
if !ok {
|
if !ok {
|
||||||
logger.ErrorCF("tool", "Tool not found",
|
logger.ErrorCF("tool", "Tool not found",
|
||||||
map[string]interface{}{
|
map[string]any{
|
||||||
"tool": name,
|
"tool": name,
|
||||||
})
|
})
|
||||||
return ErrorResult(fmt.Sprintf("tool %q not found", name)).WithError(fmt.Errorf("tool not found"))
|
return ErrorResult(fmt.Sprintf("tool %q not found", name)).WithError(fmt.Errorf("tool not found"))
|
||||||
|
|
@ -66,7 +72,7 @@ func (r *ToolRegistry) ExecuteWithContext(ctx context.Context, name string, args
|
||||||
if asyncTool, ok := tool.(AsyncTool); ok && asyncCallback != nil {
|
if asyncTool, ok := tool.(AsyncTool); ok && asyncCallback != nil {
|
||||||
asyncTool.SetCallback(asyncCallback)
|
asyncTool.SetCallback(asyncCallback)
|
||||||
logger.DebugCF("tool", "Async callback injected",
|
logger.DebugCF("tool", "Async callback injected",
|
||||||
map[string]interface{}{
|
map[string]any{
|
||||||
"tool": name,
|
"tool": name,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -78,20 +84,20 @@ func (r *ToolRegistry) ExecuteWithContext(ctx context.Context, name string, args
|
||||||
// Log based on result type
|
// Log based on result type
|
||||||
if result.IsError {
|
if result.IsError {
|
||||||
logger.ErrorCF("tool", "Tool execution failed",
|
logger.ErrorCF("tool", "Tool execution failed",
|
||||||
map[string]interface{}{
|
map[string]any{
|
||||||
"tool": name,
|
"tool": name,
|
||||||
"duration": duration.Milliseconds(),
|
"duration": duration.Milliseconds(),
|
||||||
"error": result.ForLLM,
|
"error": result.ForLLM,
|
||||||
})
|
})
|
||||||
} else if result.Async {
|
} else if result.Async {
|
||||||
logger.InfoCF("tool", "Tool started (async)",
|
logger.InfoCF("tool", "Tool started (async)",
|
||||||
map[string]interface{}{
|
map[string]any{
|
||||||
"tool": name,
|
"tool": name,
|
||||||
"duration": duration.Milliseconds(),
|
"duration": duration.Milliseconds(),
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
logger.InfoCF("tool", "Tool execution completed",
|
logger.InfoCF("tool", "Tool execution completed",
|
||||||
map[string]interface{}{
|
map[string]any{
|
||||||
"tool": name,
|
"tool": name,
|
||||||
"duration_ms": duration.Milliseconds(),
|
"duration_ms": duration.Milliseconds(),
|
||||||
"result_length": len(result.ForLLM),
|
"result_length": len(result.ForLLM),
|
||||||
|
|
@ -101,11 +107,11 @@ func (r *ToolRegistry) ExecuteWithContext(ctx context.Context, name string, args
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *ToolRegistry) GetDefinitions() []map[string]interface{} {
|
func (r *ToolRegistry) GetDefinitions() []map[string]any {
|
||||||
r.mu.RLock()
|
r.mu.RLock()
|
||||||
defer r.mu.RUnlock()
|
defer r.mu.RUnlock()
|
||||||
|
|
||||||
definitions := make([]map[string]interface{}, 0, len(r.tools))
|
definitions := make([]map[string]any, 0, len(r.tools))
|
||||||
for _, tool := range r.tools {
|
for _, tool := range r.tools {
|
||||||
definitions = append(definitions, ToolToSchema(tool))
|
definitions = append(definitions, ToolToSchema(tool))
|
||||||
}
|
}
|
||||||
|
|
@ -123,14 +129,14 @@ func (r *ToolRegistry) ToProviderDefs() []providers.ToolDefinition {
|
||||||
schema := ToolToSchema(tool)
|
schema := ToolToSchema(tool)
|
||||||
|
|
||||||
// Safely extract nested values with type checks
|
// Safely extract nested values with type checks
|
||||||
fn, ok := schema["function"].(map[string]interface{})
|
fn, ok := schema["function"].(map[string]any)
|
||||||
if !ok {
|
if !ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
name, _ := fn["name"].(string)
|
name, _ := fn["name"].(string)
|
||||||
desc, _ := fn["description"].(string)
|
desc, _ := fn["description"].(string)
|
||||||
params, _ := fn["parameters"].(map[string]interface{})
|
params, _ := fn["parameters"].(map[string]any)
|
||||||
|
|
||||||
definitions = append(definitions, providers.ToolDefinition{
|
definitions = append(definitions, providers.ToolDefinition{
|
||||||
Type: "function",
|
Type: "function",
|
||||||
|
|
|
||||||
|
|
@ -192,7 +192,7 @@ func TestToolResultJSONStructure(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify JSON structure
|
// Verify JSON structure
|
||||||
var parsed map[string]interface{}
|
var parsed map[string]any
|
||||||
if err := json.Unmarshal(data, &parsed); err != nil {
|
if err := json.Unmarshal(data, &parsed); err != nil {
|
||||||
t.Fatalf("Failed to parse JSON: %v", err)
|
t.Fatalf("Failed to parse JSON: %v", err)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -118,15 +118,15 @@ func (t *ExecTool) Description() string {
|
||||||
return "Execute a shell command and return its output. Use with caution."
|
return "Execute a shell command and return its output. Use with caution."
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *ExecTool) Parameters() map[string]interface{} {
|
func (t *ExecTool) Parameters() map[string]any {
|
||||||
return map[string]interface{}{
|
return map[string]any{
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": map[string]interface{}{
|
"properties": map[string]any{
|
||||||
"command": map[string]interface{}{
|
"command": map[string]any{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "The shell command to execute",
|
"description": "The shell command to execute",
|
||||||
},
|
},
|
||||||
"working_dir": map[string]interface{}{
|
"working_dir": map[string]any{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Optional working directory for the command",
|
"description": "Optional working directory for the command",
|
||||||
},
|
},
|
||||||
|
|
@ -135,7 +135,7 @@ func (t *ExecTool) Parameters() map[string]interface{} {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *ExecTool) Execute(ctx context.Context, args map[string]interface{}) *ToolResult {
|
func (t *ExecTool) Execute(ctx context.Context, args map[string]any) *ToolResult {
|
||||||
command, ok := args["command"].(string)
|
command, ok := args["command"].(string)
|
||||||
if !ok {
|
if !ok {
|
||||||
return ErrorResult("command is required")
|
return ErrorResult("command is required")
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ func TestShellTool_Success(t *testing.T) {
|
||||||
tool := NewExecTool("", false)
|
tool := NewExecTool("", false)
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
args := map[string]interface{}{
|
args := map[string]any{
|
||||||
"command": "echo 'hello world'",
|
"command": "echo 'hello world'",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -41,7 +41,7 @@ func TestShellTool_Failure(t *testing.T) {
|
||||||
tool := NewExecTool("", false)
|
tool := NewExecTool("", false)
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
args := map[string]interface{}{
|
args := map[string]any{
|
||||||
"command": "ls /nonexistent_directory_12345",
|
"command": "ls /nonexistent_directory_12345",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -69,7 +69,7 @@ func TestShellTool_Timeout(t *testing.T) {
|
||||||
tool.SetTimeout(100 * time.Millisecond)
|
tool.SetTimeout(100 * time.Millisecond)
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
args := map[string]interface{}{
|
args := map[string]any{
|
||||||
"command": "sleep 10",
|
"command": "sleep 10",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -91,12 +91,12 @@ func TestShellTool_WorkingDir(t *testing.T) {
|
||||||
// Create temp directory
|
// Create temp directory
|
||||||
tmpDir := t.TempDir()
|
tmpDir := t.TempDir()
|
||||||
testFile := filepath.Join(tmpDir, "test.txt")
|
testFile := filepath.Join(tmpDir, "test.txt")
|
||||||
os.WriteFile(testFile, []byte("test content"), 0644)
|
os.WriteFile(testFile, []byte("test content"), 0o644)
|
||||||
|
|
||||||
tool := NewExecTool("", false)
|
tool := NewExecTool("", false)
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
args := map[string]interface{}{
|
args := map[string]any{
|
||||||
"command": "cat test.txt",
|
"command": "cat test.txt",
|
||||||
"working_dir": tmpDir,
|
"working_dir": tmpDir,
|
||||||
}
|
}
|
||||||
|
|
@ -117,7 +117,7 @@ func TestShellTool_DangerousCommand(t *testing.T) {
|
||||||
tool := NewExecTool("", false)
|
tool := NewExecTool("", false)
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
args := map[string]interface{}{
|
args := map[string]any{
|
||||||
"command": "rm -rf /",
|
"command": "rm -rf /",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -138,7 +138,7 @@ func TestShellTool_MissingCommand(t *testing.T) {
|
||||||
tool := NewExecTool("", false)
|
tool := NewExecTool("", false)
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
args := map[string]interface{}{}
|
args := map[string]any{}
|
||||||
|
|
||||||
result := tool.Execute(ctx, args)
|
result := tool.Execute(ctx, args)
|
||||||
|
|
||||||
|
|
@ -153,7 +153,7 @@ func TestShellTool_StderrCapture(t *testing.T) {
|
||||||
tool := NewExecTool("", false)
|
tool := NewExecTool("", false)
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
args := map[string]interface{}{
|
args := map[string]any{
|
||||||
"command": "sh -c 'echo stdout; echo stderr >&2'",
|
"command": "sh -c 'echo stdout; echo stderr >&2'",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -174,7 +174,7 @@ func TestShellTool_OutputTruncation(t *testing.T) {
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
// Generate long output (>10000 chars)
|
// Generate long output (>10000 chars)
|
||||||
args := map[string]interface{}{
|
args := map[string]any{
|
||||||
"command": "python3 -c \"print('x' * 20000)\" || echo " + strings.Repeat("x", 20000),
|
"command": "python3 -c \"print('x' * 20000)\" || echo " + strings.Repeat("x", 20000),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -193,7 +193,7 @@ func TestShellTool_RestrictToWorkspace(t *testing.T) {
|
||||||
tool.SetRestrictToWorkspace(true)
|
tool.SetRestrictToWorkspace(true)
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
args := map[string]interface{}{
|
args := map[string]any{
|
||||||
"command": "cat ../../etc/passwd",
|
"command": "cat ../../etc/passwd",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -205,6 +205,10 @@ func TestShellTool_RestrictToWorkspace(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if !strings.Contains(result.ForLLM, "blocked") && !strings.Contains(result.ForUser, "blocked") {
|
if !strings.Contains(result.ForLLM, "blocked") && !strings.Contains(result.ForUser, "blocked") {
|
||||||
t.Errorf("Expected 'blocked' message for path traversal, got ForLLM: %s, ForUser: %s", result.ForLLM, result.ForUser)
|
t.Errorf(
|
||||||
|
"Expected 'blocked' message for path traversal, got ForLLM: %s, ForUser: %s",
|
||||||
|
result.ForLLM,
|
||||||
|
result.ForUser,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -34,19 +34,19 @@ func (t *SpawnTool) Description() string {
|
||||||
return "Spawn a subagent to handle a task in the background. Use this for complex or time-consuming tasks that can run independently. The subagent will complete the task and report back when done."
|
return "Spawn a subagent to handle a task in the background. Use this for complex or time-consuming tasks that can run independently. The subagent will complete the task and report back when done."
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *SpawnTool) Parameters() map[string]interface{} {
|
func (t *SpawnTool) Parameters() map[string]any {
|
||||||
return map[string]interface{}{
|
return map[string]any{
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": map[string]interface{}{
|
"properties": map[string]any{
|
||||||
"task": map[string]interface{}{
|
"task": map[string]any{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "The task for subagent to complete",
|
"description": "The task for subagent to complete",
|
||||||
},
|
},
|
||||||
"label": map[string]interface{}{
|
"label": map[string]any{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Optional short label for the task (for display)",
|
"description": "Optional short label for the task (for display)",
|
||||||
},
|
},
|
||||||
"agent_id": map[string]interface{}{
|
"agent_id": map[string]any{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Optional target agent ID to delegate the task to",
|
"description": "Optional target agent ID to delegate the task to",
|
||||||
},
|
},
|
||||||
|
|
@ -64,7 +64,7 @@ func (t *SpawnTool) SetAllowlistChecker(check func(targetAgentID string) bool) {
|
||||||
t.allowlistCheck = check
|
t.allowlistCheck = check
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *SpawnTool) Execute(ctx context.Context, args map[string]interface{}) *ToolResult {
|
func (t *SpawnTool) Execute(ctx context.Context, args map[string]any) *ToolResult {
|
||||||
task, ok := args["task"].(string)
|
task, ok := args["task"].(string)
|
||||||
if !ok {
|
if !ok {
|
||||||
return ErrorResult("task is required")
|
return ErrorResult("task is required")
|
||||||
|
|
|
||||||
|
|
@ -24,41 +24,41 @@ func (t *SPITool) Description() string {
|
||||||
return "Interact with SPI bus devices for high-speed peripheral communication. Actions: list (find SPI devices), transfer (full-duplex send/receive), read (receive bytes). Linux only."
|
return "Interact with SPI bus devices for high-speed peripheral communication. Actions: list (find SPI devices), transfer (full-duplex send/receive), read (receive bytes). Linux only."
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *SPITool) Parameters() map[string]interface{} {
|
func (t *SPITool) Parameters() map[string]any {
|
||||||
return map[string]interface{}{
|
return map[string]any{
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": map[string]interface{}{
|
"properties": map[string]any{
|
||||||
"action": map[string]interface{}{
|
"action": map[string]any{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": []string{"list", "transfer", "read"},
|
"enum": []string{"list", "transfer", "read"},
|
||||||
"description": "Action to perform: list (find available SPI devices), transfer (full-duplex send/receive), read (receive bytes by sending zeros)",
|
"description": "Action to perform: list (find available SPI devices), transfer (full-duplex send/receive), read (receive bytes by sending zeros)",
|
||||||
},
|
},
|
||||||
"device": map[string]interface{}{
|
"device": map[string]any{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "SPI device identifier (e.g. \"2.0\" for /dev/spidev2.0). Required for transfer/read.",
|
"description": "SPI device identifier (e.g. \"2.0\" for /dev/spidev2.0). Required for transfer/read.",
|
||||||
},
|
},
|
||||||
"speed": map[string]interface{}{
|
"speed": map[string]any{
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"description": "SPI clock speed in Hz. Default: 1000000 (1 MHz).",
|
"description": "SPI clock speed in Hz. Default: 1000000 (1 MHz).",
|
||||||
},
|
},
|
||||||
"mode": map[string]interface{}{
|
"mode": map[string]any{
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"description": "SPI mode (0-3). Default: 0. Mode sets CPOL and CPHA: 0=0,0 1=0,1 2=1,0 3=1,1.",
|
"description": "SPI mode (0-3). Default: 0. Mode sets CPOL and CPHA: 0=0,0 1=0,1 2=1,0 3=1,1.",
|
||||||
},
|
},
|
||||||
"bits": map[string]interface{}{
|
"bits": map[string]any{
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"description": "Bits per word. Default: 8.",
|
"description": "Bits per word. Default: 8.",
|
||||||
},
|
},
|
||||||
"data": map[string]interface{}{
|
"data": map[string]any{
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": map[string]interface{}{"type": "integer"},
|
"items": map[string]any{"type": "integer"},
|
||||||
"description": "Bytes to send (0-255 each). Required for transfer action.",
|
"description": "Bytes to send (0-255 each). Required for transfer action.",
|
||||||
},
|
},
|
||||||
"length": map[string]interface{}{
|
"length": map[string]any{
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"description": "Number of bytes to read (1-4096). Required for read action.",
|
"description": "Number of bytes to read (1-4096). Required for read action.",
|
||||||
},
|
},
|
||||||
"confirm": map[string]interface{}{
|
"confirm": map[string]any{
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"description": "Must be true for transfer operations. Safety guard to prevent accidental writes.",
|
"description": "Must be true for transfer operations. Safety guard to prevent accidental writes.",
|
||||||
},
|
},
|
||||||
|
|
@ -67,7 +67,7 @@ func (t *SPITool) Parameters() map[string]interface{} {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *SPITool) Execute(ctx context.Context, args map[string]interface{}) *ToolResult {
|
func (t *SPITool) Execute(ctx context.Context, args map[string]any) *ToolResult {
|
||||||
if runtime.GOOS != "linux" {
|
if runtime.GOOS != "linux" {
|
||||||
return ErrorResult("SPI is only supported on Linux. This tool requires /dev/spidev* device files.")
|
return ErrorResult("SPI is only supported on Linux. This tool requires /dev/spidev* device files.")
|
||||||
}
|
}
|
||||||
|
|
@ -97,7 +97,9 @@ func (t *SPITool) list() *ToolResult {
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(matches) == 0 {
|
if len(matches) == 0 {
|
||||||
return SilentResult("No SPI devices found. You may need to:\n1. Enable SPI in device tree\n2. Configure pinmux for your board (see hardware skill)\n3. Check that spidev module is loaded")
|
return SilentResult(
|
||||||
|
"No SPI devices found. You may need to:\n1. Enable SPI in device tree\n2. Configure pinmux for your board (see hardware skill)\n3. Check that spidev module is loaded",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
type devInfo struct {
|
type devInfo struct {
|
||||||
|
|
@ -118,7 +120,7 @@ func (t *SPITool) list() *ToolResult {
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseSPIArgs extracts and validates common SPI parameters
|
// parseSPIArgs extracts and validates common SPI parameters
|
||||||
func parseSPIArgs(args map[string]interface{}) (device string, speed uint32, mode uint8, bits uint8, errMsg string) {
|
func parseSPIArgs(args map[string]any) (device string, speed uint32, mode uint8, bits uint8, errMsg string) {
|
||||||
dev, ok := args["device"].(string)
|
dev, ok := args["device"].(string)
|
||||||
if !ok || dev == "" {
|
if !ok || dev == "" {
|
||||||
return "", 0, 0, 0, "device is required (e.g. \"2.0\" for /dev/spidev2.0)"
|
return "", 0, 0, 0, "device is required (e.g. \"2.0\" for /dev/spidev2.0)"
|
||||||
|
|
|
||||||
|
|
@ -66,10 +66,12 @@ func configureSPI(devPath string, mode uint8, bits uint8, speed uint32) (int, *T
|
||||||
}
|
}
|
||||||
|
|
||||||
// transfer performs a full-duplex SPI transfer
|
// transfer performs a full-duplex SPI transfer
|
||||||
func (t *SPITool) transfer(args map[string]interface{}) *ToolResult {
|
func (t *SPITool) transfer(args map[string]any) *ToolResult {
|
||||||
confirm, _ := args["confirm"].(bool)
|
confirm, _ := args["confirm"].(bool)
|
||||||
if !confirm {
|
if !confirm {
|
||||||
return ErrorResult("transfer operations require confirm: true. Please confirm with the user before sending data to SPI devices.")
|
return ErrorResult(
|
||||||
|
"transfer operations require confirm: true. Please confirm with the user before sending data to SPI devices.",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
dev, speed, mode, bits, errMsg := parseSPIArgs(args)
|
dev, speed, mode, bits, errMsg := parseSPIArgs(args)
|
||||||
|
|
@ -77,7 +79,7 @@ func (t *SPITool) transfer(args map[string]interface{}) *ToolResult {
|
||||||
return ErrorResult(errMsg)
|
return ErrorResult(errMsg)
|
||||||
}
|
}
|
||||||
|
|
||||||
dataRaw, ok := args["data"].([]interface{})
|
dataRaw, ok := args["data"].([]any)
|
||||||
if !ok || len(dataRaw) == 0 {
|
if !ok || len(dataRaw) == 0 {
|
||||||
return ErrorResult("data is required for transfer (array of byte values 0-255)")
|
return ErrorResult("data is required for transfer (array of byte values 0-255)")
|
||||||
}
|
}
|
||||||
|
|
@ -130,7 +132,7 @@ func (t *SPITool) transfer(args map[string]interface{}) *ToolResult {
|
||||||
intBytes[i] = int(b)
|
intBytes[i] = int(b)
|
||||||
}
|
}
|
||||||
|
|
||||||
result, _ := json.MarshalIndent(map[string]interface{}{
|
result, _ := json.MarshalIndent(map[string]any{
|
||||||
"device": devPath,
|
"device": devPath,
|
||||||
"sent": len(txBuf),
|
"sent": len(txBuf),
|
||||||
"received": intBytes,
|
"received": intBytes,
|
||||||
|
|
@ -140,7 +142,7 @@ func (t *SPITool) transfer(args map[string]interface{}) *ToolResult {
|
||||||
}
|
}
|
||||||
|
|
||||||
// readDevice reads bytes from SPI by sending zeros (read-only, no confirm needed)
|
// readDevice reads bytes from SPI by sending zeros (read-only, no confirm needed)
|
||||||
func (t *SPITool) readDevice(args map[string]interface{}) *ToolResult {
|
func (t *SPITool) readDevice(args map[string]any) *ToolResult {
|
||||||
dev, speed, mode, bits, errMsg := parseSPIArgs(args)
|
dev, speed, mode, bits, errMsg := parseSPIArgs(args)
|
||||||
if errMsg != "" {
|
if errMsg != "" {
|
||||||
return ErrorResult(errMsg)
|
return ErrorResult(errMsg)
|
||||||
|
|
@ -186,7 +188,7 @@ func (t *SPITool) readDevice(args map[string]interface{}) *ToolResult {
|
||||||
intBytes[i] = int(b)
|
intBytes[i] = int(b)
|
||||||
}
|
}
|
||||||
|
|
||||||
result, _ := json.MarshalIndent(map[string]interface{}{
|
result, _ := json.MarshalIndent(map[string]any{
|
||||||
"device": devPath,
|
"device": devPath,
|
||||||
"bytes": intBytes,
|
"bytes": intBytes,
|
||||||
"hex": hexBytes,
|
"hex": hexBytes,
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,11 @@
|
||||||
package tools
|
package tools
|
||||||
|
|
||||||
// transfer is a stub for non-Linux platforms.
|
// transfer is a stub for non-Linux platforms.
|
||||||
func (t *SPITool) transfer(args map[string]interface{}) *ToolResult {
|
func (t *SPITool) transfer(args map[string]any) *ToolResult {
|
||||||
return ErrorResult("SPI is only supported on Linux")
|
return ErrorResult("SPI is only supported on Linux")
|
||||||
}
|
}
|
||||||
|
|
||||||
// readDevice is a stub for non-Linux platforms.
|
// readDevice is a stub for non-Linux platforms.
|
||||||
func (t *SPITool) readDevice(args map[string]interface{}) *ToolResult {
|
func (t *SPITool) readDevice(args map[string]any) *ToolResult {
|
||||||
return ErrorResult("SPI is only supported on Linux")
|
return ErrorResult("SPI is only supported on Linux")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,11 @@ type SubagentManager struct {
|
||||||
nextID int
|
nextID int
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSubagentManager(provider providers.LLMProvider, defaultModel, workspace string, bus *bus.MessageBus) *SubagentManager {
|
func NewSubagentManager(
|
||||||
|
provider providers.LLMProvider,
|
||||||
|
defaultModel, workspace string,
|
||||||
|
bus *bus.MessageBus,
|
||||||
|
) *SubagentManager {
|
||||||
return &SubagentManager{
|
return &SubagentManager{
|
||||||
tasks: make(map[string]*SubagentTask),
|
tasks: make(map[string]*SubagentTask),
|
||||||
provider: provider,
|
provider: provider,
|
||||||
|
|
@ -62,7 +66,11 @@ func (sm *SubagentManager) RegisterTool(tool Tool) {
|
||||||
sm.tools.Register(tool)
|
sm.tools.Register(tool)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sm *SubagentManager) Spawn(ctx context.Context, task, label, agentID, originChannel, originChatID string, callback AsyncCallback) (string, error) {
|
func (sm *SubagentManager) Spawn(
|
||||||
|
ctx context.Context,
|
||||||
|
task, label, agentID, originChannel, originChatID string,
|
||||||
|
callback AsyncCallback,
|
||||||
|
) (string, error) {
|
||||||
sm.mu.Lock()
|
sm.mu.Lock()
|
||||||
defer sm.mu.Unlock()
|
defer sm.mu.Unlock()
|
||||||
|
|
||||||
|
|
@ -168,7 +176,12 @@ After completing the task, provide a clear summary of what was done.`
|
||||||
task.Status = "completed"
|
task.Status = "completed"
|
||||||
task.Result = loopResult.Content
|
task.Result = loopResult.Content
|
||||||
result = &ToolResult{
|
result = &ToolResult{
|
||||||
ForLLM: fmt.Sprintf("Subagent '%s' completed (iterations: %d): %s", task.Label, loopResult.Iterations, loopResult.Content),
|
ForLLM: fmt.Sprintf(
|
||||||
|
"Subagent '%s' completed (iterations: %d): %s",
|
||||||
|
task.Label,
|
||||||
|
loopResult.Iterations,
|
||||||
|
loopResult.Content,
|
||||||
|
),
|
||||||
ForUser: loopResult.Content,
|
ForUser: loopResult.Content,
|
||||||
Silent: false,
|
Silent: false,
|
||||||
IsError: false,
|
IsError: false,
|
||||||
|
|
@ -232,15 +245,15 @@ func (t *SubagentTool) Description() string {
|
||||||
return "Execute a subagent task synchronously and return the result. Use this for delegating specific tasks to an independent agent instance. Returns execution summary to user and full details to LLM."
|
return "Execute a subagent task synchronously and return the result. Use this for delegating specific tasks to an independent agent instance. Returns execution summary to user and full details to LLM."
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *SubagentTool) Parameters() map[string]interface{} {
|
func (t *SubagentTool) Parameters() map[string]any {
|
||||||
return map[string]interface{}{
|
return map[string]any{
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": map[string]interface{}{
|
"properties": map[string]any{
|
||||||
"task": map[string]interface{}{
|
"task": map[string]any{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "The task for subagent to complete",
|
"description": "The task for subagent to complete",
|
||||||
},
|
},
|
||||||
"label": map[string]interface{}{
|
"label": map[string]any{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Optional short label for the task (for display)",
|
"description": "Optional short label for the task (for display)",
|
||||||
},
|
},
|
||||||
|
|
@ -254,7 +267,7 @@ func (t *SubagentTool) SetContext(channel, chatID string) {
|
||||||
t.originChatID = chatID
|
t.originChatID = chatID
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *SubagentTool) Execute(ctx context.Context, args map[string]interface{}) *ToolResult {
|
func (t *SubagentTool) Execute(ctx context.Context, args map[string]any) *ToolResult {
|
||||||
task, ok := args["task"].(string)
|
task, ok := args["task"].(string)
|
||||||
if !ok {
|
if !ok {
|
||||||
return ErrorResult("task is required").WithError(fmt.Errorf("task parameter is required"))
|
return ErrorResult("task is required").WithError(fmt.Errorf("task parameter is required"))
|
||||||
|
|
@ -295,7 +308,6 @@ func (t *SubagentTool) Execute(ctx context.Context, args map[string]interface{})
|
||||||
"temperature": 0.7,
|
"temperature": 0.7,
|
||||||
},
|
},
|
||||||
}, messages, t.originChannel, t.originChatID)
|
}, messages, t.originChannel, t.originChatID)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ErrorResult(fmt.Sprintf("Subagent execution failed: %v", err)).WithError(err)
|
return ErrorResult(fmt.Sprintf("Subagent execution failed: %v", err)).WithError(err)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,13 @@ import (
|
||||||
// MockLLMProvider is a test implementation of LLMProvider
|
// MockLLMProvider is a test implementation of LLMProvider
|
||||||
type MockLLMProvider struct{}
|
type MockLLMProvider struct{}
|
||||||
|
|
||||||
func (m *MockLLMProvider) Chat(ctx context.Context, messages []providers.Message, tools []providers.ToolDefinition, model string, options map[string]interface{}) (*providers.LLMResponse, error) {
|
func (m *MockLLMProvider) Chat(
|
||||||
|
ctx context.Context,
|
||||||
|
messages []providers.Message,
|
||||||
|
tools []providers.ToolDefinition,
|
||||||
|
model string,
|
||||||
|
options map[string]any,
|
||||||
|
) (*providers.LLMResponse, error) {
|
||||||
// Find the last user message to generate a response
|
// Find the last user message to generate a response
|
||||||
for i := len(messages) - 1; i >= 0; i-- {
|
for i := len(messages) - 1; i >= 0; i-- {
|
||||||
if messages[i].Role == "user" {
|
if messages[i].Role == "user" {
|
||||||
|
|
@ -79,13 +85,13 @@ func TestSubagentTool_Parameters(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check properties
|
// Check properties
|
||||||
props, ok := params["properties"].(map[string]interface{})
|
props, ok := params["properties"].(map[string]any)
|
||||||
if !ok {
|
if !ok {
|
||||||
t.Fatal("Properties should be a map")
|
t.Fatal("Properties should be a map")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify task parameter
|
// Verify task parameter
|
||||||
task, ok := props["task"].(map[string]interface{})
|
task, ok := props["task"].(map[string]any)
|
||||||
if !ok {
|
if !ok {
|
||||||
t.Fatal("Task parameter should exist")
|
t.Fatal("Task parameter should exist")
|
||||||
}
|
}
|
||||||
|
|
@ -94,7 +100,7 @@ func TestSubagentTool_Parameters(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify label parameter
|
// Verify label parameter
|
||||||
label, ok := props["label"].(map[string]interface{})
|
label, ok := props["label"].(map[string]any)
|
||||||
if !ok {
|
if !ok {
|
||||||
t.Fatal("Label parameter should exist")
|
t.Fatal("Label parameter should exist")
|
||||||
}
|
}
|
||||||
|
|
@ -134,7 +140,7 @@ func TestSubagentTool_Execute_Success(t *testing.T) {
|
||||||
tool.SetContext("telegram", "chat-123")
|
tool.SetContext("telegram", "chat-123")
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
args := map[string]interface{}{
|
args := map[string]any{
|
||||||
"task": "Write a haiku about coding",
|
"task": "Write a haiku about coding",
|
||||||
"label": "haiku-task",
|
"label": "haiku-task",
|
||||||
}
|
}
|
||||||
|
|
@ -189,7 +195,7 @@ func TestSubagentTool_Execute_NoLabel(t *testing.T) {
|
||||||
tool := NewSubagentTool(manager)
|
tool := NewSubagentTool(manager)
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
args := map[string]interface{}{
|
args := map[string]any{
|
||||||
"task": "Test task without label",
|
"task": "Test task without label",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -212,7 +218,7 @@ func TestSubagentTool_Execute_MissingTask(t *testing.T) {
|
||||||
tool := NewSubagentTool(manager)
|
tool := NewSubagentTool(manager)
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
args := map[string]interface{}{
|
args := map[string]any{
|
||||||
"label": "test",
|
"label": "test",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -239,7 +245,7 @@ func TestSubagentTool_Execute_NilManager(t *testing.T) {
|
||||||
tool := NewSubagentTool(nil)
|
tool := NewSubagentTool(nil)
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
args := map[string]interface{}{
|
args := map[string]any{
|
||||||
"task": "test task",
|
"task": "test task",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -268,7 +274,7 @@ func TestSubagentTool_Execute_ContextPassing(t *testing.T) {
|
||||||
tool.SetContext(channel, chatID)
|
tool.SetContext(channel, chatID)
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
args := map[string]interface{}{
|
args := map[string]any{
|
||||||
"task": "Test context passing",
|
"task": "Test context passing",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -295,7 +301,7 @@ func TestSubagentTool_ForUserTruncation(t *testing.T) {
|
||||||
|
|
||||||
// Create a task that will generate long response
|
// Create a task that will generate long response
|
||||||
longTask := strings.Repeat("This is a very long task description. ", 100)
|
longTask := strings.Repeat("This is a very long task description. ", 100)
|
||||||
args := map[string]interface{}{
|
args := map[string]any{
|
||||||
"task": longTask,
|
"task": longTask,
|
||||||
"label": "long-test",
|
"label": "long-test",
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,12 @@ type ToolLoopResult struct {
|
||||||
|
|
||||||
// RunToolLoop executes the LLM + tool call iteration loop.
|
// RunToolLoop executes the LLM + tool call iteration loop.
|
||||||
// This is the core agent logic that can be reused by both main agent and subagents.
|
// This is the core agent logic that can be reused by both main agent and subagents.
|
||||||
func RunToolLoop(ctx context.Context, config ToolLoopConfig, messages []providers.Message, channel, chatID string) (*ToolLoopResult, error) {
|
func RunToolLoop(
|
||||||
|
ctx context.Context,
|
||||||
|
config ToolLoopConfig,
|
||||||
|
messages []providers.Message,
|
||||||
|
channel, chatID string,
|
||||||
|
) (*ToolLoopResult, error) {
|
||||||
iteration := 0
|
iteration := 0
|
||||||
var finalContent string
|
var finalContent string
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,11 +10,11 @@ type Message struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type ToolCall struct {
|
type ToolCall struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Function *FunctionCall `json:"function,omitempty"`
|
Function *FunctionCall `json:"function,omitempty"`
|
||||||
Name string `json:"name,omitempty"`
|
Name string `json:"name,omitempty"`
|
||||||
Arguments map[string]interface{} `json:"arguments,omitempty"`
|
Arguments map[string]any `json:"arguments,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type FunctionCall struct {
|
type FunctionCall struct {
|
||||||
|
|
@ -36,7 +36,13 @@ type UsageInfo struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type LLMProvider interface {
|
type LLMProvider interface {
|
||||||
Chat(ctx context.Context, messages []Message, tools []ToolDefinition, model string, options map[string]interface{}) (*LLMResponse, error)
|
Chat(
|
||||||
|
ctx context.Context,
|
||||||
|
messages []Message,
|
||||||
|
tools []ToolDefinition,
|
||||||
|
model string,
|
||||||
|
options map[string]any,
|
||||||
|
) (*LLMResponse, error)
|
||||||
GetDefaultModel() string
|
GetDefaultModel() string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -46,7 +52,7 @@ type ToolDefinition struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type ToolFunctionDefinition struct {
|
type ToolFunctionDefinition struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
Parameters map[string]interface{} `json:"parameters"`
|
Parameters map[string]any `json:"parameters"`
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -183,11 +183,17 @@ type PerplexitySearchProvider struct {
|
||||||
func (p *PerplexitySearchProvider) Search(ctx context.Context, query string, count int) (string, error) {
|
func (p *PerplexitySearchProvider) Search(ctx context.Context, query string, count int) (string, error) {
|
||||||
searchURL := "https://api.perplexity.ai/chat/completions"
|
searchURL := "https://api.perplexity.ai/chat/completions"
|
||||||
|
|
||||||
payload := map[string]interface{}{
|
payload := map[string]any{
|
||||||
"model": "sonar",
|
"model": "sonar",
|
||||||
"messages": []map[string]string{
|
"messages": []map[string]string{
|
||||||
{"role": "system", "content": "You are a search assistant. Provide concise search results with titles, URLs, and brief descriptions in the following format:\n1. Title\n URL\n Description\n\nDo not add extra commentary."},
|
{
|
||||||
{"role": "user", "content": fmt.Sprintf("Search for: %s. Provide up to %d relevant results.", query, count)},
|
"role": "system",
|
||||||
|
"content": "You are a search assistant. Provide concise search results with titles, URLs, and brief descriptions in the following format:\n1. Title\n URL\n Description\n\nDo not add extra commentary.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "user",
|
||||||
|
"content": fmt.Sprintf("Search for: %s. Provide up to %d relevant results.", query, count),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"max_tokens": 1000,
|
"max_tokens": 1000,
|
||||||
}
|
}
|
||||||
|
|
@ -295,15 +301,15 @@ func (t *WebSearchTool) Description() string {
|
||||||
return "Search the web for current information. Returns titles, URLs, and snippets from search results."
|
return "Search the web for current information. Returns titles, URLs, and snippets from search results."
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *WebSearchTool) Parameters() map[string]interface{} {
|
func (t *WebSearchTool) Parameters() map[string]any {
|
||||||
return map[string]interface{}{
|
return map[string]any{
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": map[string]interface{}{
|
"properties": map[string]any{
|
||||||
"query": map[string]interface{}{
|
"query": map[string]any{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Search query",
|
"description": "Search query",
|
||||||
},
|
},
|
||||||
"count": map[string]interface{}{
|
"count": map[string]any{
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"description": "Number of results (1-10)",
|
"description": "Number of results (1-10)",
|
||||||
"minimum": 1.0,
|
"minimum": 1.0,
|
||||||
|
|
@ -314,7 +320,7 @@ func (t *WebSearchTool) Parameters() map[string]interface{} {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *WebSearchTool) Execute(ctx context.Context, args map[string]interface{}) *ToolResult {
|
func (t *WebSearchTool) Execute(ctx context.Context, args map[string]any) *ToolResult {
|
||||||
query, ok := args["query"].(string)
|
query, ok := args["query"].(string)
|
||||||
if !ok {
|
if !ok {
|
||||||
return ErrorResult("query is required")
|
return ErrorResult("query is required")
|
||||||
|
|
@ -359,15 +365,15 @@ func (t *WebFetchTool) Description() string {
|
||||||
return "Fetch a URL and extract readable content (HTML to text). Use this to get weather info, news, articles, or any web content."
|
return "Fetch a URL and extract readable content (HTML to text). Use this to get weather info, news, articles, or any web content."
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *WebFetchTool) Parameters() map[string]interface{} {
|
func (t *WebFetchTool) Parameters() map[string]any {
|
||||||
return map[string]interface{}{
|
return map[string]any{
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": map[string]interface{}{
|
"properties": map[string]any{
|
||||||
"url": map[string]interface{}{
|
"url": map[string]any{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "URL to fetch",
|
"description": "URL to fetch",
|
||||||
},
|
},
|
||||||
"maxChars": map[string]interface{}{
|
"maxChars": map[string]any{
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"description": "Maximum characters to extract",
|
"description": "Maximum characters to extract",
|
||||||
"minimum": 100.0,
|
"minimum": 100.0,
|
||||||
|
|
@ -377,7 +383,7 @@ func (t *WebFetchTool) Parameters() map[string]interface{} {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *WebFetchTool) Execute(ctx context.Context, args map[string]interface{}) *ToolResult {
|
func (t *WebFetchTool) Execute(ctx context.Context, args map[string]any) *ToolResult {
|
||||||
urlStr, ok := args["url"].(string)
|
urlStr, ok := args["url"].(string)
|
||||||
if !ok {
|
if !ok {
|
||||||
return ErrorResult("url is required")
|
return ErrorResult("url is required")
|
||||||
|
|
@ -442,7 +448,7 @@ func (t *WebFetchTool) Execute(ctx context.Context, args map[string]interface{})
|
||||||
var text, extractor string
|
var text, extractor string
|
||||||
|
|
||||||
if strings.Contains(contentType, "application/json") {
|
if strings.Contains(contentType, "application/json") {
|
||||||
var jsonData interface{}
|
var jsonData any
|
||||||
if err := json.Unmarshal(body, &jsonData); err == nil {
|
if err := json.Unmarshal(body, &jsonData); err == nil {
|
||||||
formatted, _ := json.MarshalIndent(jsonData, "", " ")
|
formatted, _ := json.MarshalIndent(jsonData, "", " ")
|
||||||
text = string(formatted)
|
text = string(formatted)
|
||||||
|
|
@ -465,7 +471,7 @@ func (t *WebFetchTool) Execute(ctx context.Context, args map[string]interface{})
|
||||||
text = text[:maxChars]
|
text = text[:maxChars]
|
||||||
}
|
}
|
||||||
|
|
||||||
result := map[string]interface{}{
|
result := map[string]any{
|
||||||
"url": urlStr,
|
"url": urlStr,
|
||||||
"status": resp.StatusCode,
|
"status": resp.StatusCode,
|
||||||
"extractor": extractor,
|
"extractor": extractor,
|
||||||
|
|
@ -477,7 +483,13 @@ func (t *WebFetchTool) Execute(ctx context.Context, args map[string]interface{})
|
||||||
resultJSON, _ := json.MarshalIndent(result, "", " ")
|
resultJSON, _ := json.MarshalIndent(result, "", " ")
|
||||||
|
|
||||||
return &ToolResult{
|
return &ToolResult{
|
||||||
ForLLM: fmt.Sprintf("Fetched %d bytes from %s (extractor: %s, truncated: %v)", len(text), urlStr, extractor, truncated),
|
ForLLM: fmt.Sprintf(
|
||||||
|
"Fetched %d bytes from %s (extractor: %s, truncated: %v)",
|
||||||
|
len(text),
|
||||||
|
urlStr,
|
||||||
|
extractor,
|
||||||
|
truncated,
|
||||||
|
),
|
||||||
ForUser: string(resultJSON),
|
ForUser: string(resultJSON),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ func TestWebTool_WebFetch_Success(t *testing.T) {
|
||||||
|
|
||||||
tool := NewWebFetchTool(50000)
|
tool := NewWebFetchTool(50000)
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
args := map[string]interface{}{
|
args := map[string]any{
|
||||||
"url": server.URL,
|
"url": server.URL,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -56,7 +56,7 @@ func TestWebTool_WebFetch_JSON(t *testing.T) {
|
||||||
|
|
||||||
tool := NewWebFetchTool(50000)
|
tool := NewWebFetchTool(50000)
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
args := map[string]interface{}{
|
args := map[string]any{
|
||||||
"url": server.URL,
|
"url": server.URL,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -77,7 +77,7 @@ func TestWebTool_WebFetch_JSON(t *testing.T) {
|
||||||
func TestWebTool_WebFetch_InvalidURL(t *testing.T) {
|
func TestWebTool_WebFetch_InvalidURL(t *testing.T) {
|
||||||
tool := NewWebFetchTool(50000)
|
tool := NewWebFetchTool(50000)
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
args := map[string]interface{}{
|
args := map[string]any{
|
||||||
"url": "not-a-valid-url",
|
"url": "not-a-valid-url",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -98,7 +98,7 @@ func TestWebTool_WebFetch_InvalidURL(t *testing.T) {
|
||||||
func TestWebTool_WebFetch_UnsupportedScheme(t *testing.T) {
|
func TestWebTool_WebFetch_UnsupportedScheme(t *testing.T) {
|
||||||
tool := NewWebFetchTool(50000)
|
tool := NewWebFetchTool(50000)
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
args := map[string]interface{}{
|
args := map[string]any{
|
||||||
"url": "ftp://example.com/file.txt",
|
"url": "ftp://example.com/file.txt",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -119,7 +119,7 @@ func TestWebTool_WebFetch_UnsupportedScheme(t *testing.T) {
|
||||||
func TestWebTool_WebFetch_MissingURL(t *testing.T) {
|
func TestWebTool_WebFetch_MissingURL(t *testing.T) {
|
||||||
tool := NewWebFetchTool(50000)
|
tool := NewWebFetchTool(50000)
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
args := map[string]interface{}{}
|
args := map[string]any{}
|
||||||
|
|
||||||
result := tool.Execute(ctx, args)
|
result := tool.Execute(ctx, args)
|
||||||
|
|
||||||
|
|
@ -147,7 +147,7 @@ func TestWebTool_WebFetch_Truncation(t *testing.T) {
|
||||||
|
|
||||||
tool := NewWebFetchTool(1000) // Limit to 1000 chars
|
tool := NewWebFetchTool(1000) // Limit to 1000 chars
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
args := map[string]interface{}{
|
args := map[string]any{
|
||||||
"url": server.URL,
|
"url": server.URL,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -159,7 +159,7 @@ func TestWebTool_WebFetch_Truncation(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ForUser should contain truncated content (not the full 20000 chars)
|
// ForUser should contain truncated content (not the full 20000 chars)
|
||||||
resultMap := make(map[string]interface{})
|
resultMap := make(map[string]any)
|
||||||
json.Unmarshal([]byte(result.ForUser), &resultMap)
|
json.Unmarshal([]byte(result.ForUser), &resultMap)
|
||||||
if text, ok := resultMap["text"].(string); ok {
|
if text, ok := resultMap["text"].(string); ok {
|
||||||
if len(text) > 1100 { // Allow some margin
|
if len(text) > 1100 { // Allow some margin
|
||||||
|
|
@ -191,7 +191,7 @@ func TestWebTool_WebSearch_NoApiKey(t *testing.T) {
|
||||||
func TestWebTool_WebSearch_MissingQuery(t *testing.T) {
|
func TestWebTool_WebSearch_MissingQuery(t *testing.T) {
|
||||||
tool := NewWebSearchTool(WebSearchToolOptions{BraveEnabled: true, BraveAPIKey: "test-key", BraveMaxResults: 5})
|
tool := NewWebSearchTool(WebSearchToolOptions{BraveEnabled: true, BraveAPIKey: "test-key", BraveMaxResults: 5})
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
args := map[string]interface{}{}
|
args := map[string]any{}
|
||||||
|
|
||||||
result := tool.Execute(ctx, args)
|
result := tool.Execute(ctx, args)
|
||||||
|
|
||||||
|
|
@ -206,13 +206,17 @@ func TestWebTool_WebFetch_HTMLExtraction(t *testing.T) {
|
||||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Content-Type", "text/html")
|
w.Header().Set("Content-Type", "text/html")
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
w.Write([]byte(`<html><body><script>alert('test');</script><style>body{color:red;}</style><h1>Title</h1><p>Content</p></body></html>`))
|
w.Write(
|
||||||
|
[]byte(
|
||||||
|
`<html><body><script>alert('test');</script><style>body{color:red;}</style><h1>Title</h1><p>Content</p></body></html>`,
|
||||||
|
),
|
||||||
|
)
|
||||||
}))
|
}))
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
tool := NewWebFetchTool(50000)
|
tool := NewWebFetchTool(50000)
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
args := map[string]interface{}{
|
args := map[string]any{
|
||||||
"url": server.URL,
|
"url": server.URL,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -238,7 +242,7 @@ func TestWebTool_WebFetch_HTMLExtraction(t *testing.T) {
|
||||||
func TestWebTool_WebFetch_MissingDomain(t *testing.T) {
|
func TestWebTool_WebFetch_MissingDomain(t *testing.T) {
|
||||||
tool := NewWebFetchTool(50000)
|
tool := NewWebFetchTool(50000)
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
args := map[string]interface{}{
|
args := map[string]any{
|
||||||
"url": "https://",
|
"url": "https://",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
|
||||||
"github.com/sipeed/picoclaw/pkg/logger"
|
"github.com/sipeed/picoclaw/pkg/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -65,8 +66,8 @@ func DownloadFile(url, filename string, opts DownloadOptions) string {
|
||||||
}
|
}
|
||||||
|
|
||||||
mediaDir := filepath.Join(os.TempDir(), "picoclaw_media")
|
mediaDir := filepath.Join(os.TempDir(), "picoclaw_media")
|
||||||
if err := os.MkdirAll(mediaDir, 0700); err != nil {
|
if err := os.MkdirAll(mediaDir, 0o700); err != nil {
|
||||||
logger.ErrorCF(opts.LoggerPrefix, "Failed to create media directory", map[string]interface{}{
|
logger.ErrorCF(opts.LoggerPrefix, "Failed to create media directory", map[string]any{
|
||||||
"error": err.Error(),
|
"error": err.Error(),
|
||||||
})
|
})
|
||||||
return ""
|
return ""
|
||||||
|
|
@ -79,7 +80,7 @@ func DownloadFile(url, filename string, opts DownloadOptions) string {
|
||||||
// Create HTTP request
|
// Create HTTP request
|
||||||
req, err := http.NewRequest("GET", url, nil)
|
req, err := http.NewRequest("GET", url, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.ErrorCF(opts.LoggerPrefix, "Failed to create download request", map[string]interface{}{
|
logger.ErrorCF(opts.LoggerPrefix, "Failed to create download request", map[string]any{
|
||||||
"error": err.Error(),
|
"error": err.Error(),
|
||||||
})
|
})
|
||||||
return ""
|
return ""
|
||||||
|
|
@ -93,7 +94,7 @@ func DownloadFile(url, filename string, opts DownloadOptions) string {
|
||||||
client := &http.Client{Timeout: opts.Timeout}
|
client := &http.Client{Timeout: opts.Timeout}
|
||||||
resp, err := client.Do(req)
|
resp, err := client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.ErrorCF(opts.LoggerPrefix, "Failed to download file", map[string]interface{}{
|
logger.ErrorCF(opts.LoggerPrefix, "Failed to download file", map[string]any{
|
||||||
"error": err.Error(),
|
"error": err.Error(),
|
||||||
"url": url,
|
"url": url,
|
||||||
})
|
})
|
||||||
|
|
@ -102,7 +103,7 @@ func DownloadFile(url, filename string, opts DownloadOptions) string {
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
logger.ErrorCF(opts.LoggerPrefix, "File download returned non-200 status", map[string]interface{}{
|
logger.ErrorCF(opts.LoggerPrefix, "File download returned non-200 status", map[string]any{
|
||||||
"status": resp.StatusCode,
|
"status": resp.StatusCode,
|
||||||
"url": url,
|
"url": url,
|
||||||
})
|
})
|
||||||
|
|
@ -111,7 +112,7 @@ func DownloadFile(url, filename string, opts DownloadOptions) string {
|
||||||
|
|
||||||
out, err := os.Create(localPath)
|
out, err := os.Create(localPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.ErrorCF(opts.LoggerPrefix, "Failed to create local file", map[string]interface{}{
|
logger.ErrorCF(opts.LoggerPrefix, "Failed to create local file", map[string]any{
|
||||||
"error": err.Error(),
|
"error": err.Error(),
|
||||||
})
|
})
|
||||||
return ""
|
return ""
|
||||||
|
|
@ -121,13 +122,13 @@ func DownloadFile(url, filename string, opts DownloadOptions) string {
|
||||||
if _, err := io.Copy(out, resp.Body); err != nil {
|
if _, err := io.Copy(out, resp.Body); err != nil {
|
||||||
out.Close()
|
out.Close()
|
||||||
os.Remove(localPath)
|
os.Remove(localPath)
|
||||||
logger.ErrorCF(opts.LoggerPrefix, "Failed to write file", map[string]interface{}{
|
logger.ErrorCF(opts.LoggerPrefix, "Failed to write file", map[string]any{
|
||||||
"error": err.Error(),
|
"error": err.Error(),
|
||||||
})
|
})
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.DebugCF(opts.LoggerPrefix, "File downloaded successfully", map[string]interface{}{
|
logger.DebugCF(opts.LoggerPrefix, "File downloaded successfully", map[string]any{
|
||||||
"path": localPath,
|
"path": localPath,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ type TranscriptionResponse struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewGroqTranscriber(apiKey string) *GroqTranscriber {
|
func NewGroqTranscriber(apiKey string) *GroqTranscriber {
|
||||||
logger.DebugCF("voice", "Creating Groq transcriber", map[string]interface{}{"has_api_key": apiKey != ""})
|
logger.DebugCF("voice", "Creating Groq transcriber", map[string]any{"has_api_key": apiKey != ""})
|
||||||
|
|
||||||
apiBase := "https://api.groq.com/openai/v1"
|
apiBase := "https://api.groq.com/openai/v1"
|
||||||
return &GroqTranscriber{
|
return &GroqTranscriber{
|
||||||
|
|
@ -42,22 +42,22 @@ func NewGroqTranscriber(apiKey string) *GroqTranscriber {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *GroqTranscriber) Transcribe(ctx context.Context, audioFilePath string) (*TranscriptionResponse, error) {
|
func (t *GroqTranscriber) Transcribe(ctx context.Context, audioFilePath string) (*TranscriptionResponse, error) {
|
||||||
logger.InfoCF("voice", "Starting transcription", map[string]interface{}{"audio_file": audioFilePath})
|
logger.InfoCF("voice", "Starting transcription", map[string]any{"audio_file": audioFilePath})
|
||||||
|
|
||||||
audioFile, err := os.Open(audioFilePath)
|
audioFile, err := os.Open(audioFilePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.ErrorCF("voice", "Failed to open audio file", map[string]interface{}{"path": audioFilePath, "error": err})
|
logger.ErrorCF("voice", "Failed to open audio file", map[string]any{"path": audioFilePath, "error": err})
|
||||||
return nil, fmt.Errorf("failed to open audio file: %w", err)
|
return nil, fmt.Errorf("failed to open audio file: %w", err)
|
||||||
}
|
}
|
||||||
defer audioFile.Close()
|
defer audioFile.Close()
|
||||||
|
|
||||||
fileInfo, err := audioFile.Stat()
|
fileInfo, err := audioFile.Stat()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.ErrorCF("voice", "Failed to get file info", map[string]interface{}{"path": audioFilePath, "error": err})
|
logger.ErrorCF("voice", "Failed to get file info", map[string]any{"path": audioFilePath, "error": err})
|
||||||
return nil, fmt.Errorf("failed to get file info: %w", err)
|
return nil, fmt.Errorf("failed to get file info: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.DebugCF("voice", "Audio file details", map[string]interface{}{
|
logger.DebugCF("voice", "Audio file details", map[string]any{
|
||||||
"size_bytes": fileInfo.Size(),
|
"size_bytes": fileInfo.Size(),
|
||||||
"file_name": filepath.Base(audioFilePath),
|
"file_name": filepath.Base(audioFilePath),
|
||||||
})
|
})
|
||||||
|
|
@ -67,44 +67,44 @@ func (t *GroqTranscriber) Transcribe(ctx context.Context, audioFilePath string)
|
||||||
|
|
||||||
part, err := writer.CreateFormFile("file", filepath.Base(audioFilePath))
|
part, err := writer.CreateFormFile("file", filepath.Base(audioFilePath))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.ErrorCF("voice", "Failed to create form file", map[string]interface{}{"error": err})
|
logger.ErrorCF("voice", "Failed to create form file", map[string]any{"error": err})
|
||||||
return nil, fmt.Errorf("failed to create form file: %w", err)
|
return nil, fmt.Errorf("failed to create form file: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
copied, err := io.Copy(part, audioFile)
|
copied, err := io.Copy(part, audioFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.ErrorCF("voice", "Failed to copy file content", map[string]interface{}{"error": err})
|
logger.ErrorCF("voice", "Failed to copy file content", map[string]any{"error": err})
|
||||||
return nil, fmt.Errorf("failed to copy file content: %w", err)
|
return nil, fmt.Errorf("failed to copy file content: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.DebugCF("voice", "File copied to request", map[string]interface{}{"bytes_copied": copied})
|
logger.DebugCF("voice", "File copied to request", map[string]any{"bytes_copied": copied})
|
||||||
|
|
||||||
if err := writer.WriteField("model", "whisper-large-v3"); err != nil {
|
if err := writer.WriteField("model", "whisper-large-v3"); err != nil {
|
||||||
logger.ErrorCF("voice", "Failed to write model field", map[string]interface{}{"error": err})
|
logger.ErrorCF("voice", "Failed to write model field", map[string]any{"error": err})
|
||||||
return nil, fmt.Errorf("failed to write model field: %w", err)
|
return nil, fmt.Errorf("failed to write model field: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := writer.WriteField("response_format", "json"); err != nil {
|
if err := writer.WriteField("response_format", "json"); err != nil {
|
||||||
logger.ErrorCF("voice", "Failed to write response_format field", map[string]interface{}{"error": err})
|
logger.ErrorCF("voice", "Failed to write response_format field", map[string]any{"error": err})
|
||||||
return nil, fmt.Errorf("failed to write response_format field: %w", err)
|
return nil, fmt.Errorf("failed to write response_format field: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := writer.Close(); err != nil {
|
if err := writer.Close(); err != nil {
|
||||||
logger.ErrorCF("voice", "Failed to close multipart writer", map[string]interface{}{"error": err})
|
logger.ErrorCF("voice", "Failed to close multipart writer", map[string]any{"error": err})
|
||||||
return nil, fmt.Errorf("failed to close multipart writer: %w", err)
|
return nil, fmt.Errorf("failed to close multipart writer: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
url := t.apiBase + "/audio/transcriptions"
|
url := t.apiBase + "/audio/transcriptions"
|
||||||
req, err := http.NewRequestWithContext(ctx, "POST", url, &requestBody)
|
req, err := http.NewRequestWithContext(ctx, "POST", url, &requestBody)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.ErrorCF("voice", "Failed to create request", map[string]interface{}{"error": err})
|
logger.ErrorCF("voice", "Failed to create request", map[string]any{"error": err})
|
||||||
return nil, fmt.Errorf("failed to create request: %w", err)
|
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||||
req.Header.Set("Authorization", "Bearer "+t.apiKey)
|
req.Header.Set("Authorization", "Bearer "+t.apiKey)
|
||||||
|
|
||||||
logger.DebugCF("voice", "Sending transcription request to Groq API", map[string]interface{}{
|
logger.DebugCF("voice", "Sending transcription request to Groq API", map[string]any{
|
||||||
"url": url,
|
"url": url,
|
||||||
"request_size_bytes": requestBody.Len(),
|
"request_size_bytes": requestBody.Len(),
|
||||||
"file_size_bytes": fileInfo.Size(),
|
"file_size_bytes": fileInfo.Size(),
|
||||||
|
|
@ -112,37 +112,37 @@ func (t *GroqTranscriber) Transcribe(ctx context.Context, audioFilePath string)
|
||||||
|
|
||||||
resp, err := t.httpClient.Do(req)
|
resp, err := t.httpClient.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.ErrorCF("voice", "Failed to send request", map[string]interface{}{"error": err})
|
logger.ErrorCF("voice", "Failed to send request", map[string]any{"error": err})
|
||||||
return nil, fmt.Errorf("failed to send request: %w", err)
|
return nil, fmt.Errorf("failed to send request: %w", err)
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
body, err := io.ReadAll(resp.Body)
|
body, err := io.ReadAll(resp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.ErrorCF("voice", "Failed to read response", map[string]interface{}{"error": err})
|
logger.ErrorCF("voice", "Failed to read response", map[string]any{"error": err})
|
||||||
return nil, fmt.Errorf("failed to read response: %w", err)
|
return nil, fmt.Errorf("failed to read response: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
logger.ErrorCF("voice", "API error", map[string]interface{}{
|
logger.ErrorCF("voice", "API error", map[string]any{
|
||||||
"status_code": resp.StatusCode,
|
"status_code": resp.StatusCode,
|
||||||
"response": string(body),
|
"response": string(body),
|
||||||
})
|
})
|
||||||
return nil, fmt.Errorf("API error (status %d): %s", resp.StatusCode, string(body))
|
return nil, fmt.Errorf("API error (status %d): %s", resp.StatusCode, string(body))
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.DebugCF("voice", "Received response from Groq API", map[string]interface{}{
|
logger.DebugCF("voice", "Received response from Groq API", map[string]any{
|
||||||
"status_code": resp.StatusCode,
|
"status_code": resp.StatusCode,
|
||||||
"response_size_bytes": len(body),
|
"response_size_bytes": len(body),
|
||||||
})
|
})
|
||||||
|
|
||||||
var result TranscriptionResponse
|
var result TranscriptionResponse
|
||||||
if err := json.Unmarshal(body, &result); err != nil {
|
if err := json.Unmarshal(body, &result); err != nil {
|
||||||
logger.ErrorCF("voice", "Failed to unmarshal response", map[string]interface{}{"error": err})
|
logger.ErrorCF("voice", "Failed to unmarshal response", map[string]any{"error": err})
|
||||||
return nil, fmt.Errorf("failed to unmarshal response: %w", err)
|
return nil, fmt.Errorf("failed to unmarshal response: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.InfoCF("voice", "Transcription completed successfully", map[string]interface{}{
|
logger.InfoCF("voice", "Transcription completed successfully", map[string]any{
|
||||||
"text_length": len(result.Text),
|
"text_length": len(result.Text),
|
||||||
"language": result.Language,
|
"language": result.Language,
|
||||||
"duration_seconds": result.Duration,
|
"duration_seconds": result.Duration,
|
||||||
|
|
@ -154,6 +154,6 @@ func (t *GroqTranscriber) Transcribe(ctx context.Context, audioFilePath string)
|
||||||
|
|
||||||
func (t *GroqTranscriber) IsAvailable() bool {
|
func (t *GroqTranscriber) IsAvailable() bool {
|
||||||
available := t.apiKey != ""
|
available := t.apiKey != ""
|
||||||
logger.DebugCF("voice", "Checking transcriber availability", map[string]interface{}{"available": available})
|
logger.DebugCF("voice", "Checking transcriber availability", map[string]any{"available": available})
|
||||||
return available
|
return available
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue