cubepi 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.
- cubepi-0.1.0/.github/workflows/ci.yml +51 -0
- cubepi-0.1.0/.github/workflows/publish.yml +55 -0
- cubepi-0.1.0/.gitignore +28 -0
- cubepi-0.1.0/.python-version +1 -0
- cubepi-0.1.0/LICENSE +21 -0
- cubepi-0.1.0/PKG-INFO +246 -0
- cubepi-0.1.0/README.md +225 -0
- cubepi-0.1.0/cubepi/__init__.py +48 -0
- cubepi-0.1.0/cubepi/agent/__init__.py +53 -0
- cubepi-0.1.0/cubepi/agent/agent.py +350 -0
- cubepi-0.1.0/cubepi/agent/loop.py +334 -0
- cubepi-0.1.0/cubepi/agent/tools.py +384 -0
- cubepi-0.1.0/cubepi/agent/types.py +170 -0
- cubepi-0.1.0/cubepi/checkpointer/__init__.py +4 -0
- cubepi-0.1.0/cubepi/checkpointer/base.py +17 -0
- cubepi-0.1.0/cubepi/checkpointer/memory.py +23 -0
- cubepi-0.1.0/cubepi/checkpointer/sqlite.py +118 -0
- cubepi-0.1.0/cubepi/middleware/__init__.py +3 -0
- cubepi-0.1.0/cubepi/middleware/base.py +88 -0
- cubepi-0.1.0/cubepi/providers/__init__.py +65 -0
- cubepi-0.1.0/cubepi/providers/anthropic.py +308 -0
- cubepi-0.1.0/cubepi/providers/base.py +154 -0
- cubepi-0.1.0/cubepi/providers/faux.py +333 -0
- cubepi-0.1.0/cubepi/providers/openai.py +281 -0
- cubepi-0.1.0/cubepi/py.typed +0 -0
- cubepi-0.1.0/pyproject.toml +45 -0
- cubepi-0.1.0/tests/__init__.py +0 -0
- cubepi-0.1.0/tests/agent/__init__.py +0 -0
- cubepi-0.1.0/tests/agent/test_agent.py +289 -0
- cubepi-0.1.0/tests/agent/test_e2e.py +257 -0
- cubepi-0.1.0/tests/agent/test_loop.py +376 -0
- cubepi-0.1.0/tests/agent/test_tools.py +281 -0
- cubepi-0.1.0/tests/agent/test_types.py +149 -0
- cubepi-0.1.0/tests/checkpointer/__init__.py +0 -0
- cubepi-0.1.0/tests/checkpointer/test_memory.py +52 -0
- cubepi-0.1.0/tests/checkpointer/test_sqlite.py +63 -0
- cubepi-0.1.0/tests/conftest.py +0 -0
- cubepi-0.1.0/tests/middleware/__init__.py +0 -0
- cubepi-0.1.0/tests/middleware/test_base.py +178 -0
- cubepi-0.1.0/tests/providers/__init__.py +0 -0
- cubepi-0.1.0/tests/providers/test_anthropic.py +62 -0
- cubepi-0.1.0/tests/providers/test_base.py +149 -0
- cubepi-0.1.0/tests/providers/test_faux.py +286 -0
- cubepi-0.1.0/tests/providers/test_openai.py +63 -0
- cubepi-0.1.0/uv.lock +705 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
strategy:
|
|
13
|
+
matrix:
|
|
14
|
+
python-version: ["3.11", "3.12", "3.13"]
|
|
15
|
+
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v6
|
|
18
|
+
|
|
19
|
+
- uses: astral-sh/setup-uv@v8.1.0
|
|
20
|
+
with:
|
|
21
|
+
version: "latest"
|
|
22
|
+
|
|
23
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
24
|
+
run: uv python install ${{ matrix.python-version }}
|
|
25
|
+
|
|
26
|
+
- name: Install dependencies
|
|
27
|
+
run: uv sync --all-extras --dev
|
|
28
|
+
|
|
29
|
+
- name: Run tests
|
|
30
|
+
run: uv run pytest tests/ -v --cov=cubepi --cov-report=term-missing
|
|
31
|
+
|
|
32
|
+
- name: Check import
|
|
33
|
+
run: uv run python -c "import cubepi; print(cubepi.__all__)"
|
|
34
|
+
|
|
35
|
+
lint:
|
|
36
|
+
runs-on: ubuntu-latest
|
|
37
|
+
steps:
|
|
38
|
+
- uses: actions/checkout@v6
|
|
39
|
+
|
|
40
|
+
- uses: astral-sh/setup-uv@v8.1.0
|
|
41
|
+
with:
|
|
42
|
+
version: "latest"
|
|
43
|
+
|
|
44
|
+
- name: Install dependencies
|
|
45
|
+
run: uv sync --all-extras --dev
|
|
46
|
+
|
|
47
|
+
- name: Ruff check
|
|
48
|
+
run: uv run ruff check cubepi/ tests/
|
|
49
|
+
|
|
50
|
+
- name: Ruff format check
|
|
51
|
+
run: uv run ruff format --check cubepi/ tests/
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
build:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
steps:
|
|
11
|
+
- uses: actions/checkout@v6
|
|
12
|
+
|
|
13
|
+
- uses: astral-sh/setup-uv@v8.1.0
|
|
14
|
+
with:
|
|
15
|
+
version: "latest"
|
|
16
|
+
|
|
17
|
+
- name: Build package
|
|
18
|
+
run: uv build
|
|
19
|
+
|
|
20
|
+
- uses: actions/upload-artifact@v7
|
|
21
|
+
with:
|
|
22
|
+
name: dist
|
|
23
|
+
path: dist/
|
|
24
|
+
|
|
25
|
+
publish-testpypi:
|
|
26
|
+
needs: build
|
|
27
|
+
runs-on: ubuntu-latest
|
|
28
|
+
environment: testpypi
|
|
29
|
+
permissions:
|
|
30
|
+
id-token: write
|
|
31
|
+
steps:
|
|
32
|
+
- uses: actions/download-artifact@v8
|
|
33
|
+
with:
|
|
34
|
+
name: dist
|
|
35
|
+
path: dist/
|
|
36
|
+
|
|
37
|
+
- name: Publish to TestPyPI
|
|
38
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
39
|
+
with:
|
|
40
|
+
repository-url: https://test.pypi.org/legacy/
|
|
41
|
+
|
|
42
|
+
publish-pypi:
|
|
43
|
+
needs: publish-testpypi
|
|
44
|
+
runs-on: ubuntu-latest
|
|
45
|
+
environment: pypi
|
|
46
|
+
permissions:
|
|
47
|
+
id-token: write
|
|
48
|
+
steps:
|
|
49
|
+
- uses: actions/download-artifact@v8
|
|
50
|
+
with:
|
|
51
|
+
name: dist
|
|
52
|
+
path: dist/
|
|
53
|
+
|
|
54
|
+
- name: Publish to PyPI
|
|
55
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
cubepi-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
docs/specs
|
|
2
|
+
docs/plans
|
|
3
|
+
|
|
4
|
+
# Python
|
|
5
|
+
__pycache__/
|
|
6
|
+
*.pyc
|
|
7
|
+
*.pyo
|
|
8
|
+
*.egg-info/
|
|
9
|
+
dist/
|
|
10
|
+
build/
|
|
11
|
+
|
|
12
|
+
# Virtual environments
|
|
13
|
+
.venv/
|
|
14
|
+
|
|
15
|
+
# Testing / coverage
|
|
16
|
+
.coverage
|
|
17
|
+
.pytest_cache/
|
|
18
|
+
htmlcov/
|
|
19
|
+
|
|
20
|
+
# IDE
|
|
21
|
+
.idea/
|
|
22
|
+
.vscode/
|
|
23
|
+
*.swp
|
|
24
|
+
*.swo
|
|
25
|
+
|
|
26
|
+
# OS
|
|
27
|
+
.DS_Store
|
|
28
|
+
Thumbs.db
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.13
|
cubepi-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 xf gong
|
|
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.
|
cubepi-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cubepi
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Pythonic async-native agent framework
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Classifier: Development Status :: 3 - Alpha
|
|
8
|
+
Classifier: Framework :: AsyncIO
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
13
|
+
Classifier: Typing :: Typed
|
|
14
|
+
Requires-Python: >=3.11
|
|
15
|
+
Requires-Dist: anthropic>=0.100.0
|
|
16
|
+
Requires-Dist: openai>=2.36.0
|
|
17
|
+
Requires-Dist: pydantic>=2.13.4
|
|
18
|
+
Provides-Extra: sqlite
|
|
19
|
+
Requires-Dist: aiosqlite>=0.22.1; extra == 'sqlite'
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# cubepi
|
|
23
|
+
|
|
24
|
+
Pythonic async-native agent framework. Built to replace [langgraph](https://github.com/langchain-ai/langgraph) with something simpler, faster, and easier to reason about.
|
|
25
|
+
|
|
26
|
+
Inspired by [pi-agent-core](https://github.com/anthropics/pi-agent-core) (TypeScript), redesigned for Python.
|
|
27
|
+
|
|
28
|
+
## Why cubepi
|
|
29
|
+
|
|
30
|
+
### vs langgraph
|
|
31
|
+
|
|
32
|
+
| | langgraph | cubepi |
|
|
33
|
+
|---|---|---|
|
|
34
|
+
| **Abstraction** | Graph nodes + edges + channels — you model your agent as a state machine | Plain async functions — `run_agent_loop` is a while loop you can read in 5 minutes |
|
|
35
|
+
| **Streaming** | Callback-based, multiple handler types | `async for event in stream` — one pattern everywhere |
|
|
36
|
+
| **Checkpointing** | Full snapshot per step — serializes entire message list on every channel change | Append-only — writes only new messages, O(1) DB I/O regardless of conversation length |
|
|
37
|
+
| **Dependencies** | Pulls in langchain-core, langgraph-sdk, and transitive deps | 3 core deps: `pydantic`, `anthropic`, `openai` |
|
|
38
|
+
| **Tool execution** | Tools are graph nodes with manual wiring | Declare tools as functions, framework handles routing and parallel execution |
|
|
39
|
+
| **Multi-provider** | Via langchain chat model adapters | Native Provider protocol — Anthropic, OpenAI built in, add your own with one class |
|
|
40
|
+
| **Middleware** | Graph-level middleware on node entry/exit | Agent-level middleware with 5 typed hooks and declarative composition rules |
|
|
41
|
+
| **Observability** | LangSmith / Langfuse integration, full trace visualization | Events + middleware hooks — bring your own tracing |
|
|
42
|
+
|
|
43
|
+
### vs pi-agent-core
|
|
44
|
+
|
|
45
|
+
cubepi is a Python port of pi's architecture with Pythonic improvements:
|
|
46
|
+
|
|
47
|
+
| | pi-agent-core | cubepi |
|
|
48
|
+
|---|---|---|
|
|
49
|
+
| **Language** | TypeScript | Python (async-native) |
|
|
50
|
+
| **Type system** | Zod schemas | Pydantic v2 — validation, serialization, JSON Schema generation in one |
|
|
51
|
+
| **Cancel signal** | `AbortSignal` (Web API) | `asyncio.Event` — same semantics, native to Python |
|
|
52
|
+
| **Middleware** | Hooks only (callbacks on Agent) | Hooks + composable Middleware protocol with `compose_middleware()` |
|
|
53
|
+
| **Checkpointing** | Not built in | Built-in `MemoryCheckpointer` + `SQLiteCheckpointer` |
|
|
54
|
+
| **Test utility** | Internal test helpers | `FauxProvider` as public API — ship it, use it in your tests |
|
|
55
|
+
|
|
56
|
+
## Install
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
pip install cubepi
|
|
60
|
+
|
|
61
|
+
# With SQLite checkpointer
|
|
62
|
+
pip install cubepi[sqlite]
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Or with [uv](https://github.com/astral-sh/uv):
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
uv add cubepi
|
|
69
|
+
uv add cubepi[sqlite]
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Quick Start
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
import asyncio
|
|
76
|
+
from cubepi import Agent, AgentTool, Model
|
|
77
|
+
from cubepi.providers import AnthropicProvider
|
|
78
|
+
|
|
79
|
+
provider = AnthropicProvider(api_key="sk-...")
|
|
80
|
+
|
|
81
|
+
def get_weather(city: str) -> str:
|
|
82
|
+
"""Get current weather for a city."""
|
|
83
|
+
return f"72°F and sunny in {city}"
|
|
84
|
+
|
|
85
|
+
agent = Agent(
|
|
86
|
+
model=Model(provider=provider, model="claude-sonnet-4-20250514"),
|
|
87
|
+
tools=[
|
|
88
|
+
AgentTool(
|
|
89
|
+
name="get_weather",
|
|
90
|
+
description="Get current weather for a city",
|
|
91
|
+
parameters={
|
|
92
|
+
"type": "object",
|
|
93
|
+
"properties": {"city": {"type": "string"}},
|
|
94
|
+
"required": ["city"],
|
|
95
|
+
},
|
|
96
|
+
execute=get_weather,
|
|
97
|
+
),
|
|
98
|
+
],
|
|
99
|
+
system_prompt="You are a helpful weather assistant.",
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
async def main():
|
|
103
|
+
stream = await agent.prompt("What's the weather in Tokyo?")
|
|
104
|
+
async for event in stream:
|
|
105
|
+
if event.type == "text_delta":
|
|
106
|
+
print(event.delta, end="", flush=True)
|
|
107
|
+
print()
|
|
108
|
+
|
|
109
|
+
asyncio.run(main())
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Architecture
|
|
113
|
+
|
|
114
|
+
```
|
|
115
|
+
cubepi/
|
|
116
|
+
├── providers/ # LLM provider abstraction
|
|
117
|
+
│ ├── base.py # Provider protocol, message types, MessageStream
|
|
118
|
+
│ ├── anthropic.py # Anthropic provider
|
|
119
|
+
│ ├── openai.py # OpenAI provider
|
|
120
|
+
│ └── faux.py # Test utility — pre-configured responses with realistic streaming
|
|
121
|
+
├── agent/ # Agent runtime
|
|
122
|
+
│ ├── agent.py # Stateful Agent class
|
|
123
|
+
│ ├── loop.py # Stateless core loop (the actual algorithm)
|
|
124
|
+
│ ├── tools.py # Tool execution engine (sequential + parallel)
|
|
125
|
+
│ └── types.py # Events, AgentTool, AgentContext, hook types
|
|
126
|
+
├── middleware/ # Composable middleware protocol
|
|
127
|
+
│ └── base.py # 5 hooks with distinct composition rules
|
|
128
|
+
└── checkpointer/ # Persistence
|
|
129
|
+
├── base.py # Checkpointer protocol
|
|
130
|
+
├── memory.py # In-memory (dev/test)
|
|
131
|
+
└── sqlite.py # SQLite (lightweight persistence)
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Core Concepts
|
|
135
|
+
|
|
136
|
+
### Providers
|
|
137
|
+
|
|
138
|
+
Abstract LLM interaction behind a `Provider` protocol. All providers return `MessageStream` — an async iterator of `StreamEvent`s.
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
from cubepi.providers import AnthropicProvider, OpenAIProvider, FauxProvider
|
|
142
|
+
|
|
143
|
+
# Real providers
|
|
144
|
+
anthropic = AnthropicProvider(api_key="...")
|
|
145
|
+
openai = OpenAIProvider(api_key="...")
|
|
146
|
+
|
|
147
|
+
# Test provider — no API calls, fully deterministic
|
|
148
|
+
faux = FauxProvider()
|
|
149
|
+
faux.set_responses(["Hello!", "How can I help?"])
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Tools
|
|
153
|
+
|
|
154
|
+
Declare tools with a name, JSON Schema parameters, and a sync or async execute function. The framework handles argument parsing, parallel execution, and error wrapping.
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
from cubepi import AgentTool
|
|
158
|
+
|
|
159
|
+
tool = AgentTool(
|
|
160
|
+
name="search",
|
|
161
|
+
description="Search the web",
|
|
162
|
+
parameters={
|
|
163
|
+
"type": "object",
|
|
164
|
+
"properties": {"query": {"type": "string"}},
|
|
165
|
+
"required": ["query"],
|
|
166
|
+
},
|
|
167
|
+
execute=lambda query: f"Results for: {query}",
|
|
168
|
+
sequential=False, # allow parallel execution (default)
|
|
169
|
+
)
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### Middleware
|
|
173
|
+
|
|
174
|
+
Composable hooks that modify behavior without touching the core loop:
|
|
175
|
+
|
|
176
|
+
```python
|
|
177
|
+
from cubepi import Middleware, compose_middleware
|
|
178
|
+
|
|
179
|
+
class LoggingMiddleware(Middleware):
|
|
180
|
+
async def transform_context(self, messages, *, signal=None):
|
|
181
|
+
print(f"Context has {len(messages)} messages")
|
|
182
|
+
return messages
|
|
183
|
+
|
|
184
|
+
class SafetyMiddleware(Middleware):
|
|
185
|
+
async def before_tool_call(self, ctx, *, signal=None):
|
|
186
|
+
if ctx.tool_call.name == "dangerous_tool":
|
|
187
|
+
return BeforeToolCallResult(block=True, content="Blocked by policy")
|
|
188
|
+
return None
|
|
189
|
+
|
|
190
|
+
hooks = compose_middleware([LoggingMiddleware(), SafetyMiddleware()])
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
**Composition rules:**
|
|
194
|
+
|
|
195
|
+
| Hook | Rule |
|
|
196
|
+
|------|------|
|
|
197
|
+
| `transform_context` | Chained — each receives previous result |
|
|
198
|
+
| `convert_to_llm` | Last implementation wins |
|
|
199
|
+
| `before_tool_call` | Any block stops execution |
|
|
200
|
+
| `after_tool_call` | Later overrides earlier |
|
|
201
|
+
| `should_stop_after_turn` | Any true stops |
|
|
202
|
+
|
|
203
|
+
### Checkpointer
|
|
204
|
+
|
|
205
|
+
Persist conversation state with append-only semantics:
|
|
206
|
+
|
|
207
|
+
```python
|
|
208
|
+
from cubepi.checkpointer import MemoryCheckpointer, SQLiteCheckpointer
|
|
209
|
+
|
|
210
|
+
# In-memory for dev/test
|
|
211
|
+
cp = MemoryCheckpointer()
|
|
212
|
+
|
|
213
|
+
# SQLite for lightweight persistence
|
|
214
|
+
async with SQLiteCheckpointer("agent.db") as cp:
|
|
215
|
+
agent = Agent(model=model, checkpointer=cp, thread_id="conv-1")
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### FauxProvider for Testing
|
|
219
|
+
|
|
220
|
+
Ship your agent tests without API keys:
|
|
221
|
+
|
|
222
|
+
```python
|
|
223
|
+
from cubepi.providers import FauxProvider, faux_text, faux_tool_call, faux_assistant_message
|
|
224
|
+
|
|
225
|
+
provider = FauxProvider()
|
|
226
|
+
provider.set_responses([
|
|
227
|
+
faux_assistant_message([
|
|
228
|
+
faux_tool_call("search", {"query": "python"}),
|
|
229
|
+
]),
|
|
230
|
+
faux_assistant_message("Here are the results..."),
|
|
231
|
+
])
|
|
232
|
+
|
|
233
|
+
agent = Agent(model=Model(provider=provider, model="test"), tools=[search_tool])
|
|
234
|
+
stream = await agent.prompt("Search for python")
|
|
235
|
+
# Streams realistic deltas — content_block_start, text_delta, etc.
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
## Requirements
|
|
239
|
+
|
|
240
|
+
- Python >= 3.11
|
|
241
|
+
- Core: `pydantic`, `anthropic`, `openai`
|
|
242
|
+
- Optional: `aiosqlite` (for `SQLiteCheckpointer`)
|
|
243
|
+
|
|
244
|
+
## License
|
|
245
|
+
|
|
246
|
+
MIT
|
cubepi-0.1.0/README.md
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
# cubepi
|
|
2
|
+
|
|
3
|
+
Pythonic async-native agent framework. Built to replace [langgraph](https://github.com/langchain-ai/langgraph) with something simpler, faster, and easier to reason about.
|
|
4
|
+
|
|
5
|
+
Inspired by [pi-agent-core](https://github.com/anthropics/pi-agent-core) (TypeScript), redesigned for Python.
|
|
6
|
+
|
|
7
|
+
## Why cubepi
|
|
8
|
+
|
|
9
|
+
### vs langgraph
|
|
10
|
+
|
|
11
|
+
| | langgraph | cubepi |
|
|
12
|
+
|---|---|---|
|
|
13
|
+
| **Abstraction** | Graph nodes + edges + channels — you model your agent as a state machine | Plain async functions — `run_agent_loop` is a while loop you can read in 5 minutes |
|
|
14
|
+
| **Streaming** | Callback-based, multiple handler types | `async for event in stream` — one pattern everywhere |
|
|
15
|
+
| **Checkpointing** | Full snapshot per step — serializes entire message list on every channel change | Append-only — writes only new messages, O(1) DB I/O regardless of conversation length |
|
|
16
|
+
| **Dependencies** | Pulls in langchain-core, langgraph-sdk, and transitive deps | 3 core deps: `pydantic`, `anthropic`, `openai` |
|
|
17
|
+
| **Tool execution** | Tools are graph nodes with manual wiring | Declare tools as functions, framework handles routing and parallel execution |
|
|
18
|
+
| **Multi-provider** | Via langchain chat model adapters | Native Provider protocol — Anthropic, OpenAI built in, add your own with one class |
|
|
19
|
+
| **Middleware** | Graph-level middleware on node entry/exit | Agent-level middleware with 5 typed hooks and declarative composition rules |
|
|
20
|
+
| **Observability** | LangSmith / Langfuse integration, full trace visualization | Events + middleware hooks — bring your own tracing |
|
|
21
|
+
|
|
22
|
+
### vs pi-agent-core
|
|
23
|
+
|
|
24
|
+
cubepi is a Python port of pi's architecture with Pythonic improvements:
|
|
25
|
+
|
|
26
|
+
| | pi-agent-core | cubepi |
|
|
27
|
+
|---|---|---|
|
|
28
|
+
| **Language** | TypeScript | Python (async-native) |
|
|
29
|
+
| **Type system** | Zod schemas | Pydantic v2 — validation, serialization, JSON Schema generation in one |
|
|
30
|
+
| **Cancel signal** | `AbortSignal` (Web API) | `asyncio.Event` — same semantics, native to Python |
|
|
31
|
+
| **Middleware** | Hooks only (callbacks on Agent) | Hooks + composable Middleware protocol with `compose_middleware()` |
|
|
32
|
+
| **Checkpointing** | Not built in | Built-in `MemoryCheckpointer` + `SQLiteCheckpointer` |
|
|
33
|
+
| **Test utility** | Internal test helpers | `FauxProvider` as public API — ship it, use it in your tests |
|
|
34
|
+
|
|
35
|
+
## Install
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install cubepi
|
|
39
|
+
|
|
40
|
+
# With SQLite checkpointer
|
|
41
|
+
pip install cubepi[sqlite]
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Or with [uv](https://github.com/astral-sh/uv):
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
uv add cubepi
|
|
48
|
+
uv add cubepi[sqlite]
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Quick Start
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
import asyncio
|
|
55
|
+
from cubepi import Agent, AgentTool, Model
|
|
56
|
+
from cubepi.providers import AnthropicProvider
|
|
57
|
+
|
|
58
|
+
provider = AnthropicProvider(api_key="sk-...")
|
|
59
|
+
|
|
60
|
+
def get_weather(city: str) -> str:
|
|
61
|
+
"""Get current weather for a city."""
|
|
62
|
+
return f"72°F and sunny in {city}"
|
|
63
|
+
|
|
64
|
+
agent = Agent(
|
|
65
|
+
model=Model(provider=provider, model="claude-sonnet-4-20250514"),
|
|
66
|
+
tools=[
|
|
67
|
+
AgentTool(
|
|
68
|
+
name="get_weather",
|
|
69
|
+
description="Get current weather for a city",
|
|
70
|
+
parameters={
|
|
71
|
+
"type": "object",
|
|
72
|
+
"properties": {"city": {"type": "string"}},
|
|
73
|
+
"required": ["city"],
|
|
74
|
+
},
|
|
75
|
+
execute=get_weather,
|
|
76
|
+
),
|
|
77
|
+
],
|
|
78
|
+
system_prompt="You are a helpful weather assistant.",
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
async def main():
|
|
82
|
+
stream = await agent.prompt("What's the weather in Tokyo?")
|
|
83
|
+
async for event in stream:
|
|
84
|
+
if event.type == "text_delta":
|
|
85
|
+
print(event.delta, end="", flush=True)
|
|
86
|
+
print()
|
|
87
|
+
|
|
88
|
+
asyncio.run(main())
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Architecture
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
cubepi/
|
|
95
|
+
├── providers/ # LLM provider abstraction
|
|
96
|
+
│ ├── base.py # Provider protocol, message types, MessageStream
|
|
97
|
+
│ ├── anthropic.py # Anthropic provider
|
|
98
|
+
│ ├── openai.py # OpenAI provider
|
|
99
|
+
│ └── faux.py # Test utility — pre-configured responses with realistic streaming
|
|
100
|
+
├── agent/ # Agent runtime
|
|
101
|
+
│ ├── agent.py # Stateful Agent class
|
|
102
|
+
│ ├── loop.py # Stateless core loop (the actual algorithm)
|
|
103
|
+
│ ├── tools.py # Tool execution engine (sequential + parallel)
|
|
104
|
+
│ └── types.py # Events, AgentTool, AgentContext, hook types
|
|
105
|
+
├── middleware/ # Composable middleware protocol
|
|
106
|
+
│ └── base.py # 5 hooks with distinct composition rules
|
|
107
|
+
└── checkpointer/ # Persistence
|
|
108
|
+
├── base.py # Checkpointer protocol
|
|
109
|
+
├── memory.py # In-memory (dev/test)
|
|
110
|
+
└── sqlite.py # SQLite (lightweight persistence)
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Core Concepts
|
|
114
|
+
|
|
115
|
+
### Providers
|
|
116
|
+
|
|
117
|
+
Abstract LLM interaction behind a `Provider` protocol. All providers return `MessageStream` — an async iterator of `StreamEvent`s.
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
from cubepi.providers import AnthropicProvider, OpenAIProvider, FauxProvider
|
|
121
|
+
|
|
122
|
+
# Real providers
|
|
123
|
+
anthropic = AnthropicProvider(api_key="...")
|
|
124
|
+
openai = OpenAIProvider(api_key="...")
|
|
125
|
+
|
|
126
|
+
# Test provider — no API calls, fully deterministic
|
|
127
|
+
faux = FauxProvider()
|
|
128
|
+
faux.set_responses(["Hello!", "How can I help?"])
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Tools
|
|
132
|
+
|
|
133
|
+
Declare tools with a name, JSON Schema parameters, and a sync or async execute function. The framework handles argument parsing, parallel execution, and error wrapping.
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
from cubepi import AgentTool
|
|
137
|
+
|
|
138
|
+
tool = AgentTool(
|
|
139
|
+
name="search",
|
|
140
|
+
description="Search the web",
|
|
141
|
+
parameters={
|
|
142
|
+
"type": "object",
|
|
143
|
+
"properties": {"query": {"type": "string"}},
|
|
144
|
+
"required": ["query"],
|
|
145
|
+
},
|
|
146
|
+
execute=lambda query: f"Results for: {query}",
|
|
147
|
+
sequential=False, # allow parallel execution (default)
|
|
148
|
+
)
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Middleware
|
|
152
|
+
|
|
153
|
+
Composable hooks that modify behavior without touching the core loop:
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
from cubepi import Middleware, compose_middleware
|
|
157
|
+
|
|
158
|
+
class LoggingMiddleware(Middleware):
|
|
159
|
+
async def transform_context(self, messages, *, signal=None):
|
|
160
|
+
print(f"Context has {len(messages)} messages")
|
|
161
|
+
return messages
|
|
162
|
+
|
|
163
|
+
class SafetyMiddleware(Middleware):
|
|
164
|
+
async def before_tool_call(self, ctx, *, signal=None):
|
|
165
|
+
if ctx.tool_call.name == "dangerous_tool":
|
|
166
|
+
return BeforeToolCallResult(block=True, content="Blocked by policy")
|
|
167
|
+
return None
|
|
168
|
+
|
|
169
|
+
hooks = compose_middleware([LoggingMiddleware(), SafetyMiddleware()])
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
**Composition rules:**
|
|
173
|
+
|
|
174
|
+
| Hook | Rule |
|
|
175
|
+
|------|------|
|
|
176
|
+
| `transform_context` | Chained — each receives previous result |
|
|
177
|
+
| `convert_to_llm` | Last implementation wins |
|
|
178
|
+
| `before_tool_call` | Any block stops execution |
|
|
179
|
+
| `after_tool_call` | Later overrides earlier |
|
|
180
|
+
| `should_stop_after_turn` | Any true stops |
|
|
181
|
+
|
|
182
|
+
### Checkpointer
|
|
183
|
+
|
|
184
|
+
Persist conversation state with append-only semantics:
|
|
185
|
+
|
|
186
|
+
```python
|
|
187
|
+
from cubepi.checkpointer import MemoryCheckpointer, SQLiteCheckpointer
|
|
188
|
+
|
|
189
|
+
# In-memory for dev/test
|
|
190
|
+
cp = MemoryCheckpointer()
|
|
191
|
+
|
|
192
|
+
# SQLite for lightweight persistence
|
|
193
|
+
async with SQLiteCheckpointer("agent.db") as cp:
|
|
194
|
+
agent = Agent(model=model, checkpointer=cp, thread_id="conv-1")
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### FauxProvider for Testing
|
|
198
|
+
|
|
199
|
+
Ship your agent tests without API keys:
|
|
200
|
+
|
|
201
|
+
```python
|
|
202
|
+
from cubepi.providers import FauxProvider, faux_text, faux_tool_call, faux_assistant_message
|
|
203
|
+
|
|
204
|
+
provider = FauxProvider()
|
|
205
|
+
provider.set_responses([
|
|
206
|
+
faux_assistant_message([
|
|
207
|
+
faux_tool_call("search", {"query": "python"}),
|
|
208
|
+
]),
|
|
209
|
+
faux_assistant_message("Here are the results..."),
|
|
210
|
+
])
|
|
211
|
+
|
|
212
|
+
agent = Agent(model=Model(provider=provider, model="test"), tools=[search_tool])
|
|
213
|
+
stream = await agent.prompt("Search for python")
|
|
214
|
+
# Streams realistic deltas — content_block_start, text_delta, etc.
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
## Requirements
|
|
218
|
+
|
|
219
|
+
- Python >= 3.11
|
|
220
|
+
- Core: `pydantic`, `anthropic`, `openai`
|
|
221
|
+
- Optional: `aiosqlite` (for `SQLiteCheckpointer`)
|
|
222
|
+
|
|
223
|
+
## License
|
|
224
|
+
|
|
225
|
+
MIT
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""cubepi — Pythonic async-native agent framework."""
|
|
2
|
+
|
|
3
|
+
from cubepi.agent import (
|
|
4
|
+
Agent,
|
|
5
|
+
AgentState,
|
|
6
|
+
AgentTool,
|
|
7
|
+
AgentToolResult,
|
|
8
|
+
run_agent_loop,
|
|
9
|
+
run_agent_loop_continue,
|
|
10
|
+
)
|
|
11
|
+
from cubepi.middleware import Middleware, compose_middleware
|
|
12
|
+
from cubepi.providers import (
|
|
13
|
+
AssistantMessage,
|
|
14
|
+
Message,
|
|
15
|
+
MessageStream,
|
|
16
|
+
Model,
|
|
17
|
+
Provider,
|
|
18
|
+
StreamEvent,
|
|
19
|
+
TextContent,
|
|
20
|
+
ThinkingLevel,
|
|
21
|
+
ToolCall,
|
|
22
|
+
ToolDefinition,
|
|
23
|
+
ToolResultMessage,
|
|
24
|
+
UserMessage,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"Agent",
|
|
29
|
+
"AgentState",
|
|
30
|
+
"AgentTool",
|
|
31
|
+
"AgentToolResult",
|
|
32
|
+
"AssistantMessage",
|
|
33
|
+
"Message",
|
|
34
|
+
"MessageStream",
|
|
35
|
+
"Middleware",
|
|
36
|
+
"Model",
|
|
37
|
+
"Provider",
|
|
38
|
+
"StreamEvent",
|
|
39
|
+
"TextContent",
|
|
40
|
+
"ThinkingLevel",
|
|
41
|
+
"ToolCall",
|
|
42
|
+
"ToolDefinition",
|
|
43
|
+
"ToolResultMessage",
|
|
44
|
+
"UserMessage",
|
|
45
|
+
"compose_middleware",
|
|
46
|
+
"run_agent_loop",
|
|
47
|
+
"run_agent_loop_continue",
|
|
48
|
+
]
|