|
10 | 10 | from openinference.instrumentation.google_genai._utils import ( |
11 | 11 | _as_input_attributes, |
12 | 12 | _io_value_and_type, |
| 13 | + _ValueAndType, |
13 | 14 | ) |
14 | 15 | from openinference.semconv.trace import ( |
15 | 16 | ImageAttributes, |
@@ -39,7 +40,7 @@ def get_attributes_from_request( |
39 | 40 | yield SpanAttributes.LLM_PROVIDER, OpenInferenceLLMProviderValues.GOOGLE.value |
40 | 41 | try: |
41 | 42 | yield from _as_input_attributes( |
42 | | - _io_value_and_type(request_parameters), |
| 43 | + self._get_phoenix_friendly_input_value(request_parameters), |
43 | 44 | ) |
44 | 45 | except Exception: |
45 | 46 | logger.exception( |
@@ -459,12 +460,133 @@ def _flatten_parts(self, parts: list[Part]) -> Iterator[Tuple[str, AttributeValu |
459 | 460 | elif isinstance(value, str): |
460 | 461 | # Flatten all other string values into a single message content |
461 | 462 | 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}") |
465 | 466 | if content_values: |
466 | 467 | yield (MessageAttributes.MESSAGE_CONTENT, "\n\n".join(content_values)) |
467 | 468 |
|
| 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 | + |
468 | 590 | def _extract_tool_call_index(self, attr: str) -> int: |
469 | 591 | """Extract tool call index from message tool call attribute key. |
470 | 592 |
|
|
0 commit comments