langchain-google-genai 2.1.5__py3-none-any.whl → 2.1.7__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.
- langchain_google_genai/_function_utils.py +70 -0
- langchain_google_genai/chat_models.py +230 -115
- langchain_google_genai/embeddings.py +4 -1
- langchain_google_genai-2.1.7.dist-info/METADATA +260 -0
- {langchain_google_genai-2.1.5.dist-info → langchain_google_genai-2.1.7.dist-info}/RECORD +7 -7
- langchain_google_genai-2.1.5.dist-info/METADATA +0 -174
- {langchain_google_genai-2.1.5.dist-info → langchain_google_genai-2.1.7.dist-info}/LICENSE +0 -0
- {langchain_google_genai-2.1.5.dist-info → langchain_google_genai-2.1.7.dist-info}/WHEEL +0 -0
|
@@ -30,6 +30,7 @@ from langchain_core.utils.function_calling import (
|
|
|
30
30
|
from langchain_core.utils.json_schema import dereference_refs
|
|
31
31
|
from pydantic import BaseModel
|
|
32
32
|
from pydantic.v1 import BaseModel as BaseModelV1
|
|
33
|
+
from typing_extensions import NotRequired
|
|
33
34
|
|
|
34
35
|
logger = logging.getLogger(__name__)
|
|
35
36
|
|
|
@@ -65,11 +66,15 @@ _GoogleSearchRetrievalLike = Union[
|
|
|
65
66
|
gapic.GoogleSearchRetrieval,
|
|
66
67
|
Dict[str, Any],
|
|
67
68
|
]
|
|
69
|
+
_GoogleSearchLike = Union[gapic.Tool.GoogleSearch, Dict[str, Any]]
|
|
70
|
+
_CodeExecutionLike = Union[gapic.CodeExecution, Dict[str, Any]]
|
|
68
71
|
|
|
69
72
|
|
|
70
73
|
class _ToolDict(TypedDict):
|
|
71
74
|
function_declarations: Sequence[_FunctionDeclarationLike]
|
|
72
75
|
google_search_retrieval: Optional[_GoogleSearchRetrievalLike]
|
|
76
|
+
google_search: NotRequired[_GoogleSearchLike]
|
|
77
|
+
code_execution: NotRequired[_CodeExecutionLike]
|
|
73
78
|
|
|
74
79
|
|
|
75
80
|
# Info: This means one tool=Sequence of FunctionDeclaration
|
|
@@ -158,6 +163,8 @@ def convert_to_genai_function_declarations(
|
|
|
158
163
|
for f in [
|
|
159
164
|
"function_declarations",
|
|
160
165
|
"google_search_retrieval",
|
|
166
|
+
"google_search",
|
|
167
|
+
"code_execution",
|
|
161
168
|
]
|
|
162
169
|
):
|
|
163
170
|
fd = _format_to_gapic_function_declaration(tool) # type: ignore[arg-type]
|
|
@@ -184,6 +191,12 @@ def convert_to_genai_function_declarations(
|
|
|
184
191
|
gapic_tool.google_search_retrieval = gapic.GoogleSearchRetrieval(
|
|
185
192
|
tool["google_search_retrieval"]
|
|
186
193
|
)
|
|
194
|
+
if "google_search" in tool:
|
|
195
|
+
gapic_tool.google_search = gapic.Tool.GoogleSearch(
|
|
196
|
+
tool["google_search"]
|
|
197
|
+
)
|
|
198
|
+
if "code_execution" in tool:
|
|
199
|
+
gapic_tool.code_execution = gapic.CodeExecution(tool["code_execution"])
|
|
187
200
|
else:
|
|
188
201
|
fd = _format_to_gapic_function_declaration(tool) # type: ignore[arg-type]
|
|
189
202
|
gapic_tool.function_declarations.append(fd)
|
|
@@ -520,3 +533,60 @@ def safe_import(module_name: str, attribute_name: str = "") -> bool:
|
|
|
520
533
|
return True
|
|
521
534
|
except ImportError:
|
|
522
535
|
return False
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
def replace_defs_in_schema(original_schema: dict, defs: Optional[dict] = None) -> dict:
|
|
539
|
+
"""Given an OpenAPI schema with a property '$defs' replaces all occurrences of
|
|
540
|
+
referenced items in the dictionary.
|
|
541
|
+
|
|
542
|
+
Args:
|
|
543
|
+
original_schema: Schema generated by `BaseModel.model_schema_json`
|
|
544
|
+
defs: Definitions for recursive calls.
|
|
545
|
+
|
|
546
|
+
Returns:
|
|
547
|
+
Schema with refs replaced.
|
|
548
|
+
"""
|
|
549
|
+
|
|
550
|
+
new_defs = defs or original_schema.get("$defs")
|
|
551
|
+
|
|
552
|
+
if new_defs is None or not isinstance(new_defs, dict):
|
|
553
|
+
return original_schema.copy()
|
|
554
|
+
|
|
555
|
+
resulting_schema = {}
|
|
556
|
+
|
|
557
|
+
for key, value in original_schema.items():
|
|
558
|
+
if key == "$defs":
|
|
559
|
+
continue
|
|
560
|
+
|
|
561
|
+
if not isinstance(value, dict):
|
|
562
|
+
resulting_schema[key] = value
|
|
563
|
+
else:
|
|
564
|
+
if "$ref" in value:
|
|
565
|
+
new_value = value.copy()
|
|
566
|
+
|
|
567
|
+
path = new_value.pop("$ref")
|
|
568
|
+
def_key = _get_def_key_from_schema_path(path)
|
|
569
|
+
new_item = new_defs.get(def_key)
|
|
570
|
+
|
|
571
|
+
assert isinstance(new_item, dict)
|
|
572
|
+
new_value.update(new_item)
|
|
573
|
+
|
|
574
|
+
resulting_schema[key] = replace_defs_in_schema(new_value, defs=new_defs)
|
|
575
|
+
else:
|
|
576
|
+
resulting_schema[key] = replace_defs_in_schema(value, defs=new_defs)
|
|
577
|
+
|
|
578
|
+
return resulting_schema
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
def _get_def_key_from_schema_path(schema_path: str) -> str:
|
|
582
|
+
error_message = f"Malformed schema reference path {schema_path}"
|
|
583
|
+
|
|
584
|
+
if not isinstance(schema_path, str) or not schema_path.startswith("#/$defs/"):
|
|
585
|
+
raise ValueError(error_message)
|
|
586
|
+
|
|
587
|
+
# Schema has to have only one extra level.
|
|
588
|
+
parts = schema_path.split("/")
|
|
589
|
+
if len(parts) != 3:
|
|
590
|
+
raise ValueError(error_message)
|
|
591
|
+
|
|
592
|
+
return parts[-1]
|
|
@@ -2,11 +2,13 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import base64
|
|
5
|
+
import io
|
|
5
6
|
import json
|
|
6
7
|
import logging
|
|
7
8
|
import mimetypes
|
|
8
9
|
import uuid
|
|
9
10
|
import warnings
|
|
11
|
+
import wave
|
|
10
12
|
from difflib import get_close_matches
|
|
11
13
|
from operator import itemgetter
|
|
12
14
|
from typing import (
|
|
@@ -16,6 +18,7 @@ from typing import (
|
|
|
16
18
|
Dict,
|
|
17
19
|
Iterator,
|
|
18
20
|
List,
|
|
21
|
+
Literal,
|
|
19
22
|
Mapping,
|
|
20
23
|
Optional,
|
|
21
24
|
Sequence,
|
|
@@ -37,7 +40,9 @@ from google.ai.generativelanguage_v1beta.types import (
|
|
|
37
40
|
Blob,
|
|
38
41
|
Candidate,
|
|
39
42
|
CodeExecution,
|
|
43
|
+
CodeExecutionResult,
|
|
40
44
|
Content,
|
|
45
|
+
ExecutableCode,
|
|
41
46
|
FileData,
|
|
42
47
|
FunctionCall,
|
|
43
48
|
FunctionDeclaration,
|
|
@@ -67,8 +72,9 @@ from langchain_core.messages import (
|
|
|
67
72
|
ToolMessage,
|
|
68
73
|
is_data_content_block,
|
|
69
74
|
)
|
|
70
|
-
from langchain_core.messages.ai import UsageMetadata
|
|
75
|
+
from langchain_core.messages.ai import UsageMetadata, add_usage, subtract_usage
|
|
71
76
|
from langchain_core.messages.tool import invalid_tool_call, tool_call, tool_call_chunk
|
|
77
|
+
from langchain_core.output_parsers import JsonOutputParser, PydanticOutputParser
|
|
72
78
|
from langchain_core.output_parsers.base import OutputParserLike
|
|
73
79
|
from langchain_core.output_parsers.openai_tools import (
|
|
74
80
|
JsonOutputKeyToolsParser,
|
|
@@ -79,7 +85,11 @@ from langchain_core.outputs import ChatGeneration, ChatGenerationChunk, ChatResu
|
|
|
79
85
|
from langchain_core.runnables import Runnable, RunnableConfig, RunnablePassthrough
|
|
80
86
|
from langchain_core.tools import BaseTool
|
|
81
87
|
from langchain_core.utils import get_pydantic_field_names
|
|
82
|
-
from langchain_core.utils.function_calling import
|
|
88
|
+
from langchain_core.utils.function_calling import (
|
|
89
|
+
convert_to_json_schema,
|
|
90
|
+
convert_to_openai_tool,
|
|
91
|
+
)
|
|
92
|
+
from langchain_core.utils.pydantic import is_basemodel_subclass
|
|
83
93
|
from langchain_core.utils.utils import _build_model_kwargs
|
|
84
94
|
from pydantic import (
|
|
85
95
|
BaseModel,
|
|
@@ -88,6 +98,7 @@ from pydantic import (
|
|
|
88
98
|
SecretStr,
|
|
89
99
|
model_validator,
|
|
90
100
|
)
|
|
101
|
+
from pydantic.v1 import BaseModel as BaseModelV1
|
|
91
102
|
from tenacity import (
|
|
92
103
|
before_sleep_log,
|
|
93
104
|
retry,
|
|
@@ -104,12 +115,14 @@ from langchain_google_genai._common import (
|
|
|
104
115
|
get_client_info,
|
|
105
116
|
)
|
|
106
117
|
from langchain_google_genai._function_utils import (
|
|
118
|
+
_dict_to_gapic_schema,
|
|
107
119
|
_tool_choice_to_tool_config,
|
|
108
120
|
_ToolChoiceType,
|
|
109
121
|
_ToolConfigDict,
|
|
110
122
|
_ToolDict,
|
|
111
123
|
convert_to_genai_function_declarations,
|
|
112
124
|
is_basemodel_subclass_safe,
|
|
125
|
+
replace_defs_in_schema,
|
|
113
126
|
tool_to_dict,
|
|
114
127
|
)
|
|
115
128
|
from langchain_google_genai._image_utils import (
|
|
@@ -121,6 +134,7 @@ from . import _genai_extension as genaix
|
|
|
121
134
|
|
|
122
135
|
logger = logging.getLogger(__name__)
|
|
123
136
|
|
|
137
|
+
_allowed_params_prediction_service = ["request", "timeout", "metadata", "labels"]
|
|
124
138
|
|
|
125
139
|
_FunctionDeclarationType = Union[
|
|
126
140
|
FunctionDeclaration,
|
|
@@ -207,7 +221,14 @@ def _chat_with_retry(generation_method: Callable, **kwargs: Any) -> Any:
|
|
|
207
221
|
except Exception as e:
|
|
208
222
|
raise e
|
|
209
223
|
|
|
210
|
-
|
|
224
|
+
params = (
|
|
225
|
+
{k: v for k, v in kwargs.items() if k in _allowed_params_prediction_service}
|
|
226
|
+
if (request := kwargs.get("request"))
|
|
227
|
+
and hasattr(request, "model")
|
|
228
|
+
and "gemini" in request.model
|
|
229
|
+
else kwargs
|
|
230
|
+
)
|
|
231
|
+
return _chat_with_retry(**params)
|
|
211
232
|
|
|
212
233
|
|
|
213
234
|
async def _achat_with_retry(generation_method: Callable, **kwargs: Any) -> Any:
|
|
@@ -240,7 +261,14 @@ async def _achat_with_retry(generation_method: Callable, **kwargs: Any) -> Any:
|
|
|
240
261
|
except Exception as e:
|
|
241
262
|
raise e
|
|
242
263
|
|
|
243
|
-
|
|
264
|
+
params = (
|
|
265
|
+
{k: v for k, v in kwargs.items() if k in _allowed_params_prediction_service}
|
|
266
|
+
if (request := kwargs.get("request"))
|
|
267
|
+
and hasattr(request, "model")
|
|
268
|
+
and "gemini" in request.model
|
|
269
|
+
else kwargs
|
|
270
|
+
)
|
|
271
|
+
return await _achat_with_retry(**params)
|
|
244
272
|
|
|
245
273
|
|
|
246
274
|
def _is_lc_content_block(part: dict) -> bool:
|
|
@@ -331,6 +359,37 @@ def _convert_to_parts(
|
|
|
331
359
|
metadata = VideoMetadata(part["video_metadata"])
|
|
332
360
|
media_part.video_metadata = metadata
|
|
333
361
|
parts.append(media_part)
|
|
362
|
+
elif part["type"] == "executable_code":
|
|
363
|
+
if "executable_code" not in part or "language" not in part:
|
|
364
|
+
raise ValueError(
|
|
365
|
+
"Executable code part must have 'code' and 'language' "
|
|
366
|
+
f"keys, got {part}"
|
|
367
|
+
)
|
|
368
|
+
executable_code_part = Part(
|
|
369
|
+
executable_code=ExecutableCode(
|
|
370
|
+
language=part["language"], code=part["executable_code"]
|
|
371
|
+
)
|
|
372
|
+
)
|
|
373
|
+
parts.append(executable_code_part)
|
|
374
|
+
elif part["type"] == "code_execution_result":
|
|
375
|
+
if "code_execution_result" not in part:
|
|
376
|
+
raise ValueError(
|
|
377
|
+
"Code execution result part must have "
|
|
378
|
+
f"'code_execution_result', got {part}"
|
|
379
|
+
)
|
|
380
|
+
if "outcome" in part:
|
|
381
|
+
outcome = part["outcome"]
|
|
382
|
+
else:
|
|
383
|
+
# Backward compatibility
|
|
384
|
+
outcome = 1 # Default to success if not specified
|
|
385
|
+
code_execution_result_part = Part(
|
|
386
|
+
code_execution_result=CodeExecutionResult(
|
|
387
|
+
output=part["code_execution_result"], outcome=outcome
|
|
388
|
+
)
|
|
389
|
+
)
|
|
390
|
+
parts.append(code_execution_result_part)
|
|
391
|
+
elif part["type"] == "thinking":
|
|
392
|
+
parts.append(Part(text=part["thinking"], thought=True))
|
|
334
393
|
else:
|
|
335
394
|
raise ValueError(
|
|
336
395
|
f"Unrecognized message part type: {part['type']}. Only text, "
|
|
@@ -486,47 +545,54 @@ def _parse_chat_history(
|
|
|
486
545
|
return system_instruction, messages
|
|
487
546
|
|
|
488
547
|
|
|
548
|
+
# Helper function to append content consistently
|
|
549
|
+
def _append_to_content(
|
|
550
|
+
current_content: Union[str, List[Any], None], new_item: Any
|
|
551
|
+
) -> Union[str, List[Any]]:
|
|
552
|
+
"""Appends a new item to the content, handling different initial content types."""
|
|
553
|
+
if current_content is None and isinstance(new_item, str):
|
|
554
|
+
return new_item
|
|
555
|
+
elif current_content is None:
|
|
556
|
+
return [new_item]
|
|
557
|
+
elif isinstance(current_content, str):
|
|
558
|
+
return [current_content, new_item]
|
|
559
|
+
elif isinstance(current_content, list):
|
|
560
|
+
current_content.append(new_item)
|
|
561
|
+
return current_content
|
|
562
|
+
else:
|
|
563
|
+
# This case should ideally not be reached with proper type checking,
|
|
564
|
+
# but it catches any unexpected types that might slip through.
|
|
565
|
+
raise TypeError(f"Unexpected content type: {type(current_content)}")
|
|
566
|
+
|
|
567
|
+
|
|
489
568
|
def _parse_response_candidate(
|
|
490
569
|
response_candidate: Candidate, streaming: bool = False
|
|
491
570
|
) -> AIMessage:
|
|
492
571
|
content: Union[None, str, List[Union[str, dict]]] = None
|
|
493
|
-
additional_kwargs = {}
|
|
572
|
+
additional_kwargs: Dict[str, Any] = {}
|
|
494
573
|
tool_calls = []
|
|
495
574
|
invalid_tool_calls = []
|
|
496
575
|
tool_call_chunks = []
|
|
497
576
|
|
|
498
577
|
for part in response_candidate.content.parts:
|
|
578
|
+
text: Optional[str] = None
|
|
499
579
|
try:
|
|
500
|
-
text
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
580
|
+
if hasattr(part, "text") and part.text is not None:
|
|
581
|
+
text = part.text
|
|
582
|
+
# Remove erroneous newline character if present
|
|
583
|
+
if not streaming:
|
|
584
|
+
text = text.rstrip("\n")
|
|
504
585
|
except AttributeError:
|
|
505
|
-
|
|
586
|
+
pass
|
|
506
587
|
|
|
507
|
-
if part.thought:
|
|
588
|
+
if hasattr(part, "thought") and part.thought:
|
|
508
589
|
thinking_message = {
|
|
509
590
|
"type": "thinking",
|
|
510
591
|
"thinking": part.text,
|
|
511
592
|
}
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
content = [thinking_message, content]
|
|
516
|
-
elif isinstance(content, list):
|
|
517
|
-
content.append(thinking_message)
|
|
518
|
-
else:
|
|
519
|
-
raise Exception("Unexpected content type")
|
|
520
|
-
|
|
521
|
-
elif text is not None:
|
|
522
|
-
if not content:
|
|
523
|
-
content = text
|
|
524
|
-
elif isinstance(content, str) and text:
|
|
525
|
-
content = [content, text]
|
|
526
|
-
elif isinstance(content, list) and text:
|
|
527
|
-
content.append(text)
|
|
528
|
-
elif text:
|
|
529
|
-
raise Exception("Unexpected content type")
|
|
593
|
+
content = _append_to_content(content, thinking_message)
|
|
594
|
+
elif text is not None and text:
|
|
595
|
+
content = _append_to_content(content, text)
|
|
530
596
|
|
|
531
597
|
if hasattr(part, "executable_code") and part.executable_code is not None:
|
|
532
598
|
if part.executable_code.code and part.executable_code.language:
|
|
@@ -535,14 +601,7 @@ def _parse_response_candidate(
|
|
|
535
601
|
"executable_code": part.executable_code.code,
|
|
536
602
|
"language": part.executable_code.language,
|
|
537
603
|
}
|
|
538
|
-
|
|
539
|
-
content = [code_message]
|
|
540
|
-
elif isinstance(content, str):
|
|
541
|
-
content = [content, code_message]
|
|
542
|
-
elif isinstance(content, list):
|
|
543
|
-
content.append(code_message)
|
|
544
|
-
else:
|
|
545
|
-
raise Exception("Unexpected content type")
|
|
604
|
+
content = _append_to_content(content, code_message)
|
|
546
605
|
|
|
547
606
|
if (
|
|
548
607
|
hasattr(part, "code_execution_result")
|
|
@@ -552,20 +611,25 @@ def _parse_response_candidate(
|
|
|
552
611
|
execution_result = {
|
|
553
612
|
"type": "code_execution_result",
|
|
554
613
|
"code_execution_result": part.code_execution_result.output,
|
|
614
|
+
"outcome": part.code_execution_result.outcome,
|
|
555
615
|
}
|
|
616
|
+
content = _append_to_content(content, execution_result)
|
|
556
617
|
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
618
|
+
if part.inline_data.mime_type.startswith("audio/"):
|
|
619
|
+
buffer = io.BytesIO()
|
|
620
|
+
|
|
621
|
+
with wave.open(buffer, "wb") as wf:
|
|
622
|
+
wf.setnchannels(1)
|
|
623
|
+
wf.setsampwidth(2)
|
|
624
|
+
# TODO: Read Sample Rate from MIME content type.
|
|
625
|
+
wf.setframerate(24000)
|
|
626
|
+
wf.writeframes(part.inline_data.data)
|
|
627
|
+
|
|
628
|
+
additional_kwargs["audio"] = buffer.getvalue()
|
|
565
629
|
|
|
566
630
|
if part.inline_data.mime_type.startswith("image/"):
|
|
567
631
|
image_format = part.inline_data.mime_type[6:]
|
|
568
|
-
|
|
632
|
+
image_message = {
|
|
569
633
|
"type": "image_url",
|
|
570
634
|
"image_url": {
|
|
571
635
|
"url": image_bytes_to_b64_string(
|
|
@@ -573,15 +637,7 @@ def _parse_response_candidate(
|
|
|
573
637
|
)
|
|
574
638
|
},
|
|
575
639
|
}
|
|
576
|
-
|
|
577
|
-
if not content:
|
|
578
|
-
content = [message]
|
|
579
|
-
elif isinstance(content, str) and message:
|
|
580
|
-
content = [content, message]
|
|
581
|
-
elif isinstance(content, list) and message:
|
|
582
|
-
content.append(message)
|
|
583
|
-
elif message:
|
|
584
|
-
raise Exception("Unexpected content type")
|
|
640
|
+
content = _append_to_content(content, image_message)
|
|
585
641
|
|
|
586
642
|
if part.function_call:
|
|
587
643
|
function_call = {"name": part.function_call.name}
|
|
@@ -660,35 +716,43 @@ def _response_to_result(
|
|
|
660
716
|
"""Converts a PaLM API response into a LangChain ChatResult."""
|
|
661
717
|
llm_output = {"prompt_feedback": proto.Message.to_dict(response.prompt_feedback)}
|
|
662
718
|
|
|
663
|
-
# previous usage metadata needs to be subtracted because gemini api returns
|
|
664
|
-
# already-accumulated token counts with each chunk
|
|
665
|
-
prev_input_tokens = prev_usage["input_tokens"] if prev_usage else 0
|
|
666
|
-
prev_output_tokens = prev_usage["output_tokens"] if prev_usage else 0
|
|
667
|
-
prev_total_tokens = prev_usage["total_tokens"] if prev_usage else 0
|
|
668
|
-
|
|
669
719
|
# Get usage metadata
|
|
670
720
|
try:
|
|
671
721
|
input_tokens = response.usage_metadata.prompt_token_count
|
|
672
|
-
output_tokens = response.usage_metadata.candidates_token_count
|
|
673
|
-
total_tokens = response.usage_metadata.total_token_count
|
|
674
722
|
thought_tokens = response.usage_metadata.thoughts_token_count
|
|
723
|
+
output_tokens = response.usage_metadata.candidates_token_count + thought_tokens
|
|
724
|
+
total_tokens = response.usage_metadata.total_token_count
|
|
675
725
|
cache_read_tokens = response.usage_metadata.cached_content_token_count
|
|
676
726
|
if input_tokens + output_tokens + cache_read_tokens + total_tokens > 0:
|
|
677
727
|
if thought_tokens > 0:
|
|
678
|
-
|
|
679
|
-
input_tokens=input_tokens
|
|
680
|
-
output_tokens=output_tokens
|
|
681
|
-
total_tokens=total_tokens
|
|
728
|
+
cumulative_usage = UsageMetadata(
|
|
729
|
+
input_tokens=input_tokens,
|
|
730
|
+
output_tokens=output_tokens,
|
|
731
|
+
total_tokens=total_tokens,
|
|
682
732
|
input_token_details={"cache_read": cache_read_tokens},
|
|
683
733
|
output_token_details={"reasoning": thought_tokens},
|
|
684
734
|
)
|
|
685
735
|
else:
|
|
686
|
-
|
|
687
|
-
input_tokens=input_tokens
|
|
688
|
-
output_tokens=output_tokens
|
|
689
|
-
total_tokens=total_tokens
|
|
736
|
+
cumulative_usage = UsageMetadata(
|
|
737
|
+
input_tokens=input_tokens,
|
|
738
|
+
output_tokens=output_tokens,
|
|
739
|
+
total_tokens=total_tokens,
|
|
690
740
|
input_token_details={"cache_read": cache_read_tokens},
|
|
691
741
|
)
|
|
742
|
+
# previous usage metadata needs to be subtracted because gemini api returns
|
|
743
|
+
# already-accumulated token counts with each chunk
|
|
744
|
+
lc_usage = subtract_usage(cumulative_usage, prev_usage)
|
|
745
|
+
if prev_usage and cumulative_usage["input_tokens"] < prev_usage.get(
|
|
746
|
+
"input_tokens", 0
|
|
747
|
+
):
|
|
748
|
+
# Gemini 1.5 and 2.0 return a lower cumulative count of prompt tokens
|
|
749
|
+
# in the final chunk. We take this count to be ground truth because
|
|
750
|
+
# it's consistent with the reported total tokens. So we need to
|
|
751
|
+
# ensure this chunk compensates (the subtract_usage funcction floors
|
|
752
|
+
# at zero).
|
|
753
|
+
lc_usage["input_tokens"] = cumulative_usage[
|
|
754
|
+
"input_tokens"
|
|
755
|
+
] - prev_usage.get("input_tokens", 0)
|
|
692
756
|
else:
|
|
693
757
|
lc_usage = None
|
|
694
758
|
except AttributeError:
|
|
@@ -1074,6 +1138,21 @@ class ChatGoogleGenerativeAI(_BaseGoogleGenerativeAI, BaseChatModel):
|
|
|
1074
1138
|
Gemini does not support system messages; any unsupported messages will
|
|
1075
1139
|
raise an error."""
|
|
1076
1140
|
|
|
1141
|
+
response_mime_type: Optional[str] = None
|
|
1142
|
+
"""Optional. Output response mimetype of the generated candidate text. Only
|
|
1143
|
+
supported in Gemini 1.5 and later models. Supported mimetype:
|
|
1144
|
+
* "text/plain": (default) Text output.
|
|
1145
|
+
* "application/json": JSON response in the candidates.
|
|
1146
|
+
* "text/x.enum": Enum in plain text.
|
|
1147
|
+
The model also needs to be prompted to output the appropriate response
|
|
1148
|
+
type, otherwise the behavior is undefined. This is a preview feature.
|
|
1149
|
+
"""
|
|
1150
|
+
|
|
1151
|
+
response_schema: Optional[Dict[str, Any]] = None
|
|
1152
|
+
""" Optional. Enforce an schema to the output.
|
|
1153
|
+
The format of the dictionary should follow Open API schema.
|
|
1154
|
+
"""
|
|
1155
|
+
|
|
1077
1156
|
cached_content: Optional[str] = None
|
|
1078
1157
|
"""The name of the cached content used as context to serve the prediction.
|
|
1079
1158
|
|
|
@@ -1281,6 +1360,7 @@ class ChatGoogleGenerativeAI(_BaseGoogleGenerativeAI, BaseChatModel):
|
|
|
1281
1360
|
self,
|
|
1282
1361
|
stop: Optional[List[str]],
|
|
1283
1362
|
generation_config: Optional[Dict[str, Any]] = None,
|
|
1363
|
+
**kwargs: Any,
|
|
1284
1364
|
) -> GenerationConfig:
|
|
1285
1365
|
gen_config = {
|
|
1286
1366
|
k: v
|
|
@@ -1311,6 +1391,24 @@ class ChatGoogleGenerativeAI(_BaseGoogleGenerativeAI, BaseChatModel):
|
|
|
1311
1391
|
}
|
|
1312
1392
|
if generation_config:
|
|
1313
1393
|
gen_config = {**gen_config, **generation_config}
|
|
1394
|
+
|
|
1395
|
+
response_mime_type = kwargs.get("response_mime_type", self.response_mime_type)
|
|
1396
|
+
if response_mime_type is not None:
|
|
1397
|
+
gen_config["response_mime_type"] = response_mime_type
|
|
1398
|
+
|
|
1399
|
+
response_schema = kwargs.get("response_schema", self.response_schema)
|
|
1400
|
+
if response_schema is not None:
|
|
1401
|
+
allowed_mime_types = ("application/json", "text/x.enum")
|
|
1402
|
+
if response_mime_type not in allowed_mime_types:
|
|
1403
|
+
error_message = (
|
|
1404
|
+
"`response_schema` is only supported when "
|
|
1405
|
+
f"`response_mime_type` is set to one of {allowed_mime_types}"
|
|
1406
|
+
)
|
|
1407
|
+
raise ValueError(error_message)
|
|
1408
|
+
|
|
1409
|
+
gapic_response_schema = _dict_to_gapic_schema(response_schema)
|
|
1410
|
+
if gapic_response_schema is not None:
|
|
1411
|
+
gen_config["response_schema"] = gapic_response_schema
|
|
1314
1412
|
return GenerationConfig(**gen_config)
|
|
1315
1413
|
|
|
1316
1414
|
def _generate(
|
|
@@ -1338,6 +1436,7 @@ class ChatGoogleGenerativeAI(_BaseGoogleGenerativeAI, BaseChatModel):
|
|
|
1338
1436
|
generation_config=generation_config,
|
|
1339
1437
|
cached_content=cached_content or self.cached_content,
|
|
1340
1438
|
tool_choice=tool_choice,
|
|
1439
|
+
**kwargs,
|
|
1341
1440
|
)
|
|
1342
1441
|
response: GenerateContentResponse = _chat_with_retry(
|
|
1343
1442
|
request=request,
|
|
@@ -1387,6 +1486,7 @@ class ChatGoogleGenerativeAI(_BaseGoogleGenerativeAI, BaseChatModel):
|
|
|
1387
1486
|
generation_config=generation_config,
|
|
1388
1487
|
cached_content=cached_content or self.cached_content,
|
|
1389
1488
|
tool_choice=tool_choice,
|
|
1489
|
+
**kwargs,
|
|
1390
1490
|
)
|
|
1391
1491
|
response: GenerateContentResponse = await _achat_with_retry(
|
|
1392
1492
|
request=request,
|
|
@@ -1421,6 +1521,7 @@ class ChatGoogleGenerativeAI(_BaseGoogleGenerativeAI, BaseChatModel):
|
|
|
1421
1521
|
generation_config=generation_config,
|
|
1422
1522
|
cached_content=cached_content or self.cached_content,
|
|
1423
1523
|
tool_choice=tool_choice,
|
|
1524
|
+
**kwargs,
|
|
1424
1525
|
)
|
|
1425
1526
|
response: GenerateContentResponse = _chat_with_retry(
|
|
1426
1527
|
request=request,
|
|
@@ -1429,7 +1530,7 @@ class ChatGoogleGenerativeAI(_BaseGoogleGenerativeAI, BaseChatModel):
|
|
|
1429
1530
|
metadata=self.default_metadata,
|
|
1430
1531
|
)
|
|
1431
1532
|
|
|
1432
|
-
prev_usage_metadata: UsageMetadata | None = None
|
|
1533
|
+
prev_usage_metadata: UsageMetadata | None = None # cumulative usage
|
|
1433
1534
|
for chunk in response:
|
|
1434
1535
|
_chat_result = _response_to_result(
|
|
1435
1536
|
chunk, stream=True, prev_usage=prev_usage_metadata
|
|
@@ -1437,21 +1538,10 @@ class ChatGoogleGenerativeAI(_BaseGoogleGenerativeAI, BaseChatModel):
|
|
|
1437
1538
|
gen = cast(ChatGenerationChunk, _chat_result.generations[0])
|
|
1438
1539
|
message = cast(AIMessageChunk, gen.message)
|
|
1439
1540
|
|
|
1440
|
-
curr_usage_metadata: UsageMetadata | dict[str, int] = (
|
|
1441
|
-
message.usage_metadata or {}
|
|
1442
|
-
)
|
|
1443
|
-
|
|
1444
1541
|
prev_usage_metadata = (
|
|
1445
1542
|
message.usage_metadata
|
|
1446
1543
|
if prev_usage_metadata is None
|
|
1447
|
-
else
|
|
1448
|
-
input_tokens=prev_usage_metadata.get("input_tokens", 0)
|
|
1449
|
-
+ curr_usage_metadata.get("input_tokens", 0),
|
|
1450
|
-
output_tokens=prev_usage_metadata.get("output_tokens", 0)
|
|
1451
|
-
+ curr_usage_metadata.get("output_tokens", 0),
|
|
1452
|
-
total_tokens=prev_usage_metadata.get("total_tokens", 0)
|
|
1453
|
-
+ curr_usage_metadata.get("total_tokens", 0),
|
|
1454
|
-
)
|
|
1544
|
+
else add_usage(prev_usage_metadata, message.usage_metadata)
|
|
1455
1545
|
)
|
|
1456
1546
|
|
|
1457
1547
|
if run_manager:
|
|
@@ -1499,8 +1589,9 @@ class ChatGoogleGenerativeAI(_BaseGoogleGenerativeAI, BaseChatModel):
|
|
|
1499
1589
|
generation_config=generation_config,
|
|
1500
1590
|
cached_content=cached_content or self.cached_content,
|
|
1501
1591
|
tool_choice=tool_choice,
|
|
1592
|
+
**kwargs,
|
|
1502
1593
|
)
|
|
1503
|
-
prev_usage_metadata: UsageMetadata | None = None
|
|
1594
|
+
prev_usage_metadata: UsageMetadata | None = None # cumulative usage
|
|
1504
1595
|
async for chunk in await _achat_with_retry(
|
|
1505
1596
|
request=request,
|
|
1506
1597
|
generation_method=self.async_client.stream_generate_content,
|
|
@@ -1513,21 +1604,10 @@ class ChatGoogleGenerativeAI(_BaseGoogleGenerativeAI, BaseChatModel):
|
|
|
1513
1604
|
gen = cast(ChatGenerationChunk, _chat_result.generations[0])
|
|
1514
1605
|
message = cast(AIMessageChunk, gen.message)
|
|
1515
1606
|
|
|
1516
|
-
curr_usage_metadata: UsageMetadata | dict[str, int] = (
|
|
1517
|
-
message.usage_metadata or {}
|
|
1518
|
-
)
|
|
1519
|
-
|
|
1520
1607
|
prev_usage_metadata = (
|
|
1521
1608
|
message.usage_metadata
|
|
1522
1609
|
if prev_usage_metadata is None
|
|
1523
|
-
else
|
|
1524
|
-
input_tokens=prev_usage_metadata.get("input_tokens", 0)
|
|
1525
|
-
+ curr_usage_metadata.get("input_tokens", 0),
|
|
1526
|
-
output_tokens=prev_usage_metadata.get("output_tokens", 0)
|
|
1527
|
-
+ curr_usage_metadata.get("output_tokens", 0),
|
|
1528
|
-
total_tokens=prev_usage_metadata.get("total_tokens", 0)
|
|
1529
|
-
+ curr_usage_metadata.get("total_tokens", 0),
|
|
1530
|
-
)
|
|
1610
|
+
else add_usage(prev_usage_metadata, message.usage_metadata)
|
|
1531
1611
|
)
|
|
1532
1612
|
|
|
1533
1613
|
if run_manager:
|
|
@@ -1546,6 +1626,7 @@ class ChatGoogleGenerativeAI(_BaseGoogleGenerativeAI, BaseChatModel):
|
|
|
1546
1626
|
tool_choice: Optional[Union[_ToolChoiceType, bool]] = None,
|
|
1547
1627
|
generation_config: Optional[Dict[str, Any]] = None,
|
|
1548
1628
|
cached_content: Optional[str] = None,
|
|
1629
|
+
**kwargs: Any,
|
|
1549
1630
|
) -> Tuple[GenerateContentRequest, Dict[str, Any]]:
|
|
1550
1631
|
if tool_choice and tool_config:
|
|
1551
1632
|
raise ValueError(
|
|
@@ -1617,7 +1698,9 @@ class ChatGoogleGenerativeAI(_BaseGoogleGenerativeAI, BaseChatModel):
|
|
|
1617
1698
|
tool_config=formatted_tool_config,
|
|
1618
1699
|
safety_settings=formatted_safety_settings,
|
|
1619
1700
|
generation_config=self._prepare_params(
|
|
1620
|
-
stop,
|
|
1701
|
+
stop,
|
|
1702
|
+
generation_config=generation_config,
|
|
1703
|
+
**kwargs,
|
|
1621
1704
|
),
|
|
1622
1705
|
cached_content=cached_content,
|
|
1623
1706
|
)
|
|
@@ -1645,33 +1728,65 @@ class ChatGoogleGenerativeAI(_BaseGoogleGenerativeAI, BaseChatModel):
|
|
|
1645
1728
|
def with_structured_output(
|
|
1646
1729
|
self,
|
|
1647
1730
|
schema: Union[Dict, Type[BaseModel]],
|
|
1731
|
+
method: Optional[Literal["function_calling", "json_mode"]] = "function_calling",
|
|
1648
1732
|
*,
|
|
1649
1733
|
include_raw: bool = False,
|
|
1650
1734
|
**kwargs: Any,
|
|
1651
1735
|
) -> Runnable[LanguageModelInput, Union[Dict, BaseModel]]:
|
|
1652
|
-
_ = kwargs.pop("method", None)
|
|
1653
1736
|
_ = kwargs.pop("strict", None)
|
|
1654
1737
|
if kwargs:
|
|
1655
1738
|
raise ValueError(f"Received unsupported arguments {kwargs}")
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
)
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1739
|
+
|
|
1740
|
+
parser: OutputParserLike
|
|
1741
|
+
|
|
1742
|
+
if method == "json_mode":
|
|
1743
|
+
if isinstance(schema, type) and is_basemodel_subclass(schema):
|
|
1744
|
+
if issubclass(schema, BaseModelV1):
|
|
1745
|
+
schema_json = schema.schema()
|
|
1746
|
+
else:
|
|
1747
|
+
schema_json = schema.model_json_schema()
|
|
1748
|
+
parser = PydanticOutputParser(pydantic_object=schema)
|
|
1749
|
+
else:
|
|
1750
|
+
if is_typeddict(schema):
|
|
1751
|
+
schema_json = convert_to_json_schema(schema)
|
|
1752
|
+
elif isinstance(schema, dict):
|
|
1753
|
+
schema_json = schema
|
|
1754
|
+
else:
|
|
1755
|
+
raise ValueError(f"Unsupported schema type {type(schema)}")
|
|
1756
|
+
parser = JsonOutputParser()
|
|
1757
|
+
|
|
1758
|
+
# Resolve refs in schema because they are not supported
|
|
1759
|
+
# by the Gemini API.
|
|
1760
|
+
schema_json = replace_defs_in_schema(schema_json)
|
|
1761
|
+
|
|
1762
|
+
llm = self.bind(
|
|
1763
|
+
response_mime_type="application/json",
|
|
1764
|
+
response_schema=schema_json,
|
|
1668
1765
|
ls_structured_output_format={
|
|
1669
|
-
"kwargs": {"method":
|
|
1670
|
-
"schema":
|
|
1766
|
+
"kwargs": {"method": method},
|
|
1767
|
+
"schema": schema_json,
|
|
1671
1768
|
},
|
|
1672
1769
|
)
|
|
1673
|
-
|
|
1674
|
-
|
|
1770
|
+
else:
|
|
1771
|
+
tool_name = _get_tool_name(schema) # type: ignore[arg-type]
|
|
1772
|
+
if isinstance(schema, type) and is_basemodel_subclass_safe(schema):
|
|
1773
|
+
parser = PydanticToolsParser(tools=[schema], first_tool_only=True)
|
|
1774
|
+
else:
|
|
1775
|
+
parser = JsonOutputKeyToolsParser(
|
|
1776
|
+
key_name=tool_name, first_tool_only=True
|
|
1777
|
+
)
|
|
1778
|
+
tool_choice = tool_name if self._supports_tool_choice else None
|
|
1779
|
+
try:
|
|
1780
|
+
llm = self.bind_tools(
|
|
1781
|
+
[schema],
|
|
1782
|
+
tool_choice=tool_choice,
|
|
1783
|
+
ls_structured_output_format={
|
|
1784
|
+
"kwargs": {"method": "function_calling"},
|
|
1785
|
+
"schema": convert_to_openai_tool(schema),
|
|
1786
|
+
},
|
|
1787
|
+
)
|
|
1788
|
+
except Exception:
|
|
1789
|
+
llm = self.bind_tools([schema], tool_choice=tool_choice)
|
|
1675
1790
|
if include_raw:
|
|
1676
1791
|
parser_with_fallback = RunnablePassthrough.assign(
|
|
1677
1792
|
parsed=itemgetter("raw") | parser, parsing_error=lambda _: None
|
|
@@ -98,6 +98,7 @@ class GoogleGenerativeAIEmbeddings(BaseModel, Embeddings):
|
|
|
98
98
|
api_key=google_api_key,
|
|
99
99
|
client_info=client_info,
|
|
100
100
|
client_options=self.client_options,
|
|
101
|
+
transport=self.transport,
|
|
101
102
|
)
|
|
102
103
|
return self
|
|
103
104
|
|
|
@@ -253,7 +254,9 @@ class GoogleGenerativeAIEmbeddings(BaseModel, Embeddings):
|
|
|
253
254
|
Returns:
|
|
254
255
|
Embedding for the text.
|
|
255
256
|
"""
|
|
256
|
-
|
|
257
|
+
task_type_to_use = task_type if task_type else self.task_type
|
|
258
|
+
if task_type_to_use is None:
|
|
259
|
+
task_type_to_use = "RETRIEVAL_QUERY" # Default to RETRIEVAL_QUERY
|
|
257
260
|
try:
|
|
258
261
|
request: EmbedContentRequest = self._prepare_request(
|
|
259
262
|
text=text,
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: langchain-google-genai
|
|
3
|
+
Version: 2.1.7
|
|
4
|
+
Summary: An integration package connecting Google's genai package and LangChain
|
|
5
|
+
Home-page: https://github.com/langchain-ai/langchain-google
|
|
6
|
+
License: MIT
|
|
7
|
+
Requires-Python: >=3.9,<4.0
|
|
8
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Requires-Dist: filetype (>=1.2.0,<2.0.0)
|
|
15
|
+
Requires-Dist: google-ai-generativelanguage (>=0.6.18,<0.7.0)
|
|
16
|
+
Requires-Dist: langchain-core (>=0.3.68,<0.4.0)
|
|
17
|
+
Requires-Dist: pydantic (>=2,<3)
|
|
18
|
+
Project-URL: Repository, https://github.com/langchain-ai/langchain-google
|
|
19
|
+
Project-URL: Source Code, https://github.com/langchain-ai/langchain-google/tree/main/libs/genai
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# langchain-google-genai
|
|
23
|
+
|
|
24
|
+
**LangChain integration for Google Gemini models using the `generative-ai` SDK**
|
|
25
|
+
|
|
26
|
+
This package enables seamless access to Google Gemini's chat, vision, embeddings, and retrieval-augmented generation (RAG) features within the LangChain ecosystem.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Table of Contents
|
|
31
|
+
|
|
32
|
+
- [Overview](#overview)
|
|
33
|
+
- [Installation](#installation)
|
|
34
|
+
- [Quickstart](#quickstart)
|
|
35
|
+
- [Chat Models](#chat-models)
|
|
36
|
+
- [Multimodal Inputs](#multimodal-inputs)
|
|
37
|
+
- [Multimodal Outputs](#multimodal-outputs)
|
|
38
|
+
- [Multimodal Outputs in Chains](#multimodal-outputs-in-chains)
|
|
39
|
+
- [Thinking Support](#thinking-support)
|
|
40
|
+
- [Embeddings](#embeddings)
|
|
41
|
+
- [Semantic Retrieval (RAG)](#semantic-retrieval-rag)
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Overview
|
|
46
|
+
|
|
47
|
+
This package provides LangChain support for Google Gemini models (via the official [Google Generative AI SDK](https://googleapis.github.io/python-genai/)). It supports:
|
|
48
|
+
|
|
49
|
+
- Text and vision-based chat models
|
|
50
|
+
- Embeddings for semantic search
|
|
51
|
+
- Multimodal inputs and outputs
|
|
52
|
+
- Retrieval-Augmented Generation (RAG)
|
|
53
|
+
- Thought tracing with reasoning tokens
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## Installation
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
pip install -U langchain-google-genai
|
|
61
|
+
````
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## Quickstart
|
|
66
|
+
|
|
67
|
+
Set up your environment variable with your Gemini API key:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
export GOOGLE_API_KEY=your-api-key
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Then use the `ChatGoogleGenerativeAI` interface:
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
from langchain_google_genai import ChatGoogleGenerativeAI
|
|
77
|
+
|
|
78
|
+
llm = ChatGoogleGenerativeAI(model="gemini-pro")
|
|
79
|
+
response = llm.invoke("Sing a ballad of LangChain.")
|
|
80
|
+
print(response.content)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## Chat Models
|
|
86
|
+
|
|
87
|
+
The main interface for Gemini chat models is `ChatGoogleGenerativeAI`.
|
|
88
|
+
|
|
89
|
+
### Multimodal Inputs
|
|
90
|
+
|
|
91
|
+
Gemini vision models support image inputs in single messages.
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
from langchain_core.messages import HumanMessage
|
|
95
|
+
from langchain_google_genai import ChatGoogleGenerativeAI
|
|
96
|
+
|
|
97
|
+
llm = ChatGoogleGenerativeAI(model="gemini-pro-vision")
|
|
98
|
+
|
|
99
|
+
message = HumanMessage(
|
|
100
|
+
content=[
|
|
101
|
+
{"type": "text", "text": "What's in this image?"},
|
|
102
|
+
{"type": "image_url", "image_url": "https://picsum.photos/seed/picsum/200/300"},
|
|
103
|
+
]
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
response = llm.invoke([message])
|
|
107
|
+
print(response.content)
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
✅ `image_url` can be:
|
|
111
|
+
|
|
112
|
+
* A public image URL
|
|
113
|
+
* A Google Cloud Storage path (`gcs://...`)
|
|
114
|
+
* A base64-encoded image (e.g., `data:image/png;base64,...`)
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
### Multimodal Outputs
|
|
119
|
+
|
|
120
|
+
The Gemini 2.0 Flash Experimental model supports both text and inline image outputs.
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
from langchain_google_genai import ChatGoogleGenerativeAI
|
|
124
|
+
|
|
125
|
+
llm = ChatGoogleGenerativeAI(model="models/gemini-2.0-flash-exp-image-generation")
|
|
126
|
+
|
|
127
|
+
response = llm.invoke(
|
|
128
|
+
"Generate an image of a cat and say meow",
|
|
129
|
+
generation_config=dict(response_modalities=["TEXT", "IMAGE"]),
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
image_base64 = response.content[0].get("image_url").get("url").split(",")[-1]
|
|
133
|
+
meow_text = response.content[1]
|
|
134
|
+
print(meow_text)
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
### Audio Output
|
|
140
|
+
|
|
141
|
+
```
|
|
142
|
+
from langchain_google_genai import ChatGoogleGenerativeAI
|
|
143
|
+
|
|
144
|
+
llm = ChatGoogleGenerativeAI(model="models/gemini-2.5-flash-preview-tts")
|
|
145
|
+
# example
|
|
146
|
+
response = llm.invoke(
|
|
147
|
+
"Please say The quick brown fox jumps over the lazy dog",
|
|
148
|
+
generation_config=dict(response_modalities=["AUDIO"]),
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
# Base64 encoded binary data of the image
|
|
152
|
+
wav_data = response.additional_kwargs.get("audio")
|
|
153
|
+
with open("output.wav", "wb") as f:
|
|
154
|
+
f.write(wav_data)
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
### Multimodal Outputs in Chains
|
|
160
|
+
|
|
161
|
+
You can use Gemini models in a LangChain chain:
|
|
162
|
+
|
|
163
|
+
```python
|
|
164
|
+
from langchain_core.runnables import RunnablePassthrough
|
|
165
|
+
from langchain_core.prompts import ChatPromptTemplate
|
|
166
|
+
from langchain_google_genai import ChatGoogleGenerativeAI, Modality
|
|
167
|
+
|
|
168
|
+
llm = ChatGoogleGenerativeAI(
|
|
169
|
+
model="models/gemini-2.0-flash-exp-image-generation",
|
|
170
|
+
response_modalities=[Modality.TEXT, Modality.IMAGE],
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
prompt = ChatPromptTemplate.from_messages([
|
|
174
|
+
("human", "Generate an image of {animal} and tell me the sound it makes.")
|
|
175
|
+
])
|
|
176
|
+
|
|
177
|
+
chain = {"animal": RunnablePassthrough()} | prompt | llm
|
|
178
|
+
response = chain.invoke("cat")
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
### Thinking Support
|
|
184
|
+
|
|
185
|
+
Gemini 2.5 Flash Preview supports internal reasoning ("thoughts").
|
|
186
|
+
|
|
187
|
+
```python
|
|
188
|
+
from langchain_google_genai import ChatGoogleGenerativeAI
|
|
189
|
+
|
|
190
|
+
llm = ChatGoogleGenerativeAI(
|
|
191
|
+
model="models/gemini-2.5-flash-preview-04-17",
|
|
192
|
+
thinking_budget=1024
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
response = llm.invoke("How many O's are in Google? How did you verify your answer?")
|
|
196
|
+
reasoning_score = response.usage_metadata["output_token_details"]["reasoning"]
|
|
197
|
+
|
|
198
|
+
print("Response:", response.content)
|
|
199
|
+
print("Reasoning tokens used:", reasoning_score)
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
## Embeddings
|
|
205
|
+
|
|
206
|
+
You can use Gemini embeddings in LangChain:
|
|
207
|
+
|
|
208
|
+
```python
|
|
209
|
+
from langchain_google_genai import GoogleGenerativeAIEmbeddings
|
|
210
|
+
|
|
211
|
+
embeddings = GoogleGenerativeAIEmbeddings(model="models/embedding-001")
|
|
212
|
+
vector = embeddings.embed_query("hello, world!")
|
|
213
|
+
print(vector)
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
## Semantic Retrieval (RAG)
|
|
219
|
+
|
|
220
|
+
Use Gemini with RAG to retrieve relevant documents from your knowledge base.
|
|
221
|
+
|
|
222
|
+
```python
|
|
223
|
+
from langchain_google_genai.vectorstores import GoogleVectorStore
|
|
224
|
+
from langchain_text_splitters import CharacterTextSplitter
|
|
225
|
+
from langchain_community.document_loaders import DirectoryLoader
|
|
226
|
+
|
|
227
|
+
# Create a corpus (collection of documents)
|
|
228
|
+
corpus_store = GoogleVectorStore.create_corpus(display_name="My Corpus")
|
|
229
|
+
|
|
230
|
+
# Create a document under that corpus
|
|
231
|
+
document_store = GoogleVectorStore.create_document(
|
|
232
|
+
corpus_id=corpus_store.corpus_id, display_name="My Document"
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
# Load and upload documents
|
|
236
|
+
text_splitter = CharacterTextSplitter(chunk_size=500, chunk_overlap=0)
|
|
237
|
+
for file in DirectoryLoader(path="data/").load():
|
|
238
|
+
chunks = text_splitter.split_documents([file])
|
|
239
|
+
document_store.add_documents(chunks)
|
|
240
|
+
|
|
241
|
+
# Query the document corpus
|
|
242
|
+
aqa = corpus_store.as_aqa()
|
|
243
|
+
response = aqa.invoke("What is the meaning of life?")
|
|
244
|
+
|
|
245
|
+
print("Answer:", response.answer)
|
|
246
|
+
print("Passages:", response.attributed_passages)
|
|
247
|
+
print("Answerable probability:", response.answerable_probability)
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
---
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
## Resources
|
|
254
|
+
|
|
255
|
+
* [LangChain Documentation](https://docs.langchain.com/)
|
|
256
|
+
* [Google Generative AI SDK](https://googleapis.github.io/python-genai/)
|
|
257
|
+
* [Gemini Model Documentation](https://ai.google.dev/)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
langchain_google_genai/__init__.py,sha256=IsTvA3UcECLDckt3zWxK6u-n3MEa5KeEQpqsS-Z8shM,2784
|
|
2
2
|
langchain_google_genai/_common.py,sha256=TeAmAwcRPk_dTXiIQQubamHAhuKNb_XsWy7yllkKW4w,5936
|
|
3
3
|
langchain_google_genai/_enums.py,sha256=Zj3BXXLlkm_UybegCi6fLsfFhriJCt_LAJvgatgPWQ0,252
|
|
4
|
-
langchain_google_genai/_function_utils.py,sha256=
|
|
4
|
+
langchain_google_genai/_function_utils.py,sha256=CkQyxfUVGqHfmZQwf4JtMXBsS9SOzk99Pa2TytnuLbE,21648
|
|
5
5
|
langchain_google_genai/_genai_extension.py,sha256=81a4ly5ZHlqMf37uJfdB8K41qE6J5ujLnbUypIfFf2o,20775
|
|
6
6
|
langchain_google_genai/_image_utils.py,sha256=tPrQyMvVmO8xkuow1SvA91omxUEv9ZUy1EMHNGjMAKY,5202
|
|
7
|
-
langchain_google_genai/chat_models.py,sha256=
|
|
8
|
-
langchain_google_genai/embeddings.py,sha256=
|
|
7
|
+
langchain_google_genai/chat_models.py,sha256=HtvdPkivOMKzHRHYZkwQxDOJBwpqN47Z2dPRDfYnbNo,72934
|
|
8
|
+
langchain_google_genai/embeddings.py,sha256=IwM6PBnuweABvYmzjgShOxrVffKbQnnIzPfKBFf8xms,10636
|
|
9
9
|
langchain_google_genai/genai_aqa.py,sha256=qB6h3-BSXqe0YLR3eeVllYzmNKK6ofI6xJLdBahUVZo,4300
|
|
10
10
|
langchain_google_genai/google_vector_store.py,sha256=4wvhIiOmc3Fo046FyafPmT9NBCLek-9bgluvuTfrbpQ,16148
|
|
11
11
|
langchain_google_genai/llms.py,sha256=ASjrEk2T_1hUXVNJlfPB8PKC4PbhPe00H3_UHunMc_Q,5334
|
|
12
12
|
langchain_google_genai/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
|
-
langchain_google_genai-2.1.
|
|
14
|
-
langchain_google_genai-2.1.
|
|
15
|
-
langchain_google_genai-2.1.
|
|
16
|
-
langchain_google_genai-2.1.
|
|
13
|
+
langchain_google_genai-2.1.7.dist-info/LICENSE,sha256=DppmdYJVSc1jd0aio6ptnMUn5tIHrdAhQ12SclEBfBg,1072
|
|
14
|
+
langchain_google_genai-2.1.7.dist-info/METADATA,sha256=eoFaqwS54B_f1bM25aItVsFdwFzlToTCpZHkx_1Vqw8,6995
|
|
15
|
+
langchain_google_genai-2.1.7.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
|
|
16
|
+
langchain_google_genai-2.1.7.dist-info/RECORD,,
|
|
@@ -1,174 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.1
|
|
2
|
-
Name: langchain-google-genai
|
|
3
|
-
Version: 2.1.5
|
|
4
|
-
Summary: An integration package connecting Google's genai package and LangChain
|
|
5
|
-
Home-page: https://github.com/langchain-ai/langchain-google
|
|
6
|
-
License: MIT
|
|
7
|
-
Requires-Python: >=3.9,<4.0
|
|
8
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
9
|
-
Classifier: Programming Language :: Python :: 3
|
|
10
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
11
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
12
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
-
Requires-Dist: filetype (>=1.2.0,<2.0.0)
|
|
15
|
-
Requires-Dist: google-ai-generativelanguage (>=0.6.18,<0.7.0)
|
|
16
|
-
Requires-Dist: langchain-core (>=0.3.62,<0.4.0)
|
|
17
|
-
Requires-Dist: pydantic (>=2,<3)
|
|
18
|
-
Project-URL: Repository, https://github.com/langchain-ai/langchain-google
|
|
19
|
-
Project-URL: Source Code, https://github.com/langchain-ai/langchain-google/tree/main/libs/genai
|
|
20
|
-
Description-Content-Type: text/markdown
|
|
21
|
-
|
|
22
|
-
# langchain-google-genai
|
|
23
|
-
|
|
24
|
-
This package contains the LangChain integrations for Gemini through their generative-ai SDK.
|
|
25
|
-
|
|
26
|
-
## Installation
|
|
27
|
-
|
|
28
|
-
```bash
|
|
29
|
-
pip install -U langchain-google-genai
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
## Chat Models
|
|
34
|
-
|
|
35
|
-
This package contains the `ChatGoogleGenerativeAI` class, which is the recommended way to interface with the Google Gemini series of models.
|
|
36
|
-
|
|
37
|
-
To use, install the requirements, and configure your environment.
|
|
38
|
-
|
|
39
|
-
```bash
|
|
40
|
-
export GOOGLE_API_KEY=your-api-key
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
Then initialize
|
|
44
|
-
|
|
45
|
-
```python
|
|
46
|
-
from langchain_google_genai import ChatGoogleGenerativeAI
|
|
47
|
-
|
|
48
|
-
llm = ChatGoogleGenerativeAI(model="gemini-pro")
|
|
49
|
-
llm.invoke("Sing a ballad of LangChain.")
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
#### Multimodal inputs
|
|
53
|
-
|
|
54
|
-
Gemini vision model supports image inputs when providing a single chat message. Example:
|
|
55
|
-
|
|
56
|
-
```
|
|
57
|
-
from langchain_core.messages import HumanMessage
|
|
58
|
-
from langchain_google_genai import ChatGoogleGenerativeAI
|
|
59
|
-
|
|
60
|
-
llm = ChatGoogleGenerativeAI(model="gemini-pro-vision")
|
|
61
|
-
# example
|
|
62
|
-
message = HumanMessage(
|
|
63
|
-
content=[
|
|
64
|
-
{
|
|
65
|
-
"type": "text",
|
|
66
|
-
"text": "What's in this image?",
|
|
67
|
-
}, # You can optionally provide text parts
|
|
68
|
-
{"type": "image_url", "image_url": "https://picsum.photos/seed/picsum/200/300"},
|
|
69
|
-
]
|
|
70
|
-
)
|
|
71
|
-
llm.invoke([message])
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
The value of `image_url` can be any of the following:
|
|
75
|
-
|
|
76
|
-
- A public image URL
|
|
77
|
-
- An accessible gcs file (e.g., "gcs://path/to/file.png")
|
|
78
|
-
- A base64 encoded image (e.g., `data:image/png;base64,abcd124`)
|
|
79
|
-
|
|
80
|
-
#### Multimodal outputs
|
|
81
|
-
|
|
82
|
-
Gemini 2.0 Flash Experimental model supports text output with inline images
|
|
83
|
-
|
|
84
|
-
```
|
|
85
|
-
from langchain_google_genai import ChatGoogleGenerativeAI
|
|
86
|
-
|
|
87
|
-
llm = ChatGoogleGenerativeAI(model="models/gemini-2.0-flash-exp-image-generation")
|
|
88
|
-
# example
|
|
89
|
-
response = llm.invoke(
|
|
90
|
-
"Generate an image of a cat and say meow",
|
|
91
|
-
generation_config=dict(response_modalities=["TEXT", "IMAGE"]),
|
|
92
|
-
)
|
|
93
|
-
|
|
94
|
-
# Base64 encoded binary data of the image
|
|
95
|
-
image_base64 = response.content[0].get("image_url").get("url").split(",")[-1]
|
|
96
|
-
meow_str = response.content[1]
|
|
97
|
-
```
|
|
98
|
-
|
|
99
|
-
#### Multimodal Outputs in Chains
|
|
100
|
-
|
|
101
|
-
```
|
|
102
|
-
from langchain_core.runnables import RunnablePassthrough
|
|
103
|
-
from langchain_core.prompts import ChatPromptTemplate
|
|
104
|
-
|
|
105
|
-
from langchain_google_genai import ChatGoogleGenerativeAI, Modality
|
|
106
|
-
|
|
107
|
-
llm = ChatGoogleGenerativeAI(
|
|
108
|
-
model="models/gemini-2.0-flash-exp-image-generation",
|
|
109
|
-
response_modalities=[Modality.TEXT, Modality.IMAGE],
|
|
110
|
-
)
|
|
111
|
-
|
|
112
|
-
prompt = ChatPromptTemplate(
|
|
113
|
-
[("human", "Generate an image of {animal} and tell me the sound of the animal")]
|
|
114
|
-
)
|
|
115
|
-
chain = {"animal": RunnablePassthrough()} | prompt | llm
|
|
116
|
-
res = chain.invoke("cat")
|
|
117
|
-
```
|
|
118
|
-
|
|
119
|
-
#### Thinking support
|
|
120
|
-
|
|
121
|
-
Gemini 2.5 Flash model supports reasoning through their thoughts
|
|
122
|
-
|
|
123
|
-
```
|
|
124
|
-
from langchain_google_genai import ChatGoogleGenerativeAI
|
|
125
|
-
|
|
126
|
-
llm = ChatGoogleGenerativeAI(model="models/gemini-2.5-flash-preview-04-17", thinking_budget=1024)
|
|
127
|
-
|
|
128
|
-
response = llm.invoke(
|
|
129
|
-
"How many O's are in Google? Please tell me how you double checked the result"
|
|
130
|
-
)
|
|
131
|
-
|
|
132
|
-
assert response.usage_metadata["output_token_details"]["reasoning"] > 0
|
|
133
|
-
```
|
|
134
|
-
|
|
135
|
-
## Embeddings
|
|
136
|
-
|
|
137
|
-
This package also adds support for google's embeddings models.
|
|
138
|
-
|
|
139
|
-
```
|
|
140
|
-
from langchain_google_genai import GoogleGenerativeAIEmbeddings
|
|
141
|
-
|
|
142
|
-
embeddings = GoogleGenerativeAIEmbeddings(model="models/embedding-001")
|
|
143
|
-
embeddings.embed_query("hello, world!")
|
|
144
|
-
```
|
|
145
|
-
|
|
146
|
-
## Semantic Retrieval
|
|
147
|
-
|
|
148
|
-
Enables retrieval augmented generation (RAG) in your application.
|
|
149
|
-
|
|
150
|
-
```
|
|
151
|
-
# Create a new store for housing your documents.
|
|
152
|
-
corpus_store = GoogleVectorStore.create_corpus(display_name="My Corpus")
|
|
153
|
-
|
|
154
|
-
# Create a new document under the above corpus.
|
|
155
|
-
document_store = GoogleVectorStore.create_document(
|
|
156
|
-
corpus_id=corpus_store.corpus_id, display_name="My Document"
|
|
157
|
-
)
|
|
158
|
-
|
|
159
|
-
# Upload some texts to the document.
|
|
160
|
-
text_splitter = CharacterTextSplitter(chunk_size=500, chunk_overlap=0)
|
|
161
|
-
for file in DirectoryLoader(path="data/").load():
|
|
162
|
-
documents = text_splitter.split_documents([file])
|
|
163
|
-
document_store.add_documents(documents)
|
|
164
|
-
|
|
165
|
-
# Talk to your entire corpus with possibly many documents.
|
|
166
|
-
aqa = corpus_store.as_aqa()
|
|
167
|
-
answer = aqa.invoke("What is the meaning of life?")
|
|
168
|
-
|
|
169
|
-
# Read the response along with the attributed passages and answerability.
|
|
170
|
-
print(response.answer)
|
|
171
|
-
print(response.attributed_passages)
|
|
172
|
-
print(response.answerable_probability)
|
|
173
|
-
```
|
|
174
|
-
|
|
File without changes
|
|
File without changes
|