Skip to content

Commit 3c2c85b

Browse files
committed
Merge branch 'main' into surface-tool-calls
2 parents 3a1bd9c + 5af62b2 commit 3c2c85b

File tree

8 files changed

+428
-4
lines changed

8 files changed

+428
-4
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1212
### New features
1313

1414
* Added a `ChatSnowflake()` class to interact with [Snowflake Cortex LLM](https://docs.snowflake.com/en/user-guide/snowflake-cortex/llm-functions). (#54)
15+
* Added a `ChatAuto()` class, allowing for configuration of chat providers and models via environment variables. (#38, thanks @mconflitti-pbc)
1516

1617
### Improvements
1718

@@ -21,6 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2122

2223
### Bug fixes
2324

25+
* Fixed an issue with content getting duplicated when it overflows in a `Live()` console. (#71)
2426
* Fix an issue with tool calls not working with `ChatVertex()`. (#61)
2527

2628

chatlas/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from . import types
22
from ._anthropic import ChatAnthropic, ChatBedrockAnthropic
3+
from ._auto import ChatAuto
34
from ._chat import Chat
45
from ._content_image import content_image_file, content_image_plot, content_image_url
56
from ._github import ChatGithub
@@ -22,6 +23,7 @@
2223

2324
__all__ = (
2425
"ChatAnthropic",
26+
"ChatAuto",
2527
"ChatBedrockAnthropic",
2628
"ChatGithub",
2729
"ChatGoogle",

chatlas/_auto.py

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
from __future__ import annotations
2+
3+
import json
4+
import os
5+
from typing import Callable, Literal, Optional
6+
7+
from ._anthropic import ChatAnthropic, ChatBedrockAnthropic
8+
from ._chat import Chat
9+
from ._github import ChatGithub
10+
from ._google import ChatGoogle, ChatVertex
11+
from ._groq import ChatGroq
12+
from ._ollama import ChatOllama
13+
from ._openai import ChatAzureOpenAI, ChatOpenAI
14+
from ._perplexity import ChatPerplexity
15+
from ._snowflake import ChatSnowflake
16+
from ._turn import Turn
17+
18+
AutoProviders = Literal[
19+
"anthropic",
20+
"bedrock-anthropic",
21+
"github",
22+
"google",
23+
"groq",
24+
"ollama",
25+
"openai",
26+
"azure-openai",
27+
"perplexity",
28+
"snowflake",
29+
"vertex",
30+
]
31+
32+
_provider_chat_model_map: dict[AutoProviders, Callable[..., Chat]] = {
33+
"anthropic": ChatAnthropic,
34+
"bedrock-anthropic": ChatBedrockAnthropic,
35+
"github": ChatGithub,
36+
"google": ChatGoogle,
37+
"groq": ChatGroq,
38+
"ollama": ChatOllama,
39+
"openai": ChatOpenAI,
40+
"azure-openai": ChatAzureOpenAI,
41+
"perplexity": ChatPerplexity,
42+
"snowflake": ChatSnowflake,
43+
"vertex": ChatVertex,
44+
}
45+
46+
47+
def ChatAuto(
48+
system_prompt: Optional[str] = None,
49+
turns: Optional[list[Turn]] = None,
50+
*,
51+
provider: Optional[AutoProviders] = None,
52+
model: Optional[str] = None,
53+
**kwargs,
54+
) -> Chat:
55+
"""
56+
Use environment variables (env vars) to configure the Chat provider and model.
57+
58+
Creates a `:class:~chatlas.Chat` instance based on the specified provider.
59+
The provider may be specified through the `provider` parameter and/or the
60+
`CHATLAS_CHAT_PROVIDER` env var. If both are set, the env var takes
61+
precedence. Similarly, the provider's model may be specified through the
62+
`model` parameter and/or the `CHATLAS_CHAT_MODEL` env var. Also, additional
63+
configuration may be provided through the `kwargs` parameter and/or the
64+
`CHATLAS_CHAT_ARGS` env var (as a JSON string). In this case, when both are
65+
set, they are merged, with the env var arguments taking precedence.
66+
67+
As a result, `ChatAuto()` provides a convenient way to set a default
68+
provider and model in your Python code, while allowing you to override
69+
these settings through env vars (i.e., without modifying your code).
70+
71+
Prerequisites
72+
-------------
73+
74+
::: {.callout-note}
75+
## API key
76+
77+
Follow the instructions for the specific provider to obtain an API key.
78+
:::
79+
80+
::: {.callout-note}
81+
## Python requirements
82+
83+
Follow the instructions for the specific provider to install the required
84+
Python packages.
85+
:::
86+
87+
88+
Examples
89+
--------
90+
First, set the environment variables for the provider, arguments, and API key:
91+
92+
```bash
93+
export CHATLAS_CHAT_PROVIDER=anthropic
94+
export CHATLAS_CHAT_MODEL=claude-3-haiku-20240229
95+
export CHATLAS_CHAT_ARGS='{"kwargs": {"max_retries": 3}}'
96+
export ANTHROPIC_API_KEY=your_api_key
97+
```
98+
99+
Then, you can use the `ChatAuto` function to create a Chat instance:
100+
101+
```python
102+
from chatlas import ChatAuto
103+
104+
chat = ChatAuto()
105+
chat.chat("What is the capital of France?")
106+
```
107+
108+
Parameters
109+
----------
110+
provider
111+
The name of the default chat provider to use. Providers are strings
112+
formatted in kebab-case, e.g. to use `ChatBedrockAnthropic` set
113+
`provider="bedrock-anthropic"`.
114+
115+
This value can also be provided via the `CHATLAS_CHAT_PROVIDER`
116+
environment variable, which takes precedence over `provider`
117+
when set.
118+
model
119+
The name of the default model to use. This value can also be provided
120+
via the `CHATLAS_CHAT_MODEL` environment variable, which takes
121+
precedence over `model` when set.
122+
system_prompt
123+
A system prompt to set the behavior of the assistant.
124+
turns
125+
A list of turns to start the chat with (i.e., continuing a previous
126+
conversation). If not provided, the conversation begins from scratch. Do
127+
not provide non-`None` values for both `turns` and `system_prompt`. Each
128+
message in the list should be a dictionary with at least `role` (usually
129+
`system`, `user`, or `assistant`, but `tool` is also possible). Normally
130+
there is also a `content` field, which is a string.
131+
**kwargs
132+
Additional keyword arguments to pass to the Chat constructor. See the
133+
documentation for each provider for more details on the available
134+
options.
135+
136+
These arguments can also be provided via the `CHATLAS_CHAT_ARGS`
137+
environment variable as a JSON string. When provided, the options
138+
in the `CHATLAS_CHAT_ARGS` envvar take precedence over the options
139+
passed to `kwargs`.
140+
141+
Note that `system_prompt` and `turns` in `kwargs` or in
142+
`CHATLAS_CHAT_ARGS` are ignored.
143+
144+
Returns
145+
-------
146+
Chat
147+
A chat instance using the specified provider.
148+
149+
Raises
150+
------
151+
ValueError
152+
If no valid provider is specified either through parameters or
153+
environment variables.
154+
"""
155+
the_provider = os.environ.get("CHATLAS_CHAT_PROVIDER", provider)
156+
157+
if the_provider is None:
158+
raise ValueError(
159+
"Provider name is required as parameter or `CHATLAS_CHAT_PROVIDER` must be set."
160+
)
161+
if the_provider not in _provider_chat_model_map:
162+
raise ValueError(
163+
f"Provider name '{the_provider}' is not a known chatlas provider: "
164+
f"{', '.join(_provider_chat_model_map.keys())}"
165+
)
166+
167+
# `system_prompt` and `turns` always come from `ChatAuto()`
168+
base_args = {"system_prompt": system_prompt, "turns": turns}
169+
170+
if env_model := os.environ.get("CHATLAS_CHAT_MODEL"):
171+
model = env_model
172+
173+
if model:
174+
base_args["model"] = model
175+
176+
env_kwargs = {}
177+
if env_kwargs_str := os.environ.get("CHATLAS_CHAT_ARGS"):
178+
env_kwargs = json.loads(env_kwargs_str)
179+
180+
kwargs = {**kwargs, **env_kwargs, **base_args}
181+
kwargs = {k: v for k, v in kwargs.items() if v is not None}
182+
183+
return _provider_chat_model_map[the_provider](**kwargs)

chatlas/_display.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from rich.live import Live
77
from rich.logging import RichHandler
88

9+
from ._live_render import LiveRender
910
from ._logging import logger
1011
from ._typing_extensions import TypedDict
1112

@@ -44,13 +45,22 @@ def __init__(self, echo_options: "EchoOptions"):
4445
from rich.console import Console
4546

4647
self.content: str = ""
47-
self.live = Live(
48+
live = Live(
4849
auto_refresh=False,
49-
vertical_overflow="visible",
5050
console=Console(
5151
**echo_options["rich_console"],
5252
),
5353
)
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+
5464
self._markdown_options = echo_options["rich_markdown"]
5565

5666
def update(self, content: str):

chatlas/_live_render.py

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

docs/_quarto.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ quartodoc:
7777
desc: Start a chat with a particular large language model (llm) provider.
7878
contents:
7979
- ChatAnthropic
80+
- ChatAuto
8081
- ChatAzureOpenAI
8182
- ChatBedrockAnthropic
8283
- ChatGithub

0 commit comments

Comments
 (0)