mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-20 02:17:24 +00:00
fix(tools): return authoritative datetime fields (#1468)
Co-authored-by: ntduc <ntduc@cpp.ai.vn>
This commit is contained in:
1 parent
696efa28db
commit
08e8040c18
2 files changed
+150
-7
No files matched your search
@@ -10,9 +10,11 @@ import (
|
||||
// DateTimeTool provides precise current date/time with timezone support.
|
||||
// Models use this before creating cron jobs or any time-sensitive operation
|
||||
// instead of guessing timestamps from the system prompt's date-only field.
|
||||
type DateTimeTool struct{}
|
||||
type DateTimeTool struct {
|
||||
now func() time.Time
|
||||
}
|
||||
|
||||
func NewDateTimeTool() *DateTimeTool { return &DateTimeTool{} }
|
||||
func NewDateTimeTool() *DateTimeTool { return &DateTimeTool{now: time.Now} }
|
||||
|
||||
func (t *DateTimeTool) Name() string { return "datetime" }
|
||||
|
||||
@@ -20,7 +22,8 @@ func (t *DateTimeTool) Description() string {
|
||||
return `Get the current date and time. Use this when you need precise timestamps for scheduling (cron jobs), logging, or any time-sensitive operation.
|
||||
|
||||
Returns current time in both UTC and the requested timezone.
|
||||
If no timezone is provided, returns UTC only.`
|
||||
If no timezone is provided, returns UTC only.
|
||||
Calendar fields such as weekday and iso_weekday are computed by the server and are authoritative. Use them verbatim; never infer or recalculate the weekday from a date.`
|
||||
}
|
||||
|
||||
func (t *DateTimeTool) Parameters() map[string]any {
|
||||
@@ -37,9 +40,17 @@ func (t *DateTimeTool) Parameters() map[string]any {
|
||||
|
||||
func (t *DateTimeTool) Execute(_ context.Context, args map[string]any) *Result {
|
||||
now := time.Now()
|
||||
if t != nil && t.now != nil {
|
||||
now = t.now()
|
||||
}
|
||||
utc := now.UTC()
|
||||
result := map[string]any{
|
||||
"utc": now.UTC().Format(time.RFC3339),
|
||||
"unix_ms": now.UnixMilli(),
|
||||
"utc": utc.Format(time.RFC3339),
|
||||
"utc_date": utc.Format(time.DateOnly),
|
||||
"utc_time": utc.Format(time.TimeOnly),
|
||||
"utc_weekday": utc.Weekday().String(),
|
||||
"utc_iso_weekday": isoWeekday(utc),
|
||||
"unix_ms": now.UnixMilli(),
|
||||
}
|
||||
|
||||
if tz, ok := args["timezone"].(string); ok && tz != "" {
|
||||
@@ -47,10 +58,27 @@ func (t *DateTimeTool) Execute(_ context.Context, args map[string]any) *Result {
|
||||
if err != nil {
|
||||
return ErrorResult(fmt.Sprintf("invalid timezone '%s': use IANA names like 'Asia/Ho_Chi_Minh', 'America/New_York'", tz))
|
||||
}
|
||||
result["local"] = now.In(loc).Format(time.RFC3339)
|
||||
local := now.In(loc)
|
||||
result["local"] = local.Format(time.RFC3339)
|
||||
result["local_date"] = local.Format(time.DateOnly)
|
||||
result["local_time"] = local.Format(time.TimeOnly)
|
||||
result["weekday"] = local.Weekday().String()
|
||||
result["iso_weekday"] = isoWeekday(local)
|
||||
result["utc_offset"] = local.Format("Z07:00")
|
||||
result["timezone"] = tz
|
||||
}
|
||||
|
||||
data, _ := json.MarshalIndent(result, "", " ")
|
||||
data, err := json.MarshalIndent(result, "", " ")
|
||||
if err != nil {
|
||||
return ErrorResult(fmt.Sprintf("failed to encode datetime result: %v", err))
|
||||
}
|
||||
return NewResult(string(data))
|
||||
}
|
||||
|
||||
func isoWeekday(value time.Time) int {
|
||||
weekday := int(value.Weekday())
|
||||
if weekday == 0 {
|
||||
return 7
|
||||
}
|
||||
return weekday
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestDateTimeToolReturnsDerivedLocalCalendarFields(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tool := NewDateTimeTool()
|
||||
tool.now = func() time.Time {
|
||||
return time.Date(2026, time.July, 23, 1, 42, 19, 0, time.UTC)
|
||||
}
|
||||
result := tool.Execute(t.Context(), map[string]any{
|
||||
"timezone": "Asia/Ho_Chi_Minh",
|
||||
})
|
||||
if result.IsError {
|
||||
t.Fatalf("Execute() returned error: %s", result.ForLLM)
|
||||
}
|
||||
|
||||
var payload map[string]any
|
||||
if err := json.Unmarshal([]byte(result.ForLLM), &payload); err != nil {
|
||||
t.Fatalf("unmarshal result: %v", err)
|
||||
}
|
||||
|
||||
wants := map[string]any{
|
||||
"utc": "2026-07-23T01:42:19Z",
|
||||
"utc_date": "2026-07-23",
|
||||
"utc_time": "01:42:19",
|
||||
"utc_weekday": "Thursday",
|
||||
"utc_iso_weekday": float64(4),
|
||||
"unix_ms": float64(1784770939000),
|
||||
"local": "2026-07-23T08:42:19+07:00",
|
||||
"local_date": "2026-07-23",
|
||||
"local_time": "08:42:19",
|
||||
"weekday": "Thursday",
|
||||
"iso_weekday": float64(4),
|
||||
"utc_offset": "+07:00",
|
||||
"timezone": "Asia/Ho_Chi_Minh",
|
||||
}
|
||||
for key, want := range wants {
|
||||
if got := payload[key]; got != want {
|
||||
t.Errorf("%s = %#v, want %#v", key, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDateTimeToolReturnsUTCOnlyWithoutTimezone(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tool := NewDateTimeTool()
|
||||
tool.now = func() time.Time {
|
||||
return time.Date(2026, time.July, 26, 23, 59, 58, 0, time.UTC)
|
||||
}
|
||||
result := tool.Execute(t.Context(), nil)
|
||||
if result.IsError {
|
||||
t.Fatalf("Execute() returned error: %s", result.ForLLM)
|
||||
}
|
||||
|
||||
var payload map[string]any
|
||||
if err := json.Unmarshal([]byte(result.ForLLM), &payload); err != nil {
|
||||
t.Fatalf("unmarshal result: %v", err)
|
||||
}
|
||||
if got := payload["utc_weekday"]; got != "Sunday" {
|
||||
t.Errorf("utc_weekday = %#v, want %q", got, "Sunday")
|
||||
}
|
||||
if got := payload["utc_iso_weekday"]; got != float64(7) {
|
||||
t.Errorf("utc_iso_weekday = %#v, want 7", got)
|
||||
}
|
||||
for _, key := range []string{"local", "local_date", "local_time", "weekday", "iso_weekday", "utc_offset", "timezone"} {
|
||||
if _, ok := payload[key]; ok {
|
||||
t.Errorf("UTC-only result unexpectedly contains %q", key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDateTimeToolRejectsInvalidTimezone(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
result := NewDateTimeTool().Execute(t.Context(), map[string]any{
|
||||
"timezone": "UTC+7",
|
||||
})
|
||||
if !result.IsError {
|
||||
t.Fatal("Execute() accepted an invalid IANA timezone")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDateTimeToolExecuteSupportsZeroValueAndNilReceiver(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var nilTool *DateTimeTool
|
||||
for name, tool := range map[string]*DateTimeTool{
|
||||
"zero value": {},
|
||||
"nil receiver": nilTool,
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
result := tool.Execute(t.Context(), nil)
|
||||
if result.IsError {
|
||||
t.Fatalf("Execute() returned error: %s", result.ForLLM)
|
||||
}
|
||||
|
||||
var payload map[string]any
|
||||
if err := json.Unmarshal([]byte(result.ForLLM), &payload); err != nil {
|
||||
t.Fatalf("unmarshal result: %v", err)
|
||||
}
|
||||
for _, key := range []string{"utc", "utc_date", "utc_time", "utc_weekday", "utc_iso_weekday", "unix_ms"} {
|
||||
if payload[key] == nil {
|
||||
t.Errorf("result is missing %q", key)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user