tensorcode 0.1.0a1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. tensorcode/__init__.py +84 -0
  2. tensorcode/actions.py +137 -0
  3. tensorcode/answer_type.py +222 -0
  4. tensorcode/awareness.py +344 -0
  5. tensorcode/backends/__init__.py +0 -0
  6. tensorcode/backends/builtin.py +167 -0
  7. tensorcode/backends/hf_local.py +89 -0
  8. tensorcode/backends/linear.py +133 -0
  9. tensorcode/backends/neural.py +361 -0
  10. tensorcode/causal.py +262 -0
  11. tensorcode/change.py +566 -0
  12. tensorcode/chunking.py +195 -0
  13. tensorcode/cognition.py +311 -0
  14. tensorcode/context.py +97 -0
  15. tensorcode/control.py +291 -0
  16. tensorcode/cues.py +192 -0
  17. tensorcode/expectation.py +270 -0
  18. tensorcode/frames.py +232 -0
  19. tensorcode/language/__init__.py +36 -0
  20. tensorcode/language/chart.py +558 -0
  21. tensorcode/language/discourse.py +132 -0
  22. tensorcode/language/domains/__init__.py +0 -0
  23. tensorcode/language/domains/desktop.py +552 -0
  24. tensorcode/language/english.py +459 -0
  25. tensorcode/language/features.py +112 -0
  26. tensorcode/language/generate.py +574 -0
  27. tensorcode/language/grammar.py +893 -0
  28. tensorcode/language/semantics.py +349 -0
  29. tensorcode/learning/__init__.py +30 -0
  30. tensorcode/learning/certificate.py +148 -0
  31. tensorcode/learning/induce.py +304 -0
  32. tensorcode/learning/library.py +217 -0
  33. tensorcode/learning/literals.py +126 -0
  34. tensorcode/learning/verify.py +253 -0
  35. tensorcode/memory.py +303 -0
  36. tensorcode/metacognition.py +351 -0
  37. tensorcode/ops.py +207 -0
  38. tensorcode/outcomes.py +99 -0
  39. tensorcode/permanence.py +376 -0
  40. tensorcode/priming.py +191 -0
  41. tensorcode/py.typed +0 -0
  42. tensorcode/quantity.py +311 -0
  43. tensorcode/records.py +728 -0
  44. tensorcode/relation.py +771 -0
  45. tensorcode/runtime.py +471 -0
  46. tensorcode/semantics_bridge.py +308 -0
  47. tensorcode/social.py +380 -0
  48. tensorcode/temporal.py +189 -0
  49. tensorcode/wants.py +185 -0
  50. tensorcode-0.1.0a1.dist-info/METADATA +196 -0
  51. tensorcode-0.1.0a1.dist-info/RECORD +53 -0
  52. tensorcode-0.1.0a1.dist-info/WHEEL +4 -0
  53. tensorcode-0.1.0a1.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,552 @@
