nexusagent-runtime 0.4.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.
- nexusagent_runtime-0.4.0/LICENSE +21 -0
- nexusagent_runtime-0.4.0/PKG-INFO +192 -0
- nexusagent_runtime-0.4.0/README.md +160 -0
- nexusagent_runtime-0.4.0/pyproject.toml +78 -0
- nexusagent_runtime-0.4.0/setup.cfg +4 -0
- nexusagent_runtime-0.4.0/src/nexusagent/__init__.py +15 -0
- nexusagent_runtime-0.4.0/src/nexusagent/__main__.py +3 -0
- nexusagent_runtime-0.4.0/src/nexusagent/agent.py +175 -0
- nexusagent_runtime-0.4.0/src/nexusagent/application.py +11 -0
- nexusagent_runtime-0.4.0/src/nexusagent/calculator_tool.py +42 -0
- nexusagent_runtime-0.4.0/src/nexusagent/config.py +81 -0
- nexusagent_runtime-0.4.0/src/nexusagent/echo_tool.py +11 -0
- nexusagent_runtime-0.4.0/src/nexusagent/factory.py +13 -0
- nexusagent_runtime-0.4.0/src/nexusagent/http_provider.py +189 -0
- nexusagent_runtime-0.4.0/src/nexusagent/logging.py +60 -0
- nexusagent_runtime-0.4.0/src/nexusagent/main.py +75 -0
- nexusagent_runtime-0.4.0/src/nexusagent/provider.py +101 -0
- nexusagent_runtime-0.4.0/src/nexusagent/providers.py +58 -0
- nexusagent_runtime-0.4.0/src/nexusagent/py.typed +0 -0
- nexusagent_runtime-0.4.0/src/nexusagent/runtime.py +54 -0
- nexusagent_runtime-0.4.0/src/nexusagent/runtime_factory.py +12 -0
- nexusagent_runtime-0.4.0/src/nexusagent/tool.py +24 -0
- nexusagent_runtime-0.4.0/src/nexusagent/tool_executor.py +26 -0
- nexusagent_runtime-0.4.0/src/nexusagent/tool_factory.py +14 -0
- nexusagent_runtime-0.4.0/src/nexusagent/tool_registry.py +49 -0
- nexusagent_runtime-0.4.0/src/nexusagent/uppercase_tool.py +11 -0
- nexusagent_runtime-0.4.0/src/nexusagent_runtime.egg-info/PKG-INFO +192 -0
- nexusagent_runtime-0.4.0/src/nexusagent_runtime.egg-info/SOURCES.txt +53 -0
- nexusagent_runtime-0.4.0/src/nexusagent_runtime.egg-info/dependency_links.txt +1 -0
- nexusagent_runtime-0.4.0/src/nexusagent_runtime.egg-info/entry_points.txt +2 -0
- nexusagent_runtime-0.4.0/src/nexusagent_runtime.egg-info/requires.txt +8 -0
- nexusagent_runtime-0.4.0/src/nexusagent_runtime.egg-info/top_level.txt +1 -0
- nexusagent_runtime-0.4.0/tests/test_agent.py +731 -0
- nexusagent_runtime-0.4.0/tests/test_application.py +239 -0
- nexusagent_runtime-0.4.0/tests/test_architecture.py +321 -0
- nexusagent_runtime-0.4.0/tests/test_calculator_tool.py +248 -0
- nexusagent_runtime-0.4.0/tests/test_config.py +249 -0
- nexusagent_runtime-0.4.0/tests/test_echo_tool.py +81 -0
- nexusagent_runtime-0.4.0/tests/test_factory.py +74 -0
- nexusagent_runtime-0.4.0/tests/test_http_provider.py +329 -0
- nexusagent_runtime-0.4.0/tests/test_http_provider_tools.py +100 -0
- nexusagent_runtime-0.4.0/tests/test_http_tool_loop_integration.py +70 -0
- nexusagent_runtime-0.4.0/tests/test_init.py +123 -0
- nexusagent_runtime-0.4.0/tests/test_logging.py +81 -0
- nexusagent_runtime-0.4.0/tests/test_main.py +893 -0
- nexusagent_runtime-0.4.0/tests/test_provider.py +231 -0
- nexusagent_runtime-0.4.0/tests/test_providers.py +78 -0
- nexusagent_runtime-0.4.0/tests/test_runtime.py +648 -0
- nexusagent_runtime-0.4.0/tests/test_runtime_factory.py +193 -0
- nexusagent_runtime-0.4.0/tests/test_tool.py +166 -0
- nexusagent_runtime-0.4.0/tests/test_tool_executor.py +349 -0
- nexusagent_runtime-0.4.0/tests/test_tool_factory.py +117 -0
- nexusagent_runtime-0.4.0/tests/test_tool_loop.py +289 -0
- nexusagent_runtime-0.4.0/tests/test_tool_registry.py +422 -0
- nexusagent_runtime-0.4.0/tests/test_uppercase_tool.py +111 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Abolfazl RWM
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nexusagent-runtime
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: A small, modular AI agent runtime in Python — agents, providers, tools and a readable core.
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Project-URL: Homepage, https://github.com/Abolfazlrwm/nexusagent
|
|
7
|
+
Project-URL: Repository, https://github.com/Abolfazlrwm/nexusagent
|
|
8
|
+
Project-URL: Changelog, https://github.com/Abolfazlrwm/nexusagent/blob/main/CHANGELOG.md
|
|
9
|
+
Project-URL: Issues, https://github.com/Abolfazlrwm/nexusagent/issues
|
|
10
|
+
Keywords: ai-agents,agent-runtime,llm,tools,providers
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
|
+
Classifier: Typing :: Typed
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
26
|
+
Requires-Dist: pytest-cov>=5.0; extra == "dev"
|
|
27
|
+
Requires-Dist: mypy>=1.10; extra == "dev"
|
|
28
|
+
Requires-Dist: ruff>=0.6; extra == "dev"
|
|
29
|
+
Requires-Dist: pre-commit>=3.7; extra == "dev"
|
|
30
|
+
Requires-Dist: build>=1.2; extra == "dev"
|
|
31
|
+
Dynamic: license-file
|
|
32
|
+
|
|
33
|
+
# NexusAgent
|
|
34
|
+
|
|
35
|
+
[](https://github.com/Abolfazlrwm/nexusagent/actions/workflows/ci.yml)
|
|
36
|
+
[](https://github.com/Abolfazlrwm/nexusagent/actions/workflows/ci.yml)
|
|
37
|
+
[](https://pypi.org/project/nexusagent-runtime/)
|
|
38
|
+
[](https://pypi.org/project/nexusagent-runtime/)
|
|
39
|
+
[](LICENSE)
|
|
40
|
+
|
|
41
|
+
**A modular runtime for AI agents in Python — zero runtime dependencies, a small readable core, and an honest feature list.**
|
|
42
|
+
|
|
43
|
+
Most agent frameworks hand you a galaxy of abstractions. NexusAgent is the opposite: ~500 lines of typed, contract-tested core that you can read in one sitting — providers, tools, a bounded autonomous tool loop, and a CLI. Nothing hidden, nothing magic.
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
from nexusagent import create_application_runtime
|
|
47
|
+
|
|
48
|
+
runtime = create_application_runtime() # offline `fake` provider by default
|
|
49
|
+
|
|
50
|
+
result = runtime.run("Hello NexusAgent")
|
|
51
|
+
print(result.output) # fake response: Hello NexusAgent
|
|
52
|
+
print(result.success) # True
|
|
53
|
+
|
|
54
|
+
print(runtime.execute_tool("calculator", "2 + 3")) # 5
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Why NexusAgent
|
|
58
|
+
|
|
59
|
+
- **Zero runtime dependencies** — the whole core is the Python standard library. `pip install nexusagent-runtime` pulls nothing else.
|
|
60
|
+
- **Readable by design** — the core is small enough to audit in an afternoon; architecture rules are enforced by tests, not comments.
|
|
61
|
+
- **Honest** — the feature list below says what exists *and what doesn't*. No fake benchmarks, no aspirational docs.
|
|
62
|
+
- **Typed** — ships `py.typed`; the source itself passes `mypy --strict`.
|
|
63
|
+
- **Tested** — 600+ contract tests, ≥95% coverage floor enforced in CI.
|
|
64
|
+
|
|
65
|
+
## Features
|
|
66
|
+
|
|
67
|
+
| Capability | Status |
|
|
68
|
+
| --- | --- |
|
|
69
|
+
| Provider abstraction (bring any backend) | ✅ |
|
|
70
|
+
| Autonomous tool loop, bounded by `max_iterations` | ✅ |
|
|
71
|
+
| Vendor-neutral HTTP wire protocol | ✅ [docs/protocol.md](docs/protocol.md) |
|
|
72
|
+
| Built-in tools: `echo`, `uppercase`, `calculator` | ✅ |
|
|
73
|
+
| Custom tools / custom providers via small ABCs | ✅ [guides](docs/) |
|
|
74
|
+
| Configuration via `NEXUS_*` environment variables | ✅ |
|
|
75
|
+
| CLI (`nexusagent`, `python -m nexusagent`) | ✅ |
|
|
76
|
+
| Fully typed (`py.typed`), strict mypy clean | ✅ |
|
|
77
|
+
| Async execution / streaming | ❌ |
|
|
78
|
+
| Memory, planning, multi-agent orchestration | ❌ |
|
|
79
|
+
| Vendor adapters (OpenAI, Anthropic, …) | ❌ |
|
|
80
|
+
| Retries / backoff on HTTP failures | ❌ |
|
|
81
|
+
|
|
82
|
+
## Architecture
|
|
83
|
+
|
|
84
|
+
```mermaid
|
|
85
|
+
flowchart LR
|
|
86
|
+
U["your code / CLI"] --> R["Runtime"]
|
|
87
|
+
R --> A["Agent"]
|
|
88
|
+
A -- "generate_response()" --> P["Provider<br/>(fake · scripted · http)"]
|
|
89
|
+
P -- "TextResponse" --> A
|
|
90
|
+
P -- "ToolCallRequest" --> A
|
|
91
|
+
A --> X["ToolExecutor"]
|
|
92
|
+
X --> T["ToolRegistry<br/>echo · uppercase · calculator"]
|
|
93
|
+
X -- "ToolResult" --> A
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
The Provider answers with text or requests a tool; the Agent runs the tool
|
|
97
|
+
and feeds the result back, repeating until a text answer or the iteration
|
|
98
|
+
limit. Deeper details: [docs/architecture.md](docs/architecture.md) ·
|
|
99
|
+
concepts: [docs/concepts.md](docs/concepts.md) · decisions:
|
|
100
|
+
[docs/adr/](docs/adr/).
|
|
101
|
+
|
|
102
|
+
## Installation
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
pip install nexusagent-runtime
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
> The **import name** is `nexusagent`; the **PyPI distribution** is
|
|
109
|
+
> `nexusagent-runtime` — the exact name `nexusagent` is blocked on PyPI by an
|
|
110
|
+
> unrelated placeholder package (`nexus-agent`), see
|
|
111
|
+
> [ADR 0004](docs/adr/0004-pypi-distribution-name.md).
|
|
112
|
+
|
|
113
|
+
Requires Python 3.10–3.13. From source:
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
git clone https://github.com/Abolfazlrwm/nexusagent
|
|
117
|
+
cd nexusagent
|
|
118
|
+
pip install -e ".[dev]"
|
|
119
|
+
python scripts/check.py # lint + format + mypy --strict + tests
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Usage
|
|
123
|
+
|
|
124
|
+
### Agent with tools
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
from nexusagent import create_application_runtime
|
|
128
|
+
|
|
129
|
+
runtime = create_application_runtime()
|
|
130
|
+
|
|
131
|
+
print(runtime.list_tools())
|
|
132
|
+
# [EchoTool, UppercaseTool, CalculatorTool]
|
|
133
|
+
|
|
134
|
+
# The provider decides when to request a tool; the agent runs the loop.
|
|
135
|
+
result = runtime.run("What is 19 + 23?")
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### The tool loop, end to end, offline
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
python examples/tool_loop_demo.py
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Starts a local "model backend" speaking the [HTTP protocol](docs/protocol.md),
|
|
145
|
+
requests the calculator, and turns the result into a final answer — no API
|
|
146
|
+
key, no internet.
|
|
147
|
+
|
|
148
|
+
### CLI
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
nexusagent "your prompt" # or: python -m nexusagent "your prompt"
|
|
152
|
+
nexusagent tool list
|
|
153
|
+
nexusagent tool run calculator "2 + 3"
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Configuration
|
|
157
|
+
|
|
158
|
+
| Variable | Meaning | Default |
|
|
159
|
+
| --- | --- | --- |
|
|
160
|
+
| `NEXUS_PROVIDER` | `fake` or `http` | `fake` |
|
|
161
|
+
| `NEXUS_ENDPOINT` | HTTP provider URL | — |
|
|
162
|
+
| `NEXUS_MODEL` | model identifier passed to the provider | — |
|
|
163
|
+
| `NEXUS_API_KEY` | sent as `Bearer` token (never logged, masked in reprs) | — |
|
|
164
|
+
| `NEXUS_TIMEOUT` | HTTP timeout in seconds | `30` |
|
|
165
|
+
|
|
166
|
+
## Examples
|
|
167
|
+
|
|
168
|
+
All examples run offline, with no API keys:
|
|
169
|
+
|
|
170
|
+
- [`examples/basic.py`](examples/basic.py) — smallest possible run
|
|
171
|
+
- [`examples/custom_tool.py`](examples/custom_tool.py) — define and register your own tool
|
|
172
|
+
- [`examples/tool_loop_demo.py`](examples/tool_loop_demo.py) — the autonomous tool loop over local HTTP
|
|
173
|
+
- [`examples/http_provider_local.py`](examples/http_provider_local.py) — `http` provider against a local backend
|
|
174
|
+
|
|
175
|
+
## Documentation
|
|
176
|
+
|
|
177
|
+
- [Architecture](docs/architecture.md) — layers, module map, dependency rules
|
|
178
|
+
- [Concepts](docs/concepts.md) — Agent, Provider, Tool, Runtime, Settings
|
|
179
|
+
- [Guides](docs/) — write a custom [provider](docs/guides/custom-provider.md) or [tool](docs/guides/custom-tool.md)
|
|
180
|
+
- [HTTP protocol](docs/protocol.md) — wire format for backends
|
|
181
|
+
- [ADRs](docs/adr/) — architecture decision records
|
|
182
|
+
- [Security policy](SECURITY.md) · [Changelog](CHANGELOG.md) · [Contributing](CONTRIBUTING.md)
|
|
183
|
+
|
|
184
|
+
## Project status
|
|
185
|
+
|
|
186
|
+
Actively developed, pre-1.0. See [PROJECT_STATUS.md](PROJECT_STATUS.md) for
|
|
187
|
+
a candid snapshot of what works and what the next steps are. Breaking public
|
|
188
|
+
API changes require an ADR and are discussed in the open.
|
|
189
|
+
|
|
190
|
+
## License
|
|
191
|
+
|
|
192
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# NexusAgent
|
|
2
|
+
|
|
3
|
+
[](https://github.com/Abolfazlrwm/nexusagent/actions/workflows/ci.yml)
|
|
4
|
+
[](https://github.com/Abolfazlrwm/nexusagent/actions/workflows/ci.yml)
|
|
5
|
+
[](https://pypi.org/project/nexusagent-runtime/)
|
|
6
|
+
[](https://pypi.org/project/nexusagent-runtime/)
|
|
7
|
+
[](LICENSE)
|
|
8
|
+
|
|
9
|
+
**A modular runtime for AI agents in Python — zero runtime dependencies, a small readable core, and an honest feature list.**
|
|
10
|
+
|
|
11
|
+
Most agent frameworks hand you a galaxy of abstractions. NexusAgent is the opposite: ~500 lines of typed, contract-tested core that you can read in one sitting — providers, tools, a bounded autonomous tool loop, and a CLI. Nothing hidden, nothing magic.
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from nexusagent import create_application_runtime
|
|
15
|
+
|
|
16
|
+
runtime = create_application_runtime() # offline `fake` provider by default
|
|
17
|
+
|
|
18
|
+
result = runtime.run("Hello NexusAgent")
|
|
19
|
+
print(result.output) # fake response: Hello NexusAgent
|
|
20
|
+
print(result.success) # True
|
|
21
|
+
|
|
22
|
+
print(runtime.execute_tool("calculator", "2 + 3")) # 5
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Why NexusAgent
|
|
26
|
+
|
|
27
|
+
- **Zero runtime dependencies** — the whole core is the Python standard library. `pip install nexusagent-runtime` pulls nothing else.
|
|
28
|
+
- **Readable by design** — the core is small enough to audit in an afternoon; architecture rules are enforced by tests, not comments.
|
|
29
|
+
- **Honest** — the feature list below says what exists *and what doesn't*. No fake benchmarks, no aspirational docs.
|
|
30
|
+
- **Typed** — ships `py.typed`; the source itself passes `mypy --strict`.
|
|
31
|
+
- **Tested** — 600+ contract tests, ≥95% coverage floor enforced in CI.
|
|
32
|
+
|
|
33
|
+
## Features
|
|
34
|
+
|
|
35
|
+
| Capability | Status |
|
|
36
|
+
| --- | --- |
|
|
37
|
+
| Provider abstraction (bring any backend) | ✅ |
|
|
38
|
+
| Autonomous tool loop, bounded by `max_iterations` | ✅ |
|
|
39
|
+
| Vendor-neutral HTTP wire protocol | ✅ [docs/protocol.md](docs/protocol.md) |
|
|
40
|
+
| Built-in tools: `echo`, `uppercase`, `calculator` | ✅ |
|
|
41
|
+
| Custom tools / custom providers via small ABCs | ✅ [guides](docs/) |
|
|
42
|
+
| Configuration via `NEXUS_*` environment variables | ✅ |
|
|
43
|
+
| CLI (`nexusagent`, `python -m nexusagent`) | ✅ |
|
|
44
|
+
| Fully typed (`py.typed`), strict mypy clean | ✅ |
|
|
45
|
+
| Async execution / streaming | ❌ |
|
|
46
|
+
| Memory, planning, multi-agent orchestration | ❌ |
|
|
47
|
+
| Vendor adapters (OpenAI, Anthropic, …) | ❌ |
|
|
48
|
+
| Retries / backoff on HTTP failures | ❌ |
|
|
49
|
+
|
|
50
|
+
## Architecture
|
|
51
|
+
|
|
52
|
+
```mermaid
|
|
53
|
+
flowchart LR
|
|
54
|
+
U["your code / CLI"] --> R["Runtime"]
|
|
55
|
+
R --> A["Agent"]
|
|
56
|
+
A -- "generate_response()" --> P["Provider<br/>(fake · scripted · http)"]
|
|
57
|
+
P -- "TextResponse" --> A
|
|
58
|
+
P -- "ToolCallRequest" --> A
|
|
59
|
+
A --> X["ToolExecutor"]
|
|
60
|
+
X --> T["ToolRegistry<br/>echo · uppercase · calculator"]
|
|
61
|
+
X -- "ToolResult" --> A
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
The Provider answers with text or requests a tool; the Agent runs the tool
|
|
65
|
+
and feeds the result back, repeating until a text answer or the iteration
|
|
66
|
+
limit. Deeper details: [docs/architecture.md](docs/architecture.md) ·
|
|
67
|
+
concepts: [docs/concepts.md](docs/concepts.md) · decisions:
|
|
68
|
+
[docs/adr/](docs/adr/).
|
|
69
|
+
|
|
70
|
+
## Installation
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
pip install nexusagent-runtime
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
> The **import name** is `nexusagent`; the **PyPI distribution** is
|
|
77
|
+
> `nexusagent-runtime` — the exact name `nexusagent` is blocked on PyPI by an
|
|
78
|
+
> unrelated placeholder package (`nexus-agent`), see
|
|
79
|
+
> [ADR 0004](docs/adr/0004-pypi-distribution-name.md).
|
|
80
|
+
|
|
81
|
+
Requires Python 3.10–3.13. From source:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
git clone https://github.com/Abolfazlrwm/nexusagent
|
|
85
|
+
cd nexusagent
|
|
86
|
+
pip install -e ".[dev]"
|
|
87
|
+
python scripts/check.py # lint + format + mypy --strict + tests
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Usage
|
|
91
|
+
|
|
92
|
+
### Agent with tools
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
from nexusagent import create_application_runtime
|
|
96
|
+
|
|
97
|
+
runtime = create_application_runtime()
|
|
98
|
+
|
|
99
|
+
print(runtime.list_tools())
|
|
100
|
+
# [EchoTool, UppercaseTool, CalculatorTool]
|
|
101
|
+
|
|
102
|
+
# The provider decides when to request a tool; the agent runs the loop.
|
|
103
|
+
result = runtime.run("What is 19 + 23?")
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### The tool loop, end to end, offline
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
python examples/tool_loop_demo.py
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Starts a local "model backend" speaking the [HTTP protocol](docs/protocol.md),
|
|
113
|
+
requests the calculator, and turns the result into a final answer — no API
|
|
114
|
+
key, no internet.
|
|
115
|
+
|
|
116
|
+
### CLI
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
nexusagent "your prompt" # or: python -m nexusagent "your prompt"
|
|
120
|
+
nexusagent tool list
|
|
121
|
+
nexusagent tool run calculator "2 + 3"
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Configuration
|
|
125
|
+
|
|
126
|
+
| Variable | Meaning | Default |
|
|
127
|
+
| --- | --- | --- |
|
|
128
|
+
| `NEXUS_PROVIDER` | `fake` or `http` | `fake` |
|
|
129
|
+
| `NEXUS_ENDPOINT` | HTTP provider URL | — |
|
|
130
|
+
| `NEXUS_MODEL` | model identifier passed to the provider | — |
|
|
131
|
+
| `NEXUS_API_KEY` | sent as `Bearer` token (never logged, masked in reprs) | — |
|
|
132
|
+
| `NEXUS_TIMEOUT` | HTTP timeout in seconds | `30` |
|
|
133
|
+
|
|
134
|
+
## Examples
|
|
135
|
+
|
|
136
|
+
All examples run offline, with no API keys:
|
|
137
|
+
|
|
138
|
+
- [`examples/basic.py`](examples/basic.py) — smallest possible run
|
|
139
|
+
- [`examples/custom_tool.py`](examples/custom_tool.py) — define and register your own tool
|
|
140
|
+
- [`examples/tool_loop_demo.py`](examples/tool_loop_demo.py) — the autonomous tool loop over local HTTP
|
|
141
|
+
- [`examples/http_provider_local.py`](examples/http_provider_local.py) — `http` provider against a local backend
|
|
142
|
+
|
|
143
|
+
## Documentation
|
|
144
|
+
|
|
145
|
+
- [Architecture](docs/architecture.md) — layers, module map, dependency rules
|
|
146
|
+
- [Concepts](docs/concepts.md) — Agent, Provider, Tool, Runtime, Settings
|
|
147
|
+
- [Guides](docs/) — write a custom [provider](docs/guides/custom-provider.md) or [tool](docs/guides/custom-tool.md)
|
|
148
|
+
- [HTTP protocol](docs/protocol.md) — wire format for backends
|
|
149
|
+
- [ADRs](docs/adr/) — architecture decision records
|
|
150
|
+
- [Security policy](SECURITY.md) · [Changelog](CHANGELOG.md) · [Contributing](CONTRIBUTING.md)
|
|
151
|
+
|
|
152
|
+
## Project status
|
|
153
|
+
|
|
154
|
+
Actively developed, pre-1.0. See [PROJECT_STATUS.md](PROJECT_STATUS.md) for
|
|
155
|
+
a candid snapshot of what works and what the next steps are. Breaking public
|
|
156
|
+
API changes require an ADR and are discussed in the open.
|
|
157
|
+
|
|
158
|
+
## License
|
|
159
|
+
|
|
160
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "nexusagent-runtime"
|
|
7
|
+
description = "A small, modular AI agent runtime in Python — agents, providers, tools and a readable core."
|
|
8
|
+
readme = "README.md"
|
|
9
|
+
license = "MIT"
|
|
10
|
+
license-files = ["LICENSE"]
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
dependencies = []
|
|
13
|
+
dynamic = ["version"]
|
|
14
|
+
keywords = ["ai-agents", "agent-runtime", "llm", "tools", "providers"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 4 - Beta",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"Operating System :: OS Independent",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3.10",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Programming Language :: Python :: 3.13",
|
|
24
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
25
|
+
"Typing :: Typed",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[project.scripts]
|
|
29
|
+
nexusagent = "nexusagent.main:main"
|
|
30
|
+
|
|
31
|
+
[project.urls]
|
|
32
|
+
Homepage = "https://github.com/Abolfazlrwm/nexusagent"
|
|
33
|
+
Repository = "https://github.com/Abolfazlrwm/nexusagent"
|
|
34
|
+
Changelog = "https://github.com/Abolfazlrwm/nexusagent/blob/main/CHANGELOG.md"
|
|
35
|
+
Issues = "https://github.com/Abolfazlrwm/nexusagent/issues"
|
|
36
|
+
|
|
37
|
+
[project.optional-dependencies]
|
|
38
|
+
dev = [
|
|
39
|
+
"pytest>=8.0",
|
|
40
|
+
"pytest-cov>=5.0",
|
|
41
|
+
"mypy>=1.10",
|
|
42
|
+
"ruff>=0.6",
|
|
43
|
+
"pre-commit>=3.7",
|
|
44
|
+
"build>=1.2",
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
[tool.setuptools.packages.find]
|
|
48
|
+
where = ["src"]
|
|
49
|
+
|
|
50
|
+
[tool.setuptools.package-data]
|
|
51
|
+
nexusagent = ["py.typed"]
|
|
52
|
+
|
|
53
|
+
[tool.setuptools.dynamic]
|
|
54
|
+
version = { attr = "nexusagent.__version__" }
|
|
55
|
+
|
|
56
|
+
[tool.pytest.ini_options]
|
|
57
|
+
testpaths = ["tests"]
|
|
58
|
+
addopts = "--cov=nexusagent --cov-report=term-missing"
|
|
59
|
+
|
|
60
|
+
[tool.coverage.report]
|
|
61
|
+
fail_under = 95
|
|
62
|
+
show_missing = true
|
|
63
|
+
|
|
64
|
+
[tool.mypy]
|
|
65
|
+
strict = true
|
|
66
|
+
files = ["src"]
|
|
67
|
+
python_version = "3.10"
|
|
68
|
+
warn_unused_ignores = true
|
|
69
|
+
|
|
70
|
+
[tool.ruff]
|
|
71
|
+
line-length = 100
|
|
72
|
+
src = ["src", "tests"]
|
|
73
|
+
|
|
74
|
+
[tool.ruff.lint]
|
|
75
|
+
select = ["E", "F", "W", "I", "UP", "B", "BLE", "C4", "SIM", "RUF"]
|
|
76
|
+
|
|
77
|
+
[tool.ruff.lint.isort]
|
|
78
|
+
known-first-party = ["nexusagent"]
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from nexusagent.agent import Agent, AgentResult
|
|
2
|
+
from nexusagent.application import create_application_runtime
|
|
3
|
+
from nexusagent.config import Settings
|
|
4
|
+
from nexusagent.runtime import Runtime, create_runtime
|
|
5
|
+
|
|
6
|
+
__version__ = "0.4.0"
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"Agent",
|
|
10
|
+
"AgentResult",
|
|
11
|
+
"Runtime",
|
|
12
|
+
"Settings",
|
|
13
|
+
"create_application_runtime",
|
|
14
|
+
"create_runtime",
|
|
15
|
+
]
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""Agent Core for NexusAgent.
|
|
2
|
+
|
|
3
|
+
Defines the contract for running an agent: input text in, an AgentResult out.
|
|
4
|
+
The Agent delegates generation to a Provider. When it is given a ToolRegistry
|
|
5
|
+
and a ToolExecutor it also runs the autonomous tool loop:
|
|
6
|
+
|
|
7
|
+
provider responds -> if it asks for a tool, run it -> feed the result back
|
|
8
|
+
-> provider responds again -> ... until it answers in plain text
|
|
9
|
+
|
|
10
|
+
The loop is bounded by ``max_iterations`` so a misbehaving backend can never
|
|
11
|
+
keep the agent running forever. Explicit ``execute_tool()`` remains available.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
|
|
18
|
+
from nexusagent.logging import get_logger
|
|
19
|
+
from nexusagent.provider import (
|
|
20
|
+
Provider,
|
|
21
|
+
ProviderResponse,
|
|
22
|
+
TextResponse,
|
|
23
|
+
ToolCallRequest,
|
|
24
|
+
ToolResult,
|
|
25
|
+
ToolSpec,
|
|
26
|
+
)
|
|
27
|
+
from nexusagent.tool_executor import ToolExecutionError, ToolExecutor
|
|
28
|
+
from nexusagent.tool_registry import ToolRegistry
|
|
29
|
+
|
|
30
|
+
DEFAULT_MAX_ITERATIONS = 10
|
|
31
|
+
|
|
32
|
+
_logger = get_logger("nexusagent.agent")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(frozen=True)
|
|
36
|
+
class AgentResult:
|
|
37
|
+
"""The result of a single agent run."""
|
|
38
|
+
|
|
39
|
+
output: str
|
|
40
|
+
success: bool
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class Agent:
|
|
44
|
+
def __init__(
|
|
45
|
+
self,
|
|
46
|
+
provider: Provider,
|
|
47
|
+
tool_registry: ToolRegistry | None = None,
|
|
48
|
+
tool_executor: ToolExecutor | None = None,
|
|
49
|
+
max_iterations: int = DEFAULT_MAX_ITERATIONS,
|
|
50
|
+
) -> None:
|
|
51
|
+
if isinstance(max_iterations, bool) or not isinstance(max_iterations, int):
|
|
52
|
+
raise TypeError("max_iterations must be an integer")
|
|
53
|
+
|
|
54
|
+
if max_iterations < 1:
|
|
55
|
+
raise ValueError("max_iterations must be at least 1")
|
|
56
|
+
|
|
57
|
+
self.provider = provider
|
|
58
|
+
self.tool_registry = tool_registry
|
|
59
|
+
self.tool_executor = tool_executor
|
|
60
|
+
self.max_iterations = max_iterations
|
|
61
|
+
|
|
62
|
+
def run(self, input_text: str) -> AgentResult:
|
|
63
|
+
"""Run the agent on the given input and return an AgentResult.
|
|
64
|
+
|
|
65
|
+
If the Provider requests tools and this Agent has a ToolRegistry and a
|
|
66
|
+
ToolExecutor, the requested tools are executed and their results are
|
|
67
|
+
fed back to the Provider until it returns a plain text answer or
|
|
68
|
+
``max_iterations`` provider calls have been made.
|
|
69
|
+
"""
|
|
70
|
+
if not isinstance(input_text, str):
|
|
71
|
+
raise TypeError("input_text must be a string")
|
|
72
|
+
|
|
73
|
+
if not input_text.strip():
|
|
74
|
+
raise ValueError("input_text must not be empty or whitespace-only")
|
|
75
|
+
|
|
76
|
+
can_use_tools = self.tool_registry is not None and self.tool_executor is not None
|
|
77
|
+
tools = self._tool_specs() if can_use_tools else ()
|
|
78
|
+
tool_results: list[ToolResult] = []
|
|
79
|
+
|
|
80
|
+
for iteration in range(1, self.max_iterations + 1):
|
|
81
|
+
try:
|
|
82
|
+
response = self._ask_provider(input_text, tools, tuple(tool_results))
|
|
83
|
+
except Exception as exc: # noqa: BLE001
|
|
84
|
+
return AgentResult(output=f"provider error: {exc}", success=False)
|
|
85
|
+
|
|
86
|
+
if isinstance(response, TextResponse):
|
|
87
|
+
_logger.debug("iteration %d: provider answered with text", iteration)
|
|
88
|
+
return AgentResult(output=response.text, success=True)
|
|
89
|
+
|
|
90
|
+
if not isinstance(response, ToolCallRequest):
|
|
91
|
+
return AgentResult(
|
|
92
|
+
output=f"provider error: unexpected response type {type(response).__name__}",
|
|
93
|
+
success=False,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
if not can_use_tools:
|
|
97
|
+
return AgentResult(
|
|
98
|
+
output=(
|
|
99
|
+
f"provider requested tool {response.tool_name!r} "
|
|
100
|
+
"but the agent has no tools configured"
|
|
101
|
+
),
|
|
102
|
+
success=False,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
_logger.info("iteration %d: provider requested tool %r", iteration, response.tool_name)
|
|
106
|
+
tool_results.append(self._run_tool_call(response))
|
|
107
|
+
|
|
108
|
+
return AgentResult(
|
|
109
|
+
output=f"agent stopped: exceeded the maximum of {self.max_iterations} iterations",
|
|
110
|
+
success=False,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
def execute_tool(self, tool_name: str, input_data: str) -> str:
|
|
114
|
+
"""Execute a specific, explicitly named tool and return its output."""
|
|
115
|
+
if self.tool_registry is None:
|
|
116
|
+
raise RuntimeError("Agent requires a ToolRegistry to execute tools")
|
|
117
|
+
|
|
118
|
+
if self.tool_executor is None:
|
|
119
|
+
raise RuntimeError("Agent requires a ToolExecutor to execute tools")
|
|
120
|
+
|
|
121
|
+
tool = self.tool_registry.get(tool_name)
|
|
122
|
+
return self.tool_executor.execute(tool, input_data)
|
|
123
|
+
|
|
124
|
+
def has_tool(self, tool_name: str) -> bool:
|
|
125
|
+
"""Return whether a tool is currently available through the Agent."""
|
|
126
|
+
if self.tool_registry is None:
|
|
127
|
+
raise RuntimeError("Agent requires a ToolRegistry to check tool availability")
|
|
128
|
+
|
|
129
|
+
return self.tool_registry.has(tool_name)
|
|
130
|
+
|
|
131
|
+
def _ask_provider(
|
|
132
|
+
self,
|
|
133
|
+
input_text: str,
|
|
134
|
+
tools: tuple[ToolSpec, ...],
|
|
135
|
+
tool_results: tuple[ToolResult, ...],
|
|
136
|
+
) -> ProviderResponse:
|
|
137
|
+
"""Ask the provider for its next response.
|
|
138
|
+
|
|
139
|
+
Any object with a ``generate(prompt) -> str`` method is accepted as a
|
|
140
|
+
provider; only ones that also define ``generate_response`` can request
|
|
141
|
+
tools.
|
|
142
|
+
"""
|
|
143
|
+
generate_response = getattr(self.provider, "generate_response", None)
|
|
144
|
+
if generate_response is None:
|
|
145
|
+
return TextResponse(self.provider.generate(input_text))
|
|
146
|
+
|
|
147
|
+
response: ProviderResponse = generate_response(
|
|
148
|
+
input_text, tools=tools, tool_results=tool_results
|
|
149
|
+
)
|
|
150
|
+
return response
|
|
151
|
+
|
|
152
|
+
def _tool_specs(self) -> tuple[ToolSpec, ...]:
|
|
153
|
+
assert self.tool_registry is not None
|
|
154
|
+
return tuple(ToolSpec(t.name, t.description) for t in self.tool_registry.list_tools())
|
|
155
|
+
|
|
156
|
+
def _run_tool_call(self, request: ToolCallRequest) -> ToolResult:
|
|
157
|
+
"""Run one requested tool. Failures become error results, never crashes.
|
|
158
|
+
|
|
159
|
+
The Provider (usually a model) is told what went wrong so it can retry
|
|
160
|
+
with better input, pick another tool, or explain the problem.
|
|
161
|
+
"""
|
|
162
|
+
try:
|
|
163
|
+
output = self.execute_tool(request.tool_name, request.tool_input)
|
|
164
|
+
except KeyError:
|
|
165
|
+
return self._error_result(request, f"Tool {request.tool_name!r} is not registered")
|
|
166
|
+
except (TypeError, ValueError, ToolExecutionError) as exc:
|
|
167
|
+
return self._error_result(request, str(exc))
|
|
168
|
+
|
|
169
|
+
_logger.debug("tool %r succeeded", request.tool_name)
|
|
170
|
+
return ToolResult(request.tool_name, request.tool_input, output)
|
|
171
|
+
|
|
172
|
+
@staticmethod
|
|
173
|
+
def _error_result(request: ToolCallRequest, message: str) -> ToolResult:
|
|
174
|
+
_logger.warning("tool %r failed: %s", request.tool_name, message)
|
|
175
|
+
return ToolResult(request.tool_name, request.tool_input, message, is_error=True)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from nexusagent.config import Settings
|
|
4
|
+
from nexusagent.runtime import Runtime
|
|
5
|
+
from nexusagent.runtime_factory import create_tool_runtime
|
|
6
|
+
|
|
7
|
+
__all__ = ["Runtime", "create_application_runtime"]
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def create_application_runtime(settings: Settings | None = None) -> Runtime:
|
|
11
|
+
return create_tool_runtime(settings)
|