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,129 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Utility functions for LangChain integration.
|
|
3
|
+
|
|
4
|
+
This module provides helper functions for dependency checking, error handling,
|
|
5
|
+
and other common functionality used across the LangChain integration.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import functools
|
|
9
|
+
import logging
|
|
10
|
+
from typing import Any, Callable, TypeVar, Optional, Tuple
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger("revenium_middleware.langchain")
|
|
13
|
+
|
|
14
|
+
F = TypeVar('F', bound=Callable[..., Any])
|
|
15
|
+
|
|
16
|
+
# Global cache for LangChain availability check
|
|
17
|
+
_langchain_check_cache: Optional[Tuple[bool, Optional[Exception]]] = None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class LangChainNotInstalledError(ImportError):
|
|
21
|
+
"""
|
|
22
|
+
Custom exception raised when LangChain functionality is used without LangChain installed.
|
|
23
|
+
|
|
24
|
+
This provides a clear, actionable error message for users.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(self, message: str = None, feature: str = None):
|
|
28
|
+
if message is None:
|
|
29
|
+
feature_msg = f" for {feature}" if feature else ""
|
|
30
|
+
message = (
|
|
31
|
+
f"LangChain is required{feature_msg} but is not installed.\n"
|
|
32
|
+
"Install it with one of the following commands:\n"
|
|
33
|
+
" pip install revenium-python-sdk[openai,langchain]\n"
|
|
34
|
+
" pip install langchain>=0.1.16,<1.0"
|
|
35
|
+
)
|
|
36
|
+
super().__init__(message)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _check_langchain_cached() -> Tuple[bool, Optional[Exception]]:
|
|
40
|
+
"""
|
|
41
|
+
Check if LangChain is available with caching to avoid repeated imports.
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
Tuple of (is_available, import_error_if_any)
|
|
45
|
+
"""
|
|
46
|
+
global _langchain_check_cache
|
|
47
|
+
|
|
48
|
+
if _langchain_check_cache is not None:
|
|
49
|
+
return _langchain_check_cache
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
import langchain # noqa: F401
|
|
53
|
+
_langchain_check_cache = (True, None)
|
|
54
|
+
logger.debug("LangChain is available")
|
|
55
|
+
except ImportError as e:
|
|
56
|
+
_langchain_check_cache = (False, e)
|
|
57
|
+
logger.debug(f"LangChain is not available: {e}")
|
|
58
|
+
|
|
59
|
+
return _langchain_check_cache
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def requires_langchain(feature: str = None) -> Callable[[F], F]:
|
|
63
|
+
"""
|
|
64
|
+
Decorator that ensures LangChain is available before calling the decorated function.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
feature: Optional feature name for better error messages
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
Decorator function
|
|
71
|
+
|
|
72
|
+
Raises:
|
|
73
|
+
LangChainNotInstalledError: If LangChain is not installed
|
|
74
|
+
"""
|
|
75
|
+
def decorator(func: F) -> F:
|
|
76
|
+
@functools.wraps(func)
|
|
77
|
+
def wrapper(*args, **kwargs):
|
|
78
|
+
is_available, import_error = _check_langchain_cached()
|
|
79
|
+
if not is_available:
|
|
80
|
+
raise LangChainNotInstalledError(feature=feature) from import_error
|
|
81
|
+
|
|
82
|
+
return func(*args, **kwargs)
|
|
83
|
+
|
|
84
|
+
return wrapper
|
|
85
|
+
return decorator
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def check_langchain_version() -> str:
|
|
89
|
+
"""
|
|
90
|
+
Check the installed LangChain version and return it.
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
LangChain version string
|
|
94
|
+
|
|
95
|
+
Raises:
|
|
96
|
+
LangChainNotInstalledError: If LangChain is not installed
|
|
97
|
+
"""
|
|
98
|
+
is_available, import_error = _check_langchain_cached()
|
|
99
|
+
if not is_available:
|
|
100
|
+
raise LangChainNotInstalledError(feature="version checking") from import_error
|
|
101
|
+
|
|
102
|
+
import langchain # noqa: F401
|
|
103
|
+
return getattr(langchain, '__version__', 'unknown')
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def is_langchain_available() -> bool:
|
|
107
|
+
"""
|
|
108
|
+
Check if LangChain is available without raising an exception.
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
True if LangChain is available, False otherwise
|
|
112
|
+
"""
|
|
113
|
+
is_available, _ = _check_langchain_cached()
|
|
114
|
+
return is_available
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def require_langchain_or_raise(feature: str = None) -> None:
|
|
118
|
+
"""
|
|
119
|
+
Ensure LangChain is available, raising a helpful error if not.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
feature: Optional feature name for better error messages
|
|
123
|
+
|
|
124
|
+
Raises:
|
|
125
|
+
LangChainNotInstalledError: If LangChain is not installed
|
|
126
|
+
"""
|
|
127
|
+
is_available, import_error = _check_langchain_cached()
|
|
128
|
+
if not is_available:
|
|
129
|
+
raise LangChainNotInstalledError(feature=feature) from import_error
|
|
@@ -0,0 +1,526 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Unified LangChain callback handler implementation for Revenium middleware.
|
|
3
|
+
|
|
4
|
+
This module implements the unified architecture documented in LANGCHAIN_ARCHITECTURE.md,
|
|
5
|
+
providing a single ReveniumCallbackHandler that supports both sync and async operations.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
import time
|
|
10
|
+
from typing import Any, Dict, List, Optional
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger("revenium_middleware.langchain")
|
|
13
|
+
|
|
14
|
+
# Version compatibility pattern - support both LangChain 0.x and 1.0+
|
|
15
|
+
try:
|
|
16
|
+
# LangChain 1.0+ uses langchain_core
|
|
17
|
+
from langchain_core.callbacks.base import BaseCallbackHandler, AsyncCallbackHandler
|
|
18
|
+
except ImportError:
|
|
19
|
+
try:
|
|
20
|
+
# LangChain 0.x uses langchain.callbacks.base
|
|
21
|
+
from langchain.callbacks.base import BaseCallbackHandler, AsyncCallbackHandler
|
|
22
|
+
except ImportError:
|
|
23
|
+
from langchain.callbacks.base import BaseCallbackHandler
|
|
24
|
+
class AsyncCallbackHandler(BaseCallbackHandler): # shim for very old LC
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _safe(fn):
|
|
29
|
+
"""
|
|
30
|
+
Decorator to safely wrap callback methods and prevent errors from propagating.
|
|
31
|
+
|
|
32
|
+
This is a core part of the unified architecture - ensures callback errors
|
|
33
|
+
never break user scripts.
|
|
34
|
+
"""
|
|
35
|
+
def wrapper(self, *args, **kwargs):
|
|
36
|
+
try:
|
|
37
|
+
return fn(self, *args, **kwargs)
|
|
38
|
+
except Exception as e:
|
|
39
|
+
logger.exception(f"Revenium callback failed in {fn.__name__}: {e}")
|
|
40
|
+
# Never re-raise - graceful degradation
|
|
41
|
+
return wrapper
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class UnifiedReveniumCallbackHandler(AsyncCallbackHandler):
|
|
45
|
+
"""
|
|
46
|
+
Unified LangChain callback handler that integrates with Revenium usage tracking.
|
|
47
|
+
|
|
48
|
+
This handler implements both sync and async callback interfaces, allowing it to
|
|
49
|
+
work seamlessly with any LangChain operation. It captures LLM interactions and
|
|
50
|
+
forwards usage data to Revenium for cost tracking and analytics.
|
|
51
|
+
|
|
52
|
+
Features:
|
|
53
|
+
- Unified sync/async support (no handler selection needed)
|
|
54
|
+
- Automatic usage tracking for chat completions
|
|
55
|
+
- Transport-level embeddings tracking (via x-revenium-origin header)
|
|
56
|
+
- Support for streaming responses
|
|
57
|
+
- Configurable metadata injection
|
|
58
|
+
- Error isolation with @_safe decorator
|
|
59
|
+
- Graceful degradation when tracking fails
|
|
60
|
+
|
|
61
|
+
Architecture:
|
|
62
|
+
- Implements both BaseCallbackHandler and AsyncCallbackHandler
|
|
63
|
+
- Uses shared implementation helpers for consistency
|
|
64
|
+
- LangChain's callback manager handles sync/async routing automatically
|
|
65
|
+
- Transport hooks handle embeddings (no callback overhead)
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
def __init__(self,
|
|
69
|
+
usage_metadata: Optional[dict] = None,
|
|
70
|
+
enable_debug_logging: bool = False):
|
|
71
|
+
"""
|
|
72
|
+
Initialize the unified Revenium callback handler.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
usage_metadata: Optional metadata to include with usage tracking
|
|
76
|
+
enable_debug_logging: Enable detailed debug logging for troubleshooting
|
|
77
|
+
"""
|
|
78
|
+
# Initialize the AsyncCallbackHandler (which includes BaseCallbackHandler)
|
|
79
|
+
super().__init__()
|
|
80
|
+
|
|
81
|
+
# Store configuration
|
|
82
|
+
self.usage_metadata = usage_metadata or {}
|
|
83
|
+
self.enable_debug_logging = enable_debug_logging
|
|
84
|
+
|
|
85
|
+
# Internal state for tracking operations
|
|
86
|
+
self._active_runs = {} # Track active LLM runs by run_id
|
|
87
|
+
self._operation_timings = {} # Track timing information
|
|
88
|
+
|
|
89
|
+
# Import Revenium dependencies
|
|
90
|
+
self._import_revenium_dependencies()
|
|
91
|
+
|
|
92
|
+
if self.enable_debug_logging:
|
|
93
|
+
logger.info("Unified ReveniumCallbackHandler created successfully")
|
|
94
|
+
|
|
95
|
+
def _import_revenium_dependencies(self):
|
|
96
|
+
"""Import Revenium middleware dependencies with fallback."""
|
|
97
|
+
try:
|
|
98
|
+
# Import the correct functions from the middleware
|
|
99
|
+
from revenium_middleware.openai.middleware import create_metering_call, OperationType
|
|
100
|
+
from revenium_middleware.openai.provider import get_or_detect_provider
|
|
101
|
+
|
|
102
|
+
self._create_metering_call = create_metering_call
|
|
103
|
+
self._OperationType = OperationType
|
|
104
|
+
self._get_or_detect_provider = get_or_detect_provider
|
|
105
|
+
|
|
106
|
+
if self.enable_debug_logging:
|
|
107
|
+
logger.debug("Revenium middleware dependencies imported successfully")
|
|
108
|
+
|
|
109
|
+
except ImportError as e:
|
|
110
|
+
if self.enable_debug_logging:
|
|
111
|
+
logger.debug(f"Revenium middleware not available: {e}")
|
|
112
|
+
self._create_metering_call = self._fallback_metering_call
|
|
113
|
+
self._OperationType = None
|
|
114
|
+
self._get_or_detect_provider = None
|
|
115
|
+
|
|
116
|
+
def _fallback_metering_call(self, *args, **kwargs):
|
|
117
|
+
"""Fallback function when Revenium middleware is not available."""
|
|
118
|
+
logger.warning("Revenium middleware not available - usage tracking disabled")
|
|
119
|
+
return None
|
|
120
|
+
|
|
121
|
+
# ---- Shared Implementation Helpers ----
|
|
122
|
+
|
|
123
|
+
def _handle_llm_start(self, serialized: Dict[str, Any], prompts: List[str],
|
|
124
|
+
is_async: bool = False, **kwargs) -> None:
|
|
125
|
+
"""
|
|
126
|
+
Shared implementation for LLM start events.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
serialized: Serialized LLM configuration
|
|
130
|
+
prompts: List of prompts being sent to the LLM
|
|
131
|
+
is_async: Whether this is an async operation
|
|
132
|
+
**kwargs: Additional keyword arguments including run_id
|
|
133
|
+
"""
|
|
134
|
+
run_id = kwargs.get('run_id')
|
|
135
|
+
if not run_id:
|
|
136
|
+
return
|
|
137
|
+
|
|
138
|
+
# Store run information for later processing
|
|
139
|
+
run_info = {
|
|
140
|
+
'start_time': time.time(),
|
|
141
|
+
'serialized': serialized,
|
|
142
|
+
'prompts': prompts,
|
|
143
|
+
'is_async': is_async,
|
|
144
|
+
'usage_metadata': self.usage_metadata.copy()
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
self._active_runs[run_id] = run_info
|
|
148
|
+
|
|
149
|
+
if self.enable_debug_logging:
|
|
150
|
+
context = "async" if is_async else "sync"
|
|
151
|
+
logger.debug(f"LLM started ({context}) - run_id: {run_id}")
|
|
152
|
+
|
|
153
|
+
def _handle_llm_end(self, response: Any, is_async: bool = False, **kwargs) -> None:
|
|
154
|
+
"""
|
|
155
|
+
Shared implementation for LLM end events.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
response: LLM response object
|
|
159
|
+
is_async: Whether this is an async operation
|
|
160
|
+
**kwargs: Additional keyword arguments including run_id
|
|
161
|
+
"""
|
|
162
|
+
run_id = kwargs.get('run_id')
|
|
163
|
+
if not run_id or run_id not in self._active_runs:
|
|
164
|
+
return
|
|
165
|
+
|
|
166
|
+
run_info = self._active_runs.pop(run_id)
|
|
167
|
+
|
|
168
|
+
# Process the response and create metering call
|
|
169
|
+
self._process_llm_response(response, run_info)
|
|
170
|
+
|
|
171
|
+
if self.enable_debug_logging:
|
|
172
|
+
context = "async" if is_async else "sync"
|
|
173
|
+
logger.debug(f"LLM ended ({context}) - run_id: {run_id}")
|
|
174
|
+
|
|
175
|
+
def _process_llm_response(self, response: Any, run_info: Dict[str, Any]) -> None:
|
|
176
|
+
"""
|
|
177
|
+
Process LLM response and create Revenium metering call.
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
response: LLM response object
|
|
181
|
+
run_info: Stored information from the start of the run
|
|
182
|
+
"""
|
|
183
|
+
try:
|
|
184
|
+
# Extract usage information from response
|
|
185
|
+
usage_data = self._extract_usage_from_response(response)
|
|
186
|
+
|
|
187
|
+
if usage_data:
|
|
188
|
+
# Create metering call
|
|
189
|
+
self._process_metering_call(usage_data, run_info)
|
|
190
|
+
else:
|
|
191
|
+
logger.warning("No usage data found in LLM response")
|
|
192
|
+
|
|
193
|
+
except Exception as e:
|
|
194
|
+
logger.error(f"Error processing LLM response: {e}")
|
|
195
|
+
|
|
196
|
+
def _extract_usage_from_response(self, response: Any) -> Optional[Dict[str, Any]]:
|
|
197
|
+
"""
|
|
198
|
+
Extract usage information from LLM response.
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
response: LLM response object
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
Dictionary with usage information or None if not found
|
|
205
|
+
"""
|
|
206
|
+
# Try different ways to extract usage data
|
|
207
|
+
usage_data = None
|
|
208
|
+
|
|
209
|
+
if self.enable_debug_logging:
|
|
210
|
+
logger.debug(f"Extracting usage from response type: {type(response)}")
|
|
211
|
+
logger.debug(f"Response attributes: {dir(response)}")
|
|
212
|
+
|
|
213
|
+
# Check for usage_metadata in response (LangChain v0.2+)
|
|
214
|
+
if hasattr(response, 'usage_metadata') and response.usage_metadata:
|
|
215
|
+
usage_data = response.usage_metadata
|
|
216
|
+
if self.enable_debug_logging:
|
|
217
|
+
logger.debug(f"Found usage_metadata: {usage_data}")
|
|
218
|
+
|
|
219
|
+
# Check for response_metadata with token_usage (LangChain v0.1+)
|
|
220
|
+
elif hasattr(response, 'response_metadata') and response.response_metadata:
|
|
221
|
+
if 'token_usage' in response.response_metadata:
|
|
222
|
+
usage_data = response.response_metadata['token_usage']
|
|
223
|
+
if self.enable_debug_logging:
|
|
224
|
+
logger.debug(f"Found token_usage in response_metadata: {usage_data}")
|
|
225
|
+
elif 'usage' in response.response_metadata:
|
|
226
|
+
usage_data = response.response_metadata['usage']
|
|
227
|
+
if self.enable_debug_logging:
|
|
228
|
+
logger.debug(f"Found usage in response_metadata: {usage_data}")
|
|
229
|
+
|
|
230
|
+
# Check for llm_output (older LangChain versions)
|
|
231
|
+
elif hasattr(response, 'llm_output') and response.llm_output:
|
|
232
|
+
if 'token_usage' in response.llm_output:
|
|
233
|
+
usage_data = response.llm_output['token_usage']
|
|
234
|
+
if self.enable_debug_logging:
|
|
235
|
+
logger.debug(f"Found token_usage in llm_output: {usage_data}")
|
|
236
|
+
elif 'usage' in response.llm_output:
|
|
237
|
+
usage_data = response.llm_output['usage']
|
|
238
|
+
if self.enable_debug_logging:
|
|
239
|
+
logger.debug(f"Found usage in llm_output: {usage_data}")
|
|
240
|
+
|
|
241
|
+
# For streaming responses, check if there's accumulated usage data
|
|
242
|
+
elif hasattr(response, 'content') and hasattr(response, 'additional_kwargs'):
|
|
243
|
+
# This might be a streaming chunk with accumulated data
|
|
244
|
+
if 'usage' in response.additional_kwargs:
|
|
245
|
+
usage_data = response.additional_kwargs['usage']
|
|
246
|
+
if self.enable_debug_logging:
|
|
247
|
+
logger.debug(f"Found usage in additional_kwargs: {usage_data}")
|
|
248
|
+
|
|
249
|
+
# Last resort: check for any 'usage' attribute directly
|
|
250
|
+
elif hasattr(response, 'usage'):
|
|
251
|
+
usage_data = response.usage
|
|
252
|
+
if self.enable_debug_logging:
|
|
253
|
+
logger.debug(f"Found direct usage attribute: {usage_data}")
|
|
254
|
+
|
|
255
|
+
if not usage_data and self.enable_debug_logging:
|
|
256
|
+
logger.debug("No usage data found in response")
|
|
257
|
+
# Log response structure for debugging
|
|
258
|
+
if hasattr(response, '__dict__'):
|
|
259
|
+
logger.debug(f"Response dict: {response.__dict__}")
|
|
260
|
+
|
|
261
|
+
return usage_data
|
|
262
|
+
|
|
263
|
+
def _process_metering_call(self, usage_data: Dict[str, Any], run_info: Dict[str, Any]) -> None:
|
|
264
|
+
"""
|
|
265
|
+
Create a Revenium metering call with the usage data.
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
usage_data: Usage information extracted from response
|
|
269
|
+
run_info: Stored information from the start of the run
|
|
270
|
+
"""
|
|
271
|
+
try:
|
|
272
|
+
# Create a mock response object that matches what the middleware expects
|
|
273
|
+
mock_response = self._create_mock_response(usage_data, run_info)
|
|
274
|
+
|
|
275
|
+
# Calculate request time
|
|
276
|
+
import datetime
|
|
277
|
+
request_time_dt = datetime.datetime.fromtimestamp(run_info['start_time'], tz=datetime.timezone.utc)
|
|
278
|
+
|
|
279
|
+
# Determine operation type
|
|
280
|
+
operation_type = self._OperationType.CHAT if self._OperationType else None
|
|
281
|
+
|
|
282
|
+
if operation_type and self._create_metering_call != self._fallback_metering_call:
|
|
283
|
+
# Use the unified create_metering_call function from middleware
|
|
284
|
+
# Signature: create_metering_call(response, operation_type, request_time_dt, usage_metadata, client_instance=None, time_to_first_token=0, is_streamed=False)
|
|
285
|
+
result = self._create_metering_call(
|
|
286
|
+
mock_response, # response
|
|
287
|
+
operation_type, # operation_type
|
|
288
|
+
request_time_dt, # request_time_dt
|
|
289
|
+
run_info['usage_metadata'], # usage_metadata
|
|
290
|
+
None, # client_instance - LangChain doesn't provide direct client access
|
|
291
|
+
0, # time_to_first_token - Not available from LangChain callbacks
|
|
292
|
+
run_info.get('is_streaming', False) # is_streamed
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
if self.enable_debug_logging:
|
|
296
|
+
logger.debug(f"Revenium metering call successful: {result}")
|
|
297
|
+
else:
|
|
298
|
+
logger.warning("OperationType not available - metering call skipped")
|
|
299
|
+
|
|
300
|
+
except Exception as e:
|
|
301
|
+
logger.error(f"Error creating Revenium metering call: {e}")
|
|
302
|
+
if self.enable_debug_logging:
|
|
303
|
+
import traceback
|
|
304
|
+
logger.debug(f"Metering call traceback: {traceback.format_exc()}")
|
|
305
|
+
|
|
306
|
+
def _create_mock_response(self, usage_data: Dict[str, Any], run_info: Dict[str, Any]) -> Any:
|
|
307
|
+
"""
|
|
308
|
+
Create a mock response object that matches what the middleware expects.
|
|
309
|
+
|
|
310
|
+
Args:
|
|
311
|
+
usage_data: Usage information from LangChain response
|
|
312
|
+
run_info: Stored information from the start of the run
|
|
313
|
+
|
|
314
|
+
Returns:
|
|
315
|
+
Mock response object with required attributes
|
|
316
|
+
"""
|
|
317
|
+
class MockResponse:
|
|
318
|
+
def __init__(self, usage_data, model_name):
|
|
319
|
+
# Set usage information in the format expected by middleware
|
|
320
|
+
if 'input_tokens' in usage_data:
|
|
321
|
+
# LangChain v0.2+ format
|
|
322
|
+
self.usage = type('Usage', (), {
|
|
323
|
+
'prompt_tokens': usage_data.get('input_tokens', 0),
|
|
324
|
+
'completion_tokens': usage_data.get('output_tokens', 0),
|
|
325
|
+
'total_tokens': usage_data.get('total_tokens', 0)
|
|
326
|
+
})()
|
|
327
|
+
elif 'prompt_tokens' in usage_data:
|
|
328
|
+
# OpenAI format
|
|
329
|
+
self.usage = type('Usage', (), {
|
|
330
|
+
'prompt_tokens': usage_data.get('prompt_tokens', 0),
|
|
331
|
+
'completion_tokens': usage_data.get('completion_tokens', 0),
|
|
332
|
+
'total_tokens': usage_data.get('total_tokens', 0)
|
|
333
|
+
})()
|
|
334
|
+
else:
|
|
335
|
+
# Fallback - create minimal usage
|
|
336
|
+
self.usage = type('Usage', (), {
|
|
337
|
+
'prompt_tokens': 0,
|
|
338
|
+
'completion_tokens': 0,
|
|
339
|
+
'total_tokens': 0
|
|
340
|
+
})()
|
|
341
|
+
|
|
342
|
+
# Set model name
|
|
343
|
+
self.model = model_name
|
|
344
|
+
|
|
345
|
+
# Generate a unique ID for this response
|
|
346
|
+
import uuid
|
|
347
|
+
self.id = f"langchain-{uuid.uuid4().hex[:8]}"
|
|
348
|
+
|
|
349
|
+
# Set other expected attributes
|
|
350
|
+
self.object = "chat.completion"
|
|
351
|
+
self.created = int(time.time())
|
|
352
|
+
self.system_fingerprint = None
|
|
353
|
+
|
|
354
|
+
# Add choices array (required by middleware)
|
|
355
|
+
choice = type('Choice', (), {
|
|
356
|
+
'index': 0,
|
|
357
|
+
'message': type('Message', (), {
|
|
358
|
+
'role': 'assistant',
|
|
359
|
+
'content': 'Mock response from LangChain callback'
|
|
360
|
+
})(),
|
|
361
|
+
'finish_reason': 'stop'
|
|
362
|
+
})()
|
|
363
|
+
self.choices = [choice]
|
|
364
|
+
|
|
365
|
+
model_name = self._extract_model_name(run_info['serialized'])
|
|
366
|
+
return MockResponse(usage_data, model_name)
|
|
367
|
+
|
|
368
|
+
def _extract_model_name(self, serialized: Dict[str, Any]) -> str:
|
|
369
|
+
"""
|
|
370
|
+
Extract model name from serialized LLM configuration.
|
|
371
|
+
|
|
372
|
+
Args:
|
|
373
|
+
serialized: Serialized LLM configuration
|
|
374
|
+
|
|
375
|
+
Returns:
|
|
376
|
+
Model name string
|
|
377
|
+
"""
|
|
378
|
+
if self.enable_debug_logging:
|
|
379
|
+
logger.debug(f"Extracting model name from serialized: {serialized}")
|
|
380
|
+
|
|
381
|
+
# Try different ways to extract model name from LangChain serialized data
|
|
382
|
+
model_name = None
|
|
383
|
+
|
|
384
|
+
# Check direct model fields
|
|
385
|
+
if 'model_name' in serialized:
|
|
386
|
+
model_name = serialized['model_name']
|
|
387
|
+
elif 'model' in serialized:
|
|
388
|
+
model_name = serialized['model']
|
|
389
|
+
|
|
390
|
+
# Check in kwargs (common in LangChain)
|
|
391
|
+
elif 'kwargs' in serialized and isinstance(serialized['kwargs'], dict):
|
|
392
|
+
kwargs = serialized['kwargs']
|
|
393
|
+
if 'model_name' in kwargs:
|
|
394
|
+
model_name = kwargs['model_name']
|
|
395
|
+
elif 'model' in kwargs:
|
|
396
|
+
model_name = kwargs['model']
|
|
397
|
+
|
|
398
|
+
# Check in id field (sometimes contains class info)
|
|
399
|
+
elif 'id' in serialized and isinstance(serialized['id'], list):
|
|
400
|
+
# LangChain often stores class path in id field
|
|
401
|
+
id_parts = serialized['id']
|
|
402
|
+
for part in id_parts:
|
|
403
|
+
if isinstance(part, str) and ('gpt' in part.lower() or 'claude' in part.lower() or 'llama' in part.lower()):
|
|
404
|
+
model_name = part
|
|
405
|
+
break
|
|
406
|
+
|
|
407
|
+
# Fallback to unknown
|
|
408
|
+
if not model_name:
|
|
409
|
+
model_name = 'unknown'
|
|
410
|
+
if self.enable_debug_logging:
|
|
411
|
+
logger.warning(f"Could not extract model name from serialized data: {serialized}")
|
|
412
|
+
|
|
413
|
+
if self.enable_debug_logging:
|
|
414
|
+
logger.debug(f"Extracted model name: {model_name}")
|
|
415
|
+
|
|
416
|
+
return str(model_name)
|
|
417
|
+
|
|
418
|
+
# ---- Sync Callback Methods ----
|
|
419
|
+
|
|
420
|
+
@_safe
|
|
421
|
+
def on_llm_start(self, serialized: Dict[str, Any], prompts: List[str], **kwargs) -> None:
|
|
422
|
+
"""Sync callback for LLM start."""
|
|
423
|
+
self._handle_llm_start(serialized, prompts, is_async=False, **kwargs)
|
|
424
|
+
|
|
425
|
+
@_safe
|
|
426
|
+
def on_llm_end(self, response: Any, **kwargs) -> None:
|
|
427
|
+
"""Sync callback for LLM end."""
|
|
428
|
+
self._handle_llm_end(response, is_async=False, **kwargs)
|
|
429
|
+
|
|
430
|
+
@_safe
|
|
431
|
+
def on_llm_error(self, error: Exception, **kwargs) -> None:
|
|
432
|
+
"""Sync callback for LLM error."""
|
|
433
|
+
run_id = kwargs.get('run_id')
|
|
434
|
+
if run_id and run_id in self._active_runs:
|
|
435
|
+
self._active_runs.pop(run_id)
|
|
436
|
+
logger.warning(f"LLM error in run {run_id}: {error}")
|
|
437
|
+
|
|
438
|
+
@_safe
|
|
439
|
+
def on_chat_model_start(self, serialized: Dict[str, Any], messages: List[List], **kwargs) -> None:
|
|
440
|
+
"""Sync callback for chat model start (LangChain specific)."""
|
|
441
|
+
# Convert messages to prompts format for consistency
|
|
442
|
+
prompts = []
|
|
443
|
+
for message_list in messages:
|
|
444
|
+
if isinstance(message_list, list):
|
|
445
|
+
# Extract content from message objects
|
|
446
|
+
prompt_parts = []
|
|
447
|
+
for msg in message_list:
|
|
448
|
+
if hasattr(msg, 'content'):
|
|
449
|
+
prompt_parts.append(str(msg.content))
|
|
450
|
+
else:
|
|
451
|
+
prompt_parts.append(str(msg))
|
|
452
|
+
prompts.append(" ".join(prompt_parts))
|
|
453
|
+
else:
|
|
454
|
+
prompts.append(str(message_list))
|
|
455
|
+
|
|
456
|
+
self._handle_llm_start(serialized, prompts, is_async=False, **kwargs)
|
|
457
|
+
|
|
458
|
+
# ---- Async Callback Methods ----
|
|
459
|
+
|
|
460
|
+
@_safe
|
|
461
|
+
async def on_llm_start_async(self, serialized: Dict[str, Any], prompts: List[str], **kwargs) -> None:
|
|
462
|
+
"""Async callback for LLM start."""
|
|
463
|
+
self._handle_llm_start(serialized, prompts, is_async=True, **kwargs)
|
|
464
|
+
|
|
465
|
+
@_safe
|
|
466
|
+
async def on_llm_end_async(self, response: Any, **kwargs) -> None:
|
|
467
|
+
"""Async callback for LLM end."""
|
|
468
|
+
self._handle_llm_end(response, is_async=True, **kwargs)
|
|
469
|
+
|
|
470
|
+
@_safe
|
|
471
|
+
async def on_llm_error_async(self, error: Exception, **kwargs) -> None:
|
|
472
|
+
"""Async callback for LLM error."""
|
|
473
|
+
run_id = kwargs.get('run_id')
|
|
474
|
+
if run_id and run_id in self._active_runs:
|
|
475
|
+
self._active_runs.pop(run_id)
|
|
476
|
+
logger.warning(f"LLM error in async run {run_id}: {error}")
|
|
477
|
+
|
|
478
|
+
@_safe
|
|
479
|
+
async def on_chat_model_start_async(self, serialized: Dict[str, Any], messages: List[List], **kwargs) -> None:
|
|
480
|
+
"""Async callback for chat model start (LangChain specific)."""
|
|
481
|
+
# Convert messages to prompts format for consistency
|
|
482
|
+
prompts = []
|
|
483
|
+
for message_list in messages:
|
|
484
|
+
if isinstance(message_list, list):
|
|
485
|
+
# Extract content from message objects
|
|
486
|
+
prompt_parts = []
|
|
487
|
+
for msg in message_list:
|
|
488
|
+
if hasattr(msg, 'content'):
|
|
489
|
+
prompt_parts.append(str(msg.content))
|
|
490
|
+
else:
|
|
491
|
+
prompt_parts.append(str(msg))
|
|
492
|
+
prompts.append(" ".join(prompt_parts))
|
|
493
|
+
else:
|
|
494
|
+
prompts.append(str(message_list))
|
|
495
|
+
|
|
496
|
+
self._handle_llm_start(serialized, prompts, is_async=True, **kwargs)
|
|
497
|
+
|
|
498
|
+
# ---- Stub Methods for Other Callbacks ----
|
|
499
|
+
|
|
500
|
+
def on_chain_start(self, *args, **kwargs): pass
|
|
501
|
+
def on_chain_end(self, *args, **kwargs): pass
|
|
502
|
+
def on_chain_error(self, *args, **kwargs): pass
|
|
503
|
+
def on_tool_start(self, *args, **kwargs): pass
|
|
504
|
+
def on_tool_end(self, *args, **kwargs): pass
|
|
505
|
+
def on_tool_error(self, *args, **kwargs): pass
|
|
506
|
+
def on_text(self, *args, **kwargs): pass
|
|
507
|
+
def on_agent_action(self, *args, **kwargs): pass
|
|
508
|
+
def on_agent_finish(self, *args, **kwargs): pass
|
|
509
|
+
|
|
510
|
+
# Async versions (delegate to sync)
|
|
511
|
+
async def on_chain_start_async(self, *args, **kwargs): return self.on_chain_start(*args, **kwargs)
|
|
512
|
+
async def on_chain_end_async(self, *args, **kwargs): return self.on_chain_end(*args, **kwargs)
|
|
513
|
+
async def on_chain_error_async(self, *args, **kwargs): return self.on_chain_error(*args, **kwargs)
|
|
514
|
+
async def on_tool_start_async(self, *args, **kwargs): return self.on_tool_start(*args, **kwargs)
|
|
515
|
+
async def on_tool_end_async(self, *args, **kwargs): return self.on_tool_end(*args, **kwargs)
|
|
516
|
+
async def on_tool_error_async(self, *args, **kwargs): return self.on_tool_error(*args, **kwargs)
|
|
517
|
+
|
|
518
|
+
def get_stats(self) -> Dict[str, Any]:
|
|
519
|
+
"""Get statistics about the handler's operation."""
|
|
520
|
+
return {
|
|
521
|
+
'active_runs': len(self._active_runs),
|
|
522
|
+
'total_operations': len(self._operation_timings),
|
|
523
|
+
'has_revenium_middleware': (
|
|
524
|
+
self._create_metering_call != self._fallback_metering_call
|
|
525
|
+
)
|
|
526
|
+
}
|