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/__init__.py +38 -0
- chatlas/_anthropic.py +643 -0
- chatlas/_chat.py +1279 -0
- chatlas/_content.py +242 -0
- chatlas/_content_image.py +272 -0
- chatlas/_display.py +139 -0
- chatlas/_github.py +147 -0
- chatlas/_google.py +456 -0
- chatlas/_groq.py +143 -0
- chatlas/_interpolate.py +133 -0
- chatlas/_logging.py +61 -0
- chatlas/_merge.py +103 -0
- chatlas/_ollama.py +125 -0
- chatlas/_openai.py +654 -0
- chatlas/_perplexity.py +148 -0
- chatlas/_provider.py +143 -0
- chatlas/_tokens.py +87 -0
- chatlas/_tokens_old.py +148 -0
- chatlas/_tools.py +134 -0
- chatlas/_turn.py +147 -0
- chatlas/_typing_extensions.py +26 -0
- chatlas/_utils.py +106 -0
- chatlas/types/__init__.py +32 -0
- chatlas/types/anthropic/__init__.py +14 -0
- chatlas/types/anthropic/_client.py +29 -0
- chatlas/types/anthropic/_client_bedrock.py +23 -0
- chatlas/types/anthropic/_submit.py +57 -0
- chatlas/types/google/__init__.py +12 -0
- chatlas/types/google/_client.py +101 -0
- chatlas/types/google/_submit.py +113 -0
- chatlas/types/openai/__init__.py +14 -0
- chatlas/types/openai/_client.py +22 -0
- chatlas/types/openai/_client_azure.py +25 -0
- chatlas/types/openai/_submit.py +135 -0
- chatlas-0.2.0.dist-info/METADATA +319 -0
- chatlas-0.2.0.dist-info/RECORD +37 -0
- chatlas-0.2.0.dist-info/WHEEL +4 -0
chatlas/_content.py
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import Any, Literal, Optional
|
|
6
|
+
|
|
7
|
+
ImageContentTypes = Literal[
|
|
8
|
+
"image/png",
|
|
9
|
+
"image/jpeg",
|
|
10
|
+
"image/webp",
|
|
11
|
+
"image/gif",
|
|
12
|
+
]
|
|
13
|
+
"""
|
|
14
|
+
Allowable content types for images.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Content:
|
|
19
|
+
"""
|
|
20
|
+
Base class for all content types that can be appear in a [](`~chatlas.Turn`)
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __str__(self):
|
|
24
|
+
raise NotImplementedError
|
|
25
|
+
|
|
26
|
+
def _repr_markdown_(self):
|
|
27
|
+
raise NotImplementedError
|
|
28
|
+
|
|
29
|
+
def __repr__(self, indent: int = 0):
|
|
30
|
+
raise NotImplementedError
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class ContentText(Content):
|
|
35
|
+
"""
|
|
36
|
+
Text content for a [](`~chatlas.Turn`)
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
text: str
|
|
40
|
+
|
|
41
|
+
def __str__(self):
|
|
42
|
+
return self.text
|
|
43
|
+
|
|
44
|
+
def _repr_markdown_(self):
|
|
45
|
+
return self.text
|
|
46
|
+
|
|
47
|
+
def __repr__(self, indent: int = 0):
|
|
48
|
+
text = self.text[:50] + "..." if len(self.text) > 50 else self.text
|
|
49
|
+
return " " * indent + f"<ContentText text='{text}'>"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class ContentImage(Content):
|
|
53
|
+
"""
|
|
54
|
+
Base class for image content.
|
|
55
|
+
|
|
56
|
+
This class is not meant to be used directly. Instead, use
|
|
57
|
+
[](`~chatlas.content_image_url`), [](`~chatlas.content_image_file`), or
|
|
58
|
+
[](`~chatlas.content_image_plot`).
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
pass
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass
|
|
65
|
+
class ContentImageRemote(ContentImage):
|
|
66
|
+
"""
|
|
67
|
+
Image content from a URL.
|
|
68
|
+
|
|
69
|
+
This is the return type for [](`~chatlas.content_image_url`).
|
|
70
|
+
It's not meant to be used directly.
|
|
71
|
+
|
|
72
|
+
Parameters
|
|
73
|
+
----------
|
|
74
|
+
url
|
|
75
|
+
The URL of the image.
|
|
76
|
+
detail
|
|
77
|
+
A detail setting for the image. Can be `"auto"`, `"low"`, or `"high"`.
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
url: str
|
|
81
|
+
detail: Literal["auto", "low", "high"] = "auto"
|
|
82
|
+
|
|
83
|
+
def __str__(self):
|
|
84
|
+
return f""
|
|
85
|
+
|
|
86
|
+
def _repr_markdown_(self):
|
|
87
|
+
return self.__str__()
|
|
88
|
+
|
|
89
|
+
def __repr__(self, indent: int = 0):
|
|
90
|
+
return (
|
|
91
|
+
" " * indent
|
|
92
|
+
+ f"<ContentImageRemote url='{self.url}' detail='{self.detail}'>"
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@dataclass
|
|
97
|
+
class ContentImageInline(ContentImage):
|
|
98
|
+
"""
|
|
99
|
+
Inline image content.
|
|
100
|
+
|
|
101
|
+
This is the return type for [](`~chatlas.content_image_file`) and
|
|
102
|
+
[](`~chatlas.content_image_plot`).
|
|
103
|
+
It's not meant to be used directly.
|
|
104
|
+
|
|
105
|
+
Parameters
|
|
106
|
+
----------
|
|
107
|
+
content_type
|
|
108
|
+
The content type of the image.
|
|
109
|
+
data
|
|
110
|
+
The base64-encoded image data.
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
content_type: ImageContentTypes
|
|
114
|
+
data: Optional[str] = None
|
|
115
|
+
|
|
116
|
+
def __str__(self):
|
|
117
|
+
return f""
|
|
118
|
+
|
|
119
|
+
def _repr_markdown_(self):
|
|
120
|
+
return self.__str__()
|
|
121
|
+
|
|
122
|
+
def __repr__(self, indent: int = 0):
|
|
123
|
+
n_bytes = len(self.data) if self.data else 0
|
|
124
|
+
return (
|
|
125
|
+
" " * indent
|
|
126
|
+
+ f"<ContentImageInline content_type='{self.content_type}' size={n_bytes}>"
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@dataclass
|
|
131
|
+
class ContentToolRequest(Content):
|
|
132
|
+
"""
|
|
133
|
+
A request to call a tool/function
|
|
134
|
+
|
|
135
|
+
This content type isn't meant to be used directly. Instead, it's
|
|
136
|
+
automatically generated by [](`~chatlas.Chat`) when a tool/function is
|
|
137
|
+
requested by the model assistant.
|
|
138
|
+
|
|
139
|
+
Parameters
|
|
140
|
+
----------
|
|
141
|
+
id
|
|
142
|
+
A unique identifier for this request.
|
|
143
|
+
name
|
|
144
|
+
The name of the tool/function to call.
|
|
145
|
+
arguments
|
|
146
|
+
The arguments to pass to the tool/function.
|
|
147
|
+
"""
|
|
148
|
+
|
|
149
|
+
id: str
|
|
150
|
+
name: str
|
|
151
|
+
arguments: object
|
|
152
|
+
|
|
153
|
+
def __str__(self):
|
|
154
|
+
args_str = self._arguments_str()
|
|
155
|
+
func_call = f"{self.name}({args_str})"
|
|
156
|
+
comment = f"# tool request ({self.id})"
|
|
157
|
+
return f"\n```python\n{comment}\n{func_call}\n```\n"
|
|
158
|
+
|
|
159
|
+
def _repr_markdown_(self):
|
|
160
|
+
return self.__str__()
|
|
161
|
+
|
|
162
|
+
def __repr__(self, indent: int = 0):
|
|
163
|
+
args_str = self._arguments_str()
|
|
164
|
+
return (
|
|
165
|
+
" " * indent
|
|
166
|
+
+ f"<ContentToolRequest name='{self.name}' arguments='{args_str}' id='{self.id}'>"
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
def _arguments_str(self) -> str:
|
|
170
|
+
if isinstance(self.arguments, dict):
|
|
171
|
+
return ", ".join(f"{k}={v}" for k, v in self.arguments.items())
|
|
172
|
+
return str(self.arguments)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
@dataclass
|
|
176
|
+
class ContentToolResult(Content):
|
|
177
|
+
"""
|
|
178
|
+
The result of calling a tool/function
|
|
179
|
+
|
|
180
|
+
This content type isn't meant to be used directly. Instead, it's
|
|
181
|
+
automatically generated by [](`~chatlas.Chat`) when a tool/function is
|
|
182
|
+
called (in response to a [](`~chatlas.ContentToolRequest`)).
|
|
183
|
+
|
|
184
|
+
Parameters
|
|
185
|
+
----------
|
|
186
|
+
id
|
|
187
|
+
The unique identifier of the tool request.
|
|
188
|
+
value
|
|
189
|
+
The value returned by the tool/function.
|
|
190
|
+
error
|
|
191
|
+
An error message if the tool/function call failed.
|
|
192
|
+
"""
|
|
193
|
+
|
|
194
|
+
id: str
|
|
195
|
+
value: Any = None
|
|
196
|
+
error: Optional[str] = None
|
|
197
|
+
|
|
198
|
+
def __str__(self):
|
|
199
|
+
comment = f"# tool result ({self.id})"
|
|
200
|
+
val = self.get_final_value()
|
|
201
|
+
return f"""\n```python\n{comment}\n"{val}"\n```\n"""
|
|
202
|
+
|
|
203
|
+
def _repr_markdown_(self):
|
|
204
|
+
return self.__str__()
|
|
205
|
+
|
|
206
|
+
def __repr__(self, indent: int = 0):
|
|
207
|
+
res = " " * indent
|
|
208
|
+
res += f"<ContentToolResult value='{self.value}' id='{self.id}'"
|
|
209
|
+
if self.error:
|
|
210
|
+
res += f" error='{self.error}'"
|
|
211
|
+
return res + ">"
|
|
212
|
+
|
|
213
|
+
def get_final_value(self) -> str:
|
|
214
|
+
if self.error:
|
|
215
|
+
return f"Tool calling failed with error: '{self.error}'"
|
|
216
|
+
return str(self.value)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
@dataclass
|
|
220
|
+
class ContentJson(Content):
|
|
221
|
+
"""
|
|
222
|
+
JSON content
|
|
223
|
+
|
|
224
|
+
This content type primarily exists to signal structured data extraction
|
|
225
|
+
(i.e., data extracted via [](`~chatlas.Chat`)'s `.extract_data()` method)
|
|
226
|
+
|
|
227
|
+
Parameters
|
|
228
|
+
----------
|
|
229
|
+
value
|
|
230
|
+
The JSON data extracted
|
|
231
|
+
"""
|
|
232
|
+
|
|
233
|
+
value: dict[str, Any]
|
|
234
|
+
|
|
235
|
+
def __str__(self):
|
|
236
|
+
return json.dumps(self.value, indent=2)
|
|
237
|
+
|
|
238
|
+
def _repr_markdown_(self):
|
|
239
|
+
return f"""\n```json\n{self.__str__()}\n```\n"""
|
|
240
|
+
|
|
241
|
+
def __repr__(self, indent: int = 0):
|
|
242
|
+
return " " * indent + f"<ContentJson value={self.value}>"
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
import io
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
import warnings
|
|
8
|
+
from typing import Literal, Union, cast
|
|
9
|
+
|
|
10
|
+
from ._content import ContentImageInline, ContentImageRemote, ImageContentTypes
|
|
11
|
+
from ._utils import MISSING, MISSING_TYPE
|
|
12
|
+
|
|
13
|
+
__all__ = (
|
|
14
|
+
"content_image_url",
|
|
15
|
+
"content_image_file",
|
|
16
|
+
"content_image_plot",
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def content_image_url(
|
|
21
|
+
url: str, detail: Literal["auto", "low", "high"] = "auto"
|
|
22
|
+
) -> Union[ContentImageInline, ContentImageRemote]:
|
|
23
|
+
"""
|
|
24
|
+
Encode image content from a URL for chat input.
|
|
25
|
+
|
|
26
|
+
This function is used to prepare image URLs for input to the chatbot. It can
|
|
27
|
+
handle both regular URLs and data URLs.
|
|
28
|
+
|
|
29
|
+
Parameters
|
|
30
|
+
----------
|
|
31
|
+
url
|
|
32
|
+
The URL of the image to include in the chat input. Can be a data: URL or a
|
|
33
|
+
regular URL.
|
|
34
|
+
detail
|
|
35
|
+
The detail setting for this image. Can be `"auto"`, `"low"`, or `"high"`.
|
|
36
|
+
|
|
37
|
+
Returns
|
|
38
|
+
-------
|
|
39
|
+
[](`~chatlas.types.Content`)
|
|
40
|
+
Content suitable for a [](`~chatlas.Turn`) object.
|
|
41
|
+
|
|
42
|
+
Examples
|
|
43
|
+
--------
|
|
44
|
+
```python
|
|
45
|
+
from chatlas import ChatOpenAI, content_image_url
|
|
46
|
+
|
|
47
|
+
chat = ChatOpenAI()
|
|
48
|
+
chat.chat(
|
|
49
|
+
"What do you see in this image?",
|
|
50
|
+
content_image_url("https://www.python.org/static/img/python-logo.png"),
|
|
51
|
+
)
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Raises
|
|
55
|
+
------
|
|
56
|
+
ValueError
|
|
57
|
+
If the URL is not valid or the detail setting is invalid.
|
|
58
|
+
"""
|
|
59
|
+
if detail not in ["auto", "low", "high"]:
|
|
60
|
+
raise ValueError("detail must be 'auto', 'low', or 'high'")
|
|
61
|
+
|
|
62
|
+
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:]
|
|
68
|
+
if content_type not in ["image/png", "image/jpeg", "image/webp", "image/gif"]:
|
|
69
|
+
raise ValueError(f"Unsupported image content type: {content_type}")
|
|
70
|
+
content_type = cast(ImageContentTypes, content_type)
|
|
71
|
+
return ContentImageInline(content_type, base64_data)
|
|
72
|
+
else:
|
|
73
|
+
return ContentImageRemote(url=url, detail=detail)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def content_image_file(
|
|
77
|
+
path: str,
|
|
78
|
+
content_type: Literal["auto", ImageContentTypes] = "auto",
|
|
79
|
+
resize: Union[Literal["low", "high", "none"], str, MISSING_TYPE] = MISSING,
|
|
80
|
+
) -> ContentImageInline:
|
|
81
|
+
"""
|
|
82
|
+
Encode image content from a file for chat input.
|
|
83
|
+
|
|
84
|
+
This function is used to prepare image files for input to the chatbot. It
|
|
85
|
+
can handle various image formats and provides options for resizing.
|
|
86
|
+
|
|
87
|
+
Parameters
|
|
88
|
+
----------
|
|
89
|
+
path
|
|
90
|
+
The path to the image file to include in the chat input.
|
|
91
|
+
content_type
|
|
92
|
+
The content type of the image (e.g., `"image/png"`). If `"auto"`, the content
|
|
93
|
+
type is inferred from the file extension.
|
|
94
|
+
resize
|
|
95
|
+
Resizing option for the image. Can be:
|
|
96
|
+
- `"low"`: Resize to fit within 512x512
|
|
97
|
+
- `"high"`: Resize to fit within 2000x768 or 768x2000
|
|
98
|
+
- `"none"`: No resizing
|
|
99
|
+
- Custom string (e.g., `"200x200"`, `"300x200>!"`, etc.)
|
|
100
|
+
|
|
101
|
+
Returns
|
|
102
|
+
-------
|
|
103
|
+
[](`~chatlas.types.Content`)
|
|
104
|
+
Content suitable for a [](`~chatlas.Turn`) object.
|
|
105
|
+
|
|
106
|
+
Examples
|
|
107
|
+
--------
|
|
108
|
+
```python
|
|
109
|
+
from chatlas import ChatOpenAI, content_image_file
|
|
110
|
+
|
|
111
|
+
chat = ChatOpenAI()
|
|
112
|
+
chat.chat(
|
|
113
|
+
"What do you see in this image?",
|
|
114
|
+
content_image_file("path/to/image.png"),
|
|
115
|
+
)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Raises
|
|
119
|
+
------
|
|
120
|
+
FileNotFoundError
|
|
121
|
+
If the specified file does not exist.
|
|
122
|
+
ValueError
|
|
123
|
+
If the file extension is unsupported or the resize option is invalid.
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
if not os.path.isfile(path):
|
|
127
|
+
raise FileNotFoundError(f"{path} must be an existing file.")
|
|
128
|
+
|
|
129
|
+
if content_type == "auto":
|
|
130
|
+
ext = os.path.splitext(path)[1].lower()
|
|
131
|
+
if ext == ".png":
|
|
132
|
+
content_type = "image/png"
|
|
133
|
+
elif ext in [".jpeg", ".jpg"]:
|
|
134
|
+
content_type = "image/jpeg"
|
|
135
|
+
elif ext == ".webp":
|
|
136
|
+
content_type = "image/webp"
|
|
137
|
+
elif ext == ".gif":
|
|
138
|
+
content_type = "image/gif"
|
|
139
|
+
else:
|
|
140
|
+
raise ValueError(f"Unsupported image file extension: {ext}")
|
|
141
|
+
|
|
142
|
+
if resize == "none":
|
|
143
|
+
with open(path, "rb") as image_file:
|
|
144
|
+
base64_data = base64.b64encode(image_file.read()).decode("utf-8")
|
|
145
|
+
else:
|
|
146
|
+
try:
|
|
147
|
+
from PIL import Image
|
|
148
|
+
except ImportError:
|
|
149
|
+
raise ImportError(
|
|
150
|
+
"Image resizing requires the `Pillow` package. "
|
|
151
|
+
"Install it with `pip install Pillow`."
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
img = Image.open(path)
|
|
155
|
+
|
|
156
|
+
if isinstance(resize, MISSING_TYPE):
|
|
157
|
+
warnings.warn(
|
|
158
|
+
"The `resize` parameter is missing. Defaulting to `resize='low'`. "
|
|
159
|
+
"As a result, the image has likely lost quality before the model received it. "
|
|
160
|
+
"Set `resize='low'` to suppress this warning, `resize='high'` for higher quality, or "
|
|
161
|
+
"`resize='none'` to disable resizing.",
|
|
162
|
+
category=MissingResizeWarning,
|
|
163
|
+
stacklevel=2,
|
|
164
|
+
)
|
|
165
|
+
resize = "low"
|
|
166
|
+
|
|
167
|
+
if resize == "low":
|
|
168
|
+
img.thumbnail((512, 512))
|
|
169
|
+
elif resize == "high":
|
|
170
|
+
if img.width > img.height:
|
|
171
|
+
img.thumbnail((2000, 768))
|
|
172
|
+
else:
|
|
173
|
+
img.thumbnail((768, 2000))
|
|
174
|
+
else:
|
|
175
|
+
match = re.match(r"(\d+)x(\d+)(>?)(!?)", resize)
|
|
176
|
+
if match:
|
|
177
|
+
width, height = map(int, match.group(1, 2))
|
|
178
|
+
only_shrink = ">" in match.group(3)
|
|
179
|
+
ignore_aspect = "!" in match.group(4)
|
|
180
|
+
|
|
181
|
+
if only_shrink and (img.width <= width and img.height <= height):
|
|
182
|
+
pass # No resize needed
|
|
183
|
+
elif ignore_aspect:
|
|
184
|
+
img = img.resize((width, height))
|
|
185
|
+
else:
|
|
186
|
+
img.thumbnail((width, height))
|
|
187
|
+
else:
|
|
188
|
+
raise ValueError(f"Invalid resize value: {resize}")
|
|
189
|
+
|
|
190
|
+
buffer = io.BytesIO()
|
|
191
|
+
img.save(buffer, format=img.format)
|
|
192
|
+
base64_data = base64.b64encode(buffer.getvalue()).decode("utf-8")
|
|
193
|
+
|
|
194
|
+
return ContentImageInline(content_type, base64_data)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def content_image_plot(
|
|
198
|
+
width: int = 768, height: int = 768, dpi: int = 72
|
|
199
|
+
) -> ContentImageInline:
|
|
200
|
+
"""
|
|
201
|
+
Encode the current matplotlib plot as an image for chat input.
|
|
202
|
+
|
|
203
|
+
This function captures the current matplotlib plot, resizes it to the specified
|
|
204
|
+
dimensions, and prepares it for chat input.
|
|
205
|
+
|
|
206
|
+
Parameters
|
|
207
|
+
----------
|
|
208
|
+
width
|
|
209
|
+
The desired width of the output image in pixels.
|
|
210
|
+
height
|
|
211
|
+
The desired height of the output image in pixels.
|
|
212
|
+
dpi
|
|
213
|
+
The DPI (dots per inch) of the output image.
|
|
214
|
+
|
|
215
|
+
Returns
|
|
216
|
+
-------
|
|
217
|
+
[](`~chatlas.types.Content`)
|
|
218
|
+
Content suitable for a [](`~chatlas.Turn`) object.
|
|
219
|
+
|
|
220
|
+
Raises
|
|
221
|
+
------
|
|
222
|
+
ValueError
|
|
223
|
+
If width or height is not a positive integer.
|
|
224
|
+
|
|
225
|
+
Examples
|
|
226
|
+
--------
|
|
227
|
+
|
|
228
|
+
```python
|
|
229
|
+
from chatlas import ChatOpenAI, content_image_plot
|
|
230
|
+
import matplotlib.pyplot as plt
|
|
231
|
+
|
|
232
|
+
plt.scatter(faithful["eruptions"], faithful["waiting"])
|
|
233
|
+
chat = ChatOpenAI()
|
|
234
|
+
chat.chat(
|
|
235
|
+
"Describe this plot in one paragraph, as suitable for inclusion in "
|
|
236
|
+
"alt-text. You should briefly describe the plot type, the axes, and "
|
|
237
|
+
"2-5 major visual patterns.",
|
|
238
|
+
content_image_plot(),
|
|
239
|
+
)
|
|
240
|
+
```
|
|
241
|
+
"""
|
|
242
|
+
|
|
243
|
+
try:
|
|
244
|
+
import matplotlib.pyplot as plt
|
|
245
|
+
except ImportError:
|
|
246
|
+
raise ImportError(
|
|
247
|
+
"`content_image_plot()` requires the `matplotlib` package. "
|
|
248
|
+
"Install it with `pip install matplotlib`."
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
if not plt.get_fignums():
|
|
252
|
+
raise RuntimeError(
|
|
253
|
+
"No matplotlib figure to save. Please create one before calling `content_image_plot()`."
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
fig = plt.gcf()
|
|
257
|
+
size = fig.get_size_inches()
|
|
258
|
+
|
|
259
|
+
try:
|
|
260
|
+
fig.set_size_inches(width / dpi, height / dpi)
|
|
261
|
+
|
|
262
|
+
buf = io.BytesIO()
|
|
263
|
+
fig.savefig(buf, format="png", dpi=dpi, bbox_inches="tight")
|
|
264
|
+
buf.seek(0)
|
|
265
|
+
base64_data = base64.b64encode(buf.getvalue()).decode("utf-8")
|
|
266
|
+
return ContentImageInline("image/png", base64_data)
|
|
267
|
+
finally:
|
|
268
|
+
fig.set_size_inches(*size)
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
class MissingResizeWarning(RuntimeWarning):
|
|
272
|
+
pass
|
chatlas/_display.py
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from abc import ABC, abstractmethod
|
|
3
|
+
from typing import Any
|
|
4
|
+
from uuid import uuid4
|
|
5
|
+
|
|
6
|
+
from rich.live import Live
|
|
7
|
+
from rich.logging import RichHandler
|
|
8
|
+
|
|
9
|
+
from ._logging import logger
|
|
10
|
+
from ._typing_extensions import TypedDict
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class MarkdownDisplay(ABC):
|
|
14
|
+
@abstractmethod
|
|
15
|
+
def update(self, content: str):
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
@abstractmethod
|
|
19
|
+
def __enter__(self) -> "MarkdownDisplay":
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
@abstractmethod
|
|
23
|
+
def __exit__(self, exc_type, exc_value, traceback):
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class MockMarkdownDisplay(MarkdownDisplay):
|
|
28
|
+
def update(self, content: str):
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
def __enter__(self):
|
|
32
|
+
return self
|
|
33
|
+
|
|
34
|
+
def __exit__(self, exc_type, exc_value, traceback):
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class LiveMarkdownDisplay(MarkdownDisplay):
|
|
39
|
+
"""
|
|
40
|
+
Stream chunks of markdown into a rich-based live updating console.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def __init__(self, echo_options: "EchoOptions"):
|
|
44
|
+
from rich.console import Console
|
|
45
|
+
|
|
46
|
+
self.content: str = ""
|
|
47
|
+
self.live = Live(
|
|
48
|
+
auto_refresh=False,
|
|
49
|
+
vertical_overflow="visible",
|
|
50
|
+
console=Console(
|
|
51
|
+
**echo_options["rich_console"],
|
|
52
|
+
),
|
|
53
|
+
)
|
|
54
|
+
self._markdown_options = echo_options["rich_markdown"]
|
|
55
|
+
|
|
56
|
+
def update(self, content: str):
|
|
57
|
+
from rich.markdown import Markdown
|
|
58
|
+
|
|
59
|
+
self.content += content
|
|
60
|
+
self.live.update(
|
|
61
|
+
Markdown(
|
|
62
|
+
self.content,
|
|
63
|
+
**self._markdown_options,
|
|
64
|
+
),
|
|
65
|
+
refresh=True,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
def __enter__(self):
|
|
69
|
+
self.live.__enter__()
|
|
70
|
+
# Live() isn't smart enough to know to automatically display logs when
|
|
71
|
+
# when they get handled while it Live() is active.
|
|
72
|
+
# However, if the logging handler is a RichHandler, it can be told
|
|
73
|
+
# about the live console so it can add logs to the top of the Live console.
|
|
74
|
+
handlers = [*logging.getLogger().handlers, *logger.handlers]
|
|
75
|
+
for h in handlers:
|
|
76
|
+
if isinstance(h, RichHandler):
|
|
77
|
+
h.console = self.live.console
|
|
78
|
+
|
|
79
|
+
return self
|
|
80
|
+
|
|
81
|
+
def __exit__(self, exc_type, exc_value, traceback):
|
|
82
|
+
self.content = ""
|
|
83
|
+
return self.live.__exit__(exc_type, exc_value, traceback)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class IPyMarkdownDisplay(MarkdownDisplay):
|
|
87
|
+
"""
|
|
88
|
+
Stream chunks of markdown into an IPython notebook.
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
def __init__(self, echo_options: "EchoOptions"):
|
|
92
|
+
self.content: str = ""
|
|
93
|
+
self._css_styles = echo_options["css_styles"]
|
|
94
|
+
|
|
95
|
+
def update(self, content: str):
|
|
96
|
+
from IPython.display import Markdown, update_display
|
|
97
|
+
|
|
98
|
+
self.content += content
|
|
99
|
+
update_display(
|
|
100
|
+
Markdown(self.content),
|
|
101
|
+
display_id=self._ipy_display_id,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
def _init_display(self) -> str:
|
|
105
|
+
try:
|
|
106
|
+
from IPython.display import HTML, Markdown, display
|
|
107
|
+
except ImportError:
|
|
108
|
+
raise ImportError(
|
|
109
|
+
"The IPython package is required for displaying content in a Jupyter notebook. "
|
|
110
|
+
"Install it with `pip install ipython`."
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
if self._css_styles:
|
|
114
|
+
id_ = uuid4().hex
|
|
115
|
+
css = "".join(f"{k}: {v}; " for k, v in self._css_styles.items())
|
|
116
|
+
display(HTML(f"<style>#{id_} + .chatlas-markdown {{ {css} }}</style>"))
|
|
117
|
+
display(HTML(f"<div id='{id_}' class='chatlas-markdown'>"))
|
|
118
|
+
else:
|
|
119
|
+
# Unfortunately, there doesn't seem to be a proper way to wrap
|
|
120
|
+
# Markdown() in a div?
|
|
121
|
+
display(HTML("<div class='chatlas-markdown'>"))
|
|
122
|
+
|
|
123
|
+
handle = display(Markdown(""), display_id=True)
|
|
124
|
+
if handle is None:
|
|
125
|
+
raise ValueError("Failed to create display handle")
|
|
126
|
+
return handle.display_id
|
|
127
|
+
|
|
128
|
+
def __enter__(self):
|
|
129
|
+
self._ipy_display_id = self._init_display()
|
|
130
|
+
return self
|
|
131
|
+
|
|
132
|
+
def __exit__(self, exc_type, exc_value, traceback):
|
|
133
|
+
self._ipy_display_id = None
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class EchoOptions(TypedDict):
|
|
137
|
+
rich_markdown: dict[str, Any]
|
|
138
|
+
rich_console: dict[str, Any]
|
|
139
|
+
css_styles: dict[str, str]
|