From 0136f90047c30cc309ba363a6b3e5064d125a53d Mon Sep 17 00:00:00 2001 From: Obinna Okafor Date: Wed, 12 Mar 2025 14:40:36 +0100 Subject: [PATCH 1/2] fix serialization issue --- .../instrumentation/agno/patch.py | 506 ++++++++++-------- src/langtrace_python_sdk/utils/llm.py | 152 +++++- 2 files changed, 428 insertions(+), 230 deletions(-) diff --git a/src/langtrace_python_sdk/instrumentation/agno/patch.py b/src/langtrace_python_sdk/instrumentation/agno/patch.py index 66af5d44..f8accdd0 100644 --- a/src/langtrace_python_sdk/instrumentation/agno/patch.py +++ b/src/langtrace_python_sdk/instrumentation/agno/patch.py @@ -1,10 +1,28 @@ +""" +Copyright (c) 2024 Scale3 Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + import json +import time +from typing import Any + from importlib_metadata import version as v from langtrace.trace_attributes import FrameworkSpanAttributes from opentelemetry import baggage from opentelemetry.trace import Span, SpanKind, Tracer from opentelemetry.trace.status import Status, StatusCode -from typing import Dict, Any, Optional from langtrace_python_sdk.constants import LANGTRACE_SDK_NAME from langtrace_python_sdk.constants.instrumentation.common import ( @@ -15,50 +33,12 @@ from langtrace_python_sdk.utils.llm import get_span_name, set_span_attributes from langtrace_python_sdk.utils.misc import serialize_args, serialize_kwargs -def _safe_serialize(obj): - """Safely serialize objects that might not be JSON serializable""" - if hasattr(obj, 'to_dict'): - return obj.to_dict() - elif hasattr(obj, '__dict__'): - return {k: _safe_serialize(v) for k, v in obj.__dict__.items() if not k.startswith('_')} - elif isinstance(obj, dict): - return {k: _safe_serialize(v) for k, v in obj.items()} - elif isinstance(obj, (list, tuple)): - return [_safe_serialize(i) for i in obj] - return str(obj) - -def _safe_json_dumps(obj): - """Safely dump an object to JSON, handling non-serializable types""" - try: - return json.dumps(obj) - except (TypeError, ValueError): - return json.dumps(_safe_serialize(obj)) -def _extract_metrics(metrics: Dict[str, Any]) -> Dict[str, Any]: - """Helper function to extract and format metrics""" - if not metrics: - return {} - - if hasattr(metrics, 'to_dict'): - metrics = metrics.to_dict() - elif hasattr(metrics, '__dict__'): - metrics = {k: v for k, v in metrics.__dict__.items() if not k.startswith('_')} - - formatted_metrics = {} - - for key in ['time', 'time_to_first_token', 'input_tokens', 'output_tokens', - 'prompt_tokens', 'completion_tokens', 'total_tokens', - 'prompt_tokens_details', 'completion_tokens_details', 'tool_call_times']: - if key in metrics: - formatted_metrics[key] = metrics[key] - - return formatted_metrics - - -def patch_memory(operation_name, version, tracer: Tracer): +def patch_agent(operation_name, version, tracer: Tracer): def traced_method(wrapped, instance, args, kwargs): - service_provider = SERVICE_PROVIDERS["AGNO"] + service_provider = SERVICE_PROVIDERS.get("AGNO", "agno") extra_attributes = baggage.get_baggage(LANGTRACE_ADDITIONAL_SPAN_ATTRIBUTES_KEY) + span_attributes = { "langtrace.sdk.name": "langtrace-python-sdk", "langtrace.service.name": service_provider, @@ -68,20 +48,12 @@ def traced_method(wrapped, instance, args, kwargs): **(extra_attributes if extra_attributes is not None else {}), } - span_attributes.update({ - "agno.memory.type": type(instance).__name__, - "agno.memory.create_session_summary": str(instance.create_session_summary), - "agno.memory.create_user_memories": str(instance.create_user_memories), - "agno.memory.retrieval": str(instance.retrieval) - }) - inputs = {} if len(args) > 0: inputs["args"] = serialize_args(*args) if len(kwargs) > 0: inputs["kwargs"] = serialize_kwargs(**kwargs) - span_attributes["agno.memory.inputs"] = json.dumps(inputs) - + span_attributes["agno.agent.inputs"] = json.dumps(inputs) attributes = FrameworkSpanAttributes(**span_attributes) with tracer.start_as_current_span( @@ -89,30 +61,110 @@ def traced_method(wrapped, instance, args, kwargs): ) as span: try: set_span_attributes(span, attributes) + AgnoSpanAttributes(span=span, instance=instance) + result = wrapped(*args, **kwargs) - - if result is not None: - set_span_attribute(span, "agno.memory.output", str(result)) - - if instance.summary is not None: - set_span_attribute(span, "agno.memory.summary", str(instance.summary)) - if instance.memories is not None: - set_span_attribute(span, "agno.memory.memories_count", str(len(instance.memories))) span.set_status(Status(StatusCode.OK)) - return result + if operation_name in ["Agent._run", "Agent._arun", "Agent.run", "Agent.arun", "Agent.print_response"]: + try: + if hasattr(instance, "run_response") and instance.run_response: + if hasattr(instance.run_response, "run_id") and instance.run_response.run_id: + set_span_attribute(span, "agno.agent.run_id", instance.run_response.run_id) + + if hasattr(instance.run_response, "created_at") and instance.run_response.created_at: + set_span_attribute(span, "agno.agent.timestamp", instance.run_response.created_at) + + if hasattr(instance.run_response, "content") and instance.run_response.content: + content = str(instance.run_response.content) + set_span_attribute(span, "agno.agent.response_content", content) + + # Capture any tools that were used + if hasattr(instance.run_response, "tools") and instance.run_response.tools: + tools = instance.run_response.tools + tool_summary = [] + for tool in tools: + if 'tool_name' in tool: + tool_summary.append(tool['tool_name']) + elif 'function' in tool and 'name' in tool['function']: + tool_summary.append(tool['function']['name']) + set_span_attribute(span, "agno.agent.tools_used", json.dumps(tool_summary)) + + if hasattr(instance.run_response, "metrics") and instance.run_response.metrics: + metrics = instance.run_response.metrics + for metric_name, metric_values in metrics.items(): + if isinstance(metric_values, list): + + if all(isinstance(v, (int, float)) for v in metric_values): + set_span_attribute( + span, + f"agno.agent.metrics.{metric_name}", + sum(metric_values) / len(metric_values) if metric_values else 0 + ) + elif len(metric_values) > 0: + set_span_attribute( + span, + f"agno.agent.metrics.{metric_name}", + str(metric_values[-1]) + ) + else: + set_span_attribute( + span, + f"agno.agent.metrics.{metric_name}", + str(metric_values) + ) + + if 'input_tokens' in metrics: + if isinstance(metrics['input_tokens'], list) and metrics['input_tokens']: + set_span_attribute(span, "agno.agent.token_usage.input", + sum(metrics['input_tokens'])) + else: + set_span_attribute(span, "agno.agent.token_usage.input", + metrics['input_tokens']) + + if 'output_tokens' in metrics: + if isinstance(metrics['output_tokens'], list) and metrics['output_tokens']: + set_span_attribute(span, "agno.agent.token_usage.output", + sum(metrics['output_tokens'])) + else: + set_span_attribute(span, "agno.agent.token_usage.output", + metrics['output_tokens']) + + if 'total_tokens' in metrics: + if isinstance(metrics['total_tokens'], list) and metrics['total_tokens']: + set_span_attribute(span, "agno.agent.token_usage.total", + sum(metrics['total_tokens'])) + else: + set_span_attribute(span, "agno.agent.token_usage.total", + metrics['total_tokens']) + except Exception as err: + set_span_attribute(span, "agno.agent.run_response_error", str(err)) + + return result + except Exception as err: span.record_exception(err) span.set_status(Status(StatusCode.ERROR, str(err))) raise - + return traced_method -def patch_agent(operation_name, version, tracer: Tracer): + +def patch_memory(operation_name, version, tracer: Tracer): + """ + Apply instrumentation patches to AgentMemory class methods. + + Args: + operation_name: The name of the operation + version: The version of Agno + tracer: The OpenTelemetry tracer + """ def traced_method(wrapped, instance, args, kwargs): - service_provider = SERVICE_PROVIDERS["AGNO"] + service_provider = SERVICE_PROVIDERS.get("AGNO", "agno") extra_attributes = baggage.get_baggage(LANGTRACE_ADDITIONAL_SPAN_ATTRIBUTES_KEY) + + # Collect basic span attributes span_attributes = { "langtrace.sdk.name": "langtrace-python-sdk", "langtrace.service.name": service_provider, @@ -122,194 +174,190 @@ def traced_method(wrapped, instance, args, kwargs): **(extra_attributes if extra_attributes is not None else {}), } + # Collect inputs + inputs = {} + if len(args) > 0: + inputs["args"] = serialize_args(*args) + if len(kwargs) > 0: + inputs["kwargs"] = serialize_kwargs(**kwargs) + + span_attributes["agno.memory.inputs"] = json.dumps(inputs) + + if hasattr(instance, "messages"): + span_attributes["agno.memory.messages_count_before"] = len(instance.messages) + if hasattr(instance, "runs"): + span_attributes["agno.memory.runs_count_before"] = len(instance.runs) + if hasattr(instance, "memories") and instance.memories: + span_attributes["agno.memory.memories_count_before"] = len(instance.memories) + attributes = FrameworkSpanAttributes(**span_attributes) with tracer.start_as_current_span( get_span_name(operation_name), kind=SpanKind.CLIENT ) as span: + start_time = time.time() try: + # Set attributes set_span_attributes(span, attributes) - AgnoSpanAttributes(span=span, instance=instance) - is_streaming = kwargs.get('stream', False) + + # Execute the wrapped method result = wrapped(*args, **kwargs) - - if not is_streaming and not operation_name.startswith('Agent._'): - if hasattr(result, 'to_dict'): - _process_response(span, result) - return result - - # Handle streaming (generator) case - return _process_generator(span, result) + # Add memory stats after operation + if hasattr(instance, "messages"): + set_span_attribute(span, "agno.memory.messages_count_after", len(instance.messages)) + if hasattr(instance, "runs"): + set_span_attribute(span, "agno.memory.runs_count_after", len(instance.runs)) + if hasattr(instance, "memories") and instance.memories: + set_span_attribute(span, "agno.memory.memories_count_after", len(instance.memories)) + + # Record execution time + set_span_attribute(span, "agno.memory.execution_time_ms", int((time.time() - start_time) * 1000)) + + # Record success status + span.set_status(Status(StatusCode.OK)) + + # Add result if relevant + if result is not None: + set_span_attribute(span, "agno.memory.result", str(result)) + + return result + except Exception as err: + # Record the exception span.record_exception(err) span.set_status(Status(StatusCode.ERROR, str(err))) raise - - # Helper function to process a generator - def _process_generator(span, result_generator): - accumulated_content = "" - current_tool_call = None - response_metadata = None - seen_tool_calls = set() - - try: - for response in result_generator: - if not hasattr(response, 'to_dict'): - yield response - continue - - _process_response(span, response, - accumulated_content=accumulated_content, - current_tool_call=current_tool_call, - response_metadata=response_metadata, - seen_tool_calls=seen_tool_calls) - - if response.content: - accumulated_content += response.content - - yield response - - except Exception as err: - span.record_exception(err) - span.set_status(Status(StatusCode.ERROR, str(err))) - raise - finally: - span.set_status(Status(StatusCode.OK)) - if len(seen_tool_calls) > 0: - span.set_attribute("agno.agent.total_tool_calls", len(seen_tool_calls)) - def _process_response(span, response, accumulated_content="", current_tool_call=None, - response_metadata=None, seen_tool_calls=set()): - if not response_metadata: - response_metadata = { - "run_id": response.run_id, - "agent_id": response.agent_id, - "session_id": response.session_id, - "model": response.model, - "content_type": response.content_type, - } - for key, value in response_metadata.items(): - if value is not None: - set_span_attribute(span, f"agno.agent.{key}", str(value)) - - if response.content: - if accumulated_content: - accumulated_content += response.content - else: - accumulated_content = response.content - set_span_attribute(span, "agno.agent.response", accumulated_content) - - if response.messages: - for msg in response.messages: - if msg.tool_calls: - for tool_call in msg.tool_calls: - tool_id = tool_call.get('id') - if tool_id and tool_id not in seen_tool_calls: - seen_tool_calls.add(tool_id) - tool_info = { - 'id': tool_id, - 'name': tool_call.get('function', {}).get('name'), - 'arguments': tool_call.get('function', {}).get('arguments'), - 'start_time': msg.created_at, - } - current_tool_call = tool_info - set_span_attribute(span, f"agno.agent.tool_call.{tool_id}", _safe_json_dumps(tool_info)) - - if msg.metrics: - metrics = _extract_metrics(msg.metrics) - role_prefix = f"agno.agent.metrics.{msg.role}" - for key, value in metrics.items(): - set_span_attribute(span, f"{role_prefix}.{key}", str(value)) - - if response.tools: - for tool in response.tools: - tool_id = tool.get('tool_call_id') - if tool_id and current_tool_call and current_tool_call['id'] == tool_id: - tool_result = { - **current_tool_call, - 'result': tool.get('content'), - 'error': tool.get('tool_call_error'), - 'end_time': tool.get('created_at'), - 'metrics': tool.get('metrics'), - } - set_span_attribute(span, f"agno.agent.tool_call.{tool_id}", _safe_json_dumps(tool_result)) - current_tool_call = None - - if response.metrics: - metrics = _extract_metrics(response.metrics) - for key, value in metrics.items(): - set_span_attribute(span, f"agno.agent.metrics.{key}", str(value)) - - if len(seen_tool_calls) > 0: - span.set_attribute("agno.agent.total_tool_calls", len(seen_tool_calls)) - return traced_method -class AgnoSpanAttributes: - span: Span - agent_data: dict - def __init__(self, span: Span, instance) -> None: +class AgnoSpanAttributes: + """ + Helper class to extract and set Agno Agent attributes on spans. + """ + + def __init__(self, span: Span, instance: Any) -> None: + """ + Initialize with a span and Agno instance. + + Args: + span: OpenTelemetry span to update + instance: Agno Agent instance + """ self.span = span self.instance = instance - self.agent_data = { - "memory": {}, - "model": {}, - "tools": [], - } - + self.agent_data = {} + self.run() - - def run(self): - instance_attrs = { - "agent_id": self.instance.agent_id, - "session_id": self.instance.session_id, - "name": self.instance.name, - "markdown": self.instance.markdown, - "reasoning": self.instance.reasoning, - "add_references": self.instance.add_references, - "show_tool_calls": self.instance.show_tool_calls, - "stream": self.instance.stream, - "stream_intermediate_steps": self.instance.stream_intermediate_steps, - } + + def run(self) -> None: + """Process the instance attributes and add them to the span.""" + # Collect basic agent attributes + self.collect_agent_attributes() - for key, value in instance_attrs.items(): + # Add attributes to span + for key, value in self.agent_data.items(): if value is not None: - set_span_attribute(self.span, f"agno.agent.{key}", str(value)) - - if self.instance.model: - model_attrs = { - "id": self.instance.model.id, - "name": self.instance.model.name, - "provider": self.instance.model.provider, - "structured_outputs": self.instance.model.structured_outputs, - "supports_structured_outputs": self.instance.model.supports_structured_outputs, - } - for key, value in model_attrs.items(): - if value is not None: - set_span_attribute(self.span, f"agno.agent.model.{key}", str(value)) - - if hasattr(self.instance.model, 'metrics') and self.instance.model.metrics: - metrics = _extract_metrics(self.instance.model.metrics) - set_span_attribute(self.span, "agno.agent.model.metrics", _safe_json_dumps(metrics)) - - if self.instance.tools: - tool_list = [] + set_span_attribute( + self.span, + f"agno.agent.{key}", + str(value) if not isinstance(value, (int, float, bool)) else value + ) + + def collect_agent_attributes(self) -> None: + """Collect important attributes from the Agent instance.""" + # Extract basic agent information + if hasattr(self.instance, "agent_id"): + self.agent_data["id"] = self.instance.agent_id + + if hasattr(self.instance, "name"): + self.agent_data["name"] = self.instance.name + + if hasattr(self.instance, "session_id"): + self.agent_data["session_id"] = self.instance.session_id + + if hasattr(self.instance, "user_id"): + self.agent_data["user_id"] = self.instance.user_id + + if hasattr(self.instance, "run_id"): + self.agent_data["run_id"] = self.instance.run_id + + # Extract model information + if hasattr(self.instance, "model") and self.instance.model: + model = self.instance.model + model_info = {} + + if hasattr(model, "id"): + model_info["id"] = model.id + + if hasattr(model, "name"): + model_info["name"] = model.name + + if hasattr(model, "provider"): + model_info["provider"] = model.provider + + # Add temperature if available + if hasattr(model, "temperature") and model.temperature is not None: + model_info["temperature"] = model.temperature + + # Add max_tokens if available + if hasattr(model, "max_tokens") and model.max_tokens is not None: + model_info["max_tokens"] = model.max_tokens + + self.agent_data["model"] = json.dumps(model_info) + + # Extract tool information + if hasattr(self.instance, "tools") and self.instance.tools: + tool_info = [] for tool in self.instance.tools: + tool_data = {} + + # Handle different types of tools if hasattr(tool, "name"): - tool_list.append(tool.name) + tool_data["name"] = tool.name + + # Handle DuckDuckGoTools and similar toolkits + if hasattr(tool, "functions") and isinstance(tool.functions, dict): + tool_data["functions"] = list(tool.functions.keys()) + elif hasattr(tool, "__name__"): - tool_list.append(tool.__name__) - set_span_attribute(self.span, "agno.agent.tools", str(tool_list)) - - if self.instance.memory: - memory_attrs = { - "create_session_summary": self.instance.memory.create_session_summary, - "create_user_memories": self.instance.memory.create_user_memories, - "update_session_summary_after_run": self.instance.memory.update_session_summary_after_run, - "update_user_memories_after_run": self.instance.memory.update_user_memories_after_run, - } - for key, value in memory_attrs.items(): - if value is not None: - set_span_attribute(self.span, f"agno.agent.memory.{key}", str(value)) + tool_data["name"] = tool.__name__ + else: + tool_data["name"] = str(tool) + + # Add functions if available + if not "functions" in tool_data and hasattr(tool, "functions"): + if callable(getattr(tool, "functions")): + try: + tool_functions = tool.functions() + if isinstance(tool_functions, list): + tool_data["functions"] = [f.__name__ if hasattr(f, "__name__") else str(f) + for f in tool_functions] + except: + pass + + tool_info.append(tool_data) + + self.agent_data["tools"] = json.dumps(tool_info) + + # Extract reasoning settings + if hasattr(self.instance, "reasoning") and self.instance.reasoning: + self.agent_data["reasoning_enabled"] = True + + if hasattr(self.instance, "reasoning_model") and self.instance.reasoning_model: + self.agent_data["reasoning_model"] = str(self.instance.reasoning_model.id) + + if hasattr(self.instance, "reasoning_min_steps"): + self.agent_data["reasoning_min_steps"] = self.instance.reasoning_min_steps + + if hasattr(self.instance, "reasoning_max_steps"): + self.agent_data["reasoning_max_steps"] = self.instance.reasoning_max_steps + + # Extract knowledge settings + if hasattr(self.instance, "knowledge") and self.instance.knowledge: + self.agent_data["knowledge_enabled"] = True + + # Extract streaming settings + if hasattr(self.instance, "stream"): + self.agent_data["stream"] = self.instance.stream \ No newline at end of file diff --git a/src/langtrace_python_sdk/utils/llm.py b/src/langtrace_python_sdk/utils/llm.py index b72700e0..c2090156 100644 --- a/src/langtrace_python_sdk/utils/llm.py +++ b/src/langtrace_python_sdk/utils/llm.py @@ -94,6 +94,139 @@ def calculate_price_from_usage(model, usage): return 0 +def convert_mistral_messages_to_serializable(mistral_messages): + serializable_messages = [] + + try: + for message in mistral_messages: + serializable_message = {"role": message.role} + + # Handle content + if hasattr(message, "content"): + serializable_message["content"] = message.content + + # Handle tool_calls + if hasattr(message, "tool_calls") and message.tool_calls is not None: + serializable_tool_calls = [] + + for tool_call in message.tool_calls: + serializable_tool_call = {} + + # Handle id, type, and index + if hasattr(tool_call, "id"): + serializable_tool_call["id"] = tool_call.id + if hasattr(tool_call, "type"): + serializable_tool_call["type"] = tool_call.type + if hasattr(tool_call, "index"): + serializable_tool_call["index"] = tool_call.index + + # Handle function + if hasattr(tool_call, "function"): + function_call = tool_call.function + serializable_function = {} + + if hasattr(function_call, "name"): + serializable_function["name"] = function_call.name + if hasattr(function_call, "arguments"): + serializable_function["arguments"] = function_call.arguments + + serializable_tool_call["function"] = serializable_function + + serializable_tool_calls.append(serializable_tool_call) + + serializable_message["tool_calls"] = serializable_tool_calls + + # Handle tool_call_id for tool messages + if hasattr(message, "tool_call_id"): + serializable_message["tool_call_id"] = message.tool_call_id + + serializable_messages.append(serializable_message) + except Exception as e: + pass + + return serializable_messages + + +def convert_gemini_messages_to_serializable(formatted_messages, system_message=None): + """ + Converts Gemini-formatted messages back to a JSON serializable format. + + Args: + formatted_messages: The formatted messages from Gemini. + system_message (str, optional): System message content. + + Returns: + List[dict]: JSON serializable list of message dictionaries. + """ + serializable_messages = [] + + try: + # Add system message if present + if system_message: + serializable_messages.append({ + "role": "system", + "content": system_message + }) + + for message_item in formatted_messages: + # Handle the case where the item is a dict with 'role' and 'content' keys + if isinstance(message_item, dict) and 'role' in message_item and 'content' in message_item: + role = message_item['role'] + content_value = message_item['content'] + + # Initialize our serializable message + serializable_message = {"role": role} + + # If content is a list of Content objects + if isinstance(content_value, list) and len(content_value) > 0: + for content_obj in content_value: + # Process each Content object + if hasattr(content_obj, 'parts') and hasattr(content_obj, 'role'): + parts = content_obj.parts + + # Extract text from parts + text_parts = [] + for part in parts: + if hasattr(part, 'text') and part.text: + text_parts.append(part.text) + + if text_parts: + serializable_message["content"] = " ".join(text_parts) + + # Here you can add additional processing for other part types + # like function_call, function_response, inline_data, etc. + # Similar to the previous implementation + + # If content is a string or already a primitive type + elif isinstance(content_value, (str, int, float, bool)) or content_value is None: + serializable_message["content"] = content_value + + # Add the processed message to our list + serializable_messages.append(serializable_message) + + # Handle the case where the item is a Content object directly + elif hasattr(message_item, 'role') and hasattr(message_item, 'parts'): + # This is the case from the previous implementation + # Process a Content object directly + serializable_message = {"role": message_item.role} + + parts = message_item.parts + text_parts = [] + + for part in parts: + if hasattr(part, 'text') and part.text: + text_parts.append(part.text) + + if text_parts: + serializable_message["content"] = " ".join(text_parts) + + serializable_messages.append(serializable_message) + except Exception as e: + pass + + return serializable_messages + + def get_langtrace_attributes(version, service_provider, vendor_type="llm"): return { SpanAttributes.LANGTRACE_SDK_NAME: LANGTRACE_SDK_NAME, @@ -120,6 +253,23 @@ def get_llm_request_attributes(kwargs, prompts=None, model=None, operation_name= or kwargs.get("top_k", None) or kwargs.get("top_n", None) ) + + try: + prompts = json.dumps(prompts) if prompts else None + except Exception as e: + if "is not JSON serializable" in str(e): + # check model + if kwargs.get("model") is not None: + if kwargs.get("model").startswith("gemini"): + prompts = json.dumps(convert_gemini_messages_to_serializable(prompts)) + elif kwargs.get("model").startswith("mistral"): + prompts = json.dumps(convert_mistral_messages_to_serializable(prompts)) + else: + prompts = "[]" + else: + prompts = "[]" + else: + prompts = "[]" top_p = kwargs.get("p", None) or kwargs.get("top_p", None) tools = kwargs.get("tools", None) @@ -132,7 +282,7 @@ def get_llm_request_attributes(kwargs, prompts=None, model=None, operation_name= SpanAttributes.LLM_IS_STREAMING: kwargs.get("stream"), SpanAttributes.LLM_REQUEST_TEMPERATURE: kwargs.get("temperature"), SpanAttributes.LLM_TOP_K: top_k, - SpanAttributes.LLM_PROMPTS: json.dumps(prompts) if prompts else None, + SpanAttributes.LLM_PROMPTS: prompts if prompts else None, SpanAttributes.LLM_USER: user, SpanAttributes.LLM_REQUEST_TOP_P: top_p, SpanAttributes.LLM_REQUEST_MAX_TOKENS: kwargs.get("max_tokens"), From 25b82030bdedfa5a339ab89d60058c8467754227 Mon Sep 17 00:00:00 2001 From: Obinna Okafor Date: Wed, 12 Mar 2025 14:41:07 +0100 Subject: [PATCH 2/2] bump version --- src/langtrace_python_sdk/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/langtrace_python_sdk/version.py b/src/langtrace_python_sdk/version.py index 429dcfe5..9fd9cda1 100644 --- a/src/langtrace_python_sdk/version.py +++ b/src/langtrace_python_sdk/version.py @@ -1 +1 @@ -__version__ = "3.8.3" +__version__ = "3.8.4"