diff --git a/langfuse/_client/client.py b/langfuse/_client/client.py index ade8f3c6d..adc543639 100644 --- a/langfuse/_client/client.py +++ b/langfuse/_client/client.py @@ -22,11 +22,7 @@ _agnosticcontextmanager, ) -from langfuse._client.attributes import ( - LangfuseOtelSpanAttributes, - create_generation_attributes, - create_span_attributes, -) +from langfuse._client.attributes import LangfuseOtelSpanAttributes from langfuse._client.datasets import DatasetClient, DatasetItemClient from langfuse._client.environment_variables import ( LANGFUSE_DEBUG, @@ -142,6 +138,9 @@ class Langfuse: ``` """ + _resources: Optional[LangfuseResourceManager] = None + _mask: Optional[MaskFunction] = None + def __init__( self, *, @@ -162,7 +161,6 @@ def __init__( ): self._host = host or os.environ.get(LANGFUSE_HOST, "https://cloud.langfuse.com") self._environment = environment or os.environ.get(LANGFUSE_TRACING_ENVIRONMENT) - self._mask = mask self._project_id = None sample_rate = sample_rate or float(os.environ.get(LANGFUSE_SAMPLE_RATE, 1.0)) if not 0.0 <= sample_rate <= 1.0: @@ -191,7 +189,6 @@ def __init__( langfuse_logger.warning( "Authentication error: Langfuse client initialized without public_key. Client will be disabled. " "Provide a public_key parameter or set LANGFUSE_PUBLIC_KEY environment variable. " - "See documentation: https://langfuse.com/docs/sdk/python/low-level-sdk#initialize-client" ) self._otel_tracer = otel_trace_api.NoOpTracer() return @@ -201,7 +198,6 @@ def __init__( langfuse_logger.warning( "Authentication error: Langfuse client initialized without secret_key. Client will be disabled. " "Provide a secret_key parameter or set LANGFUSE_SECRET_KEY environment variable. " - "See documentation: https://langfuse.com/docs/sdk/python/low-level-sdk#initialize-client" ) self._otel_tracer = otel_trace_api.NoOpTracer() return @@ -219,7 +215,9 @@ def __init__( httpx_client=httpx_client, media_upload_thread_count=media_upload_thread_count, sample_rate=sample_rate, + mask=mask, ) + self._mask = self._resources.mask self._otel_tracer = ( self._resources.tracer @@ -271,15 +269,6 @@ def start_span( span.end() ``` """ - attributes = create_span_attributes( - input=input, - output=output, - metadata=metadata, - version=version, - level=level, - status_message=status_message, - ) - if trace_context: trace_id = trace_context.get("trace_id", None) parent_span_id = trace_context.get("parent_span_id", None) @@ -292,29 +281,33 @@ def start_span( with otel_trace_api.use_span( cast(otel_trace_api.Span, remote_parent_span) ): - otel_span = self._otel_tracer.start_span( - name=name, attributes=attributes - ) + otel_span = self._otel_tracer.start_span(name=name) otel_span.set_attribute(LangfuseOtelSpanAttributes.AS_ROOT, True) return LangfuseSpan( otel_span=otel_span, langfuse_client=self, + environment=self._environment, input=input, output=output, metadata=metadata, - environment=self._environment, + version=version, + level=level, + status_message=status_message, ) - otel_span = self._otel_tracer.start_span(name=name, attributes=attributes) + otel_span = self._otel_tracer.start_span(name=name) return LangfuseSpan( otel_span=otel_span, langfuse_client=self, + environment=self._environment, input=input, output=output, metadata=metadata, - environment=self._environment, + version=version, + level=level, + status_message=status_message, ) def start_as_current_span( @@ -365,15 +358,6 @@ def start_as_current_span( child_span.update(output="sub-result") ``` """ - attributes = create_span_attributes( - input=input, - output=output, - metadata=metadata, - version=version, - level=level, - status_message=status_message, - ) - if trace_context: trace_id = trace_context.get("trace_id", None) parent_span_id = trace_context.get("parent_span_id", None) @@ -388,13 +372,15 @@ def start_as_current_span( self._create_span_with_parent_context( as_type="span", name=name, - attributes=attributes, remote_parent_span=remote_parent_span, parent=None, + end_on_exit=end_on_exit, input=input, output=output, metadata=metadata, - end_on_exit=end_on_exit, + version=version, + level=level, + status_message=status_message, ), ) @@ -403,11 +389,13 @@ def start_as_current_span( self._start_as_current_otel_span_with_processed_media( as_type="span", name=name, - attributes=attributes, + end_on_exit=end_on_exit, input=input, output=output, metadata=metadata, - end_on_exit=end_on_exit, + version=version, + level=level, + status_message=status_message, ), ) @@ -479,21 +467,6 @@ def start_generation( generation.end() ``` """ - attributes = create_generation_attributes( - input=input, - output=output, - metadata=metadata, - version=version, - level=level, - status_message=status_message, - completion_start_time=completion_start_time, - model=model, - model_parameters=model_parameters, - usage_details=usage_details, - cost_details=cost_details, - prompt=prompt, - ) - if trace_context: trace_id = trace_context.get("trace_id", None) parent_span_id = trace_context.get("parent_span_id", None) @@ -506,9 +479,7 @@ def start_generation( with otel_trace_api.use_span( cast(otel_trace_api.Span, remote_parent_span) ): - otel_span = self._otel_tracer.start_span( - name=name, attributes=attributes - ) + otel_span = self._otel_tracer.start_span(name=name) otel_span.set_attribute(LangfuseOtelSpanAttributes.AS_ROOT, True) return LangfuseGeneration( @@ -517,9 +488,18 @@ def start_generation( input=input, output=output, metadata=metadata, + version=version, + level=level, + status_message=status_message, + completion_start_time=completion_start_time, + model=model, + model_parameters=model_parameters, + usage_details=usage_details, + cost_details=cost_details, + prompt=prompt, ) - otel_span = self._otel_tracer.start_span(name=name, attributes=attributes) + otel_span = self._otel_tracer.start_span(name=name) return LangfuseGeneration( otel_span=otel_span, @@ -527,6 +507,15 @@ def start_generation( input=input, output=output, metadata=metadata, + version=version, + level=level, + status_message=status_message, + completion_start_time=completion_start_time, + model=model, + model_parameters=model_parameters, + usage_details=usage_details, + cost_details=cost_details, + prompt=prompt, ) def start_as_current_generation( @@ -596,21 +585,6 @@ def start_as_current_generation( ) ``` """ - attributes = create_generation_attributes( - input=input, - output=output, - metadata=metadata, - version=version, - level=level, - status_message=status_message, - completion_start_time=completion_start_time, - model=model, - model_parameters=model_parameters, - usage_details=usage_details, - cost_details=cost_details, - prompt=prompt, - ) - if trace_context: trace_id = trace_context.get("trace_id", None) parent_span_id = trace_context.get("parent_span_id", None) @@ -625,13 +599,21 @@ def start_as_current_generation( self._create_span_with_parent_context( as_type="generation", name=name, - attributes=attributes, remote_parent_span=remote_parent_span, parent=None, + end_on_exit=end_on_exit, input=input, output=output, metadata=metadata, - end_on_exit=end_on_exit, + version=version, + level=level, + status_message=status_message, + completion_start_time=completion_start_time, + model=model, + model_parameters=model_parameters, + usage_details=usage_details, + cost_details=cost_details, + prompt=prompt, ), ) @@ -640,11 +622,19 @@ def start_as_current_generation( self._start_as_current_otel_span_with_processed_media( as_type="generation", name=name, - attributes=attributes, + end_on_exit=end_on_exit, input=input, output=output, metadata=metadata, - end_on_exit=end_on_exit, + version=version, + level=level, + status_message=status_message, + completion_start_time=completion_start_time, + model=model, + model_parameters=model_parameters, + usage_details=usage_details, + cost_details=cost_details, + prompt=prompt, ), ) @@ -655,24 +645,40 @@ def _create_span_with_parent_context( name, parent, remote_parent_span, - attributes, as_type: Literal["generation", "span"], + end_on_exit: Optional[bool] = None, input: Optional[Any] = None, output: Optional[Any] = None, metadata: Optional[Any] = None, - end_on_exit: Optional[bool] = None, + version: Optional[str] = None, + level: Optional[SpanLevel] = None, + status_message: Optional[str] = None, + completion_start_time: Optional[datetime] = None, + model: Optional[str] = None, + model_parameters: Optional[Dict[str, MapValue]] = None, + usage_details: Optional[Dict[str, int]] = None, + cost_details: Optional[Dict[str, float]] = None, + prompt: Optional[PromptClient] = None, ): parent_span = parent or cast(otel_trace_api.Span, remote_parent_span) with otel_trace_api.use_span(parent_span): with self._start_as_current_otel_span_with_processed_media( name=name, - attributes=attributes, as_type=as_type, + end_on_exit=end_on_exit, input=input, output=output, metadata=metadata, - end_on_exit=end_on_exit, + version=version, + level=level, + status_message=status_message, + completion_start_time=completion_start_time, + model=model, + model_parameters=model_parameters, + usage_details=usage_details, + cost_details=cost_details, + prompt=prompt, ) as langfuse_span: if remote_parent_span is not None: langfuse_span._otel_span.set_attribute( @@ -686,35 +692,54 @@ def _start_as_current_otel_span_with_processed_media( self, *, name: str, - attributes: Dict[str, str], as_type: Optional[Literal["generation", "span"]] = None, + end_on_exit: Optional[bool] = None, input: Optional[Any] = None, output: Optional[Any] = None, metadata: Optional[Any] = None, - end_on_exit: Optional[bool] = None, + version: Optional[str] = None, + level: Optional[SpanLevel] = None, + status_message: Optional[str] = None, + completion_start_time: Optional[datetime] = None, + model: Optional[str] = None, + model_parameters: Optional[Dict[str, MapValue]] = None, + usage_details: Optional[Dict[str, int]] = None, + cost_details: Optional[Dict[str, float]] = None, + prompt: Optional[PromptClient] = None, ): with self._otel_tracer.start_as_current_span( name=name, - attributes=attributes, end_on_exit=end_on_exit if end_on_exit is not None else True, ) as otel_span: yield ( LangfuseSpan( otel_span=otel_span, langfuse_client=self, + environment=self._environment, input=input, output=output, metadata=metadata, - environment=self._environment, + version=version, + level=level, + status_message=status_message, ) if as_type == "span" else LangfuseGeneration( otel_span=otel_span, langfuse_client=self, + environment=self._environment, input=input, output=output, metadata=metadata, - environment=self._environment, + version=version, + level=level, + status_message=status_message, + completion_start_time=completion_start_time, + model=model, + model_parameters=model_parameters, + usage_details=usage_details, + cost_details=cost_details, + prompt=prompt, ) ) @@ -986,14 +1011,6 @@ def create_event( ``` """ timestamp = time_ns() - attributes = create_span_attributes( - input=input, - output=output, - metadata=metadata, - version=version, - level=level, - status_message=status_message, - ) if trace_context: trace_id = trace_context.get("trace_id", None) @@ -1008,30 +1025,34 @@ def create_event( cast(otel_trace_api.Span, remote_parent_span) ): otel_span = self._otel_tracer.start_span( - name=name, attributes=attributes, start_time=timestamp + name=name, start_time=timestamp ) otel_span.set_attribute(LangfuseOtelSpanAttributes.AS_ROOT, True) return LangfuseEvent( otel_span=otel_span, langfuse_client=self, + environment=self._environment, input=input, output=output, metadata=metadata, - environment=self._environment, + version=version, + level=level, + status_message=status_message, ).end(end_time=timestamp) - otel_span = self._otel_tracer.start_span( - name=name, attributes=attributes, start_time=timestamp - ) + otel_span = self._otel_tracer.start_span(name=name, start_time=timestamp) return LangfuseEvent( otel_span=otel_span, langfuse_client=self, + environment=self._environment, input=input, output=output, metadata=metadata, - environment=self._environment, + version=version, + level=level, + status_message=status_message, ).end(end_time=timestamp) def _create_remote_parent_span( @@ -1304,22 +1325,20 @@ def create_score( score_id = score_id or self._create_observation_id() try: - score_event = { - "id": score_id, - "session_id": session_id, - "dataset_run_id": dataset_run_id, - "trace_id": trace_id, - "observation_id": observation_id, - "name": name, - "value": value, - "data_type": data_type, - "comment": comment, - "config_id": config_id, - "environment": self._environment, - "metadata": metadata, - } - - new_body = ScoreBody(**score_event) + new_body = ScoreBody( + id=score_id, + session_id=session_id, + dataset_run_id=dataset_run_id, + trace_id=trace_id, + observation_id=observation_id, + name=name, + value=value, + data_type=data_type, + comment=comment, + config_id=config_id, + environment=self._environment, + metadata=metadata, + ) event = { "id": self.create_trace_id(), @@ -1327,7 +1346,17 @@ def create_score( "timestamp": _get_timestamp(), "body": new_body, } - self._resources.add_score_task(event) + + if self._resources is not None: + # Force the score to be in sample if it was for a legacy trace ID, i.e. non-32 hexchar + force_sample = ( + not self._is_valid_trace_id(trace_id) if trace_id else True + ) + + self._resources.add_score_task( + event, + force_sample=force_sample, + ) except Exception as e: langfuse_logger.exception( @@ -1521,7 +1550,8 @@ def flush(self): # Continue with other work ``` """ - self._resources.flush() + if self._resources is not None: + self._resources.flush() def shutdown(self): """Shut down the Langfuse client and flush all pending data. @@ -1545,7 +1575,8 @@ def shutdown(self): langfuse.shutdown() ``` """ - self._resources.shutdown() + if self._resources is not None: + self._resources.shutdown() def get_current_trace_id(self) -> Optional[str]: """Get the trace ID of the current active span. @@ -1927,6 +1958,10 @@ def get_prompt( Exception: Propagates any exceptions raised during the fetching of a new prompt, unless there is an expired prompt in the cache, in which case it logs a warning and returns the expired prompt. """ + if self._resources is None: + raise Error( + "SDK is not correctly initalized. Check the init logs for more details." + ) if version is not None and label is not None: raise ValueError("Cannot specify both version and label at the same time.") @@ -1960,7 +1995,7 @@ def get_prompt( f"Returning fallback prompt for '{cache_key}' due to fetch error: {e}" ) - fallback_client_args = { + fallback_client_args: Dict[str, Any] = { "name": name, "prompt": fallback, "type": type, @@ -2052,7 +2087,8 @@ def fetch_prompts(): else: prompt = TextPromptClient(prompt_response) - self._resources.prompt_cache.set(cache_key, prompt, ttl_seconds) + if self._resources is not None: + self._resources.prompt_cache.set(cache_key, prompt, ttl_seconds) return prompt @@ -2151,7 +2187,8 @@ def create_prompt( ) server_prompt = self.api.prompts.create(request=request) - self._resources.prompt_cache.invalidate(name) + if self._resources is not None: + self._resources.prompt_cache.invalidate(name) return ChatPromptClient(prompt=cast(Prompt_Chat, server_prompt)) @@ -2170,7 +2207,8 @@ def create_prompt( server_prompt = self.api.prompts.create(request=request) - self._resources.prompt_cache.invalidate(name) + if self._resources is not None: + self._resources.prompt_cache.invalidate(name) return TextPromptClient(prompt=cast(Prompt_Text, server_prompt)) @@ -2201,7 +2239,9 @@ def update_prompt( version=version, new_labels=new_labels, ) - self._resources.prompt_cache.invalidate(name) + + if self._resources is not None: + self._resources.prompt_cache.invalidate(name) return updated_prompt diff --git a/langfuse/_client/get_client.py b/langfuse/_client/get_client.py index fe891e05a..b8eaa198c 100644 --- a/langfuse/_client/get_client.py +++ b/langfuse/_client/get_client.py @@ -56,7 +56,16 @@ def get_client(*, public_key: Optional[str] = None) -> Langfuse: if len(active_instances) == 1: # Only one client exists, safe to use without specifying key - return Langfuse(public_key=public_key) + instance = list(active_instances.values())[0] + + # Initialize with the credentials bound to the instance + # This is important if the original instance was instantiated + # via constructor arguments + return Langfuse( + public_key=instance.public_key, + secret_key=instance.secret_key, + host=instance.host, + ) else: # Multiple clients exist but no key specified - disable tracing diff --git a/langfuse/_client/resource_manager.py b/langfuse/_client/resource_manager.py index 548a637d9..5eceb3fe5 100644 --- a/langfuse/_client/resource_manager.py +++ b/langfuse/_client/resource_manager.py @@ -18,7 +18,7 @@ import os import threading from queue import Full, Queue -from typing import Dict, Optional, cast +from typing import Any, Dict, Optional, cast import httpx from opentelemetry import trace as otel_trace_api @@ -43,6 +43,7 @@ from langfuse._utils.request import LangfuseClient from langfuse.api.client import AsyncFernLangfuse, FernLangfuse from langfuse.logger import langfuse_logger +from langfuse.types import MaskFunction from ..version import __version__ as langfuse_version @@ -90,6 +91,7 @@ def __new__( httpx_client: Optional[httpx.Client] = None, media_upload_thread_count: Optional[int] = None, sample_rate: Optional[float] = None, + mask: Optional[MaskFunction] = None, ) -> "LangfuseResourceManager": if public_key in cls._instances: return cls._instances[public_key] @@ -110,6 +112,7 @@ def __new__( httpx_client=httpx_client, media_upload_thread_count=media_upload_thread_count, sample_rate=sample_rate, + mask=mask, ) cls._instances[public_key] = instance @@ -130,8 +133,12 @@ def _initialize_instance( media_upload_thread_count: Optional[int] = None, httpx_client: Optional[httpx.Client] = None, sample_rate: Optional[float] = None, + mask: Optional[MaskFunction] = None, ): self.public_key = public_key + self.secret_key = secret_key + self.host = host + self.mask = mask # OTEL Tracer tracer_provider = _init_tracer_provider( @@ -148,7 +155,7 @@ def _initialize_instance( ) tracer_provider.add_span_processor(langfuse_processor) - tracer_provider = otel_trace_api.get_tracer_provider() + tracer_provider = cast(TracerProvider, otel_trace_api.get_tracer_provider()) self._otel_tracer = tracer_provider.get_tracer( LANGFUSE_TRACER_NAME, langfuse_version, @@ -195,7 +202,7 @@ def _initialize_instance( LANGFUSE_MEDIA_UPLOAD_ENABLED, "True" ).lower() not in ("false", "0") - self._media_upload_queue = Queue(100_000) + self._media_upload_queue: Queue[Any] = Queue(100_000) self._media_manager = MediaManager( api_client=self.api, media_upload_queue=self._media_upload_queue, @@ -220,7 +227,7 @@ def _initialize_instance( self.prompt_cache = PromptCache() # Score ingestion - self._score_ingestion_queue = Queue(100_000) + self._score_ingestion_queue: Queue[Any] = Queue(100_000) self._ingestion_consumers = [] ingestion_consumer = ScoreIngestionConsumer( @@ -249,13 +256,17 @@ def _initialize_instance( @classmethod def reset(cls): - cls._instances.clear() + with cls._lock: + for key in cls._instances: + cls._instances[key].shutdown() + + cls._instances.clear() - def add_score_task(self, event: dict): + def add_score_task(self, event: dict, *, force_sample: bool = False): try: # Sample scores with the same sampler that is used for tracing tracer_provider = cast(TracerProvider, otel_trace_api.get_tracer_provider()) - should_sample = ( + should_sample = force_sample or ( ( tracer_provider.sampler.should_sample( parent_context=None, diff --git a/langfuse/_client/span.py b/langfuse/_client/span.py index 39d5c62eb..f3c5ef920 100644 --- a/langfuse/_client/span.py +++ b/langfuse/_client/span.py @@ -69,6 +69,15 @@ def __init__( output: Optional[Any] = None, metadata: Optional[Any] = None, environment: Optional[str] = None, + version: Optional[str] = None, + level: Optional[SpanLevel] = None, + status_message: Optional[str] = None, + completion_start_time: Optional[datetime] = None, + model: Optional[str] = None, + model_parameters: Optional[Dict[str, MapValue]] = None, + usage_details: Optional[Dict[str, int]] = None, + cost_details: Optional[Dict[str, float]] = None, + prompt: Optional[PromptClient] = None, ): """Initialize a new Langfuse span wrapper. @@ -80,6 +89,15 @@ def __init__( output: Output data from the span (any JSON-serializable object) metadata: Additional metadata to associate with the span environment: The tracing environment + version: Version identifier for the code or component + level: Importance level of the span (info, warning, error) + status_message: Optional status message for the span + completion_start_time: When the model started generating the response + model: Name/identifier of the AI model used (e.g., "gpt-4") + model_parameters: Parameters used for the model (e.g., temperature, max_tokens) + usage_details: Token usage information (e.g., prompt_tokens, completion_tokens) + cost_details: Cost information for the model call + prompt: Associated prompt template from Langfuse prompt management """ self._otel_span = otel_span self._otel_span.set_attribute( @@ -108,12 +126,35 @@ def __init__( data=metadata, field="metadata", span=self._otel_span ) - attributes = create_span_attributes( - input=media_processed_input, - output=media_processed_output, - metadata=media_processed_metadata, - ) - attributes.pop(LangfuseOtelSpanAttributes.OBSERVATION_TYPE) + attributes = {} + + if as_type == "generation": + attributes = create_generation_attributes( + input=media_processed_input, + output=media_processed_output, + metadata=media_processed_metadata, + version=version, + level=level, + status_message=status_message, + completion_start_time=completion_start_time, + model=model, + model_parameters=model_parameters, + usage_details=usage_details, + cost_details=cost_details, + prompt=prompt, + ) + + else: + attributes = create_span_attributes( + input=media_processed_input, + output=media_processed_output, + metadata=media_processed_metadata, + version=version, + level=level, + status_message=status_message, + ) + + attributes.pop(LangfuseOtelSpanAttributes.OBSERVATION_TYPE, None) self._otel_span.set_attributes( {k: v for k, v in attributes.items() if v is not None} @@ -410,7 +451,7 @@ def _process_media_and_apply_mask( The processed and masked data """ return self._mask_attribute( - data=self._process_media_in_attribute(data=data, span=span, field=field) + data=self._process_media_in_attribute(data=data, field=field) ) def _mask_attribute(self, *, data): @@ -441,7 +482,6 @@ def _process_media_in_attribute( self, *, data: Optional[Any] = None, - span: otel_trace_api.Span, field: Union[Literal["input"], Literal["output"], Literal["metadata"]], ): """Process any media content in the attribute data. @@ -457,16 +497,17 @@ def _process_media_in_attribute( Returns: The data with any media content processed """ - media_processed_attribute = ( - self._langfuse_client._resources._media_manager._find_and_process_media( - data=data, - field=field, - trace_id=self.trace_id, - observation_id=self.id, + if self._langfuse_client._resources is not None: + return ( + self._langfuse_client._resources._media_manager._find_and_process_media( + data=data, + field=field, + trace_id=self.trace_id, + observation_id=self.id, + ) ) - ) - return media_processed_attribute + return data class LangfuseSpan(LangfuseSpanWrapper): @@ -487,6 +528,9 @@ def __init__( output: Optional[Any] = None, metadata: Optional[Any] = None, environment: Optional[str] = None, + version: Optional[str] = None, + level: Optional[SpanLevel] = None, + status_message: Optional[str] = None, ): """Initialize a new LangfuseSpan. @@ -497,6 +541,9 @@ def __init__( output: Output data from the span (any JSON-serializable object) metadata: Additional metadata to associate with the span environment: The tracing environment + version: Version identifier for the code or component + level: Importance level of the span (info, warning, error) + status_message: Optional status message for the span """ super().__init__( otel_span=otel_span, @@ -506,6 +553,9 @@ def __init__( output=output, metadata=metadata, environment=environment, + version=version, + level=level, + status_message=status_message, ) def update( @@ -618,33 +668,19 @@ def start_span( parent_span.end() ``` """ - attributes = create_span_attributes( - input=input, - output=output, - metadata=metadata, - version=version, - level=level, - status_message=status_message, - ) - with otel_trace_api.use_span(self._otel_span): - new_otel_span = self._langfuse_client._otel_tracer.start_span( - name=name, attributes=attributes - ) - - if new_otel_span.is_recording: - self._set_processed_span_attributes( - span=new_otel_span, - as_type="span", - input=input, - output=output, - metadata=metadata, - ) + new_otel_span = self._langfuse_client._otel_tracer.start_span(name=name) return LangfuseSpan( otel_span=new_otel_span, langfuse_client=self._langfuse_client, environment=self._environment, + input=input, + output=output, + metadata=metadata, + version=version, + level=level, + status_message=status_message, ) def start_as_current_span( @@ -692,26 +728,19 @@ def start_as_current_span( parent_span.update(output=result) ``` """ - attributes = create_span_attributes( - input=input, - output=output, - metadata=metadata, - version=version, - level=level, - status_message=status_message, - ) - return cast( _AgnosticContextManager["LangfuseSpan"], self._langfuse_client._create_span_with_parent_context( name=name, - attributes=attributes, as_type="span", remote_parent_span=None, parent=self._otel_span, input=input, output=output, metadata=metadata, + version=version, + level=level, + status_message=status_message, ), ) @@ -789,7 +818,13 @@ def start_generation( span.end() ``` """ - attributes = create_generation_attributes( + with otel_trace_api.use_span(self._otel_span): + new_otel_span = self._langfuse_client._otel_tracer.start_span(name=name) + + return LangfuseGeneration( + otel_span=new_otel_span, + langfuse_client=self._langfuse_client, + environment=self._environment, input=input, output=output, metadata=metadata, @@ -804,26 +839,6 @@ def start_generation( prompt=prompt, ) - with otel_trace_api.use_span(self._otel_span): - new_otel_span = self._langfuse_client._otel_tracer.start_span( - name=name, attributes=attributes - ) - - if new_otel_span.is_recording: - self._set_processed_span_attributes( - span=new_otel_span, - as_type="generation", - input=input, - output=output, - metadata=metadata, - ) - - return LangfuseGeneration( - otel_span=new_otel_span, - langfuse_client=self._langfuse_client, - environment=self._environment, - ) - def start_as_current_generation( self, *, @@ -893,32 +908,25 @@ def start_as_current_generation( span.update(output={"answer": response.text, "source": "gpt-4"}) ``` """ - attributes = create_generation_attributes( - input=input, - output=output, - metadata=metadata, - version=version, - level=level, - status_message=status_message, - completion_start_time=completion_start_time, - model=model, - model_parameters=model_parameters, - usage_details=usage_details, - cost_details=cost_details, - prompt=prompt, - ) - return cast( _AgnosticContextManager["LangfuseGeneration"], self._langfuse_client._create_span_with_parent_context( name=name, - attributes=attributes, as_type="generation", remote_parent_span=None, parent=self._otel_span, input=input, output=output, metadata=metadata, + version=version, + level=level, + status_message=status_message, + completion_start_time=completion_start_time, + model=model, + model_parameters=model_parameters, + usage_details=usage_details, + cost_details=cost_details, + prompt=prompt, ), ) @@ -953,29 +961,12 @@ def create_event( ``` """ timestamp = time_ns() - attributes = create_span_attributes( - input=input, - output=output, - metadata=metadata, - version=version, - level=level, - status_message=status_message, - ) with otel_trace_api.use_span(self._otel_span): new_otel_span = self._langfuse_client._otel_tracer.start_span( - name=name, attributes=attributes, start_time=timestamp + name=name, start_time=timestamp ) - if new_otel_span.is_recording: - self._set_processed_span_attributes( - span=new_otel_span, - as_type="event", - input=input, - output=output, - metadata=metadata, - ) - return LangfuseEvent( otel_span=new_otel_span, langfuse_client=self._langfuse_client, @@ -983,6 +974,9 @@ def create_event( output=output, metadata=metadata, environment=self._environment, + version=version, + level=level, + status_message=status_message, ).end(end_time=timestamp) @@ -1003,6 +997,15 @@ def __init__( output: Optional[Any] = None, metadata: Optional[Any] = None, environment: Optional[str] = None, + version: Optional[str] = None, + level: Optional[SpanLevel] = None, + status_message: Optional[str] = None, + completion_start_time: Optional[datetime] = None, + model: Optional[str] = None, + model_parameters: Optional[Dict[str, MapValue]] = None, + usage_details: Optional[Dict[str, int]] = None, + cost_details: Optional[Dict[str, float]] = None, + prompt: Optional[PromptClient] = None, ): """Initialize a new LangfuseGeneration span. @@ -1013,6 +1016,15 @@ def __init__( output: Output from the generation (e.g., completions) metadata: Additional metadata to associate with the generation environment: The tracing environment + version: Version identifier for the model or component + level: Importance level of the generation (info, warning, error) + status_message: Optional status message for the generation + completion_start_time: When the model started generating the response + model: Name/identifier of the AI model used (e.g., "gpt-4") + model_parameters: Parameters used for the model (e.g., temperature, max_tokens) + usage_details: Token usage information (e.g., prompt_tokens, completion_tokens) + cost_details: Cost information for the model call + prompt: Associated prompt template from Langfuse prompt management """ super().__init__( otel_span=otel_span, @@ -1022,6 +1034,15 @@ def __init__( output=output, metadata=metadata, environment=environment, + version=version, + level=level, + status_message=status_message, + completion_start_time=completion_start_time, + model=model, + model_parameters=model_parameters, + usage_details=usage_details, + cost_details=cost_details, + prompt=prompt, ) def update( @@ -1134,6 +1155,9 @@ def __init__( output: Optional[Any] = None, metadata: Optional[Any] = None, environment: Optional[str] = None, + version: Optional[str] = None, + level: Optional[SpanLevel] = None, + status_message: Optional[str] = None, ): """Initialize a new LangfuseEvent span. @@ -1144,6 +1168,9 @@ def __init__( output: Output from the event metadata: Additional metadata to associate with the generation environment: The tracing environment + version: Version identifier for the model or component + level: Importance level of the generation (info, warning, error) + status_message: Optional status message for the generation """ super().__init__( otel_span=otel_span, @@ -1153,4 +1180,7 @@ def __init__( output=output, metadata=metadata, environment=environment, + version=version, + level=level, + status_message=status_message, ) diff --git a/langfuse/_utils/serializer.py b/langfuse/_utils/serializer.py index 8f9665711..f8ae4ed92 100644 --- a/langfuse/_utils/serializer.py +++ b/langfuse/_utils/serializer.py @@ -21,14 +21,16 @@ try: from langchain.load.serializable import Serializable except ImportError: - # If Serializable is not available, set it to NoneType - Serializable = type(None) + # If Serializable is not available, set it to a placeholder type + class Serializable: # type: ignore + pass + # Attempt to import numpy try: import numpy as np except ImportError: - np = None + np = None # type: ignore logger = getLogger(__name__) diff --git a/langfuse/langchain/CallbackHandler.py b/langfuse/langchain/CallbackHandler.py index 576bfdebd..a13a50384 100644 --- a/langfuse/langchain/CallbackHandler.py +++ b/langfuse/langchain/CallbackHandler.py @@ -60,8 +60,8 @@ def __init__(self, *, public_key: Optional[str] = None) -> None: self.client = get_client(public_key=public_key) self.runs: Dict[UUID, Union[LangfuseSpan, LangfuseGeneration]] = {} - self.prompt_to_parent_run_map = {} - self.updated_completion_start_time_memo = set() + self.prompt_to_parent_run_map: Dict[UUID, Any] = {} + self.updated_completion_start_time_memo: Set[UUID] = set() def on_llm_new_token( self, @@ -166,19 +166,26 @@ def on_chain_start( run_id=run_id, parent_run_id=parent_run_id, metadata=metadata ) - content = { - "name": self.get_langchain_run_name(serialized, **kwargs), - "metadata": self.__join_tags_and_metadata(tags, metadata), - "input": inputs, - "level": "DEBUG" if tags and LANGSMITH_TAG_HIDDEN in tags else None, - } + span_name = self.get_langchain_run_name(serialized, **kwargs) + span_metadata = self.__join_tags_and_metadata(tags, metadata) + span_level = "DEBUG" if tags and LANGSMITH_TAG_HIDDEN in tags else None if parent_run_id is None: - self.runs[run_id] = self.client.start_span(**content) + self.runs[run_id] = self.client.start_span( + name=span_name, + metadata=span_metadata, + input=inputs, + level=span_level, + ) else: self.runs[run_id] = cast( LangfuseSpan, self.runs[parent_run_id] - ).start_span(**content) + ).start_span( + name=span_name, + metadata=span_metadata, + input=inputs, + level=span_level, + ) except Exception as e: langfuse_logger.exception(e) @@ -431,23 +438,25 @@ def on_retriever_start( self._log_debug_event( "on_retriever_start", run_id, parent_run_id, query=query ) + span_name = self.get_langchain_run_name(serialized, **kwargs) + span_metadata = self.__join_tags_and_metadata(tags, metadata) + span_level = "DEBUG" if tags and LANGSMITH_TAG_HIDDEN in tags else None + if parent_run_id is None: - content = { - "name": self.get_langchain_run_name(serialized, **kwargs), - "metadata": self.__join_tags_and_metadata(tags, metadata), - "input": query, - "level": "DEBUG" if tags and LANGSMITH_TAG_HIDDEN in tags else None, - } - - self.runs[run_id] = self.client.start_span(**content) + self.runs[run_id] = self.client.start_span( + name=span_name, + metadata=span_metadata, + input=query, + level=span_level, + ) else: self.runs[run_id] = cast( LangfuseSpan, self.runs[parent_run_id] ).start_span( - name=self.get_langchain_run_name(serialized, **kwargs), + name=span_name, input=query, - metadata=self.__join_tags_and_metadata(tags, metadata), - level="DEBUG" if tags and LANGSMITH_TAG_HIDDEN in tags else None, + metadata=span_metadata, + level=span_level, ) except Exception as e: @@ -784,6 +793,7 @@ def _parse_usage_model(usage: typing.Union[pydantic.BaseModel, dict]): # https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/get-token-count ("prompt_token_count", "input"), ("candidates_token_count", "output"), + ("total_token_count", "total"), # Bedrock: https://docs.aws.amazon.com/bedrock/latest/userguide/monitoring-cw.html#runtime-cloudwatch-metrics ("inputTokenCount", "input"), ("outputTokenCount", "output"), diff --git a/langfuse/langchain/utils.py b/langfuse/langchain/utils.py index abbd9e70d..76be37100 100644 --- a/langfuse/langchain/utils.py +++ b/langfuse/langchain/utils.py @@ -1,7 +1,7 @@ """@private""" import re -from typing import Any, Dict, List, Literal, Optional +from typing import Any, Dict, List, Literal, Optional, cast # NOTE ON DEPENDENCIES: # - since Jan 2024, there is https://pypi.org/project/langchain-openai/ which is a separate package and imports openai models. @@ -12,7 +12,7 @@ def _extract_model_name( serialized: Optional[Dict[str, Any]], **kwargs: Any, -): +) -> Optional[str]: """Extracts the model name from the serialized or kwargs object. This is used to get the model names for Langfuse.""" # In this function we return on the first match, so the order of operations is important @@ -39,39 +39,54 @@ def _extract_model_name( for model_name, keys, select_from in models_by_id: model = _extract_model_by_path_for_id( - model_name, serialized, kwargs, keys, select_from + model_name, + serialized, + kwargs, + keys, + cast(Literal["serialized", "kwargs"], select_from), ) if model: return model # Second, we match AzureOpenAI as we need to extract the model name, fdeployment version and deployment name - if serialized.get("id")[-1] == "AzureOpenAI": - if kwargs.get("invocation_params").get("model"): - return kwargs.get("invocation_params").get("model") - - if kwargs.get("invocation_params").get("model_name"): - return kwargs.get("invocation_params").get("model_name") - - deployment_name = None - deployment_version = None - - if serialized.get("kwargs").get("openai_api_version"): - deployment_version = serialized.get("kwargs").get("deployment_version") - - if serialized.get("kwargs").get("deployment_name"): - deployment_name = serialized.get("kwargs").get("deployment_name") - - if not isinstance(deployment_name, str): - return None - - if not isinstance(deployment_version, str): - return deployment_name - - return ( - deployment_name + "-" + deployment_version - if deployment_version not in deployment_name - else deployment_name - ) + if serialized: + serialized_id = serialized.get("id") + if ( + serialized_id + and isinstance(serialized_id, list) + and len(serialized_id) > 0 + and serialized_id[-1] == "AzureOpenAI" + ): + invocation_params = kwargs.get("invocation_params") + if invocation_params and isinstance(invocation_params, dict): + if invocation_params.get("model"): + return str(invocation_params.get("model")) + + if invocation_params.get("model_name"): + return str(invocation_params.get("model_name")) + + deployment_name = None + deployment_version = None + + serialized_kwargs = serialized.get("kwargs") + if serialized_kwargs and isinstance(serialized_kwargs, dict): + if serialized_kwargs.get("openai_api_version"): + deployment_version = serialized_kwargs.get("deployment_version") + + if serialized_kwargs.get("deployment_name"): + deployment_name = serialized_kwargs.get("deployment_name") + + if not isinstance(deployment_name, str): + return None + + if not isinstance(deployment_version, str): + return deployment_name + + return ( + deployment_name + "-" + deployment_version + if deployment_version not in deployment_name + else deployment_name + ) # Third, for some models, we are unable to extract the model by a path in an object. Langfuse provides us with a string representation of the model pbjects # We use regex to extract the model from the repr string @@ -111,7 +126,9 @@ def _extract_model_name( ] for select in ["kwargs", "serialized"]: for path in random_paths: - model = _extract_model_by_path(serialized, kwargs, path, select) + model = _extract_model_by_path( + serialized, kwargs, path, cast(Literal["serialized", "kwargs"], select) + ) if model: return model @@ -123,13 +140,20 @@ def _extract_model_from_repr_by_pattern( serialized: Optional[Dict[str, Any]], pattern: str, default: Optional[str] = None, -): +) -> Optional[str]: if serialized is None: return None - if serialized.get("id")[-1] == id: - if serialized.get("repr"): - extracted = _extract_model_with_regex(pattern, serialized.get("repr")) + serialized_id = serialized.get("id") + if ( + serialized_id + and isinstance(serialized_id, list) + and len(serialized_id) > 0 + and serialized_id[-1] == id + ): + repr_str = serialized.get("repr") + if repr_str and isinstance(repr_str, str): + extracted = _extract_model_with_regex(pattern, repr_str) return extracted if extracted else default if default else None return None @@ -145,15 +169,24 @@ def _extract_model_with_regex(pattern: str, text: str): def _extract_model_by_path_for_id( id: str, serialized: Optional[Dict[str, Any]], - kwargs: dict, + kwargs: Dict[str, Any], keys: List[str], select_from: Literal["serialized", "kwargs"], -): +) -> Optional[str]: if serialized is None and select_from == "serialized": return None - if serialized.get("id")[-1] == id: - return _extract_model_by_path(serialized, kwargs, keys, select_from) + if serialized: + serialized_id = serialized.get("id") + if ( + serialized_id + and isinstance(serialized_id, list) + and len(serialized_id) > 0 + and serialized_id[-1] == id + ): + return _extract_model_by_path(serialized, kwargs, keys, select_from) + + return None def _extract_model_by_path( @@ -168,7 +201,10 @@ def _extract_model_by_path( current_obj = kwargs if select_from == "kwargs" else serialized for key in keys: - current_obj = current_obj.get(key) + if current_obj and isinstance(current_obj, dict): + current_obj = current_obj.get(key) + else: + return None if not current_obj: return None diff --git a/langfuse/version.py b/langfuse/version.py index 370ded022..96e9e9605 100644 --- a/langfuse/version.py +++ b/langfuse/version.py @@ -1,3 +1,3 @@ """@private""" -__version__ = "3.0.0" +__version__ = "3.0.1" diff --git a/pyproject.toml b/pyproject.toml index 6795d01de..4fdd69272 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [tool.poetry] name = "langfuse" -version = "3.0.0" +version = "3.0.1" description = "A client library for accessing langfuse" authors = ["langfuse "] license = "MIT" diff --git a/tests/test_core_sdk.py b/tests/test_core_sdk.py index ac8b5e529..914b2fb65 100644 --- a/tests/test_core_sdk.py +++ b/tests/test_core_sdk.py @@ -7,6 +7,7 @@ import pytest from langfuse import Langfuse +from langfuse._client.resource_manager import LangfuseResourceManager from langfuse._utils import _get_timestamp from tests.api_wrapper import LangfuseAPI from tests.utils import ( @@ -1781,8 +1782,12 @@ def test_create_trace_sampling_zero(): def test_mask_function(): + LangfuseResourceManager.reset() + def mask_func(data): if isinstance(data, dict): + if "should_raise" in data: + raise return {k: "MASKED" for k in data} elif isinstance(data, str): return "MASKED" @@ -1832,19 +1837,13 @@ def mask_func(data): assert fetched_span["input"] == {"data": "MASKED"} assert fetched_span["output"] == "MASKED" - # Test with faulty mask function - def faulty_mask_func(data): - raise Exception("Masking error") - - langfuse = Langfuse(mask=faulty_mask_func) - # Create a root span with trace properties with langfuse.start_as_current_span(name="test-span") as root_span: - root_span.update_trace(name="test_trace", input={"sensitive": "data"}) + root_span.update_trace(name="test_trace", input={"should_raise": "data"}) # Get trace ID for later use trace_id = root_span.trace_id # Add output to the trace - root_span.update_trace(output={"more": "sensitive"}) + root_span.update_trace(output={"should_raise": "sensitive"}) # Ensure data is sent langfuse.flush() diff --git a/tests/test_otel.py b/tests/test_otel.py index 0206a5d94..5611f1c3f 100644 --- a/tests/test_otel.py +++ b/tests/test_otel.py @@ -2355,7 +2355,7 @@ def mask_sensitive_data(data): # Since _process_media_in_attribute makes calls to media_manager original_process = span._process_media_in_attribute - def mock_process_media(*, data, span, field): + def mock_process_media(*, data, field): # Just return the data directly without processing return data pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy