mirror of
https://github.com/sipeed/picoclaw.git
synced 2026-06-12 18:08:54 +00:00
Merge pull request #2566 from lc6464/refactor/providers-tools-layout
refactor(providers,tools): reorganize packages and facades
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
package providers
|
||||
package cliprovider
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
//go:build integration
|
||||
|
||||
package providers
|
||||
package cliprovider
|
||||
|
||||
import (
|
||||
"context"
|
||||
+1
-80
@@ -1,4 +1,4 @@
|
||||
package providers
|
||||
package cliprovider
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -9,8 +9,6 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/sipeed/picoclaw/pkg/config"
|
||||
)
|
||||
|
||||
// --- Compile-time interface check ---
|
||||
@@ -409,83 +407,6 @@ func TestChat_EmptyWorkspaceDoesNotSetDir(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// --- CreateProvider factory tests ---
|
||||
|
||||
func TestCreateProvider_ClaudeCli(t *testing.T) {
|
||||
cfg := config.DefaultConfig()
|
||||
cfg.ModelList = []*config.ModelConfig{
|
||||
{ModelName: "claude-sonnet-4.6", Model: "claude-cli/claude-sonnet-4.6", Workspace: "/test/ws"},
|
||||
}
|
||||
cfg.Agents.Defaults.ModelName = "claude-sonnet-4.6"
|
||||
|
||||
provider, _, err := CreateProvider(cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("CreateProvider(claude-cli) error = %v", err)
|
||||
}
|
||||
|
||||
cliProvider, ok := provider.(*ClaudeCliProvider)
|
||||
if !ok {
|
||||
t.Fatalf("CreateProvider(claude-cli) returned %T, want *ClaudeCliProvider", provider)
|
||||
}
|
||||
if cliProvider.workspace != "/test/ws" {
|
||||
t.Errorf("workspace = %q, want %q", cliProvider.workspace, "/test/ws")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateProvider_ClaudeCode(t *testing.T) {
|
||||
cfg := config.DefaultConfig()
|
||||
cfg.ModelList = []*config.ModelConfig{
|
||||
{ModelName: "claude-code", Model: "claude-cli/claude-code"},
|
||||
}
|
||||
cfg.Agents.Defaults.ModelName = "claude-code"
|
||||
|
||||
provider, _, err := CreateProvider(cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("CreateProvider(claude-code) error = %v", err)
|
||||
}
|
||||
if _, ok := provider.(*ClaudeCliProvider); !ok {
|
||||
t.Fatalf("CreateProvider(claude-code) returned %T, want *ClaudeCliProvider", provider)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateProvider_ClaudeCodec(t *testing.T) {
|
||||
cfg := config.DefaultConfig()
|
||||
cfg.ModelList = []*config.ModelConfig{
|
||||
{ModelName: "claudecode", Model: "claude-cli/claudecode"},
|
||||
}
|
||||
cfg.Agents.Defaults.ModelName = "claudecode"
|
||||
|
||||
provider, _, err := CreateProvider(cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("CreateProvider(claudecode) error = %v", err)
|
||||
}
|
||||
if _, ok := provider.(*ClaudeCliProvider); !ok {
|
||||
t.Fatalf("CreateProvider(claudecode) returned %T, want *ClaudeCliProvider", provider)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateProvider_ClaudeCliDefaultWorkspace(t *testing.T) {
|
||||
cfg := config.DefaultConfig()
|
||||
cfg.ModelList = []*config.ModelConfig{
|
||||
{ModelName: "claude-cli", Model: "claude-cli/claude-sonnet"},
|
||||
}
|
||||
cfg.Agents.Defaults.ModelName = "claude-cli"
|
||||
cfg.Agents.Defaults.Workspace = ""
|
||||
|
||||
provider, _, err := CreateProvider(cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("CreateProvider error = %v", err)
|
||||
}
|
||||
|
||||
cliProvider, ok := provider.(*ClaudeCliProvider)
|
||||
if !ok {
|
||||
t.Fatalf("returned %T, want *ClaudeCliProvider", provider)
|
||||
}
|
||||
if cliProvider.workspace != "." {
|
||||
t.Errorf("workspace = %q, want %q (default)", cliProvider.workspace, ".")
|
||||
}
|
||||
}
|
||||
|
||||
// --- messagesToPrompt tests ---
|
||||
|
||||
func TestMessagesToPrompt_SingleUser(t *testing.T) {
|
||||
@@ -1,4 +1,4 @@
|
||||
package providers
|
||||
package cliprovider
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package providers
|
||||
package cliprovider
|
||||
|
||||
import (
|
||||
"os"
|
||||
@@ -1,4 +1,4 @@
|
||||
package providers
|
||||
package cliprovider
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
//go:build integration
|
||||
|
||||
package providers
|
||||
package cliprovider
|
||||
|
||||
import (
|
||||
"context"
|
||||
+11
-1
@@ -1,4 +1,4 @@
|
||||
package providers
|
||||
package cliprovider
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
@@ -400,6 +401,9 @@ func TestCodexCliProvider_GetDefaultModel(t *testing.T) {
|
||||
|
||||
func createMockCodexCLI(t *testing.T, events []string) string {
|
||||
t.Helper()
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("mock CLI scripts not supported on Windows")
|
||||
}
|
||||
tmpDir := t.TempDir()
|
||||
scriptPath := filepath.Join(tmpDir, "codex")
|
||||
|
||||
@@ -471,6 +475,9 @@ func TestCodexCliProvider_MockCLI_Error(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCodexCliProvider_MockCLI_WithModel(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("mock CLI scripts not supported on Windows")
|
||||
}
|
||||
// Mock script that captures args to verify model flag is passed
|
||||
tmpDir := t.TempDir()
|
||||
scriptPath := filepath.Join(tmpDir, "codex")
|
||||
@@ -517,6 +524,9 @@ echo '{"type":"turn.completed"}'`
|
||||
}
|
||||
|
||||
func TestCodexCliProvider_MockCLI_ContextCancel(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("mock CLI scripts not supported on Windows")
|
||||
}
|
||||
// Script that sleeps forever
|
||||
tmpDir := t.TempDir()
|
||||
scriptPath := filepath.Join(tmpDir, "codex")
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package providers
|
||||
package cliprovider
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -1,4 +1,4 @@
|
||||
package providers
|
||||
package cliprovider
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -3,7 +3,7 @@
|
||||
//
|
||||
// Copyright (c) 2026 PicoClaw contributors
|
||||
|
||||
package providers
|
||||
package cliprovider
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -0,0 +1,28 @@
|
||||
package cliprovider
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/sipeed/picoclaw/pkg/providers/protocoltypes"
|
||||
)
|
||||
|
||||
type (
|
||||
ToolCall = protocoltypes.ToolCall
|
||||
FunctionCall = protocoltypes.FunctionCall
|
||||
LLMResponse = protocoltypes.LLMResponse
|
||||
UsageInfo = protocoltypes.UsageInfo
|
||||
Message = protocoltypes.Message
|
||||
ToolDefinition = protocoltypes.ToolDefinition
|
||||
ToolFunctionDefinition = protocoltypes.ToolFunctionDefinition
|
||||
)
|
||||
|
||||
type LLMProvider interface {
|
||||
Chat(
|
||||
ctx context.Context,
|
||||
messages []Message,
|
||||
tools []ToolDefinition,
|
||||
model string,
|
||||
options map[string]any,
|
||||
) (*LLMResponse, error)
|
||||
GetDefaultModel() string
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package providers
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
cliprovider "github.com/sipeed/picoclaw/pkg/providers/cli"
|
||||
)
|
||||
|
||||
type (
|
||||
ClaudeCliProvider = cliprovider.ClaudeCliProvider
|
||||
CodexCliProvider = cliprovider.CodexCliProvider
|
||||
CodexCliAuth = cliprovider.CodexCliAuth
|
||||
GitHubCopilotProvider = cliprovider.GitHubCopilotProvider
|
||||
)
|
||||
|
||||
const CodexHomeEnvVar = cliprovider.CodexHomeEnvVar
|
||||
|
||||
func NewClaudeCliProvider(workspace string) *ClaudeCliProvider {
|
||||
return cliprovider.NewClaudeCliProvider(workspace)
|
||||
}
|
||||
|
||||
func NewCodexCliProvider(workspace string) *CodexCliProvider {
|
||||
return cliprovider.NewCodexCliProvider(workspace)
|
||||
}
|
||||
|
||||
func NewGitHubCopilotProvider(uri string, connectMode string, model string) (*GitHubCopilotProvider, error) {
|
||||
return cliprovider.NewGitHubCopilotProvider(uri, connectMode, model)
|
||||
}
|
||||
|
||||
func ReadCodexCliCredentials() (accessToken, accountID string, expiresAt time.Time, err error) {
|
||||
return cliprovider.ReadCodexCliCredentials()
|
||||
}
|
||||
|
||||
func CreateCodexCliTokenSource() func() (string, string, error) {
|
||||
return cliprovider.CreateCodexCliTokenSource()
|
||||
}
|
||||
|
||||
func NormalizeToolCall(tc ToolCall) ToolCall {
|
||||
return cliprovider.NormalizeToolCall(tc)
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package providers
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/sipeed/picoclaw/pkg/config"
|
||||
)
|
||||
|
||||
func testProviderWorkspace(t *testing.T, provider any) string {
|
||||
t.Helper()
|
||||
|
||||
v := reflect.ValueOf(provider)
|
||||
if v.Kind() != reflect.Ptr || v.IsNil() {
|
||||
t.Fatalf("provider = %T, want non-nil pointer", provider)
|
||||
}
|
||||
|
||||
field := v.Elem().FieldByName("workspace")
|
||||
if !field.IsValid() || field.Kind() != reflect.String {
|
||||
t.Fatalf("provider %T does not expose workspace field", provider)
|
||||
}
|
||||
|
||||
return field.String()
|
||||
}
|
||||
|
||||
func TestCreateProvider_ClaudeCli(t *testing.T) {
|
||||
cfg := config.DefaultConfig()
|
||||
cfg.ModelList = []*config.ModelConfig{
|
||||
{ModelName: "claude-sonnet-4.6", Model: "claude-cli/claude-sonnet-4.6", Workspace: "/test/ws"},
|
||||
}
|
||||
cfg.Agents.Defaults.ModelName = "claude-sonnet-4.6"
|
||||
|
||||
provider, _, err := CreateProvider(cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("CreateProvider(claude-cli) error = %v", err)
|
||||
}
|
||||
|
||||
cliProvider, ok := provider.(*ClaudeCliProvider)
|
||||
if !ok {
|
||||
t.Fatalf("CreateProvider(claude-cli) returned %T, want *ClaudeCliProvider", provider)
|
||||
}
|
||||
if got := testProviderWorkspace(t, cliProvider); got != "/test/ws" {
|
||||
t.Errorf("workspace = %q, want %q", got, "/test/ws")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateProvider_ClaudeCode(t *testing.T) {
|
||||
cfg := config.DefaultConfig()
|
||||
cfg.ModelList = []*config.ModelConfig{
|
||||
{ModelName: "claude-code", Model: "claude-cli/claude-code"},
|
||||
}
|
||||
cfg.Agents.Defaults.ModelName = "claude-code"
|
||||
|
||||
provider, _, err := CreateProvider(cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("CreateProvider(claude-code) error = %v", err)
|
||||
}
|
||||
if _, ok := provider.(*ClaudeCliProvider); !ok {
|
||||
t.Fatalf("CreateProvider(claude-code) returned %T, want *ClaudeCliProvider", provider)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateProvider_ClaudeCodec(t *testing.T) {
|
||||
cfg := config.DefaultConfig()
|
||||
cfg.ModelList = []*config.ModelConfig{
|
||||
{ModelName: "claudecode", Model: "claude-cli/claudecode"},
|
||||
}
|
||||
cfg.Agents.Defaults.ModelName = "claudecode"
|
||||
|
||||
provider, _, err := CreateProvider(cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("CreateProvider(claudecode) error = %v", err)
|
||||
}
|
||||
if _, ok := provider.(*ClaudeCliProvider); !ok {
|
||||
t.Fatalf("CreateProvider(claudecode) returned %T, want *ClaudeCliProvider", provider)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateProvider_ClaudeCliDefaultWorkspace(t *testing.T) {
|
||||
cfg := config.DefaultConfig()
|
||||
cfg.ModelList = []*config.ModelConfig{
|
||||
{ModelName: "claude-cli", Model: "claude-cli/claude-sonnet"},
|
||||
}
|
||||
cfg.Agents.Defaults.ModelName = "claude-cli"
|
||||
cfg.Agents.Defaults.Workspace = ""
|
||||
|
||||
provider, _, err := CreateProvider(cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("CreateProvider error = %v", err)
|
||||
}
|
||||
|
||||
cliProvider, ok := provider.(*ClaudeCliProvider)
|
||||
if !ok {
|
||||
t.Fatalf("returned %T, want *ClaudeCliProvider", provider)
|
||||
}
|
||||
if got := testProviderWorkspace(t, cliProvider); got != "." {
|
||||
t.Errorf("workspace = %q, want %q (default)", got, ".")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package providers
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
cliprovider "github.com/sipeed/picoclaw/pkg/providers/cli"
|
||||
oauthprovider "github.com/sipeed/picoclaw/pkg/providers/oauth"
|
||||
)
|
||||
|
||||
func TestNormalizeToolCallFacadeMatchesCLIProvider(t *testing.T) {
|
||||
input := ToolCall{
|
||||
ID: "call_1",
|
||||
Type: "function",
|
||||
Function: &FunctionCall{
|
||||
Name: "read_file",
|
||||
Arguments: `{"path":"README.md"}`,
|
||||
},
|
||||
}
|
||||
|
||||
got := NormalizeToolCall(input)
|
||||
want := cliprovider.NormalizeToolCall(input)
|
||||
|
||||
if got.Name != want.Name {
|
||||
t.Fatalf("Name = %q, want %q", got.Name, want.Name)
|
||||
}
|
||||
if got.Function == nil || want.Function == nil {
|
||||
t.Fatalf("Function should not be nil: got=%v want=%v", got.Function, want.Function)
|
||||
}
|
||||
if got.Function.Name != want.Function.Name {
|
||||
t.Fatalf("Function.Name = %q, want %q", got.Function.Name, want.Function.Name)
|
||||
}
|
||||
if got.Function.Arguments != want.Function.Arguments {
|
||||
t.Fatalf("Function.Arguments = %q, want %q", got.Function.Arguments, want.Function.Arguments)
|
||||
}
|
||||
if got.Arguments["path"] != want.Arguments["path"] {
|
||||
t.Fatalf("Arguments[path] = %v, want %v", got.Arguments["path"], want.Arguments["path"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestAntigravityFacadeSignaturesRemainAvailable(t *testing.T) {
|
||||
var _ func(string) (string, error) = FetchAntigravityProjectID
|
||||
var _ func(string, string) ([]AntigravityModelInfo, error) = FetchAntigravityModels
|
||||
var _ AntigravityModelInfo = oauthprovider.AntigravityModelInfo{}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
package httpapi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func normalizeStoredToolCall(tc ToolCall) (string, map[string]any, string) {
|
||||
name := tc.Name
|
||||
args := tc.Arguments
|
||||
thoughtSignature := ""
|
||||
|
||||
if name == "" && tc.Function != nil {
|
||||
name = tc.Function.Name
|
||||
thoughtSignature = tc.Function.ThoughtSignature
|
||||
} else if tc.Function != nil {
|
||||
thoughtSignature = tc.Function.ThoughtSignature
|
||||
}
|
||||
|
||||
if args == nil {
|
||||
args = map[string]any{}
|
||||
}
|
||||
|
||||
if len(args) == 0 && tc.Function != nil && tc.Function.Arguments != "" {
|
||||
var parsed map[string]any
|
||||
if err := json.Unmarshal([]byte(tc.Function.Arguments), &parsed); err == nil && parsed != nil {
|
||||
args = parsed
|
||||
}
|
||||
}
|
||||
|
||||
return name, args, thoughtSignature
|
||||
}
|
||||
|
||||
func resolveToolResponseName(toolCallID string, toolCallNames map[string]string) string {
|
||||
if toolCallID == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
if name, ok := toolCallNames[toolCallID]; ok && name != "" {
|
||||
return name
|
||||
}
|
||||
|
||||
return inferToolNameFromCallID(toolCallID)
|
||||
}
|
||||
|
||||
func inferToolNameFromCallID(toolCallID string) string {
|
||||
if !strings.HasPrefix(toolCallID, "call_") {
|
||||
return toolCallID
|
||||
}
|
||||
|
||||
rest := strings.TrimPrefix(toolCallID, "call_")
|
||||
if idx := strings.LastIndex(rest, "_"); idx > 0 {
|
||||
candidate := rest[:idx]
|
||||
if candidate != "" {
|
||||
return candidate
|
||||
}
|
||||
}
|
||||
|
||||
return toolCallID
|
||||
}
|
||||
|
||||
func extractPartThoughtSignature(thoughtSignature string, thoughtSignatureSnake string) string {
|
||||
if thoughtSignature != "" {
|
||||
return thoughtSignature
|
||||
}
|
||||
if thoughtSignatureSnake != "" {
|
||||
return thoughtSignatureSnake
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
var geminiUnsupportedKeywords = map[string]bool{
|
||||
"patternProperties": true,
|
||||
"additionalProperties": true,
|
||||
"$schema": true,
|
||||
"$id": true,
|
||||
"$ref": true,
|
||||
"$defs": true,
|
||||
"definitions": true,
|
||||
"examples": true,
|
||||
"minLength": true,
|
||||
"maxLength": true,
|
||||
"minimum": true,
|
||||
"maximum": true,
|
||||
"multipleOf": true,
|
||||
"pattern": true,
|
||||
"format": true,
|
||||
"minItems": true,
|
||||
"maxItems": true,
|
||||
"uniqueItems": true,
|
||||
"minProperties": true,
|
||||
"maxProperties": true,
|
||||
}
|
||||
|
||||
func sanitizeSchemaForGemini(schema map[string]any) map[string]any {
|
||||
if schema == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
result := make(map[string]any)
|
||||
for k, v := range schema {
|
||||
if geminiUnsupportedKeywords[k] {
|
||||
continue
|
||||
}
|
||||
switch val := v.(type) {
|
||||
case map[string]any:
|
||||
result[k] = sanitizeSchemaForGemini(val)
|
||||
case []any:
|
||||
sanitized := make([]any, len(val))
|
||||
for i, item := range val {
|
||||
if m, ok := item.(map[string]any); ok {
|
||||
sanitized[i] = sanitizeSchemaForGemini(m)
|
||||
} else {
|
||||
sanitized[i] = item
|
||||
}
|
||||
}
|
||||
result[k] = sanitized
|
||||
default:
|
||||
result[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
if _, hasProps := result["properties"]; hasProps {
|
||||
if _, hasType := result["type"]; !hasType {
|
||||
result["type"] = "object"
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func extractProtocol(model string) (protocol, modelID string) {
|
||||
model = strings.TrimSpace(model)
|
||||
protocol, modelID, found := strings.Cut(model, "/")
|
||||
if !found {
|
||||
return "openai", model
|
||||
}
|
||||
return protocol, modelID
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package providers
|
||||
package httpapi
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
@@ -303,7 +303,7 @@ func normalizeGeminiModel(model string) string {
|
||||
model = strings.TrimSpace(model)
|
||||
model = strings.TrimPrefix(model, "models/")
|
||||
if strings.Contains(model, "/") {
|
||||
_, modelID := ExtractProtocol(model)
|
||||
_, modelID := extractProtocol(model)
|
||||
if modelID != "" {
|
||||
return modelID
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package providers
|
||||
package httpapi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -4,7 +4,7 @@
|
||||
//
|
||||
// Copyright (c) 2026 PicoClaw contributors
|
||||
|
||||
package providers
|
||||
package httpapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -0,0 +1,43 @@
|
||||
package httpapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/sipeed/picoclaw/pkg/providers/protocoltypes"
|
||||
)
|
||||
|
||||
type (
|
||||
ToolCall = protocoltypes.ToolCall
|
||||
FunctionCall = protocoltypes.FunctionCall
|
||||
LLMResponse = protocoltypes.LLMResponse
|
||||
UsageInfo = protocoltypes.UsageInfo
|
||||
Message = protocoltypes.Message
|
||||
ToolDefinition = protocoltypes.ToolDefinition
|
||||
ToolFunctionDefinition = protocoltypes.ToolFunctionDefinition
|
||||
ExtraContent = protocoltypes.ExtraContent
|
||||
GoogleExtra = protocoltypes.GoogleExtra
|
||||
ContentBlock = protocoltypes.ContentBlock
|
||||
CacheControl = protocoltypes.CacheControl
|
||||
)
|
||||
|
||||
type LLMProvider interface {
|
||||
Chat(
|
||||
ctx context.Context,
|
||||
messages []Message,
|
||||
tools []ToolDefinition,
|
||||
model string,
|
||||
options map[string]any,
|
||||
) (*LLMResponse, error)
|
||||
GetDefaultModel() string
|
||||
}
|
||||
|
||||
type StreamingProvider interface {
|
||||
ChatStream(
|
||||
ctx context.Context,
|
||||
messages []Message,
|
||||
tools []ToolDefinition,
|
||||
model string,
|
||||
options map[string]any,
|
||||
onChunk func(accumulated string),
|
||||
) (*LLMResponse, error)
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package providers
|
||||
|
||||
import httpapi "github.com/sipeed/picoclaw/pkg/providers/httpapi"
|
||||
|
||||
type (
|
||||
GeminiProvider = httpapi.GeminiProvider
|
||||
HTTPProvider = httpapi.HTTPProvider
|
||||
)
|
||||
|
||||
func NewGeminiProvider(
|
||||
apiKey string,
|
||||
apiBase string,
|
||||
proxy string,
|
||||
userAgent string,
|
||||
requestTimeoutSeconds int,
|
||||
extraBody map[string]any,
|
||||
customHeaders map[string]string,
|
||||
) *GeminiProvider {
|
||||
return httpapi.NewGeminiProvider(apiKey, apiBase, proxy, userAgent, requestTimeoutSeconds, extraBody, customHeaders)
|
||||
}
|
||||
|
||||
func NewHTTPProvider(apiKey, apiBase, proxy string) *HTTPProvider {
|
||||
return httpapi.NewHTTPProvider(apiKey, apiBase, proxy)
|
||||
}
|
||||
|
||||
func NewHTTPProviderWithMaxTokensField(apiKey, apiBase, proxy, maxTokensField string) *HTTPProvider {
|
||||
return httpapi.NewHTTPProviderWithMaxTokensField(apiKey, apiBase, proxy, maxTokensField)
|
||||
}
|
||||
|
||||
func NewHTTPProviderWithMaxTokensFieldAndRequestTimeout(
|
||||
apiKey, apiBase, proxy, maxTokensField, userAgent string,
|
||||
requestTimeoutSeconds int,
|
||||
extraBody map[string]any,
|
||||
customHeaders map[string]string,
|
||||
) *HTTPProvider {
|
||||
return httpapi.NewHTTPProviderWithMaxTokensFieldAndRequestTimeout(
|
||||
apiKey,
|
||||
apiBase,
|
||||
proxy,
|
||||
maxTokensField,
|
||||
userAgent,
|
||||
requestTimeoutSeconds,
|
||||
extraBody,
|
||||
customHeaders,
|
||||
)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package providers
|
||||
package oauthprovider
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package providers
|
||||
package oauthprovider
|
||||
|
||||
import "testing"
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
package providers
|
||||
package oauthprovider
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/sipeed/picoclaw/pkg/auth"
|
||||
anthropicprovider "github.com/sipeed/picoclaw/pkg/providers/anthropic"
|
||||
)
|
||||
|
||||
@@ -55,7 +56,7 @@ func (p *ClaudeProvider) GetDefaultModel() string {
|
||||
return p.delegate.GetDefaultModel()
|
||||
}
|
||||
|
||||
func createClaudeTokenSource() func() (string, error) {
|
||||
func CreateClaudeTokenSource(getCredential func(string) (*auth.AuthCredential, error)) func() (string, error) {
|
||||
return func() (string, error) {
|
||||
cred, err := getCredential("anthropic")
|
||||
if err != nil {
|
||||
@@ -1,4 +1,4 @@
|
||||
package providers
|
||||
package oauthprovider
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -1,4 +1,4 @@
|
||||
package providers
|
||||
package oauthprovider
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -240,7 +240,7 @@ func buildCodexParams(
|
||||
return params
|
||||
}
|
||||
|
||||
func createCodexTokenSource() func() (string, string, error) {
|
||||
func CreateCodexTokenSource() func() (string, string, error) {
|
||||
return func() (string, string, error) {
|
||||
cred, err := auth.GetCredential("openai")
|
||||
if err != nil {
|
||||
@@ -1,4 +1,4 @@
|
||||
package providers
|
||||
package oauthprovider
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -0,0 +1,32 @@
|
||||
package oauthprovider
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/sipeed/picoclaw/pkg/providers/protocoltypes"
|
||||
)
|
||||
|
||||
type (
|
||||
ToolCall = protocoltypes.ToolCall
|
||||
FunctionCall = protocoltypes.FunctionCall
|
||||
LLMResponse = protocoltypes.LLMResponse
|
||||
UsageInfo = protocoltypes.UsageInfo
|
||||
Message = protocoltypes.Message
|
||||
ToolDefinition = protocoltypes.ToolDefinition
|
||||
ToolFunctionDefinition = protocoltypes.ToolFunctionDefinition
|
||||
ExtraContent = protocoltypes.ExtraContent
|
||||
GoogleExtra = protocoltypes.GoogleExtra
|
||||
ContentBlock = protocoltypes.ContentBlock
|
||||
CacheControl = protocoltypes.CacheControl
|
||||
)
|
||||
|
||||
type LLMProvider interface {
|
||||
Chat(
|
||||
ctx context.Context,
|
||||
messages []Message,
|
||||
tools []ToolDefinition,
|
||||
model string,
|
||||
options map[string]any,
|
||||
) (*LLMResponse, error)
|
||||
GetDefaultModel() string
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package providers
|
||||
|
||||
import (
|
||||
oauthprovider "github.com/sipeed/picoclaw/pkg/providers/oauth"
|
||||
)
|
||||
|
||||
type (
|
||||
AntigravityProvider = oauthprovider.AntigravityProvider
|
||||
AntigravityModelInfo = oauthprovider.AntigravityModelInfo
|
||||
ClaudeProvider = oauthprovider.ClaudeProvider
|
||||
CodexProvider = oauthprovider.CodexProvider
|
||||
)
|
||||
|
||||
func NewAntigravityProvider() *AntigravityProvider {
|
||||
return oauthprovider.NewAntigravityProvider()
|
||||
}
|
||||
|
||||
func NewClaudeProvider(token string) *ClaudeProvider {
|
||||
return oauthprovider.NewClaudeProvider(token)
|
||||
}
|
||||
|
||||
func NewClaudeProviderWithBaseURL(token, apiBase string) *ClaudeProvider {
|
||||
return oauthprovider.NewClaudeProviderWithBaseURL(token, apiBase)
|
||||
}
|
||||
|
||||
func NewClaudeProviderWithTokenSource(token string, tokenSource func() (string, error)) *ClaudeProvider {
|
||||
return oauthprovider.NewClaudeProviderWithTokenSource(token, tokenSource)
|
||||
}
|
||||
|
||||
func NewClaudeProviderWithTokenSourceAndBaseURL(
|
||||
token string, tokenSource func() (string, error), apiBase string,
|
||||
) *ClaudeProvider {
|
||||
return oauthprovider.NewClaudeProviderWithTokenSourceAndBaseURL(token, tokenSource, apiBase)
|
||||
}
|
||||
|
||||
func NewCodexProvider(token, accountID string) *CodexProvider {
|
||||
return oauthprovider.NewCodexProvider(token, accountID)
|
||||
}
|
||||
|
||||
func NewCodexProviderWithTokenSource(
|
||||
token, accountID string, tokenSource func() (string, string, error),
|
||||
) *CodexProvider {
|
||||
return oauthprovider.NewCodexProviderWithTokenSource(token, accountID, tokenSource)
|
||||
}
|
||||
|
||||
func FetchAntigravityProjectID(accessToken string) (string, error) {
|
||||
return oauthprovider.FetchAntigravityProjectID(accessToken)
|
||||
}
|
||||
|
||||
func FetchAntigravityModels(accessToken, projectID string) ([]AntigravityModelInfo, error) {
|
||||
return oauthprovider.FetchAntigravityModels(accessToken, projectID)
|
||||
}
|
||||
|
||||
func createClaudeTokenSource() func() (string, error) {
|
||||
return oauthprovider.CreateClaudeTokenSource(getCredential)
|
||||
}
|
||||
|
||||
func createCodexTokenSource() func() (string, string, error) {
|
||||
return oauthprovider.CreateCodexTokenSource()
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package tools
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestFacadeConstructorsRemainAvailable(t *testing.T) {
|
||||
if NewI2CTool() == nil {
|
||||
t.Fatal("NewI2CTool should return a tool")
|
||||
}
|
||||
if NewSPITool() == nil {
|
||||
t.Fatal("NewSPITool should return a tool")
|
||||
}
|
||||
if NewMessageTool() == nil {
|
||||
t.Fatal("NewMessageTool should return a tool")
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package tools
|
||||
package fstools
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -1,4 +1,4 @@
|
||||
package tools
|
||||
package fstools
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -1,4 +1,4 @@
|
||||
package tools
|
||||
package fstools
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
@@ -24,6 +24,18 @@ import (
|
||||
|
||||
const MaxReadFileSize = 64 * 1024 // 64KB limit to avoid context overflow
|
||||
|
||||
func ValidatePathWithAllowPaths(
|
||||
path, workspace string,
|
||||
restrict bool,
|
||||
patterns []*regexp.Regexp,
|
||||
) (string, error) {
|
||||
return validatePathWithAllowPaths(path, workspace, restrict, patterns)
|
||||
}
|
||||
|
||||
func IsAllowedPath(path string, patterns []*regexp.Regexp) bool {
|
||||
return isAllowedPath(path, patterns)
|
||||
}
|
||||
|
||||
func validatePathWithAllowPaths(
|
||||
path, workspace string,
|
||||
restrict bool,
|
||||
@@ -1,4 +1,4 @@
|
||||
package tools
|
||||
package fstools
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -1050,43 +1050,6 @@ func TestReadFileLinesTool_OffsetBeyondEOF(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadFileLinesTool_RegistryValidationSupportsMaxLinesAndRejectsLimit(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
testFile := filepath.Join(tmpDir, "registry_lines.txt")
|
||||
|
||||
err := os.WriteFile(testFile, []byte("line 1\nline 2\nline 3\n"), 0o644)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to write test file: %v", err)
|
||||
}
|
||||
|
||||
reg := NewToolRegistry()
|
||||
reg.Register(NewReadFileLinesTool(tmpDir, false, MaxReadFileSize))
|
||||
|
||||
result := reg.Execute(context.Background(), "read_file", map[string]any{
|
||||
"path": testFile,
|
||||
"start_line": 1,
|
||||
"max_lines": 1,
|
||||
})
|
||||
if result.IsError {
|
||||
t.Fatalf("expected max_lines to pass registry validation, got: %s", result.ForLLM)
|
||||
}
|
||||
if !strings.Contains(result.ForLLM, "1|line 1\n") {
|
||||
t.Fatalf("expected first line via max_lines, got: %s", result.ForLLM)
|
||||
}
|
||||
|
||||
result = reg.Execute(context.Background(), "read_file", map[string]any{
|
||||
"path": testFile,
|
||||
"start_line": 2,
|
||||
"limit": 1,
|
||||
})
|
||||
if !result.IsError {
|
||||
t.Fatalf("expected limit to be rejected, got success: %s", result.ForLLM)
|
||||
}
|
||||
if !strings.Contains(result.ForLLM, "unexpected property \"limit\"") {
|
||||
t.Fatalf("expected registry validation error for limit, got: %s", result.ForLLM)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadFileLinesTool_RejectsOffset(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
testFile := filepath.Join(tmpDir, "legacy_offset.txt")
|
||||
@@ -1,4 +1,4 @@
|
||||
package tools
|
||||
package fstools
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -1,4 +1,4 @@
|
||||
package tools
|
||||
package fstools
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
|
||||
"github.com/sipeed/picoclaw/pkg/config"
|
||||
"github.com/sipeed/picoclaw/pkg/media"
|
||||
"github.com/sipeed/picoclaw/pkg/providers"
|
||||
)
|
||||
|
||||
func TestLoadImage_PathRequired(t *testing.T) {
|
||||
@@ -78,28 +77,6 @@ func TestLoadImage_FileTooLarge(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubagentManager_SetMediaResolver_StoresResolver(t *testing.T) {
|
||||
manager := NewSubagentManager(nil, "gpt-test", "/tmp")
|
||||
|
||||
called := false
|
||||
manager.SetMediaResolver(func(msgs []providers.Message) []providers.Message {
|
||||
called = true
|
||||
return msgs
|
||||
})
|
||||
|
||||
manager.mu.RLock()
|
||||
got := manager.mediaResolver
|
||||
manager.mu.RUnlock()
|
||||
|
||||
if got == nil {
|
||||
t.Fatal("expected mediaResolver to be set")
|
||||
}
|
||||
|
||||
if called {
|
||||
t.Fatal("resolver should not be called during SetMediaResolver")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadImage_SuccessPath(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package tools
|
||||
package fstools
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -1,4 +1,4 @@
|
||||
package tools
|
||||
package fstools
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -0,0 +1,37 @@
|
||||
package fstools
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
toolshared "github.com/sipeed/picoclaw/pkg/tools/shared"
|
||||
)
|
||||
|
||||
type ToolResult = toolshared.ToolResult
|
||||
|
||||
func WithToolContext(ctx context.Context, channel, chatID string) context.Context {
|
||||
return toolshared.WithToolContext(ctx, channel, chatID)
|
||||
}
|
||||
|
||||
func ToolChannel(ctx context.Context) string {
|
||||
return toolshared.ToolChannel(ctx)
|
||||
}
|
||||
|
||||
func ToolChatID(ctx context.Context) string {
|
||||
return toolshared.ToolChatID(ctx)
|
||||
}
|
||||
|
||||
func ErrorResult(message string) *ToolResult {
|
||||
return toolshared.ErrorResult(message)
|
||||
}
|
||||
|
||||
func NewToolResult(forLLM string) *ToolResult {
|
||||
return toolshared.NewToolResult(forLLM)
|
||||
}
|
||||
|
||||
func SilentResult(forLLM string) *ToolResult {
|
||||
return toolshared.SilentResult(forLLM)
|
||||
}
|
||||
|
||||
func MediaResult(forLLM string, mediaRefs []string) *ToolResult {
|
||||
return toolshared.MediaResult(forLLM, mediaRefs)
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
|
||||
"github.com/sipeed/picoclaw/pkg/media"
|
||||
fstools "github.com/sipeed/picoclaw/pkg/tools/fs"
|
||||
)
|
||||
|
||||
type (
|
||||
ReadFileTool = fstools.ReadFileTool
|
||||
ReadFileLinesTool = fstools.ReadFileLinesTool
|
||||
WriteFileTool = fstools.WriteFileTool
|
||||
ListDirTool = fstools.ListDirTool
|
||||
EditFileTool = fstools.EditFileTool
|
||||
AppendFileTool = fstools.AppendFileTool
|
||||
LoadImageTool = fstools.LoadImageTool
|
||||
SendFileTool = fstools.SendFileTool
|
||||
)
|
||||
|
||||
const MaxReadFileSize = fstools.MaxReadFileSize
|
||||
|
||||
func NewReadFileTool(
|
||||
workspace string,
|
||||
restrict bool,
|
||||
maxReadFileSize int,
|
||||
allowPaths ...[]*regexp.Regexp,
|
||||
) *ReadFileTool {
|
||||
return fstools.NewReadFileTool(workspace, restrict, maxReadFileSize, allowPaths...)
|
||||
}
|
||||
|
||||
func NewReadFileBytesTool(
|
||||
workspace string,
|
||||
restrict bool,
|
||||
maxReadFileSize int,
|
||||
allowPaths ...[]*regexp.Regexp,
|
||||
) *ReadFileTool {
|
||||
return fstools.NewReadFileBytesTool(workspace, restrict, maxReadFileSize, allowPaths...)
|
||||
}
|
||||
|
||||
func NewReadFileLinesTool(
|
||||
workspace string,
|
||||
restrict bool,
|
||||
maxReadFileSize int,
|
||||
allowPaths ...[]*regexp.Regexp,
|
||||
) *ReadFileLinesTool {
|
||||
return fstools.NewReadFileLinesTool(workspace, restrict, maxReadFileSize, allowPaths...)
|
||||
}
|
||||
|
||||
func NewWriteFileTool(
|
||||
workspace string,
|
||||
restrict bool,
|
||||
allowPaths ...[]*regexp.Regexp,
|
||||
) *WriteFileTool {
|
||||
return fstools.NewWriteFileTool(workspace, restrict, allowPaths...)
|
||||
}
|
||||
|
||||
func NewListDirTool(
|
||||
workspace string,
|
||||
restrict bool,
|
||||
allowPaths ...[]*regexp.Regexp,
|
||||
) *ListDirTool {
|
||||
return fstools.NewListDirTool(workspace, restrict, allowPaths...)
|
||||
}
|
||||
|
||||
func NewEditFileTool(
|
||||
workspace string,
|
||||
restrict bool,
|
||||
allowPaths ...[]*regexp.Regexp,
|
||||
) *EditFileTool {
|
||||
return fstools.NewEditFileTool(workspace, restrict, allowPaths...)
|
||||
}
|
||||
|
||||
func NewAppendFileTool(
|
||||
workspace string,
|
||||
restrict bool,
|
||||
allowPaths ...[]*regexp.Regexp,
|
||||
) *AppendFileTool {
|
||||
return fstools.NewAppendFileTool(workspace, restrict, allowPaths...)
|
||||
}
|
||||
|
||||
func NewLoadImageTool(
|
||||
workspace string,
|
||||
restrict bool,
|
||||
maxFileSize int,
|
||||
store media.MediaStore,
|
||||
allowPaths ...[]*regexp.Regexp,
|
||||
) *LoadImageTool {
|
||||
return fstools.NewLoadImageTool(workspace, restrict, maxFileSize, store, allowPaths...)
|
||||
}
|
||||
|
||||
func NewSendFileTool(
|
||||
workspace string,
|
||||
restrict bool,
|
||||
maxFileSize int,
|
||||
store media.MediaStore,
|
||||
allowPaths ...[]*regexp.Regexp,
|
||||
) *SendFileTool {
|
||||
return fstools.NewSendFileTool(workspace, restrict, maxFileSize, store, allowPaths...)
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestReadFileLinesTool_RegistryValidationSupportsMaxLinesAndRejectsLimit(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
testFile := filepath.Join(tmpDir, "registry_lines.txt")
|
||||
|
||||
err := os.WriteFile(testFile, []byte("line 1\nline 2\nline 3\n"), 0o644)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to write test file: %v", err)
|
||||
}
|
||||
|
||||
reg := NewToolRegistry()
|
||||
reg.Register(NewReadFileLinesTool(tmpDir, false, MaxReadFileSize))
|
||||
|
||||
result := reg.Execute(context.Background(), "read_file", map[string]any{
|
||||
"path": testFile,
|
||||
"start_line": 1,
|
||||
"max_lines": 1,
|
||||
})
|
||||
if result.IsError {
|
||||
t.Fatalf("expected max_lines to pass registry validation, got: %s", result.ForLLM)
|
||||
}
|
||||
if !strings.Contains(result.ForLLM, "1|line 1\n") {
|
||||
t.Fatalf("expected first line via max_lines, got: %s", result.ForLLM)
|
||||
}
|
||||
|
||||
result = reg.Execute(context.Background(), "read_file", map[string]any{
|
||||
"path": testFile,
|
||||
"start_line": 2,
|
||||
"limit": 1,
|
||||
})
|
||||
if !result.IsError {
|
||||
t.Fatalf("expected limit to be rejected, got success: %s", result.ForLLM)
|
||||
}
|
||||
if !strings.Contains(result.ForLLM, "unexpected property \"limit\"") {
|
||||
t.Fatalf("expected registry validation error for limit, got: %s", result.ForLLM)
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package tools
|
||||
package hardwaretools
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -120,16 +120,12 @@ func (t *I2CTool) detect() *ToolResult {
|
||||
// Helper functions for I2C operations (used by platform-specific implementations)
|
||||
|
||||
// isValidBusID checks that a bus identifier is a simple number (prevents path injection)
|
||||
//
|
||||
//nolint:unused // Used by i2c_linux.go
|
||||
func isValidBusID(id string) bool {
|
||||
matched, _ := regexp.MatchString(`^\d+$`, id)
|
||||
return matched
|
||||
}
|
||||
|
||||
// parseI2CAddress extracts and validates an I2C address from args
|
||||
//
|
||||
//nolint:unused // Used by i2c_linux.go
|
||||
func parseI2CAddress(args map[string]any) (int, *ToolResult) {
|
||||
addrFloat, ok := args["address"].(float64)
|
||||
if !ok {
|
||||
@@ -143,8 +139,6 @@ func parseI2CAddress(args map[string]any) (int, *ToolResult) {
|
||||
}
|
||||
|
||||
// parseI2CBus extracts and validates an I2C bus from args
|
||||
//
|
||||
//nolint:unused // Used by i2c_linux.go
|
||||
func parseI2CBus(args map[string]any) (string, *ToolResult) {
|
||||
bus, ok := args["bus"].(string)
|
||||
if !ok || bus == "" {
|
||||
@@ -155,3 +149,9 @@ func parseI2CBus(args map[string]any) (string, *ToolResult) {
|
||||
}
|
||||
return bus, nil
|
||||
}
|
||||
|
||||
var (
|
||||
_ = isValidBusID
|
||||
_ = parseI2CAddress
|
||||
_ = parseI2CBus
|
||||
)
|
||||
@@ -1,4 +1,4 @@
|
||||
package tools
|
||||
package hardwaretools
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -1,6 +1,6 @@
|
||||
//go:build !linux
|
||||
|
||||
package tools
|
||||
package hardwaretools
|
||||
|
||||
// scan is a stub for non-Linux platforms.
|
||||
func (t *I2CTool) scan(args map[string]any) *ToolResult {
|
||||
@@ -0,0 +1,13 @@
|
||||
package hardwaretools
|
||||
|
||||
import toolshared "github.com/sipeed/picoclaw/pkg/tools/shared"
|
||||
|
||||
type ToolResult = toolshared.ToolResult
|
||||
|
||||
func ErrorResult(message string) *ToolResult {
|
||||
return toolshared.ErrorResult(message)
|
||||
}
|
||||
|
||||
func SilentResult(forLLM string) *ToolResult {
|
||||
return toolshared.SilentResult(forLLM)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package tools
|
||||
package hardwaretools
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -122,8 +122,6 @@ func (t *SPITool) list() *ToolResult {
|
||||
// Helper function for SPI operations (used by platform-specific implementations)
|
||||
|
||||
// parseSPIArgs extracts and validates common SPI parameters
|
||||
//
|
||||
//nolint:unused // Used by spi_linux.go
|
||||
func parseSPIArgs(args map[string]any) (device string, speed uint32, mode uint8, bits uint8, errMsg string) {
|
||||
dev, ok := args["device"].(string)
|
||||
if !ok || dev == "" {
|
||||
@@ -160,3 +158,5 @@ func parseSPIArgs(args map[string]any) (device string, speed uint32, mode uint8,
|
||||
|
||||
return dev, speed, mode, bits, ""
|
||||
}
|
||||
|
||||
var _ = parseSPIArgs
|
||||
@@ -1,4 +1,4 @@
|
||||
package tools
|
||||
package hardwaretools
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -1,6 +1,6 @@
|
||||
//go:build !linux
|
||||
|
||||
package tools
|
||||
package hardwaretools
|
||||
|
||||
// transfer is a stub for non-Linux platforms.
|
||||
func (t *SPITool) transfer(args map[string]any) *ToolResult {
|
||||
@@ -0,0 +1,16 @@
|
||||
package tools
|
||||
|
||||
import hardwaretools "github.com/sipeed/picoclaw/pkg/tools/hardware"
|
||||
|
||||
type (
|
||||
I2CTool = hardwaretools.I2CTool
|
||||
SPITool = hardwaretools.SPITool
|
||||
)
|
||||
|
||||
func NewI2CTool() *I2CTool {
|
||||
return hardwaretools.NewI2CTool()
|
||||
}
|
||||
|
||||
func NewSPITool() *SPITool {
|
||||
return hardwaretools.NewSPITool()
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package tools
|
||||
|
||||
import "strings"
|
||||
|
||||
func sanitizeIdentifierComponent(s string) string {
|
||||
const maxLen = 64
|
||||
|
||||
s = strings.ToLower(s)
|
||||
var b strings.Builder
|
||||
b.Grow(len(s))
|
||||
|
||||
prevUnderscore := false
|
||||
for _, r := range s {
|
||||
isAllowed := (r >= 'a' && r <= 'z') ||
|
||||
(r >= '0' && r <= '9') ||
|
||||
r == '_' || r == '-'
|
||||
|
||||
if !isAllowed {
|
||||
if !prevUnderscore {
|
||||
b.WriteRune('_')
|
||||
prevUnderscore = true
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if r == '_' {
|
||||
if prevUnderscore {
|
||||
continue
|
||||
}
|
||||
prevUnderscore = true
|
||||
} else {
|
||||
prevUnderscore = false
|
||||
}
|
||||
|
||||
b.WriteRune(r)
|
||||
}
|
||||
|
||||
result := strings.Trim(b.String(), "_")
|
||||
if result == "" {
|
||||
result = "unnamed"
|
||||
}
|
||||
|
||||
if len(result) > maxLen {
|
||||
result = result[:maxLen]
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
package integrationtools
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"mime"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
var (
|
||||
inlineMarkdownDataURLRe = regexp.MustCompile(`!\[[^\]]*\]\((data:[^)]+)\)`)
|
||||
inlineRawDataURLRe = regexp.MustCompile(`data:[^;\s]+;base64,[A-Za-z0-9+/=\r\n]+`)
|
||||
)
|
||||
|
||||
const (
|
||||
largeBase64OmittedMessage = "[Tool returned a large base64-like payload; omitted from model context.]"
|
||||
inlineMediaOmittedMessage = "[Tool returned inline media content; omitted from model context.]"
|
||||
)
|
||||
|
||||
func sanitizeToolLLMContent(text string) string {
|
||||
trimmed := strings.TrimSpace(text)
|
||||
if trimmed == "" {
|
||||
return text
|
||||
}
|
||||
if inlineMarkdownDataURLRe.MatchString(trimmed) || inlineRawDataURLRe.MatchString(trimmed) {
|
||||
cleaned := inlineMarkdownDataURLRe.ReplaceAllString(trimmed, "")
|
||||
cleaned = inlineRawDataURLRe.ReplaceAllString(cleaned, "")
|
||||
cleaned = strings.TrimSpace(cleaned)
|
||||
if cleaned == "" {
|
||||
return inlineMediaOmittedMessage
|
||||
}
|
||||
return cleaned + "\n" + inlineMediaOmittedMessage
|
||||
}
|
||||
if looksLikeLargeBase64Payload(trimmed) {
|
||||
return largeBase64OmittedMessage
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
func looksLikeLargeBase64Payload(text string) bool {
|
||||
trimmed := strings.TrimSpace(text)
|
||||
if len(trimmed) < 1024 {
|
||||
return false
|
||||
}
|
||||
|
||||
nonSpace := 0
|
||||
base64Like := 0
|
||||
spaceCount := 0
|
||||
|
||||
for _, r := range trimmed {
|
||||
if unicode.IsSpace(r) {
|
||||
spaceCount++
|
||||
continue
|
||||
}
|
||||
nonSpace++
|
||||
if (r >= 'A' && r <= 'Z') ||
|
||||
(r >= 'a' && r <= 'z') ||
|
||||
(r >= '0' && r <= '9') ||
|
||||
r == '+' || r == '/' || r == '=' {
|
||||
base64Like++
|
||||
}
|
||||
}
|
||||
|
||||
if nonSpace == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
ratio := float64(base64Like) / float64(nonSpace)
|
||||
return ratio >= 0.97 && spaceCount <= len(trimmed)/128
|
||||
}
|
||||
|
||||
func extensionForMIMEType(mimeType string) string {
|
||||
if mimeType == "" {
|
||||
return ".bin"
|
||||
}
|
||||
if exts, err := mime.ExtensionsByType(mimeType); err == nil && len(exts) > 0 {
|
||||
return exts[0]
|
||||
}
|
||||
|
||||
switch strings.ToLower(mimeType) {
|
||||
case "image/jpeg":
|
||||
return ".jpg"
|
||||
case "image/png":
|
||||
return ".png"
|
||||
case "image/gif":
|
||||
return ".gif"
|
||||
case "image/webp":
|
||||
return ".webp"
|
||||
case "audio/wav", "audio/x-wav":
|
||||
return ".wav"
|
||||
case "audio/mpeg":
|
||||
return ".mp3"
|
||||
case "audio/ogg":
|
||||
return ".ogg"
|
||||
case "video/mp4":
|
||||
return ".mp4"
|
||||
default:
|
||||
return filepath.Ext(mimeType)
|
||||
}
|
||||
}
|
||||
|
||||
func getInt64Arg(args map[string]any, key string, defaultVal int64) (int64, error) {
|
||||
raw, exists := args[key]
|
||||
if !exists {
|
||||
return defaultVal, nil
|
||||
}
|
||||
|
||||
switch v := raw.(type) {
|
||||
case float64:
|
||||
if v != math.Trunc(v) {
|
||||
return 0, fmt.Errorf("%s must be an integer, got float %v", key, v)
|
||||
}
|
||||
if v > math.MaxInt64 || v < math.MinInt64 {
|
||||
return 0, fmt.Errorf("%s value %v overflows int64", key, v)
|
||||
}
|
||||
return int64(v), nil
|
||||
case int:
|
||||
return int64(v), nil
|
||||
case int64:
|
||||
return v, nil
|
||||
case string:
|
||||
parsed, err := strconv.ParseInt(v, 10, 64)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("invalid integer format for %s parameter: %w", key, err)
|
||||
}
|
||||
return parsed, nil
|
||||
default:
|
||||
return 0, fmt.Errorf("unsupported type %T for %s parameter", raw, key)
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package tools
|
||||
package integrationtools
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -1,4 +1,4 @@
|
||||
package tools
|
||||
package integrationtools
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -1,4 +1,4 @@
|
||||
package tools
|
||||
package integrationtools
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -1,4 +1,4 @@
|
||||
package tools
|
||||
package integrationtools
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -1,4 +1,4 @@
|
||||
package tools
|
||||
package integrationtools
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -1,4 +1,4 @@
|
||||
package tools
|
||||
package integrationtools
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -0,0 +1,77 @@
|
||||
package integrationtools
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/sipeed/picoclaw/pkg/session"
|
||||
toolshared "github.com/sipeed/picoclaw/pkg/tools/shared"
|
||||
)
|
||||
|
||||
type (
|
||||
Tool = toolshared.Tool
|
||||
ToolResult = toolshared.ToolResult
|
||||
AsyncCallback = toolshared.AsyncCallback
|
||||
)
|
||||
|
||||
func WithToolContext(ctx context.Context, channel, chatID string) context.Context {
|
||||
return toolshared.WithToolContext(ctx, channel, chatID)
|
||||
}
|
||||
|
||||
func WithToolInboundContext(
|
||||
ctx context.Context,
|
||||
channel, chatID, messageID, replyToMessageID string,
|
||||
) context.Context {
|
||||
return toolshared.WithToolInboundContext(ctx, channel, chatID, messageID, replyToMessageID)
|
||||
}
|
||||
|
||||
func WithToolSessionContext(
|
||||
ctx context.Context,
|
||||
agentID, sessionKey string,
|
||||
scope *session.SessionScope,
|
||||
) context.Context {
|
||||
return toolshared.WithToolSessionContext(ctx, agentID, sessionKey, scope)
|
||||
}
|
||||
|
||||
func ToolChannel(ctx context.Context) string {
|
||||
return toolshared.ToolChannel(ctx)
|
||||
}
|
||||
|
||||
func ToolChatID(ctx context.Context) string {
|
||||
return toolshared.ToolChatID(ctx)
|
||||
}
|
||||
|
||||
func ToolMessageID(ctx context.Context) string {
|
||||
return toolshared.ToolMessageID(ctx)
|
||||
}
|
||||
|
||||
func ToolAgentID(ctx context.Context) string {
|
||||
return toolshared.ToolAgentID(ctx)
|
||||
}
|
||||
|
||||
func ToolSessionKey(ctx context.Context) string {
|
||||
return toolshared.ToolSessionKey(ctx)
|
||||
}
|
||||
|
||||
func ToolSessionScope(ctx context.Context) *session.SessionScope {
|
||||
return toolshared.ToolSessionScope(ctx)
|
||||
}
|
||||
|
||||
func ErrorResult(message string) *ToolResult {
|
||||
return toolshared.ErrorResult(message)
|
||||
}
|
||||
|
||||
func SilentResult(forLLM string) *ToolResult {
|
||||
return toolshared.SilentResult(forLLM)
|
||||
}
|
||||
|
||||
func NewToolResult(forLLM string) *ToolResult {
|
||||
return toolshared.NewToolResult(forLLM)
|
||||
}
|
||||
|
||||
func UserResult(content string) *ToolResult {
|
||||
return toolshared.UserResult(content)
|
||||
}
|
||||
|
||||
func MediaResult(forLLM string, mediaRefs []string) *ToolResult {
|
||||
return toolshared.MediaResult(forLLM, mediaRefs)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package tools
|
||||
package integrationtools
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -1,4 +1,4 @@
|
||||
package tools
|
||||
package integrationtools
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -1,4 +1,4 @@
|
||||
package tools
|
||||
package integrationtools
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -1,4 +1,4 @@
|
||||
package tools
|
||||
package integrationtools
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -1,4 +1,4 @@
|
||||
package tools
|
||||
package integrationtools
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -1,4 +1,4 @@
|
||||
package tools
|
||||
package integrationtools
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@@ -1,4 +1,4 @@
|
||||
package tools
|
||||
package integrationtools
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@@ -0,0 +1,101 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
|
||||
"github.com/sipeed/picoclaw/pkg/audio/tts"
|
||||
"github.com/sipeed/picoclaw/pkg/media"
|
||||
"github.com/sipeed/picoclaw/pkg/skills"
|
||||
integrationtools "github.com/sipeed/picoclaw/pkg/tools/integration"
|
||||
)
|
||||
|
||||
type (
|
||||
SendCallbackWithContext = integrationtools.SendCallbackWithContext
|
||||
ReactionCallback = integrationtools.ReactionCallback
|
||||
MCPManager = integrationtools.MCPManager
|
||||
MCPTool = integrationtools.MCPTool
|
||||
FindSkillsTool = integrationtools.FindSkillsTool
|
||||
InstallSkillTool = integrationtools.InstallSkillTool
|
||||
MessageTool = integrationtools.MessageTool
|
||||
ReactionTool = integrationtools.ReactionTool
|
||||
SendTTSTool = integrationtools.SendTTSTool
|
||||
APIKeyPool = integrationtools.APIKeyPool
|
||||
APIKeyIterator = integrationtools.APIKeyIterator
|
||||
SearchProvider = integrationtools.SearchProvider
|
||||
SearchResultItem = integrationtools.SearchResultItem
|
||||
BraveSearchProvider = integrationtools.BraveSearchProvider
|
||||
TavilySearchProvider = integrationtools.TavilySearchProvider
|
||||
SogouSearchProvider = integrationtools.SogouSearchProvider
|
||||
DuckDuckGoSearchProvider = integrationtools.DuckDuckGoSearchProvider
|
||||
PerplexitySearchProvider = integrationtools.PerplexitySearchProvider
|
||||
SearXNGSearchProvider = integrationtools.SearXNGSearchProvider
|
||||
GLMSearchProvider = integrationtools.GLMSearchProvider
|
||||
BaiduSearchProvider = integrationtools.BaiduSearchProvider
|
||||
WebSearchTool = integrationtools.WebSearchTool
|
||||
WebSearchToolOptions = integrationtools.WebSearchToolOptions
|
||||
WebFetchTool = integrationtools.WebFetchTool
|
||||
)
|
||||
|
||||
func NewMCPTool(manager MCPManager, serverName string, tool *mcp.Tool) *MCPTool {
|
||||
return integrationtools.NewMCPTool(manager, serverName, tool)
|
||||
}
|
||||
|
||||
func NewFindSkillsTool(registryMgr *skills.RegistryManager, cache *skills.SearchCache) *FindSkillsTool {
|
||||
return integrationtools.NewFindSkillsTool(registryMgr, cache)
|
||||
}
|
||||
|
||||
func NewInstallSkillTool(registryMgr *skills.RegistryManager, workspace string) *InstallSkillTool {
|
||||
return integrationtools.NewInstallSkillTool(registryMgr, workspace)
|
||||
}
|
||||
|
||||
func NewMessageTool() *MessageTool {
|
||||
return integrationtools.NewMessageTool()
|
||||
}
|
||||
|
||||
func NewReactionTool() *ReactionTool {
|
||||
return integrationtools.NewReactionTool()
|
||||
}
|
||||
|
||||
func NewSendTTSTool(provider tts.TTSProvider, store media.MediaStore) *SendTTSTool {
|
||||
return integrationtools.NewSendTTSTool(provider, store)
|
||||
}
|
||||
|
||||
func NewAPIKeyPool(keys []string) *APIKeyPool {
|
||||
return integrationtools.NewAPIKeyPool(keys)
|
||||
}
|
||||
|
||||
func SetPreferredWebSearchLanguage(lang string) {
|
||||
integrationtools.SetPreferredWebSearchLanguage(lang)
|
||||
}
|
||||
|
||||
func GetPreferredWebSearchLanguage() string {
|
||||
return integrationtools.GetPreferredWebSearchLanguage()
|
||||
}
|
||||
|
||||
func NewWebSearchTool(opts WebSearchToolOptions) (*WebSearchTool, error) {
|
||||
return integrationtools.NewWebSearchTool(opts)
|
||||
}
|
||||
|
||||
func NewWebFetchTool(maxChars int, format string, fetchLimitBytes int64) (*WebFetchTool, error) {
|
||||
return integrationtools.NewWebFetchTool(maxChars, format, fetchLimitBytes)
|
||||
}
|
||||
|
||||
func NewWebFetchToolWithProxy(
|
||||
maxChars int,
|
||||
proxy string,
|
||||
format string,
|
||||
fetchLimitBytes int64,
|
||||
privateHostWhitelist []string,
|
||||
) (*WebFetchTool, error) {
|
||||
return integrationtools.NewWebFetchToolWithProxy(maxChars, proxy, format, fetchLimitBytes, privateHostWhitelist)
|
||||
}
|
||||
|
||||
func NewWebFetchToolWithConfig(
|
||||
maxChars int,
|
||||
proxy string,
|
||||
format string,
|
||||
fetchLimitBytes int64,
|
||||
privateHostWhitelist []string,
|
||||
) (*WebFetchTool, error) {
|
||||
return integrationtools.NewWebFetchToolWithConfig(maxChars, proxy, format, fetchLimitBytes, privateHostWhitelist)
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/sipeed/picoclaw/pkg/providers"
|
||||
)
|
||||
|
||||
func TestSubagentManager_SetMediaResolver_StoresResolver(t *testing.T) {
|
||||
manager := NewSubagentManager(nil, "gpt-test", "/tmp")
|
||||
|
||||
called := false
|
||||
manager.SetMediaResolver(func(msgs []providers.Message) []providers.Message {
|
||||
called = true
|
||||
return msgs
|
||||
})
|
||||
|
||||
manager.mu.RLock()
|
||||
got := manager.mediaResolver
|
||||
manager.mu.RUnlock()
|
||||
|
||||
if got == nil {
|
||||
t.Fatal("expected mediaResolver to be set")
|
||||
}
|
||||
|
||||
if called {
|
||||
t.Fatal("resolver should not be called during SetMediaResolver")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
|
||||
fstools "github.com/sipeed/picoclaw/pkg/tools/fs"
|
||||
)
|
||||
|
||||
func validatePathWithAllowPaths(
|
||||
path, workspace string,
|
||||
restrict bool,
|
||||
patterns []*regexp.Regexp,
|
||||
) (string, error) {
|
||||
return fstools.ValidatePathWithAllowPaths(path, workspace, restrict, patterns)
|
||||
}
|
||||
|
||||
func isAllowedPath(path string, patterns []*regexp.Regexp) bool {
|
||||
return fstools.IsAllowedPath(path, patterns)
|
||||
}
|
||||
@@ -242,11 +242,3 @@ func (sm *SessionManager) List() []SessionInfo {
|
||||
func generateSessionID() string {
|
||||
return uuid.New().String()[:8]
|
||||
}
|
||||
|
||||
type SessionInfo struct {
|
||||
ID string `json:"id"`
|
||||
Command string `json:"command"`
|
||||
Status string `json:"status"`
|
||||
PID int `json:"pid"`
|
||||
StartedAt int64 `json:"startedAt"`
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package tools
|
||||
package toolshared
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -1,4 +1,4 @@
|
||||
package tools
|
||||
package toolshared
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -8,8 +8,8 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
handledToolLLMNote = "The requested output has already been delivered to the user in the current chat. Do not call send_file or any other delivery tool again. If you reply, provide only a brief confirmation."
|
||||
artifactPathsLLMNote = "Use `send_file` with one of these paths to send it to the user, or use file/exec tools to save it inside the workspace if requested."
|
||||
HandledToolLLMNote = "The requested output has already been delivered to the user in the current chat. Do not call send_file or any other delivery tool again. If you reply, provide only a brief confirmation."
|
||||
ArtifactPathsLLMNote = "Use `send_file` with one of these paths to send it to the user, or use file/exec tools to save it inside the workspace if requested."
|
||||
)
|
||||
|
||||
// ToolResult represents the structured return value from tool execution.
|
||||
@@ -73,14 +73,14 @@ func (tr *ToolResult) ContentForLLM() string {
|
||||
}
|
||||
if tr.ResponseHandled {
|
||||
if content == "" {
|
||||
return handledToolLLMNote
|
||||
return HandledToolLLMNote
|
||||
}
|
||||
if !strings.Contains(content, handledToolLLMNote) {
|
||||
content += "\n" + handledToolLLMNote
|
||||
if !strings.Contains(content, HandledToolLLMNote) {
|
||||
content += "\n" + HandledToolLLMNote
|
||||
}
|
||||
}
|
||||
if len(tr.ArtifactTags) > 0 {
|
||||
artifactNote := "Local artifact paths: " + strings.Join(tr.ArtifactTags, " ") + "\n" + artifactPathsLLMNote
|
||||
artifactNote := "Local artifact paths: " + strings.Join(tr.ArtifactTags, " ") + "\n" + ArtifactPathsLLMNote
|
||||
if content == "" {
|
||||
content = artifactNote
|
||||
} else if !strings.Contains(content, artifactNote) {
|
||||
@@ -1,4 +1,4 @@
|
||||
package tools
|
||||
package toolshared
|
||||
|
||||
import "context"
|
||||
|
||||
@@ -77,3 +77,11 @@ type ExecResponse struct {
|
||||
Error string `json:"error,omitempty"`
|
||||
Sessions []SessionInfo `json:"sessions,omitempty"`
|
||||
}
|
||||
|
||||
type SessionInfo struct {
|
||||
ID string `json:"id"`
|
||||
Command string `json:"command"`
|
||||
Status string `json:"status"`
|
||||
PID int `json:"pid"`
|
||||
StartedAt int64 `json:"startedAt"`
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/sipeed/picoclaw/pkg/session"
|
||||
toolshared "github.com/sipeed/picoclaw/pkg/tools/shared"
|
||||
)
|
||||
|
||||
type (
|
||||
Message = toolshared.Message
|
||||
ToolCall = toolshared.ToolCall
|
||||
FunctionCall = toolshared.FunctionCall
|
||||
LLMResponse = toolshared.LLMResponse
|
||||
UsageInfo = toolshared.UsageInfo
|
||||
LLMProvider = toolshared.LLMProvider
|
||||
ToolDefinition = toolshared.ToolDefinition
|
||||
ToolFunctionDefinition = toolshared.ToolFunctionDefinition
|
||||
ExecRequest = toolshared.ExecRequest
|
||||
ExecResponse = toolshared.ExecResponse
|
||||
SessionInfo = toolshared.SessionInfo
|
||||
Tool = toolshared.Tool
|
||||
AsyncCallback = toolshared.AsyncCallback
|
||||
AsyncExecutor = toolshared.AsyncExecutor
|
||||
ToolResult = toolshared.ToolResult
|
||||
)
|
||||
|
||||
const (
|
||||
handledToolLLMNote = toolshared.HandledToolLLMNote
|
||||
artifactPathsLLMNote = toolshared.ArtifactPathsLLMNote
|
||||
)
|
||||
|
||||
func WithToolContext(ctx context.Context, channel, chatID string) context.Context {
|
||||
return toolshared.WithToolContext(ctx, channel, chatID)
|
||||
}
|
||||
|
||||
func WithToolMessageContext(ctx context.Context, messageID, replyToMessageID string) context.Context {
|
||||
return toolshared.WithToolMessageContext(ctx, messageID, replyToMessageID)
|
||||
}
|
||||
|
||||
func WithToolInboundContext(
|
||||
ctx context.Context,
|
||||
channel, chatID, messageID, replyToMessageID string,
|
||||
) context.Context {
|
||||
return toolshared.WithToolInboundContext(ctx, channel, chatID, messageID, replyToMessageID)
|
||||
}
|
||||
|
||||
func WithToolSessionContext(
|
||||
ctx context.Context,
|
||||
agentID, sessionKey string,
|
||||
scope *session.SessionScope,
|
||||
) context.Context {
|
||||
return toolshared.WithToolSessionContext(ctx, agentID, sessionKey, scope)
|
||||
}
|
||||
|
||||
func ToolChannel(ctx context.Context) string {
|
||||
return toolshared.ToolChannel(ctx)
|
||||
}
|
||||
|
||||
func ToolChatID(ctx context.Context) string {
|
||||
return toolshared.ToolChatID(ctx)
|
||||
}
|
||||
|
||||
func ToolMessageID(ctx context.Context) string {
|
||||
return toolshared.ToolMessageID(ctx)
|
||||
}
|
||||
|
||||
func ToolReplyToMessageID(ctx context.Context) string {
|
||||
return toolshared.ToolReplyToMessageID(ctx)
|
||||
}
|
||||
|
||||
func ToolAgentID(ctx context.Context) string {
|
||||
return toolshared.ToolAgentID(ctx)
|
||||
}
|
||||
|
||||
func ToolSessionKey(ctx context.Context) string {
|
||||
return toolshared.ToolSessionKey(ctx)
|
||||
}
|
||||
|
||||
func ToolSessionScope(ctx context.Context) *session.SessionScope {
|
||||
return toolshared.ToolSessionScope(ctx)
|
||||
}
|
||||
|
||||
func ToolToSchema(tool Tool) map[string]any {
|
||||
return toolshared.ToolToSchema(tool)
|
||||
}
|
||||
|
||||
func NewToolResult(forLLM string) *ToolResult {
|
||||
return toolshared.NewToolResult(forLLM)
|
||||
}
|
||||
|
||||
func SilentResult(forLLM string) *ToolResult {
|
||||
return toolshared.SilentResult(forLLM)
|
||||
}
|
||||
|
||||
func AsyncResult(forLLM string) *ToolResult {
|
||||
return toolshared.AsyncResult(forLLM)
|
||||
}
|
||||
|
||||
func ErrorResult(message string) *ToolResult {
|
||||
return toolshared.ErrorResult(message)
|
||||
}
|
||||
|
||||
func UserResult(content string) *ToolResult {
|
||||
return toolshared.UserResult(content)
|
||||
}
|
||||
|
||||
func MediaResult(forLLM string, mediaRefs []string) *ToolResult {
|
||||
return toolshared.MediaResult(forLLM, mediaRefs)
|
||||
}
|
||||
Reference in New Issue
Block a user