chainlit 1.0.0rc2__py3-none-any.whl → 1.0.100__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of chainlit might be problematic. Click here for more details.

chainlit/__init__.py CHANGED
@@ -7,6 +7,7 @@ env_found = load_dotenv(dotenv_path=os.path.join(os.getcwd(), ".env"))
7
7
  import asyncio
8
8
  from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional
9
9
 
10
+ from fastapi import Request, Response
10
11
  from starlette.datastructures import Headers
11
12
 
12
13
  if TYPE_CHECKING:
@@ -54,7 +55,7 @@ from chainlit.user import PersistedUser, User
54
55
  from chainlit.user_session import user_session
55
56
  from chainlit.utils import make_module_getattr, wrap_user_function
56
57
  from chainlit.version import __version__
57
- from chainlit_client import ChatGeneration, CompletionGeneration, GenerationMessage
58
+ from literalai import ChatGeneration, CompletionGeneration, GenerationMessage
58
59
 
59
60
  if env_found:
60
61
  logger.info("Loaded .env file")
@@ -127,6 +128,17 @@ def oauth_callback(
127
128
  return func
128
129
 
129
130
 
131
+ @trace
132
+ def on_logout(func: Callable[[Request, Response], Any]) -> Callable:
133
+ """
134
+ Function called when the user logs out.
135
+ Takes the FastAPI request and response as parameters.
136
+ """
137
+
138
+ config.code.on_logout = wrap_user_function(func)
139
+ return func
140
+
141
+
130
142
  @trace
131
143
  def on_message(func: Callable) -> Callable:
132
144
  """
@@ -320,6 +332,7 @@ __all__ = [
320
332
  "ChatGeneration",
321
333
  "CompletionGeneration",
322
334
  "GenerationMessage",
335
+ "on_logout",
323
336
  "on_chat_start",
324
337
  "on_chat_end",
325
338
  "on_chat_resume",
chainlit/auth.py CHANGED
@@ -20,7 +20,7 @@ def get_jwt_secret():
20
20
  def ensure_jwt_secret():
21
21
  if require_login() and get_jwt_secret() is None:
22
22
  raise ValueError(
23
- "You must provide a JWT secret in the environment to use password authentication. Run `chainlit create-secret` to generate one."
23
+ "You must provide a JWT secret in the environment to use authentication. Run `chainlit create-secret` to generate one."
24
24
  )
25
25
 
26
26
 
@@ -30,7 +30,8 @@ def is_oauth_enabled():
30
30
 
31
31
  def require_login():
32
32
  return (
33
- config.code.password_auth_callback is not None
33
+ bool(os.environ.get("CHAINLIT_CUSTOM_AUTH"))
34
+ or config.code.password_auth_callback is not None
34
35
  or config.code.header_auth_callback is not None
35
36
  or is_oauth_enabled()
36
37
  )
@@ -74,7 +75,7 @@ async def authenticate_user(token: str = Depends(reuseable_oauth)):
74
75
  try:
75
76
  persisted_user = await data_layer.get_user(user.identifier)
76
77
  except Exception as e:
77
- raise HTTPException(status_code=500, detail=str(e))
78
+ return user
78
79
  if persisted_user == None:
79
80
  raise HTTPException(status_code=401, detail="User does not exist")
80
81
 
chainlit/config.py CHANGED
@@ -15,6 +15,7 @@ if TYPE_CHECKING:
15
15
  from chainlit.action import Action
16
16
  from chainlit.types import ChatProfile, ThreadDict
17
17
  from chainlit.user import User
18
+ from fastapi import Request, Response
18
19
 
19
20
 
20
21
  BACKEND_ROOT = os.path.dirname(__file__)
@@ -118,8 +119,6 @@ hide_cot = false
118
119
  generated_by = "{__version__}"
119
120
  """
120
121
 
121
- chainlit_prod_url = os.environ.get("CHAINLIT_PROD_URL")
122
-
123
122
 
124
123
  DEFAULT_HOST = "0.0.0.0"
125
124
  DEFAULT_PORT = 8000
@@ -200,6 +199,7 @@ class CodeSettings:
200
199
  oauth_callback: Optional[
201
200
  Callable[[str, str, Dict[str, str], "User"], Optional["User"]]
202
201
  ] = None
202
+ on_logout: Optional[Callable[["Request", "Response"], Any]] = None
203
203
  on_stop: Optional[Callable[[], Any]] = None
204
204
  on_chat_start: Optional[Callable[[], Any]] = None
205
205
  on_chat_end: Optional[Callable[[], Any]] = None
@@ -234,9 +234,6 @@ class ChainlitConfig:
234
234
  root = APP_ROOT
235
235
  # Chainlit server URL. Used only for cloud features
236
236
  chainlit_server: str
237
- # The url of the deployed app. Only set if the app is deployed.
238
- chainlit_prod_url = chainlit_prod_url
239
-
240
237
  run: RunSettings
241
238
  features: FeaturesSettings
242
239
  ui: UISettings
@@ -347,7 +344,6 @@ def load_config():
347
344
 
348
345
  config = ChainlitConfig(
349
346
  chainlit_server=chainlit_server,
350
- chainlit_prod_url=chainlit_prod_url,
351
347
  run=RunSettings(),
352
348
  **settings,
353
349
  )
chainlit/data/__init__.py CHANGED
@@ -1,20 +1,21 @@
1
1
  import functools
2
2
  import os
3
3
  from collections import deque
4
- from typing import TYPE_CHECKING, Dict, List, Optional
4
+ from typing import TYPE_CHECKING, Dict, List, Optional, Union
5
5
 
6
+ import aiofiles
6
7
  from chainlit.config import config
7
8
  from chainlit.context import context
8
9
  from chainlit.logger import logger
9
10
  from chainlit.session import WebsocketSession
10
11
  from chainlit.types import Feedback, Pagination, ThreadDict, ThreadFilter
11
12
  from chainlit.user import PersistedUser, User, UserDict
12
- from chainlit_client import Attachment
13
- from chainlit_client import Feedback as ClientFeedback
14
- from chainlit_client import PageInfo, PaginatedResponse
15
- from chainlit_client import Step as ClientStep
16
- from chainlit_client.thread import NumberListFilter, StringFilter, StringListFilter
17
- from chainlit_client.thread import ThreadFilter as ClientThreadFilter
13
+ from literalai import Attachment
14
+ from literalai import Feedback as ClientFeedback
15
+ from literalai import PageInfo, PaginatedResponse
16
+ from literalai import Step as ClientStep
17
+ from literalai.thread import NumberListFilter, StringFilter, StringListFilter
18
+ from literalai.thread import ThreadFilter as ClientThreadFilter
18
19
 
19
20
  if TYPE_CHECKING:
20
21
  from chainlit.element import Element, ElementDict
@@ -29,7 +30,7 @@ def queue_until_user_message():
29
30
  async def wrapper(self, *args, **kwargs):
30
31
  if (
31
32
  isinstance(context.session, WebsocketSession)
32
- and not context.session.has_user_message
33
+ and not context.session.has_first_interaction
33
34
  ):
34
35
  # Queue the method invocation waiting for the first user message
35
36
  queues = context.session.thread_queues
@@ -112,14 +113,29 @@ class BaseDataLayer:
112
113
  ):
113
114
  pass
114
115
 
116
+ async def create_user_session(
117
+ self,
118
+ id: str,
119
+ started_at: str,
120
+ anon_user_id: str,
121
+ user_id: Optional[str],
122
+ ) -> Dict:
123
+ return {}
124
+
125
+ async def update_user_session(
126
+ self, id: str, is_interactive: bool, ended_at: Optional[str]
127
+ ) -> Dict:
128
+ return {}
129
+
130
+ async def delete_user_session(self, id: str) -> bool:
131
+ return True
132
+
115
133
 
116
134
  class ChainlitDataLayer:
117
- def __init__(
118
- self, api_key: str, chainlit_server: Optional[str] = "https://cloud.chainlit.io"
119
- ):
120
- from chainlit_client import ChainlitClient
135
+ def __init__(self, api_key: str, server: Optional[str]):
136
+ from literalai import LiteralClient
121
137
 
122
- self.client = ChainlitClient(api_key=api_key, url=chainlit_server)
138
+ self.client = LiteralClient(api_key=api_key, url=server)
123
139
  logger.info("Chainlit data layer initialized")
124
140
 
125
141
  def attachment_to_element_dict(self, attachment: Attachment) -> "ElementDict":
@@ -128,6 +144,7 @@ class ChainlitDataLayer:
128
144
  "chainlitKey": None,
