chore: Add PyPI package support for uvx/pip installation (#1764)

* Initial plan

* Add package structure and resource path utilities

- Created langbot/ package with __init__.py and __main__.py entry point
- Added paths utility to find frontend and resource files from package installation
- Updated config loading to use resource paths
- Updated frontend serving to use resource paths
- Added MANIFEST.in for package data inclusion
- Updated pyproject.toml with build system and entry points

Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com>

* Add PyPI publishing workflow and update license

- Created GitHub Actions workflow to build frontend and publish to PyPI
- Added license field to pyproject.toml to fix deprecation warning
- Updated .gitignore to exclude build artifacts
- Tested package building successfully

Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com>

* Add PyPI installation documentation

- Created PYPI_INSTALLATION.md with detailed installation and usage instructions
- Updated README.md to feature uvx/pip installation as recommended method
- Updated README_EN.md with same changes for English documentation

Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com>

* Address code review feedback

- Made package-data configuration more specific to langbot package only
- Improved path detection with caching to avoid repeated file I/O
- Removed sys.path searching which was incorrect for package data
- Removed interactive input() call for non-interactive environment compatibility
- Simplified error messages for version check

Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com>

* Fix code review issues

- Use specific exception types instead of bare except
- Fix misleading comments about directory levels
- Remove redundant existence check before makedirs with exist_ok=True
- Use context manager for file opening to ensure proper cleanup

Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com>

* Simplify package configuration and document behavioral differences

- Removed redundant package-data configuration, relying on MANIFEST.in
- Added documentation about behavioral differences between package and source installation
- Clarified that include-package-data=true uses MANIFEST.in for data files

Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com>

* chore: update pyproject.toml

* chore: try pack templates in langbot/

* chore: update

* chore: update

* chore: update

* chore: update

* chore: update

* chore: adjust dir structure

* chore: fix imports

* fix: read default-pipeline-config.json

* fix: read default-pipeline-config.json

* fix: tests

* ci: publish pypi

* chore: bump version 4.6.0-beta.1 for testing

* chore: add templates/**

* fix: send adapters and requesters icons

* chore: bump version 4.6.0b2 for testing

* chore: add platform field for docker-compose.yaml

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com>
Co-authored-by: Junyan Qin <rockchinq@gmail.com>
This commit is contained in:
Copilot
2025-11-16 19:53:01 +08:00
committed by GitHub
parent 6a24c951e0
commit e642ffa5b3
477 changed files with 1001 additions and 1002 deletions
+9
View File
@@ -0,0 +1,9 @@
from __future__ import annotations
from ...config import manager as config_mgr
load_python_module_config = config_mgr.load_python_module_config
load_json_config = config_mgr.load_json_config
load_yaml_config = config_mgr.load_yaml_config
+79
View File
@@ -0,0 +1,79 @@
import pip
import os
from ...utils import pkgmgr
# Check dependencies to prevent users from not installing
# Left is the import name, right is the dependency name
required_deps = {
'requests': 'requests',
'openai': 'openai',
'anthropic': 'anthropic',
'colorlog': 'colorlog',
'aiocqhttp': 'aiocqhttp',
'botpy': 'qq-botpy-rc',
'PIL': 'pillow',
'nakuru': 'nakuru-project-idk',
'tiktoken': 'tiktoken',
'yaml': 'pyyaml',
'aiohttp': 'aiohttp',
'psutil': 'psutil',
'async_lru': 'async-lru',
'ollama': 'ollama',
'quart': 'quart',
'quart_cors': 'quart-cors',
'sqlalchemy': 'sqlalchemy[asyncio]',
'aiosqlite': 'aiosqlite',
'aiofiles': 'aiofiles',
'aioshutil': 'aioshutil',
'argon2': 'argon2-cffi',
'jwt': 'pyjwt',
'Crypto': 'pycryptodome',
'lark_oapi': 'lark-oapi',
'discord': 'discord.py',
'cryptography': 'cryptography',
'gewechat_client': 'gewechat-client',
'dingtalk_stream': 'dingtalk_stream',
'dashscope': 'dashscope',
'telegram': 'python-telegram-bot',
'certifi': 'certifi',
'mcp': 'mcp',
'sqlmodel': 'sqlmodel',
'telegramify_markdown': 'telegramify-markdown',
'slack_sdk': 'slack_sdk',
'asyncpg': 'asyncpg',
}
async def check_deps() -> list[str]:
global required_deps
missing_deps = []
for dep in required_deps:
try:
__import__(dep)
except ImportError:
missing_deps.append(dep)
return missing_deps
async def install_deps(deps: list[str]):
global required_deps
for dep in deps:
pip.main(['install', required_deps[dep]])
async def precheck_plugin_deps():
print('[Startup] Prechecking plugin dependencies...')
# Only execute plugin dependency installation when the plugins directory exists
if os.path.exists('plugins'):
for dir in os.listdir('plugins'):
subdir = os.path.join('plugins', dir)
if not os.path.isdir(subdir):
continue
if 'requirements.txt' in os.listdir(subdir):
pkgmgr.install_requirements(
os.path.join(subdir, 'requirements.txt'),
extra_params=[],
)
+36
View File
@@ -0,0 +1,36 @@
from __future__ import annotations
import os
import shutil
required_files = {
'data/config.yaml': 'templates/config.yaml',
}
required_paths = [
'temp',
'data',
'data/metadata',
'data/logs',
'data/labels',
]
async def generate_files() -> list[str]:
global required_files, required_paths
from ...utils import paths as path_utils
for required_paths in required_paths:
if not os.path.exists(required_paths):
os.mkdir(required_paths)
generated_files = []
for file in required_files:
if not os.path.exists(file):
template_path = path_utils.get_resource_path(required_files[file])
shutil.copyfile(template_path, file)
generated_files.append(file)
return generated_files
+67
View File
@@ -0,0 +1,67 @@
import logging
import sys
import time
import colorlog
from ...utils import constants
log_colors_config = {
'DEBUG': 'green', # cyan white
'INFO': 'white',
'WARNING': 'yellow',
'ERROR': 'red',
'CRITICAL': 'cyan',
}
async def init_logging(extra_handlers: list[logging.Handler] = None) -> logging.Logger:
# Remove all existing loggers
for handler in logging.root.handlers[:]:
logging.root.removeHandler(handler)
level = logging.INFO
if constants.debug_mode:
level = logging.DEBUG
log_file_name = 'data/logs/langbot-%s.log' % time.strftime('%Y-%m-%d', time.localtime())
qcg_logger = logging.getLogger('langbot')
qcg_logger.setLevel(level)
color_formatter = colorlog.ColoredFormatter(
fmt='%(log_color)s[%(asctime)s.%(msecs)03d] %(filename)s (%(lineno)d) - [%(levelname)s] : %(message)s',
datefmt='%m-%d %H:%M:%S',
log_colors=log_colors_config,
)
stream_handler = logging.StreamHandler(sys.stdout)
# stream_handler.setLevel(level)
# stream_handler.setFormatter(color_formatter)
stream_handler.stream = open(sys.stdout.fileno(), mode='w', encoding='utf-8', buffering=1)
log_handlers: list[logging.Handler] = [
stream_handler,
logging.FileHandler(log_file_name, encoding='utf-8'),
]
log_handlers += extra_handlers if extra_handlers is not None else []
for handler in log_handlers:
handler.setLevel(level)
handler.setFormatter(color_formatter)
qcg_logger.addHandler(handler)
qcg_logger.debug('Logging initialized, log level: %s' % level)
logging.basicConfig(
level=logging.CRITICAL, # Set log output format
format='[DEPR][%(asctime)s.%(msecs)03d] %(pathname)s (%(lineno)d) - [%(levelname)s] :\n%(message)s',
# Log output format
# -8 is a placeholder, left-align the output, and output length is 8
datefmt='%Y-%m-%d %H:%M:%S', # Time output format
handlers=[logging.NullHandler()],
)
return qcg_logger