feat: add get_current_time tool
Add a new utility tool to get current time/date with the following features: - Multiple output formats: iso, time, date, datetime, unix - Timezone support (e.g., UTC, America/New_York, Asia/Shanghai) - Configurable via PICOCLAW_TOOLS_GET_CURRENT_TIME_ENABLED env var - Web UI support with utility category and i18n translations The tool is registered in the agent tool registry and can be enabled/disabled via configuration or web interface.
This commit is contained in:
parent
0161298154
commit
0705b38b60
9 changed files with 320 additions and 0 deletions
|
|
@ -446,6 +446,9 @@
|
|||
"spi": {
|
||||
"enabled": false
|
||||
},
|
||||
"get_current_time": {
|
||||
"enabled": true
|
||||
},
|
||||
"subagent": {
|
||||
"enabled": true
|
||||
},
|
||||
|
|
|
|||
|
|
@ -117,6 +117,9 @@ func NewAgentInstance(
|
|||
if cfg.Tools.IsToolEnabled("append_file") {
|
||||
toolsRegistry.Register(tools.NewAppendFileTool(workspace, restrict, allowWritePaths))
|
||||
}
|
||||
if cfg.Tools.IsToolEnabled("get_current_time") {
|
||||
toolsRegistry.Register(tools.NewGetCurrentTimeTool(""))
|
||||
}
|
||||
|
||||
sessionsDir := filepath.Join(workspace, "sessions")
|
||||
sessions := initSessionStore(sessionsDir)
|
||||
|
|
|
|||
|
|
@ -831,6 +831,7 @@ type ToolsConfig struct {
|
|||
Subagent ToolConfig `json:"subagent" yaml:"-" envPrefix:"PICOCLAW_TOOLS_SUBAGENT_"`
|
||||
WebFetch ToolConfig `json:"web_fetch" yaml:"-" envPrefix:"PICOCLAW_TOOLS_WEB_FETCH_"`
|
||||
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
|
||||
|
|
@ -1566,6 +1567,8 @@ func (t *ToolsConfig) IsToolEnabled(name string) bool {
|
|||
return t.WriteFile.Enabled
|
||||
case "mcp":
|
||||
return t.MCP.Enabled
|
||||
case "get_current_time":
|
||||
return t.GetCurrentTime.Enabled
|
||||
default:
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
@ -466,6 +466,9 @@ func computeConfigSignature(cfg *config.Config) string {
|
|||
if cfg.Tools.SPI.Enabled {
|
||||
toolSignatures = append(toolSignatures, "spi")
|
||||
}
|
||||
if cfg.Tools.GetCurrentTime.Enabled {
|
||||
toolSignatures = append(toolSignatures, "get_current_time")
|
||||
}
|
||||
if cfg.Tools.MCP.Enabled {
|
||||
toolSignatures = append(toolSignatures, "mcp")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -171,6 +171,12 @@ var toolCatalog = []toolCatalogEntry{
|
|||
Category: "hardware",
|
||||
ConfigKey: "spi",
|
||||
},
|
||||
{
|
||||
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",
|
||||
Description: "Discover hidden MCP tools by regex search when tool discovery is enabled.",
|
||||
|
|
@ -362,6 +368,8 @@ func applyToolState(cfg *config.Config, toolName string, enabled bool) error {
|
|||
cfg.Tools.I2C.Enabled = enabled
|
||||
case "spi":
|
||||
cfg.Tools.SPI.Enabled = enabled
|
||||
case "get_current_time":
|
||||
cfg.Tools.GetCurrentTime.Enabled = enabled
|
||||
case "tool_search_tool_regex":
|
||||
cfg.Tools.MCP.Discovery.UseRegex = enabled
|
||||
if enabled {
|
||||
|
|
|
|||
|
|
@ -599,6 +599,7 @@
|
|||
"skills": "Skills",
|
||||
"agents": "Agents",
|
||||
"hardware": "Hardware",
|
||||
"utility": "Utility",
|
||||
"discovery": "Discovery"
|
||||
},
|
||||
"reasons": {
|
||||
|
|
|
|||
|
|
@ -599,6 +599,7 @@
|
|||
"skills": "技能",
|
||||
"agents": "Agent",
|
||||
"hardware": "硬件",
|
||||
"utility": "实用工具",
|
||||
"discovery": "发现"
|
||||
},
|
||||
"reasons": {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue