revenium-python-sdk 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.
Files changed (73) hide show
  1. revenium_middleware/__init__.py +184 -0
  2. revenium_middleware/_core/__init__.py +65 -0
  3. revenium_middleware/_core/config.py +165 -0
  4. revenium_middleware/_core/context.py +109 -0
  5. revenium_middleware/_core/decorators.py +202 -0
  6. revenium_middleware/_core/metering.py +207 -0
  7. revenium_middleware/_core/prompt_extraction.py +55 -0
  8. revenium_middleware/_core/subscriber.py +51 -0
  9. revenium_middleware/_core/trace_fields.py +265 -0
  10. revenium_middleware/anthropic/__init__.py +108 -0
  11. revenium_middleware/anthropic/bedrock_adapter.py +753 -0
  12. revenium_middleware/anthropic/config.py +29 -0
  13. revenium_middleware/anthropic/middleware.py +1070 -0
  14. revenium_middleware/anthropic/prompt_extractor.py +178 -0
  15. revenium_middleware/anthropic/provider.py +141 -0
  16. revenium_middleware/anthropic/summary_printer.py +286 -0
  17. revenium_middleware/anthropic/trace_fields.py +158 -0
  18. revenium_middleware/google/__init__.py +114 -0
  19. revenium_middleware/google/common/__init__.py +127 -0
  20. revenium_middleware/google/common/exceptions.py +137 -0
  21. revenium_middleware/google/common/protocols.py +192 -0
  22. revenium_middleware/google/common/summary_printer.py +271 -0
  23. revenium_middleware/google/common/trace_fields.py +205 -0
  24. revenium_middleware/google/common/types.py +208 -0
  25. revenium_middleware/google/common/utils.py +1111 -0
  26. revenium_middleware/google/config.py +64 -0
  27. revenium_middleware/google/google_ai/__init__.py +53 -0
  28. revenium_middleware/google/google_ai/middleware.py +667 -0
  29. revenium_middleware/google/google_ai/provider.py +135 -0
  30. revenium_middleware/google/prompt_extractor.py +396 -0
  31. revenium_middleware/google/vertex_ai/__init__.py +56 -0
  32. revenium_middleware/google/vertex_ai/middleware.py +1162 -0
  33. revenium_middleware/google/vertex_ai/provider.py +99 -0
  34. revenium_middleware/litellm/__init__.py +25 -0
  35. revenium_middleware/litellm/client/__init__.py +81 -0
  36. revenium_middleware/litellm/client/config.py +53 -0
  37. revenium_middleware/litellm/client/context.py +198 -0
  38. revenium_middleware/litellm/client/decorators.py +912 -0
  39. revenium_middleware/litellm/client/hooks.py +192 -0
  40. revenium_middleware/litellm/client/integrations/__init__.py +26 -0
  41. revenium_middleware/litellm/client/integrations/crewai.py +446 -0
  42. revenium_middleware/litellm/client/middleware.py +321 -0
  43. revenium_middleware/litellm/client/summary_printer.py +314 -0
  44. revenium_middleware/litellm/client/trace_fields.py +51 -0
  45. revenium_middleware/litellm/client/validation.py +207 -0
  46. revenium_middleware/litellm/proxy/__init__.py +25 -0
  47. revenium_middleware/litellm/proxy/middleware.py +217 -0
  48. revenium_middleware/ollama/__init__.py +28 -0
  49. revenium_middleware/ollama/middleware.py +569 -0
  50. revenium_middleware/ollama/trace_fields.py +63 -0
  51. revenium_middleware/openai/__init__.py +23 -0
  52. revenium_middleware/openai/azure_config.py +169 -0
  53. revenium_middleware/openai/azure_model_resolver.py +219 -0
  54. revenium_middleware/openai/config.py +45 -0
  55. revenium_middleware/openai/exceptions.py +115 -0
  56. revenium_middleware/openai/langchain/__init__.py +114 -0
  57. revenium_middleware/openai/langchain/_utils.py +129 -0
  58. revenium_middleware/openai/langchain/unified_handler.py +526 -0
  59. revenium_middleware/openai/middleware.py +1451 -0
  60. revenium_middleware/openai/prompt_extractor.py +173 -0
  61. revenium_middleware/openai/provider.py +170 -0
  62. revenium_middleware/openai/summary_printer.py +292 -0
  63. revenium_middleware/openai/trace_fields.py +98 -0
  64. revenium_middleware/perplexity/__init__.py +97 -0
  65. revenium_middleware/perplexity/middleware.py +379 -0
  66. revenium_middleware/perplexity/perplexity_sdk.py +256 -0
  67. revenium_middleware/perplexity/provider.py +84 -0
  68. revenium_middleware/perplexity/trace_fields.py +25 -0
  69. revenium_python_sdk-0.1.0.dist-info/METADATA +252 -0
  70. revenium_python_sdk-0.1.0.dist-info/RECORD +73 -0
  71. revenium_python_sdk-0.1.0.dist-info/WHEEL +5 -0
  72. revenium_python_sdk-0.1.0.dist-info/licenses/LICENSE +21 -0
  73. revenium_python_sdk-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,192 @@
1
+ """
2
+ Callback/hook system for extending Revenium LiteLLM middleware.
3
+
4
+ This module provides a hook system that allows users to register callbacks
5
+ that are executed before metadata is sent to Revenium. Hooks can modify,
6
+ enrich, or validate metadata.
7
+
8
+ Example:
9
+ >>> from revenium_middleware.litellm.client import register_metadata_hook
10
+ >>>
11
+ >>> def add_environment(metadata):
12
+ ... metadata['environment'] = 'production'
13
+ ... return metadata
14
+ >>>
15
+ >>> register_metadata_hook(add_environment)
16
+ >>>
17
+ >>> # Now all LiteLLM calls will include environment field
18
+ >>> response = litellm.completion(...)
19
+ """
20
+
21
+ import logging
22
+ from typing import Callable, Dict, Any, List, Optional
23
+
24
+ logger = logging.getLogger("revenium_middleware.hooks")
25
+
26
+ # Global registry of metadata hooks
27
+ _metadata_hooks: List[Callable[[Dict[str, Any]], Dict[str, Any]]] = []
28
+
29
+
30
+ def register_metadata_hook(
31
+ hook: Callable[[Dict[str, Any]], Dict[str, Any]],
32
+ priority: int = 0
33
+ ) -> None:
34
+ """
35
+ Register a metadata hook to be called before sending data to Revenium.
36
+
37
+ Hooks are called in order of priority (higher priority first), then
38
+ in registration order for hooks with the same priority.
39
+
40
+ Each hook receives the metadata dictionary and should return a modified
41
+ (or the same) dictionary. Hooks can:
42
+ - Add new fields
43
+ - Modify existing fields
44
+ - Remove fields (by returning a dict without them)
45
+ - Validate metadata (raise exceptions to prevent sending)
46
+
47
+ Args:
48
+ hook: Callable that takes metadata dict and returns metadata dict
49
+ priority: Priority for hook execution (higher = earlier). Default: 0
50
+
51
+ Example:
52
+ >>> def add_version(metadata):
53
+ ... metadata['app_version'] = '1.2.3'
54
+ ... return metadata
55
+ >>>
56
+ >>> register_metadata_hook(add_version, priority=10)
57
+ >>>
58
+ >>> def validate_required_fields(metadata):
59
+ ... if 'organization_id' not in metadata:
60
+ ... raise ValueError("organization_id is required")
61
+ ... return metadata
62
+ >>>
63
+ >>> register_metadata_hook(validate_required_fields, priority=100)
64
+ """
65
+ # Add hook with priority
66
+ _metadata_hooks.append((priority, hook))
67
+
68
+ # Sort by priority (descending) to maintain execution order
69
+ _metadata_hooks.sort(key=lambda x: x[0], reverse=True)
70
+
71
+ logger.debug(f"Registered metadata hook: {hook.__name__} with priority {priority}")
72
+
73
+
74
+ def unregister_metadata_hook(hook: Callable[[Dict[str, Any]], Dict[str, Any]]) -> bool:
75
+ """
76
+ Unregister a previously registered metadata hook.
77
+
78
+ Args:
79
+ hook: The hook function to unregister
80
+
81
+ Returns:
82
+ True if hook was found and removed, False otherwise
83
+
84
+ Example:
85
+ >>> def my_hook(metadata):
86
+ ... return metadata
87
+ >>>
88
+ >>> register_metadata_hook(my_hook)
89
+ >>> # ... later ...
90
+ >>> unregister_metadata_hook(my_hook)
91
+ """
92
+ global _metadata_hooks
93
+
94
+ initial_count = len(_metadata_hooks)
95
+ _metadata_hooks = [(p, h) for p, h in _metadata_hooks if h != hook]
96
+
97
+ removed = len(_metadata_hooks) < initial_count
98
+ if removed:
99
+ logger.debug(f"Unregistered metadata hook: {hook.__name__}")
100
+ else:
101
+ logger.warning(f"Hook not found for unregistration: {hook.__name__}")
102
+
103
+ return removed
104
+
105
+
106
+ def clear_metadata_hooks() -> None:
107
+ """
108
+ Clear all registered metadata hooks.
109
+
110
+ Useful for testing or resetting the hook system.
111
+
112
+ Example:
113
+ >>> clear_metadata_hooks()
114
+ """
115
+ global _metadata_hooks
116
+ count = len(_metadata_hooks)
117
+ _metadata_hooks = []
118
+ logger.debug(f"Cleared {count} metadata hooks")
119
+
120
+
121
+ def execute_metadata_hooks(metadata: Dict[str, Any]) -> Dict[str, Any]:
122
+ """
123
+ Execute all registered hooks on the metadata.
124
+
125
+ Hooks are executed in priority order (highest first). If a hook raises
126
+ an exception, it is logged and the hook is skipped, but execution continues
127
+ with remaining hooks.
128
+
129
+ This function is called internally by the middleware and should not
130
+ normally be called by user code.
131
+
132
+ Args:
133
+ metadata: The metadata dictionary to process
134
+
135
+ Returns:
136
+ The processed metadata dictionary
137
+
138
+ Example:
139
+ >>> metadata = {'agent': 'Test'}
140
+ >>> result = execute_metadata_hooks(metadata)
141
+ """
142
+ # Always return a copy to avoid modifying the original
143
+ result = metadata.copy()
144
+
145
+ if not _metadata_hooks:
146
+ return result
147
+
148
+ for priority, hook in _metadata_hooks:
149
+ try:
150
+ logger.debug(f"Executing hook: {hook.__name__} (priority: {priority})")
151
+ result = hook(result)
152
+
153
+ if not isinstance(result, dict):
154
+ logger.error(
155
+ f"Hook {hook.__name__} returned non-dict value: {type(result)}. "
156
+ f"Skipping this hook."
157
+ )
158
+ result = metadata.copy() # Restore original
159
+
160
+ except Exception as e:
161
+ logger.error(
162
+ f"Error executing hook {hook.__name__}: {e}. "
163
+ f"Skipping this hook and continuing with others.",
164
+ exc_info=True
165
+ )
166
+ # Continue with other hooks even if one fails
167
+
168
+ return result
169
+
170
+
171
+ def get_registered_hooks() -> List[Callable[[Dict[str, Any]], Dict[str, Any]]]:
172
+ """
173
+ Get a list of all registered hooks in execution order.
174
+
175
+ Returns:
176
+ List of hook functions in priority order
177
+
178
+ Example:
179
+ >>> hooks = get_registered_hooks()
180
+ >>> print(f"Registered {len(hooks)} hooks")
181
+ """
182
+ return [hook for _, hook in _metadata_hooks]
183
+
184
+
185
+ __all__ = [
186
+ 'register_metadata_hook',
187
+ 'unregister_metadata_hook',
188
+ 'clear_metadata_hooks',
189
+ 'execute_metadata_hooks',
190
+ 'get_registered_hooks'
191
+ ]
192
+
@@ -0,0 +1,26 @@
1
+ """
2
+ Framework integrations for Revenium LiteLLM middleware.
3
+
4
+ This package provides pre-built integrations for popular AI frameworks,
5
+ eliminating the need for custom monkey-patching code.
6
+
7
+ Available integrations:
8
+ - CrewAI: Multi-agent AI framework integration
9
+
10
+ Example:
11
+ >>> from revenium_middleware.litellm.client.integrations.crewai import ReveniumCrewWrapper
12
+ >>>
13
+ >>> # Wrap your Crew with automatic metadata tracking
14
+ >>> crew = ReveniumCrewWrapper(
15
+ ... agents=[...],
16
+ ... tasks=[...],
17
+ ... organization_id="AcmeCorp",
18
+ ... subscription_id="82764738",
19
+ ... product_id="Platinum"
20
+ ... )
21
+ >>>
22
+ >>> result = crew.kickoff()
23
+ """
24
+
25
+ __all__ = []
26
+
@@ -0,0 +1,446 @@
1
+ """
2
+ CrewAI integration for Revenium LiteLLM middleware.
3
+
4
+ This module provides a simple wrapper for CrewAI that automatically tracks
5
+ agent and task metadata without requiring manual monkey-patching.
6
+
7
+ Example:
8
+ >>> from revenium_middleware.litellm.client.integrations.crewai import ReveniumCrewWrapper
9
+ >>> from crewai import Agent, Task, Crew
10
+ >>>
11
+ >>> # Create your agents and tasks as normal
12
+ >>> agent = Agent(role="Lead Analyst", ...)
13
+ >>> task = Task(description="Research...", agent=agent)
14
+ >>>
15
+ >>> # Wrap with Revenium tracking
16
+ >>> crew = ReveniumCrewWrapper(
17
+ ... agents=[agent],
18
+ ... tasks=[task],
19
+ ... organization_id="AcmeCorp",
20
+ ... subscription_id="82764738",
21
+ ... product_id="Platinum"
22
+ ... )
23
+ >>>
24
+ >>> # Execute - all metadata automatically tracked
25
+ >>> result = crew.kickoff()
26
+ """
27
+
28
+ import uuid
29
+ import logging
30
+ from typing import List, Optional, Dict, Any
31
+ from ..context import metadata_context
32
+
33
+ logger = logging.getLogger("revenium_middleware.crewai")
34
+
35
+ try:
36
+ from crewai import Crew, Agent, Task
37
+ from crewai.llm import LLM
38
+ CREWAI_AVAILABLE = True
39
+ except ImportError:
40
+ CREWAI_AVAILABLE = False
41
+ Crew = object # type: ignore
42
+ Agent = object # type: ignore
43
+ Task = object # type: ignore
44
+ LLM = object # type: ignore
45
+
46
+
47
+ def _get_crewai_version() -> tuple:
48
+ """
49
+ Get the installed CrewAI version as a tuple of integers.
50
+
51
+ Returns:
52
+ Tuple of (major, minor, patch) version numbers
53
+ Returns (0, 0, 0) if version cannot be determined
54
+ """
55
+ try:
56
+ import crewai
57
+ if hasattr(crewai, '__version__'):
58
+ version_str = crewai.__version__
59
+ # Parse version string like "0.203.0" into (0, 203, 0)
60
+ parts = version_str.split('.')
61
+ return tuple(int(p) for p in parts[:3])
62
+ except Exception as e:
63
+ logger.warning(f"Could not determine CrewAI version: {e}")
64
+
65
+ return (0, 0, 0)
66
+
67
+
68
+ def _supports_task_monkey_patching() -> bool:
69
+ """
70
+ Check if the installed CrewAI version supports monkey-patching task execution.
71
+
72
+ CrewAI < 0.203.0 allows monkey-patching task.execute_sync
73
+ CrewAI >= 0.203.0 uses Pydantic models that prevent monkey-patching
74
+
75
+ Returns:
76
+ True if monkey-patching is supported, False otherwise
77
+ """
78
+ version = _get_crewai_version()
79
+
80
+ # Version (0, 0, 0) means we couldn't determine version - try monkey-patching
81
+ if version == (0, 0, 0):
82
+ logger.info("CrewAI version unknown - will attempt monkey-patching with fallback")
83
+ return True
84
+
85
+ # CrewAI < 0.203.0 supports monkey-patching
86
+ if version < (0, 203, 0):
87
+ logger.info(f"CrewAI {'.'.join(map(str, version))} detected - using monkey-patching for task-level metadata")
88
+ return True
89
+
90
+ # CrewAI >= 0.203.0 uses Pydantic models
91
+ logger.info(f"CrewAI {'.'.join(map(str, version))} detected - using callback approach (crew-level metadata only)")
92
+ return False
93
+
94
+
95
+ class ReveniumCrewWrapper:
96
+ """
97
+ Wrapper for CrewAI Crew that automatically injects Revenium metadata.
98
+
99
+ This class wraps a CrewAI Crew and automatically tracks:
100
+ - Organization, subscription, and product IDs
101
+ - Unique trace_id for each crew execution
102
+ - Agent role for each agent interaction
103
+ - Task type for each task execution
104
+
105
+ All metadata is automatically injected into LiteLLM calls via the
106
+ context API, eliminating the need for manual monkey-patching.
107
+
108
+ Attributes:
109
+ organization_id: Customer or department ID
110
+ subscription_id: Billing plan reference
111
+ product_id: Product or feature identifier
112
+ trace_id: Unique identifier for this crew execution (auto-generated)
113
+ """
114
+
115
+ def __init__(
116
+ self,
117
+ agents: List[Any],
118
+ tasks: List[Any],
119
+ organization_id: str,
120
+ subscription_id: str,
121
+ product_id: str,
122
+ trace_id: Optional[str] = None,
123
+ process: Optional[Any] = None,
124
+ verbose: bool = False,
125
+ **crew_kwargs
126
+ ):
127
+ """
128
+ Initialize the Revenium-wrapped Crew.
129
+
130
+ Args:
131
+ agents: List of CrewAI Agent objects
132
+ tasks: List of CrewAI Task objects
133
+ organization_id: Customer or department ID from non-Revenium systems
134
+ subscription_id: Reference to a billing plan in non-Revenium systems
135
+ product_id: Your product or feature making the AI call
136
+ trace_id: Optional unique identifier for this execution. If not provided,
137
+ a UUID will be generated automatically.
138
+ process: Optional CrewAI process type (sequential, hierarchical, etc.)
139
+ verbose: Whether to enable verbose logging
140
+ **crew_kwargs: Additional arguments to pass to Crew constructor
141
+
142
+ Example:
143
+ >>> crew = ReveniumCrewWrapper(
144
+ ... agents=[agent1, agent2],
145
+ ... tasks=[task1, task2],
146
+ ... organization_id="AcmeCorp",
147
+ ... subscription_id="82764738",
148
+ ... product_id="Platinum",
149
+ ... verbose=True
150
+ ... )
151
+ """
152
+ if not CREWAI_AVAILABLE:
153
+ raise ImportError(
154
+ "CrewAI is not installed. Install it with: "
155
+ "pip install 'revenium-python-sdk[litellm]'"
156
+ )
157
+
158
+ self.organization_id = organization_id
159
+ self.subscription_id = subscription_id
160
+ self.product_id = product_id
161
+ self.trace_id = trace_id or str(uuid.uuid4())
162
+
163
+ # Store agents and tasks for metadata extraction
164
+ self._agents = agents
165
+ self._tasks = tasks
166
+
167
+ # Create the underlying Crew
168
+ crew_args = {
169
+ 'agents': agents,
170
+ 'tasks': tasks,
171
+ 'verbose': verbose,
172
+ **crew_kwargs
173
+ }
174
+
175
+ if process is not None:
176
+ crew_args['process'] = process
177
+
178
+ self._crew = Crew(**crew_args)
179
+
180
+ logger.info(
181
+ f"Initialized Revenium Crew wrapper with trace_id: {self.trace_id}"
182
+ )
183
+
184
+ def kickoff(self, inputs: Optional[Dict[str, Any]] = None):
185
+ """
186
+ Execute the crew with Revenium metadata tracking.
187
+
188
+ Args:
189
+ inputs: Optional inputs to pass to the crew
190
+
191
+ Returns:
192
+ The result from the crew execution
193
+ """
194
+ # Set base metadata for the entire crew execution
195
+ # This will be picked up by the middleware for all LiteLLM calls
196
+ with metadata_context.set(
197
+ organization_id=self.organization_id,
198
+ subscription_id=self.subscription_id,
199
+ product_id=self.product_id,
200
+ trace_id=self.trace_id
201
+ ):
202
+ logger.debug(f"Starting crew execution with trace_id: {self.trace_id}")
203
+
204
+ # Setup task-level metadata (version-aware: monkey-patching or callbacks)
205
+ self._setup_task_callbacks()
206
+
207
+ try:
208
+ return self._crew.kickoff(inputs=inputs)
209
+ finally:
210
+ # Clean up any monkey-patches
211
+ self._unpatch_task_execution()
212
+
213
+ async def kickoff_async(self, inputs: Optional[Dict[str, Any]] = None):
214
+ """
215
+ Execute the crew asynchronously with Revenium metadata tracking.
216
+
217
+ Args:
218
+ inputs: Optional inputs to pass to the crew
219
+
220
+ Returns:
221
+ The result from the crew execution
222
+ """
223
+ # Set base metadata for the entire crew execution
224
+ with metadata_context.set(
225
+ organization_id=self.organization_id,
226
+ subscription_id=self.subscription_id,
227
+ product_id=self.product_id,
228
+ trace_id=self.trace_id
229
+ ):
230
+ logger.debug(f"Starting async crew execution with trace_id: {self.trace_id}")
231
+
232
+ # Setup task-level metadata (version-aware: monkey-patching or callbacks)
233
+ self._setup_task_callbacks()
234
+
235
+ try:
236
+ if hasattr(self._crew, 'kickoff_async'):
237
+ return await self._crew.kickoff_async(inputs=inputs)
238
+ else:
239
+ # Fallback to sync if async not available
240
+ return self._crew.kickoff(inputs=inputs)
241
+ finally:
242
+ # Clean up any monkey-patches
243
+ self._unpatch_task_execution()
244
+
245
+ def train(self, n_iterations: int, inputs: Optional[Dict[str, Any]] = None):
246
+ """
247
+ Train the crew with Revenium metadata tracking.
248
+
249
+ Args:
250
+ n_iterations: Number of training iterations
251
+ inputs: Optional inputs to pass to the crew
252
+
253
+ Returns:
254
+ The result from the training
255
+ """
256
+ # Set base metadata for the entire training session
257
+ with metadata_context.set(
258
+ organization_id=self.organization_id,
259
+ subscription_id=self.subscription_id,
260
+ product_id=self.product_id,
261
+ trace_id=self.trace_id
262
+ ):
263
+ logger.debug(f"Starting crew training with trace_id: {self.trace_id}")
264
+
265
+ # Setup task-level metadata (version-aware: monkey-patching or callbacks)
266
+ self._setup_task_callbacks()
267
+
268
+ try:
269
+ if hasattr(self._crew, 'train'):
270
+ return self._crew.train(n_iterations=n_iterations, inputs=inputs)
271
+ else:
272
+ raise AttributeError("Crew does not support training")
273
+ finally:
274
+ # Clean up any monkey-patches
275
+ self._unpatch_task_execution()
276
+
277
+ def _setup_task_callbacks(self):
278
+ """
279
+ Setup task execution tracking with version-aware approach.
280
+
281
+ For CrewAI < 0.203.0: Uses monkey-patching to inject metadata during execution
282
+ For CrewAI >= 0.203.0: Uses callbacks (crew-level metadata only)
283
+ """
284
+ if _supports_task_monkey_patching():
285
+ # Try monkey-patching approach (works with older CrewAI versions)
286
+ self._patch_task_execution()
287
+ else:
288
+ # Fall back to callback approach (CrewAI 0.203.0+)
289
+ self._setup_task_callbacks_only()
290
+
291
+ def _patch_task_execution(self):
292
+ """
293
+ Patch task execution methods to inject agent and task metadata.
294
+
295
+ This approach works with CrewAI < 0.203.0 where task.execute_sync can be
296
+ monkey-patched. Provides full task-level metadata injection.
297
+ """
298
+ self._original_task_executes = {}
299
+
300
+ for task in self._tasks:
301
+ # Try to patch execute_sync if it exists
302
+ try:
303
+ if hasattr(task, 'execute_sync'):
304
+ original_execute = task.execute_sync
305
+ self._original_task_executes[id(task)] = original_execute
306
+
307
+ # Create wrapped version that accepts all arguments
308
+ def wrapped_execute(*args, original=original_execute, task_obj=task, **kwargs):
309
+ # Extract agent role and task description
310
+ agent_role = self._get_agent_role(task_obj)
311
+ task_type = self._get_task_type(task_obj)
312
+
313
+ # UPDATE metadata context (don't replace it!)
314
+ metadata_context.update(
315
+ agent=agent_role,
316
+ task_type=task_type
317
+ )
318
+ logger.debug(
319
+ f"Executing task with agent={agent_role}, "
320
+ f"task_type={task_type}"
321
+ )
322
+ return original(*args, **kwargs)
323
+
324
+ # Attempt to set the wrapped method
325
+ # Use object.__setattr__ to bypass Pydantic validation
326
+ try:
327
+ object.__setattr__(task, 'execute_sync', wrapped_execute)
328
+ logger.debug("Successfully patched task execution for task-level metadata")
329
+ except (ValueError, AttributeError, TypeError):
330
+ # If object.__setattr__ fails, try regular assignment
331
+ task.execute_sync = wrapped_execute
332
+ logger.debug("Successfully patched task execution using regular assignment")
333
+
334
+ except (ValueError, AttributeError, TypeError) as e:
335
+ # Pydantic validation error or other issue - fall back to callbacks
336
+ logger.warning(
337
+ f"Could not monkey-patch task execution (likely CrewAI 0.203.0+): {e}. "
338
+ f"Falling back to callback approach (crew-level metadata only)"
339
+ )
340
+ # Clean up any partial patches
341
+ self._original_task_executes.clear()
342
+ # Use callback approach instead
343
+ self._setup_task_callbacks_only()
344
+ return
345
+
346
+ def _unpatch_task_execution(self):
347
+ """Restore original task execution methods."""
348
+ if not hasattr(self, '_original_task_executes'):
349
+ return
350
+
351
+ for task in self._tasks:
352
+ task_id = id(task)
353
+ if task_id in self._original_task_executes:
354
+ try:
355
+ # Use object.__setattr__ to bypass Pydantic validation
356
+ object.__setattr__(task, 'execute_sync', self._original_task_executes[task_id])
357
+ except (ValueError, AttributeError, TypeError):
358
+ # If object.__setattr__ fails, try regular assignment
359
+ try:
360
+ task.execute_sync = self._original_task_executes[task_id]
361
+ except (ValueError, AttributeError, TypeError):
362
+ # Ignore errors during unpatch
363
+ pass
364
+
365
+ self._original_task_executes.clear()
366
+
367
+ def _setup_task_callbacks_only(self):
368
+ """
369
+ Setup callbacks for tasks (callback-only approach for CrewAI 0.203.0+).
370
+
371
+ Note: Callbacks execute AFTER task completion, so they cannot inject
372
+ metadata into LiteLLM calls. This approach only provides logging.
373
+ """
374
+ try:
375
+ from crewai.tasks.task_output import TaskOutput
376
+ except ImportError:
377
+ logger.warning("Could not import TaskOutput - callbacks not available")
378
+ return
379
+
380
+ for task in self._tasks:
381
+ # Extract agent role and task type for this task
382
+ agent_role = self._get_agent_role(task)
383
+ task_type = self._get_task_type(task)
384
+
385
+ # Create a callback for logging only
386
+ def task_callback(output: TaskOutput, agent=agent_role, task_t=task_type):
387
+ """Callback executed after task completion (logging only)."""
388
+ logger.debug(
389
+ f"Task completed: agent={agent}, task_type={task_t}"
390
+ )
391
+
392
+ # Store original callback if it exists
393
+ original_callback = getattr(task, 'callback', None)
394
+
395
+ # Create a combined callback that calls both
396
+ if original_callback:
397
+ def combined_callback(output: TaskOutput, orig=original_callback, new=task_callback):
398
+ new(output)
399
+ orig(output)
400
+ task.callback = combined_callback
401
+ else:
402
+ task.callback = task_callback
403
+
404
+ logger.debug(
405
+ f"Setup callback for task with agent={agent_role}, task_type={task_type}"
406
+ )
407
+
408
+ def _get_agent_role(self, task: Any) -> str:
409
+ """
410
+ Extract agent role from a task.
411
+
412
+ Args:
413
+ task: CrewAI Task object
414
+
415
+ Returns:
416
+ Agent role string
417
+ """
418
+ if hasattr(task, 'agent') and task.agent and hasattr(task.agent, 'role'):
419
+ return str(task.agent.role)
420
+ return "unknown_agent"
421
+
422
+ def _get_task_type(self, task: Any) -> str:
423
+ """
424
+ Extract task type from a task.
425
+
426
+ Args:
427
+ task: CrewAI Task object
428
+
429
+ Returns:
430
+ Task type string (derived from description or name)
431
+ """
432
+ # Try to get from task description or name
433
+ if hasattr(task, 'description') and task.description:
434
+ # Use first few words of description as task type
435
+ desc = str(task.description).lower()
436
+ # Extract first meaningful phrase (up to 30 chars)
437
+ task_type = desc[:30].split('.')[0].strip()
438
+ # Clean up for use as identifier
439
+ task_type = task_type.replace(' ', '_')
440
+ return task_type
441
+
442
+ return "unknown_task"
443
+
444
+
445
+ __all__ = ['ReveniumCrewWrapper', 'CREWAI_AVAILABLE']
446
+