orchard
mirrorEvery site I host, in one repo, along with the Cloudflare Tunnel and Caddy that front them. It's all Go, Vite, and SQLite, and it runs on a desktop at home with nothing listening on an inbound port.
blogbuncaddycloudflare-tunneldockergogolanghomelabhtml-templatemonorepoself-hostedseosqlitestatic-sitetypstuptime-monitoringviteweb-analytics
1package tools
2
3import (
4 "context"
5 "fmt"
6 "strings"
7)
8
9// Asked to remember something, the model had nothing to call. Memory was
10// written only by the pass that runs after a turn, which reads the exchange and
11// decides for itself what was worth keeping, so "remember that I want to watch
12// this" was a sentence it could agree with and not act on.
13//
14// This is the deliberate half. The pass still runs and still catches what Isaac
15// never thought to state, and this is for when he says it outright.
16
17// MemoryFact is one thing known about Isaac, thinned down to what a tool needs.
18type MemoryFact struct {
19 ID int64 `json:"id"`
20 Text string `json:"fact"`
21}
22
23// Memory is chat's own fact store. It is an interface because this package
24// cannot see the database the site owns, and it is nil in a test that does not
25// care, which the tool has to survive rather than panic on.
26type Memory interface {
27 Facts() ([]MemoryFact, error)
28 Add(text string) (int64, error)
29 Replace(id int64, text string) error
30 Delete(id int64) error
31}
32
33var Remember = Tool{
34 Name: "remember",
35 Description: "Write to or read what is remembered about Isaac between conversations. " +
36 "Call it whenever he says to remember, note, forget or correct something, " +
37 "and whenever he states a preference, a plan or a fact about himself that should outlast this chat. " +
38 "Saying you will remember without calling this does not remember anything.",
39 Schema: obj(map[string]any{
40 "action": map[string]any{"type": "string",
41 "description": "what to do with memory",
42 "enum": []string{"add", "replace", "forget", "list"}},
43 "fact": map[string]any{"type": "string",
44 "description": "for add and replace, the thing to keep, written as one plain sentence about Isaac"},
45 "id": map[string]any{"type": "integer",
46 "description": "for replace and forget, which remembered fact, from a list call"},
47 }, "action"),
48 Run: func(ctx context.Context, d *Deps, a map[string]any) (any, error) {
49 if d.Memory == nil {
50 return nil, fmt.Errorf("memory is not available in this turn")
51 }
52 // A mode that writes nothing down cannot be the one that teaches it
53 // something to write down later, which is the same rule the automatic
54 // pass follows. Reading is still fine, since that reveals nothing new.
55 action := strings.ToLower(argStr(a, "action"))
56 if d.Incognito && action != "list" {
57 return nil, fmt.Errorf("this is an incognito turn, so nothing is written down, " +
58 "including memory, and Isaac has to ask again outside incognito")
59 }
60
61 switch action {
62 case "list":
63 facts, err := d.Memory.Facts()
64 if err != nil {
65 return nil, err
66 }
67 return map[string]any{"facts": facts, "count": len(facts)}, nil
68
69 case "add":
70 text := argStr(a, "fact")
71 if text == "" {
72 return nil, fmt.Errorf("nothing to remember, pass the fact to keep")
73 }
74 id, err := d.Memory.Add(text)
75 if err != nil {
76 return nil, err
77 }
78 return map[string]any{"remembered": text, "id": id,
79 "note": "Say plainly that you have remembered it, in a line, and do not " +
80 "repeat the answer you gave before."}, nil
81
82 case "replace":
83 id := argInt64(a, "id")
84 text := argStr(a, "fact")
85 if id == 0 || text == "" {
86 return nil, fmt.Errorf("replace needs both the id from a list call and the new wording")
87 }
88 if err := d.Memory.Replace(id, text); err != nil {
89 return nil, err
90 }
91 return map[string]any{"replaced": id, "now": text}, nil
92
93 case "forget":
94 id := argInt64(a, "id")
95 if id == 0 {
96 return nil, fmt.Errorf("forget needs the id from a list call")
97 }
98 if err := d.Memory.Delete(id); err != nil {
99 return nil, err
100 }
101 return map[string]any{"forgot": id}, nil
102 }
103 return nil, fmt.Errorf("no action called %q, use add, replace, forget or list", action)
104 },
105}
106
107// The model sends an id as a number, a float or a string depending on how the
108// template rendered it, and a wrong type here reads as a missing id.
109func argInt64(a map[string]any, k string) int64 {
110 v, ok := a[k]
111 if !ok {
112 return 0
113 }
114 switch n := v.(type) {
115 case float64:
116 return int64(n)
117 case int64:
118 return n
119 case int:
120 return int64(n)
121 case string:
122 var out int64
123 if _, err := fmt.Sscan(strings.TrimSpace(n), &out); err != nil {
124 return 0
125 }
126 return out
127 }
128 return 0
129}