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 main
2
3import (
4 "context"
5 "encoding/json"
6 "regexp"
7 "strings"
8
9 "chat.bythewood.me/tools"
10)
11
12// Checking a draft that called no tools against the local Wikipedia.
13//
14// The gate asks whether every fact in a draft is supported by a tool result,
15// and when the model answered straight from memory there are no tool results,
16// so there is nothing for it to check and the draft goes out. That is how an
17// answer crediting the Goodyear welt to the tyre company rather than to Charles
18// Goodyear Jr. reached the page, which the article's own opening section gets
19// right.
20//
21// So when nothing was fetched, the subject of the question is looked up in the
22// offline snapshot and handed to the gate as background. It costs one call to a
23// container on the bridge and no model call, since the gate was going to run
24// anyway.
25
26// Only when the turn fetched nothing. A turn that called tools has results for
27// the gate to work with, and a question whose subject is not a thing an article
28// is named after gets nothing useful out of this.
29
30// The shapes a question opens with. Stripping one leaves the subject, which is
31// what the wikipedia tool takes, since it refuses a question outright.
32var questionHead = regexp.MustCompile(`(?i)^\s*(what|who|which|where|when|why|how)('?s| is| are| was| were| do| does| did)?\s+|^\s*(tell me about|explain|describe|define)\s+`)
33
34// Trailing filler left behind once the head is gone, as in "kubernetes for".
35var questionTail = regexp.MustCompile(`(?i)\s+((is|are|was|were)\s+(it|this|that|they|there)|for|about|like|used for|good for|mean|means|work|works)\s*[?.!]*\s*$`)
36
37// A trailing time phrase. It is stripped rather than used as a signal on its
38// own, because the question it is attached to may still name a real subject:
39// "what is the RTX 5090 going for today" is about the card.
40var timeTail = regexp.MustCompile(`(?i)\s+(right\s+now|now|today|tonight|yesterday|currently|lately|so\s+far|this\s+(morning|afternoon|evening|week|weekend|month|year)|last\s+(night|week|weekend|month|year))\s*[?.!]*\s*$`)
41
42// A subject runs to the first clause break. "a b-tree and why do databases use
43// them" is one question about one thing.
44var clauseBreak = regexp.MustCompile(`(?i)\s+(and|but|or|so|because|since|which|that|vs\.?|versus)\s+`)
45
46// Past this it is a sentence rather than the name of something, and looking it
47// up returns whatever happened to rank.
48const subjectMaxWords = 5
49
50// subjectOf pulls the thing a question is about out of it, or returns empty
51// when there is not one worth looking up.
52func subjectOf(question string) string {
53 s := strings.TrimSpace(question)
54 if s == "" {
55 return ""
56 }
57 s = questionHead.ReplaceAllString(s, "")
58 if loc := clauseBreak.FindStringIndex(s); loc != nil {
59 s = s[:loc[0]]
60 }
61 s = questionTail.ReplaceAllString(s, "")
62 // A time word on the end is never part of a name, and leaving it there hid
63 // the head noun: "Big news today" ended in "today" and so read as an
64 // ordinary subject.
65 s = timeTail.ReplaceAllString(s, "")
66 s = strings.Trim(s, " \t?.!,:;")
67 // Leading article, which is never part of a title.
68 s = regexp.MustCompile(`(?i)^(a|an|the)\s+`).ReplaceAllString(s, "")
69 s = strings.TrimSpace(s)
70 if s == "" || len(strings.Fields(s)) > subjectMaxWords {
71 return ""
72 }
73 // A bare question word survives the head pattern, which needs a word after
74 // it to strip anything, so "why" comes through as its own subject.
75 l := strings.ToLower(s)
76 if notASubject[l] || liveSubject[l] {
77 return ""
78 }
79 // The whole phrase is not always the giveaway. "Big news today" reduces to
80 // "Big news", which is in neither map, and the snapshot answered it with
81 // the founding date of Universe Today. The head noun is what the phrase is
82 // really about, and "News Corporation" still survives because its head is
83 // the corporation.
84 if f := strings.Fields(l); len(f) > 1 && liveSubject[f[len(f)-1]] {
85 return ""
86 }
87 return s
88}
89
90// Things the snapshot has an article about and can never answer a question
91// about, since what is being asked is today's value and not what the thing is.
92// "what's the weather like" reduces to "weather" and would otherwise spend a
93// lookup and a page of context on the meteorology article.
94var liveSubject = map[string]bool{
95 "weather": true, "forecast": true, "temperature": true, "time": true, "date": true,
96 "news": true, "score": true, "scores": true, "price": true, "prices": true,
97 "stock": true, "stocks": true, "market": true, "markets": true, "traffic": true,
98 "pollen": true, "aqi": true, "air quality": true, "exchange rate": true,
99 "headline": true, "headlines": true, "story": true, "stories": true,
100}
101
102var notASubject = map[string]bool{
103 "why": true, "how": true, "what": true, "who": true, "when": true, "where": true,
104 "which": true, "it": true, "that": true, "this": true, "them": true, "they": true,
105 // A message telling the assistant to do something is not a message about a
106 // thing. "remember that i want to watch this" reduced to "remember" and
107 // fetched the Wikipedia article on memory, which then sat in front of the
108 // model while it decided what the turn was about.
109 "remember": true, "forget": true, "note": true, "save": true, "keep": true,
110 "add": true, "update": true, "delete": true, "ignore": true,
111}
112
113// background looks the question's subject up in the offline snapshot and
114// returns a line for the gate, or empty when there is nothing to add. Anything
115// that goes wrong returns empty, since this only ever adds evidence and a
116// failure here must not change a verdict.
117func (e *Engine) background(ctx context.Context, question string) string {
118 subject := subjectOf(question)
119 if subject == "" {
120 return ""
121 }
122 args, err := json.Marshal(map[string]string{"query": subject})
123 if err != nil {
124 return ""
125 }
126 res := e.reg.Call(ctx, e.deps, tools.Wikipedia.Name, args)
127 if res.Err != "" {
128 return ""
129 }
130 m, ok := res.Content.(map[string]any)
131 if !ok {
132 return ""
133 }
134 if found, _ := m["found"].(bool); !found {
135 return ""
136 }
137 title, _ := m["title"].(string)
138 summary, _ := m["summary"].(string)
139 if strings.TrimSpace(summary) == "" {
140 return ""
141 }
142 date, _ := m["snapshot_date"].(string)
143 if date == "" {
144 date = "an unknown date"
145 }
146 // The age has to travel with the text. Without it a draft that is right
147 // about something recent looks wrong against an older article, and the gate
148 // sends a correct answer back to be broken.
149 return "wikipedia on " + title + ", from an offline snapshot taken " + date +
150 ", looked up here rather than by the model: " + trimLine(summary, 1500)
151}
152
153// opening looks the question's subject up before the model decides anything.
154//
155// The snapshot is local, so this costs a call on the bridge and no web request,
156// and it is current in a way the weights are not: the model had Fumio Kishida
157// as prime minister of Japan and the snapshot has Sanae Takaichi. Handing it
158// over first means the common question is answered from something checkable
159// rather than from training, and the model can still go further from there.
160//
161// It returns the result to record and the message to put in front of the model,
162// or a zero result when there is nothing worth adding.
163func (e *Engine) opening(ctx context.Context, question string) (tools.Result, Message, bool) {
164 subject := subjectOf(question)
165 if subject == "" {
166 return tools.Result{}, Message{}, false
167 }
168 args, err := json.Marshal(map[string]string{"query": subject})
169 if err != nil {
170 return tools.Result{}, Message{}, false
171 }
172 res := e.reg.Call(ctx, e.deps, tools.Wikipedia.Name, args)
173 if res.Err != "" {
174 return tools.Result{}, Message{}, false
175 }
176 m, ok := res.Content.(map[string]any)
177 if !ok {
178 return tools.Result{}, Message{}, false
179 }
180 if found, _ := m["found"].(bool); !found {
181 return tools.Result{}, Message{}, false
182 }
183 title, _ := m["title"].(string)
184 summary, _ := m["summary"].(string)
185 date, _ := m["snapshot_date"].(string)
186 if strings.TrimSpace(summary) == "" {
187 return tools.Result{}, Message{}, false
188 }
189 if date == "" {
190 date = "an unknown date"
191 }
192 msg := Message{Role: RoleUser, Content: "Before you answer, here is the opening section of the " +
193 title + " article from the offline Wikipedia on this machine, looked up for you. It was taken " +
194 date + ", so it is newer than your training and still older than today.\n\n" +
195 summary +
196 "\n\nUse it where it answers the question, and say what it says rather than what you remember, " +
197 "since your memory of a name, a date or who currently holds an office is the part most likely to " +
198 "be out of date. Call another tool if the question needs more than this covers, and use " +
199 "web_search for anything that could have changed since " + date + "."}
200 return res, msg, true
201}