chainlit 0.7.604rc2__py3-none-any.whl → 1.0.0rc0__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 (40) hide show
  1. chainlit/__init__.py +32 -23
  2. chainlit/auth.py +9 -10
  3. chainlit/cache.py +3 -3
  4. chainlit/cli/__init__.py +12 -2
  5. chainlit/config.py +22 -13
  6. chainlit/context.py +7 -3
  7. chainlit/data/__init__.py +375 -9
  8. chainlit/data/acl.py +6 -5
  9. chainlit/element.py +86 -123
  10. chainlit/emitter.py +117 -50
  11. chainlit/frontend/dist/assets/index-6aee009a.js +697 -0
  12. chainlit/frontend/dist/assets/{react-plotly-16f7de12.js → react-plotly-2f07c02a.js} +1 -1
  13. chainlit/frontend/dist/index.html +1 -1
  14. chainlit/haystack/callbacks.py +45 -43
  15. chainlit/hello.py +1 -1
  16. chainlit/langchain/callbacks.py +135 -120
  17. chainlit/llama_index/callbacks.py +68 -48
  18. chainlit/message.py +179 -207
  19. chainlit/oauth_providers.py +39 -34
  20. chainlit/playground/provider.py +44 -30
  21. chainlit/playground/providers/anthropic.py +4 -4
  22. chainlit/playground/providers/huggingface.py +2 -2
  23. chainlit/playground/providers/langchain.py +8 -10
  24. chainlit/playground/providers/openai.py +19 -13
  25. chainlit/server.py +155 -99
  26. chainlit/session.py +109 -40
  27. chainlit/socket.py +54 -38
  28. chainlit/step.py +393 -0
  29. chainlit/types.py +78 -21
  30. chainlit/user.py +32 -0
  31. chainlit/user_session.py +1 -5
  32. {chainlit-0.7.604rc2.dist-info → chainlit-1.0.0rc0.dist-info}/METADATA +12 -31
  33. chainlit-1.0.0rc0.dist-info/RECORD +60 -0
  34. chainlit/client/base.py +0 -169
  35. chainlit/client/cloud.py +0 -500
  36. chainlit/frontend/dist/assets/index-c58dbd4b.js +0 -871
  37. chainlit/prompt.py +0 -40
  38. chainlit-0.7.604rc2.dist-info/RECORD +0 -61
  39. {chainlit-0.7.604rc2.dist-info → chainlit-1.0.0rc0.dist-info}/WHEEL +0 -0
  40. {chainlit-0.7.604rc2.dist-info → chainlit-1.0.0rc0.dist-info}/entry_points.txt +0 -0
chainlit/element.py CHANGED
@@ -2,16 +2,14 @@ import json
2
2
  import uuid
3
3
  from enum import Enum
4
4
  from io import BytesIO
5
- from typing import Any, ClassVar, Dict, List, Optional, TypeVar, Union, cast
5
+ from typing import Any, ClassVar, List, Literal, Optional, TypedDict, TypeVar, Union
6
6
 
7
- import aiofiles
8
7
  import filetype
9
- from chainlit.client.base import ElementDict, ElementDisplay, ElementSize, ElementType
10
- from chainlit.client.cloud import ChainlitCloudClient
11
8
  from chainlit.context import context
12
- from chainlit.data import chainlit_client
9
+ from chainlit.data import get_data_layer
13
10
  from chainlit.logger import logger
14
11
  from chainlit.telemetry import trace_event
12
+ from chainlit.types import FileDict
15
13
  from pydantic.dataclasses import Field, dataclass
16
14
  from syncer import asyncio
17
15
 
@@ -21,16 +19,38 @@ mime_types = {
21
19
  "plotly": "application/json",
22
20
  }
23
21
 
22
+ ElementType = Literal[
23
+ "image", "avatar", "text", "pdf", "tasklist", "audio", "video", "file", "plotly"
24
+ ]
25
+ ElementDisplay = Literal["inline", "side", "page"]
26
+ ElementSize = Literal["small", "medium", "large"]
27
+
28
+
29
+ class ElementDict(TypedDict):
30
+ id: str
31
+ threadId: Optional[str]
32
+ type: ElementType
33
+ chainlitKey: Optional[str]
34
+ url: Optional[str]
35
+ objectKey: Optional[str]
36
+ name: str
37
+ display: ElementDisplay
38
+ size: Optional[ElementSize]
39
+ language: Optional[str]
40
+ forId: Optional[str]
41
+ mime: Optional[str]
42
+
24
43
 
25
44
  @dataclass
26
45
  class Element:
27
46
  # The type of the element. This will be used to determine how to display the element in the UI.
28
47
  type: ClassVar[ElementType]
29
-
48
+ # Name of the element, this will be used to reference the element in the UI.
49
+ name: str
30
50
  # The ID of the element. This is set automatically when the element is sent to the UI.
31
51
  id: str = Field(default_factory=lambda: str(uuid.uuid4()))
32
- # Name of the element, this will be used to reference the element in the UI.
33
- name: Optional[str] = None
52
+ # The key of the element hosted on Chainlit.
53
+ chainlit_key: Optional[str] = None
34
54
  # The URL of the element if already hosted somehwere else.
35
55
  url: Optional[str] = None
36
56
  # The S3 object key.
@@ -44,7 +64,7 @@ class Element:
44
64
  # Controls element size
45
65
  size: Optional[ElementSize] = None
46
66
  # The ID of the message this element is associated with.
47
- for_ids: List[str] = Field(default_factory=list)
67
+ for_id: Optional[str] = None
48
68
  # The language, if relevant
49
69
  language: Optional[str] = None
50
70
  # Mime type, infered based on content if not provided
@@ -53,6 +73,8 @@ class Element:
53
73
  def __post_init__(self) -> None:
54
74
  trace_event(f"init {self.__class__.__name__}")
55
75
  self.persisted = False
76
+ self.updatable = False
77
+ self.thread_id = context.session.thread_id
56
78
 
57
79
  if not self.url and not self.path and not self.content:
58
80
  raise ValueError("Must provide url, path or content to instantiate element")
@@ -61,136 +83,92 @@ class Element:
61
83
  _dict = ElementDict(
62
84
  {
63
85
  "id": self.id,
86
+ "threadId": self.thread_id,
64
87
  "type": self.type,
65
- "url": self.url or "",
66
- "name": self.name or "",
88
+ "url": self.url,
89
+ "chainlitKey": self.chainlit_key,
90
+ "name": self.name,
67
91
  "display": self.display,
68
92
  "objectKey": getattr(self, "object_key", None),
69
93
  "size": getattr(self, "size", None),
70
94
  "language": getattr(self, "language", None),
71
- "forIds": getattr(self, "for_ids", None),
95
+ "forId": getattr(self, "for_id", None),
72
96
  "mime": getattr(self, "mime", None),
73
- "conversationId": None,
74
97
  }
75
98
  )
76
99
  return _dict
77
100
 
78
101
  @classmethod
79
- def from_dict(self, _dict: Dict):
80
- if "image" in _dict.get("mime", ""):
102
+ def from_dict(self, _dict: FileDict):
103
+ type = _dict.get("type", "")
104
+ if "image" in type and "svg" not in type:
81
105
  return Image(
82
106
  id=_dict.get("id", str(uuid.uuid4())),
83
- content=_dict.get("content"),
84
- name=_dict.get("name"),
85
- url=_dict.get("url"),
86
- display=_dict.get("display", "inline"),
87
- mime=_dict.get("mime"),
107
+ name=_dict.get("name", ""),
108
+ path=str(_dict.get("path")),
109
+ chainlit_key=_dict.get("id"),
110
+ display="inline",
111
+ mime=type,
88
112
  )
89
113
  else:
90
114
  return File(
91
115
  id=_dict.get("id", str(uuid.uuid4())),
92
- content=_dict.get("content"),
93
- name=_dict.get("name"),
94
- url=_dict.get("url"),
95
- language=_dict.get("language"),
96
- display=_dict.get("display", "inline"),
97
- size=_dict.get("size"),
98
- mime=_dict.get("mime"),
116
+ name=_dict.get("name", ""),
117
+ path=str(_dict.get("path")),
118
+ chainlit_key=_dict.get("id"),
119
+ display="inline",
120
+ mime=type,
99
121
  )
100
122
 
