chainlit 2.7.0__py3-none-any.whl → 2.7.1__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-2.7.0.dist-info → chainlit-2.7.1.dist-info}/METADATA +1 -1
- chainlit-2.7.1.dist-info/RECORD +4 -0
- chainlit/__init__.py +0 -207
- chainlit/__main__.py +0 -4
- chainlit/_utils.py +0 -8
- chainlit/action.py +0 -33
- chainlit/auth/__init__.py +0 -95
- chainlit/auth/cookie.py +0 -197
- chainlit/auth/jwt.py +0 -42
- chainlit/cache.py +0 -45
- chainlit/callbacks.py +0 -433
- chainlit/chat_context.py +0 -64
- chainlit/chat_settings.py +0 -34
- chainlit/cli/__init__.py +0 -235
- chainlit/config.py +0 -621
- chainlit/context.py +0 -112
- chainlit/data/__init__.py +0 -111
- chainlit/data/acl.py +0 -19
- chainlit/data/base.py +0 -107
- chainlit/data/chainlit_data_layer.py +0 -687
- chainlit/data/dynamodb.py +0 -616
- chainlit/data/literalai.py +0 -501
- chainlit/data/sql_alchemy.py +0 -741
- chainlit/data/storage_clients/__init__.py +0 -0
- chainlit/data/storage_clients/azure.py +0 -84
- chainlit/data/storage_clients/azure_blob.py +0 -94
- chainlit/data/storage_clients/base.py +0 -28
- chainlit/data/storage_clients/gcs.py +0 -101
- chainlit/data/storage_clients/s3.py +0 -88
- chainlit/data/utils.py +0 -29
- chainlit/discord/__init__.py +0 -6
- chainlit/discord/app.py +0 -364
- chainlit/element.py +0 -454
- chainlit/emitter.py +0 -450
- chainlit/hello.py +0 -12
- chainlit/input_widget.py +0 -182
- chainlit/langchain/__init__.py +0 -6
- chainlit/langchain/callbacks.py +0 -682
- chainlit/langflow/__init__.py +0 -25
- chainlit/llama_index/__init__.py +0 -6
- chainlit/llama_index/callbacks.py +0 -206
- chainlit/logger.py +0 -16
- chainlit/markdown.py +0 -57
- chainlit/mcp.py +0 -99
- chainlit/message.py +0 -619
- chainlit/mistralai/__init__.py +0 -50
- chainlit/oauth_providers.py +0 -835
- chainlit/openai/__init__.py +0 -53
- chainlit/py.typed +0 -0
- chainlit/secret.py +0 -9
- chainlit/semantic_kernel/__init__.py +0 -111
- chainlit/server.py +0 -1616
- chainlit/session.py +0 -304
- chainlit/sidebar.py +0 -55
- chainlit/slack/__init__.py +0 -6
- chainlit/slack/app.py +0 -427
- chainlit/socket.py +0 -381
- chainlit/step.py +0 -490
- chainlit/sync.py +0 -43
- chainlit/teams/__init__.py +0 -6
- chainlit/teams/app.py +0 -348
- chainlit/translations/bn.json +0 -214
- chainlit/translations/el-GR.json +0 -214
- chainlit/translations/en-US.json +0 -214
- chainlit/translations/fr-FR.json +0 -214
- chainlit/translations/gu.json +0 -214
- chainlit/translations/he-IL.json +0 -214
- chainlit/translations/hi.json +0 -214
- chainlit/translations/ja.json +0 -214
- chainlit/translations/kn.json +0 -214
- chainlit/translations/ml.json +0 -214
- chainlit/translations/mr.json +0 -214
- chainlit/translations/nl.json +0 -214
- chainlit/translations/ta.json +0 -214
- chainlit/translations/te.json +0 -214
- chainlit/translations/zh-CN.json +0 -214
- chainlit/translations.py +0 -60
- chainlit/types.py +0 -334
- chainlit/user.py +0 -43
- chainlit/user_session.py +0 -153
- chainlit/utils.py +0 -173
- chainlit/version.py +0 -8
- chainlit-2.7.0.dist-info/RECORD +0 -84
- {chainlit-2.7.0.dist-info → chainlit-2.7.1.dist-info}/WHEEL +0 -0
- {chainlit-2.7.0.dist-info → chainlit-2.7.1.dist-info}/entry_points.txt +0 -0
chainlit/data/dynamodb.py
DELETED
|
@@ -1,616 +0,0 @@
|
|
|
1
|
-
import asyncio
|
|
2
|
-
import json
|
|
3
|
-
import logging
|
|
4
|
-
import os
|
|
5
|
-
import random
|
|
6
|
-
from dataclasses import asdict
|
|
7
|
-
from datetime import datetime
|
|
8
|
-
from decimal import Decimal
|
|
9
|
-
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union
|
|
10
|
-
|
|
11
|
-
import aiofiles
|
|
12
|
-
import aiohttp
|
|
13
|
-
import boto3 # type: ignore
|
|
14
|
-
from boto3.dynamodb.types import TypeDeserializer, TypeSerializer
|
|
15
|
-
|
|
16
|
-
from chainlit.context import context
|
|
17
|
-
from chainlit.data.base import BaseDataLayer
|
|
18
|
-
from chainlit.data.storage_clients.base import BaseStorageClient
|
|
19
|
-
from chainlit.data.utils import queue_until_user_message
|
|
20
|
-
from chainlit.element import ElementDict
|
|
21
|
-
from chainlit.logger import logger
|
|
22
|
-
from chainlit.step import StepDict
|
|
23
|
-
from chainlit.types import (
|
|
24
|
-
Feedback,
|
|
25
|
-
PageInfo,
|
|
26
|
-
PaginatedResponse,
|
|
27
|
-
Pagination,
|
|
28
|
-
ThreadDict,
|
|
29
|
-
ThreadFilter,
|
|
30
|
-
)
|
|
31
|
-
from chainlit.user import PersistedUser, User
|
|
32
|
-
|
|
33
|
-
if TYPE_CHECKING:
|
|
34
|
-
from mypy_boto3_dynamodb import DynamoDBClient
|
|
35
|
-
|
|
36
|
-
from chainlit.element import Element
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
_logger = logger.getChild("DynamoDB")
|
|
40
|
-
_logger.setLevel(logging.WARNING)
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
class DynamoDBDataLayer(BaseDataLayer):
|
|
44
|
-
def __init__(
|
|
45
|
-
self,
|
|
46
|
-
table_name: str,
|
|
47
|
-
client: Optional["DynamoDBClient"] = None,
|
|
48
|
-
storage_provider: Optional[BaseStorageClient] = None,
|
|
49
|
-
user_thread_limit: int = 10,
|
|
50
|
-
):
|
|
51
|
-
if client:
|
|
52
|
-
self.client = client
|
|
53
|
-
else:
|
|
54
|
-
region_name = os.environ.get("AWS_REGION", "us-east-1")
|
|
55
|
-
self.client = boto3.client("dynamodb", region_name=region_name) # type: ignore
|
|
56
|
-
|
|
57
|
-
self.table_name = table_name
|
|
58
|
-
self.storage_provider = storage_provider
|
|
59
|
-
self.user_thread_limit = user_thread_limit
|
|
60
|
-
|
|
61
|
-
self._type_deserializer = TypeDeserializer()
|
|
62
|
-
self._type_serializer = TypeSerializer()
|
|
63
|
-
|
|
64
|
-
def _get_current_timestamp(self) -> str:
|
|
65
|
-
return datetime.now().isoformat() + "Z"
|
|
66
|
-
|
|
67
|
-
def _serialize_item(self, item: dict[str, Any]) -> dict[str, Any]:
|
|
68
|
-
def convert_floats(obj):
|
|
69
|
-
if isinstance(obj, float):
|
|
70
|
-
return Decimal(str(obj))
|
|
71
|
-
elif isinstance(obj, dict):
|
|
72
|
-
return {k: convert_floats(v) for k, v in obj.items()}
|
|
73
|
-
elif isinstance(obj, list):
|
|
74
|
-
return [convert_floats(v) for v in obj]
|
|
75
|
-
else:
|
|
76
|
-
return obj
|
|
77
|
-
|
|
78
|
-
return {
|
|
79
|
-
key: self._type_serializer.serialize(convert_floats(value))
|
|
80
|
-
for key, value in item.items()
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
def _deserialize_item(self, item: dict[str, Any]) -> dict[str, Any]:
|
|
84
|
-
def convert_decimals(obj):
|
|
85
|
-
if isinstance(obj, Decimal):
|
|
86
|
-
return float(obj)
|
|
87
|
-
elif isinstance(obj, dict):
|
|
88
|
-
return {k: convert_decimals(v) for k, v in obj.items()}
|
|
89
|
-
elif isinstance(obj, list):
|
|
90
|
-
return [convert_decimals(v) for v in obj]
|
|
91
|
-
else:
|
|
92
|
-
return obj
|
|
93
|
-
|
|
94
|
-
return {
|
|
95
|
-
key: convert_decimals(self._type_deserializer.deserialize(value))
|
|
96
|
-
for key, value in item.items()
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
def _update_item(self, key: Dict[str, Any], updates: Dict[str, Any]):
|
|
100
|
-
update_expr: List[str] = []
|
|
101
|
-
expression_attribute_names = {}
|
|
102
|
-
expression_attribute_values = {}
|
|
103
|
-
|
|
104
|
-
for index, (attr, value) in enumerate(updates.items()):
|
|
105
|
-
if not value:
|
|
106
|
-
continue
|
|
107
|
-
|
|
108
|
-
k, v = f"#{index}", f":{index}"
|
|
109
|
-
update_expr.append(f"{k} = {v}")
|
|
110
|
-
expression_attribute_names[k] = attr
|
|
111
|
-
expression_attribute_values[v] = value
|
|
112
|
-
|
|
113
|
-
self.client.update_item(
|
|
114
|
-
TableName=self.table_name,
|
|
115
|
-
Key=self._serialize_item(key),
|
|
116
|
-
UpdateExpression="SET " + ", ".join(update_expr),
|
|
117
|
-
ExpressionAttributeNames=expression_attribute_names,
|
|
118
|
-
ExpressionAttributeValues=self._serialize_item(expression_attribute_values),
|
|
119
|
-
)
|
|
120
|
-
|
|
121
|
-
@property
|
|
122
|
-
def context(self):
|
|
123
|
-
return context
|
|
124
|
-
|
|
125
|
-
async def get_user(self, identifier: str) -> Optional["PersistedUser"]:
|
|
126
|
-
_logger.info("DynamoDB: get_user identifier=%s", identifier)
|
|
127
|
-
|
|
128
|
-
response = self.client.get_item(
|
|
129
|
-
TableName=self.table_name,
|
|
130
|
-
Key={
|
|
131
|
-
"PK": {"S": f"USER#{identifier}"},
|
|
132
|
-
"SK": {"S": "USER"},
|
|
133
|
-
},
|
|
134
|
-
)
|
|
135
|
-
|
|
136
|
-
if "Item" not in response:
|
|
137
|
-
return None
|
|
138
|
-
|
|
139
|
-
user = self._deserialize_item(response["Item"])
|
|
140
|
-
|
|
141
|
-
return PersistedUser(
|
|
142
|
-
id=user["id"],
|
|
143
|
-
identifier=user["identifier"],
|
|
144
|
-
createdAt=user["createdAt"],
|
|
145
|
-
metadata=user["metadata"],
|
|
146
|
-
)
|
|
147
|
-
|
|
148
|
-
async def create_user(self, user: "User") -> Optional["PersistedUser"]:
|
|
149
|
-
_logger.info("DynamoDB: create_user user.identifier=%s", user.identifier)
|
|
150
|
-
|
|
151
|
-
ts = self._get_current_timestamp()
|
|
152
|
-
metadata: Dict[Any, Any] = user.metadata # type: ignore
|
|
153
|
-
|
|
154
|
-
item = {
|
|
155
|
-
"PK": f"USER#{user.identifier}",
|
|
156
|
-
"SK": "USER",
|
|
157
|
-
"id": user.identifier,
|
|
158
|
-
"identifier": user.identifier,
|
|
159
|
-
"metadata": metadata,
|
|
160
|
-
"createdAt": ts,
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
self.client.put_item(
|
|
164
|
-
TableName=self.table_name,
|
|
165
|
-
Item=self._serialize_item(item),
|
|
166
|
-
)
|
|
167
|
-
|
|
168
|
-
return PersistedUser(
|
|
169
|
-
id=user.identifier,
|
|
170
|
-
identifier=user.identifier,
|
|
171
|
-
createdAt=ts,
|
|
172
|
-
metadata=metadata,
|
|
173
|
-
)
|
|
174
|
-
|
|
175
|
-
async def delete_feedback(self, feedback_id: str) -> bool:
|
|
176
|
-
_logger.info("DynamoDB: delete_feedback feedback_id=%s", feedback_id)
|
|
177
|
-
|
|
178
|
-
# feedback id = THREAD#{thread_id}::STEP#{step_id}
|
|
179
|
-
thread_id, step_id = feedback_id.split("::")
|
|
180
|
-
thread_id = thread_id.strip("THREAD#")
|
|
181
|
-
step_id = step_id.strip("STEP#")
|
|
182
|
-
|
|
183
|
-
self.client.update_item(
|
|
184
|
-
TableName=self.table_name,
|
|
185
|
-
Key={
|
|
186
|
-
"PK": {"S": f"THREAD#{thread_id}"},
|
|
187
|
-
"SK": {"S": f"STEP#{step_id}"},
|
|
188
|
-
},
|
|
189
|
-
UpdateExpression="REMOVE #feedback",
|
|
190
|
-
ExpressionAttributeNames={"#feedback": "feedback"},
|
|
191
|
-
)
|
|
192
|
-
|
|
193
|
-
return True
|
|
194
|
-
|
|
195
|
-
async def upsert_feedback(self, feedback: Feedback) -> str:
|
|
196
|
-
_logger.info(
|
|
197
|
-
"DynamoDB: upsert_feedback thread=%s step=%s value=%s",
|
|
198
|
-
feedback.threadId,
|
|
199
|
-
feedback.forId,
|
|
200
|
-
feedback.value,
|
|
201
|
-
)
|
|
202
|
-
|
|
203
|
-
if not feedback.forId:
|
|
204
|
-
raise ValueError(
|
|
205
|
-
"DynamoDB data layer expects value for feedback.threadId got None"
|
|
206
|
-
)
|
|
207
|
-
|
|
208
|
-
feedback.id = f"THREAD#{feedback.threadId}::STEP#{feedback.forId}"
|
|
209
|
-
serialized_feedback = self._type_serializer.serialize(asdict(feedback))
|
|
210
|
-
|
|
211
|
-
self.client.update_item(
|
|
212
|
-
TableName=self.table_name,
|
|
213
|
-
Key={
|
|
214
|
-
"PK": {"S": f"THREAD#{feedback.threadId}"},
|
|
215
|
-
"SK": {"S": f"STEP#{feedback.forId}"},
|
|
216
|
-
},
|
|
217
|
-
UpdateExpression="SET #feedback = :feedback",
|
|
218
|
-
ExpressionAttributeNames={"#feedback": "feedback"},
|
|
219
|
-
ExpressionAttributeValues={":feedback": serialized_feedback},
|
|
220
|
-
)
|
|
221
|
-
|
|
222
|
-
return feedback.id
|
|
223
|
-
|
|
224
|
-
@queue_until_user_message()
|
|
225
|
-
async def create_element(self, element: "Element"):
|
|
226
|
-
_logger.info(
|
|
227
|
-
"DynamoDB: create_element thread=%s step=%s type=%s",
|
|
228
|
-
element.thread_id,
|
|
229
|
-
element.for_id,
|
|
230
|
-
element.type,
|
|
231
|
-
)
|
|
232
|
-
_logger.debug("DynamoDB: create_element: %s", element.to_dict())
|
|
233
|
-
|
|
234
|
-
if not element.for_id:
|
|
235
|
-
return
|
|
236
|
-
|
|
237
|
-
if not self.storage_provider:
|
|
238
|
-
_logger.warning(
|
|
239
|
-
"DynamoDB: create_element error. No storage_provider is configured!"
|
|
240
|
-
)
|
|
241
|
-
return
|
|
242
|
-
|
|
243
|
-
content: Optional[Union[bytes, str]] = None
|
|
244
|
-
|
|
245
|
-
if element.content:
|
|
246
|
-
content = element.content
|
|
247
|
-
|
|
248
|
-
elif element.path:
|
|
249
|
-
_logger.debug("DynamoDB: create_element reading file %s", element.path)
|
|
250
|
-
async with aiofiles.open(element.path, "rb") as f:
|
|
251
|
-
content = await f.read()
|
|
252
|
-
|
|
253
|
-
elif element.url:
|
|
254
|
-
_logger.debug("DynamoDB: create_element http %s", element.url)
|
|
255
|
-
async with aiohttp.ClientSession() as session:
|
|
256
|
-
async with session.get(element.url) as response:
|
|
257
|
-
if response.status == 200:
|
|
258
|
-
content = await response.read()
|
|
259
|
-
else:
|
|
260
|
-
raise ValueError(
|
|
261
|
-
f"Failed to read content from {element.url} status {response.status}",
|
|
262
|
-
)
|
|
263
|
-
|
|
264
|
-
else:
|
|
265
|
-
raise ValueError("Element url, path or content must be provided")
|
|
266
|
-
|
|
267
|
-
if content is None:
|
|
268
|
-
raise ValueError("Content is None, cannot upload file")
|
|
269
|
-
|
|
270
|
-
if not element.mime:
|
|
271
|
-
element.mime = "application/octet-stream"
|
|
272
|
-
|
|
273
|
-
context_user = self.context.session.user
|
|
274
|
-
user_folder = getattr(context_user, "id", "unknown")
|
|
275
|
-
file_object_key = f"{user_folder}/{element.thread_id}/{element.id}"
|
|
276
|
-
|
|
277
|
-
uploaded_file = await self.storage_provider.upload_file(
|
|
278
|
-
object_key=file_object_key,
|
|
279
|
-
data=content,
|
|
280
|
-
mime=element.mime,
|
|
281
|
-
overwrite=True,
|
|
282
|
-
)
|
|
283
|
-
if not uploaded_file:
|
|
284
|
-
raise ValueError(
|
|
285
|
-
"DynamoDB Error: create_element, Failed to persist data in storage_provider",
|
|
286
|
-
)
|
|
287
|
-
|
|
288
|
-
element_dict: Dict[str, Any] = element.to_dict() # type: ignore
|
|
289
|
-
element_dict.update(
|
|
290
|
-
{
|
|
291
|
-
"PK": f"THREAD#{element.thread_id}",
|
|
292
|
-
"SK": f"ELEMENT#{element.id}",
|
|
293
|
-
"url": uploaded_file.get("url"),
|
|
294
|
-
"objectKey": uploaded_file.get("object_key"),
|
|
295
|
-
}
|
|
296
|
-
)
|
|
297
|
-
|
|
298
|
-
self.client.put_item(
|
|
299
|
-
TableName=self.table_name,
|
|
300
|
-
Item=self._serialize_item(element_dict),
|
|
301
|
-
)
|
|
302
|
-
|
|
303
|
-
async def get_element(
|
|
304
|
-
self, thread_id: str, element_id: str
|
|
305
|
-
) -> Optional["ElementDict"]:
|
|
306
|
-
_logger.info(
|
|
307
|
-
"DynamoDB: get_element thread=%s element=%s", thread_id, element_id
|
|
308
|
-
)
|
|
309
|
-
|
|
310
|
-
response = self.client.get_item(
|
|
311
|
-
TableName=self.table_name,
|
|
312
|
-
Key={
|
|
313
|
-
"PK": {"S": f"THREAD#{thread_id}"},
|
|
314
|
-
"SK": {"S": f"ELEMENT#{element_id}"},
|
|
315
|
-
},
|
|
316
|
-
)
|
|
317
|
-
|
|
318
|
-
if "Item" not in response:
|
|
319
|
-
return None
|
|
320
|
-
|
|
321
|
-
return self._deserialize_item(response["Item"]) # type: ignore
|
|
322
|
-
|
|
323
|
-
@queue_until_user_message()
|
|
324
|
-
async def delete_element(self, element_id: str, thread_id: Optional[str] = None):
|
|
325
|
-
thread_id = self.context.session.thread_id
|
|
326
|
-
_logger.info(
|
|
327
|
-
"DynamoDB: delete_element thread=%s element=%s", thread_id, element_id
|
|
328
|
-
)
|
|
329
|
-
|
|
330
|
-
self.client.delete_item(
|
|
331
|
-
TableName=self.table_name,
|
|
332
|
-
Key={
|
|
333
|
-
"PK": {"S": f"THREAD#{thread_id}"},
|
|
334
|
-
"SK": {"S": f"ELEMENT#{element_id}"},
|
|
335
|
-
},
|
|
336
|
-
)
|
|
337
|
-
|
|
338
|
-
@queue_until_user_message()
|
|
339
|
-
async def create_step(self, step_dict: "StepDict"):
|
|
340
|
-
_logger.info(
|
|
341
|
-
"DynamoDB: create_step thread=%s step=%s",
|
|
342
|
-
step_dict.get("threadId"),
|
|
343
|
-
step_dict.get("id"),
|
|
344
|
-
)
|
|
345
|
-
_logger.debug("DynamoDB: create_step: %s", step_dict)
|
|
346
|
-
|
|
347
|
-
item = dict(step_dict)
|
|
348
|
-
item.update(
|
|
349
|
-
{
|
|
350
|
-
# ignore type, dynamo needs these so we want to fail if not set
|
|
351
|
-
"PK": f"THREAD#{step_dict['threadId']}", # type: ignore
|
|
352
|
-
"SK": f"STEP#{step_dict['id']}", # type: ignore
|
|
353
|
-
}
|
|
354
|
-
)
|
|
355
|
-
|
|
356
|
-
self.client.put_item(
|
|
357
|
-
TableName=self.table_name,
|
|
358
|
-
Item=self._serialize_item(item),
|
|
359
|
-
)
|
|
360
|
-
|
|
361
|
-
@queue_until_user_message()
|
|
362
|
-
async def update_step(self, step_dict: "StepDict"):
|
|
363
|
-
_logger.info(
|
|
364
|
-
"DynamoDB: update_step thread=%s step=%s",
|
|
365
|
-
step_dict.get("threadId"),
|
|
366
|
-
step_dict.get("id"),
|
|
367
|
-
)
|
|
368
|
-
_logger.debug("DynamoDB: update_step: %s", step_dict)
|
|
369
|
-
|
|
370
|
-
self._update_item(
|
|
371
|
-
key={
|
|
372
|
-
# ignore type, dynamo needs these so we want to fail if not set
|
|
373
|
-
"PK": f"THREAD#{step_dict['threadId']}", # type: ignore
|
|
374
|
-
"SK": f"STEP#{step_dict['id']}", # type: ignore
|
|
375
|
-
},
|
|
376
|
-
updates=step_dict, # type: ignore
|
|
377
|
-
)
|
|
378
|
-
|
|
379
|
-
@queue_until_user_message()
|
|
380
|
-
async def delete_step(self, step_id: str):
|
|
381
|
-
thread_id = self.context.session.thread_id
|
|
382
|
-
_logger.info("DynamoDB: delete_feedback thread=%s step=%s", thread_id, step_id)
|
|
383
|
-
|
|
384
|
-
self.client.delete_item(
|
|
385
|
-
TableName=self.table_name,
|
|
386
|
-
Key={
|
|
387
|
-
"PK": {"S": f"THREAD#{thread_id}"},
|
|
388
|
-
"SK": {"S": f"STEP#{step_id}"},
|
|
389
|
-
},
|
|
390
|
-
)
|
|
391
|
-
|
|
392
|
-
async def get_thread_author(self, thread_id: str) -> str:
|
|
393
|
-
_logger.info("DynamoDB: get_thread_author thread=%s", thread_id)
|
|
394
|
-
|
|
395
|
-
response = self.client.get_item(
|
|
396
|
-
TableName=self.table_name,
|
|
397
|
-
Key={
|
|
398
|
-
"PK": {"S": f"THREAD#{thread_id}"},
|
|
399
|
-
"SK": {"S": "THREAD"},
|
|
400
|
-
},
|
|
401
|
-
ProjectionExpression="userId",
|
|
402
|
-
)
|
|
403
|
-
|
|
404
|
-
if "Item" not in response:
|
|
405
|
-
raise ValueError(f"Author not found for thread_id {thread_id}")
|
|
406
|
-
|
|
407
|
-
item = self._deserialize_item(response["Item"])
|
|
408
|
-
return item["userId"]
|
|
409
|
-
|
|
410
|
-
async def delete_thread(self, thread_id: str):
|
|
411
|
-
_logger.info("DynamoDB: delete_thread thread=%s", thread_id)
|
|
412
|
-
|
|
413
|
-
thread = await self.get_thread(thread_id)
|
|
414
|
-
if not thread:
|
|
415
|
-
return
|
|
416
|
-
|
|
417
|
-
items: List[Any] = thread["steps"]
|
|
418
|
-
if thread["elements"]:
|
|
419
|
-
items.extend(thread["elements"])
|
|
420
|
-
|
|
421
|
-
delete_requests = []
|
|
422
|
-
for item in items:
|
|
423
|
-
key = self._serialize_item({"PK": item["PK"], "SK": item["SK"]})
|
|
424
|
-
req = {"DeleteRequest": {"Key": key}}
|
|
425
|
-
delete_requests.append(req)
|
|
426
|
-
|
|
427
|
-
BATCH_ITEM_SIZE = 25 # pylint: disable=invalid-name
|
|
428
|
-
for i in range(0, len(delete_requests), BATCH_ITEM_SIZE):
|
|
429
|
-
chunk = delete_requests[i : i + BATCH_ITEM_SIZE]
|
|
430
|
-
response = self.client.batch_write_item(
|
|
431
|
-
RequestItems={
|
|
432
|
-
self.table_name: chunk, # type: ignore
|
|
433
|
-
}
|
|
434
|
-
)
|
|
435
|
-
|
|
436
|
-
backoff_time = 1
|
|
437
|
-
while response.get("UnprocessedItems"):
|
|
438
|
-
backoff_time *= 2
|
|
439
|
-
# Cap the backoff time at 32 seconds & add jitter
|
|
440
|
-
delay = min(backoff_time, 32) + random.uniform(0, 1)
|
|
441
|
-
await asyncio.sleep(delay)
|
|
442
|
-
|
|
443
|
-
response = self.client.batch_write_item(
|
|
444
|
-
RequestItems=response["UnprocessedItems"]
|
|
445
|
-
)
|
|
446
|
-
|
|
447
|
-
self.client.delete_item(
|
|
448
|
-
TableName=self.table_name,
|
|
449
|
-
Key={
|
|
450
|
-
"PK": {"S": f"THREAD#{thread_id}"},
|
|
451
|
-
"SK": {"S": "THREAD"},
|
|
452
|
-
},
|
|
453
|
-
)
|
|
454
|
-
|
|
455
|
-
async def list_threads(
|
|
456
|
-
self, pagination: "Pagination", filters: "ThreadFilter"
|
|
457
|
-
) -> "PaginatedResponse[ThreadDict]":
|
|
458
|
-
_logger.info("DynamoDB: list_threads filters.userId=%s", filters.userId)
|
|
459
|
-
|
|
460
|
-
if filters.feedback:
|
|
461
|
-
_logger.warning("DynamoDB: filters on feedback not supported")
|
|
462
|
-
|
|
463
|
-
paginated_response: PaginatedResponse[ThreadDict] = PaginatedResponse(
|
|
464
|
-
data=[],
|
|
465
|
-
pageInfo=PageInfo(
|
|
466
|
-
hasNextPage=False, startCursor=pagination.cursor, endCursor=None
|
|
467
|
-
),
|
|
468
|
-
)
|
|
469
|
-
|
|
470
|
-
query_args: Dict[str, Any] = {
|
|
471
|
-
"TableName": self.table_name,
|
|
472
|
-
"IndexName": "UserThread",
|
|
473
|
-
"ScanIndexForward": False,
|
|
474
|
-
"Limit": self.user_thread_limit,
|
|
475
|
-
"KeyConditionExpression": "#UserThreadPK = :pk",
|
|
476
|
-
"ExpressionAttributeNames": {
|
|
477
|
-
"#UserThreadPK": "UserThreadPK",
|
|
478
|
-
},
|
|
479
|
-
"ExpressionAttributeValues": {
|
|
480
|
-
":pk": {"S": f"USER#{filters.userId}"},
|
|
481
|
-
},
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
if pagination.cursor:
|
|
485
|
-
query_args["ExclusiveStartKey"] = json.loads(pagination.cursor)
|
|
486
|
-
|
|
487
|
-
if filters.search:
|
|
488
|
-
query_args["FilterExpression"] = "contains(#name, :search)"
|
|
489
|
-
query_args["ExpressionAttributeNames"]["#name"] = "name"
|
|
490
|
-
query_args["ExpressionAttributeValues"][":search"] = {"S": filters.search}
|
|
491
|
-
|
|
492
|
-
response = self.client.query(**query_args) # type: ignore
|
|
493
|
-
|
|
494
|
-
if "LastEvaluatedKey" in response:
|
|
495
|
-
paginated_response.pageInfo.hasNextPage = True
|
|
496
|
-
paginated_response.pageInfo.endCursor = json.dumps(
|
|
497
|
-
response["LastEvaluatedKey"]
|
|
498
|
-
)
|
|
499
|
-
|
|
500
|
-
for item in response["Items"]:
|
|
501
|
-
deserialized_item: Dict[str, Any] = self._deserialize_item(item)
|
|
502
|
-
thread = ThreadDict( # type: ignore
|
|
503
|
-
id=deserialized_item["PK"].strip("THREAD#"),
|
|
504
|
-
createdAt=deserialized_item["UserThreadSK"].strip("TS#"),
|
|
505
|
-
name=deserialized_item["name"],
|
|
506
|
-
)
|
|
507
|
-
paginated_response.data.append(thread)
|
|
508
|
-
|
|
509
|
-
return paginated_response
|
|
510
|
-
|
|
511
|
-
async def get_thread(self, thread_id: str) -> "Optional[ThreadDict]":
|
|
512
|
-
_logger.info("DynamoDB: get_thread thread=%s", thread_id)
|
|
513
|
-
|
|
514
|
-
# Get all thread records
|
|
515
|
-
thread_items: List[Any] = []
|
|
516
|
-
|
|
517
|
-
cursor: Dict[str, Any] = {}
|
|
518
|
-
while True:
|
|
519
|
-
response = self.client.query(
|
|
520
|
-
TableName=self.table_name,
|
|
521
|
-
KeyConditionExpression="#pk = :pk",
|
|
522
|
-
ExpressionAttributeNames={"#pk": "PK"},
|
|
523
|
-
ExpressionAttributeValues={":pk": {"S": f"THREAD#{thread_id}"}},
|
|
524
|
-
**cursor,
|
|
525
|
-
)
|
|
526
|
-
|
|
527
|
-
deserialized_items = map(self._deserialize_item, response["Items"])
|
|
528
|
-
thread_items.extend(deserialized_items)
|
|
529
|
-
|
|
530
|
-
if "LastEvaluatedKey" not in response:
|
|
531
|
-
break
|
|
532
|
-
cursor["ExclusiveStartKey"] = response["LastEvaluatedKey"]
|
|
533
|
-
|
|
534
|
-
if len(thread_items) == 0:
|
|
535
|
-
return None
|
|
536
|
-
|
|
537
|
-
# process accordingly
|
|
538
|
-
thread_dict: Optional[ThreadDict] = None
|
|
539
|
-
steps = []
|
|
540
|
-
elements = []
|
|
541
|
-
|
|
542
|
-
for item in thread_items:
|
|
543
|
-
if item["SK"] == "THREAD":
|
|
544
|
-
thread_dict = item
|
|
545
|
-
|
|
546
|
-
elif item["SK"].startswith("ELEMENT"):
|
|
547
|
-
if self.storage_provider is not None:
|
|
548
|
-
item["url"] = await self.storage_provider.get_read_url(
|
|
549
|
-
object_key=item["objectKey"],
|
|
550
|
-
)
|
|
551
|
-
elements.append(item)
|
|
552
|
-
|
|
553
|
-
elif item["SK"].startswith("STEP"):
|
|
554
|
-
if "feedback" in item: # Decimal is not json serializable
|
|
555
|
-
item["feedback"]["value"] = int(item["feedback"]["value"])
|
|
556
|
-
steps.append(item)
|
|
557
|
-
|
|
558
|
-
if not thread_dict:
|
|
559
|
-
if len(thread_items) > 0:
|
|
560
|
-
_logger.warning(
|
|
561
|
-
"DynamoDB: found orphaned items for thread=%s", thread_id
|
|
562
|
-
)
|
|
563
|
-
return None
|
|
564
|
-
|
|
565
|
-
steps.sort(key=lambda i: i["createdAt"])
|
|
566
|
-
thread_dict.update(
|
|
567
|
-
{
|
|
568
|
-
"steps": steps,
|
|
569
|
-
"elements": elements,
|
|
570
|
-
}
|
|
571
|
-
)
|
|
572
|
-
|
|
573
|
-
return thread_dict
|
|
574
|
-
|
|
575
|
-
async def update_thread(
|
|
576
|
-
self,
|
|
577
|
-
thread_id: str,
|
|
578
|
-
name: Optional[str] = None,
|
|
579
|
-
user_id: Optional[str] = None,
|
|
580
|
-
metadata: Optional[Dict] = None,
|
|
581
|
-
tags: Optional[List[str]] = None,
|
|
582
|
-
):
|
|
583
|
-
_logger.info("DynamoDB: update_thread thread=%s userId=%s", thread_id, user_id)
|
|
584
|
-
_logger.debug(
|
|
585
|
-
"DynamoDB: update_thread name=%s tags=%s metadata=%s", name, tags, metadata
|
|
586
|
-
)
|
|
587
|
-
|
|
588
|
-
ts = self._get_current_timestamp()
|
|
589
|
-
|
|
590
|
-
item = {
|
|
591
|
-
# GSI: UserThread
|
|
592
|
-
"UserThreadSK": f"TS#{ts}",
|
|
593
|
-
#
|
|
594
|
-
"id": thread_id,
|
|
595
|
-
"createdAt": ts,
|
|
596
|
-
"name": name,
|
|
597
|
-
"userId": user_id,
|
|
598
|
-
"userIdentifier": user_id,
|
|
599
|
-
"tags": tags,
|
|
600
|
-
"metadata": metadata,
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
if user_id:
|
|
604
|
-
# user_id may be None on subsequent calls, don't update UserThreadPK to "USER#{None}"
|
|
605
|
-
item["UserThreadPK"] = f"USER#{user_id}"
|
|
606
|
-
|
|
607
|
-
self._update_item(
|
|
608
|
-
key={
|
|
609
|
-
"PK": f"THREAD#{thread_id}",
|
|
610
|
-
"SK": "THREAD",
|
|
611
|
-
},
|
|
612
|
-
updates=item,
|
|
613
|
-
)
|
|
614
|
-
|
|
615
|
-
async def build_debug_url(self) -> str:
|
|
616
|
-
return ""
|