1
+ """The assistant's domain: the words a desktop agent needs, and the acts they map to.
2
+
3
+ This exists to be measured. ``examples/browser_agents/assistant/language.py`` reads
4
+ the same requests with ~105 regexes; this reads them with the core English grammar
5
+ plus the vocabulary below, and :func:`acts` projects the resulting frames onto the
6
+ same ``(act, slots)`` vocabulary so the two can be compared on the same benchmark.
7
+
8
+ The projection is deliberately thin: it renames roles and resolves place words to
9
+ paths. Everything structural — which verb, what it acts on, where, with what text,
10
+ and whether the utterance was an order, a question or a report — comes from the
11
+ grammar.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from dataclasses import dataclass
17
+ from typing import Any, Iterable, Mapping, Sequence
18
+
19
+ from ..chart import Understanding, understand
20
+ from ..english import ENGLISH
21
+ from ..grammar import Ask, Build, Ent, Grammar, Head, Lit, Locative, Order, Qualify, production, words
22
+ from ..semantics import Entity, Frame, Question, Request
23
+
24
+ #: Place words and the paths they name.
25
+ PLACES: Mapping[str, str] = {
26
+ "desktop": "~/Desktop", "documents": "~/Documents", "docs": "~/Documents", "downloads": "~/Downloads",
27
+ "home": "~", "projects": "~/Projects", "pictures": "~/Pictures", "music": "~/Music", "videos": "~/Videos",
28
+ "tmp": "/tmp", "temp": "/tmp",
29
+ }
30
+
31
+ #: Dock applications, by the words people use for them.
32
+ APPS: Mapping[str, str] = {
33
+ "firefox": "Firefox", "browser": "Firefox", "chromium": "Chromium", "chrome": "Chromium",
34
+ "files": "Files", "nautilus": "Files", "terminal": "Terminal", "shell": "Terminal",
35
+ "editor": "Text Editor", "gedit": "Text Editor", "vscode": "Visual Studio Code",
36
+ "slack": "Slack", "mail": "Mail", "email": "Mail", "settings": "Settings", "wireshark": "Wireshark",
37
+ "rhythmbox": "Rhythmbox",
38
+ }
39
+
40
+ VERBS = (
41
+ *words("make", "create", cat="V", sem="make"),
42
+ *words("mkdir", cat="V", sem="make_folder"),
43
+ *words("touch", cat="V", sem="make_file"),
44
+ *words("add", "put", "place", cat="V", sem="add"),
45
+ *words("delete", "remove", "trash", "erase", "rm", cat="V", sem="delete"),
46
+ *words("read", "cat", "print", "display", "view", cat="V", sem="read"),
47
+ *words("show", "tell", cat="V", sem="show"),
48
+ *words("list", "ls", cat="V", sem="list"),
49
+ *words("write", "save", "type", cat="V", sem="write"),
50
+ *words("append", cat="V", sem="append"),
51
+ *words("move", cat="V", sem="move"),
52
+ *words("mv", cat="V", sem="move", ditrans=True),
53
+ *words("copy", "duplicate", cat="V", sem="copy"),
54
+ *words("cp", cat="V", sem="copy", ditrans=True),
55
+ *words("mention", "contain", cat="V", sem="mention"),
56
+ *words("rename", cat="V", sem="rename"),
57
+ *words("find", "locate", cat="V", sem="find"),
58
+ *words("search", "look", "grep", cat="V", sem="search"),
59
+ *words("count", cat="V", sem="count"),
60
+ *words("install", cat="V", sem="install"),
61
+ *words("run", "execute", "exec", cat="V", sem="run"),
62
+ *words("open", cat="V", sem="open"),
63
+ *words("launch", "start", cat="V", sem="launch"),
64
+ *words("go", "cd", "switch", "change", cat="V", sem="go"),
65
+ *words("commit", cat="V", sem="commit"),
66
+ *words("init", "initialize", "initialise", cat="V", sem="init"),
67
+ *words("clean", "organize", "organise", "tidy", "fix", cat="V", sem="tidy"), # known words, no act: honestly unknown
68
+ )
69
+
70
+ NOUNS = (
71
+ *words("folder", "directory", "dir", cat="N", sem="folder"),
72
+ *words("file", "note", "document", "doc", cat="N", sem="file"),
73
+ *words("readme", cat="N", sem="readme"),
74
+ *words("repo", "repository", cat="N", sem="repo"),
75
+ *words("project", cat="N", sem="project"),
76
+ *words("git", cat="N", sem="git"),
77
+ *words("line", cat="N", sem="line"),
78
+ *words("word", cat="N", sem="word"),
79
+ *words("item", "thing", "stuff", cat="N", sem="item"),
80
+ *words("space", cat="N", sem="space"),
81
+ *words("disk", "drive", "storage", cat="N", sem="disk"),
82
+ *words("core", "cpu", "processor", cat="N", sem="cpu"),
83
+ *words("process", "program", cat="N", sem="process"),
84
+ *words("hostname", cat="N", sem="hostname"),
85
+ *words("ip", cat="N", sem="ip"),
86
+ *words("address", cat="N", sem="address"),
87
+ *words("time", "date", "day", cat="N", sem="time"),
88
+ *words("uptime", cat="N", sem="uptime"),
89
+ *words("history", cat="N", sem="history"),
90
+ *words("message", cat="N", sem="message"),
91
+ *words("content", "contents", cat="N", sem="content"),
92
+ *words("pdf", cat="N", sem="pdf"),
93
+ *words("text", cat="N", sem="text"),
94
+ *words("system", cat="N", sem="system"),
95
+ *words("user", "username", cat="N", sem="user"),
96
+ *words("app", "application", cat="N", sem="app"),
97
+ *words("command", cat="N", sem="command"),
98
+ *words("change", cat="N", sem="change"),
99
+ *words("weather", "joke", "sandwich", "wifi", "test", cat="N", sem="offtopic"), # in the lexicon, out of scope
100
+ *[e for word, path in PLACES.items() for e in words(word, cat="N", sem=f"place:{path}", place=True)],
101
+ # "files", "mail" and "editor" are also ordinary nouns, so the app reading is
102
+ # dispreferred and only wins when a launching verb selects it
103
+ *[e for word, app in APPS.items() for e in words(word, cat="N", sem=f"app:{app}", weight=-0.5, app=True)],
104
+ *words("files", cat="N", sem="file", number="plural"),
105
+ )
106
+
107
+ ADJECTIVES = (
108
+ *words("installed", cat="Adj", sem="installed"),
109
+ *words("empty", cat="Adj", sem="empty"),
110
+ *words("big", "large", cat="Adj", sem="big"),
111
+ *words("new", cat="Adj", sem="new"),
112
+ *words("old", cat="Adj", sem="old"),
113
+ *words("many", cat="Adj", sem="many"),
114
+ *words("much", cat="Adj", sem="much"),
115
+ *words("running", cat="Adj", sem="running"),
116
+ *words("uncommitted", cat="Adj", sem="uncommitted"),
117
+ *words("git", cat="Adj", sem="git"),
118
+ )
119
+
120
+ #: Dialogue moves. They are utterances in their own right, not clauses.
121
+ SPELLINGS = (
122
+ *words("whats", "what", cat="Whats", sem="theme"),
123
+ *words("pwd", cat="Q", sem="cwd"),
124
+ *words("uptime", cat="Q", sem="uptime"),
125
+ )
126
+
127
+ DIALOGUE = (
128
+ *words("hi", "hello", "hey", "yo", cat="Move", sem="greet"),
129
+ *words("thanks", "thanx", "thx", "ty", cat="Move", sem="thanks"),
130
+ *words("yes", "yeah", "yep", "sure", "ok", "okay", cat="Move", sem="confirm"),
131
+ *words("no", "nope", "nah", cat="Move", sem="cancel"),
132
+ *words("help", cat="Move", sem="help"),
133
+ )
134
+
135
+ #: Multiword items outrank the split reading: "how many" is not "how" + "many".
136
+ MULTIWORD = [
137
+ production('N[app=true] -> "text" "editor"', Lit("app:Text Editor"), weight=0.4),
138
+ production('N[app=true] -> "vs" "code"', Lit("app:Visual Studio Code"), weight=0.4),
139
+ production('N[app=true] -> "file" "manager"', Lit("app:Files"), weight=0.4),
140
+ production('N[app=true] -> "system" "monitor"', Lit("app:System Monitor"), weight=0.4),
141
+ production('N[app=true] -> "app" "center"', Lit("app:App Center"), weight=0.4),
142
+ production('N[place=true] -> "home" "folder"', Lit("place:~"), weight=0.4),
143
+ production('N[place=true] -> "home" "directory"', Lit("place:~"), weight=0.4),
144
+ production('Move -> "thank" "you"', Lit("thanks"), weight=0.4),
145
+ production('Move -> "never" "mind"', Lit("cancel"), weight=0.4),
146
+ production('Move -> "go" "ahead"', Lit("confirm"), weight=0.4),
147
+ production('Move -> "yes" "please"', Lit("confirm"), weight=0.4),
148
+ production('Wh -> "how" "many"', Lit("count"), weight=0.4),
149
+ production('Wh -> "how" "much"', Lit("amount"), weight=0.4),
150
+ production('Wh -> "how" "big"', Lit("size"), weight=0.4),
151
+ production('Wh -> "how" "large"', Lit("size"), weight=0.4),
152
+ production('Wh -> "what" "operating" "system"', Lit("os"), weight=0.4),
153
+ production('U -> Move', Head(0), weight=0.4),
154
+ # spellings and idioms people actually type
155
+ production('V -> "get" "rid" "of"', Lit("delete"), weight=0.4),
156
+ # a verbless request: "new folder photos"
157
+ production('IMP -> "new" NBAR', Order(Build(predicate="make", roles=(("object", 1),))), weight=0.3),
158
+ production('Q -> Whats PP', Ask(Locative("located", modifier=1), asked="theme"), weight=0.4),
159
+ production('Q -> Whats NP', Ask(Build(predicate="be", roles=(("subject", 1),)), asked="theme"), weight=0.4),
160
+ production('Q -> Wh Aux NP V', Ask(Build(predicate_from=3, roles=(("subject", 2),)), asked_from=0), weight=0.2),
161
+ ]
162
+
163
+ DESKTOP: Grammar = ENGLISH.extend(
164
+ productions=MULTIWORD,
165
+ entries=[*VERBS, *NOUNS, *ADJECTIVES, *DIALOGUE, *SPELLINGS],
166
+ start=("S", "Q", "IMP", "U", "NP"),
167
+ name="english+desktop",
168
+ )
169
+
170
+
171
+ # ------------------------------------------------------------------ projection
172
+
173
+
174
+ @dataclass(frozen=True)
175
+ class Act:
176
+ """The assistant's request shape: an act name and its slots."""
177
+
178
+ act: str
179
+ slots: Mapping[str, Any]
180
+
181
+ def __repr__(self) -> str:
182
+ return f"{self.act}({', '.join(f'{k}={v!r}' for k, v in sorted(self.slots.items()))})"
183
+
184
+
185
+ #: wh-word or noun -> the machine question it asks
186
+ INFO = {
187
+ "time": "date", "user": "user", "disk": "disk", "space": "disk", "ip": "ip", "address": "ip",
188
+ "hostname": "hostname", "uptime": "uptime", "process": "processes", "cpu": "cpus", "os": "os",
189
+ }
190
+
191
+
192
+ def _speaks_of_self(value: Any) -> bool:
193
+ """"me", "us", "you", "i" name the people talking, never the file or folder."""
194
+ return isinstance(value, Entity) and value.kind == "pronoun" and value.features.get("person") in (1, 2)
195
+
196
+
197
+ def _refers_back(value: Any) -> bool:
198
+ """A third-person pronoun or a demonstrative: the utterance really did point at something."""
199
+ if not isinstance(value, Entity):
200
+ return False
201
+ return (value.kind == "pronoun" and value.features.get("person") not in (1, 2)) or bool(value.features.get("demonstrative"))
202
+
203
+
204
+ def _place(value: Any) -> str | None:
205
+ """A path for an entity, if it names one: a literal path, a place word, or a name."""
206
+ if not isinstance(value, Entity) or _speaks_of_self(value):
207
+ return None
208
+ if value.kind == "path":
209
+ return value.text
210
+ noun = value.features.get("noun")
211
+ if isinstance(noun, str) and noun.startswith("place:"):
212
+ return noun.split(":", 1)[1]
213
+ if value.kind in ("name", "literal", "resolved"):
214
+ return value.text
215
+ if value.kind == "pronoun":
216
+ return "@it"
217
+ return None
218
+
219
+
220
+ def _target(value: Any) -> str | None:
221
+ """What an act operates on: a path, a name, or the reference "@it"."""
222
+ if isinstance(value, tuple):
223
+ return _target(value[0]) if value else None
224
+ if not isinstance(value, Entity) or _speaks_of_self(value):
225
+ return None
226
+ if value.kind == "pronoun" or value.features.get("demonstrative"):
227
+ return "@it"
228
+ if value.kind in ("path", "literal", "name"):
229
+ return value.text
230
+ if value.features.get("name"):
231
+ return _text(value.features["name"])
232
+ place = _place(value)
233
+ if place:
234
+ return place
235
+ if value.features.get("noun") in (None, "item"):
236
+ return None
237
+ return _as_place(value) or value.text
238
+
239
+
240
+ def _text(value: Any) -> str | None:
241
+ if isinstance(value, Entity):
242
+ return value.text
243
+ return value if isinstance(value, str) else None
244
+
245
+
246
+ def _app(value: Any) -> str | None:
247
+ if isinstance(value, Entity):
248
+ noun = value.features.get("noun")
249
+ if isinstance(noun, str) and noun.startswith("app:"):
250
+ return noun.split(":", 1)[1]
251
+ if isinstance(value.text, str) and value.text.startswith("app:"):
252
+ return value.text.split(":", 1)[1]
253
+ if isinstance(value, str) and value.startswith("app:"):
254
+ return value.split(":", 1)[1]
255
+ return None
256
+
257
+
258
+ def _noun(value: Any) -> str | None:
259
+ if isinstance(value, Entity):
260
+ noun = value.features.get("noun")
261
+ return noun if isinstance(noun, str) else None
262
+ return None
263
+
264
+
265
+ def _kind_of(value: Any) -> str:
266
+ """folder, file or unknown — from the noun used, or from a file extension."""
267
+ noun = _noun(value)
268
+ if noun in ("folder", "repo", "project"):
269
+ return "folder"
270
+ if noun in ("file", "readme", "note", "document", "pdf"):
271
+ return "file"
272
+ text = _target(value) or ""
273
+ if "." in text.rsplit("/", 1)[-1]:
274
+ return "file"
275
+ return "unknown"
276
+
277
+
278
+ def acts(meaning: Any, *, alone: bool = True) -> list[Act]:
279
+ """Project one reading onto the assistant's ``(act, slots)`` vocabulary."""
280
+ if isinstance(meaning, str): # a dialogue move
281
+ return [Act(meaning, {})]
282
+ if isinstance(meaning, Request):
283
+ return _needed(_from_request(meaning.frame), meaning.frame)
284
+ if isinstance(meaning, Question):
285
+ return _needed(_from_question(meaning), meaning.frame)
286
+ if isinstance(meaning, Frame):
287
+ return _needed(_from_request(meaning), meaning) # a bare clause read as an instruction
288
+ if isinstance(meaning, Entity):
289
+ # a bare phrase is a request only when it names something outright — a pasted
290
+ # path, a number. A loose noun is an answer or a fragment, not an instruction.
291
+ if not alone or meaning.kind not in ("path", "literal", "number", "command"):
292
+ return []
293
+ path = _place(meaning)
294
+ return [Act("read", {"target": path})] if path else []
295
+ return []
296
+
297
+
298
+ def _from_request(frame: Frame) -> list[Act]:
299
+ if not _understood(frame):
300
+ return [] # part of the request has no expression in the act vocabulary
301
+ verb = frame.predicate
302
+ obj = frame.role("object") or frame.role("theme")
303
+ place = frame.role("location") or frame.role("destination")
304
+ content = frame.role("content")
305
+ # PP attachment is genuinely ambiguous, so "a folder called x on my desktop" may
306
+ # hang the place on the verb or on the noun. Look in both before giving up.
307
+ if isinstance(obj, Entity):
308
+ place = place or obj.features.get("location") or obj.features.get("destination")
309
+ content = content or obj.features.get("content")
310
+ if _noun(obj) == "content" and obj.features.get("of") is not None:
311
+ obj = obj.features["of"]
312
+
313
+ if isinstance(obj, tuple): # "make a folder called a and a folder called b"
314
+ out: list[Act] = []
315
+ for item in obj:
316
+ out.extend(_from_request(Frame(verb, {**frame.roles, "object": item}, frame.features)))
317
+ return out
318
+
319
+ if verb in ("make", "make_folder", "make_file"):
320
+ named = (obj.features.get("name") if isinstance(obj, Entity) else None) or frame.role("name")
321
+ if isinstance(named, tuple): # "folders called drafts and final"
322
+ return [a for item in named
323
+ for a in _from_request(Frame(verb, {**frame.roles, "object": Entity(
324
+ obj.kind, obj.text, {**obj.features, "name": item})}, frame.features))]
325
+ name = _text(named) if named is not None else _target(obj)
326
+ if isinstance(obj, Entity) and _noun(obj) == "readme" and named is None:
327
+ name = "README.md"
328
+ elif named is None and isinstance(name, str) and name.lower() in GENERIC_NOUNS:
329
+ name = None # "make a folder" names no folder
330
+ if isinstance(name, str) and name.startswith("@"):
331
+ name = None # a reference is not a name: nothing new can be called "it"
332
+ if place is None and isinstance(obj, Entity) and _place_word(obj) and named is None:
333
+ place, name = obj, None # "make a folder in documents": the noun was the place
334
+ kind = {"make_folder": "folder", "make_file": "file"}.get(verb) or _kind_of(obj)
335
+ slots = {"name": name, "place": _as_place(place), "text": _text(content)}
336
+ return [Act("create_folder" if kind != "file" else "create_file", _drop(slots))]
337
+ if verb in ("add", "write", "append", "put"):
338
+ # "put hello in a file called hi.txt" creates the file with that text in it
339
+ if isinstance(place, Entity) and place.features.get("name") is not None and _kind_of(place) in ("file", "folder"):
340
+ made = _from_request(Frame("make", {k: v for k, v in frame.roles.items() if k not in ("location", "destination", "object")}
341
+ | {"object": place}, frame.features))
342
+ return [Act(a.act, _drop({**a.slots, "text": _text(obj)})) for a in made]
343
+ if isinstance(obj, Entity) and _kind_of(obj) in ("file", "folder") and obj.features.get("name"):
344
+ merged = Frame("make", {**frame.roles, "content": obj.features.get("content") or content or frame.role("destination")}, frame.features)
345
+ text = _text(frame.role("destination")) or _text(content) or _text(frame.role("theme"))
346
+ acts_ = _from_request(Frame("make", {k: v for k, v in frame.roles.items() if k != "destination"}, frame.features))
347
+ return [Act(a.act, _drop({**a.slots, "text": text})) for a in acts_]
348
+ target = _target(place) if place is not None else _target(obj)
349
+ text = _text(obj) if place is not None else _text(content)
350
+ return [Act("write", _drop({"text": text, "target": target, "append": verb in ("add", "append", "put")}))]
351
+ if verb == "delete":
352
+ return [Act("delete", _drop({"target": _target(obj) or _target(place)}))]
353
+ if verb in ("move", "copy"):
354
+ return [Act(verb, _drop({"target": _target(obj), "dest": _as_place(place)}))]
355
+ if verb == "rename":
356
+ return [Act("rename", _drop({"target": _target(obj), "new_name": _target(frame.role("destination") or frame.role("as"))}))]
357
+ if verb in ("read", "show", "open", "view", "list"):
358
+ app = _app(obj) or _app(place)
359
+ if app and verb in ("open", "show"):
360
+ return [Act("open_app", {"app": app})]
361
+ kind = _kind_of(obj)
362
+ if verb == "list":
363
+ return [Act("list", _drop({"place": _as_place(place) or _as_place(obj)}))]
364
+ if obj is None or (kind == "folder" and verb != "read"):
365
+ return [Act("list", _drop({"place": _as_place(obj) or _as_place(place)}))]
366
+ if _place_word(obj) and verb in ("show", "open"):
367
+ return [Act("list", _drop({"place": _as_place(obj)}))]
368
+ if verb == "show" and _refers_back(obj):
369
+ return [Act("list", {"place": "@it"})]
370
+ return [Act("read", _drop({"target": _target(obj), "place": _as_place(place) if place is not None else None}))]
371
+ if verb == "launch":
372
+ app = _app(obj)
373
+ return [Act("open_app", {"app": app})] if app else []
374
+ if verb == "find":
375
+ pattern = _pattern(obj)
376
+ return [Act("find", _drop({"pattern": pattern, "place": _as_place(place) or "~"}))]
377
+ if verb == "search":
378
+ needle = _text(content) or _text(frame.role("topic")) or _text(frame.role("beneficiary")) or _text(obj)
379
+ return [Act("grep", _drop({"needle": needle, "place": _as_place(place) or "~"}))]
380
+ if verb == "count":
381
+ unit = _noun(obj)
382
+ return [Act("count", _drop({"unit": unit + "s" if unit else None, "target": _target(place) or _target(obj)}))]
383
+ if verb == "mention":
384
+ return [Act("grep", _drop({"needle": _target(obj), "place": _as_place(place) or "~"}))]
385
+ if verb == "install":
386
+ return [Act("install", _drop({"package": _target(obj)}))]
387
+ if verb == "run":
388
+ return [Act("run", _drop({"command": _text(obj)}))]
389
+ if verb == "go":
390
+ return [Act("cd", {"target": _as_place(place) or _as_place(obj) or "~"})]
391
+ if verb == "init":
392
+ return [Act("git_init", {"target": _as_place(place) or _as_place(obj) or "@it"})]
393
+ if verb == "commit":
394
+ return [Act("git_commit", _drop({"target": _as_place(place) or _as_place(obj) or "@it",
395
+ "message": _text(frame.role("as") or content)}))]
396
+ return []
397
+
398
+
399
+ def _needed(acts_: list[Act], frame: Frame | None = None) -> list[Act]:
400
+ """Withhold an act that does not say what it acts on, or that invents a reference.
401
+
402
+ "@it" means *the thing we were just talking about*. The projection may only use it
403
+ when the utterance actually pointed at something — a third-person pronoun or a
404
+ demonstrative. Otherwise the reference is invented, and an invented reference in a
405
+ core slot is how "make me a sandwich" becomes a new folder.
406
+ """
407
+ licensed = frame is not None and any(_refers_back(e) for e in frame.entities())
408
+ out: list[Act] = []
409
+ for act in acts_:
410
+ core = NEEDS_TARGET.get(act.act)
411
+ if core is not None and act.slots.get(core) is None:
412
+ continue
413
+ if not licensed and any(v == "@it" for v in act.slots.values()):
414
+ continue
415
+ out.append(act)
416
+ return out
417
+
418
+
419
+ def _place_word(value: Any) -> bool:
420
+ noun = _noun(value)
421
+ return isinstance(noun, str) and noun.startswith("place:")
422
+
423
+
424
+ def _as_place(value: Any) -> str | None:
425
+ """A place slot: a path, or "@name" for a folder the conversation knows by name."""
426
+ if value is None or _speaks_of_self(value):
427
+ return None
428
+ path = _place(value)
429
+ if path and (path.startswith(("~", "/")) or path == "@it"):
430
+ return path
431
+ if isinstance(value, Entity):
432
+ named = value.features.get("name")
433
+ if named is not None:
434
+ return f"@{_text(named)}"
435
+ if value.kind in ("name", "literal"):
436
+ return f"@{value.text}"
437
+ if value.kind == "pronoun":
438
+ return "@it"
439
+ return path
440
+
441
+
442
+ def _pattern(value: Any) -> str | None:
443
+ if isinstance(value, Entity) and value.features.get("name") is not None:
444
+ return _text(value.features["name"])
445
+ noun = _noun(value)
446
+ if noun == "pdf":
447
+ return "*.pdf"
448
+ target = _target(value)
449
+ return target
450
+
451
+
452
+ def _from_question(question: Question) -> list[Act]:
453
+ frame, asked = question.frame, question.asked
454
+ subject, theme = frame.role("subject"), frame.role("theme")
455
+ location = frame.role("location")
456
+ topic = INFO.get(_noun(subject) or "") or INFO.get(_noun(theme) or "")
457
+ if asked in ("count", "amount", "size"):
458
+ noun = _noun(theme) or _noun(subject)
459
+ if noun in ("line", "word"):
460
+ return [Act("count", _drop({"unit": noun + "s", "target": _target(location) or _target(theme)}))]
461
+ if noun in ("space", "disk"):
462
+ return [Act("info", {"topic": "disk"})]
463
+ if noun == "cpu":
464
+ return [Act("info", {"topic": "cpus"})]
465
+ if asked == "size":
466
+ return [Act("size", _drop({"target": _target(subject) or _as_place(subject)}))]
467
+ return [Act("list", _drop({"place": _place(location) or _place(theme), "count": True}))]
468
+ if topic:
469
+ return [Act("info", {"topic": topic})]
470
+ if frame.predicate == "installed" or _noun(theme) == "installed":
471
+ return [Act("which", _drop({"program": _target(subject)}))]
472
+ if frame.predicate in ("say", "mention", "contain") and asked == "theme":
473
+ return [Act("grep" if frame.predicate == "mention" else "read",
474
+ _drop({"needle": _target(frame.role("object")), "target": _target(subject),
475
+ "place": "~" if frame.predicate == "mention" else None}))]
476
+ if asked == "location" and subject is not None:
477
+ return [Act("find", _drop({"pattern": _target(subject), "place": "~"}))]
478
+ if frame.predicate == "located" or location is not None:
479
+ return [Act("list", _drop({"place": _place(location)}))]
480
+ if asked == "subject" and frame.predicate == "be":
481
+ return [Act("info", {"topic": "user"})]
482
+ if frame.predicate == "running" or _noun(subject) == "process":
483
+ return [Act("info", {"topic": "processes"})]
484
+ return []
485
+
486
+
487
+ def _drop(slots: Mapping[str, Any]) -> dict[str, Any]:
488
+ return {k: v for k, v in slots.items() if v is not None}
489
+
490
+
491
+ #: Roles each act can express. A frame carrying anything else is only *partly*
492
+ #: understood, and acting on a partly understood request is how an agent does the
493
+ #: wrong thing — so the act is withheld and the request reads as unknown.
494
+ EXPRESSIBLE: Mapping[str, frozenset[str]] = {
495
+ "make": frozenset({"object", "theme", "location", "destination", "content", "name", "as"}),
496
+ "make_folder": frozenset({"object", "theme", "location", "destination", "name"}),
497
+ "make_file": frozenset({"object", "theme", "location", "destination", "content", "name"}),
498
+ "add": frozenset({"object", "theme", "location", "destination", "content"}),
499
+ "write": frozenset({"object", "theme", "location", "destination", "content"}),
500
+ "append": frozenset({"object", "theme", "location", "destination", "content"}),
501
+ "put": frozenset({"object", "theme", "location", "destination", "content"}),
502
+ "delete": frozenset({"object", "theme", "location"}),
503
+ "move": frozenset({"object", "theme", "destination", "location"}),
504
+ "copy": frozenset({"object", "theme", "destination", "location"}),
505
+ "rename": frozenset({"object", "theme", "destination", "as"}),
506
+ "read": frozenset({"object", "theme", "location", "recipient", "of"}),
507
+ "show": frozenset({"object", "theme", "location", "recipient", "of"}),
508
+ "open": frozenset({"object", "theme", "location"}),
509
+ "view": frozenset({"object", "theme", "location"}),
510
+ "list": frozenset({"object", "theme", "location", "recipient"}),
511
+ "launch": frozenset({"object", "theme"}),
512
+ "find": frozenset({"object", "theme", "location", "name"}),
513
+ "search": frozenset({"object", "theme", "location", "content", "topic", "beneficiary"}),
514
+ "count": frozenset({"object", "theme", "location", "of"}),
515
+ "install": frozenset({"object", "theme"}),
516
+ "run": frozenset({"object", "theme"}),
517
+ "go": frozenset({"object", "theme", "destination", "location"}),
518
+ "init": frozenset({"object", "theme", "location", "destination", "as"}),
519
+ "commit": frozenset({"object", "theme", "location", "destination", "as", "content"}),
520
+ "mention": frozenset({"object", "theme", "location"}),
521
+ }
522
+
523
+ #: Words for a *kind* of thing. On their own they do not name one, so "make a folder"
524
+ #: has no name to create and the act is withheld rather than making "folder".
525
+ GENERIC_NOUNS = frozenset({"folder", "directory", "dir", "file", "note", "document", "doc", "repo", "repository",
526
+ "project", "item", "thing", "stuff", "content", "contents", "app", "application"})
527
+
528
+ #: Acts that change something need to know what they are changing.
529
+ NEEDS_TARGET = {"delete": "target", "move": "target", "copy": "target", "rename": "target",
530
+ "write": "target", "install": "package", "run": "command", "read": "target",
531
+ "create_folder": "name", "create_file": "name", "grep": "needle", "find": "pattern"}
532
+
533
+
534
+ def _understood(frame: Frame) -> bool:
535
+ allowed = EXPRESSIBLE.get(frame.predicate)
536
+ if allowed is None:
537
+ return True
538
+ return not (set(frame.roles) - allowed - {"subject", "agent"})
539
+
540
+
541
+ def read_request(text: str, *, grammar: Grammar = DESKTOP) -> tuple[list[Act], Understanding]:
542
+ """Read one chat message: the acts it asks for, and the parse they came from."""
543
+ got = understand(grammar, text)
544
+ out: list[Act] = []
545
+ alone = len(got.meanings) == 1
546
+ for meaning in got.meanings:
547
+ out.extend(acts(meaning, alone=alone))
548
+ # a bare noun phrase over a half-covered utterance is not a request: saying nothing
549
+ # is better than guessing a read, and it keeps unclear input from acting
550
+ if got.coverage < 0.7 and all(isinstance(m, Entity) for m in got.meanings):
551
+ return [], got
552
+ return out, got