zurvan 0.1.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.
- zurvan-0.1.0/.gitignore +35 -0
- zurvan-0.1.0/LICENSE +21 -0
- zurvan-0.1.0/PKG-INFO +140 -0
- zurvan-0.1.0/README.md +112 -0
- zurvan-0.1.0/examples/capabilities/README.md +21 -0
- zurvan-0.1.0/pyproject.toml +51 -0
- zurvan-0.1.0/src/zurvan/__init__.py +75 -0
- zurvan-0.1.0/src/zurvan/action.py +64 -0
- zurvan-0.1.0/src/zurvan/action_context.py +14 -0
- zurvan-0.1.0/src/zurvan/action_registry.py +54 -0
- zurvan-0.1.0/src/zurvan/action_transaction.py +38 -0
- zurvan-0.1.0/src/zurvan/agent.py +306 -0
- zurvan-0.1.0/src/zurvan/agent_function_calling_action_language.py +276 -0
- zurvan-0.1.0/src/zurvan/agent_function_calling_action_language_gemini.py +146 -0
- zurvan-0.1.0/src/zurvan/agent_function_calling_action_language_groq.py +147 -0
- zurvan-0.1.0/src/zurvan/agent_function_calling_action_language_openai.py +27 -0
- zurvan-0.1.0/src/zurvan/agent_json_action_language.py +116 -0
- zurvan-0.1.0/src/zurvan/agent_language.py +46 -0
- zurvan-0.1.0/src/zurvan/agent_registry.py +11 -0
- zurvan-0.1.0/src/zurvan/capabilities/__init__.py +2 -0
- zurvan-0.1.0/src/zurvan/capabilities/canary_capability.py +130 -0
- zurvan-0.1.0/src/zurvan/capabilities/enhanced_time_aware_capability.py +35 -0
- zurvan-0.1.0/src/zurvan/capabilities/time_aware_capability.py +64 -0
- zurvan-0.1.0/src/zurvan/capability.py +95 -0
- zurvan-0.1.0/src/zurvan/decorators.py +157 -0
- zurvan-0.1.0/src/zurvan/environment.py +40 -0
- zurvan-0.1.0/src/zurvan/goal.py +8 -0
- zurvan-0.1.0/src/zurvan/logger/local_text_file_logger.py +30 -0
- zurvan-0.1.0/src/zurvan/logger/logger.py +13 -0
- zurvan-0.1.0/src/zurvan/memory.py +62 -0
- zurvan-0.1.0/src/zurvan/progress_reporter.py +105 -0
- zurvan-0.1.0/src/zurvan/prompt.py +9 -0
- zurvan-0.1.0/src/zurvan/reversible_action.py +24 -0
- zurvan-0.1.0/src/zurvan/token_tracker.py +135 -0
zurvan-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.so
|
|
6
|
+
.Python
|
|
7
|
+
|
|
8
|
+
# Virtual environments
|
|
9
|
+
.venv/
|
|
10
|
+
venv/
|
|
11
|
+
env/
|
|
12
|
+
|
|
13
|
+
# Build artefacts
|
|
14
|
+
build/
|
|
15
|
+
dist/
|
|
16
|
+
*.egg-info/
|
|
17
|
+
*.egg
|
|
18
|
+
|
|
19
|
+
# Editor / OS
|
|
20
|
+
.DS_Store
|
|
21
|
+
.vscode/
|
|
22
|
+
.idea/
|
|
23
|
+
*.swp
|
|
24
|
+
|
|
25
|
+
# Tests / coverage
|
|
26
|
+
.pytest_cache/
|
|
27
|
+
.coverage
|
|
28
|
+
htmlcov/
|
|
29
|
+
.tox/
|
|
30
|
+
|
|
31
|
+
# Logs
|
|
32
|
+
.logs/
|
|
33
|
+
.venv
|
|
34
|
+
.vscode
|
|
35
|
+
.env
|
zurvan-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ali Afsahnoudeh
|
|
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.
|
zurvan-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: zurvan
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A small, composable Python framework for building goal-directed LLM agents (GAME loop + Capability hooks).
|
|
5
|
+
Project-URL: Homepage, https://github.com/aliafsahnoudeh/zurvan
|
|
6
|
+
Project-URL: Repository, https://github.com/aliafsahnoudeh/zurvan
|
|
7
|
+
Project-URL: Issues, https://github.com/aliafsahnoudeh/zurvan/issues
|
|
8
|
+
Author-email: Ali Afsah-Noudeh <aliafsah1988@gmail.com>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: agent,capability,framework,game-loop,llm
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
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
|
+
Requires-Python: >=3.11
|
|
21
|
+
Requires-Dist: litellm>=1.50
|
|
22
|
+
Requires-Dist: pydantic>=2
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: pytest-cov>=5; extra == 'dev'
|
|
25
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
26
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# zurvan
|
|
30
|
+
|
|
31
|
+
A small, composable Python framework for building goal-directed LLM agents.
|
|
32
|
+
|
|
33
|
+
## What it is
|
|
34
|
+
|
|
35
|
+
`zurvan` runs a **GAME loop** — Goals, Actions, Memory, Environment — over
|
|
36
|
+
an LLM. Behaviour like plan-first, time-aware, prompt-injection-resistant,
|
|
37
|
+
or progress-tracking is composed by passing a list of `Capability`
|
|
38
|
+
instances rather than by subclassing `Agent`. With no capabilities it
|
|
39
|
+
reduces to a plain Goals → Actions → Memory cycle.
|
|
40
|
+
|
|
41
|
+
LLM providers are abstracted behind `AgentLanguage`. Built-in subclasses
|
|
42
|
+
(via [LiteLLM](https://github.com/BerriAI/litellm)) cover OpenAI, Gemini,
|
|
43
|
+
and Groq.
|
|
44
|
+
|
|
45
|
+
## Install
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pip install zurvan
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Requires Python 3.11+.
|
|
52
|
+
|
|
53
|
+
## Quick start
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
from zurvan import (
|
|
57
|
+
Action,
|
|
58
|
+
ActionRegistry,
|
|
59
|
+
Agent,
|
|
60
|
+
AgentFunctionCallingActionLanguageOpenAI,
|
|
61
|
+
Environment,
|
|
62
|
+
Goal,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
def terminate(message: str) -> str:
|
|
66
|
+
return message
|
|
67
|
+
|
|
68
|
+
actions = ActionRegistry()
|
|
69
|
+
actions.register(Action(
|
|
70
|
+
name="terminate",
|
|
71
|
+
function=terminate,
|
|
72
|
+
description="End the conversation with a final message.",
|
|
73
|
+
parameters={
|
|
74
|
+
"type": "object",
|
|
75
|
+
"properties": {"message": {"type": "string"}},
|
|
76
|
+
"required": ["message"],
|
|
77
|
+
},
|
|
78
|
+
terminal=True,
|
|
79
|
+
))
|
|
80
|
+
|
|
81
|
+
agent = Agent(
|
|
82
|
+
goals=[Goal(priority=1, name="Greet", description="Greet the user warmly.")],
|
|
83
|
+
agent_language=AgentFunctionCallingActionLanguageOpenAI(model="openai/gpt-4o-mini"),
|
|
84
|
+
action_registry=actions,
|
|
85
|
+
environment=Environment(),
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
memory = agent.run("Say hi.")
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Pydantic-validated tool inputs
|
|
92
|
+
|
|
93
|
+
`Action` accepts either a JSON-Schema dict (`parameters=`) or a Pydantic
|
|
94
|
+
model (`input_model=`). With a model, the schema sent to the LLM is
|
|
95
|
+
derived from the model and the model's args are **validated before the
|
|
96
|
+
function runs** — a `ValidationError` is caught by `Environment` and
|
|
97
|
+
fed back to the LLM as a failed-tool result, so the model gets a chance
|
|
98
|
+
to retry with corrected args.
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
from typing import Literal
|
|
102
|
+
from pydantic import BaseModel
|
|
103
|
+
from zurvan import Action
|
|
104
|
+
|
|
105
|
+
class GetWeatherArgs(BaseModel):
|
|
106
|
+
city: str
|
|
107
|
+
unit: Literal["c", "f"] = "c"
|
|
108
|
+
|
|
109
|
+
def get_weather(city: str, unit: str = "c") -> str:
|
|
110
|
+
return f"It's nice in {city} ({unit}°)"
|
|
111
|
+
|
|
112
|
+
action = Action(
|
|
113
|
+
name="get_weather",
|
|
114
|
+
function=get_weather,
|
|
115
|
+
description="Look up the weather for a city.",
|
|
116
|
+
input_model=GetWeatherArgs,
|
|
117
|
+
)
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
`parameters=` and `input_model=` are mutually exclusive — pass exactly one.
|
|
121
|
+
|
|
122
|
+
## Capabilities
|
|
123
|
+
|
|
124
|
+
Capabilities hook into every loop phase (`init`, `start_agent_loop`,
|
|
125
|
+
`process_prompt`, `process_response`, `process_action`, `process_result`,
|
|
126
|
+
`process_new_memories`, `end_agent_loop`, `should_terminate`,
|
|
127
|
+
`terminate`). Compose them — don't subclass `Agent`.
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
from zurvan import Agent, CanaryCapability, TimeAwareCapability
|
|
131
|
+
|
|
132
|
+
agent = Agent(
|
|
133
|
+
...,
|
|
134
|
+
capabilities=[CanaryCapability(), TimeAwareCapability()],
|
|
135
|
+
)
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## License
|
|
139
|
+
|
|
140
|
+
MIT.
|
zurvan-0.1.0/README.md
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# zurvan
|
|
2
|
+
|
|
3
|
+
A small, composable Python framework for building goal-directed LLM agents.
|
|
4
|
+
|
|
5
|
+
## What it is
|
|
6
|
+
|
|
7
|
+
`zurvan` runs a **GAME loop** — Goals, Actions, Memory, Environment — over
|
|
8
|
+
an LLM. Behaviour like plan-first, time-aware, prompt-injection-resistant,
|
|
9
|
+
or progress-tracking is composed by passing a list of `Capability`
|
|
10
|
+
instances rather than by subclassing `Agent`. With no capabilities it
|
|
11
|
+
reduces to a plain Goals → Actions → Memory cycle.
|
|
12
|
+
|
|
13
|
+
LLM providers are abstracted behind `AgentLanguage`. Built-in subclasses
|
|
14
|
+
(via [LiteLLM](https://github.com/BerriAI/litellm)) cover OpenAI, Gemini,
|
|
15
|
+
and Groq.
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pip install zurvan
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Requires Python 3.11+.
|
|
24
|
+
|
|
25
|
+
## Quick start
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
from zurvan import (
|
|
29
|
+
Action,
|
|
30
|
+
ActionRegistry,
|
|
31
|
+
Agent,
|
|
32
|
+
AgentFunctionCallingActionLanguageOpenAI,
|
|
33
|
+
Environment,
|
|
34
|
+
Goal,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
def terminate(message: str) -> str:
|
|
38
|
+
return message
|
|
39
|
+
|
|
40
|
+
actions = ActionRegistry()
|
|
41
|
+
actions.register(Action(
|
|
42
|
+
name="terminate",
|
|
43
|
+
function=terminate,
|
|
44
|
+
description="End the conversation with a final message.",
|
|
45
|
+
parameters={
|
|
46
|
+
"type": "object",
|
|
47
|
+
"properties": {"message": {"type": "string"}},
|
|
48
|
+
"required": ["message"],
|
|
49
|
+
},
|
|
50
|
+
terminal=True,
|
|
51
|
+
))
|
|
52
|
+
|
|
53
|
+
agent = Agent(
|
|
54
|
+
goals=[Goal(priority=1, name="Greet", description="Greet the user warmly.")],
|
|
55
|
+
agent_language=AgentFunctionCallingActionLanguageOpenAI(model="openai/gpt-4o-mini"),
|
|
56
|
+
action_registry=actions,
|
|
57
|
+
environment=Environment(),
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
memory = agent.run("Say hi.")
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Pydantic-validated tool inputs
|
|
64
|
+
|
|
65
|
+
`Action` accepts either a JSON-Schema dict (`parameters=`) or a Pydantic
|
|
66
|
+
model (`input_model=`). With a model, the schema sent to the LLM is
|
|
67
|
+
derived from the model and the model's args are **validated before the
|
|
68
|
+
function runs** — a `ValidationError` is caught by `Environment` and
|
|
69
|
+
fed back to the LLM as a failed-tool result, so the model gets a chance
|
|
70
|
+
to retry with corrected args.
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
from typing import Literal
|
|
74
|
+
from pydantic import BaseModel
|
|
75
|
+
from zurvan import Action
|
|
76
|
+
|
|
77
|
+
class GetWeatherArgs(BaseModel):
|
|
78
|
+
city: str
|
|
79
|
+
unit: Literal["c", "f"] = "c"
|
|
80
|
+
|
|
81
|
+
def get_weather(city: str, unit: str = "c") -> str:
|
|
82
|
+
return f"It's nice in {city} ({unit}°)"
|
|
83
|
+
|
|
84
|
+
action = Action(
|
|
85
|
+
name="get_weather",
|
|
86
|
+
function=get_weather,
|
|
87
|
+
description="Look up the weather for a city.",
|
|
88
|
+
input_model=GetWeatherArgs,
|
|
89
|
+
)
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
`parameters=` and `input_model=` are mutually exclusive — pass exactly one.
|
|
93
|
+
|
|
94
|
+
## Capabilities
|
|
95
|
+
|
|
96
|
+
Capabilities hook into every loop phase (`init`, `start_agent_loop`,
|
|
97
|
+
`process_prompt`, `process_response`, `process_action`, `process_result`,
|
|
98
|
+
`process_new_memories`, `end_agent_loop`, `should_terminate`,
|
|
99
|
+
`terminate`). Compose them — don't subclass `Agent`.
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
from zurvan import Agent, CanaryCapability, TimeAwareCapability
|
|
103
|
+
|
|
104
|
+
agent = Agent(
|
|
105
|
+
...,
|
|
106
|
+
capabilities=[CanaryCapability(), TimeAwareCapability()],
|
|
107
|
+
)
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## License
|
|
111
|
+
|
|
112
|
+
MIT.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Example capabilities (illustrative — not tested, not shipped)
|
|
2
|
+
|
|
3
|
+
The capabilities in this directory are **sketches**, not part of the
|
|
4
|
+
installed package. They reference helpers (`prompt_llm`,
|
|
5
|
+
`action_context.get_action_registry()`) that don't exist in zurvan and
|
|
6
|
+
will not run as-is.
|
|
7
|
+
|
|
8
|
+
They live here as design references for users writing their own
|
|
9
|
+
capabilities — patterns for "plan first" and "track progress after
|
|
10
|
+
each iteration" — not as drop-in code.
|
|
11
|
+
|
|
12
|
+
If you want a working version, you'll need to:
|
|
13
|
+
|
|
14
|
+
1. Replace `prompt_llm(...)` calls with `agent.agent_language.generate_response(Prompt(messages=[...]))`.
|
|
15
|
+
2. Get the `ActionRegistry` from the agent (`agent.actions`) rather
|
|
16
|
+
than via `action_context.get_action_registry()` (no such method).
|
|
17
|
+
3. Decide where the plan/progress prompt should run — `init` for
|
|
18
|
+
plan-first, `end_agent_loop` for progress-tracking — and wire it
|
|
19
|
+
end-to-end.
|
|
20
|
+
|
|
21
|
+
Tested, working capabilities live in [src/zurvan/capabilities/](../../src/zurvan/capabilities/).
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.27"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "zurvan"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "A small, composable Python framework for building goal-directed LLM agents (GAME loop + Capability hooks)."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
requires-python = ">=3.11"
|
|
13
|
+
authors = [{ name = "Ali Afsah-Noudeh", email = "aliafsah1988@gmail.com" }]
|
|
14
|
+
keywords = ["agent", "llm", "framework", "game-loop", "capability"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 3 - Alpha",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.11",
|
|
20
|
+
"Programming Language :: Python :: 3.12",
|
|
21
|
+
"Programming Language :: Python :: 3.13",
|
|
22
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
23
|
+
"Operating System :: OS Independent",
|
|
24
|
+
]
|
|
25
|
+
dependencies = ["litellm>=1.50", "pydantic>=2"]
|
|
26
|
+
|
|
27
|
+
[project.optional-dependencies]
|
|
28
|
+
dev = ["pytest>=8", "pytest-cov>=5", "ruff>=0.6"]
|
|
29
|
+
|
|
30
|
+
[project.urls]
|
|
31
|
+
Homepage = "https://github.com/aliafsahnoudeh/zurvan"
|
|
32
|
+
Repository = "https://github.com/aliafsahnoudeh/zurvan"
|
|
33
|
+
Issues = "https://github.com/aliafsahnoudeh/zurvan/issues"
|
|
34
|
+
|
|
35
|
+
[tool.hatch.build.targets.wheel]
|
|
36
|
+
packages = ["src/zurvan"]
|
|
37
|
+
|
|
38
|
+
[tool.hatch.build.targets.sdist]
|
|
39
|
+
include = ["src/zurvan", "README.md", "LICENSE", "pyproject.toml"]
|
|
40
|
+
|
|
41
|
+
[tool.pytest.ini_options]
|
|
42
|
+
testpaths = ["tests"]
|
|
43
|
+
addopts = "-ra --strict-markers"
|
|
44
|
+
filterwarnings = [
|
|
45
|
+
"ignore::DeprecationWarning:pydantic.*",
|
|
46
|
+
"ignore::DeprecationWarning:litellm.*",
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
[tool.ruff]
|
|
50
|
+
line-length = 100
|
|
51
|
+
target-version = "py311"
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""zurvan — composable Python framework for goal-directed LLM agents.
|
|
2
|
+
|
|
3
|
+
The package implements the **GAME loop** (Goals, Actions, Memory,
|
|
4
|
+
Environment) and a :class:`Capability` hook system. Behaviour is
|
|
5
|
+
composed by passing a list of capabilities to the :class:`Agent`,
|
|
6
|
+
not by subclassing.
|
|
7
|
+
|
|
8
|
+
LLM providers are abstracted behind :class:`AgentLanguage`. The
|
|
9
|
+
function-calling subclasses (Gemini, Groq, OpenAI) talk to providers
|
|
10
|
+
via LiteLLM.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from zurvan.action import Action
|
|
14
|
+
from zurvan.action_context import ActionContext
|
|
15
|
+
from zurvan.action_registry import ActionRegistry
|
|
16
|
+
from zurvan.agent import Agent
|
|
17
|
+
from zurvan.agent_function_calling_action_language import (
|
|
18
|
+
AgentFunctionCallingActionLanguage,
|
|
19
|
+
)
|
|
20
|
+
from zurvan.agent_function_calling_action_language_gemini import (
|
|
21
|
+
AgentFunctionCallingActionLanguageGemini,
|
|
22
|
+
)
|
|
23
|
+
from zurvan.agent_function_calling_action_language_groq import (
|
|
24
|
+
AgentFunctionCallingActionLanguageGroq,
|
|
25
|
+
)
|
|
26
|
+
from zurvan.agent_function_calling_action_language_openai import (
|
|
27
|
+
AgentFunctionCallingActionLanguageOpenAI,
|
|
28
|
+
)
|
|
29
|
+
from zurvan.agent_language import AgentLanguage
|
|
30
|
+
from zurvan.capabilities.canary_capability import (
|
|
31
|
+
CanaryCapability,
|
|
32
|
+
PromptInjectionDetected,
|
|
33
|
+
)
|
|
34
|
+
from zurvan.capabilities.time_aware_capability import TimeAwareCapability
|
|
35
|
+
from zurvan.capability import Capability
|
|
36
|
+
from zurvan.decorators import register_tool
|
|
37
|
+
from zurvan.environment import Environment
|
|
38
|
+
from zurvan.goal import Goal
|
|
39
|
+
from zurvan.logger.logger import LogLevel, Logger
|
|
40
|
+
from zurvan.memory import Memory
|
|
41
|
+
from zurvan.prompt import Prompt
|
|
42
|
+
from zurvan.token_tracker import TokenTracker
|
|
43
|
+
|
|
44
|
+
__version__ = "0.1.0"
|
|
45
|
+
|
|
46
|
+
__all__ = [
|
|
47
|
+
"__version__",
|
|
48
|
+
# Core GAME primitives
|
|
49
|
+
"Action",
|
|
50
|
+
"ActionContext",
|
|
51
|
+
"ActionRegistry",
|
|
52
|
+
"Agent",
|
|
53
|
+
"Capability",
|
|
54
|
+
"Environment",
|
|
55
|
+
"Goal",
|
|
56
|
+
"Memory",
|
|
57
|
+
"Prompt",
|
|
58
|
+
# Logging
|
|
59
|
+
"LogLevel",
|
|
60
|
+
"Logger",
|
|
61
|
+
# Tool decorator
|
|
62
|
+
"register_tool",
|
|
63
|
+
# Token tracking
|
|
64
|
+
"TokenTracker",
|
|
65
|
+
# Agent languages
|
|
66
|
+
"AgentLanguage",
|
|
67
|
+
"AgentFunctionCallingActionLanguage",
|
|
68
|
+
"AgentFunctionCallingActionLanguageGemini",
|
|
69
|
+
"AgentFunctionCallingActionLanguageGroq",
|
|
70
|
+
"AgentFunctionCallingActionLanguageOpenAI",
|
|
71
|
+
# Capabilities
|
|
72
|
+
"CanaryCapability",
|
|
73
|
+
"PromptInjectionDetected",
|
|
74
|
+
"TimeAwareCapability",
|
|
75
|
+
]
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Action — the interface definition of what an agent can do.
|
|
2
|
+
|
|
3
|
+
Two equivalent shapes are supported:
|
|
4
|
+
|
|
5
|
+
1. **Dict / JSON Schema** (the original): pass a JSON-Schema dict as
|
|
6
|
+
``parameters=``. Nothing validates the model's args before
|
|
7
|
+
``function`` runs — it's on the LLM to send the right shape.
|
|
8
|
+
2. **Pydantic model**: pass a ``BaseModel`` subclass as
|
|
9
|
+
``input_model=``. The schema sent to the LLM is derived from the
|
|
10
|
+
model and the model's args are **validated before** ``function``
|
|
11
|
+
runs. ``pydantic.ValidationError`` is allowed to propagate and is
|
|
12
|
+
trapped by ``Environment.execute_action`` like any action exception
|
|
13
|
+
— the failure goes back to the LLM as a tool result, giving the
|
|
14
|
+
model a chance to retry with corrected args.
|
|
15
|
+
|
|
16
|
+
The two paths are mutually exclusive — pass exactly one.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from typing import Any, Callable, Dict, Optional, Type
|
|
20
|
+
|
|
21
|
+
from pydantic import BaseModel
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Action:
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
name: str,
|
|
28
|
+
function: Callable,
|
|
29
|
+
description: str,
|
|
30
|
+
parameters: Optional[Dict] = None,
|
|
31
|
+
input_model: Optional[Type[BaseModel]] = None,
|
|
32
|
+
terminal: bool = False,
|
|
33
|
+
):
|
|
34
|
+
if parameters is None and input_model is None:
|
|
35
|
+
raise ValueError(
|
|
36
|
+
"Action requires either `parameters` (a JSON-Schema dict) "
|
|
37
|
+
"or `input_model` (a Pydantic BaseModel subclass)."
|
|
38
|
+
)
|
|
39
|
+
if parameters is not None and input_model is not None:
|
|
40
|
+
raise ValueError(
|
|
41
|
+
"Action accepts either `parameters` or `input_model`, not both."
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
self.name = name
|
|
45
|
+
self.function = function
|
|
46
|
+
self.description = description
|
|
47
|
+
self.terminal = terminal
|
|
48
|
+
self.input_model = input_model
|
|
49
|
+
self.parameters = (
|
|
50
|
+
input_model.model_json_schema() if input_model is not None else parameters
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
def execute(self, **args) -> Any:
|
|
54
|
+
"""Execute the action's function.
|
|
55
|
+
|
|
56
|
+
With ``input_model`` set, args are first validated through the
|
|
57
|
+
model. ``pydantic.ValidationError`` propagates on failure —
|
|
58
|
+
``Environment.execute_action`` catches it and surfaces the
|
|
59
|
+
failure to the LLM so the model can retry with corrected args.
|
|
60
|
+
"""
|
|
61
|
+
if self.input_model is not None:
|
|
62
|
+
validated = self.input_model(**args)
|
|
63
|
+
return self.function(**validated.model_dump())
|
|
64
|
+
return self.function(**args)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from typing import Dict
|
|
2
|
+
import uuid
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class ActionContext:
|
|
6
|
+
def __init__(self, properties: Dict = None):
|
|
7
|
+
self.context_id = str(uuid.uuid4())
|
|
8
|
+
self.properties = properties or {}
|
|
9
|
+
|
|
10
|
+
def get(self, key: str, default=None):
|
|
11
|
+
return self.properties.get(key, default)
|
|
12
|
+
|
|
13
|
+
def get_memory(self):
|
|
14
|
+
return self.properties.get("memory", None)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from typing import List
|
|
2
|
+
|
|
3
|
+
from zurvan.action import Action
|
|
4
|
+
from zurvan.decorators import tools, tools_by_tag # the global dicts
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ActionRegistry:
|
|
8
|
+
def __init__(self, tags: list[str] | None = None):
|
|
9
|
+
self.actions = {}
|
|
10
|
+
if tags:
|
|
11
|
+
self._load_from_tags(tags)
|
|
12
|
+
|
|
13
|
+
def _load_from_tags(self, tags: list[str]):
|
|
14
|
+
"""Load tools matching any of the given tags via tools_by_tag."""
|
|
15
|
+
seen = set()
|
|
16
|
+
for tag in tags:
|
|
17
|
+
for tool_name in tools_by_tag.get(tag, []):
|
|
18
|
+
if tool_name not in seen:
|
|
19
|
+
seen.add(tool_name)
|
|
20
|
+
desc = tools[tool_name]
|
|
21
|
+
self.register(
|
|
22
|
+
Action(
|
|
23
|
+
name=tool_name,
|
|
24
|
+
function=desc["function"],
|
|
25
|
+
description=desc["description"],
|
|
26
|
+
parameters=desc.get("parameters", {}),
|
|
27
|
+
terminal=desc.get("terminal", False),
|
|
28
|
+
)
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
def register(self, action: Action):
|
|
32
|
+
self.actions[action.name] = action
|
|
33
|
+
|
|
34
|
+
def get_action(self, name: str) -> Action | None:
|
|
35
|
+
return self.actions.get(name, None)
|
|
36
|
+
|
|
37
|
+
def get_actions(self) -> List[Action]:
|
|
38
|
+
"""Get all registered actions"""
|
|
39
|
+
return list(self.actions.values())
|
|
40
|
+
|
|
41
|
+
def register_from_tools(self, tags: list[str] | None = None):
|
|
42
|
+
"""Load decorated tools into this registry, optionally filtered by tags."""
|
|
43
|
+
for name, desc in tools.items():
|
|
44
|
+
if tags and not set(tags) & set(desc.get("tags", [])):
|
|
45
|
+
continue
|
|
46
|
+
self.register(
|
|
47
|
+
Action(
|
|
48
|
+
name=name,
|
|
49
|
+
function=desc["function"],
|
|
50
|
+
description=desc["description"],
|
|
51
|
+
parameters=desc.get("parameters", {}),
|
|
52
|
+
terminal=desc.get("terminal", False),
|
|
53
|
+
)
|
|
54
|
+
)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import uuid
|
|
2
|
+
|
|
3
|
+
from zurvan.reversible_action import ReversibleAction
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ActionTransaction:
|
|
7
|
+
def __init__(self):
|
|
8
|
+
self.actions = []
|
|
9
|
+
self.executed = []
|
|
10
|
+
self.committed = False
|
|
11
|
+
self.transaction_id = str(uuid.uuid4())
|
|
12
|
+
|
|
13
|
+
def add(self, action: ReversibleAction, **args):
|
|
14
|
+
"""Queue an action for execution."""
|
|
15
|
+
if self.committed:
|
|
16
|
+
raise ValueError("Transaction already committed")
|
|
17
|
+
self.actions.append((action, args))
|
|
18
|
+
|
|
19
|
+
async def execute(self):
|
|
20
|
+
"""Execute all actions in the transaction."""
|
|
21
|
+
try:
|
|
22
|
+
for action, args in self.actions:
|
|
23
|
+
result = action.run(**args)
|
|
24
|
+
self.executed.append(action)
|
|
25
|
+
except Exception as e:
|
|
26
|
+
# If any action fails, reverse everything done so far
|
|
27
|
+
await self.rollback()
|
|
28
|
+
raise e
|
|
29
|
+
|
|
30
|
+
async def rollback(self):
|
|
31
|
+
"""Reverse all executed actions in reverse order."""
|
|
32
|
+
for action in reversed(self.executed):
|
|
33
|
+
await action.undo()
|
|
34
|
+
self.executed = []
|
|
35
|
+
|
|
36
|
+
def commit(self):
|
|
37
|
+
"""Mark transaction as committed."""
|
|
38
|
+
self.committed = True
|