daita-agents 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.
Files changed (69) hide show
  1. daita/__init__.py +216 -0
  2. daita/agents/__init__.py +33 -0
  3. daita/agents/base.py +743 -0
  4. daita/agents/substrate.py +1141 -0
  5. daita/cli/__init__.py +145 -0
  6. daita/cli/__main__.py +7 -0
  7. daita/cli/ascii_art.py +44 -0
  8. daita/cli/core/__init__.py +0 -0
  9. daita/cli/core/create.py +254 -0
  10. daita/cli/core/deploy.py +473 -0
  11. daita/cli/core/deployments.py +309 -0
  12. daita/cli/core/import_detector.py +219 -0
  13. daita/cli/core/init.py +481 -0
  14. daita/cli/core/logs.py +239 -0
  15. daita/cli/core/managed_deploy.py +709 -0
  16. daita/cli/core/run.py +648 -0
  17. daita/cli/core/status.py +421 -0
  18. daita/cli/core/test.py +239 -0
  19. daita/cli/core/webhooks.py +172 -0
  20. daita/cli/main.py +588 -0
  21. daita/cli/utils.py +541 -0
  22. daita/config/__init__.py +62 -0
  23. daita/config/base.py +159 -0
  24. daita/config/settings.py +184 -0
  25. daita/core/__init__.py +262 -0
  26. daita/core/decision_tracing.py +701 -0
  27. daita/core/exceptions.py +480 -0
  28. daita/core/focus.py +251 -0
  29. daita/core/interfaces.py +76 -0
  30. daita/core/plugin_tracing.py +550 -0
  31. daita/core/relay.py +779 -0
  32. daita/core/reliability.py +381 -0
  33. daita/core/scaling.py +459 -0
  34. daita/core/tools.py +554 -0
  35. daita/core/tracing.py +770 -0
  36. daita/core/workflow.py +1144 -0
  37. daita/display/__init__.py +1 -0
  38. daita/display/console.py +160 -0
  39. daita/execution/__init__.py +58 -0
  40. daita/execution/client.py +856 -0
  41. daita/execution/exceptions.py +92 -0
  42. daita/execution/models.py +317 -0
  43. daita/llm/__init__.py +60 -0
  44. daita/llm/anthropic.py +291 -0
  45. daita/llm/base.py +530 -0
  46. daita/llm/factory.py +101 -0
  47. daita/llm/gemini.py +355 -0
  48. daita/llm/grok.py +219 -0
  49. daita/llm/mock.py +172 -0
  50. daita/llm/openai.py +220 -0
  51. daita/plugins/__init__.py +141 -0
  52. daita/plugins/base.py +37 -0
  53. daita/plugins/base_db.py +167 -0
  54. daita/plugins/elasticsearch.py +849 -0
  55. daita/plugins/mcp.py +481 -0
  56. daita/plugins/mongodb.py +520 -0
  57. daita/plugins/mysql.py +362 -0
  58. daita/plugins/postgresql.py +342 -0
  59. daita/plugins/redis_messaging.py +500 -0
  60. daita/plugins/rest.py +537 -0
  61. daita/plugins/s3.py +770 -0
  62. daita/plugins/slack.py +729 -0
  63. daita/utils/__init__.py +18 -0
  64. daita_agents-0.2.0.dist-info/METADATA +409 -0
  65. daita_agents-0.2.0.dist-info/RECORD +69 -0
  66. daita_agents-0.2.0.dist-info/WHEEL +5 -0
  67. daita_agents-0.2.0.dist-info/entry_points.txt +2 -0
  68. daita_agents-0.2.0.dist-info/licenses/LICENSE +56 -0
  69. daita_agents-0.2.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
+ ]