Skip to content

Commit 2bb4862

Browse files
Grafi 49/pub sub refactoring (#74)
remove sync flow change everything to async add serial workflow add event queue interface
1 parent bd19208 commit 2bb4862

195 files changed

Lines changed: 3014 additions & 4065 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/docs/getting-started/quickstart.md

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -85,9 +85,12 @@ Create a file named `react_agent_app.py` and create a build-in react-agent:
8585
8686
```python
8787
# react_agent_app.py
88+
import asyncio
89+
8890
from grafi.agents.react_agent import create_react_agent
8991
90-
def main():
92+
93+
async def main():
9194
print("ReAct Agent Chat Interface")
9295
print("Type your questions and press Enter. Type '/bye' to exit.")
9396
print("-" * 50)
@@ -97,7 +100,7 @@ def main():
97100
while True:
98101
user_input = input("\nYou: ").strip()
99102
100-
if user_input.lower() == '/bye':
103+
if user_input.lower() == "/bye":
101104
print("Goodbye!")
102105
break
103106
@@ -106,20 +109,22 @@ def main():
106109
107110
try:
108111
# Get synchronized response from agent
109-
output = react_agent.run(user_input)
112+
output = await react_agent.run(user_input)
110113
print(f"\nAgent: {output}")
111114
112115
except Exception as e:
113116
print(f"Error: {e}")
114117
118+
115119
if __name__ == "__main__":
116-
main()
120+
asyncio.run(main())
117121
```
118122
119-
And finally export an `OPENAI_API_KEY` key as an environment variable:
123+
And finally export an `OPENAI_API_KEY` and `TAVILY_API_KEY` key as an environment variable:
120124
121125
```bash
122126
export OPENAI_API_KEY="sk-proj-******"
127+
export TAVILY_API_KEY="tvly-******"
123128
```
124129
125130
---

docs/docs/guide/configuring-event-store.md

Lines changed: 25 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -130,20 +130,25 @@ event_store = container.event_store
130130
Graphite has a built in react agent that can be used out of the box for your needs, we will use it for the simple use case of passing in input and retrieving the output from OpenAI.
131131

132132
```python
133+
import asyncio
133134
from grafi.agents.react_agent import create_react_agent
134135
135-
react_agent = create_react_agent()
136+
async def run_agent():
136137
137-
result = react_agent.run(user_input, invoke_context)
138+
react_agent = create_react_agent()
138139
139-
print("Output from React Agent:", result)
140+
result = await react_agent.run(user_input, invoke_context)
140141
142+
print("Output from React Agent:", result)
141143
142-
events = event_store.get_conversation_events(conversation_id)
143144
144-
print(f"Events for conversation {conversation_id}:")
145+
events = event_store.get_conversation_events(conversation_id)
145146
146-
print(f"Events: {events} ")
147+
print(f"Events for conversation {conversation_id}:")
148+
149+
print(f"Events: {events} ")
150+
151+
asyncio.run(run_agent())
147152
148153
```
149154

@@ -167,8 +172,8 @@ import uuid
167172
from grafi.agents.react_agent import create_react_agent
168173
from grafi.common.containers.container import container
169174
from grafi.common.event_stores.event_store_postgres import EventStorePostgres
170-
from grafi.common.models.invoke_context import InvokeContext
171-
from grafi.common.models.message import Message
175+
from grafi.models.invoke_context import InvokeContext
176+
from grafi.models.message import Message
172177
173178
postgres_event_store = EventStorePostgres(
174179
db_url="postgresql://postgres:postgres@localhost:5432/grafi_test_db",
@@ -202,21 +207,22 @@ invoke_context = InvokeContext(
202207
assistant_request_id=assistant_request_id,
203208
)
204209
205-
message = Message(role="user", content=user_input)
210+
async def run_agent():
211+
212+
react_agent = create_react_agent()
206213
214+
result = await react_agent.run(user_input, invoke_context)
207215
208-
react_agent = create_react_agent()
216+
print("Output from React Agent:", result)
209217
210-
result = react_agent.run(user_input, invoke_context)
211218
219+
events = event_store.get_conversation_events(conversation_id)
212220
213-
for output_message in result:
214-
print("Output message:", output_message)
221+
print(f"Events for conversation {conversation_id}:")
215222
216-
events = event_store.get_conversation_events(conversation_id)
223+
print(f"Events: {events} ")
217224
218-
print(f"Events for conversation {conversation_id}:")
219-
print(f"Events: {events} ")
225+
asyncio.run(run_agent())
220226
221227
```
222228

@@ -246,10 +252,10 @@ print(f"Events: {events} ")
246252
2025-07-12 17:38:55.566 | WARNING | grafi.tools.function_calls.function_call_tool:__init_subclass__:86 - GoogleSearchTool: no method decorated with @llm_function found.
247253
2025-07-12 17:38:55.895 | DEBUG | grafi.common.instrumentations.tracing:is_local_endpoint_available:30 - Endpoint check failed: [Errno 111] Connection refused
248254
2025-07-12 17:38:55.895 | DEBUG | grafi.common.instrumentations.tracing:setup_tracing:117 - OTLP endpoint is not available. Using InMemorySpanExporter.
249-
2025-07-12 17:38:55.905 | INFO | grafi.common.topics.topic:publish_data:64 - [agent_input_topic] Message published with event_id: 1da4f45008264dc98fd63dc154dcaa6e
255+
2025-07-12 17:38:55.905 | INFO | grafi.topics.topic:publish_data:64 - [agent_input_topic] Message published with event_id: 1da4f45008264dc98fd63dc154dcaa6e
250256
2025-07-12 17:38:55.912 | DEBUG | grafi.nodes.node:invoke:49 - Executing Node with inputs: [ConsumeFromTopicEvent(event_id='8dd13f159fbc40b29ca030501f4d2859', event_version='1.0', invoke_context=InvokeContext(conversation_id='9051491ca7a84b71a3f3e9d790b79e4f', invoke_id='6e56da25ca824bf4a8d454a731b6f336', assistant_request_id='0e0317d25c9b40e4a76a130bbbb7bc43', user_id='9e396be8f8c442bc9269a31a9b3f69a2'), event_type=<EventType.CONSUME_FROM_TOPIC: 'ConsumeFromTopic'>, timestamp=datetime.datetime(2025, 7, 12, 16, 38, 55, 909090, tzinfo=datetime.timezone.utc), name='agent_input_topic', offset=0, data=[Message(name=None, message_id='ae2638fa5f3047fbbfb43dbd1545e6d2', timestamp=1752338335885634346, content='What is the capital of the United Kingdom', refusal=None, annotations=None, audio=None, role='user', tool_call_id=None, tools=None, function_call=None, tool_calls=None, is_streaming=False)], consumer_name='OpenAIInputNode', consumer_type='OpenAIInputNode')]
251-
2025-07-12 17:38:57.400 | INFO | grafi.common.topics.topic:publish_data:69 - [function_call_topic] Message NOT published (condition not met)
252-
2025-07-12 17:38:57.400 | INFO | grafi.common.topics.output_topic:publish_data:91 - [agent_output_topic] Message published with event_id: d83d3339fbd94d6e983fc51965177891
257+
2025-07-12 17:38:57.400 | INFO | grafi.topics.topic:publish_data:69 - [function_call_topic] Message NOT published (condition not met)
258+
2025-07-12 17:38:57.400 | INFO | grafi.topics.output_topic:publish_data:91 - [agent_output_topic] Message published with event_id: d83d3339fbd94d6e983fc51965177891
253259
<span style="color:#FF4689">Output from React Agent:</span> The capital of the United Kingdom is London.
254260
<span style="color:#FF4689">Events for conversation: </span> 9051491ca7a84b71a3f3e9d790b79e4f:
255261
<span style="color:#FF4689">Events: </span>[AssistantInvokeEvent(event_id='7d97059ad3404c74be582ed4d01e365e', event_version='1.0', invoke_context=InvokeContext(conversation_id='9051491ca7a84b71a3f3e9d790b79e4f', invoke_id='6e56da25ca824bf4a8d454a731b6f336', assistant_request_id='0e0317d25c9b40e4a76a130bbbb7bc43', user_id='9e396be8f8c442bc9269a31a9b3f69a2'), event_type=<EventType.ASSISTANT_INVOKE: 'AssistantInvoke'>, timestamp=datetime.datetime(2025, 7, 12, 16, 38, 55, 885710), assistant_id='de51912ecc6c44ce803a0cd5eb358ba5', assistant_name='ReActAgent', assistant_type='ReActAgent', input_data=[Message(name=None, message_id='ae2638fa5f3047fbbfb43dbd1545e6d2', timestamp=1752338335885634346, content='What is the capital of the United Kingdom', refusal=None, annotations=No```</code></pre></div>

docs/docs/guide/connecting-to-an-mcp-server.md

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -176,10 +176,10 @@ from pydantic import Field
176176
177177
from grafi.assistants.assistant import Assistant
178178
from grafi.assistants.assistant_base import AssistantBaseBuilder
179-
from grafi.common.topics.input_topic import InputTopic
180-
from grafi.common.topics.output_topic import OutputTopic
181-
from grafi.common.topics.subscription_builder import SubscriptionBuilder
182-
from grafi.common.topics.topic import Topic
179+
from grafi.topics.input_topic import InputTopic
180+
from grafi.topics.output_topic import OutputTopic
181+
from grafi.topics.subscription_builder import SubscriptionBuilder
182+
from grafi.topics.topic import Topic
183183
from grafi.nodes.node import Node
184184
from grafi.tools.function_calls.function_call_tool import FunctionCallTool
185185
from grafi.tools.llms.impl.openai_tool import OpenAITool
@@ -337,7 +337,7 @@ class StockAssistantBuilder(
337337
```
338338

339339

340-
Graphtie is natively asychrnous, but you can chose to run syncrhous coroutines as well. For this case we are making a fully asynchronous workflow by overrding the `async def a_run()` method of the `Assistant` class. In order to run this create a `main.py` that will instantiate the assistant and execute it asynchrnously.
340+
Graphtie is natively asychrnous, but you can chose to run syncrhous coroutines as well. For this case we are making a fully asynchronous workflow by overrding the `async def run()` method of the `Assistant` class. In order to run this create a `main.py` that will instantiate the assistant and execute it asynchrnously.
341341
342342
```python
343343
#main.py
@@ -347,9 +347,10 @@ import uuid
347347
from typing import Dict
348348
349349
from grafi.common.containers.container import container
350-
from grafi.common.models.invoke_context import InvokeContext
351-
from grafi.common.models.mcp_connections import StreamableHttpConnection
352-
from grafi.common.models.message import Message
350+
from grafi.common.events.topic_events.publish_to_topic_event import PublishToTopicEvent
351+
from grafi.models.invoke_context import InvokeContext
352+
from grafi.models.mcp_connections import StreamableHttpConnection
353+
from grafi.models.message import Message
353354
from grafi.tools.function_calls.impl.mcp_tool import MCPTool
354355
355356
from assistant import StockAssistant
@@ -372,7 +373,7 @@ async def create_assistant():
372373
.model(model)
373374
.api_key(api_key)
374375
.function_call_llm_system_message(function_call_llm_system_message)
375-
.function_tool(await MCPTool.builder().connections(mcp_config).a_build())
376+
.function_tool(await MCPTool.builder().connections(mcp_config).build())
376377
.summary_llm_system_message(
377378
"You are a helpful assistant that provides a summary of the company overview. return all output as json formatted string")
378379
.build()
@@ -392,7 +393,11 @@ async def main():
392393
question = "What is the overview of the company Tesla?"
393394
input_data = [Message(role="user", content=question)]
394395
395-
async for response in assistant.a_invoke(invoke_context, input_data):
396+
publish_event = PublishToTopicEvent(
397+
invoke_context=execution_context, data=input_messages
398+
)
399+
400+
async for response in assistant.invoke(publish_event):
396401
print("Assistant output:")
397402
for output in response:
398403
print(output.content)
@@ -524,11 +529,11 @@ Topics can have conditions attached to them to trigger them, in the case of `Inp
524529
525530
526531
527-
When you call the assistant's `a_invoke()` method from `main.py`
532+
When you call the assistant's `invoke()` method from `main.py`
528533
529534
```python
530535
531-
async for output in assistant.a_invoke(invoke_context, input_data):
536+
async for output in assistant.invoke(invoke_context, input_data):
532537
```
533538
534539
The `input_data` gets published on the assistants `InputTopic` during workflow initialization. This is done [within the framework](https://github.com/binome-dev/graphite/blob/0b6b666b6f0e122ac970f437ee24f5d87c30a81a/grafi/workflows/impl/event_driven_workflow.py#L681-L687) but we are outlying it here so you can follow the flow of data.

docs/docs/guide/creating-a-simple-workflow.md

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,8 @@ The main function orchestrates the entire workflow. We start by defining a sampl
4343

4444
```python linenums="9"
4545
import uuid
46-
from grafi.common.models.message import Message
47-
from grafi.common.models.invoke_context import InvokeContext
46+
from grafi.models.message import Message
47+
from grafi.models.invoke_context import InvokeContext
4848

4949
def main():
5050
user_input = "What is the capital of the United Kingdom"
@@ -85,8 +85,8 @@ The node subscribes to the `agent_input_topic` (the root topic in Graphite), use
8585
```python linenums="27"
8686
from grafi.nodes.node import Node
8787
from grafi.tools.llms.impl.openai_tool import OpenAITool
88-
from grafi.common.topics.input_topic import InputTopic
89-
from grafi.common.topics.output_topic import OutputTopic
88+
from grafi.topics.input_topic import InputTopic
89+
from grafi.topics.output_topic import OutputTopic
9090

9191

9292
agent_input_topic = InputTopic(name="agent_input_topic")
@@ -131,13 +131,12 @@ workflow = (
131131
With the `EventDrivenWorkflow` object created, we can invoke it by passing our `invoke_context` and a `List[Message]`. The workflow will execute and return the results, which we can then print. Save this complete code as `main.py`.
132132

133133
```python linenums="54"
134-
result = workflow.invoke(
134+
async for result in workflow.invoke(
135135
invoke_context,
136136
[message]
137-
)
138-
139-
for output_message in result:
140-
print("Output message:", output_message.content)
137+
):
138+
for output_message in result:
139+
print("Output message:", output_message.content)
141140
```
142141

143142
### 6. Entry Point

docs/docs/guide/getting-started-with-assistants.md

Lines changed: 28 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -129,8 +129,8 @@ Implement the builder pattern for fluent configuration of the assistant. This pi
129129

130130
```python
131131
from grafi.nodes.node import Node
132-
from grafi.common.topics.input_topic import InputTopic
133-
from grafi.common.topics.output_topic import OutputTopic
132+
from grafi.topics.input_topic import InputTopic
133+
from grafi.topics.output_topic import OutputTopic
134134
from grafi.tools.llms.impl.openai_tool import OpenAITool
135135
from grafi.workflows.impl.event_driven_workflow import EventDrivenWorkflow
136136

@@ -188,15 +188,16 @@ Prepare input data and context for workflow execution:
188188
- Returns both the input data and context
189189

190190
```python
191-
from grafi.common.models.invoke_context import InvokeContext
191+
from grafi.common.events.topic_events.publish_to_topic_event import PublishToTopicEvent
192+
from grafi.models.invoke_context import InvokeContext
192193
from typing import Optional
193-
from grafi.common.models.message import Message
194+
from grafi.models.message import Message
194195

195196
class FinanceAssistant(Assistant):
196197

197198
...
198199

199-
def get_input(self, question: str, invoke_context: Optional[InvokeContext] = None) -> tuple[list[Message], InvokeContext]:
200+
def get_input(self, question: str, invoke_context: Optional[InvokeContext] = None) -> PublishToTopicEvent:
200201
"""Prepare input data and invoke context."""
201202
if invoke_context is None:
202203
logger.debug("Creating new InvokeContext with default conversation id for FinanceAssistant")
@@ -214,7 +215,9 @@ class FinanceAssistant(Assistant):
214215
)
215216
]
216217

217-
return input_data, invoke_context
218+
return PublishToTopicEvent(
219+
invoke_context=execution_context, data=input_messages
220+
)
218221
```
219222

220223

@@ -227,22 +230,22 @@ This function is not part of the framework, but rather a helper function used to
227230
class FinanceAssistant(Assistant):
228231

229232
...
230-
def run(self, question: str, invoke_context: Optional[InvokeContext] = None) -> str:
233+
async def run(self, question: str, invoke_context: Optional[InvokeContext] = None) -> str:
231234
"""Run the assistant with a question and return the response."""
232235
# Call helper function get_input()
233-
input_data, invoke_context = self.get_input(question, invoke_context)
236+
input_event= self.get_input(question, invoke_context)
234237
# This is the line that invokes the workflow
235-
output = super().invoke(invoke_context, input_data)
236-
237-
# Handle different content types
238-
if output and len(output) > 0:
239-
content = output[0].content
240-
if isinstance(content, str):
241-
return content
242-
elif content is not None:
243-
return str(content)
244-
245-
return "No response generated"
238+
response_str = ""
239+
async for output in super().invoke(input_event):
240+
# Handle different content types
241+
if output and len(output) > 0:
242+
content = output.data[0].content
243+
if isinstance(content, str):
244+
response_str += content
245+
elif content is not None:
246+
response_str += str(content)
247+
248+
return response_str
246249
```
247250

248251
Main execution method that:
@@ -258,6 +261,8 @@ Now that we have created the class for the assistance, we have to instantiate it
258261

259262
```python
260263
# main.py
264+
import asyncio
265+
261266
def main():
262267
system_message = os.getenv("OPENAI_SYSTEM_MESSAGE")
263268
api_key = os.getenv("OPENAI_API_KEY")
@@ -274,12 +279,12 @@ def main():
274279

275280
"""Main function to run the assistant."""
276281
user_input = "What are the key factors to consider when choosing between a 401(k) and a Roth IRA?"
277-
result = assistant.run(user_input)
282+
result = await assistant.run(user_input)
278283
print("Output message:", result)
279284

280285

281286
if __name__ == "__main__":
282-
main()
287+
asyncio.run(main())
283288

284289
```
285290

@@ -319,12 +324,12 @@ def main():
319324

320325
"""Main function to run the assistant."""
321326
user_input = "What are the key factors to consider when choosing between a 401(k) and a Roth IRA?"
322-
result = assistant.run(user_input)
327+
result = await assistant.run(user_input)
323328
print("Output message:", result)
324329

325330

326331
if __name__ == "__main__":
327-
main()
332+
asyncio.run(main())
328333
```
329334

330335

0 commit comments

Comments
 (0)