calfkit 0.1.1__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.
@@ -0,0 +1,48 @@
1
+ from abc import ABC
2
+ from typing import Any, cast
3
+
4
+ from pydantic_ai import ModelResponse, ModelSettings
5
+ from pydantic_ai.direct import model_request
6
+ from pydantic_ai.models import Model, ModelRequestParameters
7
+
8
+ from calfkit.models.event_envelope import EventEnvelope
9
+ from calfkit.nodes.base_node import BaseNode, publish_to, subscribe_to
10
+
11
+
12
+ class ChatNode(BaseNode, ABC):
13
+ """Node defining the llm chat node internal wiring.
14
+ Separate from any logic for LLM persona or behaviour."""
15
+
16
+ _on_enter_topic_name = "ai_prompted"
17
+ _post_to_topic_name = "ai_generated"
18
+
19
+ def __init__(
20
+ self,
21
+ model_client: Model | None = None,
22
+ *,
23
+ request_parameters: ModelRequestParameters | None = None,
24
+ **kwargs: Any,
25
+ ):
26
+ self.model_client = model_client
27
+ self.request_parameters = request_parameters
28
+ super().__init__(**kwargs)
29
+
30
+ @subscribe_to(_on_enter_topic_name)
31
+ @publish_to(_post_to_topic_name)
32
+ async def _call_llm(self, event_envelope: EventEnvelope) -> EventEnvelope:
33
+ if self.model_client is None:
34
+ raise RuntimeError("Unable to handle incoming request because Model client is None.")
35
+ if event_envelope.latest_message_in_history is None:
36
+ raise RuntimeError("latest message must not be None")
37
+ request_parameters = event_envelope.patch_model_request_params or self.request_parameters
38
+ patch_model_settings = event_envelope.patch_model_settings
39
+ model_response: ModelResponse = await model_request(
40
+ model=self.model_client,
41
+ messages=event_envelope.message_history,
42
+ model_settings=cast(ModelSettings | None, patch_model_settings),
43
+ model_request_parameters=request_parameters,
44
+ )
45
+ return_envelope = event_envelope.model_copy(
46
+ update={"kind": "ai_response", "incoming_node_messages": [model_response]}
47
+ )
48
+ return return_envelope
@@ -0,0 +1,11 @@
1
+ from abc import ABC, abstractmethod
2
+ from typing import Any
3
+
4
+ from calfkit.broker.broker import Broker
5
+
6
+
7
+ class Registrator(ABC):
8
+ @abstractmethod
9
+ def register_on(self, broker: Broker, *args: Any, **kwargs: Any) -> None:
10
+ """Function to fluently register a node's handlers onto a broker to serve traffic"""
11
+ pass
@@ -0,0 +1,5 @@
1
+ """Calf LLM Provider System."""
2
+
3
+ from calfkit.providers.pydantic_ai import OpenAIModelClient
4
+
5
+ __all__ = ["OpenAIModelClient"]
@@ -0,0 +1,3 @@
1
+ from calfkit.providers.pydantic_ai.openai import OpenAIModelClient
2
+
3
+ __all__ = ["OpenAIModelClient"]
@@ -0,0 +1,61 @@
1
+ from typing import Any
2
+
3
+ from httpx import Timeout
4
+ from pydantic_ai.models.openai import OpenAIChatModel, OpenAIChatModelSettings
5
+ from pydantic_ai.providers.openai import OpenAIProvider
6
+
7
+
8
+ class OpenAIModelClient(OpenAIChatModel):
9
+ def __init__(
10
+ self,
11
+ model_name: str,
12
+ *,
13
+ base_url: str | None = None,
14
+ api_key: str | None = None,
15
+ reasoning_effort: str | None = None,
16
+ max_tokens: int | None = None,
17
+ temperature: float | None = None,
18
+ top_p: float | None = None,
19
+ timeout: float | Timeout | None = None,
20
+ parallel_tool_calls: bool | None = None,
21
+ seed: int | None = None,
22
+ presence_penalty: float | None = None,
23
+ frequency_penalty: float | None = None,
24
+ logit_bias: dict[str, int] | None = None,
25
+ stop_sequences: list[str] | None = None,
26
+ extra_headers: dict[str, str] | None = None,
27
+ extra_body: object | None = None,
28
+ **kwargs: Any,
29
+ ):
30
+ settings_kwargs: dict[str, object] = {}
31
+ if reasoning_effort is not None:
32
+ settings_kwargs["openai_reasoning_effort"] = reasoning_effort
33
+ if max_tokens is not None:
34
+ settings_kwargs["max_tokens"] = max_tokens
35
+ if temperature is not None:
36
+ settings_kwargs["temperature"] = temperature
37
+ if top_p is not None:
38
+ settings_kwargs["top_p"] = top_p
39
+ if timeout is not None:
40
+ settings_kwargs["timeout"] = timeout
41
+ if parallel_tool_calls is not None:
42
+ settings_kwargs["parallel_tool_calls"] = parallel_tool_calls
43
+ if seed is not None:
44
+ settings_kwargs["seed"] = seed
45
+ if presence_penalty is not None:
46
+ settings_kwargs["presence_penalty"] = presence_penalty
47
+ if frequency_penalty is not None:
48
+ settings_kwargs["frequency_penalty"] = frequency_penalty
49
+ if logit_bias is not None:
50
+ settings_kwargs["logit_bias"] = logit_bias
51
+ if stop_sequences is not None:
52
+ settings_kwargs["stop_sequences"] = stop_sequences
53
+ if extra_headers is not None:
54
+ settings_kwargs["extra_headers"] = extra_headers
55
+ if extra_body is not None:
56
+ settings_kwargs["extra_body"] = extra_body
57
+ model_settings: OpenAIChatModelSettings = OpenAIChatModelSettings(**settings_kwargs) # type: ignore[typeddict-item]
58
+
59
+ openai_client = OpenAIProvider(base_url=base_url, api_key=api_key)
60
+ self.model_settings = model_settings
61
+ super().__init__(model_name, provider=openai_client, settings=model_settings)
@@ -0,0 +1,14 @@
1
+ """Calf Agent System.
2
+
3
+ This module provides runners for deploying agent nodes to a message broker.
4
+ Runners handle the registration and lifecycle of agent nodes within the broker system.
5
+ """
6
+
7
+ from calfkit.runners.node_runner import AgentRouterRunner, ChatRunner, NodeRunner, ToolRunner
8
+
9
+ __all__ = [
10
+ "NodeRunner",
11
+ "ChatRunner",
12
+ "ToolRunner",
13
+ "AgentRouterRunner",
14
+ ]
@@ -0,0 +1,38 @@
1
+ from typing import Any, TypeAlias
2
+
3
+ from calfkit.broker.broker import Broker
4
+ from calfkit.nodes.base_node import BaseNode
5
+ from calfkit.nodes.registrator import Registrator
6
+
7
+
8
+ class NodeRunner(Registrator):
9
+ """The NodeRunner makes node logic deployable after registering on a broker. (Control plane)"""
10
+
11
+ def __init__(self, node: BaseNode, *args: Any, **kwargs: Any):
12
+ self.node = node
13
+ super().__init__(*args, **kwargs)
14
+
15
+ def register_on(
16
+ self,
17
+ broker: Broker,
18
+ *,
19
+ max_workers: int | None = None,
20
+ # group_id explicitly set as to avoid duplicated processing for separate deployments
21
+ group_id: str = "default",
22
+ extra_publish_kwargs: dict[str, Any] = {},
23
+ extra_subscribe_kwargs: dict[str, Any] = {},
24
+ ) -> None:
25
+ for handler_fn, topics_dict in self.node.bound_registry.items():
26
+ pub = topics_dict.get("publish_topic")
27
+ sub = topics_dict.get("subscribe_topic")
28
+ if sub is not None:
29
+ handler_fn = broker.subscriber(
30
+ sub, max_workers=max_workers, group_id=group_id, **extra_subscribe_kwargs
31
+ )(handler_fn)
32
+ if pub is not None:
33
+ handler_fn = broker.publisher(pub, **extra_publish_kwargs)(handler_fn)
34
+
35
+
36
+ ChatRunner: TypeAlias = NodeRunner
37
+ ToolRunner: TypeAlias = NodeRunner
38
+ AgentRouterRunner: TypeAlias = NodeRunner
@@ -0,0 +1,38 @@
1
+ """Calf Message History Store System.
2
+
3
+ The message history store provides persistent storage for conversation messages
4
+ in event-driven agent systems. It follows these principles:
5
+
6
+ * Thread-based: Conversations are identified by a thread_id
7
+ * Strong consistency: Messages are persisted atomically as they flow through the system
8
+ * Pluggable backends: Swap between in-memory, PostgreSQL, Redis, etc.
9
+ * Auto-create: New threads are created automatically on first append
10
+
11
+ Example:
12
+ from calfkit.stores import MemoryMessageHistoryStore
13
+
14
+ # Deployment-time configuration
15
+ store = MemoryMessageHistoryStore()
16
+
17
+ # Inject into router node
18
+ router_node = AgentRouterNode(
19
+ chat_node=chat_node,
20
+ tool_nodes=[get_weather],
21
+ message_store=store,
22
+ )
23
+
24
+ # Invocation-time - specify which conversation
25
+ await router_node.invoke(
26
+ user_prompt="What's the weather?",
27
+ broker=broker,
28
+ thread_id="user-123-conv-456",
29
+ )
30
+ """
31
+
32
+ from calfkit.stores.base import MessageHistoryStore
33
+ from calfkit.stores.in_memory import InMemoryMessageHistoryStore
34
+
35
+ __all__ = [
36
+ "MessageHistoryStore",
37
+ "InMemoryMessageHistoryStore",
38
+ ]
calfkit/stores/base.py ADDED
@@ -0,0 +1,60 @@
1
+ from abc import ABC, abstractmethod
2
+ from collections.abc import Sequence
3
+
4
+ from pydantic_ai.messages import ModelMessage
5
+
6
+
7
+ class MessageHistoryStore(ABC):
8
+ """Abstract store for conversation message history.
9
+
10
+ Designed for strong consistency in event-driven agent systems.
11
+ Messages are persisted atomically as they flow through the agent graph.
12
+ """
13
+
14
+ @abstractmethod
15
+ async def get(self, thread_id: str) -> list[ModelMessage]:
16
+ """Load message history for a thread.
17
+
18
+ Args:
19
+ thread_id: Unique identifier for the conversation thread.
20
+
21
+ Returns:
22
+ List of messages in the thread. Returns empty list if thread
23
+ doesn't exist (auto-create behavior).
24
+ """
25
+ ...
26
+
27
+ @abstractmethod
28
+ async def append(self, thread_id: str, message: ModelMessage) -> None:
29
+ """Append a single message to history.
30
+
31
+ This is the primary method for strong consistency - each message
32
+ is persisted immediately before the next step in the agentic loop.
33
+
34
+ Args:
35
+ thread_id: Unique identifier for the conversation thread.
36
+ message: The message to append.
37
+ """
38
+ ...
39
+
40
+ async def append_many(self, thread_id: str, messages: Sequence[ModelMessage]) -> None:
41
+ """Append multiple messages to history.
42
+
43
+ Default implementation calls append() for each message.
44
+ Override for batch optimization if needed.
45
+
46
+ Args:
47
+ thread_id: Unique identifier for the conversation thread.
48
+ messages: List of messages to append.
49
+ """
50
+ for message in messages:
51
+ await self.append(thread_id, message)
52
+
53
+ @abstractmethod
54
+ async def delete(self, thread_id: str) -> None:
55
+ """Delete all messages for a thread.
56
+
57
+ Args:
58
+ thread_id: Unique identifier for the conversation thread.
59
+ """
60
+ ...
@@ -0,0 +1,29 @@
1
+ from collections import defaultdict
2
+
3
+ from pydantic_ai.messages import ModelMessage
4
+
5
+ from calfkit.stores.base import MessageHistoryStore
6
+
7
+
8
+ class InMemoryMessageHistoryStore(MessageHistoryStore):
9
+ """In-memory message history store.
10
+
11
+ Useful for testing and development. Not suitable for production
12
+ as data is lost when the process exits.
13
+ """
14
+
15
+ def __init__(self) -> None:
16
+ """Initialize an empty in-memory store."""
17
+ self._messages: dict[str, list[ModelMessage]] = defaultdict(list)
18
+
19
+ async def get(self, thread_id: str) -> list[ModelMessage]:
20
+ """Load message history for a thread."""
21
+ return list(self._messages.get(thread_id, []))
22
+
23
+ async def append(self, thread_id: str, message: ModelMessage) -> None:
24
+ """Append a single message to history."""
25
+ self._messages[thread_id].append(message)
26
+
27
+ async def delete(self, thread_id: str) -> None:
28
+ """Delete all messages for a thread."""
29
+ self._messages.pop(thread_id, None)
@@ -0,0 +1,129 @@
1
+ Metadata-Version: 2.4
2
+ Name: calfkit
3
+ Version: 0.1.1
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,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[cli]>=0.6.0
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
+ # Calf SDK
30
+
31
+ Event-driven AI agents that scale like microservices. Build loosely-coupled, distributed agent systems where tools, chats, and workflows each run as independent services—communicating through events.
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
+ ## What Calf Gives You
45
+
46
+ Calf 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
+ | Benefit | What It Means for You |
49
+ |---------|----------------------|
50
+ | **Add tools without touching existing code** | Deploy new capabilities as independent services that other agents discover automatically |
51
+ | **Scale what you need, when you need it** | Chat handling, tool execution, and routing each scale independently based on demand |
52
+ | **Nothing gets lost** | Event persistence ensures reliable message delivery—even during failures or restarts |
53
+ | **Real-time responses** | Low-latency event processing enables agents to react instantly to incoming data |
54
+ | **Team independence** | Different teams can develop and deploy chat, tools, and routing concurrently without coordination overhead |
55
+ | **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. |
56
+
57
+ ## Quick Start
58
+
59
+ ### Prerequisites
60
+
61
+ ```bash
62
+ # Kafka (single command via Docker)
63
+ docker run -d -p 9092:9092 apache/kafka:latest
64
+
65
+ # Python 3.10+
66
+ python --version
67
+
68
+ # OpenAI API key
69
+ export OPENAI_API_KEY=sk-...
70
+ ```
71
+
72
+ ### Install
73
+
74
+ ```bash
75
+ pip install git+https://github.com/calf-ai/calf-sdk.git
76
+ ```
77
+
78
+ ### Create and Run an Agent
79
+
80
+ ```python
81
+ import asyncio
82
+ from calfkit.nodes import agent_tool, AgentRouterNode, ChatNode
83
+ from calfkit.providers import OpenAIModelClient
84
+ from calfkit.stores import InMemoryMessageHistoryStore
85
+ from calfkit.broker import Broker
86
+ from calfkit.runners import ChatRunner, ToolRunner, AgentRouterRunner
87
+
88
+ # 1. Define a tool
89
+ @agent_tool
90
+ def get_weather(location: str) -> str:
91
+ """Get the current weather at a location"""
92
+ return f"It's sunny in {location}"
93
+
94
+ # 2. Setup broker and nodes
95
+ async def main():
96
+ broker = Broker(bootstrap_servers="localhost:9092")
97
+ model_client = OpenAIModelClient(model_name="gpt-4o")
98
+
99
+ # Deploy chat node
100
+ chat_node = ChatNode(model_client)
101
+ ChatRunner(chat_node).register_on(broker)
102
+
103
+ # Deploy tool node
104
+ ToolRunner(get_weather).register_on(broker)
105
+
106
+ # Deploy router node
107
+ router_node = AgentRouterNode(
108
+ chat_node=ChatNode(model_client),
109
+ tool_nodes=[get_weather],
110
+ system_prompt="You are a helpful assistant",
111
+ message_history_store=InMemoryMessageHistoryStore(),
112
+ )
113
+ AgentRouterRunner(router_node).register_on(broker)
114
+
115
+ # Run broker and invoke
116
+ await broker.run_app()
117
+ correlation_id = await router_node.invoke(
118
+ user_prompt="What's the weather in Tokyo?",
119
+ broker=broker,
120
+ final_response_topic="final_response",
121
+ )
122
+ print(f"Request started: {correlation_id}")
123
+
124
+ asyncio.run(main())
125
+ ```
126
+
127
+ ## License
128
+
129
+ Apache-2.0
@@ -0,0 +1,28 @@
1
+ calfkit/__init__.py,sha256=TNjZ-vfuYWSFldFQniKQvb0XRB55YzNqgLHyAKQcYrg,99
2
+ calfkit/broker/__init__.py,sha256=35HCFxzWmmh0hhBf7bf7Ix7XPUJaS-GQjAKwDpZ-EnI,63
3
+ calfkit/broker/broker.py,sha256=9s8RbkxItA5NcuQX6U7lXqaL2WYrQ8JIZAusD_YARxs,996
4
+ calfkit/broker/deployable.py,sha256=mzvYllCbh4m3lMCdbMsVlOyQ2jTHlx92RYjkHgry1zQ,122
5
+ calfkit/broker/middleware.py,sha256=X9Ta4gLYkivOTTNjLzDXNIJSwLPUOlojvz2SmxgIHco,458
6
+ calfkit/experimental/rpc_worker.py,sha256=_FeUmcS4aFY4IRaz-qrMelFXCGNKdEySE0BSWTOzudE,1439
7
+ calfkit/messages/__init__.py,sha256=4Wv45OrQMI0opPK70D1IyysbjxwdxJP4y56PkkEEtq0,309
8
+ calfkit/messages/util.py,sha256=EamqjS74LlHRBzOgER5o2VWwQFkcxt9Kwtd6QE4pqNw,3295
9
+ calfkit/models/event_envelope.py,sha256=zd10sQ51LgqxroZsxrm3kNkB5hWRtou73iLRdWeig6Y,1607
10
+ calfkit/models/types.py,sha256=PQBJFeiwsoY5najanNkL6IymM3mZYv2voFQR9smcObs,1989
11
+ calfkit/nodes/__init__.py,sha256=nWU-r76xj_cxQ9W6RXs5IDhvXAwsMDAVrx_ZuxRmLgI,457
12
+ calfkit/nodes/agent_router_node.py,sha256=Zo1jApqLdnm2BPQD101Ay9UUVwSg2ADBRFti9V5Ts8A,7362
13
+ calfkit/nodes/base_node.py,sha256=sEynWeQejGV-jmConBTY09fMIfTsnYnvtCIYZOp-vlk,2147
14
+ calfkit/nodes/base_tool_node.py,sha256=t1epxURAkGiqSCcnmPp1f41TacF9mwYnuShngX_7hfQ,2000
15
+ calfkit/nodes/chat_node.py,sha256=F3pBvJxiKcWNdviGtpYtWZFQn3ffc4Ka-mh8yIBw_PQ,1943
16
+ calfkit/nodes/registrator.py,sha256=WJLiZ-aIJ8w9nFHNd0ya6pux4zKaZASDT6MIRCXLpa8,331
17
+ calfkit/providers/__init__.py,sha256=NwS6wYfpJXledImfWVQKAgDsrNJAxzJYde3avdNiBDE,126
18
+ calfkit/providers/pydantic_ai/__init__.py,sha256=5zVl-r7Cj7nQUnIQ41zSDzRYCKi8GwDgT-PXNw8M4OQ,100
19
+ calfkit/providers/pydantic_ai/openai.py,sha256=8fKLvYpiqUzDOBrZxTdxtsB0tDhCUniv_N-FRFeBNvE,2604
20
+ calfkit/runners/__init__.py,sha256=4CHTwdPpkFa-Z3vA1PR1S17F9phsaBRXs_SyFO-9Xgo,379
21
+ calfkit/runners/node_runner.py,sha256=bJ8wk4gPADX2ctmyoXgslwGItOxG2fH2MKk3ORPtLNI,1411
22
+ calfkit/stores/__init__.py,sha256=JIaqYv5OnddLdf3l3KO6ET2Wsd0sOGwxIFiUPRjKn_U,1167
23
+ calfkit/stores/base.py,sha256=hPR0V2HAzrOGO5HNUfZSQ82YEoCKKHfY3EQVpHODpa0,1900
24
+ calfkit/stores/in_memory.py,sha256=ZKGfPdLUxri8Q3otzVsWqaDYFYvDpBjYgWRDRoF2C70,1002
25
+ calfkit-0.1.1.dist-info/METADATA,sha256=tG8BXjPadC5KFUz8vsLzEQYxzH0jxGKAe_5TYxs0WY4,5018
26
+ calfkit-0.1.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
27
+ calfkit-0.1.1.dist-info/licenses/LICENSE,sha256=nSECZEo9kht4X50TajtlnrVg21bXN4PVs5mSi5ihAGs,11338
28
+ calfkit-0.1.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.28.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any