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.
- revenium_middleware/__init__.py +184 -0
- revenium_middleware/_core/__init__.py +65 -0
- revenium_middleware/_core/config.py +165 -0
- revenium_middleware/_core/context.py +109 -0
- revenium_middleware/_core/decorators.py +202 -0
- revenium_middleware/_core/metering.py +207 -0
- revenium_middleware/_core/prompt_extraction.py +55 -0
- revenium_middleware/_core/subscriber.py +51 -0
- revenium_middleware/_core/trace_fields.py +265 -0
- revenium_middleware/anthropic/__init__.py +108 -0
- revenium_middleware/anthropic/bedrock_adapter.py +753 -0
- revenium_middleware/anthropic/config.py +29 -0
- revenium_middleware/anthropic/middleware.py +1070 -0
- revenium_middleware/anthropic/prompt_extractor.py +178 -0
- revenium_middleware/anthropic/provider.py +141 -0
- revenium_middleware/anthropic/summary_printer.py +286 -0
- revenium_middleware/anthropic/trace_fields.py +158 -0
- revenium_middleware/google/__init__.py +114 -0
- revenium_middleware/google/common/__init__.py +127 -0
- revenium_middleware/google/common/exceptions.py +137 -0
- revenium_middleware/google/common/protocols.py +192 -0
- revenium_middleware/google/common/summary_printer.py +271 -0
- revenium_middleware/google/common/trace_fields.py +205 -0
- revenium_middleware/google/common/types.py +208 -0
- revenium_middleware/google/common/utils.py +1111 -0
- revenium_middleware/google/config.py +64 -0
- revenium_middleware/google/google_ai/__init__.py +53 -0
- revenium_middleware/google/google_ai/middleware.py +667 -0
- revenium_middleware/google/google_ai/provider.py +135 -0
- revenium_middleware/google/prompt_extractor.py +396 -0
- revenium_middleware/google/vertex_ai/__init__.py +56 -0
- revenium_middleware/google/vertex_ai/middleware.py +1162 -0
- revenium_middleware/google/vertex_ai/provider.py +99 -0
- revenium_middleware/litellm/__init__.py +25 -0
- revenium_middleware/litellm/client/__init__.py +81 -0
- revenium_middleware/litellm/client/config.py +53 -0
- revenium_middleware/litellm/client/context.py +198 -0
- revenium_middleware/litellm/client/decorators.py +912 -0
- revenium_middleware/litellm/client/hooks.py +192 -0
- revenium_middleware/litellm/client/integrations/__init__.py +26 -0
- revenium_middleware/litellm/client/integrations/crewai.py +446 -0
- revenium_middleware/litellm/client/middleware.py +321 -0
- revenium_middleware/litellm/client/summary_printer.py +314 -0
- revenium_middleware/litellm/client/trace_fields.py +51 -0
- revenium_middleware/litellm/client/validation.py +207 -0
- revenium_middleware/litellm/proxy/__init__.py +25 -0
- revenium_middleware/litellm/proxy/middleware.py +217 -0
- revenium_middleware/ollama/__init__.py +28 -0
- revenium_middleware/ollama/middleware.py +569 -0
- revenium_middleware/ollama/trace_fields.py +63 -0
- revenium_middleware/openai/__init__.py +23 -0
- revenium_middleware/openai/azure_config.py +169 -0
- revenium_middleware/openai/azure_model_resolver.py +219 -0
- revenium_middleware/openai/config.py +45 -0
- revenium_middleware/openai/exceptions.py +115 -0
- revenium_middleware/openai/langchain/__init__.py +114 -0
- revenium_middleware/openai/langchain/_utils.py +129 -0
- revenium_middleware/openai/langchain/unified_handler.py +526 -0
- revenium_middleware/openai/middleware.py +1451 -0
- revenium_middleware/openai/prompt_extractor.py +173 -0
- revenium_middleware/openai/provider.py +170 -0
- revenium_middleware/openai/summary_printer.py +292 -0
- revenium_middleware/openai/trace_fields.py +98 -0
- revenium_middleware/perplexity/__init__.py +97 -0
- revenium_middleware/perplexity/middleware.py +379 -0
- revenium_middleware/perplexity/perplexity_sdk.py +256 -0
- revenium_middleware/perplexity/provider.py +84 -0
- revenium_middleware/perplexity/trace_fields.py +25 -0
- revenium_python_sdk-0.1.0.dist-info/METADATA +252 -0
- revenium_python_sdk-0.1.0.dist-info/RECORD +73 -0
- revenium_python_sdk-0.1.0.dist-info/WHEEL +5 -0
- revenium_python_sdk-0.1.0.dist-info/licenses/LICENSE +21 -0
- 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
|
+
|