Skip to content

Commit 88e82de

Browse files
GuanyiLi-Craigcraig.guanyi.li@gmail.com
andauthored
Grafi 46/use event interface (#70)
* add event as output * return list of consumer events * update code and unit tests * update integration tests * update recover integration tests * lint * lint * update version * update documents * lint * address comments --------- Co-authored-by: craig.guanyi.li@gmail.com <guanyili@Guanyis-MacBook-Pro-2.local>
1 parent 2be2162 commit 88e82de

125 files changed

Lines changed: 3512 additions & 2389 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/user-guide/assistant.md

Lines changed: 26 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,22 @@ The `AssistantBase` class provides an abstract foundation for all assistants, de
1717

1818
```python
1919
from grafi.assistants.assistant_base import AssistantBase
20-
from grafi.common.models.invoke_context import InvokeContext
21-
from grafi.common.models.message import Messages, MsgsAGen
20+
from grafi.common.events.topic_events.publish_to_topic_event import PublishToTopicEvent
21+
from grafi.common.events.topic_events.consume_from_topic_event import ConsumeFromTopicEvent
22+
from typing import List, AsyncGenerator
2223

2324
class MyAssistant(AssistantBase):
2425
def _construct_workflow(self):
2526
# Implementation required
2627
pass
28+
29+
def invoke(self, input_event: PublishToTopicEvent) -> List[ConsumeFromTopicEvent]:
30+
# Implementation required
31+
pass
32+
33+
async def a_invoke(self, input_event: PublishToTopicEvent) -> AsyncGenerator[ConsumeFromTopicEvent, None]:
34+
# Implementation required
35+
pass
2736
```
2837

2938
### AssistantBase Fields
@@ -43,8 +52,8 @@ Subclasses must implement these abstract methods:
4352
| Method | Signature | Description |
4453
|--------|-----------|-------------|
4554
| `_construct_workflow` | `() -> AssistantBase` | Constructs and configures the assistant's workflow |
46-
| `invoke` | `(InvokeContext, Messages) -> Messages` | Synchronous message processing |
47-
| `a_invoke` | `(InvokeContext, Messages) -> MsgsAGen` | Asynchronous message processing with streaming support |
55+
| `invoke` | `(PublishToTopicEvent) -> List[ConsumeFromTopicEvent]` | Synchronous event processing |
56+
| `a_invoke` | `(PublishToTopicEvent) -> AsyncGenerator[ConsumeFromTopicEvent, None]` | Asynchronous event processing with streaming support |
4857

4958
### Lifecycle
5059

@@ -65,50 +74,48 @@ The concrete `Assistant` class extends `AssistantBase` and provides a complete i
6574

6675
| Method | Signature | Description |
6776
|--------|-----------|-------------|
68-
| `invoke` | `(InvokeContext, Messages) -> Messages` | Processes input messages synchronously through the workflow |
69-
| `a_invoke` | `(InvokeContext, Messages) -> MsgsAGen` | Processes input messages asynchronously with streaming support |
77+
| `invoke` | `(PublishToTopicEvent) -> List[ConsumeFromTopicEvent]` | Processes input events synchronously through the workflow |
78+
| `a_invoke` | `(PublishToTopicEvent) -> AsyncGenerator[ConsumeFromTopicEvent, None]` | Processes input events asynchronously with streaming support |
7079
| `to_dict` | `() -> dict[str, Any]` | Serializes the assistant's workflow configuration |
7180
| `generate_manifest` | `(output_dir: str = ".") -> str` | Generates a JSON manifest file for the assistant |
7281

7382
### Method Details
7483

7584
#### invoke()
7685

77-
Synchronously processes input messages through the configured workflow.
86+
Synchronously processes input events through the configured workflow.
7887

7988
```python
8089
@record_assistant_invoke
81-
def invoke(self, invoke_context: InvokeContext, input_data: Messages) -> Messages:
82-
sorted_outputs = self.workflow.invoke(invoke_context, input_data)
83-
return sorted_outputs
90+
def invoke(self, input_event: PublishToTopicEvent) -> List[ConsumeFromTopicEvent]:
91+
events = self.workflow.invoke(input_event)
92+
return events
8493
```
8594

8695
**Parameters**:
8796

88-
- `invoke_context`: Context containing invocation metadata
89-
- `input_data`: List of input messages to process
97+
- `input_event`: A `PublishToTopicEvent` containing the data to be processed and context information
9098

91-
**Returns**: List of response messages sorted by timestamp
99+
**Returns**: List of `ConsumeFromTopicEvent` objects representing the workflow output
92100

93101
**Raises**: `ValueError` if required configuration (e.g., API keys) is missing
94102

95103
#### a_invoke()
96104

97-
Asynchronously processes input messages with support for streaming responses.
105+
Asynchronously processes input events with support for streaming responses.
98106

99107
```python
100108
@record_assistant_a_invoke
101-
async def a_invoke(self, invoke_context: InvokeContext, input_data: Messages) -> MsgsAGen:
102-
async for output in self.workflow.a_invoke(invoke_context, input_data):
109+
async def a_invoke(self, input_event: PublishToTopicEvent) -> AsyncGenerator[ConsumeFromTopicEvent, None]:
110+
async for output in self.workflow.a_invoke(input_event):
103111
yield output
104112
```
105113

106114
**Parameters**:
107115

108-
- `invoke_context`: Context containing invocation metadata
109-
- `input_data`: List of input messages to process
116+
- `input_event`: A `PublishToTopicEvent` containing the data to be processed and context information
110117

111-
**Returns**: Async generator yielding message batches
118+
**Returns**: Async generator yielding `ConsumeFromTopicEvent` objects
112119

113120
**Use Cases**: Streaming responses, real-time processing, concurrent operations
114121

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
# Event Interfaces
2+
3+
## Overview
4+
5+
Graphite uses a consistent event-driven interface pattern where components communicate exclusively through two primary event types:
6+
- **PublishToTopicEvent**: Used to publish data to topics
7+
- **ConsumeFromTopicEvent**: Used to consume data from topics
8+
9+
This design creates a clean separation of concerns and enables loose coupling between components.
10+
11+
## Interface Patterns
12+
13+
### Component Communication Flow
14+
15+
The event interface creates a bidirectional flow pattern:
16+
17+
```mermaid
18+
graph LR
19+
A[Assistant/Workflow] -->|PublishToTopicEvent| B[Topics]
20+
B -->|ConsumeFromTopicEvent| C[Nodes]
21+
C -->|PublishToTopicEvent| D[Topics]
22+
D -->|ConsumeFromTopicEvent| A
23+
```
24+
25+
### Component Interfaces
26+
27+
| Component | Input Type | Output Type | Description |
28+
|-----------|------------|-------------|-------------|
29+
| **Assistant** | `PublishToTopicEvent` | `List[ConsumeFromTopicEvent]` | Receives published events, returns consumed events |
30+
| **Workflow** | `PublishToTopicEvent` | `List[ConsumeFromTopicEvent]` | Orchestrates nodes through event flow |
31+
| **Node** | `List[ConsumeFromTopicEvent]` | `PublishToTopicEvent` | Consumes events, processes, publishes results |
32+
| **Tool** | `Messages` | `Messages` | Transforms message data (used within nodes) |
33+
34+
## Event Structure
35+
36+
### PublishToTopicEvent
37+
38+
Published when a component sends data to a topic:
39+
40+
```python
41+
from grafi.common.events.topic_events.publish_to_topic_event import PublishToTopicEvent
42+
from grafi.common.models.invoke_context import InvokeContext
43+
from grafi.common.models.message import Message
44+
45+
event = PublishToTopicEvent(
46+
publisher_name="ProcessorNode",
47+
publisher_type="node",
48+
invoke_context=InvokeContext(session_id="session-123"),
49+
consumed_event_ids=["evt_1", "evt_2"], # Events that led to this publication
50+
data=[Message(role="assistant", content="Processed result")]
51+
)
52+
```
53+
54+
### ConsumeFromTopicEvent
55+
56+
Created when data is consumed from a topic:
57+
58+
```python
59+
from grafi.common.events.topic_events.consume_from_topic_event import ConsumeFromTopicEvent
60+
61+
event = ConsumeFromTopicEvent(
62+
topic_name="output_topic",
63+
offset=42,
64+
publisher_name="ProcessorNode", # Original publisher
65+
publisher_type="node",
66+
invoke_context=InvokeContext(session_id="session-123"),
67+
consumed_event_ids=["evt_1", "evt_2"],
68+
data=[Message(role="assistant", content="Processed result")]
69+
)
70+
```
71+
72+
## Implementation Examples
73+
74+
### Assistant Implementation
75+
76+
```python
77+
from grafi.assistants.assistant import Assistant
78+
from grafi.common.events.topic_events.publish_to_topic_event import PublishToTopicEvent
79+
from grafi.common.events.topic_events.consume_from_topic_event import ConsumeFromTopicEvent
80+
from typing import List, AsyncGenerator
81+
82+
class MyAssistant(Assistant):
83+
def invoke(self, input_event: PublishToTopicEvent) -> List[ConsumeFromTopicEvent]:
84+
"""Synchronous processing of events."""
85+
# Delegate to workflow
86+
events = self.workflow.invoke(input_event)
87+
return events
88+
89+
async def a_invoke(
90+
self,
91+
input_event: PublishToTopicEvent
92+
) -> AsyncGenerator[ConsumeFromTopicEvent, None]:
93+
"""Asynchronous streaming of events."""
94+
async for output in self.workflow.a_invoke(input_event):
95+
yield output
96+
```
97+
98+
### Node Implementation
99+
100+
```python
101+
from grafi.nodes.node import Node
102+
from grafi.common.models.invoke_context import InvokeContext
103+
from grafi.common.events.topic_events.consume_from_topic_event import ConsumeFromTopicEvent
104+
from grafi.common.events.topic_events.publish_to_topic_event import PublishToTopicEvent
105+
from typing import List
106+
107+
class ProcessorNode(Node):
108+
def invoke(
109+
self,
110+
invoke_context: InvokeContext,
111+
node_input: List[ConsumeFromTopicEvent]
112+
) -> PublishToTopicEvent:
113+
"""Process consumed events and publish result."""
114+
# Execute command on input data
115+
response = self.command.invoke(invoke_context, node_input)
116+
117+
# Wrap response in PublishToTopicEvent
118+
return PublishToTopicEvent(
119+
publisher_name=self.name,
120+
publisher_type=self.type,
121+
invoke_context=invoke_context,
122+
consumed_event_ids=[event.event_id for event in node_input],
123+
data=response
124+
)
125+
```
126+
127+
### Workflow Integration
128+
129+
```python
130+
from grafi.workflows.workflow import Workflow
131+
from grafi.common.events.topic_events.publish_to_topic_event import PublishToTopicEvent
132+
from grafi.common.events.topic_events.consume_from_topic_event import ConsumeFromTopicEvent
133+
134+
class MyWorkflow(Workflow):
135+
def invoke(self, input_event: PublishToTopicEvent) -> List[ConsumeFromTopicEvent]:
136+
"""Execute workflow synchronously."""
137+
# Initialize workflow with input event
138+
self.initial_workflow(input_event)
139+
140+
# Process nodes until completion
141+
while not self._invoke_queue.empty():
142+
node = self._invoke_queue.get()
143+
output = node.invoke(...)
144+
# Publish output to topics
145+
146+
# Return consumed events from output topics
147+
return self._get_output_events()
148+
```
149+
150+
## Benefits of Event Interfaces
151+
152+
### 1. Loose Coupling
153+
Components don't need direct references to each other - they communicate through events and topics.
154+
155+
### 2. Traceability
156+
Every event carries:
157+
- `invoke_context`: Request correlation information
158+
- `consumed_event_ids`: Chain of events that led to this event
159+
- Publisher information: Source component details
160+
161+
### 3. Flexibility
162+
- Easy to add new components without modifying existing ones
163+
- Components can be tested in isolation
164+
- Workflows can be composed dynamically
165+
166+
### 4. Observability
167+
- Complete audit trail through event chain
168+
- Easy to trace data flow through the system
169+
- Built-in support for distributed tracing
170+
171+
### 5. Recovery
172+
- Events can be replayed from any point
173+
- Workflows can resume from interruption
174+
- State can be reconstructed from event history
175+
176+
## Migration from Direct Invocation
177+
178+
If migrating from older patterns that used direct method calls:
179+
180+
**Old Pattern:**
181+
```python
182+
def invoke(self, invoke_context: InvokeContext, input_data: Messages) -> Messages:
183+
return self.workflow.invoke(invoke_context, input_data)
184+
```
185+
186+
**New Pattern:**
187+
```python
188+
def invoke(self, input_event: PublishToTopicEvent) -> List[ConsumeFromTopicEvent]:
189+
return self.workflow.invoke(input_event)
190+
```
191+
192+
Key changes:
193+
1. Input is now a single `PublishToTopicEvent` instead of separate context and data
194+
2. Output is a list of `ConsumeFromTopicEvent` objects
195+
3. Context and data are embedded within the event objects
196+
4. Event IDs enable tracing the full processing chain
197+
198+
## Best Practices
199+
200+
1. **Always preserve event chains**: Include `consumed_event_ids` when creating new events
201+
2. **Use descriptive names**: Set meaningful `publisher_name` values for debugging
202+
3. **Type your components**: Use proper type hints for event interfaces
203+
4. **Handle streaming properly**: Use async generators for streaming responses
204+
5. **Validate event data**: Ensure data types match expected formats
205+
206+
## See Also
207+
208+
- [Events System](./events/events.md) - Detailed event documentation
209+
- [Topics](./topics/topic.md) - Topic-based messaging
210+
- [Event-Driven Workflow](./event-driven-workflow.md) - Workflow orchestration
211+
- [Node](./node.md) - Node component documentation

0 commit comments

Comments
 (0)