Skip to content

Commit fd65407

Browse files
committed
wip
1 parent 97dbf3a commit fd65407

File tree

1 file changed

+126
-4
lines changed

1 file changed

+126
-4
lines changed

python/instrumentation/openinference-instrumentation-google-genai/src/openinference/instrumentation/google_genai/_request_attributes_extractor.py

Lines changed: 126 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from openinference.instrumentation.google_genai._utils import (
1111
_as_input_attributes,
1212
_io_value_and_type,
13+
_ValueAndType,
1314
)
1415
from openinference.semconv.trace import (
1516
ImageAttributes,
@@ -39,7 +40,7 @@ def get_attributes_from_request(
3940
yield SpanAttributes.LLM_PROVIDER, OpenInferenceLLMProviderValues.GOOGLE.value
4041
try:
4142
yield from _as_input_attributes(
42-
_io_value_and_type(request_parameters),
43+
self._get_phoenix_friendly_input_value(request_parameters),
4344
)
4445
except Exception:
4546
logger.exception(
@@ -459,12 +460,133 @@ def _flatten_parts(self, parts: list[Part]) -> Iterator[Tuple[str, AttributeValu
459460
elif isinstance(value, str):
460461
# Flatten all other string values into a single message content
461462
content_values.append(value)
462-
else:
463-
# TODO: Handle other types of parts
464-
logger.debug(f"Non-text part encountered: {part}")
463+
else:
464+
# TODO: Handle other types of parts
465+
logger.debug(f"Non-text part encountered: {part}")
465466
if content_values:
466467
yield (MessageAttributes.MESSAGE_CONTENT, "\n\n".join(content_values))
467468

469+
def _get_phoenix_friendly_input_value(self, request_parameters: Any) -> _ValueAndType:
470+
"""
471+
Create a Phoenix-friendly input value by replacing binary data with descriptive text.
472+
This ensures the Phoenix UI shows readable content instead of binary data.
473+
"""
474+
try:
475+
# First try the standard approach for non-binary content
476+
if not isinstance(request_parameters, Mapping):
477+
return _io_value_and_type(request_parameters)
478+
479+
# Check if this request contains binary data (images/files)
480+
contents = request_parameters.get("contents")
481+
if not contents:
482+
return _io_value_and_type(request_parameters)
483+
484+
# Create a copy of request parameters to modify
485+
cleaned_params = dict(request_parameters)
486+
487+
# Process contents to replace binary data with descriptive text
488+
if hasattr(contents, "parts"):
489+
# Single Content object
490+
cleaned_params["contents"] = self._clean_content_for_display(contents)
491+
elif isinstance(contents, (list, tuple)):
492+
# List of Content objects
493+
cleaned_params["contents"] = [
494+
self._clean_content_for_display(content)
495+
if hasattr(content, "parts")
496+
else content
497+
for content in contents
498+
]
499+
500+
# Use the standard processing on the cleaned parameters
501+
return _io_value_and_type(cleaned_params)
502+
503+
except Exception:
504+
logger.exception(
505+
"Failed to create Phoenix-friendly input value, falling back to default"
506+
)
507+
return _io_value_and_type(request_parameters)
508+
509+
def _clean_content_for_display(self, content: Any) -> Dict[str, Any]:
510+
"""Clean a Content object by replacing binary data with descriptive text."""
511+
try:
512+
# Create a simplified representation
513+
result = {"role": get_attribute(content, "role", "user"), "parts": []}
514+
515+
parts = get_attribute(content, "parts", [])
516+
for part in parts:
517+
if text := get_attribute(part, "text"):
518+
result["parts"].append({"text": text})
519+
elif inline_data := get_attribute(part, "inline_data"):
520+
mime_type = get_attribute(inline_data, "mime_type", "unknown") or "unknown"
521+
data = get_attribute(inline_data, "data")
522+
523+
if mime_type.startswith("image/"):
524+
# For images, include the actual data URL so Phoenix can display them
525+
if data:
526+
import base64
527+
528+
# Handle both bytes and string data properly
529+
if isinstance(data, bytes):
530+
base64_data = base64.b64encode(data).decode()
531+
elif isinstance(data, str):
532+
# Assume it's already base64 encoded
533+
base64_data = data
534+
else:
535+
# Convert other types to string and base64 encode
536+
base64_data = base64.b64encode(str(data).encode()).decode()
537+
538+
data_url = f"data:{mime_type};base64,{base64_data}"
539+
result["parts"].append(
540+
{
541+
"inline_data": {
542+
"mime_type": mime_type,
543+
"data_url": data_url, # Phoenix-friendly image URL
544+
"description": f"Image ({mime_type})",
545+
}
546+
}
547+
)
548+
else:
549+
result["parts"].append(
550+
{
551+
"inline_data": {
552+
"mime_type": mime_type,
553+
"description": f"[Image: {mime_type}, no data]",
554+
}
555+
}
556+
)
557+
else:
558+
try:
559+
data_size_value = len(data) if data else 0
560+
data_size_str = str(data_size_value)
561+
except (TypeError, AttributeError):
562+
data_size_str = "unknown"
563+
result["parts"].append(
564+
{
565+
"inline_data": {
566+
"mime_type": mime_type,
567+
"description": f"[File data: {mime_type}, {data_size_str} bytes]", # noqa: E501
568+
}
569+
}
570+
)
571+
elif file_data := get_attribute(part, "file_data"):
572+
file_uri = get_attribute(file_data, "file_uri", "unknown") or "unknown"
573+
mime_type = get_attribute(file_data, "mime_type", "unknown") or "unknown"
574+
result["parts"].append(
575+
{"file_data": {"file_uri": file_uri, "mime_type": mime_type}}
576+
)
577+
elif function_call := get_attribute(part, "function_call"):
578+
result["parts"].append({"function_call": str(function_call)})
579+
elif function_response := get_attribute(part, "function_response"):
580+
result["parts"].append({"function_response": str(function_response)})
581+
else:
582+
result["parts"].append({"unknown_part": str(type(part))})
583+
584+
return result
585+
586+
except Exception:
587+
logger.exception("Failed to clean content for display")
588+
return {"role": "user", "parts": [{"error": "Failed to process content"}]}
589+
468590
def _extract_tool_call_index(self, attr: str) -> int:
469591
"""Extract tool call index from message tool call attribute key.
470592

0 commit comments

Comments
 (0)