Skip to content

Commit 12644a6

Browse files
ci: Add google-genai to uv typing group (#7323)
Add `google-genai` to the typing dependency group and fix the resulting mypy errors.
1 parent 58823cc commit 12644a6

4 files changed

Lines changed: 183 additions & 87 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ typing = [
7474
"pydantic-ai-slim>=2.23.0",
7575
"langchain-core>=1.5.3",
7676
"huggingface-hub>=1.26.1",
77+
"google-genai>=2.21.0",
7778
]
7879
test = [
7980
"dataclasses ; python_full_version < '3.7'",

sentry_sdk/integrations/google_genai/__init__.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,20 +47,20 @@ def __init__(self: "GoogleGenAIIntegration", include_prompts: bool = True) -> No
4747
@staticmethod
4848
def setup_once() -> None:
4949
# Patch sync methods
50-
Models.generate_content = _wrap_generate_content(Models.generate_content)
51-
Models.generate_content_stream = _wrap_generate_content_stream(
50+
Models.generate_content = _wrap_generate_content(Models.generate_content) # type: ignore[method-assign]
51+
Models.generate_content_stream = _wrap_generate_content_stream( # type: ignore[method-assign]
5252
Models.generate_content_stream
5353
)
54-
Models.embed_content = _wrap_embed_content(Models.embed_content)
54+
Models.embed_content = _wrap_embed_content(Models.embed_content) # type: ignore[method-assign]
5555

5656
# Patch async methods
57-
AsyncModels.generate_content = _wrap_async_generate_content(
57+
AsyncModels.generate_content = _wrap_async_generate_content( # type: ignore[method-assign]
5858
AsyncModels.generate_content
5959
)
60-
AsyncModels.generate_content_stream = _wrap_async_generate_content_stream(
60+
AsyncModels.generate_content_stream = _wrap_async_generate_content_stream( # type: ignore[method-assign]
6161
AsyncModels.generate_content_stream
6262
)
63-
AsyncModels.embed_content = _wrap_async_embed_content(AsyncModels.embed_content)
63+
AsyncModels.embed_content = _wrap_async_embed_content(AsyncModels.embed_content) # type: ignore[method-assign]
6464

6565

6666
def _wrap_generate_content_stream(f: "Callable[..., Any]") -> "Callable[..., Any]":

sentry_sdk/integrations/google_genai/utils.py

Lines changed: 76 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
Optional,
1414
TypedDict,
1515
Union,
16+
cast,
1617
)
1718

1819
from google.genai.types import Content, GenerateContentConfig, Part, PartDict
@@ -44,13 +45,15 @@
4445

4546
if TYPE_CHECKING:
4647
from google.genai.types import (
48+
ContentDict,
4749
ContentListUnion,
4850
ContentUnion,
4951
ContentUnionDict,
5052
EmbedContentResponse,
5153
GenerateContentResponse,
5254
Model,
5355
Tool,
56+
ToolUnion,
5457
)
5558

5659
from sentry_sdk._types import TextPart
@@ -555,7 +558,7 @@ def extract_contents_text(contents: "ContentListUnion") -> "Optional[str]":
555558

556559

557560
def _format_tools_for_span(
558-
tools: "Iterable[Tool | Callable[..., Any]]",
561+
tools: "Iterable[ToolUnion]",
559562
) -> "Optional[List[dict[str, Any]]]":
560563
"""Format tools parameter for span data."""
561564
formatted_tools = []
@@ -604,13 +607,17 @@ def extract_tool_calls(
604607
tool_calls = []
605608

606609
# Extract from candidates, sometimes tool calls are nested under the content.parts object
607-
if getattr(response, "candidates", []):
608-
for candidate in response.candidates:
610+
candidates = getattr(response, "candidates", [])
611+
if response is not None:
612+
for candidate in candidates:
609613
if not hasattr(candidate, "content") or not getattr(
610614
candidate.content, "parts", []
611615
):
612616
continue
613617

618+
if candidate.content is None or candidate.content.parts is None:
619+
continue
620+
614621
for part in candidate.content.parts:
615622
if getattr(part, "function_call", None):
616623
function_call = part.function_call
@@ -627,34 +634,42 @@ def extract_tool_calls(
627634

628635
# Extract from automatic_function_calling_history
629636
# This is the history of tool calls made by the model
630-
if getattr(response, "automatic_function_calling_history", None):
631-
for content in response.automatic_function_calling_history:
632-
if not getattr(content, "parts", None):
633-
continue
637+
automatic_function_calling_history = getattr(
638+
response, "automatic_function_calling_history", None
639+
)
640+
if automatic_function_calling_history is None:
641+
return tool_calls if tool_calls else None
634642

635-
for part in getattr(content, "parts", []):
636-
if getattr(part, "function_call", None):
637-
function_call = part.function_call
638-
tool_call = {
639-
"name": getattr(function_call, "name", None),
640-
"type": "function_call",
641-
}
643+
for content in automatic_function_calling_history:
644+
if not getattr(content, "parts", None):
645+
continue
642646

643-
# Extract arguments if available
644-
if hasattr(function_call, "args"):
645-
tool_call["arguments"] = safe_serialize(function_call.args)
647+
for part in getattr(content, "parts", []):
648+
if getattr(part, "function_call", None):
649+
function_call = part.function_call
650+
tool_call = {
651+
"name": getattr(function_call, "name", None),
652+
"type": "function_call",
653+
}
646654

647-
tool_calls.append(tool_call)
655+
# Extract arguments if available
656+
if hasattr(function_call, "args"):
657+
tool_call["arguments"] = safe_serialize(function_call.args)
658+
659+
tool_calls.append(tool_call)
648660

649661
return tool_calls if tool_calls else None
650662

651663

652664
def _capture_tool_input(
653-
args: "tuple[Any, ...]", kwargs: "dict[str, Any]", tool: "Tool"
665+
args: "tuple[Any, ...]", kwargs: "dict[str, Any]", tool: "Tool | Callable[..., Any]"
654666
) -> "dict[str, Any]":
655667
"""Capture tool input from args and kwargs."""
656668
tool_input = kwargs.copy() if kwargs else {}
657669

670+
if not callable(tool):
671+
return tool_input
672+
658673
# If we have positional args, try to map them to the function signature
659674
if args:
660675
try:
@@ -767,29 +782,35 @@ def sync_wrapped(*args: "Any", **kwargs: "Any") -> "Any":
767782

768783

769784
def wrapped_config_with_tools(
770-
config: "GenerateContentConfig",
771-
) -> "GenerateContentConfig":
785+
config: "GenerateContentConfig | None",
786+
) -> "GenerateContentConfig | None":
772787
"""Wrap tools in config to emit execute_tool spans. Tools are sometimes passed directly as
773788
callable functions as a part of the config object."""
789+
if not config:
790+
return config
774791

775-
if not config or not getattr(config, "tools", None):
792+
tools = getattr(config, "tools", None)
793+
if tools is None:
776794
return config
777795

778796
result = copy.copy(config)
779-
result.tools = [wrapped_tool(tool) for tool in config.tools]
797+
result.tools = [wrapped_tool(tool) for tool in tools]
780798

781799
return result
782800

783801

784802
def _extract_response_text(
785803
response: "GenerateContentResponse",
786-
) -> "Optional[List[str]]":
804+
) -> "Optional[List[str | None]]":
787805
"""Extract text from response candidates."""
788806

789807
if not response or not getattr(response, "candidates", []):
790808
return None
791809

792-
texts = []
810+
texts: "list[str | None]" = []
811+
if response.candidates is None:
812+
return texts if texts else None
813+
793814
for candidate in response.candidates:
794815
if not hasattr(candidate, "content") or not hasattr(candidate.content, "parts"):
795816
continue
@@ -811,7 +832,11 @@ def extract_finish_reasons(
811832
if not response or not getattr(response, "candidates", []):
812833
return None
813834

814-
finish_reasons = []
835+
finish_reasons: "list[str]" = []
836+
837+
if response.candidates is None:
838+
return finish_reasons if finish_reasons else None
839+
815840
for candidate in response.candidates:
816841
if getattr(candidate, "finish_reason", None):
817842
# Convert enum value to string if necessary
@@ -843,16 +868,29 @@ def _transform_system_instruction_one_level(
843868
text_parts.append({"type": "text", "content": part.text})
844869
return text_parts
845870

846-
if isinstance(system_instructions, dict) and system_instructions.get("text"):
847-
return [{"type": "text", "content": system_instructions["text"]}]
871+
if isinstance(system_instructions, dict) and "text" in system_instructions:
872+
text = cast("PartDict", system_instructions)["text"]
873+
if text is None:
874+
return []
875+
876+
return [{"type": "text", "content": text}]
848877

849878
elif can_be_content and isinstance(system_instructions, dict):
850-
parts = system_instructions.get("parts", [])
879+
parts = cast("ContentDict", system_instructions).get("parts", [])
880+
if parts is None:
881+
return text_parts
882+
851883
for part in parts:
852884
if isinstance(part, Part) and isinstance(part.text, str):
853885
text_parts.append({"type": "text", "content": part.text})
854-
elif isinstance(part, dict) and isinstance(part.get("text"), str):
855-
text_parts.append({"type": "text", "content": part["text"]})
886+
continue
887+
888+
if not isinstance(part, dict):
889+
continue
890+
891+
text = part.get("text")
892+
if isinstance(text, str):
893+
text_parts.append({"type": "text", "content": text})
856894
return text_parts
857895

858896
return text_parts
@@ -1002,11 +1040,13 @@ def set_span_data_for_response(
10021040
span, SPANDATA.GEN_AI_RESPONSE_FINISH_REASONS, finish_reasons
10031041
)
10041042

1005-
if getattr(response, "response_id", None):
1006-
set_on_span(SPANDATA.GEN_AI_RESPONSE_ID, response.response_id)
1043+
response_id = getattr(response, "response_id", None)
1044+
if response_id is not None:
1045+
set_on_span(SPANDATA.GEN_AI_RESPONSE_ID, response_id)
10071046

1008-
if getattr(response, "model_version", None):
1009-
set_on_span(SPANDATA.GEN_AI_RESPONSE_MODEL, response.model_version)
1047+
model_version = getattr(response, "model_version", None)
1048+
if model_version is not None:
1049+
set_on_span(SPANDATA.GEN_AI_RESPONSE_MODEL, model_version)
10101050

10111051
usage_data = extract_usage_data(response)
10121052

@@ -1065,7 +1105,7 @@ def prepare_generate_content_args(
10651105
contents = args[1] if len(args) > 1 else kwargs.get("contents")
10661106
model_name = get_model_name(model)
10671107

1068-
config = kwargs.get("config")
1108+
config: "GenerateContentConfig | None" = kwargs.get("config")
10691109
wrapped_config = wrapped_config_with_tools(config)
10701110
if wrapped_config is not config:
10711111
kwargs["config"] = wrapped_config

0 commit comments

Comments
 (0)