chainlit 2.0rc0__py3-none-any.whl → 2.0rc1__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 (66) hide show
  1. chainlit/__init__.py +5 -0
  2. chainlit/action.py +4 -2
  3. chainlit/{auth.py → auth/__init__.py} +20 -34
  4. chainlit/auth/cookie.py +124 -0
  5. chainlit/auth/jwt.py +37 -0
  6. chainlit/callbacks.py +28 -0
  7. chainlit/chat_context.py +2 -2
  8. chainlit/chat_settings.py +3 -1
  9. chainlit/cli/__init__.py +14 -1
  10. chainlit/config.py +18 -5
  11. chainlit/context.py +3 -2
  12. chainlit/copilot/dist/index.js +220 -220
  13. chainlit/data/__init__.py +29 -17
  14. chainlit/data/acl.py +3 -2
  15. chainlit/data/base.py +1 -1
  16. chainlit/data/dynamodb.py +5 -3
  17. chainlit/data/literalai.py +3 -5
  18. chainlit/data/sql_alchemy.py +6 -5
  19. chainlit/data/storage_clients/azure.py +1 -0
  20. chainlit/data/storage_clients/s3.py +1 -0
  21. chainlit/discord/app.py +2 -1
  22. chainlit/element.py +6 -5
  23. chainlit/emitter.py +19 -10
  24. chainlit/frontend/dist/assets/{DailyMotion-CleI-8Dh.js → DailyMotion-C-_sjrtO.js} +1 -1
  25. chainlit/frontend/dist/assets/{Facebook-C4PuTowX.js → Facebook-bB34P03l.js} +1 -1
  26. chainlit/frontend/dist/assets/{FilePlayer-D49YToZz.js → FilePlayer-BWgqGrXv.js} +1 -1
  27. chainlit/frontend/dist/assets/{Kaltura-BkZcQEIs.js → Kaltura-OY4P9Ofd.js} +1 -1
  28. chainlit/frontend/dist/assets/{Mixcloud-DzvBFYsm.js → Mixcloud-9CtT8w5Y.js} +1 -1
  29. chainlit/frontend/dist/assets/{Mux-UXPyWWYv.js → Mux-BH9A0qEi.js} +1 -1
  30. chainlit/frontend/dist/assets/{Preview-0YXzpiVm.js → Preview-Og00EJ05.js} +1 -1
  31. chainlit/frontend/dist/assets/{SoundCloud-CS54COex.js → SoundCloud-D7resGfn.js} +1 -1
  32. chainlit/frontend/dist/assets/{Streamable-DYYShO6Q.js → Streamable-6f_6bYz1.js} +1 -1
  33. chainlit/frontend/dist/assets/{Twitch-DG7403Hm.js → Twitch-BZJl3peM.js} +1 -1
  34. chainlit/frontend/dist/assets/{Vidyard-C5JbOHIQ.js → Vidyard-B7tv4b8_.js} +1 -1
  35. chainlit/frontend/dist/assets/{Vimeo-dFLZbhqH.js → Vimeo-F-eA4zQI.js} +1 -1
  36. chainlit/frontend/dist/assets/{Wistia-143Q9V9c.js → Wistia-Dhxhn3IB.js} +1 -1
  37. chainlit/frontend/dist/assets/{YouTube-Dct4gpfH.js → YouTube-aFdJGjI1.js} +1 -1
  38. chainlit/frontend/dist/assets/{index-2yAiK0R5.js → index-Ba33_hdJ.js} +122 -122
  39. chainlit/frontend/dist/assets/{react-plotly-CFHBSMgg.js → react-plotly-DoUJXMgz.js} +1 -1
  40. chainlit/frontend/dist/index.html +1 -1
  41. chainlit/haystack/callbacks.py +5 -4
  42. chainlit/input_widget.py +6 -4
  43. chainlit/langchain/callbacks.py +56 -47
  44. chainlit/langflow/__init__.py +1 -0
  45. chainlit/llama_index/callbacks.py +7 -7
  46. chainlit/message.py +6 -5
  47. chainlit/mistralai/__init__.py +3 -2
  48. chainlit/oauth_providers.py +70 -3
  49. chainlit/openai/__init__.py +3 -2
  50. chainlit/secret.py +1 -1
  51. chainlit/server.py +232 -156
  52. chainlit/session.py +7 -5
  53. chainlit/slack/app.py +3 -2
  54. chainlit/socket.py +88 -63
  55. chainlit/step.py +11 -10
  56. chainlit/sync.py +2 -1
  57. chainlit/teams/app.py +1 -0
  58. chainlit/translations/nl-NL.json +229 -0
  59. chainlit/types.py +3 -1
  60. chainlit/user.py +2 -1
  61. chainlit/utils.py +3 -2
  62. {chainlit-2.0rc0.dist-info → chainlit-2.0rc1.dist-info}/METADATA +1 -1
  63. chainlit-2.0rc1.dist-info/RECORD +102 -0
  64. chainlit-2.0rc0.dist-info/RECORD +0 -99
  65. {chainlit-2.0rc0.dist-info → chainlit-2.0rc1.dist-info}/WHEEL +0 -0
  66. {chainlit-2.0rc0.dist-info → chainlit-2.0rc1.dist-info}/entry_points.txt +0 -0
chainlit/__init__.py CHANGED
@@ -69,7 +69,9 @@ from .callbacks import (
69
69
  on_message,
70
70
  on_settings_update,
71
71
  on_stop,
72
+ on_window_message,
72
73
  password_auth_callback,
74
+ send_window_message,
73
75
  set_chat_profiles,
74
76
  set_starters,
75
77
  )
@@ -132,6 +134,7 @@ __all__ = [
132
134
  "Image",
133
135
  "Text",
134
136
  "Component",
137
+ "Dataframe",
135
138
  "Pyplot",
136
139
  "File",
137
140
  "Task",
@@ -151,6 +154,8 @@ __all__ = [
151
154
  "CompletionGeneration",
152
155
  "GenerationMessage",
153
156
  "on_logout",
157
+ "on_window_message",
158
+ "send_window_message",
154
159
  "on_chat_start",
155
160
  "on_chat_end",
156
161
  "on_chat_resume",
chainlit/action.py CHANGED
@@ -1,10 +1,12 @@
1
1
  import uuid
2
2
  from typing import Optional
3
3
 
4
+ from dataclasses_json import DataClassJsonMixin
5
+ from pydantic import Field
6
+ from pydantic.dataclasses import dataclass
7
+
4
8
  from chainlit.context import context
5
9
  from chainlit.telemetry import trace_event
6
- from dataclasses_json import DataClassJsonMixin
7
- from pydantic.dataclasses import Field, dataclass
8
10
 
9
11
 
10
12
  @dataclass
@@ -1,20 +1,16 @@
1
1
  import os
2
- from datetime import datetime, timedelta
3
- from typing import Any, Dict
4
2
 
5
- import jwt
3
+ from fastapi import Depends, HTTPException
4
+
6
5
  from chainlit.config import config
7
6
  from chainlit.data import get_data_layer
7
+ from chainlit.logger import logger
8
8
  from chainlit.oauth_providers import get_configured_oauth_providers
9
- from chainlit.user import User
10
- from fastapi import Depends, HTTPException
11
- from fastapi.security import OAuth2PasswordBearer
12
-
13
- reuseable_oauth = OAuth2PasswordBearer(tokenUrl="/login", auto_error=False)
14
9
 
10
+ from .cookie import OAuth2PasswordBearerWithCookie
11
+ from .jwt import create_jwt, decode_jwt, get_jwt_secret
15
12
 
16
- def get_jwt_secret():
17
- return os.environ.get("CHAINLIT_AUTH_SECRET")
13
+ reuseable_oauth = OAuth2PasswordBearerWithCookie(tokenUrl="/login", auto_error=False)
18
14
 
19
15
 
20
16
  def ensure_jwt_secret():
@@ -42,52 +38,39 @@ def get_configuration():
42
38
  "requireLogin": require_login(),
43
39
  "passwordAuth": config.code.password_auth_callback is not None,
44
40
  "headerAuth": config.code.header_auth_callback is not None,
41
+ "cookieAuth": config.project.cookie_auth,
45
42
  "oauthProviders": (
46
43
  get_configured_oauth_providers() if is_oauth_enabled() else []
47
44
  ),
48
45
  }
49
46
 
50
47
 
51
- def create_jwt(data: User) -> str:
52
- to_encode: Dict[str, Any] = data.to_dict()
53
- to_encode.update(
54
- {
55
- "exp": datetime.utcnow() + timedelta(
56
- seconds=config.project.user_session_timeout
57
- ),
58
- }
59
- )
60
- encoded_jwt = jwt.encode(to_encode, get_jwt_secret(), algorithm="HS256")
61
- return encoded_jwt
62
-
63
-
64
48
  async def authenticate_user(token: str = Depends(reuseable_oauth)):
65
49
  try:
66
- dict = jwt.decode(
67
- token,
68
- get_jwt_secret(),
69
- algorithms=["HS256"],
70
- options={"verify_signature": True},
71
- )
72
- del dict["exp"]
73
- user = User(**dict)
50
+ user = decode_jwt(token)
74
51
  except Exception as e:
75
52
  raise HTTPException(
76
53
  status_code=401, detail="Invalid authentication token"
77
54
  ) from e
55
+
78
56
  if data_layer := get_data_layer():
57
+ # Get or create persistent user if we've a data layer available.
79
58
  try:
80
59
  persisted_user = await data_layer.get_user(user.identifier)
81
60
  if persisted_user is None:
82
61
  persisted_user = await data_layer.create_user(user)
83
- except Exception:
62
+ assert persisted_user
63
+ except Exception as e:
64
+ logger.exception("Unable to get persisted_user from data layer: %s", e)
84
65
  return user
85
66
 
86
67
  if user and user.display_name:
68
+ # Copy ephemeral display_name from authenticated user to persistent user.
87
69
  persisted_user.display_name = user.display_name
70
+
88
71
  return persisted_user
89
- else:
90
- return user
72
+
73
+ return user
91
74
 
92
75
 
93
76
  async def get_current_user(token: str = Depends(reuseable_oauth)):
@@ -95,3 +78,6 @@ async def get_current_user(token: str = Depends(reuseable_oauth)):
95
78
  return None
96
79
 
97
80
  return await authenticate_user(token)
81
+
82
+
83
+ __all__ = ["create_jwt", "get_configuration", "get_current_user"]
@@ -0,0 +1,124 @@
1
+ import os
2
+ from typing import Literal, Optional, cast
3
+
4
+ from fastapi import Request, Response
5
+ from fastapi.exceptions import HTTPException
6
+ from fastapi.security.base import SecurityBase
7
+ from fastapi.security.utils import get_authorization_scheme_param
8
+ from starlette.status import HTTP_401_UNAUTHORIZED
9
+
10
+ """ Module level cookie settings. """
11
+ _cookie_samesite = cast(
12
+ Literal["lax", "strict", "none"],
13
+ os.environ.get("CHAINLIT_COOKIE_SAMESITE", "lax"),
14
+ )
15
+
16
+ assert (
17
+ _cookie_samesite
18
+ in [
19
+ "lax",
20
+ "strict",
21
+ "none",
22
+ ]
23
+ ), "Invalid value for CHAINLIT_COOKIE_SAMESITE. Must be one of 'lax', 'strict' or 'none'."
24
+ _cookie_secure = _cookie_samesite == "none"
25
+
26
+ _auth_cookie_lifetime = 60 * 60 # 1 hour
27
+ _state_cookie_lifetime = 3 * 60 # 3m
28
+ _auth_cookie_name = "access_token"
29
+ _state_cookie_name = "oauth_state"
30
+
31
+
32
+ class OAuth2PasswordBearerWithCookie(SecurityBase):
33
+ """
34
+ OAuth2 password flow with cookie support with fallback to bearer token.
35
+ """
36
+
37
+ def __init__(
38
+ self,
39
+ tokenUrl: str,
40
+ scheme_name: Optional[str] = None,
41
+ auto_error: bool = True,
42
+ ):
43
+ self.tokenUrl = tokenUrl
44
+ self.scheme_name = scheme_name or self.__class__.__name__
45
+ self.auto_error = auto_error
46
+
47
+ async def __call__(self, request: Request) -> Optional[str]:
48
+ # First try to get the token from the cookie
49
+ token = request.cookies.get(_auth_cookie_name)
50
+
51
+ # If no cookie, try the Authorization header as fallback
52
+ if not token:
53
+ # TODO: Only bother to check if cookie auth is explicitly disabled.
54
+ authorization = request.headers.get("Authorization")
55
+ if authorization:
56
+ scheme, token = get_authorization_scheme_param(authorization)
57
+ if scheme.lower() != "bearer":
58
+ if self.auto_error:
59
+ raise HTTPException(
60
+ status_code=HTTP_401_UNAUTHORIZED,
61
+ detail="Invalid authentication credentials",
62
+ headers={"WWW-Authenticate": "Bearer"},
63
+ )
64
+ else:
65
+ return None
66
+ else:
67
+ if self.auto_error:
68
+ raise HTTPException(
69
+ status_code=HTTP_401_UNAUTHORIZED,
70
+ detail="Not authenticated",
71
+ headers={"WWW-Authenticate": "Bearer"},
72
+ )
73
+ else:
74
+ return None
75
+
76
+ return token
77
+
78
+
79
+ def set_auth_cookie(response: Response, token: str):
80
+ """
81
+ Helper function to set the authentication cookie with secure parameters
82
+ """
83
+
84
+ response.set_cookie(
85
+ key=_auth_cookie_name,
86
+ value=token,
87
+ httponly=True,
88
+ secure=_cookie_secure,
89
+ samesite=_cookie_samesite,
90
+ max_age=_auth_cookie_lifetime,
91
+ path="/", # Why is path set here and not below?
92
+ )
93
+
94
+
95
+ def clear_auth_cookie(response: Response):
96
+ """
97
+ Helper function to clear the authentication cookie
98
+ """
99
+ response.delete_cookie(key=_auth_cookie_name, path="/")
100
+
101
+
102
+ def set_oauth_state_cookie(response: Response, token: str):
103
+ response.set_cookie(
104
+ _state_cookie_name,
105
+ token,
106
+ httponly=True,
107
+ samesite=_cookie_samesite,
108
+ secure=_cookie_secure,
109
+ max_age=_state_cookie_lifetime,
110
+ )
111
+
112
+
113
+ def validate_oauth_state_cookie(request: Request, state: str):
114
+ """Check the state from the oauth provider against the browser cookie."""
115
+
116
+ oauth_state = request.cookies.get(_state_cookie_name)
117
+
118
+ if oauth_state != state:
119
+ raise Exception("oauth state does not correspond")
120
+
121
+
122
+ def clear_oauth_state_cookie(response: Response):
123
+ """Oauth complete, delete state token."""
124
+ response.delete_cookie(_state_cookie_name) # Do we set path here?
chainlit/auth/jwt.py ADDED
@@ -0,0 +1,37 @@
1
+ import datetime
2
+ import os
3
+ from typing import Any, Dict, Optional
4
+
5
+ import jwt as pyjwt
6
+
7
+ from chainlit.config import config
8
+ from chainlit.user import User
9
+
10
+
11
+ def get_jwt_secret() -> Optional[str]:
12
+ return os.environ.get("CHAINLIT_AUTH_SECRET")
13
+
14
+
15
+ def create_jwt(data: User) -> str:
16
+ to_encode: Dict[str, Any] = data.to_dict()
17
+ to_encode.update(
18
+ {
19
+ "exp": datetime.datetime.utcnow()
20
+ + datetime.timedelta(seconds=config.project.user_session_timeout),
21
+ }
22
+ )
23
+ secret = get_jwt_secret()
24
+ assert secret
25
+ encoded_jwt = pyjwt.encode(to_encode, secret, algorithm="HS256")
26
+ return encoded_jwt
27
+
28
+
29
+ def decode_jwt(token: str) -> User:
30
+ dict = pyjwt.decode(
31
+ token,
32
+ get_jwt_secret(),
33
+ algorithms=["HS256"],
34
+ options={"verify_signature": True},
35
+ )
36
+ del dict["exp"]
37
+ return User(**dict)
chainlit/callbacks.py CHANGED
@@ -6,6 +6,7 @@ from starlette.datastructures import Headers
6
6
 
7
7
  from chainlit.action import Action
8
8
  from chainlit.config import config
9
+ from chainlit.context import context
9
10
  from chainlit.data.base import BaseDataLayer
10
11
  from chainlit.message import Message
11
12
  from chainlit.oauth_providers import get_configured_oauth_providers
@@ -125,6 +126,33 @@ def on_message(func: Callable) -> Callable:
125
126
  return func
126
127
 
127
128
 
129
+ @trace
130
+ async def send_window_message(data: Any):
131
+ """
132
+ Send custom data to the host window via a window.postMessage event.
133
+
134
+ Args:
135
+ data (Any): The data to send with the event.
136
+ """
137
+ await context.emitter.send_window_message(data)
138
+
139
+
140
+ @trace
141
+ def on_window_message(func: Callable[[str], Any]) -> Callable:
142
+ """
143
+ Hook to react to javascript postMessage events coming from the UI.
144
+
145
+ Args:
146
+ func (Callable[[str], Any]): The function to be called when a window message is received.
147
+ Takes the message content as a string parameter.
148
+
149
+ Returns:
150
+ Callable[[str], Any]: The decorated on_window_message function.
151
+ """
152
+ config.code.on_window_message = wrap_user_function(func)
153
+ return func
154
+
155
+
128
156
  @trace
129
157
  def on_chat_start(func: Callable) -> Callable:
130
158
  """
chainlit/chat_context.py CHANGED
@@ -25,10 +25,10 @@ class ChatContext:
25
25
 
26
26
  if context.session.id not in chat_contexts:
27
27
  chat_contexts[context.session.id] = []
28
-
28
+
29
29
  if message not in chat_contexts[context.session.id]:
30
30
  chat_contexts[context.session.id].append(message)
31
-
31
+
32
32
  return message
33
33
 
34
34
  def remove(self, message: "Message") -> bool:
chainlit/chat_settings.py CHANGED
@@ -1,8 +1,10 @@
1
1
  from typing import List
2
2
 
3
+ from pydantic import Field
4
+ from pydantic.dataclasses import dataclass
5
+
3
6
  from chainlit.context import context
4
7
  from chainlit.input_widget import InputWidget
5
- from pydantic.dataclasses import Field, dataclass
6
8
 
7
9
 
8
10
  @dataclass
chainlit/cli/__init__.py CHANGED
@@ -9,6 +9,7 @@ import uvicorn
9
9
  nest_asyncio.apply()
10
10
 
11
11
  # ruff: noqa: E402
12
+ from chainlit.auth import ensure_jwt_secret
12
13
  from chainlit.cache import init_lc_cache
13
14
  from chainlit.config import (
14
15
  BACKEND_ROOT,
@@ -24,7 +25,18 @@ from chainlit.logger import logger
24
25
  from chainlit.markdown import init_markdown
25
26
  from chainlit.secret import random_secret
26
27
  from chainlit.telemetry import trace_event
27
- from chainlit.utils import check_file, ensure_jwt_secret
28
+ from chainlit.utils import check_file
29
+
30
+
31
+ def assert_app():
32
+ if (
33
+ not config.code.on_chat_start
34
+ and not config.code.on_message
35
+ and not config.code.on_audio_chunk
36
+ ):
37
+ raise Exception(
38
+ "You need to configure at least one of on_chat_start, on_message or on_audio_chunk callback"
39
+ )
28
40
 
29
41
 
30
42
  # Create the main command group for Chainlit CLI
@@ -66,6 +78,7 @@ def run_chainlit(target: str):
66
78
  load_module(config.run.module_name)
67
79
 
68
80
  ensure_jwt_secret()
81
+ assert_app()
69
82
 
70
83
  # Create the chainlit.md file if it doesn't exist
71
84
  init_markdown(config.root)
chainlit/config.py CHANGED
@@ -18,7 +18,8 @@ from typing import (
18
18
 
19
19
  import tomli
20
20
  from dataclasses_json import DataClassJsonMixin
21
- from pydantic.dataclasses import Field, dataclass
21
+ from pydantic import Field
22
+ from pydantic.dataclasses import dataclass
22
23
  from starlette.datastructures import Headers
23
24
 
24
25
  from chainlit.data.base import BaseDataLayer
@@ -35,7 +36,11 @@ if TYPE_CHECKING:
35
36
  from chainlit.message import Message
36
37
  from chainlit.types import ChatProfile, InputAudioChunk, Starter, ThreadDict
37
38
  from chainlit.user import User
38
-
39
+ else:
40
+ # Pydantic needs to resolve forward annotations. Because all of these are used
41
+ # within `typing.Callable`, alias to `Any` as Pydantic does not perform validation
42
+ # of callable argument/return types anyway.
43
+ Request = Response = Action = Message = ChatProfile = InputAudioChunk = Starter = ThreadDict = User = Any # fmt: off
39
44
 
40
45
  BACKEND_ROOT = os.path.dirname(__file__)
41
46
  PACKAGE_ROOT = os.path.dirname(os.path.dirname(BACKEND_ROOT))
@@ -90,6 +95,9 @@ auto_tag_thread = true
90
95
  # Allow users to edit their own messages
91
96
  edit_message = true
92
97
 
98
+ # Use httponly cookie for client->server authentication, required to be able to use file upload and elements.
99
+ cookie_auth = true
100
+
93
101
  # Authorize users to spontaneously upload files with messages
94
102
  [features.spontaneous_file_upload]
95
103
  enabled = true
@@ -287,6 +295,7 @@ class CodeSettings:
287
295
  on_chat_end: Optional[Callable[[], Any]] = None
288
296
  on_chat_resume: Optional[Callable[["ThreadDict"], Any]] = None
289
297
  on_message: Optional[Callable[["Message"], Any]] = None
298
+ on_window_message: Optional[Callable[[str], Any]] = None
290
299
  on_audio_start: Optional[Callable[[], Any]] = None
291
300
  on_audio_chunk: Optional[Callable[["InputAudioChunk"], Any]] = None
292
301
  on_audio_end: Optional[Callable[[], Any]] = None
@@ -305,6 +314,8 @@ class CodeSettings:
305
314
  @dataclass()
306
315
  class ProjectSettings(DataClassJsonMixin):
307
316
  allow_origins: List[str] = Field(default_factory=lambda: ["*"])
317
+ # Socket.io client transports option
318
+ transports: Optional[List[str]] = None
308
319
  enable_telemetry: bool = True
309
320
  # List of environment variables to be provided by each user to use the app. If empty, no environment variables will be asked to the user.
310
321
  user_env: Optional[List[str]] = None
@@ -319,6 +330,8 @@ class ProjectSettings(DataClassJsonMixin):
319
330
  cache: bool = False
320
331
  # Follow symlink for asset mount (see https://github.com/Chainlit/chainlit/issues/317)
321
332
  follow_symlink: bool = False
333
+ # Use httponly cookie for client->server authentication, required to be able to use file upload and elements.
334
+ cookie_auth: bool = True
322
335
 
323
336
 
324
337
  @dataclass()
@@ -399,7 +412,7 @@ def init_config(log=False):
399
412
  dst = os.path.join(config_translation_dir, file)
400
413
  if not os.path.exists(dst):
401
414
  src = os.path.join(TRANSLATIONS_DIR, file)
402
- with open(src, "r", encoding="utf-8") as f:
415
+ with open(src, encoding="utf-8") as f:
403
416
  translation = json.load(f)
404
417
  with open(dst, "w", encoding="utf-8") as f:
405
418
  json.dump(translation, f, indent=4)
@@ -514,7 +527,7 @@ def load_config():
514
527
  def lint_translations():
515
528
  # Load the ground truth (en-US.json file from chainlit source code)
516
529
  src = os.path.join(TRANSLATIONS_DIR, "en-US.json")
517
- with open(src, "r", encoding="utf-8") as f:
530
+ with open(src, encoding="utf-8") as f:
518
531
  truth = json.load(f)
519
532
 
520
533
  # Find the local app translations
@@ -522,7 +535,7 @@ def lint_translations():
522
535
  if file.endswith(".json"):
523
536
  # Load the translation file
524
537
  to_lint = os.path.join(config_translation_dir, file)
525
- with open(to_lint, "r", encoding="utf-8") as f:
538
+ with open(to_lint, encoding="utf-8") as f:
526
539
  translation = json.load(f)
527
540
 
528
541
  # Lint the translation file
chainlit/context.py CHANGED
@@ -3,9 +3,10 @@ import uuid
3
3
  from contextvars import ContextVar
4
4
  from typing import TYPE_CHECKING, Dict, List, Optional, Union
5
5
 
6
- from chainlit.session import ClientType, HTTPSession, WebsocketSession
7
6
  from lazify import LazyProxy
8
7
 
8
+ from chainlit.session import ClientType, HTTPSession, WebsocketSession
9
+
9
10
  if TYPE_CHECKING:
10
11
  from chainlit.emitter import BaseChainlitEmitter
11
12
  from chainlit.step import Step
@@ -104,7 +105,7 @@ def get_context() -> ChainlitContext:
104
105
  try:
105
106
  return context_var.get()
106
107
  except LookupError as e:
107
- raise ChainlitContextException() from e
108
+ raise ChainlitContextException from e
108
109
 
109
110
 
110
111
  context: ChainlitContext = LazyProxy(get_context, enable_cache=False)