nuagent 0.1.2__tar.gz → 0.2.0__tar.gz
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.
- nuagent-0.2.0/.gitignore +103 -0
- nuagent-0.2.0/PKG-INFO +22 -0
- nuagent-0.2.0/README.md +5 -0
- nuagent-0.2.0/pyproject.toml +35 -0
- nuagent-0.2.0/src/nuagent/__init__.py +9 -0
- nuagent-0.2.0/src/nuagent/core/__init__.py +16 -0
- nuagent-0.2.0/src/nuagent/core/loop.py +205 -0
- nuagent-0.2.0/src/nuagent/core/program.py +134 -0
- nuagent-0.2.0/src/nuagent/presets/__init__.py +1 -0
- nuagent-0.2.0/src/nuagent/presets/backends/__init__.py +11 -0
- nuagent-0.2.0/src/nuagent/presets/backends/cc.py +192 -0
- nuagent-0.2.0/src/nuagent/presets/backends/llm.py +115 -0
- nuagent-0.2.0/src/nuagent/presets/store/__init__.py +15 -0
- nuagent-0.2.0/src/nuagent/presets/store/kv.py +109 -0
- nuagent-0.2.0/src/nuagent/presets/store/mem.py +80 -0
- nuagent-0.2.0/src/nuagent/prompt/__init__.py +157 -0
- nuagent-0.2.0/src/nuagent/prompt/catalogue.py +107 -0
- nuagent-0.2.0/src/nuagent/prompt/section.py +56 -0
- nuagent-0.2.0/src/nuagent/prompt/surface.py +154 -0
- nuagent-0.2.0/src/nuagent/protocol/__init__.py +52 -0
- nuagent-0.2.0/src/nuagent/protocol/observation.py +125 -0
- nuagent-0.2.0/src/nuagent/protocol/protocol.md +75 -0
- nuagent-0.1.2/LICENSE.md +0 -201
- nuagent-0.1.2/PKG-INFO +0 -131
- nuagent-0.1.2/README.md +0 -88
- nuagent-0.1.2/pyproject.toml +0 -256
- nuagent-0.1.2/setup.cfg +0 -4
- nuagent-0.1.2/src/nuagent/__init__.py +0 -74
- nuagent-0.1.2/src/nuagent/agent/__init__.py +0 -30
- nuagent-0.1.2/src/nuagent/agent/agent.py +0 -208
- nuagent-0.1.2/src/nuagent/agent/shapes.py +0 -68
- nuagent-0.1.2/src/nuagent/agent/utils.py +0 -158
- nuagent-0.1.2/src/nuagent/prompt/__init__.py +0 -28
- nuagent-0.1.2/src/nuagent/prompt/assemble.py +0 -46
- nuagent-0.1.2/src/nuagent/prompt/catalogue.py +0 -135
- nuagent-0.1.2/src/nuagent/prompt/sections.py +0 -120
- nuagent-0.1.2/src/nuagent/prompt/surface.py +0 -218
- nuagent-0.1.2/src/nuagent/prompt/text/crashcourse.md +0 -96
- nuagent-0.1.2/src/nuagent/prompt/text/examples.md +0 -93
- nuagent-0.1.2/src/nuagent/prompt/text/fabrics.md +0 -22
- nuagent-0.1.2/src/nuagent/prompt/text/finish.md +0 -24
- nuagent-0.1.2/src/nuagent/prompt/text/inspect.md +0 -23
- nuagent-0.1.2/src/nuagent/prompt/text/protocol.md +0 -78
- nuagent-0.1.2/src/nuagent/prompt/text/role.md +0 -11
- nuagent-0.1.2/src/nuagent/prompt/text/thesis.md +0 -114
- nuagent-0.1.2/src/nuagent.egg-info/PKG-INFO +0 -131
- nuagent-0.1.2/src/nuagent.egg-info/SOURCES.txt +0 -30
- nuagent-0.1.2/src/nuagent.egg-info/dependency_links.txt +0 -1
- nuagent-0.1.2/src/nuagent.egg-info/requires.txt +0 -18
- nuagent-0.1.2/src/nuagent.egg-info/top_level.txt +0 -1
- nuagent-0.1.2/tests/test_agent.py +0 -328
- nuagent-0.1.2/tests/test_prompt.py +0 -409
- nuagent-0.1.2/tests/test_smoke.py +0 -30
- nuagent-0.1.2/tests/test_utils.py +0 -209
nuagent-0.2.0/.gitignore
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# C extensions
|
|
7
|
+
*.so
|
|
8
|
+
|
|
9
|
+
# Distribution / packaging
|
|
10
|
+
.Python
|
|
11
|
+
build/
|
|
12
|
+
# The nuspace-ui package links its build/ to the vite output so an editable
|
|
13
|
+
# install serves it. A symlink, not a directory, but say so explicitly.
|
|
14
|
+
!pkgs/ts/nuspace-ui/python/nuspace_ui/build
|
|
15
|
+
develop-eggs/
|
|
16
|
+
dist/
|
|
17
|
+
downloads/
|
|
18
|
+
eggs/
|
|
19
|
+
.eggs/
|
|
20
|
+
lib64/
|
|
21
|
+
parts/
|
|
22
|
+
sdist/
|
|
23
|
+
var/
|
|
24
|
+
wheels/
|
|
25
|
+
share/python-wheels/
|
|
26
|
+
*.egg-info/
|
|
27
|
+
.installed.cfg
|
|
28
|
+
*.egg
|
|
29
|
+
MANIFEST
|
|
30
|
+
|
|
31
|
+
# Installer logs
|
|
32
|
+
pip-log.txt
|
|
33
|
+
pip-delete-this-directory.txt
|
|
34
|
+
|
|
35
|
+
# Unit test / coverage reports
|
|
36
|
+
htmlcov/
|
|
37
|
+
.tox/
|
|
38
|
+
.nox/
|
|
39
|
+
.coverage
|
|
40
|
+
.coverage.*
|
|
41
|
+
.cache
|
|
42
|
+
nosetests.xml
|
|
43
|
+
coverage.xml
|
|
44
|
+
*.cover
|
|
45
|
+
*.py,cover
|
|
46
|
+
.hypothesis/
|
|
47
|
+
.pytest_cache/
|
|
48
|
+
cover/
|
|
49
|
+
|
|
50
|
+
# Environments
|
|
51
|
+
.env
|
|
52
|
+
.venv
|
|
53
|
+
env/
|
|
54
|
+
venv/
|
|
55
|
+
ENV/
|
|
56
|
+
env.bak/
|
|
57
|
+
venv.bak/
|
|
58
|
+
|
|
59
|
+
# uv
|
|
60
|
+
uv.lock
|
|
61
|
+
|
|
62
|
+
# mypy
|
|
63
|
+
.mypy_cache/
|
|
64
|
+
.dmypy.json
|
|
65
|
+
dmypy.json
|
|
66
|
+
|
|
67
|
+
# IDEs
|
|
68
|
+
.idea/
|
|
69
|
+
.vscode/
|
|
70
|
+
|
|
71
|
+
.DS_Store
|
|
72
|
+
|
|
73
|
+
# Profiling
|
|
74
|
+
*.prof
|
|
75
|
+
*.lprof
|
|
76
|
+
|
|
77
|
+
# Benchmarking
|
|
78
|
+
benchmark_results/
|
|
79
|
+
*.benchmark
|
|
80
|
+
*.benchmarks
|
|
81
|
+
|
|
82
|
+
# Ruff
|
|
83
|
+
.ruff_cache/
|
|
84
|
+
|
|
85
|
+
# Misc
|
|
86
|
+
/*playground
|
|
87
|
+
.db*/
|
|
88
|
+
.state*/
|
|
89
|
+
.logs*/
|
|
90
|
+
|
|
91
|
+
# syncthing
|
|
92
|
+
.stfolder
|
|
93
|
+
|
|
94
|
+
# nuspace-ui (vite / node)
|
|
95
|
+
node_modules/
|
|
96
|
+
!src/nuspace/web/ui/dist/
|
|
97
|
+
src/nuspace/web/ui/dist/*
|
|
98
|
+
!src/nuspace/web/ui/dist/.gitkeep
|
|
99
|
+
|
|
100
|
+
*.db/
|
|
101
|
+
*.nuspace/
|
|
102
|
+
# Local archive of the v0 prototype (reference only, never edited)
|
|
103
|
+
_archive/
|
nuagent-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: nuagent
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: nuagent: the model answers in Nu, Nu runs the answer.
|
|
5
|
+
Project-URL: Repository, https://github.com/nustackdev/nuverse
|
|
6
|
+
Author-email: Gor Arakelyan <gorarkln@gmail.com>
|
|
7
|
+
License-Expression: Apache-2.0
|
|
8
|
+
Requires-Python: >=3.10
|
|
9
|
+
Requires-Dist: nucore>=0.6.4
|
|
10
|
+
Requires-Dist: nuskills>=0.1.0
|
|
11
|
+
Requires-Dist: nustd[kv]>=0.6.4
|
|
12
|
+
Provides-Extra: cc
|
|
13
|
+
Requires-Dist: nustd[cc]; extra == 'cc'
|
|
14
|
+
Provides-Extra: llm
|
|
15
|
+
Requires-Dist: nustd[llm]; extra == 'llm'
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
18
|
+
# nuagent
|
|
19
|
+
|
|
20
|
+
The model answers in Nu, Nu runs the answer. A small loop: prompt, reply, build, run, observe, repeat until the model ends the run. Storage and backends are presets around it.
|
|
21
|
+
|
|
22
|
+
Early and unsettled: the package is a skeleton, the API is not there yet.
|
nuagent-0.2.0/README.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "nuagent"
|
|
7
|
+
version = "0.2.0"
|
|
8
|
+
description = "nuagent: the model answers in Nu, Nu runs the answer."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "Apache-2.0"
|
|
11
|
+
authors = [{ name = "Gor Arakelyan", email = "gorarkln@gmail.com" }]
|
|
12
|
+
requires-python = ">=3.10"
|
|
13
|
+
dependencies = [
|
|
14
|
+
# The core needs only nu. nuspace hosting comes later as separate packages.
|
|
15
|
+
"nucore>=0.6.4",
|
|
16
|
+
"nustd[kv]>=0.6.4",
|
|
17
|
+
# The generic Nu prose the opening prompt is assembled from.
|
|
18
|
+
"nuskills>=0.1.0",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
[project.optional-dependencies]
|
|
22
|
+
# The cc backend drives Claude Code through nustd.cc, on the local claude login.
|
|
23
|
+
cc = [
|
|
24
|
+
"nustd[cc]",
|
|
25
|
+
]
|
|
26
|
+
# The llm backend talks to a stateless endpoint through nustd.llm.
|
|
27
|
+
llm = [
|
|
28
|
+
"nustd[llm]",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
[project.urls]
|
|
32
|
+
Repository = "https://github.com/nustackdev/nuverse"
|
|
33
|
+
|
|
34
|
+
[tool.hatch.build.targets.wheel]
|
|
35
|
+
packages = ["src/nuagent"]
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""nuagent: the model answers in Nu, Nu runs the answer.
|
|
2
|
+
|
|
3
|
+
- :mod:`.core`: the loop, one turn after another until the model ends the run.
|
|
4
|
+
- :mod:`.protocol`: what the model is told about replying in Nu.
|
|
5
|
+
- :mod:`.prompt`: the opening prompt, the protocol plus nuskills, the catalogue and the world.
|
|
6
|
+
- :mod:`.presets`: what you swap: the store and the backend.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
__version__ = "0.2.0"
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""The loop: prompt, reply, extract, load, eval, observe, next prompt.
|
|
2
|
+
|
|
3
|
+
- :mod:`.loop`: :func:`agent`, :func:`turn`, :func:`seed`, the mem
|
|
4
|
+
:class:`Run` the model ends the run through, and the :class:`Store` and
|
|
5
|
+
:class:`Backend` seams.
|
|
6
|
+
- :mod:`.program`: :func:`extract` the program from a reply, :func:`attempt`
|
|
7
|
+
it into a typed outcome.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from .loop import Backend, Run, Store, agent, seed, turn
|
|
13
|
+
from .program import FENCE, attempt, extract
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
__all__ = ["FENCE", "Backend", "Run", "Store", "agent", "attempt", "extract", "seed", "turn"]
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"""The loop: one turn, and that turn under a ``WhileDo`` until the model ends the run.
|
|
2
|
+
|
|
3
|
+
A turn is one prompt and one response, then the program in it::
|
|
4
|
+
|
|
5
|
+
read the conversation short bracket -> plain data
|
|
6
|
+
ask(conversation) no bracket -> text
|
|
7
|
+
reply, message, program short bracket
|
|
8
|
+
attempt(program) no bracket -> outcome the program runs exactly once
|
|
9
|
+
outcome, observation short bracket
|
|
10
|
+
|
|
11
|
+
Every step writes a session slot, so a watcher reads the run's progress off
|
|
12
|
+
state and the loop has no hooks. The conversation is a list that grows by
|
|
13
|
+
append and is never rebuilt or cut.
|
|
14
|
+
|
|
15
|
+
**The model ends the run, and nothing else does.** It redeclares the store's
|
|
16
|
+
``run`` Shape in its own module and sets ``done`` in the program that
|
|
17
|
+
finishes the work. There is no turn counter, budget or stall guard here, and
|
|
18
|
+
no predicate over the world: a predicate tests what a program did, not
|
|
19
|
+
whether the task is done. Limits like those are layers around the loop,
|
|
20
|
+
never inside it.
|
|
21
|
+
|
|
22
|
+
**The core knows no storage and no backend.** A :class:`Store` brings the
|
|
23
|
+
session Shape (``messages``, ``reply``, ``program``, ``outcome``), the run
|
|
24
|
+
Shape, and two brackets: ``bracket`` around the loop's own short session
|
|
25
|
+
steps, ``rewrite`` around anything on the world (``done``, the state line,
|
|
26
|
+
the model's program, which it gets through ``LoadNu``). Neither is ever held
|
|
27
|
+
across the backend's answer or the program's run, so a store that locks
|
|
28
|
+
(one sqlite file for session and world) never waits on itself. The backend
|
|
29
|
+
is any :class:`Backend`: it answers a conversation with text and wraps the
|
|
30
|
+
whole run in whatever it needs open for it (a process, a session id, a
|
|
31
|
+
client). What it does between prompt and response is its own business.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
from __future__ import annotations
|
|
35
|
+
|
|
36
|
+
from typing import TYPE_CHECKING, Protocol
|
|
37
|
+
|
|
38
|
+
import nu
|
|
39
|
+
from nuagent.protocol import observation
|
|
40
|
+
|
|
41
|
+
from .program import attempt, extract
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
if TYPE_CHECKING:
|
|
45
|
+
from collections.abc import Callable
|
|
46
|
+
|
|
47
|
+
from nu.lang import Nu, StrArg
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
__all__ = ["Backend", "Run", "Store", "agent", "seed", "turn"]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class Run(nu.Shape):
|
|
54
|
+
"""The run itself, on mem: the model sets ``done`` and the loop stops after that turn.
|
|
55
|
+
|
|
56
|
+
Notes:
|
|
57
|
+
- Untagged and flat on purpose. The model redeclares it in its own
|
|
58
|
+
module, which makes a different class, and only an untagged Shape
|
|
59
|
+
is reached by both: a tagged one is a store the model's ``Run``
|
|
60
|
+
never writes, and the run would never end.
|
|
61
|
+
- The mem store's run. A store with its world elsewhere brings its
|
|
62
|
+
own one-slot Shape on that fabric.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
done = nu.BoolRef.slot()
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class Store(Protocol):
|
|
69
|
+
"""Where a run keeps its session and its world, and how a term reaches them.
|
|
70
|
+
|
|
71
|
+
Attributes:
|
|
72
|
+
session: the session Shape, slots ``messages``, ``reply``,
|
|
73
|
+
``program`` and ``outcome``.
|
|
74
|
+
run: the world's one-slot Shape the model ends the run through.
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
session: type[nu.Shape]
|
|
78
|
+
run: type[nu.Shape]
|
|
79
|
+
|
|
80
|
+
def bracket(self, body: Nu) -> Nu:
|
|
81
|
+
"""``body``, a short step on the session, ready to run on the session's store."""
|
|
82
|
+
...
|
|
83
|
+
|
|
84
|
+
def rewrite(self, term: Nu) -> Nu:
|
|
85
|
+
"""``term``, a read or write on the world, ready to run on the world's store.
|
|
86
|
+
|
|
87
|
+
The model's program is one: it reaches this through ``LoadNu``'s
|
|
88
|
+
``rewrite``, so it gets its brackets without knowing it needs any.
|
|
89
|
+
"""
|
|
90
|
+
...
|
|
91
|
+
|
|
92
|
+
def bound(self, body: Nu) -> Nu:
|
|
93
|
+
"""``body`` with the session's store and the world's bound around it."""
|
|
94
|
+
...
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class Backend(Protocol):
|
|
98
|
+
"""What the loop needs from a model: an answer, and a scope to run in."""
|
|
99
|
+
|
|
100
|
+
def ask(self, messages: Nu) -> Nu:
|
|
101
|
+
"""The model's reply to the conversation, as a Str-yielding term.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
messages: the conversation as plain data, a list of
|
|
105
|
+
``{role, content}`` dicts read out of the session before the
|
|
106
|
+
ask. A stateless backend sends them all, a stateful one the
|
|
107
|
+
newest.
|
|
108
|
+
"""
|
|
109
|
+
...
|
|
110
|
+
|
|
111
|
+
def wrap(self, body: Nu) -> Nu:
|
|
112
|
+
"""The whole run inside whatever the backend holds open for its lifetime."""
|
|
113
|
+
...
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def seed(store: Store, prompt: StrArg) -> Nu:
|
|
117
|
+
"""Open a run: the conversation is the prompt alone, and the run is not done.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
store: the store the run keeps its session and world in.
|
|
121
|
+
prompt: the opening message as the caller composed it (the protocol,
|
|
122
|
+
the Nu prose, the task). Sent as the first ``user`` message, so
|
|
123
|
+
every backend reads the run the same way.
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
A Command.
|
|
127
|
+
|
|
128
|
+
Notes:
|
|
129
|
+
- ``done`` is written False before the first turn. Unset it reads
|
|
130
|
+
EMPTY, the loop condition is never true, and no turn runs.
|
|
131
|
+
"""
|
|
132
|
+
opening = nu.Dict.of(role="user", content=prompt)
|
|
133
|
+
return store.bracket(store.session.messages.set(nu.List.of(opening))) >> store.rewrite(
|
|
134
|
+
store.run.done.set(False)
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def turn(store: Store, ask: Callable[[Nu], Nu], *, state: Nu | None = None) -> Nu:
|
|
139
|
+
"""One turn: ask, extract, attempt, observe.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
store: the store the run keeps its session and world in.
|
|
143
|
+
ask: the backend's ``ask``.
|
|
144
|
+
state: what the model sees of the world after each run, a read on
|
|
145
|
+
the world. Omitted, the observation is the outcome alone.
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
A term, one turn, ready to run once or under a loop.
|
|
149
|
+
|
|
150
|
+
Notes:
|
|
151
|
+
- The slow parts, the ask and the program's run, sit outside every
|
|
152
|
+
bracket. What crosses between steps is plain data held in a
|
|
153
|
+
``nu.let``: the conversation, the reply, the source, the outcome.
|
|
154
|
+
- The program runs once. Its outcome is held, then written to a slot
|
|
155
|
+
and rendered into the observation from the held value.
|
|
156
|
+
- The reply goes into the conversation as soon as it is in, so the
|
|
157
|
+
conversation holds it even when what follows fails.
|
|
158
|
+
"""
|
|
159
|
+
session = store.session
|
|
160
|
+
|
|
161
|
+
def asked(said: Nu) -> Nu:
|
|
162
|
+
"""The conversation is read: ask it, then take the answer."""
|
|
163
|
+
return nu.let(ask(said), answered)
|
|
164
|
+
|
|
165
|
+
def answered(text: Nu) -> Nu:
|
|
166
|
+
"""The reply is in: take the source out of it."""
|
|
167
|
+
return nu.let(extract(text), lambda source: kept(text, source))
|
|
168
|
+
|
|
169
|
+
def kept(text: Nu, source: Nu) -> Nu:
|
|
170
|
+
"""Keep the reply, its message and its source, then run the source on the world."""
|
|
171
|
+
said = nu.Dict.of(role="assistant", content=text)
|
|
172
|
+
keep = (
|
|
173
|
+
session.reply.set(text) >> session.messages.append(said) >> session.program.set(source)
|
|
174
|
+
)
|
|
175
|
+
ran = attempt(source, reply=text, rewrite=store.rewrite)
|
|
176
|
+
return store.bracket(keep) >> nu.let(ran, observed)
|
|
177
|
+
|
|
178
|
+
def observed(out: Nu) -> Nu:
|
|
179
|
+
"""The program ran: read the world for the observation, then keep both."""
|
|
180
|
+
return nu.let(store.rewrite(observation(out, state)), lambda told: noted(out, told))
|
|
181
|
+
|
|
182
|
+
def noted(out: Nu, told: Nu) -> Nu:
|
|
183
|
+
"""Keep the outcome and send the observation as the next message."""
|
|
184
|
+
said = nu.Dict.of(role="user", content=told)
|
|
185
|
+
return store.bracket(session.outcome.set(out) >> session.messages.append(said))
|
|
186
|
+
|
|
187
|
+
return nu.let(store.bracket(session.messages.extract()), asked)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def agent(store: Store, backend: Backend, prompt: StrArg, *, state: Nu | None = None) -> Nu:
|
|
191
|
+
"""The whole run: seed, then turns until the model sets ``done``.
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
store: the store the run keeps its session and world in. Bind it
|
|
195
|
+
around this term with ``store.bound``.
|
|
196
|
+
backend: answers each turn and wraps the run.
|
|
197
|
+
prompt: the opening message.
|
|
198
|
+
state: what the model sees of the world after each run.
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
The run as one term, inside the backend's wrap.
|
|
202
|
+
"""
|
|
203
|
+
going = store.rewrite(store.run.done.not_())
|
|
204
|
+
body = seed(store, prompt) >> nu.WhileDo(going, turn(store, backend.ask, state=state))
|
|
205
|
+
return backend.wrap(body)
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""The model's program: taken out of the reply, built, run once, and told as an outcome.
|
|
2
|
+
|
|
3
|
+
Two terms, both evaluated inside the turn. A Python helper here would be a
|
|
4
|
+
hole in the tree, and a tree with a hole runs nowhere but the host process.
|
|
5
|
+
|
|
6
|
+
**Extract** takes the first fenced block. A model correcting itself quotes
|
|
7
|
+
the broken version first, and a model adding an illustration puts it last,
|
|
8
|
+
so neither choice is always right. First is the safer one: re-reading broken
|
|
9
|
+
code costs a turn, running an illustration does something nobody asked for.
|
|
10
|
+
A reply with no complete block is used whole, which is right for bare source
|
|
11
|
+
and wrong for prose. Extract cannot tell the two apart. Attempt can.
|
|
12
|
+
|
|
13
|
+
**Attempt** builds the program with ``LoadNu`` and runs it with ``Eval``,
|
|
14
|
+
once. The store's ``rewrite`` lands the built term on the world (on kv it
|
|
15
|
+
brackets it), and the yield is read out as plain data and repr'd inside that
|
|
16
|
+
landing, while the store is still open. Its own yield is the outcome record, and the error type picks the branch
|
|
17
|
+
that writes it: a ``ConstructionError`` is ``construct`` (or ``no_code``,
|
|
18
|
+
when the reply had no fence to begin with), anything else ``runtime``.
|
|
19
|
+
Nothing downstream reads labels out of text. The repr sits inside the inner
|
|
20
|
+
catch, so a failure is never repr'd and never arrives quoted.
|
|
21
|
+
|
|
22
|
+
A failure is the model's input, not the loop's crash. Source that builds and
|
|
23
|
+
then raises is as much the model's mistake as source that does not parse,
|
|
24
|
+
so both are caught and both become the next thing it reads.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
from typing import TYPE_CHECKING
|
|
30
|
+
|
|
31
|
+
import nu
|
|
32
|
+
import nu.prog
|
|
33
|
+
from nu.domains.shape.interactions import Extract
|
|
34
|
+
from nuagent.protocol import CONSTRUCT, NO_CODE, OK, RUNTIME
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
if TYPE_CHECKING:
|
|
38
|
+
from collections.abc import Callable
|
|
39
|
+
|
|
40
|
+
from nu.lang import Nu, StrArg
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
__all__ = ["FENCE", "Shown", "attempt", "extract", "fenced"]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
#: What opens and closes a block. Code sits at the odd indices of the split.
|
|
47
|
+
FENCE = "```"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def fenced(reply: StrArg) -> Nu:
|
|
51
|
+
"""Whether the reply holds a complete block: a Bool term, three chunks or more."""
|
|
52
|
+
return nu.Str(reply).split(FENCE).len() >= 3
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def extract(reply: StrArg) -> Nu:
|
|
56
|
+
"""The source in the reply's first fenced block, or the whole reply when there is none.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
reply: the model's text, a str or any Str-yielding term.
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
A ``Str`` term: the block's body with its ``python``/``py`` tag gone,
|
|
63
|
+
stripped. Fewer than three chunks means no complete block, and then
|
|
64
|
+
it is the reply itself, stripped.
|
|
65
|
+
"""
|
|
66
|
+
text = nu.Str(reply)
|
|
67
|
+
chunks = text.split(FENCE)
|
|
68
|
+
block = nu.Str(chunks[1]).removeprefix("python").removeprefix("py").strip()
|
|
69
|
+
return nu.Str(nu.If(fenced(reply), block, text.strip()))
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class Shown:
|
|
73
|
+
"""A ``LoadNu`` rewrite: the program's yield as plain data, repr'd, then landed.
|
|
74
|
+
|
|
75
|
+
A class rather than a closure so a load carrying it pickles.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
landing: the store's ``rewrite``. None leaves the term where it is.
|
|
79
|
+
|
|
80
|
+
Notes:
|
|
81
|
+
- The repr sits inside the landing, so a kv view the program yields
|
|
82
|
+
is read out while its bracket is open. Outside it, a view is a
|
|
83
|
+
lazy handle and its repr says nothing.
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
__slots__ = ("landing",)
|
|
87
|
+
|
|
88
|
+
def __init__(self, landing: Callable[[Nu], Nu] | None = None) -> None:
|
|
89
|
+
self.landing = landing
|
|
90
|
+
|
|
91
|
+
def __call__(self, term: Nu) -> Nu:
|
|
92
|
+
"""The term's yield, materialised and repr'd, on the world."""
|
|
93
|
+
shown = nu.Repr(Extract(term))
|
|
94
|
+
return shown if self.landing is None else self.landing(shown)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def attempt(
|
|
98
|
+
program: Nu, *, reply: StrArg | None = None, rewrite: Callable[[Nu], Nu] | None = None
|
|
99
|
+
) -> Nu:
|
|
100
|
+
"""Build the program, run it once, and yield what happened as an outcome record.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
program: the source, a ``ProgramRef`` or any Str-yielding term.
|
|
104
|
+
reply: the raw reply the source came from. Given, a construction
|
|
105
|
+
failure on a reply with no complete fence is ``no_code``, so a
|
|
106
|
+
model is never handed a parser error about its own English.
|
|
107
|
+
rewrite: the store's ``rewrite``, applied to the built term before
|
|
108
|
+
it runs. Omitted, the term runs as it was built.
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
A dict term, one of:
|
|
112
|
+
``{kind: ok, value}``, ``{kind: no_code}``,
|
|
113
|
+
``{kind: construct, message, line}`` (``line`` is None when the
|
|
114
|
+
failure has no source position), ``{kind: runtime, message}``.
|
|
115
|
+
"""
|
|
116
|
+
ran = nu.Dict.of(kind=OK, value=nu.Eval(nu.LoadNu(program, rewrite=Shown(rewrite))))
|
|
117
|
+
|
|
118
|
+
def built(err: Nu) -> Nu:
|
|
119
|
+
"""The construction failure, off the Diagnostic the error carries."""
|
|
120
|
+
diagnostic = err.exception.diagnostic
|
|
121
|
+
failed = nu.Dict.of(kind=CONSTRUCT, message=diagnostic.message, line=diagnostic.lineno)
|
|
122
|
+
if reply is None:
|
|
123
|
+
return failed
|
|
124
|
+
return nu.If(fenced(reply), failed, nu.Dict.of(kind=NO_CODE))
|
|
125
|
+
|
|
126
|
+
def raised(err: Nu) -> Nu:
|
|
127
|
+
"""The runtime failure: the error's type, then its text."""
|
|
128
|
+
typed = nu.Str(nu.GetAttr(nu.Type(err.exception), "__name__"))
|
|
129
|
+
return nu.Dict.of(kind=RUNTIME, message=typed + ": " + err)
|
|
130
|
+
|
|
131
|
+
return nu.TryCatch(
|
|
132
|
+
nu.TryCatch(ran, catch=built, errors=nu.prog.ConstructionError),
|
|
133
|
+
catch=raised,
|
|
134
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""What you swap around the loop: the store and the backend."""
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Backends behind one interface, a conversation in and text out.
|
|
2
|
+
|
|
3
|
+
Each is its own module with one class that satisfies
|
|
4
|
+
:class:`nuagent.core.Backend`, imported by name so a run pays only for the
|
|
5
|
+
backend it uses, and the core imports none of them:
|
|
6
|
+
|
|
7
|
+
- :mod:`.cc`: Claude Code, stateful. One session for the run, the newest
|
|
8
|
+
message per turn (``nuagent[cc]``).
|
|
9
|
+
- :mod:`.llm`: an OpenAI-compatible chat endpoint, stateless. The whole
|
|
10
|
+
conversation every turn (``nuagent[llm]``).
|
|
11
|
+
"""
|