chatlas 0.4.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 +12 -0
- chatlas/_anthropic.py +22 -9
- chatlas/_auto.py +183 -0
- chatlas/_chat.py +9 -7
- chatlas/_content.py +116 -24
- chatlas/_content_image.py +5 -8
- chatlas/_content_pdf.py +94 -0
- chatlas/_display.py +12 -2
- chatlas/_github.py +1 -1
- chatlas/_google.py +17 -9
- chatlas/_groq.py +1 -1
- chatlas/_live_render.py +116 -0
- chatlas/_ollama.py +1 -1
- chatlas/_openai.py +22 -6
- chatlas/_perplexity.py +1 -1
- chatlas/_snowflake.py +321 -0
- chatlas/_turn.py +30 -24
- chatlas/_utils.py +7 -0
- chatlas/_version.py +21 -0
- chatlas/types/anthropic/_submit.py +23 -1
- chatlas/types/openai/_submit.py +5 -1
- chatlas/types/snowflake/__init__.py +8 -0
- chatlas/types/snowflake/_submit.py +24 -0
- {chatlas-0.4.0.dist-info → chatlas-0.6.0.dist-info}/METADATA +26 -1
- chatlas-0.6.0.dist-info/RECORD +45 -0
- chatlas-0.4.0.dist-info/RECORD +0 -38
- {chatlas-0.4.0.dist-info → chatlas-0.6.0.dist-info}/WHEEL +0 -0
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/_display.py
CHANGED
|
@@ -6,6 +6,7 @@ from uuid import uuid4
|
|
|
6
6
|
from rich.live import Live
|
|
7
7
|
from rich.logging import RichHandler
|
|
8
8
|
|
|
9
|
+
from ._live_render import LiveRender
|
|
9
10
|
from ._logging import logger
|
|
10
11
|
from ._typing_extensions import TypedDict
|
|
11
12
|
|
|
@@ -44,13 +45,22 @@ class LiveMarkdownDisplay(MarkdownDisplay):
|
|
|
44
45
|
from rich.console import Console
|
|
45
46
|
|
|
46
47
|
self.content: str = ""
|
|
47
|
-
|
|
48
|
+
live = Live(
|
|
48
49
|
auto_refresh=False,
|
|
49
|
-
vertical_overflow="visible",
|
|
50
50
|
console=Console(
|
|
51
51
|
**echo_options["rich_console"],
|
|
52
52
|
),
|
|
53
53
|
)
|
|
54
|
+
|
|
55
|
+
# Monkeypatch LiveRender() with our own version that add "crop_above"
|
|
56
|
+
# https://github.com/Textualize/rich/blob/43d3b047/rich/live.py#L87-L89
|
|
57
|
+
live.vertical_overflow = "crop_above"
|
|
58
|
+
live._live_render = LiveRender( # pyright: ignore[reportAttributeAccessIssue]
|
|
59
|
+
live.get_renderable(), vertical_overflow="crop_above"
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
self.live = live
|
|
63
|
+
|
|
54
64
|
self._markdown_options = echo_options["rich_markdown"]
|
|
55
65
|
|
|
56
66
|
def update(self, content: str):
|
chatlas/_github.py
CHANGED
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,
|
|
@@ -61,8 +62,7 @@ def ChatGoogle(
|
|
|
61
62
|
::: {.callout-note}
|
|
62
63
|
## Python requirements
|
|
63
64
|
|
|
64
|
-
`ChatGoogle` requires the `google-genai` package
|
|
65
|
-
(e.g., `pip install google-genai`).
|
|
65
|
+
`ChatGoogle` requires the `google-genai` package: `pip install "chatlas[google]"`.
|
|
66
66
|
:::
|
|
67
67
|
|
|
68
68
|
Examples
|
|
@@ -400,10 +400,19 @@ class GoogleProvider(
|
|
|
400
400
|
return Part.from_text(text=content.text)
|
|
401
401
|
elif isinstance(content, ContentJson):
|
|
402
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
|
+
)
|
|
403
412
|
elif isinstance(content, ContentImageInline) and content.data:
|
|
404
413
|
return Part.from_bytes(
|
|
405
414
|
data=base64.b64decode(content.data),
|
|
406
|
-
mime_type=content.
|
|
415
|
+
mime_type=content.image_content_type,
|
|
407
416
|
)
|
|
408
417
|
elif isinstance(content, ContentImageRemote):
|
|
409
418
|
raise NotImplementedError(
|
|
@@ -413,7 +422,7 @@ class GoogleProvider(
|
|
|
413
422
|
elif isinstance(content, ContentToolRequest):
|
|
414
423
|
return Part(
|
|
415
424
|
function_call=FunctionCall(
|
|
416
|
-
id=content.id,
|
|
425
|
+
id=content.id if content.name != content.id else None,
|
|
417
426
|
name=content.name,
|
|
418
427
|
# Goes in a dict, so should come out as a dict
|
|
419
428
|
args=cast(dict[str, Any], content.arguments),
|
|
@@ -428,7 +437,7 @@ class GoogleProvider(
|
|
|
428
437
|
# TODO: seems function response parts might need role='tool'???
|
|
429
438
|
# https://github.com/googleapis/python-genai/blame/c8cfef85c/README.md#L344
|
|
430
439
|
function_response=FunctionResponse(
|
|
431
|
-
id=content.id,
|
|
440
|
+
id=content.id if content.name != content.id else None,
|
|
432
441
|
name=content.name,
|
|
433
442
|
response=resp,
|
|
434
443
|
)
|
|
@@ -461,9 +470,9 @@ class GoogleProvider(
|
|
|
461
470
|
text = part.get("text")
|
|
462
471
|
if text:
|
|
463
472
|
if has_data_model:
|
|
464
|
-
contents.append(ContentJson(json.loads(text)))
|
|
473
|
+
contents.append(ContentJson(value=json.loads(text)))
|
|
465
474
|
else:
|
|
466
|
-
contents.append(ContentText(text))
|
|
475
|
+
contents.append(ContentText(text=text))
|
|
467
476
|
function_call = part.get("function_call")
|
|
468
477
|
if function_call:
|
|
469
478
|
# Seems name is required but id is optional?
|
|
@@ -530,8 +539,7 @@ def ChatVertex(
|
|
|
530
539
|
::: {.callout-note}
|
|
531
540
|
## Python requirements
|
|
532
541
|
|
|
533
|
-
`ChatGoogle` requires the `google-genai` package
|
|
534
|
-
(e.g., `pip install google-genai`).
|
|
542
|
+
`ChatGoogle` requires the `google-genai` package: `pip install "chatlas[vertex]"`.
|
|
535
543
|
:::
|
|
536
544
|
|
|
537
545
|
::: {.callout-note}
|
chatlas/_groq.py
CHANGED
chatlas/_live_render.py
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# A 'patched' version of LiveRender that adds the 'crop_above' vertical overflow method.
|
|
2
|
+
# Derives from https://github.com/Textualize/rich/pull/3637
|
|
3
|
+
import sys
|
|
4
|
+
from typing import Optional, Tuple
|
|
5
|
+
|
|
6
|
+
if sys.version_info >= (3, 8):
|
|
7
|
+
from typing import Literal
|
|
8
|
+
else:
|
|
9
|
+
from typing_extensions import Literal # pragma: no cover
|
|
10
|
+
|
|
11
|
+
from rich._loop import loop_last
|
|
12
|
+
from rich.console import Console, ConsoleOptions, RenderableType, RenderResult
|
|
13
|
+
from rich.control import Control
|
|
14
|
+
from rich.segment import ControlType, Segment
|
|
15
|
+
from rich.style import StyleType
|
|
16
|
+
from rich.text import Text
|
|
17
|
+
|
|
18
|
+
VerticalOverflowMethod = Literal["crop", "crop_above", "ellipsis", "visible"]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class LiveRender:
|
|
22
|
+
"""Creates a renderable that may be updated.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
renderable (RenderableType): Any renderable object.
|
|
26
|
+
style (StyleType, optional): An optional style to apply to the renderable. Defaults to "".
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
renderable: RenderableType,
|
|
32
|
+
style: StyleType = "",
|
|
33
|
+
vertical_overflow: VerticalOverflowMethod = "ellipsis",
|
|
34
|
+
) -> None:
|
|
35
|
+
self.renderable = renderable
|
|
36
|
+
self.style = style
|
|
37
|
+
self.vertical_overflow = vertical_overflow
|
|
38
|
+
self._shape: Optional[Tuple[int, int]] = None
|
|
39
|
+
|
|
40
|
+
def set_renderable(self, renderable: RenderableType) -> None:
|
|
41
|
+
"""Set a new renderable.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
renderable (RenderableType): Any renderable object, including str.
|
|
45
|
+
"""
|
|
46
|
+
self.renderable = renderable
|
|
47
|
+
|
|
48
|
+
def position_cursor(self) -> Control:
|
|
49
|
+
"""Get control codes to move cursor to beginning of live render.
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
Control: A control instance that may be printed.
|
|
53
|
+
"""
|
|
54
|
+
if self._shape is not None:
|
|
55
|
+
_, height = self._shape
|
|
56
|
+
return Control(
|
|
57
|
+
ControlType.CARRIAGE_RETURN,
|
|
58
|
+
(ControlType.ERASE_IN_LINE, 2),
|
|
59
|
+
*(
|
|
60
|
+
(
|
|
61
|
+
(ControlType.CURSOR_UP, 1),
|
|
62
|
+
(ControlType.ERASE_IN_LINE, 2),
|
|
63
|
+
)
|
|
64
|
+
* (height - 1)
|
|
65
|
+
),
|
|
66
|
+
)
|
|
67
|
+
return Control()
|
|
68
|
+
|
|
69
|
+
def restore_cursor(self) -> Control:
|
|
70
|
+
"""Get control codes to clear the render and restore the cursor to its previous position.
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
Control: A Control instance that may be printed.
|
|
74
|
+
"""
|
|
75
|
+
if self._shape is not None:
|
|
76
|
+
_, height = self._shape
|
|
77
|
+
return Control(
|
|
78
|
+
ControlType.CARRIAGE_RETURN,
|
|
79
|
+
*((ControlType.CURSOR_UP, 1), (ControlType.ERASE_IN_LINE, 2)) * height,
|
|
80
|
+
)
|
|
81
|
+
return Control()
|
|
82
|
+
|
|
83
|
+
def __rich_console__(
|
|
84
|
+
self, console: Console, options: ConsoleOptions
|
|
85
|
+
) -> RenderResult:
|
|
86
|
+
renderable = self.renderable
|
|
87
|
+
style = console.get_style(self.style)
|
|
88
|
+
lines = console.render_lines(renderable, options, style=style, pad=False)
|
|
89
|
+
shape = Segment.get_shape(lines)
|
|
90
|
+
|
|
91
|
+
_, height = shape
|
|
92
|
+
if height > options.size.height:
|
|
93
|
+
if self.vertical_overflow == "crop":
|
|
94
|
+
lines = lines[: options.size.height]
|
|
95
|
+
shape = Segment.get_shape(lines)
|
|
96
|
+
elif self.vertical_overflow == "crop_above":
|
|
97
|
+
lines = lines[-(options.size.height) :]
|
|
98
|
+
shape = Segment.get_shape(lines)
|
|
99
|
+
elif self.vertical_overflow == "ellipsis":
|
|
100
|
+
lines = lines[: (options.size.height - 1)]
|
|
101
|
+
overflow_text = Text(
|
|
102
|
+
"...",
|
|
103
|
+
overflow="crop",
|
|
104
|
+
justify="center",
|
|
105
|
+
end="",
|
|
106
|
+
style="live.ellipsis",
|
|
107
|
+
)
|
|
108
|
+
lines.append(list(console.render(overflow_text)))
|
|
109
|
+
shape = Segment.get_shape(lines)
|
|
110
|
+
self._shape = shape
|
|
111
|
+
|
|
112
|
+
new_line = Segment.line()
|
|
113
|
+
for last, line in loop_last(lines):
|
|
114
|
+
yield from line
|
|
115
|
+
if not last:
|
|
116
|
+
yield new_line
|
chatlas/_ollama.py
CHANGED
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,
|
|
@@ -79,7 +81,7 @@ def ChatOpenAI(
|
|
|
79
81
|
::: {.callout-note}
|
|
80
82
|
## Python requirements
|
|
81
83
|
|
|
82
|
-
`ChatOpenAI` requires the `openai` package
|
|
84
|
+
`ChatOpenAI` requires the `openai` package: `pip install "chatlas[openai]"`.
|
|
83
85
|
:::
|
|
84
86
|
|
|
85
87
|
Examples
|
|
@@ -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
|
)
|
|
@@ -592,7 +607,8 @@ def ChatAzureOpenAI(
|
|
|
592
607
|
::: {.callout-note}
|
|
593
608
|
## Python requirements
|
|
594
609
|
|
|
595
|
-
`ChatAzureOpenAI` requires the `openai` package
|
|
610
|
+
`ChatAzureOpenAI` requires the `openai` package:
|
|
611
|
+
`pip install "chatlas[azure-openai]"`.
|
|
596
612
|
:::
|
|
597
613
|
|
|
598
614
|
Examples
|
chatlas/_perplexity.py
CHANGED