calfkit 0.1.1__tar.gz → 0.1.2__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.
- calfkit-0.1.2/.github/CODEOWNERS +1 -0
- calfkit-0.1.2/.release-please-manifest.json +3 -0
- {calfkit-0.1.1 → calfkit-0.1.2}/CHANGELOG.md +7 -0
- calfkit-0.1.2/PKG-INFO +192 -0
- calfkit-0.1.2/README.md +164 -0
- {calfkit-0.1.1 → calfkit-0.1.2}/calfkit/broker/middleware.py +9 -1
- {calfkit-0.1.1 → calfkit-0.1.2}/calfkit/models/types.py +1 -0
- {calfkit-0.1.1 → calfkit-0.1.2}/calfkit/nodes/agent_router_node.py +78 -6
- calfkit-0.1.2/examples/real_broker/chat_node.py +68 -0
- calfkit-0.1.2/examples/real_broker/chat_repl_cli.py +175 -0
- calfkit-0.1.2/examples/real_broker/invoke_agent.py +113 -0
- calfkit-0.1.2/examples/real_broker/router_node.py +64 -0
- calfkit-0.1.2/examples/real_broker/tool_nodes.py +179 -0
- {calfkit-0.1.1 → calfkit-0.1.2}/pyproject.toml +3 -3
- {calfkit-0.1.1 → calfkit-0.1.2}/tests/test_agent_runner.py +11 -1
- calfkit-0.1.2/tests/test_agent_unit.py +283 -0
- {calfkit-0.1.1 → calfkit-0.1.2}/tests/utils.py +0 -3
- calfkit-0.1.1/.release-please-manifest.json +0 -3
- calfkit-0.1.1/CLAUDE.md +0 -1
- calfkit-0.1.1/PKG-INFO +0 -129
- calfkit-0.1.1/README.md +0 -101
- calfkit-0.1.1/tests/test_broker.py +0 -46
- {calfkit-0.1.1 → calfkit-0.1.2}/.github/dependabot.yml +0 -0
- {calfkit-0.1.1 → calfkit-0.1.2}/.github/workflows/build.yml +0 -0
- {calfkit-0.1.1 → calfkit-0.1.2}/.github/workflows/code-checks.yml +0 -0
- {calfkit-0.1.1 → calfkit-0.1.2}/.github/workflows/release.yml +0 -0
- {calfkit-0.1.1 → calfkit-0.1.2}/.github/workflows/security.yml +0 -0
- {calfkit-0.1.1 → calfkit-0.1.2}/.github/workflows/test.yml +0 -0
- {calfkit-0.1.1 → calfkit-0.1.2}/.gitignore +0 -0
- {calfkit-0.1.1 → calfkit-0.1.2}/LICENSE +0 -0
- {calfkit-0.1.1 → calfkit-0.1.2}/Makefile +0 -0
- {calfkit-0.1.1 → calfkit-0.1.2}/calfkit/__init__.py +0 -0
- {calfkit-0.1.1 → calfkit-0.1.2}/calfkit/broker/__init__.py +0 -0
- {calfkit-0.1.1 → calfkit-0.1.2}/calfkit/broker/broker.py +0 -0
- {calfkit-0.1.1 → calfkit-0.1.2}/calfkit/broker/deployable.py +0 -0
- {calfkit-0.1.1 → calfkit-0.1.2}/calfkit/experimental/rpc_worker.py +0 -0
- {calfkit-0.1.1 → calfkit-0.1.2}/calfkit/messages/__init__.py +0 -0
- {calfkit-0.1.1 → calfkit-0.1.2}/calfkit/messages/util.py +0 -0
- {calfkit-0.1.1 → calfkit-0.1.2}/calfkit/models/event_envelope.py +0 -0
- {calfkit-0.1.1 → calfkit-0.1.2}/calfkit/nodes/__init__.py +0 -0
- {calfkit-0.1.1 → calfkit-0.1.2}/calfkit/nodes/base_node.py +0 -0
- {calfkit-0.1.1 → calfkit-0.1.2}/calfkit/nodes/base_tool_node.py +0 -0
- {calfkit-0.1.1 → calfkit-0.1.2}/calfkit/nodes/chat_node.py +0 -0
- {calfkit-0.1.1 → calfkit-0.1.2}/calfkit/nodes/registrator.py +0 -0
- {calfkit-0.1.1 → calfkit-0.1.2}/calfkit/providers/__init__.py +0 -0
- {calfkit-0.1.1 → calfkit-0.1.2}/calfkit/providers/pydantic_ai/__init__.py +0 -0
- {calfkit-0.1.1 → calfkit-0.1.2}/calfkit/providers/pydantic_ai/openai.py +0 -0
- {calfkit-0.1.1 → calfkit-0.1.2}/calfkit/runners/__init__.py +0 -0
- {calfkit-0.1.1 → calfkit-0.1.2}/calfkit/runners/node_runner.py +0 -0
- {calfkit-0.1.1 → calfkit-0.1.2}/calfkit/stores/__init__.py +0 -0
- {calfkit-0.1.1 → calfkit-0.1.2}/calfkit/stores/base.py +0 -0
- {calfkit-0.1.1 → calfkit-0.1.2}/calfkit/stores/in_memory.py +0 -0
- {calfkit-0.1.1 → calfkit-0.1.2}/release-please-config.json +0 -0
- {calfkit-0.1.1 → calfkit-0.1.2}/tests/__init__.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
* @ryan-yuuu
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.1.2](https://github.com/calf-ai/calfkit-sdk/compare/v0.1.1...v0.1.2) (2026-02-04)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* invoke function on unstarted broker + doc: readme quickstart examples ([#30](https://github.com/calf-ai/calfkit-sdk/issues/30)) ([bad4216](https://github.com/calf-ai/calfkit-sdk/commit/bad4216a779fe785f76bc627d4c61a98df800922))
|
|
9
|
+
|
|
3
10
|
## [0.1.1](https://github.com/calf-ai/calf-sdk/compare/v0.1.0...v0.1.1) (2026-02-03)
|
|
4
11
|
|
|
5
12
|
|
calfkit-0.1.2/PKG-INFO
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: calfkit
|
|
3
|
+
Version: 0.1.2
|
|
4
|
+
Summary: Event-driven SDK for building AI workflows on Kafka
|
|
5
|
+
Project-URL: Homepage, https://github.com/calf-ai/calf-sdk
|
|
6
|
+
Project-URL: Repository, https://github.com/calf-ai/calf-sdk
|
|
7
|
+
Project-URL: Issues, https://github.com/calf-ai/calf-sdk/issues
|
|
8
|
+
Author: Ryan Yu
|
|
9
|
+
License-Expression: Apache-2.0
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: agents,ai,decoupled,distributed,event-driven,kafka,llm,workflows
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Requires-Dist: faststream[kafka]>=0.6.6
|
|
22
|
+
Requires-Dist: pydantic-ai>=1.47.0
|
|
23
|
+
Requires-Dist: pydantic>=2.12.5
|
|
24
|
+
Requires-Dist: python-dotenv>=1.2.1
|
|
25
|
+
Requires-Dist: uuid-utils>=0.14.0
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# Calfkit SDK
|
|
30
|
+
|
|
31
|
+
The SDK to build AI agents that scale. Calfkit lets you compose agents from independent services—chat, tools, routing—that communicate through events, not API calls. Add capabilities without coordination. Scale each component independently. Stream agent outputs to any downstream system.
|
|
32
|
+
|
|
33
|
+
## Why Event-Driven Agents?
|
|
34
|
+
|
|
35
|
+
Building agents like traditional web applications—with tight coupling and direct API calls—creates the same scalability problems that plagued early microservices.
|
|
36
|
+
|
|
37
|
+
When agents connect through APIs and RPC:
|
|
38
|
+
- **Tight coupling** — Changing one tool breaks dependent agents
|
|
39
|
+
- **Scaling bottlenecks** — Everything must scale together
|
|
40
|
+
- **Siloed outputs** — Agent responses stay trapped in your AI layer
|
|
41
|
+
|
|
42
|
+
Event-driven architecture provides the solution. Instead of direct API calls between components, agents interact through asynchronous event streams. Each component runs independently, scales horizontally, and outputs can flow anywhere—CRMs, data warehouses, analytics platforms, other agents.
|
|
43
|
+
|
|
44
|
+
## Why use Calfkit?
|
|
45
|
+
|
|
46
|
+
Calfkit is a Python SDK that makes event-driven agents simple. You get the benefits of a distributed system—loose coupling, horizontal scalability, durability—without the complexity of managing Kafka infrastructure yourself.
|
|
47
|
+
|
|
48
|
+
- **Distributed agents out of the box** — Build event-driven, multi-service agents without writing orchestration code or managing infrastructure
|
|
49
|
+
- **Add tools without touching existing code** — Deploy new capabilities as independent services that other agents discover automatically
|
|
50
|
+
- **Scale what you need, when you need it** — Chat handling, tool execution, and routing each scale independently based on demand
|
|
51
|
+
- **Nothing gets lost** — Event persistence ensures reliable message delivery—even during failures or restarts
|
|
52
|
+
- **Real-time responses** — Low-latency event processing enables agents to react instantly to incoming data
|
|
53
|
+
- **Team independence** — Different teams can develop and deploy chat, tools, and routing concurrently without coordination overhead
|
|
54
|
+
- **Universal data flow** — Decoupling enables data to flow freely in both directions. Downstream, agent outputs integrate with any system (CRMs, CDPs, warehouses). Upstream, tools wrap data sources and deploy independently—no coordination needed.
|
|
55
|
+
|
|
56
|
+
## Quick Start
|
|
57
|
+
|
|
58
|
+
### Prerequisites
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
# Kafka (single command via Docker)
|
|
62
|
+
docker run -d -p 9092:9092 apache/kafka:latest
|
|
63
|
+
|
|
64
|
+
# Python 3.10+
|
|
65
|
+
python --version
|
|
66
|
+
|
|
67
|
+
# OpenAI API key
|
|
68
|
+
export OPENAI_API_KEY=sk-...
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Install
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
pip install git+https://github.com/calf-ai/calf-sdk.git
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Deploy the Tool Node
|
|
78
|
+
|
|
79
|
+
Define and deploy a tool as an independent service.
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
import asyncio
|
|
83
|
+
from calfkit.nodes import agent_tool
|
|
84
|
+
from calfkit.broker import Broker
|
|
85
|
+
from calfkit.runners import ToolRunner
|
|
86
|
+
|
|
87
|
+
@agent_tool
|
|
88
|
+
def get_weather(location: str) -> str:
|
|
89
|
+
"""Get the current weather at a location"""
|
|
90
|
+
return f"It's sunny in {location}"
|
|
91
|
+
|
|
92
|
+
async def main():
|
|
93
|
+
broker = Broker(bootstrap_servers="localhost:9092")
|
|
94
|
+
ToolRunner(get_weather).register_on(broker)
|
|
95
|
+
await broker.run_app()
|
|
96
|
+
|
|
97
|
+
asyncio.run(main())
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Deploy the Chat Node
|
|
101
|
+
|
|
102
|
+
Deploy the LLM chat handler as its own service.
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
import asyncio
|
|
106
|
+
from calfkit.nodes import ChatNode
|
|
107
|
+
from calfkit.providers import OpenAIModelClient
|
|
108
|
+
from calfkit.broker import Broker
|
|
109
|
+
from calfkit.runners import ChatRunner
|
|
110
|
+
|
|
111
|
+
async def main():
|
|
112
|
+
broker = Broker(bootstrap_servers="localhost:9092")
|
|
113
|
+
model_client = OpenAIModelClient(model_name="gpt-5-nano")
|
|
114
|
+
chat_node = ChatNode(model_client)
|
|
115
|
+
ChatRunner(chat_node).register_on(broker)
|
|
116
|
+
await broker.run_app()
|
|
117
|
+
|
|
118
|
+
asyncio.run(main())
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Deploy the Agent Router Node
|
|
122
|
+
|
|
123
|
+
Deploy the router that orchestrates chat and tools.
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
import asyncio
|
|
127
|
+
from calfkit.nodes import agent_tool, AgentRouterNode, ChatNode
|
|
128
|
+
from calfkit.providers import OpenAIModelClient
|
|
129
|
+
from calfkit.stores import InMemoryMessageHistoryStore
|
|
130
|
+
from calfkit.broker import Broker
|
|
131
|
+
from calfkit.runners import AgentRouterRunner
|
|
132
|
+
|
|
133
|
+
@agent_tool
|
|
134
|
+
def get_weather(location: str) -> str:
|
|
135
|
+
"""Get the current weather at a location"""
|
|
136
|
+
return f"It's sunny in {location}"
|
|
137
|
+
|
|
138
|
+
async def main():
|
|
139
|
+
broker = Broker(bootstrap_servers="localhost:9092")
|
|
140
|
+
model_client = OpenAIModelClient(model_name="gpt-4o")
|
|
141
|
+
router_node = AgentRouterNode(
|
|
142
|
+
chat_node=ChatNode(model_client),
|
|
143
|
+
tool_nodes=[get_weather],
|
|
144
|
+
system_prompt="You are a helpful assistant",
|
|
145
|
+
message_history_store=InMemoryMessageHistoryStore(),
|
|
146
|
+
)
|
|
147
|
+
AgentRouterRunner(router_node).register_on(broker)
|
|
148
|
+
await broker.run_app()
|
|
149
|
+
|
|
150
|
+
asyncio.run(main())
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### Invoke the Agent
|
|
154
|
+
|
|
155
|
+
Send a request and receive the response.
|
|
156
|
+
|
|
157
|
+
```python
|
|
158
|
+
import asyncio
|
|
159
|
+
from calfkit.nodes import agent_tool, AgentRouterNode, ChatNode
|
|
160
|
+
from calfkit.providers import OpenAIModelClient
|
|
161
|
+
from calfkit.stores import InMemoryMessageHistoryStore
|
|
162
|
+
from calfkit.broker import Broker
|
|
163
|
+
|
|
164
|
+
@agent_tool
|
|
165
|
+
def get_weather(location: str) -> str:
|
|
166
|
+
"""Get the current weather at a location"""
|
|
167
|
+
return f"It's sunny in {location}"
|
|
168
|
+
|
|
169
|
+
async def main():
|
|
170
|
+
broker = Broker(bootstrap_servers="localhost:9092")
|
|
171
|
+
model_client = OpenAIModelClient(model_name="gpt-5-nano")
|
|
172
|
+
|
|
173
|
+
router_node = AgentRouterNode(
|
|
174
|
+
chat_node=ChatNode(model_client),
|
|
175
|
+
tool_nodes=[get_weather],
|
|
176
|
+
system_prompt="You are a helpful assistant",
|
|
177
|
+
message_history_store=InMemoryMessageHistoryStore(),
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
correlation_id = await router_node.invoke(
|
|
181
|
+
user_prompt="What's the weather in Tokyo?",
|
|
182
|
+
broker=broker,
|
|
183
|
+
final_response_topic="final_response",
|
|
184
|
+
)
|
|
185
|
+
print(f"Request started: {correlation_id}")
|
|
186
|
+
|
|
187
|
+
asyncio.run(main())
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## License
|
|
191
|
+
|
|
192
|
+
Apache-2.0
|
calfkit-0.1.2/README.md
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# Calfkit SDK
|
|
2
|
+
|
|
3
|
+
The SDK to build AI agents that scale. Calfkit lets you compose agents from independent services—chat, tools, routing—that communicate through events, not API calls. Add capabilities without coordination. Scale each component independently. Stream agent outputs to any downstream system.
|
|
4
|
+
|
|
5
|
+
## Why Event-Driven Agents?
|
|
6
|
+
|
|
7
|
+
Building agents like traditional web applications—with tight coupling and direct API calls—creates the same scalability problems that plagued early microservices.
|
|
8
|
+
|
|
9
|
+
When agents connect through APIs and RPC:
|
|
10
|
+
- **Tight coupling** — Changing one tool breaks dependent agents
|
|
11
|
+
- **Scaling bottlenecks** — Everything must scale together
|
|
12
|
+
- **Siloed outputs** — Agent responses stay trapped in your AI layer
|
|
13
|
+
|
|
14
|
+
Event-driven architecture provides the solution. Instead of direct API calls between components, agents interact through asynchronous event streams. Each component runs independently, scales horizontally, and outputs can flow anywhere—CRMs, data warehouses, analytics platforms, other agents.
|
|
15
|
+
|
|
16
|
+
## Why use Calfkit?
|
|
17
|
+
|
|
18
|
+
Calfkit is a Python SDK that makes event-driven agents simple. You get the benefits of a distributed system—loose coupling, horizontal scalability, durability—without the complexity of managing Kafka infrastructure yourself.
|
|
19
|
+
|
|
20
|
+
- **Distributed agents out of the box** — Build event-driven, multi-service agents without writing orchestration code or managing infrastructure
|
|
21
|
+
- **Add tools without touching existing code** — Deploy new capabilities as independent services that other agents discover automatically
|
|
22
|
+
- **Scale what you need, when you need it** — Chat handling, tool execution, and routing each scale independently based on demand
|
|
23
|
+
- **Nothing gets lost** — Event persistence ensures reliable message delivery—even during failures or restarts
|
|
24
|
+
- **Real-time responses** — Low-latency event processing enables agents to react instantly to incoming data
|
|
25
|
+
- **Team independence** — Different teams can develop and deploy chat, tools, and routing concurrently without coordination overhead
|
|
26
|
+
- **Universal data flow** — Decoupling enables data to flow freely in both directions. Downstream, agent outputs integrate with any system (CRMs, CDPs, warehouses). Upstream, tools wrap data sources and deploy independently—no coordination needed.
|
|
27
|
+
|
|
28
|
+
## Quick Start
|
|
29
|
+
|
|
30
|
+
### Prerequisites
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
# Kafka (single command via Docker)
|
|
34
|
+
docker run -d -p 9092:9092 apache/kafka:latest
|
|
35
|
+
|
|
36
|
+
# Python 3.10+
|
|
37
|
+
python --version
|
|
38
|
+
|
|
39
|
+
# OpenAI API key
|
|
40
|
+
export OPENAI_API_KEY=sk-...
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Install
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pip install git+https://github.com/calf-ai/calf-sdk.git
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Deploy the Tool Node
|
|
50
|
+
|
|
51
|
+
Define and deploy a tool as an independent service.
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
import asyncio
|
|
55
|
+
from calfkit.nodes import agent_tool
|
|
56
|
+
from calfkit.broker import Broker
|
|
57
|
+
from calfkit.runners import ToolRunner
|
|
58
|
+
|
|
59
|
+
@agent_tool
|
|
60
|
+
def get_weather(location: str) -> str:
|
|
61
|
+
"""Get the current weather at a location"""
|
|
62
|
+
return f"It's sunny in {location}"
|
|
63
|
+
|
|
64
|
+
async def main():
|
|
65
|
+
broker = Broker(bootstrap_servers="localhost:9092")
|
|
66
|
+
ToolRunner(get_weather).register_on(broker)
|
|
67
|
+
await broker.run_app()
|
|
68
|
+
|
|
69
|
+
asyncio.run(main())
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Deploy the Chat Node
|
|
73
|
+
|
|
74
|
+
Deploy the LLM chat handler as its own service.
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
import asyncio
|
|
78
|
+
from calfkit.nodes import ChatNode
|
|
79
|
+
from calfkit.providers import OpenAIModelClient
|
|
80
|
+
from calfkit.broker import Broker
|
|
81
|
+
from calfkit.runners import ChatRunner
|
|
82
|
+
|
|
83
|
+
async def main():
|
|
84
|
+
broker = Broker(bootstrap_servers="localhost:9092")
|
|
85
|
+
model_client = OpenAIModelClient(model_name="gpt-5-nano")
|
|
86
|
+
chat_node = ChatNode(model_client)
|
|
87
|
+
ChatRunner(chat_node).register_on(broker)
|
|
88
|
+
await broker.run_app()
|
|
89
|
+
|
|
90
|
+
asyncio.run(main())
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Deploy the Agent Router Node
|
|
94
|
+
|
|
95
|
+
Deploy the router that orchestrates chat and tools.
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
import asyncio
|
|
99
|
+
from calfkit.nodes import agent_tool, AgentRouterNode, ChatNode
|
|
100
|
+
from calfkit.providers import OpenAIModelClient
|
|
101
|
+
from calfkit.stores import InMemoryMessageHistoryStore
|
|
102
|
+
from calfkit.broker import Broker
|
|
103
|
+
from calfkit.runners import AgentRouterRunner
|
|
104
|
+
|
|
105
|
+
@agent_tool
|
|
106
|
+
def get_weather(location: str) -> str:
|
|
107
|
+
"""Get the current weather at a location"""
|
|
108
|
+
return f"It's sunny in {location}"
|
|
109
|
+
|
|
110
|
+
async def main():
|
|
111
|
+
broker = Broker(bootstrap_servers="localhost:9092")
|
|
112
|
+
model_client = OpenAIModelClient(model_name="gpt-4o")
|
|
113
|
+
router_node = AgentRouterNode(
|
|
114
|
+
chat_node=ChatNode(model_client),
|
|
115
|
+
tool_nodes=[get_weather],
|
|
116
|
+
system_prompt="You are a helpful assistant",
|
|
117
|
+
message_history_store=InMemoryMessageHistoryStore(),
|
|
118
|
+
)
|
|
119
|
+
AgentRouterRunner(router_node).register_on(broker)
|
|
120
|
+
await broker.run_app()
|
|
121
|
+
|
|
122
|
+
asyncio.run(main())
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Invoke the Agent
|
|
126
|
+
|
|
127
|
+
Send a request and receive the response.
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
import asyncio
|
|
131
|
+
from calfkit.nodes import agent_tool, AgentRouterNode, ChatNode
|
|
132
|
+
from calfkit.providers import OpenAIModelClient
|
|
133
|
+
from calfkit.stores import InMemoryMessageHistoryStore
|
|
134
|
+
from calfkit.broker import Broker
|
|
135
|
+
|
|
136
|
+
@agent_tool
|
|
137
|
+
def get_weather(location: str) -> str:
|
|
138
|
+
"""Get the current weather at a location"""
|
|
139
|
+
return f"It's sunny in {location}"
|
|
140
|
+
|
|
141
|
+
async def main():
|
|
142
|
+
broker = Broker(bootstrap_servers="localhost:9092")
|
|
143
|
+
model_client = OpenAIModelClient(model_name="gpt-5-nano")
|
|
144
|
+
|
|
145
|
+
router_node = AgentRouterNode(
|
|
146
|
+
chat_node=ChatNode(model_client),
|
|
147
|
+
tool_nodes=[get_weather],
|
|
148
|
+
system_prompt="You are a helpful assistant",
|
|
149
|
+
message_history_store=InMemoryMessageHistoryStore(),
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
correlation_id = await router_node.invoke(
|
|
153
|
+
user_prompt="What's the weather in Tokyo?",
|
|
154
|
+
broker=broker,
|
|
155
|
+
final_response_topic="final_response",
|
|
156
|
+
)
|
|
157
|
+
print(f"Request started: {correlation_id}")
|
|
158
|
+
|
|
159
|
+
asyncio.run(main())
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## License
|
|
163
|
+
|
|
164
|
+
Apache-2.0
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
from collections.abc import Awaitable, Callable
|
|
1
2
|
from typing import Any
|
|
2
3
|
|
|
3
|
-
from faststream import BaseMiddleware
|
|
4
|
+
from faststream import BaseMiddleware, PublishCommand
|
|
4
5
|
from faststream.message import StreamMessage
|
|
5
6
|
from faststream.types import AsyncFuncAny
|
|
6
7
|
|
|
@@ -13,3 +14,10 @@ class ContextInjectionMiddleware(BaseMiddleware):
|
|
|
13
14
|
) -> Any:
|
|
14
15
|
with self.context.scope("correlation_id", msg.correlation_id):
|
|
15
16
|
return await super().consume_scope(call_next, msg)
|
|
17
|
+
|
|
18
|
+
async def publish_scope(
|
|
19
|
+
self,
|
|
20
|
+
call_next: Callable[[PublishCommand], Awaitable[Any]],
|
|
21
|
+
cmd: PublishCommand,
|
|
22
|
+
) -> Any:
|
|
23
|
+
return await call_next(cmd)
|
|
@@ -57,6 +57,7 @@ class CompactBaseModel(BaseModel):
|
|
|
57
57
|
def model_dump(self, **kwargs: Any) -> dict[str, Any]:
|
|
58
58
|
kwargs.setdefault("exclude_unset", True)
|
|
59
59
|
kwargs.setdefault("exclude_none", True)
|
|
60
|
+
kwargs.setdefault("mode", "json") # Converts datetime, etc. to JSON-serializable types
|
|
60
61
|
return super().model_dump(**kwargs)
|
|
61
62
|
|
|
62
63
|
def model_dump_json(self, **kwargs: Any) -> str:
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from typing import Annotated, Any
|
|
1
|
+
from typing import Annotated, Any, overload
|
|
2
2
|
|
|
3
3
|
import uuid_utils
|
|
4
4
|
from faststream import Context
|
|
@@ -23,16 +23,84 @@ class AgentRouterNode(BaseNode):
|
|
|
23
23
|
_router_sub_topic_name = "agent_router.input"
|
|
24
24
|
_router_pub_topic_name = "agent_router.output"
|
|
25
25
|
|
|
26
|
+
@overload
|
|
26
27
|
def __init__(
|
|
27
28
|
self,
|
|
28
29
|
chat_node: BaseNode,
|
|
30
|
+
*,
|
|
31
|
+
system_prompt: str,
|
|
29
32
|
tool_nodes: list[BaseToolNode],
|
|
33
|
+
handoff_nodes: list[type[BaseNode]] = [],
|
|
34
|
+
message_history_store: MessageHistoryStore,
|
|
35
|
+
**kwargs: Any,
|
|
36
|
+
): ...
|
|
37
|
+
|
|
38
|
+
@overload
|
|
39
|
+
def __init__(
|
|
40
|
+
self,
|
|
41
|
+
chat_node: BaseNode,
|
|
42
|
+
*,
|
|
30
43
|
system_prompt: str | None = None,
|
|
44
|
+
tool_nodes: list[BaseToolNode] = [],
|
|
45
|
+
handoff_nodes: list[type[BaseNode]] = [],
|
|
46
|
+
message_history_store: MessageHistoryStore | None = None,
|
|
47
|
+
**kwargs: Any,
|
|
48
|
+
): ...
|
|
49
|
+
|
|
50
|
+
@overload
|
|
51
|
+
def __init__(self) -> None: ...
|
|
52
|
+
|
|
53
|
+
@overload
|
|
54
|
+
def __init__(
|
|
55
|
+
self,
|
|
56
|
+
*,
|
|
57
|
+
system_prompt: str | None = None,
|
|
58
|
+
tool_nodes: list[BaseToolNode] = [],
|
|
59
|
+
handoff_nodes: list[type[BaseNode]] = [],
|
|
60
|
+
): ...
|
|
61
|
+
|
|
62
|
+
def __init__(
|
|
63
|
+
self,
|
|
64
|
+
chat_node: BaseNode | None = None,
|
|
65
|
+
*,
|
|
66
|
+
system_prompt: str | None = None,
|
|
67
|
+
tool_nodes: list[BaseToolNode] = [],
|
|
31
68
|
handoff_nodes: list[type[BaseNode]] = [],
|
|
32
69
|
message_history_store: MessageHistoryStore | None = None,
|
|
33
|
-
*args: Any,
|
|
34
70
|
**kwargs: Any,
|
|
35
71
|
):
|
|
72
|
+
"""Initialize an AgentRouterNode.
|
|
73
|
+
|
|
74
|
+
The AgentRouterNode supports multiple initialization patterns depending on use case:
|
|
75
|
+
|
|
76
|
+
1. **Deployable Router Service** (with required parameters):
|
|
77
|
+
Use when deploying the router as a service with all dependencies explicitly provided.
|
|
78
|
+
Requires: chat_node, system_prompt (str), tool_nodes, message_history_store
|
|
79
|
+
|
|
80
|
+
2. **Deployable Router Service** (with optional parameters):
|
|
81
|
+
Use when deploying with optional or runtime-configurable dependencies.
|
|
82
|
+
Requires: chat_node
|
|
83
|
+
Optional: system_prompt, tool_nodes, handoff_nodes, message_history_store
|
|
84
|
+
|
|
85
|
+
3. **Minimal Client**:
|
|
86
|
+
Use when creating a client to invoke an already-deployed router service.
|
|
87
|
+
No parameters needed - connects to the deployed service via the broker.
|
|
88
|
+
|
|
89
|
+
4. **Client with Runtime Patches**:
|
|
90
|
+
Use when creating a client that provides its own tools/system prompt at runtime,
|
|
91
|
+
overriding or supplementing what the deployed router service provides.
|
|
92
|
+
Optional: system_prompt, tool_nodes, handoff_nodes
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
chat_node: The chat node for LLM interactions. Required for deployable services.
|
|
96
|
+
system_prompt: Optional system prompt to override the default. Must be str for
|
|
97
|
+
deployable service, optional for client with runtime patches.
|
|
98
|
+
tool_nodes: List of tool nodes that the agent can call. Optional for all forms.
|
|
99
|
+
handoff_nodes: List of node types for agent handoff scenarios. Optional.
|
|
100
|
+
message_history_store: Store for persisting conversation history across requests.
|
|
101
|
+
Required for deployable service, optional otherwise.
|
|
102
|
+
**kwargs: Additional keyword arguments passed to BaseNode.
|
|
103
|
+
"""
|
|
36
104
|
self.chat = chat_node
|
|
37
105
|
self.tools = tool_nodes
|
|
38
106
|
self.handoffs = handoff_nodes
|
|
@@ -52,7 +120,7 @@ class AgentRouterNode(BaseNode):
|
|
|
52
120
|
|
|
53
121
|
self.tool_response_topics = [tool.publish_to_topic for tool in self.tools]
|
|
54
122
|
|
|
55
|
-
super().__init__(
|
|
123
|
+
super().__init__(**kwargs)
|
|
56
124
|
|
|
57
125
|
@subscribe_to(_router_sub_topic_name)
|
|
58
126
|
@publish_to(_router_pub_topic_name)
|
|
@@ -148,7 +216,7 @@ class AgentRouterNode(BaseNode):
|
|
|
148
216
|
)
|
|
149
217
|
await broker.publish(
|
|
150
218
|
event_envelope,
|
|
151
|
-
topic=self.chat.subscribed_topic,
|
|
219
|
+
topic=self.chat.subscribed_topic, # type: ignore
|
|
152
220
|
correlation_id=correlation_id,
|
|
153
221
|
reply_to=self.subscribed_topic,
|
|
154
222
|
)
|
|
@@ -172,12 +240,16 @@ class AgentRouterNode(BaseNode):
|
|
|
172
240
|
Returns:
|
|
173
241
|
str: The correlation ID for this request
|
|
174
242
|
"""
|
|
175
|
-
|
|
176
|
-
|
|
243
|
+
|
|
244
|
+
patch_model_request_params = (
|
|
245
|
+
ModelRequestParameters(function_tools=[tool.tool_schema() for tool in self.tools])
|
|
246
|
+
if self.tools
|
|
247
|
+
else None
|
|
177
248
|
)
|
|
178
249
|
if correlation_id is None:
|
|
179
250
|
correlation_id = uuid_utils.uuid7().hex
|
|
180
251
|
new_node_messages = [ModelRequest.user_text_prompt(user_prompt)]
|
|
252
|
+
await broker.start()
|
|
181
253
|
await broker.publish(
|
|
182
254
|
EventEnvelope(
|
|
183
255
|
kind="user_prompt",
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
from dotenv import load_dotenv
|
|
6
|
+
|
|
7
|
+
from calfkit.broker.broker import Broker
|
|
8
|
+
from calfkit.nodes.chat_node import ChatNode
|
|
9
|
+
from calfkit.providers.pydantic_ai.openai import OpenAIModelClient
|
|
10
|
+
from calfkit.runners.node_runner import ChatRunner
|
|
11
|
+
|
|
12
|
+
load_dotenv()
|
|
13
|
+
|
|
14
|
+
# Check for API key
|
|
15
|
+
if not os.getenv("OPENAI_API_KEY"):
|
|
16
|
+
print("ERROR: OPENAI_API_KEY environment variable is not set")
|
|
17
|
+
print("Please set it before running:")
|
|
18
|
+
print(" export OPENAI_API_KEY='your-api-key'")
|
|
19
|
+
sys.exit(1)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# Chat Node - Deploys the LLM chat worker.
|
|
23
|
+
|
|
24
|
+
# This runs independently and handles all LLM inference requests.
|
|
25
|
+
|
|
26
|
+
# Usage:
|
|
27
|
+
# uv run python examples/real_broker/chat_node.py
|
|
28
|
+
|
|
29
|
+
# Prerequisites:
|
|
30
|
+
# - Kafka broker running at localhost:9092
|
|
31
|
+
# - OPENAI_API_KEY environment variable set
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
async def main():
|
|
35
|
+
print("=" * 50)
|
|
36
|
+
print("Chat Node Deployment")
|
|
37
|
+
print("=" * 50)
|
|
38
|
+
|
|
39
|
+
# Connect to the real Kafka broker
|
|
40
|
+
print("\nConnecting to Kafka broker at localhost:9092...")
|
|
41
|
+
broker = Broker(bootstrap_servers="localhost:9092")
|
|
42
|
+
|
|
43
|
+
# Configure the LLM model
|
|
44
|
+
print("Configuring OpenAI model client...")
|
|
45
|
+
model_client = OpenAIModelClient(
|
|
46
|
+
model_name="gpt-5-nano",
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
# Deploy the chat node
|
|
50
|
+
print("Registering chat node...")
|
|
51
|
+
chat_node = ChatNode(model_client)
|
|
52
|
+
chat_runner = ChatRunner(node=chat_node)
|
|
53
|
+
chat_runner.register_on(broker)
|
|
54
|
+
print(" - ChatNode registered")
|
|
55
|
+
print(f" Subscribe topic: {chat_node.subscribed_topic}")
|
|
56
|
+
print(f" Publish topic: {chat_node.publish_to_topic}")
|
|
57
|
+
|
|
58
|
+
print("\nChat node ready. Waiting for requests...")
|
|
59
|
+
|
|
60
|
+
# Run the broker app (this blocks)
|
|
61
|
+
await broker.run_app()
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
if __name__ == "__main__":
|
|
65
|
+
try:
|
|
66
|
+
asyncio.run(main())
|
|
67
|
+
except KeyboardInterrupt:
|
|
68
|
+
print("\nChat node stopped.")
|