670 lines
22 KiB
Python
670 lines
22 KiB
Python
|
|
import json
|
||
|
|
import os
|
||
|
|
import site
|
||
|
|
import sys
|
||
|
|
from importlib import util
|
||
|
|
from pathlib import Path
|
||
|
|
from typing import (
|
||
|
|
TYPE_CHECKING,
|
||
|
|
Any,
|
||
|
|
Awaitable,
|
||
|
|
Callable,
|
||
|
|
Dict,
|
||
|
|
List,
|
||
|
|
Literal,
|
||
|
|
Optional,
|
||
|
|
Union,
|
||
|
|
)
|
||
|
|
|
||
|
|
import tomli
|
||
|
|
from pydantic import BaseModel, Field
|
||
|
|
from pydantic_settings import BaseSettings
|
||
|
|
from starlette.datastructures import Headers
|
||
|
|
|
||
|
|
from chainlit.data.base import BaseDataLayer
|
||
|
|
from chainlit.logger import logger
|
||
|
|
from chainlit.translations import lint_translation_json
|
||
|
|
from chainlit.version import __version__
|
||
|
|
|
||
|
|
from ._utils import is_path_inside
|
||
|
|
|
||
|
|
if TYPE_CHECKING:
|
||
|
|
from fastapi import Request, Response
|
||
|
|
|
||
|
|
from chainlit.action import Action
|
||
|
|
from chainlit.message import Message
|
||
|
|
from chainlit.types import (
|
||
|
|
ChatProfile,
|
||
|
|
Feedback,
|
||
|
|
InputAudioChunk,
|
||
|
|
Starter,
|
||
|
|
ThreadDict,
|
||
|
|
)
|
||
|
|
from chainlit.user import User
|
||
|
|
else:
|
||
|
|
# Pydantic needs to resolve forward annotations. Because all of these are used
|
||
|
|
# within `typing.Callable`, alias to `Any` as Pydantic does not perform validation
|
||
|
|
# of callable argument/return types anyway.
|
||
|
|
Request = Response = Action = Message = ChatProfile = InputAudioChunk = Starter = ThreadDict = User = Feedback = Any # fmt: off
|
||
|
|
|
||
|
|
BACKEND_ROOT = os.path.dirname(__file__)
|
||
|
|
PACKAGE_ROOT = os.path.dirname(os.path.dirname(BACKEND_ROOT))
|
||
|
|
TRANSLATIONS_DIR = os.path.join(BACKEND_ROOT, "translations")
|
||
|
|
|
||
|
|
|
||
|
|
# Get the directory the script is running from
|
||
|
|
APP_ROOT = os.getenv("CHAINLIT_APP_ROOT", os.getcwd())
|
||
|
|
|
||
|
|
# Create the directory to store the uploaded files
|
||
|
|
FILES_DIRECTORY = Path(APP_ROOT) / ".files"
|
||
|
|
FILES_DIRECTORY.mkdir(exist_ok=True)
|
||
|
|
|
||
|
|
config_dir = os.path.join(APP_ROOT, ".chainlit")
|
||
|
|
public_dir = os.path.join(APP_ROOT, "public")
|
||
|
|
config_file = os.path.join(config_dir, "config.toml")
|
||
|
|
config_translation_dir = os.path.join(config_dir, "translations")
|
||
|
|
|
||
|
|
# Default config file created if none exists
|
||
|
|
DEFAULT_CONFIG_STR = f"""[project]
|
||
|
|
# List of environment variables to be provided by each user to use the app.
|
||
|
|
user_env = []
|
||
|
|
|
||
|
|
# Duration (in seconds) during which the session is saved when the connection is lost
|
||
|
|
session_timeout = 3600
|
||
|
|
|
||
|
|
# Duration (in seconds) of the user session expiry
|
||
|
|
user_session_timeout = 1296000 # 15 days
|
||
|
|
|
||
|
|
# Enable third parties caching (e.g., LangChain cache)
|
||
|
|
cache = false
|
||
|
|
|
||
|
|
# Whether to persist user environment variables (API keys) to the database
|
||
|
|
# Set to true to store user env vars in DB, false to exclude them for security
|
||
|
|
persist_user_env = false
|
||
|
|
|
||
|
|
# Whether to mask user environment variables (API keys) in the UI with password type
|
||
|
|
# Set to true to show API keys as ***, false to show them as plain text
|
||
|
|
mask_user_env = false
|
||
|
|
|
||
|
|
# Authorized origins
|
||
|
|
allow_origins = ["*"]
|
||
|
|
|
||
|
|
[features]
|
||
|
|
# Process and display HTML in messages. This can be a security risk (see https://stackoverflow.com/questions/19603097/why-is-it-dangerous-to-render-user-generated-html-or-javascript)
|
||
|
|
unsafe_allow_html = false
|
||
|
|
|
||
|
|
# Process and display mathematical expressions. This can clash with "$" characters in messages.
|
||
|
|
latex = false
|
||
|
|
|
||
|
|
# Autoscroll new user messages at the top of the window
|
||
|
|
user_message_autoscroll = true
|
||
|
|
|
||
|
|
# Autoscroll new assistant messages
|
||
|
|
assistant_message_autoscroll = true
|
||
|
|
|
||
|
|
# Automatically tag threads with the current chat profile (if a chat profile is used)
|
||
|
|
auto_tag_thread = true
|
||
|
|
|
||
|
|
# Allow users to edit their own messages
|
||
|
|
edit_message = true
|
||
|
|
|
||
|
|
# Allow users to share threads (backend + UI). Requires an app-defined on_shared_thread_view callback.
|
||
|
|
allow_thread_sharing = false
|
||
|
|
|
||
|
|
[features.slack]
|
||
|
|
# Add emoji reaction when message is received (requires reactions:write OAuth scope)
|
||
|
|
reaction_on_message_received = false
|
||
|
|
|
||
|
|
# Authorize users to spontaneously upload files with messages
|
||
|
|
[features.spontaneous_file_upload]
|
||
|
|
enabled = true
|
||
|
|
# Define accepted file types using MIME types
|
||
|
|
# Examples:
|
||
|
|
# 1. For specific file types:
|
||
|
|
# accept = ["image/jpeg", "image/png", "application/pdf"]
|
||
|
|
# 2. For all files of certain type:
|
||
|
|
# accept = ["image/*", "audio/*", "video/*"]
|
||
|
|
# 3. For specific file extensions:
|
||
|
|
# accept = {{ "application/octet-stream" = [".xyz", ".pdb"] }}
|
||
|
|
# Note: Using "*/*" is not recommended as it may cause browser warnings
|
||
|
|
accept = ["*/*"]
|
||
|
|
max_files = 20
|
||
|
|
max_size_mb = 500
|
||
|
|
|
||
|
|
[features.audio]
|
||
|
|
# Enable audio features
|
||
|
|
enabled = false
|
||
|
|
# Sample rate of the audio
|
||
|
|
sample_rate = 24000
|
||
|
|
|
||
|
|
[features.mcp]
|
||
|
|
# Enable Model Context Protocol (MCP) features
|
||
|
|
enabled = false
|
||
|
|
|
||
|
|
[features.mcp.sse]
|
||
|
|
enabled = true
|
||
|
|
|
||
|
|
[features.mcp.streamable-http]
|
||
|
|
enabled = true
|
||
|
|
|
||
|
|
[features.mcp.stdio]
|
||
|
|
enabled = true
|
||
|
|
# Only the executables in the allow list can be used for MCP stdio server.
|
||
|
|
# Only need the base name of the executable, e.g. "npx", not "/usr/bin/npx".
|
||
|
|
# Please don't comment this line for now, we need it to parse the executable name.
|
||
|
|
allowed_executables = [ "npx", "uvx" ]
|
||
|
|
|
||
|
|
[UI]
|
||
|
|
# Name of the assistant.
|
||
|
|
name = "Assistant"
|
||
|
|
|
||
|
|
# default_theme = "dark"
|
||
|
|
|
||
|
|
# Force a specific language for all users (e.g., "en-US", "he-IL", "fr-FR")
|
||
|
|
# If not set, the browser's language will be used
|
||
|
|
# language = "en-US"
|
||
|
|
|
||
|
|
# layout = "wide"
|
||
|
|
|
||
|
|
# default_sidebar_state = "open"
|
||
|
|
|
||
|
|
# Description of the assistant. This is used for HTML tags.
|
||
|
|
# description = ""
|
||
|
|
|
||
|
|
# Chain of Thought (CoT) display mode. Can be "hidden", "tool_call" or "full".
|
||
|
|
cot = "full"
|
||
|
|
|
||
|
|
# Specify a CSS file that can be used to customize the user interface.
|
||
|
|
# The CSS file can be served from the public directory or via an external link.
|
||
|
|
# custom_css = "/public/test.css"
|
||
|
|
|
||
|
|
# Specify additional attributes for a custom CSS file
|
||
|
|
# custom_css_attributes = "media=\\\"print\\\""
|
||
|
|
|
||
|
|
# Specify a JavaScript file that can be used to customize the user interface.
|
||
|
|
# The JavaScript file can be served from the public directory.
|
||
|
|
# custom_js = "/public/test.js"
|
||
|
|
|
||
|
|
# The style of alert boxes. Can be "classic" or "modern".
|
||
|
|
alert_style = "classic"
|
||
|
|
|
||
|
|
# Specify additional attributes for custom JS file
|
||
|
|
# custom_js_attributes = "async type = \\\"module\\\""
|
||
|
|
|
||
|
|
# Custom login page image, relative to public directory or external URL
|
||
|
|
# login_page_image = "/public/custom-background.jpg"
|
||
|
|
|
||
|
|
# Custom login page image filter (Tailwind internal filters, no dark/light variants)
|
||
|
|
# login_page_image_filter = "brightness-50 grayscale"
|
||
|
|
# login_page_image_dark_filter = "contrast-200 blur-sm"
|
||
|
|
|
||
|
|
# Specify a custom meta URL (used for meta tags like og:url)
|
||
|
|
# custom_meta_url = "https://github.com/Chainlit/chainlit"
|
||
|
|
|
||
|
|
# Specify a custom meta image url.
|
||
|
|
# custom_meta_image_url = "https://chainlit-cloud.s3.eu-west-3.amazonaws.com/logo/chainlit_banner.png"
|
||
|
|
|
||
|
|
# Load assistant logo directly from URL.
|
||
|
|
logo_file_url = ""
|
||
|
|
|
||
|
|
# Load assistant avatar image directly from URL.
|
||
|
|
default_avatar_file_url = ""
|
||
|
|
|
||
|
|
# Specify a custom build directory for the frontend.
|
||
|
|
# This can be used to customize the frontend code.
|
||
|
|
# Be careful: If this is a relative path, it should not start with a slash.
|
||
|
|
# custom_build = "./public/build"
|
||
|
|
|
||
|
|
# Specify optional one or more custom links in the header.
|
||
|
|
# [[UI.header_links]]
|
||
|
|
# name = "Issues"
|
||
|
|
# display_name = "Report Issue"
|
||
|
|
# icon_url = "https://avatars.githubusercontent.com/u/128686189?s=200&v=4"
|
||
|
|
# url = "https://github.com/Chainlit/chainlit/issues"
|
||
|
|
# target = "_blank" (default) # Optional: "_self", "_parent", "_top".
|
||
|
|
|
||
|
|
[meta]
|
||
|
|
generated_by = "{__version__}"
|
||
|
|
"""
|
||
|
|
|
||
|
|
|
||
|
|
DEFAULT_HOST = "127.0.0.1"
|
||
|
|
DEFAULT_PORT = 8000
|
||
|
|
DEFAULT_ROOT_PATH = ""
|
||
|
|
|
||
|
|
|
||
|
|
class RunSettings(BaseModel):
|
||
|
|
# Name of the module (python file) used in the run command
|
||
|
|
module_name: Optional[str] = None
|
||
|
|
host: str = DEFAULT_HOST
|
||
|
|
port: int = DEFAULT_PORT
|
||
|
|
ssl_cert: Optional[str] = None
|
||
|
|
ssl_key: Optional[str] = None
|
||
|
|
root_path: str = DEFAULT_ROOT_PATH
|
||
|
|
headless: bool = False
|
||
|
|
watch: bool = False
|
||
|
|
no_cache: bool = False
|
||
|
|
debug: bool = False
|
||
|
|
ci: bool = False
|
||
|
|
|
||
|
|
|
||
|
|
class PaletteOptions(BaseModel):
|
||
|
|
main: Optional[str] = ""
|
||
|
|
light: Optional[str] = ""
|
||
|
|
dark: Optional[str] = ""
|
||
|
|
|
||
|
|
|
||
|
|
class TextOptions(BaseModel):
|
||
|
|
primary: Optional[str] = ""
|
||
|
|
secondary: Optional[str] = ""
|
||
|
|
|
||
|
|
|
||
|
|
class Palette(BaseModel):
|
||
|
|
primary: Optional[PaletteOptions] = None
|
||
|
|
background: Optional[str] = ""
|
||
|
|
paper: Optional[str] = ""
|
||
|
|
text: Optional[TextOptions] = None
|
||
|
|
|
||
|
|
|
||
|
|
class SpontaneousFileUploadFeature(BaseModel):
|
||
|
|
enabled: Optional[bool] = None
|
||
|
|
accept: Optional[Union[List[str], Dict[str, List[str]]]] = None
|
||
|
|
max_files: Optional[int] = None
|
||
|
|
max_size_mb: Optional[int] = None
|
||
|
|
|
||
|
|
|
||
|
|
class AudioFeature(BaseModel):
|
||
|
|
sample_rate: int = 24000
|
||
|
|
enabled: bool = False
|
||
|
|
|
||
|
|
|
||
|
|
class McpSseFeature(BaseModel):
|
||
|
|
enabled: bool = True
|
||
|
|
|
||
|
|
|
||
|
|
class McpStreamableHttpFeature(BaseModel):
|
||
|
|
enabled: bool = True
|
||
|
|
|
||
|
|
|
||
|
|
class McpStdioFeature(BaseModel):
|
||
|
|
enabled: bool = True
|
||
|
|
allowed_executables: Optional[list[str]] = None
|
||
|
|
|
||
|
|
|
||
|
|
class SlackFeature(BaseModel):
|
||
|
|
reaction_on_message_received: bool = False
|
||
|
|
|
||
|
|
|
||
|
|
class McpFeature(BaseModel):
|
||
|
|
enabled: bool = False
|
||
|
|
sse: McpSseFeature = Field(default_factory=McpSseFeature)
|
||
|
|
streamable_http: McpStreamableHttpFeature = Field(
|
||
|
|
default_factory=McpStreamableHttpFeature
|
||
|
|
)
|
||
|
|
stdio: McpStdioFeature = Field(default_factory=McpStdioFeature)
|
||
|
|
|
||
|
|
|
||
|
|
class FeaturesSettings(BaseModel):
|
||
|
|
spontaneous_file_upload: Optional[SpontaneousFileUploadFeature] = None
|
||
|
|
audio: Optional[AudioFeature] = Field(default_factory=AudioFeature)
|
||
|
|
mcp: McpFeature = Field(default_factory=McpFeature)
|
||
|
|
slack: SlackFeature = Field(default_factory=SlackFeature)
|
||
|
|
latex: bool = False
|
||
|
|
user_message_autoscroll: bool = True
|
||
|
|
assistant_message_autoscroll: bool = True
|
||
|
|
unsafe_allow_html: bool = False
|
||
|
|
auto_tag_thread: bool = True
|
||
|
|
edit_message: bool = True
|
||
|
|
allow_thread_sharing: bool = False
|
||
|
|
|
||
|
|
|
||
|
|
class HeaderLink(BaseModel):
|
||
|
|
name: str
|
||
|
|
icon_url: str
|
||
|
|
url: str
|
||
|
|
display_name: Optional[str] = None
|
||
|
|
target: Optional[Literal["_blank", "_self", "_parent", "_top"]] = None
|
||
|
|
|
||
|
|
|
||
|
|
class UISettings(BaseModel):
|
||
|
|
name: str
|
||
|
|
description: str = ""
|
||
|
|
cot: Literal["hidden", "tool_call", "full"] = "full"
|
||
|
|
default_theme: Optional[Literal["light", "dark"]] = "dark"
|
||
|
|
language: Optional[str] = None
|
||
|
|
layout: Optional[Literal["default", "wide"]] = "default"
|
||
|
|
default_sidebar_state: Optional[Literal["open", "closed"]] = "open"
|
||
|
|
github: Optional[str] = None
|
||
|
|
# Optional custom CSS file that allows you to customize the UI
|
||
|
|
custom_css: Optional[str] = None
|
||
|
|
custom_css_attributes: Optional[str] = ""
|
||
|
|
# Optional custom JS file that allows you to customize the UI
|
||
|
|
custom_js: Optional[str] = None
|
||
|
|
|
||
|
|
alert_style: Optional[Literal["classic", "modern"]] = "classic"
|
||
|
|
custom_js_attributes: Optional[str] = "defer"
|
||
|
|
# Optional custom background image for login page
|
||
|
|
login_page_image: Optional[str] = None
|
||
|
|
login_page_image_filter: Optional[str] = None
|
||
|
|
login_page_image_dark_filter: Optional[str] = None
|
||
|
|
|
||
|
|
# Optional custom meta tag for URL preview
|
||
|
|
custom_meta_url: Optional[str] = None
|
||
|
|
# Optional custom meta tag for image preview
|
||
|
|
custom_meta_image_url: Optional[str] = None
|
||
|
|
# Optional logo file url
|
||
|
|
logo_file_url: Optional[str] = None
|
||
|
|
# Optional avatar image file url
|
||
|
|
default_avatar_file_url: Optional[str] = None
|
||
|
|
# Optional custom build directory for the frontend
|
||
|
|
custom_build: Optional[str] = None
|
||
|
|
# Optional header links
|
||
|
|
header_links: Optional[List[HeaderLink]] = None
|
||
|
|
|
||
|
|
|
||
|
|
class CodeSettings(BaseModel):
|
||
|
|
# App action functions
|
||
|
|
action_callbacks: Dict[str, Callable[["Action"], Any]]
|
||
|
|
|
||
|
|
# Module object loaded from the module_name
|
||
|
|
module: Any = None
|
||
|
|
|
||
|
|
# App life cycle callbacks
|
||
|
|
on_app_startup: Optional[Callable[[], Union[None, Awaitable[None]]]] = None
|
||
|
|
on_app_shutdown: Optional[Callable[[], Union[None, Awaitable[None]]]] = None
|
||
|
|
|
||
|
|
# Session life cycle callbacks
|
||
|
|
on_logout: Optional[Callable[["Request", "Response"], Any]] = None
|
||
|
|
on_stop: Optional[Callable[[], Any]] = None
|
||
|
|
on_chat_start: Optional[Callable[[], Any]] = None
|
||
|
|
on_chat_end: Optional[Callable[[], Any]] = None
|
||
|
|
on_chat_resume: Optional[Callable[["ThreadDict"], Any]] = None
|
||
|
|
on_message: Optional[Callable[["Message"], Any]] = None
|
||
|
|
on_feedback: Optional[Callable[["Feedback"], Any]] = None
|
||
|
|
on_slack_reaction_added: Optional[Callable[[Dict[str, Any]], Any]] = None
|
||
|
|
on_audio_start: Optional[Callable[[], Any]] = None
|
||
|
|
on_audio_chunk: Optional[Callable[["InputAudioChunk"], Any]] = None
|
||
|
|
on_audio_end: Optional[Callable[[], Any]] = None
|
||
|
|
on_mcp_connect: Optional[Callable] = None
|
||
|
|
on_mcp_disconnect: Optional[Callable] = None
|
||
|
|
on_settings_update: Optional[Callable[[Dict[str, Any]], Any]] = None
|
||
|
|
set_chat_profiles: Optional[
|
||
|
|
Callable[[Optional["User"], Optional["str"]], Awaitable[List["ChatProfile"]]]
|
||
|
|
] = None
|
||
|
|
set_starters: Optional[
|
||
|
|
Callable[[Optional["User"], Optional["str"]], Awaitable[List["Starter"]]]
|
||
|
|
] = None
|
||
|
|
on_shared_thread_view: Optional[
|
||
|
|
Callable[["ThreadDict", Optional["User"]], Awaitable[bool]]
|
||
|
|
] = None
|
||
|
|
# Auth callbacks
|
||
|
|
password_auth_callback: Optional[
|
||
|
|
Callable[[str, str], Awaitable[Optional["User"]]]
|
||
|
|
] = None
|
||
|
|
header_auth_callback: Optional[Callable[[Headers], Awaitable[Optional["User"]]]] = (
|
||
|
|
None
|
||
|
|
)
|
||
|
|
oauth_callback: Optional[
|
||
|
|
Callable[[str, str, Dict[str, str], "User"], Awaitable[Optional["User"]]]
|
||
|
|
] = None
|
||
|
|
|
||
|
|
# Helpers
|
||
|
|
on_window_message: Optional[Callable[[str], Any]] = None
|
||
|
|
author_rename: Optional[Callable[[str], Awaitable[str]]] = None
|
||
|
|
data_layer: Optional[Callable[[], BaseDataLayer]] = None
|
||
|
|
|
||
|
|
|
||
|
|
class ProjectSettings(BaseModel):
|
||
|
|
allow_origins: List[str] = Field(default_factory=lambda: ["*"])
|
||
|
|
# Socket.io client transports option
|
||
|
|
transports: Optional[List[str]] = None
|
||
|
|
# 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.
|
||
|
|
user_env: Optional[List[str]] = None
|
||
|
|
# Path to the local langchain cache database
|
||
|
|
lc_cache_path: Optional[str] = None
|
||
|
|
# Path to the local chat db
|
||
|
|
# Duration (in seconds) during which the session is saved when the connection is lost
|
||
|
|
session_timeout: int = 300
|
||
|
|
# Duration (in seconds) of the user session expiry
|
||
|
|
user_session_timeout: int = 1296000 # 15 days
|
||
|
|
# Enable third parties caching (e.g LangChain cache)
|
||
|
|
cache: bool = False
|
||
|
|
# Whether to persist user environment variables (API keys) to the database
|
||
|
|
persist_user_env: Optional[bool] = False
|
||
|
|
# Whether to mask user environment variables (API keys) in the UI with password type
|
||
|
|
mask_user_env: Optional[bool] = False
|
||
|
|
|
||
|
|
|
||
|
|
class ChainlitConfigOverrides(BaseModel):
|
||
|
|
"""Configuration overrides that can be applied to specific chat profiles."""
|
||
|
|
|
||
|
|
ui: Optional[UISettings] = None
|
||
|
|
features: Optional[FeaturesSettings] = None
|
||
|
|
project: Optional[ProjectSettings] = None
|
||
|
|
|
||
|
|
|
||
|
|
class ChainlitConfig(BaseSettings):
|
||
|
|
root: str = APP_ROOT
|
||
|
|
chainlit_server: str = Field(default="")
|
||
|
|
run: RunSettings = Field(default_factory=RunSettings)
|
||
|
|
features: FeaturesSettings
|
||
|
|
ui: UISettings
|
||
|
|
project: ProjectSettings
|
||
|
|
code: CodeSettings
|
||
|
|
|
||
|
|
def load_translation(self, language: str):
|
||
|
|
translation = {}
|
||
|
|
default_language = "en-US"
|
||
|
|
# fallback to root language (ex: `de` when `de-DE` is not found)
|
||
|
|
parent_language = language.split("-")[0]
|
||
|
|
|
||
|
|
translation_dir = Path(config_translation_dir)
|
||
|
|
|
||
|
|
translation_lib_file_path = translation_dir / f"{language}.json"
|
||
|
|
translation_lib_parent_language_file_path = (
|
||
|
|
translation_dir / f"{parent_language}.json"
|
||
|
|
)
|
||
|
|
default_translation_lib_file_path = translation_dir / f"{default_language}.json"
|
||
|
|
|
||
|
|
if (
|
||
|
|
is_path_inside(translation_lib_file_path, translation_dir)
|
||
|
|
and translation_lib_file_path.is_file()
|
||
|
|
):
|
||
|
|
translation = json.loads(
|
||
|
|
translation_lib_file_path.read_text(encoding="utf-8")
|
||
|
|
)
|
||
|
|
elif (
|
||
|
|
is_path_inside(translation_lib_parent_language_file_path, translation_dir)
|
||
|
|
and translation_lib_parent_language_file_path.is_file()
|
||
|
|
):
|
||
|
|
logger.warning(
|
||
|
|
f"Translation file for {language} not found. Using parent translation {parent_language}."
|
||
|
|
)
|
||
|
|
translation = json.loads(
|
||
|
|
translation_lib_parent_language_file_path.read_text(encoding="utf-8")
|
||
|
|
)
|
||
|
|
elif (
|
||
|
|
is_path_inside(default_translation_lib_file_path, translation_dir)
|
||
|
|
and default_translation_lib_file_path.is_file()
|
||
|
|
):
|
||
|
|
logger.warning(
|
||
|
|
f"Translation file for {language} not found. Using default translation {default_language}."
|
||
|
|
)
|
||
|
|
translation = json.loads(
|
||
|
|
default_translation_lib_file_path.read_text(encoding="utf-8")
|
||
|
|
)
|
||
|
|
|
||
|
|
return translation
|
||
|
|
|
||
|
|
def with_overrides(
|
||
|
|
self, overrides: "ChainlitConfigOverrides | None"
|
||
|
|
) -> "ChainlitConfig":
|
||
|
|
base = self.model_dump()
|
||
|
|
patch = overrides.model_dump(exclude_unset=True) if overrides else {}
|
||
|
|
|
||
|
|
def _merge(a, b):
|
||
|
|
if isinstance(a, dict) and isinstance(b, dict):
|
||
|
|
out = dict(a)
|
||
|
|
for k, v in b.items():
|
||
|
|
out[k] = _merge(out.get(k), v)
|
||
|
|
return out
|
||
|
|
return b
|
||
|
|
|
||
|
|
merged = _merge(base, patch) if patch else base
|
||
|
|
return type(self).model_validate(merged)
|
||
|
|
|
||
|
|
|
||
|
|
def init_config(log: bool = False):
|
||
|
|
"""Initialize the configuration file if it doesn't exist."""
|
||
|
|
if not os.path.exists(config_file):
|
||
|
|
os.makedirs(config_dir, exist_ok=True)
|
||
|
|
with open(config_file, "w", encoding="utf-8") as f:
|
||
|
|
f.write(DEFAULT_CONFIG_STR)
|
||
|
|
logger.info(f"Created default config file at {config_file}")
|
||
|
|
elif log:
|
||
|
|
logger.info(f"Config file already exists at {config_file}")
|
||
|
|
|
||
|
|
if not os.path.exists(config_translation_dir):
|
||
|
|
os.makedirs(config_translation_dir, exist_ok=True)
|
||
|
|
logger.info(
|
||
|
|
f"Created default translation directory at {config_translation_dir}"
|
||
|
|
)
|
||
|
|
|
||
|
|
for file in os.listdir(TRANSLATIONS_DIR):
|
||
|
|
if file.endswith(".json"):
|
||
|
|
dst = os.path.join(config_translation_dir, file)
|
||
|
|
if not os.path.exists(dst):
|
||
|
|
src = os.path.join(TRANSLATIONS_DIR, file)
|
||
|
|
with open(src, encoding="utf-8") as f:
|
||
|
|
translation = json.load(f)
|
||
|
|
with open(dst, "w", encoding="utf-8") as f:
|
||
|
|
json.dump(translation, f, indent=4)
|
||
|
|
logger.info(f"Created default translation file at {dst}")
|
||
|
|
|
||
|
|
|
||
|
|
def load_module(target: str, force_refresh: bool = False):
|
||
|
|
"""Load the specified module."""
|
||
|
|
|
||
|
|
# Get the target's directory
|
||
|
|
target_dir = os.path.dirname(os.path.abspath(target))
|
||
|
|
|
||
|
|
# Add the target's directory to the Python path
|
||
|
|
sys.path.insert(0, target_dir)
|
||
|
|
|
||
|
|
if force_refresh:
|
||
|
|
# Get current site packages dirs
|
||
|
|
site_package_dirs = site.getsitepackages()
|
||
|
|
|
||
|
|
# Clear the modules related to the app from sys.modules
|
||
|
|
for module_name, module in list(sys.modules.items()):
|
||
|
|
if (
|
||
|
|
hasattr(module, "__file__")
|
||
|
|
and module.__file__
|
||
|
|
and module.__file__.startswith(target_dir)
|
||
|
|
and not any(module.__file__.startswith(p) for p in site_package_dirs)
|
||
|
|
):
|
||
|
|
sys.modules.pop(module_name, None)
|
||
|
|
|
||
|
|
spec = util.spec_from_file_location(target, target)
|
||
|
|
if not spec or not spec.loader:
|
||
|
|
sys.path.pop(0)
|
||
|
|
return
|
||
|
|
|
||
|
|
module = util.module_from_spec(spec)
|
||
|
|
if not module:
|
||
|
|
sys.path.pop(0)
|
||
|
|
return
|
||
|
|
|
||
|
|
spec.loader.exec_module(module)
|
||
|
|
|
||
|
|
sys.modules[target] = module
|
||
|
|
|
||
|
|
# Remove the target's directory from the Python path
|
||
|
|
sys.path.pop(0)
|
||
|
|
|
||
|
|
|
||
|
|
def load_settings():
|
||
|
|
with open(config_file, "rb") as f:
|
||
|
|
toml_dict = tomli.load(f)
|
||
|
|
# Load project settings
|
||
|
|
project_config = toml_dict.get("project", {})
|
||
|
|
features_settings = toml_dict.get("features", {})
|
||
|
|
ui_settings = toml_dict.get("UI", {})
|
||
|
|
meta = toml_dict.get("meta")
|
||
|
|
|
||
|
|
if not meta or meta.get("generated_by") <= "0.3.0":
|
||
|
|
raise ValueError(
|
||
|
|
f"Your config file '{config_file}' is outdated. Please delete it and restart the app to regenerate it."
|
||
|
|
)
|
||
|
|
|
||
|
|
lc_cache_path = os.path.join(config_dir, ".langchain.db")
|
||
|
|
|
||
|
|
project_settings = ProjectSettings(
|
||
|
|
lc_cache_path=lc_cache_path,
|
||
|
|
**project_config,
|
||
|
|
)
|
||
|
|
|
||
|
|
features_settings = FeaturesSettings(**features_settings)
|
||
|
|
|
||
|
|
ui_settings = UISettings(**ui_settings)
|
||
|
|
|
||
|
|
code_settings = CodeSettings(action_callbacks={})
|
||
|
|
|
||
|
|
return {
|
||
|
|
"features": features_settings,
|
||
|
|
"ui": ui_settings,
|
||
|
|
"project": project_settings,
|
||
|
|
"code": code_settings,
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
def reload_config():
|
||
|
|
"""Reload the configuration from the config file."""
|
||
|
|
global config
|
||
|
|
if config is None:
|
||
|
|
return
|
||
|
|
|
||
|
|
# Preserve the module_name during config reload to ensure hot reload works
|
||
|
|
original_module_name = config.run.module_name if config.run else None
|
||
|
|
|
||
|
|
new_cfg = ChainlitConfig(**load_settings())
|
||
|
|
config.root = new_cfg.root
|
||
|
|
config.chainlit_server = new_cfg.chainlit_server
|
||
|
|
config.run = new_cfg.run
|
||
|
|
config.features = new_cfg.features
|
||
|
|
config.ui = new_cfg.ui
|
||
|
|
|
||
|
|
# Restore the preserved module_name
|
||
|
|
if original_module_name and config.run:
|
||
|
|
config.run.module_name = original_module_name
|
||
|
|
config.project = new_cfg.project
|
||
|
|
config.code = new_cfg.code
|
||
|
|
|
||
|
|
|
||
|
|
def load_config():
|
||
|
|
"""Load the configuration from the config file."""
|
||
|
|
init_config()
|
||
|
|
settings = load_settings()
|
||
|
|
return ChainlitConfig(**settings)
|
||
|
|
|
||
|
|
|
||
|
|
def lint_translations():
|
||
|
|
# Load the ground truth (en-US.json file from chainlit source code)
|
||
|
|
src = os.path.join(TRANSLATIONS_DIR, "en-US.json")
|
||
|
|
with open(src, encoding="utf-8") as f:
|
||
|
|
truth = json.load(f)
|
||
|
|
|
||
|
|
# Find the local app translations
|
||
|
|
for file in os.listdir(config_translation_dir):
|
||
|
|
if file.endswith(".json"):
|
||
|
|
# Load the translation file
|
||
|
|
to_lint = os.path.join(config_translation_dir, file)
|
||
|
|
with open(to_lint, encoding="utf-8") as f2:
|
||
|
|
translation = json.load(f2)
|
||
|
|
|
||
|
|
# Lint the translation file
|
||
|
|
lint_translation_json(file, truth, translation)
|
||
|
|
|
||
|
|
|
||
|
|
config = load_config()
|