Skip to content
Open
4 changes: 4 additions & 0 deletions backend/chainlit/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,9 @@
# Allow users to share threads (backend + UI). Requires an app-defined on_shared_thread_view callback.
allow_thread_sharing = false

# Enable favorite messages
favorites = false

[features.slack]
# Add emoji reaction when message is received (requires reactions:write OAuth scope)
reaction_on_message_received = false
Expand Down Expand Up @@ -316,6 +319,7 @@ class FeaturesSettings(BaseModel):
auto_tag_thread: bool = True
edit_message: bool = True
allow_thread_sharing: bool = False
favorites: bool = False


class HeaderLink(BaseModel):
Expand Down
4 changes: 4 additions & 0 deletions backend/chainlit/data/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,3 +109,7 @@ async def build_debug_url(self) -> str:
@abstractmethod
async def close(self) -> None:
pass

@abstractmethod
async def get_favorite_steps(self, user_id: str) -> List["StepDict"]:
pass
14 changes: 13 additions & 1 deletion backend/chainlit/data/chainlit_data_layer.py
Original file line number Diff line number Diff line change
Expand Up @@ -631,8 +631,20 @@ async def update_thread(

await self.execute_query(query, {str(i + 1): v for i, v in enumerate(values)})

async def get_favorite_steps(self, user_id: str) -> List[StepDict]:
query = """
SELECT s.*
FROM "Step" s
JOIN "Thread" t ON s."threadId" = t.id
WHERE t."userId" = $1
AND s.metadata::jsonb->>'favorite' = 'true'
ORDER BY s."createdAt" DESC \
"""
results = await self.execute_query(query, {"user_id": user_id})
return [self._convert_step_row_to_dict(row) for row in results]

def _extract_feedback_dict_from_step_row(self, row: Dict) -> Optional[FeedbackDict]:
if row["feedback_id"] is not None:
if row.get("feedback_id", None) is not None:
return FeedbackDict(
forId=row["id"],
id=row["feedback_id"],
Expand Down
65 changes: 64 additions & 1 deletion backend/chainlit/data/dynamodb.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from dataclasses import asdict
from datetime import datetime
from decimal import Decimal
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union, cast

import aiofiles
import aiohttp
Expand Down Expand Up @@ -612,6 +612,69 @@ async def update_thread(
updates=item,
)

async def get_favorite_steps(self, user_id: str) -> List["StepDict"]:
_logger.info("DynamoDB: get_favorite_steps user_id=%s", user_id)

thread_ids = []
query_args: Dict[str, Any] = {
"TableName": self.table_name,
"IndexName": "UserThread",
"KeyConditionExpression": "#UserThreadPK = :pk",
"ExpressionAttributeNames": {"#UserThreadPK": "UserThreadPK"},
"ExpressionAttributeValues": {":pk": {"S": f"USER#{user_id}"}},
}

while True:
response = self.client.query(**query_args) # type: ignore
for item in response.get("Items", []):
pk = item.get("PK", {}).get("S")
if pk:
thread_ids.append(pk.removeprefix("THREAD#"))

if "LastEvaluatedKey" not in response:
break
query_args["ExclusiveStartKey"] = response["LastEvaluatedKey"]

favorite_steps: List[Dict[str, Any]] = []

for thread_id in thread_ids:
t_query_args: Dict[str, Any] = {
"TableName": self.table_name,
"KeyConditionExpression": "#pk = :pk AND begins_with(#sk, :sk_prefix)",
"FilterExpression": "#metadata.#favorite = :true",
"ExpressionAttributeNames": {
"#pk": "PK",
"#sk": "SK",
"#metadata": "metadata",
"#favorite": "favorite",
},
"ExpressionAttributeValues": {
":pk": {"S": f"THREAD#{thread_id}"},
":sk_prefix": {"S": "STEP#"},
":true": {"BOOL": True},
},
}

while True:
response = self.client.query(**t_query_args) # type: ignore
for item in response.get("Items", []):
step = self._deserialize_item(item)
if "PK" in step:
del step["PK"]
if "SK" in step:
del step["SK"]
if "feedback" in step:
del step["feedback"]

favorite_steps.append(step)

if "LastEvaluatedKey" not in response:
break
t_query_args["ExclusiveStartKey"] = response["LastEvaluatedKey"]

favorite_steps.sort(key=lambda x: x.get("createdAt", ""), reverse=True)
return cast(List["StepDict"], favorite_steps)

async def build_debug_url(self) -> str:
return ""

Expand Down
4 changes: 4 additions & 0 deletions backend/chainlit/data/literalai.py
Original file line number Diff line number Diff line change
Expand Up @@ -516,5 +516,9 @@ async def update_thread(
tags=tags,
)

async def get_favorite_steps(self, user_id: str) -> List[StepDict]:
"""noop for literalai"""
return []

async def close(self):
self.client.flush_and_stop()
78 changes: 78 additions & 0 deletions backend/chainlit/data/sql_alchemy.py
Original file line number Diff line number Diff line change
Expand Up @@ -777,6 +777,84 @@ async def get_all_user_threads(

return list(thread_dicts.values())

async def get_favorite_steps(self, user_id: str) -> List[StepDict]:
if self.show_logger:
logger.info(f"SQLAlchemy: get_favorite_steps, user_id={user_id}")

query = """
SELECT
s."id" AS step_id,
s."name" AS step_name,
s."type" AS step_type,
s."threadId" AS step_threadid,
s."parentId" AS step_parentid,
s."streaming" AS step_streaming,
s."waitForAnswer" AS step_waitforanswer,
s."isError" AS step_iserror,
s."metadata" AS step_metadata,
s."tags" AS step_tags,
s."input" AS step_input,
s."output" AS step_output,
s."createdAt" AS step_createdat,
s."start" AS step_start,
s."end" AS step_end,
s."generation" AS step_generation,
s."showInput" AS step_showinput,
s."language" AS step_language
FROM steps s
JOIN threads t ON s."threadId" = t.id
WHERE t."userId" = :user_id
AND s."metadata" LIKE :favorite_pattern
ORDER BY s."createdAt" DESC \
"""

result = await self.execute_sql(
query, {"user_id": user_id, "favorite_pattern": '%"favorite": true%'}
)

steps = []
if isinstance(result, list):
for row in result:
metadata_raw = row["step_metadata"]
meta_dict = {}
if isinstance(metadata_raw, str):
try:
meta_dict = json.loads(metadata_raw)
except Exception:
pass
elif isinstance(metadata_raw, dict):
meta_dict = metadata_raw

if meta_dict.get("favorite"):
steps.append(
StepDict(
id=row["step_id"],
name=row["step_name"],
type=row["step_type"],
threadId=row["step_threadid"],
parentId=row["step_parentid"],
streaming=row.get("step_streaming", False),
waitForAnswer=row.get("step_waitforanswer"),
isError=row.get("step_iserror"),
metadata=meta_dict,
tags=row.get("step_tags"),
input=(
row.get("step_input", "")
if row.get("step_showinput") not in [None, "false"]
else ""
),
output=row.get("step_output", ""),
createdAt=row.get("step_createdat"),
start=row.get("step_start"),
end=row.get("step_end"),
generation=row.get("step_generation"),
showInput=row.get("step_showinput"),
language=row.get("step_language"),
feedback=None,
)
)
return steps

async def close(self) -> None:
if self.storage_provider:
await self.storage_provider.close()
Expand Down
11 changes: 11 additions & 0 deletions backend/chainlit/emitter.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,10 @@ def send_toast(self, message: str, type: Optional[ToastType] = "info"):
"""Stub method to send a toast message to the UI."""
pass

async def set_favorites(self, steps: List[StepDict]):
"""Stub method to send the favorite messages to the UI."""
pass


class ChainlitEmitter(BaseChainlitEmitter):
"""
Expand Down Expand Up @@ -450,6 +454,13 @@ def set_modes(self, modes: List[Mode]):
[mode.to_dict() for mode in modes],
)

def set_favorites(self, steps: List[StepDict]):
"""Send the favorite messages to the UI."""
return self.emit(
"set_favorites",
steps,
)

def send_window_message(self, data: Any):
"""Send custom data to the host window."""
return self.emit("window_message", data)
Expand Down
30 changes: 30 additions & 0 deletions backend/chainlit/socket.py
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,36 @@ async def edit_message(sid, payload: MessagePayload):
await context.emitter.task_end()


@sio.on("message_favorite") # pyright: ignore [reportOptionalCall]
async def message_favorite(sid, payload: MessagePayload):
"""Handle a message favorite toggle."""
session = WebsocketSession.require(sid)
init_ws_context(session)
messages = chat_context.get()
if config.features.favorites:
for message in messages:
if message.id == payload["message"]["id"]:
if message.metadata is None:
message.metadata = {}

message.metadata["favorite"] = not message.metadata.get(
"favorite", False
)
await message.update()
await fetch_favorites(sid)
break


@sio.on("fetch_favorites") # pyright: ignore [reportOptionalCall]
async def fetch_favorites(sid):
session = WebsocketSession.require(sid)
context = init_ws_context(session)
if session.user and config.features.favorites:
if data_layer := get_data_layer():
favorites = await data_layer.get_favorite_steps(session.user.id)
await context.emitter.set_favorites(favorites)


@sio.on("client_message") # pyright: ignore [reportOptionalCall]
async def message(sid, payload: MessagePayload):
"""Handle a message sent by the User."""
Expand Down
4 changes: 4 additions & 0 deletions backend/chainlit/translations/bn.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,10 @@
"stop": "রেকর্ডিং বন্ধ করুন",
"connecting": "সংযোগ করা হচ্ছে"
},
"favorites": {
"use": "একটি পছন্দের মেসেজ ব্যবহার করুন",
"headline": "পছন্দের মেসেজ"
},
"commands": {
"button": "টুলস",
"changeTool": "টুল পরিবর্তন করুন",
Expand Down
4 changes: 4 additions & 0 deletions backend/chainlit/translations/de-DE.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@
"attachFiles": "Dateien anhängen"
}
},
"favorites": {
"use": "Eine favorisierte Nachricht verwenden",
"headline": "Favorisierte Nachrichten"
},
"commands": {
"button": "Tools",
"changeTool": "Tool wechseln",
Expand Down
4 changes: 4 additions & 0 deletions backend/chainlit/translations/el-GR.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@
"attachFiles": "Επισύναψη αρχείων"
}
},
"favorites": {
"use": "Χρησιμοποιήστε ένα αγαπημένο μήνυμα",
"headline": "Αγαπημένα μηνύματα"
},
"commands": {
"button": "Εργαλεία",
"changeTool": "Αλλαγή Εργαλείου",
Expand Down
4 changes: 4 additions & 0 deletions backend/chainlit/translations/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@
"attachFiles": "Attach files"
}
},
"favorites": {
"use": "Use a favorite message",
"headline": "Favorite Messages"
},
"commands": {
"button": "Tools",
"changeTool": "Change Tool",
Expand Down
4 changes: 4 additions & 0 deletions backend/chainlit/translations/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@
"attachFiles": "Adjuntar archivos"
}
},
"favorites": {
"use": "Usar un mensaje favorito",
"headline": "Mensajes favoritos"
},
"commands": {
"button": "Herramientas",
"changeTool": "Cambiar herramienta",
Expand Down
4 changes: 4 additions & 0 deletions backend/chainlit/translations/fr-FR.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@
"attachFiles": "Joindre des fichiers"
}
},
"favorites": {
"use": "Utiliser un message favori",
"headline": "Messages favoris"
},
"commands": {
"button": "Outils",
"changeTool": "Changer d'outil",
Expand Down
4 changes: 4 additions & 0 deletions backend/chainlit/translations/gu.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,10 @@
"stop": "રેકોર્ડિંગ બંધ કરો",
"connecting": "કનેક્ટ થઈ રહ્યું છે"
},
"favorites": {
"use": "મનપસંદ સંદેશનો ઉપયોગ કરો",
"headline": "મનપસંદ સંદેશાઓ"
},
"commands": {
"button": "ટૂલ્સ",
"changeTool": "ટૂલ બદલો",
Expand Down
4 changes: 4 additions & 0 deletions backend/chainlit/translations/he-IL.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,10 @@
"stop": "עצור הקלטה",
"connecting": "מתחבר"
},
"favorites": {
"use": "השתמש בהודעה מועדפת",
"headline": "הודעות מועדפות"
},
"commands": {
"button": "כלים",
"changeTool": "שנה כלי",
Expand Down
4 changes: 4 additions & 0 deletions backend/chainlit/translations/hi.json
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,10 @@
"removeAttachment": "संलग्नक हटाएं"
}
},
"favorites": {
"use": "पसंदीदा संदेश का उपयोग करें",
"headline": "पसंदीदा संदेश"
},
"commands": {
"button": "उपकरण",
"changeTool": "उपकरण बदलें",
Expand Down
4 changes: 4 additions & 0 deletions backend/chainlit/translations/it.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@
"attachFiles": "Allega file"
}
},
"favorites": {
"use": "Usa un messaggio preferito",
"headline": "Messaggi preferiti"
},
"commands": {
"button": "Strumenti",
"changeTool": "Cambia strumento",
Expand Down
Loading
Loading