89f3334512
Grow executus/tools into a real generic tool library: - Register(reg): the always-available, zero-config tools — think, now (UTC unless a CurrentTimeProvider is wired), cite (inert unless a CitationStorage is wired). All nil-safe; a light host calls Register and is useful. - RegisterMeta(reg, MetaDeps): the LLM-backed meta tools — classify, extract_entities, summarize — over the llmmeta helper. Budget defaults to the shipped in-memory per-run cap; Files optional; caps default. - Seams moved (interface/type-only, no host coupling): research_providers.go (CurrentTimeProvider/CitationStorage/SearchBudget/PageExtractor/PDFFetcher/…) and file_storage.go (FileStorage + FileDomainMeta). Plus the in-memory budget default (research_defaults.go) and scope_validate.go. calculate deferred (drags github.com/Krognol/go-wolfram + a module-path replace — not worth it in the lean core for one tool). Core go.sum still free of gorm/redis/discordgo/sqlite/wolfram. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
102 lines
3.6 KiB
Go
102 lines
3.6 KiB
Go
package tools
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"gitea.stevedudenhoeffer.com/steve/executus/tool"
|
|
)
|
|
|
|
// nowParams is the LLM-facing param struct for current_time / now.
|
|
//
|
|
// Why optional `timezone`: most agent prompts know the user's local
|
|
// timezone (it's in the chatbot's system prompt) but the agent has
|
|
// no way to override on a per-call basis. An explicit arg lets a
|
|
// research skill ask "what time is it in NYC for the user reading
|
|
// this report?" without needing access to a member-config lookup
|
|
// tool.
|
|
type nowParams struct {
|
|
Timezone string `json:"timezone,omitempty" description:"Optional IANA timezone name (e.g. 'America/Chicago', 'Europe/London'). Defaults to the calling user's configured timezone, falling back to UTC."`
|
|
}
|
|
|
|
// nowResponse is the JSON envelope returned to the agent.
|
|
//
|
|
// Why a structured shape: the v1 tool returned a markdown blob.
|
|
// Agents that needed just the year had to substring-parse, which
|
|
// fails on locale variations. JSON lets the agent pick the field
|
|
// it cares about.
|
|
type nowResponse struct {
|
|
NowISO string `json:"now_iso"`
|
|
NowHuman string `json:"now_human"`
|
|
Timezone string `json:"timezone"`
|
|
Weekday string `json:"weekday"`
|
|
Year int `json:"year"`
|
|
Month int `json:"month"`
|
|
Day int `json:"day"`
|
|
Hour int `json:"hour"`
|
|
Minute int `json:"minute"`
|
|
Second int `json:"second"`
|
|
Warning string `json:"warning,omitempty"`
|
|
}
|
|
|
|
// NewNow constructs the v11 current_time / now tool. The provider
|
|
// supplies the calling member's configured timezone (per-user
|
|
// localisation). nil falls back to UTC.
|
|
//
|
|
// V11 keeps the registered tool name "now" for back-compat with the
|
|
// existing tool catalog tests AND adds the same tool surface under
|
|
// the agent-facing description "current time". The design spec
|
|
// called the tool "current_time" but the v1 registry already used
|
|
// "now" — switching the registry name would break stored skills'
|
|
// `tools` lists. Same name, expanded behaviour.
|
|
func NewNow(provider CurrentTimeProvider) tool.Tool {
|
|
return tool.NewGatedTool[nowParams](
|
|
"now",
|
|
"Return the current time. Optional 'timezone' (IANA name e.g. 'America/Chicago'); defaults to the calling user's configured timezone or UTC. Returns ISO + human-readable formats plus structured year/month/day/weekday for time-relative reasoning. Use this BEFORE assuming a year — the agent's knowledge cut-off may differ from real time.",
|
|
tool.Permission{
|
|
AuthoringRequirement: tool.RequirementAnyone,
|
|
OperatesOn: tool.ScopeGlobal,
|
|
SafeForShare: true,
|
|
Categories: []string{"utility"},
|
|
},
|
|
func(ctx context.Context, inv tool.Invocation, p nowParams) (string, error) {
|
|
tzName := strings.TrimSpace(p.Timezone)
|
|
warning := ""
|
|
if tzName == "" && provider != nil {
|
|
tzName = provider.UserTimezone(ctx, inv.CallerID)
|
|
}
|
|
if tzName == "" {
|
|
tzName = "UTC"
|
|
}
|
|
loc, err := time.LoadLocation(tzName)
|
|
if err != nil {
|
|
warning = fmt.Sprintf("unknown timezone %q; falling back to UTC", tzName)
|
|
tzName = "UTC"
|
|
loc = time.UTC
|
|
}
|
|
t := time.Now().In(loc)
|
|
out := nowResponse{
|
|
NowISO: t.Format(time.RFC3339),
|
|
NowHuman: t.Format("Monday, January 2, 2006 at 3:04 PM MST"),
|
|
Timezone: tzName,
|
|
Weekday: t.Weekday().String(),
|
|
Year: t.Year(),
|
|
Month: int(t.Month()),
|
|
Day: t.Day(),
|
|
Hour: t.Hour(),
|
|
Minute: t.Minute(),
|
|
Second: t.Second(),
|
|
Warning: warning,
|
|
}
|
|
b, mErr := json.Marshal(out)
|
|
if mErr != nil {
|
|
return "", fmt.Errorf("now: marshal: %w", mErr)
|
|
}
|
|
return string(b), nil
|
|
},
|
|
)
|
|
}
|