chatlas 0.2.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/_github.py ADDED
@@ -0,0 +1,147 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from typing import TYPE_CHECKING, Optional
5
+
6
+ from ._chat import Chat
7
+ from ._logging import log_model_default
8
+ from ._openai import ChatOpenAI
9
+ from ._turn import Turn
10
+ from ._utils import MISSING, MISSING_TYPE
11
+
12
+ if TYPE_CHECKING:
13
+ from ._openai import ChatCompletion
14
+ from .types.openai import ChatClientArgs, SubmitInputArgs
15
+
16
+
17
+ def ChatGithub(
18
+ *,
19
+ system_prompt: Optional[str] = None,
20
+ turns: Optional[list[Turn]] = None,
21
+ model: Optional[str] = None,
22
+ api_key: Optional[str] = None,
23
+ base_url: str = "https://models.inference.ai.azure.com/",
24
+ seed: Optional[int] | MISSING_TYPE = MISSING,
25
+ kwargs: Optional["ChatClientArgs"] = None,
26
+ ) -> Chat["SubmitInputArgs", ChatCompletion]:
27
+ """
28
+ Chat with a model hosted on the GitHub model marketplace.
29
+
30
+ GitHub (via Azure) hosts a wide variety of open source models, some of
31
+ which are fined tuned for specific tasks.
32
+
33
+ Prerequisites
34
+ -------------
35
+
36
+ ::: {.callout-note}
37
+ ## API key
38
+
39
+ Sign up at <https://github.com/marketplace/models> to get an API key.
40
+ You may need to apply for and be accepted into a beta access program.
41
+ :::
42
+
43
+ ::: {.callout-note}
44
+ ## Python requirements
45
+
46
+ `ChatGithub` requires the `openai` package (e.g., `pip install openai`).
47
+ :::
48
+
49
+
50
+ Examples
51
+ --------
52
+
53
+ ```python
54
+ import os
55
+ from chatlas import ChatGithub
56
+
57
+ chat = ChatGithub(api_key=os.getenv("GITHUB_PAT"))
58
+ chat.chat("What is the capital of France?")
59
+ ```
60
+
61
+ Parameters
62
+ ----------
63
+ system_prompt
64
+ A system prompt to set the behavior of the assistant.
65
+ turns
66
+ A list of turns to start the chat with (i.e., continuing a previous
67
+ conversation). If not provided, the conversation begins from scratch. Do
68
+ not provide non-`None` values for both `turns` and `system_prompt`. Each
69
+ message in the list should be a dictionary with at least `role` (usually
70
+ `system`, `user`, or `assistant`, but `tool` is also possible). Normally
71
+ there is also a `content` field, which is a string.
72
+ model
73
+ The model to use for the chat. The default, None, will pick a reasonable
74
+ default, and warn you about it. We strongly recommend explicitly
75
+ choosing a model for all but the most casual use.
76
+ api_key
77
+ The API key to use for authentication. You generally should not supply
78
+ this directly, but instead set the `GITHUB_PAT` environment variable.
79
+ base_url
80
+ The base URL to the endpoint; the default uses Github's API.
81
+ seed
82
+ Optional integer seed that ChatGPT uses to try and make output more
83
+ reproducible.
84
+ kwargs
85
+ Additional arguments to pass to the `openai.OpenAI()` client
86
+ constructor.
87
+
88
+ Returns
89
+ -------
90
+ Chat
91
+ A chat object that retains the state of the conversation.
92
+
93
+ Note
94
+ ----
95
+ This function is a lightweight wrapper around [](`~chatlas.ChatOpenAI`) with
96
+ the defaults tweaked for the GitHub model marketplace.
97
+
98
+ Note
99
+ ----
100
+ Pasting an API key into a chat constructor (e.g., `ChatGithub(api_key="...")`)
101
+ is the simplest way to get started, and is fine for interactive use, but is
102
+ problematic for code that may be shared with others.
103
+
104
+ Instead, consider using environment variables or a configuration file to manage
105
+ your credentials. One popular way to manage credentials is to use a `.env` file
106
+ to store your credentials, and then use the `python-dotenv` package to load them
107
+ into your environment.
108
+
109
+ ```shell
110
+ pip install python-dotenv
111
+ ```
112
+
113
+ ```shell
114
+ # .env
115
+ GITHUB_PAT=...
116
+ ```
117
+
118
+ ```python
119
+ from chatlas import ChatGithub
120
+ from dotenv import load_dotenv
121
+
122
+ load_dotenv()
123
+ chat = ChatGithub()
124
+ chat.console()
125
+ ```
126
+
127
+ Another, more general, solution is to load your environment variables into the shell
128
+ before starting Python (maybe in a `.bashrc`, `.zshrc`, etc. file):
129
+
130
+ ```shell
131
+ export GITHUB_PAT=...
132
+ ```
133
+ """
134
+ if model is None:
135
+ model = log_model_default("gpt-4o")
136
+ if api_key is None:
137
+ api_key = os.getenv("GITHUB_PAT")
138
+
139
+ return ChatOpenAI(
140
+ system_prompt=system_prompt,
141
+ turns=turns,
142
+ model=model,
143
+ api_key=api_key,
144
+ base_url=base_url,
145
+ seed=seed,
146
+ kwargs=kwargs,
147
+ )
chatlas/_google.py ADDED
@@ -0,0 +1,456 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from typing import TYPE_CHECKING, Any, Literal, Optional, overload
5
+
6
+ from pydantic import BaseModel
7
+
8
+ from ._chat import Chat
9
+ from ._content import (
10
+ Content,
11
+ ContentImageInline,
12
+ ContentImageRemote,
13
+ ContentJson,
14
+ ContentText,
15
+ ContentToolRequest,
16
+ ContentToolResult,
17
+ )
18
+ from ._logging import log_model_default
19
+ from ._provider import Provider
20
+ from ._tools import Tool, basemodel_to_param_schema
21
+ from ._turn import Turn, normalize_turns
22
+
23
+ if TYPE_CHECKING:
24
+ from google.generativeai.types.content_types import (
25
+ ContentDict,
26
+ FunctionDeclaration,
27
+ PartType,
28
+ )
29
+ from google.generativeai.types.generation_types import (
30
+ AsyncGenerateContentResponse,
31
+ GenerateContentResponse,
32
+ GenerationConfig,
33
+ )
34
+
35
+ from .types.google import ChatClientArgs, SubmitInputArgs
36
+ else:
37
+ GenerateContentResponse = object
38
+
39
+
40
+ def ChatGoogle(
41
+ *,
42
+ system_prompt: Optional[str] = None,
43
+ turns: Optional[list[Turn]] = None,
44
+ model: Optional[str] = None,
45
+ api_key: Optional[str] = None,
46
+ kwargs: Optional["ChatClientArgs"] = None,
47
+ ) -> Chat["SubmitInputArgs", GenerateContentResponse]:
48
+ """
49
+ Chat with a Google Gemini model.
50
+
51
+ Prerequisites
52
+ -------------
53
+
54
+ ::: {.callout-note}
55
+ ## API key
56
+
57
+ To use Google's models (i.e., Gemini), you'll need to sign up for an account
58
+ and [get an API key](https://ai.google.dev/gemini-api/docs/get-started/tutorial?lang=python).
59
+ :::
60
+
61
+ ::: {.callout-note}
62
+ ## Python requirements
63
+
64
+ `ChatGoogle` requires the `google-generativeai` package
65
+ (e.g., `pip install google-generativeai`).
66
+ :::
67
+
68
+ Examples
69
+ --------
70
+
71
+ ```python
72
+ import os
73
+ from chatlas import ChatGoogle
74
+
75
+ chat = ChatGoogle(api_key=os.getenv("GOOGLE_API_KEY"))
76
+ chat.chat("What is the capital of France?")
77
+ ```
78
+
79
+ Parameters
80
+ ----------
81
+ system_prompt
82
+ A system prompt to set the behavior of the assistant.
83
+ turns
84
+ A list of turns to start the chat with (i.e., continuing a previous
85
+ conversation). If not provided, the conversation begins from scratch.
86
+ Do not provide non-`None` values for both `turns` and `system_prompt`.
87
+ Each message in the list should be a dictionary with at least `role`
88
+ (usually `system`, `user`, or `assistant`, but `tool` is also possible).
89
+ Normally there is also a `content` field, which is a string.
90
+ model
91
+ The model to use for the chat. The default, None, will pick a reasonable
92
+ default, and warn you about it. We strongly recommend explicitly choosing
93
+ a model for all but the most casual use.
94
+ api_key
95
+ The API key to use for authentication. You generally should not supply
96
+ this directly, but instead set the `GOOGLE_API_KEY` environment variable.
97
+ kwargs
98
+ Additional arguments to pass to the `genai.GenerativeModel` constructor.
99
+
100
+ Returns
101
+ -------
102
+ Chat
103
+ A Chat object.
104
+
105
+ Limitations
106
+ -----------
107
+ `ChatGoogle` currently doesn't work with streaming tools.
108
+
109
+ Note
110
+ ----
111
+ Pasting an API key into a chat constructor (e.g., `ChatGoogle(api_key="...")`)
112
+ is the simplest way to get started, and is fine for interactive use, but is
113
+ problematic for code that may be shared with others.
114
+
115
+ Instead, consider using environment variables or a configuration file to manage
116
+ your credentials. One popular way to manage credentials is to use a `.env` file
117
+ to store your credentials, and then use the `python-dotenv` package to load them
118
+ into your environment.
119
+
120
+ ```shell
121
+ pip install python-dotenv
122
+ ```
123
+
124
+ ```shell
125
+ # .env
126
+ GOOGLE_API_KEY=...
127
+ ```
128
+
129
+ ```python
130
+ from chatlas import ChatGoogle
131
+ from dotenv import load_dotenv
132
+
133
+ load_dotenv()
134
+ chat = ChatGoogle()
135
+ chat.console()
136
+ ```
137
+
138
+ Another, more general, solution is to load your environment variables into the shell
139
+ before starting Python (maybe in a `.bashrc`, `.zshrc`, etc. file):
140
+
141
+ ```shell
142
+ export GOOGLE_API_KEY=...
143
+ ```
144
+ """
145
+
146
+ if model is None:
147
+ model = log_model_default("gemini-1.5-flash")
148
+
149
+ turns = normalize_turns(
150
+ turns or [],
151
+ system_prompt=system_prompt,
152
+ )
153
+
154
+ return Chat(
155
+ provider=GoogleProvider(
156
+ turns=turns,
157
+ model=model,
158
+ api_key=api_key,
159
+ kwargs=kwargs,
160
+ ),
161
+ turns=turns,
162
+ )
163
+
164
+
165
+ # The dictionary form of ChatCompletion (TODO: stronger typing)?
166
+ GenerateContentDict = dict[str, Any]
167
+
168
+
169
+ class GoogleProvider(
170
+ Provider[GenerateContentResponse, GenerateContentResponse, GenerateContentDict]
171
+ ):
172
+ def __init__(
173
+ self,
174
+ *,
175
+ turns: list[Turn],
176
+ model: str,
177
+ api_key: str | None,
178
+ kwargs: Optional["ChatClientArgs"],
179
+ ):
180
+ try:
181
+ from google.generativeai import GenerativeModel
182
+ except ImportError:
183
+ raise ImportError(
184
+ f"The {self.__class__.__name__} class requires the `google-generativeai` package. "
185
+ "Install it with `pip install google-generativeai`."
186
+ )
187
+
188
+ if api_key is not None:
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
196
+
197
+ kwargs_full: "ChatClientArgs" = {
198
+ "model_name": model,
199
+ "system_instruction": system_prompt,
200
+ **(kwargs or {}),
201
+ }
202
+
203
+ self._client = GenerativeModel(**kwargs_full)
204
+
205
+ @overload
206
+ def chat_perform(
207
+ self,
208
+ *,
209
+ stream: Literal[False],
210
+ turns: list[Turn],
211
+ tools: dict[str, Tool],
212
+ data_model: Optional[type[BaseModel]] = None,
213
+ kwargs: Optional["SubmitInputArgs"] = None,
214
+ ): ...
215
+
216
+ @overload
217
+ def chat_perform(
218
+ self,
219
+ *,
220
+ stream: Literal[True],
221
+ turns: list[Turn],
222
+ tools: dict[str, Tool],
223
+ data_model: Optional[type[BaseModel]] = None,
224
+ kwargs: Optional["SubmitInputArgs"] = None,
225
+ ): ...
226
+
227
+ def chat_perform(
228
+ self,
229
+ stream: bool,
230
+ turns: list[Turn],
231
+ tools: dict[str, Tool],
232
+ data_model: Optional[type[BaseModel]] = None,
233
+ kwargs: Optional["SubmitInputArgs"] = None,
234
+ ):
235
+ kwargs = self._chat_perform_args(stream, turns, tools, data_model, kwargs)
236
+ return self._client.generate_content(**kwargs)
237
+
238
+ @overload
239
+ async def chat_perform_async(
240
+ self,
241
+ *,
242
+ stream: Literal[False],
243
+ turns: list[Turn],
244
+ tools: dict[str, Tool],
245
+ data_model: Optional[type[BaseModel]] = None,
246
+ kwargs: Optional["SubmitInputArgs"] = None,
247
+ ): ...
248
+
249
+ @overload
250
+ async def chat_perform_async(
251
+ self,
252
+ *,
253
+ stream: Literal[True],
254
+ turns: list[Turn],
255
+ tools: dict[str, Tool],
256
+ data_model: Optional[type[BaseModel]] = None,
257
+ kwargs: Optional["SubmitInputArgs"] = None,
258
+ ): ...
259
+
260
+ async def chat_perform_async(
261
+ self,
262
+ stream: bool,
263
+ turns: list[Turn],
264
+ tools: dict[str, Tool],
265
+ data_model: Optional[type[BaseModel]] = None,
266
+ kwargs: Optional["SubmitInputArgs"] = None,
267
+ ):
268
+ kwargs = self._chat_perform_args(stream, turns, tools, data_model, kwargs)
269
+ return await self._client.generate_content_async(**kwargs)
270
+
271
+ def _chat_perform_args(
272
+ self,
273
+ stream: bool,
274
+ turns: list[Turn],
275
+ tools: dict[str, Tool],
276
+ data_model: Optional[type[BaseModel]] = None,
277
+ kwargs: Optional["SubmitInputArgs"] = None,
278
+ ) -> "SubmitInputArgs":
279
+ kwargs_full: "SubmitInputArgs" = {
280
+ "contents": self._google_contents(turns),
281
+ "stream": stream,
282
+ "tools": self._gemini_tools(list(tools.values())) if tools else None,
283
+ **(kwargs or {}),
284
+ }
285
+
286
+ if data_model:
287
+ config = kwargs_full.get("generation_config", {})
288
+ params = basemodel_to_param_schema(data_model)
289
+
290
+ if "additionalProperties" in params:
291
+ del params["additionalProperties"]
292
+
293
+ mime_type = "application/json"
294
+ if isinstance(config, dict):
295
+ config["response_schema"] = params
296
+ config["response_mime_type"] = mime_type
297
+ elif isinstance(config, GenerationConfig):
298
+ config.response_schema = params
299
+ config.response_mime_type = mime_type
300
+
301
+ kwargs_full["generation_config"] = config
302
+
303
+ return kwargs_full
304
+
305
+ def stream_text(self, chunk) -> Optional[str]:
306
+ if chunk.parts:
307
+ return chunk.text
308
+ return None
309
+
310
+ def stream_merge_chunks(self, completion, chunk):
311
+ # The .resolve() in .stream_turn() does the merging for us
312
+ return {}
313
+
314
+ def stream_turn(
315
+ self, completion, has_data_model, stream: GenerateContentResponse
316
+ ) -> Turn:
317
+ stream.resolve()
318
+ return self._as_turn(
319
+ stream,
320
+ has_data_model,
321
+ )
322
+
323
+ async def stream_turn_async(
324
+ self, completion, has_data_model, stream: AsyncGenerateContentResponse
325
+ ) -> Turn:
326
+ await stream.resolve()
327
+ return self._as_turn(
328
+ stream,
329
+ has_data_model,
330
+ )
331
+
332
+ def value_turn(self, completion, has_data_model) -> Turn:
333
+ return self._as_turn(completion, has_data_model)
334
+
335
+ def _google_contents(self, turns: list[Turn]) -> list["ContentDict"]:
336
+ contents: list["ContentDict"] = []
337
+ for turn in turns:
338
+ if turn.role == "system":
339
+ continue # System messages are handled separately
340
+ elif turn.role == "user":
341
+ parts = [self._as_part_type(c) for c in turn.contents]
342
+ contents.append({"role": turn.role, "parts": parts})
343
+ elif turn.role == "assistant":
344
+ parts = [self._as_part_type(c) for c in turn.contents]
345
+ contents.append({"role": "model", "parts": parts})
346
+ else:
347
+ raise ValueError(f"Unknown role {turn.role}")
348
+ return contents
349
+
350
+ def _as_part_type(self, content: Content) -> "PartType":
351
+ from google.generativeai.types.content_types import protos
352
+
353
+ if isinstance(content, ContentText):
354
+ return protos.Part(text=content.text)
355
+ elif isinstance(content, ContentJson):
356
+ return protos.Part(text="<structured data/>")
357
+ elif isinstance(content, ContentImageInline):
358
+ return protos.Part(
359
+ inline_data={
360
+ "mime_type": content.content_type,
361
+ "data": content.data,
362
+ }
363
+ )
364
+ elif isinstance(content, ContentImageRemote):
365
+ raise NotImplementedError(
366
+ "Remote images aren't supported by Google (Gemini). "
367
+ "Consider downloading the image and using content_image_file() instead."
368
+ )
369
+ elif isinstance(content, ContentToolRequest):
370
+ return protos.Part(
371
+ function_call={
372
+ "name": content.id,
373
+ "args": content.arguments,
374
+ }
375
+ )
376
+ elif isinstance(content, ContentToolResult):
377
+ return protos.Part(
378
+ function_response={
379
+ "name": content.id,
380
+ "response": {"value": content.get_final_value()},
381
+ }
382
+ )
383
+ raise ValueError(f"Unknown content type: {type(content)}")
384
+
385
+ def _as_turn(
386
+ self,
387
+ message: "GenerateContentResponse | AsyncGenerateContentResponse",
388
+ has_data_model: bool,
389
+ ) -> Turn:
390
+ contents = []
391
+
392
+ msg = message.candidates[0].content
393
+
394
+ for part in msg.parts:
395
+ if part.text:
396
+ if has_data_model:
397
+ contents.append(ContentJson(json.loads(part.text)))
398
+ else:
399
+ contents.append(ContentText(part.text))
400
+ if part.function_call:
401
+ func = part.function_call
402
+ contents.append(
403
+ ContentToolRequest(
404
+ func.name,
405
+ name=func.name,
406
+ arguments=dict(func.args),
407
+ )
408
+ )
409
+ if part.function_response:
410
+ func = part.function_response
411
+ contents.append(
412
+ ContentToolResult(
413
+ func.name,
414
+ value=func.response,
415
+ )
416
+ )
417
+
418
+ usage = message.usage_metadata
419
+ tokens = (
420
+ usage.prompt_token_count,
421
+ usage.candidates_token_count,
422
+ )
423
+
424
+ finish = message.candidates[0].finish_reason
425
+
426
+ return Turn(
427
+ "assistant",
428
+ contents,
429
+ tokens=tokens,
430
+ finish_reason=finish.name,
431
+ completion=message,
432
+ )
433
+
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
+
456
+ return res