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.
- tensorcode/__init__.py +84 -0
- tensorcode/actions.py +137 -0
- tensorcode/answer_type.py +222 -0
- tensorcode/awareness.py +344 -0
- tensorcode/backends/__init__.py +0 -0
- tensorcode/backends/builtin.py +167 -0
- tensorcode/backends/hf_local.py +89 -0
- tensorcode/backends/linear.py +133 -0
- tensorcode/backends/neural.py +361 -0
- tensorcode/causal.py +262 -0
- tensorcode/change.py +566 -0
- tensorcode/chunking.py +195 -0
- tensorcode/cognition.py +311 -0
- tensorcode/context.py +97 -0
- tensorcode/control.py +291 -0
- tensorcode/cues.py +192 -0
- tensorcode/expectation.py +270 -0
- tensorcode/frames.py +232 -0
- tensorcode/language/__init__.py +36 -0
- tensorcode/language/chart.py +558 -0
- tensorcode/language/discourse.py +132 -0
- tensorcode/language/domains/__init__.py +0 -0
- tensorcode/language/domains/desktop.py +552 -0
- tensorcode/language/english.py +459 -0
- tensorcode/language/features.py +112 -0
- tensorcode/language/generate.py +574 -0
- tensorcode/language/grammar.py +893 -0
- tensorcode/language/semantics.py +349 -0
- tensorcode/learning/__init__.py +30 -0
- tensorcode/learning/certificate.py +148 -0
- tensorcode/learning/induce.py +304 -0
- tensorcode/learning/library.py +217 -0
- tensorcode/learning/literals.py +126 -0
- tensorcode/learning/verify.py +253 -0
- tensorcode/memory.py +303 -0
- tensorcode/metacognition.py +351 -0
- tensorcode/ops.py +207 -0
- tensorcode/outcomes.py +99 -0
- tensorcode/permanence.py +376 -0
- tensorcode/priming.py +191 -0
- tensorcode/py.typed +0 -0
- tensorcode/quantity.py +311 -0
- tensorcode/records.py +728 -0
- tensorcode/relation.py +771 -0
- tensorcode/runtime.py +471 -0
- tensorcode/semantics_bridge.py +308 -0
- tensorcode/social.py +380 -0
- tensorcode/temporal.py +189 -0
- tensorcode/wants.py +185 -0
- tensorcode-0.1.0a1.dist-info/METADATA +196 -0
- tensorcode-0.1.0a1.dist-info/RECORD +53 -0
- tensorcode-0.1.0a1.dist-info/WHEEL +4 -0
- 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
|