mcp-hangar 0.2.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.
- mcp_hangar/__init__.py +139 -0
- mcp_hangar/application/__init__.py +1 -0
- mcp_hangar/application/commands/__init__.py +67 -0
- mcp_hangar/application/commands/auth_commands.py +118 -0
- mcp_hangar/application/commands/auth_handlers.py +296 -0
- mcp_hangar/application/commands/commands.py +59 -0
- mcp_hangar/application/commands/handlers.py +189 -0
- mcp_hangar/application/discovery/__init__.py +21 -0
- mcp_hangar/application/discovery/discovery_metrics.py +283 -0
- mcp_hangar/application/discovery/discovery_orchestrator.py +497 -0
- mcp_hangar/application/discovery/lifecycle_manager.py +315 -0
- mcp_hangar/application/discovery/security_validator.py +414 -0
- mcp_hangar/application/event_handlers/__init__.py +50 -0
- mcp_hangar/application/event_handlers/alert_handler.py +191 -0
- mcp_hangar/application/event_handlers/audit_handler.py +203 -0
- mcp_hangar/application/event_handlers/knowledge_base_handler.py +120 -0
- mcp_hangar/application/event_handlers/logging_handler.py +69 -0
- mcp_hangar/application/event_handlers/metrics_handler.py +152 -0
- mcp_hangar/application/event_handlers/persistent_audit_store.py +217 -0
- mcp_hangar/application/event_handlers/security_handler.py +604 -0
- mcp_hangar/application/mcp/tooling.py +158 -0
- mcp_hangar/application/ports/__init__.py +9 -0
- mcp_hangar/application/ports/observability.py +237 -0
- mcp_hangar/application/queries/__init__.py +52 -0
- mcp_hangar/application/queries/auth_handlers.py +237 -0
- mcp_hangar/application/queries/auth_queries.py +118 -0
- mcp_hangar/application/queries/handlers.py +227 -0
- mcp_hangar/application/read_models/__init__.py +11 -0
- mcp_hangar/application/read_models/provider_views.py +139 -0
- mcp_hangar/application/sagas/__init__.py +11 -0
- mcp_hangar/application/sagas/group_rebalance_saga.py +137 -0
- mcp_hangar/application/sagas/provider_failover_saga.py +266 -0
- mcp_hangar/application/sagas/provider_recovery_saga.py +172 -0
- mcp_hangar/application/services/__init__.py +9 -0
- mcp_hangar/application/services/provider_service.py +208 -0
- mcp_hangar/application/services/traced_provider_service.py +211 -0
- mcp_hangar/bootstrap/runtime.py +328 -0
- mcp_hangar/context.py +178 -0
- mcp_hangar/domain/__init__.py +117 -0
- mcp_hangar/domain/contracts/__init__.py +57 -0
- mcp_hangar/domain/contracts/authentication.py +225 -0
- mcp_hangar/domain/contracts/authorization.py +229 -0
- mcp_hangar/domain/contracts/event_store.py +178 -0
- mcp_hangar/domain/contracts/metrics_publisher.py +59 -0
- mcp_hangar/domain/contracts/persistence.py +383 -0
- mcp_hangar/domain/contracts/provider_runtime.py +146 -0
- mcp_hangar/domain/discovery/__init__.py +20 -0
- mcp_hangar/domain/discovery/conflict_resolver.py +267 -0
- mcp_hangar/domain/discovery/discovered_provider.py +185 -0
- mcp_hangar/domain/discovery/discovery_service.py +412 -0
- mcp_hangar/domain/discovery/discovery_source.py +192 -0
- mcp_hangar/domain/events.py +433 -0
- mcp_hangar/domain/exceptions.py +525 -0
- mcp_hangar/domain/model/__init__.py +70 -0
- mcp_hangar/domain/model/aggregate.py +58 -0
- mcp_hangar/domain/model/circuit_breaker.py +152 -0
- mcp_hangar/domain/model/event_sourced_api_key.py +413 -0
- mcp_hangar/domain/model/event_sourced_provider.py +423 -0
- mcp_hangar/domain/model/event_sourced_role_assignment.py +268 -0
- mcp_hangar/domain/model/health_tracker.py +183 -0
- mcp_hangar/domain/model/load_balancer.py +185 -0
- mcp_hangar/domain/model/provider.py +810 -0
- mcp_hangar/domain/model/provider_group.py +656 -0
- mcp_hangar/domain/model/tool_catalog.py +105 -0
- mcp_hangar/domain/policies/__init__.py +19 -0
- mcp_hangar/domain/policies/provider_health.py +187 -0
- mcp_hangar/domain/repository.py +249 -0
- mcp_hangar/domain/security/__init__.py +85 -0
- mcp_hangar/domain/security/input_validator.py +710 -0
- mcp_hangar/domain/security/rate_limiter.py +387 -0
- mcp_hangar/domain/security/roles.py +237 -0
- mcp_hangar/domain/security/sanitizer.py +387 -0
- mcp_hangar/domain/security/secrets.py +501 -0
- mcp_hangar/domain/services/__init__.py +20 -0
- mcp_hangar/domain/services/audit_service.py +376 -0
- mcp_hangar/domain/services/image_builder.py +328 -0
- mcp_hangar/domain/services/provider_launcher.py +1046 -0
- mcp_hangar/domain/value_objects.py +1138 -0
- mcp_hangar/errors.py +818 -0
- mcp_hangar/fastmcp_server.py +1105 -0
- mcp_hangar/gc.py +134 -0
- mcp_hangar/infrastructure/__init__.py +79 -0
- mcp_hangar/infrastructure/async_executor.py +133 -0
- mcp_hangar/infrastructure/auth/__init__.py +37 -0
- mcp_hangar/infrastructure/auth/api_key_authenticator.py +388 -0
- mcp_hangar/infrastructure/auth/event_sourced_store.py +567 -0
- mcp_hangar/infrastructure/auth/jwt_authenticator.py +360 -0
- mcp_hangar/infrastructure/auth/middleware.py +340 -0
- mcp_hangar/infrastructure/auth/opa_authorizer.py +243 -0
- mcp_hangar/infrastructure/auth/postgres_store.py +659 -0
- mcp_hangar/infrastructure/auth/projections.py +366 -0
- mcp_hangar/infrastructure/auth/rate_limiter.py +311 -0
- mcp_hangar/infrastructure/auth/rbac_authorizer.py +323 -0
- mcp_hangar/infrastructure/auth/sqlite_store.py +624 -0
- mcp_hangar/infrastructure/command_bus.py +112 -0
- mcp_hangar/infrastructure/discovery/__init__.py +110 -0
- mcp_hangar/infrastructure/discovery/docker_source.py +289 -0
- mcp_hangar/infrastructure/discovery/entrypoint_source.py +249 -0
- mcp_hangar/infrastructure/discovery/filesystem_source.py +383 -0
- mcp_hangar/infrastructure/discovery/kubernetes_source.py +247 -0
- mcp_hangar/infrastructure/event_bus.py +260 -0
- mcp_hangar/infrastructure/event_sourced_repository.py +443 -0
- mcp_hangar/infrastructure/event_store.py +396 -0
- mcp_hangar/infrastructure/knowledge_base/__init__.py +259 -0
- mcp_hangar/infrastructure/knowledge_base/contracts.py +202 -0
- mcp_hangar/infrastructure/knowledge_base/memory.py +177 -0
- mcp_hangar/infrastructure/knowledge_base/postgres.py +545 -0
- mcp_hangar/infrastructure/knowledge_base/sqlite.py +513 -0
- mcp_hangar/infrastructure/metrics_publisher.py +36 -0
- mcp_hangar/infrastructure/observability/__init__.py +10 -0
- mcp_hangar/infrastructure/observability/langfuse_adapter.py +534 -0
- mcp_hangar/infrastructure/persistence/__init__.py +33 -0
- mcp_hangar/infrastructure/persistence/audit_repository.py +371 -0
- mcp_hangar/infrastructure/persistence/config_repository.py +398 -0
- mcp_hangar/infrastructure/persistence/database.py +333 -0
- mcp_hangar/infrastructure/persistence/database_common.py +330 -0
- mcp_hangar/infrastructure/persistence/event_serializer.py +280 -0
- mcp_hangar/infrastructure/persistence/event_upcaster.py +166 -0
- mcp_hangar/infrastructure/persistence/in_memory_event_store.py +150 -0
- mcp_hangar/infrastructure/persistence/recovery_service.py +312 -0
- mcp_hangar/infrastructure/persistence/sqlite_event_store.py +386 -0
- mcp_hangar/infrastructure/persistence/unit_of_work.py +409 -0
- mcp_hangar/infrastructure/persistence/upcasters/README.md +13 -0
- mcp_hangar/infrastructure/persistence/upcasters/__init__.py +7 -0
- mcp_hangar/infrastructure/query_bus.py +153 -0
- mcp_hangar/infrastructure/saga_manager.py +401 -0
- mcp_hangar/logging_config.py +209 -0
- mcp_hangar/metrics.py +1007 -0
- mcp_hangar/models.py +31 -0
- mcp_hangar/observability/__init__.py +54 -0
- mcp_hangar/observability/health.py +487 -0
- mcp_hangar/observability/metrics.py +319 -0
- mcp_hangar/observability/tracing.py +433 -0
- mcp_hangar/progress.py +542 -0
- mcp_hangar/retry.py +613 -0
- mcp_hangar/server/__init__.py +120 -0
- mcp_hangar/server/__main__.py +6 -0
- mcp_hangar/server/auth_bootstrap.py +340 -0
- mcp_hangar/server/auth_cli.py +335 -0
- mcp_hangar/server/auth_config.py +305 -0
- mcp_hangar/server/bootstrap.py +735 -0
- mcp_hangar/server/cli.py +161 -0
- mcp_hangar/server/config.py +224 -0
- mcp_hangar/server/context.py +215 -0
- mcp_hangar/server/http_auth_middleware.py +165 -0
- mcp_hangar/server/lifecycle.py +467 -0
- mcp_hangar/server/state.py +117 -0
- mcp_hangar/server/tools/__init__.py +16 -0
- mcp_hangar/server/tools/discovery.py +186 -0
- mcp_hangar/server/tools/groups.py +75 -0
- mcp_hangar/server/tools/health.py +301 -0
- mcp_hangar/server/tools/provider.py +939 -0
- mcp_hangar/server/tools/registry.py +320 -0
- mcp_hangar/server/validation.py +113 -0
- mcp_hangar/stdio_client.py +229 -0
- mcp_hangar-0.2.0.dist-info/METADATA +347 -0
- mcp_hangar-0.2.0.dist-info/RECORD +160 -0
- mcp_hangar-0.2.0.dist-info/WHEEL +4 -0
- mcp_hangar-0.2.0.dist-info/entry_points.txt +2 -0
- mcp_hangar-0.2.0.dist-info/licenses/LICENSE +21 -0
mcp_hangar/progress.py
ADDED
|
@@ -0,0 +1,542 @@
|
|
|
1
|
+
"""Real-time operation feedback with progress events.
|
|
2
|
+
|
|
3
|
+
This module provides streaming progress updates for long-running
|
|
4
|
+
operations, giving users visibility into:
|
|
5
|
+
|
|
6
|
+
- Cold starts and provider launches
|
|
7
|
+
- Container initialization
|
|
8
|
+
- Tool discovery
|
|
9
|
+
- Network calls and execution
|
|
10
|
+
|
|
11
|
+
Usage with callback::
|
|
12
|
+
|
|
13
|
+
from mcp_hangar import ProgressTracker, ProgressStage
|
|
14
|
+
|
|
15
|
+
def on_progress(stage: str, message: str, elapsed_ms: float):
|
|
16
|
+
print(f"⏳ [{stage}] {message} ({elapsed_ms:.0f}ms)")
|
|
17
|
+
|
|
18
|
+
tracker = ProgressTracker(callback=on_progress)
|
|
19
|
+
tracker.report(ProgressStage.LAUNCHING, "Starting...")
|
|
20
|
+
tracker.complete(result)
|
|
21
|
+
|
|
22
|
+
See docs/guides/UX_IMPROVEMENTS.md for more examples.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
import asyncio
|
|
26
|
+
from dataclasses import dataclass, field
|
|
27
|
+
from enum import Enum
|
|
28
|
+
import threading
|
|
29
|
+
import time
|
|
30
|
+
from typing import Any, AsyncIterator, Callable, Dict, Iterator, List, Optional
|
|
31
|
+
|
|
32
|
+
from .logging_config import get_logger
|
|
33
|
+
|
|
34
|
+
logger = get_logger(__name__)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class ProgressStage(str, Enum):
|
|
38
|
+
"""Stages of operation progress."""
|
|
39
|
+
|
|
40
|
+
# Pre-execution stages
|
|
41
|
+
COLD_START = "cold_start"
|
|
42
|
+
LAUNCHING = "launching"
|
|
43
|
+
INITIALIZING = "initializing"
|
|
44
|
+
DISCOVERING_TOOLS = "discovering_tools"
|
|
45
|
+
CONNECTING = "connecting"
|
|
46
|
+
|
|
47
|
+
# Execution stages
|
|
48
|
+
READY = "ready"
|
|
49
|
+
EXECUTING = "executing"
|
|
50
|
+
PROCESSING = "processing"
|
|
51
|
+
|
|
52
|
+
# Completion stages
|
|
53
|
+
COMPLETE = "complete"
|
|
54
|
+
FAILED = "failed"
|
|
55
|
+
RETRYING = "retrying"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class EventType(str, Enum):
|
|
59
|
+
"""Types of events in the stream."""
|
|
60
|
+
|
|
61
|
+
PROGRESS = "progress"
|
|
62
|
+
RESULT = "result"
|
|
63
|
+
ERROR = "error"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass
|
|
67
|
+
class ProgressEvent:
|
|
68
|
+
"""A single progress update event.
|
|
69
|
+
|
|
70
|
+
Attributes:
|
|
71
|
+
type: Event type (progress, result, error)
|
|
72
|
+
stage: Current operation stage
|
|
73
|
+
message: Human-readable description
|
|
74
|
+
elapsed_ms: Time since operation started
|
|
75
|
+
details: Additional context data
|
|
76
|
+
timestamp: Event timestamp
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
type: EventType = EventType.PROGRESS
|
|
80
|
+
stage: ProgressStage = ProgressStage.EXECUTING
|
|
81
|
+
message: str = ""
|
|
82
|
+
elapsed_ms: float = 0.0
|
|
83
|
+
details: Dict[str, Any] = field(default_factory=dict)
|
|
84
|
+
timestamp: float = field(default_factory=time.time)
|
|
85
|
+
|
|
86
|
+
# For result/error events
|
|
87
|
+
data: Any = None
|
|
88
|
+
exception: Optional[Exception] = None
|
|
89
|
+
|
|
90
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
91
|
+
"""Convert to dictionary for serialization."""
|
|
92
|
+
result: Dict[str, Any] = {
|
|
93
|
+
"type": self.type.value,
|
|
94
|
+
"stage": self.stage.value,
|
|
95
|
+
"message": self.message,
|
|
96
|
+
"elapsed_ms": round(self.elapsed_ms, 2),
|
|
97
|
+
"timestamp": self.timestamp,
|
|
98
|
+
}
|
|
99
|
+
if self.details:
|
|
100
|
+
result["details"] = self.details
|
|
101
|
+
if self.data is not None:
|
|
102
|
+
result["data"] = self.data
|
|
103
|
+
if self.exception is not None:
|
|
104
|
+
result["error"] = str(self.exception)
|
|
105
|
+
return result
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
# Type alias for progress callback
|
|
109
|
+
ProgressCallback = Callable[[str, str, float], None]
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class ProgressTracker:
|
|
113
|
+
"""Tracks and reports progress for a single operation.
|
|
114
|
+
|
|
115
|
+
Can be used with callbacks or as a streaming iterator.
|
|
116
|
+
|
|
117
|
+
Usage with callback:
|
|
118
|
+
tracker = ProgressTracker(callback=my_callback)
|
|
119
|
+
tracker.report(ProgressStage.COLD_START, "Launching container...")
|
|
120
|
+
tracker.report(ProgressStage.READY, "Provider ready")
|
|
121
|
+
tracker.complete(result)
|
|
122
|
+
|
|
123
|
+
Usage as iterator:
|
|
124
|
+
tracker = ProgressTracker()
|
|
125
|
+
# In one thread/task:
|
|
126
|
+
tracker.report(ProgressStage.LAUNCHING, "Starting...")
|
|
127
|
+
tracker.complete(result)
|
|
128
|
+
# In another:
|
|
129
|
+
for event in tracker:
|
|
130
|
+
logger.info("progress_event", event=event.to_dict())
|
|
131
|
+
"""
|
|
132
|
+
|
|
133
|
+
def __init__(
|
|
134
|
+
self,
|
|
135
|
+
callback: Optional[ProgressCallback] = None,
|
|
136
|
+
provider: str = "",
|
|
137
|
+
operation: str = "",
|
|
138
|
+
):
|
|
139
|
+
"""Initialize progress tracker.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
callback: Optional callback for progress updates
|
|
143
|
+
provider: Provider name for context
|
|
144
|
+
operation: Operation name for context
|
|
145
|
+
"""
|
|
146
|
+
self._callback = callback
|
|
147
|
+
self._provider = provider
|
|
148
|
+
self._operation = operation
|
|
149
|
+
self._start_time = time.time()
|
|
150
|
+
self._events: List[ProgressEvent] = []
|
|
151
|
+
self._completed = False
|
|
152
|
+
self._result: Any = None
|
|
153
|
+
self._error: Optional[Exception] = None
|
|
154
|
+
|
|
155
|
+
# For iterator support
|
|
156
|
+
self._event_queue: asyncio.Queue = asyncio.Queue()
|
|
157
|
+
self._sync_queue: List[ProgressEvent] = []
|
|
158
|
+
self._lock = threading.Lock()
|
|
159
|
+
self._done = threading.Event()
|
|
160
|
+
|
|
161
|
+
@property
|
|
162
|
+
def elapsed_ms(self) -> float:
|
|
163
|
+
"""Time elapsed since operation started."""
|
|
164
|
+
return (time.time() - self._start_time) * 1000
|
|
165
|
+
|
|
166
|
+
def report(
|
|
167
|
+
self,
|
|
168
|
+
stage: ProgressStage,
|
|
169
|
+
message: str,
|
|
170
|
+
details: Optional[Dict[str, Any]] = None,
|
|
171
|
+
) -> None:
|
|
172
|
+
"""Report a progress update.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
stage: Current progress stage
|
|
176
|
+
message: Human-readable description
|
|
177
|
+
details: Additional context
|
|
178
|
+
"""
|
|
179
|
+
elapsed = self.elapsed_ms
|
|
180
|
+
event = ProgressEvent(
|
|
181
|
+
type=EventType.PROGRESS,
|
|
182
|
+
stage=stage,
|
|
183
|
+
message=message,
|
|
184
|
+
elapsed_ms=elapsed,
|
|
185
|
+
details=details or {},
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
with self._lock:
|
|
189
|
+
self._events.append(event)
|
|
190
|
+
self._sync_queue.append(event)
|
|
191
|
+
|
|
192
|
+
# Try to put in async queue (non-blocking)
|
|
193
|
+
try:
|
|
194
|
+
self._event_queue.put_nowait(event)
|
|
195
|
+
except asyncio.QueueFull:
|
|
196
|
+
pass
|
|
197
|
+
|
|
198
|
+
# Invoke callback if set
|
|
199
|
+
if self._callback:
|
|
200
|
+
try:
|
|
201
|
+
self._callback(stage.value, message, elapsed)
|
|
202
|
+
except Exception as e:
|
|
203
|
+
logger.debug("progress_callback_error", error=str(e))
|
|
204
|
+
|
|
205
|
+
logger.debug(
|
|
206
|
+
"progress_reported",
|
|
207
|
+
provider=self._provider,
|
|
208
|
+
operation=self._operation,
|
|
209
|
+
stage=stage.value,
|
|
210
|
+
message=message,
|
|
211
|
+
elapsed_ms=elapsed,
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
def complete(self, result: Any) -> None:
|
|
215
|
+
"""Mark operation as complete with result.
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
result: The operation result
|
|
219
|
+
"""
|
|
220
|
+
elapsed = self.elapsed_ms
|
|
221
|
+
event = ProgressEvent(
|
|
222
|
+
type=EventType.RESULT,
|
|
223
|
+
stage=ProgressStage.COMPLETE,
|
|
224
|
+
message=f"Operation completed (total: {elapsed:.0f}ms)",
|
|
225
|
+
elapsed_ms=elapsed,
|
|
226
|
+
data=result,
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
with self._lock:
|
|
230
|
+
self._events.append(event)
|
|
231
|
+
self._sync_queue.append(event)
|
|
232
|
+
self._completed = True
|
|
233
|
+
self._result = result
|
|
234
|
+
|
|
235
|
+
try:
|
|
236
|
+
self._event_queue.put_nowait(event)
|
|
237
|
+
except asyncio.QueueFull:
|
|
238
|
+
pass
|
|
239
|
+
|
|
240
|
+
self._done.set()
|
|
241
|
+
|
|
242
|
+
if self._callback:
|
|
243
|
+
try:
|
|
244
|
+
self._callback(ProgressStage.COMPLETE.value, event.message, elapsed)
|
|
245
|
+
except Exception:
|
|
246
|
+
pass
|
|
247
|
+
|
|
248
|
+
logger.debug(
|
|
249
|
+
"progress_complete",
|
|
250
|
+
provider=self._provider,
|
|
251
|
+
operation=self._operation,
|
|
252
|
+
elapsed_ms=elapsed,
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
def fail(self, error: Exception) -> None:
|
|
256
|
+
"""Mark operation as failed with error.
|
|
257
|
+
|
|
258
|
+
Args:
|
|
259
|
+
error: The exception that caused failure
|
|
260
|
+
"""
|
|
261
|
+
elapsed = self.elapsed_ms
|
|
262
|
+
event = ProgressEvent(
|
|
263
|
+
type=EventType.ERROR,
|
|
264
|
+
stage=ProgressStage.FAILED,
|
|
265
|
+
message=f"Operation failed: {str(error)[:100]}",
|
|
266
|
+
elapsed_ms=elapsed,
|
|
267
|
+
exception=error,
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
with self._lock:
|
|
271
|
+
self._events.append(event)
|
|
272
|
+
self._sync_queue.append(event)
|
|
273
|
+
self._completed = True
|
|
274
|
+
self._error = error
|
|
275
|
+
|
|
276
|
+
try:
|
|
277
|
+
self._event_queue.put_nowait(event)
|
|
278
|
+
except asyncio.QueueFull:
|
|
279
|
+
pass
|
|
280
|
+
|
|
281
|
+
self._done.set()
|
|
282
|
+
|
|
283
|
+
if self._callback:
|
|
284
|
+
try:
|
|
285
|
+
self._callback(ProgressStage.FAILED.value, event.message, elapsed)
|
|
286
|
+
except (TypeError, ValueError, RuntimeError) as e:
|
|
287
|
+
logger.debug("progress_callback_error", stage="failed", error=str(e))
|
|
288
|
+
|
|
289
|
+
logger.debug(
|
|
290
|
+
"progress_failed",
|
|
291
|
+
provider=self._provider,
|
|
292
|
+
operation=self._operation,
|
|
293
|
+
error=str(error)[:200],
|
|
294
|
+
elapsed_ms=elapsed,
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
def __iter__(self) -> Iterator[ProgressEvent]:
|
|
298
|
+
"""Iterate over events synchronously."""
|
|
299
|
+
while True:
|
|
300
|
+
with self._lock:
|
|
301
|
+
if self._sync_queue:
|
|
302
|
+
event = self._sync_queue.pop(0)
|
|
303
|
+
yield event
|
|
304
|
+
if event.type in (EventType.RESULT, EventType.ERROR):
|
|
305
|
+
return
|
|
306
|
+
elif self._completed:
|
|
307
|
+
return
|
|
308
|
+
|
|
309
|
+
# Wait a bit before checking again
|
|
310
|
+
if not self._done.wait(timeout=0.01):
|
|
311
|
+
continue
|
|
312
|
+
|
|
313
|
+
async def __aiter__(self) -> AsyncIterator[ProgressEvent]:
|
|
314
|
+
"""Iterate over events asynchronously."""
|
|
315
|
+
while True:
|
|
316
|
+
try:
|
|
317
|
+
event = await asyncio.wait_for(
|
|
318
|
+
self._event_queue.get(),
|
|
319
|
+
timeout=0.1,
|
|
320
|
+
)
|
|
321
|
+
yield event
|
|
322
|
+
if event.type in (EventType.RESULT, EventType.ERROR):
|
|
323
|
+
return
|
|
324
|
+
except asyncio.TimeoutError:
|
|
325
|
+
if self._completed:
|
|
326
|
+
return
|
|
327
|
+
|
|
328
|
+
def get_all_events(self) -> List[ProgressEvent]:
|
|
329
|
+
"""Get all recorded events."""
|
|
330
|
+
with self._lock:
|
|
331
|
+
return list(self._events)
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
# =============================================================================
|
|
335
|
+
# Progress-Aware Operation Wrapper
|
|
336
|
+
# =============================================================================
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
class ProgressOperation:
|
|
340
|
+
"""Context manager for progress-tracked operations.
|
|
341
|
+
|
|
342
|
+
Usage:
|
|
343
|
+
async with ProgressOperation("math", "add", callback=my_cb) as progress:
|
|
344
|
+
progress.report(ProgressStage.LAUNCHING, "Starting provider...")
|
|
345
|
+
result = await do_work()
|
|
346
|
+
progress.complete(result)
|
|
347
|
+
"""
|
|
348
|
+
|
|
349
|
+
def __init__(
|
|
350
|
+
self,
|
|
351
|
+
provider: str,
|
|
352
|
+
operation: str,
|
|
353
|
+
callback: Optional[ProgressCallback] = None,
|
|
354
|
+
):
|
|
355
|
+
self.provider = provider
|
|
356
|
+
self.operation = operation
|
|
357
|
+
self.tracker = ProgressTracker(
|
|
358
|
+
callback=callback,
|
|
359
|
+
provider=provider,
|
|
360
|
+
operation=operation,
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
def __enter__(self) -> ProgressTracker:
|
|
364
|
+
return self.tracker
|
|
365
|
+
|
|
366
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
367
|
+
if exc_val is not None and not self.tracker._completed:
|
|
368
|
+
self.tracker.fail(exc_val)
|
|
369
|
+
return False
|
|
370
|
+
|
|
371
|
+
async def __aenter__(self) -> ProgressTracker:
|
|
372
|
+
return self.tracker
|
|
373
|
+
|
|
374
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
375
|
+
if exc_val is not None and not self.tracker._completed:
|
|
376
|
+
self.tracker.fail(exc_val)
|
|
377
|
+
return False
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
# =============================================================================
|
|
381
|
+
# Standard Progress Messages
|
|
382
|
+
# =============================================================================
|
|
383
|
+
|
|
384
|
+
# Pre-defined messages for common operations
|
|
385
|
+
PROGRESS_MESSAGES = {
|
|
386
|
+
ProgressStage.COLD_START: "Provider is cold, launching...",
|
|
387
|
+
ProgressStage.LAUNCHING: "Starting {mode} provider...",
|
|
388
|
+
ProgressStage.INITIALIZING: "Container started (PID: {pid}), initializing...",
|
|
389
|
+
ProgressStage.DISCOVERING_TOOLS: "Discovering available tools...",
|
|
390
|
+
ProgressStage.CONNECTING: "Connecting to provider...",
|
|
391
|
+
ProgressStage.READY: "Provider ready, executing request...",
|
|
392
|
+
ProgressStage.EXECUTING: "Calling tool '{tool}'...",
|
|
393
|
+
ProgressStage.PROCESSING: "Processing response...",
|
|
394
|
+
ProgressStage.COMPLETE: "Operation completed (total: {elapsed_ms:.0f}ms)",
|
|
395
|
+
ProgressStage.FAILED: "Operation failed: {error}",
|
|
396
|
+
ProgressStage.RETRYING: "Retrying (attempt {attempt}/{max_attempts})...",
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def get_stage_message(
|
|
401
|
+
stage: ProgressStage,
|
|
402
|
+
**kwargs,
|
|
403
|
+
) -> str:
|
|
404
|
+
"""Get formatted message for a progress stage.
|
|
405
|
+
|
|
406
|
+
Args:
|
|
407
|
+
stage: The progress stage
|
|
408
|
+
**kwargs: Format arguments for message template
|
|
409
|
+
|
|
410
|
+
Returns:
|
|
411
|
+
Formatted progress message
|
|
412
|
+
"""
|
|
413
|
+
template = PROGRESS_MESSAGES.get(stage, stage.value)
|
|
414
|
+
try:
|
|
415
|
+
return template.format(**kwargs)
|
|
416
|
+
except KeyError:
|
|
417
|
+
return template
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
# =============================================================================
|
|
421
|
+
# Event Bus Integration
|
|
422
|
+
# =============================================================================
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
class ProgressEventHandler:
|
|
426
|
+
"""Event handler that forwards domain events to progress tracker.
|
|
427
|
+
|
|
428
|
+
This bridges the gap between domain events and progress reporting,
|
|
429
|
+
allowing existing code to emit progress updates without modification.
|
|
430
|
+
"""
|
|
431
|
+
|
|
432
|
+
# Map domain event types to progress stages
|
|
433
|
+
EVENT_STAGE_MAP = {
|
|
434
|
+
"ProviderStarted": ProgressStage.READY,
|
|
435
|
+
"ProviderStopped": ProgressStage.COMPLETE,
|
|
436
|
+
"ProviderStateChanged": ProgressStage.INITIALIZING,
|
|
437
|
+
"ToolInvocationRequested": ProgressStage.EXECUTING,
|
|
438
|
+
"ToolInvocationCompleted": ProgressStage.COMPLETE,
|
|
439
|
+
"ToolInvocationFailed": ProgressStage.FAILED,
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
def __init__(self):
|
|
443
|
+
self._trackers: Dict[str, ProgressTracker] = {}
|
|
444
|
+
self._lock = threading.Lock()
|
|
445
|
+
|
|
446
|
+
def register_tracker(self, correlation_id: str, tracker: ProgressTracker) -> None:
|
|
447
|
+
"""Register a tracker for a correlation ID."""
|
|
448
|
+
with self._lock:
|
|
449
|
+
self._trackers[correlation_id] = tracker
|
|
450
|
+
|
|
451
|
+
def unregister_tracker(self, correlation_id: str) -> None:
|
|
452
|
+
"""Unregister a tracker."""
|
|
453
|
+
with self._lock:
|
|
454
|
+
self._trackers.pop(correlation_id, None)
|
|
455
|
+
|
|
456
|
+
def handle(self, event: Any) -> None:
|
|
457
|
+
"""Handle a domain event and forward to appropriate tracker."""
|
|
458
|
+
event_type = type(event).__name__
|
|
459
|
+
stage = self.EVENT_STAGE_MAP.get(event_type)
|
|
460
|
+
|
|
461
|
+
if not stage:
|
|
462
|
+
return
|
|
463
|
+
|
|
464
|
+
# Try to find correlation ID
|
|
465
|
+
correlation_id = getattr(event, "correlation_id", None)
|
|
466
|
+
if not correlation_id:
|
|
467
|
+
return
|
|
468
|
+
|
|
469
|
+
with self._lock:
|
|
470
|
+
tracker = self._trackers.get(correlation_id)
|
|
471
|
+
|
|
472
|
+
if tracker:
|
|
473
|
+
message = self._format_event_message(event, event_type)
|
|
474
|
+
tracker.report(stage, message, self._extract_details(event))
|
|
475
|
+
|
|
476
|
+
def _format_event_message(self, event: Any, event_type: str) -> str:
|
|
477
|
+
"""Format event into progress message."""
|
|
478
|
+
if event_type == "ProviderStarted":
|
|
479
|
+
return f"Provider started ({getattr(event, 'tools_count', 0)} tools)"
|
|
480
|
+
elif event_type == "ProviderStateChanged":
|
|
481
|
+
new_state = getattr(event, "new_state", "unknown")
|
|
482
|
+
return f"Provider state: {new_state}"
|
|
483
|
+
elif event_type == "ToolInvocationRequested":
|
|
484
|
+
tool = getattr(event, "tool_name", "unknown")
|
|
485
|
+
return f"Invoking tool: {tool}"
|
|
486
|
+
elif event_type == "ToolInvocationCompleted":
|
|
487
|
+
duration = getattr(event, "duration_ms", 0)
|
|
488
|
+
return f"Tool completed ({duration:.0f}ms)"
|
|
489
|
+
elif event_type == "ToolInvocationFailed":
|
|
490
|
+
error = getattr(event, "error_message", "unknown error")
|
|
491
|
+
return f"Tool failed: {error[:50]}"
|
|
492
|
+
return event_type
|
|
493
|
+
|
|
494
|
+
def _extract_details(self, event: Any) -> Dict[str, Any]:
|
|
495
|
+
"""Extract relevant details from event."""
|
|
496
|
+
details = {}
|
|
497
|
+
for attr in ["provider_id", "tool_name", "duration_ms", "error_type"]:
|
|
498
|
+
if hasattr(event, attr):
|
|
499
|
+
details[attr] = getattr(event, attr)
|
|
500
|
+
return details
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
# Global progress event handler instance
|
|
504
|
+
_progress_handler: Optional[ProgressEventHandler] = None
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
def get_progress_handler() -> ProgressEventHandler:
|
|
508
|
+
"""Get or create the global progress event handler."""
|
|
509
|
+
global _progress_handler
|
|
510
|
+
if _progress_handler is None:
|
|
511
|
+
_progress_handler = ProgressEventHandler()
|
|
512
|
+
return _progress_handler
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
def create_progress_tracker(
|
|
516
|
+
provider: str = "",
|
|
517
|
+
operation: str = "",
|
|
518
|
+
callback: Optional[ProgressCallback] = None,
|
|
519
|
+
correlation_id: Optional[str] = None,
|
|
520
|
+
) -> ProgressTracker:
|
|
521
|
+
"""Create a progress tracker with optional event bus integration.
|
|
522
|
+
|
|
523
|
+
Args:
|
|
524
|
+
provider: Provider name
|
|
525
|
+
operation: Operation name
|
|
526
|
+
callback: Optional progress callback
|
|
527
|
+
correlation_id: Optional correlation ID for event bus integration
|
|
528
|
+
|
|
529
|
+
Returns:
|
|
530
|
+
New ProgressTracker instance
|
|
531
|
+
"""
|
|
532
|
+
tracker = ProgressTracker(
|
|
533
|
+
callback=callback,
|
|
534
|
+
provider=provider,
|
|
535
|
+
operation=operation,
|
|
536
|
+
)
|
|
537
|
+
|
|
538
|
+
if correlation_id:
|
|
539
|
+
handler = get_progress_handler()
|
|
540
|
+
handler.register_tracker(correlation_id, tracker)
|
|
541
|
+
|
|
542
|
+
return tracker
|