daita-agents 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.
Potentially problematic release.
This version of daita-agents might be problematic. Click here for more details.
- daita/__init__.py +208 -0
- daita/agents/__init__.py +33 -0
- daita/agents/base.py +722 -0
- daita/agents/substrate.py +895 -0
- daita/cli/__init__.py +145 -0
- daita/cli/__main__.py +7 -0
- daita/cli/ascii_art.py +44 -0
- daita/cli/core/__init__.py +0 -0
- daita/cli/core/create.py +254 -0
- daita/cli/core/deploy.py +473 -0
- daita/cli/core/deployments.py +309 -0
- daita/cli/core/import_detector.py +219 -0
- daita/cli/core/init.py +382 -0
- daita/cli/core/logs.py +239 -0
- daita/cli/core/managed_deploy.py +709 -0
- daita/cli/core/run.py +648 -0
- daita/cli/core/status.py +421 -0
- daita/cli/core/test.py +239 -0
- daita/cli/core/webhooks.py +172 -0
- daita/cli/main.py +588 -0
- daita/cli/utils.py +541 -0
- daita/config/__init__.py +62 -0
- daita/config/base.py +159 -0
- daita/config/settings.py +184 -0
- daita/core/__init__.py +262 -0
- daita/core/decision_tracing.py +701 -0
- daita/core/exceptions.py +480 -0
- daita/core/focus.py +251 -0
- daita/core/interfaces.py +76 -0
- daita/core/plugin_tracing.py +550 -0
- daita/core/relay.py +695 -0
- daita/core/reliability.py +381 -0
- daita/core/scaling.py +444 -0
- daita/core/tools.py +402 -0
- daita/core/tracing.py +770 -0
- daita/core/workflow.py +1084 -0
- daita/display/__init__.py +1 -0
- daita/display/console.py +160 -0
- daita/execution/__init__.py +58 -0
- daita/execution/client.py +856 -0
- daita/execution/exceptions.py +92 -0
- daita/execution/models.py +317 -0
- daita/llm/__init__.py +60 -0
- daita/llm/anthropic.py +166 -0
- daita/llm/base.py +373 -0
- daita/llm/factory.py +101 -0
- daita/llm/gemini.py +152 -0
- daita/llm/grok.py +114 -0
- daita/llm/mock.py +135 -0
- daita/llm/openai.py +109 -0
- daita/plugins/__init__.py +141 -0
- daita/plugins/base.py +37 -0
- daita/plugins/base_db.py +167 -0
- daita/plugins/elasticsearch.py +844 -0
- daita/plugins/mcp.py +481 -0
- daita/plugins/mongodb.py +510 -0
- daita/plugins/mysql.py +351 -0
- daita/plugins/postgresql.py +331 -0
- daita/plugins/redis_messaging.py +500 -0
- daita/plugins/rest.py +529 -0
- daita/plugins/s3.py +761 -0
- daita/plugins/slack.py +729 -0
- daita/utils/__init__.py +18 -0
- daita_agents-0.1.0.dist-info/METADATA +350 -0
- daita_agents-0.1.0.dist-info/RECORD +69 -0
- daita_agents-0.1.0.dist-info/WHEEL +5 -0
- daita_agents-0.1.0.dist-info/entry_points.txt +2 -0
- daita_agents-0.1.0.dist-info/licenses/LICENSE +56 -0
- daita_agents-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,701 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Agent Decision Tracing Utilities for Daita Agents - Fixed Complete Version
|
|
3
|
+
|
|
4
|
+
Provides simple tools for agents to trace their decision-making process,
|
|
5
|
+
reasoning chains, and confidence scores. Integrates seamlessly with the
|
|
6
|
+
TraceManager for automatic decision observability.
|
|
7
|
+
|
|
8
|
+
FIXED ISSUES:
|
|
9
|
+
- Completed the trace_decision decorator implementation
|
|
10
|
+
- Fixed async context manager issues
|
|
11
|
+
- Added missing helper functions
|
|
12
|
+
- Improved error handling
|
|
13
|
+
- Fixed circular import issues
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import asyncio
|
|
17
|
+
import logging
|
|
18
|
+
import time
|
|
19
|
+
import functools
|
|
20
|
+
from typing import Dict, Any, Optional, List, Union, Callable, Tuple
|
|
21
|
+
from contextlib import asynccontextmanager
|
|
22
|
+
from dataclasses import dataclass
|
|
23
|
+
from enum import Enum
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
class DecisionEventType(str, Enum):
|
|
28
|
+
"""Types of decision events that can be streamed in real-time."""
|
|
29
|
+
DECISION_STARTED = "decision_started"
|
|
30
|
+
CONFIDENCE_UPDATED = "confidence_updated"
|
|
31
|
+
REASONING_ADDED = "reasoning_added"
|
|
32
|
+
ALTERNATIVE_ADDED = "alternative_added"
|
|
33
|
+
FACTOR_SET = "factor_set"
|
|
34
|
+
DECISION_COMPLETED = "decision_completed"
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class DecisionEvent:
|
|
38
|
+
"""Real-time decision event for streaming."""
|
|
39
|
+
event_type: DecisionEventType
|
|
40
|
+
decision_point: str
|
|
41
|
+
span_id: Optional[str]
|
|
42
|
+
timestamp: float
|
|
43
|
+
data: Dict[str, Any]
|
|
44
|
+
|
|
45
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
46
|
+
"""Convert to dictionary for serialization."""
|
|
47
|
+
return {
|
|
48
|
+
"event_type": self.event_type.value,
|
|
49
|
+
"decision_point": self.decision_point,
|
|
50
|
+
"span_id": self.span_id,
|
|
51
|
+
"timestamp": self.timestamp,
|
|
52
|
+
"data": self.data
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
class DecisionType(str, Enum):
|
|
56
|
+
"""Common types of agent decisions."""
|
|
57
|
+
CLASSIFICATION = "classification"
|
|
58
|
+
ANALYSIS = "analysis"
|
|
59
|
+
RECOMMENDATION = "recommendation"
|
|
60
|
+
ROUTING = "routing"
|
|
61
|
+
VALIDATION = "validation"
|
|
62
|
+
SELECTION = "selection"
|
|
63
|
+
PRIORITIZATION = "prioritization"
|
|
64
|
+
CUSTOM = "custom"
|
|
65
|
+
|
|
66
|
+
@dataclass
|
|
67
|
+
class DecisionContext:
|
|
68
|
+
"""Simple container for decision context and metadata."""
|
|
69
|
+
decision_point: str
|
|
70
|
+
decision_type: DecisionType
|
|
71
|
+
confidence_score: float = 0.0
|
|
72
|
+
reasoning_chain: List[str] = None
|
|
73
|
+
alternatives: List[str] = None
|
|
74
|
+
factors: Dict[str, Any] = None
|
|
75
|
+
|
|
76
|
+
def __post_init__(self):
|
|
77
|
+
if self.reasoning_chain is None:
|
|
78
|
+
self.reasoning_chain = []
|
|
79
|
+
if self.alternatives is None:
|
|
80
|
+
self.alternatives = []
|
|
81
|
+
if self.factors is None:
|
|
82
|
+
self.factors = {}
|
|
83
|
+
|
|
84
|
+
class DecisionRecorder:
|
|
85
|
+
"""
|
|
86
|
+
Context manager for recording agent decisions.
|
|
87
|
+
|
|
88
|
+
Provides a simple interface for agents to record their decision-making
|
|
89
|
+
process without needing to understand the underlying tracing system.
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
def __init__(self, decision_point: str, decision_type: Union[str, DecisionType] = DecisionType.CUSTOM, agent_id: Optional[str] = None, stream_callback: Optional[Callable[[DecisionEvent], None]] = None):
|
|
93
|
+
self.decision_point = decision_point
|
|
94
|
+
self.decision_type = DecisionType(decision_type) if isinstance(decision_type, str) else decision_type
|
|
95
|
+
self.agent_id = agent_id
|
|
96
|
+
self.span_id = None
|
|
97
|
+
self.context = DecisionContext(decision_point, self.decision_type)
|
|
98
|
+
self.stream_callback = stream_callback
|
|
99
|
+
|
|
100
|
+
# Import here to avoid circular imports
|
|
101
|
+
from .tracing import get_trace_manager
|
|
102
|
+
self.trace_manager = get_trace_manager()
|
|
103
|
+
|
|
104
|
+
def _emit_event(self, event_type: DecisionEventType, data: Dict[str, Any]):
|
|
105
|
+
"""Emit a real-time decision event if callback is provided."""
|
|
106
|
+
event = DecisionEvent(
|
|
107
|
+
event_type=event_type,
|
|
108
|
+
decision_point=self.decision_point,
|
|
109
|
+
span_id=self.span_id,
|
|
110
|
+
timestamp=time.time(),
|
|
111
|
+
data=data
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
# Emit to local callback if provided
|
|
115
|
+
if self.stream_callback:
|
|
116
|
+
try:
|
|
117
|
+
self.stream_callback(event)
|
|
118
|
+
except Exception as e:
|
|
119
|
+
logger.debug(f"Error calling local decision stream callback: {e}")
|
|
120
|
+
|
|
121
|
+
# Also emit to TraceManager for centralized streaming
|
|
122
|
+
if self.agent_id:
|
|
123
|
+
try:
|
|
124
|
+
self.trace_manager.emit_decision_event(self.agent_id, event)
|
|
125
|
+
except Exception as e:
|
|
126
|
+
logger.debug(f"Error emitting decision event to TraceManager: {e}")
|
|
127
|
+
|
|
128
|
+
async def __aenter__(self):
|
|
129
|
+
"""Start decision tracing."""
|
|
130
|
+
try:
|
|
131
|
+
# Start the decision span
|
|
132
|
+
self.span_id = self.trace_manager.start_span(
|
|
133
|
+
operation_name=f"decision_{self.decision_point}",
|
|
134
|
+
trace_type="decision_trace", # Use string to avoid enum import issues
|
|
135
|
+
agent_id=self.agent_id,
|
|
136
|
+
decision_point=self.decision_point,
|
|
137
|
+
decision_type=self.decision_type.value
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
logger.debug(f"Started decision trace: {self.decision_point}")
|
|
141
|
+
|
|
142
|
+
# Emit decision started event
|
|
143
|
+
self._emit_event(DecisionEventType.DECISION_STARTED, {
|
|
144
|
+
"decision_type": self.decision_type.value,
|
|
145
|
+
"agent_id": self.agent_id
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
return self
|
|
149
|
+
|
|
150
|
+
except Exception as e:
|
|
151
|
+
logger.error(f"Failed to start decision trace: {e}")
|
|
152
|
+
# Return self anyway so calling code doesn't break
|
|
153
|
+
return self
|
|
154
|
+
|
|
155
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
156
|
+
"""End decision tracing with recorded context."""
|
|
157
|
+
try:
|
|
158
|
+
if self.span_id:
|
|
159
|
+
# Record the decision details
|
|
160
|
+
self.trace_manager.record_decision(
|
|
161
|
+
span_id=self.span_id,
|
|
162
|
+
confidence=self.context.confidence_score,
|
|
163
|
+
reasoning=self.context.reasoning_chain,
|
|
164
|
+
alternatives=self.context.alternatives,
|
|
165
|
+
**self.context.factors
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
# End the span
|
|
169
|
+
from .tracing import TraceStatus
|
|
170
|
+
status = TraceStatus.ERROR if exc_type else TraceStatus.SUCCESS
|
|
171
|
+
error_message = str(exc_val) if exc_val else None
|
|
172
|
+
|
|
173
|
+
self.trace_manager.end_span(
|
|
174
|
+
span_id=self.span_id,
|
|
175
|
+
status=status,
|
|
176
|
+
error_message=error_message,
|
|
177
|
+
output_data=self.get_summary()
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
logger.debug(f"Ended decision trace: {self.decision_point} (confidence: {self.context.confidence_score:.2f})")
|
|
181
|
+
|
|
182
|
+
# Emit decision completed event
|
|
183
|
+
self._emit_event(DecisionEventType.DECISION_COMPLETED, {
|
|
184
|
+
"final_confidence": self.context.confidence_score,
|
|
185
|
+
"total_reasoning_steps": len(self.context.reasoning_chain),
|
|
186
|
+
"total_alternatives": len(self.context.alternatives),
|
|
187
|
+
"status": status.value if hasattr(status, 'value') else str(status),
|
|
188
|
+
"success": exc_type is None
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
except Exception as e:
|
|
192
|
+
logger.error(f"Failed to end decision trace: {e}")
|
|
193
|
+
|
|
194
|
+
def set_confidence(self, confidence: float):
|
|
195
|
+
"""Set confidence score (0.0 to 1.0)."""
|
|
196
|
+
old_confidence = self.context.confidence_score
|
|
197
|
+
self.context.confidence_score = max(0.0, min(1.0, confidence))
|
|
198
|
+
|
|
199
|
+
# Emit confidence updated event
|
|
200
|
+
self._emit_event(DecisionEventType.CONFIDENCE_UPDATED, {
|
|
201
|
+
"old_confidence": old_confidence,
|
|
202
|
+
"new_confidence": self.context.confidence_score,
|
|
203
|
+
"confidence_change": self.context.confidence_score - old_confidence
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
def add_reasoning(self, reasoning: str):
|
|
207
|
+
"""Add a reasoning step to the decision chain."""
|
|
208
|
+
self.context.reasoning_chain.append(reasoning)
|
|
209
|
+
|
|
210
|
+
# Emit reasoning added event
|
|
211
|
+
self._emit_event(DecisionEventType.REASONING_ADDED, {
|
|
212
|
+
"reasoning": reasoning,
|
|
213
|
+
"step_number": len(self.context.reasoning_chain),
|
|
214
|
+
"total_steps": len(self.context.reasoning_chain)
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
def add_alternative(self, alternative: str):
|
|
218
|
+
"""Add an alternative option that was considered."""
|
|
219
|
+
self.context.alternatives.append(alternative)
|
|
220
|
+
|
|
221
|
+
# Emit alternative added event
|
|
222
|
+
self._emit_event(DecisionEventType.ALTERNATIVE_ADDED, {
|
|
223
|
+
"alternative": alternative,
|
|
224
|
+
"total_alternatives": len(self.context.alternatives)
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
def set_factor(self, key: str, value: Any):
|
|
228
|
+
"""Set a decision factor (e.g., data_quality: 0.9)."""
|
|
229
|
+
self.context.factors[key] = value
|
|
230
|
+
|
|
231
|
+
# Emit factor set event
|
|
232
|
+
self._emit_event(DecisionEventType.FACTOR_SET, {
|
|
233
|
+
"factor_key": key,
|
|
234
|
+
"factor_value": value,
|
|
235
|
+
"total_factors": len(self.context.factors)
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
def get_summary(self) -> Dict[str, Any]:
|
|
239
|
+
"""Get a summary of the decision for logging or return."""
|
|
240
|
+
return {
|
|
241
|
+
"decision_point": self.decision_point,
|
|
242
|
+
"decision_type": self.decision_type.value,
|
|
243
|
+
"confidence": self.context.confidence_score,
|
|
244
|
+
"reasoning_steps": len(self.context.reasoning_chain),
|
|
245
|
+
"alternatives_considered": len(self.context.alternatives),
|
|
246
|
+
"factors": list(self.context.factors.keys())
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
# Context manager factory
|
|
250
|
+
@asynccontextmanager
|
|
251
|
+
async def record_decision_point(
|
|
252
|
+
decision_point: str,
|
|
253
|
+
decision_type: Union[str, DecisionType] = DecisionType.CUSTOM,
|
|
254
|
+
agent_id: Optional[str] = None,
|
|
255
|
+
stream_callback: Optional[Callable[[DecisionEvent], None]] = None
|
|
256
|
+
):
|
|
257
|
+
"""
|
|
258
|
+
Context manager for recording a decision point.
|
|
259
|
+
|
|
260
|
+
Usage:
|
|
261
|
+
async with record_decision_point("data_classification") as decision:
|
|
262
|
+
result = classify(data)
|
|
263
|
+
decision.set_confidence(0.85)
|
|
264
|
+
decision.add_reasoning("Pattern match found")
|
|
265
|
+
return result
|
|
266
|
+
"""
|
|
267
|
+
recorder = DecisionRecorder(decision_point, decision_type, agent_id, stream_callback)
|
|
268
|
+
async with recorder as decision:
|
|
269
|
+
yield decision
|
|
270
|
+
|
|
271
|
+
# Complete decorator for automatic decision tracing
|
|
272
|
+
def trace_decision(
|
|
273
|
+
decision_point: str,
|
|
274
|
+
decision_type: Union[str, DecisionType] = DecisionType.CUSTOM,
|
|
275
|
+
extract_confidence: bool = True,
|
|
276
|
+
extract_reasoning: bool = True
|
|
277
|
+
):
|
|
278
|
+
"""
|
|
279
|
+
Decorator for automatic decision tracing.
|
|
280
|
+
|
|
281
|
+
The decorated function can return:
|
|
282
|
+
1. Just the result
|
|
283
|
+
2. (result, confidence)
|
|
284
|
+
3. (result, confidence, reasoning_list)
|
|
285
|
+
4. (result, {"confidence": X, "reasoning": [...], "alternatives": [...]})
|
|
286
|
+
|
|
287
|
+
Usage:
|
|
288
|
+
@trace_decision("classification", DecisionType.CLASSIFICATION)
|
|
289
|
+
async def classify_data(self, data):
|
|
290
|
+
# Your logic here
|
|
291
|
+
result = {"class": "positive"}
|
|
292
|
+
confidence = 0.85
|
|
293
|
+
reasoning = ["Pattern A detected", "Threshold met"]
|
|
294
|
+
return result, confidence, reasoning
|
|
295
|
+
"""
|
|
296
|
+
def decorator(func: Callable):
|
|
297
|
+
if asyncio.iscoroutinefunction(func):
|
|
298
|
+
@functools.wraps(func)
|
|
299
|
+
async def async_wrapper(*args, **kwargs):
|
|
300
|
+
# Extract agent_id from self if available
|
|
301
|
+
agent_id = None
|
|
302
|
+
if args and hasattr(args[0], 'agent_id'):
|
|
303
|
+
agent_id = args[0].agent_id
|
|
304
|
+
|
|
305
|
+
async with record_decision_point(decision_point, decision_type, agent_id) as decision:
|
|
306
|
+
try:
|
|
307
|
+
result = await func(*args, **kwargs)
|
|
308
|
+
|
|
309
|
+
# Extract decision metadata from result
|
|
310
|
+
confidence, reasoning, alternatives = _extract_decision_metadata(
|
|
311
|
+
result, extract_confidence, extract_reasoning
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
if confidence is not None:
|
|
315
|
+
decision.set_confidence(confidence)
|
|
316
|
+
|
|
317
|
+
if reasoning:
|
|
318
|
+
for reason in reasoning:
|
|
319
|
+
decision.add_reasoning(reason)
|
|
320
|
+
|
|
321
|
+
if alternatives:
|
|
322
|
+
for alt in alternatives:
|
|
323
|
+
decision.add_alternative(alt)
|
|
324
|
+
|
|
325
|
+
# Return just the main result (strip metadata)
|
|
326
|
+
if isinstance(result, tuple) and len(result) > 1:
|
|
327
|
+
return result[0]
|
|
328
|
+
return result
|
|
329
|
+
|
|
330
|
+
except Exception as e:
|
|
331
|
+
logger.error(f"Decision function {func.__name__} failed: {e}")
|
|
332
|
+
raise
|
|
333
|
+
|
|
334
|
+
return async_wrapper
|
|
335
|
+
else:
|
|
336
|
+
@functools.wraps(func)
|
|
337
|
+
def sync_wrapper(*args, **kwargs):
|
|
338
|
+
# For sync functions, use asyncio.run if needed
|
|
339
|
+
async def async_exec():
|
|
340
|
+
agent_id = None
|
|
341
|
+
if args and hasattr(args[0], 'agent_id'):
|
|
342
|
+
agent_id = args[0].agent_id
|
|
343
|
+
|
|
344
|
+
async with record_decision_point(decision_point, decision_type, agent_id) as decision:
|
|
345
|
+
try:
|
|
346
|
+
result = func(*args, **kwargs)
|
|
347
|
+
|
|
348
|
+
confidence, reasoning, alternatives = _extract_decision_metadata(
|
|
349
|
+
result, extract_confidence, extract_reasoning
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
if confidence is not None:
|
|
353
|
+
decision.set_confidence(confidence)
|
|
354
|
+
|
|
355
|
+
if reasoning:
|
|
356
|
+
for reason in reasoning:
|
|
357
|
+
decision.add_reasoning(reason)
|
|
358
|
+
|
|
359
|
+
if alternatives:
|
|
360
|
+
for alt in alternatives:
|
|
361
|
+
decision.add_alternative(alt)
|
|
362
|
+
|
|
363
|
+
if isinstance(result, tuple) and len(result) > 1:
|
|
364
|
+
return result[0]
|
|
365
|
+
return result
|
|
366
|
+
|
|
367
|
+
except Exception as e:
|
|
368
|
+
logger.error(f"Decision function {func.__name__} failed: {e}")
|
|
369
|
+
raise
|
|
370
|
+
|
|
371
|
+
try:
|
|
372
|
+
# Try to get current event loop
|
|
373
|
+
loop = asyncio.get_event_loop()
|
|
374
|
+
if loop.is_running():
|
|
375
|
+
# We're already in an async context
|
|
376
|
+
task = asyncio.create_task(async_exec())
|
|
377
|
+
task.add_done_callback(lambda t: t.exception() if not t.cancelled() else None)
|
|
378
|
+
return task
|
|
379
|
+
else:
|
|
380
|
+
return loop.run_until_complete(async_exec())
|
|
381
|
+
except RuntimeError:
|
|
382
|
+
# No event loop, create one
|
|
383
|
+
return asyncio.run(async_exec())
|
|
384
|
+
|
|
385
|
+
return sync_wrapper
|
|
386
|
+
|
|
387
|
+
return decorator
|
|
388
|
+
|
|
389
|
+
def _extract_decision_metadata(result, extract_confidence: bool, extract_reasoning: bool) -> Tuple[Optional[float], List[str], List[str]]:
|
|
390
|
+
"""Extract confidence, reasoning, and alternatives from function result."""
|
|
391
|
+
confidence = None
|
|
392
|
+
reasoning = []
|
|
393
|
+
alternatives = []
|
|
394
|
+
|
|
395
|
+
try:
|
|
396
|
+
if isinstance(result, tuple):
|
|
397
|
+
if len(result) >= 2 and extract_confidence:
|
|
398
|
+
# (result, confidence) or (result, confidence, reasoning)
|
|
399
|
+
conf_value = result[1]
|
|
400
|
+
if isinstance(conf_value, (int, float)):
|
|
401
|
+
confidence = float(conf_value)
|
|
402
|
+
elif isinstance(conf_value, dict):
|
|
403
|
+
# (result, metadata_dict)
|
|
404
|
+
confidence = conf_value.get('confidence')
|
|
405
|
+
reasoning = conf_value.get('reasoning', [])
|
|
406
|
+
alternatives = conf_value.get('alternatives', [])
|
|
407
|
+
|
|
408
|
+
if len(result) >= 3 and extract_reasoning:
|
|
409
|
+
# (result, confidence, reasoning)
|
|
410
|
+
reasoning_value = result[2]
|
|
411
|
+
if isinstance(reasoning_value, list):
|
|
412
|
+
reasoning = reasoning_value
|
|
413
|
+
elif isinstance(reasoning_value, str):
|
|
414
|
+
reasoning = [reasoning_value]
|
|
415
|
+
|
|
416
|
+
if len(result) >= 4:
|
|
417
|
+
# (result, confidence, reasoning, alternatives)
|
|
418
|
+
alt_value = result[3]
|
|
419
|
+
if isinstance(alt_value, list):
|
|
420
|
+
alternatives = alt_value
|
|
421
|
+
|
|
422
|
+
elif isinstance(result, dict) and 'confidence' in result:
|
|
423
|
+
# Result is a dict with metadata
|
|
424
|
+
confidence = result.get('confidence')
|
|
425
|
+
reasoning = result.get('reasoning', [])
|
|
426
|
+
alternatives = result.get('alternatives', [])
|
|
427
|
+
|
|
428
|
+
except Exception as e:
|
|
429
|
+
logger.debug(f"Error extracting decision metadata: {e}")
|
|
430
|
+
|
|
431
|
+
return confidence, reasoning, alternatives
|
|
432
|
+
|
|
433
|
+
# Helper functions for common decision patterns
|
|
434
|
+
|
|
435
|
+
async def record_classification_decision(
|
|
436
|
+
decision_point: str,
|
|
437
|
+
classification_result: str,
|
|
438
|
+
confidence: float,
|
|
439
|
+
features_used: List[str],
|
|
440
|
+
alternatives_considered: Optional[List[str]] = None,
|
|
441
|
+
feature_weights: Optional[Dict[str, float]] = None,
|
|
442
|
+
agent_id: Optional[str] = None,
|
|
443
|
+
stream_callback: Optional[Callable[[DecisionEvent], None]] = None
|
|
444
|
+
) -> Dict[str, Any]:
|
|
445
|
+
"""
|
|
446
|
+
Helper for recording classification decisions.
|
|
447
|
+
|
|
448
|
+
Usage:
|
|
449
|
+
result = await record_classification_decision(
|
|
450
|
+
"sentiment_analysis",
|
|
451
|
+
classification_result="positive",
|
|
452
|
+
confidence=0.87,
|
|
453
|
+
features_used=["word_sentiment", "context_analysis"],
|
|
454
|
+
alternatives_considered=["neutral", "negative"],
|
|
455
|
+
feature_weights={"word_sentiment": 0.6, "context_analysis": 0.4}
|
|
456
|
+
)
|
|
457
|
+
"""
|
|
458
|
+
async with record_decision_point(decision_point, DecisionType.CLASSIFICATION, agent_id, stream_callback) as decision:
|
|
459
|
+
decision.set_confidence(confidence)
|
|
460
|
+
|
|
461
|
+
for feature in features_used:
|
|
462
|
+
decision.add_reasoning(f"Feature used: {feature}")
|
|
463
|
+
|
|
464
|
+
if alternatives_considered:
|
|
465
|
+
for alt in alternatives_considered:
|
|
466
|
+
decision.add_alternative(alt)
|
|
467
|
+
|
|
468
|
+
if feature_weights:
|
|
469
|
+
for feature, weight in feature_weights.items():
|
|
470
|
+
decision.set_factor(f"weight_{feature}", weight)
|
|
471
|
+
|
|
472
|
+
return {
|
|
473
|
+
"classification": classification_result,
|
|
474
|
+
"confidence": confidence,
|
|
475
|
+
"decision_summary": decision.get_summary()
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
async def record_analysis_decision(
|
|
479
|
+
decision_point: str,
|
|
480
|
+
analysis_result: Dict[str, Any],
|
|
481
|
+
confidence: float,
|
|
482
|
+
key_insights: List[str],
|
|
483
|
+
data_quality_factors: Optional[Dict[str, float]] = None,
|
|
484
|
+
agent_id: Optional[str] = None,
|
|
485
|
+
stream_callback: Optional[Callable[[DecisionEvent], None]] = None
|
|
486
|
+
) -> Dict[str, Any]:
|
|
487
|
+
"""
|
|
488
|
+
Helper for recording analysis decisions.
|
|
489
|
+
|
|
490
|
+
Usage:
|
|
491
|
+
result = await record_analysis_decision(
|
|
492
|
+
"financial_analysis",
|
|
493
|
+
analysis_result={"trend": "upward", "volatility": "low"},
|
|
494
|
+
confidence=0.91,
|
|
495
|
+
key_insights=["Strong growth pattern", "Low risk indicators"],
|
|
496
|
+
data_quality_factors={"completeness": 0.95, "accuracy": 0.88}
|
|
497
|
+
)
|
|
498
|
+
"""
|
|
499
|
+
async with record_decision_point(decision_point, DecisionType.ANALYSIS, agent_id, stream_callback) as decision:
|
|
500
|
+
decision.set_confidence(confidence)
|
|
501
|
+
|
|
502
|
+
for insight in key_insights:
|
|
503
|
+
decision.add_reasoning(insight)
|
|
504
|
+
|
|
505
|
+
if data_quality_factors:
|
|
506
|
+
for factor, value in data_quality_factors.items():
|
|
507
|
+
decision.set_factor(factor, value)
|
|
508
|
+
|
|
509
|
+
return {
|
|
510
|
+
"analysis": analysis_result,
|
|
511
|
+
"confidence": confidence,
|
|
512
|
+
"decision_summary": decision.get_summary()
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
async def record_recommendation_decision(
|
|
516
|
+
decision_point: str,
|
|
517
|
+
recommendation: str,
|
|
518
|
+
confidence: float,
|
|
519
|
+
rationale: List[str],
|
|
520
|
+
alternatives_considered: Optional[List[str]] = None,
|
|
521
|
+
risk_factors: Optional[Dict[str, str]] = None,
|
|
522
|
+
agent_id: Optional[str] = None,
|
|
523
|
+
stream_callback: Optional[Callable[[DecisionEvent], None]] = None
|
|
524
|
+
) -> Dict[str, Any]:
|
|
525
|
+
"""
|
|
526
|
+
Helper for recording recommendation decisions.
|
|
527
|
+
|
|
528
|
+
Usage:
|
|
529
|
+
result = await record_recommendation_decision(
|
|
530
|
+
"investment_advice",
|
|
531
|
+
recommendation="buy",
|
|
532
|
+
confidence=0.78,
|
|
533
|
+
rationale=["Strong fundamentals", "Market conditions favorable"],
|
|
534
|
+
alternatives_considered=["hold", "sell"],
|
|
535
|
+
risk_factors={"market_volatility": "medium", "liquidity": "high"}
|
|
536
|
+
)
|
|
537
|
+
"""
|
|
538
|
+
async with record_decision_point(decision_point, DecisionType.RECOMMENDATION, agent_id, stream_callback) as decision:
|
|
539
|
+
decision.set_confidence(confidence)
|
|
540
|
+
|
|
541
|
+
for reason in rationale:
|
|
542
|
+
decision.add_reasoning(reason)
|
|
543
|
+
|
|
544
|
+
if alternatives_considered:
|
|
545
|
+
for alt in alternatives_considered:
|
|
546
|
+
decision.add_alternative(alt)
|
|
547
|
+
|
|
548
|
+
if risk_factors:
|
|
549
|
+
for risk, level in risk_factors.items():
|
|
550
|
+
decision.set_factor(f"risk_{risk}", level)
|
|
551
|
+
|
|
552
|
+
return {
|
|
553
|
+
"recommendation": recommendation,
|
|
554
|
+
"confidence": confidence,
|
|
555
|
+
"decision_summary": decision.get_summary()
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
# Utility functions for decision analysis
|
|
559
|
+
|
|
560
|
+
def get_recent_decisions(agent_id: Optional[str] = None, decision_type: Optional[str] = None, limit: int = 20) -> List[Dict[str, Any]]:
|
|
561
|
+
"""Get recent decision traces."""
|
|
562
|
+
try:
|
|
563
|
+
from .tracing import get_trace_manager
|
|
564
|
+
trace_manager = get_trace_manager()
|
|
565
|
+
operations = trace_manager.get_recent_operations(agent_id=agent_id, limit=limit * 2)
|
|
566
|
+
|
|
567
|
+
# Filter for decision traces
|
|
568
|
+
decisions = [
|
|
569
|
+
op for op in operations
|
|
570
|
+
if op.get('type') == 'decision_trace'
|
|
571
|
+
]
|
|
572
|
+
|
|
573
|
+
# Filter by decision type if specified
|
|
574
|
+
if decision_type:
|
|
575
|
+
decisions = [
|
|
576
|
+
op for op in decisions
|
|
577
|
+
if op.get('metadata', {}).get('decision_type') == decision_type
|
|
578
|
+
]
|
|
579
|
+
|
|
580
|
+
return decisions[:limit]
|
|
581
|
+
except Exception as e:
|
|
582
|
+
logger.error(f"Error getting recent decisions: {e}")
|
|
583
|
+
return []
|
|
584
|
+
|
|
585
|
+
def register_agent_decision_stream(agent_id: str, callback: Callable[[DecisionEvent], None]) -> None:
|
|
586
|
+
"""
|
|
587
|
+
Register a callback to receive streaming decision events for a specific agent.
|
|
588
|
+
|
|
589
|
+
This provides centralized decision event streaming through the TraceManager.
|
|
590
|
+
|
|
591
|
+
Usage:
|
|
592
|
+
def my_callback(event: DecisionEvent):
|
|
593
|
+
print(f"Decision event: {event.event_type} - {event.data}")
|
|
594
|
+
|
|
595
|
+
register_agent_decision_stream("my-agent-id", my_callback)
|
|
596
|
+
"""
|
|
597
|
+
try:
|
|
598
|
+
from .tracing import get_trace_manager
|
|
599
|
+
trace_manager = get_trace_manager()
|
|
600
|
+
trace_manager.register_decision_stream_callback(agent_id, callback)
|
|
601
|
+
logger.info(f"Registered decision stream for agent {agent_id}")
|
|
602
|
+
except Exception as e:
|
|
603
|
+
logger.error(f"Failed to register agent decision stream: {e}")
|
|
604
|
+
|
|
605
|
+
def unregister_agent_decision_stream(agent_id: str, callback: Callable[[DecisionEvent], None]) -> None:
|
|
606
|
+
"""
|
|
607
|
+
Unregister a decision stream callback for a specific agent.
|
|
608
|
+
|
|
609
|
+
Usage:
|
|
610
|
+
unregister_agent_decision_stream("my-agent-id", my_callback)
|
|
611
|
+
"""
|
|
612
|
+
try:
|
|
613
|
+
from .tracing import get_trace_manager
|
|
614
|
+
trace_manager = get_trace_manager()
|
|
615
|
+
trace_manager.unregister_decision_stream_callback(agent_id, callback)
|
|
616
|
+
logger.info(f"Unregistered decision stream for agent {agent_id}")
|
|
617
|
+
except Exception as e:
|
|
618
|
+
logger.error(f"Failed to unregister agent decision stream: {e}")
|
|
619
|
+
|
|
620
|
+
def get_streaming_agents() -> List[str]:
|
|
621
|
+
"""Get list of agents that have decision streaming enabled."""
|
|
622
|
+
try:
|
|
623
|
+
from .tracing import get_trace_manager
|
|
624
|
+
trace_manager = get_trace_manager()
|
|
625
|
+
return trace_manager.get_streaming_agents()
|
|
626
|
+
except Exception as e:
|
|
627
|
+
logger.error(f"Failed to get streaming agents: {e}")
|
|
628
|
+
return []
|
|
629
|
+
|
|
630
|
+
def get_decision_stats(agent_id: Optional[str] = None, decision_type: Optional[str] = None) -> Dict[str, Any]:
|
|
631
|
+
"""Get decision statistics for analysis."""
|
|
632
|
+
try:
|
|
633
|
+
decisions = get_recent_decisions(agent_id, decision_type, limit=50)
|
|
634
|
+
|
|
635
|
+
if not decisions:
|
|
636
|
+
return {"total_decisions": 0, "average_confidence": 0}
|
|
637
|
+
|
|
638
|
+
# Calculate statistics
|
|
639
|
+
total_decisions = len(decisions)
|
|
640
|
+
successful_decisions = len([d for d in decisions if d.get('status') == 'success'])
|
|
641
|
+
|
|
642
|
+
# Extract confidence scores
|
|
643
|
+
confidences = []
|
|
644
|
+
for decision in decisions:
|
|
645
|
+
confidence = decision.get('metadata', {}).get('confidence_score')
|
|
646
|
+
if confidence is not None:
|
|
647
|
+
confidences.append(confidence)
|
|
648
|
+
|
|
649
|
+
avg_confidence = sum(confidences) / len(confidences) if confidences else 0
|
|
650
|
+
|
|
651
|
+
# Decision type distribution
|
|
652
|
+
type_counts = {}
|
|
653
|
+
for decision in decisions:
|
|
654
|
+
dec_type = decision.get('metadata', {}).get('decision_type', 'unknown')
|
|
655
|
+
type_counts[dec_type] = type_counts.get(dec_type, 0) + 1
|
|
656
|
+
|
|
657
|
+
return {
|
|
658
|
+
"total_decisions": total_decisions,
|
|
659
|
+
"successful_decisions": successful_decisions,
|
|
660
|
+
"success_rate": successful_decisions / total_decisions if total_decisions > 0 else 0,
|
|
661
|
+
"average_confidence": avg_confidence,
|
|
662
|
+
"confidence_count": len(confidences),
|
|
663
|
+
"decision_types": type_counts,
|
|
664
|
+
"agent_id": agent_id,
|
|
665
|
+
"filter_type": decision_type
|
|
666
|
+
}
|
|
667
|
+
except Exception as e:
|
|
668
|
+
logger.error(f"Error getting decision stats: {e}")
|
|
669
|
+
return {"total_decisions": 0, "average_confidence": 0}
|
|
670
|
+
|
|
671
|
+
# Export everything
|
|
672
|
+
__all__ = [
|
|
673
|
+
# Enums
|
|
674
|
+
"DecisionType",
|
|
675
|
+
"DecisionEventType",
|
|
676
|
+
|
|
677
|
+
# Event classes
|
|
678
|
+
"DecisionEvent",
|
|
679
|
+
|
|
680
|
+
# Main interfaces
|
|
681
|
+
"record_decision_point",
|
|
682
|
+
"trace_decision",
|
|
683
|
+
|
|
684
|
+
# Helper functions
|
|
685
|
+
"record_classification_decision",
|
|
686
|
+
"record_analysis_decision",
|
|
687
|
+
"record_recommendation_decision",
|
|
688
|
+
|
|
689
|
+
# Analysis functions
|
|
690
|
+
"get_recent_decisions",
|
|
691
|
+
"get_decision_stats",
|
|
692
|
+
|
|
693
|
+
# Streaming functions
|
|
694
|
+
"register_agent_decision_stream",
|
|
695
|
+
"unregister_agent_decision_stream",
|
|
696
|
+
"get_streaming_agents",
|
|
697
|
+
|
|
698
|
+
# Classes (for advanced usage)
|
|
699
|
+
"DecisionRecorder",
|
|
700
|
+
"DecisionContext"
|
|
701
|
+
]
|