chainlit 0.2.111__py3-none-any.whl → 0.3.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 chainlit might be problematic. Click here for more details.
- chainlit/__init__.py +48 -32
- chainlit/action.py +12 -12
- chainlit/cache.py +20 -0
- chainlit/cli/__init__.py +43 -62
- chainlit/cli/mock.py +48 -0
- chainlit/client.py +111 -89
- chainlit/config.py +22 -3
- chainlit/element.py +95 -124
- chainlit/{sdk.py → emitter.py} +55 -64
- chainlit/frontend/dist/assets/index-0b7e367e.js +717 -0
- chainlit/frontend/dist/assets/index-0cc9e355.css +1 -0
- chainlit/frontend/dist/index.html +3 -3
- chainlit/hello.py +3 -3
- chainlit/lc/__init__.py +11 -0
- chainlit/lc/agent.py +32 -0
- chainlit/lc/callbacks.py +411 -0
- chainlit/message.py +72 -96
- chainlit/server.py +280 -195
- chainlit/session.py +4 -2
- chainlit/sync.py +37 -0
- chainlit/types.py +18 -1
- chainlit/user_session.py +16 -16
- {chainlit-0.2.111.dist-info → chainlit-0.3.0.dist-info}/METADATA +15 -14
- chainlit-0.3.0.dist-info/RECORD +37 -0
- chainlit/frontend/dist/assets/index-4d8f8873.js +0 -713
- chainlit/frontend/dist/assets/index-bdffdaa0.css +0 -1
- chainlit/lc/chainlit_handler.py +0 -271
- chainlit/lc/monkey.py +0 -28
- chainlit/lc/new_monkey.py +0 -167
- chainlit/lc/old_monkey.py +0 -119
- chainlit/lc/utils.py +0 -38
- chainlit/watch.py +0 -54
- chainlit-0.2.111.dist-info/RECORD +0 -38
- {chainlit-0.2.111.dist-info → chainlit-0.3.0.dist-info}/WHEEL +0 -0
- {chainlit-0.2.111.dist-info → chainlit-0.3.0.dist-info}/entry_points.txt +0 -0
chainlit/client.py
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
from typing import Dict, Any, Optional
|
|
2
|
-
from python_graphql_client import GraphqlClient
|
|
3
2
|
from abc import ABC, abstractmethod
|
|
4
3
|
import uuid
|
|
5
|
-
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import aiohttp
|
|
7
|
+
from python_graphql_client import GraphqlClient
|
|
8
|
+
|
|
6
9
|
from chainlit.types import ElementType, ElementSize
|
|
7
10
|
from chainlit.logger import logger
|
|
8
11
|
from chainlit.config import config
|
|
@@ -13,31 +16,31 @@ class BaseClient(ABC):
|
|
|
13
16
|
session_id: str
|
|
14
17
|
|
|
15
18
|
@abstractmethod
|
|
16
|
-
def is_project_member(self, access_token: str) -> bool:
|
|
19
|
+
async def is_project_member(self, access_token: str) -> bool:
|
|
17
20
|
pass
|
|
18
21
|
|
|
19
22
|
@abstractmethod
|
|
20
|
-
def create_conversation(self, session_id: str) -> int:
|
|
23
|
+
async def create_conversation(self, session_id: str) -> int:
|
|
21
24
|
pass
|
|
22
25
|
|
|
23
26
|
@abstractmethod
|
|
24
|
-
def create_message(self, variables: Dict[str, Any]) -> int:
|
|
27
|
+
async def create_message(self, variables: Dict[str, Any]) -> int:
|
|
25
28
|
pass
|
|
26
29
|
|
|
27
30
|
@abstractmethod
|
|
28
|
-
def update_message(self, message_id: int, variables: Dict[str, Any]) -> bool:
|
|
31
|
+
async def update_message(self, message_id: int, variables: Dict[str, Any]) -> bool:
|
|
29
32
|
pass
|
|
30
33
|
|
|
31
34
|
@abstractmethod
|
|
32
|
-
def delete_message(self, message_id: int) -> bool:
|
|
35
|
+
async def delete_message(self, message_id: int) -> bool:
|
|
33
36
|
pass
|
|
34
37
|
|
|
35
38
|
@abstractmethod
|
|
36
|
-
def upload_element(self, content: bytes, mime: str) ->
|
|
39
|
+
async def upload_element(self, content: bytes, mime: str) -> str:
|
|
37
40
|
pass
|
|
38
41
|
|
|
39
42
|
@abstractmethod
|
|
40
|
-
def create_element(
|
|
43
|
+
async def create_element(
|
|
41
44
|
self,
|
|
42
45
|
type: ElementType,
|
|
43
46
|
url: str,
|
|
@@ -50,16 +53,23 @@ class BaseClient(ABC):
|
|
|
50
53
|
pass
|
|
51
54
|
|
|
52
55
|
|
|
56
|
+
conversation_lock = asyncio.Lock()
|
|
57
|
+
|
|
58
|
+
|
|
53
59
|
class CloudClient(BaseClient):
|
|
54
60
|
conversation_id: Optional[str] = None
|
|
55
61
|
|
|
56
|
-
def __init__(self, project_id: str, session_id: str, access_token: str
|
|
62
|
+
def __init__(self, project_id: str, session_id: str, access_token: str):
|
|
57
63
|
self.project_id = project_id
|
|
58
64
|
self.session_id = session_id
|
|
59
|
-
self.
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
65
|
+
self.headers = {
|
|
66
|
+
"Authorization": access_token,
|
|
67
|
+
"content-type": "application/json",
|
|
68
|
+
}
|
|
69
|
+
graphql_endpoint = f"{config.chainlit_server}/api/graphql"
|
|
70
|
+
self.graphql_client = GraphqlClient(
|
|
71
|
+
endpoint=graphql_endpoint, headers=self.headers
|
|
72
|
+
)
|
|
63
73
|
|
|
64
74
|
def query(self, query: str, variables: Dict[str, Any] = {}) -> Dict[str, Any]:
|
|
65
75
|
"""
|
|
@@ -69,7 +79,7 @@ class CloudClient(BaseClient):
|
|
|
69
79
|
:param variables: A dictionary of variables for the query.
|
|
70
80
|
:return: The response data as a dictionary.
|
|
71
81
|
"""
|
|
72
|
-
return self.
|
|
82
|
+
return self.graphql_client.execute_async(query=query, variables=variables)
|
|
73
83
|
|
|
74
84
|
def check_for_errors(self, response: Dict[str, Any]):
|
|
75
85
|
if "errors" in response:
|
|
@@ -85,50 +95,53 @@ class CloudClient(BaseClient):
|
|
|
85
95
|
:param variables: A dictionary of variables for the mutation.
|
|
86
96
|
:return: The response data as a dictionary.
|
|
87
97
|
"""
|
|
88
|
-
return self.
|
|
89
|
-
|
|
90
|
-
def is_project_member(self) -> bool:
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
"
|
|
98
|
+
return self.graphql_client.execute_async(query=mutation, variables=variables)
|
|
99
|
+
|
|
100
|
+
async def is_project_member(self) -> bool:
|
|
101
|
+
data = {"projectId": self.project_id}
|
|
102
|
+
async with aiohttp.ClientSession() as session:
|
|
103
|
+
async with session.post(
|
|
104
|
+
f"{config.chainlit_server}/api/role",
|
|
105
|
+
json=data,
|
|
106
|
+
headers=self.headers,
|
|
107
|
+
) as r:
|
|
108
|
+
if not r.ok:
|
|
109
|
+
reason = await r.text()
|
|
110
|
+
logger.error(f"Failed to get user role. {r.status}: {reason}")
|
|
111
|
+
return False
|
|
112
|
+
json = await r.json()
|
|
113
|
+
role = json.get("role", "ANONYMOUS")
|
|
114
|
+
return role != "ANONYMOUS"
|
|
115
|
+
|
|
116
|
+
async def create_conversation(self, session_id: str) -> int:
|
|
117
|
+
# If we run multiple send concurrently, we need to make sure we don't create multiple conversations.
|
|
118
|
+
async with conversation_lock:
|
|
119
|
+
if self.conversation_id:
|
|
120
|
+
return self.conversation_id
|
|
121
|
+
|
|
122
|
+
mutation = """
|
|
123
|
+
mutation ($projectId: String!, $sessionId: String!) {
|
|
124
|
+
createConversation(projectId: $projectId, sessionId: $sessionId) {
|
|
125
|
+
id
|
|
126
|
+
}
|
|
95
127
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
)
|
|
100
|
-
|
|
101
|
-
role = response.json().get("role", "ANONYMOUS")
|
|
102
|
-
return role != "ANONYMOUS"
|
|
103
|
-
except Exception as e:
|
|
104
|
-
logger.exception(e)
|
|
105
|
-
return False
|
|
128
|
+
"""
|
|
129
|
+
variables = {"projectId": self.project_id, "sessionId": session_id}
|
|
130
|
+
res = await self.mutation(mutation, variables)
|
|
106
131
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
createConversation(projectId: $projectId, sessionId: $sessionId) {
|
|
111
|
-
id
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
"""
|
|
115
|
-
variables = {"projectId": self.project_id, "sessionId": session_id}
|
|
116
|
-
res = self.mutation(mutation, variables)
|
|
117
|
-
|
|
118
|
-
if self.check_for_errors(res):
|
|
119
|
-
logger.warning("Could not create conversation.")
|
|
120
|
-
return None
|
|
132
|
+
if self.check_for_errors(res):
|
|
133
|
+
logger.warning("Could not create conversation.")
|
|
134
|
+
return None
|
|
121
135
|
|
|
122
|
-
|
|
136
|
+
return int(res["data"]["createConversation"]["id"])
|
|
123
137
|
|
|
124
|
-
def get_conversation_id(self):
|
|
125
|
-
|
|
126
|
-
self.conversation_id = self.create_conversation(self.session_id)
|
|
138
|
+
async def get_conversation_id(self):
|
|
139
|
+
self.conversation_id = await self.create_conversation(self.session_id)
|
|
127
140
|
|
|
128
141
|
return self.conversation_id
|
|
129
142
|
|
|
130
|
-
def create_message(self, variables: Dict[str, Any]) -> int:
|
|
131
|
-
c_id = self.get_conversation_id()
|
|
143
|
+
async def create_message(self, variables: Dict[str, Any]) -> int:
|
|
144
|
+
c_id = await self.get_conversation_id()
|
|
132
145
|
|
|
133
146
|
if not c_id:
|
|
134
147
|
logger.warning("Missing conversation ID, could not persist the message.")
|
|
@@ -137,21 +150,20 @@ class CloudClient(BaseClient):
|
|
|
137
150
|
variables["conversationId"] = c_id
|
|
138
151
|
|
|
139
152
|
mutation = """
|
|
140
|
-
mutation ($conversationId: ID!, $author: String!, $content: String!, $language: String, $prompt: String, $llmSettings: Json, $isError: Boolean, $indent: Int, $authorIsUser: Boolean, $waitForAnswer: Boolean) {
|
|
141
|
-
createMessage(conversationId: $conversationId, author: $author, content: $content, language: $language, prompt: $prompt, llmSettings: $llmSettings, isError: $isError, indent: $indent, authorIsUser: $authorIsUser, waitForAnswer: $waitForAnswer) {
|
|
153
|
+
mutation ($conversationId: ID!, $author: String!, $content: String!, $language: String, $prompt: String, $llmSettings: Json, $isError: Boolean, $indent: Int, $authorIsUser: Boolean, $waitForAnswer: Boolean, $createdAt: Float) {
|
|
154
|
+
createMessage(conversationId: $conversationId, author: $author, content: $content, language: $language, prompt: $prompt, llmSettings: $llmSettings, isError: $isError, indent: $indent, authorIsUser: $authorIsUser, waitForAnswer: $waitForAnswer, createdAt: $createdAt) {
|
|
142
155
|
id
|
|
143
156
|
}
|
|
144
157
|
}
|
|
145
158
|
"""
|
|
146
|
-
res = self.mutation(mutation, variables)
|
|
147
|
-
|
|
159
|
+
res = await self.mutation(mutation, variables)
|
|
148
160
|
if self.check_for_errors(res):
|
|
149
161
|
logger.warning("Could not create message.")
|
|
150
162
|
return None
|
|
151
163
|
|
|
152
164
|
return int(res["data"]["createMessage"]["id"])
|
|
153
165
|
|
|
154
|
-
def update_message(self, message_id: int, variables: Dict[str, Any]) -> bool:
|
|
166
|
+
async def update_message(self, message_id: int, variables: Dict[str, Any]) -> bool:
|
|
155
167
|
mutation = """
|
|
156
168
|
mutation ($messageId: ID!, $author: String!, $content: String!, $language: String, $prompt: String, $llmSettings: Json) {
|
|
157
169
|
updateMessage(messageId: $messageId, author: $author, content: $content, language: $language, prompt: $prompt, llmSettings: $llmSettings) {
|
|
@@ -160,7 +172,7 @@ class CloudClient(BaseClient):
|
|
|
160
172
|
}
|
|
161
173
|
"""
|
|
162
174
|
variables["messageId"] = message_id
|
|
163
|
-
res = self.mutation(mutation, variables)
|
|
175
|
+
res = await self.mutation(mutation, variables)
|
|
164
176
|
|
|
165
177
|
if self.check_for_errors(res):
|
|
166
178
|
logger.warning("Could not update message.")
|
|
@@ -168,7 +180,7 @@ class CloudClient(BaseClient):
|
|
|
168
180
|
|
|
169
181
|
return True
|
|
170
182
|
|
|
171
|
-
def delete_message(self, message_id: int) -> bool:
|
|
183
|
+
async def delete_message(self, message_id: int) -> bool:
|
|
172
184
|
mutation = """
|
|
173
185
|
mutation ($messageId: ID!) {
|
|
174
186
|
deleteMessage(messageId: $messageId) {
|
|
@@ -176,7 +188,7 @@ class CloudClient(BaseClient):
|
|
|
176
188
|
}
|
|
177
189
|
}
|
|
178
190
|
"""
|
|
179
|
-
res = self.mutation(mutation, {"messageId": message_id})
|
|
191
|
+
res = await self.mutation(mutation, {"messageId": message_id})
|
|
180
192
|
|
|
181
193
|
if self.check_for_errors(res):
|
|
182
194
|
logger.warning("Could not delete message.")
|
|
@@ -184,7 +196,7 @@ class CloudClient(BaseClient):
|
|
|
184
196
|
|
|
185
197
|
return True
|
|
186
198
|
|
|
187
|
-
def create_element(
|
|
199
|
+
async def create_element(
|
|
188
200
|
self,
|
|
189
201
|
type: ElementType,
|
|
190
202
|
url: str,
|
|
@@ -194,7 +206,7 @@ class CloudClient(BaseClient):
|
|
|
194
206
|
language: str = None,
|
|
195
207
|
for_id: str = None,
|
|
196
208
|
) -> Dict[str, Any]:
|
|
197
|
-
c_id = self.get_conversation_id()
|
|
209
|
+
c_id = await self.get_conversation_id()
|
|
198
210
|
|
|
199
211
|
if not c_id:
|
|
200
212
|
logger.warning("Missing conversation ID, could not persist the element.")
|
|
@@ -224,7 +236,7 @@ class CloudClient(BaseClient):
|
|
|
224
236
|
"language": language,
|
|
225
237
|
"forId": for_id,
|
|
226
238
|
}
|
|
227
|
-
res = self.mutation(mutation, variables)
|
|
239
|
+
res = await self.mutation(mutation, variables)
|
|
228
240
|
|
|
229
241
|
if self.check_for_errors(res):
|
|
230
242
|
logger.warning("Could not persist element.")
|
|
@@ -232,34 +244,44 @@ class CloudClient(BaseClient):
|
|
|
232
244
|
|
|
233
245
|
return res["data"]["createElement"]
|
|
234
246
|
|
|
235
|
-
def upload_element(self, content: bytes, mime: str) -> str:
|
|
247
|
+
async def upload_element(self, content: bytes, mime: str) -> str:
|
|
236
248
|
id = f"{uuid.uuid4()}"
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
249
|
+
body = {"projectId": self.project_id, "fileName": id, "contentType": mime}
|
|
250
|
+
|
|
251
|
+
path = f"/api/upload/file"
|
|
252
|
+
|
|
253
|
+
async with aiohttp.ClientSession() as session:
|
|
254
|
+
async with session.post(
|
|
255
|
+
f"{config.chainlit_server}{path}",
|
|
256
|
+
json=body,
|
|
257
|
+
headers=self.headers,
|
|
258
|
+
) as r:
|
|
259
|
+
if not r.ok:
|
|
260
|
+
reason = await r.text()
|
|
261
|
+
logger.error(f"Failed to upload file: {reason}")
|
|
262
|
+
return ""
|
|
263
|
+
json_res = await r.json()
|
|
241
264
|
|
|
242
|
-
res = requests.post(url, json=body, headers=self.headers)
|
|
243
|
-
|
|
244
|
-
if not res.ok:
|
|
245
|
-
logger.error(f"Failed to upload file: {res.text}")
|
|
246
|
-
return ""
|
|
247
|
-
|
|
248
|
-
json_res = res.json()
|
|
249
265
|
upload_details = json_res["post"]
|
|
250
266
|
permanent_url = json_res["permanentUrl"]
|
|
251
267
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
268
|
+
form_data = aiohttp.FormData()
|
|
269
|
+
|
|
270
|
+
# Add fields to the form_data
|
|
271
|
+
for field_name, field_value in upload_details["fields"].items():
|
|
272
|
+
form_data.add_field(field_name, field_value)
|
|
273
|
+
|
|
274
|
+
# Add file to the form_data
|
|
275
|
+
form_data.add_field("file", content, content_type="multipart/form-data")
|
|
276
|
+
async with aiohttp.ClientSession() as session:
|
|
277
|
+
async with session.post(
|
|
278
|
+
upload_details["url"],
|
|
279
|
+
data=form_data,
|
|
280
|
+
) as upload_response:
|
|
281
|
+
if not upload_response.ok:
|
|
282
|
+
reason = await upload_response.text()
|
|
283
|
+
logger.error(f"Failed to upload file: {reason}")
|
|
284
|
+
return ""
|
|
285
|
+
|
|
286
|
+
url = f'{upload_details["url"]}/{upload_details["fields"]["key"]}'
|
|
287
|
+
return permanent_url
|
chainlit/config.py
CHANGED
|
@@ -9,11 +9,12 @@ from chainlit.logger import logger
|
|
|
9
9
|
if TYPE_CHECKING:
|
|
10
10
|
from chainlit.action import Action
|
|
11
11
|
|
|
12
|
+
PACKAGE_ROOT = os.path.dirname(__file__)
|
|
12
13
|
|
|
13
14
|
# Get the directory the script is running from
|
|
14
|
-
|
|
15
|
+
APP_ROOT = os.getcwd()
|
|
15
16
|
|
|
16
|
-
config_dir = os.path.join(
|
|
17
|
+
config_dir = os.path.join(APP_ROOT, ".chainlit")
|
|
17
18
|
config_file = os.path.join(config_dir, "config.toml")
|
|
18
19
|
|
|
19
20
|
# Default config file created if none exists
|
|
@@ -53,8 +54,24 @@ chainlit_prod_url = os.environ.get("CHAINLIT_PROD_URL")
|
|
|
53
54
|
chainlit_server = "https://cloud.chainlit.io"
|
|
54
55
|
|
|
55
56
|
|
|
57
|
+
DEFAULT_HOST = "0.0.0.0"
|
|
58
|
+
DEFAULT_PORT = 8000
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass()
|
|
62
|
+
class RunSettings:
|
|
63
|
+
host: str = DEFAULT_HOST
|
|
64
|
+
port: int = DEFAULT_PORT
|
|
65
|
+
headless: bool = False
|
|
66
|
+
watch: bool = False
|
|
67
|
+
no_cache: bool = False
|
|
68
|
+
debug: bool = False
|
|
69
|
+
ci: bool = False
|
|
70
|
+
|
|
71
|
+
|
|
56
72
|
@dataclass()
|
|
57
73
|
class ChainlitConfig:
|
|
74
|
+
run_settings: RunSettings
|
|
58
75
|
# Chainlit server URL. Used only for cloud features
|
|
59
76
|
chainlit_server: str
|
|
60
77
|
# Name of the app and chatbot. Used as the default message author.
|
|
@@ -74,7 +91,7 @@ class ChainlitConfig:
|
|
|
74
91
|
# Developer defined callbacks for each action. Key is the action name, value is the callback function.
|
|
75
92
|
action_callbacks: Dict[str, Callable[["Action"], Any]]
|
|
76
93
|
# Directory where the Chainlit project is located
|
|
77
|
-
root =
|
|
94
|
+
root = APP_ROOT
|
|
78
95
|
# The url of the deployed app. Only set if the app is deployed.
|
|
79
96
|
chainlit_prod_url = chainlit_prod_url
|
|
80
97
|
# Link to your github repo. This will add a github button in the UI's header.
|
|
@@ -91,6 +108,7 @@ class ChainlitConfig:
|
|
|
91
108
|
on_stop: Optional[Callable[[], Any]] = None
|
|
92
109
|
on_chat_start: Optional[Callable[[], Any]] = None
|
|
93
110
|
on_message: Optional[Callable[[str], Any]] = None
|
|
111
|
+
lc_agent_is_async: Optional[bool] = None
|
|
94
112
|
lc_run: Optional[Callable[[Any, str], str]] = None
|
|
95
113
|
lc_postprocess: Optional[Callable[[Any], str]] = None
|
|
96
114
|
lc_factory: Optional[Callable[[], Any]] = None
|
|
@@ -174,6 +192,7 @@ def load_config():
|
|
|
174
192
|
lc_cache_path = os.path.join(config_dir, ".langchain.db")
|
|
175
193
|
|
|
176
194
|
config = ChainlitConfig(
|
|
195
|
+
run_settings=RunSettings(),
|
|
177
196
|
action_callbacks={},
|
|
178
197
|
github=github,
|
|
179
198
|
request_limit=request_limit,
|
chainlit/element.py
CHANGED
|
@@ -1,97 +1,75 @@
|
|
|
1
1
|
from pydantic.dataclasses import dataclass
|
|
2
2
|
from dataclasses_json import dataclass_json
|
|
3
|
-
from typing import Dict
|
|
3
|
+
from typing import Dict, Union
|
|
4
4
|
import uuid
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
import aiofiles
|
|
6
|
+
|
|
7
|
+
from chainlit.emitter import get_emitter, BaseClient
|
|
7
8
|
from chainlit.telemetry import trace_event
|
|
8
9
|
from chainlit.types import ElementType, ElementDisplay, ElementSize
|
|
9
|
-
|
|
10
|
-
|
|
10
|
+
|
|
11
|
+
type_to_mime = {
|
|
12
|
+
"image": "binary/octet-stream",
|
|
13
|
+
"text": "text/plain",
|
|
14
|
+
"pdf": "application/pdf",
|
|
15
|
+
}
|
|
11
16
|
|
|
12
17
|
|
|
13
18
|
@dataclass_json
|
|
14
19
|
@dataclass
|
|
15
|
-
class Element
|
|
20
|
+
class Element:
|
|
21
|
+
# Name of the element, this will be used to reference the element in the UI.
|
|
16
22
|
name: str
|
|
23
|
+
# The type of the element. This will be used to determine how to display the element in the UI.
|
|
17
24
|
type: ElementType
|
|
25
|
+
# Controls how the image element should be displayed in the UI. Choices are “side” (default), “inline”, or “page”.
|
|
18
26
|
display: ElementDisplay = "side"
|
|
27
|
+
# The URL of the element if already hosted somehwere else.
|
|
28
|
+
url: str = None
|
|
29
|
+
# The local path of the element.
|
|
30
|
+
path: str = None
|
|
31
|
+
# The byte content of the element.
|
|
32
|
+
content: bytes = None
|
|
33
|
+
# The ID of the element. This is set automatically when the element is sent to the UI if cloud is enabled.
|
|
19
34
|
id: int = None
|
|
35
|
+
# The ID of the element if cloud is disabled.
|
|
20
36
|
tempId: str = None
|
|
37
|
+
# The ID of the message this element is associated with.
|
|
21
38
|
forId: str = None
|
|
22
39
|
|
|
23
40
|
def __post_init__(self) -> None:
|
|
24
41
|
trace_event(f"init {self.__class__.__name__}")
|
|
42
|
+
self.emitter = get_emitter()
|
|
43
|
+
if not self.emitter:
|
|
44
|
+
raise RuntimeError("Element should be instantiated in a Chainlit context")
|
|
25
45
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
pass
|
|
29
|
-
|
|
30
|
-
def before_emit(self, element: Dict) -> Dict:
|
|
31
|
-
return element
|
|
46
|
+
if not self.url and not self.path and not self.content:
|
|
47
|
+
raise ValueError("Must provide url, path or content to instantiate element")
|
|
32
48
|
|
|
33
|
-
|
|
34
|
-
sdk = get_sdk()
|
|
35
|
-
|
|
36
|
-
element = None
|
|
37
|
-
|
|
38
|
-
# Cloud is enabled, upload the element to S3
|
|
39
|
-
if sdk.client:
|
|
40
|
-
element = self.persist(sdk.client, for_id)
|
|
41
|
-
self.id = element["id"]
|
|
42
|
-
|
|
43
|
-
if not element:
|
|
44
|
-
self.tempId = uuid.uuid4().hex
|
|
45
|
-
element = self.to_dict()
|
|
46
|
-
if for_id:
|
|
47
|
-
element["forId"] = for_id
|
|
48
|
-
|
|
49
|
-
if sdk.emit and element:
|
|
50
|
-
trace_event(f"send {self.__class__.__name__}")
|
|
51
|
-
element = self.before_emit(element)
|
|
52
|
-
sdk.emit("element", element)
|
|
49
|
+
self.tempId = uuid.uuid4().hex
|
|
53
50
|
|
|
51
|
+
async def preprocess_content(self):
|
|
52
|
+
pass
|
|
54
53
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
54
|
+
async def load(self):
|
|
55
|
+
if self.path:
|
|
56
|
+
async with aiofiles.open(self.path, "rb") as f:
|
|
57
|
+
self.content = await f.read()
|
|
58
|
+
await self.preprocess_content()
|
|
59
|
+
elif self.content:
|
|
60
|
+
await self.preprocess_content()
|
|
61
|
+
else:
|
|
62
|
+
raise ValueError("Must provide path or content to load element")
|
|
58
63
|
|
|
59
|
-
def persist(self, client: BaseClient, for_id: str = None):
|
|
60
|
-
if not self.content:
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
if url:
|
|
64
|
-
size = getattr(self, "size", None)
|
|
65
|
-
language = getattr(self, "language", None)
|
|
66
|
-
element = client.create_element(
|
|
67
|
-
name=self.name,
|
|
68
|
-
url=url,
|
|
69
|
-
type=self.type,
|
|
70
|
-
display=self.display,
|
|
71
|
-
size=size,
|
|
72
|
-
language=language,
|
|
73
|
-
for_id=for_id,
|
|
64
|
+
async def persist(self, client: BaseClient, for_id: str = None):
|
|
65
|
+
if not self.url and self.content:
|
|
66
|
+
self.url = await client.upload_element(
|
|
67
|
+
content=self.content, mime=type_to_mime[self.type]
|
|
74
68
|
)
|
|
75
|
-
return element
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
@dataclass
|
|
79
|
-
class RemoteElementBase:
|
|
80
|
-
url: str
|
|
81
69
|
|
|
82
|
-
|
|
83
|
-
@dataclass
|
|
84
|
-
class ImageBase:
|
|
85
|
-
type: ElementType = "image"
|
|
86
|
-
size: ElementSize = "medium"
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
@dataclass
|
|
90
|
-
class RemoteElement(Element, RemoteElementBase):
|
|
91
|
-
def persist(self, client: BaseClient, for_id: str = None):
|
|
92
70
|
size = getattr(self, "size", None)
|
|
93
71
|
language = getattr(self, "language", None)
|
|
94
|
-
element = client.create_element(
|
|
72
|
+
element = await client.create_element(
|
|
95
73
|
name=self.name,
|
|
96
74
|
url=self.url,
|
|
97
75
|
type=self.type,
|
|
@@ -102,47 +80,73 @@ class RemoteElement(Element, RemoteElementBase):
|
|
|
102
80
|
)
|
|
103
81
|
return element
|
|
104
82
|
|
|
83
|
+
async def before_emit(self, element: Dict) -> Dict:
|
|
84
|
+
return element
|
|
105
85
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
"""Useful to send an image living on the local filesystem to the UI."""
|
|
86
|
+
async def send(self, for_id: str = None):
|
|
87
|
+
element = None
|
|
109
88
|
|
|
110
|
-
|
|
89
|
+
if not self.content and not self.url and self.path:
|
|
90
|
+
await self.load()
|
|
111
91
|
|
|
112
|
-
|
|
113
|
-
if self.
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
elif self.content:
|
|
117
|
-
self.content = self.content
|
|
118
|
-
else:
|
|
119
|
-
raise ValueError("Must provide either path or content")
|
|
92
|
+
# Cloud is enabled, upload the element to S3
|
|
93
|
+
if self.emitter.client and not self.id:
|
|
94
|
+
element = await self.persist(self.emitter.client, for_id)
|
|
95
|
+
self.id = element["id"]
|
|
120
96
|
|
|
97
|
+
elif not self.url and not self.content:
|
|
98
|
+
raise ValueError("Must provide url or content to send element")
|
|
99
|
+
|
|
100
|
+
element = self.to_dict()
|
|
101
|
+
if for_id:
|
|
102
|
+
element["forId"] = for_id
|
|
103
|
+
|
|
104
|
+
if self.emitter.emit and element:
|
|
105
|
+
trace_event(f"send {self.__class__.__name__}")
|
|
106
|
+
element = await self.before_emit(element)
|
|
107
|
+
await self.emitter.emit("element", element)
|
|
121
108
|
|
|
122
|
-
@dataclass
|
|
123
|
-
class RemoteImage(ImageBase, RemoteElement):
|
|
124
|
-
"""Useful to send an image based on an URL to the UI."""
|
|
125
109
|
|
|
126
|
-
|
|
110
|
+
@dataclass
|
|
111
|
+
class Image(Element):
|
|
112
|
+
type: ElementType = "image"
|
|
113
|
+
size: ElementSize = "medium"
|
|
127
114
|
|
|
128
115
|
|
|
129
116
|
@dataclass
|
|
130
|
-
class
|
|
131
|
-
|
|
117
|
+
class Avatar(Element):
|
|
118
|
+
type: ElementType = "avatar"
|
|
119
|
+
|
|
120
|
+
async def send(self):
|
|
121
|
+
element = None
|
|
122
|
+
|
|
123
|
+
if not self.content and not self.url and self.path:
|
|
124
|
+
await self.load()
|
|
125
|
+
|
|
126
|
+
if not self.url and not self.content:
|
|
127
|
+
raise ValueError("Must provide url or content to send element")
|
|
128
|
+
|
|
129
|
+
element = self.to_dict()
|
|
130
|
+
|
|
131
|
+
if self.emitter.emit and element:
|
|
132
|
+
trace_event(f"send {self.__class__.__name__}")
|
|
133
|
+
element = await self.before_emit(element)
|
|
134
|
+
await self.emitter.emit("element", element)
|
|
132
135
|
|
|
133
136
|
|
|
134
137
|
@dataclass
|
|
135
|
-
class Text(
|
|
138
|
+
class Text(Element):
|
|
136
139
|
"""Useful to send a text (not a message) to the UI."""
|
|
137
140
|
|
|
141
|
+
content: Union[str, bytes] = None
|
|
138
142
|
type: ElementType = "text"
|
|
139
|
-
content = bytes("", "utf-8")
|
|
140
143
|
language: str = None
|
|
141
144
|
|
|
142
|
-
def
|
|
143
|
-
|
|
145
|
+
async def preprocess_content(self):
|
|
146
|
+
if isinstance(self.content, str):
|
|
147
|
+
self.content = self.content.encode("utf-8")
|
|
144
148
|
|
|
145
|
-
def before_emit(self, text_element):
|
|
149
|
+
async def before_emit(self, text_element):
|
|
146
150
|
if "content" in text_element and isinstance(text_element["content"], bytes):
|
|
147
151
|
text_element["content"] = text_element["content"].decode("utf-8")
|
|
148
152
|
return text_element
|
|
@@ -150,39 +154,6 @@ class Text(LocalElement, TextBase):
|
|
|
150
154
|
|
|
151
155
|
@dataclass
|
|
152
156
|
class Pdf(Element):
|
|
153
|
-
"""Useful to send a pdf
|
|
157
|
+
"""Useful to send a pdf to the UI."""
|
|
154
158
|
|
|
155
159
|
type: ElementType = "pdf"
|
|
156
|
-
url: str = None
|
|
157
|
-
path: str = None
|
|
158
|
-
content: bytes = None
|
|
159
|
-
|
|
160
|
-
def persist(self, client: BaseClient, for_id: str = None):
|
|
161
|
-
if not self.content and not self.url:
|
|
162
|
-
raise ValueError("Must provide content or url")
|
|
163
|
-
|
|
164
|
-
# Either upload the content or use the url
|
|
165
|
-
url = None
|
|
166
|
-
if self.content:
|
|
167
|
-
url = client.upload_element(content=self.content, mime="application/pdf")
|
|
168
|
-
else:
|
|
169
|
-
url = self.url
|
|
170
|
-
|
|
171
|
-
if url:
|
|
172
|
-
element = client.create_element(
|
|
173
|
-
name=self.name,
|
|
174
|
-
url=url,
|
|
175
|
-
type=self.type,
|
|
176
|
-
display=self.display,
|
|
177
|
-
for_id=for_id,
|
|
178
|
-
)
|
|
179
|
-
return element
|
|
180
|
-
|
|
181
|
-
def __post_init__(self):
|
|
182
|
-
if self.path:
|
|
183
|
-
with open(self.path, "rb") as f:
|
|
184
|
-
self.content = f.read()
|
|
185
|
-
elif self.content or self.url:
|
|
186
|
-
pass # do nothing here
|
|
187
|
-
else:
|
|
188
|
-
raise ValueError("Must provide either path, content or url")
|