on master help sign in
diagonal/host/prompts.py
114 lines 4.8 kB history raw
1"""Section 9 prompt templates. Pure functions: payload in, Prompt out.
3A Prompt has fixed `instructions` (passed to `fm respond -i`, so it may show in the process list and must hold
4no tab or group text) and a `text` with everything from the browser (sent on stdin). Keeping the rules in
5the instructions also makes it harder for a tab title to override them."""
6from collections import namedtuple
7from urllib.parse import urlsplit
9from validate import COLORS, all_emoji
11EMOJI_LIST = " ".join(all_emoji())
12STRICT_LINE = "Reply with only the object, nothing before or after it."
14Prompt = namedtuple("Prompt", "instructions text")
17def size(p):
18 return len(p.instructions) + len(p.text)
21def _clean(s, limit):
22 s = " ".join(str(s or "").split())
23 return s.replace("|", "/")[:limit]
26def render_address(url):
27 """host without www. + path; the query is kept as the extension sent it (it already dropped tracking keys)."""
28 try:
29 u = urlsplit(str(url))
30 except ValueError:
31 return _clean(url, 160)
32 host = (u.hostname or "").removeprefix("www.")
33 if not host:
34 return _clean(url, 160)
35 path = "" if u.path in ("", "/") else u.path
36 query = f"?{u.query}" if u.query else ""
37 return _clean(host + path + query, 160)
40def render_items(items, desc_cap=500):
41 lines = []
42 for it in items:
43 desc = _clean(it.get("description"), desc_cap) or "-"
44 lines.append(f"{it['i']} | {_clean(it.get('title'), 120)} | {render_address(it.get('url', ''))} | {desc}")
45 return "\n".join(lines)
48def _quoted(titles):
49 return ", ".join(f'"{_clean(t, 60)}"' for t in titles)
52def build_name_prompt(payload, strict=False, desc_cap=500):
53 rules = [
54 "- Title: 2 to 4 words, sentence case, no punctuation at the end, no quotes.",
55 "- Describe the topic or task, not the website, unless every tab is the same site.",
56 "- Never use the words tab, tabs, group, misc, various, stuff.",
57 "- Write the title in the language most tab titles use.",
58 f"- Emoji: exactly one, chosen from this list: {EMOJI_LIST}",
59 "- If a current title is given, keep it exactly if it still fits; change it only if the tabs have clearly moved on.",
60 "- Never reuse a name listed under other groups or not allowed.",
61 ]
62 if strict:
63 rules.append(f"- {STRICT_LINE}")
64 instructions = (
65 "You name a browser tab group. Reply with a title and one emoji that describe what these tabs have in common.\n\n"
66 "Rules:\n" + "\n".join(rules)
67 )
68 context = []
69 current = payload.get("currentTitle")
70 if current:
71 context.append(f'Current title: "{_clean(current, 60)}"')
72 siblings = payload.get("siblingTitles") or []
73 if siblings:
74 context.append(f"Other groups in this window: {_quoted(siblings)}")
75 differ = payload.get("mustDifferFrom") or []
76 if differ:
77 context.append(f"Not allowed: {_quoted(differ)}")
78 text = ("\n".join(context) + "\n\n" if context else "") + "Tabs (index | title | address | what the page says):\n" + render_items(payload["items"], desc_cap) + "\n"
79 return Prompt(instructions, text)
82# Organize asks for one broad topic per tab and the host groups tabs with the same topic. On fm (macOS 27)
83# this was reliable where asking the model for whole groups was not: it set "existing" on nearly every
84# new group, and specific topics gave every tab its own.
85TOPICS_INSTRUCTIONS = (
86 "You label browser tabs with broad topics so that related tabs can be grouped.\n\n"
87 "Rules:\n"
88 "- Give every listed index exactly one topic of one or two words.\n"
89 "- Keep topics as general as possible, for example Programming, Travel, Cooking, Shopping, News, Finance, Music, Health, Work.\n"
90 "- Use exactly the same topic words for tabs that belong together.\n"
91 "- Group by what the page is about, not by website.\n"
92 "- Write topics in English."
96def topic_items(payload, desc_cap=500):
97 """The tabs to sort, then up to two example tabs of each existing group, numbered after them.
98 Returns (lines, owner) where owner maps an example's index to its group number."""
99 items = payload["items"]
100 lines = [render_items(items, desc_cap)] if items else []
101 owner = {}
102 n = len(items)
103 for g in payload.get("existingGroups") or []:
104 for sample in (g.get("samples") or [])[:2]:
105 owner[n] = g["g"]
106 lines.append(f"{n} | {_clean(sample, 120)} | - | -")
107 n += 1
108 return "\n".join(lines), owner
111def build_topics_prompt(payload, strict=False, desc_cap=500):
112 instructions = TOPICS_INSTRUCTIONS + (f"\n- {STRICT_LINE}" if strict else "")
113 body, _ = topic_items(payload, desc_cap)
114 return Prompt(instructions, "Tabs (index | title | address | what the page says):\n" + body + "\n")