loom-agent 0.0.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.
Potentially problematic release.
This version of loom-agent might be problematic. Click here for more details.
- loom/__init__.py +77 -0
- loom/agent.py +217 -0
- loom/agents/__init__.py +10 -0
- loom/agents/refs.py +28 -0
- loom/agents/registry.py +50 -0
- loom/builtin/compression/__init__.py +4 -0
- loom/builtin/compression/structured.py +79 -0
- loom/builtin/embeddings/__init__.py +9 -0
- loom/builtin/embeddings/openai_embedding.py +135 -0
- loom/builtin/embeddings/sentence_transformers_embedding.py +145 -0
- loom/builtin/llms/__init__.py +8 -0
- loom/builtin/llms/mock.py +34 -0
- loom/builtin/llms/openai.py +168 -0
- loom/builtin/llms/rule.py +102 -0
- loom/builtin/memory/__init__.py +5 -0
- loom/builtin/memory/in_memory.py +21 -0
- loom/builtin/memory/persistent_memory.py +278 -0
- loom/builtin/retriever/__init__.py +9 -0
- loom/builtin/retriever/chroma_store.py +265 -0
- loom/builtin/retriever/in_memory.py +106 -0
- loom/builtin/retriever/milvus_store.py +307 -0
- loom/builtin/retriever/pinecone_store.py +237 -0
- loom/builtin/retriever/qdrant_store.py +274 -0
- loom/builtin/retriever/vector_store.py +128 -0
- loom/builtin/retriever/vector_store_config.py +217 -0
- loom/builtin/tools/__init__.py +32 -0
- loom/builtin/tools/calculator.py +49 -0
- loom/builtin/tools/document_search.py +111 -0
- loom/builtin/tools/glob.py +27 -0
- loom/builtin/tools/grep.py +56 -0
- loom/builtin/tools/http_request.py +86 -0
- loom/builtin/tools/python_repl.py +73 -0
- loom/builtin/tools/read_file.py +32 -0
- loom/builtin/tools/task.py +158 -0
- loom/builtin/tools/web_search.py +64 -0
- loom/builtin/tools/write_file.py +31 -0
- loom/callbacks/base.py +9 -0
- loom/callbacks/logging.py +12 -0
- loom/callbacks/metrics.py +27 -0
- loom/callbacks/observability.py +248 -0
- loom/components/agent.py +107 -0
- loom/core/agent_executor.py +450 -0
- loom/core/circuit_breaker.py +178 -0
- loom/core/compression_manager.py +329 -0
- loom/core/context_retriever.py +185 -0
- loom/core/error_classifier.py +193 -0
- loom/core/errors.py +66 -0
- loom/core/message_queue.py +167 -0
- loom/core/permission_store.py +62 -0
- loom/core/permissions.py +69 -0
- loom/core/scheduler.py +125 -0
- loom/core/steering_control.py +47 -0
- loom/core/structured_logger.py +279 -0
- loom/core/subagent_pool.py +232 -0
- loom/core/system_prompt.py +141 -0
- loom/core/system_reminders.py +283 -0
- loom/core/tool_pipeline.py +113 -0
- loom/core/types.py +269 -0
- loom/interfaces/compressor.py +59 -0
- loom/interfaces/embedding.py +51 -0
- loom/interfaces/llm.py +33 -0
- loom/interfaces/memory.py +29 -0
- loom/interfaces/retriever.py +179 -0
- loom/interfaces/tool.py +27 -0
- loom/interfaces/vector_store.py +80 -0
- loom/llm/__init__.py +14 -0
- loom/llm/config.py +228 -0
- loom/llm/factory.py +111 -0
- loom/llm/model_health.py +235 -0
- loom/llm/model_pool_advanced.py +305 -0
- loom/llm/pool.py +170 -0
- loom/llm/registry.py +201 -0
- loom/mcp/__init__.py +4 -0
- loom/mcp/client.py +86 -0
- loom/mcp/registry.py +58 -0
- loom/mcp/tool_adapter.py +48 -0
- loom/observability/__init__.py +5 -0
- loom/patterns/__init__.py +5 -0
- loom/patterns/multi_agent.py +123 -0
- loom/patterns/rag.py +262 -0
- loom/plugins/registry.py +55 -0
- loom/resilience/__init__.py +5 -0
- loom/tooling.py +72 -0
- loom/utils/agent_loader.py +218 -0
- loom/utils/token_counter.py +19 -0
- loom_agent-0.0.1.dist-info/METADATA +457 -0
- loom_agent-0.0.1.dist-info/RECORD +89 -0
- loom_agent-0.0.1.dist-info/WHEEL +4 -0
- loom_agent-0.0.1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Dict
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class PerformanceMetrics:
|
|
9
|
+
total_iterations: int = 0
|
|
10
|
+
llm_calls: int = 0
|
|
11
|
+
tool_calls: int = 0
|
|
12
|
+
total_errors: int = 0
|
|
13
|
+
extras: Dict[str, float] = field(default_factory=dict)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class MetricsCollector:
|
|
17
|
+
def __init__(self) -> None:
|
|
18
|
+
self.metrics = PerformanceMetrics()
|
|
19
|
+
|
|
20
|
+
def summary(self) -> Dict:
|
|
21
|
+
return {
|
|
22
|
+
"iterations": self.metrics.total_iterations,
|
|
23
|
+
"llm_calls": self.metrics.llm_calls,
|
|
24
|
+
"tool_calls": self.metrics.tool_calls,
|
|
25
|
+
"errors": self.metrics.total_errors,
|
|
26
|
+
}
|
|
27
|
+
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
"""US7: Observability Callbacks
|
|
2
|
+
|
|
3
|
+
Enhanced callbacks for production observability and monitoring.
|
|
4
|
+
|
|
5
|
+
New event types:
|
|
6
|
+
- compression_start: Before context compression
|
|
7
|
+
- compression_complete: After compression with metrics
|
|
8
|
+
- subagent_spawned: When sub-agent is created
|
|
9
|
+
- subagent_completed: When sub-agent finishes
|
|
10
|
+
- retry_attempt: When operation is retried
|
|
11
|
+
- circuit_breaker_opened: When circuit opens
|
|
12
|
+
- circuit_breaker_closed: When circuit closes
|
|
13
|
+
- performance_metric: General performance tracking
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from typing import Any, Dict, List, Optional
|
|
19
|
+
from datetime import datetime
|
|
20
|
+
|
|
21
|
+
from loom.callbacks.base import BaseCallback
|
|
22
|
+
from loom.core.structured_logger import StructuredLogger
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ObservabilityCallback(BaseCallback):
|
|
26
|
+
"""Callback for structured logging and observability.
|
|
27
|
+
|
|
28
|
+
Logs all agent events in JSON format for aggregation.
|
|
29
|
+
|
|
30
|
+
Example:
|
|
31
|
+
logger = StructuredLogger("my_agent")
|
|
32
|
+
callback = ObservabilityCallback(logger)
|
|
33
|
+
|
|
34
|
+
agent = Agent(llm=llm, callbacks=[callback])
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def __init__(
|
|
38
|
+
self,
|
|
39
|
+
logger: Optional[StructuredLogger] = None,
|
|
40
|
+
log_all_events: bool = True,
|
|
41
|
+
log_performance: bool = True,
|
|
42
|
+
):
|
|
43
|
+
"""Initialize observability callback.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
logger: StructuredLogger instance (creates one if None)
|
|
47
|
+
log_all_events: Log all events (default True)
|
|
48
|
+
log_performance: Log performance metrics separately (default True)
|
|
49
|
+
"""
|
|
50
|
+
if logger is None:
|
|
51
|
+
logger = StructuredLogger("loom.observability")
|
|
52
|
+
|
|
53
|
+
self.logger = logger
|
|
54
|
+
self.log_all_events = log_all_events
|
|
55
|
+
self.log_performance = log_performance
|
|
56
|
+
|
|
57
|
+
async def on_event(self, event_type: str, payload: Dict[str, Any]) -> None:
|
|
58
|
+
"""Handle agent events.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
event_type: Event type string
|
|
62
|
+
payload: Event payload
|
|
63
|
+
"""
|
|
64
|
+
# Extract correlation ID if available
|
|
65
|
+
correlation_id = payload.get("correlation_id")
|
|
66
|
+
if correlation_id and not self.logger.get_correlation_id():
|
|
67
|
+
self.logger.set_correlation_id(correlation_id)
|
|
68
|
+
|
|
69
|
+
# Log all events if enabled
|
|
70
|
+
if self.log_all_events:
|
|
71
|
+
self.logger.info(
|
|
72
|
+
f"Agent event: {event_type}",
|
|
73
|
+
event_type=event_type,
|
|
74
|
+
**payload
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
# Special handling for specific events
|
|
78
|
+
if event_type == "compression_start":
|
|
79
|
+
self._log_compression_start(payload)
|
|
80
|
+
elif event_type == "compression_complete":
|
|
81
|
+
self._log_compression_complete(payload)
|
|
82
|
+
elif event_type == "subagent_spawned":
|
|
83
|
+
self._log_subagent_spawned(payload)
|
|
84
|
+
elif event_type == "retry_attempt":
|
|
85
|
+
self._log_retry_attempt(payload)
|
|
86
|
+
elif event_type == "error":
|
|
87
|
+
self._log_error(payload)
|
|
88
|
+
elif event_type in ["llm_call", "tool_call"] and self.log_performance:
|
|
89
|
+
self._log_performance(event_type, payload)
|
|
90
|
+
|
|
91
|
+
def _log_compression_start(self, payload: Dict[str, Any]) -> None:
|
|
92
|
+
"""Log compression start event."""
|
|
93
|
+
self.logger.info(
|
|
94
|
+
"Context compression starting",
|
|
95
|
+
token_count=payload.get("token_count"),
|
|
96
|
+
message_count=payload.get("message_count"),
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
def _log_compression_complete(self, payload: Dict[str, Any]) -> None:
|
|
100
|
+
"""Log compression completion with metrics."""
|
|
101
|
+
before_tokens = payload.get("before_tokens", 0)
|
|
102
|
+
after_tokens = payload.get("after_tokens", 0)
|
|
103
|
+
ratio = payload.get("compression_ratio", 0)
|
|
104
|
+
|
|
105
|
+
self.logger.log_performance(
|
|
106
|
+
"context_compression",
|
|
107
|
+
duration_ms=payload.get("duration_ms", 0),
|
|
108
|
+
success=True,
|
|
109
|
+
before_tokens=before_tokens,
|
|
110
|
+
after_tokens=after_tokens,
|
|
111
|
+
compression_ratio=ratio,
|
|
112
|
+
tokens_saved=before_tokens - after_tokens,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
def _log_subagent_spawned(self, payload: Dict[str, Any]) -> None:
|
|
116
|
+
"""Log sub-agent spawn event."""
|
|
117
|
+
self.logger.info(
|
|
118
|
+
"Sub-agent spawned",
|
|
119
|
+
subagent_id=payload.get("subagent_id"),
|
|
120
|
+
execution_depth=payload.get("execution_depth"),
|
|
121
|
+
tool_whitelist=payload.get("tool_whitelist"),
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
def _log_retry_attempt(self, payload: Dict[str, Any]) -> None:
|
|
125
|
+
"""Log retry attempt."""
|
|
126
|
+
self.logger.warning(
|
|
127
|
+
"Retry attempt",
|
|
128
|
+
attempt=payload.get("attempt"),
|
|
129
|
+
max_attempts=payload.get("max_attempts"),
|
|
130
|
+
operation=payload.get("operation"),
|
|
131
|
+
error=payload.get("error"),
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
def _log_error(self, payload: Dict[str, Any]) -> None:
|
|
135
|
+
"""Log error with context."""
|
|
136
|
+
self.logger.error(
|
|
137
|
+
f"Agent error in {payload.get('stage', 'unknown')}",
|
|
138
|
+
stage=payload.get("stage"),
|
|
139
|
+
message=payload.get("message"),
|
|
140
|
+
iteration=payload.get("iteration"),
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
def _log_performance(self, operation: str, payload: Dict[str, Any]) -> None:
|
|
144
|
+
"""Log performance metric."""
|
|
145
|
+
duration_ms = payload.get("duration_ms", 0)
|
|
146
|
+
if duration_ms > 0:
|
|
147
|
+
self.logger.log_performance(
|
|
148
|
+
operation,
|
|
149
|
+
duration_ms,
|
|
150
|
+
success=True,
|
|
151
|
+
**payload
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class MetricsAggregator(BaseCallback):
|
|
156
|
+
"""Aggregates metrics for monitoring dashboards.
|
|
157
|
+
|
|
158
|
+
Example:
|
|
159
|
+
aggregator = MetricsAggregator()
|
|
160
|
+
agent = Agent(llm=llm, callbacks=[aggregator])
|
|
161
|
+
|
|
162
|
+
# After running agent
|
|
163
|
+
summary = aggregator.get_summary()
|
|
164
|
+
print(f"Total LLM calls: {summary['llm_calls']}")
|
|
165
|
+
print(f"Avg LLM latency: {summary['avg_llm_latency_ms']:.2f}ms")
|
|
166
|
+
"""
|
|
167
|
+
|
|
168
|
+
def __init__(self):
|
|
169
|
+
"""Initialize metrics aggregator."""
|
|
170
|
+
self.metrics = {
|
|
171
|
+
"llm_calls": 0,
|
|
172
|
+
"llm_total_ms": 0.0,
|
|
173
|
+
"tool_calls": 0,
|
|
174
|
+
"tool_total_ms": 0.0,
|
|
175
|
+
"compressions": 0,
|
|
176
|
+
"compression_total_ms": 0.0,
|
|
177
|
+
"errors": 0,
|
|
178
|
+
"subagents_spawned": 0,
|
|
179
|
+
"retry_attempts": 0,
|
|
180
|
+
}
|
|
181
|
+
self.start_time = datetime.now()
|
|
182
|
+
|
|
183
|
+
async def on_event(self, event_type: str, payload: Dict[str, Any]) -> None:
|
|
184
|
+
"""Aggregate metrics from events."""
|
|
185
|
+
if event_type == "llm_call":
|
|
186
|
+
self.metrics["llm_calls"] += 1
|
|
187
|
+
self.metrics["llm_total_ms"] += payload.get("duration_ms", 0)
|
|
188
|
+
|
|
189
|
+
elif event_type == "tool_call":
|
|
190
|
+
self.metrics["tool_calls"] += 1
|
|
191
|
+
self.metrics["tool_total_ms"] += payload.get("duration_ms", 0)
|
|
192
|
+
|
|
193
|
+
elif event_type == "compression_complete":
|
|
194
|
+
self.metrics["compressions"] += 1
|
|
195
|
+
self.metrics["compression_total_ms"] += payload.get("duration_ms", 0)
|
|
196
|
+
|
|
197
|
+
elif event_type == "error":
|
|
198
|
+
self.metrics["errors"] += 1
|
|
199
|
+
|
|
200
|
+
elif event_type == "subagent_spawned":
|
|
201
|
+
self.metrics["subagents_spawned"] += 1
|
|
202
|
+
|
|
203
|
+
elif event_type == "retry_attempt":
|
|
204
|
+
self.metrics["retry_attempts"] += 1
|
|
205
|
+
|
|
206
|
+
def get_summary(self) -> Dict[str, Any]:
|
|
207
|
+
"""Get aggregated metrics summary.
|
|
208
|
+
|
|
209
|
+
Returns:
|
|
210
|
+
Dict with summary statistics
|
|
211
|
+
"""
|
|
212
|
+
elapsed_seconds = (datetime.now() - self.start_time).total_seconds()
|
|
213
|
+
|
|
214
|
+
summary = {
|
|
215
|
+
**self.metrics,
|
|
216
|
+
"uptime_seconds": elapsed_seconds,
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
# Calculate averages
|
|
220
|
+
if self.metrics["llm_calls"] > 0:
|
|
221
|
+
summary["avg_llm_latency_ms"] = (
|
|
222
|
+
self.metrics["llm_total_ms"] / self.metrics["llm_calls"]
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
if self.metrics["tool_calls"] > 0:
|
|
226
|
+
summary["avg_tool_latency_ms"] = (
|
|
227
|
+
self.metrics["tool_total_ms"] / self.metrics["tool_calls"]
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
if self.metrics["compressions"] > 0:
|
|
231
|
+
summary["avg_compression_ms"] = (
|
|
232
|
+
self.metrics["compression_total_ms"] / self.metrics["compressions"]
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
# Calculate rates (per minute)
|
|
236
|
+
if elapsed_seconds > 0:
|
|
237
|
+
minutes = elapsed_seconds / 60
|
|
238
|
+
summary["llm_calls_per_minute"] = self.metrics["llm_calls"] / minutes
|
|
239
|
+
summary["errors_per_minute"] = self.metrics["errors"] / minutes
|
|
240
|
+
|
|
241
|
+
return summary
|
|
242
|
+
|
|
243
|
+
def reset(self) -> None:
|
|
244
|
+
"""Reset all metrics."""
|
|
245
|
+
for key in self.metrics:
|
|
246
|
+
if isinstance(self.metrics[key], (int, float)):
|
|
247
|
+
self.metrics[key] = 0
|
|
248
|
+
self.start_time = datetime.now()
|
loom/components/agent.py
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from typing import AsyncGenerator, Dict, List, Optional
|
|
5
|
+
|
|
6
|
+
from loom.core.agent_executor import AgentExecutor
|
|
7
|
+
from loom.core.types import StreamEvent
|
|
8
|
+
from loom.interfaces.llm import BaseLLM
|
|
9
|
+
from loom.interfaces.memory import BaseMemory
|
|
10
|
+
from loom.interfaces.tool import BaseTool
|
|
11
|
+
from loom.interfaces.compressor import BaseCompressor
|
|
12
|
+
from loom.callbacks.base import BaseCallback
|
|
13
|
+
from loom.callbacks.metrics import MetricsCollector
|
|
14
|
+
from loom.core.steering_control import SteeringControl
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Agent:
|
|
18
|
+
"""高层 Agent 组件:对外暴露 run/stream,内部委托 AgentExecutor。"""
|
|
19
|
+
|
|
20
|
+
def __init__(
|
|
21
|
+
self,
|
|
22
|
+
llm: BaseLLM,
|
|
23
|
+
tools: List[BaseTool] | None = None,
|
|
24
|
+
memory: Optional[BaseMemory] = None,
|
|
25
|
+
compressor: Optional[BaseCompressor] = None,
|
|
26
|
+
max_iterations: int = 50,
|
|
27
|
+
max_context_tokens: int = 16000,
|
|
28
|
+
permission_policy: Optional[Dict[str, str]] = None,
|
|
29
|
+
ask_handler=None,
|
|
30
|
+
safe_mode: bool = False,
|
|
31
|
+
permission_store=None,
|
|
32
|
+
# Advanced options
|
|
33
|
+
context_retriever=None,
|
|
34
|
+
system_instructions: Optional[str] = None,
|
|
35
|
+
callbacks: Optional[List[BaseCallback]] = None,
|
|
36
|
+
steering_control: Optional[SteeringControl] = None,
|
|
37
|
+
metrics: Optional[MetricsCollector] = None,
|
|
38
|
+
) -> None:
|
|
39
|
+
# v4.0.0: Auto-instantiate CompressionManager (always enabled)
|
|
40
|
+
if compressor is None:
|
|
41
|
+
from loom.core.compression_manager import CompressionManager
|
|
42
|
+
compressor = CompressionManager(
|
|
43
|
+
llm=llm,
|
|
44
|
+
max_retries=3,
|
|
45
|
+
compression_threshold=0.92,
|
|
46
|
+
target_reduction=0.75,
|
|
47
|
+
sliding_window_size=20,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
tools_map = {t.name: t for t in (tools or [])}
|
|
51
|
+
self.executor = AgentExecutor(
|
|
52
|
+
llm=llm,
|
|
53
|
+
tools=tools_map,
|
|
54
|
+
memory=memory,
|
|
55
|
+
compressor=compressor,
|
|
56
|
+
context_retriever=context_retriever,
|
|
57
|
+
steering_control=steering_control,
|
|
58
|
+
max_iterations=max_iterations,
|
|
59
|
+
max_context_tokens=max_context_tokens,
|
|
60
|
+
metrics=metrics,
|
|
61
|
+
permission_manager=None,
|
|
62
|
+
system_instructions=system_instructions,
|
|
63
|
+
callbacks=callbacks,
|
|
64
|
+
enable_steering=True, # v4.0.0: Always enabled
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# 始终构造 PermissionManager(以便支持 safe_mode/持久化);保持默认语义
|
|
68
|
+
from loom.core.permissions import PermissionManager
|
|
69
|
+
|
|
70
|
+
pm = PermissionManager(
|
|
71
|
+
policy=permission_policy or {},
|
|
72
|
+
default="allow", # 保持默认放行语义
|
|
73
|
+
ask_handler=ask_handler,
|
|
74
|
+
safe_mode=safe_mode,
|
|
75
|
+
permission_store=permission_store,
|
|
76
|
+
)
|
|
77
|
+
self.executor.permission_manager = pm
|
|
78
|
+
self.executor.tool_pipeline.permission_manager = pm
|
|
79
|
+
|
|
80
|
+
async def run(
|
|
81
|
+
self,
|
|
82
|
+
input: str,
|
|
83
|
+
cancel_token: Optional[asyncio.Event] = None, # 🆕 US1
|
|
84
|
+
correlation_id: Optional[str] = None, # 🆕 US1
|
|
85
|
+
) -> str:
|
|
86
|
+
return await self.executor.execute(input, cancel_token=cancel_token, correlation_id=correlation_id)
|
|
87
|
+
|
|
88
|
+
async def stream(self, input: str) -> AsyncGenerator[StreamEvent, None]:
|
|
89
|
+
async for ev in self.executor.stream(input):
|
|
90
|
+
yield ev
|
|
91
|
+
|
|
92
|
+
# LangChain 风格的别名,便于迁移/调用
|
|
93
|
+
async def ainvoke(
|
|
94
|
+
self,
|
|
95
|
+
input: str,
|
|
96
|
+
cancel_token: Optional[asyncio.Event] = None, # 🆕 US1
|
|
97
|
+
correlation_id: Optional[str] = None, # 🆕 US1
|
|
98
|
+
) -> str:
|
|
99
|
+
return await self.run(input, cancel_token=cancel_token, correlation_id=correlation_id)
|
|
100
|
+
|
|
101
|
+
async def astream(self, input: str) -> AsyncGenerator[StreamEvent, None]:
|
|
102
|
+
async for ev in self.stream(input):
|
|
103
|
+
yield ev
|
|
104
|
+
|
|
105
|
+
def get_metrics(self) -> Dict:
|
|
106
|
+
"""返回当前指标摘要。"""
|
|
107
|
+
return self.executor.metrics.summary()
|