chainlit 1.0.400__py3-none-any.whl → 2.0.3__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.

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