langfuse-prompt-library-iauro 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.
- langfuse_prompt_library/__init__.py +54 -0
- langfuse_prompt_library/config.py +153 -0
- langfuse_prompt_library/exceptions.py +95 -0
- langfuse_prompt_library/manager.py +663 -0
- langfuse_prompt_library/models.py +42 -0
- langfuse_prompt_library_iauro-0.1.0.dist-info/METADATA +252 -0
- langfuse_prompt_library_iauro-0.1.0.dist-info/RECORD +13 -0
- langfuse_prompt_library_iauro-0.1.0.dist-info/WHEEL +5 -0
- langfuse_prompt_library_iauro-0.1.0.dist-info/licenses/LICENSE +21 -0
- langfuse_prompt_library_iauro-0.1.0.dist-info/top_level.txt +2 -0
- utils/__init__.py +1 -0
- utils/logger.py +122 -0
- utils/utility.py +302 -0
|
@@ -0,0 +1,663 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Core Langfuse manager for handling prompts, tracing, and LLM calls.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
import atexit
|
|
7
|
+
from typing import Optional, Dict, Any, Literal
|
|
8
|
+
from langfuse import Langfuse, get_client
|
|
9
|
+
from langfuse.openai import OpenAI
|
|
10
|
+
|
|
11
|
+
from .config import LangfuseConfig
|
|
12
|
+
from .models import LLMResponse
|
|
13
|
+
from utils.logger import get_logger
|
|
14
|
+
from .exceptions import (
|
|
15
|
+
ProviderError,
|
|
16
|
+
PromptNotFoundError,
|
|
17
|
+
APITimeoutError,
|
|
18
|
+
TracingError,
|
|
19
|
+
ConfigurationError
|
|
20
|
+
)
|
|
21
|
+
from utils.utility import (
|
|
22
|
+
ThreadSafeCache,
|
|
23
|
+
retry_with_backoff,
|
|
24
|
+
validate_prompt_name,
|
|
25
|
+
validate_model_name,
|
|
26
|
+
validate_temperature,
|
|
27
|
+
validate_max_tokens,
|
|
28
|
+
sanitize_user_input
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
# Try to import Anthropic (optional dependency)
|
|
32
|
+
try:
|
|
33
|
+
from anthropic import Anthropic
|
|
34
|
+
ANTHROPIC_AVAILABLE = True
|
|
35
|
+
except ImportError:
|
|
36
|
+
ANTHROPIC_AVAILABLE = False
|
|
37
|
+
Anthropic = None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class LangfuseManager:
|
|
41
|
+
"""Main entry point for Langfuse library.
|
|
42
|
+
|
|
43
|
+
Provides a high-level API for fetching prompts, calling LLMs,
|
|
44
|
+
and automatic tracing/observability.
|
|
45
|
+
|
|
46
|
+
Example:
|
|
47
|
+
>>> lf = LangfuseManager()
|
|
48
|
+
>>> response = lf.call_llm(
|
|
49
|
+
... prompt_name="customer_support_agent",
|
|
50
|
+
... user_input="How do I reset my password?",
|
|
51
|
+
... prompt_label="production"
|
|
52
|
+
... )
|
|
53
|
+
>>> print(response.content)
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
def __init__(self, config: Optional[LangfuseConfig] = None):
|
|
57
|
+
"""Initialize LangfuseManager.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
config: Optional LangfuseConfig. If None, loads from environment.
|
|
61
|
+
|
|
62
|
+
Raises:
|
|
63
|
+
ConfigurationError: If required configuration is missing
|
|
64
|
+
"""
|
|
65
|
+
# Load and validate configuration
|
|
66
|
+
self._config = config or LangfuseConfig.from_env()
|
|
67
|
+
self._config.validate()
|
|
68
|
+
|
|
69
|
+
# Initialize logger
|
|
70
|
+
self._logger = get_logger("langfuse_prompt_library", self._config.log_level)
|
|
71
|
+
|
|
72
|
+
self._logger.info("Initializing LangfuseManager", debug_mode=self._config.debug)
|
|
73
|
+
|
|
74
|
+
# Initialize Langfuse client
|
|
75
|
+
try:
|
|
76
|
+
self._langfuse = Langfuse(
|
|
77
|
+
secret_key=self._config.secret_key,
|
|
78
|
+
public_key=self._config.public_key,
|
|
79
|
+
host=self._config.host
|
|
80
|
+
)
|
|
81
|
+
self._logger.debug("Langfuse client initialized", host=self._config.host)
|
|
82
|
+
except Exception as e:
|
|
83
|
+
self._logger.error("Failed to initialize Langfuse client", exc_info=True)
|
|
84
|
+
raise ConfigurationError(f"Failed to initialize Langfuse client: {str(e)}")
|
|
85
|
+
|
|
86
|
+
# Initialize OpenAI client with Langfuse integration (if API key provided)
|
|
87
|
+
self._openai = None
|
|
88
|
+
if self._config.openai_api_key:
|
|
89
|
+
try:
|
|
90
|
+
self._openai = OpenAI(api_key=self._config.openai_api_key)
|
|
91
|
+
self._logger.debug("OpenAI client initialized")
|
|
92
|
+
except Exception as e:
|
|
93
|
+
self._logger.warning(f"Failed to initialize OpenAI client: {str(e)}")
|
|
94
|
+
|
|
95
|
+
# Initialize Anthropic client (if API key provided and library available)
|
|
96
|
+
self._anthropic = None
|
|
97
|
+
if self._config.anthropic_api_key and ANTHROPIC_AVAILABLE:
|
|
98
|
+
try:
|
|
99
|
+
self._anthropic = Anthropic(api_key=self._config.anthropic_api_key)
|
|
100
|
+
self._logger.debug("Anthropic client initialized")
|
|
101
|
+
except Exception as e:
|
|
102
|
+
self._logger.warning(f"Failed to initialize Anthropic client: {str(e)}")
|
|
103
|
+
|
|
104
|
+
# Initialize thread-safe prompt cache
|
|
105
|
+
self._prompt_cache = ThreadSafeCache(
|
|
106
|
+
default_ttl=self._config.cache_ttl
|
|
107
|
+
) if self._config.enable_caching else None
|
|
108
|
+
|
|
109
|
+
# Register cleanup handler
|
|
110
|
+
if self._config.flush_at_shutdown:
|
|
111
|
+
atexit.register(self._cleanup)
|
|
112
|
+
self._logger.debug("Registered cleanup handler for shutdown")
|
|
113
|
+
|
|
114
|
+
def get_prompt(
|
|
115
|
+
self,
|
|
116
|
+
name: str,
|
|
117
|
+
version: Optional[int] = None,
|
|
118
|
+
label: Optional[str] = None,
|
|
119
|
+
cache: bool = True
|
|
120
|
+
):
|
|
121
|
+
"""Fetch a prompt from Langfuse with optional caching.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
name: Prompt name
|
|
125
|
+
version: Specific version number (optional)
|
|
126
|
+
label: Prompt label like "production" (optional)
|
|
127
|
+
cache: Whether to use cache (default: True)
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
Langfuse prompt object
|
|
131
|
+
|
|
132
|
+
Raises:
|
|
133
|
+
PromptNotFoundError: If prompt is not found
|
|
134
|
+
ValidationError: If parameters are invalid
|
|
135
|
+
|
|
136
|
+
Example:
|
|
137
|
+
>>> prompt = lf.get_prompt("my_prompt", label="production")
|
|
138
|
+
>>> messages = prompt.compile(user_input="Hello")
|
|
139
|
+
"""
|
|
140
|
+
# Validate inputs
|
|
141
|
+
validate_prompt_name(name)
|
|
142
|
+
|
|
143
|
+
# Build cache key
|
|
144
|
+
cache_key = f"{name}:{version or label or 'latest'}"
|
|
145
|
+
|
|
146
|
+
# Check cache if enabled
|
|
147
|
+
if cache and self._prompt_cache is not None:
|
|
148
|
+
cached_prompt = self._prompt_cache.get(cache_key)
|
|
149
|
+
if cached_prompt is not None:
|
|
150
|
+
self._logger.debug("Using cached prompt", prompt_name=name, cache_key=cache_key)
|
|
151
|
+
return cached_prompt
|
|
152
|
+
|
|
153
|
+
# Fetch from Langfuse with retry logic
|
|
154
|
+
try:
|
|
155
|
+
self._logger.debug(
|
|
156
|
+
"Fetching prompt from Langfuse",
|
|
157
|
+
prompt_name=name,
|
|
158
|
+
version=version,
|
|
159
|
+
label=label
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
if version is not None:
|
|
163
|
+
prompt = self._langfuse.get_prompt(name, version=version)
|
|
164
|
+
elif label:
|
|
165
|
+
prompt = self._langfuse.get_prompt(name, label=label)
|
|
166
|
+
else:
|
|
167
|
+
prompt = self._langfuse.get_prompt(name)
|
|
168
|
+
|
|
169
|
+
self._logger.info(
|
|
170
|
+
"Fetched prompt from Langfuse",
|
|
171
|
+
prompt_name=name,
|
|
172
|
+
version=prompt.version
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
# Update cache if enabled
|
|
176
|
+
if cache and self._prompt_cache is not None:
|
|
177
|
+
self._prompt_cache.set(cache_key, prompt)
|
|
178
|
+
|
|
179
|
+
return prompt
|
|
180
|
+
|
|
181
|
+
except Exception as e:
|
|
182
|
+
self._logger.error(
|
|
183
|
+
"Failed to fetch prompt",
|
|
184
|
+
exc_info=True,
|
|
185
|
+
prompt_name=name,
|
|
186
|
+
version=version,
|
|
187
|
+
label=label
|
|
188
|
+
)
|
|
189
|
+
raise PromptNotFoundError(name, version, label)
|
|
190
|
+
|
|
191
|
+
def compile_prompt(self, prompt, **variables) -> list:
|
|
192
|
+
"""Compile a prompt with variables.
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
prompt: Langfuse prompt object
|
|
196
|
+
**variables: Variables to substitute in the prompt
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
List of compiled messages ready for LLM call
|
|
200
|
+
|
|
201
|
+
Example:
|
|
202
|
+
>>> prompt = lf.get_prompt("assistant")
|
|
203
|
+
>>> messages = lf.compile_prompt(prompt, user_input="Hello")
|
|
204
|
+
"""
|
|
205
|
+
return prompt.compile(**variables)
|
|
206
|
+
|
|
207
|
+
def _detect_provider(self, model: str) -> Literal["openai", "anthropic"]:
|
|
208
|
+
"""Detect provider from model name.
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
model: Model name
|
|
212
|
+
|
|
213
|
+
Returns:
|
|
214
|
+
Provider name ("openai" or "anthropic")
|
|
215
|
+
"""
|
|
216
|
+
model_lower = model.lower()
|
|
217
|
+
|
|
218
|
+
# Anthropic models
|
|
219
|
+
if any(name in model_lower for name in ["claude", "anthropic"]):
|
|
220
|
+
return "anthropic"
|
|
221
|
+
|
|
222
|
+
# Default to OpenAI (gpt-*, etc.)
|
|
223
|
+
return "openai"
|
|
224
|
+
|
|
225
|
+
@retry_with_backoff(max_retries=3, initial_delay=1.0)
|
|
226
|
+
def call_llm(
|
|
227
|
+
self,
|
|
228
|
+
prompt_name: str,
|
|
229
|
+
user_input: str,
|
|
230
|
+
prompt_version: Optional[int] = None,
|
|
231
|
+
prompt_label: Optional[str] = None,
|
|
232
|
+
model: str = "gpt-3.5-turbo",
|
|
233
|
+
provider: Optional[Literal["openai", "anthropic"]] = None,
|
|
234
|
+
temperature: float = 0.7,
|
|
235
|
+
max_tokens: int = 500,
|
|
236
|
+
**kwargs
|
|
237
|
+
) -> LLMResponse:
|
|
238
|
+
"""High-level method to fetch prompt, call LLM, and handle tracing.
|
|
239
|
+
|
|
240
|
+
This method automatically:
|
|
241
|
+
- Validates all inputs
|
|
242
|
+
- Fetches the prompt from Langfuse
|
|
243
|
+
- Compiles it with user_input
|
|
244
|
+
- Creates trace and generation observations
|
|
245
|
+
- Calls LLM API (OpenAI or Anthropic) with retry logic
|
|
246
|
+
- Tracks tokens and metadata
|
|
247
|
+
- Handles errors gracefully
|
|
248
|
+
|
|
249
|
+
Args:
|
|
250
|
+
prompt_name: Name of the Langfuse prompt
|
|
251
|
+
user_input: User's query/input to be substituted in prompt
|
|
252
|
+
prompt_version: Optional specific version number
|
|
253
|
+
prompt_label: Optional prompt label (e.g., "production")
|
|
254
|
+
model: Model name (e.g., "gpt-3.5-turbo" or "claude-sonnet-4")
|
|
255
|
+
provider: LLM provider ("openai" or "anthropic"). Auto-detected if None
|
|
256
|
+
temperature: Model temperature (default: 0.7)
|
|
257
|
+
max_tokens: Maximum tokens in response (default: 500)
|
|
258
|
+
**kwargs: Additional parameters for LLM API
|
|
259
|
+
|
|
260
|
+
Returns:
|
|
261
|
+
LLMResponse with content, usage, and metadata
|
|
262
|
+
|
|
263
|
+
Raises:
|
|
264
|
+
ProviderError: If provider is not configured or encounters an error
|
|
265
|
+
ValidationError: If input validation fails
|
|
266
|
+
PromptNotFoundError: If prompt is not found
|
|
267
|
+
Exception: Re-raises any errors after logging to Langfuse
|
|
268
|
+
|
|
269
|
+
Example (OpenAI):
|
|
270
|
+
>>> response = lf.call_llm(
|
|
271
|
+
... prompt_name="assistant",
|
|
272
|
+
... user_input="What is Python?",
|
|
273
|
+
... model="gpt-3.5-turbo"
|
|
274
|
+
... )
|
|
275
|
+
|
|
276
|
+
Example (Anthropic):
|
|
277
|
+
>>> response = lf.call_llm(
|
|
278
|
+
... prompt_name="assistant",
|
|
279
|
+
... user_input="What is Python?",
|
|
280
|
+
... model="claude-sonnet-4"
|
|
281
|
+
... )
|
|
282
|
+
"""
|
|
283
|
+
# Validate inputs
|
|
284
|
+
validate_prompt_name(prompt_name)
|
|
285
|
+
validate_model_name(model)
|
|
286
|
+
validate_temperature(temperature)
|
|
287
|
+
validate_max_tokens(max_tokens)
|
|
288
|
+
user_input = sanitize_user_input(user_input)
|
|
289
|
+
|
|
290
|
+
self._logger.info(
|
|
291
|
+
"Starting LLM call",
|
|
292
|
+
prompt_name=prompt_name,
|
|
293
|
+
model=model,
|
|
294
|
+
temperature=temperature,
|
|
295
|
+
max_tokens=max_tokens
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
# Step 1: Determine provider
|
|
299
|
+
detected_provider = provider or self._detect_provider(model)
|
|
300
|
+
|
|
301
|
+
# Validate provider is configured
|
|
302
|
+
if detected_provider == "openai" and not self._openai:
|
|
303
|
+
raise ProviderError(
|
|
304
|
+
"openai",
|
|
305
|
+
"OpenAI provider not configured. Please set OPENAI_API_KEY environment variable."
|
|
306
|
+
)
|
|
307
|
+
if detected_provider == "anthropic":
|
|
308
|
+
if not ANTHROPIC_AVAILABLE:
|
|
309
|
+
raise ProviderError(
|
|
310
|
+
"anthropic",
|
|
311
|
+
"Anthropic library not installed. Install with: pip install anthropic"
|
|
312
|
+
)
|
|
313
|
+
if not self._anthropic:
|
|
314
|
+
raise ProviderError(
|
|
315
|
+
"anthropic",
|
|
316
|
+
"Anthropic provider not configured. Please set ANTHROPIC_API_KEY environment variable."
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
# Step 2: Fetch prompt
|
|
320
|
+
prompt = self.get_prompt(
|
|
321
|
+
prompt_name,
|
|
322
|
+
version=prompt_version,
|
|
323
|
+
label=prompt_label
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
# Step 3: Compile prompt with user input
|
|
327
|
+
messages = prompt.compile(user_input=user_input)
|
|
328
|
+
|
|
329
|
+
# Step 4: Get Langfuse client for tracing
|
|
330
|
+
lf_client = get_client()
|
|
331
|
+
|
|
332
|
+
# Step 5: Update trace metadata
|
|
333
|
+
lf_client.update_current_trace(
|
|
334
|
+
metadata={
|
|
335
|
+
"prompt_name": prompt_name,
|
|
336
|
+
"prompt_version": prompt.version,
|
|
337
|
+
"user_query": user_input,
|
|
338
|
+
"model": model,
|
|
339
|
+
"provider": detected_provider
|
|
340
|
+
}
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
# Step 6: Call appropriate provider
|
|
344
|
+
if detected_provider == "openai":
|
|
345
|
+
return self._call_openai(
|
|
346
|
+
lf_client, messages, model, temperature, max_tokens,
|
|
347
|
+
prompt_name, prompt.version, **kwargs
|
|
348
|
+
)
|
|
349
|
+
else: # anthropic
|
|
350
|
+
return self._call_anthropic(
|
|
351
|
+
lf_client, messages, model, temperature, max_tokens,
|
|
352
|
+
prompt_name, prompt.version, **kwargs
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
def flush(self) -> None:
|
|
356
|
+
"""Flush pending traces to Langfuse.
|
|
357
|
+
|
|
358
|
+
Call this before application shutdown to ensure all traces are sent.
|
|
359
|
+
|
|
360
|
+
Example:
|
|
361
|
+
>>> lf = LangfuseManager()
|
|
362
|
+
>>> # ... make LLM calls ...
|
|
363
|
+
>>> lf.flush() # Ensure all traces are sent
|
|
364
|
+
"""
|
|
365
|
+
try:
|
|
366
|
+
self._logger.info("Flushing traces to Langfuse")
|
|
367
|
+
self._langfuse.flush()
|
|
368
|
+
self._logger.info("Successfully flushed traces")
|
|
369
|
+
except Exception as e:
|
|
370
|
+
self._logger.error("Failed to flush traces", exc_info=True)
|
|
371
|
+
raise TracingError("Failed to flush traces", e)
|
|
372
|
+
|
|
373
|
+
def _cleanup(self) -> None:
|
|
374
|
+
"""Cleanup handler called at shutdown."""
|
|
375
|
+
try:
|
|
376
|
+
self._logger.info("Running cleanup at shutdown")
|
|
377
|
+
|
|
378
|
+
# Log cache statistics if caching is enabled
|
|
379
|
+
if self._prompt_cache is not None:
|
|
380
|
+
stats = self._prompt_cache.get_stats()
|
|
381
|
+
self._logger.info("Cache statistics", **stats)
|
|
382
|
+
|
|
383
|
+
# Flush traces
|
|
384
|
+
self.flush()
|
|
385
|
+
|
|
386
|
+
self._logger.info("Cleanup completed successfully")
|
|
387
|
+
except Exception as e:
|
|
388
|
+
self._logger.error("Error during cleanup", exc_info=True)
|
|
389
|
+
|
|
390
|
+
def _call_openai(
|
|
391
|
+
self,
|
|
392
|
+
lf_client,
|
|
393
|
+
messages: list,
|
|
394
|
+
model: str,
|
|
395
|
+
temperature: float,
|
|
396
|
+
max_tokens: int,
|
|
397
|
+
prompt_name: str,
|
|
398
|
+
prompt_version: int,
|
|
399
|
+
**kwargs
|
|
400
|
+
) -> LLMResponse:
|
|
401
|
+
"""Call OpenAI API with Langfuse tracing and error handling."""
|
|
402
|
+
self._logger.debug("Calling OpenAI API", model=model)
|
|
403
|
+
|
|
404
|
+
with lf_client.start_as_current_observation(
|
|
405
|
+
as_type="generation",
|
|
406
|
+
name=f"{model}-call",
|
|
407
|
+
model=model,
|
|
408
|
+
model_parameters={
|
|
409
|
+
"temperature": temperature,
|
|
410
|
+
"max_tokens": max_tokens,
|
|
411
|
+
**kwargs
|
|
412
|
+
},
|
|
413
|
+
input=messages
|
|
414
|
+
) as generation:
|
|
415
|
+
try:
|
|
416
|
+
response = self._openai.chat.completions.create(
|
|
417
|
+
model=model,
|
|
418
|
+
messages=messages,
|
|
419
|
+
temperature=temperature,
|
|
420
|
+
max_tokens=max_tokens,
|
|
421
|
+
timeout=self._config.request_timeout,
|
|
422
|
+
**kwargs
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
content = response.choices[0].message.content
|
|
426
|
+
usage = self._extract_openai_usage(response)
|
|
427
|
+
|
|
428
|
+
self._logger.info(
|
|
429
|
+
"OpenAI API call successful",
|
|
430
|
+
model=model,
|
|
431
|
+
tokens_used=usage["total"]
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
generation.update(
|
|
435
|
+
output=content,
|
|
436
|
+
usage_details={
|
|
437
|
+
"input": usage["input"],
|
|
438
|
+
"output": usage["output"],
|
|
439
|
+
"total": usage["total"]
|
|
440
|
+
}
|
|
441
|
+
)
|
|
442
|
+
|
|
443
|
+
return LLMResponse(
|
|
444
|
+
content=content,
|
|
445
|
+
model=model,
|
|
446
|
+
usage=usage,
|
|
447
|
+
metadata={
|
|
448
|
+
"prompt_name": prompt_name,
|
|
449
|
+
"prompt_version": prompt_version,
|
|
450
|
+
"provider": "openai",
|
|
451
|
+
"temperature": temperature,
|
|
452
|
+
"max_tokens": max_tokens
|
|
453
|
+
},
|
|
454
|
+
raw_response=response
|
|
455
|
+
)
|
|
456
|
+
|
|
457
|
+
except Exception as e:
|
|
458
|
+
self._logger.error(
|
|
459
|
+
"OpenAI API call failed",
|
|
460
|
+
exc_info=True,
|
|
461
|
+
model=model,
|
|
462
|
+
error=str(e)
|
|
463
|
+
)
|
|
464
|
+
generation.update(level="ERROR", status_message=str(e))
|
|
465
|
+
raise ProviderError("openai", str(e), e)
|
|
466
|
+
|
|
467
|
+
def _call_anthropic(
|
|
468
|
+
self,
|
|
469
|
+
lf_client,
|
|
470
|
+
messages: list,
|
|
471
|
+
model: str,
|
|
472
|
+
temperature: float,
|
|
473
|
+
max_tokens: int,
|
|
474
|
+
prompt_name: str,
|
|
475
|
+
prompt_version: int,
|
|
476
|
+
**kwargs
|
|
477
|
+
) -> LLMResponse:
|
|
478
|
+
"""Call Anthropic API with Langfuse tracing and error handling."""
|
|
479
|
+
self._logger.debug("Calling Anthropic API", model=model)
|
|
480
|
+
|
|
481
|
+
with lf_client.start_as_current_observation(
|
|
482
|
+
as_type="generation",
|
|
483
|
+
name=f"{model}-call",
|
|
484
|
+
model=model,
|
|
485
|
+
model_parameters={
|
|
486
|
+
"temperature": temperature,
|
|
487
|
+
"max_tokens": max_tokens,
|
|
488
|
+
**kwargs
|
|
489
|
+
},
|
|
490
|
+
input=messages
|
|
491
|
+
) as generation:
|
|
492
|
+
try:
|
|
493
|
+
# Convert OpenAI format to Anthropic format
|
|
494
|
+
system_message = next((m["content"] for m in messages if m["role"] == "system"), "")
|
|
495
|
+
user_messages = [m for m in messages if m["role"] != "system"]
|
|
496
|
+
|
|
497
|
+
response = self._anthropic.messages.create(
|
|
498
|
+
model=model,
|
|
499
|
+
system=system_message,
|
|
500
|
+
messages=user_messages,
|
|
501
|
+
temperature=temperature,
|
|
502
|
+
max_tokens=max_tokens,
|
|
503
|
+
timeout=self._config.request_timeout,
|
|
504
|
+
**kwargs
|
|
505
|
+
)
|
|
506
|
+
|
|
507
|
+
content = response.content[0].text
|
|
508
|
+
usage = self._extract_anthropic_usage(response)
|
|
509
|
+
|
|
510
|
+
self._logger.info(
|
|
511
|
+
"Anthropic API call successful",
|
|
512
|
+
model=model,
|
|
513
|
+
tokens_used=usage["total"]
|
|
514
|
+
)
|
|
515
|
+
|
|
516
|
+
generation.update(
|
|
517
|
+
output=content,
|
|
518
|
+
usage_details={
|
|
519
|
+
"input": usage["input"],
|
|
520
|
+
"output": usage["output"],
|
|
521
|
+
"total": usage["total"]
|
|
522
|
+
}
|
|
523
|
+
)
|
|
524
|
+
|
|
525
|
+
return LLMResponse(
|
|
526
|
+
content=content,
|
|
527
|
+
model=model,
|
|
528
|
+
usage=usage,
|
|
529
|
+
metadata={
|
|
530
|
+
"prompt_name": prompt_name,
|
|
531
|
+
"prompt_version": prompt_version,
|
|
532
|
+
"provider": "anthropic",
|
|
533
|
+
"temperature": temperature,
|
|
534
|
+
"max_tokens": max_tokens
|
|
535
|
+
},
|
|
536
|
+
raw_response=response
|
|
537
|
+
)
|
|
538
|
+
|
|
539
|
+
except Exception as e:
|
|
540
|
+
self._logger.error(
|
|
541
|
+
"Anthropic API call failed",
|
|
542
|
+
exc_info=True,
|
|
543
|
+
model=model,
|
|
544
|
+
error=str(e)
|
|
545
|
+
)
|
|
546
|
+
generation.update(level="ERROR", status_message=str(e))
|
|
547
|
+
raise ProviderError("anthropic", str(e), e)
|
|
548
|
+
|
|
549
|
+
def _extract_openai_usage(self, response) -> Dict[str, int]:
|
|
550
|
+
"""Extract token usage from OpenAI response."""
|
|
551
|
+
return {
|
|
552
|
+
"input": response.usage.prompt_tokens,
|
|
553
|
+
"output": response.usage.completion_tokens,
|
|
554
|
+
"total": response.usage.total_tokens
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
def _extract_anthropic_usage(self, response) -> Dict[str, int]:
|
|
558
|
+
"""Extract token usage from Anthropic response."""
|
|
559
|
+
return {
|
|
560
|
+
"input": response.usage.input_tokens,
|
|
561
|
+
"output": response.usage.output_tokens,
|
|
562
|
+
"total": response.usage.input_tokens + response.usage.output_tokens
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
def get_prompt_by_version_and_label(
|
|
567
|
+
self,
|
|
568
|
+
name: str,
|
|
569
|
+
version: int,
|
|
570
|
+
expected_label: str,
|
|
571
|
+
cache: bool = True
|
|
572
|
+
):
|
|
573
|
+
"""Get prompt by version and validate it has the expected label.
|
|
574
|
+
|
|
575
|
+
This method fetches a specific prompt version and validates that it has
|
|
576
|
+
the expected label. Raises an error if the validation fails.
|
|
577
|
+
|
|
578
|
+
Args:
|
|
579
|
+
name: Prompt name
|
|
580
|
+
version: Specific version number
|
|
581
|
+
expected_label: Label that the version must have
|
|
582
|
+
cache: Whether to use cache (default: True)
|
|
583
|
+
|
|
584
|
+
Returns:
|
|
585
|
+
Langfuse prompt object if validation passes
|
|
586
|
+
|
|
587
|
+
Raises:
|
|
588
|
+
ValidationError: If the version doesn't have the expected label
|
|
589
|
+
PromptNotFoundError: If prompt is not found
|
|
590
|
+
|
|
591
|
+
Example:
|
|
592
|
+
>>> lf = LangfuseManager()
|
|
593
|
+
>>> prompt = lf.get_prompt_by_version_and_label(
|
|
594
|
+
... name="customer_support_agent",
|
|
595
|
+
... version=3,
|
|
596
|
+
... expected_label="production"
|
|
597
|
+
... )
|
|
598
|
+
"""
|
|
599
|
+
from .exceptions import ValidationError
|
|
600
|
+
|
|
601
|
+
# Get prompt by version
|
|
602
|
+
prompt = self.get_prompt(name=name, version=version, cache=cache)
|
|
603
|
+
|
|
604
|
+
# Validate it has the expected label
|
|
605
|
+
if expected_label not in prompt.labels:
|
|
606
|
+
error_msg = (
|
|
607
|
+
f"Validation failed: Version {version} of '{name}' does not have label '{expected_label}'. "
|
|
608
|
+
f"Available labels: {prompt.labels}"
|
|
609
|
+
)
|
|
610
|
+
self._logger.error(error_msg, prompt_name=name, version=version)
|
|
611
|
+
raise ValidationError("prompt_label", error_msg)
|
|
612
|
+
|
|
613
|
+
self._logger.info(
|
|
614
|
+
"Validation passed",
|
|
615
|
+
prompt_name=name,
|
|
616
|
+
version=version,
|
|
617
|
+
label=expected_label
|
|
618
|
+
)
|
|
619
|
+
|
|
620
|
+
return prompt
|
|
621
|
+
|
|
622
|
+
def get_cache_stats(self) -> Optional[Dict[str, Any]]:
|
|
623
|
+
"""Get cache statistics.
|
|
624
|
+
|
|
625
|
+
Returns:
|
|
626
|
+
Dictionary with cache statistics or None if caching is disabled
|
|
627
|
+
"""
|
|
628
|
+
if self._prompt_cache is not None:
|
|
629
|
+
return self._prompt_cache.get_stats()
|
|
630
|
+
return None
|
|
631
|
+
|
|
632
|
+
def clear_cache(self) -> None:
|
|
633
|
+
"""Clear the prompt cache."""
|
|
634
|
+
if self._prompt_cache is not None:
|
|
635
|
+
self._prompt_cache.clear()
|
|
636
|
+
self._logger.info("Prompt cache cleared")
|
|
637
|
+
|
|
638
|
+
@property
|
|
639
|
+
def config(self) -> LangfuseConfig:
|
|
640
|
+
"""Get current configuration.
|
|
641
|
+
|
|
642
|
+
Returns:
|
|
643
|
+
LangfuseConfig instance
|
|
644
|
+
"""
|
|
645
|
+
return self._config
|
|
646
|
+
|
|
647
|
+
@property
|
|
648
|
+
def langfuse_client(self) -> Langfuse:
|
|
649
|
+
"""Get underlying Langfuse client for advanced usage.
|
|
650
|
+
|
|
651
|
+
Returns:
|
|
652
|
+
Langfuse client instance
|
|
653
|
+
"""
|
|
654
|
+
return self._langfuse
|
|
655
|
+
|
|
656
|
+
@property
|
|
657
|
+
def openai_client(self) -> OpenAI:
|
|
658
|
+
"""Get underlying OpenAI client for advanced usage.
|
|
659
|
+
|
|
660
|
+
Returns:
|
|
661
|
+
OpenAI client instance
|
|
662
|
+
"""
|
|
663
|
+
return self._openai
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Data models for Langfuse library responses.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Dict, Any, Optional
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class LLMResponse:
|
|
11
|
+
"""Normalized response from LLM providers.
|
|
12
|
+
|
|
13
|
+
Attributes:
|
|
14
|
+
content: The text content of the LLM response
|
|
15
|
+
model: The model name used for the generation
|
|
16
|
+
usage: Token usage information (input, output, total)
|
|
17
|
+
metadata: Additional metadata about the generation
|
|
18
|
+
raw_response: The raw response object from the provider (optional)
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
content: str
|
|
22
|
+
model: str
|
|
23
|
+
usage: Dict[str, int]
|
|
24
|
+
metadata: Optional[Dict[str, Any]] = None
|
|
25
|
+
raw_response: Optional[Any] = None
|
|
26
|
+
|
|
27
|
+
def __str__(self) -> str:
|
|
28
|
+
"""String representation showing the content."""
|
|
29
|
+
return self.content
|
|
30
|
+
|
|
31
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
32
|
+
"""Convert response to dictionary.
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
Dictionary representation of the response
|
|
36
|
+
"""
|
|
37
|
+
return {
|
|
38
|
+
"content": self.content,
|
|
39
|
+
"model": self.model,
|
|
40
|
+
"usage": self.usage,
|
|
41
|
+
"metadata": self.metadata
|
|
42
|
+
}
|