chainlit 2.0.dev2__tar.gz → 2.0rc1__tar.gz

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 (102) hide show
  1. {chainlit-2.0.dev2 → chainlit-2.0rc1}/PKG-INFO +3 -3
  2. {chainlit-2.0.dev2 → chainlit-2.0rc1}/chainlit/__init__.py +12 -4
  3. {chainlit-2.0.dev2 → chainlit-2.0rc1}/chainlit/action.py +4 -2
  4. chainlit-2.0.dev2/chainlit/auth.py → chainlit-2.0rc1/chainlit/auth/__init__.py +20 -34
  5. chainlit-2.0rc1/chainlit/auth/cookie.py +124 -0
  6. chainlit-2.0rc1/chainlit/auth/jwt.py +37 -0
  7. {chainlit-2.0.dev2 → chainlit-2.0rc1}/chainlit/cache.py +2 -1
  8. {chainlit-2.0.dev2 → chainlit-2.0rc1}/chainlit/callbacks.py +51 -6
  9. {chainlit-2.0.dev2 → chainlit-2.0rc1}/chainlit/chat_context.py +2 -2
  10. {chainlit-2.0.dev2 → chainlit-2.0rc1}/chainlit/chat_settings.py +3 -1
  11. {chainlit-2.0.dev2 → chainlit-2.0rc1}/chainlit/cli/__init__.py +14 -1
  12. {chainlit-2.0.dev2 → chainlit-2.0rc1}/chainlit/config.py +32 -15
  13. {chainlit-2.0.dev2 → chainlit-2.0rc1}/chainlit/context.py +3 -2
  14. {chainlit-2.0.dev2 → chainlit-2.0rc1}/chainlit/copilot/dist/index.js +559 -261
  15. chainlit-2.0rc1/chainlit/data/__init__.py +44 -0
  16. {chainlit-2.0.dev2 → chainlit-2.0rc1}/chainlit/data/acl.py +3 -2
  17. {chainlit-2.0.dev2 → chainlit-2.0rc1}/chainlit/data/base.py +1 -1
  18. {chainlit-2.0.dev2 → chainlit-2.0rc1}/chainlit/data/dynamodb.py +5 -3
  19. {chainlit-2.0.dev2 → chainlit-2.0rc1}/chainlit/data/literalai.py +3 -5
  20. {chainlit-2.0.dev2 → chainlit-2.0rc1}/chainlit/data/sql_alchemy.py +6 -5
  21. {chainlit-2.0.dev2 → chainlit-2.0rc1}/chainlit/data/storage_clients/azure.py +1 -0
  22. {chainlit-2.0.dev2 → chainlit-2.0rc1}/chainlit/data/storage_clients/s3.py +1 -0
  23. {chainlit-2.0.dev2 → chainlit-2.0rc1}/chainlit/discord/app.py +2 -1
  24. {chainlit-2.0.dev2 → chainlit-2.0rc1}/chainlit/element.py +6 -5
  25. {chainlit-2.0.dev2 → chainlit-2.0rc1}/chainlit/emitter.py +19 -10
  26. chainlit-2.0.dev2/chainlit/frontend/dist/assets/DailyMotion-D1ipkdPJ.js → chainlit-2.0rc1/chainlit/frontend/dist/assets/DailyMotion-C-_sjrtO.js +1 -1
  27. chainlit-2.0.dev2/chainlit/frontend/dist/assets/Facebook-d4TLeTik.js → chainlit-2.0rc1/chainlit/frontend/dist/assets/Facebook-bB34P03l.js +1 -1
  28. chainlit-2.0.dev2/chainlit/frontend/dist/assets/FilePlayer-BcU7tttX.js → chainlit-2.0rc1/chainlit/frontend/dist/assets/FilePlayer-BWgqGrXv.js +1 -1
  29. chainlit-2.0.dev2/chainlit/frontend/dist/assets/Kaltura-DdaRjZrh.js → chainlit-2.0rc1/chainlit/frontend/dist/assets/Kaltura-OY4P9Ofd.js +1 -1
  30. chainlit-2.0.dev2/chainlit/frontend/dist/assets/Mixcloud-BaJoMsaU.js → chainlit-2.0rc1/chainlit/frontend/dist/assets/Mixcloud-9CtT8w5Y.js +1 -1
  31. chainlit-2.0.dev2/chainlit/frontend/dist/assets/Mux-DxPCM5d3.js → chainlit-2.0rc1/chainlit/frontend/dist/assets/Mux-BH9A0qEi.js +1 -1
  32. chainlit-2.0.dev2/chainlit/frontend/dist/assets/Preview-tUK_Z9pZ.js → chainlit-2.0rc1/chainlit/frontend/dist/assets/Preview-Og00EJ05.js +1 -1
  33. chainlit-2.0.dev2/chainlit/frontend/dist/assets/SoundCloud-K8-lFZC6.js → chainlit-2.0rc1/chainlit/frontend/dist/assets/SoundCloud-D7resGfn.js +1 -1
  34. chainlit-2.0.dev2/chainlit/frontend/dist/assets/Streamable-hB-AQ54w.js → chainlit-2.0rc1/chainlit/frontend/dist/assets/Streamable-6f_6bYz1.js +1 -1
  35. chainlit-2.0.dev2/chainlit/frontend/dist/assets/Twitch-pmuNY0J5.js → chainlit-2.0rc1/chainlit/frontend/dist/assets/Twitch-BZJl3peM.js +1 -1
  36. chainlit-2.0.dev2/chainlit/frontend/dist/assets/Vidyard-BSUm6trV.js → chainlit-2.0rc1/chainlit/frontend/dist/assets/Vidyard-B7tv4b8_.js +1 -1
  37. chainlit-2.0.dev2/chainlit/frontend/dist/assets/Vimeo-JIPn71zS.js → chainlit-2.0rc1/chainlit/frontend/dist/assets/Vimeo-F-eA4zQI.js +1 -1
  38. chainlit-2.0.dev2/chainlit/frontend/dist/assets/Wistia-D75KkqOG.js → chainlit-2.0rc1/chainlit/frontend/dist/assets/Wistia-Dhxhn3IB.js +1 -1
  39. chainlit-2.0.dev2/chainlit/frontend/dist/assets/YouTube-CPlwqNm_.js → chainlit-2.0rc1/chainlit/frontend/dist/assets/YouTube-aFdJGjI1.js +1 -1
  40. chainlit-2.0.dev2/chainlit/frontend/dist/assets/index-CuSbXjG5.js → chainlit-2.0rc1/chainlit/frontend/dist/assets/index-Ba33_hdJ.js +122 -122
  41. chainlit-2.0.dev2/chainlit/frontend/dist/assets/react-plotly-DALmanjC.js → chainlit-2.0rc1/chainlit/frontend/dist/assets/react-plotly-DoUJXMgz.js +1 -1
  42. {chainlit-2.0.dev2 → chainlit-2.0rc1}/chainlit/frontend/dist/index.html +1 -1
  43. {chainlit-2.0.dev2 → chainlit-2.0rc1}/chainlit/haystack/callbacks.py +5 -4
  44. {chainlit-2.0.dev2 → chainlit-2.0rc1}/chainlit/input_widget.py +6 -4
  45. {chainlit-2.0.dev2 → chainlit-2.0rc1}/chainlit/langchain/callbacks.py +56 -47
  46. {chainlit-2.0.dev2 → chainlit-2.0rc1}/chainlit/langflow/__init__.py +1 -0
  47. {chainlit-2.0.dev2 → chainlit-2.0rc1}/chainlit/llama_index/callbacks.py +7 -7
  48. {chainlit-2.0.dev2 → chainlit-2.0rc1}/chainlit/message.py +6 -5
  49. {chainlit-2.0.dev2 → chainlit-2.0rc1}/chainlit/mistralai/__init__.py +3 -2
  50. {chainlit-2.0.dev2 → chainlit-2.0rc1}/chainlit/oauth_providers.py +70 -3
  51. {chainlit-2.0.dev2 → chainlit-2.0rc1}/chainlit/openai/__init__.py +3 -2
  52. {chainlit-2.0.dev2 → chainlit-2.0rc1}/chainlit/secret.py +1 -1
  53. {chainlit-2.0.dev2 → chainlit-2.0rc1}/chainlit/server.py +232 -156
  54. {chainlit-2.0.dev2 → chainlit-2.0rc1}/chainlit/session.py +7 -5
  55. {chainlit-2.0.dev2 → chainlit-2.0rc1}/chainlit/slack/app.py +3 -2
  56. {chainlit-2.0.dev2 → chainlit-2.0rc1}/chainlit/socket.py +88 -63
  57. {chainlit-2.0.dev2 → chainlit-2.0rc1}/chainlit/step.py +11 -10
  58. {chainlit-2.0.dev2 → chainlit-2.0rc1}/chainlit/sync.py +2 -1
  59. {chainlit-2.0.dev2 → chainlit-2.0rc1}/chainlit/teams/app.py +1 -0
  60. chainlit-2.0rc1/chainlit/translations/nl-NL.json +229 -0
  61. {chainlit-2.0.dev2 → chainlit-2.0rc1}/chainlit/types.py +3 -1
  62. {chainlit-2.0.dev2 → chainlit-2.0rc1}/chainlit/user.py +2 -1
  63. {chainlit-2.0.dev2 → chainlit-2.0rc1}/chainlit/utils.py +3 -2
  64. {chainlit-2.0.dev2 → chainlit-2.0rc1}/pyproject.toml +9 -9
  65. chainlit-2.0.dev2/chainlit/data/__init__.py +0 -25
  66. {chainlit-2.0.dev2 → chainlit-2.0rc1}/README.md +0 -0
  67. {chainlit-2.0.dev2 → chainlit-2.0rc1}/chainlit/__main__.py +0 -0
  68. {chainlit-2.0.dev2 → chainlit-2.0rc1}/chainlit/_utils.py +0 -0
  69. {chainlit-2.0.dev2 → chainlit-2.0rc1}/chainlit/copilot/dist/assets/logo_dark-IkGJ_IwC.svg +0 -0
  70. {chainlit-2.0.dev2 → chainlit-2.0rc1}/chainlit/copilot/dist/assets/logo_light-Bb_IPh6r.svg +0 -0
  71. {chainlit-2.0.dev2 → chainlit-2.0rc1}/chainlit/data/storage_clients/__init__.py +0 -0
  72. {chainlit-2.0.dev2 → chainlit-2.0rc1}/chainlit/data/storage_clients/base.py +0 -0
  73. {chainlit-2.0.dev2 → chainlit-2.0rc1}/chainlit/data/utils.py +0 -0
  74. {chainlit-2.0.dev2 → chainlit-2.0rc1}/chainlit/discord/__init__.py +0 -0
  75. {chainlit-2.0.dev2 → chainlit-2.0rc1}/chainlit/frontend/dist/assets/index-CwmincdQ.css +0 -0
  76. {chainlit-2.0.dev2 → chainlit-2.0rc1}/chainlit/frontend/dist/assets/logo_dark-IkGJ_IwC.svg +0 -0
  77. {chainlit-2.0.dev2 → chainlit-2.0rc1}/chainlit/frontend/dist/assets/logo_light-Bb_IPh6r.svg +0 -0
  78. {chainlit-2.0.dev2 → chainlit-2.0rc1}/chainlit/frontend/dist/favicon.svg +0 -0
  79. {chainlit-2.0.dev2 → chainlit-2.0rc1}/chainlit/haystack/__init__.py +0 -0
  80. {chainlit-2.0.dev2 → chainlit-2.0rc1}/chainlit/hello.py +0 -0
  81. {chainlit-2.0.dev2 → chainlit-2.0rc1}/chainlit/langchain/__init__.py +0 -0
  82. {chainlit-2.0.dev2 → chainlit-2.0rc1}/chainlit/llama_index/__init__.py +0 -0
  83. {chainlit-2.0.dev2 → chainlit-2.0rc1}/chainlit/logger.py +0 -0
  84. {chainlit-2.0.dev2 → chainlit-2.0rc1}/chainlit/markdown.py +0 -0
  85. {chainlit-2.0.dev2 → chainlit-2.0rc1}/chainlit/py.typed +0 -0
  86. {chainlit-2.0.dev2 → chainlit-2.0rc1}/chainlit/slack/__init__.py +0 -0
  87. {chainlit-2.0.dev2 → chainlit-2.0rc1}/chainlit/teams/__init__.py +0 -0
  88. {chainlit-2.0.dev2 → chainlit-2.0rc1}/chainlit/telemetry.py +0 -0
  89. {chainlit-2.0.dev2 → chainlit-2.0rc1}/chainlit/translations/bn.json +0 -0
  90. {chainlit-2.0.dev2 → chainlit-2.0rc1}/chainlit/translations/en-US.json +0 -0
  91. {chainlit-2.0.dev2 → chainlit-2.0rc1}/chainlit/translations/gu.json +0 -0
  92. {chainlit-2.0.dev2 → chainlit-2.0rc1}/chainlit/translations/he-IL.json +0 -0
  93. {chainlit-2.0.dev2 → chainlit-2.0rc1}/chainlit/translations/hi.json +0 -0
  94. {chainlit-2.0.dev2 → chainlit-2.0rc1}/chainlit/translations/kn.json +0 -0
  95. {chainlit-2.0.dev2 → chainlit-2.0rc1}/chainlit/translations/ml.json +0 -0
  96. {chainlit-2.0.dev2 → chainlit-2.0rc1}/chainlit/translations/mr.json +0 -0
  97. {chainlit-2.0.dev2 → chainlit-2.0rc1}/chainlit/translations/ta.json +0 -0
  98. {chainlit-2.0.dev2 → chainlit-2.0rc1}/chainlit/translations/te.json +0 -0
  99. {chainlit-2.0.dev2 → chainlit-2.0rc1}/chainlit/translations/zh-CN.json +0 -0
  100. {chainlit-2.0.dev2 → chainlit-2.0rc1}/chainlit/translations.py +0 -0
  101. {chainlit-2.0.dev2 → chainlit-2.0rc1}/chainlit/user_session.py +0 -0
  102. {chainlit-2.0.dev2 → chainlit-2.0rc1}/chainlit/version.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: chainlit
