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

2.8 KB · 90 lines · Go Raw History
 1package skills
 2
 3import (
 4	"context"
 5	neturl "net/url"
 6	"strings"
 7)
 8
 9// A pasted wall of links is not a question, and five pages is already more of
10// the context window than a summary can use well.
11const maxGivenURLs = 5
12
13// Page claims any question carrying a web address. The address is the
14// instruction: the reader has already found the page and wants that page read,
15// so planning a search off the words around it finds five other pages and
16// answers from those instead.
17//
18// It gathers rather than answers, which is what makes it different from every
19// other skill here. The addresses go back to the caller, which fetches them
20// through the same cache and the same browser headers a search result goes
21// through, and the answer is written and checked the usual way.
22type Page struct{}
23
24func (Page) Card() Card {
25	return Card{
26		Name: "page",
27		Does: "reads the web page whose address the question carries, and answers from that page and nothing else.",
28		Fires: []string{
29			"summary of this https://example.com/post",
30			"what does https://example.com/post say",
31			"tl;dr https://example.com/post",
32		},
33	}
34}
35
36// Claims is the whole routing decision, and it does not need the model. A
37// question either carries an address or it does not.
38func (Page) Claims(question string) bool { return len(URLsIn(question)) > 0 }
39
40func (Page) Run(ctx context.Context, question string, d Deps) (*Result, error) {
41	urls := URLsIn(question)
42	if len(urls) == 0 {
43		return nil, nil
44	}
45	return &Result{Skill: "page", Shape: "summary", URLs: urls}, nil
46}
47
48// URLsIn pulls the web addresses out of a question, in the order they were
49// written.
50func URLsIn(question string) []string {
51	var out []string
52	seen := map[string]bool{}
53	for _, f := range strings.FieldsFunc(question, func(r rune) bool {
54		return r == ' ' || r == '\t' || r == '\n' || r == '\r' || r == '<' || r == '>' || r == '"' || r == '\''
55	}) {
56		// A pasted address is often inside brackets, and one at the end of a
57		// sentence takes the punctuation with it.
58		f = strings.TrimLeft(f, "([{`*")
59		if !strings.HasPrefix(f, "http://") && !strings.HasPrefix(f, "https://") {
60			continue
61		}
62		f = strings.TrimRight(f, ".,;:!?`*")
63		for unbalanced(f) {
64			f = f[:len(f)-1]
65		}
66		u, err := neturl.Parse(f)
67		if err != nil || !strings.Contains(u.Host, ".") || seen[f] {
68			continue
69		}
70		seen[f] = true
71		out = append(out, f)
72		if len(out) == maxGivenURLs {
73			break
74		}
75	}
76	return out
77}
78
79// unbalanced says whether the address ends on a bracket it never opened, which
80// means the bracket belongs to the sentence around it. A Wikipedia title opens
81// one of its own, so the count is what decides rather than the last character.
82func unbalanced(u string) bool {
83	for _, p := range [][2]string{{"(", ")"}, {"[", "]"}, {"{", "}"}} {
84		if strings.HasSuffix(u, p[1]) && strings.Count(u, p[0]) < strings.Count(u, p[1]) {
85			return true
86		}
87	}
88	return false
89}