chatlas 0.2.0__py3-none-any.whl → 0.4.0__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 chatlas might be problematic. Click here for more details.
- chatlas/__init__.py +2 -1
- chatlas/_anthropic.py +104 -6
- chatlas/_chat.py +246 -24
- chatlas/_content.py +20 -7
- chatlas/_google.py +312 -161
- chatlas/_merge.py +1 -1
- chatlas/_ollama.py +8 -0
- chatlas/_openai.py +64 -7
- chatlas/_provider.py +16 -8
- chatlas/py.typed +0 -0
- chatlas/types/__init__.py +5 -1
- chatlas/types/anthropic/_client.py +0 -8
- chatlas/types/anthropic/_submit.py +2 -3
- chatlas/types/google/_client.py +12 -91
- chatlas/types/google/_submit.py +40 -87
- chatlas/types/openai/_client.py +1 -0
- chatlas/types/openai/_client_azure.py +1 -0
- chatlas/types/openai/_submit.py +10 -2
- {chatlas-0.2.0.dist-info → chatlas-0.4.0.dist-info}/METADATA +25 -11
- chatlas-0.4.0.dist-info/RECORD +38 -0
- {chatlas-0.2.0.dist-info → chatlas-0.4.0.dist-info}/WHEEL +1 -1
- chatlas-0.2.0.dist-info/RECORD +0 -37
chatlas/_google.py
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import base64
|
|
3
4
|
import json
|
|
4
|
-
from typing import TYPE_CHECKING, Any, Literal, Optional, overload
|
|
5
|
+
from typing import TYPE_CHECKING, Any, Literal, Optional, cast, overload
|
|
5
6
|
|
|
6
7
|
from pydantic import BaseModel
|
|
7
8
|
|
|
@@ -16,20 +17,19 @@ from ._content import (
|
|
|
16
17
|
ContentToolResult,
|
|
17
18
|
)
|
|
18
19
|
from ._logging import log_model_default
|
|
20
|
+
from ._merge import merge_dicts
|
|
19
21
|
from ._provider import Provider
|
|
20
|
-
from .
|
|
21
|
-
from .
|
|
22
|
+
from ._tokens import tokens_log
|
|
23
|
+
from ._tools import Tool
|
|
24
|
+
from ._turn import Turn, normalize_turns, user_turn
|
|
22
25
|
|
|
23
26
|
if TYPE_CHECKING:
|
|
24
|
-
from google.
|
|
25
|
-
|
|
26
|
-
FunctionDeclaration,
|
|
27
|
-
PartType,
|
|
28
|
-
)
|
|
29
|
-
from google.generativeai.types.generation_types import (
|
|
30
|
-
AsyncGenerateContentResponse,
|
|
27
|
+
from google.genai.types import Content as GoogleContent
|
|
28
|
+
from google.genai.types import (
|
|
31
29
|
GenerateContentResponse,
|
|
32
|
-
|
|
30
|
+
GenerateContentResponseDict,
|
|
31
|
+
Part,
|
|
32
|
+
PartDict,
|
|
33
33
|
)
|
|
34
34
|
|
|
35
35
|
from .types.google import ChatClientArgs, SubmitInputArgs
|
|
@@ -61,8 +61,8 @@ def ChatGoogle(
|
|
|
61
61
|
::: {.callout-note}
|
|
62
62
|
## Python requirements
|
|
63
63
|
|
|
64
|
-
`ChatGoogle` requires the `google-
|
|
65
|
-
(e.g., `pip install google-
|
|
64
|
+
`ChatGoogle` requires the `google-genai` package
|
|
65
|
+
(e.g., `pip install google-genai`).
|
|
66
66
|
:::
|
|
67
67
|
|
|
68
68
|
Examples
|
|
@@ -95,17 +95,13 @@ def ChatGoogle(
|
|
|
95
95
|
The API key to use for authentication. You generally should not supply
|
|
96
96
|
this directly, but instead set the `GOOGLE_API_KEY` environment variable.
|
|
97
97
|
kwargs
|
|
98
|
-
Additional arguments to pass to the `genai.
|
|
98
|
+
Additional arguments to pass to the `genai.Client` constructor.
|
|
99
99
|
|
|
100
100
|
Returns
|
|
101
101
|
-------
|
|
102
102
|
Chat
|
|
103
103
|
A Chat object.
|
|
104
104
|
|
|
105
|
-
Limitations
|
|
106
|
-
-----------
|
|
107
|
-
`ChatGoogle` currently doesn't work with streaming tools.
|
|
108
|
-
|
|
109
105
|
Note
|
|
110
106
|
----
|
|
111
107
|
Pasting an API key into a chat constructor (e.g., `ChatGoogle(api_key="...")`)
|
|
@@ -144,63 +140,49 @@ def ChatGoogle(
|
|
|
144
140
|
"""
|
|
145
141
|
|
|
146
142
|
if model is None:
|
|
147
|
-
model = log_model_default("gemini-
|
|
148
|
-
|
|
149
|
-
turns = normalize_turns(
|
|
150
|
-
turns or [],
|
|
151
|
-
system_prompt=system_prompt,
|
|
152
|
-
)
|
|
143
|
+
model = log_model_default("gemini-2.0-flash")
|
|
153
144
|
|
|
154
145
|
return Chat(
|
|
155
146
|
provider=GoogleProvider(
|
|
156
|
-
turns=turns,
|
|
157
147
|
model=model,
|
|
158
148
|
api_key=api_key,
|
|
159
149
|
kwargs=kwargs,
|
|
160
150
|
),
|
|
161
|
-
turns=
|
|
151
|
+
turns=normalize_turns(
|
|
152
|
+
turns or [],
|
|
153
|
+
system_prompt=system_prompt,
|
|
154
|
+
),
|
|
162
155
|
)
|
|
163
156
|
|
|
164
157
|
|
|
165
|
-
# The dictionary form of ChatCompletion (TODO: stronger typing)?
|
|
166
|
-
GenerateContentDict = dict[str, Any]
|
|
167
|
-
|
|
168
|
-
|
|
169
158
|
class GoogleProvider(
|
|
170
|
-
Provider[
|
|
159
|
+
Provider[
|
|
160
|
+
GenerateContentResponse, GenerateContentResponse, "GenerateContentResponseDict"
|
|
161
|
+
]
|
|
171
162
|
):
|
|
172
163
|
def __init__(
|
|
173
164
|
self,
|
|
174
165
|
*,
|
|
175
|
-
turns: list[Turn],
|
|
176
166
|
model: str,
|
|
177
167
|
api_key: str | None,
|
|
178
168
|
kwargs: Optional["ChatClientArgs"],
|
|
179
169
|
):
|
|
180
170
|
try:
|
|
181
|
-
from google
|
|
171
|
+
from google import genai
|
|
182
172
|
except ImportError:
|
|
183
173
|
raise ImportError(
|
|
184
|
-
f"The {self.__class__.__name__} class requires the `google-
|
|
185
|
-
"Install it with `pip install google-
|
|
174
|
+
f"The {self.__class__.__name__} class requires the `google-genai` package. "
|
|
175
|
+
"Install it with `pip install google-genai`."
|
|
186
176
|
)
|
|
187
177
|
|
|
188
|
-
|
|
189
|
-
import google.generativeai as genai
|
|
190
|
-
|
|
191
|
-
genai.configure(api_key=api_key)
|
|
192
|
-
|
|
193
|
-
system_prompt = None
|
|
194
|
-
if len(turns) > 0 and turns[0].role == "system":
|
|
195
|
-
system_prompt = turns[0].text
|
|
178
|
+
self._model = model
|
|
196
179
|
|
|
197
180
|
kwargs_full: "ChatClientArgs" = {
|
|
198
|
-
"
|
|
199
|
-
"system_instruction": system_prompt,
|
|
181
|
+
"api_key": api_key,
|
|
200
182
|
**(kwargs or {}),
|
|
201
183
|
}
|
|
202
184
|
|
|
203
|
-
self._client =
|
|
185
|
+
self._client = genai.Client(**kwargs_full)
|
|
204
186
|
|
|
205
187
|
@overload
|
|
206
188
|
def chat_perform(
|
|
@@ -232,8 +214,11 @@ class GoogleProvider(
|
|
|
232
214
|
data_model: Optional[type[BaseModel]] = None,
|
|
233
215
|
kwargs: Optional["SubmitInputArgs"] = None,
|
|
234
216
|
):
|
|
235
|
-
kwargs = self._chat_perform_args(
|
|
236
|
-
|
|
217
|
+
kwargs = self._chat_perform_args(turns, tools, data_model, kwargs)
|
|
218
|
+
if stream:
|
|
219
|
+
return self._client.models.generate_content_stream(**kwargs)
|
|
220
|
+
else:
|
|
221
|
+
return self._client.models.generate_content(**kwargs)
|
|
237
222
|
|
|
238
223
|
@overload
|
|
239
224
|
async def chat_perform_async(
|
|
@@ -265,101 +250,160 @@ class GoogleProvider(
|
|
|
265
250
|
data_model: Optional[type[BaseModel]] = None,
|
|
266
251
|
kwargs: Optional["SubmitInputArgs"] = None,
|
|
267
252
|
):
|
|
268
|
-
kwargs = self._chat_perform_args(
|
|
269
|
-
|
|
253
|
+
kwargs = self._chat_perform_args(turns, tools, data_model, kwargs)
|
|
254
|
+
if stream:
|
|
255
|
+
return await self._client.aio.models.generate_content_stream(**kwargs)
|
|
256
|
+
else:
|
|
257
|
+
return await self._client.aio.models.generate_content(**kwargs)
|
|
270
258
|
|
|
271
259
|
def _chat_perform_args(
|
|
272
260
|
self,
|
|
273
|
-
stream: bool,
|
|
274
261
|
turns: list[Turn],
|
|
275
262
|
tools: dict[str, Tool],
|
|
276
263
|
data_model: Optional[type[BaseModel]] = None,
|
|
277
264
|
kwargs: Optional["SubmitInputArgs"] = None,
|
|
278
265
|
) -> "SubmitInputArgs":
|
|
266
|
+
from google.genai.types import FunctionDeclaration, GenerateContentConfig
|
|
267
|
+
from google.genai.types import Tool as GoogleTool
|
|
268
|
+
|
|
279
269
|
kwargs_full: "SubmitInputArgs" = {
|
|
280
|
-
"
|
|
281
|
-
"
|
|
282
|
-
"tools": self._gemini_tools(list(tools.values())) if tools else None,
|
|
270
|
+
"model": self._model,
|
|
271
|
+
"contents": cast("GoogleContent", self._google_contents(turns)),
|
|
283
272
|
**(kwargs or {}),
|
|
284
273
|
}
|
|
285
274
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
275
|
+
config = kwargs_full.get("config")
|
|
276
|
+
if config is None:
|
|
277
|
+
config = GenerateContentConfig()
|
|
278
|
+
if isinstance(config, dict):
|
|
279
|
+
config = GenerateContentConfig.model_construct(**config)
|
|
289
280
|
|
|
290
|
-
|
|
291
|
-
|
|
281
|
+
if config.system_instruction is None:
|
|
282
|
+
if len(turns) > 0 and turns[0].role == "system":
|
|
283
|
+
config.system_instruction = turns[0].text
|
|
292
284
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
285
|
+
if data_model:
|
|
286
|
+
config.response_schema = data_model
|
|
287
|
+
config.response_mime_type = "application/json"
|
|
288
|
+
|
|
289
|
+
if tools:
|
|
290
|
+
config.tools = [
|
|
291
|
+
GoogleTool(
|
|
292
|
+
function_declarations=[
|
|
293
|
+
FunctionDeclaration.from_callable(
|
|
294
|
+
client=self._client, callable=tool.func
|
|
295
|
+
)
|
|
296
|
+
for tool in tools.values()
|
|
297
|
+
]
|
|
298
|
+
)
|
|
299
|
+
]
|
|
300
300
|
|
|
301
|
-
|
|
301
|
+
kwargs_full["config"] = config
|
|
302
302
|
|
|
303
303
|
return kwargs_full
|
|
304
304
|
|
|
305
305
|
def stream_text(self, chunk) -> Optional[str]:
|
|
306
|
-
|
|
306
|
+
try:
|
|
307
|
+
# Errors if there is no text (e.g., tool request)
|
|
307
308
|
return chunk.text
|
|
308
|
-
|
|
309
|
+
except Exception:
|
|
310
|
+
return None
|
|
309
311
|
|
|
310
312
|
def stream_merge_chunks(self, completion, chunk):
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
stream.resolve()
|
|
318
|
-
return self._as_turn(
|
|
319
|
-
stream,
|
|
320
|
-
has_data_model,
|
|
313
|
+
chunkd = chunk.model_dump()
|
|
314
|
+
if completion is None:
|
|
315
|
+
return cast("GenerateContentResponseDict", chunkd)
|
|
316
|
+
return cast(
|
|
317
|
+
"GenerateContentResponseDict",
|
|
318
|
+
merge_dicts(completion, chunkd), # type: ignore
|
|
321
319
|
)
|
|
322
320
|
|
|
323
|
-
|
|
324
|
-
self, completion, has_data_model, stream: AsyncGenerateContentResponse
|
|
325
|
-
) -> Turn:
|
|
326
|
-
await stream.resolve()
|
|
321
|
+
def stream_turn(self, completion, has_data_model) -> Turn:
|
|
327
322
|
return self._as_turn(
|
|
328
|
-
|
|
323
|
+
completion,
|
|
329
324
|
has_data_model,
|
|
330
325
|
)
|
|
331
326
|
|
|
332
327
|
def value_turn(self, completion, has_data_model) -> Turn:
|
|
328
|
+
completion = cast("GenerateContentResponseDict", completion.model_dump())
|
|
333
329
|
return self._as_turn(completion, has_data_model)
|
|
334
330
|
|
|
335
|
-
def
|
|
336
|
-
|
|
331
|
+
def token_count(
|
|
332
|
+
self,
|
|
333
|
+
*args: Content | str,
|
|
334
|
+
tools: dict[str, Tool],
|
|
335
|
+
data_model: Optional[type[BaseModel]],
|
|
336
|
+
):
|
|
337
|
+
kwargs = self._token_count_args(
|
|
338
|
+
*args,
|
|
339
|
+
tools=tools,
|
|
340
|
+
data_model=data_model,
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
res = self._client.models.count_tokens(**kwargs)
|
|
344
|
+
return res.total_tokens or 0
|
|
345
|
+
|
|
346
|
+
async def token_count_async(
|
|
347
|
+
self,
|
|
348
|
+
*args: Content | str,
|
|
349
|
+
tools: dict[str, Tool],
|
|
350
|
+
data_model: Optional[type[BaseModel]],
|
|
351
|
+
):
|
|
352
|
+
kwargs = self._token_count_args(
|
|
353
|
+
*args,
|
|
354
|
+
tools=tools,
|
|
355
|
+
data_model=data_model,
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
res = await self._client.aio.models.count_tokens(**kwargs)
|
|
359
|
+
return res.total_tokens or 0
|
|
360
|
+
|
|
361
|
+
def _token_count_args(
|
|
362
|
+
self,
|
|
363
|
+
*args: Content | str,
|
|
364
|
+
tools: dict[str, Tool],
|
|
365
|
+
data_model: Optional[type[BaseModel]],
|
|
366
|
+
) -> dict[str, Any]:
|
|
367
|
+
turn = user_turn(*args)
|
|
368
|
+
|
|
369
|
+
kwargs = self._chat_perform_args(
|
|
370
|
+
turns=[turn],
|
|
371
|
+
tools=tools,
|
|
372
|
+
data_model=data_model,
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
args_to_keep = ["model", "contents", "tools"]
|
|
376
|
+
|
|
377
|
+
return {arg: kwargs[arg] for arg in args_to_keep if arg in kwargs}
|
|
378
|
+
|
|
379
|
+
def _google_contents(self, turns: list[Turn]) -> list["GoogleContent"]:
|
|
380
|
+
from google.genai.types import Content as GoogleContent
|
|
381
|
+
|
|
382
|
+
contents: list["GoogleContent"] = []
|
|
337
383
|
for turn in turns:
|
|
338
384
|
if turn.role == "system":
|
|
339
385
|
continue # System messages are handled separately
|
|
340
386
|
elif turn.role == "user":
|
|
341
387
|
parts = [self._as_part_type(c) for c in turn.contents]
|
|
342
|
-
contents.append(
|
|
388
|
+
contents.append(GoogleContent(role=turn.role, parts=parts))
|
|
343
389
|
elif turn.role == "assistant":
|
|
344
390
|
parts = [self._as_part_type(c) for c in turn.contents]
|
|
345
|
-
contents.append(
|
|
391
|
+
contents.append(GoogleContent(role="model", parts=parts))
|
|
346
392
|
else:
|
|
347
393
|
raise ValueError(f"Unknown role {turn.role}")
|
|
348
394
|
return contents
|
|
349
395
|
|
|
350
|
-
def _as_part_type(self, content: Content) -> "
|
|
351
|
-
from google.
|
|
396
|
+
def _as_part_type(self, content: Content) -> "Part":
|
|
397
|
+
from google.genai.types import FunctionCall, FunctionResponse, Part
|
|
352
398
|
|
|
353
399
|
if isinstance(content, ContentText):
|
|
354
|
-
return
|
|
400
|
+
return Part.from_text(text=content.text)
|
|
355
401
|
elif isinstance(content, ContentJson):
|
|
356
|
-
return
|
|
357
|
-
elif isinstance(content, ContentImageInline):
|
|
358
|
-
return
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
"data": content.data,
|
|
362
|
-
}
|
|
402
|
+
return Part.from_text(text="<structured data/>")
|
|
403
|
+
elif isinstance(content, ContentImageInline) and content.data:
|
|
404
|
+
return Part.from_bytes(
|
|
405
|
+
data=base64.b64decode(content.data),
|
|
406
|
+
mime_type=content.content_type,
|
|
363
407
|
)
|
|
364
408
|
elif isinstance(content, ContentImageRemote):
|
|
365
409
|
raise NotImplementedError(
|
|
@@ -367,90 +411,197 @@ class GoogleProvider(
|
|
|
367
411
|
"Consider downloading the image and using content_image_file() instead."
|
|
368
412
|
)
|
|
369
413
|
elif isinstance(content, ContentToolRequest):
|
|
370
|
-
return
|
|
371
|
-
function_call=
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
414
|
+
return Part(
|
|
415
|
+
function_call=FunctionCall(
|
|
416
|
+
id=content.id,
|
|
417
|
+
name=content.name,
|
|
418
|
+
# Goes in a dict, so should come out as a dict
|
|
419
|
+
args=cast(dict[str, Any], content.arguments),
|
|
420
|
+
)
|
|
375
421
|
)
|
|
376
422
|
elif isinstance(content, ContentToolResult):
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
423
|
+
if content.error:
|
|
424
|
+
resp = {"error": content.error}
|
|
425
|
+
else:
|
|
426
|
+
resp = {"result": str(content.value)}
|
|
427
|
+
return Part(
|
|
428
|
+
# TODO: seems function response parts might need role='tool'???
|
|
429
|
+
# https://github.com/googleapis/python-genai/blame/c8cfef85c/README.md#L344
|
|
430
|
+
function_response=FunctionResponse(
|
|
431
|
+
id=content.id,
|
|
432
|
+
name=content.name,
|
|
433
|
+
response=resp,
|
|
434
|
+
)
|
|
382
435
|
)
|
|
383
436
|
raise ValueError(f"Unknown content type: {type(content)}")
|
|
384
437
|
|
|
385
438
|
def _as_turn(
|
|
386
439
|
self,
|
|
387
|
-
message: "
|
|
440
|
+
message: "GenerateContentResponseDict",
|
|
388
441
|
has_data_model: bool,
|
|
389
442
|
) -> Turn:
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
443
|
+
from google.genai.types import FinishReason
|
|
444
|
+
|
|
445
|
+
candidates = message.get("candidates")
|
|
446
|
+
if not candidates:
|
|
447
|
+
return Turn("assistant", "")
|
|
448
|
+
|
|
449
|
+
parts: list["PartDict"] = []
|
|
450
|
+
finish_reason = None
|
|
451
|
+
for candidate in candidates:
|
|
452
|
+
content = candidate.get("content")
|
|
453
|
+
if content:
|
|
454
|
+
parts.extend(content.get("parts") or {})
|
|
455
|
+
finish = candidate.get("finish_reason")
|
|
456
|
+
if finish:
|
|
457
|
+
finish_reason = finish
|
|
458
|
+
|
|
459
|
+
contents: list[Content] = []
|
|
460
|
+
for part in parts:
|
|
461
|
+
text = part.get("text")
|
|
462
|
+
if text:
|
|
396
463
|
if has_data_model:
|
|
397
|
-
contents.append(ContentJson(json.loads(
|
|
464
|
+
contents.append(ContentJson(json.loads(text)))
|
|
398
465
|
else:
|
|
399
|
-
contents.append(ContentText(
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
466
|
+
contents.append(ContentText(text))
|
|
467
|
+
function_call = part.get("function_call")
|
|
468
|
+
if function_call:
|
|
469
|
+
# Seems name is required but id is optional?
|
|
470
|
+
name = function_call.get("name")
|
|
471
|
+
if name:
|
|
472
|
+
contents.append(
|
|
473
|
+
ContentToolRequest(
|
|
474
|
+
id=function_call.get("id") or name,
|
|
475
|
+
name=name,
|
|
476
|
+
arguments=function_call.get("args"),
|
|
477
|
+
)
|
|
407
478
|
)
|
|
408
|
-
|
|
409
|
-
if
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
479
|
+
function_response = part.get("function_response")
|
|
480
|
+
if function_response:
|
|
481
|
+
# Seems name is required but id is optional?
|
|
482
|
+
name = function_response.get("name")
|
|
483
|
+
if name:
|
|
484
|
+
contents.append(
|
|
485
|
+
ContentToolResult(
|
|
486
|
+
id=function_response.get("id") or name,
|
|
487
|
+
value=function_response.get("response"),
|
|
488
|
+
name=name,
|
|
489
|
+
)
|
|
415
490
|
)
|
|
416
|
-
)
|
|
417
491
|
|
|
418
|
-
usage = message.usage_metadata
|
|
419
|
-
tokens = (
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
492
|
+
usage = message.get("usage_metadata")
|
|
493
|
+
tokens = (0, 0)
|
|
494
|
+
if usage:
|
|
495
|
+
tokens = (
|
|
496
|
+
usage.get("prompt_token_count") or 0,
|
|
497
|
+
usage.get("candidates_token_count") or 0,
|
|
498
|
+
)
|
|
423
499
|
|
|
424
|
-
|
|
500
|
+
tokens_log(self, tokens)
|
|
501
|
+
|
|
502
|
+
if isinstance(finish_reason, FinishReason):
|
|
503
|
+
finish_reason = finish_reason.name
|
|
425
504
|
|
|
426
505
|
return Turn(
|
|
427
506
|
"assistant",
|
|
428
507
|
contents,
|
|
429
508
|
tokens=tokens,
|
|
430
|
-
finish_reason=
|
|
509
|
+
finish_reason=finish_reason,
|
|
431
510
|
completion=message,
|
|
432
511
|
)
|
|
433
512
|
|
|
434
|
-
def _gemini_tools(self, tools: list[Tool]) -> list["FunctionDeclaration"]:
|
|
435
|
-
from google.generativeai.types.content_types import FunctionDeclaration
|
|
436
|
-
|
|
437
|
-
res: list["FunctionDeclaration"] = []
|
|
438
|
-
for tool in tools:
|
|
439
|
-
fn = tool.schema["function"]
|
|
440
|
-
params = None
|
|
441
|
-
if "parameters" in fn and fn["parameters"]["properties"]:
|
|
442
|
-
params = {
|
|
443
|
-
"type": "object",
|
|
444
|
-
"properties": fn["parameters"]["properties"],
|
|
445
|
-
"required": fn["parameters"]["required"],
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
res.append(
|
|
449
|
-
FunctionDeclaration(
|
|
450
|
-
name=fn["name"],
|
|
451
|
-
description=fn.get("description", ""),
|
|
452
|
-
parameters=params,
|
|
453
|
-
)
|
|
454
|
-
)
|
|
455
513
|
|
|
456
|
-
|
|
514
|
+
def ChatVertex(
|
|
515
|
+
*,
|
|
516
|
+
model: Optional[str] = None,
|
|
517
|
+
project: Optional[str] = None,
|
|
518
|
+
location: Optional[str] = None,
|
|
519
|
+
api_key: Optional[str] = None,
|
|
520
|
+
system_prompt: Optional[str] = None,
|
|
521
|
+
turns: Optional[list[Turn]] = None,
|
|
522
|
+
kwargs: Optional["ChatClientArgs"] = None,
|
|
523
|
+
) -> Chat["SubmitInputArgs", GenerateContentResponse]:
|
|
524
|
+
"""
|
|
525
|
+
Chat with a Google Vertex AI model.
|
|
526
|
+
|
|
527
|
+
Prerequisites
|
|
528
|
+
-------------
|
|
529
|
+
|
|
530
|
+
::: {.callout-note}
|
|
531
|
+
## Python requirements
|
|
532
|
+
|
|
533
|
+
`ChatGoogle` requires the `google-genai` package
|
|
534
|
+
(e.g., `pip install google-genai`).
|
|
535
|
+
:::
|
|
536
|
+
|
|
537
|
+
::: {.callout-note}
|
|
538
|
+
## Credentials
|
|
539
|
+
|
|
540
|
+
To use Google's models (i.e., Vertex AI), you'll need to sign up for an account
|
|
541
|
+
with [Vertex AI](https://cloud.google.com/vertex-ai), then specify the appropriate
|
|
542
|
+
model, project, and location.
|
|
543
|
+
:::
|
|
544
|
+
|
|
545
|
+
Parameters
|
|
546
|
+
----------
|
|
547
|
+
model
|
|
548
|
+
The model to use for the chat. The default, None, will pick a reasonable
|
|
549
|
+
default, and warn you about it. We strongly recommend explicitly choosing
|
|
550
|
+
a model for all but the most casual use.
|
|
551
|
+
project
|
|
552
|
+
The Google Cloud project ID (e.g., "your-project-id"). If not provided, the
|
|
553
|
+
GOOGLE_CLOUD_PROJECT environment variable will be used.
|
|
554
|
+
location
|
|
555
|
+
The Google Cloud location (e.g., "us-central1"). If not provided, the
|
|
556
|
+
GOOGLE_CLOUD_LOCATION environment variable will be used.
|
|
557
|
+
system_prompt
|
|
558
|
+
A system prompt to set the behavior of the assistant.
|
|
559
|
+
turns
|
|
560
|
+
A list of turns to start the chat with (i.e., continuing a previous
|
|
561
|
+
conversation). If not provided, the conversation begins from scratch.
|
|
562
|
+
Do not provide non-`None` values for both `turns` and `system_prompt`.
|
|
563
|
+
Each message in the list should be a dictionary with at least `role`
|
|
564
|
+
(usually `system`, `user`, or `assistant`, but `tool` is also possible).
|
|
565
|
+
Normally there is also a `content` field, which is a string.
|
|
566
|
+
|
|
567
|
+
Returns
|
|
568
|
+
-------
|
|
569
|
+
Chat
|
|
570
|
+
A Chat object.
|
|
571
|
+
|
|
572
|
+
Examples
|
|
573
|
+
--------
|
|
574
|
+
|
|
575
|
+
```python
|
|
576
|
+
import os
|
|
577
|
+
from chatlas import ChatVertex
|
|
578
|
+
|
|
579
|
+
chat = ChatVertex(
|
|
580
|
+
project="your-project-id",
|
|
581
|
+
location="us-central1",
|
|
582
|
+
)
|
|
583
|
+
chat.chat("What is the capital of France?")
|
|
584
|
+
```
|
|
585
|
+
"""
|
|
586
|
+
|
|
587
|
+
if kwargs is None:
|
|
588
|
+
kwargs = {}
|
|
589
|
+
|
|
590
|
+
kwargs["vertexai"] = True
|
|
591
|
+
kwargs["project"] = project
|
|
592
|
+
kwargs["location"] = location
|
|
593
|
+
|
|
594
|
+
if model is None:
|
|
595
|
+
model = log_model_default("gemini-2.0-flash")
|
|
596
|
+
|
|
597
|
+
return Chat(
|
|
598
|
+
provider=GoogleProvider(
|
|
599
|
+
model=model,
|
|
600
|
+
api_key=api_key,
|
|
601
|
+
kwargs=kwargs,
|
|
602
|
+
),
|
|
603
|
+
turns=normalize_turns(
|
|
604
|
+
turns or [],
|
|
605
|
+
system_prompt=system_prompt,
|
|
606
|
+
),
|
|
607
|
+
)
|
chatlas/_merge.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# Adapted from https://github.com/langchain-ai/langchain/blob/master/libs/core/langchain_core/utils/_merge.py
|
|
2
|
-
# Also tweaked to more closely match https://github.com/hadley/
|
|
2
|
+
# Also tweaked to more closely match https://github.com/hadley/ellmer/blob/main/R/utils-merge.R
|
|
3
3
|
|
|
4
4
|
from __future__ import annotations
|
|
5
5
|
|
chatlas/_ollama.py
CHANGED
|
@@ -48,6 +48,13 @@ def ChatOllama(
|
|
|
48
48
|
(e.g. `ollama pull llama3.2`).
|
|
49
49
|
:::
|
|
50
50
|
|
|
51
|
+
::: {.callout-note}
|
|
52
|
+
## Python requirements
|
|
53
|
+
|
|
54
|
+
`ChatOllama` requires the `openai` package (e.g., `pip install openai`).
|
|
55
|
+
:::
|
|
56
|
+
|
|
57
|
+
|
|
51
58
|
Examples
|
|
52
59
|
--------
|
|
53
60
|
|
|
@@ -103,6 +110,7 @@ def ChatOllama(
|
|
|
103
110
|
|
|
104
111
|
return ChatOpenAI(
|
|
105
112
|
system_prompt=system_prompt,
|
|
113
|
+
api_key="ollama", # ignored
|
|
106
114
|
turns=turns,
|
|
107
115
|
base_url=f"{base_url}/v1",
|
|
108
116
|
model=model,
|