langchain-google-genai 2.1.12__py3-none-any.whl → 3.0.0a1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of langchain-google-genai might be problematic. Click here for more details.
- langchain_google_genai/_common.py +2 -1
- langchain_google_genai/_compat.py +248 -0
- langchain_google_genai/_function_utils.py +30 -3
- langchain_google_genai/_genai_extension.py +25 -6
- langchain_google_genai/chat_models.py +405 -79
- langchain_google_genai/embeddings.py +4 -16
- {langchain_google_genai-2.1.12.dist-info → langchain_google_genai-3.0.0a1.dist-info}/METADATA +6 -6
- langchain_google_genai-3.0.0a1.dist-info/RECORD +18 -0
- langchain_google_genai-2.1.12.dist-info/RECORD +0 -17
- {langchain_google_genai-2.1.12.dist-info → langchain_google_genai-3.0.0a1.dist-info}/WHEEL +0 -0
- {langchain_google_genai-2.1.12.dist-info → langchain_google_genai-3.0.0a1.dist-info}/entry_points.txt +0 -0
- {langchain_google_genai-2.1.12.dist-info → langchain_google_genai-3.0.0a1.dist-info}/licenses/LICENSE +0 -0
|
@@ -52,7 +52,8 @@ Examples:
|
|
|
52
52
|
|
|
53
53
|
max_output_tokens: Optional[int] = Field(default=None, alias="max_tokens")
|
|
54
54
|
"""Maximum number of tokens to include in a candidate. Must be greater than zero.
|
|
55
|
-
If unset, will default
|
|
55
|
+
If unset, will use the model's default value, which varies by model.
|
|
56
|
+
See https://ai.google.dev/gemini-api/docs/models for model-specific limits."""
|
|
56
57
|
|
|
57
58
|
n: int = 1
|
|
58
59
|
"""Number of chat completions to generate for each prompt. Note that the API may
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
"""Go from v1 content blocks to generativelanguage_v1beta format."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import Any, Optional
|
|
5
|
+
|
|
6
|
+
from langchain_core.messages import content as types
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def translate_citations_to_grounding_metadata(
|
|
10
|
+
citations: list[types.Citation], web_search_queries: Optional[list[str]] = None
|
|
11
|
+
) -> dict[str, Any]:
|
|
12
|
+
"""Translate LangChain Citations to Google AI grounding metadata format.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
citations: List of Citation content blocks.
|
|
16
|
+
web_search_queries: Optional list of search queries that generated
|
|
17
|
+
the grounding data.
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
Google AI grounding metadata dictionary.
|
|
21
|
+
|
|
22
|
+
Example:
|
|
23
|
+
>>> citations = [
|
|
24
|
+
... create_citation(
|
|
25
|
+
... url="https://uefa.com/euro2024",
|
|
26
|
+
... title="UEFA Euro 2024 Results",
|
|
27
|
+
... start_index=0,
|
|
28
|
+
... end_index=47,
|
|
29
|
+
... cited_text="Spain won the UEFA Euro 2024 championship",
|
|
30
|
+
... )
|
|
31
|
+
... ]
|
|
32
|
+
>>> metadata = translate_citations_to_grounding_metadata(citations)
|
|
33
|
+
>>> len(metadata["groundingChunks"])
|
|
34
|
+
1
|
|
35
|
+
>>> metadata["groundingChunks"][0]["web"]["uri"]
|
|
36
|
+
'https://uefa.com/euro2024'
|
|
37
|
+
"""
|
|
38
|
+
if not citations:
|
|
39
|
+
return {}
|
|
40
|
+
|
|
41
|
+
# Group citations by text segment (start_index, end_index, cited_text)
|
|
42
|
+
segment_to_citations: dict[
|
|
43
|
+
tuple[Optional[int], Optional[int], Optional[str]], list[types.Citation]
|
|
44
|
+
] = {}
|
|
45
|
+
|
|
46
|
+
for citation in citations:
|
|
47
|
+
key = (
|
|
48
|
+
citation.get("start_index"),
|
|
49
|
+
citation.get("end_index"),
|
|
50
|
+
citation.get("cited_text"),
|
|
51
|
+
)
|
|
52
|
+
if key not in segment_to_citations:
|
|
53
|
+
segment_to_citations[key] = []
|
|
54
|
+
segment_to_citations[key].append(citation)
|
|
55
|
+
|
|
56
|
+
# Build grounding chunks from unique URLs
|
|
57
|
+
url_to_chunk_index: dict[str, int] = {}
|
|
58
|
+
grounding_chunks: list[dict[str, Any]] = []
|
|
59
|
+
|
|
60
|
+
for citation in citations:
|
|
61
|
+
url = citation.get("url")
|
|
62
|
+
if url and url not in url_to_chunk_index:
|
|
63
|
+
url_to_chunk_index[url] = len(grounding_chunks)
|
|
64
|
+
grounding_chunks.append(
|
|
65
|
+
{"web": {"uri": url, "title": citation.get("title", "")}}
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
# Build grounding supports
|
|
69
|
+
grounding_supports: list[dict[str, Any]] = []
|
|
70
|
+
|
|
71
|
+
for (
|
|
72
|
+
start_index,
|
|
73
|
+
end_index,
|
|
74
|
+
cited_text,
|
|
75
|
+
), citations_group in segment_to_citations.items():
|
|
76
|
+
if start_index is not None and end_index is not None and cited_text:
|
|
77
|
+
chunk_indices = []
|
|
78
|
+
confidence_scores = []
|
|
79
|
+
|
|
80
|
+
for citation in citations_group:
|
|
81
|
+
url = citation.get("url")
|
|
82
|
+
if url and url in url_to_chunk_index:
|
|
83
|
+
chunk_indices.append(url_to_chunk_index[url])
|
|
84
|
+
|
|
85
|
+
# Extract confidence scores from extras if available
|
|
86
|
+
extras = citation.get("extras", {})
|
|
87
|
+
google_metadata = extras.get("google_ai_metadata", {})
|
|
88
|
+
scores = google_metadata.get("confidence_scores", [])
|
|
89
|
+
confidence_scores.extend(scores)
|
|
90
|
+
|
|
91
|
+
support = {
|
|
92
|
+
"segment": {
|
|
93
|
+
"startIndex": start_index,
|
|
94
|
+
"endIndex": end_index,
|
|
95
|
+
"text": cited_text,
|
|
96
|
+
},
|
|
97
|
+
"groundingChunkIndices": chunk_indices,
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if confidence_scores:
|
|
101
|
+
support["confidenceScores"] = confidence_scores
|
|
102
|
+
|
|
103
|
+
grounding_supports.append(support)
|
|
104
|
+
|
|
105
|
+
# Extract search queries from extras if not provided
|
|
106
|
+
if web_search_queries is None:
|
|
107
|
+
web_search_queries = []
|
|
108
|
+
for citation in citations:
|
|
109
|
+
extras = citation.get("extras", {})
|
|
110
|
+
google_metadata = extras.get("google_ai_metadata", {})
|
|
111
|
+
queries = google_metadata.get("web_search_queries", [])
|
|
112
|
+
web_search_queries.extend(queries)
|
|
113
|
+
# Remove duplicates while preserving order
|
|
114
|
+
web_search_queries = list(dict.fromkeys(web_search_queries))
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
"webSearchQueries": web_search_queries,
|
|
118
|
+
"groundingChunks": grounding_chunks,
|
|
119
|
+
"groundingSupports": grounding_supports,
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _convert_from_v1_to_generativelanguage_v1beta(
|
|
124
|
+
content: list[types.ContentBlock], model_provider: str | None
|
|
125
|
+
) -> list[dict[str, Any]]:
|
|
126
|
+
"""Convert v1 content blocks to `google.ai.generativelanguage_v1beta.types.Content`.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
content: List of v1 `ContentBlock` objects.
|
|
130
|
+
model_provider: The model provider name that generated the v1 content.
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
List of dictionaries in `google.ai.generativelanguage_v1beta.types.Content`
|
|
134
|
+
format, ready to be sent to the API.
|
|
135
|
+
"""
|
|
136
|
+
new_content: list = []
|
|
137
|
+
for block in content:
|
|
138
|
+
if not isinstance(block, dict) or "type" not in block:
|
|
139
|
+
continue
|
|
140
|
+
|
|
141
|
+
block_dict = dict(block) # (For typing)
|
|
142
|
+
|
|
143
|
+
# TextContentBlock
|
|
144
|
+
if block_dict["type"] == "text":
|
|
145
|
+
new_block = {"text": block_dict.get("text", "")}
|
|
146
|
+
new_content.append(new_block)
|
|
147
|
+
# Citations are only handled on output. Can't pass them back :/
|
|
148
|
+
|
|
149
|
+
# ReasoningContentBlock -> thinking
|
|
150
|
+
elif block_dict["type"] == "reasoning" and model_provider == "google_genai":
|
|
151
|
+
# Google requires passing back the thought_signature when available.
|
|
152
|
+
# Signatures are only provided when function calling is enabled.
|
|
153
|
+
# If no signature is available, we skip the reasoning block as it cannot
|
|
154
|
+
# be properly serialized back to the API.
|
|
155
|
+
if "extras" in block_dict and isinstance(block_dict["extras"], dict):
|
|
156
|
+
extras = block_dict["extras"]
|
|
157
|
+
if "signature" in extras:
|
|
158
|
+
new_block = {
|
|
159
|
+
"thought": True,
|
|
160
|
+
"text": block_dict.get("reasoning", ""),
|
|
161
|
+
"thought_signature": extras["signature"],
|
|
162
|
+
}
|
|
163
|
+
new_content.append(new_block)
|
|
164
|
+
# else: skip reasoning blocks without signatures
|
|
165
|
+
# TODO: log a warning?
|
|
166
|
+
# else: skip reasoning blocks without extras
|
|
167
|
+
# TODO: log a warning?
|
|
168
|
+
|
|
169
|
+
# ImageContentBlock
|
|
170
|
+
elif block_dict["type"] == "image":
|
|
171
|
+
if base64 := block_dict.get("base64"):
|
|
172
|
+
new_block = {
|
|
173
|
+
"inline_data": {
|
|
174
|
+
"mime_type": block_dict.get("mime_type", "image/jpeg"),
|
|
175
|
+
"data": base64.encode("utf-8")
|
|
176
|
+
if isinstance(base64, str)
|
|
177
|
+
else base64,
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
new_content.append(new_block)
|
|
181
|
+
elif url := block_dict.get("url") and model_provider == "google_genai":
|
|
182
|
+
# Google file service
|
|
183
|
+
new_block = {
|
|
184
|
+
"file_data": {
|
|
185
|
+
"mime_type": block_dict.get("mime_type", "image/jpeg"),
|
|
186
|
+
"file_uri": block_dict[str(url)],
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
new_content.append(new_block)
|
|
190
|
+
|
|
191
|
+
# TODO: AudioContentBlock -> audio once models support passing back in
|
|
192
|
+
|
|
193
|
+
# FileContentBlock (documents)
|
|
194
|
+
elif block_dict["type"] == "file":
|
|
195
|
+
if base64 := block_dict.get("base64"):
|
|
196
|
+
new_block = {
|
|
197
|
+
"inline_data": {
|
|
198
|
+
"mime_type": block_dict.get(
|
|
199
|
+
"mime_type", "application/octet-stream"
|
|
200
|
+
),
|
|
201
|
+
"data": base64.encode("utf-8")
|
|
202
|
+
if isinstance(base64, str)
|
|
203
|
+
else base64,
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
new_content.append(new_block)
|
|
207
|
+
elif url := block_dict.get("url") and model_provider == "google_genai":
|
|
208
|
+
# Google file service
|
|
209
|
+
new_block = {
|
|
210
|
+
"file_data": {
|
|
211
|
+
"mime_type": block_dict.get(
|
|
212
|
+
"mime_type", "application/octet-stream"
|
|
213
|
+
),
|
|
214
|
+
"file_uri": block_dict[str(url)],
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
new_content.append(new_block)
|
|
218
|
+
|
|
219
|
+
# ToolCall -> FunctionCall
|
|
220
|
+
elif block_dict["type"] == "tool_call":
|
|
221
|
+
function_call = {
|
|
222
|
+
"function_call": {
|
|
223
|
+
"name": block_dict.get("name", ""),
|
|
224
|
+
"args": block_dict.get("args", {}),
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
new_content.append(function_call)
|
|
228
|
+
|
|
229
|
+
# ToolCallChunk -> FunctionCall
|
|
230
|
+
elif block_dict["type"] == "tool_call_chunk":
|
|
231
|
+
try:
|
|
232
|
+
args_str = block_dict.get("args") or "{}"
|
|
233
|
+
input_ = json.loads(args_str) if isinstance(args_str, str) else args_str
|
|
234
|
+
except json.JSONDecodeError:
|
|
235
|
+
input_ = {}
|
|
236
|
+
|
|
237
|
+
function_call = {
|
|
238
|
+
"function_call": {
|
|
239
|
+
"name": block_dict.get("name", "no_tool_name_present"),
|
|
240
|
+
"args": input_,
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
new_content.append(function_call)
|
|
244
|
+
|
|
245
|
+
# NonStandardContentBlock
|
|
246
|
+
# TODO: Handle new server tools
|
|
247
|
+
|
|
248
|
+
return new_content
|
|
@@ -330,8 +330,10 @@ def _get_properties_from_schema(schema: Dict) -> Dict[str, Any]:
|
|
|
330
330
|
continue
|
|
331
331
|
properties_item: Dict[str, Union[str, int, Dict, List]] = {}
|
|
332
332
|
|
|
333
|
-
#
|
|
334
|
-
|
|
333
|
+
# Preserve description and other schema properties before manipulation
|
|
334
|
+
original_description = v.get("description")
|
|
335
|
+
original_enum = v.get("enum")
|
|
336
|
+
original_items = v.get("items")
|
|
335
337
|
|
|
336
338
|
if v.get("anyOf") and all(
|
|
337
339
|
anyOf_type.get("type") != "null" for anyOf_type in v.get("anyOf", [])
|
|
@@ -354,11 +356,34 @@ def _get_properties_from_schema(schema: Dict) -> Dict[str, Any]:
|
|
|
354
356
|
if any_of_types and item_type_ in [glm.Type.ARRAY, glm.Type.OBJECT]:
|
|
355
357
|
json_type_ = "array" if item_type_ == glm.Type.ARRAY else "object"
|
|
356
358
|
# Use Index -1 for consistency with `_get_nullable_type_from_schema`
|
|
357
|
-
|
|
359
|
+
filtered_schema = [
|
|
360
|
+
val for val in any_of_types if val.get("type") == json_type_
|
|
361
|
+
][-1]
|
|
362
|
+
# Merge filtered schema with original properties to preserve enum/items
|
|
363
|
+
v = filtered_schema.copy()
|
|
364
|
+
if original_enum and not v.get("enum"):
|
|
365
|
+
v["enum"] = original_enum
|
|
366
|
+
if original_items and not v.get("items"):
|
|
367
|
+
v["items"] = original_items
|
|
368
|
+
elif any_of_types:
|
|
369
|
+
# For other types (like strings with enums), find the non-null schema
|
|
370
|
+
# and preserve enum/items from the original anyOf structure
|
|
371
|
+
non_null_schemas = [
|
|
372
|
+
val for val in any_of_types if val.get("type") != "null"
|
|
373
|
+
]
|
|
374
|
+
if non_null_schemas:
|
|
375
|
+
filtered_schema = non_null_schemas[-1]
|
|
376
|
+
v = filtered_schema.copy()
|
|
377
|
+
if original_enum and not v.get("enum"):
|
|
378
|
+
v["enum"] = original_enum
|
|
379
|
+
if original_items and not v.get("items"):
|
|
380
|
+
v["items"] = original_items
|
|
358
381
|
|
|
359
382
|
if v.get("enum"):
|
|
360
383
|
properties_item["enum"] = v["enum"]
|
|
361
384
|
|
|
385
|
+
# Prefer description from the filtered schema, fall back to original
|
|
386
|
+
description = v.get("description") or original_description
|
|
362
387
|
if description and isinstance(description, str):
|
|
363
388
|
properties_item["description"] = description
|
|
364
389
|
|
|
@@ -415,6 +440,8 @@ def _get_items_from_schema(schema: Union[Dict, List, str]) -> Dict[str, Any]:
|
|
|
415
440
|
items["description"] = (
|
|
416
441
|
schema.get("description") or schema.get("title") or ""
|
|
417
442
|
)
|
|
443
|
+
if "enum" in schema:
|
|
444
|
+
items["enum"] = schema["enum"]
|
|
418
445
|
if _is_nullable_schema(schema):
|
|
419
446
|
items["nullable"] = True
|
|
420
447
|
if "required" in schema:
|
|
@@ -632,22 +632,41 @@ def generate_answer(
|
|
|
632
632
|
)
|
|
633
633
|
|
|
634
634
|
|
|
635
|
-
# TODO: Use candidate.finish_message when that field is launched.
|
|
636
|
-
# For now, we derive this message from other existing fields.
|
|
637
635
|
def _get_finish_message(candidate: genai.Candidate) -> str:
|
|
636
|
+
"""Get a human-readable finish message from the candidate.
|
|
637
|
+
|
|
638
|
+
Uses the official finish_message field if available, otherwise falls back
|
|
639
|
+
to a manual mapping of finish reasons to descriptive messages.
|
|
640
|
+
"""
|
|
641
|
+
# Use the official field when available
|
|
642
|
+
if hasattr(candidate, "finish_message") and candidate.finish_message:
|
|
643
|
+
return candidate.finish_message
|
|
644
|
+
|
|
645
|
+
# Fallback to manual mapping for all known finish reasons
|
|
638
646
|
finish_messages: Dict[int, str] = {
|
|
647
|
+
genai.Candidate.FinishReason.STOP: "Generation completed successfully",
|
|
639
648
|
genai.Candidate.FinishReason.MAX_TOKENS: (
|
|
640
649
|
"Maximum token in context window reached"
|
|
641
650
|
),
|
|
642
651
|
genai.Candidate.FinishReason.SAFETY: "Blocked because of safety",
|
|
643
652
|
genai.Candidate.FinishReason.RECITATION: "Blocked because of recitation",
|
|
653
|
+
genai.Candidate.FinishReason.LANGUAGE: "Unsupported language detected",
|
|
654
|
+
genai.Candidate.FinishReason.BLOCKLIST: "Content hit forbidden terms",
|
|
655
|
+
genai.Candidate.FinishReason.PROHIBITED_CONTENT: (
|
|
656
|
+
"Inappropriate content detected"
|
|
657
|
+
),
|
|
658
|
+
genai.Candidate.FinishReason.SPII: "Sensitive personal information detected",
|
|
659
|
+
genai.Candidate.FinishReason.IMAGE_SAFETY: "Image safety violation",
|
|
660
|
+
genai.Candidate.FinishReason.MALFORMED_FUNCTION_CALL: "Malformed function call",
|
|
661
|
+
genai.Candidate.FinishReason.UNEXPECTED_TOOL_CALL: "Unexpected tool call",
|
|
662
|
+
genai.Candidate.FinishReason.OTHER: "Other generation issue",
|
|
663
|
+
genai.Candidate.FinishReason.FINISH_REASON_UNSPECIFIED: (
|
|
664
|
+
"Unspecified finish reason"
|
|
665
|
+
),
|
|
644
666
|
}
|
|
645
667
|
|
|
646
668
|
finish_reason = candidate.finish_reason
|
|
647
|
-
|
|
648
|
-
return "Unexpected generation error"
|
|
649
|
-
|
|
650
|
-
return finish_messages[finish_reason]
|
|
669
|
+
return finish_messages.get(finish_reason, "Unexpected generation error")
|
|
651
670
|
|
|
652
671
|
|
|
653
672
|
def _convert_to_metadata(metadata: Dict[str, Any]) -> List[genai.CustomMetadata]:
|
|
@@ -58,11 +58,16 @@ from google.api_core.exceptions import (
|
|
|
58
58
|
ResourceExhausted,
|
|
59
59
|
ServiceUnavailable,
|
|
60
60
|
)
|
|
61
|
+
from google.protobuf.json_format import MessageToDict
|
|
61
62
|
from langchain_core.callbacks.manager import (
|
|
62
63
|
AsyncCallbackManagerForLLMRun,
|
|
63
64
|
CallbackManagerForLLMRun,
|
|
64
65
|
)
|
|
65
|
-
from langchain_core.language_models import
|
|
66
|
+
from langchain_core.language_models import (
|
|
67
|
+
LangSmithParams,
|
|
68
|
+
LanguageModelInput,
|
|
69
|
+
is_openai_data_block,
|
|
70
|
+
)
|
|
66
71
|
from langchain_core.language_models.chat_models import BaseChatModel
|
|
67
72
|
from langchain_core.messages import (
|
|
68
73
|
AIMessage,
|
|
@@ -74,6 +79,7 @@ from langchain_core.messages import (
|
|
|
74
79
|
ToolMessage,
|
|
75
80
|
is_data_content_block,
|
|
76
81
|
)
|
|
82
|
+
from langchain_core.messages import content as types
|
|
77
83
|
from langchain_core.messages.ai import UsageMetadata, add_usage, subtract_usage
|
|
78
84
|
from langchain_core.messages.tool import invalid_tool_call, tool_call, tool_call_chunk
|
|
79
85
|
from langchain_core.output_parsers import JsonOutputParser, PydanticOutputParser
|
|
@@ -110,6 +116,9 @@ from langchain_google_genai._common import (
|
|
|
110
116
|
_BaseGoogleGenerativeAI,
|
|
111
117
|
get_client_info,
|
|
112
118
|
)
|
|
119
|
+
from langchain_google_genai._compat import (
|
|
120
|
+
_convert_from_v1_to_generativelanguage_v1beta,
|
|
121
|
+
)
|
|
113
122
|
from langchain_google_genai._function_utils import (
|
|
114
123
|
_dict_to_gapic_schema,
|
|
115
124
|
_tool_choice_to_tool_config,
|
|
@@ -287,56 +296,78 @@ async def _achat_with_retry(generation_method: Callable, **kwargs: Any) -> Any:
|
|
|
287
296
|
return await _achat_with_retry(**params)
|
|
288
297
|
|
|
289
298
|
|
|
290
|
-
def _is_lc_content_block(part: dict) -> bool:
|
|
291
|
-
return "type" in part
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
def _is_openai_image_block(block: dict) -> bool:
|
|
295
|
-
"""Check if the block contains image data in OpenAI Chat Completions format."""
|
|
296
|
-
if block.get("type") == "image_url":
|
|
297
|
-
if (
|
|
298
|
-
(set(block.keys()) <= {"type", "image_url", "detail"})
|
|
299
|
-
and (image_url := block.get("image_url"))
|
|
300
|
-
and isinstance(image_url, dict)
|
|
301
|
-
):
|
|
302
|
-
url = image_url.get("url")
|
|
303
|
-
if isinstance(url, str):
|
|
304
|
-
return True
|
|
305
|
-
else:
|
|
306
|
-
return False
|
|
307
|
-
|
|
308
|
-
return False
|
|
309
|
-
|
|
310
|
-
|
|
311
299
|
def _convert_to_parts(
|
|
312
300
|
raw_content: Union[str, Sequence[Union[str, dict]]],
|
|
313
301
|
) -> List[Part]:
|
|
314
|
-
"""Converts
|
|
315
|
-
|
|
302
|
+
"""Converts LangChain message content into generativelanguage_v1beta parts.
|
|
303
|
+
|
|
304
|
+
Used when preparing Human, System and AI messages for sending to the API.
|
|
305
|
+
|
|
306
|
+
Handles both legacy (pre-v1) dict-based content blocks and v1 ContentBlock objects.
|
|
307
|
+
"""
|
|
316
308
|
content = [raw_content] if isinstance(raw_content, str) else raw_content
|
|
317
309
|
image_loader = ImageBytesLoader()
|
|
310
|
+
|
|
311
|
+
parts = []
|
|
312
|
+
# Iterate over each item in the content list, constructing a list of Parts
|
|
318
313
|
for part in content:
|
|
319
314
|
if isinstance(part, str):
|
|
320
315
|
parts.append(Part(text=part))
|
|
321
316
|
elif isinstance(part, Mapping):
|
|
322
|
-
if
|
|
317
|
+
if "type" in part:
|
|
323
318
|
if part["type"] == "text":
|
|
324
|
-
|
|
319
|
+
# Either old dict-style CC text block or new TextContentBlock
|
|
320
|
+
# Check if there's a signature attached to this text block
|
|
321
|
+
thought_sig = None
|
|
322
|
+
if "extras" in part and isinstance(part["extras"], dict):
|
|
323
|
+
sig = part["extras"].get("signature")
|
|
324
|
+
if sig and isinstance(sig, str):
|
|
325
|
+
# Decode base64-encoded signature back to bytes
|
|
326
|
+
thought_sig = base64.b64decode(sig)
|
|
327
|
+
if thought_sig:
|
|
328
|
+
parts.append(
|
|
329
|
+
Part(text=part["text"], thought_signature=thought_sig)
|
|
330
|
+
)
|
|
331
|
+
else:
|
|
332
|
+
parts.append(Part(text=part["text"]))
|
|
325
333
|
elif is_data_content_block(part):
|
|
326
|
-
|
|
334
|
+
# Handle both legacy LC blocks (with `source_type`) and blocks >= v1
|
|
335
|
+
|
|
336
|
+
if "source_type" in part:
|
|
337
|
+
# Catch legacy v0 formats
|
|
338
|
+
# Safe since v1 content blocks don't have `source_type` key
|
|
339
|
+
if part["source_type"] == "url":
|
|
340
|
+
bytes_ = image_loader._bytes_from_url(part["url"])
|
|
341
|
+
elif part["source_type"] == "base64":
|
|
342
|
+
bytes_ = base64.b64decode(part["data"])
|
|
343
|
+
else:
|
|
344
|
+
# Unable to support IDContentBlock
|
|
345
|
+
msg = "source_type must be url or base64."
|
|
346
|
+
raise ValueError(msg)
|
|
347
|
+
elif "url" in part:
|
|
348
|
+
# v1 multimodal block w/ URL
|
|
327
349
|
bytes_ = image_loader._bytes_from_url(part["url"])
|
|
328
|
-
elif
|
|
329
|
-
|
|
350
|
+
elif "base64" in part:
|
|
351
|
+
# v1 multimodal block w/ base64
|
|
352
|
+
bytes_ = base64.b64decode(part["base64"])
|
|
330
353
|
else:
|
|
331
|
-
msg =
|
|
354
|
+
msg = (
|
|
355
|
+
"Data content block must contain 'url', 'base64', or "
|
|
356
|
+
"'data' field."
|
|
357
|
+
)
|
|
332
358
|
raise ValueError(msg)
|
|
333
359
|
inline_data: dict = {"data": bytes_}
|
|
334
360
|
if "mime_type" in part:
|
|
335
361
|
inline_data["mime_type"] = part["mime_type"]
|
|
336
362
|
else:
|
|
337
|
-
|
|
363
|
+
# Guess mime type based on data field if not provided
|
|
364
|
+
source = cast(
|
|
365
|
+
"str",
|
|
366
|
+
part.get("url") or part.get("base64") or part.get("data"),
|
|
367
|
+
)
|
|
338
368
|
mime_type, _ = mimetypes.guess_type(source)
|
|
339
369
|
if not mime_type:
|
|
370
|
+
# Last resort - try to guess based on file bytes
|
|
340
371
|
kind = filetype.guess(bytes_)
|
|
341
372
|
if kind:
|
|
342
373
|
mime_type = kind.mime
|
|
@@ -344,6 +375,7 @@ def _convert_to_parts(
|
|
|
344
375
|
inline_data["mime_type"] = mime_type
|
|
345
376
|
parts.append(Part(inline_data=inline_data))
|
|
346
377
|
elif part["type"] == "image_url":
|
|
378
|
+
# Chat Completions image format
|
|
347
379
|
img_url = part["image_url"]
|
|
348
380
|
if isinstance(img_url, dict):
|
|
349
381
|
if "url" not in img_url:
|
|
@@ -351,9 +383,9 @@ def _convert_to_parts(
|
|
|
351
383
|
raise ValueError(msg)
|
|
352
384
|
img_url = img_url["url"]
|
|
353
385
|
parts.append(image_loader.load_part(img_url))
|
|
354
|
-
# Handle media type like LangChain.js
|
|
355
|
-
# https://github.com/langchain-ai/langchainjs/blob/e536593e2585f1dd7b0afc187de4d07cb40689ba/libs/langchain-google-common/src/utils/gemini.ts#L93-L106
|
|
356
386
|
elif part["type"] == "media":
|
|
387
|
+
# Handle `media` following pattern established in LangChain.js
|
|
388
|
+
# https://github.com/langchain-ai/langchainjs/blob/e536593e2585f1dd7b0afc187de4d07cb40689ba/libs/langchain-google-common/src/utils/gemini.ts#L93-L106
|
|
357
389
|
if "mime_type" not in part:
|
|
358
390
|
msg = f"Missing mime_type in media part: {part}"
|
|
359
391
|
raise ValueError(msg)
|
|
@@ -361,10 +393,12 @@ def _convert_to_parts(
|
|
|
361
393
|
media_part = Part()
|
|
362
394
|
|
|
363
395
|
if "data" in part:
|
|
396
|
+
# Embedded media
|
|
364
397
|
media_part.inline_data = Blob(
|
|
365
398
|
data=part["data"], mime_type=mime_type
|
|
366
399
|
)
|
|
367
400
|
elif "file_uri" in part:
|
|
401
|
+
# Referenced files (e.g. stored in GCS)
|
|
368
402
|
media_part.file_data = FileData(
|
|
369
403
|
file_uri=part["file_uri"], mime_type=mime_type
|
|
370
404
|
)
|
|
@@ -375,7 +409,59 @@ def _convert_to_parts(
|
|
|
375
409
|
metadata = VideoMetadata(part["video_metadata"])
|
|
376
410
|
media_part.video_metadata = metadata
|
|
377
411
|
parts.append(media_part)
|
|
412
|
+
elif part["type"] == "function_call_signature":
|
|
413
|
+
# Signature for function_call Part - skip it here as it should be
|
|
414
|
+
# attached to the actual function_call Part
|
|
415
|
+
# This is handled separately in the history parsing logic
|
|
416
|
+
pass
|
|
417
|
+
elif part["type"] == "thinking":
|
|
418
|
+
# Pre-existing thinking block format that we continue to store as
|
|
419
|
+
thought_sig = None
|
|
420
|
+
if "signature" in part:
|
|
421
|
+
sig = part["signature"]
|
|
422
|
+
if sig and isinstance(sig, str):
|
|
423
|
+
# Decode base64-encoded signature back to bytes
|
|
424
|
+
thought_sig = base64.b64decode(sig)
|
|
425
|
+
parts.append(
|
|
426
|
+
Part(
|
|
427
|
+
text=part["thinking"],
|
|
428
|
+
thought=True,
|
|
429
|
+
thought_signature=thought_sig,
|
|
430
|
+
)
|
|
431
|
+
)
|
|
432
|
+
elif part["type"] == "reasoning":
|
|
433
|
+
# ReasoningContentBlock (when output_version = "v1")
|
|
434
|
+
extras = part.get("extras", {}) or {}
|
|
435
|
+
sig = extras.get("signature")
|
|
436
|
+
thought_sig = None
|
|
437
|
+
if sig and isinstance(sig, str):
|
|
438
|
+
# Decode base64-encoded signature back to bytes
|
|
439
|
+
thought_sig = base64.b64decode(sig)
|
|
440
|
+
parts.append(
|
|
441
|
+
Part(
|
|
442
|
+
text=part["reasoning"],
|
|
443
|
+
thought=True,
|
|
444
|
+
thought_signature=thought_sig,
|
|
445
|
+
)
|
|
446
|
+
)
|
|
447
|
+
elif part["type"] == "server_tool_call":
|
|
448
|
+
if part.get("name") == "code_interpreter":
|
|
449
|
+
args = part.get("args", {})
|
|
450
|
+
code = args.get("code", "")
|
|
451
|
+
language = args.get("language", "python")
|
|
452
|
+
executable_code_part = Part(
|
|
453
|
+
executable_code=ExecutableCode(language=language, code=code)
|
|
454
|
+
)
|
|
455
|
+
parts.append(executable_code_part)
|
|
456
|
+
else:
|
|
457
|
+
warnings.warn(
|
|
458
|
+
f"Server tool call with name '{part.get('name')}' is not "
|
|
459
|
+
"currently supported by Google GenAI. Only "
|
|
460
|
+
"'code_interpreter' is supported.",
|
|
461
|
+
stacklevel=2,
|
|
462
|
+
)
|
|
378
463
|
elif part["type"] == "executable_code":
|
|
464
|
+
# Legacy executable_code format (backward compat)
|
|
379
465
|
if "executable_code" not in part or "language" not in part:
|
|
380
466
|
msg = (
|
|
381
467
|
"Executable code part must have 'code' and 'language' "
|
|
@@ -388,7 +474,22 @@ def _convert_to_parts(
|
|
|
388
474
|
)
|
|
389
475
|
)
|
|
390
476
|
parts.append(executable_code_part)
|
|
477
|
+
elif part["type"] == "server_tool_result":
|
|
478
|
+
output = part.get("output", "")
|
|
479
|
+
status = part.get("status", "success")
|
|
480
|
+
# Map status to outcome: success → 1 (OUTCOME_OK), error → 2
|
|
481
|
+
outcome = 1 if status == "success" else 2
|
|
482
|
+
# Check extras for original outcome if available
|
|
483
|
+
if "extras" in part and "outcome" in part["extras"]:
|
|
484
|
+
outcome = part["extras"]["outcome"]
|
|
485
|
+
code_execution_result_part = Part(
|
|
486
|
+
code_execution_result=CodeExecutionResult(
|
|
487
|
+
output=str(output), outcome=outcome
|
|
488
|
+
)
|
|
489
|
+
)
|
|
490
|
+
parts.append(code_execution_result_part)
|
|
391
491
|
elif part["type"] == "code_execution_result":
|
|
492
|
+
# Legacy code_execution_result format (backward compat)
|
|
392
493
|
if "code_execution_result" not in part:
|
|
393
494
|
msg = (
|
|
394
495
|
"Code execution result part must have "
|
|
@@ -406,24 +507,17 @@ def _convert_to_parts(
|
|
|
406
507
|
)
|
|
407
508
|
)
|
|
408
509
|
parts.append(code_execution_result_part)
|
|
409
|
-
elif part["type"] == "thinking":
|
|
410
|
-
parts.append(Part(text=part["thinking"], thought=True))
|
|
411
510
|
else:
|
|
412
|
-
msg =
|
|
413
|
-
f"Unrecognized message part type: {part['type']}. Only text, "
|
|
414
|
-
f"image_url, and media types are supported."
|
|
415
|
-
)
|
|
511
|
+
msg = f"Unrecognized message part type: {part['type']}."
|
|
416
512
|
raise ValueError(msg)
|
|
417
513
|
else:
|
|
418
|
-
# Yolo
|
|
514
|
+
# Yolo. The input message content doesn't have a `type` key
|
|
419
515
|
logger.warning(
|
|
420
516
|
"Unrecognized message part format. Assuming it's a text part."
|
|
421
517
|
)
|
|
422
518
|
parts.append(Part(text=str(part)))
|
|
423
519
|
else:
|
|
424
|
-
|
|
425
|
-
# would hit this branch.
|
|
426
|
-
msg = "Gemini only supports text and inline_data parts."
|
|
520
|
+
msg = "Unknown error occurred while converting LC message content to parts."
|
|
427
521
|
raise ChatGoogleGenerativeAIError(msg)
|
|
428
522
|
return parts
|
|
429
523
|
|
|
@@ -441,7 +535,7 @@ def _convert_tool_message_to_parts(
|
|
|
441
535
|
other_blocks = []
|
|
442
536
|
for block in message.content:
|
|
443
537
|
if isinstance(block, dict) and (
|
|
444
|
-
is_data_content_block(block) or
|
|
538
|
+
is_data_content_block(block) or is_openai_data_block(block)
|
|
445
539
|
):
|
|
446
540
|
media_blocks.append(block)
|
|
447
541
|
else:
|
|
@@ -496,7 +590,20 @@ def _get_ai_message_tool_messages_parts(
|
|
|
496
590
|
def _parse_chat_history(
|
|
497
591
|
input_messages: Sequence[BaseMessage], convert_system_message_to_human: bool = False
|
|
498
592
|
) -> Tuple[Optional[Content], List[Content]]:
|
|
499
|
-
|
|
593
|
+
"""Parses sequence of `BaseMessage` into system instruction and formatted messages.
|
|
594
|
+
|
|
595
|
+
Args:
|
|
596
|
+
input_messages: Sequence of `BaseMessage` objects representing the chat history.
|
|
597
|
+
convert_system_message_to_human: Whether to convert the first system message
|
|
598
|
+
into a human message. Deprecated, use system instructions instead.
|
|
599
|
+
|
|
600
|
+
Returns:
|
|
601
|
+
A tuple containing:
|
|
602
|
+
- An optional `google.ai.generativelanguage_v1beta.types.Content` representing
|
|
603
|
+
the system instruction (if any).
|
|
604
|
+
- A list of `google.ai.generativelanguage_v1beta.types.Content` representing the
|
|
605
|
+
formatted messages.
|
|
606
|
+
"""
|
|
500
607
|
|
|
501
608
|
if convert_system_message_to_human:
|
|
502
609
|
warnings.warn(
|
|
@@ -505,6 +612,28 @@ def _parse_chat_history(
|
|
|
505
612
|
DeprecationWarning,
|
|
506
613
|
stacklevel=2,
|
|
507
614
|
)
|
|
615
|
+
input_messages = list(input_messages) # Make a mutable copy
|
|
616
|
+
|
|
617
|
+
# Case where content was serialized to v1 format
|
|
618
|
+
for idx, message in enumerate(input_messages):
|
|
619
|
+
if (
|
|
620
|
+
isinstance(message, AIMessage)
|
|
621
|
+
and message.response_metadata.get("output_version") == "v1"
|
|
622
|
+
):
|
|
623
|
+
# Unpack known v1 content to v1beta format for the request
|
|
624
|
+
#
|
|
625
|
+
# Old content types and any previously serialized messages passed back in to
|
|
626
|
+
# history will skip this, but hit and processed in `_convert_to_parts`
|
|
627
|
+
input_messages[idx] = message.model_copy(
|
|
628
|
+
update={
|
|
629
|
+
"content": _convert_from_v1_to_generativelanguage_v1beta(
|
|
630
|
+
cast(list[types.ContentBlock], message.content),
|
|
631
|
+
message.response_metadata.get("model_provider"),
|
|
632
|
+
)
|
|
633
|
+
}
|
|
634
|
+
)
|
|
635
|
+
|
|
636
|
+
formatted_messages: List[Content] = []
|
|
508
637
|
|
|
509
638
|
system_instruction: Optional[Content] = None
|
|
510
639
|
messages_without_tool_messages = [
|
|
@@ -527,19 +656,43 @@ def _parse_chat_history(
|
|
|
527
656
|
role = "model"
|
|
528
657
|
if message.tool_calls:
|
|
529
658
|
ai_message_parts = []
|
|
530
|
-
|
|
659
|
+
# Extract any function_call_signature blocks from content
|
|
660
|
+
function_call_sigs: dict[int, bytes] = {}
|
|
661
|
+
if isinstance(message.content, list):
|
|
662
|
+
for idx, item in enumerate(message.content):
|
|
663
|
+
if (
|
|
664
|
+
isinstance(item, dict)
|
|
665
|
+
and item.get("type") == "function_call_signature"
|
|
666
|
+
):
|
|
667
|
+
sig_str = item.get("signature", "")
|
|
668
|
+
if sig_str and isinstance(sig_str, str):
|
|
669
|
+
# Decode base64-encoded signature back to bytes
|
|
670
|
+
sig_bytes = base64.b64decode(sig_str)
|
|
671
|
+
function_call_sigs[idx] = sig_bytes
|
|
672
|
+
|
|
673
|
+
for tool_call_idx, tool_call in enumerate(message.tool_calls):
|
|
531
674
|
function_call = FunctionCall(
|
|
532
675
|
{
|
|
533
676
|
"name": tool_call["name"],
|
|
534
677
|
"args": tool_call["args"],
|
|
535
678
|
}
|
|
536
679
|
)
|
|
537
|
-
|
|
680
|
+
# Check if there's a signature for this function call
|
|
681
|
+
# (We use the index to match signature to function call)
|
|
682
|
+
sig = function_call_sigs.get(tool_call_idx)
|
|
683
|
+
if sig:
|
|
684
|
+
ai_message_parts.append(
|
|
685
|
+
Part(function_call=function_call, thought_signature=sig)
|
|
686
|
+
)
|
|
687
|
+
else:
|
|
688
|
+
ai_message_parts.append(Part(function_call=function_call))
|
|
538
689
|
tool_messages_parts = _get_ai_message_tool_messages_parts(
|
|
539
690
|
tool_messages=tool_messages, ai_message=message
|
|
540
691
|
)
|
|
541
|
-
|
|
542
|
-
|
|
692
|
+
formatted_messages.append(Content(role=role, parts=ai_message_parts))
|
|
693
|
+
formatted_messages.append(
|
|
694
|
+
Content(role="user", parts=tool_messages_parts)
|
|
695
|
+
)
|
|
543
696
|
continue
|
|
544
697
|
if raw_function_call := message.additional_kwargs.get("function_call"):
|
|
545
698
|
function_call = FunctionCall(
|
|
@@ -550,7 +703,12 @@ def _parse_chat_history(
|
|
|
550
703
|
)
|
|
551
704
|
parts = [Part(function_call=function_call)]
|
|
552
705
|
else:
|
|
553
|
-
|
|
706
|
+
if message.response_metadata.get("output_version") == "v1":
|
|
707
|
+
# Already converted to v1beta format above
|
|
708
|
+
parts = message.content # type: ignore[assignment]
|
|
709
|
+
else:
|
|
710
|
+
# Prepare request content parts from message.content field
|
|
711
|
+
parts = _convert_to_parts(message.content)
|
|
554
712
|
elif isinstance(message, HumanMessage):
|
|
555
713
|
role = "user"
|
|
556
714
|
parts = _convert_to_parts(message.content)
|
|
@@ -564,8 +722,11 @@ def _parse_chat_history(
|
|
|
564
722
|
msg = f"Unexpected message with type {type(message)} at the position {i}."
|
|
565
723
|
raise ValueError(msg)
|
|
566
724
|
|
|
567
|
-
|
|
568
|
-
|
|
725
|
+
# Final step; assemble the Content object to pass to the API
|
|
726
|
+
# If version = "v1", the parts are already in v1beta format and will be
|
|
727
|
+
# automatically converted using protobuf's auto-conversion
|
|
728
|
+
formatted_messages.append(Content(role=role, parts=parts))
|
|
729
|
+
return system_instruction, formatted_messages
|
|
569
730
|
|
|
570
731
|
|
|
571
732
|
# Helper function to append content consistently
|
|
@@ -589,13 +750,20 @@ def _append_to_content(
|
|
|
589
750
|
|
|
590
751
|
|
|
591
752
|
def _parse_response_candidate(
|
|
592
|
-
response_candidate: Candidate,
|
|
753
|
+
response_candidate: Candidate,
|
|
754
|
+
streaming: bool = False,
|
|
755
|
+
model_name: Optional[str] = None,
|
|
593
756
|
) -> AIMessage:
|
|
594
757
|
content: Union[None, str, List[Union[str, dict]]] = None
|
|
595
758
|
additional_kwargs: Dict[str, Any] = {}
|
|
759
|
+
response_metadata: Dict[str, Any] = {"model_provider": "google_genai"}
|
|
760
|
+
if model_name:
|
|
761
|
+
response_metadata["model_name"] = model_name
|
|
596
762
|
tool_calls = []
|
|
597
763
|
invalid_tool_calls = []
|
|
598
764
|
tool_call_chunks = []
|
|
765
|
+
# Track function call signatures separately to handle them conditionally
|
|
766
|
+
function_call_signatures: List[dict] = []
|
|
599
767
|
|
|
600
768
|
for part in response_candidate.content.parts:
|
|
601
769
|
text: Optional[str] = None
|
|
@@ -608,21 +776,49 @@ def _parse_response_candidate(
|
|
|
608
776
|
except AttributeError:
|
|
609
777
|
pass
|
|
610
778
|
|
|
779
|
+
# Extract thought signature if present (can be on any Part type)
|
|
780
|
+
# Signatures are binary data, encode to base64 string for JSON serialization
|
|
781
|
+
thought_sig: Optional[str] = None
|
|
782
|
+
if hasattr(part, "thought_signature") and part.thought_signature:
|
|
783
|
+
try:
|
|
784
|
+
# Encode binary signature to base64 string
|
|
785
|
+
thought_sig = base64.b64encode(part.thought_signature).decode("ascii")
|
|
786
|
+
if not thought_sig: # Empty string
|
|
787
|
+
thought_sig = None
|
|
788
|
+
except (AttributeError, TypeError):
|
|
789
|
+
thought_sig = None
|
|
790
|
+
|
|
611
791
|
if hasattr(part, "thought") and part.thought:
|
|
612
792
|
thinking_message = {
|
|
613
793
|
"type": "thinking",
|
|
614
794
|
"thinking": part.text,
|
|
615
795
|
}
|
|
796
|
+
# Include signature if present
|
|
797
|
+
if thought_sig:
|
|
798
|
+
thinking_message["signature"] = thought_sig
|
|
616
799
|
content = _append_to_content(content, thinking_message)
|
|
617
800
|
elif text is not None and text:
|
|
618
|
-
|
|
801
|
+
# Check if this text Part has a signature attached
|
|
802
|
+
if thought_sig:
|
|
803
|
+
# Text with signature needs structured block to preserve signature
|
|
804
|
+
# We use a v1 TextContentBlock
|
|
805
|
+
text_with_sig = {
|
|
806
|
+
"type": "text",
|
|
807
|
+
"text": text,
|
|
808
|
+
"extras": {"signature": thought_sig},
|
|
809
|
+
}
|
|
810
|
+
content = _append_to_content(content, text_with_sig)
|
|
811
|
+
else:
|
|
812
|
+
content = _append_to_content(content, text)
|
|
619
813
|
|
|
620
814
|
if hasattr(part, "executable_code") and part.executable_code is not None:
|
|
621
815
|
if part.executable_code.code and part.executable_code.language:
|
|
816
|
+
code_id = str(uuid.uuid4()) # Generate ID if not present, needed later
|
|
622
817
|
code_message = {
|
|
623
818
|
"type": "executable_code",
|
|
624
819
|
"executable_code": part.executable_code.code,
|
|
625
820
|
"language": part.executable_code.language,
|
|
821
|
+
"id": code_id,
|
|
626
822
|
}
|
|
627
823
|
content = _append_to_content(content, code_message)
|
|
628
824
|
|
|
@@ -630,14 +826,21 @@ def _parse_response_candidate(
|
|
|
630
826
|
hasattr(part, "code_execution_result")
|
|
631
827
|
and part.code_execution_result is not None
|
|
632
828
|
) and part.code_execution_result.output:
|
|
829
|
+
# outcome: 1 = OUTCOME_OK (success), else = error
|
|
830
|
+
outcome = part.code_execution_result.outcome
|
|
633
831
|
execution_result = {
|
|
634
832
|
"type": "code_execution_result",
|
|
635
833
|
"code_execution_result": part.code_execution_result.output,
|
|
636
|
-
"outcome":
|
|
834
|
+
"outcome": outcome,
|
|
835
|
+
"tool_call_id": "", # Linked via block translator
|
|
637
836
|
}
|
|
638
837
|
content = _append_to_content(content, execution_result)
|
|
639
838
|
|
|
640
|
-
if
|
|
839
|
+
if (
|
|
840
|
+
hasattr(part, "inline_data")
|
|
841
|
+
and part.inline_data
|
|
842
|
+
and part.inline_data.mime_type.startswith("audio/")
|
|
843
|
+
):
|
|
641
844
|
buffer = io.BytesIO()
|
|
642
845
|
|
|
643
846
|
with wave.open(buffer, "wb") as wf:
|
|
@@ -647,9 +850,17 @@ def _parse_response_candidate(
|
|
|
647
850
|
wf.setframerate(24000)
|
|
648
851
|
wf.writeframes(part.inline_data.data)
|
|
649
852
|
|
|
650
|
-
|
|
853
|
+
audio_data = buffer.getvalue()
|
|
854
|
+
additional_kwargs["audio"] = audio_data
|
|
855
|
+
|
|
856
|
+
# For backwards compatibility, audio stays in additional_kwargs by default
|
|
857
|
+
# and is accessible via .content_blocks property
|
|
651
858
|
|
|
652
|
-
if
|
|
859
|
+
if (
|
|
860
|
+
hasattr(part, "inline_data")
|
|
861
|
+
and part.inline_data
|
|
862
|
+
and part.inline_data.mime_type.startswith("image/")
|
|
863
|
+
):
|
|
653
864
|
image_format = part.inline_data.mime_type[6:]
|
|
654
865
|
image_message = {
|
|
655
866
|
"type": "image_url",
|
|
@@ -708,6 +919,23 @@ def _parse_response_candidate(
|
|
|
708
919
|
id=tool_call_dict.get("id", str(uuid.uuid4())),
|
|
709
920
|
)
|
|
710
921
|
)
|
|
922
|
+
|
|
923
|
+
# If this function_call Part has a signature, track it separately
|
|
924
|
+
# We'll add it to content only if there's other content present
|
|
925
|
+
if thought_sig:
|
|
926
|
+
sig_block = {
|
|
927
|
+
"type": "function_call_signature",
|
|
928
|
+
"signature": thought_sig,
|
|
929
|
+
}
|
|
930
|
+
function_call_signatures.append(sig_block)
|
|
931
|
+
|
|
932
|
+
# Add function call signatures to content only if there's already other content
|
|
933
|
+
# This preserves backward compatibility where content is "" for
|
|
934
|
+
# function-only responses
|
|
935
|
+
if function_call_signatures and content is not None:
|
|
936
|
+
for sig_block in function_call_signatures:
|
|
937
|
+
content = _append_to_content(content, sig_block)
|
|
938
|
+
|
|
711
939
|
if content is None:
|
|
712
940
|
content = ""
|
|
713
941
|
if isinstance(content, list) and any(
|
|
@@ -722,22 +950,100 @@ def _parse_response_candidate(
|
|
|
722
950
|
Validate before using in production.
|
|
723
951
|
"""
|
|
724
952
|
)
|
|
725
|
-
|
|
726
953
|
if streaming:
|
|
727
954
|
return AIMessageChunk(
|
|
728
955
|
content=content,
|
|
729
956
|
additional_kwargs=additional_kwargs,
|
|
957
|
+
response_metadata=response_metadata,
|
|
730
958
|
tool_call_chunks=tool_call_chunks,
|
|
731
959
|
)
|
|
732
960
|
|
|
733
961
|
return AIMessage(
|
|
734
962
|
content=content,
|
|
735
963
|
additional_kwargs=additional_kwargs,
|
|
964
|
+
response_metadata=response_metadata,
|
|
736
965
|
tool_calls=tool_calls,
|
|
737
966
|
invalid_tool_calls=invalid_tool_calls,
|
|
738
967
|
)
|
|
739
968
|
|
|
740
969
|
|
|
970
|
+
def _extract_grounding_metadata(candidate: Any) -> Dict[str, Any]:
|
|
971
|
+
"""Extract grounding metadata from candidate.
|
|
972
|
+
|
|
973
|
+
core's block translator converts this metadata into citation annotations.
|
|
974
|
+
|
|
975
|
+
Uses `MessageToDict` for complete unfiltered extraction.
|
|
976
|
+
|
|
977
|
+
Falls back to custom field extraction in cases of failure for robustness.
|
|
978
|
+
"""
|
|
979
|
+
if not hasattr(candidate, "grounding_metadata") or not candidate.grounding_metadata:
|
|
980
|
+
return {}
|
|
981
|
+
|
|
982
|
+
grounding_metadata = candidate.grounding_metadata
|
|
983
|
+
|
|
984
|
+
try:
|
|
985
|
+
# proto-plus wraps protobuf messages - access ._pb to get the raw protobuf
|
|
986
|
+
# message that MessageToDict expects
|
|
987
|
+
pb_message = (
|
|
988
|
+
grounding_metadata._pb
|
|
989
|
+
if hasattr(grounding_metadata, "_pb")
|
|
990
|
+
else grounding_metadata
|
|
991
|
+
)
|
|
992
|
+
|
|
993
|
+
return MessageToDict( # type: ignore[call-arg]
|
|
994
|
+
pb_message,
|
|
995
|
+
preserving_proto_field_name=True,
|
|
996
|
+
always_print_fields_with_no_presence=True,
|
|
997
|
+
# type stub issue - ensures that protobuf fields with default values
|
|
998
|
+
# (like start_index=0) are included in the output
|
|
999
|
+
)
|
|
1000
|
+
except (AttributeError, TypeError, ImportError):
|
|
1001
|
+
# Attempt manual extraction of known fields
|
|
1002
|
+
result: Dict[str, Any] = {}
|
|
1003
|
+
|
|
1004
|
+
# Grounding chunks
|
|
1005
|
+
if hasattr(grounding_metadata, "grounding_chunks"):
|
|
1006
|
+
grounding_chunks = []
|
|
1007
|
+
for chunk in grounding_metadata.grounding_chunks:
|
|
1008
|
+
chunk_data: Dict[str, Any] = {}
|
|
1009
|
+
if hasattr(chunk, "web") and chunk.web:
|
|
1010
|
+
chunk_data["web"] = {
|
|
1011
|
+
"uri": chunk.web.uri if hasattr(chunk.web, "uri") else "",
|
|
1012
|
+
"title": chunk.web.title if hasattr(chunk.web, "title") else "",
|
|
1013
|
+
}
|
|
1014
|
+
grounding_chunks.append(chunk_data)
|
|
1015
|
+
result["grounding_chunks"] = grounding_chunks
|
|
1016
|
+
|
|
1017
|
+
# Grounding supports
|
|
1018
|
+
if hasattr(grounding_metadata, "grounding_supports"):
|
|
1019
|
+
grounding_supports = []
|
|
1020
|
+
for support in grounding_metadata.grounding_supports:
|
|
1021
|
+
support_data: Dict[str, Any] = {}
|
|
1022
|
+
if hasattr(support, "segment") and support.segment:
|
|
1023
|
+
support_data["segment"] = {
|
|
1024
|
+
"start_index": getattr(support.segment, "start_index", 0),
|
|
1025
|
+
"end_index": getattr(support.segment, "end_index", 0),
|
|
1026
|
+
"text": getattr(support.segment, "text", ""),
|
|
1027
|
+
"part_index": getattr(support.segment, "part_index", 0),
|
|
1028
|
+
}
|
|
1029
|
+
if hasattr(support, "grounding_chunk_indices"):
|
|
1030
|
+
support_data["grounding_chunk_indices"] = list(
|
|
1031
|
+
support.grounding_chunk_indices
|
|
1032
|
+
)
|
|
1033
|
+
if hasattr(support, "confidence_scores"):
|
|
1034
|
+
support_data["confidence_scores"] = [
|
|
1035
|
+
round(score, 6) for score in support.confidence_scores
|
|
1036
|
+
]
|
|
1037
|
+
grounding_supports.append(support_data)
|
|
1038
|
+
result["grounding_supports"] = grounding_supports
|
|
1039
|
+
|
|
1040
|
+
# Web search queries
|
|
1041
|
+
if hasattr(grounding_metadata, "web_search_queries"):
|
|
1042
|
+
result["web_search_queries"] = list(grounding_metadata.web_search_queries)
|
|
1043
|
+
|
|
1044
|
+
return result
|
|
1045
|
+
|
|
1046
|
+
|
|
741
1047
|
def _response_to_result(
|
|
742
1048
|
response: GenerateContentResponse,
|
|
743
1049
|
stream: bool = False,
|
|
@@ -800,15 +1106,16 @@ def _response_to_result(
|
|
|
800
1106
|
proto.Message.to_dict(safety_rating, use_integers_for_enums=False)
|
|
801
1107
|
for safety_rating in candidate.safety_ratings
|
|
802
1108
|
]
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
generation_info["grounding_metadata"] = proto.Message.to_dict(
|
|
806
|
-
candidate.grounding_metadata
|
|
807
|
-
)
|
|
808
|
-
except AttributeError:
|
|
809
|
-
pass
|
|
1109
|
+
grounding_metadata = _extract_grounding_metadata(candidate)
|
|
1110
|
+
generation_info["grounding_metadata"] = grounding_metadata
|
|
810
1111
|
message = _parse_response_candidate(candidate, streaming=stream)
|
|
1112
|
+
|
|
811
1113
|
message.usage_metadata = lc_usage
|
|
1114
|
+
|
|
1115
|
+
if not hasattr(message, "response_metadata"):
|
|
1116
|
+
message.response_metadata = {}
|
|
1117
|
+
message.response_metadata["grounding_metadata"] = grounding_metadata
|
|
1118
|
+
|
|
812
1119
|
if stream:
|
|
813
1120
|
generations.append(
|
|
814
1121
|
ChatGenerationChunk(
|
|
@@ -1054,7 +1361,7 @@ class ChatGoogleGenerativeAI(_BaseGoogleGenerativeAI, BaseChatModel):
|
|
|
1054
1361
|
file = client.files.get(name=file.name)
|
|
1055
1362
|
|
|
1056
1363
|
# Create cache
|
|
1057
|
-
model = "models/gemini-
|
|
1364
|
+
model = "models/gemini-2.5-flash"
|
|
1058
1365
|
cache = client.caches.create(
|
|
1059
1366
|
model=model,
|
|
1060
1367
|
config=types.CreateCachedContentConfig(
|
|
@@ -1110,7 +1417,7 @@ class ChatGoogleGenerativeAI(_BaseGoogleGenerativeAI, BaseChatModel):
|
|
|
1110
1417
|
],
|
|
1111
1418
|
)
|
|
1112
1419
|
]
|
|
1113
|
-
model = "gemini-
|
|
1420
|
+
model = "gemini-2.5-flash"
|
|
1114
1421
|
cache = client.caches.create(
|
|
1115
1422
|
model=model,
|
|
1116
1423
|
config=CreateCachedContentConfig(
|
|
@@ -1216,8 +1523,12 @@ class ChatGoogleGenerativeAI(_BaseGoogleGenerativeAI, BaseChatModel):
|
|
|
1216
1523
|
)
|
|
1217
1524
|
|
|
1218
1525
|
|
|
1526
|
+
# Default method uses function calling
|
|
1219
1527
|
structured_llm = llm.with_structured_output(Joke)
|
|
1220
|
-
|
|
1528
|
+
|
|
1529
|
+
# For more reliable output, use json_schema with native responseSchema
|
|
1530
|
+
structured_llm_json = llm.with_structured_output(Joke, method="json_schema")
|
|
1531
|
+
structured_llm_json.invoke("Tell me a joke about cats")
|
|
1221
1532
|
|
|
1222
1533
|
.. code-block:: python
|
|
1223
1534
|
|
|
@@ -1227,6 +1538,18 @@ class ChatGoogleGenerativeAI(_BaseGoogleGenerativeAI, BaseChatModel):
|
|
|
1227
1538
|
rating=None,
|
|
1228
1539
|
)
|
|
1229
1540
|
|
|
1541
|
+
Two methods are supported for structured output:
|
|
1542
|
+
|
|
1543
|
+
* ``method="function_calling"`` (default): Uses tool calling to extract
|
|
1544
|
+
structured data. Compatible with all models.
|
|
1545
|
+
* ``method="json_schema"``: Uses Gemini's native structured output with
|
|
1546
|
+
responseSchema. More reliable but requires Gemini 1.5+ models.
|
|
1547
|
+
``method="json_mode"`` also works for backwards compatibility but is a misnomer.
|
|
1548
|
+
|
|
1549
|
+
The ``json_schema`` method is recommended for better reliability as it
|
|
1550
|
+
constrains the model's generation process directly rather than relying on
|
|
1551
|
+
post-processing tool calls.
|
|
1552
|
+
|
|
1230
1553
|
Image input:
|
|
1231
1554
|
.. code-block:: python
|
|
1232
1555
|
|
|
@@ -1649,12 +1972,12 @@ class ChatGoogleGenerativeAI(_BaseGoogleGenerativeAI, BaseChatModel):
|
|
|
1649
1972
|
code_execution: Optional[bool] = None,
|
|
1650
1973
|
stop: Optional[list[str]] = None,
|
|
1651
1974
|
**kwargs: Any,
|
|
1652
|
-
) ->
|
|
1653
|
-
"""
|
|
1654
|
-
|
|
1655
|
-
|
|
1975
|
+
) -> AIMessage:
|
|
1976
|
+
"""Override invoke to add code_execution parameter.
|
|
1977
|
+
|
|
1978
|
+
Supported on: gemini-1.5-pro, gemini-1.5-flash, gemini-2.0-flash, and
|
|
1979
|
+
gemini-2.0-pro. When enabled, the model can execute code to solve problems.
|
|
1656
1980
|
"""
|
|
1657
|
-
"""Override invoke to add code_execution parameter."""
|
|
1658
1981
|
|
|
1659
1982
|
if code_execution is not None:
|
|
1660
1983
|
if not self._supports_code_execution:
|
|
@@ -2053,7 +2376,7 @@ class ChatGoogleGenerativeAI(_BaseGoogleGenerativeAI, BaseChatModel):
|
|
|
2053
2376
|
]
|
|
2054
2377
|
request = GenerateContentRequest(
|
|
2055
2378
|
model=self.model,
|
|
2056
|
-
contents=history,
|
|
2379
|
+
contents=history, # google.ai.generativelanguage_v1beta.types.Content
|
|
2057
2380
|
tools=formatted_tools,
|
|
2058
2381
|
tool_config=formatted_tool_config,
|
|
2059
2382
|
safety_settings=formatted_safety_settings,
|
|
@@ -2070,7 +2393,7 @@ class ChatGoogleGenerativeAI(_BaseGoogleGenerativeAI, BaseChatModel):
|
|
|
2070
2393
|
return request
|
|
2071
2394
|
|
|
2072
2395
|
def get_num_tokens(self, text: str) -> int:
|
|
2073
|
-
"""Get the number of tokens present in the text.
|
|
2396
|
+
"""Get the number of tokens present in the text. Uses the model's tokenizer.
|
|
2074
2397
|
|
|
2075
2398
|
Useful for checking if an input will fit in a model's context window.
|
|
2076
2399
|
|
|
@@ -2088,7 +2411,9 @@ class ChatGoogleGenerativeAI(_BaseGoogleGenerativeAI, BaseChatModel):
|
|
|
2088
2411
|
def with_structured_output(
|
|
2089
2412
|
self,
|
|
2090
2413
|
schema: Union[Dict, Type[BaseModel]],
|
|
2091
|
-
method: Optional[
|
|
2414
|
+
method: Optional[
|
|
2415
|
+
Literal["function_calling", "json_mode", "json_schema"]
|
|
2416
|
+
] = "function_calling",
|
|
2092
2417
|
*,
|
|
2093
2418
|
include_raw: bool = False,
|
|
2094
2419
|
**kwargs: Any,
|
|
@@ -2100,7 +2425,8 @@ class ChatGoogleGenerativeAI(_BaseGoogleGenerativeAI, BaseChatModel):
|
|
|
2100
2425
|
|
|
2101
2426
|
parser: OutputParserLike
|
|
2102
2427
|
|
|
2103
|
-
|
|
2428
|
+
# `json_schema` preferred, but `json_mode` kept for backwards compatibility
|
|
2429
|
+
if method in ("json_mode", "json_schema"):
|
|
2104
2430
|
if isinstance(schema, type) and is_basemodel_subclass(schema):
|
|
2105
2431
|
if issubclass(schema, BaseModelV1):
|
|
2106
2432
|
schema_json = schema.schema()
|
|
@@ -2168,7 +2494,7 @@ class ChatGoogleGenerativeAI(_BaseGoogleGenerativeAI, BaseChatModel):
|
|
|
2168
2494
|
*,
|
|
2169
2495
|
tool_choice: Optional[Union[_ToolChoiceType, bool]] = None,
|
|
2170
2496
|
**kwargs: Any,
|
|
2171
|
-
) -> Runnable[LanguageModelInput,
|
|
2497
|
+
) -> Runnable[LanguageModelInput, AIMessage]:
|
|
2172
2498
|
"""Bind tool-like objects to this chat model.
|
|
2173
2499
|
|
|
2174
2500
|
Assumes model is compatible with google-generativeAI tool-calling API.
|
|
@@ -116,22 +116,10 @@ class GoogleGenerativeAIEmbeddings(BaseModel, Embeddings):
|
|
|
116
116
|
client_options=self.client_options,
|
|
117
117
|
transport=self.transport,
|
|
118
118
|
)
|
|
119
|
-
#
|
|
120
|
-
#
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
transport = self.transport
|
|
124
|
-
if transport == "rest":
|
|
125
|
-
transport = "grpc_asyncio"
|
|
126
|
-
self.async_client = build_generative_async_service(
|
|
127
|
-
credentials=self.credentials,
|
|
128
|
-
api_key=google_api_key,
|
|
129
|
-
client_info=client_info,
|
|
130
|
-
client_options=self.client_options,
|
|
131
|
-
transport=transport,
|
|
132
|
-
)
|
|
133
|
-
else:
|
|
134
|
-
self.async_client = None
|
|
119
|
+
# Always defer async client initialization to first async call.
|
|
120
|
+
# Avoids implicit event loop creation and aligns with lazy init
|
|
121
|
+
# in chat models.
|
|
122
|
+
self.async_client = None
|
|
135
123
|
return self
|
|
136
124
|
|
|
137
125
|
@property
|
{langchain_google_genai-2.1.12.dist-info → langchain_google_genai-3.0.0a1.dist-info}/METADATA
RENAMED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: langchain-google-genai
|
|
3
|
-
Version:
|
|
3
|
+
Version: 3.0.0a1
|
|
4
4
|
Summary: An integration package connecting Google's genai package and LangChain
|
|
5
5
|
License: MIT
|
|
6
6
|
Project-URL: Source Code, https://github.com/langchain-ai/langchain-google/tree/main/libs/genai
|
|
7
7
|
Project-URL: Release Notes, https://github.com/langchain-ai/langchain-google/releases
|
|
8
8
|
Project-URL: repository, https://github.com/langchain-ai/langchain-google
|
|
9
|
-
Requires-Python:
|
|
10
|
-
Requires-Dist: langchain-core
|
|
11
|
-
Requires-Dist: google-ai-generativelanguage<1,>=0.7
|
|
12
|
-
Requires-Dist: pydantic<3,>=2
|
|
13
|
-
Requires-Dist: filetype<2,>=1.2
|
|
9
|
+
Requires-Python: <4.0.0,>=3.10.0
|
|
10
|
+
Requires-Dist: langchain-core<2.0.0,>=1.0.0a4
|
|
11
|
+
Requires-Dist: google-ai-generativelanguage<1.0.0,>=0.7.0
|
|
12
|
+
Requires-Dist: pydantic<3.0.0,>=2.0.0
|
|
13
|
+
Requires-Dist: filetype<2.0.0,>=1.2.0
|
|
14
14
|
Description-Content-Type: text/markdown
|
|
15
15
|
|
|
16
16
|
# langchain-google-genai
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
langchain_google_genai-3.0.0a1.dist-info/METADATA,sha256=XsuNCcaJIFJEKlPM9IxFK5bh7rT2ZJ2tUEjCtcoNl-c,7090
|
|
2
|
+
langchain_google_genai-3.0.0a1.dist-info/WHEEL,sha256=9P2ygRxDrTJz3gsagc0Z96ukrxjr-LFBGOgv3AuKlCA,90
|
|
3
|
+
langchain_google_genai-3.0.0a1.dist-info/entry_points.txt,sha256=6OYgBcLyFCUgeqLgnvMyOJxPCWzgy7se4rLPKtNonMs,34
|
|
4
|
+
langchain_google_genai-3.0.0a1.dist-info/licenses/LICENSE,sha256=DppmdYJVSc1jd0aio6ptnMUn5tIHrdAhQ12SclEBfBg,1072
|
|
5
|
+
langchain_google_genai/__init__.py,sha256=yVc4wCHzajs8mvDVQLGmkxxivzApvdu7jTytrLr_i7g,2891
|
|
6
|
+
langchain_google_genai/_common.py,sha256=qV7EqGEsi1_BCnUDJf1ZDgOhHOIGKbgw8-D1g7Afo3g,6307
|
|
7
|
+
langchain_google_genai/_compat.py,sha256=HKGqd4k8hs9NS6OqU0nmLgQhZRxx7S2h_9R2-1kULDs,9304
|
|
8
|
+
langchain_google_genai/_enums.py,sha256=Zj3BXXLlkm_UybegCi6fLsfFhriJCt_LAJvgatgPWQ0,252
|
|
9
|
+
langchain_google_genai/_function_utils.py,sha256=zlH0YhFZolP6mhppBPr-7d1t6psvZcpSmkuS1oER0S0,23647
|
|
10
|
+
langchain_google_genai/_genai_extension.py,sha256=3W8N_vMxrwtzZvmPgmyIr6T8q6UkyS3y-IYMcr_67Ac,22597
|
|
11
|
+
langchain_google_genai/_image_utils.py,sha256=iJ3KrFWQhA0UwsT8ryAFAmNM7-_2ahHAppT8t9WFZGQ,5304
|
|
12
|
+
langchain_google_genai/chat_models.py,sha256=jVeoq3r1ekE2KXQmbEalLaMgAFxXATB3i1HDz4S1zDI,101386
|
|
13
|
+
langchain_google_genai/embeddings.py,sha256=MF_AoQNVhWJ6ekH6NPfZ_UGk_ixL-BX05gvZtcjYpEo,16193
|
|
14
|
+
langchain_google_genai/genai_aqa.py,sha256=NVW8wOWxU7T6VVshFrFlFHa5HzEPAedrgh1fOwFH7Og,4380
|
|
15
|
+
langchain_google_genai/google_vector_store.py,sha256=x4OcXkcYWTubu-AESNzNDJG_dbge16GeNCAOCsBoc4g,16537
|
|
16
|
+
langchain_google_genai/llms.py,sha256=P_e_ImBWexhKqc7LZPo6tQbwLH2-ljr6oqpE5M27TRc,5755
|
|
17
|
+
langchain_google_genai/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
18
|
+
langchain_google_genai-3.0.0a1.dist-info/RECORD,,
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
langchain_google_genai-2.1.12.dist-info/METADATA,sha256=cIpzlJRZXFdHG-aZmSE6MJUmSr-JAZzH7pDvU30nzOs,7051
|
|
2
|
-
langchain_google_genai-2.1.12.dist-info/WHEEL,sha256=9P2ygRxDrTJz3gsagc0Z96ukrxjr-LFBGOgv3AuKlCA,90
|
|
3
|
-
langchain_google_genai-2.1.12.dist-info/entry_points.txt,sha256=6OYgBcLyFCUgeqLgnvMyOJxPCWzgy7se4rLPKtNonMs,34
|
|
4
|
-
langchain_google_genai-2.1.12.dist-info/licenses/LICENSE,sha256=DppmdYJVSc1jd0aio6ptnMUn5tIHrdAhQ12SclEBfBg,1072
|
|
5
|
-
langchain_google_genai/__init__.py,sha256=yVc4wCHzajs8mvDVQLGmkxxivzApvdu7jTytrLr_i7g,2891
|
|
6
|
-
langchain_google_genai/_common.py,sha256=dzzcJeDVnUUI_8Y20F40SZ5NjW6RHD4xJu74naYK58k,6192
|
|
7
|
-
langchain_google_genai/_enums.py,sha256=Zj3BXXLlkm_UybegCi6fLsfFhriJCt_LAJvgatgPWQ0,252
|
|
8
|
-
langchain_google_genai/_function_utils.py,sha256=Pur365t4M8O5dkrnFkDc_jHiPfuI_C7ucSxvxA3tioM,22254
|
|
9
|
-
langchain_google_genai/_genai_extension.py,sha256=LiNa7b6BZp41meJMtgaSEbaGJa59BY3UMCRMHO_CCtQ,21467
|
|
10
|
-
langchain_google_genai/_image_utils.py,sha256=iJ3KrFWQhA0UwsT8ryAFAmNM7-_2ahHAppT8t9WFZGQ,5304
|
|
11
|
-
langchain_google_genai/chat_models.py,sha256=G619buVU9Sef33Ubk9JL8gYLd5y_jsGMgFXeuPq7wJ0,85067
|
|
12
|
-
langchain_google_genai/embeddings.py,sha256=Kar6nHpAJ0rWQZ0nc7_anWETlwCIovd4DhieIviGNQ8,16687
|
|
13
|
-
langchain_google_genai/genai_aqa.py,sha256=NVW8wOWxU7T6VVshFrFlFHa5HzEPAedrgh1fOwFH7Og,4380
|
|
14
|
-
langchain_google_genai/google_vector_store.py,sha256=x4OcXkcYWTubu-AESNzNDJG_dbge16GeNCAOCsBoc4g,16537
|
|
15
|
-
langchain_google_genai/llms.py,sha256=P_e_ImBWexhKqc7LZPo6tQbwLH2-ljr6oqpE5M27TRc,5755
|
|
16
|
-
langchain_google_genai/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
17
|
-
langchain_google_genai-2.1.12.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|