// Package clock is an example skill giving any agent reliable time // awareness: a current-time tool (models cannot know "now") and a timezone // conversion tool, with an injectable clock for tests. package clock import ( "context" "encoding/json" "fmt" "time" "gitea.stevedudenhoeffer.com/steve/majordomo/llm" "gitea.stevedudenhoeffer.com/steve/majordomo/skill" ) // Option configures the skill. type Option func(*config) type config struct { now func() time.Time } // WithClock injects the time source (tests). func WithClock(now func() time.Time) Option { return func(c *config) { c.now = now } } // New builds the clock skill. func New(opts ...Option) *skill.Skill { cfg := config{now: time.Now} for _, opt := range opts { opt(&cfg) } nowTool := llm.Tool{ Name: "time_now", Description: "Get the current date and time. Use whenever the user's request depends on today's date or the current time.", Parameters: json.RawMessage(`{ "type": "object", "properties": { "timezone": {"type": "string", "description": "IANA timezone like America/New_York; defaults to UTC"} } }`), Handler: func(_ context.Context, args json.RawMessage) (any, error) { var p struct { Timezone string `json:"timezone"` } if err := json.Unmarshal(args, &p); err != nil { return nil, fmt.Errorf("bad arguments: %w", err) } loc := time.UTC if p.Timezone != "" { var err error if loc, err = time.LoadLocation(p.Timezone); err != nil { return nil, fmt.Errorf("unknown timezone %q", p.Timezone) } } t := cfg.now().In(loc) return map[string]string{ "rfc3339": t.Format(time.RFC3339), "weekday": t.Weekday().String(), "human": t.Format("Monday, January 2, 2006 at 15:04 MST"), }, nil }, } convertTool := llm.Tool{ Name: "time_convert", Description: "Convert an RFC3339 timestamp to another timezone.", Parameters: json.RawMessage(`{ "type": "object", "properties": { "time": {"type": "string", "description": "RFC3339 timestamp"}, "timezone": {"type": "string", "description": "target IANA timezone"} }, "required": ["time", "timezone"] }`), Handler: func(_ context.Context, args json.RawMessage) (any, error) { var p struct { Time string `json:"time"` Timezone string `json:"timezone"` } if err := json.Unmarshal(args, &p); err != nil { return nil, fmt.Errorf("bad arguments: %w", err) } t, err := time.Parse(time.RFC3339, p.Time) if err != nil { return nil, fmt.Errorf("unparseable time %q: %w", p.Time, err) } loc, err := time.LoadLocation(p.Timezone) if err != nil { return nil, fmt.Errorf("unknown timezone %q", p.Timezone) } return map[string]string{"rfc3339": t.In(loc).Format(time.RFC3339)}, nil }, } return skill.New("clock", skill.WithInstructions("You have time tools. Never guess the current date or time — call time_now. Use time_convert for timezone math."), skill.WithTools(nowTool, convertTool), ) }