129
145
  "display": metadata.get("display", "side"),
130
146
  "language": metadata.get("language"),
147
+ "page": metadata.get("page"),
131
148
  "size": metadata.get("size"),
132
149
  "type": metadata.get("type", "file"),
133
150
  "forId": attachment.step_id,
@@ -193,6 +210,8 @@ class ChainlitDataLayer:
193
210
  _user = await self.client.api.create_user(
194
211
  identifier=user.identifier, metadata=user.metadata
195
212
  )
213
+ elif _user.id:
214
+ await self.client.api.update_user(id=_user.id, metadata=user.metadata)
196
215
  return PersistedUser(
197
216
  id=_user.id or "",
198
217
  identifier=_user.identifier or "",
@@ -230,17 +249,44 @@ class ChainlitDataLayer:
230
249
  "language": element.language,
231
250
  "display": element.display,
232
251
  "type": element.type,
252
+ "page": getattr(element, "page", None),
233
253
  }
234
254
 
235
- await self.client.api.create_attachment(
236
- thread_id=element.thread_id,
237
- step_id=element.for_id or "",
238
- mime=element.mime,
239
- name=element.name,
240
- url=element.url,
241
- content=element.content,
242
- path=element.path,
243
- metadata=metadata,
255
+ if not element.for_id:
256
+ return
257
+
258
+ object_key = None
259
+
260
+ if not element.url:
261
+ if element.path:
262
+ async with aiofiles.open(element.path, "rb") as f:
263
+ content = await f.read() # type: Union[bytes, str]
264
+ elif element.content:
265
+ content = element.content
266
+ else:
267
+ raise ValueError("Either path or content must be provided")
268
+ uploaded = await self.client.api.upload_file(
269
+ content=content, mime=element.mime, thread_id=element.thread_id
270
+ )
271
+ object_key = uploaded["object_key"]
272
+
273
+ await self.client.api.send_steps(
274
+ [
275
+ {
276
+ "id": element.for_id,
277
+ "threadId": element.thread_id,
278
+ "attachments": [
279
+ {
280
+ "id": element.id,
281
+ "name": element.name,
282
+ "metadata": metadata,
283
+ "mime": element.mime,
284
+ "url": element.url,
285
+ "objectKey": object_key,
286
+ }
287
+ ],
288
+ }
289
+ ]
244
290
  )
245
291
 
246
292
  async def get_element(
@@ -337,6 +383,8 @@ class ChainlitDataLayer:
337
383
  continue
338
384
  for attachment in step.attachments:
339
385
  elements.append(self.attachment_to_element_dict(attachment))
386
+ if not config.features.prompt_playground and step.generation:
387
+ step.generation = None
340
388
  steps.append(self.step_to_step_dict(step))
341
389
 
342
390
  user = None # type: Optional["UserDict"]
@@ -372,10 +420,40 @@ class ChainlitDataLayer:
372
420
  tags=tags,
373
421
  )
374
422
 
423
+ async def create_user_session(
424
+ self,
425
+ id: str,
426
+ started_at: str,
427
+ anon_user_id: str,
428
+ user_id: Optional[str],
429
+ ) -> Dict:
430
+ existing_session = await self.client.api.get_user_session(id=id)
431
+ if existing_session:
432
+ return existing_session
433
+ session = await self.client.api.create_user_session(
434
+ id=id,
435
+ started_at=started_at,
436
+ participant_identifier=user_id,
437
+ anon_participant_identifier=anon_user_id,
438
+ )
439
+ return session
440
+
441
+ async def update_user_session(
442
+ self, id: str, is_interactive: bool, ended_at: Optional[str]
443
+ ) -> Dict:
444
+ session = await self.client.api.update_user_session(
445
+ id=id, is_interactive=is_interactive, ended_at=ended_at
446
+ )
447
+ return session
448
+
449
+ async def delete_user_session(self, id: str) -> bool:
450
+ await self.client.api.delete_user_session(id=id)
451
+ return True
452
+
375
453
 
376
- if api_key := os.environ.get("CHAINLIT_API_KEY"):
377
- chainlit_server = os.environ.get("CHAINLIT_SERVER")
378
- _data_layer = ChainlitDataLayer(api_key=api_key, chainlit_server=chainlit_server)
454
+ if api_key := os.environ.get("LITERAL_API_KEY"):
455
+ server = os.environ.get("LITERAL_SERVER")
456
+ _data_layer = ChainlitDataLayer(api_key=api_key, server=server)
379
457
 
380
458
 
381
459
  def get_data_layer():
chainlit/element.py CHANGED
@@ -37,6 +37,7 @@ class ElementDict(TypedDict):
37
37
  display: ElementDisplay
38
38
  size: Optional[ElementSize]
39
39
  language: Optional[str]
40
+ page: Optional[int]
40
41
  forId: Optional[str]
41
42
  mime: Optional[str]
42
43
 
@@ -46,7 +47,7 @@ class Element:
46
47
  # The type of the element. This will be used to determine how to display the element in the UI.
47
48
  type: ClassVar[ElementType]
48
49
  # Name of the element, this will be used to reference the element in the UI.
49
- name: str
50
+ name: str = ""
50
51
  # The ID of the element. This is set automatically when the element is sent to the UI.
51
52
  id: str = Field(default_factory=lambda: str(uuid.uuid4()))
52
53
  # The key of the element hosted on Chainlit.
@@ -91,6 +92,7 @@ class Element:
91
92
  "display": self.display,
92
93
  "objectKey": getattr(self, "object_key", None),
93
94
  "size": getattr(self, "size", None),
95
+ "page": getattr(self, "page", None),
94
96
  "language": getattr(self, "language", None),
95
97
  "forId": getattr(self, "for_id", None),
96
98
  "mime": getattr(self, "mime", None),
@@ -201,6 +203,7 @@ class Text(Element):
201
203
  class Pdf(Element):
202
204
  """Useful to send a pdf to the UI."""
203
205
 
206
+ page: Optional[int] = None
204
207
  type: ClassVar[ElementType] = "pdf"
205
208
 
206
209
 
chainlit/emitter.py CHANGED
@@ -5,6 +5,7 @@ from typing import Any, Dict, List, Optional, Union, cast
5
5
 
6
6
  from chainlit.data import get_data_layer
7
7
  from chainlit.element import Element, File
8
+ from chainlit.logger import logger
8
9
  from chainlit.message import Message
9
10
  from chainlit.session import BaseSession, WebsocketSession
10
11
  from chainlit.step import StepDict
@@ -64,8 +65,7 @@ class BaseChainlitEmitter:
64
65
  """Stub method to clear the prompt from the UI."""
65
66
  pass
66
67
 
67
- async def init_thread(self, step_dict: StepDict):
68
- """Signal the UI that a new thread (with a user message) exists"""
68
+ async def init_thread(self, interaction: str):
69
69
  pass
70
70
 
71
71
  async def process_user_message(self, payload: UIMessagePayload) -> Message:
@@ -167,23 +167,25 @@ class ChainlitEmitter(BaseChainlitEmitter):
167
167
 
168
168
  return self.emit("clear_ask", {})
169
169
 
170
- async def flush_thread_queues(self, name: str):
170
+ async def flush_thread_queues(self, interaction: str):
171
171
  if data_layer := get_data_layer():
172
172
  if isinstance(self.session.user, PersistedUser):
173
173
  user_id = self.session.user.id
174
174
  else:
175
175
  user_id = None
176
- await data_layer.update_thread(
177
- thread_id=self.session.thread_id,
178
- user_id=user_id,
179
- metadata={"name": name},
180
- )
176
+ try:
177
+ await data_layer.update_thread(
178
+ thread_id=self.session.thread_id,
179
+ user_id=user_id,
180
+ metadata={"name": interaction},
181
+ )
182
+ except Exception as e:
183
+ logger.error(f"Error updating thread: {e}")
181
184
  await self.session.flush_method_queue()
182
185
 
183
- async def init_thread(self, step: StepDict):
184
- """Signal the UI that a new thread (with a user message) exists"""
185
- await self.flush_thread_queues(name=step["output"])
186
- await self.emit("init_thread", step)
186
+ async def init_thread(self, interaction: str):
187
+ await self.flush_thread_queues(interaction)
188
+ await self.emit("first_interaction", interaction)
187
189
 
188
190
  async def process_user_message(self, payload: UIMessagePayload):
189
191
  step_dict = payload["message"]
@@ -197,9 +199,9 @@ class ChainlitEmitter(BaseChainlitEmitter):
197
199
 
198
200
  asyncio.create_task(message._create())
199
201
 
200
- if not self.session.has_user_message:
201
- self.session.has_user_message = True
202
- asyncio.create_task(self.init_thread(message.to_dict()))
202
+ if not self.session.has_first_interaction:
203
+ self.session.has_first_interaction = True
204
+ asyncio.create_task(self.init_thread(message.content))
203
205
 
204
206
  if file_refs:
205
207
  files = [
@@ -239,11 +241,13 @@ class ChainlitEmitter(BaseChainlitEmitter):
239
241
  ] = None
240
242
 
241
243
  if user_res:
244
+ interaction = None
242
245
  if spec.type == "text":
243
246
  message_dict_res = cast(StepDict, user_res)
244
247
  await self.process_user_message(
245
248
  {"message": message_dict_res, "fileReferences": None}
246
249
  )
250
+ interaction = message_dict_res["output"]
247
251
  final_res = message_dict_res
248
252
  elif spec.type == "file":
249
253
  file_refs = cast(List[FileReference], user_res)
@@ -253,12 +257,7 @@ class ChainlitEmitter(BaseChainlitEmitter):
253
257
  if file["id"] in self.session.files
254
258
  ]
255
259
  final_res = files
256
- if not self.session.has_user_message:
257
- self.session.has_user_message = True
258
- await self.flush_thread_queues(
259
- name=",".join([file["name"] for file in files])
260
- )
261
-
260
+ interaction = ",".join([file["name"] for file in files])
262
261
  if get_data_layer():
263
262
  coros = [
264
263
  File(
@@ -274,6 +273,12 @@ class ChainlitEmitter(BaseChainlitEmitter):
274
273
  elif spec.type == "action":
275
274
  action_res = cast(AskActionResponse, user_res)
276
275
  final_res = action_res
276
+ interaction = action_res["value"]
277
+
278
+ if not self.session.has_first_interaction and interaction:
279
+ self.session.has_first_interaction = True
280
+ await self.init_thread(interaction=interaction)
281
+
277
282
  await self.clear_ask()
278
283
  return final_res
279
284
  except TimeoutError as e: