mcal-ai 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.
- mcal/__init__.py +165 -0
- mcal/backends/__init__.py +42 -0
- mcal/backends/base.py +383 -0
- mcal/baselines/__init__.py +1 -0
- mcal/core/__init__.py +101 -0
- mcal/core/embeddings.py +266 -0
- mcal/core/extraction_cache.py +398 -0
- mcal/core/goal_retriever.py +539 -0
- mcal/core/intent_tracker.py +734 -0
- mcal/core/models.py +445 -0
- mcal/core/rate_limiter.py +372 -0
- mcal/core/reasoning_store.py +1061 -0
- mcal/core/retry.py +188 -0
- mcal/core/storage.py +456 -0
- mcal/core/streaming.py +254 -0
- mcal/core/unified_extractor.py +1466 -0
- mcal/core/vector_index.py +206 -0
- mcal/evaluation/__init__.py +1 -0
- mcal/integrations/__init__.py +88 -0
- mcal/integrations/autogen.py +95 -0
- mcal/integrations/crewai.py +92 -0
- mcal/integrations/langchain.py +112 -0
- mcal/integrations/langgraph.py +50 -0
- mcal/mcal.py +1697 -0
- mcal/providers/bedrock.py +217 -0
- mcal/storage/__init__.py +1 -0
- mcal_ai-0.1.0.dist-info/METADATA +319 -0
- mcal_ai-0.1.0.dist-info/RECORD +32 -0
- mcal_ai-0.1.0.dist-info/WHEEL +5 -0
- mcal_ai-0.1.0.dist-info/entry_points.txt +2 -0
- mcal_ai-0.1.0.dist-info/licenses/LICENSE +21 -0
- mcal_ai-0.1.0.dist-info/top_level.txt +1 -0
mcal/core/streaming.py
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Streaming Response API for MCAL
|
|
3
|
+
|
|
4
|
+
Provides streaming support for the add() operation, yielding partial results
|
|
5
|
+
as they become available. This improves perceived latency by showing progress
|
|
6
|
+
immediately rather than waiting for complete extraction.
|
|
7
|
+
|
|
8
|
+
Issue #10: Streaming Response API
|
|
9
|
+
|
|
10
|
+
Key benefits:
|
|
11
|
+
- User sees progress immediately (not 22s blank screen)
|
|
12
|
+
- Partial results usable before completion
|
|
13
|
+
- Better UX for demo and production use
|
|
14
|
+
|
|
15
|
+
Usage:
|
|
16
|
+
async for event in mcal.add_stream(messages, user_id):
|
|
17
|
+
if event.type == StreamEventType.INTENT_EXTRACTED:
|
|
18
|
+
print(f"Found intent: {event.data.content}")
|
|
19
|
+
elif event.type == StreamEventType.DECISION_EXTRACTED:
|
|
20
|
+
print(f"Found decision: {event.data.decision}")
|
|
21
|
+
elif event.type == StreamEventType.COMPLETE:
|
|
22
|
+
print(f"Total time: {event.data.timing.total_ms}ms")
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
from dataclasses import dataclass, field
|
|
28
|
+
from datetime import datetime
|
|
29
|
+
from enum import Enum
|
|
30
|
+
from typing import Any, Optional, Union
|
|
31
|
+
|
|
32
|
+
from .models import IntentNode, IntentGraph, DecisionTrail
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class StreamEventType(str, Enum):
|
|
36
|
+
"""Types of events emitted during streaming extraction."""
|
|
37
|
+
|
|
38
|
+
# Progress events
|
|
39
|
+
STARTED = "started" # Extraction started
|
|
40
|
+
PHASE_STARTED = "phase_started" # New phase (facts/intents/decisions)
|
|
41
|
+
PHASE_COMPLETE = "phase_complete" # Phase finished
|
|
42
|
+
|
|
43
|
+
# Partial result events
|
|
44
|
+
FACT_EXTRACTED = "fact_extracted" # Single fact from Mem0
|
|
45
|
+
INTENT_EXTRACTED = "intent_extracted" # Single intent node extracted
|
|
46
|
+
INTENT_UPDATED = "intent_updated" # Existing intent updated
|
|
47
|
+
DECISION_EXTRACTED = "decision_extracted" # Single decision extracted
|
|
48
|
+
|
|
49
|
+
# Status events
|
|
50
|
+
CACHE_HIT = "cache_hit" # Results from cache
|
|
51
|
+
ERROR = "error" # Non-fatal error
|
|
52
|
+
|
|
53
|
+
# Completion events
|
|
54
|
+
COMPLETE = "complete" # All extraction finished
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class ExtractionPhase(str, Enum):
|
|
58
|
+
"""Phases of the extraction process."""
|
|
59
|
+
FACTS = "facts"
|
|
60
|
+
INTENTS = "intents"
|
|
61
|
+
DECISIONS = "decisions"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass
|
|
65
|
+
class StreamProgress:
|
|
66
|
+
"""Progress information for streaming updates."""
|
|
67
|
+
phase: ExtractionPhase
|
|
68
|
+
current: int = 0
|
|
69
|
+
total: Optional[int] = None
|
|
70
|
+
elapsed_ms: int = 0
|
|
71
|
+
message: str = ""
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@dataclass
|
|
75
|
+
class CacheHitInfo:
|
|
76
|
+
"""Information about a cache hit."""
|
|
77
|
+
hit_type: str # "full" or "partial"
|
|
78
|
+
messages_cached: int
|
|
79
|
+
messages_to_process: int
|
|
80
|
+
saved_time_ms: int
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@dataclass
|
|
84
|
+
class PhaseResult:
|
|
85
|
+
"""Result from a completed phase."""
|
|
86
|
+
phase: ExtractionPhase
|
|
87
|
+
items_count: int
|
|
88
|
+
duration_ms: int
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@dataclass
|
|
92
|
+
class StreamEvent:
|
|
93
|
+
"""
|
|
94
|
+
A single event in the streaming extraction process.
|
|
95
|
+
|
|
96
|
+
Events are yielded as extraction progresses, allowing clients to:
|
|
97
|
+
- Show real-time progress
|
|
98
|
+
- Display partial results immediately
|
|
99
|
+
- Track timing per phase
|
|
100
|
+
|
|
101
|
+
Attributes:
|
|
102
|
+
type: The type of event
|
|
103
|
+
timestamp: When the event occurred
|
|
104
|
+
data: Event-specific payload (varies by type)
|
|
105
|
+
progress: Optional progress information
|
|
106
|
+
"""
|
|
107
|
+
type: StreamEventType
|
|
108
|
+
timestamp: datetime = field(default_factory=datetime.now)
|
|
109
|
+
data: Optional[Union[
|
|
110
|
+
IntentNode,
|
|
111
|
+
DecisionTrail,
|
|
112
|
+
dict, # For facts (MemoryEntry dict)
|
|
113
|
+
PhaseResult,
|
|
114
|
+
CacheHitInfo,
|
|
115
|
+
Any, # For COMPLETE event (AddResult from mcal module)
|
|
116
|
+
str, # For errors
|
|
117
|
+
]] = None
|
|
118
|
+
progress: Optional[StreamProgress] = None
|
|
119
|
+
|
|
120
|
+
def to_dict(self) -> dict:
|
|
121
|
+
"""Convert to dictionary for JSON serialization."""
|
|
122
|
+
result = {
|
|
123
|
+
"type": self.type.value,
|
|
124
|
+
"timestamp": self.timestamp.isoformat(),
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if self.data is not None:
|
|
128
|
+
if hasattr(self.data, "model_dump"):
|
|
129
|
+
result["data"] = self.data.model_dump()
|
|
130
|
+
elif hasattr(self.data, "__dict__"):
|
|
131
|
+
result["data"] = self.data.__dict__
|
|
132
|
+
elif isinstance(self.data, dict):
|
|
133
|
+
result["data"] = self.data
|
|
134
|
+
else:
|
|
135
|
+
result["data"] = str(self.data)
|
|
136
|
+
|
|
137
|
+
if self.progress is not None:
|
|
138
|
+
result["progress"] = {
|
|
139
|
+
"phase": self.progress.phase.value,
|
|
140
|
+
"current": self.progress.current,
|
|
141
|
+
"total": self.progress.total,
|
|
142
|
+
"elapsed_ms": self.progress.elapsed_ms,
|
|
143
|
+
"message": self.progress.message,
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return result
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
# =============================================================================
|
|
150
|
+
# Stream Builder Helpers
|
|
151
|
+
# =============================================================================
|
|
152
|
+
|
|
153
|
+
def event_started() -> StreamEvent:
|
|
154
|
+
"""Create a STARTED event."""
|
|
155
|
+
return StreamEvent(type=StreamEventType.STARTED)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def event_phase_started(phase: ExtractionPhase, message: str = "") -> StreamEvent:
|
|
159
|
+
"""Create a PHASE_STARTED event."""
|
|
160
|
+
return StreamEvent(
|
|
161
|
+
type=StreamEventType.PHASE_STARTED,
|
|
162
|
+
progress=StreamProgress(phase=phase, message=message)
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def event_phase_complete(
|
|
167
|
+
phase: ExtractionPhase,
|
|
168
|
+
items_count: int,
|
|
169
|
+
duration_ms: int
|
|
170
|
+
) -> StreamEvent:
|
|
171
|
+
"""Create a PHASE_COMPLETE event."""
|
|
172
|
+
return StreamEvent(
|
|
173
|
+
type=StreamEventType.PHASE_COMPLETE,
|
|
174
|
+
data=PhaseResult(phase=phase, items_count=items_count, duration_ms=duration_ms)
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def event_fact_extracted(fact: dict, progress: Optional[StreamProgress] = None) -> StreamEvent:
|
|
179
|
+
"""Create a FACT_EXTRACTED event."""
|
|
180
|
+
return StreamEvent(
|
|
181
|
+
type=StreamEventType.FACT_EXTRACTED,
|
|
182
|
+
data=fact,
|
|
183
|
+
progress=progress
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def event_intent_extracted(
|
|
188
|
+
intent: IntentNode,
|
|
189
|
+
progress: Optional[StreamProgress] = None
|
|
190
|
+
) -> StreamEvent:
|
|
191
|
+
"""Create an INTENT_EXTRACTED event."""
|
|
192
|
+
return StreamEvent(
|
|
193
|
+
type=StreamEventType.INTENT_EXTRACTED,
|
|
194
|
+
data=intent,
|
|
195
|
+
progress=progress
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def event_intent_updated(
|
|
200
|
+
intent: IntentNode,
|
|
201
|
+
progress: Optional[StreamProgress] = None
|
|
202
|
+
) -> StreamEvent:
|
|
203
|
+
"""Create an INTENT_UPDATED event."""
|
|
204
|
+
return StreamEvent(
|
|
205
|
+
type=StreamEventType.INTENT_UPDATED,
|
|
206
|
+
data=intent,
|
|
207
|
+
progress=progress
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def event_decision_extracted(
|
|
212
|
+
decision: DecisionTrail,
|
|
213
|
+
progress: Optional[StreamProgress] = None
|
|
214
|
+
) -> StreamEvent:
|
|
215
|
+
"""Create a DECISION_EXTRACTED event."""
|
|
216
|
+
return StreamEvent(
|
|
217
|
+
type=StreamEventType.DECISION_EXTRACTED,
|
|
218
|
+
data=decision,
|
|
219
|
+
progress=progress
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def event_cache_hit(
|
|
224
|
+
hit_type: str,
|
|
225
|
+
messages_cached: int,
|
|
226
|
+
messages_to_process: int,
|
|
227
|
+
saved_time_ms: int
|
|
228
|
+
) -> StreamEvent:
|
|
229
|
+
"""Create a CACHE_HIT event."""
|
|
230
|
+
return StreamEvent(
|
|
231
|
+
type=StreamEventType.CACHE_HIT,
|
|
232
|
+
data=CacheHitInfo(
|
|
233
|
+
hit_type=hit_type,
|
|
234
|
+
messages_cached=messages_cached,
|
|
235
|
+
messages_to_process=messages_to_process,
|
|
236
|
+
saved_time_ms=saved_time_ms
|
|
237
|
+
)
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def event_error(message: str) -> StreamEvent:
|
|
242
|
+
"""Create an ERROR event."""
|
|
243
|
+
return StreamEvent(
|
|
244
|
+
type=StreamEventType.ERROR,
|
|
245
|
+
data=message
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def event_complete(result: Any) -> StreamEvent:
|
|
250
|
+
"""Create a COMPLETE event with final results."""
|
|
251
|
+
return StreamEvent(
|
|
252
|
+
type=StreamEventType.COMPLETE,
|
|
253
|
+
data=result
|
|
254
|
+
)
|