chatlas 0.5.0__py3-none-any.whl → 0.6.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 +3 -0
- chatlas/_anthropic.py +17 -4
- chatlas/_chat.py +6 -6
- chatlas/_content.py +103 -14
- chatlas/_content_image.py +5 -8
- chatlas/_content_pdf.py +94 -0
- chatlas/_google.py +13 -3
- chatlas/_openai.py +19 -4
- chatlas/_turn.py +30 -24
- chatlas/_version.py +2 -2
- chatlas/types/openai/_submit.py +4 -5
- {chatlas-0.5.0.dist-info → chatlas-0.6.0.dist-info}/METADATA +1 -1
- {chatlas-0.5.0.dist-info → chatlas-0.6.0.dist-info}/RECORD +14 -13
- {chatlas-0.5.0.dist-info → chatlas-0.6.0.dist-info}/WHEEL +0 -0
chatlas/__init__.py
CHANGED
|
@@ -3,6 +3,7 @@ from ._anthropic import ChatAnthropic, ChatBedrockAnthropic
|
|
|
3
3
|
from ._auto import ChatAuto
|
|
4
4
|
from ._chat import Chat
|
|
5
5
|
from ._content_image import content_image_file, content_image_plot, content_image_url
|
|
6
|
+
from ._content_pdf import content_pdf_file, content_pdf_url
|
|
6
7
|
from ._github import ChatGithub
|
|
7
8
|
from ._google import ChatGoogle, ChatVertex
|
|
8
9
|
from ._groq import ChatGroq
|
|
@@ -38,6 +39,8 @@ __all__ = (
|
|
|
38
39
|
"content_image_file",
|
|
39
40
|
"content_image_plot",
|
|
40
41
|
"content_image_url",
|
|
42
|
+
"content_pdf_file",
|
|
43
|
+
"content_pdf_url",
|
|
41
44
|
"interpolate",
|
|
42
45
|
"interpolate_file",
|
|
43
46
|
"Provider",
|
chatlas/_anthropic.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import base64
|
|
3
4
|
import json
|
|
4
5
|
import warnings
|
|
5
6
|
from typing import TYPE_CHECKING, Any, Literal, Optional, Union, cast, overload
|
|
@@ -12,6 +13,7 @@ from ._content import (
|
|
|
12
13
|
ContentImageInline,
|
|
13
14
|
ContentImageRemote,
|
|
14
15
|
ContentJson,
|
|
16
|
+
ContentPDF,
|
|
15
17
|
ContentText,
|
|
16
18
|
ContentToolRequest,
|
|
17
19
|
ContentToolResult,
|
|
@@ -31,6 +33,7 @@ if TYPE_CHECKING:
|
|
|
31
33
|
ToolParam,
|
|
32
34
|
ToolUseBlock,
|
|
33
35
|
)
|
|
36
|
+
from anthropic.types.document_block_param import DocumentBlockParam
|
|
34
37
|
from anthropic.types.image_block_param import ImageBlockParam
|
|
35
38
|
from anthropic.types.model_param import ModelParam
|
|
36
39
|
from anthropic.types.text_block_param import TextBlockParam
|
|
@@ -45,6 +48,7 @@ if TYPE_CHECKING:
|
|
|
45
48
|
ImageBlockParam,
|
|
46
49
|
ToolUseBlockParam,
|
|
47
50
|
ToolResultBlockParam,
|
|
51
|
+
DocumentBlockParam,
|
|
48
52
|
]
|
|
49
53
|
else:
|
|
50
54
|
Message = object
|
|
@@ -450,12 +454,21 @@ class AnthropicProvider(Provider[Message, RawMessageStreamEvent, Message]):
|
|
|
450
454
|
return {"text": content.text, "type": "text"}
|
|
451
455
|
elif isinstance(content, ContentJson):
|
|
452
456
|
return {"text": "<structured data/>", "type": "text"}
|
|
457
|
+
elif isinstance(content, ContentPDF):
|
|
458
|
+
return {
|
|
459
|
+
"type": "document",
|
|
460
|
+
"source": {
|
|
461
|
+
"type": "base64",
|
|
462
|
+
"media_type": "application/pdf",
|
|
463
|
+
"data": base64.b64encode(content.data).decode("utf-8"),
|
|
464
|
+
},
|
|
465
|
+
}
|
|
453
466
|
elif isinstance(content, ContentImageInline):
|
|
454
467
|
return {
|
|
455
468
|
"type": "image",
|
|
456
469
|
"source": {
|
|
457
470
|
"type": "base64",
|
|
458
|
-
"media_type": content.
|
|
471
|
+
"media_type": content.image_content_type,
|
|
459
472
|
"data": content.data or "",
|
|
460
473
|
},
|
|
461
474
|
}
|
|
@@ -504,7 +517,7 @@ class AnthropicProvider(Provider[Message, RawMessageStreamEvent, Message]):
|
|
|
504
517
|
contents = []
|
|
505
518
|
for content in completion.content:
|
|
506
519
|
if content.type == "text":
|
|
507
|
-
contents.append(ContentText(content.text))
|
|
520
|
+
contents.append(ContentText(text=content.text))
|
|
508
521
|
elif content.type == "tool_use":
|
|
509
522
|
if has_data_model and content.name == "_structured_tool_call":
|
|
510
523
|
if not isinstance(content.input, dict):
|
|
@@ -515,11 +528,11 @@ class AnthropicProvider(Provider[Message, RawMessageStreamEvent, Message]):
|
|
|
515
528
|
raise ValueError(
|
|
516
529
|
"Expected data extraction tool to return a 'data' field."
|
|
517
530
|
)
|
|
518
|
-
contents.append(ContentJson(content.input["data"]))
|
|
531
|
+
contents.append(ContentJson(value=content.input["data"]))
|
|
519
532
|
else:
|
|
520
533
|
contents.append(
|
|
521
534
|
ContentToolRequest(
|
|
522
|
-
content.id,
|
|
535
|
+
id=content.id,
|
|
523
536
|
name=content.name,
|
|
524
537
|
arguments=content.input,
|
|
525
538
|
)
|
chatlas/_chat.py
CHANGED
|
@@ -1248,7 +1248,7 @@ class Chat(Generic[SubmitInputArgsT, CompletionT]):
|
|
|
1248
1248
|
id_: str,
|
|
1249
1249
|
) -> ContentToolResult:
|
|
1250
1250
|
if func is None:
|
|
1251
|
-
return ContentToolResult(id_, value=None, error="Unknown tool")
|
|
1251
|
+
return ContentToolResult(id=id_, value=None, error="Unknown tool")
|
|
1252
1252
|
|
|
1253
1253
|
name = func.__name__
|
|
1254
1254
|
|
|
@@ -1258,10 +1258,10 @@ class Chat(Generic[SubmitInputArgsT, CompletionT]):
|
|
|
1258
1258
|
else:
|
|
1259
1259
|
result = func(arguments)
|
|
1260
1260
|
|
|
1261
|
-
return ContentToolResult(id_, value=result, error=None, name=name)
|
|
1261
|
+
return ContentToolResult(id=id_, value=result, error=None, name=name)
|
|
1262
1262
|
except Exception as e:
|
|
1263
1263
|
log_tool_error(name, str(arguments), e)
|
|
1264
|
-
return ContentToolResult(id_, value=None, error=str(e), name=name)
|
|
1264
|
+
return ContentToolResult(id=id_, value=None, error=str(e), name=name)
|
|
1265
1265
|
|
|
1266
1266
|
@staticmethod
|
|
1267
1267
|
async def _invoke_tool_async(
|
|
@@ -1270,7 +1270,7 @@ class Chat(Generic[SubmitInputArgsT, CompletionT]):
|
|
|
1270
1270
|
id_: str,
|
|
1271
1271
|
) -> ContentToolResult:
|
|
1272
1272
|
if func is None:
|
|
1273
|
-
return ContentToolResult(id_, value=None, error="Unknown tool")
|
|
1273
|
+
return ContentToolResult(id=id_, value=None, error="Unknown tool")
|
|
1274
1274
|
|
|
1275
1275
|
name = func.__name__
|
|
1276
1276
|
|
|
@@ -1280,10 +1280,10 @@ class Chat(Generic[SubmitInputArgsT, CompletionT]):
|
|
|
1280
1280
|
else:
|
|
1281
1281
|
result = await func(arguments)
|
|
1282
1282
|
|
|
1283
|
-
return ContentToolResult(id_, value=result, error=None, name=name)
|
|
1283
|
+
return ContentToolResult(id=id_, value=result, error=None, name=name)
|
|
1284
1284
|
except Exception as e:
|
|
1285
1285
|
log_tool_error(func.__name__, str(arguments), e)
|
|
1286
|
-
return ContentToolResult(id_, value=None, error=str(e), name=name)
|
|
1286
|
+
return ContentToolResult(id=id_, value=None, error=str(e), name=name)
|
|
1287
1287
|
|
|
1288
1288
|
def _markdown_display(
|
|
1289
1289
|
self, echo: Literal["text", "all", "none"]
|
chatlas/_content.py
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
|
-
from dataclasses import dataclass
|
|
5
4
|
from pprint import pformat
|
|
6
|
-
from typing import Any, Literal, Optional
|
|
5
|
+
from typing import Any, Literal, Optional, Union
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, ConfigDict
|
|
7
8
|
|
|
8
9
|
ImageContentTypes = Literal[
|
|
9
10
|
"image/png",
|
|
@@ -15,12 +16,28 @@ ImageContentTypes = Literal[
|
|
|
15
16
|
Allowable content types for images.
|
|
16
17
|
"""
|
|
17
18
|
|
|
19
|
+
ContentTypeEnum = Literal[
|
|
20
|
+
"text",
|
|
21
|
+
"image_remote",
|
|
22
|
+
"image_inline",
|
|
23
|
+
"tool_request",
|
|
24
|
+
"tool_result",
|
|
25
|
+
"json",
|
|
26
|
+
"pdf",
|
|
27
|
+
]
|
|
28
|
+
"""
|
|
29
|
+
A discriminated union of all content types.
|
|
30
|
+
"""
|
|
31
|
+
|
|
18
32
|
|
|
19
|
-
class Content:
|
|
33
|
+
class Content(BaseModel):
|
|
20
34
|
"""
|
|
21
35
|
Base class for all content types that can be appear in a [](`~chatlas.Turn`)
|
|
22
36
|
"""
|
|
23
37
|
|
|
38
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
39
|
+
content_type: ContentTypeEnum
|
|
40
|
+
|
|
24
41
|
def __str__(self):
|
|
25
42
|
raise NotImplementedError
|
|
26
43
|
|
|
@@ -31,13 +48,13 @@ class Content:
|
|
|
31
48
|
raise NotImplementedError
|
|
32
49
|
|
|
33
50
|
|
|
34
|
-
@dataclass
|
|
35
51
|
class ContentText(Content):
|
|
36
52
|
"""
|
|
37
53
|
Text content for a [](`~chatlas.Turn`)
|
|
38
54
|
"""
|
|
39
55
|
|
|
40
56
|
text: str
|
|
57
|
+
content_type: ContentTypeEnum = "text"
|
|
41
58
|
|
|
42
59
|
def __str__(self):
|
|
43
60
|
return self.text
|
|
@@ -62,7 +79,6 @@ class ContentImage(Content):
|
|
|
62
79
|
pass
|
|
63
80
|
|
|
64
81
|
|
|
65
|
-
@dataclass
|
|
66
82
|
class ContentImageRemote(ContentImage):
|
|
67
83
|
"""
|
|
68
84
|
Image content from a URL.
|
|
@@ -81,6 +97,8 @@ class ContentImageRemote(ContentImage):
|
|
|
81
97
|
url: str
|
|
82
98
|
detail: Literal["auto", "low", "high"] = "auto"
|
|
83
99
|
|
|
100
|
+
content_type: ContentTypeEnum = "image_remote"
|
|
101
|
+
|
|
84
102
|
def __str__(self):
|
|
85
103
|
return f""
|
|
86
104
|
|
|
@@ -94,7 +112,6 @@ class ContentImageRemote(ContentImage):
|
|
|
94
112
|
)
|
|
95
113
|
|
|
96
114
|
|
|
97
|
-
@dataclass
|
|
98
115
|
class ContentImageInline(ContentImage):
|
|
99
116
|
"""
|
|
100
117
|
Inline image content.
|
|
@@ -105,17 +122,19 @@ class ContentImageInline(ContentImage):
|
|
|
105
122
|
|
|
106
123
|
Parameters
|
|
107
124
|
----------
|
|
108
|
-
|
|
125
|
+
image_content_type
|
|
109
126
|
The content type of the image.
|
|
110
127
|
data
|
|
111
128
|
The base64-encoded image data.
|
|
112
129
|
"""
|
|
113
130
|
|
|
114
|
-
|
|
131
|
+
image_content_type: ImageContentTypes
|
|
115
132
|
data: Optional[str] = None
|
|
116
133
|
|
|
134
|
+
content_type: ContentTypeEnum = "image_inline"
|
|
135
|
+
|
|
117
136
|
def __str__(self):
|
|
118
|
-
return f""
|
|
119
138
|
|
|
120
139
|
def _repr_markdown_(self):
|
|
121
140
|
return self.__str__()
|
|
@@ -124,11 +143,10 @@ class ContentImageInline(ContentImage):
|
|
|
124
143
|
n_bytes = len(self.data) if self.data else 0
|
|
125
144
|
return (
|
|
126
145
|
" " * indent
|
|
127
|
-
+ f"<ContentImageInline content_type='{self.
|
|
146
|
+
+ f"<ContentImageInline content_type='{self.image_content_type}' size={n_bytes}>"
|
|
128
147
|
)
|
|
129
148
|
|
|
130
149
|
|
|
131
|
-
@dataclass
|
|
132
150
|
class ContentToolRequest(Content):
|
|
133
151
|
"""
|
|
134
152
|
A request to call a tool/function
|
|
@@ -151,6 +169,8 @@ class ContentToolRequest(Content):
|
|
|
151
169
|
name: str
|
|
152
170
|
arguments: object
|
|
153
171
|
|
|
172
|
+
content_type: ContentTypeEnum = "tool_request"
|
|
173
|
+
|
|
154
174
|
def __str__(self):
|
|
155
175
|
args_str = self._arguments_str()
|
|
156
176
|
func_call = f"{self.name}({args_str})"
|
|
@@ -173,7 +193,6 @@ class ContentToolRequest(Content):
|
|
|
173
193
|
return str(self.arguments)
|
|
174
194
|
|
|
175
195
|
|
|
176
|
-
@dataclass
|
|
177
196
|
class ContentToolResult(Content):
|
|
178
197
|
"""
|
|
179
198
|
The result of calling a tool/function
|
|
@@ -199,6 +218,8 @@ class ContentToolResult(Content):
|
|
|
199
218
|
name: Optional[str] = None
|
|
200
219
|
error: Optional[str] = None
|
|
201
220
|
|
|
221
|
+
content_type: ContentTypeEnum = "tool_result"
|
|
222
|
+
|
|
202
223
|
def _get_value(self, pretty: bool = False) -> str:
|
|
203
224
|
if self.error:
|
|
204
225
|
return f"Tool calling failed with error: '{self.error}'"
|
|
@@ -207,7 +228,7 @@ class ContentToolResult(Content):
|
|
|
207
228
|
try:
|
|
208
229
|
json_val = json.loads(self.value) # type: ignore
|
|
209
230
|
return pformat(json_val, indent=2, sort_dicts=False)
|
|
210
|
-
except: # noqa
|
|
231
|
+
except: # noqa
|
|
211
232
|
return str(self.value)
|
|
212
233
|
|
|
213
234
|
# Primarily used for `echo="all"`...
|
|
@@ -232,7 +253,6 @@ class ContentToolResult(Content):
|
|
|
232
253
|
return self._get_value()
|
|
233
254
|
|
|
234
255
|
|
|
235
|
-
@dataclass
|
|
236
256
|
class ContentJson(Content):
|
|
237
257
|
"""
|
|
238
258
|
JSON content
|
|
@@ -248,6 +268,8 @@ class ContentJson(Content):
|
|
|
248
268
|
|
|
249
269
|
value: dict[str, Any]
|
|
250
270
|
|
|
271
|
+
content_type: ContentTypeEnum = "json"
|
|
272
|
+
|
|
251
273
|
def __str__(self):
|
|
252
274
|
return json.dumps(self.value, indent=2)
|
|
253
275
|
|
|
@@ -256,3 +278,70 @@ class ContentJson(Content):
|
|
|
256
278
|
|
|
257
279
|
def __repr__(self, indent: int = 0):
|
|
258
280
|
return " " * indent + f"<ContentJson value={self.value}>"
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
class ContentPDF(Content):
|
|
284
|
+
"""
|
|
285
|
+
PDF content
|
|
286
|
+
|
|
287
|
+
This content type primarily exists to signal PDF data extraction
|
|
288
|
+
(i.e., data extracted via [](`~chatlas.Chat`)'s `.extract_data()` method)
|
|
289
|
+
|
|
290
|
+
Parameters
|
|
291
|
+
----------
|
|
292
|
+
value
|
|
293
|
+
The PDF data extracted
|
|
294
|
+
"""
|
|
295
|
+
|
|
296
|
+
data: bytes
|
|
297
|
+
|
|
298
|
+
content_type: ContentTypeEnum = "pdf"
|
|
299
|
+
|
|
300
|
+
def __str__(self):
|
|
301
|
+
return "<PDF document>"
|
|
302
|
+
|
|
303
|
+
def _repr_markdown_(self):
|
|
304
|
+
return self.__str__()
|
|
305
|
+
|
|
306
|
+
def __repr__(self, indent: int = 0):
|
|
307
|
+
return " " * indent + f"<ContentPDF size={len(self.data)}>"
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
ContentUnion = Union[
|
|
311
|
+
ContentText,
|
|
312
|
+
ContentImageRemote,
|
|
313
|
+
ContentImageInline,
|
|
314
|
+
ContentToolRequest,
|
|
315
|
+
ContentToolResult,
|
|
316
|
+
ContentJson,
|
|
317
|
+
ContentPDF,
|
|
318
|
+
]
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def create_content(data: dict[str, Any]) -> ContentUnion:
|
|
322
|
+
"""
|
|
323
|
+
Factory function to create the appropriate Content subclass based on the data.
|
|
324
|
+
|
|
325
|
+
This is useful when deserializing content from JSON.
|
|
326
|
+
"""
|
|
327
|
+
if not isinstance(data, dict):
|
|
328
|
+
raise ValueError("Content data must be a dictionary")
|
|
329
|
+
|
|
330
|
+
ct = data.get("content_type")
|
|
331
|
+
|
|
332
|
+
if ct == "text":
|
|
333
|
+
return ContentText.model_validate(data)
|
|
334
|
+
elif ct == "image_remote":
|
|
335
|
+
return ContentImageRemote.model_validate(data)
|
|
336
|
+
elif ct == "image_inline":
|
|
337
|
+
return ContentImageInline.model_validate(data)
|
|
338
|
+
elif ct == "tool_request":
|
|
339
|
+
return ContentToolRequest.model_validate(data)
|
|
340
|
+
elif ct == "tool_result":
|
|
341
|
+
return ContentToolResult.model_validate(data)
|
|
342
|
+
elif ct == "json":
|
|
343
|
+
return ContentJson.model_validate(data)
|
|
344
|
+
elif ct == "pdf":
|
|
345
|
+
return ContentPDF.model_validate(data)
|
|
346
|
+
else:
|
|
347
|
+
raise ValueError(f"Unknown content type: {ct}")
|
chatlas/_content_image.py
CHANGED
|
@@ -8,6 +8,7 @@ import warnings
|
|
|
8
8
|
from typing import Literal, Union, cast
|
|
9
9
|
|
|
10
10
|
from ._content import ContentImageInline, ContentImageRemote, ImageContentTypes
|
|
11
|
+
from ._content_pdf import parse_data_url
|
|
11
12
|
from ._utils import MISSING, MISSING_TYPE
|
|
12
13
|
|
|
13
14
|
__all__ = (
|
|
@@ -60,15 +61,11 @@ def content_image_url(
|
|
|
60
61
|
raise ValueError("detail must be 'auto', 'low', or 'high'")
|
|
61
62
|
|
|
62
63
|
if url.startswith("data:"):
|
|
63
|
-
|
|
64
|
-
if len(parts) != 2 or not parts[1].startswith("base64,"):
|
|
65
|
-
raise ValueError("url is not a valid data URL.")
|
|
66
|
-
content_type = parts[0]
|
|
67
|
-
base64_data = parts[1][7:]
|
|
64
|
+
content_type, base64_data = parse_data_url(url)
|
|
68
65
|
if content_type not in ["image/png", "image/jpeg", "image/webp", "image/gif"]:
|
|
69
66
|
raise ValueError(f"Unsupported image content type: {content_type}")
|
|
70
67
|
content_type = cast(ImageContentTypes, content_type)
|
|
71
|
-
return ContentImageInline(content_type, base64_data)
|
|
68
|
+
return ContentImageInline(image_content_type=content_type, data=base64_data)
|
|
72
69
|
else:
|
|
73
70
|
return ContentImageRemote(url=url, detail=detail)
|
|
74
71
|
|
|
@@ -191,7 +188,7 @@ def content_image_file(
|
|
|
191
188
|
img.save(buffer, format=img.format)
|
|
192
189
|
base64_data = base64.b64encode(buffer.getvalue()).decode("utf-8")
|
|
193
190
|
|
|
194
|
-
return ContentImageInline(content_type, base64_data)
|
|
191
|
+
return ContentImageInline(image_content_type=content_type, data=base64_data)
|
|
195
192
|
|
|
196
193
|
|
|
197
194
|
def content_image_plot(
|
|
@@ -263,7 +260,7 @@ def content_image_plot(
|
|
|
263
260
|
fig.savefig(buf, format="png", dpi=dpi, bbox_inches="tight")
|
|
264
261
|
buf.seek(0)
|
|
265
262
|
base64_data = base64.b64encode(buf.getvalue()).decode("utf-8")
|
|
266
|
-
return ContentImageInline("image/png", base64_data)
|
|
263
|
+
return ContentImageInline(image_content_type="image/png", data=base64_data)
|
|
267
264
|
finally:
|
|
268
265
|
fig.set_size_inches(*size)
|
|
269
266
|
|
chatlas/_content_pdf.py
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import tempfile
|
|
3
|
+
from typing import Union
|
|
4
|
+
|
|
5
|
+
import requests
|
|
6
|
+
|
|
7
|
+
from ._content import ContentPDF
|
|
8
|
+
|
|
9
|
+
__all__ = (
|
|
10
|
+
"content_pdf_url",
|
|
11
|
+
"content_pdf_file",
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def content_pdf_file(path: Union[str, os.PathLike]) -> ContentPDF:
|
|
16
|
+
"""
|
|
17
|
+
Prepare a local PDF for input to a chat.
|
|
18
|
+
|
|
19
|
+
Not all providers support PDF input, so check the documentation for the
|
|
20
|
+
provider you are using.
|
|
21
|
+
|
|
22
|
+
Parameters
|
|
23
|
+
----------
|
|
24
|
+
path
|
|
25
|
+
A path to a local PDF file.
|
|
26
|
+
|
|
27
|
+
Returns
|
|
28
|
+
-------
|
|
29
|
+
[](`~chatlas.types.Content`)
|
|
30
|
+
Content suitable for a [](`~chatlas.Turn`) object.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
if not os.path.isfile(path):
|
|
34
|
+
raise FileNotFoundError(f"PDF file not found: {path}")
|
|
35
|
+
|
|
36
|
+
with open(path, "rb") as f:
|
|
37
|
+
data = f.read()
|
|
38
|
+
|
|
39
|
+
return ContentPDF(data=data)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def content_pdf_url(url: str) -> ContentPDF:
|
|
43
|
+
"""
|
|
44
|
+
Use a remote PDF for input to a chat.
|
|
45
|
+
|
|
46
|
+
Not all providers support PDF input, so check the documentation for the
|
|
47
|
+
provider you are using.
|
|
48
|
+
|
|
49
|
+
Parameters
|
|
50
|
+
----------
|
|
51
|
+
url
|
|
52
|
+
A URL to a remote PDF file.
|
|
53
|
+
|
|
54
|
+
Returns
|
|
55
|
+
-------
|
|
56
|
+
[](`~chatlas.types.Content`)
|
|
57
|
+
Content suitable for a [](`~chatlas.Turn`) object.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
if url.startswith("data:"):
|
|
61
|
+
content_type, base64_data = parse_data_url(url)
|
|
62
|
+
if content_type != "application/pdf":
|
|
63
|
+
raise ValueError(f"Unsupported PDF content type: {content_type}")
|
|
64
|
+
return ContentPDF(data=base64_data.encode("utf-8"))
|
|
65
|
+
# TODO: need separate ContentPDFRemote type so we can use file upload
|
|
66
|
+
# apis where they exist. Might need some kind of mutable state so can
|
|
67
|
+
# record point to uploaded file.
|
|
68
|
+
data = download_pdf_bytes(url)
|
|
69
|
+
return ContentPDF(data=data)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def parse_data_url(url: str) -> tuple[str, str]:
|
|
73
|
+
parts = url[5:].split(";", 1)
|
|
74
|
+
if len(parts) != 2 or not parts[1].startswith("base64,"):
|
|
75
|
+
raise ValueError("url is not a valid data URL.")
|
|
76
|
+
return (parts[0], parts[1][7:])
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def download_pdf_bytes(url):
|
|
80
|
+
with tempfile.NamedTemporaryFile(suffix=".pdf", delete=True) as temp_file:
|
|
81
|
+
try:
|
|
82
|
+
response = requests.get(url, stream=True)
|
|
83
|
+
response.raise_for_status()
|
|
84
|
+
|
|
85
|
+
for chunk in response.iter_content(chunk_size=8192):
|
|
86
|
+
temp_file.write(chunk)
|
|
87
|
+
|
|
88
|
+
temp_file.flush()
|
|
89
|
+
temp_file.seek(0)
|
|
90
|
+
|
|
91
|
+
return temp_file.read()
|
|
92
|
+
|
|
93
|
+
except Exception as e:
|
|
94
|
+
raise e
|
chatlas/_google.py
CHANGED
|
@@ -12,6 +12,7 @@ from ._content import (
|
|
|
12
12
|
ContentImageInline,
|
|
13
13
|
ContentImageRemote,
|
|
14
14
|
ContentJson,
|
|
15
|
+
ContentPDF,
|
|
15
16
|
ContentText,
|
|
16
17
|
ContentToolRequest,
|
|
17
18
|
ContentToolResult,
|
|
@@ -399,10 +400,19 @@ class GoogleProvider(
|
|
|
399
400
|
return Part.from_text(text=content.text)
|
|
400
401
|
elif isinstance(content, ContentJson):
|
|
401
402
|
return Part.from_text(text="<structured data/>")
|
|
403
|
+
elif isinstance(content, ContentPDF):
|
|
404
|
+
from google.genai.types import Blob
|
|
405
|
+
|
|
406
|
+
return Part(
|
|
407
|
+
inline_data=Blob(
|
|
408
|
+
data=content.data,
|
|
409
|
+
mime_type="application/pdf",
|
|
410
|
+
)
|
|
411
|
+
)
|
|
402
412
|
elif isinstance(content, ContentImageInline) and content.data:
|
|
403
413
|
return Part.from_bytes(
|
|
404
414
|
data=base64.b64decode(content.data),
|
|
405
|
-
mime_type=content.
|
|
415
|
+
mime_type=content.image_content_type,
|
|
406
416
|
)
|
|
407
417
|
elif isinstance(content, ContentImageRemote):
|
|
408
418
|
raise NotImplementedError(
|
|
@@ -460,9 +470,9 @@ class GoogleProvider(
|
|
|
460
470
|
text = part.get("text")
|
|
461
471
|
if text:
|
|
462
472
|
if has_data_model:
|
|
463
|
-
contents.append(ContentJson(json.loads(text)))
|
|
473
|
+
contents.append(ContentJson(value=json.loads(text)))
|
|
464
474
|
else:
|
|
465
|
-
contents.append(ContentText(text))
|
|
475
|
+
contents.append(ContentText(text=text))
|
|
466
476
|
function_call = part.get("function_call")
|
|
467
477
|
if function_call:
|
|
468
478
|
# Seems name is required but id is optional?
|
chatlas/_openai.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import base64
|
|
3
4
|
import json
|
|
4
5
|
from typing import TYPE_CHECKING, Any, Literal, Optional, cast, overload
|
|
5
6
|
|
|
@@ -12,6 +13,7 @@ from ._content import (
|
|
|
12
13
|
ContentImageInline,
|
|
13
14
|
ContentImageRemote,
|
|
14
15
|
ContentJson,
|
|
16
|
+
ContentPDF,
|
|
15
17
|
ContentText,
|
|
16
18
|
ContentToolRequest,
|
|
17
19
|
ContentToolResult,
|
|
@@ -461,6 +463,19 @@ class OpenAIProvider(Provider[ChatCompletion, ChatCompletionChunk, ChatCompletio
|
|
|
461
463
|
contents.append({"type": "text", "text": x.text})
|
|
462
464
|
elif isinstance(x, ContentJson):
|
|
463
465
|
contents.append({"type": "text", "text": ""})
|
|
466
|
+
elif isinstance(x, ContentPDF):
|
|
467
|
+
contents.append(
|
|
468
|
+
{
|
|
469
|
+
"type": "file",
|
|
470
|
+
"file": {
|
|
471
|
+
"filename": "",
|
|
472
|
+
"file_data": (
|
|
473
|
+
"data:application/pdf;base64,"
|
|
474
|
+
f"{base64.b64encode(x.data).decode('utf-8')}"
|
|
475
|
+
),
|
|
476
|
+
},
|
|
477
|
+
}
|
|
478
|
+
)
|
|
464
479
|
elif isinstance(x, ContentImageRemote):
|
|
465
480
|
contents.append(
|
|
466
481
|
{
|
|
@@ -476,7 +491,7 @@ class OpenAIProvider(Provider[ChatCompletion, ChatCompletionChunk, ChatCompletio
|
|
|
476
491
|
{
|
|
477
492
|
"type": "image_url",
|
|
478
493
|
"image_url": {
|
|
479
|
-
"url": f"data:{x.
|
|
494
|
+
"url": f"data:{x.image_content_type};base64,{x.data}"
|
|
480
495
|
},
|
|
481
496
|
}
|
|
482
497
|
)
|
|
@@ -514,9 +529,9 @@ class OpenAIProvider(Provider[ChatCompletion, ChatCompletionChunk, ChatCompletio
|
|
|
514
529
|
if message.content is not None:
|
|
515
530
|
if has_data_model:
|
|
516
531
|
data = json.loads(message.content)
|
|
517
|
-
contents = [ContentJson(data)]
|
|
532
|
+
contents = [ContentJson(value=data)]
|
|
518
533
|
else:
|
|
519
|
-
contents = [ContentText(message.content)]
|
|
534
|
+
contents = [ContentText(text=message.content)]
|
|
520
535
|
|
|
521
536
|
tool_calls = message.tool_calls
|
|
522
537
|
|
|
@@ -540,7 +555,7 @@ class OpenAIProvider(Provider[ChatCompletion, ChatCompletionChunk, ChatCompletio
|
|
|
540
555
|
|
|
541
556
|
contents.append(
|
|
542
557
|
ContentToolRequest(
|
|
543
|
-
call.id,
|
|
558
|
+
id=call.id,
|
|
544
559
|
name=func.name,
|
|
545
560
|
arguments=args,
|
|
546
561
|
)
|
chatlas/_turn.py
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from typing import
|
|
3
|
+
from typing import Generic, Literal, Optional, Sequence, TypeVar
|
|
4
4
|
|
|
5
|
-
from
|
|
5
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
6
|
+
|
|
7
|
+
from ._content import Content, ContentText, ContentUnion, create_content
|
|
6
8
|
|
|
7
9
|
__all__ = ("Turn",)
|
|
8
10
|
|
|
9
11
|
CompletionT = TypeVar("CompletionT")
|
|
10
12
|
|
|
11
13
|
|
|
12
|
-
class Turn(Generic[CompletionT]):
|
|
14
|
+
class Turn(BaseModel, Generic[CompletionT]):
|
|
13
15
|
"""
|
|
14
16
|
A user or assistant turn
|
|
15
17
|
|
|
@@ -64,6 +66,14 @@ class Turn(Generic[CompletionT]):
|
|
|
64
66
|
This is only relevant for assistant turns.
|
|
65
67
|
"""
|
|
66
68
|
|
|
69
|
+
role: Literal["user", "assistant", "system"]
|
|
70
|
+
contents: list[ContentUnion] = Field(default_factory=list)
|
|
71
|
+
tokens: Optional[tuple[int, int]] = None
|
|
72
|
+
finish_reason: Optional[str] = None
|
|
73
|
+
completion: Optional[CompletionT] = Field(default=None, exclude=True)
|
|
74
|
+
|
|
75
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
76
|
+
|
|
67
77
|
def __init__(
|
|
68
78
|
self,
|
|
69
79
|
role: Literal["user", "assistant", "system"],
|
|
@@ -72,26 +82,34 @@ class Turn(Generic[CompletionT]):
|
|
|
72
82
|
tokens: Optional[tuple[int, int]] = None,
|
|
73
83
|
finish_reason: Optional[str] = None,
|
|
74
84
|
completion: Optional[CompletionT] = None,
|
|
85
|
+
**kwargs,
|
|
75
86
|
):
|
|
76
|
-
self.role = role
|
|
77
|
-
|
|
78
87
|
if isinstance(contents, str):
|
|
79
|
-
contents = [ContentText(contents)]
|
|
88
|
+
contents = [ContentText(text=contents)]
|
|
80
89
|
|
|
81
90
|
contents2: list[Content] = []
|
|
82
91
|
for x in contents:
|
|
83
92
|
if isinstance(x, Content):
|
|
84
93
|
contents2.append(x)
|
|
85
94
|
elif isinstance(x, str):
|
|
86
|
-
contents2.append(ContentText(x))
|
|
95
|
+
contents2.append(ContentText(text=x))
|
|
96
|
+
elif isinstance(x, dict):
|
|
97
|
+
contents2.append(create_content(x))
|
|
87
98
|
else:
|
|
88
99
|
raise ValueError("All contents must be Content objects or str.")
|
|
89
100
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
101
|
+
super().__init__(
|
|
102
|
+
role=role,
|
|
103
|
+
contents=contents2,
|
|
104
|
+
tokens=tokens,
|
|
105
|
+
finish_reason=finish_reason,
|
|
106
|
+
completion=completion,
|
|
107
|
+
**kwargs,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
@property
|
|
111
|
+
def text(self) -> str:
|
|
112
|
+
return "".join(x.text for x in self.contents if isinstance(x, ContentText))
|
|
95
113
|
|
|
96
114
|
def __str__(self) -> str:
|
|
97
115
|
return self.text
|
|
@@ -109,18 +127,6 @@ class Turn(Generic[CompletionT]):
|
|
|
109
127
|
res += "\n" + content.__repr__(indent=indent + 2)
|
|
110
128
|
return res + "\n"
|
|
111
129
|
|
|
112
|
-
def __eq__(self, other: Any) -> bool:
|
|
113
|
-
if not isinstance(other, Turn):
|
|
114
|
-
return False
|
|
115
|
-
res = (
|
|
116
|
-
self.role == other.role
|
|
117
|
-
and self.contents == other.contents
|
|
118
|
-
and self.tokens == other.tokens
|
|
119
|
-
and self.finish_reason == other.finish_reason
|
|
120
|
-
and self.completion == other.completion
|
|
121
|
-
)
|
|
122
|
-
return res
|
|
123
|
-
|
|
124
130
|
|
|
125
131
|
def user_turn(*args: Content | str) -> Turn:
|
|
126
132
|
if len(args) == 0:
|
chatlas/_version.py
CHANGED
chatlas/types/openai/_submit.py
CHANGED
|
@@ -46,11 +46,6 @@ class SubmitInputArgs(TypedDict, total=False):
|
|
|
46
46
|
"o1-preview-2024-09-12",
|
|
47
47
|
"o1-mini",
|
|
48
48
|
"o1-mini-2024-09-12",
|
|
49
|
-
"computer-use-preview",
|
|
50
|
-
"computer-use-preview-2025-02-04",
|
|
51
|
-
"computer-use-preview-2025-03-11",
|
|
52
|
-
"gpt-4.5-preview",
|
|
53
|
-
"gpt-4.5-preview-2025-02-27",
|
|
54
49
|
"gpt-4o",
|
|
55
50
|
"gpt-4o-2024-11-20",
|
|
56
51
|
"gpt-4o-2024-08-06",
|
|
@@ -60,6 +55,10 @@ class SubmitInputArgs(TypedDict, total=False):
|
|
|
60
55
|
"gpt-4o-audio-preview-2024-12-17",
|
|
61
56
|
"gpt-4o-mini-audio-preview",
|
|
62
57
|
"gpt-4o-mini-audio-preview-2024-12-17",
|
|
58
|
+
"gpt-4o-search-preview",
|
|
59
|
+
"gpt-4o-mini-search-preview",
|
|
60
|
+
"gpt-4o-search-preview-2025-03-11",
|
|
61
|
+
"gpt-4o-mini-search-preview-2025-03-11",
|
|
63
62
|
"chatgpt-4o-latest",
|
|
64
63
|
"gpt-4o-mini",
|
|
65
64
|
"gpt-4o-mini-2024-07-18",
|
|
@@ -1,29 +1,30 @@
|
|
|
1
|
-
chatlas/__init__.py,sha256=
|
|
2
|
-
chatlas/_anthropic.py,sha256
|
|
1
|
+
chatlas/__init__.py,sha256=IVHVEEN6pspb-5WqWfBLc9wOQH-1R8vmi1Eeh-OSVFY,1358
|
|
2
|
+
chatlas/_anthropic.py,sha256=IvTC1xJYeKi7Liz_Czt1wmkG_Tx12e2ME663xZTNpdI,24745
|
|
3
3
|
chatlas/_auto.py,sha256=4tpwla09la4VA2PAh3phAMWs2Amgtp_4Qsjx6K02ib0,6032
|
|
4
|
-
chatlas/_chat.py,sha256=
|
|
5
|
-
chatlas/_content.py,sha256=
|
|
6
|
-
chatlas/_content_image.py,sha256=
|
|
4
|
+
chatlas/_chat.py,sha256=czfSjsEbRX5bHLclF1IbYtfHlyZrAOH0xcP-1hzcNNk,46032
|
|
5
|
+
chatlas/_content.py,sha256=yXB1IukyMfK9-Zc8ISm4h1p09O4i79YEJandzyT4UtM,8726
|
|
6
|
+
chatlas/_content_image.py,sha256=EUK6wAint-JatLsiwvaPDu4D3W-NcIsDCkzABkXgfDg,8304
|
|
7
|
+
chatlas/_content_pdf.py,sha256=cffeuJxzhUDukQ-Srkmpy62M8X12skYpU_FVq-Wvya4,2420
|
|
7
8
|
chatlas/_display.py,sha256=eqdRIwQenyJxswmTEjnJ1n9YxxSxsa8vHVmA79449_o,4439
|
|
8
9
|
chatlas/_github.py,sha256=8_vvUIBCprgrQ5UItky5yETfEQPG2fCMM57ga77p28E,4377
|
|
9
|
-
chatlas/_google.py,sha256=
|
|
10
|
+
chatlas/_google.py,sha256=lXqqLwXlqFoKh0GWx-OSgJ1pge0Dv7FH8Sg-MkcXpJs,19138
|
|
10
11
|
chatlas/_groq.py,sha256=iuFvxeXkq81sDHxVV9zbVHjf2ZuNT94P-XkuXvqtGms,4160
|
|
11
12
|
chatlas/_interpolate.py,sha256=ykwLP3x-ya9Q33U4knSU75dtk6pzJAeythEEIW-43Pc,3631
|
|
12
13
|
chatlas/_live_render.py,sha256=UMZltE35LxziDKPMEeDwQ9meZ95SeqwhJi7j-y9pcro,4004
|
|
13
14
|
chatlas/_logging.py,sha256=7a20sAl1PkW1qBNrfd_ieUbQXV8Gf4Vuf0Wn62LNBmk,2290
|
|
14
15
|
chatlas/_merge.py,sha256=SGj_BetgA7gaOqSBKOhYmW3CYeQKTEehFrXvx3y4OYE,3924
|
|
15
16
|
chatlas/_ollama.py,sha256=EgTwmphVwBV7xCIqmPC_cNlr4Uo9N5Xy4eDCb1sJoPI,3764
|
|
16
|
-
chatlas/_openai.py,sha256=
|
|
17
|
+
chatlas/_openai.py,sha256=xnJPzZzVuRoH7al7Tq01J7SIgF7bZm3UwcO2noDENk4,24523
|
|
17
18
|
chatlas/_perplexity.py,sha256=j-jfOIYefZC5XzGjmya9GCCGQN003cRmiAv6vmo0rTQ,4454
|
|
18
19
|
chatlas/_provider.py,sha256=YmdBbz_u5aP_kBxl6s26OPiSnWG_vZ_fvf9L2qvBmyI,3809
|
|
19
20
|
chatlas/_snowflake.py,sha256=WUNdT3irxgLVqoc1TAeDmxnYsjBWiBw-CoH-dY4mFps,10944
|
|
20
21
|
chatlas/_tokens.py,sha256=3W3EPUp9eWXUiwuzJwEPBv43AUznbK46pm59Htti7z4,2392
|
|
21
22
|
chatlas/_tokens_old.py,sha256=L9d9oafrXvEx2u4nIn_Jjn7adnQyLBnYBuPwJUE8Pl8,5005
|
|
22
23
|
chatlas/_tools.py,sha256=-qt4U1AFkebQoX9kpsBy5QXK8a2PpHX6Amgm44gcQ68,4113
|
|
23
|
-
chatlas/_turn.py,sha256=
|
|
24
|
+
chatlas/_turn.py,sha256=7pve6YmD-L4c7Oxd6_ZAPkDudJ8AMpa6pP-pSroA1dM,5067
|
|
24
25
|
chatlas/_typing_extensions.py,sha256=YdzmlyPSBpIEcsOkoz12e6jETT1XEMV2Q72haE4cfwY,1036
|
|
25
26
|
chatlas/_utils.py,sha256=2TPy5_8dr9QDF1YShZN-CjxRVHeArSujRiaF0SKnI4o,2895
|
|
26
|
-
chatlas/_version.py,sha256=
|
|
27
|
+
chatlas/_version.py,sha256=jF9TuoEIJRaca3ScKo6qaz6PzaMlu7jjuSQIrJ3nX4U,511
|
|
27
28
|
chatlas/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
28
29
|
chatlas/types/__init__.py,sha256=P_EDL4eqsigKwB-u2qRmKlYQS5Y65m7oWjGC3cYmxO4,719
|
|
29
30
|
chatlas/types/anthropic/__init__.py,sha256=OwubA-DPHYpYo0XyRyAFwftOI0mOxtHzAyhUSLcDx54,417
|
|
@@ -36,9 +37,9 @@ chatlas/types/google/_submit.py,sha256=b-ZqMvI551Ia7pFlWdqUQJjov3neHmVwLFw-P2bgU
|
|
|
36
37
|
chatlas/types/openai/__init__.py,sha256=Q2RAr1bSH1nHsxICK05nAmKmxdhKmhbBkWD_XHiVSrI,411
|
|
37
38
|
chatlas/types/openai/_client.py,sha256=YGm_EHtRSSHeeOZe-CV7oNvMJpEblEta3UTuU7lSRO8,754
|
|
38
39
|
chatlas/types/openai/_client_azure.py,sha256=jx8D_p46CLDGzTP-k-TtGzj-f3junj6or-86m8DD_0w,858
|
|
39
|
-
chatlas/types/openai/_submit.py,sha256=
|
|
40
|
+
chatlas/types/openai/_submit.py,sha256=mflYHZ5Q3dWBR2PdVEq6lhC9qNrQGNvyMiORglYLByE,6271
|
|
40
41
|
chatlas/types/snowflake/__init__.py,sha256=NVKw_gLVnSlMNdE6BpikrQw8GV8LvIn5SR8eI8Afgbs,273
|
|
41
42
|
chatlas/types/snowflake/_submit.py,sha256=Fgcb2Z4mXYwAR2b7Kn3SdEYFlO4gJiUvkDJ3lDoN0IY,799
|
|
42
|
-
chatlas-0.
|
|
43
|
-
chatlas-0.
|
|
44
|
-
chatlas-0.
|
|
43
|
+
chatlas-0.6.0.dist-info/METADATA,sha256=qJ2toXASnFjXvQOYJg0cXGx5AABxsgWBDBWN-1gZf04,14409
|
|
44
|
+
chatlas-0.6.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
45
|
+
chatlas-0.6.0.dist-info/RECORD,,
|
|
File without changes
|