reflex-agentkit 0.1.0__py3-none-any.whl
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.
- reflex_agentkit/__init__.py +68 -0
- reflex_agentkit/adapters/__init__.py +1 -0
- reflex_agentkit/adapters/_require.py +27 -0
- reflex_agentkit/adapters/langgraph.py +277 -0
- reflex_agentkit/adapters/pydantic_ai.py +93 -0
- reflex_agentkit/components/__init__.py +21 -0
- reflex_agentkit/components/approval.py +123 -0
- reflex_agentkit/components/chat.py +42 -0
- reflex_agentkit/components/composer.py +44 -0
- reflex_agentkit/components/messages.py +104 -0
- reflex_agentkit/components/shared_state.py +32 -0
- reflex_agentkit/components/status.py +41 -0
- reflex_agentkit/components/tools.py +97 -0
- reflex_agentkit/core/__init__.py +56 -0
- reflex_agentkit/core/_log.py +47 -0
- reflex_agentkit/core/_patch.py +46 -0
- reflex_agentkit/core/clients/__init__.py +14 -0
- reflex_agentkit/core/clients/base.py +71 -0
- reflex_agentkit/core/clients/http.py +147 -0
- reflex_agentkit/core/clients/in_process.py +67 -0
- reflex_agentkit/core/clients/sse.py +75 -0
- reflex_agentkit/core/commands.py +589 -0
- reflex_agentkit/core/conversation.py +93 -0
- reflex_agentkit/core/errors.py +74 -0
- reflex_agentkit/core/models.py +243 -0
- reflex_agentkit/core/reducer.py +1078 -0
- reflex_agentkit/core/run_input.py +122 -0
- reflex_agentkit/core/tools.py +204 -0
- reflex_agentkit/py.typed +0 -0
- reflex_agentkit/state/__init__.py +5 -0
- reflex_agentkit/state/_driver.py +520 -0
- reflex_agentkit/state/_projection.py +120 -0
- reflex_agentkit/state/_registry.py +46 -0
- reflex_agentkit/state/agent_state.py +496 -0
- reflex_agentkit-0.1.0.dist-info/METADATA +457 -0
- reflex_agentkit-0.1.0.dist-info/RECORD +38 -0
- reflex_agentkit-0.1.0.dist-info/WHEEL +4 -0
- reflex_agentkit-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: reflex-agentkit
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: The production UI layer for Python AI agents, built on Reflex and the AG-UI protocol.
|
|
5
|
+
Project-URL: Homepage, https://github.com/ecrespo/reflex-agentkit
|
|
6
|
+
Project-URL: Repository, https://github.com/ecrespo/reflex-agentkit
|
|
7
|
+
Project-URL: Issues, https://github.com/ecrespo/reflex-agentkit/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/ecrespo/reflex-agentkit/blob/main/CHANGELOG.md
|
|
9
|
+
Author-email: Ernesto Crespo <ecrespo@gmail.com>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: ag-ui,agents,chat,langgraph,llm,pydantic-ai,reflex
|
|
13
|
+
Classifier: Development Status :: 2 - Pre-Alpha
|
|
14
|
+
Classifier: Framework :: AsyncIO
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
22
|
+
Classifier: Typing :: Typed
|
|
23
|
+
Requires-Python: <3.14,>=3.11
|
|
24
|
+
Requires-Dist: ag-ui-protocol<2,>=1.0.0
|
|
25
|
+
Requires-Dist: httpx<1,>=0.28
|
|
26
|
+
Requires-Dist: jsonpatch<2,>=1.33
|
|
27
|
+
Requires-Dist: pydantic<3,>=2.11.2
|
|
28
|
+
Requires-Dist: reflex<0.11,>=0.10.1
|
|
29
|
+
Provides-Extra: all
|
|
30
|
+
Requires-Dist: ag-ui-langgraph[fastapi]<0.1,>=0.0.47; extra == 'all'
|
|
31
|
+
Requires-Dist: langgraph<2,>=1.2; extra == 'all'
|
|
32
|
+
Requires-Dist: pydantic-ai-slim[ag-ui,anthropic,openai]<3,>=2.64; extra == 'all'
|
|
33
|
+
Provides-Extra: langgraph
|
|
34
|
+
Requires-Dist: ag-ui-langgraph[fastapi]<0.1,>=0.0.47; extra == 'langgraph'
|
|
35
|
+
Requires-Dist: langgraph<2,>=1.2; extra == 'langgraph'
|
|
36
|
+
Provides-Extra: pydantic-ai
|
|
37
|
+
Requires-Dist: pydantic-ai-slim[ag-ui,anthropic,openai]<3,>=2.64; extra == 'pydantic-ai'
|
|
38
|
+
Description-Content-Type: text/markdown
|
|
39
|
+
|
|
40
|
+
# reflex-agentkit
|
|
41
|
+
|
|
42
|
+
Build and serve AI agents in pure Python with Reflex. AG-UI protocol client, streaming
|
|
43
|
+
chat, tool-call cards and human-in-the-loop approval for LangGraph, PydanticAI and any
|
|
44
|
+
AG-UI-compatible agent. The production UI layer your agent was missing — no TypeScript,
|
|
45
|
+
no Streamlit.
|
|
46
|
+
|
|
47
|
+
> **Status: `0.1.0`, the first release.** Everything in v0.1 is implemented and tested: the pure core,
|
|
48
|
+
> the agent clients, the Reflex state engine (`AgentState`), the chat components, both
|
|
49
|
+
> adapters (PydanticAI and LangGraph) and the AG-UI Dojo examples.
|
|
50
|
+
> `pip install "reflex-agentkit[pydantic-ai]"` or `uv add "reflex-agentkit[pydantic-ai]"`. Documentation: [`docs/`](docs/) (`uv run mkdocs serve`).
|
|
51
|
+
|
|
52
|
+
## Why
|
|
53
|
+
|
|
54
|
+
A Python developer with a working agent usually has two options for its UI: Streamlit,
|
|
55
|
+
which re-runs the whole script on every interaction and has to work around streaming tool
|
|
56
|
+
calls, mid-run approvals and cancellation; or a Next.js frontend, which adds a second
|
|
57
|
+
language, build and deployment. reflex-agentkit is the middle: Reflex already holds the
|
|
58
|
+
state on the server, keeps a WebSocket to every browser and runs background tasks, and
|
|
59
|
+
this library adds the agent layer on top, speaking the AG-UI protocol that PydanticAI and
|
|
60
|
+
LangGraph already emit.
|
|
61
|
+
|
|
62
|
+
## Quickstart
|
|
63
|
+
|
|
64
|
+
A streaming chat with a PydanticAI travel agent, one tool call shown as a card and one
|
|
65
|
+
tool that waits for your approval. This is the whole app
|
|
66
|
+
([`examples/quickstart/`](examples/quickstart/)):
|
|
67
|
+
|
|
68
|
+
<!-- quickstart:start -->
|
|
69
|
+
```python
|
|
70
|
+
from typing import ClassVar
|
|
71
|
+
|
|
72
|
+
import reflex as rx
|
|
73
|
+
from pydantic_ai import Agent
|
|
74
|
+
|
|
75
|
+
from reflex_agentkit import AgentState, agent_chat
|
|
76
|
+
from reflex_agentkit.adapters.pydantic_ai import PydanticAIAgent
|
|
77
|
+
|
|
78
|
+
travel_agent = Agent(
|
|
79
|
+
"anthropic:claude-sonnet-5-5",
|
|
80
|
+
instructions="You are a helpful travel assistant.",
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@travel_agent.tool_plain
|
|
85
|
+
def get_weather(city: str) -> str:
|
|
86
|
+
"""Get the current weather for a city."""
|
|
87
|
+
return f"It is sunny and 22 °C in {city}."
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@travel_agent.tool_plain(requires_approval=True)
|
|
91
|
+
def book_hotel(city: str, nights: int) -> str:
|
|
92
|
+
"""Book a hotel room. Requires human approval."""
|
|
93
|
+
return f"Booked {nights} nights in {city}."
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class ChatState(AgentState, rx.State):
|
|
97
|
+
agent: ClassVar = PydanticAIAgent(travel_agent)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def index() -> rx.Component:
|
|
101
|
+
return agent_chat(ChatState)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
app = rx.App()
|
|
105
|
+
app.add_page(index)
|
|
106
|
+
```
|
|
107
|
+
<!-- quickstart:end -->
|
|
108
|
+
|
|
109
|
+
Run it from a clone of this repo:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
uv sync --all-extras
|
|
113
|
+
cd examples/quickstart
|
|
114
|
+
ANTHROPIC_API_KEY=sk-ant-... uv run --package quickstart reflex run
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Ask it to plan a trip: it shows the `get_weather` call and opens the approval dialog
|
|
118
|
+
before `book_hotel` runs. The agentkit components are built on Radix Themes, so the
|
|
119
|
+
example's `rxconfig.py` adds `rx.plugins.RadixThemesPlugin()`.
|
|
120
|
+
|
|
121
|
+
To use a local model instead of Claude, swap the model line for an Ollama model through
|
|
122
|
+
PydanticAI's Ollama provider, for example `Agent("ollama:qwen3", ...)` with
|
|
123
|
+
`OLLAMA_BASE_URL=http://localhost:11434/v1`.
|
|
124
|
+
|
|
125
|
+
The components are in `reflex_agentkit.components`: `agent_chat` composes
|
|
126
|
+
`message_list`, `tool_call_card`, `composer`, `run_status_bar`, `approval_dialog` and,
|
|
127
|
+
with `show_shared_state=True`, `shared_state_panel`. Custom renderers for a tool
|
|
128
|
+
(`tool_renderers={"get_weather": ...}`) or an interrupt reason
|
|
129
|
+
(`interrupt_renderers={...}`) replace the default card body or form. Every control has
|
|
130
|
+
an accessible label.
|
|
131
|
+
|
|
132
|
+
## Features
|
|
133
|
+
|
|
134
|
+
| Feature | How |
|
|
135
|
+
|---|---|
|
|
136
|
+
| Streaming replies | Markdown streamed token by token, batched to at most one push per 50 ms |
|
|
137
|
+
| Tool calls | A card per call with arguments, status and result; custom renderers per tool |
|
|
138
|
+
| Approvals | Approve, edit the arguments, or reject a tool call before it runs |
|
|
139
|
+
| Questions from the agent | AG-UI standard interrupts, answered with a replaceable form |
|
|
140
|
+
| Shared state | The agent's state mirrored in `shared_state`, optionally validated by a Pydantic model, editable by the app |
|
|
141
|
+
| Client tools | Tools the agent calls that run in the Reflex app, e.g. to draw a chart |
|
|
142
|
+
| Real cancellation | Stop closes the agent's stream within a second, also across workers |
|
|
143
|
+
| Any AG-UI agent | In process (PydanticAI, LangGraph adapters) or over HTTP (`HttpAGUIClient`) |
|
|
144
|
+
| Safe by default | Agent, clients and keys never become browser state; every handler validates its input |
|
|
145
|
+
|
|
146
|
+
## How it compares
|
|
147
|
+
|
|
148
|
+
| | reflex-agentkit | Streamlit | CopilotKit | agentstage |
|
|
149
|
+
|---|---|---|---|---|
|
|
150
|
+
| UI written in | Python (Reflex) | Python | React / TypeScript | Python |
|
|
151
|
+
| Agent protocol | AG-UI | none (your own wiring) | AG-UI | its own JSON API over SSE |
|
|
152
|
+
| Agent frameworks | PydanticAI, LangGraph, any AG-UI server | any, by hand | many, through AG-UI | LangChain, LangGraph |
|
|
153
|
+
| Streaming and tool calls | built in | by hand, script reruns | built in | built in |
|
|
154
|
+
| Approvals and questions | AG-UI interrupts, edit before approving | by hand | built in | LangGraph interrupts |
|
|
155
|
+
| Shared state, client tools | built in | by hand | built in | — |
|
|
156
|
+
| Your own UI | any Reflex component | any Streamlit widget | any React component | configured app |
|
|
157
|
+
|
|
158
|
+
*agentstage (`agentstage-ui` on PyPI, 0.1.2, September 2026) per its README.*
|
|
159
|
+
|
|
160
|
+
## Documentation
|
|
161
|
+
|
|
162
|
+
The MkDocs site in [`docs/`](docs/) covers getting started (uv and pip), the concepts,
|
|
163
|
+
guides (your own UI, human in the loop, sidecar deployment, local models with Ollama), the
|
|
164
|
+
API reference, the security model and troubleshooting. Preview it with
|
|
165
|
+
`uv run mkdocs serve`.
|
|
166
|
+
|
|
167
|
+
## Progress
|
|
168
|
+
|
|
169
|
+
Work follows the approved spec chain in [`specs/`](specs/) and the task list in
|
|
170
|
+
[`specs/tasks/agentkit-tasks.md`](specs/tasks/agentkit-tasks.md).
|
|
171
|
+
|
|
172
|
+
| Phase | Scope | Status |
|
|
173
|
+
|---|---|---|
|
|
174
|
+
| WP0 — Foundation | Packaging (uv, hatchling), lint, types, tests, CI | ✅ done |
|
|
175
|
+
| WP1 — Pure core | Models, reducer, commands, client tools, `RunInputBuilder`, property tests | ✅ done |
|
|
176
|
+
| WP2 — Clients | `AgentClient`/`AGUIAgent` protocols, SSE parser, `HttpAGUIClient`, `InProcessAGUIClient` | ✅ done |
|
|
177
|
+
| WP3 — Reflex state engine | `AgentState` mixin, streaming driver, cancellation, client-tool rounds | ✅ done |
|
|
178
|
+
| WP4 — PydanticAI adapter | `PydanticAIAgent`: a PydanticAI agent as an `AGUIAgent`, tool approvals, client tools | ✅ done |
|
|
179
|
+
| WP5 — Components and quickstart | `agent_chat` and its parts, quickstart example, release workflow | ✅ done |
|
|
180
|
+
| WP6 — LangGraph adapter | `LangGraphAGUIAgent`, `request_tool_approval`, legacy interrupts | ✅ done |
|
|
181
|
+
| WP7 — Dojo examples | The five AG-UI Dojo demos for PydanticAI and LangGraph | ✅ done |
|
|
182
|
+
| WP8 — Hardening, docs, release | Security review, benchmarks, documentation site, `0.1.0` on PyPI | ✅ done |
|
|
183
|
+
|
|
184
|
+
## What works today: `reflex_agentkit.core`
|
|
185
|
+
|
|
186
|
+
The core never imports `reflex`. AG-UI is its only event model, and a pure, total reducer
|
|
187
|
+
is the only way agent events change the conversation state.
|
|
188
|
+
|
|
189
|
+
- **`reduce(state, event)`**: handles all 31 AG-UI 1.0 event types (lifecycle and
|
|
190
|
+
`RUN_FINISHED` outcomes, text, tool calls, shared state via JSON Patch, messages
|
|
191
|
+
snapshots, reasoning, custom events). It never raises: problems become diagnostics.
|
|
192
|
+
- **Commands** (`reflex_agentkit.core.commands`): `begin_run`, `apply_approval`
|
|
193
|
+
(optionally with edited arguments), `apply_rejection`, `apply_interrupt_response`,
|
|
194
|
+
`apply_cancel`, `apply_client_tool_result`, `new_thread`, `resume_entries`.
|
|
195
|
+
Human-in-the-loop follows AG-UI standard interrupts and the PydanticAI approval payload.
|
|
196
|
+
- **Client tools**: `@client_tool` builds the JSON Schema from type hints. `invoke()`
|
|
197
|
+
runs sync handlers in a worker thread under a timeout and returns failures as tool
|
|
198
|
+
results.
|
|
199
|
+
- **`RunInputBuilder`**: turns the conversation into an AG-UI `RunAgentInput` (full
|
|
200
|
+
history, encrypted reasoning, edited tool args, shared state, tools, resume entries).
|
|
201
|
+
- **Agent clients**: `HttpAGUIClient` talks to any AG-UI server over HTTP + SSE.
|
|
202
|
+
`InProcessAGUIClient` runs an agent object (anything with
|
|
203
|
+
`run(run_input) -> AsyncIterator[event]`) in the same event loop. Both yield validated
|
|
204
|
+
events, skip invalid ones with a log record, raise typed `AgentHTTPError` /
|
|
205
|
+
`AgentStreamError` on transport failures, and close the upstream when the consumer is
|
|
206
|
+
cancelled. Headers such as API keys are never logged or put in error messages.
|
|
207
|
+
|
|
208
|
+
```python
|
|
209
|
+
from reflex_agentkit.core import ConversationState, RunInputBuilder, client_tool
|
|
210
|
+
from reflex_agentkit.core.commands import apply_approval, begin_run
|
|
211
|
+
from reflex_agentkit.core.models import ReducerConfig
|
|
212
|
+
from reflex_agentkit.core.reducer import parse_event, reduce
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
@client_tool
|
|
216
|
+
def pick_date(day: int) -> str:
|
|
217
|
+
"""Let the user pick a day of the month."""
|
|
218
|
+
return f"2026-05-{day:02d}"
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
config = ReducerConfig(client_tool_names=frozenset({"pick_date"}))
|
|
222
|
+
conv = begin_run(ConversationState.initial(config), config=config, user_text="book it")
|
|
223
|
+
run_input = RunInputBuilder(client_tools=(pick_date,)).build(conv) # send to the agent
|
|
224
|
+
|
|
225
|
+
for raw in [ # AG-UI events as they arrive on the wire
|
|
226
|
+
{"type": "RUN_STARTED", "threadId": conv.thread_id, "runId": conv.run_id},
|
|
227
|
+
{"type": "TOOL_CALL_START", "toolCallId": "c1", "toolCallName": "book_hotel"},
|
|
228
|
+
{"type": "TOOL_CALL_ARGS", "toolCallId": "c1", "delta": '{"city": "Lisbon"}'},
|
|
229
|
+
{"type": "TOOL_CALL_END", "toolCallId": "c1"},
|
|
230
|
+
{"type": "RUN_FINISHED", "threadId": conv.thread_id, "runId": conv.run_id,
|
|
231
|
+
"outcome": {"type": "interrupt",
|
|
232
|
+
"interrupts": [{"id": "int-c1", "reason": "tool_call", "toolCallId": "c1"}]}},
|
|
233
|
+
]:
|
|
234
|
+
event = parse_event(raw)
|
|
235
|
+
conv = reduce(conv, event)
|
|
236
|
+
|
|
237
|
+
print(conv.run_status) # awaiting_input
|
|
238
|
+
conv = apply_approval(conv, "c1", {"city": "Porto"})
|
|
239
|
+
conv = begin_run(conv, config=config, resume=True)
|
|
240
|
+
print(RunInputBuilder().build(conv).resume) # [ResumeEntry(..., payload={'approved': True, 'editedArgs': {...}})]
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
Streaming a run from an AG-UI server:
|
|
244
|
+
|
|
245
|
+
```python
|
|
246
|
+
import asyncio
|
|
247
|
+
import os
|
|
248
|
+
|
|
249
|
+
from reflex_agentkit import HttpAGUIClient
|
|
250
|
+
from reflex_agentkit.core import ConversationState, RunInputBuilder
|
|
251
|
+
from reflex_agentkit.core.commands import begin_run
|
|
252
|
+
from reflex_agentkit.core.models import ReducerConfig
|
|
253
|
+
from reflex_agentkit.core.reducer import reduce
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
async def main() -> None:
|
|
257
|
+
client = HttpAGUIClient(
|
|
258
|
+
"http://localhost:8000/agent",
|
|
259
|
+
headers=lambda: {"Authorization": f"Bearer {os.environ['AGENT_TOKEN']}"},
|
|
260
|
+
)
|
|
261
|
+
config = ReducerConfig()
|
|
262
|
+
conv = begin_run(ConversationState.initial(config), config=config, user_text="Hello!")
|
|
263
|
+
try:
|
|
264
|
+
async for event in client.stream(RunInputBuilder().build(conv)):
|
|
265
|
+
conv = reduce(conv, event)
|
|
266
|
+
finally:
|
|
267
|
+
await client.aclose()
|
|
268
|
+
print(conv.run_status, conv.messages[-1].content)
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
asyncio.run(main())
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
## `AgentState` with your own UI
|
|
275
|
+
|
|
276
|
+
`AgentState` is a Reflex state mixin that holds a conversation with an AG-UI agent. Its
|
|
277
|
+
configuration is class-level (`ClassVar`), so the agent, its client and any API keys
|
|
278
|
+
never become state vars sent to the browser. `agent_chat` is optional: this example
|
|
279
|
+
builds its own UI from plain Reflex components:
|
|
280
|
+
|
|
281
|
+
```python
|
|
282
|
+
from typing import ClassVar
|
|
283
|
+
|
|
284
|
+
import reflex as rx
|
|
285
|
+
|
|
286
|
+
from reflex_agentkit import AgentState, HttpAGUIClient
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
class ChatState(AgentState, rx.State):
|
|
290
|
+
agent: ClassVar = HttpAGUIClient("http://localhost:8000/agent")
|
|
291
|
+
draft: str = ""
|
|
292
|
+
|
|
293
|
+
@rx.event
|
|
294
|
+
def set_draft(self, value: str) -> None:
|
|
295
|
+
self.draft = value
|
|
296
|
+
|
|
297
|
+
@rx.event
|
|
298
|
+
def submit(self):
|
|
299
|
+
text, self.draft = self.draft, ""
|
|
300
|
+
return ChatState.send_message(text)
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def index() -> rx.Component:
|
|
304
|
+
return rx.vstack(
|
|
305
|
+
rx.foreach(ChatState.messages, lambda m: rx.text(m.role, ": ", m.content)),
|
|
306
|
+
rx.cond(ChatState.error != "", rx.callout(ChatState.error, color_scheme="red")),
|
|
307
|
+
rx.hstack(
|
|
308
|
+
rx.input(value=ChatState.draft, on_change=ChatState.set_draft),
|
|
309
|
+
rx.button("Send", on_click=ChatState.submit, disabled=~ChatState.can_send),
|
|
310
|
+
rx.button("Stop", on_click=ChatState.cancel_run, disabled=~ChatState.is_running),
|
|
311
|
+
),
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
app = rx.App()
|
|
316
|
+
app.add_page(index)
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
- **Streaming**: the reply streams in a background task; state pushes are batched (at most
|
|
320
|
+
one per `flush_interval_ms`, default 50 ms).
|
|
321
|
+
- **Handlers**: `send_message`, `cancel_run` (stops within 1 s, also when another backend
|
|
322
|
+
worker handles the click), `approve_tool` / `reject_tool` (with edited arguments),
|
|
323
|
+
`respond_to_interrupt`, `new_thread`, `set_shared_state` / `patch_shared_state`,
|
|
324
|
+
`retry_run`. Calls that are not allowed in the current status are no-ops, so they are
|
|
325
|
+
safe to call from the browser at any time.
|
|
326
|
+
- **Client tools**: `client_tools: ClassVar = (my_tool,)` runs `@client_tool` handlers on
|
|
327
|
+
the server and starts the follow-up run, with optional approval
|
|
328
|
+
(`@client_tool(requires_approval=True)`).
|
|
329
|
+
- **Hooks**: override `create_client` (for example, per-user auth headers),
|
|
330
|
+
`prepare_run_input`, `on_custom_event` or `on_run_end`.
|
|
331
|
+
|
|
332
|
+
## What works today: the PydanticAI adapter
|
|
333
|
+
|
|
334
|
+
Install the extra (`reflex-agentkit[pydantic-ai]`) and wrap the agent. Tools declared with
|
|
335
|
+
`requires_approval=True` pause the run for `approve_tool` / `reject_tool` with no extra
|
|
336
|
+
configuration; edited arguments reach the tool.
|
|
337
|
+
|
|
338
|
+
```python
|
|
339
|
+
from typing import ClassVar
|
|
340
|
+
|
|
341
|
+
import reflex as rx
|
|
342
|
+
from pydantic_ai import Agent
|
|
343
|
+
|
|
344
|
+
from reflex_agentkit import AgentState
|
|
345
|
+
from reflex_agentkit.adapters.pydantic_ai import PydanticAIAgent
|
|
346
|
+
|
|
347
|
+
agent = Agent("anthropic:claude-sonnet-5-5", instructions="You book hotels.")
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
@agent.tool_plain(requires_approval=True)
|
|
351
|
+
def book_hotel(city: str) -> str:
|
|
352
|
+
"""Book a hotel in a city."""
|
|
353
|
+
return f"Booked a hotel in {city}."
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
class ChatState(AgentState, rx.State):
|
|
357
|
+
agent: ClassVar = PydanticAIAgent(agent)
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
## What works today: the LangGraph adapter
|
|
361
|
+
|
|
362
|
+
Install the extra (`reflex-agentkit[langgraph]`) and wrap a compiled graph that has a
|
|
363
|
+
checkpointer. `interrupt(value)` in the graph shows up as `pending_interrupt` and is
|
|
364
|
+
answered with `respond_to_interrupt`. `request_tool_approval` pauses for a tool call and
|
|
365
|
+
shows the approval dialog; it returns the decision, with edited arguments when the user
|
|
366
|
+
changed them. In this sketch, `call_model` stands for your own chat-model node:
|
|
367
|
+
|
|
368
|
+
```python
|
|
369
|
+
from typing import Any, ClassVar
|
|
370
|
+
|
|
371
|
+
import reflex as rx
|
|
372
|
+
from langchain_core.messages import AIMessage, ToolMessage
|
|
373
|
+
from langgraph.checkpoint.memory import InMemorySaver
|
|
374
|
+
from langgraph.graph import END, START, MessagesState, StateGraph
|
|
375
|
+
|
|
376
|
+
from reflex_agentkit import AgentState, agent_chat
|
|
377
|
+
from reflex_agentkit.adapters.langgraph import LangGraphAGUIAgent, request_tool_approval
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def tools(state: MessagesState) -> dict[str, Any]:
|
|
381
|
+
last = state["messages"][-1]
|
|
382
|
+
assert isinstance(last, AIMessage)
|
|
383
|
+
call = last.tool_calls[0]
|
|
384
|
+
decision = request_tool_approval(str(call["id"]), call["name"], call["args"])
|
|
385
|
+
content = f"Booked {decision.args['city']}" if decision.approved else "Not booked"
|
|
386
|
+
return {"messages": [ToolMessage(content=content, tool_call_id=call["id"])]}
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def route_to_tools_or_end(state: MessagesState) -> str:
|
|
390
|
+
return "tools" if getattr(state["messages"][-1], "tool_calls", None) else END
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
builder = StateGraph(MessagesState)
|
|
394
|
+
builder.add_node("model", call_model) # your chat-model node, e.g. ChatAnthropic + bind_tools
|
|
395
|
+
builder.add_node("tools", tools)
|
|
396
|
+
builder.add_edge(START, "model")
|
|
397
|
+
builder.add_conditional_edges("model", route_to_tools_or_end, ["tools", END])
|
|
398
|
+
builder.add_edge("tools", "model")
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
class ChatState(AgentState, rx.State):
|
|
402
|
+
agent: ClassVar = LangGraphAGUIAgent(builder.compile(checkpointer=InMemorySaver()))
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def index() -> rx.Component:
|
|
406
|
+
return agent_chat(ChatState)
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
- One agent instance serves every session; each run works on a fresh copy.
|
|
410
|
+
- Several interrupts at once are answered one at a time in the UI and resumed together,
|
|
411
|
+
each node receiving its own answer.
|
|
412
|
+
- Cancelling while paused resumes the next run with
|
|
413
|
+
`ApprovalDecision(approved=False, reason="cancelled")` for an approval, or the
|
|
414
|
+
`{"__agui_cancelled__": True}` value for a generic interrupt.
|
|
415
|
+
- `legacy_interrupts=True` supports integrations that only send the older `on_interrupt`
|
|
416
|
+
event.
|
|
417
|
+
|
|
418
|
+
## Examples
|
|
419
|
+
|
|
420
|
+
| Example | What it is |
|
|
421
|
+
|---|---|
|
|
422
|
+
| [`examples/quickstart/`](examples/quickstart/) | The quickstart above: a chat with a PydanticAI agent in 23 lines |
|
|
423
|
+
| [`examples/pydantic_ai_dojo/`](examples/pydantic_ai_dojo/) | The five AG-UI Dojo demos with PydanticAI |
|
|
424
|
+
| [`examples/langgraph_dojo/`](examples/langgraph_dojo/) | The same five demos with LangGraph graphs |
|
|
425
|
+
|
|
426
|
+
The Dojo demos are agentic chat, backend tool rendering, human in the loop (approve, edit
|
|
427
|
+
or reject a tool call; answer a question), shared state and tool-based generative UI.
|
|
428
|
+
They use Claude by default; `AGENTKIT_MODEL=ollama:<model>` switches to a local Ollama
|
|
429
|
+
model, and `AGENTKIT_MODEL=test` to an offline model (see
|
|
430
|
+
[`examples/_shared`](examples/_shared/README.md)).
|
|
431
|
+
|
|
432
|
+
## Development
|
|
433
|
+
|
|
434
|
+
The workflow is uv only (Python 3.11–3.13).
|
|
435
|
+
|
|
436
|
+
| Purpose | Command |
|
|
437
|
+
|---|---|
|
|
438
|
+
| Set up the environment | `uv sync --all-extras --all-packages` |
|
|
439
|
+
| Lint and format check | `uv run ruff check .` · `uv run ruff format --check .` |
|
|
440
|
+
| Type check (strict on `core` and `state`) | `uv run mypy` |
|
|
441
|
+
| Tests with coverage | `uv run pytest` |
|
|
442
|
+
| Coverage gate (≥ 85 % on `core` and `state`) | `uv run python scripts/check_gated_coverage.py` |
|
|
443
|
+
| Core isolation check | `uv run python scripts/assert_core_isolated.py` |
|
|
444
|
+
| Quickstart smoke (offline) | `uv run pytest tests/smoke` |
|
|
445
|
+
| Benchmarks (NFR-01, NFR-04) | `uv run pytest -m bench -q` |
|
|
446
|
+
| Docs site | `uv run mkdocs serve` · `uv run mkdocs build --strict` |
|
|
447
|
+
|
|
448
|
+
Releases are automatic: merging a new version into `main` publishes it to PyPI (see
|
|
449
|
+
[`CONTRIBUTING.md`](CONTRIBUTING.md#releases)).
|
|
450
|
+
|
|
451
|
+
Read [`CONTRIBUTING.md`](CONTRIBUTING.md) and [`specs/constitution.md`](specs/constitution.md)
|
|
452
|
+
before contributing. The rules: the specs come first, `core` and `state` are written
|
|
453
|
+
test-first, and changes use Conventional Commits.
|
|
454
|
+
|
|
455
|
+
## License
|
|
456
|
+
|
|
457
|
+
MIT
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
reflex_agentkit/__init__.py,sha256=Ku0cuOQcdeZLQ3jWoyUS6QqRNDg0UuG_emwY_T5OKso,1798
|
|
2
|
+
reflex_agentkit/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
+
reflex_agentkit/adapters/__init__.py,sha256=n9IlDT1P3hc2a9ijspsgJ77rFVdi332WXyhhUTaDkX0,61
|
|
4
|
+
reflex_agentkit/adapters/_require.py,sha256=U6IkO2Z30CUsDW98QKf_POAh9nP6TEldYBpiqodAM4M,915
|
|
5
|
+
reflex_agentkit/adapters/langgraph.py,sha256=YzltOq70njlmN9Nusa03_evtUDvk01C0qh7rPNNlaIc,11015
|
|
6
|
+
reflex_agentkit/adapters/pydantic_ai.py,sha256=NMWTK0p9RdgYvOUlKD3ICd_5Q-L5UzSRXJH204c41b8,3674
|
|
7
|
+
reflex_agentkit/components/__init__.py,sha256=2RGgiX-l_Qq2rvYX8BW1k65DkeEd3EjBRgjZtxBAq4s,758
|
|
8
|
+
reflex_agentkit/components/approval.py,sha256=0JUDnZxUdg2fLLpwZcMndWhwWapcJPdFfBFWcAlX_gc,4317
|
|
9
|
+
reflex_agentkit/components/chat.py,sha256=oHOEPhqj_KR3H0neGr9TTrotRY84wW9_uZmOuZj2lBw,1555
|
|
10
|
+
reflex_agentkit/components/composer.py,sha256=XUG1pKY1iIRFCx2lL2wu0wxWoBGBlFnYO2z1vxB_a20,1247
|
|
11
|
+
reflex_agentkit/components/messages.py,sha256=ym2NvSSaq5CfAl5-hyGxhUnxPwZRUAgg0qO19kcMzdo,3283
|
|
12
|
+
reflex_agentkit/components/shared_state.py,sha256=Lg_OTriMS5AAPwtUBuaXutPTysJK-JAjcA3QfSWWmiY,936
|
|
13
|
+
reflex_agentkit/components/status.py,sha256=hDm54yFfOWTqESFb4cCl0eZl4zED4ZUCa5Vfb_NURGo,1270
|
|
14
|
+
reflex_agentkit/components/tools.py,sha256=4eyvHnA85W4mVN1L5WLJ_jyn8X1k5favrO1RPf5TbSA,3022
|
|
15
|
+
reflex_agentkit/core/__init__.py,sha256=rUu0wnqkMbBeq8r-52EX6tUn8zNPjiJ13_0Umr39aVo,1311
|
|
16
|
+
reflex_agentkit/core/_log.py,sha256=EVHmNKUoX6P27edmTGwQ8zaeoDPxuPM1IbDkzk-L960,1623
|
|
17
|
+
reflex_agentkit/core/_patch.py,sha256=cqwcDCnDKjpX12_qaNKoyEpPiQJLZ-AZIl3sblpkJgc,1521
|
|
18
|
+
reflex_agentkit/core/commands.py,sha256=P2AQqR-qL5MfhTq0p3ZHHBDkONitdHdHcRWnYAuRqfQ,22753
|
|
19
|
+
reflex_agentkit/core/conversation.py,sha256=2m5OZPQe5yX8EKoO9VvAWqT34frWxGtacrqcxbOaQqk,3458
|
|
20
|
+
reflex_agentkit/core/errors.py,sha256=DPEkpDW8SyDdBkcNQtKYsT6QHRy8riBICFs9YBiJJz8,2470
|
|
21
|
+
reflex_agentkit/core/models.py,sha256=NpqlpT3NcgJW0R39s3WWDtVtAZCOjFqwtrzw5F4cJPo,7486
|
|
22
|
+
reflex_agentkit/core/reducer.py,sha256=gVWZt9UhcXqjbbdPn4atAIEj7z5fH9Ao0nWM_mtkZZA,42291
|
|
23
|
+
reflex_agentkit/core/run_input.py,sha256=IZJw6gwEyDNo-bbXgIFN_Pj2Z2usoOPaRN2QypckmqE,4292
|
|
24
|
+
reflex_agentkit/core/tools.py,sha256=1xbbI7sAO1Xnplm2Dkh8NvqKj6D5yphNAEMfSyJhu1I,7527
|
|
25
|
+
reflex_agentkit/core/clients/__init__.py,sha256=Bj_l9IvQg4vqq6QU0-kVw5ueODWOydghf1iiFu3N480,453
|
|
26
|
+
reflex_agentkit/core/clients/base.py,sha256=AHrnuDQiFw9PgKMeKq3X9TcGlc0ge5OyL6UyhYGzOck,2627
|
|
27
|
+
reflex_agentkit/core/clients/http.py,sha256=ORp9CrPlzc9wgt09KVK2W1eIpeLTCnNVu_lVqt1cM5c,5650
|
|
28
|
+
reflex_agentkit/core/clients/in_process.py,sha256=tH_wX3LuaLNz-l5udON0_hEn0ByOFxpb_iLOvwWQ7xs,2648
|
|
29
|
+
reflex_agentkit/core/clients/sse.py,sha256=I5BGu7ujYFsBcSazt9-upvqqixLDVINk5vpBAS7It9I,2871
|
|
30
|
+
reflex_agentkit/state/__init__.py,sha256=XcTzdMxvPZommiOw34wbERSGLv8USIYvVO-SP6lnupk,130
|
|
31
|
+
reflex_agentkit/state/_driver.py,sha256=jAOc28KLU_hz_8vqCpbvmqNt7uK-v7A4PVkiZdvb5oU,22915
|
|
32
|
+
reflex_agentkit/state/_projection.py,sha256=ogbJXRJ8xogCIP7OOFXND-i44EWHlmOFE_YI-iVrx9I,4164
|
|
33
|
+
reflex_agentkit/state/_registry.py,sha256=pdrIoDv_CgV6Su3nWS4Pzq1qh9s-ClCRFqD49GKz7B0,1622
|
|
34
|
+
reflex_agentkit/state/agent_state.py,sha256=acpO64_vtOX4a7CQeOaCwDsmlsRAZ7LovwK5H2rRgKg,20859
|
|
35
|
+
reflex_agentkit-0.1.0.dist-info/METADATA,sha256=4YxjT9LTHgF9gLlwo-kwwci4UIcoGiHJTHRojZHGj_Q,19819
|
|
36
|
+
reflex_agentkit-0.1.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
|
|
37
|
+
reflex_agentkit-0.1.0.dist-info/licenses/LICENSE,sha256=ikDZVzB6LJXytfIXkllDZj9ningVU9BduOAcL8jLKYI,1071
|
|
38
|
+
reflex_agentkit-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ernesto Crespo
|
|
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.
|