repos
/ orchard main

orchard

mirror

Every 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

4.2 KB · 130 lines · Go Raw History
  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}