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:
dtapps 2026-04-27 21:21:16 +08:00
parent 0161298154
commit 0705b38b60
9 changed files with 320 additions and 0 deletions

View file

@ -446,6 +446,9 @@
"spi": {
"enabled": false
},
"get_current_time": {
"enabled": true
},
"subagent": {
"enabled": true
},

View file

@ -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)

View file

@ -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
View 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
View 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)
}
}

View file

@ -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")
}

View file

@ -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 {

View file

@ -599,6 +599,7 @@
"skills": "Skills",
"agents": "Agents",
"hardware": "Hardware",
"utility": "Utility",
"discovery": "Discovery"
},
"reasons": {

View file

@ -599,6 +599,7 @@
"skills": "技能",
"agents": "Agent",
"hardware": "硬件",
"utility": "实用工具",
"discovery": "发现"
},
"reasons": {