shinychat 0.0.1a2__py3-none-any.whl → 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.
- shinychat/__init__.py +11 -2
- shinychat/__version.py +16 -3
- shinychat/_chat.py +88 -107
- shinychat/_chat_normalize.py +252 -287
- shinychat/_chat_normalize_chatlas.py +449 -0
- shinychat/_chat_provider_types.py +11 -3
- shinychat/_chat_types.py +9 -2
- shinychat/_markdown_stream.py +9 -5
- shinychat/express/__init__.py +2 -1
- shinychat/playwright/__init__.py +25 -0
- shinychat/playwright/_chat.py +42 -3
- shinychat/py.typed +0 -0
- shinychat/types/__init__.py +10 -0
- shinychat/www/GIT_VERSION +1 -1
- shinychat/www/chat/chat.css +1 -1
- shinychat/www/chat/chat.css.map +2 -2
- shinychat/www/chat/chat.js +101 -22
- shinychat/www/chat/chat.js.map +4 -4
- shinychat/www/markdown-stream/markdown-stream.js +125 -46
- shinychat/www/markdown-stream/markdown-stream.js.map +4 -4
- {shinychat-0.0.1a2.dist-info → shinychat-0.2.0.dist-info}/METADATA +10 -9
- shinychat-0.2.0.dist-info/RECORD +31 -0
- shinychat-0.0.1a2.dist-info/RECORD +0 -28
- {shinychat-0.0.1a2.dist-info → shinychat-0.2.0.dist-info}/WHEEL +0 -0
- {shinychat-0.0.1a2.dist-info → shinychat-0.2.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,449 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import json
|
4
|
+
import os
|
5
|
+
import warnings
|
6
|
+
from typing import TYPE_CHECKING, Any, Literal, Optional, Sequence, Union
|
7
|
+
|
8
|
+
from htmltools import (
|
9
|
+
HTML,
|
10
|
+
MetadataNode,
|
11
|
+
RenderedHTML,
|
12
|
+
ReprHtml,
|
13
|
+
Tag,
|
14
|
+
Tagifiable,
|
15
|
+
TagList,
|
16
|
+
)
|
17
|
+
from packaging import version
|
18
|
+
from pydantic import BaseModel, field_serializer, field_validator
|
19
|
+
from typing_extensions import TypeAliasType
|
20
|
+
|
21
|
+
from ._typing_extensions import TypeGuard
|
22
|
+
|
23
|
+
if TYPE_CHECKING:
|
24
|
+
from chatlas.types import ContentToolRequest, ContentToolResult
|
25
|
+
|
26
|
+
__all__ = [
|
27
|
+
"ToolResultDisplay",
|
28
|
+
]
|
29
|
+
|
30
|
+
# A version of the (recursive) TagChild type that actually works with Pydantic
|
31
|
+
# https://docs.pydantic.dev/2.11/concepts/types/#named-type-aliases
|
32
|
+
TagNode = Union[Tagifiable, MetadataNode, ReprHtml, str, HTML]
|
33
|
+
TagChild = TypeAliasType(
|
34
|
+
"TagChild",
|
35
|
+
"Union[TagNode, TagList, float, None, Sequence[TagChild]]",
|
36
|
+
)
|
37
|
+
|
38
|
+
|
39
|
+
class ToolCardComponent(BaseModel):
|
40
|
+
"A class that mirrors the ShinyToolCard component class in chat-tools.ts"
|
41
|
+
|
42
|
+
request_id: str
|
43
|
+
"""
|
44
|
+
Unique identifier for the tool request or result.
|
45
|
+
This value links a request to a result and is therefore not unique on the page.
|
46
|
+
"""
|
47
|
+
|
48
|
+
tool_name: str
|
49
|
+
"Name of the tool being executed, e.g. `get_weather`."
|
50
|
+
|
51
|
+
tool_title: Optional[str] = None
|
52
|
+
"Display title for the card. If not provided, falls back to `tool_name`."
|
53
|
+
|
54
|
+
icon: TagChild = None
|
55
|
+
"HTML content for the icon displayed in the card header."
|
56
|
+
|
57
|
+
intent: Optional[str] = None
|
58
|
+
"Optional intent description explaining the purpose of the tool execution."
|
59
|
+
|
60
|
+
expanded: bool = False
|
61
|
+
"Controls whether the card content is expanded/visible."
|
62
|
+
|
63
|
+
model_config = {"arbitrary_types_allowed": True}
|
64
|
+
|
65
|
+
@field_serializer("icon")
|
66
|
+
def _serialize_icon(self, value: TagChild):
|
67
|
+
return TagList(value).render()
|
68
|
+
|
69
|
+
@field_validator("icon", mode="before")
|
70
|
+
@classmethod
|
71
|
+
def _validate_icon(cls, value: TagChild) -> TagChild:
|
72
|
+
if isinstance(value, dict):
|
73
|
+
return restore_rendered_html(value)
|
74
|
+
else:
|
75
|
+
return value
|
76
|
+
|
77
|
+
|
78
|
+
class ToolRequestComponent(ToolCardComponent):
|
79
|
+
"A class that mirrors the ShinyToolRequest component class from chat-tools.ts"
|
80
|
+
|
81
|
+
arguments: str = ""
|
82
|
+
"The function arguments as requested by the LLM, typically in JSON format."
|
83
|
+
|
84
|
+
def tagify(self):
|
85
|
+
icon_ui = TagList(self.icon).render()
|
86
|
+
|
87
|
+
return Tag(
|
88
|
+
"shiny-tool-request",
|
89
|
+
request_id=self.request_id,
|
90
|
+
tool_name=self.tool_name,
|
91
|
+
tool_title=self.tool_title,
|
92
|
+
icon=icon_ui["html"] if self.icon else None,
|
93
|
+
intent=self.intent,
|
94
|
+
expanded="" if self.expanded else None,
|
95
|
+
arguments=self.arguments,
|
96
|
+
*icon_ui["dependencies"],
|
97
|
+
)
|
98
|
+
|
99
|
+
|
100
|
+
ValueType = Literal["html", "markdown", "text", "code"]
|
101
|
+
|
102
|
+
|
103
|
+
class ToolResultComponent(ToolCardComponent):
|
104
|
+
"A class that mirrors the ShinyToolResult component class from chat-tools.ts"
|
105
|
+
|
106
|
+
request_call: str = ""
|
107
|
+
"The original tool call that generated this result. Used to display the tool invocation."
|
108
|
+
|
109
|
+
status: Literal["success", "error"] = "success"
|
110
|
+
"""
|
111
|
+
The status of the tool execution. When set to "error", displays in an error state with
|
112
|
+
red text and an exclamation icon.
|
113
|
+
"""
|
114
|
+
|
115
|
+
show_request: bool = True
|
116
|
+
"Should the tool request should be displayed alongside the result?"
|
117
|
+
|
118
|
+
value: TagChild = None
|
119
|
+
"The actual result content returned by the tool execution."
|
120
|
+
|
121
|
+
value_type: ValueType = "code"
|
122
|
+
"""
|
123
|
+
Specifies how the value should be rendered. Supported types:
|
124
|
+
- "html": Renders the value as raw HTML
|
125
|
+
- "text": Renders the value as plain text in a paragraph
|
126
|
+
- "markdown": Renders the value as Markdown (default)
|
127
|
+
- "code": Renders the value as a code block
|
128
|
+
Any other value defaults to markdown rendering.
|
129
|
+
"""
|
130
|
+
|
131
|
+
def tagify(self):
|
132
|
+
icon_ui = TagList(self.icon).render()
|
133
|
+
|
134
|
+
if self.value_type == "html":
|
135
|
+
value_ui = TagList(self.value).render()
|
136
|
+
else:
|
137
|
+
value_ui: "RenderedHTML" = {
|
138
|
+
"html": str(self.value),
|
139
|
+
"dependencies": [],
|
140
|
+
}
|
141
|
+
|
142
|
+
return Tag(
|
143
|
+
"shiny-tool-result",
|
144
|
+
request_id=self.request_id,
|
145
|
+
tool_name=self.tool_name,
|
146
|
+
tool_title=self.tool_title,
|
147
|
+
icon=icon_ui["html"] if self.icon else None,
|
148
|
+
intent=self.intent,
|
149
|
+
request_call=self.request_call,
|
150
|
+
status=self.status,
|
151
|
+
value=value_ui["html"],
|
152
|
+
value_type=self.value_type,
|
153
|
+
show_request="" if self.show_request else None,
|
154
|
+
expanded="" if self.expanded else None,
|
155
|
+
*icon_ui["dependencies"],
|
156
|
+
*value_ui["dependencies"],
|
157
|
+
)
|
158
|
+
|
159
|
+
|
160
|
+
class ToolResultDisplay(BaseModel):
|
161
|
+
"""
|
162
|
+
Customize how tool results are displayed.
|
163
|
+
|
164
|
+
Assign a `ToolResultDisplay` instance to a
|
165
|
+
[`chatlas.ContentToolResult`](https://posit-dev.github.io/chatlas/reference/types.ContentToolResult.html)
|
166
|
+
to customize the UI shown to the user when tool calls occur.
|
167
|
+
|
168
|
+
Examples
|
169
|
+
--------
|
170
|
+
|
171
|
+
```python
|
172
|
+
import chatlas as ctl
|
173
|
+
from shinychat.types import ToolResultDisplay
|
174
|
+
|
175
|
+
|
176
|
+
def my_tool():
|
177
|
+
display = ToolResultDisplay(
|
178
|
+
title="Tool result title",
|
179
|
+
markdown="A _markdown_ message shown to user.",
|
180
|
+
)
|
181
|
+
return ctl.ContentToolResult(
|
182
|
+
value="Value the model sees",
|
183
|
+
extra={"display": display},
|
184
|
+
)
|
185
|
+
|
186
|
+
|
187
|
+
chat_client = ctl.ChatAuto()
|
188
|
+
chat_client.register_tool(my_tool)
|
189
|
+
```
|
190
|
+
|
191
|
+
Parameters
|
192
|
+
---------
|
193
|
+
title
|
194
|
+
The title to display in the header of the tool result.
|
195
|
+
icon
|
196
|
+
An icon to display in the header (alongside the title).
|
197
|
+
show_request
|
198
|
+
Whether to show the tool request inside the tool result container.
|
199
|
+
open
|
200
|
+
Whether or not the tool result details are expanded by default.
|
201
|
+
html
|
202
|
+
Custom HTML content (to use in place of the default result display).
|
203
|
+
markdown
|
204
|
+
Custom Markdown string (to use in place of the default result display).
|
205
|
+
text
|
206
|
+
Custom plain text string (to use in place of the default result display).
|
207
|
+
"""
|
208
|
+
|
209
|
+
title: Optional[str] = None
|
210
|
+
icon: TagChild = None
|
211
|
+
html: TagChild = None
|
212
|
+
show_request: bool = True
|
213
|
+
open: bool = False
|
214
|
+
markdown: Optional[str] = None
|
215
|
+
text: Optional[str] = None
|
216
|
+
|
217
|
+
model_config = {"arbitrary_types_allowed": True}
|
218
|
+
|
219
|
+
@field_serializer("html", "icon")
|
220
|
+
def _serialize_html_icon(self, value: TagChild):
|
221
|
+
return TagList(value).render()
|
222
|
+
|
223
|
+
@field_validator("html", "icon", mode="before")
|
224
|
+
@classmethod
|
225
|
+
def _validate_html_icon(cls, value: TagChild) -> TagChild:
|
226
|
+
if isinstance(value, dict):
|
227
|
+
return restore_rendered_html(value)
|
228
|
+
else:
|
229
|
+
return value
|
230
|
+
|
231
|
+
|
232
|
+
def tool_request_contents(x: "ContentToolRequest") -> Tagifiable:
|
233
|
+
if tool_display_override() == "none":
|
234
|
+
return TagList()
|
235
|
+
|
236
|
+
# These content objects do have tagify() methods,
|
237
|
+
# but that's for legacy behavior
|
238
|
+
if is_legacy():
|
239
|
+
return x
|
240
|
+
|
241
|
+
intent = None
|
242
|
+
if isinstance(x.arguments, dict):
|
243
|
+
intent = x.arguments.get("_intent")
|
244
|
+
|
245
|
+
tool_title = None
|
246
|
+
if x.tool and x.tool.annotations:
|
247
|
+
tool_title = x.tool.annotations.get("title")
|
248
|
+
|
249
|
+
return ToolRequestComponent(
|
250
|
+
request_id=x.id,
|
251
|
+
tool_name=x.name,
|
252
|
+
arguments=json.dumps(x.arguments),
|
253
|
+
intent=intent,
|
254
|
+
tool_title=tool_title,
|
255
|
+
)
|
256
|
+
|
257
|
+
|
258
|
+
def tool_result_contents(x: "ContentToolResult") -> Tagifiable:
|
259
|
+
if tool_display_override() == "none":
|
260
|
+
return TagList()
|
261
|
+
|
262
|
+
# These content objects do have tagify() methods,
|
263
|
+
# but that's the legacy behavior
|
264
|
+
if is_legacy():
|
265
|
+
return x
|
266
|
+
|
267
|
+
if x.request is None:
|
268
|
+
raise ValueError(
|
269
|
+
"`ContentToolResult` objects must have an associated `.request` attribute."
|
270
|
+
)
|
271
|
+
|
272
|
+
# TODO: look into better formating of the call?
|
273
|
+
request_call = json.dumps(
|
274
|
+
{
|
275
|
+
"id": x.id,
|
276
|
+
"name": x.request.name,
|
277
|
+
"arguments": x.request.arguments,
|
278
|
+
},
|
279
|
+
indent=2,
|
280
|
+
)
|
281
|
+
|
282
|
+
display = get_tool_result_display(x, x.request)
|
283
|
+
value, value_type = tool_result_display(x, display)
|
284
|
+
|
285
|
+
intent = None
|
286
|
+
if isinstance(x.arguments, dict):
|
287
|
+
intent = x.arguments.get("_intent")
|
288
|
+
|
289
|
+
tool = x.request.tool
|
290
|
+
tool_title = None
|
291
|
+
icon = None
|
292
|
+
if tool and tool.annotations:
|
293
|
+
tool_title = tool.annotations.get("title")
|
294
|
+
icon = tool.annotations.get("extra", {}).get("icon")
|
295
|
+
icon = icon or tool.annotations.get("icon")
|
296
|
+
|
297
|
+
# Icon strings and HTML display never get escaped
|
298
|
+
icon = display.icon or icon
|
299
|
+
if icon and isinstance(icon, str):
|
300
|
+
icon = HTML(icon)
|
301
|
+
if value_type == "html" and isinstance(value, str):
|
302
|
+
value = HTML(value)
|
303
|
+
|
304
|
+
# display (tool *result* level) takes precedence over
|
305
|
+
# annotations (tool *definition* level)
|
306
|
+
return ToolResultComponent(
|
307
|
+
request_id=x.id,
|
308
|
+
request_call=request_call,
|
309
|
+
tool_name=x.request.name,
|
310
|
+
tool_title=display.title or tool_title,
|
311
|
+
status="success" if x.error is None else "error",
|
312
|
+
value=value,
|
313
|
+
value_type=value_type,
|
314
|
+
icon=icon,
|
315
|
+
intent=intent,
|
316
|
+
show_request=display.show_request,
|
317
|
+
expanded=display.open,
|
318
|
+
)
|
319
|
+
|
320
|
+
|
321
|
+
def get_tool_result_display(
|
322
|
+
x: "ContentToolResult",
|
323
|
+
request: "ContentToolRequest",
|
324
|
+
) -> ToolResultDisplay:
|
325
|
+
if not isinstance(x.extra, dict) or tool_display_override() == "basic":
|
326
|
+
return ToolResultDisplay()
|
327
|
+
|
328
|
+
display = x.extra.get("display", ToolResultDisplay())
|
329
|
+
|
330
|
+
if isinstance(display, ToolResultDisplay):
|
331
|
+
return display
|
332
|
+
|
333
|
+
if isinstance(display, dict):
|
334
|
+
return ToolResultDisplay(**display)
|
335
|
+
|
336
|
+
warnings.warn(
|
337
|
+
"Invalid `display` value inside `ContentToolResult(extra={'display': display})` "
|
338
|
+
f"from {request.name} (call id: {request.id}). "
|
339
|
+
"Expected either a `shinychat.ToolResultDisplay()` instance or a dictionary, "
|
340
|
+
f"but got {type(display)}."
|
341
|
+
)
|
342
|
+
|
343
|
+
return ToolResultDisplay()
|
344
|
+
|
345
|
+
|
346
|
+
def tool_result_display(
|
347
|
+
x: "ContentToolResult",
|
348
|
+
display: ToolResultDisplay,
|
349
|
+
) -> tuple[TagChild, ValueType]:
|
350
|
+
if x.error is not None:
|
351
|
+
return str(x.error), "code"
|
352
|
+
|
353
|
+
if tool_display_override() == "basic":
|
354
|
+
return str(x.get_model_value()), "code"
|
355
|
+
|
356
|
+
if display.html is not None:
|
357
|
+
return display.html, "html"
|
358
|
+
|
359
|
+
if display.markdown is not None:
|
360
|
+
return display.markdown, "markdown"
|
361
|
+
|
362
|
+
if display.text is not None:
|
363
|
+
return display.text, "text"
|
364
|
+
|
365
|
+
return str(x.get_model_value()), "code"
|
366
|
+
|
367
|
+
|
368
|
+
async def hide_corresponding_request(x: "ContentToolResult"):
|
369
|
+
if x.request is None:
|
370
|
+
return
|
371
|
+
|
372
|
+
session = None
|
373
|
+
try:
|
374
|
+
from shiny.session import get_current_session
|
375
|
+
|
376
|
+
session = get_current_session()
|
377
|
+
except Exception:
|
378
|
+
return
|
379
|
+
|
380
|
+
if session is None:
|
381
|
+
return
|
382
|
+
|
383
|
+
await session.send_custom_message(
|
384
|
+
"shiny-tool-request-hide",
|
385
|
+
x.request.id, # type: ignore
|
386
|
+
)
|
387
|
+
|
388
|
+
|
389
|
+
def is_tool_result(val: object) -> "TypeGuard[ContentToolResult]":
|
390
|
+
try:
|
391
|
+
from chatlas.types import ContentToolResult
|
392
|
+
|
393
|
+
return isinstance(val, ContentToolResult)
|
394
|
+
except ImportError:
|
395
|
+
return False
|
396
|
+
|
397
|
+
|
398
|
+
# Tools started getting added to ContentToolRequest staring with 0.11.1
|
399
|
+
def is_legacy():
|
400
|
+
import chatlas
|
401
|
+
|
402
|
+
v = chatlas._version.version_tuple
|
403
|
+
ver = f"{v[0]}.{v[1]}.{v[2]}"
|
404
|
+
return version.parse(ver) < version.parse("0.11.1")
|
405
|
+
|
406
|
+
|
407
|
+
def tool_display_override() -> Literal["none", "basic", "rich"]:
|
408
|
+
val = os.getenv("SHINYCHAT_TOOL_DISPLAY", "rich")
|
409
|
+
if val == "rich" or val == "basic" or val == "none":
|
410
|
+
return val
|
411
|
+
else:
|
412
|
+
raise ValueError(
|
413
|
+
'The `SHINYCHAT_TOOL_DISPLAY` env var must be one of: "none", "basic", or "rich"'
|
414
|
+
)
|
415
|
+
|
416
|
+
|
417
|
+
def restore_rendered_html(x: dict[str, Any]):
|
418
|
+
from htmltools import HTMLDependency
|
419
|
+
|
420
|
+
if "html" not in x or "dependencies" not in x:
|
421
|
+
raise ValueError(f"Don't know how to restore HTML from {x}")
|
422
|
+
|
423
|
+
deps: list[HTMLDependency] = []
|
424
|
+
for d in x["dependencies"]:
|
425
|
+
if not isinstance(d, dict):
|
426
|
+
continue
|
427
|
+
name = d["name"]
|
428
|
+
version = d["version"]
|
429
|
+
other = {k: v for k, v in d.items() if k not in ("name", "version")}
|
430
|
+
# TODO: warn if the source is a tempdir?
|
431
|
+
deps.append(HTMLDependency(name=name, version=version, **other))
|
432
|
+
|
433
|
+
res = TagList(HTML(x["html"]), *deps)
|
434
|
+
if not deps:
|
435
|
+
return res
|
436
|
+
|
437
|
+
session = None
|
438
|
+
try:
|
439
|
+
from shiny.session import get_current_session
|
440
|
+
|
441
|
+
session = get_current_session()
|
442
|
+
except Exception:
|
443
|
+
pass
|
444
|
+
|
445
|
+
# De-dupe dependencies for the current Shiny session
|
446
|
+
if session:
|
447
|
+
session._process_ui(res)
|
448
|
+
|
449
|
+
return res
|
@@ -4,7 +4,9 @@ from typing import TYPE_CHECKING, Literal, Union
|
|
4
4
|
from ._chat_types import ChatMessageDict
|
5
5
|
|
6
6
|
if TYPE_CHECKING:
|
7
|
-
from anthropic.types import
|
7
|
+
from anthropic.types import ( # pyright: ignore[reportMissingImports]
|
8
|
+
MessageParam as AnthropicMessage,
|
9
|
+
)
|
8
10
|
from langchain_core.messages import AIMessage, HumanMessage, SystemMessage
|
9
11
|
from ollama import Message as OllamaMessage
|
10
12
|
from openai.types.chat import (
|
@@ -28,7 +30,11 @@ if TYPE_CHECKING:
|
|
28
30
|
]
|
29
31
|
|
30
32
|
ProviderMessage = Union[
|
31
|
-
AnthropicMessage,
|
33
|
+
AnthropicMessage,
|
34
|
+
GoogleMessage,
|
35
|
+
LangChainMessage,
|
36
|
+
OpenAIMessage,
|
37
|
+
OllamaMessage,
|
32
38
|
]
|
33
39
|
else:
|
34
40
|
AnthropicMessage = GoogleMessage = LangChainMessage = OpenAIMessage = (
|
@@ -63,7 +69,9 @@ def as_provider_message(
|
|
63
69
|
|
64
70
|
|
65
71
|
def as_anthropic_message(message: ChatMessageDict) -> "AnthropicMessage":
|
66
|
-
from anthropic.types import
|
72
|
+
from anthropic.types import ( # pyright: ignore[reportMissingImports]
|
73
|
+
MessageParam as AnthropicMessage,
|
74
|
+
)
|
67
75
|
|
68
76
|
if message["role"] == "system":
|
69
77
|
raise ValueError(
|
shinychat/_chat_types.py
CHANGED
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|
3
3
|
from dataclasses import dataclass
|
4
4
|
from typing import Literal, TypedDict
|
5
5
|
|
6
|
-
from htmltools import HTML, TagChild
|
6
|
+
from htmltools import HTML, Tag, TagChild, Tagifiable, TagList
|
7
7
|
from shiny.session import require_active_session
|
8
8
|
|
9
9
|
from ._typing_extensions import NotRequired
|
@@ -22,10 +22,12 @@ class ChatMessage:
|
|
22
22
|
def __init__(
|
23
23
|
self,
|
24
24
|
content: TagChild,
|
25
|
-
role: Role,
|
25
|
+
role: Role = "assistant",
|
26
26
|
):
|
27
27
|
self.role: Role = role
|
28
28
|
|
29
|
+
is_html = isinstance(content, (Tag, TagList, HTML, Tagifiable))
|
30
|
+
|
29
31
|
# content _can_ be a TagChild, but it's most likely just a string (of
|
30
32
|
# markdown), so only process it if it's not a string.
|
31
33
|
deps = []
|
@@ -35,6 +37,11 @@ class ChatMessage:
|
|
35
37
|
content = res["html"]
|
36
38
|
deps = res["deps"]
|
37
39
|
|
40
|
+
if is_html:
|
41
|
+
# Code blocks with `{=html}` infostrings are rendered as-is by a
|
42
|
+
# custom rendering method in markdown-stream.ts
|
43
|
+
content = f"\n\n````````{{=html}}\n{content}\n````````\n\n"
|
44
|
+
|
38
45
|
self.content = content
|
39
46
|
self.html_deps = deps
|
40
47
|
|
shinychat/_markdown_stream.py
CHANGED
@@ -92,9 +92,9 @@ class MarkdownStream:
|
|
92
92
|
async def _mock_task() -> str:
|
93
93
|
return ""
|
94
94
|
|
95
|
-
self._latest_stream: reactive.Value[
|
96
|
-
reactive.
|
97
|
-
)
|
95
|
+
self._latest_stream: reactive.Value[
|
96
|
+
reactive.ExtendedTask[[], str]
|
97
|
+
] = reactive.Value(_mock_task)
|
98
98
|
|
99
99
|
async def stream(
|
100
100
|
self,
|
@@ -149,7 +149,9 @@ class MarkdownStream:
|
|
149
149
|
ui = self._session._process_ui(x)
|
150
150
|
|
151
151
|
result += ui["html"]
|
152
|
-
await self._send_content_message(
|
152
|
+
await self._send_content_message(
|
153
|
+
ui["html"], "append", ui["deps"]
|
154
|
+
)
|
153
155
|
|
154
156
|
return result
|
155
157
|
|
@@ -249,7 +251,9 @@ class MarkdownStream:
|
|
249
251
|
):
|
250
252
|
if self._session.is_stub_session():
|
251
253
|
return
|
252
|
-
await self._session.send_custom_message(
|
254
|
+
await self._session.send_custom_message(
|
255
|
+
"shinyMarkdownStreamMessage", {**msg}
|
256
|
+
)
|
253
257
|
|
254
258
|
async def _raise_exception(self, e: BaseException):
|
255
259
|
if self.on_error == "unhandled":
|
shinychat/express/__init__.py
CHANGED
shinychat/playwright/__init__.py
CHANGED
@@ -1,3 +1,28 @@
|
|
1
|
+
try:
|
2
|
+
import playwright # noqa: F401 # pyright: ignore[reportUnusedImport, reportMissingTypeStubs]
|
3
|
+
except ImportError:
|
4
|
+
raise ImportError(
|
5
|
+
"The shinychat.playwright module requires the playwright package to be installed. "
|
6
|
+
"Please install it with: `pip install playwright`"
|
7
|
+
)
|
8
|
+
|
9
|
+
# If `pytest` is installed...
|
10
|
+
try:
|
11
|
+
import pytest # noqa: F401 # pyright: ignore[reportUnusedImport, reportMissingTypeStubs]
|
12
|
+
|
13
|
+
# At this point, `playwright` and `pytest` are installed.
|
14
|
+
# Try to make sure `pytest-playwright` is installed
|
15
|
+
try:
|
16
|
+
import pytest_playwright # noqa: F401 # pyright: ignore[reportUnusedImport, reportMissingTypeStubs]
|
17
|
+
|
18
|
+
except ImportError:
|
19
|
+
raise ImportError(
|
20
|
+
"If you are using pytest to test your shiny app, install the pytest-playwright "
|
21
|
+
"shim package with: `pip install pytest-playwright`"
|
22
|
+
)
|
23
|
+
except ImportError:
|
24
|
+
pass
|
25
|
+
|
1
26
|
from ._chat import Chat as ChatController
|
2
27
|
|
3
28
|
__all__ = ["ChatController"]
|
shinychat/playwright/_chat.py
CHANGED
@@ -1,11 +1,50 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
|
-
from typing import Literal
|
3
|
+
from typing import Literal, Pattern, Union
|
4
4
|
|
5
5
|
from playwright.sync_api import Locator, Page
|
6
6
|
from playwright.sync_api import expect as playwright_expect
|
7
|
-
|
8
|
-
|
7
|
+
|
8
|
+
PatternStr = Pattern[str]
|
9
|
+
PatternOrStr = Union[str, PatternStr]
|
10
|
+
Timeout = Union[float, None]
|
11
|
+
|
12
|
+
|
13
|
+
# Avoid circular import with shiny.playwright by copy/pasting UiBase class
|
14
|
+
class UiBase:
|
15
|
+
"""A base class representing shiny UI components."""
|
16
|
+
|
17
|
+
id: str
|
18
|
+
"""
|
19
|
+
The browser DOM `id` of the UI element.
|
20
|
+
"""
|
21
|
+
loc: Locator
|
22
|
+
"""
|
23
|
+
Playwright `Locator` of the UI element.
|
24
|
+
"""
|
25
|
+
page: Page
|
26
|
+
"""
|
27
|
+
Playwright `Page` of the Shiny app.
|
28
|
+
"""
|
29
|
+
|
30
|
+
def __init__(
|
31
|
+
self,
|
32
|
+
page: Page,
|
33
|
+
*,
|
34
|
+
id: str,
|
35
|
+
loc: Locator | str,
|
36
|
+
) -> None:
|
37
|
+
self.page = page
|
38
|
+
# Needed?!? This is covered by `self.loc_root` and possibly `self.loc`
|
39
|
+
self.id = id
|
40
|
+
if isinstance(loc, str):
|
41
|
+
loc = page.locator(loc)
|
42
|
+
self.loc = loc
|
43
|
+
|
44
|
+
@property
|
45
|
+
def expect(self):
|
46
|
+
"""Expectation method equivalent to `playwright.expect(self.loc)`."""
|
47
|
+
return playwright_expect(self.loc)
|
9
48
|
|
10
49
|
|
11
50
|
class Chat(UiBase):
|
shinychat/py.typed
ADDED
File without changes
|
shinychat/www/GIT_VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
|
1
|
+
f77d543efedf2425f0859d204779218de3cafcf3
|
shinychat/www/chat/chat.css
CHANGED
@@ -1,2 +1,2 @@
|
|
1
|
-
@charset "UTF-8";shiny-chat-container{--shiny-chat-border: var(--bs-border-width, 1px) solid var(--bs-border-color, #e9ecef);--shiny-chat-user-message-bg: RGBA(var(--bs-primary-rgb, 0, 123, 194), .06);--_chat-container-padding: .25rem;display:grid;grid-template-columns:1fr;grid-template-rows:1fr auto;margin:0 auto;gap:0;padding:var(--_chat-container-padding);padding-bottom:0}shiny-chat-container p:last-child{margin-bottom:0}shiny-chat-container .suggestion,shiny-chat-container [data-suggestion]{cursor:pointer}shiny-chat-container .suggestion{color:var(--bs-link-color, #007bc2);text-decoration-color:var(--bs-link-color, #007bc2);text-decoration-line:underline;text-decoration-style:dotted;text-underline-offset:2px;text-underline-offset:4px;text-decoration-thickness:2px;padding-inline:2px}shiny-chat-container .suggestion:hover{text-decoration-style:solid}shiny-chat-container .suggestion:after{content:"\2726";display:inline-block;margin-inline-start:.15em}shiny-chat-container .suggestion.submit:after,shiny-chat-container .suggestion[data-suggestion-submit=""]:after,shiny-chat-container .suggestion[data-suggestion-submit=true]:after{content:"\21b5"}shiny-chat-container .card[data-suggestion]:hover{color:var(--bs-link-color, #007bc2);border-color:rgba(var(--bs-link-color-rgb),.5)}shiny-chat-messages{display:flex;flex-direction:column;gap:2rem;overflow:auto;margin-bottom:1rem;--_scroll-margin: 1rem;padding-right:var(--_scroll-margin);margin-right:calc(-1 * var(--_scroll-margin))}shiny-chat-message{display:grid;grid-template-columns:auto minmax(0,1fr);gap:1rem}shiny-chat-message>*{height:fit-content}shiny-chat-message .message-icon{border-radius:50%;border:var(--shiny-chat-border);height:2rem;width:2rem;display:grid;place-items:center;overflow:clip}shiny-chat-message .message-icon>*{height:100%;width:100%;max-width:100%;max-height:100%;margin:0!important;object-fit:contain}shiny-chat-message .message-icon>svg,shiny-chat-message .message-icon>.icon,shiny-chat-message .message-icon>.fa,shiny-chat-message .message-icon>.bi{max-height:66%;max-width:66%}shiny-chat-message .message-icon:has(>.border-0){border:none;border-radius:unset;overflow:unset}shiny-chat-message shiny-markdown-stream{align-self:center}shiny-user-message{align-self:flex-end;padding:.75rem 1rem;border-radius:10px;background-color:var(--shiny-chat-user-message-bg);max-width:100%}shiny-user-message[content_type=text],shiny-chat-message[content_type=text]{white-space:pre;overflow-x:auto}shiny-chat-input{--_input-padding-top: 0;--_input-padding-bottom: var(--_chat-container-padding, .25rem);margin-top:calc(-1 * var(--_input-padding-top));position:sticky;bottom:calc(-1 * var(--_input-padding-bottom) + 4px);padding-block:var(--_input-padding-top) var(--_input-padding-bottom)}shiny-chat-input textarea{--bs-border-radius: 26px;resize:none;padding-right:36px!important;max-height:175px}shiny-chat-input textarea::placeholder{color:var(--bs-gray-600, #707782)!important}shiny-chat-input button{position:absolute;bottom:calc(6px + var(--_input-padding-bottom));right:8px;background-color:transparent;color:var(--bs-primary, #007bc2);transition:color .25s ease-in-out;border:none;padding:0;cursor:pointer;line-height:16px;border-radius:50%}shiny-chat-input button:disabled{cursor:not-allowed;color:var(--bs-gray-500, #8d959e)}.shiny-busy:has(shiny-chat-input[disabled]):after{display:none}
|
1
|
+
@charset "UTF-8";.shiny-tool-card{max-height:var(--shiny-tool-card-max-height, 500px)}.shiny-tool-card .tool-title{min-width:25%;flex-shrink:2}.shiny-tool-card .tool-intent{opacity:.66;font-style:italic;font-weight:400;text-align:end;flex-shrink:3;max-width:60%;min-width:20%}.shiny-tool-card .tool-spacer{margin-inline-start:auto}.shiny-tool-card .tool-icon{--_icon-size: var(--shiny-tool-card-icon-size, 16px);width:var(--_icon-size);height:var(--_icon-size);display:flex;align-items:center;flex:none}.shiny-tool-card .tool-icon [class^=spinner]{--bs-spinner-width: var(--_icon-size);--bs-spinner-height: var(--_icon-size);--bs-spinner-border-width: .2em;color:var(--shiny-tool-card-spinner-color, var(--bs-primary, #007bc2))}.shiny-tool-card .tool-title-name{font-weight:600}.shiny-tool-card .card-header,.shiny-tool-card .card-footer{font-size:inherit;font-weight:400;word-break:break-word}.shiny-tool-card>.card-header{display:flex;flex-direction:row;align-items:center;align-self:stretch;gap:.5rem;width:100%;text-align:left;cursor:pointer;border-top-width:0;border-right-width:0;border-left-width:0}.shiny-tool-card>.card-header[aria-expanded=false]{border-bottom-width:0}.shiny-tool-card>.card-header>*{line-height:1}.shiny-tool-card>.card-header:hover,.shiny-tool-card>.card-header:focus-visible{background-color:rgba(var(--bs-emphasis-color-rgb),.05);outline:none}.shiny-tool-card>.card-header .function-name{font-weight:700}.shiny-tool-card>.card-header .collapse-indicator{display:grid;place-items:center;width:1em;height:1em;opacity:.66}.shiny-tool-card>.card-header .collapse-indicator,.shiny-tool-card>.card-header .collapse-indicator>.horizontal{transition:.3s ease-in-out all;transform-origin:center center}.shiny-tool-card>.card-header[aria-expanded=true]>.collapse-indicator{transform:rotate(-90deg)}.shiny-tool-card>.card-header[aria-expanded=true]>.collapse-indicator .horizontal{transform:scale(0)}.shiny-tool-card>.card-body{transition:max-height .3s ease-out,opacity .2s ease-out;opacity:1;overflow:auto}.shiny-tool-card>.card-body.collapsed{max-height:0;opacity:0;padding:0}.shiny-tool-card pre{margin:0;white-space:pre-wrap;padding:1em;border-radius:var(--bs-border-radius, 4px)}shiny-tool-result,shiny-tool-request{display:block;margin:1em 0;border-radius:var(--bs-border-radius, 4px);overflow:visible;padding:0;font-size:.8em}shiny-tool-result[hidden],shiny-tool-request[hidden]{display:none}shiny-tool-result+p,shiny-tool-request+p{margin-top:1rem}shiny-tool-result:first-child,shiny-tool-request:first-child{margin-top:0}shiny-tool-result:last-child,shiny-tool-request:last-child{margin-bottom:0}shiny-tool-request[hidden]+shiny-tool-result:first-of-type{margin-top:0}shiny-chat-container{--shiny-chat-border: var(--bs-border-width, 1px) solid var(--bs-border-color, #e9ecef);--shiny-chat-user-message-bg: RGBA(var(--bs-primary-rgb, 0, 123, 194), .06);--_chat-container-padding: .25rem;display:grid;grid-template-columns:1fr;grid-template-rows:1fr auto;margin:0 auto;gap:0;padding:var(--_chat-container-padding);padding-bottom:0}shiny-chat-container p:last-child{margin-bottom:0}shiny-chat-container .suggestion,shiny-chat-container [data-suggestion]{cursor:pointer}shiny-chat-container .suggestion{color:var(--bs-link-color, #007bc2);text-decoration-color:var(--bs-link-color, #007bc2);text-decoration-line:underline;text-decoration-style:dotted;text-underline-offset:2px;text-underline-offset:4px;text-decoration-thickness:2px;padding-inline:2px}shiny-chat-container .suggestion:hover{text-decoration-style:solid}shiny-chat-container .suggestion:after{content:"\2726";display:inline-block;margin-inline-start:.15em}shiny-chat-container .suggestion.submit:after,shiny-chat-container .suggestion[data-suggestion-submit=""]:after,shiny-chat-container .suggestion[data-suggestion-submit=true]:after{content:"\21b5"}shiny-chat-container .card[data-suggestion]:hover{color:var(--bs-link-color, #007bc2);border-color:rgba(var(--bs-link-color-rgb),.5)}shiny-chat-messages{display:flex;flex-direction:column;gap:2rem;overflow:auto;margin-bottom:1rem;--_scroll-margin: 1rem;padding-right:var(--_scroll-margin);margin-right:calc(-1 * var(--_scroll-margin))}shiny-chat-message{display:grid;grid-template-columns:auto minmax(0,1fr);gap:1rem}shiny-chat-message>*{height:fit-content}shiny-chat-message .message-icon{border-radius:50%;border:var(--shiny-chat-border);height:2rem;width:2rem;display:grid;place-items:center;overflow:clip}shiny-chat-message .message-icon>*{height:100%;width:100%;max-width:100%;max-height:100%;margin:0!important;object-fit:contain}shiny-chat-message .message-icon>svg,shiny-chat-message .message-icon>.icon,shiny-chat-message .message-icon>.fa,shiny-chat-message .message-icon>.bi{max-height:66%;max-width:66%}shiny-chat-message .message-icon:has(>.border-0){border:none;border-radius:unset;overflow:unset}shiny-chat-message shiny-markdown-stream{align-self:center}shiny-user-message,shiny-chat-message[data-role=user]{align-self:flex-end;padding:.75rem 1rem;border-radius:10px;background-color:var(--shiny-chat-user-message-bg);max-width:100%}shiny-chat-message[data-role=user]:not([icon]){grid-template-columns:auto}shiny-user-message[content_type=text],shiny-chat-message[content_type=text]{white-space:pre;overflow-x:auto}shiny-chat-input{--_input-padding-top: 0;--_input-padding-bottom: var(--_chat-container-padding, .25rem);margin-top:calc(-1 * var(--_input-padding-top));position:sticky;bottom:calc(-1 * var(--_input-padding-bottom) + 4px);padding-block:var(--_input-padding-top) var(--_input-padding-bottom)}shiny-chat-input textarea{--bs-border-radius: 26px;resize:none;padding-right:36px!important;max-height:175px}shiny-chat-input textarea::placeholder{color:var(--bs-gray-600, #707782)!important}shiny-chat-input button{position:absolute;bottom:calc(6px + var(--_input-padding-bottom));right:8px;background-color:transparent;color:var(--bs-primary, #007bc2);transition:color .25s ease-in-out;border:none;padding:0;cursor:pointer;line-height:16px;border-radius:50%}shiny-chat-input button:disabled{cursor:not-allowed;color:var(--bs-gray-500, #8d959e)}.shiny-busy:has(shiny-chat-input[disabled]):after{display:none}.shinychat-external-link-dialog{padding:0;border:none;background:none;max-width:min(680px,90%)}.shinychat-external-link-dialog::backdrop{background-color:#00000080}.shinychat-external-link-dialog .link-url{word-break:break-all;font-weight:500}
|
2
2
|
/*# sourceMappingURL=chat.css.map */
|