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

8.5 KB · 202 lines · Go Raw History
  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}