chainlit 1.0.401__py3-none-any.whl → 2.0.4__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 +123 -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 +191 -102
  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 +614 -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 +81 -29
  33. chainlit/frontend/dist/assets/DailyMotion-Ce9dQoqZ.js +1 -0
  34. chainlit/frontend/dist/assets/Dataframe-C1XonMcV.js +22 -0
  35. chainlit/frontend/dist/assets/Facebook-DVVt6lrr.js +1 -0
  36. chainlit/frontend/dist/assets/FilePlayer-c7stW4vz.js +1 -0
  37. chainlit/frontend/dist/assets/Kaltura-BmMmgorA.js +1 -0
  38. chainlit/frontend/dist/assets/Mixcloud-Cw8hDmiO.js +1 -0
  39. chainlit/frontend/dist/assets/Mux-DiRZfeUf.js +1 -0
  40. chainlit/frontend/dist/assets/Preview-6Jt2mRHx.js +1 -0
  41. chainlit/frontend/dist/assets/SoundCloud-DKwcT58_.js +1 -0
  42. chainlit/frontend/dist/assets/Streamable-BVdxrEeX.js +1 -0
  43. chainlit/frontend/dist/assets/Twitch-DFqZR7Gu.js +1 -0
  44. chainlit/frontend/dist/assets/Vidyard-0BQAAtVk.js +1 -0
  45. chainlit/frontend/dist/assets/Vimeo-CRFSH0Vu.js +1 -0
  46. chainlit/frontend/dist/assets/Wistia-CKrmdQaG.js +1 -0
  47. chainlit/frontend/dist/assets/YouTube-CQpL-rvU.js +1 -0
  48. chainlit/frontend/dist/assets/index-DQmLRKyv.css +1 -0
  49. chainlit/frontend/dist/assets/index-QdmxtIMQ.js +8665 -0
  50. chainlit/frontend/dist/assets/react-plotly-B9hvVpUG.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 +103 -68
  55. chainlit/langflow/__init__.py +1 -0
  56. chainlit/llama_index/callbacks.py +65 -40
  57. chainlit/markdown.py +22 -6
  58. chainlit/message.py +54 -56
  59. chainlit/mistralai/__init__.py +50 -0
  60. chainlit/oauth_providers.py +266 -8
  61. chainlit/openai/__init__.py +10 -18
  62. chainlit/secret.py +1 -1
  63. chainlit/server.py +789 -228
  64. chainlit/session.py +108 -90
  65. chainlit/slack/__init__.py +6 -0
  66. chainlit/slack/app.py +397 -0
  67. chainlit/socket.py +199 -116
  68. chainlit/step.py +141 -89
  69. chainlit/sync.py +2 -1
  70. chainlit/teams/__init__.py +6 -0
  71. chainlit/teams/app.py +338 -0
  72. chainlit/translations/bn.json +244 -0
  73. chainlit/translations/en-US.json +122 -8
  74. chainlit/translations/gu.json +244 -0
  75. chainlit/translations/he-IL.json +244 -0
  76. chainlit/translations/hi.json +244 -0
  77. chainlit/translations/ja.json +242 -0
  78. chainlit/translations/kn.json +244 -0
  79. chainlit/translations/ml.json +244 -0
  80. chainlit/translations/mr.json +244 -0
  81. chainlit/translations/nl-NL.json +242 -0
  82. chainlit/translations/ta.json +244 -0
  83. chainlit/translations/te.json +244 -0
  84. chainlit/translations/zh-CN.json +243 -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.401.dist-info → chainlit-2.0.4.dist-info}/METADATA +48 -50
  92. chainlit-2.0.4.dist-info/RECORD +107 -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.401.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.401.dist-info → chainlit-2.0.4.dist-info}/WHEEL +0 -0
  113. {chainlit-1.0.401.dist-info → chainlit-2.0.4.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,500 @@
1
+ import json
2
+ from typing import Dict, List, Literal, Optional, Union, cast
3
+
4
+ import aiofiles
5
+ from httpx import HTTPStatusError, RequestError
6
+ from literalai import (
7
+ Attachment as LiteralAttachment,
8
+ Score as LiteralScore,
9
+ Step as LiteralStep,
10
+ Thread as LiteralThread,
11
+ )
12
+ from literalai.observability.filter import threads_filters as LiteralThreadsFilters
13
+ from literalai.observability.step import StepDict as LiteralStepDict
14
+
15
+ from chainlit.data.base import BaseDataLayer
16
+ from chainlit.data.utils import queue_until_user_message
17
+ from chainlit.element import Audio, Element, ElementDict, File, Image, Pdf, Text, Video
18
+ from chainlit.logger import logger
19
+ from chainlit.step import (
20
+ FeedbackDict,
21
+ Step,
22
+ StepDict,
23
+ StepType,
24
+ TrueStepType,
25
+ check_add_step_in_cot,
26
+ stub_step,
27
+ )
28
+ from chainlit.types import (
29
+ Feedback,
30
+ PageInfo,
31
+ PaginatedResponse,
32
+ Pagination,
33
+ ThreadDict,
34
+ ThreadFilter,
35
+ )
36
+ from chainlit.user import PersistedUser, User
37
+
38
+
39
+ class LiteralToChainlitConverter:
40
+ @classmethod
41
+ def steptype_to_steptype(cls, step_type: Optional[StepType]) -> TrueStepType:
42
+ return cast(TrueStepType, step_type or "undefined")
43
+
44
+ @classmethod
45
+ def score_to_feedbackdict(
46
+ cls,
47
+ score: Optional[LiteralScore],
48
+ ) -> "Optional[FeedbackDict]":
49
+ if not score:
50
+ return None
51
+ return {
52
+ "id": score.id or "",
53
+ "forId": score.step_id or "",
54
+ "value": cast(Literal[0, 1], score.value),
55
+ "comment": score.comment,
56
+ }
57
+
58
+ @classmethod
59
+ def step_to_stepdict(cls, step: LiteralStep) -> "StepDict":
60
+ metadata = step.metadata or {}
61
+ input = (step.input or {}).get("content") or (
62
+ json.dumps(step.input) if step.input and step.input != {} else ""
63
+ )
64
+ output = (step.output or {}).get("content") or (
65
+ json.dumps(step.output) if step.output and step.output != {} else ""
66
+ )
67
+
68
+ user_feedback = (
69
+ next(
70
+ (
71
+ s
72
+ for s in step.scores
73
+ if s.type == "HUMAN" and s.name == "user-feedback"
74
+ ),
75
+ None,
76
+ )
77
+ if step.scores
78
+ else None
79
+ )
80
+
81
+ return {
82
+ "createdAt": step.created_at,
83
+ "id": step.id or "",
84
+ "threadId": step.thread_id or "",
85
+ "parentId": step.parent_id,
86
+ "feedback": cls.score_to_feedbackdict(user_feedback),
87
+ "start": step.start_time,
88
+ "end": step.end_time,
89
+ "type": step.type or "undefined",
90
+ "name": step.name or "",
91
+ "generation": step.generation.to_dict() if step.generation else None,
92
+ "input": input,
93
+ "output": output,
94
+ "showInput": metadata.get("showInput", False),
95
+ "language": metadata.get("language"),
96
+ "isError": bool(step.error),
97
+ "waitForAnswer": metadata.get("waitForAnswer", False),
98
+ }
99
+
100
+ @classmethod
101
+ def attachment_to_elementdict(cls, attachment: LiteralAttachment) -> ElementDict:
102
+ metadata = attachment.metadata or {}
103
+ return {
104
+ "chainlitKey": None,
105
+ "display": metadata.get("display", "side"),
106
+ "language": metadata.get("language"),
107
+ "autoPlay": metadata.get("autoPlay", None),
108
+ "playerConfig": metadata.get("playerConfig", None),
109
+ "page": metadata.get("page"),
110
+ "props": metadata.get("props"),
111
+ "size": metadata.get("size"),
112
+ "type": metadata.get("type", "file"),
113
+ "forId": attachment.step_id,
114
+ "id": attachment.id or "",
115
+ "mime": attachment.mime,
116
+ "name": attachment.name or "",
117
+ "objectKey": attachment.object_key,
118
+ "url": attachment.url,
119
+ "threadId": attachment.thread_id,
120
+ }
121
+
122
+ @classmethod
123
+ def attachment_to_element(
124
+ cls, attachment: LiteralAttachment, thread_id: Optional[str] = None
125
+ ) -> Element:
126
+ metadata = attachment.metadata or {}
127
+ element_type = metadata.get("type", "file")
128
+
129
+ element_class = {
130
+ "file": File,
131
+ "image": Image,
132
+ "audio": Audio,
133
+ "video": Video,
134
+ "text": Text,
135
+ "pdf": Pdf,
136
+ }.get(element_type, Element)
137
+
138
+ assert thread_id or attachment.thread_id
139
+
140
+ element = element_class(
141
+ name=attachment.name or "",
142
+ display=metadata.get("display", "side"),
143
+ language=metadata.get("language"),
144
+ size=metadata.get("size"),
145
+ url=attachment.url,
146
+ mime=attachment.mime,
147
+ thread_id=thread_id or attachment.thread_id,
148
+ )
149
+ element.id = attachment.id or ""
150
+ element.for_id = attachment.step_id
151
+ element.object_key = attachment.object_key
152
+ return element
153
+
154
+ @classmethod
155
+ def step_to_step(cls, step: LiteralStep) -> Step:
156
+ chainlit_step = Step(
157
+ name=step.name or "",
158
+ type=cls.steptype_to_steptype(step.type),
159
+ id=step.id,
160
+ parent_id=step.parent_id,
161
+ thread_id=step.thread_id or None,
162
+ )
163
+ chainlit_step.start = step.start_time
164
+ chainlit_step.end = step.end_time
165
+ chainlit_step.created_at = step.created_at
166
+ chainlit_step.input = step.input.get("content", "") if step.input else ""
167
+ chainlit_step.output = step.output.get("content", "") if step.output else ""
168
+ chainlit_step.is_error = bool(step.error)
169
+ chainlit_step.metadata = step.metadata or {}
170
+ chainlit_step.tags = step.tags
171
+ chainlit_step.generation = step.generation
172
+
173
+ if step.attachments:
174
+ chainlit_step.elements = [
175
+ cls.attachment_to_element(attachment, chainlit_step.thread_id)
176
+ for attachment in step.attachments
177
+ ]
178
+
179
+ return chainlit_step
180
+
181
+ @classmethod
182
+ def thread_to_threaddict(cls, thread: LiteralThread) -> ThreadDict:
183
+ return {
184
+ "id": thread.id,
185
+ "createdAt": getattr(thread, "created_at", ""),
186
+ "name": thread.name,
187
+ "userId": thread.participant_id,
188
+ "userIdentifier": thread.participant_identifier,
189
+ "tags": thread.tags,
190
+ "metadata": thread.metadata,
191
+ "steps": [cls.step_to_stepdict(step) for step in thread.steps]
192
+ if thread.steps
193
+ else [],
194
+ "elements": [
195
+ cls.attachment_to_elementdict(attachment)
196
+ for step in thread.steps
197
+ for attachment in step.attachments
198
+ ]
199
+ if thread.steps
200
+ else [],
201
+ }
202
+
203
+
204
+ class LiteralDataLayer(BaseDataLayer):
205
+ def __init__(self, api_key: str, server: Optional[str]):
206
+ from literalai import AsyncLiteralClient
207
+
208
+ self.client = AsyncLiteralClient(api_key=api_key, url=server)
209
+ logger.info("Chainlit data layer initialized")
210
+
211
+ async def build_debug_url(self) -> str:
212
+ try:
213
+ project_id = await self.client.api.get_my_project_id()
214
+ return f"{self.client.api.url}/projects/{project_id}/logs/threads/[thread_id]?currentStepId=[step_id]"
215
+ except Exception as e:
216
+ logger.error(f"Error building debug url: {e}")
217
+ return ""
218
+
219
+ async def get_user(self, identifier: str) -> Optional[PersistedUser]:
220
+ user = await self.client.api.get_user(identifier=identifier)
221
+ if not user:
222
+ return None
223
+ return PersistedUser(
224
+ id=user.id or "",
225
+ identifier=user.identifier or "",
226
+ metadata=user.metadata,
227
+ createdAt=user.created_at or "",
228
+ )
229
+
230
+ async def create_user(self, user: User) -> Optional[PersistedUser]:
231
+ _user = await self.client.api.get_user(identifier=user.identifier)
232
+ if not _user:
233
+ _user = await self.client.api.create_user(
234
+ identifier=user.identifier, metadata=user.metadata
235
+ )
236
+ elif _user.id:
237
+ await self.client.api.update_user(id=_user.id, metadata=user.metadata)
238
+ return PersistedUser(
239
+ id=_user.id or "",
240
+ identifier=_user.identifier or "",
241
+ metadata=user.metadata,
242
+ createdAt=_user.created_at or "",
243
+ )
244
+
245
+ async def delete_feedback(
246
+ self,
247
+ feedback_id: str,
248
+ ):
249
+ if feedback_id:
250
+ await self.client.api.delete_score(
251
+ id=feedback_id,
252
+ )
253
+ return True
254
+ return False
255
+
256
+ async def upsert_feedback(
257
+ self,
258
+ feedback: Feedback,
259
+ ):
260
+ if feedback.id:
261
+ await self.client.api.update_score(
262
+ id=feedback.id,
263
+ update_params={
264
+ "comment": feedback.comment,
265
+ "value": feedback.value,
266
+ },
267
+ )
268
+ return feedback.id
269
+ else:
270
+ created = await self.client.api.create_score(
271
+ step_id=feedback.forId,
272
+ value=feedback.value,
273
+ comment=feedback.comment,
274
+ name="user-feedback",
275
+ type="HUMAN",
276
+ )
277
+ return created.id or ""
278
+
279
+ async def safely_send_steps(self, steps):
280
+ try:
281
+ await self.client.api.send_steps(steps)
282
+ except HTTPStatusError as e:
283
+ logger.error(f"HTTP Request: error sending steps: {e.response.status_code}")
284
+ except RequestError as e:
285
+ logger.error(f"HTTP Request: error for {e.request.url!r}.")
286
+
287
+ @queue_until_user_message()
288
+ async def create_element(self, element: "Element"):
289
+ metadata = {
290
+ "size": element.size,
291
+ "language": element.language,
292
+ "display": element.display,
293
+ "type": element.type,
294
+ "page": getattr(element, "page", None),
295
+ }
296
+
297
+ if not element.for_id:
298
+ return
299
+
300
+ object_key = None
301
+
302
+ if not element.url:
303
+ if element.path:
304
+ async with aiofiles.open(element.path, "rb") as f:
305
+ content: Union[bytes, str] = await f.read()
306
+ elif element.content:
307
+ content = element.content
308
+ else:
309
+ raise ValueError("Either path or content must be provided")
310
+ uploaded = await self.client.api.upload_file(
311
+ content=content, mime=element.mime, thread_id=element.thread_id
312
+ )
313
+ object_key = uploaded["object_key"]
314
+
315
+ await self.safely_send_steps(
316
+ [
317
+ {
318
+ "id": element.for_id,
319
+ "threadId": element.thread_id,
320
+ "attachments": [
321
+ {
322
+ "id": element.id,
323
+ "name": element.name,
324
+ "metadata": metadata,
325
+ "mime": element.mime,
326
+ "url": element.url,
327
+ "objectKey": object_key,
328
+ }
329
+ ],
330
+ }
331
+ ]
332
+ )
333
+
334
+ async def get_element(
335
+ self, thread_id: str, element_id: str
336
+ ) -> Optional["ElementDict"]:
337
+ attachment = await self.client.api.get_attachment(id=element_id)
338
+ if not attachment:
339
+ return None
340
+ return LiteralToChainlitConverter.attachment_to_elementdict(attachment)
341
+
342
+ @queue_until_user_message()
343
+ async def delete_element(self, element_id: str, thread_id: Optional[str] = None):
344
+ await self.client.api.delete_attachment(id=element_id)
345
+
346
+ @queue_until_user_message()
347
+ async def create_step(self, step_dict: "StepDict"):
348
+ metadata = dict(
349
+ step_dict.get("metadata", {}),
350
+ waitForAnswer=step_dict.get("waitForAnswer"),
351
+ language=step_dict.get("language"),
352
+ showInput=step_dict.get("showInput"),
353
+ )
354
+
355
+ step: LiteralStepDict = {
356
+ "createdAt": step_dict.get("createdAt"),
357
+ "startTime": step_dict.get("start"),
358
+ "endTime": step_dict.get("end"),
359
+ "generation": step_dict.get("generation"),
360
+ "id": step_dict.get("id"),
361
+ "parentId": step_dict.get("parentId"),
362
+ "name": step_dict.get("name"),
363
+ "threadId": step_dict.get("threadId"),
364
+ "type": step_dict.get("type"),
365
+ "tags": step_dict.get("tags"),
366
+ "metadata": metadata,
367
+ }
368
+ if step_dict.get("input"):
369
+ step["input"] = {"content": step_dict.get("input")}
370
+ if step_dict.get("output"):
371
+ step["output"] = {"content": step_dict.get("output")}
372
+ if step_dict.get("isError"):
373
+ step["error"] = step_dict.get("output")
374
+
375
+ await self.safely_send_steps([step])
376
+
377
+ @queue_until_user_message()
378
+ async def update_step(self, step_dict: "StepDict"):
379
+ await self.create_step(step_dict)
380
+
381
+ @queue_until_user_message()
382
+ async def delete_step(self, step_id: str):
383
+ await self.client.api.delete_step(id=step_id)
384
+
385
+ async def get_thread_author(self, thread_id: str) -> str:
386
+ thread = await self.get_thread(thread_id)
387
+ if not thread:
388
+ return ""
389
+ user_identifier = thread.get("userIdentifier")
390
+ if not user_identifier:
391
+ return ""
392
+
393
+ return user_identifier
394
+
395
+ async def delete_thread(self, thread_id: str):
396
+ await self.client.api.delete_thread(id=thread_id)
397
+
398
+ async def list_threads(
399
+ self, pagination: "Pagination", filters: "ThreadFilter"
400
+ ) -> "PaginatedResponse[ThreadDict]":
401
+ if not filters.userId:
402
+ raise ValueError("userId is required")
403
+
404
+ literal_filters: LiteralThreadsFilters = [
405
+ {
406
+ "field": "participantId",
407
+ "operator": "eq",
408
+ "value": filters.userId,
409
+ }
410
+ ]
411
+
412
+ if filters.search:
413
+ literal_filters.append(
414
+ {
415
+ "field": "stepOutput",
416
+ "operator": "ilike",
417
+ "value": filters.search,
418
+ "path": "content",
419
+ }
420
+ )
421
+
422
+ if filters.feedback is not None:
423
+ literal_filters.append(
424
+ {
425
+ "field": "scoreValue",
426
+ "operator": "eq",
427
+ "value": filters.feedback,
428
+ "path": "user-feedback",
429
+ }
430
+ )
431
+
432
+ literal_response = await self.client.api.list_threads(
433
+ first=pagination.first,
434
+ after=pagination.cursor,
435
+ filters=literal_filters,
436
+ order_by={"column": "createdAt", "direction": "DESC"},
437
+ )
438
+
439
+ chainlit_threads = [
440
+ *map(LiteralToChainlitConverter.thread_to_threaddict, literal_response.data)
441
+ ]
442
+
443
+ return PaginatedResponse(
444
+ pageInfo=PageInfo(
445
+ hasNextPage=literal_response.page_info.has_next_page,
446
+ startCursor=literal_response.page_info.start_cursor,
447
+ endCursor=literal_response.page_info.end_cursor,
448
+ ),
449
+ data=chainlit_threads,
450
+ )
451
+
452
+ async def get_thread(self, thread_id: str) -> Optional[ThreadDict]:
453
+ thread = await self.client.api.get_thread(id=thread_id)
454
+ if not thread:
455
+ return None
456
+
457
+ elements: List[ElementDict] = []
458
+ steps: List[StepDict] = []
459
+ if thread.steps:
460
+ for step in thread.steps:
461
+ for attachment in step.attachments:
462
+ elements.append(
463
+ LiteralToChainlitConverter.attachment_to_elementdict(attachment)
464
+ )
465
+
466
+ chainlit_step = LiteralToChainlitConverter.step_to_step(step)
467
+ if check_add_step_in_cot(chainlit_step):
468
+ steps.append(
469
+ LiteralToChainlitConverter.step_to_stepdict(step)
470
+ ) # TODO: chainlit_step.to_dict()
471
+ else:
472
+ steps.append(stub_step(chainlit_step))
473
+
474
+ return {
475
+ "createdAt": thread.created_at or "",
476
+ "id": thread.id,
477
+ "name": thread.name or None,
478
+ "steps": steps,
479
+ "elements": elements,
480
+ "metadata": thread.metadata,
481
+ "userId": thread.participant_id,
482
+ "userIdentifier": thread.participant_identifier,
483
+ "tags": thread.tags,
484
+ }
485
+
486
+ async def update_thread(
487
+ self,
488
+ thread_id: str,
489
+ name: Optional[str] = None,
490
+ user_id: Optional[str] = None,
491
+ metadata: Optional[Dict] = None,
492
+ tags: Optional[List[str]] = None,
493
+ ):
494
+ await self.client.api.upsert_thread(
495
+ id=thread_id,
496
+ name=name,
497
+ participant_id=user_id,
498
+ metadata=metadata,
499
+ tags=tags,
500
+ )