chatlas 0.5.0__py3-none-any.whl → 0.6.1__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 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.content_type,
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"![]({self.url})"
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
- content_type
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
- content_type: ImageContentTypes
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"![](data:{self.content_type};base64,{self.data})"
137
+ return f"![](data:{self.image_content_type};base64,{self.data})"
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.content_type}' size={n_bytes}>"
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: E722
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
- parts = url[5:].split(";", 1)
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
 
@@ -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.content_type,
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.content_type};base64,{x.data}"
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 Any, Generic, Literal, Optional, Sequence, TypeVar
3
+ from typing import Generic, Literal, Optional, Sequence, TypeVar
4
4
 
5
- from ._content import Content, ContentText
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
- self.contents = contents2
91
- self.text = "".join(x.text for x in self.contents if isinstance(x, ContentText))
92
- self.tokens = tokens
93
- self.finish_reason = finish_reason
94
- self.completion = completion
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
@@ -17,5 +17,5 @@ __version__: str
17
17
  __version_tuple__: VERSION_TUPLE
18
18
  version_tuple: VERSION_TUPLE
19
19
 
20
- __version__ = version = '0.5.0'
21
- __version_tuple__ = version_tuple = (0, 5, 0)
20
+ __version__ = version = '0.6.1'
21
+ __version_tuple__ = version_tuple = (0, 6, 1)
@@ -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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: chatlas
3
- Version: 0.5.0
3
+ Version: 0.6.1
4
4
  Summary: A simple and consistent interface for chatting with LLMs
5
5
  Project-URL: Homepage, https://posit-dev.github.io/chatlas
6
6
  Project-URL: Documentation, https://posit-dev.github.io/chatlas
@@ -19,6 +19,7 @@ Classifier: Programming Language :: Python :: 3.13
19
19
  Requires-Python: >=3.9
20
20
  Requires-Dist: jinja2
21
21
  Requires-Dist: pydantic>=2.0
22
+ Requires-Dist: requests
22
23
  Requires-Dist: rich
23
24
  Provides-Extra: anthropic
24
25
  Requires-Dist: anthropic; extra == 'anthropic'
@@ -281,6 +282,26 @@ chat.extract_data(
281
282
 
282
283
  Learn more in the [structured data article](https://posit-dev.github.io/chatlas/structured-data.html)
283
284
 
285
+ ### Multi-modal input
286
+
287
+ Attach images and pdfs when submitting input to using any one of the `content_*` functions.
288
+
289
+ ```python
290
+ from chatlas import content_image_url
291
+
292
+ chat.chat(
293
+ content_image_url("https://www.python.org/static/img/python-logo.png"),
294
+ "What do you see in this image?"
295
+ )
296
+ ```
297
+
298
+ ```
299
+ This image displays the logo of the Python programming language. It features the word "python" alongside the distinctive two snake heads logo, which is colored in blue and yellow.
300
+ ```
301
+
302
+ Learn more in the [content reference pages](https://posit-dev.github.io/chatlas/reference/content_image_url.html) for more details on the available content types.
303
+
304
+
284
305
  ### Export chat
285
306
 
286
307
  Easily get a full markdown or HTML export of a conversation:
@@ -1,29 +1,30 @@
1
- chatlas/__init__.py,sha256=U-uNJWeVjMj4aSre2evTaVunabzLksZ5Toz19ygQB-o,1251
2
- chatlas/_anthropic.py,sha256=-Qrq-8jgNIpW_temJYXKTb7VCl2ldnpe-KSX89L5Lhc,24264
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=hS0sirnZBwMaOJ47wyNgdA61Xd7AJ_xVhj4v8RbLoHA,46014
5
- chatlas/_content.py,sha256=L_ry6W5D2h6Eag2tZmqIbMP4OQj-vVXRircLcUtgQ6c,6454
6
- chatlas/_content_image.py,sha256=4nk9wTvLtNmtcytdFp8p9otEV5-0_K6wzIxCyK0PIEI,8367
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=ehL8hf8Hogg9qK9OaXqiWainOirz21yy_lx_yvUwKf8,18831
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=9KB4hEh6acFfBvLV0yXwLcjP-CyIpsMbbVt7eJ03_Vg,23870
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=nKwk20FrOIrZX4xJxdGyUEpwUH2H-UYcoJLlO2ZD5iU,4836
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=N2U3TRgLfYxjXfoF4Dy9PxAbZq24zjCym3P3cwuxKP8,511
27
+ chatlas/_version.py,sha256=a3_WODLDfpmAw3pMw7qGqmRuXHTCC3STyQd2R1iEOgA,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=pJ6_G5R3ouI17-20fvjv0pzs_VizTlMbrBx7GXiOpNs,6294
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.5.0.dist-info/METADATA,sha256=ObTTRN6dDvUuAWuW07VDvkvwbRbeMlH6AwRvLmVFDvM,14409
43
- chatlas-0.5.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
44
- chatlas-0.5.0.dist-info/RECORD,,
43
+ chatlas-0.6.1.dist-info/METADATA,sha256=9mB4Dz3d0zCabRMuZX6E-MG8RnT4M14dhncult9gMAQ,15085
44
+ chatlas-0.6.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
45
+ chatlas-0.6.1.dist-info/RECORD,,