chainlit 1.1.0rc1__py3-none-any.whl → 1.1.200__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/config.py +10 -2
- chainlit/context.py +19 -7
- chainlit/copilot/dist/index.js +648 -534
- chainlit/data/acl.py +4 -1
- chainlit/data/sql_alchemy.py +22 -21
- chainlit/discord/__init__.py +6 -0
- chainlit/discord/app.py +331 -0
- chainlit/element.py +5 -2
- chainlit/emitter.py +11 -2
- chainlit/frontend/dist/assets/{index-032fca02.js → index-d9bad4f1.js} +131 -135
- chainlit/frontend/dist/assets/react-plotly-48cc0858.js +3602 -0
- chainlit/frontend/dist/index.html +1 -1
- chainlit/message.py +2 -2
- chainlit/server.py +30 -5
- chainlit/session.py +72 -61
- chainlit/slack/__init__.py +6 -0
- chainlit/slack/app.py +390 -0
- chainlit/socket.py +21 -15
- chainlit/step.py +36 -13
- chainlit/types.py +3 -1
- chainlit/user_session.py +5 -2
- {chainlit-1.1.0rc1.dist-info → chainlit-1.1.200.dist-info}/METADATA +4 -3
- {chainlit-1.1.0rc1.dist-info → chainlit-1.1.200.dist-info}/RECORD +25 -21
- chainlit/frontend/dist/assets/react-plotly-8c993614.js +0 -3484
- {chainlit-1.1.0rc1.dist-info → chainlit-1.1.200.dist-info}/WHEEL +0 -0
- {chainlit-1.1.0rc1.dist-info → chainlit-1.1.200.dist-info}/entry_points.txt +0 -0
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
<script>
|
|
23
23
|
const global = globalThis;
|
|
24
24
|
</script>
|
|
25
|
-
<script type="module" crossorigin src="/assets/index-
|
|
25
|
+
<script type="module" crossorigin src="/assets/index-d9bad4f1.js"></script>
|
|
26
26
|
<link rel="stylesheet" href="/assets/index-d088547c.css">
|
|
27
27
|
</head>
|
|
28
28
|
<body>
|
chainlit/message.py
CHANGED
|
@@ -206,7 +206,7 @@ class Message(MessageBase):
|
|
|
206
206
|
def __init__(
|
|
207
207
|
self,
|
|
208
208
|
content: Union[str, Dict],
|
|
209
|
-
author: str =
|
|
209
|
+
author: Optional[str] = None,
|
|
210
210
|
language: Optional[str] = None,
|
|
211
211
|
actions: Optional[List[Action]] = None,
|
|
212
212
|
elements: Optional[List[ElementBased]] = None,
|
|
@@ -243,7 +243,7 @@ class Message(MessageBase):
|
|
|
243
243
|
self.metadata = metadata
|
|
244
244
|
self.tags = tags
|
|
245
245
|
|
|
246
|
-
self.author = author
|
|
246
|
+
self.author = author or config.ui.name
|
|
247
247
|
self.type = type
|
|
248
248
|
self.actions = actions if actions is not None else []
|
|
249
249
|
self.elements = elements if elements is not None else []
|
chainlit/server.py
CHANGED
|
@@ -119,16 +119,27 @@ async def lifespan(app: FastAPI):
|
|
|
119
119
|
|
|
120
120
|
watch_task = asyncio.create_task(watch_files_for_changes())
|
|
121
121
|
|
|
122
|
+
discord_task = None
|
|
123
|
+
|
|
124
|
+
if discord_bot_token := os.environ.get("DISCORD_BOT_TOKEN"):
|
|
125
|
+
from chainlit.discord.app import client
|
|
126
|
+
|
|
127
|
+
discord_task = asyncio.create_task(client.start(discord_bot_token))
|
|
128
|
+
|
|
122
129
|
try:
|
|
123
130
|
yield
|
|
124
131
|
finally:
|
|
125
|
-
|
|
126
|
-
|
|
132
|
+
try:
|
|
133
|
+
if watch_task:
|
|
127
134
|
stop_event.set()
|
|
128
135
|
watch_task.cancel()
|
|
129
136
|
await watch_task
|
|
130
|
-
|
|
131
|
-
|
|
137
|
+
|
|
138
|
+
if discord_task:
|
|
139
|
+
discord_task.cancel()
|
|
140
|
+
await discord_task
|
|
141
|
+
except asyncio.exceptions.CancelledError:
|
|
142
|
+
pass
|
|
132
143
|
|
|
133
144
|
if FILES_DIRECTORY.is_dir():
|
|
134
145
|
shutil.rmtree(FILES_DIRECTORY)
|
|
@@ -195,6 +206,18 @@ socket = SocketManager(
|
|
|
195
206
|
)
|
|
196
207
|
|
|
197
208
|
|
|
209
|
+
# -------------------------------------------------------------------------------
|
|
210
|
+
# SLACK HANDLER
|
|
211
|
+
# -------------------------------------------------------------------------------
|
|
212
|
+
|
|
213
|
+
if os.environ.get("SLACK_BOT_TOKEN") and os.environ.get("SLACK_SIGNING_SECRET"):
|
|
214
|
+
from chainlit.slack.app import slack_app_handler
|
|
215
|
+
|
|
216
|
+
@app.post("/slack/events")
|
|
217
|
+
async def endpoint(req: Request):
|
|
218
|
+
return await slack_app_handler.handle(req)
|
|
219
|
+
|
|
220
|
+
|
|
198
221
|
# -------------------------------------------------------------------------------
|
|
199
222
|
# HTTP HANDLERS
|
|
200
223
|
# -------------------------------------------------------------------------------
|
|
@@ -211,14 +234,16 @@ def get_html_template():
|
|
|
211
234
|
CSS_PLACEHOLDER = "<!-- CSS INJECTION PLACEHOLDER -->"
|
|
212
235
|
|
|
213
236
|
default_url = "https://github.com/Chainlit/chainlit"
|
|
237
|
+
default_meta_image_url = "https://chainlit-cloud.s3.eu-west-3.amazonaws.com/logo/chainlit_banner.png"
|
|
214
238
|
url = config.ui.github or default_url
|
|
239
|
+
meta_image_url = config.ui.custom_meta_image_url or default_meta_image_url
|
|
215
240
|
|
|
216
241
|
tags = f"""<title>{config.ui.name}</title>
|
|
217
242
|
<meta name="description" content="{config.ui.description}">
|
|
218
243
|
<meta property="og:type" content="website">
|
|
219
244
|
<meta property="og:title" content="{config.ui.name}">
|
|
220
245
|
<meta property="og:description" content="{config.ui.description}">
|
|
221
|
-
<meta property="og:image" content="
|
|
246
|
+
<meta property="og:image" content="{meta_image_url}">
|
|
222
247
|
<meta property="og:url" content="{url}">"""
|
|
223
248
|
|
|
224
249
|
js = f"""<script>{f"window.theme = {json.dumps(config.ui.theme.to_dict())}; " if config.ui.theme else ""}</script>"""
|
chainlit/session.py
CHANGED
|
@@ -24,7 +24,7 @@ if TYPE_CHECKING:
|
|
|
24
24
|
from chainlit.types import FileDict, FileReference
|
|
25
25
|
from chainlit.user import PersistedUser, User
|
|
26
26
|
|
|
27
|
-
ClientType = Literal["
|
|
27
|
+
ClientType = Literal["webapp", "copilot", "teams", "slack", "discord"]
|
|
28
28
|
|
|
29
29
|
|
|
30
30
|
class JSONEncoderIgnoreNonSerializable(json.JSONEncoder):
|
|
@@ -35,11 +35,21 @@ class JSONEncoderIgnoreNonSerializable(json.JSONEncoder):
|
|
|
35
35
|
return None
|
|
36
36
|
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
|
|
39
|
+
def clean_metadata(metadata: Dict, max_size: int = 1048576):
|
|
40
|
+
cleaned_metadata = json.loads(
|
|
40
41
|
json.dumps(metadata, cls=JSONEncoderIgnoreNonSerializable, ensure_ascii=False)
|
|
41
42
|
)
|
|
42
43
|
|
|
44
|
+
metadata_size = len(json.dumps(cleaned_metadata).encode('utf-8'))
|
|
45
|
+
if metadata_size > max_size:
|
|
46
|
+
# Redact the metadata if it exceeds the maximum size
|
|
47
|
+
cleaned_metadata = {
|
|
48
|
+
'message': f'Metadata size exceeds the limit of {max_size} bytes. Redacted.'
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return cleaned_metadata
|
|
52
|
+
|
|
43
53
|
|
|
44
54
|
class BaseSession:
|
|
45
55
|
"""Base object."""
|
|
@@ -80,18 +90,66 @@ class BaseSession:
|
|
|
80
90
|
self.chat_profile = chat_profile
|
|
81
91
|
self.http_referer = http_referer
|
|
82
92
|
|
|
93
|
+
self.files = {} # type: Dict[str, "FileDict"]
|
|
94
|
+
|
|
83
95
|
self.id = id
|
|
84
96
|
|
|
85
97
|
self.chat_settings: Dict[str, Any] = {}
|
|
86
98
|
|
|
99
|
+
@property
|
|
100
|
+
def files_dir(self):
|
|
101
|
+
from chainlit.config import FILES_DIRECTORY
|
|
102
|
+
|
|
103
|
+
return FILES_DIRECTORY / self.id
|
|
104
|
+
|
|
87
105
|
async def persist_file(
|
|
88
106
|
self,
|
|
89
107
|
name: str,
|
|
90
108
|
mime: str,
|
|
91
109
|
path: Optional[str] = None,
|
|
92
110
|
content: Optional[Union[bytes, str]] = None,
|
|
93
|
-
):
|
|
94
|
-
|
|
111
|
+
) -> "FileReference":
|
|
112
|
+
if not path and not content:
|
|
113
|
+
raise ValueError(
|
|
114
|
+
"Either path or content must be provided to persist a file"
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
self.files_dir.mkdir(exist_ok=True)
|
|
118
|
+
|
|
119
|
+
file_id = str(uuid.uuid4())
|
|
120
|
+
|
|
121
|
+
file_path = self.files_dir / file_id
|
|
122
|
+
|
|
123
|
+
file_extension = mimetypes.guess_extension(mime)
|
|
124
|
+
|
|
125
|
+
if file_extension:
|
|
126
|
+
file_path = file_path.with_suffix(file_extension)
|
|
127
|
+
|
|
128
|
+
if path:
|
|
129
|
+
# Copy the file from the given path
|
|
130
|
+
async with aiofiles.open(path, "rb") as src, aiofiles.open(
|
|
131
|
+
file_path, "wb"
|
|
132
|
+
) as dst:
|
|
133
|
+
await dst.write(await src.read())
|
|
134
|
+
elif content:
|
|
135
|
+
# Write the provided content to the file
|
|
136
|
+
async with aiofiles.open(file_path, "wb") as buffer:
|
|
137
|
+
if isinstance(content, str):
|
|
138
|
+
content = content.encode("utf-8")
|
|
139
|
+
await buffer.write(content)
|
|
140
|
+
|
|
141
|
+
# Get the file size
|
|
142
|
+
file_size = file_path.stat().st_size
|
|
143
|
+
# Store the file content in memory
|
|
144
|
+
self.files[file_id] = {
|
|
145
|
+
"id": file_id,
|
|
146
|
+
"path": file_path,
|
|
147
|
+
"name": name,
|
|
148
|
+
"type": mime,
|
|
149
|
+
"size": file_size,
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return {"id": file_id}
|
|
95
153
|
|
|
96
154
|
def to_persistable(self) -> Dict:
|
|
97
155
|
from chainlit.user_session import user_sessions
|
|
@@ -99,6 +157,8 @@ class BaseSession:
|
|
|
99
157
|
user_session = user_sessions.get(self.id) or {} # type: Dict
|
|
100
158
|
user_session["chat_settings"] = self.chat_settings
|
|
101
159
|
user_session["chat_profile"] = self.chat_profile
|
|
160
|
+
user_session["http_referer"] = self.http_referer
|
|
161
|
+
user_session["client_type"] = self.client_type
|
|
102
162
|
metadata = clean_metadata(user_session)
|
|
103
163
|
return metadata
|
|
104
164
|
|
|
@@ -134,6 +194,11 @@ class HTTPSession(BaseSession):
|
|
|
134
194
|
http_referer=http_referer,
|
|
135
195
|
)
|
|
136
196
|
|
|
197
|
+
def delete(self):
|
|
198
|
+
"""Delete the session."""
|
|
199
|
+
if self.files_dir.is_dir():
|
|
200
|
+
shutil.rmtree(self.files_dir)
|
|
201
|
+
|
|
137
202
|
|
|
138
203
|
class WebsocketSession(BaseSession):
|
|
139
204
|
"""Internal web socket session object.
|
|
@@ -147,6 +212,8 @@ class WebsocketSession(BaseSession):
|
|
|
147
212
|
socket id for convenience.
|
|
148
213
|
"""
|
|
149
214
|
|
|
215
|
+
to_clear: bool = False
|
|
216
|
+
|
|
150
217
|
def __init__(
|
|
151
218
|
self,
|
|
152
219
|
# Id from the session cookie
|
|
@@ -194,68 +261,12 @@ class WebsocketSession(BaseSession):
|
|
|
194
261
|
self.restored = False
|
|
195
262
|
|
|
196
263
|
self.thread_queues = {} # type: Dict[str, Deque[Callable]]
|
|
197
|
-
self.files = {} # type: Dict[str, "FileDict"]
|
|
198
264
|
|
|
199
265
|
ws_sessions_id[self.id] = self
|
|
200
266
|
ws_sessions_sid[socket_id] = self
|
|
201
267
|
|
|
202
268
|
self.languages = languages
|
|
203
269
|
|
|
204
|
-
@property
|
|
205
|
-
def files_dir(self):
|
|
206
|
-
from chainlit.config import FILES_DIRECTORY
|
|
207
|
-
|
|
208
|
-
return FILES_DIRECTORY / self.id
|
|
209
|
-
|
|
210
|
-
async def persist_file(
|
|
211
|
-
self,
|
|
212
|
-
name: str,
|
|
213
|
-
mime: str,
|
|
214
|
-
path: Optional[str] = None,
|
|
215
|
-
content: Optional[Union[bytes, str]] = None,
|
|
216
|
-
) -> "FileReference":
|
|
217
|
-
if not path and not content:
|
|
218
|
-
raise ValueError(
|
|
219
|
-
"Either path or content must be provided to persist a file"
|
|
220
|
-
)
|
|
221
|
-
|
|
222
|
-
self.files_dir.mkdir(exist_ok=True)
|
|
223
|
-
|
|
224
|
-
file_id = str(uuid.uuid4())
|
|
225
|
-
|
|
226
|
-
file_path = self.files_dir / file_id
|
|
227
|
-
|
|
228
|
-
file_extension = mimetypes.guess_extension(mime)
|
|
229
|
-
|
|
230
|
-
if file_extension:
|
|
231
|
-
file_path = file_path.with_suffix(file_extension)
|
|
232
|
-
|
|
233
|
-
if path:
|
|
234
|
-
# Copy the file from the given path
|
|
235
|
-
async with aiofiles.open(path, "rb") as src, aiofiles.open(
|
|
236
|
-
file_path, "wb"
|
|
237
|
-
) as dst:
|
|
238
|
-
await dst.write(await src.read())
|
|
239
|
-
elif content:
|
|
240
|
-
# Write the provided content to the file
|
|
241
|
-
async with aiofiles.open(file_path, "wb") as buffer:
|
|
242
|
-
if isinstance(content, str):
|
|
243
|
-
content = content.encode("utf-8")
|
|
244
|
-
await buffer.write(content)
|
|
245
|
-
|
|
246
|
-
# Get the file size
|
|
247
|
-
file_size = file_path.stat().st_size
|
|
248
|
-
# Store the file content in memory
|
|
249
|
-
self.files[file_id] = {
|
|
250
|
-
"id": file_id,
|
|
251
|
-
"path": file_path,
|
|
252
|
-
"name": name,
|
|
253
|
-
"type": mime,
|
|
254
|
-
"size": file_size,
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
return {"id": file_id}
|
|
258
|
-
|
|
259
270
|
def restore(self, new_socket_id: str):
|
|
260
271
|
"""Associate a new socket id to the session."""
|
|
261
272
|
ws_sessions_sid.pop(self.socket_id, None)
|
chainlit/slack/app.py
ADDED
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import os
|
|
3
|
+
import re
|
|
4
|
+
import uuid
|
|
5
|
+
from functools import partial
|
|
6
|
+
from typing import Dict, List, Optional, Union
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
from chainlit.config import config
|
|
10
|
+
from chainlit.context import ChainlitContext, HTTPSession, context_var
|
|
11
|
+
from chainlit.data import get_data_layer
|
|
12
|
+
from chainlit.element import Element, ElementDict
|
|
13
|
+
from chainlit.emitter import BaseChainlitEmitter
|
|
14
|
+
from chainlit.logger import logger
|
|
15
|
+
from chainlit.message import Message, StepDict
|
|
16
|
+
from chainlit.telemetry import trace
|
|
17
|
+
from chainlit.types import Feedback
|
|
18
|
+
from chainlit.user import PersistedUser, User
|
|
19
|
+
from chainlit.user_session import user_session
|
|
20
|
+
from slack_bolt.adapter.fastapi.async_handler import AsyncSlackRequestHandler
|
|
21
|
+
from slack_bolt.async_app import AsyncApp
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class SlackEmitter(BaseChainlitEmitter):
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
session: HTTPSession,
|
|
28
|
+
app: AsyncApp,
|
|
29
|
+
channel_id: str,
|
|
30
|
+
say,
|
|
31
|
+
enabled=False,
|
|
32
|
+
thread_ts: Optional[str] = None,
|
|
33
|
+
):
|
|
34
|
+
super().__init__(session)
|
|
35
|
+
self.app = app
|
|
36
|
+
self.channel_id = channel_id
|
|
37
|
+
self.say = say
|
|
38
|
+
self.enabled = enabled
|
|
39
|
+
self.thread_ts = thread_ts
|
|
40
|
+
|
|
41
|
+
async def send_element(self, element_dict: ElementDict):
|
|
42
|
+
if not self.enabled or element_dict.get("display") != "inline":
|
|
43
|
+
return
|
|
44
|
+
|
|
45
|
+
persisted_file = self.session.files.get(element_dict.get("chainlitKey") or "")
|
|
46
|
+
file: Optional[Union[bytes, str]] = None
|
|
47
|
+
|
|
48
|
+
if persisted_file:
|
|
49
|
+
file = str(persisted_file["path"])
|
|
50
|
+
elif file_url := element_dict.get("url"):
|
|
51
|
+
async with httpx.AsyncClient() as client:
|
|
52
|
+
response = await client.get(file_url)
|
|
53
|
+
if response.status_code == 200:
|
|
54
|
+
file = response.content
|
|
55
|
+
|
|
56
|
+
if not file:
|
|
57
|
+
return
|
|
58
|
+
|
|
59
|
+
await self.app.client.files_upload_v2(
|
|
60
|
+
channel=self.channel_id,
|
|
61
|
+
thread_ts=self.thread_ts,
|
|
62
|
+
file=file,
|
|
63
|
+
title=element_dict.get("name"),
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
async def send_step(self, step_dict: StepDict):
|
|
67
|
+
if not self.enabled:
|
|
68
|
+
return
|
|
69
|
+
|
|
70
|
+
step_type = step_dict.get("type")
|
|
71
|
+
is_message = step_type in [
|
|
72
|
+
"user_message",
|
|
73
|
+
"assistant_message",
|
|
74
|
+
"system_message",
|
|
75
|
+
]
|
|
76
|
+
is_chain_of_thought = bool(step_dict.get("parentId"))
|
|
77
|
+
is_empty_output = not step_dict.get("output")
|
|
78
|
+
|
|
79
|
+
if is_chain_of_thought or is_empty_output or not is_message:
|
|
80
|
+
return
|
|
81
|
+
|
|
82
|
+
enable_feedback = not step_dict.get("disableFeedback") and get_data_layer()
|
|
83
|
+
blocks: List[Dict] = [
|
|
84
|
+
{
|
|
85
|
+
"type": "section",
|
|
86
|
+
"text": {"type": "mrkdwn", "text": step_dict["output"]},
|
|
87
|
+
}
|
|
88
|
+
]
|
|
89
|
+
if enable_feedback:
|
|
90
|
+
blocks.append(
|
|
91
|
+
{
|
|
92
|
+
"type": "actions",
|
|
93
|
+
"elements": [
|
|
94
|
+
{
|
|
95
|
+
"action_id": "thumbdown",
|
|
96
|
+
"type": "button",
|
|
97
|
+
"text": {
|
|
98
|
+
"type": "plain_text",
|
|
99
|
+
"emoji": True,
|
|
100
|
+
"text": ":thumbsdown:",
|
|
101
|
+
},
|
|
102
|
+
"value": step_dict.get("id"),
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
"action_id": "thumbup",
|
|
106
|
+
"type": "button",
|
|
107
|
+
"text": {
|
|
108
|
+
"type": "plain_text",
|
|
109
|
+
"emoji": True,
|
|
110
|
+
"text": ":thumbsup:",
|
|
111
|
+
},
|
|
112
|
+
"value": step_dict.get("id"),
|
|
113
|
+
},
|
|
114
|
+
],
|
|
115
|
+
}
|
|
116
|
+
)
|
|
117
|
+
await self.say(
|
|
118
|
+
text=step_dict["output"], blocks=blocks, thread_ts=self.thread_ts
|
|
119
|
+
)
|
|
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
|
+
slack_app = AsyncApp(
|
|
129
|
+
token=os.environ.get("SLACK_BOT_TOKEN"),
|
|
130
|
+
signing_secret=os.environ.get("SLACK_SIGNING_SECRET"),
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@trace
|
|
135
|
+
def init_slack_context(
|
|
136
|
+
session: HTTPSession,
|
|
137
|
+
slack_channel_id: str,
|
|
138
|
+
event,
|
|
139
|
+
say,
|
|
140
|
+
thread_ts: Optional[str] = None,
|
|
141
|
+
) -> ChainlitContext:
|
|
142
|
+
emitter = SlackEmitter(
|
|
143
|
+
session=session,
|
|
144
|
+
app=slack_app,
|
|
145
|
+
channel_id=slack_channel_id,
|
|
146
|
+
say=say,
|
|
147
|
+
thread_ts=thread_ts,
|
|
148
|
+
)
|
|
149
|
+
context = ChainlitContext(session=session, emitter=emitter)
|
|
150
|
+
context_var.set(context)
|
|
151
|
+
user_session.set("slack_event", event)
|
|
152
|
+
user_session.set(
|
|
153
|
+
"fetch_slack_message_history",
|
|
154
|
+
partial(
|
|
155
|
+
fetch_message_history, channel_id=slack_channel_id, thread_ts=thread_ts
|
|
156
|
+
),
|
|
157
|
+
)
|
|
158
|
+
return context
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
slack_app_handler = AsyncSlackRequestHandler(slack_app)
|
|
162
|
+
|
|
163
|
+
users_by_slack_id: Dict[str, Union[User, PersistedUser]] = {}
|
|
164
|
+
|
|
165
|
+
USER_PREFIX = "slack_"
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def clean_content(message: str):
|
|
169
|
+
cleaned_text = re.sub(r"<@[\w]+>", "", message).strip()
|
|
170
|
+
return cleaned_text
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
async def get_user(slack_user_id: str):
|
|
174
|
+
if slack_user_id in users_by_slack_id:
|
|
175
|
+
return users_by_slack_id[slack_user_id]
|
|
176
|
+
|
|
177
|
+
slack_user = await slack_app.client.users_info(user=slack_user_id)
|
|
178
|
+
slack_user_profile = slack_user["user"]["profile"]
|
|
179
|
+
|
|
180
|
+
user_email = slack_user_profile.get("email")
|
|
181
|
+
user = User(identifier=USER_PREFIX + user_email, metadata=slack_user_profile)
|
|
182
|
+
|
|
183
|
+
users_by_slack_id[slack_user_id] = user
|
|
184
|
+
|
|
185
|
+
if data_layer := get_data_layer():
|
|
186
|
+
try:
|
|
187
|
+
persisted_user = await data_layer.create_user(user)
|
|
188
|
+
if persisted_user:
|
|
189
|
+
users_by_slack_id[slack_user_id] = persisted_user
|
|
190
|
+
except Exception as e:
|
|
191
|
+
logger.error(f"Error creating user: {e}")
|
|
192
|
+
|
|
193
|
+
return users_by_slack_id[slack_user_id]
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
async def fetch_message_history(
|
|
197
|
+
channel_id: str, thread_ts: Optional[str] = None, limit=30
|
|
198
|
+
):
|
|
199
|
+
if not thread_ts:
|
|
200
|
+
result = await slack_app.client.conversations_history(
|
|
201
|
+
channel=channel_id, limit=limit
|
|
202
|
+
)
|
|
203
|
+
else:
|
|
204
|
+
result = await slack_app.client.conversations_replies(
|
|
205
|
+
channel=channel_id, ts=thread_ts, limit=limit
|
|
206
|
+
)
|
|
207
|
+
if result["ok"]:
|
|
208
|
+
messages = result["messages"]
|
|
209
|
+
return messages
|
|
210
|
+
else:
|
|
211
|
+
raise Exception(f"Failed to fetch messages: {result['error']}")
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
async def download_slack_file(url, token):
|
|
215
|
+
headers = {"Authorization": f"Bearer {token}"}
|
|
216
|
+
async with httpx.AsyncClient() as client:
|
|
217
|
+
response = await client.get(url, headers=headers)
|
|
218
|
+
if response.status_code == 200:
|
|
219
|
+
return response.content
|
|
220
|
+
else:
|
|
221
|
+
return None
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
async def download_slack_files(session: HTTPSession, files, token):
|
|
225
|
+
download_coros = [
|
|
226
|
+
download_slack_file(file.get("url_private"), token) for file in files
|
|
227
|
+
]
|
|
228
|
+
file_bytes_list = await asyncio.gather(*download_coros)
|
|
229
|
+
file_refs = []
|
|
230
|
+
for idx, file_bytes in enumerate(file_bytes_list):
|
|
231
|
+
if file_bytes:
|
|
232
|
+
name = files[idx].get("name")
|
|
233
|
+
mime_type = files[idx].get("mimetype")
|
|
234
|
+
file_ref = await session.persist_file(
|
|
235
|
+
name=name, mime=mime_type, content=file_bytes
|
|
236
|
+
)
|
|
237
|
+
file_refs.append(file_ref)
|
|
238
|
+
|
|
239
|
+
files_dicts = [
|
|
240
|
+
session.files[file["id"]] for file in file_refs if file["id"] in session.files
|
|
241
|
+
]
|
|
242
|
+
|
|
243
|
+
file_elements = [Element.from_dict(file_dict) for file_dict in files_dicts]
|
|
244
|
+
|
|
245
|
+
return file_elements
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
async def process_slack_message(
|
|
249
|
+
event,
|
|
250
|
+
say,
|
|
251
|
+
thread_name: Optional[str] = None,
|
|
252
|
+
bind_thread_to_user=False,
|
|
253
|
+
thread_ts: Optional[str] = None,
|
|
254
|
+
):
|
|
255
|
+
user = await get_user(event["user"])
|
|
256
|
+
|
|
257
|
+
channel_id = event["channel"]
|
|
258
|
+
thread_id = str(uuid.uuid5(uuid.NAMESPACE_DNS, thread_ts or channel_id))
|
|
259
|
+
|
|
260
|
+
text = event.get("text")
|
|
261
|
+
slack_files = event.get("files", [])
|
|
262
|
+
|
|
263
|
+
session_id = str(uuid.uuid4())
|
|
264
|
+
session = HTTPSession(
|
|
265
|
+
id=session_id,
|
|
266
|
+
thread_id=thread_id,
|
|
267
|
+
user=user,
|
|
268
|
+
client_type="slack",
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
ctx = init_slack_context(
|
|
272
|
+
session=session,
|
|
273
|
+
slack_channel_id=channel_id,
|
|
274
|
+
event=event,
|
|
275
|
+
say=say,
|
|
276
|
+
thread_ts=thread_ts,
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
file_elements = await download_slack_files(
|
|
280
|
+
session, slack_files, slack_app.client.token
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
msg = Message(
|
|
284
|
+
content=clean_content(text),
|
|
285
|
+
elements=file_elements,
|
|
286
|
+
type="user_message",
|
|
287
|
+
author=user.metadata.get("real_name"),
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
await msg.send()
|
|
291
|
+
|
|
292
|
+
ctx.emitter.enabled = True
|
|
293
|
+
|
|
294
|
+
if on_chat_start := config.code.on_chat_start:
|
|
295
|
+
await on_chat_start()
|
|
296
|
+
|
|
297
|
+
if on_message := config.code.on_message:
|
|
298
|
+
await on_message(msg)
|
|
299
|
+
|
|
300
|
+
if on_chat_end := config.code.on_chat_end:
|
|
301
|
+
await on_chat_end()
|
|
302
|
+
|
|
303
|
+
if data_layer := get_data_layer():
|
|
304
|
+
user_id = None
|
|
305
|
+
if isinstance(user, PersistedUser):
|
|
306
|
+
user_id = user.id if bind_thread_to_user else None
|
|
307
|
+
|
|
308
|
+
try:
|
|
309
|
+
await data_layer.update_thread(
|
|
310
|
+
thread_id=thread_id,
|
|
311
|
+
name=thread_name or msg.content,
|
|
312
|
+
metadata=ctx.session.to_persistable(),
|
|
313
|
+
user_id=user_id,
|
|
314
|
+
)
|
|
315
|
+
except Exception as e:
|
|
316
|
+
logger.error(f"Error updating thread: {e}")
|
|
317
|
+
|
|
318
|
+
ctx.session.delete()
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
@slack_app.event("app_home_opened")
|
|
322
|
+
async def handle_app_home_opened(event, say):
|
|
323
|
+
pass
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
@slack_app.event("app_mention")
|
|
327
|
+
async def handle_app_mentions(event, say):
|
|
328
|
+
thread_ts = event.get("thread_ts", event["ts"])
|
|
329
|
+
await process_slack_message(event, say, thread_ts=thread_ts)
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
@slack_app.event("message")
|
|
333
|
+
async def handle_message(message, say):
|
|
334
|
+
user = await get_user(message["user"])
|
|
335
|
+
thread_name = f"{user.identifier} Slack DM"
|
|
336
|
+
await process_slack_message(message, say, thread_name)
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
@slack_app.block_action("thumbdown")
|
|
340
|
+
async def thumb_down(ack, context, body):
|
|
341
|
+
await ack()
|
|
342
|
+
step_id = body["actions"][0]["value"]
|
|
343
|
+
|
|
344
|
+
if data_layer := get_data_layer():
|
|
345
|
+
thread_id = context_var.get().session.thread_id
|
|
346
|
+
feedback = Feedback(forId=step_id, threadId=thread_id, value=0)
|
|
347
|
+
await data_layer.upsert_feedback(feedback)
|
|
348
|
+
|
|
349
|
+
text = body["message"]["text"]
|
|
350
|
+
blocks = body["message"]["blocks"]
|
|
351
|
+
updated_blocks = [block for block in blocks if block["type"] != "actions"]
|
|
352
|
+
updated_blocks.append(
|
|
353
|
+
{
|
|
354
|
+
"type": "section",
|
|
355
|
+
"text": {"type": "mrkdwn", "text": ":thumbsdown: Feedback received."},
|
|
356
|
+
}
|
|
357
|
+
)
|
|
358
|
+
await context.client.chat_update(
|
|
359
|
+
channel=body["channel"]["id"],
|
|
360
|
+
ts=body["container"]["message_ts"],
|
|
361
|
+
text=text,
|
|
362
|
+
blocks=updated_blocks,
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
@slack_app.block_action("thumbup")
|
|
367
|
+
async def thumb_up(ack, context, body):
|
|
368
|
+
await ack()
|
|
369
|
+
step_id = body["actions"][0]["value"]
|
|
370
|
+
|
|
371
|
+
if data_layer := get_data_layer():
|
|
372
|
+
thread_id = context_var.get().session.thread_id
|
|
373
|
+
feedback = Feedback(forId=step_id, threadId=thread_id, value=1)
|
|
374
|
+
await data_layer.upsert_feedback(feedback)
|
|
375
|
+
|
|
376
|
+
text = body["message"]["text"]
|
|
377
|
+
blocks = body["message"]["blocks"]
|
|
378
|
+
updated_blocks = [block for block in blocks if block["type"] != "actions"]
|
|
379
|
+
updated_blocks.append(
|
|
380
|
+
{
|
|
381
|
+
"type": "section",
|
|
382
|
+
"text": {"type": "mrkdwn", "text": ":thumbsup: Feedback received."},
|
|
383
|
+
}
|
|
384
|
+
)
|
|
385
|
+
await context.client.chat_update(
|
|
386
|
+
channel=body["channel"]["id"],
|
|
387
|
+
ts=body["container"]["message_ts"],
|
|
388
|
+
text=text,
|
|
389
|
+
blocks=updated_blocks,
|
|
390
|
+
)
|