X-Git-Url: https://git.giacomolli.uk/onechat/blobdiff_plain/e1e48ba388c28105fff3b70c8ca02304f5d57f53..39d1a3fd09f8255298590b4413115ae72e0e3928:/tools.go diff --git a/tools.go b/tools.go index dd28ae0..4c00350 100644 --- a/tools.go +++ b/tools.go @@ -49,15 +49,18 @@ type ToolChat interface { // the log as it happens: the call as a tool message, the result as an echo // message. The call is durable before its tool runs, so the log never misses an // action outside the agent. Commands and files run as the user in the working -// directory, with no sandbox. Zoom with a name reads the log of a subagent. -// The toolbox of the agent has the runs of its subagents for spawn and tell. -// The toolbox of a subagent has none, so spawn and tell are unknown tools +// directory, with no sandbox. Zoom with a name reads the log of a subagent. The +// toolbox of the agent has the runs of its subagents for spawn and tell, the +// WhatsApp link for the WhatsApp tools, and the schedules for the schedule +// tools. The toolbox of a subagent has none of them, so these are unknown tools // there. type Toolbox struct { log ToolLog chat ToolChat subagents *Subagents runs *SubagentRuns + whatsapp *WhatsApp + schedules *Schedules workdir string } @@ -68,12 +71,12 @@ type Toolbox struct { func (b *Toolbox) Call(ctx context.Context, name, arguments string) (string, error) { _, err := b.log.Append("tool", toolCallText(name, arguments)) if err != nil { - return "", err + return "", fmt.Errorf("append tool call: %w", err) } err = b.log.Flush(ctx) if err != nil && ctx.Err() == nil { - return "", err + return "", fmt.Errorf("flush tool call: %w", err) } output := "not run: the turn was interrupted" @@ -83,7 +86,7 @@ func (b *Toolbox) Call(ctx context.Context, name, arguments string) (string, err _, err = b.log.Append("echo", output) if err != nil { - return "", err + return "", fmt.Errorf("append tool result: %w", err) } return output, nil @@ -91,11 +94,15 @@ func (b *Toolbox) Call(ctx context.Context, name, arguments string) (string, err // run runs one tool and returns its output, or its error as text. func (b *Toolbox) run(ctx context.Context, name, arguments string) string { - var output string - var err error + var ( + output string + err error + ) + switch name { case "zoom": var args ToolZoomArguments + err = toolDecodeArguments(arguments, &args) switch { case err != nil: @@ -106,36 +113,42 @@ func (b *Toolbox) run(ctx context.Context, name, arguments string) string { } case "date": var args ToolDateArguments + err = toolDecodeArguments(arguments, &args) if err == nil { output, err = b.date(args.ID) } case "shell": var args ToolShellArguments + err = toolDecodeArguments(arguments, &args) if err == nil { output = b.shell(ctx, args.Command) } case "read": var args ToolPathArguments + err = toolDecodeArguments(arguments, &args) if err == nil { output, err = b.read(args.Path) } case "write": var args ToolWriteArguments + err = toolDecodeArguments(arguments, &args) if err == nil { output, err = b.write(args.Path, args.Text) } case "edit": var args ToolEditArguments + err = toolDecodeArguments(arguments, &args) if err == nil { output, err = b.edit(args.Path, args.Old, args.New) } case "spawn": var args ToolSpawnArguments + err = toolDecodeArguments(arguments, &args) switch { case b.runs == nil: @@ -145,6 +158,7 @@ func (b *Toolbox) run(ctx context.Context, name, arguments string) string { } case "tell": var args ToolTellArguments + err = toolDecodeArguments(arguments, &args) switch { case b.runs == nil: @@ -152,6 +166,54 @@ func (b *Toolbox) run(ctx context.Context, name, arguments string) string { case err == nil: output, err = b.runs.Tell(ctx, args.Name, args.Message) } + case "whatsapp_find": + var args ToolWhatsAppFindArguments + + err = toolDecodeArguments(arguments, &args) + switch { + case b.whatsapp == nil: + err = fmt.Errorf("unknown tool %q", name) + case err == nil: + output, err = b.whatsapp.Find(ctx, args.Name) + } + case "whatsapp_send": + var args ToolWhatsAppSendArguments + + err = toolDecodeArguments(arguments, &args) + switch { + case b.whatsapp == nil: + err = fmt.Errorf("unknown tool %q", name) + case err == nil: + output, err = b.whatsapp.Send(ctx, args.ID, args.Chat, args.Text) + } + case "schedule_set": + var args ToolScheduleSetArguments + + err = toolDecodeArguments(arguments, &args) + switch { + case b.schedules == nil: + err = fmt.Errorf("unknown tool %q", name) + case err == nil: + output, err = b.schedules.Set(args.Text, args.At, args.Cron) + } + case "schedule_list": + err = toolDecodeArguments(arguments, &struct{}{}) + switch { + case b.schedules == nil: + err = fmt.Errorf("unknown tool %q", name) + case err == nil: + output = b.schedules.List() + } + case "schedule_remove": + var args ToolScheduleRemoveArguments + + err = toolDecodeArguments(arguments, &args) + switch { + case b.schedules == nil: + err = fmt.Errorf("unknown tool %q", name) + case err == nil: + output, err = b.schedules.Remove(args.ID) + } default: err = fmt.Errorf("unknown tool %q", name) } @@ -187,18 +249,20 @@ func (b *Toolbox) zoom(id, n int) (string, error) { if n == 1 { message, err := b.chat.Message(id) if err != nil { - return "", err + return "", fmt.Errorf("read zoom message: %w", err) } return leafSource(message), nil } level := bits.TrailingZeros(uint(n)) - 1 + var lines []string + for _, index := range []int{2 * id / n, 2*id/n + 1} { node, built, err := b.chat.Node(level, index) if err != nil { - return "", err + return "", fmt.Errorf("read zoom node: %w", err) } text := viewPending @@ -220,12 +284,12 @@ func (b *Toolbox) date(id int) (string, error) { message, err := b.chat.Message(id) if err != nil { - return "", err + return "", fmt.Errorf("read dated message: %w", err) } date, err := time.Parse(time.RFC3339Nano, message.Date) if err != nil { - return "", err + return "", fmt.Errorf("parse message date: %w", err) } return date.Local().Format("Monday 2006-01-02 15:04:05 -07:00"), nil @@ -251,7 +315,9 @@ func (b *Toolbox) shell(ctx context.Context, command string) string { err := cmd.Run() text := output.String() + var exit *exec.ExitError + switch { case ctx.Err() != nil: text += "\n[interrupted]" @@ -270,7 +336,7 @@ func (b *Toolbox) shell(ctx context.Context, command string) string { func (b *Toolbox) read(path string) (text string, err error) { file, err := os.Open(b.path(path)) if err != nil { - return "", err + return "", fmt.Errorf("open tool file: %w", err) } defer func() { @@ -280,7 +346,7 @@ func (b *Toolbox) read(path string) (text string, err error) { output := &ToolHeadTail{} _, err = io.Copy(output, file) if err != nil { - return "", err + return "", fmt.Errorf("read tool file: %w", err) } return output.String(), nil @@ -292,12 +358,12 @@ func (b *Toolbox) write(path, text string) (string, error) { path = b.path(path) err := os.MkdirAll(filepath.Dir(path), 0o755) if err != nil { - return "", err + return "", fmt.Errorf("create tool file directory: %w", err) } err = os.WriteFile(path, []byte(text), 0o644) if err != nil { - return "", err + return "", fmt.Errorf("write tool file: %w", err) } return fmt.Sprintf("wrote %d bytes to %s", len(text), path), nil @@ -309,7 +375,7 @@ func (b *Toolbox) edit(path, old, replacement string) (string, error) { path = b.path(path) data, err := os.ReadFile(path) if err != nil { - return "", err + return "", fmt.Errorf("read file to edit: %w", err) } if old == "" { @@ -323,12 +389,12 @@ func (b *Toolbox) edit(path, old, replacement string) (string, error) { info, err := os.Stat(path) if err != nil { - return "", err + return "", fmt.Errorf("read file mode: %w", err) } err = os.WriteFile(path, []byte(strings.Replace(string(data), old, replacement, 1)), info.Mode().Perm()) if err != nil { - return "", err + return "", fmt.Errorf("write edited file: %w", err) } return "edited " + path, nil @@ -393,6 +459,31 @@ type ToolTellArguments struct { Message string `json:"message"` } +// ToolWhatsAppFindArguments are the arguments of whatsapp_find. +type ToolWhatsAppFindArguments struct { + Name string `json:"name"` +} + +// ToolWhatsAppSendArguments are the arguments of whatsapp_send. +type ToolWhatsAppSendArguments struct { + ID string `json:"id"` + Chat string `json:"chat"` + Text string `json:"text"` +} + +// ToolScheduleSetArguments are the arguments of schedule_set. Exactly one of At +// and Cron is not nil. +type ToolScheduleSetArguments struct { + Text string `json:"text"` + At *string `json:"at"` + Cron *string `json:"cron"` +} + +// ToolScheduleRemoveArguments are the arguments of schedule_remove. +type ToolScheduleRemoveArguments struct { + ID int `json:"id"` +} + // ToolHeadTail keeps the first and the last toolClipBytes bytes written to it, // and counts the bytes between them that it drops. type ToolHeadTail struct { @@ -458,6 +549,7 @@ func toolClip(text string) string { func toolDecodeArguments(arguments string, args any) error { decoder := json.NewDecoder(strings.NewReader(arguments)) decoder.DisallowUnknownFields() + err := decoder.Decode(args) if err != nil { return fmt.Errorf("arguments: %w", err)