Coverage for src / agent / providers / gemini / types.py: 97%
70 statements
« prev ^ index » next coverage.py v7.13.0, created at 2025-12-11 14:30 +0000
« prev ^ index » next coverage.py v7.13.0, created at 2025-12-11 14:30 +0000
1# Copyright 2025-2026 Microsoft Corporation
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
15"""
16Type definitions and conversion utilities for Gemini provider.
18This module provides functions to convert between agent-framework's message
19formats and Google Gemini's expected formats.
20"""
22from typing import Any
24from agent_framework import (
25 AIFunction,
26 ChatMessage,
27 FunctionCallContent,
28 FunctionResultContent,
29 TextContent,
30)
33def to_gemini_message(
34 message: ChatMessage, call_id_to_name: dict[str, str] | None = None
35) -> dict[str, Any]:
36 """Convert agent-framework ChatMessage to Gemini message format.
38 Args:
39 message: ChatMessage from agent-framework
41 Returns:
42 Dictionary in Gemini's message format
44 Example:
45 >>> msg = ChatMessage(role="user", contents=[TextContent(text="Hello")])
46 >>> to_gemini_message(msg)
47 {"role": "user", "parts": [{"text": "Hello"}]}
48 """
49 # Map roles (agent-framework -> Gemini)
50 role_mapping = {
51 "user": "user",
52 "assistant": "model",
53 "system": "user", # Gemini doesn't have system role, treat as user
54 "tool": "user", # Tool results are supplied as user role in Gemini
55 }
56 # Handle Role enum or string
57 role_str = message.role.value if hasattr(message.role, "value") else str(message.role)
58 gemini_role = role_mapping.get(role_str, "user")
60 # Convert contents to parts
61 parts = []
63 # Get contents from message (agent-framework uses 'contents' attribute)
64 contents = message.contents if hasattr(message, "contents") else []
66 # Handle different content types with role-aware rules
67 for item in contents:
68 if isinstance(item, TextContent):
69 # Text content is allowed in any turn
70 parts.append({"text": item.text})
71 continue
73 if isinstance(item, FunctionCallContent):
74 # Function call should only appear in a model (assistant) turn
75 if gemini_role == "model":
76 parts.append({"function_call": {"name": item.name, "args": item.arguments}}) # type: ignore[dict-item]
77 # Skip function_call in non-model turns
78 continue
80 if isinstance(item, FunctionResultContent):
81 # Function result should appear only in a user/tool turn
82 if gemini_role == "user":
83 name = None
84 if call_id_to_name and item.call_id in call_id_to_name:
85 name = call_id_to_name[item.call_id]
86 # If we can't resolve the name, skip to avoid invalid request
87 if not name:
88 continue
89 response_dict: dict[str, Any] = {
90 "function_response": {
91 "name": name,
92 "response": {"result": item.result},
93 }
94 }
95 parts.append(response_dict)
96 # Skip function_result in model turns
97 continue
99 # If no parts were added, attempt to use message.text; otherwise add empty text part
100 if not parts:
101 # Some ChatMessage instances may expose text; use it if present
102 text_value = getattr(message, "text", "")
103 parts.append({"text": text_value})
105 return {"role": gemini_role, "parts": parts}
108def from_gemini_message(gemini_response: Any) -> ChatMessage:
109 """Convert Gemini response to agent-framework ChatMessage.
111 Args:
112 gemini_response: Response object from Gemini API
114 Returns:
115 ChatMessage for agent-framework
117 Example:
118 >>> response = gemini_client.generate_content("Hello")
119 >>> msg = from_gemini_message(response)
120 >>> msg.role
121 'assistant'
122 """
123 content_items: list[TextContent | FunctionCallContent] = []
125 # Extract text and function calls from response
126 if hasattr(gemini_response, "candidates") and gemini_response.candidates:
127 candidate = gemini_response.candidates[0]
128 if hasattr(candidate, "content") and candidate.content:
129 for part in candidate.content.parts:
130 # Text content
131 if hasattr(part, "text") and part.text:
132 content_items.append(TextContent(text=part.text))
134 # Function call
135 if hasattr(part, "function_call") and part.function_call:
136 fc = part.function_call
137 # Generate a call_id if not provided by Gemini
138 call_id = getattr(fc, "id", None) or f"{fc.name}_{id(fc)}"
139 content_items.append(
140 FunctionCallContent(
141 call_id=call_id,
142 name=fc.name,
143 arguments=dict(fc.args) if hasattr(fc, "args") else {},
144 )
145 )
147 # Return ChatMessage with contents list
148 # Note: agent-framework uses 'contents' (plural) not 'content'
149 if not content_items:
150 content_items = [TextContent(text="")]
152 return ChatMessage(role="assistant", contents=content_items)
155def to_gemini_tools(tools: list[AIFunction]) -> list[dict[str, Any]]:
156 """Convert agent-framework AIFunction tools to Gemini function declarations.
158 Args:
159 tools: List of AIFunction objects from agent-framework
161 Returns:
162 List of Gemini function declaration dictionaries
164 Example:
165 >>> tools = [AIFunction(name="get_weather", description="Get weather")]
166 >>> gemini_tools = to_gemini_tools(tools)
167 >>> gemini_tools[0]["name"]
168 'get_weather'
169 """
170 function_declarations = []
172 for tool in tools:
173 # Extract function schema
174 function_declaration = {
175 "name": tool.name,
176 "description": tool.description or "",
177 }
179 # Add parameters if available
180 # Note: tool.parameters might be a method, so check if callable
181 if hasattr(tool, "parameters"):
182 params = tool.parameters() if callable(tool.parameters) else tool.parameters
183 if params:
184 # Convert parameters to Gemini format
185 function_declaration["parameters"] = _convert_parameters(params) # type: ignore[assignment]
187 function_declarations.append(function_declaration)
189 # Return a single tools object with all function declarations
190 return [{"function_declarations": function_declarations}] if function_declarations else []
193def _convert_parameters(parameters: dict[str, Any]) -> dict[str, Any]:
194 """Convert agent-framework parameter schema to Gemini format.
196 Args:
197 parameters: Parameter schema from AIFunction
199 Returns:
200 Gemini-compatible parameter schema
201 """
202 # Gemini expects OpenAPI-like schema
203 gemini_params = {
204 "type": "object",
205 "properties": {},
206 }
208 # Copy properties if they exist
209 if "properties" in parameters:
210 gemini_params["properties"] = parameters["properties"]
212 # Copy required fields if they exist
213 if "required" in parameters:
214 gemini_params["required"] = parameters["required"]
216 return gemini_params
219def extract_usage_metadata(gemini_response: Any) -> dict[str, Any]:
220 """Extract token usage information from Gemini response.
222 Args:
223 gemini_response: Response object from Gemini API
225 Returns:
226 Dictionary with usage metadata
228 Example:
229 >>> usage = extract_usage_metadata(response)
230 >>> usage["prompt_tokens"]
231 15
232 """
233 usage = {}
235 if hasattr(gemini_response, "usage_metadata"):
236 metadata = gemini_response.usage_metadata
237 usage["prompt_tokens"] = getattr(metadata, "prompt_token_count", 0)
238 usage["completion_tokens"] = getattr(metadata, "candidates_token_count", 0)
239 usage["total_tokens"] = getattr(metadata, "total_token_count", 0)
241 return usage