101
- async def with_conversation_id(self):
102
- _dict = self.to_dict()
103
- _dict["conversationId"] = await context.session.get_conversation_id()
104
- return _dict
105
-
106
- async def preprocess_content(self):
107
- pass
108
-
109
- async def load(self):
110
- if self.path:
111
- async with aiofiles.open(self.path, "rb") as f:
112
- self.content = await f.read()
113
- else:
114
- raise ValueError("Must provide path or content to load element")
115
-
116
- async def persist(self, client: ChainlitCloudClient) -> Optional[ElementDict]:
117
- if not self.url and self.content and not self.persisted:
118
- conversation_id = await context.session.get_conversation_id()
119
- upload_res = await client.upload_element(
123
+ async def _create(self) -> bool:
124
+ if (self.persisted or self.url) and not self.updatable:
125
+ return True
126
+ if data_layer := get_data_layer():
127
+ try:
128
+ asyncio.create_task(data_layer.create_element(self))
129
+ except Exception as e:
130
+ logger.error(f"Failed to create element: {str(e)}")
131
+ if not self.chainlit_key or self.updatable:
132
+ file_dict = await context.session.persist_file(
133
+ name=self.name,
134
+ path=self.path,
120
135
  content=self.content,
121
136
  mime=self.mime or "",
122
- conversation_id=conversation_id,
123
137
  )
124
- self.url = upload_res["url"]
125
- self.object_key = upload_res["object_key"]
126
- element_dict = await self.with_conversation_id()
138
+ self.chainlit_key = file_dict["id"]
127
139
 
128
- asyncio.create_task(self._persist(element_dict))
140
+ self.persisted = True
129
141
 
130
- return element_dict
131
-
132
- async def _persist(self, element: ElementDict):
133
- if not chainlit_client:
134
- return
135
-
136
- try:
137
- if self.persisted:
138
- await chainlit_client.update_element(element)
139
- else:
140
- await chainlit_client.create_element(element)
141
- self.persisted = True
142
- except Exception as e:
143
- logger.error(f"Failed to persist element: {str(e)}")
144
-
145
- async def before_emit(self, element: Dict) -> Dict:
146
- return element
142
+ return True
147
143
 
148
144
  async def remove(self):
149
145
  trace_event(f"remove {self.__class__.__name__}")
146
+ data_layer = get_data_layer()
147
+ if data_layer and self.persisted:
148
+ await data_layer.delete_element(self.id)
150
149
  await context.emitter.emit("remove_element", {"id": self.id})
151
150
 
152
- async def send(self, for_id: Optional[str] = None):
153
- if not self.content and not self.url and self.path:
154
- await self.load()
151
+ async def send(self, for_id: str):
152
+ if self.persisted and not self.updatable:
153
+ return
155
154
 
156
- await self.preprocess_content()
155
+ self.for_id = for_id
157
156
 
158
157
  if not self.mime:
159
158
  # Only guess the mime type when the content is binary
160
159
  self.mime = (
161
160
  mime_types[self.type]
162
161
  if self.type in mime_types
163
- else filetype.guess_mime(self.content)
162
+ else filetype.guess_mime(self.path or self.content)
164
163
  )
165
164
 
166
- if for_id and for_id not in self.for_ids:
167
- self.for_ids.append(for_id)
165
+ await self._create()
168
166
 
169
- # We have a client, persist the element
170
- if chainlit_client:
171
- element_dict = await self.persist(chainlit_client)
172
- if element_dict:
173
- self.id = element_dict["id"]
167
+ if not self.url and not self.chainlit_key:
168
+ raise ValueError("Must provide url or chainlit key to send element")
174
169
 
175
- elif not self.url and not self.content:
176
- raise ValueError("Must provide url or content to send element")
177
-
178
- emit_dict = cast(Dict, self.to_dict())
179
-
180
- # Adding this out of to_dict since the dict will be persisted in the DB
181
- emit_dict["content"] = self.content
182
-
183
- # Element was already sent
184
- if len(self.for_ids) > 1:
185
- trace_event(f"update {self.__class__.__name__}")
186
- await context.emitter.emit(
187
- "update_element",
188
- {"id": self.id, "forIds": self.for_ids},
189
- )
190
- else:
191
- trace_event(f"send {self.__class__.__name__}")
192
- emit_dict = await self.before_emit(emit_dict)
193
- await context.emitter.emit("element", emit_dict)
170
+ trace_event(f"send {self.__class__.__name__}")
171
+ await context.emitter.emit("element", self.to_dict())
194
172
 
195
173
 
196
174
  ElementBased = TypeVar("ElementBased", bound=Element)
@@ -208,23 +186,7 @@ class Avatar(Element):
208
186
  type: ClassVar[ElementType] = "avatar"
209
187
 
210
188
  async def send(self):
211
- element = None
212
-
213
- if not self.content and not self.url and self.path:
214
- await self.load()
215
-
216
- if not self.url and not self.content:
217
- raise ValueError("Must provide url or content to send element")
218
-
219
- element = self.to_dict()
220
-
221
- # Adding this out of to_dict since the dict will be persisted in the DB
222
- element["content"] = self.content
223
-
224
- if element:
225
- trace_event(f"send {self.__class__.__name__}")
226
- element = await self.before_emit(element)
227
- await context.emitter.emit("element", element)
189
+ await super().send(for_id="")
228
190
 
229
191
 
230
192
  @dataclass
@@ -232,15 +194,8 @@ class Text(Element):
232
194
  """Useful to send a text (not a message) to the UI."""
233
195
 
234
196
  type: ClassVar[ElementType] = "text"
235
-
236
- content: bytes = b""
237
197
  language: Optional[str] = None
238
198
 
239
- async def before_emit(self, text_element):
240
- if "content" in text_element and isinstance(text_element["content"], bytes):
241
- text_element["content"] = text_element["content"].decode("utf-8")
242
- return text_element
243
-
244
199
 
245
200
  @dataclass
246
201
  class Pdf(Element):
@@ -312,12 +267,20 @@ class TaskList(Element):
312
267
  name: str = "tasklist"
313
268
  content: str = "dummy content to pass validation"
314
269
 
270
+ def __post_init__(self) -> None:
271
+ super().__post_init__()
272
+ self.updatable = True
273
+
315
274
  async def add_task(self, task: Task):
316
275
  self.tasks.append(task)
317
276
 
318
277
  async def update(self):
319
278
  await self.send()
320
279
 
280
+ async def send(self):
281
+ await self.preprocess_content()
282
+ await super().send(for_id="")
283
+
321
284
  async def preprocess_content(self):
322
285
  # serialize enum
323
286
  tasks = [
chainlit/emitter.py CHANGED
@@ -1,12 +1,22 @@
1
1
  import asyncio
2
2
  import uuid
3
- from typing import Any, Dict, Optional
3
+ from datetime import datetime
4
+ from typing import Any, Dict, List, Optional, Union, cast
4
5
 
5
- from chainlit.client.base import ConversationDict, MessageDict
6
- from chainlit.element import Element
6
+ from chainlit.data import get_data_layer
7
+ from chainlit.element import Element, File
7
8
  from chainlit.message import Message
8
9
  from chainlit.session import BaseSession, WebsocketSession
9
- from chainlit.types import AskSpec, UIMessagePayload
10
+ from chainlit.step import StepDict
11
+ from chainlit.types import (
12
+ AskActionResponse,
13
+ AskSpec,
14
+ FileDict,
15
+ FileReference,
16
+ ThreadDict,
17
+ UIMessagePayload,
18
+ )
19
+ from chainlit.user import PersistedUser
10
20
  from socketio.exceptions import TimeoutError
11
21
 
12
22
 
@@ -30,19 +40,19 @@ class BaseChainlitEmitter:
30
40
  """Stub method to get the 'ask_user' property from the session."""
31
41
  pass
32
42
 
33
- async def resume_conversation(self, conv_dict: ConversationDict):
34
- """Stub method to resume a conversation."""
43
+ async def resume_thread(self, thread_dict: ThreadDict):
44
+ """Stub method to resume a thread."""
35
45
  pass
36
46
 
37
- async def send_message(self, msg_dict: dict):
47
+ async def send_step(self, step_dict: StepDict):
38
48
  """Stub method to send a message to the UI."""
39
49
  pass
40
50
 
41
- async def update_message(self, msg_dict: dict):
51
+ async def update_step(self, step_dict: StepDict):
42
52
  """Stub method to update a message in the UI."""
43
53
  pass
44
54
 
45
- async def delete_message(self, msg_dict: dict):
55
+ async def delete_step(self, step_dict: StepDict):
46
56
  """Stub method to delete a message in the UI."""
47
57
  pass
48
58
 
@@ -54,15 +64,17 @@ class BaseChainlitEmitter:
54
64
  """Stub method to clear the prompt from the UI."""
55
65
  pass
56
66
 
57
- async def init_conversation(self, msg_dict: MessageDict):
58
- """Signal the UI that a new conversation (with a user message) exists"""
67
+ async def init_thread(self, step_dict: StepDict):
68
+ """Signal the UI that a new thread (with a user message) exists"""
59
69
  pass
60
70
 
61
71
  async def process_user_message(self, payload: UIMessagePayload) -> Message:
62
72
  """Stub method to process user message."""
63
73
  return Message(content="")
64
74
 
65
- async def send_ask_user(self, msg_dict: dict, spec, raise_on_timeout=False):
75
+ async def send_ask_user(
76
+ self, step_dict: StepDict, spec: AskSpec, raise_on_timeout=False
77
+ ) -> Optional[Union["StepDict", "AskActionResponse", List["FileDict"]]]:
66
78
  """Stub method to send a prompt to the UI and wait for a response."""
67
79
  pass
68
80
 
@@ -78,7 +90,7 @@ class BaseChainlitEmitter:
78
90
  """Stub method to send a task end signal to the UI."""
79
91
  pass
80
92
 
81
- async def stream_start(self, msg_dict: dict):
93
+ async def stream_start(self, step_dict: StepDict):
82
94
  """Stub method to send a stream start signal to the UI."""
83
95
  pass
84
96
 
@@ -90,6 +102,12 @@ class BaseChainlitEmitter:
90
102
  """Stub method to set chat settings."""
91
103
  pass
92
104
 
105
+ async def send_action_response(
106
+ self, id: str, status: bool, response: Optional[str] = None
107
+ ):
108
+ """Send an action response to the UI."""
109
+ pass
110
+
93
111
 
94
112
  class ChainlitEmitter(BaseChainlitEmitter):
95
113
  """
@@ -123,23 +141,21 @@ class ChainlitEmitter(BaseChainlitEmitter):
123
141
  """Get the 'ask_user' property from the session."""
124
142
  return self._get_session_property("ask_user")
125
143
 
126
- def resume_conversation(self, conv_dict: ConversationDict):
127
- """Send a conversation to the UI to resume it"""
128
- return self.emit("resume_conversation", conv_dict)
144
+ def resume_thread(self, thread_dict: ThreadDict):
145
+ """Send a thread to the UI to resume it"""
146
+ return self.emit("resume_thread", thread_dict)
129
147
 
130
- def send_message(self, msg_dict: Dict):
148
+ def send_step(self, step_dict: StepDict):
131
149
  """Send a message to the UI."""
132
- return self.emit("new_message", msg_dict)
150
+ return self.emit("new_message", step_dict)
133
151
 
134
- def update_message(self, msg_dict: Dict):
152
+ def update_step(self, step_dict: StepDict):
135
153
  """Update a message in the UI."""
154
+ return self.emit("update_message", step_dict)
136
155
 
137
- return self.emit("update_message", msg_dict)
138
-
139
- def delete_message(self, msg_dict):
156
+ def delete_step(self, step_dict: StepDict):
140
157
  """Delete a message in the UI."""
141
-
142
- return self.emit("delete_message", msg_dict)
158
+ return self.emit("delete_message", step_dict)
143
159
 
144
160
  def send_ask_timeout(self):
145
161
  """Send a prompt timeout message to the UI."""
@@ -151,22 +167,44 @@ class ChainlitEmitter(BaseChainlitEmitter):
151
167
 
152
168
  return self.emit("clear_ask", {})
153
169
 
154
- def init_conversation(self, message: MessageDict):
155
- """Signal the UI that a new conversation (with a user message) exists"""
170
+ async def init_thread(self, step: StepDict):
171
+ """Signal the UI that a new thread (with a user message) exists"""
172
+ if data_layer := get_data_layer():
173
+ if isinstance(self.session.user, PersistedUser):
174
+ user_id = self.session.user.id
175
+ else:
176
+ user_id = None
177
+ await data_layer.update_thread(
178
+ thread_id=self.session.thread_id,
179
+ user_id=user_id,
180
+ metadata={"name": step["output"]},
181
+ )
182
+ await self.session.flush_method_queue()
156
183
 
157
- return self.emit("init_conversation", message)
184
+ await self.emit("init_thread", step)
158
185
 
159
186
  async def process_user_message(self, payload: UIMessagePayload):
160
- message_dict = payload["message"]
161
- files = payload["files"]
162
- # Temporary UUID generated by the frontend should use v4
163
- assert uuid.UUID(message_dict["id"]).version == 4
187
+ step_dict = payload["message"]
188
+ file_refs = payload["fileReferences"]
189
+ # UUID generated by the frontend should use v4
190
+ assert uuid.UUID(step_dict["id"]).version == 4
164
191
 
165
- message = Message.from_dict(message_dict)
192
+ message = Message.from_dict(step_dict)
193
+ # Overwrite the created_at timestamp with the current time
194
+ message.created_at = datetime.utcnow().isoformat()
166
195
 
167
196
  asyncio.create_task(message._create())
168
197
 
169
- if files:
198
+ if not self.session.has_user_message:
199
+ self.session.has_user_message = True
200
+ asyncio.create_task(self.init_thread(message.to_dict()))
201
+
202
+ if file_refs:
203
+ files = [
204
+ self.session.files[file["id"]]
205
+ for file in file_refs
206
+ if file["id"] in self.session.files
207
+ ]
170
208
  file_elements = [Element.from_dict(file) for file in files]
171
209
  message.elements = file_elements
172
210
 
@@ -176,38 +214,60 @@ class ChainlitEmitter(BaseChainlitEmitter):
176
214
 
177
215
  asyncio.create_task(send_elements())
178
216
 
179
- if not self.session.has_user_message:
180
- self.session.has_user_message = True
181
- await self.init_conversation(await message.with_conversation_id())
182
-
183
217
  self.session.root_message = message
184
218
 
185
219
  return message
186
220
 
187
221
  async def send_ask_user(
188
- self, msg_dict: Dict, spec: AskSpec, raise_on_timeout=False
222
+ self, step_dict: StepDict, spec: AskSpec, raise_on_timeout=False
189
223
  ):
190
224
  """Send a prompt to the UI and wait for a response."""
191
225
 
192
226
  try:
193
227
  # Send the prompt to the UI
194
- res = await self.ask_user(
195
- {"msg": msg_dict, "spec": spec.to_dict()}, spec.timeout
196
- ) # type: Optional["MessageDict"]
228
+ user_res = await self.ask_user(
229
+ {"msg": step_dict, "spec": spec.to_dict()}, spec.timeout
230
+ ) # type: Optional[Union["StepDict", "AskActionResponse", List["FileReference"]]]
197
231
 
198
232
  # End the task temporarily so that the User can answer the prompt
199
233
  await self.task_end()
200
234
 
201
- if res:
202
- # If cloud is enabled, store the response in the database/S3
235
+ final_res: Optional[
236
+ Union["StepDict", "AskActionResponse", List["FileDict"]]
237
+ ] = None
238
+
239
+ if user_res:
203
240
  if spec.type == "text":
204
- await self.process_user_message({"message": res, "files": None})
241
+ message_dict_res = cast(StepDict, user_res)
242
+ await self.process_user_message(
243
+ {"message": message_dict_res, "fileReferences": None}
244
+ )
245
+ final_res = message_dict_res
205
246
  elif spec.type == "file":
206
- # TODO: upload file to S3
207
- pass
208
-
247
+ file_refs = cast(List[FileReference], user_res)
248
+ files = [
249
+ self.session.files[file["id"]]
250
+ for file in file_refs
251
+ if file["id"] in self.session.files
252
+ ]
253
+ final_res = files
254
+ if get_data_layer():
255
+ coros = [
256
+ File(
257
+ name=file["name"],
258
+ path=str(file["path"]),
259
+ mime=file["type"],
260
+ chainlit_key=file["id"],
261
+ for_id=step_dict["id"],
262
+ )._create()
263
+ for file in files
264
+ ]
265
+ await asyncio.gather(*coros)
266
+ elif spec.type == "action":
267
+ action_res = cast(AskActionResponse, user_res)
268
+ final_res = action_res
209
269
  await self.clear_ask()
210
- return res
270
+ return final_res
211
271
  except TimeoutError as e:
212
272
  await self.send_ask_timeout()
213
273
 
@@ -231,11 +291,11 @@ class ChainlitEmitter(BaseChainlitEmitter):
231
291
  """Send a task end signal to the UI."""
232
292
  return self.emit("task_end", {})
233
293
 
234
- def stream_start(self, msg_dict: Dict):
294
+ def stream_start(self, step_dict: StepDict):
235
295
  """Send a stream start signal to the UI."""
236
296
  return self.emit(
237
297
  "stream_start",
238
- msg_dict,
298
+ step_dict,
239
299
  )
240
300
 
241
301
  def send_token(self, id: str, token: str, is_sequence=False):
@@ -246,3 +306,10 @@ class ChainlitEmitter(BaseChainlitEmitter):
246
306
 
247
307
  def set_chat_settings(self, settings: Dict[str, Any]):
248
308
  self.session.chat_settings = settings
309
+
310
+ def send_action_response(
311
+ self, id: str, status: bool, response: Optional[str] = None
312
+ ):
313
+ return self.emit(
314
+ "action_response", {"id": id, "status": status, "response": response}
315
+ )