chainlit 1.1.300rc4__py3-none-any.whl → 1.1.300rc5__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 +20 -1
- chainlit/cli/__init__.py +48 -6
- chainlit/config.py +5 -0
- chainlit/context.py +9 -0
- chainlit/copilot/dist/index.js +180 -180
- chainlit/data/__init__.py +6 -3
- chainlit/data/sql_alchemy.py +3 -3
- chainlit/element.py +33 -9
- chainlit/emitter.py +3 -3
- chainlit/frontend/dist/assets/{DailyMotion-1a2b7d60.js → DailyMotion-f9db5a1d.js} +1 -1
- chainlit/frontend/dist/assets/{Facebook-8422f48c.js → Facebook-f95b29c9.js} +1 -1
- chainlit/frontend/dist/assets/{FilePlayer-a0a41349.js → FilePlayer-ba3f562c.js} +1 -1
- chainlit/frontend/dist/assets/{Kaltura-aa7990f2.js → Kaltura-195ed801.js} +1 -1
- chainlit/frontend/dist/assets/{Mixcloud-1647b3e4.js → Mixcloud-f64c6d87.js} +1 -1
- chainlit/frontend/dist/assets/{Mux-7e57be81.js → Mux-206cbddc.js} +1 -1
- chainlit/frontend/dist/assets/{Preview-cb89b2c6.js → Preview-af249586.js} +1 -1
- chainlit/frontend/dist/assets/{SoundCloud-c0d86d55.js → SoundCloud-80a26cdf.js} +1 -1
- chainlit/frontend/dist/assets/{Streamable-57c43c18.js → Streamable-f80b255d.js} +1 -1
- chainlit/frontend/dist/assets/{Twitch-bed3f21d.js → Twitch-0e2f1d13.js} +1 -1
- chainlit/frontend/dist/assets/{Vidyard-4dd76e44.js → Vidyard-bd67bfc6.js} +1 -1
- chainlit/frontend/dist/assets/{Vimeo-93bb5ae2.js → Vimeo-f9496a5d.js} +1 -1
- chainlit/frontend/dist/assets/{Wistia-97199246.js → Wistia-a943e0aa.js} +1 -1
- chainlit/frontend/dist/assets/{YouTube-fe1a7afe.js → YouTube-cf572a1f.js} +1 -1
- chainlit/frontend/dist/assets/{index-919bea8f.js → index-5511e258.js} +131 -131
- chainlit/frontend/dist/assets/{react-plotly-b416b8f9.js → react-plotly-74b55763.js} +1 -1
- chainlit/frontend/dist/index.html +1 -2
- chainlit/message.py +13 -8
- chainlit/oauth_providers.py +63 -1
- chainlit/server.py +170 -48
- chainlit/socket.py +40 -20
- chainlit/step.py +12 -3
- chainlit/teams/__init__.py +6 -0
- chainlit/teams/app.py +332 -0
- chainlit/translations/en-US.json +1 -1
- chainlit/types.py +7 -17
- chainlit/user.py +9 -1
- chainlit/utils.py +42 -0
- {chainlit-1.1.300rc4.dist-info → chainlit-1.1.300rc5.dist-info}/METADATA +2 -2
- chainlit-1.1.300rc5.dist-info/RECORD +79 -0
- chainlit/cli/utils.py +0 -24
- chainlit-1.1.300rc4.dist-info/RECORD +0 -78
- {chainlit-1.1.300rc4.dist-info → chainlit-1.1.300rc5.dist-info}/WHEEL +0 -0
- {chainlit-1.1.300rc4.dist-info → chainlit-1.1.300rc5.dist-info}/entry_points.txt +0 -0
chainlit/socket.py
CHANGED
|
@@ -2,8 +2,8 @@ import asyncio
|
|
|
2
2
|
import json
|
|
3
3
|
import time
|
|
4
4
|
import uuid
|
|
5
|
-
from urllib.parse import unquote
|
|
6
5
|
from typing import Any, Dict, Literal
|
|
6
|
+
from urllib.parse import unquote
|
|
7
7
|
|
|
8
8
|
from chainlit.action import Action
|
|
9
9
|
from chainlit.auth import get_current_user, require_login
|
|
@@ -13,14 +13,15 @@ from chainlit.data import get_data_layer
|
|
|
13
13
|
from chainlit.element import Element
|
|
14
14
|
from chainlit.logger import logger
|
|
15
15
|
from chainlit.message import ErrorMessage, Message
|
|
16
|
-
from chainlit.server import
|
|
16
|
+
from chainlit.server import sio
|
|
17
17
|
from chainlit.session import WebsocketSession
|
|
18
18
|
from chainlit.telemetry import trace_event
|
|
19
19
|
from chainlit.types import (
|
|
20
20
|
AudioChunk,
|
|
21
21
|
AudioChunkPayload,
|
|
22
22
|
AudioEndPayload,
|
|
23
|
-
|
|
23
|
+
SystemMessagePayload,
|
|
24
|
+
UserMessagePayload,
|
|
24
25
|
)
|
|
25
26
|
from chainlit.user_session import user_sessions
|
|
26
27
|
|
|
@@ -53,7 +54,7 @@ async def resume_thread(session: WebsocketSession):
|
|
|
53
54
|
user_is_author = author == session.user.identifier
|
|
54
55
|
|
|
55
56
|
if user_is_author:
|
|
56
|
-
metadata = thread.get("metadata"
|
|
57
|
+
metadata = thread.get("metadata") or {}
|
|
57
58
|
user_sessions[session.id] = metadata.copy()
|
|
58
59
|
if chat_profile := metadata.get("chat_profile"):
|
|
59
60
|
session.chat_profile = chat_profile
|
|
@@ -98,8 +99,8 @@ def build_anon_user_identifier(environ):
|
|
|
98
99
|
return str(uuid.uuid5(uuid.NAMESPACE_DNS, ip))
|
|
99
100
|
|
|
100
101
|
|
|
101
|
-
@
|
|
102
|
-
async def connect(sid, environ
|
|
102
|
+
@sio.on("connect")
|
|
103
|
+
async def connect(sid, environ):
|
|
103
104
|
if (
|
|
104
105
|
not config.code.on_chat_start
|
|
105
106
|
and not config.code.on_message
|
|
@@ -124,11 +125,11 @@ async def connect(sid, environ, auth):
|
|
|
124
125
|
|
|
125
126
|
# Session scoped function to emit to the client
|
|
126
127
|
def emit_fn(event, data):
|
|
127
|
-
return
|
|
128
|
+
return sio.emit(event, data, to=sid)
|
|
128
129
|
|
|
129
130
|
# Session scoped function to emit to the client and wait for a response
|
|
130
131
|
def emit_call_fn(event: Literal["ask", "call_fn"], data, timeout):
|
|
131
|
-
return
|
|
132
|
+
return sio.call(event, data, timeout=timeout, to=sid)
|
|
132
133
|
|
|
133
134
|
session_id = environ.get("HTTP_X_CHAINLIT_SESSION_ID")
|
|
134
135
|
if restore_existing_session(sid, session_id, emit_fn, emit_call_fn):
|
|
@@ -140,7 +141,9 @@ async def connect(sid, environ, auth):
|
|
|
140
141
|
client_type = environ.get("HTTP_X_CHAINLIT_CLIENT_TYPE")
|
|
141
142
|
http_referer = environ.get("HTTP_REFERER")
|
|
142
143
|
url_encoded_chat_profile = environ.get("HTTP_X_CHAINLIT_CHAT_PROFILE")
|
|
143
|
-
chat_profile =
|
|
144
|
+
chat_profile = (
|
|
145
|
+
unquote(url_encoded_chat_profile) if url_encoded_chat_profile else None
|
|
146
|
+
)
|
|
144
147
|
|
|
145
148
|
ws_session = WebsocketSession(
|
|
146
149
|
id=session_id,
|
|
@@ -161,7 +164,7 @@ async def connect(sid, environ, auth):
|
|
|
161
164
|
return True
|
|
162
165
|
|
|
163
166
|
|
|
164
|
-
@
|
|
167
|
+
@sio.on("connection_successful")
|
|
165
168
|
async def connection_successful(sid):
|
|
166
169
|
context = init_ws_context(sid)
|
|
167
170
|
|
|
@@ -189,14 +192,14 @@ async def connection_successful(sid):
|
|
|
189
192
|
context.session.current_task = task
|
|
190
193
|
|
|
191
194
|
|
|
192
|
-
@
|
|
195
|
+
@sio.on("clear_session")
|
|
193
196
|
async def clean_session(sid):
|
|
194
197
|
session = WebsocketSession.get(sid)
|
|
195
198
|
if session:
|
|
196
199
|
session.to_clear = True
|
|
197
200
|
|
|
198
201
|
|
|
199
|
-
@
|
|
202
|
+
@sio.on("disconnect")
|
|
200
203
|
async def disconnect(sid):
|
|
201
204
|
session = WebsocketSession.get(sid)
|
|
202
205
|
|
|
@@ -230,7 +233,7 @@ async def disconnect(sid):
|
|
|
230
233
|
asyncio.ensure_future(clear_on_timeout(sid))
|
|
231
234
|
|
|
232
235
|
|
|
233
|
-
@
|
|
236
|
+
@sio.on("stop")
|
|
234
237
|
async def stop(sid):
|
|
235
238
|
if session := WebsocketSession.get(sid):
|
|
236
239
|
trace_event("stop_task")
|
|
@@ -245,7 +248,7 @@ async def stop(sid):
|
|
|
245
248
|
await config.code.on_stop()
|
|
246
249
|
|
|
247
250
|
|
|
248
|
-
async def process_message(session: WebsocketSession, payload:
|
|
251
|
+
async def process_message(session: WebsocketSession, payload: UserMessagePayload):
|
|
249
252
|
"""Process a message from the user."""
|
|
250
253
|
try:
|
|
251
254
|
context = init_ws_context(session)
|
|
@@ -267,8 +270,8 @@ async def process_message(session: WebsocketSession, payload: UIMessagePayload):
|
|
|
267
270
|
await context.emitter.task_end()
|
|
268
271
|
|
|
269
272
|
|
|
270
|
-
@
|
|
271
|
-
async def message(sid, payload:
|
|
273
|
+
@sio.on("user_message")
|
|
274
|
+
async def message(sid, payload: UserMessagePayload):
|
|
272
275
|
"""Handle a message sent by the User."""
|
|
273
276
|
session = WebsocketSession.require(sid)
|
|
274
277
|
|
|
@@ -276,7 +279,24 @@ async def message(sid, payload: UIMessagePayload):
|
|
|
276
279
|
session.current_task = task
|
|
277
280
|
|
|
278
281
|
|
|
279
|
-
@
|
|
282
|
+
@sio.on("system_message")
|
|
283
|
+
async def on_system_message(sid, payload: SystemMessagePayload):
|
|
284
|
+
"""Handle a message sent by the User."""
|
|
285
|
+
session = WebsocketSession.require(sid)
|
|
286
|
+
|
|
287
|
+
init_ws_context(session)
|
|
288
|
+
message = Message(
|
|
289
|
+
type="system_message",
|
|
290
|
+
content=payload["content"],
|
|
291
|
+
metadata=payload.get("metadata"),
|
|
292
|
+
)
|
|
293
|
+
asyncio.create_task(message._create())
|
|
294
|
+
|
|
295
|
+
if config.code.on_system_message:
|
|
296
|
+
await config.code.on_system_message(message)
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
@sio.on("audio_chunk")
|
|
280
300
|
async def audio_chunk(sid, payload: AudioChunkPayload):
|
|
281
301
|
"""Handle an audio chunk sent by the user."""
|
|
282
302
|
session = WebsocketSession.require(sid)
|
|
@@ -287,7 +307,7 @@ async def audio_chunk(sid, payload: AudioChunkPayload):
|
|
|
287
307
|
asyncio.create_task(config.code.on_audio_chunk(AudioChunk(**payload)))
|
|
288
308
|
|
|
289
309
|
|
|
290
|
-
@
|
|
310
|
+
@sio.on("audio_end")
|
|
291
311
|
async def audio_end(sid, payload: AudioEndPayload):
|
|
292
312
|
"""Handle the end of the audio stream."""
|
|
293
313
|
session = WebsocketSession.require(sid)
|
|
@@ -331,7 +351,7 @@ async def process_action(action: Action):
|
|
|
331
351
|
logger.warning("No callback found for action %s", action.name)
|
|
332
352
|
|
|
333
353
|
|
|
334
|
-
@
|
|
354
|
+
@sio.on("action_call")
|
|
335
355
|
async def call_action(sid, action):
|
|
336
356
|
"""Handle an action call from the UI."""
|
|
337
357
|
context = init_ws_context(sid)
|
|
@@ -358,7 +378,7 @@ async def call_action(sid, action):
|
|
|
358
378
|
)
|
|
359
379
|
|
|
360
380
|
|
|
361
|
-
@
|
|
381
|
+
@sio.on("chat_settings_change")
|
|
362
382
|
async def change_settings(sid, settings: Dict[str, Any]):
|
|
363
383
|
"""Handle change settings submit from the UI."""
|
|
364
384
|
context = init_ws_context(sid)
|
chainlit/step.py
CHANGED
|
@@ -295,7 +295,10 @@ class Step:
|
|
|
295
295
|
tasks = [el.send(for_id=self.id) for el in self.elements]
|
|
296
296
|
await asyncio.gather(*tasks)
|
|
297
297
|
|
|
298
|
-
if config.ui.hide_cot and
|
|
298
|
+
if config.ui.hide_cot and self.type not in [
|
|
299
|
+
"user_message",
|
|
300
|
+
"assistant_message",
|
|
301
|
+
]:
|
|
299
302
|
return True
|
|
300
303
|
|
|
301
304
|
await context.emitter.update_step(step_dict)
|
|
@@ -349,7 +352,10 @@ class Step:
|
|
|
349
352
|
tasks = [el.send(for_id=self.id) for el in self.elements]
|
|
350
353
|
await asyncio.gather(*tasks)
|
|
351
354
|
|
|
352
|
-
if config.ui.hide_cot and
|
|
355
|
+
if config.ui.hide_cot and self.type not in [
|
|
356
|
+
"user_message",
|
|
357
|
+
"assistant_message",
|
|
358
|
+
]:
|
|
353
359
|
return self
|
|
354
360
|
|
|
355
361
|
await context.emitter.send_step(step_dict)
|
|
@@ -374,7 +380,10 @@ class Step:
|
|
|
374
380
|
|
|
375
381
|
assert self.id
|
|
376
382
|
|
|
377
|
-
if config.ui.hide_cot and
|
|
383
|
+
if config.ui.hide_cot and self.type not in [
|
|
384
|
+
"user_message",
|
|
385
|
+
"assistant_message",
|
|
386
|
+
]:
|
|
378
387
|
return
|
|
379
388
|
|
|
380
389
|
if not self.streaming:
|
chainlit/teams/app.py
ADDED
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import base64
|
|
3
|
+
import mimetypes
|
|
4
|
+
import os
|
|
5
|
+
import uuid
|
|
6
|
+
from typing import TYPE_CHECKING, Dict, List, Literal, Optional, Union
|
|
7
|
+
|
|
8
|
+
import filetype
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from botbuilder.core import TurnContext
|
|
12
|
+
from botbuilder.schema import Activity
|
|
13
|
+
|
|
14
|
+
import httpx
|
|
15
|
+
from botbuilder.core import (
|
|
16
|
+
BotFrameworkAdapter,
|
|
17
|
+
BotFrameworkAdapterSettings,
|
|
18
|
+
MessageFactory,
|
|
19
|
+
TurnContext,
|
|
20
|
+
)
|
|
21
|
+
from botbuilder.schema import (
|
|
22
|
+
ActionTypes,
|
|
23
|
+
Activity,
|
|
24
|
+
ActivityTypes,
|
|
25
|
+
Attachment,
|
|
26
|
+
CardAction,
|
|
27
|
+
ChannelAccount,
|
|
28
|
+
HeroCard,
|
|
29
|
+
)
|
|
30
|
+
from chainlit.config import config
|
|
31
|
+
from chainlit.context import ChainlitContext, HTTPSession, context_var
|
|
32
|
+
from chainlit.data import get_data_layer
|
|
33
|
+
from chainlit.element import Element, ElementDict
|
|
34
|
+
from chainlit.emitter import BaseChainlitEmitter
|
|
35
|
+
from chainlit.logger import logger
|
|
36
|
+
from chainlit.message import Message, StepDict
|
|
37
|
+
from chainlit.telemetry import trace
|
|
38
|
+
from chainlit.types import Feedback
|
|
39
|
+
from chainlit.user import PersistedUser, User
|
|
40
|
+
from chainlit.user_session import user_session
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class TeamsEmitter(BaseChainlitEmitter):
|
|
44
|
+
def __init__(self, session: HTTPSession, turn_context: TurnContext, enabled=False):
|
|
45
|
+
super().__init__(session)
|
|
46
|
+
self.turn_context = turn_context
|
|
47
|
+
self.enabled = enabled
|
|
48
|
+
|
|
49
|
+
async def send_element(self, element_dict: ElementDict):
|
|
50
|
+
if not self.enabled or element_dict.get("display") != "inline":
|
|
51
|
+
return
|
|
52
|
+
|
|
53
|
+
persisted_file = self.session.files.get(element_dict.get("chainlitKey") or "")
|
|
54
|
+
attachment: Optional[Attachment] = None
|
|
55
|
+
mime: Optional[str] = None
|
|
56
|
+
|
|
57
|
+
element_name: str = element_dict.get("name", "Untitled")
|
|
58
|
+
|
|
59
|
+
if mime:
|
|
60
|
+
file_extension = mimetypes.guess_extension(mime)
|
|
61
|
+
if file_extension:
|
|
62
|
+
element_name += file_extension
|
|
63
|
+
|
|
64
|
+
if persisted_file:
|
|
65
|
+
mime = element_dict.get("mime")
|
|
66
|
+
with open(persisted_file["path"], "rb") as file:
|
|
67
|
+
dencoded_string = base64.b64encode(file.read()).decode()
|
|
68
|
+
content_url = f"data:{mime};base64,{dencoded_string}"
|
|
69
|
+
attachment = Attachment(
|
|
70
|
+
content_type=mime, content_url=content_url, name=element_name
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
elif url := element_dict.get("url"):
|
|
74
|
+
attachment = Attachment(
|
|
75
|
+
content_type=mime, content_url=url, name=element_name
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
if not attachment:
|
|
79
|
+
return
|
|
80
|
+
|
|
81
|
+
await self.turn_context.send_activity(Activity(attachments=[attachment]))
|
|
82
|
+
|
|
83
|
+
async def send_step(self, step_dict: StepDict):
|
|
84
|
+
if not self.enabled:
|
|
85
|
+
return
|
|
86
|
+
|
|
87
|
+
step_type = step_dict.get("type")
|
|
88
|
+
is_message = step_type in [
|
|
89
|
+
"user_message",
|
|
90
|
+
"assistant_message",
|
|
91
|
+
]
|
|
92
|
+
is_chain_of_thought = bool(step_dict.get("parentId"))
|
|
93
|
+
is_empty_output = not step_dict.get("output")
|
|
94
|
+
|
|
95
|
+
if is_chain_of_thought or is_empty_output or not is_message:
|
|
96
|
+
return
|
|
97
|
+
else:
|
|
98
|
+
reply = MessageFactory.text(step_dict["output"])
|
|
99
|
+
enable_feedback = not step_dict.get("disableFeedback") and get_data_layer()
|
|
100
|
+
if enable_feedback:
|
|
101
|
+
like_button = CardAction(
|
|
102
|
+
type=ActionTypes.message_back,
|
|
103
|
+
title="👍",
|
|
104
|
+
text="like",
|
|
105
|
+
value={"feedback": "like", "step_id": step_dict["id"]},
|
|
106
|
+
)
|
|
107
|
+
dislike_button = CardAction(
|
|
108
|
+
type=ActionTypes.message_back,
|
|
109
|
+
title="👎",
|
|
110
|
+
text="dislike",
|
|
111
|
+
value={"feedback": "dislike", "step_id": step_dict["id"]},
|
|
112
|
+
)
|
|
113
|
+
card = HeroCard(buttons=[like_button, dislike_button])
|
|
114
|
+
attachment = Attachment(
|
|
115
|
+
content_type="application/vnd.microsoft.card.hero", content=card
|
|
116
|
+
)
|
|
117
|
+
reply.attachments = [attachment]
|
|
118
|
+
|
|
119
|
+
await self.turn_context.send_activity(reply)
|
|
120
|
+
|
|
121
|
+
async def update_step(self, step_dict: StepDict):
|
|
122
|
+
if not self.enabled:
|
|
123
|
+
return
|
|
124
|
+
|
|
125
|
+
await self.send_step(step_dict)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
adapter_settings = BotFrameworkAdapterSettings(
|
|
129
|
+
app_id=os.environ.get("TEAMS_APP_ID"),
|
|
130
|
+
app_password=os.environ.get("TEAMS_APP_PASSWORD"),
|
|
131
|
+
)
|
|
132
|
+
adapter = BotFrameworkAdapter(adapter_settings)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@trace
|
|
136
|
+
def init_teams_context(
|
|
137
|
+
session: HTTPSession,
|
|
138
|
+
turn_context: TurnContext,
|
|
139
|
+
) -> ChainlitContext:
|
|
140
|
+
emitter = TeamsEmitter(session=session, turn_context=turn_context)
|
|
141
|
+
context = ChainlitContext(session=session, emitter=emitter)
|
|
142
|
+
context_var.set(context)
|
|
143
|
+
user_session.set("teams_turn_context", turn_context)
|
|
144
|
+
return context
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
users_by_teams_id: Dict[str, Union[User, PersistedUser]] = {}
|
|
148
|
+
|
|
149
|
+
USER_PREFIX = "teams_"
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
async def get_user(teams_user: ChannelAccount):
|
|
153
|
+
if teams_user.id in users_by_teams_id:
|
|
154
|
+
return users_by_teams_id[teams_user.id]
|
|
155
|
+
|
|
156
|
+
metadata = {
|
|
157
|
+
"name": teams_user.name,
|
|
158
|
+
"id": teams_user.id,
|
|
159
|
+
}
|
|
160
|
+
user = User(identifier=USER_PREFIX + str(teams_user.name), metadata=metadata)
|
|
161
|
+
|
|
162
|
+
users_by_teams_id[teams_user.id] = user
|
|
163
|
+
|
|
164
|
+
if data_layer := get_data_layer():
|
|
165
|
+
try:
|
|
166
|
+
persisted_user = await data_layer.create_user(user)
|
|
167
|
+
if persisted_user:
|
|
168
|
+
users_by_teams_id[teams_user.id] = persisted_user
|
|
169
|
+
except Exception as e:
|
|
170
|
+
logger.error(f"Error creating user: {e}")
|
|
171
|
+
|
|
172
|
+
return users_by_teams_id[teams_user.id]
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
async def download_teams_file(url: str):
|
|
176
|
+
async with httpx.AsyncClient() as client:
|
|
177
|
+
response = await client.get(url)
|
|
178
|
+
if response.status_code == 200:
|
|
179
|
+
return response.content
|
|
180
|
+
else:
|
|
181
|
+
return None
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
async def download_teams_files(
|
|
185
|
+
session: HTTPSession, attachments: Optional[List[Attachment]] = None
|
|
186
|
+
):
|
|
187
|
+
if not attachments:
|
|
188
|
+
return []
|
|
189
|
+
|
|
190
|
+
attachments = [
|
|
191
|
+
attachment for attachment in attachments if isinstance(attachment.content, dict)
|
|
192
|
+
]
|
|
193
|
+
download_coros = [
|
|
194
|
+
download_teams_file(attachment.content.get("downloadUrl"))
|
|
195
|
+
for attachment in attachments
|
|
196
|
+
]
|
|
197
|
+
file_bytes_list = await asyncio.gather(*download_coros)
|
|
198
|
+
file_refs = []
|
|
199
|
+
for idx, file_bytes in enumerate(file_bytes_list):
|
|
200
|
+
if file_bytes:
|
|
201
|
+
name = attachments[idx].name
|
|
202
|
+
mime_type = filetype.guess_mime(file_bytes) or "application/octet-stream"
|
|
203
|
+
file_ref = await session.persist_file(
|
|
204
|
+
name=name, mime=mime_type, content=file_bytes
|
|
205
|
+
)
|
|
206
|
+
file_refs.append(file_ref)
|
|
207
|
+
|
|
208
|
+
files_dicts = [
|
|
209
|
+
session.files[file["id"]] for file in file_refs if file["id"] in session.files
|
|
210
|
+
]
|
|
211
|
+
|
|
212
|
+
file_elements = [Element.from_dict(file_dict) for file_dict in files_dicts]
|
|
213
|
+
|
|
214
|
+
return file_elements
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def clean_content(activity: Activity):
|
|
218
|
+
return activity.text.strip()
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
async def process_teams_message(
|
|
222
|
+
turn_context: TurnContext,
|
|
223
|
+
thread_name: str,
|
|
224
|
+
):
|
|
225
|
+
user = await get_user(turn_context.activity.from_property)
|
|
226
|
+
|
|
227
|
+
thread_id = str(
|
|
228
|
+
uuid.uuid5(uuid.NAMESPACE_DNS, str(turn_context.activity.conversation.id))
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
text = clean_content(turn_context.activity)
|
|
232
|
+
teams_files = turn_context.activity.attachments
|
|
233
|
+
|
|
234
|
+
session_id = str(uuid.uuid4())
|
|
235
|
+
|
|
236
|
+
session = HTTPSession(
|
|
237
|
+
id=session_id,
|
|
238
|
+
thread_id=thread_id,
|
|
239
|
+
user=user,
|
|
240
|
+
client_type="teams",
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
ctx = init_teams_context(
|
|
244
|
+
session=session,
|
|
245
|
+
turn_context=turn_context,
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
file_elements = await download_teams_files(session, teams_files)
|
|
249
|
+
|
|
250
|
+
msg = Message(
|
|
251
|
+
content=text,
|
|
252
|
+
elements=file_elements,
|
|
253
|
+
type="user_message",
|
|
254
|
+
author=user.metadata.get("name"),
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
await msg.send()
|
|
258
|
+
|
|
259
|
+
ctx.emitter.enabled = True
|
|
260
|
+
|
|
261
|
+
if on_chat_start := config.code.on_chat_start:
|
|
262
|
+
await on_chat_start()
|
|
263
|
+
|
|
264
|
+
if on_message := config.code.on_message:
|
|
265
|
+
await on_message(msg)
|
|
266
|
+
|
|
267
|
+
if on_chat_end := config.code.on_chat_end:
|
|
268
|
+
await on_chat_end()
|
|
269
|
+
|
|
270
|
+
if data_layer := get_data_layer():
|
|
271
|
+
if isinstance(user, PersistedUser):
|
|
272
|
+
try:
|
|
273
|
+
await data_layer.update_thread(
|
|
274
|
+
thread_id=thread_id,
|
|
275
|
+
name=thread_name,
|
|
276
|
+
metadata=ctx.session.to_persistable(),
|
|
277
|
+
user_id=user.id,
|
|
278
|
+
)
|
|
279
|
+
except Exception as e:
|
|
280
|
+
logger.error(f"Error updating thread: {e}")
|
|
281
|
+
|
|
282
|
+
ctx.session.delete()
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
async def handle_message(turn_context: TurnContext):
|
|
286
|
+
if turn_context.activity.type == ActivityTypes.message:
|
|
287
|
+
if (
|
|
288
|
+
turn_context.activity.text == "like"
|
|
289
|
+
or turn_context.activity.text == "dislike"
|
|
290
|
+
):
|
|
291
|
+
feedback_value: Literal[0, 1] = (
|
|
292
|
+
0 if turn_context.activity.text == "dislike" else 1
|
|
293
|
+
)
|
|
294
|
+
step_id = turn_context.activity.value.get("step_id")
|
|
295
|
+
if data_layer := get_data_layer():
|
|
296
|
+
await data_layer.upsert_feedback(
|
|
297
|
+
Feedback(forId=step_id, value=feedback_value)
|
|
298
|
+
)
|
|
299
|
+
updated_text = "👍" if turn_context.activity.text == "like" else "👎"
|
|
300
|
+
# Update the existing message to remove the buttons
|
|
301
|
+
updated_message = Activity(
|
|
302
|
+
type=ActivityTypes.message,
|
|
303
|
+
id=turn_context.activity.reply_to_id,
|
|
304
|
+
text=updated_text,
|
|
305
|
+
attachments=[],
|
|
306
|
+
)
|
|
307
|
+
await turn_context.update_activity(updated_message)
|
|
308
|
+
else:
|
|
309
|
+
# Send typing activity
|
|
310
|
+
typing_activity = Activity(
|
|
311
|
+
type=ActivityTypes.typing,
|
|
312
|
+
from_property=turn_context.activity.recipient,
|
|
313
|
+
recipient=turn_context.activity.from_property,
|
|
314
|
+
conversation=turn_context.activity.conversation,
|
|
315
|
+
)
|
|
316
|
+
await turn_context.send_activity(typing_activity)
|
|
317
|
+
thread_name = f"{turn_context.activity.from_property.name} Teams DM"
|
|
318
|
+
await process_teams_message(turn_context, thread_name)
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
async def on_turn(turn_context: TurnContext):
|
|
322
|
+
await handle_message(turn_context)
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
# Create the main bot class
|
|
326
|
+
class TeamsBot:
|
|
327
|
+
async def on_turn(self, turn_context: TurnContext):
|
|
328
|
+
await on_turn(turn_context)
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
# Create the bot instance
|
|
332
|
+
bot = TeamsBot()
|
chainlit/translations/en-US.json
CHANGED
chainlit/types.py
CHANGED
|
@@ -150,11 +150,16 @@ class FileDict(TypedDict):
|
|
|
150
150
|
type: str
|
|
151
151
|
|
|
152
152
|
|
|
153
|
-
class
|
|
153
|
+
class UserMessagePayload(TypedDict):
|
|
154
154
|
message: "StepDict"
|
|
155
155
|
fileReferences: Optional[List[FileReference]]
|
|
156
156
|
|
|
157
157
|
|
|
158
|
+
class SystemMessagePayload(TypedDict):
|
|
159
|
+
content: str
|
|
160
|
+
metadata: Optional[Dict[str, Any]]
|
|
161
|
+
|
|
162
|
+
|
|
158
163
|
class AudioChunkPayload(TypedDict):
|
|
159
164
|
isStart: bool
|
|
160
165
|
mimeType: str
|
|
@@ -193,21 +198,6 @@ class AskActionResponse(TypedDict):
|
|
|
193
198
|
collapsed: bool
|
|
194
199
|
|
|
195
200
|
|
|
196
|
-
class GenerationRequest(BaseModel):
|
|
197
|
-
chatGeneration: Optional[ChatGeneration] = None
|
|
198
|
-
completionGeneration: Optional[CompletionGeneration] = None
|
|
199
|
-
userEnv: Dict[str, str]
|
|
200
|
-
|
|
201
|
-
@property
|
|
202
|
-
def generation(self):
|
|
203
|
-
if self.chatGeneration:
|
|
204
|
-
return self.chatGeneration
|
|
205
|
-
return self.completionGeneration
|
|
206
|
-
|
|
207
|
-
def is_chat(self):
|
|
208
|
-
return self.chatGeneration is not None
|
|
209
|
-
|
|
210
|
-
|
|
211
201
|
class DeleteThreadRequest(BaseModel):
|
|
212
202
|
threadId: str
|
|
213
203
|
|
|
@@ -259,8 +249,8 @@ class FeedbackDict(TypedDict):
|
|
|
259
249
|
@dataclass
|
|
260
250
|
class Feedback:
|
|
261
251
|
forId: str
|
|
262
|
-
threadId: Optional[str]
|
|
263
252
|
value: Literal[0, 1]
|
|
253
|
+
threadId: Optional[str] = None
|
|
264
254
|
id: Optional[str] = None
|
|
265
255
|
comment: Optional[str] = None
|
|
266
256
|
|
chainlit/user.py
CHANGED
|
@@ -4,7 +4,15 @@ from dataclasses_json import DataClassJsonMixin
|
|
|
4
4
|
from pydantic.dataclasses import Field, dataclass
|
|
5
5
|
|
|
6
6
|
Provider = Literal[
|
|
7
|
-
"credentials",
|
|
7
|
+
"credentials",
|
|
8
|
+
"header",
|
|
9
|
+
"github",
|
|
10
|
+
"google",
|
|
11
|
+
"azure-ad",
|
|
12
|
+
"azure-ad-hybrid",
|
|
13
|
+
"okta",
|
|
14
|
+
"auth0",
|
|
15
|
+
"descope",
|
|
8
16
|
]
|
|
9
17
|
|
|
10
18
|
|
chainlit/utils.py
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import functools
|
|
2
2
|
import importlib
|
|
3
3
|
import inspect
|
|
4
|
+
import os
|
|
4
5
|
from asyncio import CancelledError
|
|
5
6
|
from typing import Callable
|
|
6
7
|
|
|
8
|
+
import click
|
|
9
|
+
from chainlit.auth import ensure_jwt_secret
|
|
7
10
|
from chainlit.context import context
|
|
8
11
|
from chainlit.logger import logger
|
|
9
12
|
from chainlit.message import ErrorMessage
|
|
13
|
+
from fastapi import FastAPI
|
|
10
14
|
from packaging import version
|
|
11
15
|
|
|
12
16
|
|
|
@@ -87,3 +91,41 @@ def check_module_version(name, required_version):
|
|
|
87
91
|
except ModuleNotFoundError:
|
|
88
92
|
return False
|
|
89
93
|
return version.parse(module.__version__) >= version.parse(required_version)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def check_file(target: str):
|
|
97
|
+
# Define accepted file extensions for Chainlit
|
|
98
|
+
ACCEPTED_FILE_EXTENSIONS = ("py", "py3")
|
|
99
|
+
|
|
100
|
+
_, extension = os.path.splitext(target)
|
|
101
|
+
|
|
102
|
+
# Check file extension
|
|
103
|
+
if extension[1:] not in ACCEPTED_FILE_EXTENSIONS:
|
|
104
|
+
if extension[1:] == "":
|
|
105
|
+
raise click.BadArgumentUsage(
|
|
106
|
+
"Chainlit requires raw Python (.py) files, but the provided file has no extension."
|
|
107
|
+
)
|
|
108
|
+
else:
|
|
109
|
+
raise click.BadArgumentUsage(
|
|
110
|
+
f"Chainlit requires raw Python (.py) files, not {extension}."
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
if not os.path.exists(target):
|
|
114
|
+
raise click.BadParameter(f"File does not exist: {target}")
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def mount_chainlit(app: FastAPI, target: str, path="/chainlit"):
|
|
118
|
+
os.environ["CHAINLIT_ROOT_PATH"] = path
|
|
119
|
+
from chainlit.config import config, load_module
|
|
120
|
+
from chainlit.server import combined_asgi_app as chainlit_app
|
|
121
|
+
|
|
122
|
+
config.run.root_path = path
|
|
123
|
+
|
|
124
|
+
check_file(target)
|
|
125
|
+
# Load the module provided by the user
|
|
126
|
+
config.run.module_name = target
|
|
127
|
+
load_module(config.run.module_name)
|
|
128
|
+
|
|
129
|
+
ensure_jwt_secret()
|
|
130
|
+
|
|
131
|
+
app.mount("/", chainlit_app)
|