fifty-agent-sdk 1.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.
- fifty_agent_sdk-1.1.0/LICENSE +21 -0
- fifty_agent_sdk-1.1.0/PKG-INFO +225 -0
- fifty_agent_sdk-1.1.0/README.md +181 -0
- fifty_agent_sdk-1.1.0/pyproject.toml +101 -0
- fifty_agent_sdk-1.1.0/setup.cfg +4 -0
- fifty_agent_sdk-1.1.0/src/fifty_agent_sdk/__init__.py +191 -0
- fifty_agent_sdk-1.1.0/src/fifty_agent_sdk/audit/__init__.py +45 -0
- fifty_agent_sdk-1.1.0/src/fifty_agent_sdk/audit/console.py +69 -0
- fifty_agent_sdk-1.1.0/src/fifty_agent_sdk/audit/protocol.py +112 -0
- fifty_agent_sdk-1.1.0/src/fifty_agent_sdk/audit/sql.py +328 -0
- fifty_agent_sdk-1.1.0/src/fifty_agent_sdk/errors.py +119 -0
- fifty_agent_sdk-1.1.0/src/fifty_agent_sdk/llm/__init__.py +30 -0
- fifty_agent_sdk-1.1.0/src/fifty_agent_sdk/llm/openai_compat.py +345 -0
- fifty_agent_sdk-1.1.0/src/fifty_agent_sdk/llm/protocol.py +87 -0
- fifty_agent_sdk-1.1.0/src/fifty_agent_sdk/llm/types.py +149 -0
- fifty_agent_sdk-1.1.0/src/fifty_agent_sdk/loop.py +836 -0
- fifty_agent_sdk-1.1.0/src/fifty_agent_sdk/mcp/__init__.py +39 -0
- fifty_agent_sdk-1.1.0/src/fifty_agent_sdk/mcp/client.py +622 -0
- fifty_agent_sdk-1.1.0/src/fifty_agent_sdk/mcp/transport.py +177 -0
- fifty_agent_sdk-1.1.0/src/fifty_agent_sdk/observability/__init__.py +18 -0
- fifty_agent_sdk-1.1.0/src/fifty_agent_sdk/observability/hooks.py +232 -0
- fifty_agent_sdk-1.1.0/src/fifty_agent_sdk/parser/__init__.py +32 -0
- fifty_agent_sdk-1.1.0/src/fifty_agent_sdk/parser/base.py +118 -0
- fifty_agent_sdk-1.1.0/src/fifty_agent_sdk/parser/json_mode.py +219 -0
- fifty_agent_sdk-1.1.0/src/fifty_agent_sdk/parser/native_tools.py +93 -0
- fifty_agent_sdk-1.1.0/src/fifty_agent_sdk/parser/prose_mode.py +162 -0
- fifty_agent_sdk-1.1.0/src/fifty_agent_sdk/prompts.py +186 -0
- fifty_agent_sdk-1.1.0/src/fifty_agent_sdk/py.typed +0 -0
- fifty_agent_sdk-1.1.0/src/fifty_agent_sdk/runner.py +797 -0
- fifty_agent_sdk-1.1.0/src/fifty_agent_sdk/safety.py +134 -0
- fifty_agent_sdk-1.1.0/src/fifty_agent_sdk/state/__init__.py +53 -0
- fifty_agent_sdk-1.1.0/src/fifty_agent_sdk/state/memory.py +156 -0
- fifty_agent_sdk-1.1.0/src/fifty_agent_sdk/state/protocol.py +112 -0
- fifty_agent_sdk-1.1.0/src/fifty_agent_sdk/state/redis.py +371 -0
- fifty_agent_sdk-1.1.0/src/fifty_agent_sdk/state/sql.py +688 -0
- fifty_agent_sdk-1.1.0/src/fifty_agent_sdk/streaming.py +273 -0
- fifty_agent_sdk-1.1.0/src/fifty_agent_sdk/tools/__init__.py +32 -0
- fifty_agent_sdk-1.1.0/src/fifty_agent_sdk/tools/inproc_provider.py +232 -0
- fifty_agent_sdk-1.1.0/src/fifty_agent_sdk/tools/mcp_provider.py +345 -0
- fifty_agent_sdk-1.1.0/src/fifty_agent_sdk/tools/protocol.py +124 -0
- fifty_agent_sdk-1.1.0/src/fifty_agent_sdk/tools/registry.py +165 -0
- fifty_agent_sdk-1.1.0/src/fifty_agent_sdk.egg-info/PKG-INFO +225 -0
- fifty_agent_sdk-1.1.0/src/fifty_agent_sdk.egg-info/SOURCES.txt +47 -0
- fifty_agent_sdk-1.1.0/src/fifty_agent_sdk.egg-info/dependency_links.txt +1 -0
- fifty_agent_sdk-1.1.0/src/fifty_agent_sdk.egg-info/requires.txt +21 -0
- fifty_agent_sdk-1.1.0/src/fifty_agent_sdk.egg-info/top_level.txt +1 -0
- fifty_agent_sdk-1.1.0/tests/test_e2e_example.py +144 -0
- fifty_agent_sdk-1.1.0/tests/test_errors.py +101 -0
- fifty_agent_sdk-1.1.0/tests/test_prompts.py +156 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 fifty.dev
|
|
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,225 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fifty-agent-sdk
|
|
3
|
+
Version: 1.1.0
|
|
4
|
+
Summary: Production-grade reusable agent loop SDK — custom ReACT, JSON-mode tool calls, MCP client, pluggable LLM/state/tools.
|
|
5
|
+
Author: fifty.dev
|
|
6
|
+
Maintainer: fifty.dev
|
|
7
|
+
License: MIT
|
|
8
|
+
Project-URL: Homepage, https://github.com/fiftynotai/fifty-agent-sdk
|
|
9
|
+
Project-URL: Repository, https://github.com/fiftynotai/fifty-agent-sdk
|
|
10
|
+
Project-URL: Issues, https://github.com/fiftynotai/fifty-agent-sdk/issues
|
|
11
|
+
Project-URL: Changelog, https://github.com/fiftynotai/fifty-agent-sdk/blob/main/CHANGELOG.md
|
|
12
|
+
Keywords: agent,llm,react,mcp,tool-calling,openai,ai,sdk
|
|
13
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
21
|
+
Classifier: Typing :: Typed
|
|
22
|
+
Requires-Python: >=3.11
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Requires-Dist: pydantic>=2.5.0
|
|
26
|
+
Requires-Dist: httpx>=0.26.0
|
|
27
|
+
Requires-Dist: structlog>=24.1.0
|
|
28
|
+
Requires-Dist: openai<3.0.0,>=1.30.0
|
|
29
|
+
Requires-Dist: mcp<2.0.0,>=1.27.0
|
|
30
|
+
Provides-Extra: sql
|
|
31
|
+
Requires-Dist: sqlalchemy[asyncio]>=2.0.0; extra == "sql"
|
|
32
|
+
Provides-Extra: redis
|
|
33
|
+
Requires-Dist: redis>=5.0.0; extra == "redis"
|
|
34
|
+
Provides-Extra: dev
|
|
35
|
+
Requires-Dist: pytest>=7.4.0; extra == "dev"
|
|
36
|
+
Requires-Dist: pytest-asyncio>=0.23.0; extra == "dev"
|
|
37
|
+
Requires-Dist: pytest-cov>=4.1.0; extra == "dev"
|
|
38
|
+
Requires-Dist: pytest-httpx>=0.30.0; extra == "dev"
|
|
39
|
+
Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
40
|
+
Requires-Dist: mypy>=1.8.0; extra == "dev"
|
|
41
|
+
Requires-Dist: aiosqlite>=0.19.0; extra == "dev"
|
|
42
|
+
Requires-Dist: fakeredis>=2.20.0; extra == "dev"
|
|
43
|
+
Dynamic: license-file
|
|
44
|
+
|
|
45
|
+
<p align="center">
|
|
46
|
+
<img src=".github/banner.png" alt="fifty-agent-sdk — an embeddable ReACT loop. any endpoint. no infra." width="100%">
|
|
47
|
+
</p>
|
|
48
|
+
|
|
49
|
+
# fifty-agent-sdk
|
|
50
|
+
|
|
51
|
+
[](https://pypi.org/project/fifty-agent-sdk/)
|
|
52
|
+
[](https://pypi.org/project/fifty-agent-sdk/)
|
|
53
|
+
[](https://github.com/fiftynotai/fifty-agent-sdk/actions/workflows/ci.yml)
|
|
54
|
+
[](LICENSE)
|
|
55
|
+
|
|
56
|
+
an embeddable ReACT agent loop you drop into your own service. it talks to
|
|
57
|
+
any OpenAI-compatible Chat Completions endpoint — OpenAI, Google Distributed
|
|
58
|
+
Cloud, a local OSS server — by changing one `base_url`. transport-free,
|
|
59
|
+
infra-free by default, and pluggable behind protocols the whole way down.
|
|
60
|
+
|
|
61
|
+
## At a glance
|
|
62
|
+
|
|
63
|
+
- endpoint-agnostic — one `base_url` points the loop at OpenAI, GDC, or a local OSS server; the loop doesn't care which.
|
|
64
|
+
- pluggable tools — `@tool` derives a JSON Schema from type hints; MCP discovery adds remote tools to the same registry.
|
|
65
|
+
- pluggable state — conversation state lives behind a `StateStore` protocol: in-memory, SQL, or Redis.
|
|
66
|
+
- typed event stream — every ReACT step emits exactly one `AgentEvent`; every run ends with exactly one `FinalEvent`.
|
|
67
|
+
- safety caps — iteration ceiling, per-tool timeouts, and a fallback answer on error or cap.
|
|
68
|
+
- zero-infra default — memory backends ship in core; run an agent with no extra dependency.
|
|
69
|
+
|
|
70
|
+
## Installation
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
pip install fifty-agent-sdk
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
The SDK requires Python 3.11 or newer. The core install ships with the
|
|
77
|
+
in-memory backends, so an agent can run with no infrastructure dependency.
|
|
78
|
+
|
|
79
|
+
Two optional extras add durable backends:
|
|
80
|
+
|
|
81
|
+
- `pip install 'fifty-agent-sdk[sql]'` — pulls SQLAlchemy and enables
|
|
82
|
+
`SqlStateStore` (durable conversation state) and `SqlAuditSink` (durable
|
|
83
|
+
audit log).
|
|
84
|
+
- `pip install 'fifty-agent-sdk[redis]'` — pulls redis-py and enables
|
|
85
|
+
`RedisStateStore` (Redis-backed conversation state).
|
|
86
|
+
|
|
87
|
+
Importing `fifty_agent_sdk` itself pulls neither SQLAlchemy nor redis-py. The
|
|
88
|
+
extra symbols are re-exported lazily; first access to one without the
|
|
89
|
+
relevant extra installed raises a clear `ImportError`.
|
|
90
|
+
|
|
91
|
+
## Quickstart
|
|
92
|
+
|
|
93
|
+
A complete agent fits in a handful of lines. The example below defines a
|
|
94
|
+
tool, wires the loop and runner, and drains the event stream:
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
import asyncio
|
|
98
|
+
from typing import Any
|
|
99
|
+
|
|
100
|
+
from fifty_agent_sdk import (
|
|
101
|
+
JSON_MODE_OUTPUT_FORMAT,
|
|
102
|
+
AgentLoop,
|
|
103
|
+
AgentRunner,
|
|
104
|
+
JsonModeParser,
|
|
105
|
+
MemoryStateStore,
|
|
106
|
+
OpenAICompatibleClient,
|
|
107
|
+
PromptSections,
|
|
108
|
+
Registry,
|
|
109
|
+
SafetyConfig,
|
|
110
|
+
tool,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@tool()
|
|
115
|
+
async def get_weather(city: str) -> dict[str, Any]:
|
|
116
|
+
"""Return the current weather for a city."""
|
|
117
|
+
return {"city": city, "temp_c": 21}
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
async def main() -> None:
|
|
121
|
+
# 1. An LLM client — points at any OpenAI-compatible endpoint.
|
|
122
|
+
llm = OpenAICompatibleClient(api_key="sk-...")
|
|
123
|
+
|
|
124
|
+
# 2. A tool registry — register the decorated tool.
|
|
125
|
+
registry = Registry()
|
|
126
|
+
registry.register(get_weather)
|
|
127
|
+
|
|
128
|
+
# 3. The ReACT loop — LLM + registry + parser + prompts + safety.
|
|
129
|
+
# `output_format` shows the model the JSON envelope the parser
|
|
130
|
+
# expects; without it JsonModeParser raises ParserError on every turn.
|
|
131
|
+
loop = AgentLoop(
|
|
132
|
+
llm=llm,
|
|
133
|
+
registry=registry,
|
|
134
|
+
parser=JsonModeParser(),
|
|
135
|
+
prompts=PromptSections(persona="You are helpful."),
|
|
136
|
+
safety=SafetyConfig(),
|
|
137
|
+
model="gpt-4o",
|
|
138
|
+
output_format=JSON_MODE_OUTPUT_FORMAT,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
# 4. The runner — wraps the loop with conversation-state persistence.
|
|
142
|
+
runner = AgentRunner(
|
|
143
|
+
loop=loop,
|
|
144
|
+
state=MemoryStateStore(),
|
|
145
|
+
system_prompt="You are a helpful weather assistant.",
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
# 5. Drive a turn and consume the event stream.
|
|
149
|
+
async for event in runner.run("session-1", "What's the weather in Paris?"):
|
|
150
|
+
print(event)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
asyncio.run(main())
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## Core concepts
|
|
157
|
+
|
|
158
|
+
### Tools
|
|
159
|
+
|
|
160
|
+
The `@tool` decorator turns an async function into a `Tool`: it derives a
|
|
161
|
+
JSON Schema for the arguments from the function's type annotations and
|
|
162
|
+
docstring. A `Registry` is the dispatch table the loop talks to —
|
|
163
|
+
`Registry().register(my_tool)` adds a tool by name. `InProcProvider` is a
|
|
164
|
+
convenience helper for bulk-registering a batch of decorated callables.
|
|
165
|
+
|
|
166
|
+
### LLM clients
|
|
167
|
+
|
|
168
|
+
`LLMClient` is the protocol the loop depends on — anything implementing it
|
|
169
|
+
can drive the agent. `OpenAICompatibleClient` is the shipped implementation;
|
|
170
|
+
it works against any OpenAI-compatible Chat Completions endpoint. Point it at
|
|
171
|
+
GDC, a local OSS server, or OpenAI itself by passing `base_url` — the
|
|
172
|
+
provider differences are absorbed entirely by that one argument.
|
|
173
|
+
|
|
174
|
+
### State stores
|
|
175
|
+
|
|
176
|
+
`StateStore` is the protocol for conversation-state persistence across turns.
|
|
177
|
+
`MemoryStateStore` is the default in-memory implementation and needs no
|
|
178
|
+
infrastructure. `SqlStateStore` and `RedisStateStore` are durable backends
|
|
179
|
+
behind the `sql` and `redis` extras respectively.
|
|
180
|
+
|
|
181
|
+
### The event stream
|
|
182
|
+
|
|
183
|
+
Every step of the ReACT cycle emits exactly one event from the `AgentEvent`
|
|
184
|
+
union: `ThoughtEvent`, `ActionEvent`, `ToolStartedEvent`,
|
|
185
|
+
`ToolProgressEvent` (reserved — never emitted by the v1 loop),
|
|
186
|
+
`ObservationEvent`, `ToolFailedEvent`, `TokenEvent`, `FinalEvent`, and
|
|
187
|
+
`ErrorEvent`. Events carry a monotonic `sequence` counter and a `timestamp`
|
|
188
|
+
so consumers can detect drops or reorders. Every run ends with exactly one
|
|
189
|
+
`FinalEvent` — consumers can rely on it as the "iteration done" signal.
|
|
190
|
+
|
|
191
|
+
### Safety
|
|
192
|
+
|
|
193
|
+
`SafetyConfig` bounds a run: it caps the iteration count, sets per-tool
|
|
194
|
+
timeouts, and supplies the fallback answer used when a run terminates on an
|
|
195
|
+
error or safety cap.
|
|
196
|
+
|
|
197
|
+
### Audit & observability
|
|
198
|
+
|
|
199
|
+
Two optional collaborators plug into the runner. An `AuditSink` records an
|
|
200
|
+
`AuditEvent` at session start, each tool invocation, the final answer, and
|
|
201
|
+
any error. A `Hooks` instance fires lifecycle callbacks for logging, metrics,
|
|
202
|
+
and tracing. Both are best-effort and isolated — a raising sink or hook
|
|
203
|
+
never aborts a live run.
|
|
204
|
+
|
|
205
|
+
## Design principles
|
|
206
|
+
|
|
207
|
+
- **Pluggable everything.** LLM clients, tool sources, state stores, audit
|
|
208
|
+
sinks, observability hooks all sit behind protocols.
|
|
209
|
+
- **Transport-free.** No HTTP, no WebSocket, no auth. Consumers wrap the
|
|
210
|
+
`Runner` in whatever transport makes sense.
|
|
211
|
+
- **Production-first.** Iter caps, tool timeouts, structured errors,
|
|
212
|
+
graceful fallbacks, full-fidelity event stream.
|
|
213
|
+
- **Standalone usable.** Memory backends ship by default so you can run an
|
|
214
|
+
agent without any infrastructure dependency.
|
|
215
|
+
|
|
216
|
+
## Links
|
|
217
|
+
|
|
218
|
+
- package — https://pypi.org/project/fifty-agent-sdk/
|
|
219
|
+
- source & issues — https://github.com/fiftynotai/fifty-agent-sdk
|
|
220
|
+
- changelog — [CHANGELOG.md](CHANGELOG.md)
|
|
221
|
+
- contributing — issues and PRs welcome.
|
|
222
|
+
|
|
223
|
+
## License
|
|
224
|
+
|
|
225
|
+
[MIT](LICENSE) © fifty.dev
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src=".github/banner.png" alt="fifty-agent-sdk — an embeddable ReACT loop. any endpoint. no infra." width="100%">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
# fifty-agent-sdk
|
|
6
|
+
|
|
7
|
+
[](https://pypi.org/project/fifty-agent-sdk/)
|
|
8
|
+
[](https://pypi.org/project/fifty-agent-sdk/)
|
|
9
|
+
[](https://github.com/fiftynotai/fifty-agent-sdk/actions/workflows/ci.yml)
|
|
10
|
+
[](LICENSE)
|
|
11
|
+
|
|
12
|
+
an embeddable ReACT agent loop you drop into your own service. it talks to
|
|
13
|
+
any OpenAI-compatible Chat Completions endpoint — OpenAI, Google Distributed
|
|
14
|
+
Cloud, a local OSS server — by changing one `base_url`. transport-free,
|
|
15
|
+
infra-free by default, and pluggable behind protocols the whole way down.
|
|
16
|
+
|
|
17
|
+
## At a glance
|
|
18
|
+
|
|
19
|
+
- endpoint-agnostic — one `base_url` points the loop at OpenAI, GDC, or a local OSS server; the loop doesn't care which.
|
|
20
|
+
- pluggable tools — `@tool` derives a JSON Schema from type hints; MCP discovery adds remote tools to the same registry.
|
|
21
|
+
- pluggable state — conversation state lives behind a `StateStore` protocol: in-memory, SQL, or Redis.
|
|
22
|
+
- typed event stream — every ReACT step emits exactly one `AgentEvent`; every run ends with exactly one `FinalEvent`.
|
|
23
|
+
- safety caps — iteration ceiling, per-tool timeouts, and a fallback answer on error or cap.
|
|
24
|
+
- zero-infra default — memory backends ship in core; run an agent with no extra dependency.
|
|
25
|
+
|
|
26
|
+
## Installation
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
pip install fifty-agent-sdk
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
The SDK requires Python 3.11 or newer. The core install ships with the
|
|
33
|
+
in-memory backends, so an agent can run with no infrastructure dependency.
|
|
34
|
+
|
|
35
|
+
Two optional extras add durable backends:
|
|
36
|
+
|
|
37
|
+
- `pip install 'fifty-agent-sdk[sql]'` — pulls SQLAlchemy and enables
|
|
38
|
+
`SqlStateStore` (durable conversation state) and `SqlAuditSink` (durable
|
|
39
|
+
audit log).
|
|
40
|
+
- `pip install 'fifty-agent-sdk[redis]'` — pulls redis-py and enables
|
|
41
|
+
`RedisStateStore` (Redis-backed conversation state).
|
|
42
|
+
|
|
43
|
+
Importing `fifty_agent_sdk` itself pulls neither SQLAlchemy nor redis-py. The
|
|
44
|
+
extra symbols are re-exported lazily; first access to one without the
|
|
45
|
+
relevant extra installed raises a clear `ImportError`.
|
|
46
|
+
|
|
47
|
+
## Quickstart
|
|
48
|
+
|
|
49
|
+
A complete agent fits in a handful of lines. The example below defines a
|
|
50
|
+
tool, wires the loop and runner, and drains the event stream:
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
import asyncio
|
|
54
|
+
from typing import Any
|
|
55
|
+
|
|
56
|
+
from fifty_agent_sdk import (
|
|
57
|
+
JSON_MODE_OUTPUT_FORMAT,
|
|
58
|
+
AgentLoop,
|
|
59
|
+
AgentRunner,
|
|
60
|
+
JsonModeParser,
|
|
61
|
+
MemoryStateStore,
|
|
62
|
+
OpenAICompatibleClient,
|
|
63
|
+
PromptSections,
|
|
64
|
+
Registry,
|
|
65
|
+
SafetyConfig,
|
|
66
|
+
tool,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@tool()
|
|
71
|
+
async def get_weather(city: str) -> dict[str, Any]:
|
|
72
|
+
"""Return the current weather for a city."""
|
|
73
|
+
return {"city": city, "temp_c": 21}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
async def main() -> None:
|
|
77
|
+
# 1. An LLM client — points at any OpenAI-compatible endpoint.
|
|
78
|
+
llm = OpenAICompatibleClient(api_key="sk-...")
|
|
79
|
+
|
|
80
|
+
# 2. A tool registry — register the decorated tool.
|
|
81
|
+
registry = Registry()
|
|
82
|
+
registry.register(get_weather)
|
|
83
|
+
|
|
84
|
+
# 3. The ReACT loop — LLM + registry + parser + prompts + safety.
|
|
85
|
+
# `output_format` shows the model the JSON envelope the parser
|
|
86
|
+
# expects; without it JsonModeParser raises ParserError on every turn.
|
|
87
|
+
loop = AgentLoop(
|
|
88
|
+
llm=llm,
|
|
89
|
+
registry=registry,
|
|
90
|
+
parser=JsonModeParser(),
|
|
91
|
+
prompts=PromptSections(persona="You are helpful."),
|
|
92
|
+
safety=SafetyConfig(),
|
|
93
|
+
model="gpt-4o",
|
|
94
|
+
output_format=JSON_MODE_OUTPUT_FORMAT,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
# 4. The runner — wraps the loop with conversation-state persistence.
|
|
98
|
+
runner = AgentRunner(
|
|
99
|
+
loop=loop,
|
|
100
|
+
state=MemoryStateStore(),
|
|
101
|
+
system_prompt="You are a helpful weather assistant.",
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
# 5. Drive a turn and consume the event stream.
|
|
105
|
+
async for event in runner.run("session-1", "What's the weather in Paris?"):
|
|
106
|
+
print(event)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
asyncio.run(main())
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Core concepts
|
|
113
|
+
|
|
114
|
+
### Tools
|
|
115
|
+
|
|
116
|
+
The `@tool` decorator turns an async function into a `Tool`: it derives a
|
|
117
|
+
JSON Schema for the arguments from the function's type annotations and
|
|
118
|
+
docstring. A `Registry` is the dispatch table the loop talks to —
|
|
119
|
+
`Registry().register(my_tool)` adds a tool by name. `InProcProvider` is a
|
|
120
|
+
convenience helper for bulk-registering a batch of decorated callables.
|
|
121
|
+
|
|
122
|
+
### LLM clients
|
|
123
|
+
|
|
124
|
+
`LLMClient` is the protocol the loop depends on — anything implementing it
|
|
125
|
+
can drive the agent. `OpenAICompatibleClient` is the shipped implementation;
|
|
126
|
+
it works against any OpenAI-compatible Chat Completions endpoint. Point it at
|
|
127
|
+
GDC, a local OSS server, or OpenAI itself by passing `base_url` — the
|
|
128
|
+
provider differences are absorbed entirely by that one argument.
|
|
129
|
+
|
|
130
|
+
### State stores
|
|
131
|
+
|
|
132
|
+
`StateStore` is the protocol for conversation-state persistence across turns.
|
|
133
|
+
`MemoryStateStore` is the default in-memory implementation and needs no
|
|
134
|
+
infrastructure. `SqlStateStore` and `RedisStateStore` are durable backends
|
|
135
|
+
behind the `sql` and `redis` extras respectively.
|
|
136
|
+
|
|
137
|
+
### The event stream
|
|
138
|
+
|
|
139
|
+
Every step of the ReACT cycle emits exactly one event from the `AgentEvent`
|
|
140
|
+
union: `ThoughtEvent`, `ActionEvent`, `ToolStartedEvent`,
|
|
141
|
+
`ToolProgressEvent` (reserved — never emitted by the v1 loop),
|
|
142
|
+
`ObservationEvent`, `ToolFailedEvent`, `TokenEvent`, `FinalEvent`, and
|
|
143
|
+
`ErrorEvent`. Events carry a monotonic `sequence` counter and a `timestamp`
|
|
144
|
+
so consumers can detect drops or reorders. Every run ends with exactly one
|
|
145
|
+
`FinalEvent` — consumers can rely on it as the "iteration done" signal.
|
|
146
|
+
|
|
147
|
+
### Safety
|
|
148
|
+
|
|
149
|
+
`SafetyConfig` bounds a run: it caps the iteration count, sets per-tool
|
|
150
|
+
timeouts, and supplies the fallback answer used when a run terminates on an
|
|
151
|
+
error or safety cap.
|
|
152
|
+
|
|
153
|
+
### Audit & observability
|
|
154
|
+
|
|
155
|
+
Two optional collaborators plug into the runner. An `AuditSink` records an
|
|
156
|
+
`AuditEvent` at session start, each tool invocation, the final answer, and
|
|
157
|
+
any error. A `Hooks` instance fires lifecycle callbacks for logging, metrics,
|
|
158
|
+
and tracing. Both are best-effort and isolated — a raising sink or hook
|
|
159
|
+
never aborts a live run.
|
|
160
|
+
|
|
161
|
+
## Design principles
|
|
162
|
+
|
|
163
|
+
- **Pluggable everything.** LLM clients, tool sources, state stores, audit
|
|
164
|
+
sinks, observability hooks all sit behind protocols.
|
|
165
|
+
- **Transport-free.** No HTTP, no WebSocket, no auth. Consumers wrap the
|
|
166
|
+
`Runner` in whatever transport makes sense.
|
|
167
|
+
- **Production-first.** Iter caps, tool timeouts, structured errors,
|
|
168
|
+
graceful fallbacks, full-fidelity event stream.
|
|
169
|
+
- **Standalone usable.** Memory backends ship by default so you can run an
|
|
170
|
+
agent without any infrastructure dependency.
|
|
171
|
+
|
|
172
|
+
## Links
|
|
173
|
+
|
|
174
|
+
- package — https://pypi.org/project/fifty-agent-sdk/
|
|
175
|
+
- source & issues — https://github.com/fiftynotai/fifty-agent-sdk
|
|
176
|
+
- changelog — [CHANGELOG.md](CHANGELOG.md)
|
|
177
|
+
- contributing — issues and PRs welcome.
|
|
178
|
+
|
|
179
|
+
## License
|
|
180
|
+
|
|
181
|
+
[MIT](LICENSE) © fifty.dev
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "fifty-agent-sdk"
|
|
7
|
+
version = "1.1.0"
|
|
8
|
+
description = "Production-grade reusable agent loop SDK — custom ReACT, JSON-mode tool calls, MCP client, pluggable LLM/state/tools."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = {text = "MIT"}
|
|
12
|
+
authors = [{name = "fifty.dev"}]
|
|
13
|
+
maintainers = [{name = "fifty.dev"}]
|
|
14
|
+
keywords = ["agent", "llm", "react", "mcp", "tool-calling", "openai", "ai", "sdk"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 5 - Production/Stable",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Programming Language :: Python :: 3.13",
|
|
23
|
+
"Topic :: Software Development :: Libraries :: Application Frameworks",
|
|
24
|
+
"Typing :: Typed",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
dependencies = [
|
|
28
|
+
"pydantic>=2.5.0",
|
|
29
|
+
"httpx>=0.26.0",
|
|
30
|
+
"structlog>=24.1.0",
|
|
31
|
+
"openai>=1.30.0,<3.0.0",
|
|
32
|
+
# Official MCP SDK. Standard-only: the MCP path is the ONLY MCP path, so
|
|
33
|
+
# this is a core dependency, not an optional extra. Floor pinned at the
|
|
34
|
+
# probed 1.27.0; <2.0.0 ceiling because FastMCP/transport surfaces drift
|
|
35
|
+
# across minors (re-probe the live API before coding against new minors).
|
|
36
|
+
"mcp>=1.27.0,<2.0.0",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
[project.urls]
|
|
40
|
+
Homepage = "https://github.com/fiftynotai/fifty-agent-sdk"
|
|
41
|
+
Repository = "https://github.com/fiftynotai/fifty-agent-sdk"
|
|
42
|
+
Issues = "https://github.com/fiftynotai/fifty-agent-sdk/issues"
|
|
43
|
+
Changelog = "https://github.com/fiftynotai/fifty-agent-sdk/blob/main/CHANGELOG.md"
|
|
44
|
+
|
|
45
|
+
[project.optional-dependencies]
|
|
46
|
+
sql = [
|
|
47
|
+
"sqlalchemy[asyncio]>=2.0.0",
|
|
48
|
+
]
|
|
49
|
+
redis = [
|
|
50
|
+
"redis>=5.0.0",
|
|
51
|
+
]
|
|
52
|
+
dev = [
|
|
53
|
+
"pytest>=7.4.0",
|
|
54
|
+
"pytest-asyncio>=0.23.0",
|
|
55
|
+
"pytest-cov>=4.1.0",
|
|
56
|
+
"pytest-httpx>=0.30.0",
|
|
57
|
+
"ruff>=0.1.0",
|
|
58
|
+
"mypy>=1.8.0",
|
|
59
|
+
"aiosqlite>=0.19.0",
|
|
60
|
+
# fakeredis must track the redis major version (redis>=5.0.0 above):
|
|
61
|
+
# the SDK relies on 5.x async semantics — aclose(), pipeline(transaction=True).
|
|
62
|
+
"fakeredis>=2.20.0",
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
[tool.setuptools.packages.find]
|
|
66
|
+
where = ["src"]
|
|
67
|
+
include = ["fifty_agent_sdk*"]
|
|
68
|
+
|
|
69
|
+
[tool.setuptools.package-data]
|
|
70
|
+
# Ship the PEP 561 marker so downstream type-checkers see fifty-agent-sdk as typed.
|
|
71
|
+
fifty_agent_sdk = ["py.typed"]
|
|
72
|
+
|
|
73
|
+
[tool.ruff]
|
|
74
|
+
target-version = "py311"
|
|
75
|
+
line-length = 100
|
|
76
|
+
|
|
77
|
+
[tool.ruff.lint]
|
|
78
|
+
select = ["E", "W", "F", "I", "B", "UP", "SIM"]
|
|
79
|
+
ignore = ["E501"]
|
|
80
|
+
|
|
81
|
+
[tool.ruff.lint.per-file-ignores]
|
|
82
|
+
"__init__.py" = ["F401"]
|
|
83
|
+
|
|
84
|
+
[tool.ruff.lint.isort]
|
|
85
|
+
known-first-party = ["fifty_agent_sdk"]
|
|
86
|
+
|
|
87
|
+
[tool.mypy]
|
|
88
|
+
python_version = "3.11"
|
|
89
|
+
strict = true
|
|
90
|
+
exclude = ["tests/", "build/", ".venv/"]
|
|
91
|
+
|
|
92
|
+
[tool.pytest.ini_options]
|
|
93
|
+
asyncio_mode = "auto"
|
|
94
|
+
testpaths = ["tests"]
|
|
95
|
+
python_files = ["test_*.py"]
|
|
96
|
+
addopts = ["-v", "--tb=short", "--strict-markers", "--import-mode=importlib"]
|
|
97
|
+
markers = [
|
|
98
|
+
"integration: Integration tests requiring external services",
|
|
99
|
+
"postgres: Integration tests against a real Postgres database (skipped unless POSTGRES_TEST_URL is set)",
|
|
100
|
+
"redis: Integration tests against a real Redis instance (skipped unless REDIS_TEST_URL is set)",
|
|
101
|
+
]
|