3
- Version: 2.0.dev2
3
+ Version: 2.0rc1
4
4
  Summary: Build Conversational AI.
5
5
  Home-page: https://chainlit.io/
6
6
  License: Apache-2.0
@@ -24,7 +24,7 @@ Requires-Dist: aiofiles (>=23.1.0,<24.0.0)
24
24
  Requires-Dist: asyncer (>=0.0.7,<0.0.8)
25
25
  Requires-Dist: click (>=8.1.3,<9.0.0)
26
26
  Requires-Dist: dataclasses_json (>=0.6.7,<0.7.0)
27
- Requires-Dist: fastapi (>=0.110.1,<0.113)
27
+ Requires-Dist: fastapi (>=0.115.3,<0.116)
28
28
  Requires-Dist: filetype (>=1.2.0,<2.0.0)
29
29
  Requires-Dist: httpx (>=0.23.0)
30
30
  Requires-Dist: lazify (>=0.4.0,<0.5.0)
@@ -37,7 +37,7 @@ Requires-Dist: pyjwt (>=2.8.0,<3.0.0)
37
37
  Requires-Dist: python-dotenv (>=1.0.0,<2.0.0)
38
38
  Requires-Dist: python-multipart (>=0.0.9,<0.0.10)
39
39
  Requires-Dist: python-socketio (>=5.11.0,<6.0.0)
40
- Requires-Dist: starlette (>=0.37.2,<0.38.0)
40
+ Requires-Dist: starlette (>=0.41.2,<0.42.0)
41
41
  Requires-Dist: syncer (>=2.0.3,<3.0.0)
42
42
  Requires-Dist: tomli (>=2.0.1,<3.0.0)
43
43
  Requires-Dist: uptrace (>=1.22.0,<2.0.0)
@@ -14,6 +14,9 @@ if env_found:
14
14
  import asyncio
15
15
  from typing import TYPE_CHECKING, Any, Dict
16
16
 
17
+ from literalai import ChatGeneration, CompletionGeneration, GenerationMessage
18
+ from pydantic.dataclasses import dataclass
19
+
17
20
  import chainlit.input_widget as input_widget
18
21
  from chainlit.action import Action
19
22
  from chainlit.cache import cache
@@ -44,22 +47,21 @@ from chainlit.message import (
44
47
  )
45
48
  from chainlit.step import Step, step
46
49
  from chainlit.sync import make_async, run_sync
47
- from chainlit.types import InputAudioChunk, OutputAudioChunk, ChatProfile, Starter
50
+ from chainlit.types import ChatProfile, InputAudioChunk, OutputAudioChunk, Starter
48
51
  from chainlit.user import PersistedUser, User
49
52
  from chainlit.user_session import user_session
50
53
  from chainlit.utils import make_module_getattr
51
54
  from chainlit.version import __version__
52
- from literalai import ChatGeneration, CompletionGeneration, GenerationMessage
53
- from pydantic.dataclasses import dataclass
54
55
 
55
56
  from .callbacks import (
56
57
  action_callback,
57
58
  author_rename,
59
+ data_layer,
58
60
  header_auth_callback,
59
61
  oauth_callback,
60
- on_audio_start,
61
62
  on_audio_chunk,
62
63
  on_audio_end,
64
+ on_audio_start,
63
65
  on_chat_end,
64
66
  on_chat_resume,
65
67
  on_chat_start,
@@ -67,7 +69,9 @@ from .callbacks import (
67
69
  on_message,
68
70
  on_settings_update,
69
71
  on_stop,
72
+ on_window_message,
70
73
  password_auth_callback,
74
+ send_window_message,
71
75
  set_chat_profiles,
72
76
  set_starters,
73
77
  )
@@ -130,6 +134,7 @@ __all__ = [
130
134
  "Image",
131
135
  "Text",
132
136
  "Component",
137
+ "Dataframe",
133
138
  "Pyplot",
134
139
  "File",
135
140
  "Task",
@@ -149,6 +154,8 @@ __all__ = [
149
154
  "CompletionGeneration",
150
155
  "GenerationMessage",
151
156
  "on_logout",
157
+ "on_window_message",
158
+ "send_window_message",
152
159
  "on_chat_start",
153
160
  "on_chat_end",
154
161
  "on_chat_resume",
@@ -186,6 +193,7 @@ __all__ = [
186
193
  "on_stop",
187
194
  "action_callback",
188
195
  "on_settings_update",
196
+ "data_layer",
189
197
  ]
190
198
 
191
199
 
@@ -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?
@@ -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)
@@ -1,6 +1,7 @@
1
1
  import importlib.util
2
2
  import os
3
3
  import threading
4
+ from typing import Any
4
5
 
5
6
  from chainlit.config import config
6
7
  from chainlit.logger import logger
@@ -22,7 +23,7 @@ def init_lc_cache():
22
23
  )
23
24
 
24
25
 
25
- _cache = {}
26
+ _cache: dict[tuple, Any] = {}
26
27
  _cache_lock = threading.Lock()
27
28
 
28
29
 
@@ -1,8 +1,13 @@
1
1
  import inspect
2
2
  from typing import Any, Awaitable, Callable, Dict, List, Optional
3
3
 
4
+ from fastapi import Request, Response
5
+ from starlette.datastructures import Headers
6
+
4
7
  from chainlit.action import Action
5
8
  from chainlit.config import config
9
+ from chainlit.context import context
10
+ from chainlit.data.base import BaseDataLayer
6
11
  from chainlit.message import Message
7
12
  from chainlit.oauth_providers import get_configured_oauth_providers
8
13
  from chainlit.step import Step, step
@@ -10,13 +15,11 @@ from chainlit.telemetry import trace
10
15
  from chainlit.types import ChatProfile, Starter, ThreadDict
11
16
  from chainlit.user import User
12
17
  from chainlit.utils import wrap_user_function
13
- from fastapi import Request, Response
14
- from starlette.datastructures import Headers
15
18
 
16
19
 
17
20
  @trace
18
21
  def password_auth_callback(
19
- func: Callable[[str, str], Awaitable[Optional[User]]]
22
+ func: Callable[[str, str], Awaitable[Optional[User]]],
20
23
  ) -> Callable:
21
24
  """
22
25
  Framework agnostic decorator to authenticate the user.
@@ -38,7 +41,7 @@ def password_auth_callback(
38
41
 
39
42
  @trace
40
43
  def header_auth_callback(
41
- func: Callable[[Headers], Awaitable[Optional[User]]]
44
+ func: Callable[[Headers], Awaitable[Optional[User]]],
42
45
  ) -> Callable:
43
46
  """
44
47
  Framework agnostic decorator to authenticate the user via a header
@@ -123,6 +126,33 @@ def on_message(func: Callable) -> Callable:
123
126
  return func
124
127
 
125
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
+
126
156
  @trace
127
157
  def on_chat_start(func: Callable) -> Callable:
128
158
  """
@@ -177,7 +207,7 @@ def set_chat_profiles(
177
207
 
178
208
  @trace
179
209
  def set_starters(
180
- func: Callable[[Optional["User"]], Awaitable[List["Starter"]]]
210
+ func: Callable[[Optional["User"]], Awaitable[List["Starter"]]],
181
211
  ) -> Callable:
182
212
  """
183
213
  Programmatic declaration of the available starter (can depend on the User from the session if authentication is setup).
@@ -221,6 +251,7 @@ def on_audio_start(func: Callable) -> Callable:
221
251
  config.code.on_audio_start = wrap_user_function(func, with_task=False)
222
252
  return func
223
253
 
254
+
224
255
  @trace
225
256
  def on_audio_chunk(func: Callable) -> Callable:
226
257
  """
@@ -254,7 +285,7 @@ def on_audio_end(func: Callable) -> Callable:
254
285
 
255
286
  @trace
256
287
  def author_rename(
257
- func: Callable[[str], Awaitable[str]]
288
+ func: Callable[[str], Awaitable[str]],
258
289
  ) -> Callable[[str], Awaitable[str]]:
259
290
  """
260
291
  Useful to rename the author of message to display more friendly author names in the UI.
@@ -315,3 +346,17 @@ def on_settings_update(
315
346
 
316
347
  config.code.on_settings_update = wrap_user_function(func, with_task=True)
317
348
  return func
349
+
350
+
351
+ def data_layer(
352
+ func: Callable[[], BaseDataLayer],
353
+ ) -> Callable[[], BaseDataLayer]:
354
+ """
355
+ Hook to configure custom data layer.
356
+ """
357
+
358
+ # We don't use wrap_user_function here because:
359
+ # 1. We don't need to support async here and;
360
+ # 2. We don't want to change the API for get_data_layer() to be async, everywhere (at this point).
361
+ config.code.data_layer = func
362
+ return func
@@ -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:
@@ -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
@@ -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)
@@ -17,22 +17,30 @@ from typing import (
17
17
  )
18
18
 
19
19
  import tomli
20
+ from dataclasses_json import DataClassJsonMixin
21
+ from pydantic import Field
22
+ from pydantic.dataclasses import dataclass
23
+ from starlette.datastructures import Headers
24
+
25
+ from chainlit.data.base import BaseDataLayer
20
26
  from chainlit.logger import logger
21
27
  from chainlit.translations import lint_translation_json
22
28
  from chainlit.version import __version__
23
- from dataclasses_json import DataClassJsonMixin
24
- from pydantic.dataclasses import Field, dataclass
25
- from starlette.datastructures import Headers
26
29
 
27
30
  from ._utils import is_path_inside
28
31
 
29
32
  if TYPE_CHECKING:
33
+ from fastapi import Request, Response
34
+
30
35
  from chainlit.action import Action
31
36
  from chainlit.message import Message
32
- from chainlit.types import InputAudioChunk, ChatProfile, Starter, ThreadDict
37
+ from chainlit.types import ChatProfile, InputAudioChunk, Starter, ThreadDict
33
38
  from chainlit.user import User
34
- from fastapi import Request, Response
35
-
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
36
44
 
37
45
  BACKEND_ROOT = os.path.dirname(__file__)
38
46
  PACKAGE_ROOT = os.path.dirname(os.path.dirname(BACKEND_ROOT))
@@ -87,6 +95,9 @@ auto_tag_thread = true
87
95
  # Allow users to edit their own messages
88
96
  edit_message = true
89
97
 
98
+ # Use httponly cookie for client->server authentication, required to be able to use file upload and elements.
99
+ cookie_auth = true
100
+
90
101
  # Authorize users to spontaneously upload files with messages
91
102
  [features.spontaneous_file_upload]
92
103
  enabled = true
@@ -272,9 +283,9 @@ class CodeSettings:
272
283
  password_auth_callback: Optional[
273
284
  Callable[[str, str], Awaitable[Optional["User"]]]
274
285
  ] = None
275
- header_auth_callback: Optional[
276
- Callable[[Headers], Awaitable[Optional["User"]]]
277
- ] = None
286
+ header_auth_callback: Optional[Callable[[Headers], Awaitable[Optional["User"]]]] = (
287
+ None
288
+ )
278
289
  oauth_callback: Optional[
279
290
  Callable[[str, str, Dict[str, str], "User"], Awaitable[Optional["User"]]]
280
291
  ] = None
@@ -284,6 +295,7 @@ class CodeSettings:
284
295
  on_chat_end: Optional[Callable[[], Any]] = None
285
296
  on_chat_resume: Optional[Callable[["ThreadDict"], Any]] = None
286
297
  on_message: Optional[Callable[["Message"], Any]] = None
298
+ on_window_message: Optional[Callable[[str], Any]] = None
287
299
  on_audio_start: Optional[Callable[[], Any]] = None
288
300
  on_audio_chunk: Optional[Callable[["InputAudioChunk"], Any]] = None
289
301
  on_audio_end: Optional[Callable[[], Any]] = None
@@ -293,14 +305,17 @@ class CodeSettings:
293
305
  set_chat_profiles: Optional[
294
306
  Callable[[Optional["User"]], Awaitable[List["ChatProfile"]]]
295
307
  ] = None
296
- set_starters: Optional[
297
- Callable[[Optional["User"]], Awaitable[List["Starter"]]]
298
- ] = None
308
+ set_starters: Optional[Callable[[Optional["User"]], Awaitable[List["Starter"]]]] = (
309
+ None
310
+ )
311
+ data_layer: Optional[Callable[[], BaseDataLayer]] = None
299
312
 
300
313
 
301
314
  @dataclass()
302
315
  class ProjectSettings(DataClassJsonMixin):
303
316
  allow_origins: List[str] = Field(default_factory=lambda: ["*"])
317
+ # Socket.io client transports option
318
+ transports: Optional[List[str]] = None
304
319
  enable_telemetry: bool = True
305
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.
306
321
  user_env: Optional[List[str]] = None
@@ -315,6 +330,8 @@ class ProjectSettings(DataClassJsonMixin):
315
330
  cache: bool = False
316
331
  # Follow symlink for asset mount (see https://github.com/Chainlit/chainlit/issues/317)
317
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
318
335
 
319
336
 
320
337
  @dataclass()
@@ -395,7 +412,7 @@ def init_config(log=False):
395
412
  dst = os.path.join(config_translation_dir, file)
396
413
  if not os.path.exists(dst):
397
414
  src = os.path.join(TRANSLATIONS_DIR, file)
398
- with open(src, "r", encoding="utf-8") as f:
415
+ with open(src, encoding="utf-8") as f:
399
416
  translation = json.load(f)
400
417
  with open(dst, "w", encoding="utf-8") as f:
401
418
  json.dump(translation, f, indent=4)
@@ -510,7 +527,7 @@ def load_config():
510
527
  def lint_translations():
511
528
  # Load the ground truth (en-US.json file from chainlit source code)
512
529
  src = os.path.join(TRANSLATIONS_DIR, "en-US.json")
513
- with open(src, "r", encoding="utf-8") as f:
530
+ with open(src, encoding="utf-8") as f:
514
531
  truth = json.load(f)
515
532
 
516
533
  # Find the local app translations
@@ -518,7 +535,7 @@ def lint_translations():
518
535
  if file.endswith(".json"):
519
536
  # Load the translation file
520
537
  to_lint = os.path.join(config_translation_dir, file)
521
- with open(to_lint, "r", encoding="utf-8") as f:
538
+ with open(to_lint, encoding="utf-8") as f:
522
539
  translation = json.load(f)
523
540
 
524
541
  # Lint the translation file