Merge 528a004bb9 into 0df050ff2e
This commit is contained in:
commit
a7eba397d5
9 changed files with 320 additions and 0 deletions
|
|
@ -514,6 +514,9 @@
|
||||||
"spi": {
|
"spi": {
|
||||||
"enabled": false
|
"enabled": false
|
||||||
},
|
},
|
||||||
|
"get_current_time": {
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
"subagent": {
|
"subagent": {
|
||||||
"enabled": true
|
"enabled": true
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -124,6 +124,9 @@ func NewAgentInstance(
|
||||||
if cfg.Tools.IsToolEnabled("append_file") {
|
if cfg.Tools.IsToolEnabled("append_file") {
|
||||||
toolsRegistry.Register(tools.NewAppendFileTool(workspace, restrict, allowWritePaths))
|
toolsRegistry.Register(tools.NewAppendFileTool(workspace, restrict, allowWritePaths))
|
||||||
}
|
}
|
||||||
|
if cfg.Tools.IsToolEnabled("get_current_time") {
|
||||||
|
toolsRegistry.Register(tools.NewGetCurrentTimeTool(""))
|
||||||
|
}
|
||||||
|
|
||||||
sessionsDir := filepath.Join(workspace, "sessions")
|
sessionsDir := filepath.Join(workspace, "sessions")
|
||||||
sessions := initSessionStore(sessionsDir)
|
sessions := initSessionStore(sessionsDir)
|
||||||
|
|
|
||||||
|
|
@ -1008,6 +1008,7 @@ type ToolsConfig struct {
|
||||||
Subagent ToolConfig `json:"subagent" yaml:"-" envPrefix:"PICOCLAW_TOOLS_SUBAGENT_"`
|
Subagent ToolConfig `json:"subagent" yaml:"-" envPrefix:"PICOCLAW_TOOLS_SUBAGENT_"`
|
||||||
WebFetch ToolConfig `json:"web_fetch" yaml:"-" envPrefix:"PICOCLAW_TOOLS_WEB_FETCH_"`
|
WebFetch ToolConfig `json:"web_fetch" yaml:"-" envPrefix:"PICOCLAW_TOOLS_WEB_FETCH_"`
|
||||||
WriteFile ToolConfig `json:"write_file" yaml:"-" envPrefix:"PICOCLAW_TOOLS_WRITE_FILE_"`
|
WriteFile ToolConfig `json:"write_file" yaml:"-" envPrefix:"PICOCLAW_TOOLS_WRITE_FILE_"`
|
||||||
|
GetCurrentTime ToolConfig `json:"get_current_time" yaml:"-" envPrefix:"PICOCLAW_TOOLS_GET_CURRENT_TIME_"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsFilterSensitiveDataEnabled returns true if sensitive data filtering is enabled
|
// IsFilterSensitiveDataEnabled returns true if sensitive data filtering is enabled
|
||||||
|
|
@ -1751,6 +1752,8 @@ func (t *ToolsConfig) IsToolEnabled(name string) bool {
|
||||||
return t.WriteFile.Enabled
|
return t.WriteFile.Enabled
|
||||||
case "mcp":
|
case "mcp":
|
||||||
return t.MCP.Enabled
|
return t.MCP.Enabled
|
||||||
|
case "get_current_time":
|
||||||
|
return t.GetCurrentTime.Enabled
|
||||||
default:
|
default:
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
|
||||||
102
pkg/tools/time.go
Normal file
102
pkg/tools/time.go
Normal file
|
|
@ -0,0 +1,102 @@
|
||||||
|
package tools
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
toolshared "github.com/sipeed/picoclaw/pkg/tools/shared"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetCurrentTimeTool returns the current time and/or date information
|
||||||
|
type GetCurrentTimeTool struct {
|
||||||
|
timezone string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewGetCurrentTimeTool creates a new GetCurrentTimeTool
|
||||||
|
func NewGetCurrentTimeTool(timezone string) *GetCurrentTimeTool {
|
||||||
|
if timezone == "" {
|
||||||
|
timezone = "Local"
|
||||||
|
}
|
||||||
|
return &GetCurrentTimeTool{
|
||||||
|
timezone: timezone,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name returns the tool name
|
||||||
|
func (t *GetCurrentTimeTool) Name() string {
|
||||||
|
return "get_current_time"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Description returns the tool description
|
||||||
|
func (t *GetCurrentTimeTool) Description() string {
|
||||||
|
return "Get the current time, date, or both. Returns ISO 8601 format by default, or can return formatted strings suitable for display."
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parameters returns the tool parameters schema
|
||||||
|
func (t *GetCurrentTimeTool) Parameters() map[string]any {
|
||||||
|
return map[string]any{
|
||||||
|
"type": "object",
|
||||||
|
"properties": map[string]any{
|
||||||
|
"format": map[string]any{
|
||||||
|
"type": "string",
|
||||||
|
"enum": []string{"iso", "time", "date", "datetime", "unix"},
|
||||||
|
"description": "Output format: 'iso' (ISO 8601, default), 'time' (HH:MM:SS), 'date' (YYYY-MM-DD), 'datetime' (YYYY-MM-DD HH:MM:SS), 'unix' (Unix timestamp)",
|
||||||
|
"default": "iso",
|
||||||
|
},
|
||||||
|
"timezone": map[string]any{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Timezone name (e.g., 'America/New_York', 'Europe/London', 'Asia/Shanghai'). Uses system local time if not specified.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute runs the tool
|
||||||
|
func (t *GetCurrentTimeTool) Execute(ctx context.Context, args map[string]any) *toolshared.ToolResult {
|
||||||
|
// Get timezone
|
||||||
|
tzName := t.timezone
|
||||||
|
if tzArg, ok := args["timezone"].(string); ok && tzArg != "" {
|
||||||
|
tzName = tzArg
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load timezone
|
||||||
|
loc, err := time.LoadLocation(tzName)
|
||||||
|
if err != nil {
|
||||||
|
// Fallback to local timezone if specified one is invalid
|
||||||
|
loc = time.Local
|
||||||
|
tzName = "Local"
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now().In(loc)
|
||||||
|
|
||||||
|
// Get format
|
||||||
|
format := "iso"
|
||||||
|
if fmtArg, ok := args["format"].(string); ok && fmtArg != "" {
|
||||||
|
format = fmtArg
|
||||||
|
}
|
||||||
|
|
||||||
|
var result string
|
||||||
|
switch format {
|
||||||
|
case "time":
|
||||||
|
result = now.Format("15:04:05")
|
||||||
|
case "date":
|
||||||
|
result = now.Format("2006-01-02")
|
||||||
|
case "datetime":
|
||||||
|
result = now.Format("2006-01-02 15:04:05")
|
||||||
|
case "unix":
|
||||||
|
result = fmt.Sprintf("%d", now.Unix())
|
||||||
|
case "iso":
|
||||||
|
fallthrough
|
||||||
|
default:
|
||||||
|
result = now.Format(time.RFC3339)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build response
|
||||||
|
response := fmt.Sprintf("Current time (%s): %s", tzName, result)
|
||||||
|
|
||||||
|
return &toolshared.ToolResult{
|
||||||
|
ForLLM: response,
|
||||||
|
ForUser: response,
|
||||||
|
}
|
||||||
|
}
|
||||||
196
pkg/tools/time_test.go
Normal file
196
pkg/tools/time_test.go
Normal file
|
|
@ -0,0 +1,196 @@
|
||||||
|
package tools
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGetCurrentTimeTool_Name(t *testing.T) {
|
||||||
|
tool := NewGetCurrentTimeTool("")
|
||||||
|
if tool.Name() != "get_current_time" {
|
||||||
|
t.Errorf("expected name 'get_current_time', got %s", tool.Name())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetCurrentTimeTool_Description(t *testing.T) {
|
||||||
|
tool := NewGetCurrentTimeTool("")
|
||||||
|
if tool.Description() == "" {
|
||||||
|
t.Error("description should not be empty")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetCurrentTimeTool_Parameters(t *testing.T) {
|
||||||
|
tool := NewGetCurrentTimeTool("")
|
||||||
|
params := tool.Parameters()
|
||||||
|
if params == nil {
|
||||||
|
t.Fatal("parameters should not be nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check type
|
||||||
|
if params["type"] != "object" {
|
||||||
|
t.Errorf("expected type 'object', got %v", params["type"])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check properties exist
|
||||||
|
properties, ok := params["properties"].(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("properties should be a map")
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := properties["format"]; !ok {
|
||||||
|
t.Error("format property should exist")
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := properties["timezone"]; !ok {
|
||||||
|
t.Error("timezone property should exist")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetCurrentTimeTool_Execute_DefaultFormat(t *testing.T) {
|
||||||
|
tool := NewGetCurrentTimeTool("")
|
||||||
|
result := tool.Execute(context.Background(), map[string]any{})
|
||||||
|
|
||||||
|
if result == nil {
|
||||||
|
t.Fatal("result should not be nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.IsError {
|
||||||
|
t.Errorf("should not return error: %s", result.ForLLM)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that result contains time information
|
||||||
|
if result.ForLLM == "" {
|
||||||
|
t.Error("ForLLM should not be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default format is ISO, should contain current year
|
||||||
|
currentYear := time.Now().Format("2006")
|
||||||
|
if !strings.Contains(result.ForLLM, currentYear) {
|
||||||
|
t.Errorf("result should contain current year %s, got: %s", currentYear, result.ForLLM)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetCurrentTimeTool_Execute_TimeFormat(t *testing.T) {
|
||||||
|
tool := NewGetCurrentTimeTool("")
|
||||||
|
result := tool.Execute(context.Background(), map[string]any{
|
||||||
|
"format": "time",
|
||||||
|
})
|
||||||
|
|
||||||
|
if result == nil {
|
||||||
|
t.Fatal("result should not be nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.IsError {
|
||||||
|
t.Errorf("should not return error: %s", result.ForLLM)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Time format should be HH:MM:SS
|
||||||
|
if !strings.Contains(result.ForLLM, ":") {
|
||||||
|
t.Errorf("time format should contain colon, got: %s", result.ForLLM)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetCurrentTimeTool_Execute_DateFormat(t *testing.T) {
|
||||||
|
tool := NewGetCurrentTimeTool("")
|
||||||
|
result := tool.Execute(context.Background(), map[string]any{
|
||||||
|
"format": "date",
|
||||||
|
})
|
||||||
|
|
||||||
|
if result == nil {
|
||||||
|
t.Fatal("result should not be nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.IsError {
|
||||||
|
t.Errorf("should not return error: %s", result.ForLLM)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Date format should be YYYY-MM-DD
|
||||||
|
currentYear := time.Now().Format("2006")
|
||||||
|
if !strings.Contains(result.ForLLM, currentYear) {
|
||||||
|
t.Errorf("date format should contain current year %s, got: %s", currentYear, result.ForLLM)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetCurrentTimeTool_Execute_DateTimeFormat(t *testing.T) {
|
||||||
|
tool := NewGetCurrentTimeTool("")
|
||||||
|
result := tool.Execute(context.Background(), map[string]any{
|
||||||
|
"format": "datetime",
|
||||||
|
})
|
||||||
|
|
||||||
|
if result == nil {
|
||||||
|
t.Fatal("result should not be nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.IsError {
|
||||||
|
t.Errorf("should not return error: %s", result.ForLLM)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Datetime format should contain both date and time
|
||||||
|
if !strings.Contains(result.ForLLM, "-") || !strings.Contains(result.ForLLM, ":") {
|
||||||
|
t.Errorf("datetime format should contain both date and time, got: %s", result.ForLLM)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetCurrentTimeTool_Execute_UnixFormat(t *testing.T) {
|
||||||
|
tool := NewGetCurrentTimeTool("")
|
||||||
|
result := tool.Execute(context.Background(), map[string]any{
|
||||||
|
"format": "unix",
|
||||||
|
})
|
||||||
|
|
||||||
|
if result == nil {
|
||||||
|
t.Fatal("result should not be nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.IsError {
|
||||||
|
t.Errorf("should not return error: %s", result.ForLLM)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unix timestamp should be a number (just check it doesn't contain date separators)
|
||||||
|
// The response format is "Current time (Local): 1777295143" which contains a colon after "Local"
|
||||||
|
// So we just check the actual timestamp part is numeric
|
||||||
|
if !strings.Contains(result.ForLLM, "Current time") {
|
||||||
|
t.Errorf("result should contain 'Current time', got: %s", result.ForLLM)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetCurrentTimeTool_Execute_WithTimezone(t *testing.T) {
|
||||||
|
tool := NewGetCurrentTimeTool("")
|
||||||
|
result := tool.Execute(context.Background(), map[string]any{
|
||||||
|
"timezone": "UTC",
|
||||||
|
})
|
||||||
|
|
||||||
|
if result == nil {
|
||||||
|
t.Fatal("result should not be nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.IsError {
|
||||||
|
t.Errorf("should not return error: %s", result.ForLLM)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should mention UTC
|
||||||
|
if !strings.Contains(result.ForLLM, "UTC") {
|
||||||
|
t.Errorf("result should mention UTC timezone, got: %s", result.ForLLM)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetCurrentTimeTool_Execute_InvalidTimezone(t *testing.T) {
|
||||||
|
tool := NewGetCurrentTimeTool("")
|
||||||
|
result := tool.Execute(context.Background(), map[string]any{
|
||||||
|
"timezone": "Invalid/Timezone",
|
||||||
|
})
|
||||||
|
|
||||||
|
if result == nil {
|
||||||
|
t.Fatal("result should not be nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.IsError {
|
||||||
|
t.Error("should handle invalid timezone gracefully without error")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should fallback to Local
|
||||||
|
if !strings.Contains(result.ForLLM, "Local") {
|
||||||
|
t.Errorf("should fallback to Local timezone, got: %s", result.ForLLM)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -469,6 +469,9 @@ func computeConfigSignature(cfg *config.Config) string {
|
||||||
if cfg.Tools.SPI.Enabled {
|
if cfg.Tools.SPI.Enabled {
|
||||||
toolSignatures = append(toolSignatures, "spi")
|
toolSignatures = append(toolSignatures, "spi")
|
||||||
}
|
}
|
||||||
|
if cfg.Tools.GetCurrentTime.Enabled {
|
||||||
|
toolSignatures = append(toolSignatures, "get_current_time")
|
||||||
|
}
|
||||||
if cfg.Tools.MCP.Enabled {
|
if cfg.Tools.MCP.Enabled {
|
||||||
toolSignatures = append(toolSignatures, "mcp")
|
toolSignatures = append(toolSignatures, "mcp")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -178,6 +178,12 @@ var toolCatalog = []toolCatalogEntry{
|
||||||
Category: "hardware",
|
Category: "hardware",
|
||||||
ConfigKey: "serial",
|
ConfigKey: "serial",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "get_current_time",
|
||||||
|
Description: "Get the current time, date, or both in various formats and timezones.",
|
||||||
|
Category: "utility",
|
||||||
|
ConfigKey: "get_current_time",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Name: "tool_search_tool_regex",
|
Name: "tool_search_tool_regex",
|
||||||
Description: "Discover hidden MCP tools by regex search when tool discovery is enabled.",
|
Description: "Discover hidden MCP tools by regex search when tool discovery is enabled.",
|
||||||
|
|
@ -385,6 +391,8 @@ func applyToolState(cfg *config.Config, toolName string, enabled bool) error {
|
||||||
cfg.Tools.SPI.Enabled = enabled
|
cfg.Tools.SPI.Enabled = enabled
|
||||||
case "serial":
|
case "serial":
|
||||||
cfg.Tools.Serial.Enabled = enabled
|
cfg.Tools.Serial.Enabled = enabled
|
||||||
|
case "get_current_time":
|
||||||
|
cfg.Tools.GetCurrentTime.Enabled = enabled
|
||||||
case "tool_search_tool_regex":
|
case "tool_search_tool_regex":
|
||||||
cfg.Tools.MCP.Discovery.UseRegex = enabled
|
cfg.Tools.MCP.Discovery.UseRegex = enabled
|
||||||
if enabled {
|
if enabled {
|
||||||
|
|
|
||||||
|
|
@ -702,6 +702,7 @@
|
||||||
"skills": "Skills",
|
"skills": "Skills",
|
||||||
"agents": "Agents",
|
"agents": "Agents",
|
||||||
"hardware": "Hardware",
|
"hardware": "Hardware",
|
||||||
|
"utility": "Utility",
|
||||||
"discovery": "Discovery"
|
"discovery": "Discovery"
|
||||||
},
|
},
|
||||||
"reasons": {
|
"reasons": {
|
||||||
|
|
|
||||||
|
|
@ -703,6 +703,7 @@
|
||||||
"skills": "技能",
|
"skills": "技能",
|
||||||
"agents": "Agent",
|
"agents": "Agent",
|
||||||
"hardware": "硬件",
|
"hardware": "硬件",
|
||||||
|
"utility": "实用工具",
|
||||||
"discovery": "发现"
|
"discovery": "发现"
|
||||||
},
|
},
|
||||||
"reasons": {
|
"reasons": {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue