mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-02 12:05:54 +00:00
Compare commits
115 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d6c10763a8 | ||
|
|
9df50d2cab | ||
|
|
6c6b510a0a | ||
|
|
063dc6fe97 | ||
|
|
42caae1bcf | ||
|
|
aa09a27a63 | ||
|
|
96e32a10e2 | ||
|
|
9a9f0eaa7d | ||
|
|
f5dea3c64c | ||
|
|
e213046302 | ||
|
|
41d31d77d8 | ||
|
|
6fb7fc80cc | ||
|
|
7bee5ff2f8 | ||
|
|
afe82ebdfd | ||
|
|
65c10ea54b | ||
|
|
ff0023c6c2 | ||
|
|
0e17d869ab | ||
|
|
7ec41bb91a | ||
|
|
da164c214e | ||
|
|
32a5de9bbb | ||
|
|
1b12b1fc35 | ||
|
|
caa1ed9d6a | ||
|
|
05f40e72ff | ||
|
|
27fb22d7be | ||
|
|
ca504384d2 | ||
|
|
b7e1e43fbd | ||
|
|
deabb19389 | ||
|
|
809035daac | ||
|
|
1eac87b89f | ||
|
|
70a2d137f0 | ||
|
|
c72b785c1f | ||
|
|
8588199640 | ||
|
|
2e42cd2faf | ||
|
|
7b3555af45 | ||
|
|
e12a77ca05 | ||
|
|
9ce3ad8300 | ||
|
|
1f60d9c3d6 | ||
|
|
d855d29c15 | ||
|
|
18083e9160 | ||
|
|
7f9e8ecac1 | ||
|
|
995c852f0a | ||
|
|
682962cc47 | ||
|
|
24e90a7f9b | ||
|
|
6a5a7182db | ||
|
|
c581c8e809 | ||
|
|
ffd2423920 | ||
|
|
c388339bd5 | ||
|
|
28492a62bb | ||
|
|
6a687ebeeb | ||
|
|
29dfae1518 | ||
|
|
791877d391 | ||
|
|
8fd0c3cc18 | ||
|
|
10dd8c86d0 | ||
|
|
c2574bdd3a | ||
|
|
d2d7892325 | ||
|
|
6d858475d7 | ||
|
|
59d55b382d | ||
|
|
8c17e55913 | ||
|
|
af509fe61f | ||
|
|
87e2a2099a | ||
|
|
3f22f62332 | ||
|
|
d1ee5f931a | ||
|
|
35506dd2bb | ||
|
|
2f06321ebf | ||
|
|
023281ae56 | ||
|
|
50dff55217 | ||
|
|
3204292360 | ||
|
|
e0d72969e3 | ||
|
|
a65b7ad413 | ||
|
|
45df44e01b | ||
|
|
d8addb105a | ||
|
|
f17ccad665 | ||
|
|
120ceb0b55 | ||
|
|
8a6f80a181 | ||
|
|
b19e468668 | ||
|
|
aeac79e1b3 | ||
|
|
b89a240250 | ||
|
|
13f42857f5 | ||
|
|
61f3f31edc | ||
|
|
3663d9dc10 | ||
|
|
89ec86c530 | ||
|
|
d9ba2a17ff | ||
|
|
c4ea6188f9 | ||
|
|
5d9f6ec763 | ||
|
|
b73847f1a6 | ||
|
|
d6e1e79f07 | ||
|
|
525008b8b2 | ||
|
|
bbf77bac4c | ||
|
|
f4ae829f59 | ||
|
|
3af8c13fab | ||
|
|
a8f7924867 | ||
|
|
77047e87d6 | ||
|
|
24d865bcd3 | ||
|
|
81ec7c201c | ||
|
|
fc6e414be4 | ||
|
|
e60cb6ad0e | ||
|
|
c90f2d6a12 | ||
|
|
fe8a738cd7 | ||
|
|
604cc53973 | ||
|
|
195b694ecc | ||
|
|
ee2d4e3ab9 | ||
|
|
d21f23beee | ||
|
|
558587883b | ||
|
|
2e6a1daf4f | ||
|
|
1fc5e75f93 | ||
|
|
a332206ba3 | ||
|
|
8e620dc635 | ||
|
|
c9a21ebace | ||
|
|
a05cdcac50 | ||
|
|
ecfb2bfb34 | ||
|
|
e17dba0a98 | ||
|
|
6b138943ce | ||
|
|
eb0e6aff68 | ||
|
|
4d0095626a | ||
|
|
aa0a501ade |
2
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
2
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
@@ -19,7 +19,7 @@ body:
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: 复现步骤
|
||||
description: 提供越多信息,我们会越快解决问题,建议多提供配置截图;**如果你不认真填写(只一两句话概括),我们会很生气并且立即关闭 issue 或两年后才回复你**
|
||||
description: 提供越多信息,我们会越快解决问题,建议多提供配置截图;**如果涉及 Dify、n8n、Langflow 等外部平台,请提供应用的导出文件(如 Dify 应用的 DSL),我们将更快回复您。**
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/bug-report_en.yml
vendored
2
.github/ISSUE_TEMPLATE/bug-report_en.yml
vendored
@@ -19,7 +19,7 @@ body:
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Reproduction steps
|
||||
description: How to reproduce this problem, the more detailed the better; the more information you provide, the faster we will solve the problem. 【注意】请务必认真填写此部分,若不提供完整信息(如只有一两句话的概括),我们将不会回复!
|
||||
description: How to reproduce this problem, the more detailed the better; the more information you provide, the faster we will solve the problem.
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
|
||||
60
.github/workflows/lint.yml
vendored
Normal file
60
.github/workflows/lint.yml
vendored
Normal file
@@ -0,0 +1,60 @@
|
||||
name: Lint
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- master
|
||||
- dev
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, ready_for_review]
|
||||
|
||||
jobs:
|
||||
ruff:
|
||||
name: Ruff Lint & Format
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.12'
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v4
|
||||
|
||||
- name: Install dependencies
|
||||
run: uv sync --dev
|
||||
|
||||
- name: Run ruff check
|
||||
run: uv run ruff check src
|
||||
|
||||
- name: Run ruff format
|
||||
run: uv run ruff format src --check
|
||||
|
||||
frontend:
|
||||
name: Frontend Lint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '25'
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: web
|
||||
run: pnpm install
|
||||
|
||||
- name: Run lint
|
||||
working-directory: web
|
||||
run: pnpm lint
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -42,7 +42,6 @@ botpy.log*
|
||||
test.py
|
||||
/web_ui
|
||||
.venv/
|
||||
uv.lock
|
||||
/test
|
||||
plugins.bak
|
||||
coverage.xml
|
||||
|
||||
229
README.md
229
README.md
@@ -1,49 +1,64 @@
|
||||
|
||||
<p align="center">
|
||||
<a href="https://langbot.app">
|
||||
<img width="130" src="https://docs.langbot.app/langbot-logo.png" alt="LangBot"/>
|
||||
<img width="130" src="res/logo-blue.png" alt="LangBot"/>
|
||||
</a>
|
||||
|
||||
<div align="center">
|
||||
|
||||
<a href="https://hellogithub.com/repository/langbot-app/LangBot" target="_blank"><img src="https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=5ce8ae2aa4f74316bf393b57b952433c&claim_uid=gtmc6YWjMZkT21R" alt="Featured|HelloGitHub" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||
<a href="https://www.producthunt.com/products/langbot?utm_source=badge-follow&utm_medium=badge&utm_source=badge-langbot" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/follow.svg?product_id=1077185&theme=light" alt="LangBot - Production-grade IM bot made easy. | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||
|
||||
<h3>使用 LangBot 快速构建、调试、部署即时通信机器人。</h3>
|
||||
<h3>Production-grade platform for building agentic IM bots.</h3>
|
||||
<h4>Quickly build, debug, and ship AI bots to Slack, Discord, Telegram, WeChat, and more.</h4>
|
||||
|
||||
[English](README_EN.md) / 简体中文 / [繁體中文](README_TW.md) / [日本語](README_JP.md) / [Español](README_ES.md) / [Français](README_FR.md) / [한국어](README_KO.md) / [Русский](README_RU.md) / [Tiếng Việt](README_VI.md)
|
||||
English / [简体中文](README_CN.md) / [繁體中文](README_TW.md) / [日本語](README_JP.md) / [Español](README_ES.md) / [Français](README_FR.md) / [한국어](README_KO.md) / [Русский](README_RU.md) / [Tiếng Việt](README_VI.md)
|
||||
|
||||
[](https://discord.gg/wdNEHETs87)
|
||||
[](https://qm.qq.com/q/DxZZcNxM1W)
|
||||
[](https://deepwiki.com/langbot-app/LangBot)
|
||||
[](https://github.com/langbot-app/LangBot/releases/latest)
|
||||
<img src="https://img.shields.io/badge/python-3.10 ~ 3.13 -blue.svg" alt="python">
|
||||
[](https://gitcode.com/RockChinQ/LangBot)
|
||||
[](https://github.com/langbot-app/LangBot/stargazers)
|
||||
|
||||
<a href="https://langbot.app">项目主页</a> |
|
||||
<a href="https://docs.langbot.app/zh/insight/features.html">规格特性</a> |
|
||||
<a href="https://docs.langbot.app/zh/insight/guide.html">部署文档</a> |
|
||||
<a href="https://docs.langbot.app/zh/tags/readme.html">API 集成</a> |
|
||||
<a href="https://space.langbot.app">插件市场</a> |
|
||||
<a href="https://langbot.featurebase.app/roadmap">路线图</a>
|
||||
<a href="https://langbot.app">Website</a> |
|
||||
<a href="https://docs.langbot.app/en/insight/features.html">Features</a> |
|
||||
<a href="https://docs.langbot.app/en/insight/guide.html">Docs</a> |
|
||||
<a href="https://docs.langbot.app/en/tags/readme.html">API</a> |
|
||||
<a href="https://space.langbot.app">Plugin Market</a> |
|
||||
<a href="https://langbot.featurebase.app/roadmap">Roadmap</a>
|
||||
|
||||
</div>
|
||||
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
## 📦 开始使用
|
||||
## What is LangBot?
|
||||
|
||||
#### 快速部署
|
||||
LangBot is an **open-source, production-grade platform** for building AI-powered instant messaging bots. It connects Large Language Models (LLMs) to any chat platform, enabling you to create intelligent agents that can converse, execute tasks, and integrate with your existing workflows.
|
||||
|
||||
使用 `uvx` 一键启动(需要先安装 [uv](https://docs.astral.sh/uv/getting-started/installation/)):
|
||||
### Key Capabilities
|
||||
|
||||
- **AI Conversations & Agents** — Multi-turn dialogues, tool calling, multi-modal support, streaming output. Built-in RAG (knowledge base) with deep integration to [Dify](https://dify.ai), [Coze](https://coze.com), [n8n](https://n8n.io), [Langflow](https://langflow.org).
|
||||
- **Universal IM Platform Support** — One codebase for Discord, Telegram, Slack, LINE, QQ, WeChat, WeCom, Lark, DingTalk, KOOK.
|
||||
- **Production-Ready** — Access control, rate limiting, sensitive word filtering, comprehensive monitoring, and exception handling. Trusted by enterprises.
|
||||
- **Plugin Ecosystem** — Hundreds of plugins, event-driven architecture, component extensions, and [MCP protocol](https://modelcontextprotocol.io/) support.
|
||||
- **Web Management Panel** — Configure, manage, and monitor your bots through an intuitive browser interface. No YAML editing required.
|
||||
- **Multi-Pipeline Architecture** — Different bots for different scenarios, with comprehensive monitoring and exception handling.
|
||||
|
||||
[→ Learn more about all features](https://docs.langbot.app/en/insight/features.html)
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
|
||||
### One-Line Launch
|
||||
|
||||
```bash
|
||||
uvx langbot
|
||||
```
|
||||
|
||||
访问 http://localhost:5300 即可开始使用。
|
||||
> Requires [uv](https://docs.astral.sh/uv/getting-started/installation/). Visit http://localhost:5300 — done.
|
||||
|
||||
#### Docker Compose 部署
|
||||
### Docker Compose
|
||||
|
||||
```bash
|
||||
git clone https://github.com/langbot-app/LangBot
|
||||
@@ -51,126 +66,102 @@ cd LangBot/docker
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
访问 http://localhost:5300 即可开始使用。
|
||||
|
||||
详细文档[Docker 部署](https://docs.langbot.app/zh/deploy/langbot/docker.html)。
|
||||
|
||||
#### 宝塔面板部署
|
||||
|
||||
已上架宝塔面板,若您已安装宝塔面板,可以根据[文档](https://docs.langbot.app/zh/deploy/langbot/one-click/bt.html)使用。
|
||||
|
||||
#### Zeabur 云部署
|
||||
|
||||
社区贡献的 Zeabur 模板。
|
||||
|
||||
[](https://zeabur.com/zh-CN/templates/ZKTBDH)
|
||||
|
||||
#### Railway 云部署
|
||||
### One-Click Cloud Deploy
|
||||
|
||||
[](https://zeabur.com/en-US/templates/ZKTBDH)
|
||||
[](https://railway.app/template/yRrAyL?referralCode=vogKPF)
|
||||
|
||||
#### 手动部署
|
||||
**More options:** [Docker](https://docs.langbot.app/en/deploy/langbot/docker.html) · [Manual](https://docs.langbot.app/en/deploy/langbot/manual.html) · [BTPanel](https://docs.langbot.app/en/deploy/langbot/one-click/bt.html) · [Kubernetes](./docker/README_K8S.md)
|
||||
|
||||
直接使用发行版运行,查看文档[手动部署](https://docs.langbot.app/zh/deploy/langbot/manual.html)。
|
||||
---
|
||||
|
||||
#### Kubernetes 部署
|
||||
## Supported Platforms
|
||||
|
||||
参考 [Kubernetes 部署](./docker/README_K8S.md) 文档。
|
||||
|
||||
## 😎 保持更新
|
||||
|
||||
点击仓库右上角 Star 和 Watch 按钮,获取最新动态。
|
||||
|
||||

|
||||
|
||||
## ✨ 特性
|
||||
|
||||
<img width="500" src="https://docs.langbot.app/ui/bot-page-zh-rounded.png" />
|
||||
|
||||
|
||||
- 💬 大模型对话、Agent:支持多种大模型,适配群聊和私聊;具有多轮对话、工具调用、多模态、流式输出能力,自带 RAG(知识库)实现,并深度适配 [Dify](https://dify.ai)、[Coze](https://coze.com)、[n8n](https://n8n.io)等 LLMOps 平台。
|
||||
- 🤖 多平台支持:目前支持 QQ、QQ频道、企业微信、个人微信、飞书、Discord、Telegram、KOOK、Slack、LINE 等平台。
|
||||
- 🛠️ 高稳定性、功能完备:原生支持访问控制、限速、敏感词过滤等机制;配置简单,支持多种部署方式。支持多流水线配置,不同机器人用于不同应用场景。
|
||||
- 🧩 插件扩展、活跃社区:高稳定性、高安全性的生产级插件系统,支持事件驱动、组件扩展等插件机制;适配 Anthropic [MCP 协议](https://modelcontextprotocol.io/);目前已有数百个插件。
|
||||
- 😻 Web 管理面板:支持通过浏览器管理 LangBot 实例,不再需要手动编写配置文件。
|
||||
|
||||
详细规格特性请访问[文档](https://docs.langbot.app/zh/insight/features.html)。
|
||||
|
||||
或访问 demo 环境:https://demo.langbot.dev/
|
||||
- 登录信息:邮箱:`demo@langbot.app` 密码:`langbot123456`
|
||||
- 注意:仅展示 WebUI 效果,公开环境,请不要在其中填入您的任何敏感信息。
|
||||
|
||||
### 消息平台
|
||||
|
||||
| 平台 | 状态 | 备注 |
|
||||
| --- | --- | --- |
|
||||
| QQ 个人号 | ✅ | QQ 个人号私聊、群聊 |
|
||||
| QQ 官方机器人 | ✅ | QQ 官方机器人,支持频道、私聊、群聊 |
|
||||
| 企业微信 | ✅ | |
|
||||
| 企微对外客服 | ✅ | |
|
||||
| 企微智能机器人 | ✅ | |
|
||||
| 个人微信 | ✅ | |
|
||||
| 微信公众号 | ✅ | |
|
||||
| 飞书 | ✅ | |
|
||||
| 钉钉 | ✅ | |
|
||||
| KOOK | ✅ | |
|
||||
| Platform | Status | Notes |
|
||||
|----------|--------|-------|
|
||||
| Discord | ✅ | |
|
||||
| Telegram | ✅ | |
|
||||
| Slack | ✅ | |
|
||||
| LINE | ✅ | |
|
||||
| QQ | ✅ | Personal & Official API |
|
||||
| WeCom | ✅ | Enterprise WeChat, External CS, AI Bot |
|
||||
| WeChat | ✅ | Personal & Official Account |
|
||||
| Lark | ✅ | |
|
||||
| DingTalk | ✅ | |
|
||||
| KOOK | ✅ | |
|
||||
| Satori | ✅ | |
|
||||
|
||||
### 大模型能力
|
||||
---
|
||||
|
||||
| 模型 | 状态 | 备注 |
|
||||
| --- | --- | --- |
|
||||
| [OpenAI](https://platform.openai.com/) | ✅ | 可接入任何 OpenAI 接口格式模型 |
|
||||
| [DeepSeek](https://www.deepseek.com/) | ✅ | |
|
||||
| [Moonshot](https://www.moonshot.cn/) | ✅ | |
|
||||
| [Anthropic](https://www.anthropic.com/) | ✅ | |
|
||||
| [xAI](https://x.ai/) | ✅ | |
|
||||
| [智谱AI](https://open.bigmodel.cn/) | ✅ | |
|
||||
| [胜算云](https://www.shengsuanyun.com/?from=CH_KYIPP758) | ✅ | 全球大模型都可调用(友情推荐) |
|
||||
| [优云智算](https://www.compshare.cn/?ytag=GPU_YY-gh_langbot) | ✅ | 大模型和 GPU 资源平台 |
|
||||
| [PPIO](https://ppinfra.com/user/register?invited_by=QJKFYD&utm_source=github_langbot) | ✅ | 大模型和 GPU 资源平台 |
|
||||
| [接口 AI](https://jiekou.ai/) | ✅ | 大模型聚合平台,专注全球大模型接入 |
|
||||
| [302.AI](https://share.302.ai/SuTG99) | ✅ | 大模型聚合平台 |
|
||||
| [Google Gemini](https://aistudio.google.com/prompts/new_chat) | ✅ | |
|
||||
| [Dify](https://dify.ai) | ✅ | LLMOps 平台 |
|
||||
| [Ollama](https://ollama.com/) | ✅ | 本地大模型运行平台 |
|
||||
| [LMStudio](https://lmstudio.ai/) | ✅ | 本地大模型运行平台 |
|
||||
| [GiteeAI](https://ai.gitee.com/) | ✅ | 大模型接口聚合平台 |
|
||||
| [SiliconFlow](https://siliconflow.cn/) | ✅ | 大模型聚合平台 |
|
||||
| [小马算力](https://www.tokenpony.cn/453z1) | ✅ | 大模型聚合平台 |
|
||||
| [阿里云百炼](https://bailian.console.aliyun.com/) | ✅ | 大模型聚合平台, LLMOps 平台 |
|
||||
| [火山方舟](https://console.volcengine.com/ark/region:ark+cn-beijing/model?vendor=Bytedance&view=LIST_VIEW) | ✅ | 大模型聚合平台, LLMOps 平台 |
|
||||
| [ModelScope](https://modelscope.cn/docs/model-service/API-Inference/intro) | ✅ | 大模型聚合平台 |
|
||||
| [MCP](https://modelcontextprotocol.io/) | ✅ | 支持通过 MCP 协议获取工具 |
|
||||
| [百宝箱Tbox](https://www.tbox.cn/open) | ✅ | 蚂蚁百宝箱智能体平台,每月免费10亿大模型Token |
|
||||
## Supported LLMs & Integrations
|
||||
|
||||
### TTS
|
||||
| Provider | Type | Status |
|
||||
|----------|------|--------|
|
||||
| [OpenAI](https://platform.openai.com/) | LLM | ✅ |
|
||||
| [Anthropic](https://www.anthropic.com/) | LLM | ✅ |
|
||||
| [DeepSeek](https://www.deepseek.com/) | LLM | ✅ |
|
||||
| [Google Gemini](https://aistudio.google.com/prompts/new_chat) | LLM | ✅ |
|
||||
| [xAI](https://x.ai/) | LLM | ✅ |
|
||||
| [Moonshot](https://www.moonshot.cn/) | LLM | ✅ |
|
||||
| [Zhipu AI](https://open.bigmodel.cn/) | LLM | ✅ |
|
||||
| [Ollama](https://ollama.com/) | Local LLM | ✅ |
|
||||
| [LM Studio](https://lmstudio.ai/) | Local LLM | ✅ |
|
||||
| [Dify](https://dify.ai) | LLMOps | ✅ |
|
||||
| [MCP](https://modelcontextprotocol.io/) | Protocol | ✅ |
|
||||
| [SiliconFlow](https://siliconflow.cn/) | Gateway | ✅ |
|
||||
| [Aliyun Bailian](https://bailian.console.aliyun.com/) | Gateway | ✅ |
|
||||
| [Volc Engine Ark](https://console.volcengine.com/ark/region:ark+cn-beijing/model?vendor=Bytedance&view=LIST_VIEW) | Gateway | ✅ |
|
||||
| [ModelScope](https://modelscope.cn/docs/model-service/API-Inference/intro) | Gateway | ✅ |
|
||||
| [GiteeAI](https://ai.gitee.com/) | Gateway | ✅ |
|
||||
| [CompShare](https://www.compshare.cn/?ytag=GPU_YY-gh_langbot) | GPU Platform | ✅ |
|
||||
| [PPIO](https://ppinfra.com/user/register?invited_by=QJKFYD&utm_source=github_langbot) | GPU Platform | ✅ |
|
||||
| [ShengSuanYun](https://www.shengsuanyun.com/?from=CH_KYIPP758) | GPU Platform | ✅ |
|
||||
| [接口 AI](https://jiekou.ai/) | Gateway | ✅ |
|
||||
| [302.AI](https://share.302.ai/SuTG99) | Gateway | ✅ |
|
||||
|
||||
| 平台/模型 | 备注 |
|
||||
| --- | --- |
|
||||
| [FishAudio](https://fish.audio/zh-CN/discovery/) | [插件](https://github.com/the-lazy-me/NewChatVoice) |
|
||||
| [海豚 AI](https://www.ttson.cn/?source=thelazy) | [插件](https://github.com/the-lazy-me/NewChatVoice) |
|
||||
| [AzureTTS](https://portal.azure.com/) | [插件](https://github.com/Ingnaryk/LangBot_AzureTTS) |
|
||||
[→ View all integrations](https://docs.langbot.app/en/insight/features.html)
|
||||
|
||||
### 文生图
|
||||
---
|
||||
|
||||
| 平台/模型 | 备注 |
|
||||
| --- | --- |
|
||||
| 阿里云百炼 | [插件](https://github.com/Thetail001/LangBot_BailianTextToImagePlugin)
|
||||
## Why LangBot?
|
||||
|
||||
## 😘 社区贡献
|
||||
| Use Case | How LangBot Helps |
|
||||
|----------|-------------------|
|
||||
| **Customer Support** | Deploy AI agents to Slack/Discord/Telegram that answer questions using your knowledge base |
|
||||
| **Internal Tools** | Connect n8n/Dify workflows to WeCom/DingTalk for automated business processes |
|
||||
| **Community Management** | Moderate QQ/Discord groups with AI-powered content filtering and interaction |
|
||||
| **Multi-Platform Presence** | One bot, all platforms. Manage from a single dashboard |
|
||||
|
||||
感谢以下[代码贡献者](https://github.com/langbot-app/LangBot/graphs/contributors)和社区里其他成员对 LangBot 的贡献:
|
||||
---
|
||||
|
||||
## Live Demo
|
||||
|
||||
**Try it now:** https://demo.langbot.dev/
|
||||
- Email: `demo@langbot.app`
|
||||
- Password: `langbot123456`
|
||||
|
||||
*Note: Public demo environment. Do not enter sensitive information.*
|
||||
|
||||
---
|
||||
|
||||
## Community
|
||||
|
||||
[](https://discord.gg/wdNEHETs87)
|
||||
|
||||
- [Discord Community](https://discord.gg/wdNEHETs87)
|
||||
|
||||
---
|
||||
|
||||
## Star History
|
||||
|
||||
[](https://star-history.com/#langbot-app/LangBot&Date)
|
||||
|
||||
---
|
||||
|
||||
## Contributors
|
||||
|
||||
Thanks to all [contributors](https://github.com/langbot-app/LangBot/graphs/contributors) who have helped make LangBot better:
|
||||
|
||||
<a href="https://github.com/langbot-app/LangBot/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=langbot-app/LangBot" />
|
||||
</a>
|
||||
|
||||
<!--
|
||||
## For Code Agents
|
||||
|
||||
If you're a code agent based on LLM, you should read [AGENTS.md](AGENTS.md) to understand the project structure and development process before you start any development.
|
||||
-->
|
||||
|
||||
192
README_CN.md
Normal file
192
README_CN.md
Normal file
@@ -0,0 +1,192 @@
|
||||
<p align="center">
|
||||
<a href="https://langbot.app">
|
||||
<img width="130" src="res/logo-blue.png" alt="LangBot"/>
|
||||
</a>
|
||||
|
||||
<div align="center">
|
||||
|
||||
<a href="https://hellogithub.com/repository/langbot-app/LangBot" target="_blank"><img src="https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=5ce8ae2aa4f74316bf393b57b952433c&claim_uid=gtmc6YWjMZkT21R" alt="Featured|HelloGitHub" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||
|
||||
<h3>生产级 AI 即时通信机器人开发平台。</h3>
|
||||
<h4>快速构建、调试和部署 AI 机器人到微信、QQ、飞书、Slack、Discord、Telegram 等平台。</h4>
|
||||
|
||||
[English](README.md) / 简体中文 / [繁體中文](README_TW.md) / [日本語](README_JP.md) / [Español](README_ES.md) / [Français](README_FR.md) / [한국어](README_KO.md) / [Русский](README_RU.md) / [Tiếng Việt](README_VI.md)
|
||||
|
||||
[](https://discord.gg/wdNEHETs87)
|
||||
[](https://qm.qq.com/q/DxZZcNxM1W)
|
||||
[](https://deepwiki.com/langbot-app/LangBot)
|
||||
[](https://github.com/langbot-app/LangBot/releases/latest)
|
||||
<img src="https://img.shields.io/badge/python-3.10 ~ 3.13 -blue.svg" alt="python">
|
||||
[](https://github.com/langbot-app/LangBot/stargazers)
|
||||
[](https://gitcode.com/RockChinQ/LangBot)
|
||||
|
||||
<a href="https://langbot.app">官网</a> |
|
||||
<a href="https://docs.langbot.app/zh/insight/features.html">特性</a> |
|
||||
<a href="https://docs.langbot.app/zh/insight/guide.html">文档</a> |
|
||||
<a href="https://docs.langbot.app/zh/tags/readme.html">API</a> |
|
||||
<a href="https://space.langbot.app">插件市场</a> |
|
||||
<a href="https://langbot.featurebase.app/roadmap">路线图</a>
|
||||
|
||||
</div>
|
||||
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
## 什么是 LangBot?
|
||||
|
||||
LangBot 是一个**开源的生产级平台**,用于构建 AI 驱动的即时通信机器人。它将大语言模型(LLM)连接到各种聊天平台,帮助你创建能够对话、执行任务、并集成到现有工作流程中的智能 Agent。
|
||||
|
||||
### 核心能力
|
||||
|
||||
- **AI 对话与 Agent** — 多轮对话、工具调用、多模态、流式输出。自带 RAG(知识库),深度集成 [Dify](https://dify.ai)、[Coze](https://coze.com)、[n8n](https://n8n.io)、[Langflow](https://langflow.org) 等 LLMOps 平台。
|
||||
- **全平台支持** — 一套代码,覆盖 QQ、微信、企业微信、飞书、钉钉、Discord、Telegram、Slack、LINE、KOOK 等平台。
|
||||
- **生产就绪** — 访问控制、限速、敏感词过滤、全面监控与异常处理,已被多家企业采用。
|
||||
- **插件生态** — 数百个插件,事件驱动架构,组件扩展,适配 [MCP 协议](https://modelcontextprotocol.io/)。
|
||||
- **Web 管理面板** — 通过浏览器直观地配置、管理和监控机器人,无需手动编辑配置文件。
|
||||
- **多流水线架构** — 不同机器人用于不同场景,具备全面的监控和异常处理能力。
|
||||
|
||||
[→ 了解更多功能特性](https://docs.langbot.app/zh/insight/features.html)
|
||||
|
||||
---
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 一键启动
|
||||
|
||||
```bash
|
||||
uvx langbot
|
||||
```
|
||||
|
||||
> 需要安装 [uv](https://docs.astral.sh/uv/getting-started/installation/)。访问 http://localhost:5300 即可使用。
|
||||
|
||||
### Docker Compose
|
||||
|
||||
```bash
|
||||
git clone https://github.com/langbot-app/LangBot
|
||||
cd LangBot/docker
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
### 一键云部署
|
||||
|
||||
[](https://zeabur.com/zh-CN/templates/ZKTBDH)
|
||||
[](https://railway.app/template/yRrAyL?referralCode=vogKPF)
|
||||
|
||||
**更多方式:** [Docker](https://docs.langbot.app/zh/deploy/langbot/docker.html) · [手动部署](https://docs.langbot.app/zh/deploy/langbot/manual.html) · [宝塔面板](https://docs.langbot.app/zh/deploy/langbot/one-click/bt.html) · [Kubernetes](./docker/README_K8S.md)
|
||||
|
||||
---
|
||||
|
||||
## 支持的平台
|
||||
|
||||
| 平台 | 状态 | 备注 |
|
||||
|------|------|------|
|
||||
| QQ | ✅ | 个人号、官方机器人(频道、私聊、群聊) |
|
||||
| 微信 | ✅ | 个人微信、微信公众号 |
|
||||
| 企业微信 | ✅ | 应用消息、对外客服、智能机器人 |
|
||||
| 飞书 | ✅ | |
|
||||
| 钉钉 | ✅ | |
|
||||
| Discord | ✅ | |
|
||||
| Telegram | ✅ | |
|
||||
| Slack | ✅ | |
|
||||
| LINE | ✅ | |
|
||||
| KOOK | ✅ | |
|
||||
|
||||
---
|
||||
|
||||
## 支持的大模型与集成
|
||||
|
||||
| 提供商 | 类型 | 状态 |
|
||||
|--------|------|------|
|
||||
| [OpenAI](https://platform.openai.com/) | LLM | ✅ |
|
||||
| [Anthropic](https://www.anthropic.com/) | LLM | ✅ |
|
||||
| [DeepSeek](https://www.deepseek.com/) | LLM | ✅ |
|
||||
| [Google Gemini](https://aistudio.google.com/prompts/new_chat) | LLM | ✅ |
|
||||
| [xAI](https://x.ai/) | LLM | ✅ |
|
||||
| [Moonshot](https://www.moonshot.cn/) | LLM | ✅ |
|
||||
| [智谱AI](https://open.bigmodel.cn/) | LLM | ✅ |
|
||||
| [Ollama](https://ollama.com/) | 本地 LLM | ✅ |
|
||||
| [LM Studio](https://lmstudio.ai/) | 本地 LLM | ✅ |
|
||||
| [Dify](https://dify.ai) | LLMOps | ✅ |
|
||||
| [MCP](https://modelcontextprotocol.io/) | 协议 | ✅ |
|
||||
| [SiliconFlow](https://siliconflow.cn/) | 聚合平台 | ✅ |
|
||||
| [阿里云百炼](https://bailian.console.aliyun.com/) | 聚合平台 | ✅ |
|
||||
| [火山方舟](https://console.volcengine.com/ark/region:ark+cn-beijing/model?vendor=Bytedance&view=LIST_VIEW) | 聚合平台 | ✅ |
|
||||
| [ModelScope](https://modelscope.cn/docs/model-service/API-Inference/intro) | 聚合平台 | ✅ |
|
||||
| [GiteeAI](https://ai.gitee.com/) | 聚合平台 | ✅ |
|
||||
| [胜算云](https://www.shengsuanyun.com/?from=CH_KYIPP758) | GPU 平台 | ✅ |
|
||||
| [优云智算](https://www.compshare.cn/?ytag=GPU_YY-gh_langbot) | GPU 平台 | ✅ |
|
||||
| [PPIO](https://ppinfra.com/user/register?invited_by=QJKFYD&utm_source=github_langbot) | GPU 平台 | ✅ |
|
||||
| [接口 AI](https://jiekou.ai/) | 聚合平台 | ✅ |
|
||||
| [302.AI](https://share.302.ai/SuTG99) | 聚合平台 | ✅ |
|
||||
| [小马算力](https://www.tokenpony.cn/453z1) | 聚合平台 | ✅ |
|
||||
| [百宝箱Tbox](https://www.tbox.cn/open) | 智能体平台 | ✅ |
|
||||
|
||||
[→ 查看完整集成列表](https://docs.langbot.app/zh/insight/features.html)
|
||||
|
||||
### TTS(语音合成)
|
||||
|
||||
| 平台/模型 | 备注 |
|
||||
|-----------|------|
|
||||
| [FishAudio](https://fish.audio/zh-CN/discovery/) | [插件](https://github.com/the-lazy-me/NewChatVoice) |
|
||||
| [海豚 AI](https://www.ttson.cn/?source=thelazy) | [插件](https://github.com/the-lazy-me/NewChatVoice) |
|
||||
| [AzureTTS](https://portal.azure.com/) | [插件](https://github.com/Ingnaryk/LangBot_AzureTTS) |
|
||||
|
||||
### 文生图
|
||||
|
||||
| 平台/模型 | 备注 |
|
||||
|-----------|------|
|
||||
| 阿里云百炼 | [插件](https://github.com/Thetail001/LangBot_BailianTextToImagePlugin) |
|
||||
|
||||
---
|
||||
|
||||
## 为什么选择 LangBot?
|
||||
|
||||
| 使用场景 | LangBot 如何帮助 |
|
||||
|----------|------------------|
|
||||
| **客户服务** | 将 AI Agent 部署到微信/企微/钉钉/飞书,基于知识库自动回答用户问题 |
|
||||
| **内部工具** | 将 n8n/Dify 工作流接入企微/钉钉,实现业务流程自动化 |
|
||||
| **社群运营** | 在 QQ/Discord 群中使用 AI 驱动的内容审核与智能互动 |
|
||||
| **多平台触达** | 一个机器人,覆盖所有平台。通过统一面板集中管理 |
|
||||
|
||||
---
|
||||
|
||||
## 在线演示
|
||||
|
||||
**立即体验:** https://demo.langbot.dev/
|
||||
- 邮箱:`demo@langbot.app`
|
||||
- 密码:`langbot123456`
|
||||
|
||||
*注意:公开演示环境,请不要在其中填入任何敏感信息。*
|
||||
|
||||
---
|
||||
|
||||
## 社区
|
||||
|
||||
[](https://discord.gg/wdNEHETs87)
|
||||
[](https://qm.qq.com/q/DxZZcNxM1W)
|
||||
|
||||
- [Discord 社区](https://discord.gg/wdNEHETs87)
|
||||
- [QQ 社区群](https://qm.qq.com/q/DxZZcNxM1W)
|
||||
|
||||
---
|
||||
|
||||
## Star 趋势
|
||||
|
||||
[](https://star-history.com/#langbot-app/LangBot&Date)
|
||||
|
||||
---
|
||||
|
||||
## 贡献者
|
||||
|
||||
感谢所有[贡献者](https://github.com/langbot-app/LangBot/graphs/contributors)对 LangBot 的帮助:
|
||||
|
||||
<a href="https://github.com/langbot-app/LangBot/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=langbot-app/LangBot" />
|
||||
</a>
|
||||
|
||||
<!--
|
||||
## For Code Agents
|
||||
|
||||
If you're a code agent based on LLM, you should read [AGENTS.md](AGENTS.md) to understand the project structure and development process before you start any development.
|
||||
-->
|
||||
150
README_EN.md
150
README_EN.md
@@ -1,150 +0,0 @@
|
||||
<p align="center">
|
||||
<a href="https://langbot.app">
|
||||
<img width="130" src="https://docs.langbot.app/langbot-logo.png" alt="LangBot"/>
|
||||
</a>
|
||||
|
||||
<div align="center">
|
||||
|
||||
<a href="https://www.producthunt.com/products/langbot?utm_source=badge-follow&utm_medium=badge&utm_source=badge-langbot" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/follow.svg?product_id=1077185&theme=light" alt="LangBot - Production-grade IM bot made easy. | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||
|
||||
<h3>Quickly build, debug, and ship IM bots with LangBot.</h3>
|
||||
|
||||
English / [简体中文](README.md) / [繁體中文](README_TW.md) / [日本語](README_JP.md) / [Español](README_ES.md) / [Français](README_FR.md) / [한국어](README_KO.md) / [Русский](README_RU.md) / [Tiếng Việt](README_VI.md)
|
||||
|
||||
[](https://discord.gg/wdNEHETs87)
|
||||
[](https://deepwiki.com/langbot-app/LangBot)
|
||||
[](https://github.com/langbot-app/LangBot/releases/latest)
|
||||
<img src="https://img.shields.io/badge/python-3.10 ~ 3.13 -blue.svg" alt="python">
|
||||
|
||||
<a href="https://langbot.app">Home</a> |
|
||||
<a href="https://docs.langbot.app/en/insight/features.html">Features</a> |
|
||||
<a href="https://docs.langbot.app/en/insight/guide.html">Deployment</a> |
|
||||
<a href="https://docs.langbot.app/en/tags/readme.html">API Integration</a> |
|
||||
<a href="https://space.langbot.app">Plugin Market</a> |
|
||||
<a href="https://langbot.featurebase.app/roadmap">Roadmap</a>
|
||||
|
||||
</div>
|
||||
|
||||
</p>
|
||||
|
||||
|
||||
## 📦 Getting Started
|
||||
|
||||
#### Quick Start
|
||||
|
||||
Use `uvx` to start with one command (need to install [uv](https://docs.astral.sh/uv/getting-started/installation/)):
|
||||
|
||||
```bash
|
||||
uvx langbot
|
||||
```
|
||||
|
||||
Visit http://localhost:5300 to start using it.
|
||||
|
||||
#### Docker Compose Deployment
|
||||
|
||||
```bash
|
||||
git clone https://github.com/langbot-app/LangBot
|
||||
cd LangBot/docker
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
Visit http://localhost:5300 to start using it.
|
||||
|
||||
Detailed documentation [Docker Deployment](https://docs.langbot.app/en/deploy/langbot/docker.html).
|
||||
|
||||
#### One-click Deployment on BTPanel
|
||||
|
||||
LangBot has been listed on the BTPanel, if you have installed the BTPanel, you can use the [document](https://docs.langbot.app/en/deploy/langbot/one-click/bt.html) to use it.
|
||||
|
||||
#### Zeabur Cloud Deployment
|
||||
|
||||
Community contributed Zeabur template.
|
||||
|
||||
[](https://zeabur.com/en-US/templates/ZKTBDH)
|
||||
|
||||
#### Railway Cloud Deployment
|
||||
|
||||
[](https://railway.app/template/yRrAyL?referralCode=vogKPF)
|
||||
|
||||
#### Other Deployment Methods
|
||||
|
||||
Directly use the released version to run, see the [Manual Deployment](https://docs.langbot.app/en/deploy/langbot/manual.html) documentation.
|
||||
|
||||
#### Kubernetes Deployment
|
||||
|
||||
Refer to the [Kubernetes Deployment](./docker/README_K8S.md) documentation.
|
||||
|
||||
## 😎 Stay Ahead
|
||||
|
||||
Click the Star and Watch button in the upper right corner of the repository to get the latest updates.
|
||||
|
||||

|
||||
|
||||
## ✨ Features
|
||||
|
||||
<img width="500" src="https://docs.langbot.app/ui/bot-page-en-rounded.png" />
|
||||
|
||||
|
||||
- 💬 Chat with LLM / Agent: Supports multiple LLMs, adapt to group chats and private chats; Supports multi-round conversations, tool calls, multi-modal, and streaming output capabilities. Built-in RAG (knowledge base) implementation, and deeply integrates with [Dify](https://dify.ai), [Coze](https://coze.com), [n8n](https://n8n.io) etc. LLMOps platforms.
|
||||
- 🤖 Multi-platform Support: Currently supports QQ, QQ Channel, WeCom, personal WeChat, Lark, DingTalk, Discord, Telegram, KOOK, Slack, LINE, etc.
|
||||
- 🛠️ High Stability, Feature-rich: Native access control, rate limiting, sensitive word filtering, etc. mechanisms; Easy to use, supports multiple deployment methods. Supports multiple pipeline configurations, different bots can be used for different scenarios.
|
||||
- 🧩 Plugin Extension, Active Community: High stability, high security production-level plugin system; Support event-driven, component extension, etc. plugin mechanisms; Integrate Anthropic [MCP protocol](https://modelcontextprotocol.io/); Currently has hundreds of plugins.
|
||||
- 😻 Web UI: Support management LangBot instance through the browser. No need to manually write configuration files.
|
||||
|
||||
For more detailed specifications, please refer to the [documentation](https://docs.langbot.app/en/insight/features.html).
|
||||
|
||||
Or visit the demo environment: https://demo.langbot.dev/
|
||||
- Login information: Email: `demo@langbot.app` Password: `langbot123456`
|
||||
- Note: For WebUI demo only, please do not fill in any sensitive information in the public environment.
|
||||
|
||||
### Message Platform
|
||||
|
||||
| Platform | Status | Remarks |
|
||||
| --- | --- | --- |
|
||||
| Discord | ✅ | |
|
||||
| Telegram | ✅ | |
|
||||
| Slack | ✅ | |
|
||||
| LINE | ✅ | |
|
||||
| Personal QQ | ✅ | |
|
||||
| QQ Official API | ✅ | |
|
||||
| WeCom | ✅ | |
|
||||
| WeComCS | ✅ | |
|
||||
| WeCom AI Bot | ✅ | |
|
||||
| Personal WeChat | ✅ | |
|
||||
| Lark | ✅ | |
|
||||
| DingTalk | ✅ | |
|
||||
| KOOK | ✅ | |
|
||||
|
||||
### LLMs
|
||||
|
||||
| LLM | Status | Remarks |
|
||||
| --- | --- | --- |
|
||||
| [OpenAI](https://platform.openai.com/) | ✅ | Available for any OpenAI interface format model |
|
||||
| [DeepSeek](https://www.deepseek.com/) | ✅ | |
|
||||
| [Moonshot](https://www.moonshot.cn/) | ✅ | |
|
||||
| [Anthropic](https://www.anthropic.com/) | ✅ | |
|
||||
| [xAI](https://x.ai/) | ✅ | |
|
||||
| [Zhipu AI](https://open.bigmodel.cn/) | ✅ | |
|
||||
| [CompShare](https://www.compshare.cn/?ytag=GPU_YY-gh_langbot) | ✅ | LLM and GPU resource platform |
|
||||
| [Dify](https://dify.ai) | ✅ | LLMOps platform |
|
||||
| [PPIO](https://ppinfra.com/user/register?invited_by=QJKFYD&utm_source=github_langbot) | ✅ | LLM and GPU resource platform |
|
||||
| [接口 AI](https://jiekou.ai/) | ✅ | LLM aggregation platform, dedicated to global LLMs |
|
||||
| [ShengSuanYun](https://www.shengsuanyun.com/?from=CH_KYIPP758) | ✅ | LLM and GPU resource platform |
|
||||
| [302.AI](https://share.302.ai/SuTG99) | ✅ | LLM gateway(MaaS) |
|
||||
| [Google Gemini](https://aistudio.google.com/prompts/new_chat) | ✅ | |
|
||||
| [Ollama](https://ollama.com/) | ✅ | Local LLM running platform |
|
||||
| [LMStudio](https://lmstudio.ai/) | ✅ | Local LLM running platform |
|
||||
| [GiteeAI](https://ai.gitee.com/) | ✅ | LLM interface gateway(MaaS) |
|
||||
| [SiliconFlow](https://siliconflow.cn/) | ✅ | LLM gateway(MaaS) |
|
||||
| [Aliyun Bailian](https://bailian.console.aliyun.com/) | ✅ | LLM gateway(MaaS), LLMOps platform |
|
||||
| [Volc Engine Ark](https://console.volcengine.com/ark/region:ark+cn-beijing/model?vendor=Bytedance&view=LIST_VIEW) | ✅ | LLM gateway(MaaS), LLMOps platform |
|
||||
| [ModelScope](https://modelscope.cn/docs/model-service/API-Inference/intro) | ✅ | LLM gateway(MaaS) |
|
||||
| [MCP](https://modelcontextprotocol.io/) | ✅ | Support tool access through MCP protocol |
|
||||
|
||||
## 🤝 Community Contribution
|
||||
|
||||
Thank you for the following [code contributors](https://github.com/langbot-app/LangBot/graphs/contributors) and other members in the community for their contributions to LangBot:
|
||||
|
||||
<a href="https://github.com/langbot-app/LangBot/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=langbot-app/LangBot" />
|
||||
</a>
|
||||
193
README_ES.md
193
README_ES.md
@@ -1,25 +1,27 @@
|
||||
<p align="center">
|
||||
<a href="https://langbot.app">
|
||||
<img width="130" src="https://docs.langbot.app/langbot-logo.png" alt="LangBot"/>
|
||||
<img width="130" src="res/logo-blue.png" alt="LangBot"/>
|
||||
</a>
|
||||
|
||||
<div align="center">
|
||||
|
||||
<a href="https://www.producthunt.com/products/langbot?utm_source=badge-follow&utm_medium=badge&utm_source=badge-langbot" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/follow.svg?product_id=1077185&theme=light" alt="LangBot - Production-grade IM bot made easy. | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||
|
||||
<h3>Cree, depure y despliegue bots de mensajería instantánea rápidamente con LangBot.</h3>
|
||||
<h3>Plataforma de grado de producción para construir bots de mensajería instantánea con agentes de IA.</h3>
|
||||
<h4>Construya, depure y despliegue bots de IA rápidamente en Slack, Discord, Telegram, WeChat y más.</h4>
|
||||
|
||||
[English](README_EN.md) / [简体中文](README.md) / [繁體中文](README_TW.md) / [日本語](README_JP.md) / Español / [Français](README_FR.md) / [한국어](README_KO.md) / [Русский](README_RU.md) / [Tiếng Việt](README_VI.md)
|
||||
[English](README.md) / [简体中文](README_CN.md) / [繁體中文](README_TW.md) / [日本語](README_JP.md) / Español / [Français](README_FR.md) / [한국어](README_KO.md) / [Русский](README_RU.md) / [Tiếng Việt](README_VI.md)
|
||||
|
||||
[](https://discord.gg/wdNEHETs87)
|
||||
[](https://deepwiki.com/langbot-app/LangBot)
|
||||
[](https://github.com/langbot-app/LangBot/releases/latest)
|
||||
<img src="https://img.shields.io/badge/python-3.10 ~ 3.13 -blue.svg" alt="python">
|
||||
[](https://github.com/langbot-app/LangBot/stargazers)
|
||||
|
||||
<a href="https://langbot.app">Inicio</a> |
|
||||
<a href="https://docs.langbot.app/en/insight/features.html">Características</a> |
|
||||
<a href="https://docs.langbot.app/en/insight/guide.html">Despliegue</a> |
|
||||
<a href="https://docs.langbot.app/en/tags/readme.html">Integración API</a> |
|
||||
<a href="https://docs.langbot.app/en/insight/guide.html">Documentación</a> |
|
||||
<a href="https://docs.langbot.app/en/tags/readme.html">API</a> |
|
||||
<a href="https://space.langbot.app">Mercado de Plugins</a> |
|
||||
<a href="https://langbot.featurebase.app/roadmap">Hoja de Ruta</a>
|
||||
|
||||
@@ -27,20 +29,36 @@
|
||||
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
## 📦 Comenzar
|
||||
## ¿Qué es LangBot?
|
||||
|
||||
#### Inicio Rápido
|
||||
LangBot es una **plataforma de código abierto y grado de producción** para construir bots de mensajería instantánea impulsados por IA. Conecta modelos de lenguaje de gran escala (LLMs) con cualquier plataforma de chat, permitiéndole crear agentes inteligentes que pueden conversar, ejecutar tareas e integrarse con sus flujos de trabajo existentes.
|
||||
|
||||
Use `uvx` para iniciar con un comando (necesita instalar [uv](https://docs.astral.sh/uv/getting-started/installation/)):
|
||||
### Capacidades Clave
|
||||
|
||||
- **Conversaciones e Agentes IA** — Diálogos de múltiples turnos, llamadas a herramientas, soporte multimodal, salida en streaming. RAG (base de conocimientos) incorporado con integración profunda con [Dify](https://dify.ai), [Coze](https://coze.com), [n8n](https://n8n.io), [Langflow](https://langflow.org).
|
||||
- **Soporte Universal de Plataformas de MI** — Un solo código base para Discord, Telegram, Slack, LINE, QQ, WeChat, WeCom, Lark, DingTalk, KOOK.
|
||||
- **Listo para Producción** — Control de acceso, limitación de velocidad, filtrado de palabras sensibles, monitoreo completo y manejo de excepciones. De confianza para empresas.
|
||||
- **Ecosistema de Plugins** — Cientos de plugins, arquitectura basada en eventos, extensiones de componentes y soporte del [protocolo MCP](https://modelcontextprotocol.io/).
|
||||
- **Panel de Gestión Web** — Configure, gestione y monitoree sus bots a través de una interfaz de navegador intuitiva. Sin necesidad de editar YAML.
|
||||
- **Arquitectura Multi-Pipeline** — Diferentes bots para diferentes escenarios, con monitoreo completo y manejo de excepciones.
|
||||
|
||||
[→ Conocer más sobre todas las funcionalidades](https://docs.langbot.app/en/insight/features.html)
|
||||
|
||||
---
|
||||
|
||||
## Inicio Rápido
|
||||
|
||||
### Lanzamiento en una línea
|
||||
|
||||
```bash
|
||||
uvx langbot
|
||||
```
|
||||
|
||||
Visite http://localhost:5300 para comenzar a usarlo.
|
||||
> Requiere [uv](https://docs.astral.sh/uv/getting-started/installation/). Visite http://localhost:5300 — listo.
|
||||
|
||||
#### Despliegue con Docker Compose
|
||||
### Docker Compose
|
||||
|
||||
```bash
|
||||
git clone https://github.com/langbot-app/LangBot
|
||||
@@ -48,102 +66,101 @@ cd LangBot/docker
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
Visite http://localhost:5300 para comenzar a usarlo.
|
||||
|
||||
Documentación detallada [Despliegue con Docker](https://docs.langbot.app/en/deploy/langbot/docker.html).
|
||||
|
||||
#### Despliegue con un clic en BTPanel
|
||||
|
||||
LangBot ha sido listado en BTPanel. Si tiene BTPanel instalado, puede usar la [documentación](https://docs.langbot.app/en/deploy/langbot/one-click/bt.html) para usarlo.
|
||||
|
||||
#### Despliegue en la Nube Zeabur
|
||||
|
||||
Plantilla de Zeabur contribuida por la comunidad.
|
||||
### Despliegue en la Nube con un Clic
|
||||
|
||||
[](https://zeabur.com/en-US/templates/ZKTBDH)
|
||||
|
||||
#### Despliegue en la Nube Railway
|
||||
|
||||
[](https://railway.app/template/yRrAyL?referralCode=vogKPF)
|
||||
|
||||
#### Otros Métodos de Despliegue
|
||||
**Más opciones:** [Docker](https://docs.langbot.app/en/deploy/langbot/docker.html) · [Manual](https://docs.langbot.app/en/deploy/langbot/manual.html) · [BTPanel](https://docs.langbot.app/en/deploy/langbot/one-click/bt.html) · [Kubernetes](./docker/README_K8S.md)
|
||||
|
||||
Use directamente la versión publicada para ejecutar, consulte la documentación de [Despliegue Manual](https://docs.langbot.app/en/deploy/langbot/manual.html).
|
||||
---
|
||||
|
||||
#### Despliegue en Kubernetes
|
||||
## Plataformas Soportadas
|
||||
|
||||
Consulte la documentación de [Despliegue en Kubernetes](./docker/README_K8S.md).
|
||||
|
||||
## 😎 Manténgase Actualizado
|
||||
|
||||
Haga clic en los botones Star y Watch en la esquina superior derecha del repositorio para obtener las últimas actualizaciones.
|
||||
|
||||

|
||||
|
||||
## ✨ Características
|
||||
|
||||
<img width="500" src="https://docs.langbot.app/ui/bot-page-en-rounded.png" />
|
||||
|
||||
|
||||
- 💬 Chat con LLM / Agent: Compatible con múltiples LLMs, adaptado para chats grupales y privados; Admite conversaciones de múltiples rondas, llamadas a herramientas, capacidades multimodales y de salida en streaming. Implementación RAG (base de conocimientos) incorporada, e integración profunda con [Dify](https://dify.ai), [Coze](https://coze.com), [n8n](https://n8n.io) etc. LLMOps platforms.
|
||||
- 🤖 Soporte Multiplataforma: Actualmente compatible con QQ, QQ Channel, WeCom, WeChat personal, Lark, DingTalk, Discord, Telegram, KOOK, Slack, LINE, etc.
|
||||
- 🛠️ Alta Estabilidad, Rico en Funciones: Control de acceso nativo, limitación de velocidad, filtrado de palabras sensibles, etc.; Fácil de usar, admite múltiples métodos de despliegue. Compatible con múltiples configuraciones de pipeline, diferentes bots para diferentes escenarios.
|
||||
- 🧩 Extensión de Plugin, Comunidad Activa: Sistema de plugin de alta estabilidad, alta seguridad de nivel de producción; Compatible con mecanismos de plugin impulsados por eventos, extensión de componentes, etc.; Integración del protocolo [MCP](https://modelcontextprotocol.io/) de Anthropic; Actualmente cuenta con cientos de plugins.
|
||||
- 😻 Interfaz Web: Admite la gestión de instancias de LangBot a través del navegador. No es necesario escribir archivos de configuración manualmente.
|
||||
|
||||
Para especificaciones más detalladas, consulte la [documentación](https://docs.langbot.app/en/insight/features.html).
|
||||
|
||||
O visite el entorno de demostración: https://demo.langbot.dev/
|
||||
- Información de inicio de sesión: Correo electrónico: `demo@langbot.app` Contraseña: `langbot123456`
|
||||
- Nota: Solo para demostración de WebUI, por favor no ingrese información confidencial en el entorno público.
|
||||
|
||||
### Plataformas de Mensajería
|
||||
|
||||
| Plataforma | Estado | Observaciones |
|
||||
| --- | --- | --- |
|
||||
| Plataforma | Estado | Notas |
|
||||
|----------|--------|-------|
|
||||
| Discord | ✅ | |
|
||||
| Telegram | ✅ | |
|
||||
| Slack | ✅ | |
|
||||
| LINE | ✅ | |
|
||||
| QQ Personal | ✅ | |
|
||||
| QQ API Oficial | ✅ | |
|
||||
| WeCom | ✅ | |
|
||||
| WeComCS | ✅ | |
|
||||
| WeCom AI Bot | ✅ | |
|
||||
| WeChat Personal | ✅ | |
|
||||
| QQ | ✅ | Personal y API Oficial |
|
||||
| WeCom | ✅ | WeChat Empresarial, CS Externo, AI Bot |
|
||||
| WeChat | ✅ | Personal y Cuenta Oficial |
|
||||
| Lark | ✅ | |
|
||||
| DingTalk | ✅ | |
|
||||
| KOOK | ✅ | |
|
||||
| Satori | ✅ | |
|
||||
|
||||
### LLMs
|
||||
---
|
||||
|
||||
| LLM | Estado | Observaciones |
|
||||
| --- | --- | --- |
|
||||
| [OpenAI](https://platform.openai.com/) | ✅ | Disponible para cualquier modelo con formato de interfaz OpenAI |
|
||||
| [DeepSeek](https://www.deepseek.com/) | ✅ | |
|
||||
| [Moonshot](https://www.moonshot.cn/) | ✅ | |
|
||||
| [Anthropic](https://www.anthropic.com/) | ✅ | |
|
||||
| [xAI](https://x.ai/) | ✅ | |
|
||||
| [Zhipu AI](https://open.bigmodel.cn/) | ✅ | |
|
||||
| [CompShare](https://www.compshare.cn/?ytag=GPU_YY-gh_langbot) | ✅ | Plataforma de recursos LLM y GPU |
|
||||
| [PPIO](https://ppinfra.com/user/register?invited_by=QJKFYD&utm_source=github_langbot) | ✅ | Plataforma de recursos LLM y GPU |
|
||||
| [接口 AI](https://jiekou.ai/) | ✅ | Plataforma de agregación LLM |
|
||||
| [ShengSuanYun](https://www.shengsuanyun.com/?from=CH_KYIPP758) | ✅ | Plataforma de recursos LLM y GPU |
|
||||
| [302.AI](https://share.302.ai/SuTG99) | ✅ | Gateway LLM (MaaS) |
|
||||
| [Google Gemini](https://aistudio.google.com/prompts/new_chat) | ✅ | |
|
||||
| [Dify](https://dify.ai) | ✅ | Plataforma LLMOps |
|
||||
| [Ollama](https://ollama.com/) | ✅ | Plataforma de ejecución de LLM local |
|
||||
| [LMStudio](https://lmstudio.ai/) | ✅ | Plataforma de ejecución de LLM local |
|
||||
| [GiteeAI](https://ai.gitee.com/) | ✅ | Gateway de interfaz LLM (MaaS) |
|
||||
| [SiliconFlow](https://siliconflow.cn/) | ✅ | Gateway LLM (MaaS) |
|
||||
| [Aliyun Bailian](https://bailian.console.aliyun.com/) | ✅ | Gateway LLM (MaaS), plataforma LLMOps |
|
||||
| [Volc Engine Ark](https://console.volcengine.com/ark/region:ark+cn-beijing/model?vendor=Bytedance&view=LIST_VIEW) | ✅ | Gateway LLM (MaaS), plataforma LLMOps |
|
||||
| [ModelScope](https://modelscope.cn/docs/model-service/API-Inference/intro) | ✅ | Gateway LLM (MaaS) |
|
||||
| [MCP](https://modelcontextprotocol.io/) | ✅ | Compatible con acceso a herramientas a través del protocolo MCP |
|
||||
## LLMs e Integraciones Soportadas
|
||||
|
||||
## 🤝 Contribución de la Comunidad
|
||||
| Proveedor | Tipo | Estado |
|
||||
|----------|------|--------|
|
||||
| [OpenAI](https://platform.openai.com/) | LLM | ✅ |
|
||||
| [Anthropic](https://www.anthropic.com/) | LLM | ✅ |
|
||||
| [DeepSeek](https://www.deepseek.com/) | LLM | ✅ |
|
||||
| [Google Gemini](https://aistudio.google.com/prompts/new_chat) | LLM | ✅ |
|
||||
| [xAI](https://x.ai/) | LLM | ✅ |
|
||||
| [Moonshot](https://www.moonshot.cn/) | LLM | ✅ |
|
||||
| [Zhipu AI](https://open.bigmodel.cn/) | LLM | ✅ |
|
||||
| [Ollama](https://ollama.com/) | LLM Local | ✅ |
|
||||
| [LM Studio](https://lmstudio.ai/) | LLM Local | ✅ |
|
||||
| [Dify](https://dify.ai) | LLMOps | ✅ |
|
||||
| [MCP](https://modelcontextprotocol.io/) | Protocolo | ✅ |
|
||||
| [SiliconFlow](https://siliconflow.cn/) | Pasarela | ✅ |
|
||||
| [Aliyun Bailian](https://bailian.console.aliyun.com/) | Pasarela | ✅ |
|
||||
| [Volc Engine Ark](https://console.volcengine.com/ark/region:ark+cn-beijing/model?vendor=Bytedance&view=LIST_VIEW) | Pasarela | ✅ |
|
||||
| [ModelScope](https://modelscope.cn/docs/model-service/API-Inference/intro) | Pasarela | ✅ |
|
||||
| [GiteeAI](https://ai.gitee.com/) | Pasarela | ✅ |
|
||||
| [CompShare](https://www.compshare.cn/?ytag=GPU_YY-gh_langbot) | Plataforma GPU | ✅ |
|
||||
| [PPIO](https://ppinfra.com/user/register?invited_by=QJKFYD&utm_source=github_langbot) | Plataforma GPU | ✅ |
|
||||
| [ShengSuanYun](https://www.shengsuanyun.com/?from=CH_KYIPP758) | Plataforma GPU | ✅ |
|
||||
| [接口 AI](https://jiekou.ai/) | Pasarela | ✅ |
|
||||
| [302.AI](https://share.302.ai/SuTG99) | Pasarela | ✅ |
|
||||
|
||||
Gracias a los siguientes [contribuidores de código](https://github.com/langbot-app/LangBot/graphs/contributors) y otros miembros de la comunidad por sus contribuciones a LangBot:
|
||||
[→ Ver todas las integraciones](https://docs.langbot.app/en/insight/features.html)
|
||||
|
||||
---
|
||||
|
||||
## ¿Por qué LangBot?
|
||||
|
||||
| Caso de Uso | Cómo Ayuda LangBot |
|
||||
|----------|-------------------|
|
||||
| **Atención al cliente** | Despliegue agentes de IA en Slack/Discord/Telegram que respondan preguntas usando su base de conocimientos |
|
||||
| **Herramientas internas** | Conecte flujos de trabajo de n8n/Dify a WeCom/DingTalk para procesos empresariales automatizados |
|
||||
| **Gestión de comunidades** | Modere grupos de QQ/Discord con filtrado de contenido e interacción impulsados por IA |
|
||||
| **Presencia multiplataforma** | Un solo bot, todas las plataformas. Gestione desde un único panel de control |
|
||||
|
||||
---
|
||||
|
||||
## Demo en Vivo
|
||||
|
||||
**Pruébelo ahora:** https://demo.langbot.dev/
|
||||
- Correo electrónico: `demo@langbot.app`
|
||||
- Contraseña: `langbot123456`
|
||||
|
||||
*Nota: Entorno de demostración público. No ingrese información confidencial.*
|
||||
|
||||
---
|
||||
|
||||
## Comunidad
|
||||
|
||||
[](https://discord.gg/wdNEHETs87)
|
||||
|
||||
- [Comunidad de Discord](https://discord.gg/wdNEHETs87)
|
||||
|
||||
---
|
||||
|
||||
## Historial de Stars
|
||||
|
||||
[](https://star-history.com/#langbot-app/LangBot&Date)
|
||||
|
||||
---
|
||||
|
||||
## Colaboradores
|
||||
|
||||
Gracias a todos los [colaboradores](https://github.com/langbot-app/LangBot/graphs/contributors) que han ayudado a mejorar LangBot:
|
||||
|
||||
<a href="https://github.com/langbot-app/LangBot/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=langbot-app/LangBot" />
|
||||
|
||||
194
README_FR.md
194
README_FR.md
@@ -1,25 +1,27 @@
|
||||
<p align="center">
|
||||
<a href="https://langbot.app">
|
||||
<img width="130" src="https://docs.langbot.app/langbot-logo.png" alt="LangBot"/>
|
||||
<img width="130" src="res/logo-blue.png" alt="LangBot"/>
|
||||
</a>
|
||||
|
||||
<div align="center">
|
||||
|
||||
<a href="https://www.producthunt.com/products/langbot?utm_source=badge-follow&utm_medium=badge&utm_source=badge-langbot" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/follow.svg?product_id=1077185&theme=light" alt="LangBot - Production-grade IM bot made easy. | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||
|
||||
<h3>Créez, déboguez et déployez rapidement des bots de messagerie instantanée avec LangBot.</h3>
|
||||
<h3>Plateforme de niveau production pour construire des bots de messagerie instantanée avec agents IA.</h3>
|
||||
<h4>Créez, déboguez et déployez rapidement des bots IA sur Slack, Discord, Telegram, WeChat et plus.</h4>
|
||||
|
||||
[English](README_EN.md) / [简体中文](README.md) / [繁體中文](README_TW.md) / [日本語](README_JP.md) / [Español](README_ES.md) / Français / [한국어](README_KO.md) / [Русский](README_RU.md) / [Tiếng Việt](README_VI.md)
|
||||
[English](README.md) / [简体中文](README_CN.md) / [繁體中文](README_TW.md) / [日本語](README_JP.md) / [Español](README_ES.md) / Français / [한국어](README_KO.md) / [Русский](README_RU.md) / [Tiếng Việt](README_VI.md)
|
||||
|
||||
[](https://discord.gg/wdNEHETs87)
|
||||
[](https://deepwiki.com/langbot-app/LangBot)
|
||||
[](https://github.com/langbot-app/LangBot/releases/latest)
|
||||
<img src="https://img.shields.io/badge/python-3.10 ~ 3.13 -blue.svg" alt="python">
|
||||
[](https://github.com/langbot-app/LangBot/stargazers)
|
||||
|
||||
<a href="https://langbot.app">Accueil</a> |
|
||||
<a href="https://docs.langbot.app/en/insight/features.html">Fonctionnalités</a> |
|
||||
<a href="https://docs.langbot.app/en/insight/guide.html">Déploiement</a> |
|
||||
<a href="https://docs.langbot.app/en/tags/readme.html">Intégration API</a> |
|
||||
<a href="https://docs.langbot.app/en/insight/guide.html">Documentation</a> |
|
||||
<a href="https://docs.langbot.app/en/tags/readme.html">API</a> |
|
||||
<a href="https://space.langbot.app">Marché des Plugins</a> |
|
||||
<a href="https://langbot.featurebase.app/roadmap">Feuille de Route</a>
|
||||
|
||||
@@ -27,19 +29,36 @@
|
||||
|
||||
</p>
|
||||
|
||||
## 📦 Commencer
|
||||
---
|
||||
|
||||
#### Démarrage Rapide
|
||||
## Qu'est-ce que LangBot ?
|
||||
|
||||
Utilisez `uvx` pour démarrer avec une commande (besoin d'installer [uv](https://docs.astral.sh/uv/getting-started/installation/)) :
|
||||
LangBot est une **plateforme open-source de niveau production** pour créer des bots de messagerie instantanée alimentés par l'IA. Elle connecte les grands modèles de langage (LLMs) à n'importe quelle plateforme de chat, vous permettant de créer des agents intelligents capables de converser, d'exécuter des tâches et de s'intégrer à vos workflows existants.
|
||||
|
||||
### Capacités Clés
|
||||
|
||||
- **Conversations IA & Agents** — Dialogues multi-tours, appels d'outils, support multimodal, sortie en streaming. RAG (base de connaissances) intégré avec intégration profonde de [Dify](https://dify.ai), [Coze](https://coze.com), [n8n](https://n8n.io), [Langflow](https://langflow.org).
|
||||
- **Support Universel des Plateformes de MI** — Un seul code pour Discord, Telegram, Slack, LINE, QQ, WeChat, WeCom, Lark, DingTalk, KOOK.
|
||||
- **Prêt pour la Production** — Contrôle d'accès, limitation de débit, filtrage de mots sensibles, surveillance complète et gestion des exceptions. Approuvé par les entreprises.
|
||||
- **Écosystème de Plugins** — Des centaines de plugins, architecture événementielle, extensions de composants, et support du [protocole MCP](https://modelcontextprotocol.io/).
|
||||
- **Panneau de Gestion Web** — Configurez, gérez et surveillez vos bots via une interface navigateur intuitive. Aucune édition de YAML requise.
|
||||
- **Architecture Multi-Pipeline** — Différents bots pour différents scénarios, avec surveillance complète et gestion des exceptions.
|
||||
|
||||
[→ En savoir plus sur toutes les fonctionnalités](https://docs.langbot.app/en/insight/features.html)
|
||||
|
||||
---
|
||||
|
||||
## Démarrage Rapide
|
||||
|
||||
### Lancement en une ligne
|
||||
|
||||
```bash
|
||||
uvx langbot
|
||||
```
|
||||
|
||||
Visitez http://localhost:5300 pour commencer à l'utiliser.
|
||||
> Nécessite [uv](https://docs.astral.sh/uv/getting-started/installation/). Visitez http://localhost:5300 — c'est prêt.
|
||||
|
||||
#### Déploiement avec Docker Compose
|
||||
### Docker Compose
|
||||
|
||||
```bash
|
||||
git clone https://github.com/langbot-app/LangBot
|
||||
@@ -47,102 +66,101 @@ cd LangBot/docker
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
Visitez http://localhost:5300 pour commencer à l'utiliser.
|
||||
|
||||
Documentation détaillée [Déploiement Docker](https://docs.langbot.app/en/deploy/langbot/docker.html).
|
||||
|
||||
#### Déploiement en un clic sur BTPanel
|
||||
|
||||
LangBot a été répertorié sur BTPanel. Si vous avez installé BTPanel, vous pouvez utiliser la [documentation](https://docs.langbot.app/en/deploy/langbot/one-click/bt.html) pour l'utiliser.
|
||||
|
||||
#### Déploiement Cloud Zeabur
|
||||
|
||||
Modèle Zeabur contribué par la communauté.
|
||||
### Déploiement Cloud en un Clic
|
||||
|
||||
[](https://zeabur.com/en-US/templates/ZKTBDH)
|
||||
|
||||
#### Déploiement Cloud Railway
|
||||
|
||||
[](https://railway.app/template/yRrAyL?referralCode=vogKPF)
|
||||
|
||||
#### Autres Méthodes de Déploiement
|
||||
**Plus d'options :** [Docker](https://docs.langbot.app/en/deploy/langbot/docker.html) · [Manuel](https://docs.langbot.app/en/deploy/langbot/manual.html) · [BTPanel](https://docs.langbot.app/en/deploy/langbot/one-click/bt.html) · [Kubernetes](./docker/README_K8S.md)
|
||||
|
||||
Utilisez directement la version publiée pour exécuter, consultez la documentation de [Déploiement Manuel](https://docs.langbot.app/en/deploy/langbot/manual.html).
|
||||
---
|
||||
|
||||
#### Déploiement Kubernetes
|
||||
## Plateformes Supportées
|
||||
|
||||
Consultez la documentation de [Déploiement Kubernetes](./docker/README_K8S.md).
|
||||
|
||||
## 😎 Restez à Jour
|
||||
|
||||
Cliquez sur les boutons Star et Watch dans le coin supérieur droit du dépôt pour obtenir les dernières mises à jour.
|
||||
|
||||

|
||||
|
||||
## ✨ Fonctionnalités
|
||||
|
||||
<img width="500" src="https://docs.langbot.app/ui/bot-page-en-rounded.png" />
|
||||
|
||||
|
||||
- 💬 Chat avec LLM / Agent : Prend en charge plusieurs LLM, adapté aux chats de groupe et privés ; Prend en charge les conversations multi-tours, les appels d'outils, les capacités multimodales et de sortie en streaming. Implémentation RAG (base de connaissances) intégrée, et intégration profonde avec [Dify](https://dify.ai), [Coze](https://coze.com), [n8n](https://n8n.io) etc. LLMOps platforms.
|
||||
- 🤖 Support Multi-plateforme : Actuellement compatible avec QQ, QQ Channel, WeCom, WeChat personnel, Lark, DingTalk, Discord, Telegram, KOOK, Slack, LINE, etc.
|
||||
- 🛠️ Haute Stabilité, Riche en Fonctionnalités : Contrôle d'accès natif, limitation de débit, filtrage de mots sensibles, etc. ; Facile à utiliser, prend en charge plusieurs méthodes de déploiement. Prend en charge plusieurs configurations de pipeline, différents bots pour différents scénarios.
|
||||
- 🧩 Extension de Plugin, Communauté Active : Système de plugin de haute stabilité, haute sécurité de niveau production; Prend en charge les mécanismes de plugin pilotés par événements, l'extension de composants, etc. ; Intégration du protocole [MCP](https://modelcontextprotocol.io/) d'Anthropic ; Dispose actuellement de centaines de plugins.
|
||||
- 😻 Interface Web : Prend en charge la gestion des instances LangBot via le navigateur. Pas besoin d'écrire manuellement les fichiers de configuration.
|
||||
|
||||
Pour des spécifications plus détaillées, veuillez consulter la [documentation](https://docs.langbot.app/en/insight/features.html).
|
||||
|
||||
Ou visitez l'environnement de démonstration : https://demo.langbot.dev/
|
||||
- Informations de connexion : Email : `demo@langbot.app` Mot de passe : `langbot123456`
|
||||
- Note : Pour la démonstration WebUI uniquement, veuillez ne pas entrer d'informations sensibles dans l'environnement public.
|
||||
|
||||
### Plateformes de Messagerie
|
||||
|
||||
| Plateforme | Statut | Remarques |
|
||||
| --- | --- | --- |
|
||||
| Plateforme | Statut | Notes |
|
||||
|----------|--------|-------|
|
||||
| Discord | ✅ | |
|
||||
| Telegram | ✅ | |
|
||||
| Slack | ✅ | |
|
||||
| LINE | ✅ | |
|
||||
| QQ Personnel | ✅ | |
|
||||
| API Officielle QQ | ✅ | |
|
||||
| WeCom | ✅ | |
|
||||
| WeComCS | ✅ | |
|
||||
| WeCom AI Bot | ✅ | |
|
||||
| WeChat Personnel | ✅ | |
|
||||
| QQ | ✅ | Personnel & API Officielle |
|
||||
| WeCom | ✅ | WeChat Entreprise, CS Externe, AI Bot |
|
||||
| WeChat | ✅ | Personnel & Compte Officiel |
|
||||
| Lark | ✅ | |
|
||||
| DingTalk | ✅ | |
|
||||
| KOOK | ✅ | |
|
||||
| Satori | ✅ | |
|
||||
|
||||
### LLMs
|
||||
---
|
||||
|
||||
| LLM | Statut | Remarques |
|
||||
| --- | --- | --- |
|
||||
| [OpenAI](https://platform.openai.com/) | ✅ | Disponible pour tout modèle au format d'interface OpenAI |
|
||||
| [DeepSeek](https://www.deepseek.com/) | ✅ | |
|
||||
| [Moonshot](https://www.moonshot.cn/) | ✅ | |
|
||||
| [Anthropic](https://www.anthropic.com/) | ✅ | |
|
||||
| [xAI](https://x.ai/) | ✅ | |
|
||||
| [Zhipu AI](https://open.bigmodel.cn/) | ✅ | |
|
||||
| [CompShare](https://www.compshare.cn/?ytag=GPU_YY-gh_langbot) | ✅ | Plateforme de ressources LLM et GPU |
|
||||
| [PPIO](https://ppinfra.com/user/register?invited_by=QJKFYD&utm_source=github_langbot) | ✅ | Plateforme de ressources LLM et GPU |
|
||||
| [接口 AI](https://jiekou.ai/) | ✅ | Plateforme d'agrégation LLM |
|
||||
| [ShengSuanYun](https://www.shengsuanyun.com/?from=CH_KYIPP758) | ✅ | Plateforme de ressources LLM et GPU |
|
||||
| [302.AI](https://share.302.ai/SuTG99) | ✅ | Passerelle LLM (MaaS) |
|
||||
| [Google Gemini](https://aistudio.google.com/prompts/new_chat) | ✅ | |
|
||||
| [Dify](https://dify.ai) | ✅ | Plateforme LLMOps |
|
||||
| [Ollama](https://ollama.com/) | ✅ | Plateforme d'exécution LLM locale |
|
||||
| [LMStudio](https://lmstudio.ai/) | ✅ | Plateforme d'exécution LLM locale |
|
||||
| [GiteeAI](https://ai.gitee.com/) | ✅ | Passerelle d'interface LLM (MaaS) |
|
||||
| [SiliconFlow](https://siliconflow.cn/) | ✅ | Passerelle LLM (MaaS) |
|
||||
| [Aliyun Bailian](https://bailian.console.aliyun.com/) | ✅ | Passerelle LLM (MaaS), plateforme LLMOps |
|
||||
| [Volc Engine Ark](https://console.volcengine.com/ark/region:ark+cn-beijing/model?vendor=Bytedance&view=LIST_VIEW) | ✅ | Passerelle LLM (MaaS), plateforme LLMOps |
|
||||
| [ModelScope](https://modelscope.cn/docs/model-service/API-Inference/intro) | ✅ | Passerelle LLM (MaaS) |
|
||||
| [MCP](https://modelcontextprotocol.io/) | ✅ | Prend en charge l'accès aux outils via le protocole MCP |
|
||||
## LLMs et Intégrations Supportés
|
||||
|
||||
## 🤝 Contribution de la Communauté
|
||||
| Fournisseur | Type | Statut |
|
||||
|----------|------|--------|
|
||||
| [OpenAI](https://platform.openai.com/) | LLM | ✅ |
|
||||
| [Anthropic](https://www.anthropic.com/) | LLM | ✅ |
|
||||
| [DeepSeek](https://www.deepseek.com/) | LLM | ✅ |
|
||||
| [Google Gemini](https://aistudio.google.com/prompts/new_chat) | LLM | ✅ |
|
||||
| [xAI](https://x.ai/) | LLM | ✅ |
|
||||
| [Moonshot](https://www.moonshot.cn/) | LLM | ✅ |
|
||||
| [Zhipu AI](https://open.bigmodel.cn/) | LLM | ✅ |
|
||||
| [Ollama](https://ollama.com/) | LLM Local | ✅ |
|
||||
| [LM Studio](https://lmstudio.ai/) | LLM Local | ✅ |
|
||||
| [Dify](https://dify.ai) | LLMOps | ✅ |
|
||||
| [MCP](https://modelcontextprotocol.io/) | Protocole | ✅ |
|
||||
| [SiliconFlow](https://siliconflow.cn/) | Passerelle | ✅ |
|
||||
| [Aliyun Bailian](https://bailian.console.aliyun.com/) | Passerelle | ✅ |
|
||||
| [Volc Engine Ark](https://console.volcengine.com/ark/region:ark+cn-beijing/model?vendor=Bytedance&view=LIST_VIEW) | Passerelle | ✅ |
|
||||
| [ModelScope](https://modelscope.cn/docs/model-service/API-Inference/intro) | Passerelle | ✅ |
|
||||
| [GiteeAI](https://ai.gitee.com/) | Passerelle | ✅ |
|
||||
| [接口 AI](https://jiekou.ai/) | Passerelle | ✅ |
|
||||
| [302.AI](https://share.302.ai/SuTG99) | Passerelle | ✅ |
|
||||
| [CompShare](https://www.compshare.cn/?ytag=GPU_YY-gh_langbot) | Plateforme GPU | ✅ |
|
||||
| [PPIO](https://ppinfra.com/user/register?invited_by=QJKFYD&utm_source=github_langbot) | Plateforme GPU | ✅ |
|
||||
| [ShengSuanYun](https://www.shengsuanyun.com/?from=CH_KYIPP758) | Plateforme GPU | ✅ |
|
||||
|
||||
Merci aux [contributeurs de code](https://github.com/langbot-app/LangBot/graphs/contributors) suivants et aux autres membres de la communauté pour leurs contributions à LangBot :
|
||||
[→ Voir toutes les intégrations](https://docs.langbot.app/en/insight/features.html)
|
||||
|
||||
---
|
||||
|
||||
## Pourquoi LangBot ?
|
||||
|
||||
| Cas d'Usage | Comment LangBot Aide |
|
||||
|----------|-------------------|
|
||||
| **Support Client** | Déployez des agents IA sur Slack/Discord/Telegram qui répondent aux questions en utilisant votre base de connaissances |
|
||||
| **Outils Internes** | Connectez les workflows n8n/Dify à WeCom/DingTalk pour automatiser vos processus métier |
|
||||
| **Gestion de Communauté** | Modérez les groupes QQ/Discord avec un filtrage de contenu et des interactions alimentés par l'IA |
|
||||
| **Présence Multi-plateforme** | Un seul bot, toutes les plateformes. Gérez tout depuis un tableau de bord unique |
|
||||
|
||||
---
|
||||
|
||||
## Démo en Ligne
|
||||
|
||||
**Essayez maintenant :** https://demo.langbot.dev/
|
||||
- Email : `demo@langbot.app`
|
||||
- Mot de passe : `langbot123456`
|
||||
|
||||
*Note : Environnement de démonstration public. Ne saisissez pas d'informations sensibles.*
|
||||
|
||||
---
|
||||
|
||||
## Communauté
|
||||
|
||||
[](https://discord.gg/wdNEHETs87)
|
||||
|
||||
- [Communauté Discord](https://discord.gg/wdNEHETs87)
|
||||
|
||||
---
|
||||
|
||||
## Historique des Stars
|
||||
|
||||
[](https://star-history.com/#langbot-app/LangBot&Date)
|
||||
|
||||
---
|
||||
|
||||
## Contributeurs
|
||||
|
||||
Merci à tous les [contributeurs](https://github.com/langbot-app/LangBot/graphs/contributors) qui ont aidé à améliorer LangBot :
|
||||
|
||||
<a href="https://github.com/langbot-app/LangBot/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=langbot-app/LangBot" />
|
||||
|
||||
194
README_JP.md
194
README_JP.md
@@ -1,25 +1,27 @@
|
||||
<p align="center">
|
||||
<a href="https://langbot.app">
|
||||
<img width="130" src="https://docs.langbot.app/langbot-logo.png" alt="LangBot"/>
|
||||
<img width="130" src="res/logo-blue.png" alt="LangBot"/>
|
||||
</a>
|
||||
|
||||
<div align="center">
|
||||
|
||||
<a href="https://www.producthunt.com/products/langbot?utm_source=badge-follow&utm_medium=badge&utm_source=badge-langbot" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/follow.svg?product_id=1077185&theme=light" alt="LangBot - Production-grade IM bot made easy. | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||
|
||||
<h3>LangBotでIMボットを素早く構築、デバッグ、デプロイ。</h3>
|
||||
<h3>AIエージェント搭載IMボットを構築するための本番グレードプラットフォーム。</h3>
|
||||
<h4>Slack、Discord、Telegram、WeChat などに AI ボットを素早く構築、デバッグ、デプロイ。</h4>
|
||||
|
||||
[English](README_EN.md) / [简体中文](README.md) / [繁體中文](README_TW.md) / 日本語 / [Español](README_ES.md) / [Français](README_FR.md) / [한국어](README_KO.md) / [Русский](README_RU.md) / [Tiếng Việt](README_VI.md)
|
||||
[English](README.md) / [简体中文](README_CN.md) / [繁體中文](README_TW.md) / 日本語 / [Español](README_ES.md) / [Français](README_FR.md) / [한국어](README_KO.md) / [Русский](README_RU.md) / [Tiếng Việt](README_VI.md)
|
||||
|
||||
[](https://discord.gg/wdNEHETs87)
|
||||
[](https://deepwiki.com/langbot-app/LangBot)
|
||||
[](https://github.com/langbot-app/LangBot/releases/latest)
|
||||
<img src="https://img.shields.io/badge/python-3.10 ~ 3.13 -blue.svg" alt="python">
|
||||
[](https://github.com/langbot-app/LangBot/stargazers)
|
||||
|
||||
<a href="https://langbot.app">ホーム</a> |
|
||||
<a href="https://docs.langbot.app/ja/insight/features.html">機能仕様</a> |
|
||||
<a href="https://docs.langbot.app/ja/insight/guide.html">デプロイ</a> |
|
||||
<a href="https://docs.langbot.app/ja/tags/readme.html">API統合</a> |
|
||||
<a href="https://docs.langbot.app/ja/insight/features.html">機能</a> |
|
||||
<a href="https://docs.langbot.app/ja/insight/guide.html">ドキュメント</a> |
|
||||
<a href="https://docs.langbot.app/ja/tags/readme.html">API</a> |
|
||||
<a href="https://space.langbot.app">プラグインマーケット</a> |
|
||||
<a href="https://langbot.featurebase.app/roadmap">ロードマップ</a>
|
||||
|
||||
@@ -27,19 +29,36 @@
|
||||
|
||||
</p>
|
||||
|
||||
## 📦 始め方
|
||||
---
|
||||
|
||||
#### クイックスタート
|
||||
## LangBot とは?
|
||||
|
||||
`uvx` を使用した迅速なデプロイ([uv](https://docs.astral.sh/uv/getting-started/installation/) が必要です):
|
||||
LangBot は、AI搭載のインスタントメッセージングボットを構築するための**オープンソースの本番グレードプラットフォーム**です。大規模言語モデル(LLM)をあらゆるチャットプラットフォームに接続し、会話、タスク実行、既存のワークフローとの統合が可能なインテリジェントエージェントを作成できます。
|
||||
|
||||
### 主な機能
|
||||
|
||||
- **AI対話とエージェント** — マルチターン対話、ツール呼び出し、マルチモーダル対応、ストリーミング出力。RAG(ナレッジベース)を内蔵し、[Dify](https://dify.ai)、[Coze](https://coze.com)、[n8n](https://n8n.io)、[Langflow](https://langflow.org) と深く統合。
|
||||
- **ユニバーサルIMプラットフォーム対応** — 単一のコードベースで Discord、Telegram、Slack、LINE、QQ、WeChat、WeCom、Lark、DingTalk、KOOK に対応。
|
||||
- **本番環境対応** — アクセス制御、レート制限、センシティブワードフィルタリング、包括的な監視、例外処理を搭載。エンタープライズの信頼に応える品質。
|
||||
- **プラグインエコシステム** — 数百のプラグイン、イベント駆動アーキテクチャ、コンポーネント拡張、[MCPプロトコル](https://modelcontextprotocol.io/)対応。
|
||||
- **Web管理パネル** — 直感的なブラウザインターフェースからボットの設定、管理、監視が可能。YAML編集は不要。
|
||||
- **マルチパイプラインアーキテクチャ** — 異なるシナリオに異なるボットを配置し、包括的な監視と例外処理を実現。
|
||||
|
||||
[→ すべての機能について詳しく見る](https://docs.langbot.app/ja/insight/features.html)
|
||||
|
||||
---
|
||||
|
||||
## クイックスタート
|
||||
|
||||
### ワンライン起動
|
||||
|
||||
```bash
|
||||
uvx langbot
|
||||
```
|
||||
|
||||
http://localhost:5300 にアクセスして使用を開始します。
|
||||
> [uv](https://docs.astral.sh/uv/getting-started/installation/) が必要です。http://localhost:5300 にアクセスして完了。
|
||||
|
||||
#### Docker Compose デプロイ
|
||||
### Docker Compose
|
||||
|
||||
```bash
|
||||
git clone https://github.com/langbot-app/LangBot
|
||||
@@ -47,102 +66,101 @@ cd LangBot/docker
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
http://localhost:5300 にアクセスして使用を開始します。
|
||||
|
||||
詳細なドキュメントは[Dockerデプロイ](https://docs.langbot.app/en/deploy/langbot/docker.html)を参照してください。
|
||||
|
||||
#### Panelでのワンクリックデプロイ
|
||||
|
||||
LangBotはBTPanelにリストされています。BTPanelをインストールしている場合は、[ドキュメント](https://docs.langbot.app/en/deploy/langbot/one-click/bt.html)を使用して使用できます。
|
||||
|
||||
#### Zeaburクラウドデプロイ
|
||||
|
||||
コミュニティが提供するZeaburテンプレート。
|
||||
### ワンクリッククラウドデプロイ
|
||||
|
||||
[](https://zeabur.com/en-US/templates/ZKTBDH)
|
||||
|
||||
#### Railwayクラウドデプロイ
|
||||
|
||||
[](https://railway.app/template/yRrAyL?referralCode=vogKPF)
|
||||
|
||||
#### その他のデプロイ方法
|
||||
**その他:** [Docker](https://docs.langbot.app/en/deploy/langbot/docker.html) · [手動デプロイ](https://docs.langbot.app/en/deploy/langbot/manual.html) · [BTPanel](https://docs.langbot.app/en/deploy/langbot/one-click/bt.html) · [Kubernetes](./docker/README_K8S.md)
|
||||
|
||||
リリースバージョンを直接使用して実行します。[手動デプロイ](https://docs.langbot.app/en/deploy/langbot/manual.html)のドキュメントを参照してください。
|
||||
---
|
||||
|
||||
#### Kubernetes デプロイ
|
||||
|
||||
[Kubernetes デプロイ](./docker/README_K8S.md) ドキュメントを参照してください。
|
||||
|
||||
## 😎 最新情報を入手
|
||||
|
||||
リポジトリの右上にある Star と Watch ボタンをクリックして、最新の更新を取得してください。
|
||||
|
||||

|
||||
|
||||
## ✨ 機能
|
||||
|
||||
<img width="500" src="https://docs.langbot.app/ui/bot-page-en-rounded.png" />
|
||||
|
||||
|
||||
- 💬 LLM / エージェントとのチャット: 複数のLLMをサポートし、グループチャットとプライベートチャットに対応。マルチラウンドの会話、ツールの呼び出し、マルチモーダル、ストリーミング出力機能をサポート、RAG(知識ベース)を組み込み、[Dify](https://dify.ai)、[Coze](https://coze.com)、[n8n](https://n8n.io) などの LLMOps プラットフォームと深く統合。
|
||||
- 🤖 多プラットフォーム対応: 現在、QQ、QQ チャンネル、WeChat、個人 WeChat、Lark、DingTalk、Discord、Telegram、KOOK、Slack、LINE など、複数のプラットフォームをサポートしています。
|
||||
- 🛠️ 高い安定性、豊富な機能: ネイティブのアクセス制御、レート制限、敏感な単語のフィルタリングなどのメカニズムをサポート。使いやすく、複数のデプロイ方法をサポート。複数のパイプライン設定をサポートし、異なるボットを異なる用途に使用できます。
|
||||
- 🧩 プラグイン拡張、活発なコミュニティ: 高い安定性、高いセキュリティの生産レベルのプラグインシステム;イベント駆動、コンポーネント拡張などのプラグインメカニズムをサポート。適配 Anthropic [MCP プロトコル](https://modelcontextprotocol.io/);豊富なエコシステム、現在数百のプラグインが存在。
|
||||
- 😻 Web UI: ブラウザを通じてLangBotインスタンスを管理することをサポート。
|
||||
|
||||
詳細な仕様については、[ドキュメント](https://docs.langbot.app/en/insight/features.html)を参照してください。
|
||||
|
||||
または、デモ環境にアクセスしてください: https://demo.langbot.dev/
|
||||
- ログイン情報: メール: `demo@langbot.app` パスワード: `langbot123456`
|
||||
- 注意: WebUI のデモンストレーションのみの場合、公開環境では機密情報を入力しないでください。
|
||||
|
||||
### メッセージプラットフォーム
|
||||
## 対応プラットフォーム
|
||||
|
||||
| プラットフォーム | ステータス | 備考 |
|
||||
| --- | --- | --- |
|
||||
|----------|--------|-------|
|
||||
| Discord | ✅ | |
|
||||
| Telegram | ✅ | |
|
||||
| Slack | ✅ | |
|
||||
| LINE | ✅ | |
|
||||
| 個人QQ | ✅ | |
|
||||
| QQ公式API | ✅ | |
|
||||
| WeCom | ✅ | |
|
||||
| WeComCS | ✅ | |
|
||||
| WeCom AI Bot | ✅ | |
|
||||
| 個人WeChat | ✅ | |
|
||||
| QQ | ✅ | 個人 & 公式API |
|
||||
| WeCom | ✅ | 企業WeChat、外部CS、AIボット |
|
||||
| WeChat | ✅ | 個人 & 公式アカウント |
|
||||
| Lark | ✅ | |
|
||||
| DingTalk | ✅ | |
|
||||
| KOOK | ✅ | |
|
||||
| Satori | ✅ | |
|
||||
|
||||
### LLMs
|
||||
---
|
||||
|
||||
| LLM | ステータス | 備考 |
|
||||
| --- | --- | --- |
|
||||
| [OpenAI](https://platform.openai.com/) | ✅ | 任意のOpenAIインターフェース形式モデルに対応 |
|
||||
| [DeepSeek](https://www.deepseek.com/) | ✅ | |
|
||||
| [Moonshot](https://www.moonshot.cn/) | ✅ | |
|
||||
| [Anthropic](https://www.anthropic.com/) | ✅ | |
|
||||
| [xAI](https://x.ai/) | ✅ | |
|
||||
| [Zhipu AI](https://open.bigmodel.cn/) | ✅ | |
|
||||
| [CompShare](https://www.compshare.cn/?ytag=GPU_YY-gh_langbot) | ✅ | 大模型とGPUリソースプラットフォーム |
|
||||
| [PPIO](https://ppinfra.com/user/register?invited_by=QJKFYD&utm_source=github_langbot) | ✅ | 大模型とGPUリソースプラットフォーム |
|
||||
| [接口 AI](https://jiekou.ai/) | ✅ | LLMゲートウェイ(MaaS) |
|
||||
| [ShengSuanYun](https://www.shengsuanyun.com/?from=CH_KYIPP758) | ✅ | LLMとGPUリソースプラットフォーム |
|
||||
| [302.AI](https://share.302.ai/SuTG99) | ✅ | LLMゲートウェイ(MaaS) |
|
||||
| [Google Gemini](https://aistudio.google.com/prompts/new_chat) | ✅ | |
|
||||
| [Dify](https://dify.ai) | ✅ | LLMOpsプラットフォーム |
|
||||
| [Ollama](https://ollama.com/) | ✅ | ローカルLLM実行プラットフォーム |
|
||||
| [LMStudio](https://lmstudio.ai/) | ✅ | ローカルLLM実行プラットフォーム |
|
||||
| [GiteeAI](https://ai.gitee.com/) | ✅ | LLMインターフェースゲートウェイ(MaaS) |
|
||||
| [SiliconFlow](https://siliconflow.cn/) | ✅ | LLMゲートウェイ(MaaS) |
|
||||
| [Aliyun Bailian](https://bailian.console.aliyun.com/) | ✅ | LLMゲートウェイ(MaaS), LLMOpsプラットフォーム |
|
||||
| [Volc Engine Ark](https://console.volcengine.com/ark/region:ark+cn-beijing/model?vendor=Bytedance&view=LIST_VIEW) | ✅ | LLMゲートウェイ(MaaS), LLMOpsプラットフォーム |
|
||||
| [ModelScope](https://modelscope.cn/docs/model-service/API-Inference/intro) | ✅ | LLMゲートウェイ(MaaS) |
|
||||
| [MCP](https://modelcontextprotocol.io/) | ✅ | MCPプロトコルをサポート |
|
||||
## 対応LLMと統合
|
||||
|
||||
## 🤝 コミュニティ貢献
|
||||
| プロバイダー | タイプ | ステータス |
|
||||
|----------|------|--------|
|
||||
| [OpenAI](https://platform.openai.com/) | LLM | ✅ |
|
||||
| [Anthropic](https://www.anthropic.com/) | LLM | ✅ |
|
||||
| [DeepSeek](https://www.deepseek.com/) | LLM | ✅ |
|
||||
| [Google Gemini](https://aistudio.google.com/prompts/new_chat) | LLM | ✅ |
|
||||
| [xAI](https://x.ai/) | LLM | ✅ |
|
||||
| [Moonshot](https://www.moonshot.cn/) | LLM | ✅ |
|
||||
| [Zhipu AI](https://open.bigmodel.cn/) | LLM | ✅ |
|
||||
| [Ollama](https://ollama.com/) | ローカルLLM | ✅ |
|
||||
| [LM Studio](https://lmstudio.ai/) | ローカルLLM | ✅ |
|
||||
| [Dify](https://dify.ai) | LLMOps | ✅ |
|
||||
| [MCP](https://modelcontextprotocol.io/) | プロトコル | ✅ |
|
||||
| [SiliconFlow](https://siliconflow.cn/) | ゲートウェイ | ✅ |
|
||||
| [Aliyun Bailian](https://bailian.console.aliyun.com/) | ゲートウェイ | ✅ |
|
||||
| [Volc Engine Ark](https://console.volcengine.com/ark/region:ark+cn-beijing/model?vendor=Bytedance&view=LIST_VIEW) | ゲートウェイ | ✅ |
|
||||
| [ModelScope](https://modelscope.cn/docs/model-service/API-Inference/intro) | ゲートウェイ | ✅ |
|
||||
| [GiteeAI](https://ai.gitee.com/) | ゲートウェイ | ✅ |
|
||||
| [CompShare](https://www.compshare.cn/?ytag=GPU_YY-gh_langbot) | GPUプラットフォーム | ✅ |
|
||||
| [PPIO](https://ppinfra.com/user/register?invited_by=QJKFYD&utm_source=github_langbot) | GPUプラットフォーム | ✅ |
|
||||
| [ShengSuanYun](https://www.shengsuanyun.com/?from=CH_KYIPP758) | GPUプラットフォーム | ✅ |
|
||||
| [接口 AI](https://jiekou.ai/) | ゲートウェイ | ✅ |
|
||||
| [302.AI](https://share.302.ai/SuTG99) | ゲートウェイ | ✅ |
|
||||
|
||||
LangBot への貢献に対して、以下の [コード貢献者](https://github.com/langbot-app/LangBot/graphs/contributors) とコミュニティの他のメンバーに感謝します。
|
||||
[→ すべての統合を表示](https://docs.langbot.app/en/insight/features.html)
|
||||
|
||||
---
|
||||
|
||||
## なぜ LangBot?
|
||||
|
||||
| ユースケース | LangBot の活用方法 |
|
||||
|----------|-------------------|
|
||||
| **カスタマーサポート** | ナレッジベースを活用して質問に回答するAIエージェントをSlack/Discord/Telegramにデプロイ |
|
||||
| **社内ツール** | n8n/Difyのワークフローを WeCom/DingTalk に接続し、業務プロセスを自動化 |
|
||||
| **コミュニティ管理** | AI搭載のコンテンツフィルタリングとインタラクションでQQ/Discordグループをモデレーション |
|
||||
| **マルチプラットフォーム展開** | 1つのボットで全プラットフォームに対応。単一のダッシュボードから管理 |
|
||||
|
||||
---
|
||||
|
||||
## ライブデモ
|
||||
|
||||
**今すぐ試す:** https://demo.langbot.dev/
|
||||
- メール: `demo@langbot.app`
|
||||
- パスワード: `langbot123456`
|
||||
|
||||
*注意: 公開デモ環境です。機密情報を入力しないでください。*
|
||||
|
||||
---
|
||||
|
||||
## コミュニティ
|
||||
|
||||
[](https://discord.gg/wdNEHETs87)
|
||||
|
||||
- [Discord コミュニティ](https://discord.gg/wdNEHETs87)
|
||||
|
||||
---
|
||||
|
||||
## Star 推移
|
||||
|
||||
[](https://star-history.com/#langbot-app/LangBot&Date)
|
||||
|
||||
---
|
||||
|
||||
## コントリビューター
|
||||
|
||||
LangBot をより良くするために貢献してくださったすべての[コントリビューター](https://github.com/langbot-app/LangBot/graphs/contributors)に感謝します:
|
||||
|
||||
<a href="https://github.com/langbot-app/LangBot/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=langbot-app/LangBot" />
|
||||
|
||||
196
README_KO.md
196
README_KO.md
@@ -1,25 +1,27 @@
|
||||
<p align="center">
|
||||
<a href="https://langbot.app">
|
||||
<img width="130" src="https://docs.langbot.app/langbot-logo.png" alt="LangBot"/>
|
||||
<img width="130" src="res/logo-blue.png" alt="LangBot"/>
|
||||
</a>
|
||||
|
||||
<div align="center">
|
||||
|
||||
<a href="https://www.producthunt.com/products/langbot?utm_source=badge-follow&utm_medium=badge&utm_source=badge-langbot" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/follow.svg?product_id=1077185&theme=light" alt="LangBot - Production-grade IM bot made easy. | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||
|
||||
<h3>LangBot으로 IM 봇을 빠르게 구축, 디버그 및 배포하세요.</h3>
|
||||
<h3>AI 에이전트 IM 봇 구축을 위한 프로덕션 등급 플랫폼.</h3>
|
||||
<h4>Slack, Discord, Telegram, WeChat 등에 AI 봇을 빠르게 구축, 디버그 및 배포.</h4>
|
||||
|
||||
[English](README_EN.md) / [简体中文](README.md) / [繁體中文](README_TW.md) / [日本語](README_JP.md) / [Español](README_ES.md) / [Français](README_FR.md) / 한국어 / [Русский](README_RU.md) / [Tiếng Việt](README_VI.md)
|
||||
[English](README.md) / [简体中文](README_CN.md) / [繁體中文](README_TW.md) / [日本語](README_JP.md) / [Español](README_ES.md) / [Français](README_FR.md) / 한국어 / [Русский](README_RU.md) / [Tiếng Việt](README_VI.md)
|
||||
|
||||
[](https://discord.gg/wdNEHETs87)
|
||||
[](https://deepwiki.com/langbot-app/LangBot)
|
||||
[](https://github.com/langbot-app/LangBot/releases/latest)
|
||||
<img src="https://img.shields.io/badge/python-3.10 ~ 3.13 -blue.svg" alt="python">
|
||||
[](https://github.com/langbot-app/LangBot/stargazers)
|
||||
|
||||
<a href="https://langbot.app">홈</a> |
|
||||
<a href="https://docs.langbot.app/en/insight/features.html">기능 사양</a> |
|
||||
<a href="https://docs.langbot.app/en/insight/guide.html">배포</a> |
|
||||
<a href="https://docs.langbot.app/en/tags/readme.html">API 통합</a> |
|
||||
<a href="https://docs.langbot.app/en/insight/features.html">기능</a> |
|
||||
<a href="https://docs.langbot.app/en/insight/guide.html">문서</a> |
|
||||
<a href="https://docs.langbot.app/en/tags/readme.html">API</a> |
|
||||
<a href="https://space.langbot.app">플러그인 마켓</a> |
|
||||
<a href="https://langbot.featurebase.app/roadmap">로드맵</a>
|
||||
|
||||
@@ -27,19 +29,36 @@
|
||||
|
||||
</p>
|
||||
|
||||
## 📦 시작하기
|
||||
---
|
||||
|
||||
#### 빠른 시작
|
||||
## LangBot이란?
|
||||
|
||||
`uvx`를 사용하여 한 명령으로 시작하세요 ([uv](https://docs.astral.sh/uv/getting-started/installation/) 설치 필요):
|
||||
LangBot은 AI 기반 인스턴트 메시징 봇을 구축하기 위한 **오픈소스 프로덕션 등급 플랫폼**입니다. 대규모 언어 모델(LLM)을 모든 채팅 플랫폼에 연결하여 대화, 작업 실행, 기존 워크플로우와의 통합이 가능한 지능형 에이전트를 만들 수 있습니다.
|
||||
|
||||
### 핵심 기능
|
||||
|
||||
- **AI 대화 및 에이전트** — 멀티턴 대화, 도구 호출, 멀티모달 지원, 스트리밍 출력. 내장 RAG(지식 베이스)와 [Dify](https://dify.ai), [Coze](https://coze.com), [n8n](https://n8n.io), [Langflow](https://langflow.org) 심층 통합.
|
||||
- **유니버설 IM 플랫폼 지원** — 단일 코드베이스로 Discord, Telegram, Slack, LINE, QQ, WeChat, WeCom, Lark, DingTalk, KOOK 지원.
|
||||
- **프로덕션 레디** — 접근 제어, 속도 제한, 민감어 필터링, 종합 모니터링 및 예외 처리. 기업 환경에서 검증됨.
|
||||
- **플러그인 생태계** — 수백 개의 플러그인, 이벤트 기반 아키텍처, 컴포넌트 확장, [MCP 프로토콜](https://modelcontextprotocol.io/) 지원.
|
||||
- **웹 관리 패널** — 직관적인 브라우저 인터페이스로 봇을 구성, 관리 및 모니터링. YAML 편집 불필요.
|
||||
- **멀티 파이프라인 아키텍처** — 다양한 시나리오에 맞는 다양한 봇 구성, 종합 모니터링 및 예외 처리.
|
||||
|
||||
[→ 모든 기능 자세히 보기](https://docs.langbot.app/en/insight/features.html)
|
||||
|
||||
---
|
||||
|
||||
## 빠른 시작
|
||||
|
||||
### 원라인 실행
|
||||
|
||||
```bash
|
||||
uvx langbot
|
||||
```
|
||||
|
||||
http://localhost:5300을 방문하여 사용을 시작하세요.
|
||||
> [uv](https://docs.astral.sh/uv/getting-started/installation/) 설치 필요. http://localhost:5300 방문 — 완료.
|
||||
|
||||
#### Docker Compose 배포
|
||||
### Docker Compose
|
||||
|
||||
```bash
|
||||
git clone https://github.com/langbot-app/LangBot
|
||||
@@ -47,102 +66,101 @@ cd LangBot/docker
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
http://localhost:5300을 방문하여 사용을 시작하세요.
|
||||
|
||||
자세한 문서는 [Docker 배포](https://docs.langbot.app/en/deploy/langbot/docker.html)를 참조하세요.
|
||||
|
||||
#### BTPanel 원클릭 배포
|
||||
|
||||
LangBot은 BTPanel에 등록되어 있습니다. BTPanel을 설치한 경우 [문서](https://docs.langbot.app/en/deploy/langbot/one-click/bt.html)를 사용하여 사용할 수 있습니다.
|
||||
|
||||
#### Zeabur 클라우드 배포
|
||||
|
||||
커뮤니티에서 제공하는 Zeabur 템플릿입니다.
|
||||
### 원클릭 클라우드 배포
|
||||
|
||||
[](https://zeabur.com/en-US/templates/ZKTBDH)
|
||||
|
||||
#### Railway 클라우드 배포
|
||||
|
||||
[](https://railway.app/template/yRrAyL?referralCode=vogKPF)
|
||||
|
||||
#### 기타 배포 방법
|
||||
**더 많은 옵션:** [Docker](https://docs.langbot.app/en/deploy/langbot/docker.html) · [수동 배포](https://docs.langbot.app/en/deploy/langbot/manual.html) · [BTPanel](https://docs.langbot.app/en/deploy/langbot/one-click/bt.html) · [Kubernetes](./docker/README_K8S.md)
|
||||
|
||||
릴리스 버전을 직접 사용하여 실행하려면 [수동 배포](https://docs.langbot.app/en/deploy/langbot/manual.html) 문서를 참조하세요.
|
||||
---
|
||||
|
||||
#### Kubernetes 배포
|
||||
|
||||
[Kubernetes 배포](./docker/README_K8S.md) 문서를 참조하세요.
|
||||
|
||||
## 😎 최신 정보 받기
|
||||
|
||||
리포지토리 오른쪽 상단의 Star 및 Watch 버튼을 클릭하여 최신 업데이트를 받으세요.
|
||||
|
||||

|
||||
|
||||
## ✨ 기능
|
||||
|
||||
<img width="500" src="https://docs.langbot.app/ui/bot-page-en-rounded.png" />
|
||||
|
||||
|
||||
- 💬 LLM / Agent와 채팅: 여러 LLM을 지원하며 그룹 채팅 및 개인 채팅에 적응; 멀티 라운드 대화, 도구 호출, 멀티모달, 스트리밍 출력 기능을 지원합니다. 내장된 RAG(지식 베이스) 구현 및 [Dify](https://dify.ai)、[Coze](https://coze.com)、[n8n](https://n8n.io) 등의 LLMOps 플랫폼과 깊이 통합됩니다.
|
||||
- 🤖 다중 플랫폼 지원: 현재 QQ, QQ Channel, WeCom, 개인 WeChat, Lark, DingTalk, Discord, Telegram, KOOK, Slack, LINE 등을 지원합니다.
|
||||
- 🛠️ 높은 안정성, 풍부한 기능: 네이티브 액세스 제어, 속도 제한, 민감한 단어 필터링 등의 메커니즘; 사용하기 쉽고 여러 배포 방법을 지원합니다. 여러 파이프라인 구성을 지원하며 다양한 시나리오에 대해 다른 봇을 사용할 수 있습니다.
|
||||
- 🧩 플러그인 확장, 활발한 커뮤니티: 고안정성, 고보안 생산 수준의 플러그인 시스템; 이벤트 기반, 컴포넌트 확장 등의 플러그인 메커니즘을 지원; Anthropic [MCP 프로토콜](https://modelcontextprotocol.io/) 통합; 현재 수백 개의 플러그인이 있습니다.
|
||||
- 😻 웹 UI: 브라우저를 통해 LangBot 인스턴스 관리를 지원합니다. 구성 파일을 수동으로 작성할 필요가 없습니다.
|
||||
|
||||
더 자세한 사양은 [문서](https://docs.langbot.app/en/insight/features.html)를 참조하세요.
|
||||
|
||||
또는 데모 환경을 방문하세요: https://demo.langbot.dev/
|
||||
- 로그인 정보: 이메일: `demo@langbot.app` 비밀번호: `langbot123456`
|
||||
- 참고: WebUI 데모 전용이므로 공개 환경에서는 민감한 정보를 입력하지 마세요.
|
||||
|
||||
### 메시징 플랫폼
|
||||
## 지원 플랫폼
|
||||
|
||||
| 플랫폼 | 상태 | 비고 |
|
||||
| --- | --- | --- |
|
||||
|--------|------|------|
|
||||
| Discord | ✅ | |
|
||||
| Telegram | ✅ | |
|
||||
| Slack | ✅ | |
|
||||
| LINE | ✅ | |
|
||||
| 개인 QQ | ✅ | |
|
||||
| QQ 공식 API | ✅ | |
|
||||
| WeCom | ✅ | |
|
||||
| WeComCS | ✅ | |
|
||||
| WeCom AI Bot | ✅ | |
|
||||
| 개인 WeChat | ✅ | |
|
||||
| KOOK | ✅ | |
|
||||
| QQ | ✅ | 개인 및 공식 API |
|
||||
| WeCom | ✅ | 기업 WeChat, 외부 CS, AI Bot |
|
||||
| WeChat | ✅ | 개인 및 공식 계정 |
|
||||
| Lark | ✅ | |
|
||||
| DingTalk | ✅ | |
|
||||
| KOOK | ✅ | |
|
||||
| Satori | ✅ | |
|
||||
|
||||
### LLMs
|
||||
---
|
||||
|
||||
| LLM | 상태 | 비고 |
|
||||
| --- | --- | --- |
|
||||
| [OpenAI](https://platform.openai.com/) | ✅ | 모든 OpenAI 인터페이스 형식 모델에 사용 가능 |
|
||||
| [DeepSeek](https://www.deepseek.com/) | ✅ | |
|
||||
| [Moonshot](https://www.moonshot.cn/) | ✅ | |
|
||||
| [Anthropic](https://www.anthropic.com/) | ✅ | |
|
||||
| [xAI](https://x.ai/) | ✅ | |
|
||||
| [Zhipu AI](https://open.bigmodel.cn/) | ✅ | |
|
||||
| [CompShare](https://www.compshare.cn/?ytag=GPU_YY-gh_langbot) | ✅ | LLM 및 GPU 리소스 플랫폼 |
|
||||
| [PPIO](https://ppinfra.com/user/register?invited_by=QJKFYD&utm_source=github_langbot) | ✅ | LLM 및 GPU 리소스 플랫폼 |
|
||||
| [接口 AI](https://jiekou.ai/) | ✅ | LLM 집계 플랫폼 |
|
||||
| [ShengSuanYun](https://www.shengsuanyun.com/?from=CH_KYIPP758) | ✅ | LLM 및 GPU 리소스 플랫폼 |
|
||||
| [302.AI](https://share.302.ai/SuTG99) | ✅ | LLM 게이트웨이(MaaS) |
|
||||
| [Google Gemini](https://aistudio.google.com/prompts/new_chat) | ✅ | |
|
||||
| [Dify](https://dify.ai) | ✅ | LLMOps 플랫폼 |
|
||||
| [Ollama](https://ollama.com/) | ✅ | 로컬 LLM 실행 플랫폼 |
|
||||
| [LMStudio](https://lmstudio.ai/) | ✅ | 로컬 LLM 실행 플랫폼 |
|
||||
| [GiteeAI](https://ai.gitee.com/) | ✅ | LLM 인터페이스 게이트웨이(MaaS) |
|
||||
| [SiliconFlow](https://siliconflow.cn/) | ✅ | LLM 게이트웨이(MaaS) |
|
||||
| [Aliyun Bailian](https://bailian.console.aliyun.com/) | ✅ | LLM 게이트웨이(MaaS), LLMOps 플랫폼 |
|
||||
| [Volc Engine Ark](https://console.volcengine.com/ark/region:ark+cn-beijing/model?vendor=Bytedance&view=LIST_VIEW) | ✅ | LLM 게이트웨이(MaaS), LLMOps 플랫폼 |
|
||||
| [ModelScope](https://modelscope.cn/docs/model-service/API-Inference/intro) | ✅ | LLM 게이트웨이(MaaS) |
|
||||
| [MCP](https://modelcontextprotocol.io/) | ✅ | MCP 프로토콜을 통한 도구 액세스 지원 |
|
||||
## 지원 LLM 및 통합
|
||||
|
||||
## 🤝 커뮤니티 기여
|
||||
| 제공자 | 유형 | 상태 |
|
||||
|--------|------|------|
|
||||
| [OpenAI](https://platform.openai.com/) | LLM | ✅ |
|
||||
| [Anthropic](https://www.anthropic.com/) | LLM | ✅ |
|
||||
| [DeepSeek](https://www.deepseek.com/) | LLM | ✅ |
|
||||
| [Google Gemini](https://aistudio.google.com/prompts/new_chat) | LLM | ✅ |
|
||||
| [xAI](https://x.ai/) | LLM | ✅ |
|
||||
| [Moonshot](https://www.moonshot.cn/) | LLM | ✅ |
|
||||
| [Zhipu AI](https://open.bigmodel.cn/) | LLM | ✅ |
|
||||
| [Ollama](https://ollama.com/) | 로컬 LLM | ✅ |
|
||||
| [LM Studio](https://lmstudio.ai/) | 로컬 LLM | ✅ |
|
||||
| [Dify](https://dify.ai) | LLMOps | ✅ |
|
||||
| [MCP](https://modelcontextprotocol.io/) | 프로토콜 | ✅ |
|
||||
| [SiliconFlow](https://siliconflow.cn/) | 게이트웨이 | ✅ |
|
||||
| [Aliyun Bailian](https://bailian.console.aliyun.com/) | 게이트웨이 | ✅ |
|
||||
| [Volc Engine Ark](https://console.volcengine.com/ark/region:ark+cn-beijing/model?vendor=Bytedance&view=LIST_VIEW) | 게이트웨이 | ✅ |
|
||||
| [ModelScope](https://modelscope.cn/docs/model-service/API-Inference/intro) | 게이트웨이 | ✅ |
|
||||
| [GiteeAI](https://ai.gitee.com/) | 게이트웨이 | ✅ |
|
||||
| [CompShare](https://www.compshare.cn/?ytag=GPU_YY-gh_langbot) | GPU 플랫폼 | ✅ |
|
||||
| [PPIO](https://ppinfra.com/user/register?invited_by=QJKFYD&utm_source=github_langbot) | GPU 플랫폼 | ✅ |
|
||||
| [ShengSuanYun](https://www.shengsuanyun.com/?from=CH_KYIPP758) | GPU 플랫폼 | ✅ |
|
||||
| [接口 AI](https://jiekou.ai/) | 게이트웨이 | ✅ |
|
||||
| [302.AI](https://share.302.ai/SuTG99) | 게이트웨이 | ✅ |
|
||||
|
||||
다음 [코드 기여자](https://github.com/langbot-app/LangBot/graphs/contributors) 및 커뮤니티의 다른 구성원들의 LangBot 기여에 감사드립니다:
|
||||
[→ 모든 통합 보기](https://docs.langbot.app/en/insight/features.html)
|
||||
|
||||
---
|
||||
|
||||
## 왜 LangBot인가?
|
||||
|
||||
| 사용 사례 | LangBot 활용 방법 |
|
||||
|-----------|-------------------|
|
||||
| **고객 지원** | 지식 베이스를 활용하여 질문에 답변하는 AI 에이전트를 Slack/Discord/Telegram에 배포 |
|
||||
| **내부 도구** | n8n/Dify 워크플로우를 WeCom/DingTalk에 연결하여 비즈니스 프로세스 자동화 |
|
||||
| **커뮤니티 관리** | AI 기반 콘텐츠 필터링 및 상호작용으로 QQ/Discord 그룹 관리 |
|
||||
| **멀티 플랫폼** | 하나의 봇으로 모든 플랫폼 지원. 단일 대시보드에서 관리 |
|
||||
|
||||
---
|
||||
|
||||
## 라이브 데모
|
||||
|
||||
**지금 체험:** https://demo.langbot.dev/
|
||||
- 이메일: `demo@langbot.app`
|
||||
- 비밀번호: `langbot123456`
|
||||
|
||||
*참고: 공개 데모 환경입니다. 민감한 정보를 입력하지 마세요.*
|
||||
|
||||
---
|
||||
|
||||
## 커뮤니티
|
||||
|
||||
[](https://discord.gg/wdNEHETs87)
|
||||
|
||||
- [Discord 커뮤니티](https://discord.gg/wdNEHETs87)
|
||||
|
||||
---
|
||||
|
||||
## Star 추이
|
||||
|
||||
[](https://star-history.com/#langbot-app/LangBot&Date)
|
||||
|
||||
---
|
||||
|
||||
## 기여자
|
||||
|
||||
LangBot을 더 나은 프로젝트로 만들어 주신 모든 [기여자](https://github.com/langbot-app/LangBot/graphs/contributors)분들께 감사드립니다:
|
||||
|
||||
<a href="https://github.com/langbot-app/LangBot/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=langbot-app/LangBot" />
|
||||
|
||||
196
README_RU.md
196
README_RU.md
@@ -1,25 +1,27 @@
|
||||
<p align="center">
|
||||
<a href="https://langbot.app">
|
||||
<img width="130" src="https://docs.langbot.app/langbot-logo.png" alt="LangBot"/>
|
||||
<img width="130" src="res/logo-blue.png" alt="LangBot"/>
|
||||
</a>
|
||||
|
||||
<div align="center">
|
||||
|
||||
<a href="https://www.producthunt.com/products/langbot?utm_source=badge-follow&utm_medium=badge&utm_source=badge-langbot" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/follow.svg?product_id=1077185&theme=light" alt="LangBot - Production-grade IM bot made easy. | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||
|
||||
<h3>Быстро создавайте, отлаживайте и развертывайте IM-ботов с LangBot.</h3>
|
||||
<h3>Платформа производственного уровня для создания агентных IM-ботов.</h3>
|
||||
<h4>Быстро создавайте, отлаживайте и развертывайте ИИ-ботов в Slack, Discord, Telegram, WeChat и других платформах.</h4>
|
||||
|
||||
[English](README_EN.md) / [简体中文](README.md) / [繁體中文](README_TW.md) / [日本語](README_JP.md) / [Español](README_ES.md) / [Français](README_FR.md) / [한국어](README_KO.md) / Русский / [Tiếng Việt](README_VI.md)
|
||||
[English](README.md) / [简体中文](README_CN.md) / [繁體中文](README_TW.md) / [日本語](README_JP.md) / [Español](README_ES.md) / [Français](README_FR.md) / [한국어](README_KO.md) / Русский / [Tiếng Việt](README_VI.md)
|
||||
|
||||
[](https://discord.gg/wdNEHETs87)
|
||||
[](https://deepwiki.com/langbot-app/LangBot)
|
||||
[](https://github.com/langbot-app/LangBot/releases/latest)
|
||||
<img src="https://img.shields.io/badge/python-3.10 ~ 3.13 -blue.svg" alt="python">
|
||||
[](https://github.com/langbot-app/LangBot/stargazers)
|
||||
|
||||
<a href="https://langbot.app">Главная</a> |
|
||||
<a href="https://docs.langbot.app/en/insight/features.html">Характеристики</a> |
|
||||
<a href="https://docs.langbot.app/en/insight/guide.html">Развертывание</a> |
|
||||
<a href="https://docs.langbot.app/en/tags/readme.html">Интеграция API</a> |
|
||||
<a href="https://docs.langbot.app/en/insight/features.html">Возможности</a> |
|
||||
<a href="https://docs.langbot.app/en/insight/guide.html">Документация</a> |
|
||||
<a href="https://docs.langbot.app/en/tags/readme.html">API</a> |
|
||||
<a href="https://space.langbot.app">Магазин плагинов</a> |
|
||||
<a href="https://langbot.featurebase.app/roadmap">Дорожная карта</a>
|
||||
|
||||
@@ -27,19 +29,36 @@
|
||||
|
||||
</p>
|
||||
|
||||
## 📦 Начало работы
|
||||
---
|
||||
|
||||
#### Быстрый старт
|
||||
## Что такое LangBot?
|
||||
|
||||
Используйте `uvx` для запуска одной командой (требуется установка [uv](https://docs.astral.sh/uv/getting-started/installation/)):
|
||||
LangBot — это **платформа с открытым исходным кодом производственного уровня** для создания ИИ-ботов в мессенджерах. Она связывает большие языковые модели (LLM) с любой чат-платформой, позволяя создавать интеллектуальных агентов, которые могут вести диалоги, выполнять задачи и интегрироваться с вашими существующими рабочими процессами.
|
||||
|
||||
### Ключевые возможности
|
||||
|
||||
- **ИИ-диалоги и агенты** — Многораундовые диалоги, вызов инструментов, мультимодальная поддержка, потоковый вывод. Встроенная реализация RAG (база знаний) с глубокой интеграцией в [Dify](https://dify.ai), [Coze](https://coze.com), [n8n](https://n8n.io), [Langflow](https://langflow.org).
|
||||
- **Универсальная поддержка IM-платформ** — Единая кодовая база для Discord, Telegram, Slack, LINE, QQ, WeChat, WeCom, Lark, DingTalk, KOOK.
|
||||
- **Готовность к продакшену** — Контроль доступа, ограничение скорости, фильтрация чувствительных слов, комплексный мониторинг и обработка исключений. Проверено в корпоративной среде.
|
||||
- **Экосистема плагинов** — Сотни плагинов, событийно-ориентированная архитектура, расширения компонентов и поддержка [протокола MCP](https://modelcontextprotocol.io/).
|
||||
- **Веб-панель управления** — Настраивайте, управляйте и мониторьте ваших ботов через интуитивный браузерный интерфейс. Ручное редактирование YAML не требуется.
|
||||
- **Мультиконвейерная архитектура** — Разные боты для разных сценариев с комплексным мониторингом и обработкой исключений.
|
||||
|
||||
[→ Подробнее обо всех возможностях](https://docs.langbot.app/en/insight/features.html)
|
||||
|
||||
---
|
||||
|
||||
## Быстрый старт
|
||||
|
||||
### Запуск одной командой
|
||||
|
||||
```bash
|
||||
uvx langbot
|
||||
```
|
||||
|
||||
Посетите http://localhost:5300, чтобы начать использование.
|
||||
> Требуется [uv](https://docs.astral.sh/uv/getting-started/installation/). Откройте http://localhost:5300 — готово.
|
||||
|
||||
#### Развертывание с Docker Compose
|
||||
### Docker Compose
|
||||
|
||||
```bash
|
||||
git clone https://github.com/langbot-app/LangBot
|
||||
@@ -47,102 +66,101 @@ cd LangBot/docker
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
Посетите http://localhost:5300, чтобы начать использование.
|
||||
|
||||
Подробная документация [Развертывание Docker](https://docs.langbot.app/en/deploy/langbot/docker.html).
|
||||
|
||||
#### Развертывание одним кликом на BTPanel
|
||||
|
||||
LangBot добавлен в BTPanel. Если у вас установлен BTPanel, вы можете использовать [документацию](https://docs.langbot.app/en/deploy/langbot/one-click/bt.html) для его использования.
|
||||
|
||||
#### Облачное развертывание Zeabur
|
||||
|
||||
Шаблон Zeabur, предоставленный сообществом.
|
||||
### Облачное развертывание одним кликом
|
||||
|
||||
[](https://zeabur.com/en-US/templates/ZKTBDH)
|
||||
|
||||
#### Облачное развертывание Railway
|
||||
|
||||
[](https://railway.app/template/yRrAyL?referralCode=vogKPF)
|
||||
|
||||
#### Другие методы развертывания
|
||||
**Другие варианты:** [Docker](https://docs.langbot.app/en/deploy/langbot/docker.html) · [Ручная установка](https://docs.langbot.app/en/deploy/langbot/manual.html) · [BTPanel](https://docs.langbot.app/en/deploy/langbot/one-click/bt.html) · [Kubernetes](./docker/README_K8S.md)
|
||||
|
||||
Используйте выпущенную версию напрямую для запуска, см. документацию [Ручное развертывание](https://docs.langbot.app/en/deploy/langbot/manual.html).
|
||||
---
|
||||
|
||||
#### Развертывание Kubernetes
|
||||
|
||||
См. документацию [Развертывание Kubernetes](./docker/README_K8S.md).
|
||||
|
||||
## 😎 Оставайтесь в курсе
|
||||
|
||||
Нажмите кнопки Star и Watch в правом верхнем углу репозитория, чтобы получать последние обновления.
|
||||
|
||||

|
||||
|
||||
## ✨ Функции
|
||||
|
||||
<img width="500" src="https://docs.langbot.app/ui/bot-page-en-rounded.png" />
|
||||
|
||||
|
||||
- 💬 Чат с LLM / Agent: Поддержка нескольких LLM, адаптация к групповым и личным чатам; Поддержка многораундовых разговоров, вызовов инструментов, мультимодальных возможностей и потоковой передачи. Встроенная реализация RAG (база знаний) и глубокая интеграция с [Dify](https://dify.ai), [Coze](https://coze.com), [n8n](https://n8n.io) 등의 LLMOps 플랫포트폼과 깊이 통합됩니다.
|
||||
- 🤖 Многоплатформенная поддержка: В настоящее время поддерживает QQ, QQ Channel, WeCom, личный WeChat, Lark, DingTalk, Discord, Telegram, KOOK, Slack, LINE и т.д.
|
||||
- 🛠️ Высокая стабильность, богатство функций: Нативный контроль доступа, ограничение скорости, фильтрация чувствительных слов и т.д.; Простота в использовании, поддержка нескольких методов развертывания. Поддержка нескольких конфигураций конвейера, разные боты для разных сценариев.
|
||||
- 🧩 Расширение плагинов, активное сообщество: Высокая стабильность, высокая безопасность уровня производства; Поддержка механизмов плагинов, управляемых событиями, расширения компонентов и т.д.; Интеграция протокола [MCP](https://modelcontextprotocol.io/) от Anthropic; В настоящее время сотни плагинов.
|
||||
- 😻 Веб-интерфейс: Поддержка управления экземплярами LangBot через браузер. Нет необходимости вручную писать конфигурационные файлы.
|
||||
|
||||
Для более подробных спецификаций обратитесь к [документации](https://docs.langbot.app/en/insight/features.html).
|
||||
|
||||
Или посетите демонстрационную среду: https://demo.langbot.dev/
|
||||
- Информация для входа: Email: `demo@langbot.app` Пароль: `langbot123456`
|
||||
- Примечание: Только для демонстрации WebUI, пожалуйста, не вводите конфиденциальную информацию в общедоступной среде.
|
||||
|
||||
### Платформы обмена сообщениями
|
||||
## Поддерживаемые платформы
|
||||
|
||||
| Платформа | Статус | Примечания |
|
||||
| --- | --- | --- |
|
||||
|-----------|--------|------------|
|
||||
| Discord | ✅ | |
|
||||
| Telegram | ✅ | |
|
||||
| Slack | ✅ | |
|
||||
| LINE | ✅ | |
|
||||
| Личный QQ | ✅ | |
|
||||
| Официальный API QQ | ✅ | |
|
||||
| WeCom | ✅ | |
|
||||
| WeComCS | ✅ | |
|
||||
| WeCom AI Bot | ✅ | |
|
||||
| Личный WeChat | ✅ | |
|
||||
| KOOK | ✅ | |
|
||||
| QQ | ✅ | Личный и официальный API |
|
||||
| WeCom | ✅ | Корпоративный WeChat, внешний CS, AI-бот |
|
||||
| WeChat | ✅ | Личный и официальный аккаунт |
|
||||
| Lark | ✅ | |
|
||||
| DingTalk | ✅ | |
|
||||
| KOOK | ✅ | |
|
||||
| Satori | ✅ | |
|
||||
|
||||
### LLMs
|
||||
---
|
||||
|
||||
| LLM | Статус | Примечания |
|
||||
| --- | --- | --- |
|
||||
| [OpenAI](https://platform.openai.com/) | ✅ | Доступна для любой модели формата интерфейса OpenAI |
|
||||
| [DeepSeek](https://www.deepseek.com/) | ✅ | |
|
||||
| [Moonshot](https://www.moonshot.cn/) | ✅ | |
|
||||
| [Anthropic](https://www.anthropic.com/) | ✅ | |
|
||||
| [xAI](https://x.ai/) | ✅ | |
|
||||
| [Zhipu AI](https://open.bigmodel.cn/) | ✅ | |
|
||||
| [CompShare](https://www.compshare.cn/?ytag=GPU_YY-gh_langbot) | ✅ | Платформа ресурсов LLM и GPU |
|
||||
| [PPIO](https://ppinfra.com/user/register?invited_by=QJKFYD&utm_source=github_langbot) | ✅ | Платформа ресурсов LLM и GPU |
|
||||
| [接口 AI](https://jiekou.ai/) | ✅ | Платформа агрегации LLM |
|
||||
| [ShengSuanYun](https://www.shengsuanyun.com/?from=CH_KYIPP758) | ✅ | Платформа ресурсов LLM и GPU |
|
||||
| [302.AI](https://share.302.ai/SuTG99) | ✅ | Шлюз LLM (MaaS) |
|
||||
| [Google Gemini](https://aistudio.google.com/prompts/new_chat) | ✅ | |
|
||||
| [Dify](https://dify.ai) | ✅ | Платформа LLMOps |
|
||||
| [Ollama](https://ollama.com/) | ✅ | Платформа локального запуска LLM |
|
||||
| [LMStudio](https://lmstudio.ai/) | ✅ | Платформа локального запуска LLM |
|
||||
| [GiteeAI](https://ai.gitee.com/) | ✅ | Шлюз интерфейса LLM (MaaS) |
|
||||
| [SiliconFlow](https://siliconflow.cn/) | ✅ | Шлюз LLM (MaaS) |
|
||||
| [Aliyun Bailian](https://bailian.console.aliyun.com/) | ✅ | Шлюз LLM (MaaS), платформа LLMOps |
|
||||
| [Volc Engine Ark](https://console.volcengine.com/ark/region:ark+cn-beijing/model?vendor=Bytedance&view=LIST_VIEW) | ✅ | Шлюз LLM (MaaS), платформа LLMOps |
|
||||
| [ModelScope](https://modelscope.cn/docs/model-service/API-Inference/intro) | ✅ | Шлюз LLM (MaaS) |
|
||||
| [MCP](https://modelcontextprotocol.io/) | ✅ | Поддержка доступа к инструментам через протокол MCP |
|
||||
## Поддерживаемые LLM и интеграции
|
||||
|
||||
## 🤝 Вклад сообщества
|
||||
| Провайдер | Тип | Статус |
|
||||
|-----------|-----|--------|
|
||||
| [OpenAI](https://platform.openai.com/) | LLM | ✅ |
|
||||
| [Anthropic](https://www.anthropic.com/) | LLM | ✅ |
|
||||
| [DeepSeek](https://www.deepseek.com/) | LLM | ✅ |
|
||||
| [Google Gemini](https://aistudio.google.com/prompts/new_chat) | LLM | ✅ |
|
||||
| [xAI](https://x.ai/) | LLM | ✅ |
|
||||
| [Moonshot](https://www.moonshot.cn/) | LLM | ✅ |
|
||||
| [Zhipu AI](https://open.bigmodel.cn/) | LLM | ✅ |
|
||||
| [Ollama](https://ollama.com/) | Локальный LLM | ✅ |
|
||||
| [LM Studio](https://lmstudio.ai/) | Локальный LLM | ✅ |
|
||||
| [Dify](https://dify.ai) | LLMOps | ✅ |
|
||||
| [MCP](https://modelcontextprotocol.io/) | Протокол | ✅ |
|
||||
| [SiliconFlow](https://siliconflow.cn/) | Шлюз | ✅ |
|
||||
| [Aliyun Bailian](https://bailian.console.aliyun.com/) | Шлюз | ✅ |
|
||||
| [Volc Engine Ark](https://console.volcengine.com/ark/region:ark+cn-beijing/model?vendor=Bytedance&view=LIST_VIEW) | Шлюз | ✅ |
|
||||
| [ModelScope](https://modelscope.cn/docs/model-service/API-Inference/intro) | Шлюз | ✅ |
|
||||
| [GiteeAI](https://ai.gitee.com/) | Шлюз | ✅ |
|
||||
| [302.AI](https://share.302.ai/SuTG99) | Шлюз | ✅ |
|
||||
| [接口 AI](https://jiekou.ai/) | Шлюз | ✅ |
|
||||
| [CompShare](https://www.compshare.cn/?ytag=GPU_YY-gh_langbot) | Платформа GPU | ✅ |
|
||||
| [PPIO](https://ppinfra.com/user/register?invited_by=QJKFYD&utm_source=github_langbot) | Платформа GPU | ✅ |
|
||||
| [ShengSuanYun](https://www.shengsuanyun.com/?from=CH_KYIPP758) | Платформа GPU | ✅ |
|
||||
|
||||
Спасибо следующим [контрибьюторам кода](https://github.com/langbot-app/LangBot/graphs/contributors) и другим членам сообщества за их вклад в LangBot:
|
||||
[→ Смотреть все интеграции](https://docs.langbot.app/en/insight/features.html)
|
||||
|
||||
---
|
||||
|
||||
## Почему LangBot?
|
||||
|
||||
| Сценарий использования | Как помогает LangBot |
|
||||
|------------------------|----------------------|
|
||||
| **Поддержка клиентов** | Разверните ИИ-агентов в Slack/Discord/Telegram, которые отвечают на вопросы, используя вашу базу знаний |
|
||||
| **Внутренние инструменты** | Подключите рабочие процессы n8n/Dify к WeCom/DingTalk для автоматизации бизнес-процессов |
|
||||
| **Управление сообществом** | Модерируйте группы QQ/Discord с помощью ИИ-фильтрации контента и взаимодействия |
|
||||
| **Мультиплатформенное присутствие** | Один бот — все платформы. Управляйте из единой панели |
|
||||
|
||||
---
|
||||
|
||||
## Демо
|
||||
|
||||
**Попробуйте прямо сейчас:** https://demo.langbot.dev/
|
||||
- Email: `demo@langbot.app`
|
||||
- Пароль: `langbot123456`
|
||||
|
||||
*Примечание: Публичная демо-среда. Не вводите конфиденциальную информацию.*
|
||||
|
||||
---
|
||||
|
||||
## Сообщество
|
||||
|
||||
[](https://discord.gg/wdNEHETs87)
|
||||
|
||||
- [Сообщество Discord](https://discord.gg/wdNEHETs87)
|
||||
|
||||
---
|
||||
|
||||
## История Stars
|
||||
|
||||
[](https://star-history.com/#langbot-app/LangBot&Date)
|
||||
|
||||
---
|
||||
|
||||
## Участники
|
||||
|
||||
Спасибо всем [участникам](https://github.com/langbot-app/LangBot/graphs/contributors), которые помогли сделать LangBot лучше:
|
||||
|
||||
<a href="https://github.com/langbot-app/LangBot/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=langbot-app/LangBot" />
|
||||
|
||||
216
README_TW.md
216
README_TW.md
@@ -1,25 +1,29 @@
|
||||
<p align="center">
|
||||
<a href="https://langbot.app">
|
||||
<img width="130" src="https://docs.langbot.app/langbot-logo.png" alt="LangBot"/>
|
||||
<img width="130" src="res/logo-blue.png" alt="LangBot"/>
|
||||
</a>
|
||||
|
||||
<div align="center"><a href="https://hellogithub.com/repository/langbot-app/LangBot" target="_blank"><img src="https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=5ce8ae2aa4f74316bf393b57b952433c&claim_uid=gtmc6YWjMZkT21R" alt="Featured|HelloGitHub" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||
<div align="center">
|
||||
|
||||
<h3>使用 LangBot 快速建構、除錯和部署 IM 機器人。</h3>
|
||||
<a href="https://hellogithub.com/repository/langbot-app/LangBot" target="_blank"><img src="https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=5ce8ae2aa4f74316bf393b57b952433c&claim_uid=gtmc6YWjMZkT21R" alt="Featured|HelloGitHub" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||
|
||||
[English](README_EN.md) / [简体中文](README.md) / 繁體中文 / [日本語](README_JP.md) / [Español](README_ES.md) / [Français](README_FR.md) / [한국어](README_KO.md) / [Русский](README_RU.md) / [Tiếng Việt](README_VI.md)
|
||||
<h3>生產級 AI 即時通訊機器人開發平台。</h3>
|
||||
<h4>快速建構、除錯和部署 AI 機器人到微信、QQ、飛書、Slack、Discord、Telegram 等平台。</h4>
|
||||
|
||||
[English](README.md) / [简体中文](README_CN.md) / 繁體中文 / [日本語](README_JP.md) / [Español](README_ES.md) / [Français](README_FR.md) / [한국어](README_KO.md) / [Русский](README_RU.md) / [Tiếng Việt](README_VI.md)
|
||||
|
||||
[](https://discord.gg/wdNEHETs87)
|
||||
[](https://qm.qq.com/q/JLi38whHum)
|
||||
[](https://deepwiki.com/langbot-app/LangBot)
|
||||
[](https://github.com/langbot-app/LangBot/releases/latest)
|
||||
<img src="https://img.shields.io/badge/python-3.10 ~ 3.13 -blue.svg" alt="python">
|
||||
[](https://github.com/langbot-app/LangBot/stargazers)
|
||||
[](https://gitcode.com/RockChinQ/LangBot)
|
||||
|
||||
<a href="https://langbot.app">主頁</a> |
|
||||
<a href="https://docs.langbot.app/zh/insight/features.html">規格特性</a> |
|
||||
<a href="https://docs.langbot.app/zh/insight/guide.html">部署文件</a> |
|
||||
<a href="https://docs.langbot.app/zh/tags/readme.html">API 整合</a> |
|
||||
<a href="https://langbot.app">官網</a> |
|
||||
<a href="https://docs.langbot.app/zh/insight/features.html">特性</a> |
|
||||
<a href="https://docs.langbot.app/zh/insight/guide.html">文件</a> |
|
||||
<a href="https://docs.langbot.app/zh/tags/readme.html">API</a> |
|
||||
<a href="https://space.langbot.app">外掛市場</a> |
|
||||
<a href="https://langbot.featurebase.app/roadmap">路線圖</a>
|
||||
|
||||
@@ -27,19 +31,36 @@
|
||||
|
||||
</p>
|
||||
|
||||
## 📦 開始使用
|
||||
---
|
||||
|
||||
#### 快速部署
|
||||
## 什麼是 LangBot?
|
||||
|
||||
使用 `uvx` 一鍵啟動(需要先安裝 [uv](https://docs.astral.sh/uv/getting-started/installation/) ):
|
||||
LangBot 是一個**開源的生產級平台**,用於建構 AI 驅動的即時通訊機器人。它將大語言模型(LLM)連接到各種聊天平台,幫助你創建能夠對話、執行任務、並整合到現有工作流程中的智能 Agent。
|
||||
|
||||
### 核心能力
|
||||
|
||||
- **AI 對話與 Agent** — 多輪對話、工具調用、多模態、流式輸出。自帶 RAG(知識庫),深度整合 [Dify](https://dify.ai)、[Coze](https://coze.com)、[n8n](https://n8n.io)、[Langflow](https://langflow.org) 等 LLMOps 平台。
|
||||
- **全平台支援** — 一套程式碼,覆蓋 QQ、微信、企業微信、飛書、釘釘、Discord、Telegram、Slack、LINE、KOOK 等平台。
|
||||
- **生產就緒** — 存取控制、限速、敏感詞過濾、全面監控與異常處理,已被多家企業採用。
|
||||
- **外掛生態** — 數百個外掛,事件驅動架構,組件擴展,適配 [MCP 協議](https://modelcontextprotocol.io/)。
|
||||
- **Web 管理面板** — 透過瀏覽器直觀地配置、管理和監控機器人,無需手動編輯設定檔。
|
||||
- **多流水線架構** — 不同機器人用於不同場景,具備全面的監控和異常處理能力。
|
||||
|
||||
[→ 了解更多功能特性](https://docs.langbot.app/zh/insight/features.html)
|
||||
|
||||
---
|
||||
|
||||
## 快速開始
|
||||
|
||||
### 一鍵啟動
|
||||
|
||||
```bash
|
||||
uvx langbot
|
||||
```
|
||||
|
||||
訪問 http://localhost:5300 即可開始使用。
|
||||
> 需要安裝 [uv](https://docs.astral.sh/uv/getting-started/installation/)。訪問 http://localhost:5300 即可使用。
|
||||
|
||||
#### Docker Compose 部署
|
||||
### Docker Compose
|
||||
|
||||
```bash
|
||||
git clone https://github.com/langbot-app/LangBot
|
||||
@@ -47,103 +68,63 @@ cd LangBot/docker
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
訪問 http://localhost:5300 即可開始使用。
|
||||
|
||||
詳細文件[Docker 部署](https://docs.langbot.app/zh/deploy/langbot/docker.html)。
|
||||
|
||||
#### 寶塔面板部署
|
||||
|
||||
已上架寶塔面板,若您已安裝寶塔面板,可以根據[文件](https://docs.langbot.app/zh/deploy/langbot/one-click/bt.html)使用。
|
||||
|
||||
#### Zeabur 雲端部署
|
||||
|
||||
社群貢獻的 Zeabur 模板。
|
||||
### 一鍵雲端部署
|
||||
|
||||
[](https://zeabur.com/zh-CN/templates/ZKTBDH)
|
||||
|
||||
#### Railway 雲端部署
|
||||
|
||||
[](https://railway.app/template/yRrAyL?referralCode=vogKPF)
|
||||
|
||||
#### 手動部署
|
||||
**更多方式:** [Docker](https://docs.langbot.app/zh/deploy/langbot/docker.html) · [手動部署](https://docs.langbot.app/zh/deploy/langbot/manual.html) · [寶塔面板](https://docs.langbot.app/zh/deploy/langbot/one-click/bt.html) · [Kubernetes](./docker/README_K8S.md)
|
||||
|
||||
直接使用發行版運行,查看文件[手動部署](https://docs.langbot.app/zh/deploy/langbot/manual.html)。
|
||||
---
|
||||
|
||||
#### Kubernetes 部署
|
||||
|
||||
參考 [Kubernetes 部署](./docker/README_K8S.md) 文件。
|
||||
|
||||
## 😎 保持更新
|
||||
|
||||
點擊倉庫右上角 Star 和 Watch 按鈕,獲取最新動態。
|
||||
|
||||

|
||||
|
||||
## ✨ 特性
|
||||
|
||||
<img width="500" src="https://docs.langbot.app/ui/bot-page-en-rounded.png" />
|
||||
|
||||
|
||||
- 💬 大模型對話、Agent:支援多種大模型,適配群聊和私聊;具有多輪對話、工具調用、多模態、流式輸出能力,自帶 RAG(知識庫)實現,並深度適配 [Dify](https://dify.ai)、[Coze](https://coze.com)、[n8n](https://n8n.io) 等 LLMOps 平台。
|
||||
- 🤖 多平台支援:目前支援 QQ、QQ頻道、企業微信、個人微信、飛書、Discord、Telegram、KOOK、Slack、LINE 等平台。
|
||||
- 🛠️ 高穩定性、功能完備:原生支援訪問控制、限速、敏感詞過濾等機制;配置簡單,支援多種部署方式。支援多流水線配置,不同機器人用於不同應用場景。
|
||||
- 🧩 外掛擴展、活躍社群:高穩定性、高安全性的生產級外掛系統;支援事件驅動、組件擴展等外掛機制;適配 Anthropic [MCP 協議](https://modelcontextprotocol.io/);目前已有數百個外掛。
|
||||
- 😻 Web 管理面板:支援通過瀏覽器管理 LangBot 實例,不再需要手動編寫配置文件。
|
||||
|
||||
詳細規格特性請訪問[文件](https://docs.langbot.app/zh/insight/features.html)。
|
||||
|
||||
或訪問 demo 環境:https://demo.langbot.dev/
|
||||
- 登入資訊:郵箱:`demo@langbot.app` 密碼:`langbot123456`
|
||||
- 注意:僅展示 WebUI 效果,公開環境,請不要在其中填入您的任何敏感資訊。
|
||||
|
||||
### 訊息平台
|
||||
## 支援的平台
|
||||
|
||||
| 平台 | 狀態 | 備註 |
|
||||
| --- | --- | --- |
|
||||
|------|------|------|
|
||||
| QQ | ✅ | 個人號、官方機器人(頻道、私聊、群聊) |
|
||||
| 微信 | ✅ | 個人微信、微信公眾號 |
|
||||
| 企業微信 | ✅ | 應用訊息、對外客服、智能機器人 |
|
||||
| 飛書 | ✅ | |
|
||||
| 釘釘 | ✅ | |
|
||||
| Discord | ✅ | |
|
||||
| Telegram | ✅ | |
|
||||
| Slack | ✅ | |
|
||||
| LINE | ✅ | |
|
||||
| QQ 個人號 | ✅ | QQ 個人號私聊、群聊 |
|
||||
| QQ 官方機器人 | ✅ | QQ 官方機器人,支援頻道、私聊、群聊 |
|
||||
| 微信 | ✅ | |
|
||||
| 企微對外客服 | ✅ | |
|
||||
| 企微智能機器人 | ✅ | |
|
||||
| 微信公眾號 | ✅ | |
|
||||
| KOOK | ✅ | |
|
||||
| Lark | ✅ | |
|
||||
| DingTalk | ✅ | |
|
||||
| Satori | ✅ | |
|
||||
|
||||
### 大模型能力
|
||||
---
|
||||
|
||||
| 模型 | 狀態 | 備註 |
|
||||
| --- | --- | --- |
|
||||
| [OpenAI](https://platform.openai.com/) | ✅ | 可接入任何 OpenAI 介面格式模型 |
|
||||
| [DeepSeek](https://www.deepseek.com/) | ✅ | |
|
||||
| [Moonshot](https://www.moonshot.cn/) | ✅ | |
|
||||
| [Anthropic](https://www.anthropic.com/) | ✅ | |
|
||||
| [xAI](https://x.ai/) | ✅ | |
|
||||
| [智譜AI](https://open.bigmodel.cn/) | ✅ | |
|
||||
| [勝算雲](https://www.shengsuanyun.com/?from=CH_KYIPP758) | ✅ | 大模型和 GPU 資源平台 |
|
||||
| [優雲智算](https://www.compshare.cn/?ytag=GPU_YY-gh_langbot) | ✅ | 大模型和 GPU 資源平台 |
|
||||
| [PPIO](https://ppinfra.com/user/register?invited_by=QJKFYD&utm_source=github_langbot) | ✅ | 大模型和 GPU 資源平台 |
|
||||
| [接口 AI](https://jiekou.ai/) | ✅ | 大模型聚合平台,專注全球大模型接入 |
|
||||
| [302.AI](https://share.302.ai/SuTG99) | ✅ | 大模型聚合平台 |
|
||||
| [Google Gemini](https://aistudio.google.com/prompts/new_chat) | ✅ | |
|
||||
| [Dify](https://dify.ai) | ✅ | LLMOps 平台 |
|
||||
| [Ollama](https://ollama.com/) | ✅ | 本地大模型運行平台 |
|
||||
| [LMStudio](https://lmstudio.ai/) | ✅ | 本地大模型運行平台 |
|
||||
| [GiteeAI](https://ai.gitee.com/) | ✅ | 大模型介面聚合平台 |
|
||||
| [SiliconFlow](https://siliconflow.cn/) | ✅ | 大模型聚合平台 |
|
||||
| [阿里雲百煉](https://bailian.console.aliyun.com/) | ✅ | 大模型聚合平台, LLMOps 平台 |
|
||||
| [火山方舟](https://console.volcengine.com/ark/region:ark+cn-beijing/model?vendor=Bytedance&view=LIST_VIEW) | ✅ | 大模型聚合平台, LLMOps 平台 |
|
||||
| [ModelScope](https://modelscope.cn/docs/model-service/API-Inference/intro) | ✅ | 大模型聚合平台 |
|
||||
| [MCP](https://modelcontextprotocol.io/) | ✅ | 支援通過 MCP 協議獲取工具 |
|
||||
## 支援的大模型與整合
|
||||
|
||||
### TTS
|
||||
| 提供商 | 類型 | 狀態 |
|
||||
|--------|------|------|
|
||||
| [OpenAI](https://platform.openai.com/) | LLM | ✅ |
|
||||
| [Anthropic](https://www.anthropic.com/) | LLM | ✅ |
|
||||
| [DeepSeek](https://www.deepseek.com/) | LLM | ✅ |
|
||||
| [Google Gemini](https://aistudio.google.com/prompts/new_chat) | LLM | ✅ |
|
||||
| [xAI](https://x.ai/) | LLM | ✅ |
|
||||
| [Moonshot](https://www.moonshot.cn/) | LLM | ✅ |
|
||||
| [智譜AI](https://open.bigmodel.cn/) | LLM | ✅ |
|
||||
| [Ollama](https://ollama.com/) | 本地 LLM | ✅ |
|
||||
| [LM Studio](https://lmstudio.ai/) | 本地 LLM | ✅ |
|
||||
| [Dify](https://dify.ai) | LLMOps | ✅ |
|
||||
| [MCP](https://modelcontextprotocol.io/) | 協議 | ✅ |
|
||||
| [SiliconFlow](https://siliconflow.cn/) | 聚合平台 | ✅ |
|
||||
| [阿里雲百煉](https://bailian.console.aliyun.com/) | 聚合平台 | ✅ |
|
||||
| [火山方舟](https://console.volcengine.com/ark/region:ark+cn-beijing/model?vendor=Bytedance&view=LIST_VIEW) | 聚合平台 | ✅ |
|
||||
| [ModelScope](https://modelscope.cn/docs/model-service/API-Inference/intro) | 聚合平台 | ✅ |
|
||||
| [GiteeAI](https://ai.gitee.com/) | 聚合平台 | ✅ |
|
||||
| [勝算雲](https://www.shengsuanyun.com/?from=CH_KYIPP758) | GPU 平台 | ✅ |
|
||||
| [優雲智算](https://www.compshare.cn/?ytag=GPU_YY-gh_langbot) | GPU 平台 | ✅ |
|
||||
| [PPIO](https://ppinfra.com/user/register?invited_by=QJKFYD&utm_source=github_langbot) | GPU 平台 | ✅ |
|
||||
| [接口 AI](https://jiekou.ai/) | 聚合平台 | ✅ |
|
||||
| [302.AI](https://share.302.ai/SuTG99) | 聚合平台 | ✅ |
|
||||
|
||||
### TTS(語音合成)
|
||||
|
||||
| 平台/模型 | 備註 |
|
||||
| --- | --- |
|
||||
|-----------|------|
|
||||
| [FishAudio](https://fish.audio/zh-CN/discovery/) | [外掛](https://github.com/the-lazy-me/NewChatVoice) |
|
||||
| [海豚 AI](https://www.ttson.cn/?source=thelazy) | [外掛](https://github.com/the-lazy-me/NewChatVoice) |
|
||||
| [AzureTTS](https://portal.azure.com/) | [外掛](https://github.com/Ingnaryk/LangBot_AzureTTS) |
|
||||
@@ -151,13 +132,54 @@ docker compose up -d
|
||||
### 文生圖
|
||||
|
||||
| 平台/模型 | 備註 |
|
||||
| --- | --- |
|
||||
| 阿里雲百煉 | [外掛](https://github.com/Thetail001/LangBot_BailianTextToImagePlugin)
|
||||
|-----------|------|
|
||||
| 阿里雲百煉 | [外掛](https://github.com/Thetail001/LangBot_BailianTextToImagePlugin) |
|
||||
|
||||
## 😘 社群貢獻
|
||||
[→ 查看完整整合列表](https://docs.langbot.app/zh/insight/features.html)
|
||||
|
||||
感謝以下[程式碼貢獻者](https://github.com/langbot-app/LangBot/graphs/contributors)和社群裡其他成員對 LangBot 的貢獻:
|
||||
---
|
||||
|
||||
## 為什麼選擇 LangBot?
|
||||
|
||||
| 使用場景 | LangBot 如何幫助 |
|
||||
|----------|------------------|
|
||||
| **客戶服務** | 將 AI Agent 部署到微信/企微/釘釘/飛書,基於知識庫自動回答使用者問題 |
|
||||
| **內部工具** | 將 n8n/Dify 工作流接入企微/釘釘,實現業務流程自動化 |
|
||||
| **社群運營** | 在 QQ/Discord 群中使用 AI 驅動的內容審核與智能互動 |
|
||||
| **多平台觸達** | 一個機器人,覆蓋所有平台。透過統一面板集中管理 |
|
||||
|
||||
---
|
||||
|
||||
## 線上演示
|
||||
|
||||
**立即體驗:** https://demo.langbot.dev/
|
||||
- 信箱:`demo@langbot.app`
|
||||
- 密碼:`langbot123456`
|
||||
|
||||
*注意:公開演示環境,請不要在其中填入任何敏感資訊。*
|
||||
|
||||
---
|
||||
|
||||
## 社群
|
||||
|
||||
[](https://discord.gg/wdNEHETs87)
|
||||
[](https://qm.qq.com/q/JLi38whHum)
|
||||
|
||||
- [Discord 社群](https://discord.gg/wdNEHETs87)
|
||||
- [QQ 社群群](https://qm.qq.com/q/JLi38whHum)
|
||||
|
||||
---
|
||||
|
||||
## Star 趨勢
|
||||
|
||||
[](https://star-history.com/#langbot-app/LangBot&Date)
|
||||
|
||||
---
|
||||
|
||||
## 貢獻者
|
||||
|
||||
感謝所有[貢獻者](https://github.com/langbot-app/LangBot/graphs/contributors)對 LangBot 的幫助:
|
||||
|
||||
<a href="https://github.com/langbot-app/LangBot/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=langbot-app/LangBot" />
|
||||
</a>
|
||||
</a>
|
||||
|
||||
194
README_VI.md
194
README_VI.md
@@ -1,25 +1,27 @@
|
||||
<p align="center">
|
||||
<a href="https://langbot.app">
|
||||
<img width="130" src="https://docs.langbot.app/langbot-logo.png" alt="LangBot"/>
|
||||
<img width="130" src="res/logo-blue.png" alt="LangBot"/>
|
||||
</a>
|
||||
|
||||
<div align="center">
|
||||
|
||||
<a href="https://www.producthunt.com/products/langbot?utm_source=badge-follow&utm_medium=badge&utm_source=badge-langbot" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/follow.svg?product_id=1077185&theme=light" alt="LangBot - Production-grade IM bot made easy. | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||
|
||||
<h3>Xây dựng, gỡ lỗi và triển khai bot IM nhanh chóng với LangBot.</h3>
|
||||
<h3>Nền tảng cấp sản xuất để xây dựng bot IM với AI agent.</h3>
|
||||
<h4>Xây dựng, gỡ lỗi và triển khai bot AI nhanh chóng trên Slack, Discord, Telegram, WeChat và nhiều nền tảng khác.</h4>
|
||||
|
||||
[English](README_EN.md) / [简体中文](README.md) / [繁體中文](README_TW.md) / [日本語](README_JP.md) / [Español](README_ES.md) / [Français](README_FR.md) / [한국어](README_KO.md) / [Русский](README_RU.md) / Tiếng Việt
|
||||
[English](README.md) / [简体中文](README_CN.md) / [繁體中文](README_TW.md) / [日本語](README_JP.md) / [Español](README_ES.md) / [Français](README_FR.md) / [한국어](README_KO.md) / [Русский](README_RU.md) / Tiếng Việt
|
||||
|
||||
[](https://discord.gg/wdNEHETs87)
|
||||
[](https://deepwiki.com/langbot-app/LangBot)
|
||||
[](https://github.com/langbot-app/LangBot/releases/latest)
|
||||
<img src="https://img.shields.io/badge/python-3.10 ~ 3.13 -blue.svg" alt="python">
|
||||
[](https://github.com/langbot-app/LangBot/stargazers)
|
||||
|
||||
<a href="https://langbot.app">Trang chủ</a> |
|
||||
<a href="https://docs.langbot.app/en/insight/features.html">Tính năng</a> |
|
||||
<a href="https://docs.langbot.app/en/insight/guide.html">Triển khai</a> |
|
||||
<a href="https://docs.langbot.app/en/tags/readme.html">Tích hợp API</a> |
|
||||
<a href="https://docs.langbot.app/en/insight/guide.html">Tài liệu</a> |
|
||||
<a href="https://docs.langbot.app/en/tags/readme.html">API</a> |
|
||||
<a href="https://space.langbot.app">Chợ Plugin</a> |
|
||||
<a href="https://langbot.featurebase.app/roadmap">Lộ trình</a>
|
||||
|
||||
@@ -27,19 +29,36 @@
|
||||
|
||||
</p>
|
||||
|
||||
## 📦 Bắt đầu
|
||||
---
|
||||
|
||||
#### Khởi động Nhanh
|
||||
## LangBot là gì?
|
||||
|
||||
Sử dụng `uvx` để khởi động bằng một lệnh (cần cài đặt [uv](https://docs.astral.sh/uv/getting-started/installation/)):
|
||||
LangBot là một **nền tảng mã nguồn mở, cấp sản xuất** để xây dựng bot nhắn tin tức thời được hỗ trợ bởi AI. Nó kết nối các Mô hình Ngôn ngữ Lớn (LLM) với bất kỳ nền tảng chat nào, cho phép bạn tạo các agent thông minh có thể trò chuyện, thực hiện tác vụ và tích hợp với quy trình làm việc hiện có của bạn.
|
||||
|
||||
### Khả năng chính
|
||||
|
||||
- **Hội thoại AI & Agent** — Đối thoại nhiều lượt, gọi công cụ, hỗ trợ đa phương thức, đầu ra streaming. RAG (cơ sở kiến thức) tích hợp sẵn với tích hợp sâu vào [Dify](https://dify.ai), [Coze](https://coze.com), [n8n](https://n8n.io), [Langflow](https://langflow.org).
|
||||
- **Hỗ trợ đa nền tảng IM** — Một mã nguồn cho Discord, Telegram, Slack, LINE, QQ, WeChat, WeCom, Lark, DingTalk, KOOK.
|
||||
- **Sẵn sàng cho sản xuất** — Kiểm soát truy cập, giới hạn tốc độ, lọc từ nhạy cảm, giám sát toàn diện và xử lý ngoại lệ. Được doanh nghiệp tin dùng.
|
||||
- **Hệ sinh thái Plugin** — Hàng trăm plugin, kiến trúc hướng sự kiện, mở rộng thành phần, và hỗ trợ [giao thức MCP](https://modelcontextprotocol.io/).
|
||||
- **Bảng quản lý Web** — Cấu hình, quản lý và giám sát bot thông qua giao diện trình duyệt trực quan. Không cần chỉnh sửa YAML.
|
||||
- **Kiến trúc đa Pipeline** — Các bot khác nhau cho các kịch bản khác nhau, với giám sát toàn diện và xử lý ngoại lệ.
|
||||
|
||||
[→ Tìm hiểu thêm về tất cả tính năng](https://docs.langbot.app/en/insight/features.html)
|
||||
|
||||
---
|
||||
|
||||
## Bắt đầu nhanh
|
||||
|
||||
### Khởi chạy một dòng
|
||||
|
||||
```bash
|
||||
uvx langbot
|
||||
```
|
||||
|
||||
Truy cập http://localhost:5300 để bắt đầu sử dụng.
|
||||
> Yêu cầu [uv](https://docs.astral.sh/uv/getting-started/installation/). Truy cập http://localhost:5300 — xong.
|
||||
|
||||
#### Triển khai Docker Compose
|
||||
### Docker Compose
|
||||
|
||||
```bash
|
||||
git clone https://github.com/langbot-app/LangBot
|
||||
@@ -47,102 +66,101 @@ cd LangBot/docker
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
Truy cập http://localhost:5300 để bắt đầu sử dụng.
|
||||
|
||||
Tài liệu chi tiết [Triển khai Docker](https://docs.langbot.app/en/deploy/langbot/docker.html).
|
||||
|
||||
#### Triển khai Một cú nhấp chuột trên BTPanel
|
||||
|
||||
LangBot đã được liệt kê trên BTPanel. Nếu bạn đã cài đặt BTPanel, bạn có thể sử dụng [tài liệu](https://docs.langbot.app/en/deploy/langbot/one-click/bt.html) để sử dụng nó.
|
||||
|
||||
#### Triển khai Cloud Zeabur
|
||||
|
||||
Mẫu Zeabur được đóng góp bởi cộng đồng.
|
||||
### Triển khai đám mây một cú nhấp
|
||||
|
||||
[](https://zeabur.com/en-US/templates/ZKTBDH)
|
||||
|
||||
#### Triển khai Cloud Railway
|
||||
|
||||
[](https://railway.app/template/yRrAyL?referralCode=vogKPF)
|
||||
|
||||
#### Các Phương pháp Triển khai Khác
|
||||
**Thêm tùy chọn:** [Docker](https://docs.langbot.app/en/deploy/langbot/docker.html) · [Thủ công](https://docs.langbot.app/en/deploy/langbot/manual.html) · [BTPanel](https://docs.langbot.app/en/deploy/langbot/one-click/bt.html) · [Kubernetes](./docker/README_K8S.md)
|
||||
|
||||
Sử dụng trực tiếp phiên bản phát hành để chạy, xem tài liệu [Triển khai Thủ công](https://docs.langbot.app/en/deploy/langbot/manual.html).
|
||||
---
|
||||
|
||||
#### Triển khai Kubernetes
|
||||
|
||||
Tham khảo tài liệu [Triển khai Kubernetes](./docker/README_K8S.md).
|
||||
|
||||
## 😎 Cập nhật Mới nhất
|
||||
|
||||
Nhấp vào các nút Star và Watch ở góc trên bên phải của kho lưu trữ để nhận các bản cập nhật mới nhất.
|
||||
|
||||

|
||||
|
||||
## ✨ Tính năng
|
||||
|
||||
<img width="500" src="https://docs.langbot.app/ui/bot-page-en-rounded.png" />
|
||||
|
||||
|
||||
- 💬 Chat với LLM / Agent: Hỗ trợ nhiều LLM, thích ứng với chat nhóm và chat riêng tư; Hỗ trợ các cuộc trò chuyện nhiều vòng, gọi công cụ, khả năng đa phương thức và đầu ra streaming. Triển khai RAG (cơ sở kiến thức) tích hợp sẵn và tích hợp sâu với [Dify](https://dify.ai), [Coze](https://coze.com), [n8n](https://n8n.io) v.v. LLMOps platforms.
|
||||
- 🤖 Hỗ trợ Đa nền tảng: Hiện hỗ trợ QQ, QQ Channel, WeCom, WeChat cá nhân, Lark, DingTalk, Discord, Telegram, KOOK, Slack, LINE, v.v.
|
||||
- 🛠️ Độ ổn định Cao, Tính năng Phong phú: Kiểm soát truy cập gốc, giới hạn tốc độ, lọc từ nhạy cảm, v.v.; Dễ sử dụng, hỗ trợ nhiều phương pháp triển khai. Hỗ trợ nhiều cấu hình pipeline, các bot khác nhau cho các kịch bản khác nhau.
|
||||
- 🧩 Mở rộng Plugin, Cộng đồng Hoạt động: Hỗ trợ các cơ chế plugin hướng sự kiện, mở rộng thành phần, v.v.; Tích hợp giao thức [MCP](https://modelcontextprotocol.io/) của Anthropic; Hiện có hàng trăng plugin.
|
||||
- 😻 Giao diện Web: Hỗ trợ quản lý các phiên bản LangBot thông qua trình duyệt. Không cần viết tệp cấu hình thủ công.
|
||||
|
||||
Để biết thêm thông số kỹ thuật chi tiết, vui lòng tham khảo [tài liệu](https://docs.langbot.app/en/insight/features.html).
|
||||
|
||||
Hoặc truy cập môi trường demo: https://demo.langbot.dev/
|
||||
- Thông tin đăng nhập: Email: `demo@langbot.app` Mật khẩu: `langbot123456`
|
||||
- Lưu ý: Chỉ dành cho demo WebUI, vui lòng không nhập bất kỳ thông tin nhạy cảm nào trong môi trường công cộng.
|
||||
|
||||
### Nền tảng Nhắn tin
|
||||
## Nền tảng được hỗ trợ
|
||||
|
||||
| Nền tảng | Trạng thái | Ghi chú |
|
||||
| --- | --- | --- |
|
||||
|----------|--------|-------|
|
||||
| Discord | ✅ | |
|
||||
| Telegram | ✅ | |
|
||||
| Slack | ✅ | |
|
||||
| LINE | ✅ | |
|
||||
| QQ Cá nhân | ✅ | |
|
||||
| QQ API Chính thức | ✅ | |
|
||||
| WeCom | ✅ | |
|
||||
| WeComCS | ✅ | |
|
||||
| WeCom AI Bot | ✅ | |
|
||||
| WeChat Cá nhân | ✅ | |
|
||||
| KOOK | ✅ | |
|
||||
| QQ | ✅ | Cá nhân & API chính thức |
|
||||
| WeCom | ✅ | WeChat doanh nghiệp, CS bên ngoài, AI Bot |
|
||||
| WeChat | ✅ | Cá nhân & Tài khoản công khai |
|
||||
| Lark | ✅ | |
|
||||
| DingTalk | ✅ | |
|
||||
| KOOK | ✅ | |
|
||||
| Satori | ✅ | |
|
||||
|
||||
### LLMs
|
||||
---
|
||||
|
||||
| LLM | Trạng thái | Ghi chú |
|
||||
| --- | --- | --- |
|
||||
| [OpenAI](https://platform.openai.com/) | ✅ | Có sẵn cho bất kỳ mô hình định dạng giao diện OpenAI nào |
|
||||
| [DeepSeek](https://www.deepseek.com/) | ✅ | |
|
||||
| [Moonshot](https://www.moonshot.cn/) | ✅ | |
|
||||
| [Anthropic](https://www.anthropic.com/) | ✅ | |
|
||||
| [xAI](https://x.ai/) | ✅ | |
|
||||
| [Zhipu AI](https://open.bigmodel.cn/) | ✅ | |
|
||||
| [CompShare](https://www.compshare.cn/?ytag=GPU_YY-gh_langbot) | ✅ | Nền tảng tài nguyên LLM và GPU |
|
||||
| [PPIO](https://ppinfra.com/user/register?invited_by=QJKFYD&utm_source=github_langbot) | ✅ | Nền tảng tài nguyên LLM và GPU |
|
||||
| [接口 AI](https://jiekou.ai/) | ✅ | Nền tảng tổng hợp LLM |
|
||||
| [ShengSuanYun](https://www.shengsuanyun.com/?from=CH_KYIPP758) | ✅ | Nền tảng tài nguyên LLM và GPU |
|
||||
| [302.AI](https://share.302.ai/SuTG99) | ✅ | Cổng LLM (MaaS) |
|
||||
| [Google Gemini](https://aistudio.google.com/prompts/new_chat) | ✅ | |
|
||||
| [Dify](https://dify.ai) | ✅ | Nền tảng LLMOps |
|
||||
| [Ollama](https://ollama.com/) | ✅ | Nền tảng chạy LLM cục bộ |
|
||||
| [LMStudio](https://lmstudio.ai/) | ✅ | Nền tảng chạy LLM cục bộ |
|
||||
| [GiteeAI](https://ai.gitee.com/) | ✅ | Cổng giao diện LLM (MaaS) |
|
||||
| [SiliconFlow](https://siliconflow.cn/) | ✅ | Cổng LLM (MaaS) |
|
||||
| [Aliyun Bailian](https://bailian.console.aliyun.com/) | ✅ | Cổng LLM (MaaS), nền tảng LLMOps |
|
||||
| [Volc Engine Ark](https://console.volcengine.com/ark/region:ark+cn-beijing/model?vendor=Bytedance&view=LIST_VIEW) | ✅ | Cổng LLM (MaaS), nền tảng LLMOps |
|
||||
| [ModelScope](https://modelscope.cn/docs/model-service/API-Inference/intro) | ✅ | Cổng LLM (MaaS) |
|
||||
| [MCP](https://modelcontextprotocol.io/) | ✅ | Hỗ trợ truy cập công cụ qua giao thức MCP |
|
||||
## LLM và tích hợp được hỗ trợ
|
||||
|
||||
## 🤝 Đóng góp Cộng đồng
|
||||
| Nhà cung cấp | Loại | Trạng thái |
|
||||
|----------|------|--------|
|
||||
| [OpenAI](https://platform.openai.com/) | LLM | ✅ |
|
||||
| [Anthropic](https://www.anthropic.com/) | LLM | ✅ |
|
||||
| [DeepSeek](https://www.deepseek.com/) | LLM | ✅ |
|
||||
| [Google Gemini](https://aistudio.google.com/prompts/new_chat) | LLM | ✅ |
|
||||
| [xAI](https://x.ai/) | LLM | ✅ |
|
||||
| [Moonshot](https://www.moonshot.cn/) | LLM | ✅ |
|
||||
| [Zhipu AI](https://open.bigmodel.cn/) | LLM | ✅ |
|
||||
| [Ollama](https://ollama.com/) | LLM cục bộ | ✅ |
|
||||
| [LM Studio](https://lmstudio.ai/) | LLM cục bộ | ✅ |
|
||||
| [Dify](https://dify.ai) | LLMOps | ✅ |
|
||||
| [MCP](https://modelcontextprotocol.io/) | Giao thức | ✅ |
|
||||
| [SiliconFlow](https://siliconflow.cn/) | Cổng | ✅ |
|
||||
| [Aliyun Bailian](https://bailian.console.aliyun.com/) | Cổng | ✅ |
|
||||
| [Volc Engine Ark](https://console.volcengine.com/ark/region:ark+cn-beijing/model?vendor=Bytedance&view=LIST_VIEW) | Cổng | ✅ |
|
||||
| [ModelScope](https://modelscope.cn/docs/model-service/API-Inference/intro) | Cổng | ✅ |
|
||||
| [GiteeAI](https://ai.gitee.com/) | Cổng | ✅ |
|
||||
| [CompShare](https://www.compshare.cn/?ytag=GPU_YY-gh_langbot) | Nền tảng GPU | ✅ |
|
||||
| [PPIO](https://ppinfra.com/user/register?invited_by=QJKFYD&utm_source=github_langbot) | Nền tảng GPU | ✅ |
|
||||
| [ShengSuanYun](https://www.shengsuanyun.com/?from=CH_KYIPP758) | Nền tảng GPU | ✅ |
|
||||
| [接口 AI](https://jiekou.ai/) | Cổng | ✅ |
|
||||
| [302.AI](https://share.302.ai/SuTG99) | Cổng | ✅ |
|
||||
|
||||
Cảm ơn các [người đóng góp mã](https://github.com/langbot-app/LangBot/graphs/contributors) sau đây và các thành viên khác trong cộng đồng vì những đóng góp của họ cho LangBot:
|
||||
[→ Xem tất cả tích hợp](https://docs.langbot.app/en/insight/features.html)
|
||||
|
||||
---
|
||||
|
||||
## Tại sao chọn LangBot?
|
||||
|
||||
| Trường hợp sử dụng | LangBot giúp như thế nào |
|
||||
|----------|-------------------|
|
||||
| **Hỗ trợ khách hàng** | Triển khai agent AI trên Slack/Discord/Telegram để trả lời câu hỏi bằng cơ sở kiến thức của bạn |
|
||||
| **Công cụ nội bộ** | Kết nối quy trình n8n/Dify với WeCom/DingTalk để tự động hóa quy trình kinh doanh |
|
||||
| **Quản lý cộng đồng** | Quản lý nhóm QQ/Discord với tính năng lọc nội dung và tương tác được hỗ trợ bởi AI |
|
||||
| **Đa nền tảng** | Một bot, tất cả nền tảng. Quản lý từ một bảng điều khiển duy nhất |
|
||||
|
||||
---
|
||||
|
||||
## Demo trực tuyến
|
||||
|
||||
**Thử ngay:** https://demo.langbot.dev/
|
||||
- Email: `demo@langbot.app`
|
||||
- Mật khẩu: `langbot123456`
|
||||
|
||||
*Lưu ý: Môi trường demo công khai. Không nhập thông tin nhạy cảm.*
|
||||
|
||||
---
|
||||
|
||||
## Cộng đồng
|
||||
|
||||
[](https://discord.gg/wdNEHETs87)
|
||||
|
||||
- [Cộng đồng Discord](https://discord.gg/wdNEHETs87)
|
||||
|
||||
---
|
||||
|
||||
## Lịch sử Star
|
||||
|
||||
[](https://star-history.com/#langbot-app/LangBot&Date)
|
||||
|
||||
---
|
||||
|
||||
## Người đóng góp
|
||||
|
||||
Cảm ơn tất cả [người đóng góp](https://github.com/langbot-app/LangBot/graphs/contributors) đã giúp LangBot trở nên tốt hơn:
|
||||
|
||||
<a href="https://github.com/langbot-app/LangBot/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=langbot-app/LangBot" />
|
||||
|
||||
@@ -14,7 +14,7 @@ services:
|
||||
restart: on-failure
|
||||
environment:
|
||||
- TZ=Asia/Shanghai
|
||||
command: ["uv", "run", "-m", "langbot_plugin.cli.__init__", "rt"]
|
||||
command: ["uv", "run", "--no-sync", "-m", "langbot_plugin.cli.__init__", "rt"]
|
||||
networks:
|
||||
- langbot_network
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"url": "https://langbot.app"
|
||||
},
|
||||
"license": {
|
||||
"name": "AGPL-3.0",
|
||||
"name": "Apache-2.0",
|
||||
"url": "https://github.com/langbot-app/LangBot/blob/master/LICENSE"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[project]
|
||||
name = "langbot"
|
||||
version = "4.7.0"
|
||||
description = "Production-grade platform for building IM bots"
|
||||
version = "4.8.4"
|
||||
description = "Production-grade platform for building agentic IM bots"
|
||||
readme = "README.md"
|
||||
license-files = ["LICENSE"]
|
||||
requires-python = ">=3.11,<4.0"
|
||||
@@ -17,13 +17,13 @@ dependencies = [
|
||||
"certifi>=2025.4.26",
|
||||
"colorlog~=6.6.0",
|
||||
"cryptography>=44.0.3",
|
||||
"dashscope>=1.23.2",
|
||||
"dashscope>=1.25.10",
|
||||
"dingtalk-stream>=0.24.0",
|
||||
"discord-py>=2.5.2",
|
||||
"pynacl>=1.5.0", # Required for Discord voice support
|
||||
"gewechat-client>=0.1.5",
|
||||
"lark-oapi>=1.4.15",
|
||||
"mcp>=1.8.1",
|
||||
"mcp>=1.25.0",
|
||||
"nakuru-project-idk>=0.0.2.1",
|
||||
"ollama>=0.4.8",
|
||||
"openai>1.0.0",
|
||||
@@ -63,8 +63,8 @@ dependencies = [
|
||||
"langchain-text-splitters>=0.0.1",
|
||||
"chromadb>=0.4.24",
|
||||
"qdrant-client (>=1.15.1,<2.0.0)",
|
||||
"pyseekdb>=0.1.0",
|
||||
"langbot-plugin==0.2.4",
|
||||
"pyseekdb==1.0.0b7",
|
||||
"langbot-plugin==0.2.6",
|
||||
"asyncpg>=0.30.0",
|
||||
"line-bot-sdk>=3.19.0",
|
||||
"tboxsdk>=0.0.10",
|
||||
|
||||
BIN
res/logo-blue.png
Normal file
BIN
res/logo-blue.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
@@ -1,3 +1,3 @@
|
||||
"""LangBot - Production-grade platform for building IM bots"""
|
||||
"""LangBot - Production-grade platform for building agentic IM bots"""
|
||||
|
||||
__version__ = '4.7.0'
|
||||
__version__ = '4.8.4'
|
||||
|
||||
@@ -347,10 +347,15 @@ class DingTalkClient:
|
||||
raise Exception(f'failed to send proactive massage to group: {traceback.format_exc()}')
|
||||
|
||||
async def create_and_card(
|
||||
self, temp_card_id: str, incoming_message: dingtalk_stream.ChatbotMessage, quote_origin: bool = False
|
||||
self,
|
||||
temp_card_id: str,
|
||||
incoming_message: dingtalk_stream.ChatbotMessage,
|
||||
quote_origin: bool = False,
|
||||
card_auto_layout: bool = False,
|
||||
):
|
||||
content_key = 'content'
|
||||
card_data = {content_key: ''}
|
||||
card_data = {}
|
||||
card_data['config'] = json.dumps({'autoLayout': card_auto_layout})
|
||||
card_data['content'] = ''
|
||||
|
||||
card_instance = dingtalk_stream.AICardReplier(self.client, incoming_message)
|
||||
# print(card_instance)
|
||||
|
||||
@@ -85,7 +85,6 @@ class QQOfficialClient:
|
||||
req: Quart Request 对象
|
||||
"""
|
||||
try:
|
||||
|
||||
body = await req.get_data()
|
||||
|
||||
print(f'[QQ Official] Received request, body length: {len(body)}')
|
||||
@@ -96,7 +95,6 @@ class QQOfficialClient:
|
||||
|
||||
payload = json.loads(body)
|
||||
|
||||
|
||||
if payload.get('op') == 13:
|
||||
validation_data = payload.get('d')
|
||||
if not validation_data:
|
||||
@@ -276,21 +274,21 @@ class QQOfficialClient:
|
||||
seed = bot_secret
|
||||
while len(seed) < target_size:
|
||||
seed *= 2
|
||||
return seed[:target_size].encode("utf-8")
|
||||
return seed[:target_size].encode('utf-8')
|
||||
|
||||
async def verify(self, validation_payload: dict):
|
||||
seed = await self.repeat_seed(self.secret)
|
||||
private_key = ed25519.Ed25519PrivateKey.from_private_bytes(seed)
|
||||
|
||||
event_ts = validation_payload.get("event_ts", "")
|
||||
plain_token = validation_payload.get("plain_token", "")
|
||||
event_ts = validation_payload.get('event_ts', '')
|
||||
plain_token = validation_payload.get('plain_token', '')
|
||||
msg = event_ts + plain_token
|
||||
|
||||
# sign
|
||||
signature = private_key.sign(msg.encode()).hex()
|
||||
|
||||
response = {
|
||||
"plain_token": plain_token,
|
||||
"signature": signature,
|
||||
'plain_token': plain_token,
|
||||
'signature': signature,
|
||||
}
|
||||
return response
|
||||
|
||||
@@ -36,7 +36,12 @@ class WecomBotEvent(dict):
|
||||
"""
|
||||
用户名称
|
||||
"""
|
||||
return self.get('username', '') or self.get('from', {}).get('alias', '') or self.get('from', {}).get('name', '') or self.userid
|
||||
return (
|
||||
self.get('username', '')
|
||||
or self.get('from', {}).get('alias', '')
|
||||
or self.get('from', {}).get('name', '')
|
||||
or self.userid
|
||||
)
|
||||
|
||||
@property
|
||||
def chatname(self) -> str:
|
||||
@@ -121,7 +126,7 @@ class WecomBotEvent(dict):
|
||||
消息id
|
||||
"""
|
||||
return self.get('msgid', '')
|
||||
|
||||
|
||||
@property
|
||||
def ai_bot_id(self) -> str:
|
||||
"""
|
||||
|
||||
486
src/langbot/pkg/api/http/controller/groups/monitoring.py
Normal file
486
src/langbot/pkg/api/http/controller/groups/monitoring.py
Normal file
@@ -0,0 +1,486 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
import quart
|
||||
|
||||
from .. import group
|
||||
|
||||
|
||||
def parse_iso_datetime(datetime_str: str | None) -> datetime.datetime | None:
|
||||
"""Parse ISO 8601 datetime string, handling 'Z' suffix for UTC timezone"""
|
||||
if not datetime_str:
|
||||
return None
|
||||
# Replace 'Z' with '+00:00' for Python 3.10 compatibility
|
||||
if datetime_str.endswith('Z'):
|
||||
datetime_str = datetime_str[:-1] + '+00:00'
|
||||
dt = datetime.datetime.fromisoformat(datetime_str)
|
||||
# Convert to UTC and remove timezone info to match database storage (which stores UTC as naive datetime)
|
||||
if dt.tzinfo is not None:
|
||||
# Convert to UTC and remove timezone info
|
||||
dt = dt.astimezone(datetime.timezone.utc).replace(tzinfo=None)
|
||||
return dt
|
||||
|
||||
|
||||
@group.group_class('monitoring', '/api/v1/monitoring')
|
||||
class MonitoringRouterGroup(group.RouterGroup):
|
||||
async def initialize(self) -> None:
|
||||
@self.route('/overview', methods=['GET'], auth_type=group.AuthType.USER_TOKEN)
|
||||
async def get_overview() -> str:
|
||||
"""Get overview metrics"""
|
||||
# Parse query parameters
|
||||
bot_ids = quart.request.args.getlist('botId')
|
||||
pipeline_ids = quart.request.args.getlist('pipelineId')
|
||||
start_time_str = quart.request.args.get('startTime')
|
||||
end_time_str = quart.request.args.get('endTime')
|
||||
|
||||
# Parse datetime
|
||||
start_time = parse_iso_datetime(start_time_str)
|
||||
end_time = parse_iso_datetime(end_time_str)
|
||||
|
||||
metrics = await self.ap.monitoring_service.get_overview_metrics(
|
||||
bot_ids=bot_ids if bot_ids else None,
|
||||
pipeline_ids=pipeline_ids if pipeline_ids else None,
|
||||
start_time=start_time,
|
||||
end_time=end_time,
|
||||
)
|
||||
|
||||
return self.success(data=metrics)
|
||||
|
||||
@self.route('/messages', methods=['GET'], auth_type=group.AuthType.USER_TOKEN)
|
||||
async def get_messages() -> str:
|
||||
"""Get message logs"""
|
||||
# Parse query parameters
|
||||
bot_ids = quart.request.args.getlist('botId')
|
||||
pipeline_ids = quart.request.args.getlist('pipelineId')
|
||||
start_time_str = quart.request.args.get('startTime')
|
||||
end_time_str = quart.request.args.get('endTime')
|
||||
limit = int(quart.request.args.get('limit', 100))
|
||||
offset = int(quart.request.args.get('offset', 0))
|
||||
|
||||
# Parse datetime
|
||||
start_time = parse_iso_datetime(start_time_str)
|
||||
end_time = parse_iso_datetime(end_time_str)
|
||||
|
||||
messages, total = await self.ap.monitoring_service.get_messages(
|
||||
bot_ids=bot_ids if bot_ids else None,
|
||||
pipeline_ids=pipeline_ids if pipeline_ids else None,
|
||||
start_time=start_time,
|
||||
end_time=end_time,
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
)
|
||||
|
||||
return self.success(
|
||||
data={
|
||||
'messages': messages,
|
||||
'total': total,
|
||||
'limit': limit,
|
||||
'offset': offset,
|
||||
}
|
||||
)
|
||||
|
||||
@self.route('/llm-calls', methods=['GET'], auth_type=group.AuthType.USER_TOKEN)
|
||||
async def get_llm_calls() -> str:
|
||||
"""Get LLM call records"""
|
||||
# Parse query parameters
|
||||
bot_ids = quart.request.args.getlist('botId')
|
||||
pipeline_ids = quart.request.args.getlist('pipelineId')
|
||||
start_time_str = quart.request.args.get('startTime')
|
||||
end_time_str = quart.request.args.get('endTime')
|
||||
limit = int(quart.request.args.get('limit', 100))
|
||||
offset = int(quart.request.args.get('offset', 0))
|
||||
|
||||
# Parse datetime
|
||||
start_time = parse_iso_datetime(start_time_str)
|
||||
end_time = parse_iso_datetime(end_time_str)
|
||||
|
||||
llm_calls, total = await self.ap.monitoring_service.get_llm_calls(
|
||||
bot_ids=bot_ids if bot_ids else None,
|
||||
pipeline_ids=pipeline_ids if pipeline_ids else None,
|
||||
start_time=start_time,
|
||||
end_time=end_time,
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
)
|
||||
|
||||
return self.success(
|
||||
data={
|
||||
'llm_calls': llm_calls,
|
||||
'total': total,
|
||||
'limit': limit,
|
||||
'offset': offset,
|
||||
}
|
||||
)
|
||||
|
||||
@self.route('/embedding-calls', methods=['GET'], auth_type=group.AuthType.USER_TOKEN)
|
||||
async def get_embedding_calls() -> str:
|
||||
"""Get embedding call records"""
|
||||
# Parse query parameters
|
||||
start_time_str = quart.request.args.get('startTime')
|
||||
end_time_str = quart.request.args.get('endTime')
|
||||
knowledge_base_id = quart.request.args.get('knowledgeBaseId')
|
||||
limit = int(quart.request.args.get('limit', 100))
|
||||
offset = int(quart.request.args.get('offset', 0))
|
||||
|
||||
# Parse datetime
|
||||
start_time = parse_iso_datetime(start_time_str)
|
||||
end_time = parse_iso_datetime(end_time_str)
|
||||
|
||||
embedding_calls, total = await self.ap.monitoring_service.get_embedding_calls(
|
||||
start_time=start_time,
|
||||
end_time=end_time,
|
||||
knowledge_base_id=knowledge_base_id if knowledge_base_id else None,
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
)
|
||||
|
||||
return self.success(
|
||||
data={
|
||||
'embedding_calls': embedding_calls,
|
||||
'total': total,
|
||||
'limit': limit,
|
||||
'offset': offset,
|
||||
}
|
||||
)
|
||||
|
||||
@self.route('/sessions', methods=['GET'], auth_type=group.AuthType.USER_TOKEN)
|
||||
async def get_sessions() -> str:
|
||||
"""Get session information"""
|
||||
# Parse query parameters
|
||||
bot_ids = quart.request.args.getlist('botId')
|
||||
pipeline_ids = quart.request.args.getlist('pipelineId')
|
||||
start_time_str = quart.request.args.get('startTime')
|
||||
end_time_str = quart.request.args.get('endTime')
|
||||
is_active_str = quart.request.args.get('isActive')
|
||||
limit = int(quart.request.args.get('limit', 100))
|
||||
offset = int(quart.request.args.get('offset', 0))
|
||||
|
||||
# Parse datetime
|
||||
start_time = parse_iso_datetime(start_time_str)
|
||||
end_time = parse_iso_datetime(end_time_str)
|
||||
|
||||
# Parse is_active
|
||||
is_active = None
|
||||
if is_active_str:
|
||||
is_active = is_active_str.lower() == 'true'
|
||||
|
||||
sessions, total = await self.ap.monitoring_service.get_sessions(
|
||||
bot_ids=bot_ids if bot_ids else None,
|
||||
pipeline_ids=pipeline_ids if pipeline_ids else None,
|
||||
start_time=start_time,
|
||||
end_time=end_time,
|
||||
is_active=is_active,
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
)
|
||||
|
||||
return self.success(
|
||||
data={
|
||||
'sessions': sessions,
|
||||
'total': total,
|
||||
'limit': limit,
|
||||
'offset': offset,
|
||||
}
|
||||
)
|
||||
|
||||
@self.route('/errors', methods=['GET'], auth_type=group.AuthType.USER_TOKEN)
|
||||
async def get_errors() -> str:
|
||||
"""Get error logs"""
|
||||
# Parse query parameters
|
||||
bot_ids = quart.request.args.getlist('botId')
|
||||
pipeline_ids = quart.request.args.getlist('pipelineId')
|
||||
start_time_str = quart.request.args.get('startTime')
|
||||
end_time_str = quart.request.args.get('endTime')
|
||||
limit = int(quart.request.args.get('limit', 100))
|
||||
offset = int(quart.request.args.get('offset', 0))
|
||||
|
||||
# Parse datetime
|
||||
start_time = parse_iso_datetime(start_time_str)
|
||||
end_time = parse_iso_datetime(end_time_str)
|
||||
|
||||
errors, total = await self.ap.monitoring_service.get_errors(
|
||||
bot_ids=bot_ids if bot_ids else None,
|
||||
pipeline_ids=pipeline_ids if pipeline_ids else None,
|
||||
start_time=start_time,
|
||||
end_time=end_time,
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
)
|
||||
|
||||
return self.success(
|
||||
data={
|
||||
'errors': errors,
|
||||
'total': total,
|
||||
'limit': limit,
|
||||
'offset': offset,
|
||||
}
|
||||
)
|
||||
|
||||
@self.route('/data', methods=['GET'], auth_type=group.AuthType.USER_TOKEN)
|
||||
async def get_all_data() -> str:
|
||||
"""Get all monitoring data in a single request"""
|
||||
# Parse query parameters
|
||||
bot_ids = quart.request.args.getlist('botId')
|
||||
pipeline_ids = quart.request.args.getlist('pipelineId')
|
||||
start_time_str = quart.request.args.get('startTime')
|
||||
end_time_str = quart.request.args.get('endTime')
|
||||
limit = int(quart.request.args.get('limit', 50))
|
||||
|
||||
# Parse datetime
|
||||
start_time = parse_iso_datetime(start_time_str)
|
||||
end_time = parse_iso_datetime(end_time_str)
|
||||
|
||||
# Get overview metrics
|
||||
overview = await self.ap.monitoring_service.get_overview_metrics(
|
||||
bot_ids=bot_ids if bot_ids else None,
|
||||
pipeline_ids=pipeline_ids if pipeline_ids else None,
|
||||
start_time=start_time,
|
||||
end_time=end_time,
|
||||
)
|
||||
|
||||
# Get messages
|
||||
messages, messages_total = await self.ap.monitoring_service.get_messages(
|
||||
bot_ids=bot_ids if bot_ids else None,
|
||||
pipeline_ids=pipeline_ids if pipeline_ids else None,
|
||||
start_time=start_time,
|
||||
end_time=end_time,
|
||||
limit=limit,
|
||||
offset=0,
|
||||
)
|
||||
|
||||
# Get LLM calls
|
||||
llm_calls, llm_calls_total = await self.ap.monitoring_service.get_llm_calls(
|
||||
bot_ids=bot_ids if bot_ids else None,
|
||||
pipeline_ids=pipeline_ids if pipeline_ids else None,
|
||||
start_time=start_time,
|
||||
end_time=end_time,
|
||||
limit=limit,
|
||||
offset=0,
|
||||
)
|
||||
|
||||
# Get sessions
|
||||
sessions, sessions_total = await self.ap.monitoring_service.get_sessions(
|
||||
bot_ids=bot_ids if bot_ids else None,
|
||||
pipeline_ids=pipeline_ids if pipeline_ids else None,
|
||||
start_time=start_time,
|
||||
end_time=end_time,
|
||||
is_active=None,
|
||||
limit=limit,
|
||||
offset=0,
|
||||
)
|
||||
|
||||
# Get errors
|
||||
errors, errors_total = await self.ap.monitoring_service.get_errors(
|
||||
bot_ids=bot_ids if bot_ids else None,
|
||||
pipeline_ids=pipeline_ids if pipeline_ids else None,
|
||||
start_time=start_time,
|
||||
end_time=end_time,
|
||||
limit=limit,
|
||||
offset=0,
|
||||
)
|
||||
|
||||
# Get embedding calls
|
||||
embedding_calls, embedding_calls_total = await self.ap.monitoring_service.get_embedding_calls(
|
||||
start_time=start_time,
|
||||
end_time=end_time,
|
||||
limit=limit,
|
||||
offset=0,
|
||||
)
|
||||
|
||||
return self.success(
|
||||
data={
|
||||
'overview': overview,
|
||||
'messages': messages,
|
||||
'llmCalls': llm_calls,
|
||||
'embeddingCalls': embedding_calls,
|
||||
'sessions': sessions,
|
||||
'errors': errors,
|
||||
'totalCount': {
|
||||
'messages': messages_total,
|
||||
'llmCalls': llm_calls_total,
|
||||
'embeddingCalls': embedding_calls_total,
|
||||
'sessions': sessions_total,
|
||||
'errors': errors_total,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
@self.route('/sessions/<session_id>/analysis', methods=['GET'], auth_type=group.AuthType.USER_TOKEN)
|
||||
async def get_session_analysis(session_id: str) -> str:
|
||||
"""Get detailed analysis for a specific session"""
|
||||
analysis = await self.ap.monitoring_service.get_session_analysis(session_id)
|
||||
|
||||
# Always return success with the analysis data
|
||||
# The frontend will handle the 'found: false' case
|
||||
return self.success(data=analysis)
|
||||
|
||||
@self.route('/messages/<message_id>/details', methods=['GET'], auth_type=group.AuthType.USER_TOKEN)
|
||||
async def get_message_details(message_id: str) -> str:
|
||||
"""Get detailed information for a specific message"""
|
||||
details = await self.ap.monitoring_service.get_message_details(message_id)
|
||||
|
||||
if not details.get('found'):
|
||||
return self.error(message=f'Message {message_id} not found', code=404)
|
||||
|
||||
return self.success(data=details)
|
||||
|
||||
@self.route('/export', methods=['GET'], auth_type=group.AuthType.USER_TOKEN)
|
||||
async def export_data() -> tuple[str, int]:
|
||||
"""Export monitoring data as CSV"""
|
||||
# Parse query parameters
|
||||
export_type = quart.request.args.get('type', 'messages')
|
||||
bot_ids = quart.request.args.getlist('botId')
|
||||
pipeline_ids = quart.request.args.getlist('pipelineId')
|
||||
start_time_str = quart.request.args.get('startTime')
|
||||
end_time_str = quart.request.args.get('endTime')
|
||||
limit = int(quart.request.args.get('limit', 100000))
|
||||
|
||||
# Parse datetime
|
||||
start_time = parse_iso_datetime(start_time_str)
|
||||
end_time = parse_iso_datetime(end_time_str)
|
||||
|
||||
# Get data based on export type
|
||||
if export_type == 'messages':
|
||||
data = await self.ap.monitoring_service.export_messages(
|
||||
bot_ids=bot_ids if bot_ids else None,
|
||||
pipeline_ids=pipeline_ids if pipeline_ids else None,
|
||||
start_time=start_time,
|
||||
end_time=end_time,
|
||||
limit=limit,
|
||||
)
|
||||
headers = [
|
||||
'id',
|
||||
'timestamp',
|
||||
'bot_id',
|
||||
'bot_name',
|
||||
'pipeline_id',
|
||||
'pipeline_name',
|
||||
'runner_name',
|
||||
'message_content',
|
||||
'message_text',
|
||||
'session_id',
|
||||
'status',
|
||||
'level',
|
||||
'platform',
|
||||
'user_id',
|
||||
]
|
||||
elif export_type == 'llm-calls':
|
||||
data = await self.ap.monitoring_service.export_llm_calls(
|
||||
bot_ids=bot_ids if bot_ids else None,
|
||||
pipeline_ids=pipeline_ids if pipeline_ids else None,
|
||||
start_time=start_time,
|
||||
end_time=end_time,
|
||||
limit=limit,
|
||||
)
|
||||
headers = [
|
||||
'id',
|
||||
'timestamp',
|
||||
'model_name',
|
||||
'input_tokens',
|
||||
'output_tokens',
|
||||
'total_tokens',
|
||||
'duration_ms',
|
||||
'cost',
|
||||
'status',
|
||||
'bot_id',
|
||||
'bot_name',
|
||||
'pipeline_id',
|
||||
'pipeline_name',
|
||||
'session_id',
|
||||
'message_id',
|
||||
'error_message',
|
||||
]
|
||||
elif export_type == 'embedding-calls':
|
||||
data = await self.ap.monitoring_service.export_embedding_calls(
|
||||
start_time=start_time,
|
||||
end_time=end_time,
|
||||
limit=limit,
|
||||
)
|
||||
headers = [
|
||||
'id',
|
||||
'timestamp',
|
||||
'model_name',
|
||||
'prompt_tokens',
|
||||
'total_tokens',
|
||||
'duration_ms',
|
||||
'input_count',
|
||||
'status',
|
||||
'error_message',
|
||||
'knowledge_base_id',
|
||||
'query_text',
|
||||
'session_id',
|
||||
'message_id',
|
||||
'call_type',
|
||||
]
|
||||
elif export_type == 'errors':
|
||||
data = await self.ap.monitoring_service.export_errors(
|
||||
bot_ids=bot_ids if bot_ids else None,
|
||||
pipeline_ids=pipeline_ids if pipeline_ids else None,
|
||||
start_time=start_time,
|
||||
end_time=end_time,
|
||||
limit=limit,
|
||||
)
|
||||
headers = [
|
||||
'id',
|
||||
'timestamp',
|
||||
'error_type',
|
||||
'error_message',
|
||||
'bot_id',
|
||||
'bot_name',
|
||||
'pipeline_id',
|
||||
'pipeline_name',
|
||||
'session_id',
|
||||
'message_id',
|
||||
'stack_trace',
|
||||
]
|
||||
elif export_type == 'sessions':
|
||||
data = await self.ap.monitoring_service.export_sessions(
|
||||
bot_ids=bot_ids if bot_ids else None,
|
||||
pipeline_ids=pipeline_ids if pipeline_ids else None,
|
||||
start_time=start_time,
|
||||
end_time=end_time,
|
||||
limit=limit,
|
||||
)
|
||||
headers = [
|
||||
'session_id',
|
||||
'bot_id',
|
||||
'bot_name',
|
||||
'pipeline_id',
|
||||
'pipeline_name',
|
||||
'message_count',
|
||||
'start_time',
|
||||
'last_activity',
|
||||
'is_active',
|
||||
'platform',
|
||||
'user_id',
|
||||
]
|
||||
else:
|
||||
return self.error(message=f'Invalid export type: {export_type}', code=400)
|
||||
|
||||
# Generate CSV content with UTF-8 BOM for Excel compatibility
|
||||
import io
|
||||
|
||||
output = io.StringIO()
|
||||
# Write UTF-8 BOM for Excel
|
||||
output.write('\ufeff')
|
||||
# Write header
|
||||
output.write(','.join(headers) + '\n')
|
||||
|
||||
# Escape and write each row
|
||||
for row in data:
|
||||
escaped_values = []
|
||||
for header in headers:
|
||||
value = row.get(header, '')
|
||||
escaped_values.append(self.ap.monitoring_service._escape_csv_field(value))
|
||||
output.write(','.join(escaped_values) + '\n')
|
||||
|
||||
csv_content = output.getvalue()
|
||||
|
||||
# Return as file download
|
||||
response = await quart.make_response(csv_content)
|
||||
response.headers['Content-Type'] = 'text/csv; charset=utf-8'
|
||||
response.headers['Content-Disposition'] = (
|
||||
f'attachment; filename="monitoring-{export_type}-{int(datetime.datetime.now().timestamp())}.csv"'
|
||||
)
|
||||
|
||||
return response, 200
|
||||
@@ -14,6 +14,18 @@ from langbot_plugin.runtime.plugin.mgr import PluginInstallSource
|
||||
|
||||
@group.group_class('plugins', '/api/v1/plugins')
|
||||
class PluginsRouterGroup(group.RouterGroup):
|
||||
async def _check_extensions_limit(self) -> str | None:
|
||||
"""Check if extensions limit is reached. Returns error response if limit exceeded, None otherwise."""
|
||||
limitation = self.ap.instance_config.data.get('system', {}).get('limitation', {})
|
||||
max_extensions = limitation.get('max_extensions', -1)
|
||||
if max_extensions >= 0:
|
||||
plugins = await self.ap.plugin_connector.list_plugins()
|
||||
mcp_servers = await self.ap.mcp_service.get_mcp_servers()
|
||||
total_extensions = len(plugins) + len(mcp_servers)
|
||||
if total_extensions >= max_extensions:
|
||||
return self.http_status(400, -1, f'Maximum number of extensions ({max_extensions}) reached')
|
||||
return None
|
||||
|
||||
async def initialize(self) -> None:
|
||||
@self.route('', methods=['GET'], auth_type=group.AuthType.USER_TOKEN_OR_API_KEY)
|
||||
async def _() -> str:
|
||||
@@ -239,6 +251,10 @@ class PluginsRouterGroup(group.RouterGroup):
|
||||
@self.route('/install/github', methods=['POST'], auth_type=group.AuthType.USER_TOKEN_OR_API_KEY)
|
||||
async def _() -> str:
|
||||
"""Install plugin from GitHub release asset"""
|
||||
limit_error = await self._check_extensions_limit()
|
||||
if limit_error is not None:
|
||||
return limit_error
|
||||
|
||||
data = await quart.request.json
|
||||
asset_url = data.get('asset_url', '')
|
||||
owner = data.get('owner', '')
|
||||
@@ -273,6 +289,10 @@ class PluginsRouterGroup(group.RouterGroup):
|
||||
auth_type=group.AuthType.USER_TOKEN_OR_API_KEY,
|
||||
)
|
||||
async def _() -> str:
|
||||
limit_error = await self._check_extensions_limit()
|
||||
if limit_error is not None:
|
||||
return limit_error
|
||||
|
||||
data = await quart.request.json
|
||||
|
||||
ctx = taskmgr.TaskContext.new()
|
||||
@@ -288,6 +308,10 @@ class PluginsRouterGroup(group.RouterGroup):
|
||||
|
||||
@self.route('/install/local', methods=['POST'], auth_type=group.AuthType.USER_TOKEN_OR_API_KEY)
|
||||
async def _() -> str:
|
||||
limit_error = await self._check_extensions_limit()
|
||||
if limit_error is not None:
|
||||
return limit_error
|
||||
|
||||
file = (await quart.request.files).get('file')
|
||||
if file is None:
|
||||
return self.http_status(400, -1, 'file is required')
|
||||
|
||||
@@ -13,6 +13,7 @@ class SystemRouterGroup(group.RouterGroup):
|
||||
data={
|
||||
'version': constants.semantic_version,
|
||||
'debug': constants.debug_mode,
|
||||
'edition': constants.edition,
|
||||
'enable_marketplace': self.ap.instance_config.data.get('plugin', {}).get(
|
||||
'enable_marketplace', True
|
||||
),
|
||||
@@ -25,6 +26,7 @@ class SystemRouterGroup(group.RouterGroup):
|
||||
'disable_models_service': self.ap.instance_config.data.get('space', {}).get(
|
||||
'disable_models_service', False
|
||||
),
|
||||
'limitation': self.ap.instance_config.data.get('system', {}).get('limitation', {}),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -30,7 +30,6 @@ class WebhookRouterGroup(group.RouterGroup):
|
||||
适配器返回的响应
|
||||
"""
|
||||
try:
|
||||
|
||||
runtime_bot = await self.ap.platform_mgr.get_bot_by_uuid(bot_uuid)
|
||||
|
||||
if not runtime_bot:
|
||||
@@ -39,11 +38,9 @@ class WebhookRouterGroup(group.RouterGroup):
|
||||
if not runtime_bot.enable:
|
||||
return quart.jsonify({'error': 'Bot is disabled'}), 403
|
||||
|
||||
|
||||
if not hasattr(runtime_bot.adapter, 'handle_unified_webhook'):
|
||||
return quart.jsonify({'error': 'Adapter does not support unified webhook'}), 501
|
||||
|
||||
|
||||
response = await runtime_bot.adapter.handle_unified_webhook(
|
||||
bot_uuid=bot_uuid,
|
||||
path=path,
|
||||
|
||||
@@ -59,7 +59,16 @@ class BotService:
|
||||
adapter_runtime_values['bot_account_id'] = runtime_bot.adapter.bot_account_id
|
||||
|
||||
# Webhook URL for unified webhook adapters (independent of bot running state)
|
||||
if persistence_bot['adapter'] in ['wecom', 'wecombot', 'officialaccount', 'qqofficial', 'slack', 'wecomcs', 'LINE', 'lark']:
|
||||
if persistence_bot['adapter'] in [
|
||||
'wecom',
|
||||
'wecombot',
|
||||
'officialaccount',
|
||||
'qqofficial',
|
||||
'slack',
|
||||
'wecomcs',
|
||||
'LINE',
|
||||
'lark',
|
||||
]:
|
||||
webhook_prefix = self.ap.instance_config.data['api'].get('webhook_prefix', 'http://127.0.0.1:5300')
|
||||
webhook_url = f'/bots/{bot_uuid}'
|
||||
adapter_runtime_values['webhook_url'] = webhook_url
|
||||
@@ -74,6 +83,14 @@ class BotService:
|
||||
|
||||
async def create_bot(self, bot_data: dict) -> str:
|
||||
"""Create bot"""
|
||||
# Check limitation
|
||||
limitation = self.ap.instance_config.data.get('system', {}).get('limitation', {})
|
||||
max_bots = limitation.get('max_bots', -1)
|
||||
if max_bots >= 0:
|
||||
existing_bots = await self.get_bots()
|
||||
if len(existing_bots) >= max_bots:
|
||||
raise ValueError(f'Maximum number of bots ({max_bots}) reached')
|
||||
|
||||
# TODO: 检查配置信息格式
|
||||
bot_data['uuid'] = str(uuid.uuid4())
|
||||
|
||||
|
||||
@@ -38,6 +38,16 @@ class MCPService:
|
||||
return serialized_servers
|
||||
|
||||
async def create_mcp_server(self, server_data: dict) -> str:
|
||||
# Check limitation (extensions = MCP servers + plugins)
|
||||
limitation = self.ap.instance_config.data.get('system', {}).get('limitation', {})
|
||||
max_extensions = limitation.get('max_extensions', -1)
|
||||
if max_extensions >= 0:
|
||||
existing_mcp_servers = await self.get_mcp_servers()
|
||||
plugins = await self.ap.plugin_connector.list_plugins()
|
||||
total_extensions = len(existing_mcp_servers) + len(plugins)
|
||||
if total_extensions >= max_extensions:
|
||||
raise ValueError(f'Maximum number of extensions ({max_extensions}) reached')
|
||||
|
||||
server_data['uuid'] = str(uuid.uuid4())
|
||||
await self.ap.persistence_mgr.execute_async(sqlalchemy.insert(persistence_mcp.MCPServer).values(server_data))
|
||||
|
||||
|
||||
@@ -64,7 +64,9 @@ class LLMModelsService:
|
||||
models = result.all()
|
||||
return [self.ap.persistence_mgr.serialize_model(persistence_model.LLMModel, m) for m in models]
|
||||
|
||||
async def create_llm_model(self, model_data: dict, preserve_uuid: bool = False) -> str:
|
||||
async def create_llm_model(
|
||||
self, model_data: dict, preserve_uuid: bool = False, auto_set_to_default_pipeline: bool = True
|
||||
) -> str:
|
||||
"""Create a new LLM model"""
|
||||
if not preserve_uuid:
|
||||
model_data['uuid'] = str(uuid.uuid4())
|
||||
@@ -95,18 +97,19 @@ class LLMModelsService:
|
||||
)
|
||||
self.ap.model_mgr.llm_models.append(runtime_llm_model)
|
||||
|
||||
# set the default pipeline model to this model
|
||||
result = await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.select(persistence_pipeline.LegacyPipeline).where(
|
||||
persistence_pipeline.LegacyPipeline.is_default == True
|
||||
if auto_set_to_default_pipeline:
|
||||
# set the default pipeline model to this model
|
||||
result = await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.select(persistence_pipeline.LegacyPipeline).where(
|
||||
persistence_pipeline.LegacyPipeline.is_default == True
|
||||
)
|
||||
)
|
||||
)
|
||||
pipeline = result.first()
|
||||
if pipeline is not None and pipeline.config['ai']['local-agent']['model'] == '':
|
||||
pipeline_config = pipeline.config
|
||||
pipeline_config['ai']['local-agent']['model'] = model_data['uuid']
|
||||
pipeline_data = {'config': pipeline_config}
|
||||
await self.ap.pipeline_service.update_pipeline(pipeline.uuid, pipeline_data)
|
||||
pipeline = result.first()
|
||||
if pipeline is not None and pipeline.config['ai']['local-agent']['model'] == '':
|
||||
pipeline_config = pipeline.config
|
||||
pipeline_config['ai']['local-agent']['model'] = model_data['uuid']
|
||||
pipeline_data = {'config': pipeline_config}
|
||||
await self.ap.pipeline_service.update_pipeline(pipeline.uuid, pipeline_data)
|
||||
|
||||
return model_data['uuid']
|
||||
|
||||
@@ -192,7 +195,7 @@ class LLMModelsService:
|
||||
runtime_llm_model = await self.ap.model_mgr.init_temporary_runtime_llm_model(model_data)
|
||||
|
||||
extra_args = model_data.get('extra_args', {})
|
||||
await runtime_llm_model.provider.requester.invoke_llm(
|
||||
await runtime_llm_model.provider.invoke_llm(
|
||||
query=None,
|
||||
model=runtime_llm_model,
|
||||
messages=[provider_message.Message(role='user', content='Hello, world! Please just reply a "Hello".')],
|
||||
@@ -354,7 +357,7 @@ class EmbeddingModelsService:
|
||||
else:
|
||||
runtime_embedding_model = await self.ap.model_mgr.init_temporary_runtime_embedding_model(model_data)
|
||||
|
||||
await runtime_embedding_model.provider.requester.invoke_embedding(
|
||||
await runtime_embedding_model.provider.invoke_embedding(
|
||||
model=runtime_embedding_model,
|
||||
input_text=['Hello, world!'],
|
||||
extra_args={},
|
||||
|
||||
1125
src/langbot/pkg/api/http/service/monitoring.py
Normal file
1125
src/langbot/pkg/api/http/service/monitoring.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -76,6 +76,14 @@ class PipelineService:
|
||||
async def create_pipeline(self, pipeline_data: dict, default: bool = False) -> str:
|
||||
from ....utils import paths as path_utils
|
||||
|
||||
# Check limitation
|
||||
limitation = self.ap.instance_config.data.get('system', {}).get('limitation', {})
|
||||
max_pipelines = limitation.get('max_pipelines', -1)
|
||||
if max_pipelines >= 0:
|
||||
existing_pipelines = await self.get_pipelines()
|
||||
if len(existing_pipelines) >= max_pipelines:
|
||||
raise ValueError(f'Maximum number of pipelines ({max_pipelines}) reached')
|
||||
|
||||
pipeline_data['uuid'] = str(uuid.uuid4())
|
||||
pipeline_data['for_version'] = self.ap.ver_mgr.get_current_version()
|
||||
pipeline_data['stages'] = default_stage_order.copy()
|
||||
@@ -153,6 +161,14 @@ class PipelineService:
|
||||
|
||||
async def copy_pipeline(self, pipeline_uuid: str) -> str:
|
||||
"""Copy a pipeline with all its configurations"""
|
||||
# Check limitation
|
||||
limitation = self.ap.instance_config.data.get('system', {}).get('limitation', {})
|
||||
max_pipelines = limitation.get('max_pipelines', -1)
|
||||
if max_pipelines >= 0:
|
||||
existing_pipelines = await self.get_pipelines()
|
||||
if len(existing_pipelines) >= max_pipelines:
|
||||
raise ValueError(f'Maximum number of pipelines ({max_pipelines}) reached')
|
||||
|
||||
# Get the original pipeline
|
||||
result = await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.select(persistence_pipeline.LegacyPipeline).where(
|
||||
|
||||
@@ -295,4 +295,7 @@ class UserService:
|
||||
)
|
||||
)
|
||||
|
||||
# Update Space model provider API keys
|
||||
await self.ap.provider_service.update_space_model_provider_api_keys(api_key)
|
||||
|
||||
return await self.get_user_by_email(space_email)
|
||||
|
||||
@@ -29,6 +29,7 @@ from ..api.http.service import mcp as mcp_service
|
||||
from ..api.http.service import apikey as apikey_service
|
||||
from ..api.http.service import webhook as webhook_service
|
||||
from ..api.http.service import external_kb as external_kb_service
|
||||
from ..api.http.service import monitoring as monitoring_service
|
||||
from ..discover import engine as discover_engine
|
||||
from ..storage import mgr as storagemgr
|
||||
from ..utils import logcache
|
||||
@@ -36,6 +37,7 @@ from . import taskmgr
|
||||
from . import entities as core_entities
|
||||
from ..rag.knowledge import kbmgr as rag_mgr
|
||||
from ..vector import mgr as vectordb_mgr
|
||||
from ..telemetry import telemetry as telemetry_module
|
||||
|
||||
|
||||
class Application:
|
||||
@@ -140,6 +142,10 @@ class Application:
|
||||
|
||||
webhook_service: webhook_service.WebhookService = None
|
||||
|
||||
telemetry: telemetry_module.TelemetryManager = None
|
||||
|
||||
monitoring_service: monitoring_service.MonitoringService = None
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from .. import migration
|
||||
|
||||
|
||||
@migration.migration_class('dingtalk_card_auto_layout', 41)
|
||||
class DingTalkCardAutoLayoutMigration(migration.Migration):
|
||||
"""迁移"""
|
||||
|
||||
async def need_migrate(self) -> bool:
|
||||
"""判断当前环境是否需要运行此迁移"""
|
||||
return True
|
||||
|
||||
async def run(self):
|
||||
"""执行迁移"""
|
||||
self.ap.platform_cfg.data['platform-adapters']['app']['dingtalk']['card_auto_layout'] = False
|
||||
await self.ap.platform_cfg.dump_config()
|
||||
@@ -26,11 +26,13 @@ from ...api.http.service import mcp as mcp_service
|
||||
from ...api.http.service import apikey as apikey_service
|
||||
from ...api.http.service import webhook as webhook_service
|
||||
from ...api.http.service import external_kb as external_kb_service
|
||||
from ...api.http.service import monitoring as monitoring_service
|
||||
from ...discover import engine as discover_engine
|
||||
from ...storage import mgr as storagemgr
|
||||
from ...utils import logcache
|
||||
from ...vector import mgr as vectordb_mgr
|
||||
from .. import taskmgr
|
||||
from ...telemetry import telemetry as telemetry_module
|
||||
|
||||
|
||||
@stage.stage_class('BuildAppStage')
|
||||
@@ -102,6 +104,11 @@ class BuildAppStage(stage.BootingStage):
|
||||
ap.persistence_mgr = persistence_mgr_inst
|
||||
await persistence_mgr_inst.initialize()
|
||||
|
||||
# Telemetry manager: attach to app so other components can call via self.ap.telemetry
|
||||
telemetry_inst = telemetry_module.TelemetryManager(ap)
|
||||
await telemetry_inst.initialize()
|
||||
ap.telemetry = telemetry_inst
|
||||
|
||||
cmd_mgr_inst = cmdmgr.CommandManager(ap)
|
||||
await cmd_mgr_inst.initialize()
|
||||
ap.cmd_mgr = cmd_mgr_inst
|
||||
@@ -143,6 +150,9 @@ class BuildAppStage(stage.BootingStage):
|
||||
await http_ctrl.initialize()
|
||||
ap.http_ctrl = http_ctrl
|
||||
|
||||
monitoring_service_inst = monitoring_service.MonitoringService(ap)
|
||||
ap.monitoring_service = monitoring_service_inst
|
||||
|
||||
async def runtime_disconnect_callback(connector: plugin_connector.PluginRuntimeConnector) -> None:
|
||||
await asyncio.sleep(3)
|
||||
await plugin_connector_inst.initialize()
|
||||
|
||||
@@ -156,8 +156,10 @@ class LoadConfigStage(stage.BootingStage):
|
||||
)
|
||||
|
||||
constants.instance_id = ap.instance_id.data['instance_id']
|
||||
constants.edition = ap.instance_config.data.get('system', {}).get('edition', 'community')
|
||||
|
||||
print(f'LangBot instance id: {constants.instance_id}')
|
||||
print(f'LangBot edition: {constants.edition}')
|
||||
|
||||
await ap.instance_id.dump_config()
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ class MCPServer(Base):
|
||||
uuid = sqlalchemy.Column(sqlalchemy.String(255), primary_key=True, unique=True)
|
||||
name = sqlalchemy.Column(sqlalchemy.String(255), nullable=False)
|
||||
enable = sqlalchemy.Column(sqlalchemy.Boolean, nullable=False, default=False)
|
||||
mode = sqlalchemy.Column(sqlalchemy.String(255), nullable=False) # stdio, sse
|
||||
mode = sqlalchemy.Column(sqlalchemy.String(255), nullable=False) # stdio, sse, http
|
||||
extra_args = sqlalchemy.Column(sqlalchemy.JSON, nullable=False, default={})
|
||||
created_at = sqlalchemy.Column(sqlalchemy.DateTime, nullable=False, server_default=sqlalchemy.func.now())
|
||||
updated_at = sqlalchemy.Column(
|
||||
|
||||
105
src/langbot/pkg/entity/persistence/monitoring.py
Normal file
105
src/langbot/pkg/entity/persistence/monitoring.py
Normal file
@@ -0,0 +1,105 @@
|
||||
import sqlalchemy
|
||||
|
||||
from .base import Base
|
||||
|
||||
|
||||
class MonitoringMessage(Base):
|
||||
"""Monitoring message records"""
|
||||
|
||||
__tablename__ = 'monitoring_messages'
|
||||
|
||||
id = sqlalchemy.Column(sqlalchemy.String(255), primary_key=True)
|
||||
timestamp = sqlalchemy.Column(sqlalchemy.DateTime, nullable=False, index=True)
|
||||
bot_id = sqlalchemy.Column(sqlalchemy.String(255), nullable=False, index=True)
|
||||
bot_name = sqlalchemy.Column(sqlalchemy.String(255), nullable=False)
|
||||
pipeline_id = sqlalchemy.Column(sqlalchemy.String(255), nullable=False, index=True)
|
||||
pipeline_name = sqlalchemy.Column(sqlalchemy.String(255), nullable=False)
|
||||
message_content = sqlalchemy.Column(sqlalchemy.Text, nullable=False)
|
||||
session_id = sqlalchemy.Column(sqlalchemy.String(255), nullable=False, index=True)
|
||||
status = sqlalchemy.Column(sqlalchemy.String(50), nullable=False) # success, error, pending
|
||||
level = sqlalchemy.Column(sqlalchemy.String(50), nullable=False) # info, warning, error, debug
|
||||
platform = sqlalchemy.Column(sqlalchemy.String(255), nullable=True)
|
||||
user_id = sqlalchemy.Column(sqlalchemy.String(255), nullable=True)
|
||||
runner_name = sqlalchemy.Column(sqlalchemy.String(255), nullable=True) # Runner name for this query
|
||||
variables = sqlalchemy.Column(sqlalchemy.Text, nullable=True) # Query variables as JSON string
|
||||
|
||||
|
||||
class MonitoringLLMCall(Base):
|
||||
"""LLM call records"""
|
||||
|
||||
__tablename__ = 'monitoring_llm_calls'
|
||||
|
||||
id = sqlalchemy.Column(sqlalchemy.String(255), primary_key=True)
|
||||
timestamp = sqlalchemy.Column(sqlalchemy.DateTime, nullable=False, index=True)
|
||||
model_name = sqlalchemy.Column(sqlalchemy.String(255), nullable=False)
|
||||
input_tokens = sqlalchemy.Column(sqlalchemy.Integer, nullable=False)
|
||||
output_tokens = sqlalchemy.Column(sqlalchemy.Integer, nullable=False)
|
||||
total_tokens = sqlalchemy.Column(sqlalchemy.Integer, nullable=False)
|
||||
duration = sqlalchemy.Column(sqlalchemy.Integer, nullable=False) # milliseconds
|
||||
cost = sqlalchemy.Column(sqlalchemy.Float, nullable=True)
|
||||
status = sqlalchemy.Column(sqlalchemy.String(50), nullable=False) # success, error
|
||||
bot_id = sqlalchemy.Column(sqlalchemy.String(255), nullable=False, index=True)
|
||||
bot_name = sqlalchemy.Column(sqlalchemy.String(255), nullable=False)
|
||||
pipeline_id = sqlalchemy.Column(sqlalchemy.String(255), nullable=False, index=True)
|
||||
pipeline_name = sqlalchemy.Column(sqlalchemy.String(255), nullable=False)
|
||||
session_id = sqlalchemy.Column(sqlalchemy.String(255), nullable=False)
|
||||
error_message = sqlalchemy.Column(sqlalchemy.Text, nullable=True)
|
||||
message_id = sqlalchemy.Column(sqlalchemy.String(255), nullable=True, index=True) # Associated message ID
|
||||
|
||||
|
||||
class MonitoringSession(Base):
|
||||
"""Session tracking records"""
|
||||
|
||||
__tablename__ = 'monitoring_sessions'
|
||||
|
||||
session_id = sqlalchemy.Column(sqlalchemy.String(255), primary_key=True)
|
||||
bot_id = sqlalchemy.Column(sqlalchemy.String(255), nullable=False, index=True)
|
||||
bot_name = sqlalchemy.Column(sqlalchemy.String(255), nullable=False)
|
||||
pipeline_id = sqlalchemy.Column(sqlalchemy.String(255), nullable=False, index=True)
|
||||
pipeline_name = sqlalchemy.Column(sqlalchemy.String(255), nullable=False)
|
||||
message_count = sqlalchemy.Column(sqlalchemy.Integer, nullable=False, default=0)
|
||||
start_time = sqlalchemy.Column(sqlalchemy.DateTime, nullable=False, index=True)
|
||||
last_activity = sqlalchemy.Column(sqlalchemy.DateTime, nullable=False, index=True)
|
||||
is_active = sqlalchemy.Column(sqlalchemy.Boolean, nullable=False, default=True, index=True)
|
||||
platform = sqlalchemy.Column(sqlalchemy.String(255), nullable=True)
|
||||
user_id = sqlalchemy.Column(sqlalchemy.String(255), nullable=True)
|
||||
|
||||
|
||||
class MonitoringError(Base):
|
||||
"""Error log records"""
|
||||
|
||||
__tablename__ = 'monitoring_errors'
|
||||
|
||||
id = sqlalchemy.Column(sqlalchemy.String(255), primary_key=True)
|
||||
timestamp = sqlalchemy.Column(sqlalchemy.DateTime, nullable=False, index=True)
|
||||
error_type = sqlalchemy.Column(sqlalchemy.String(255), nullable=False)
|
||||
error_message = sqlalchemy.Column(sqlalchemy.Text, nullable=False)
|
||||
bot_id = sqlalchemy.Column(sqlalchemy.String(255), nullable=False, index=True)
|
||||
bot_name = sqlalchemy.Column(sqlalchemy.String(255), nullable=False)
|
||||
pipeline_id = sqlalchemy.Column(sqlalchemy.String(255), nullable=False, index=True)
|
||||
pipeline_name = sqlalchemy.Column(sqlalchemy.String(255), nullable=False)
|
||||
session_id = sqlalchemy.Column(sqlalchemy.String(255), nullable=True)
|
||||
stack_trace = sqlalchemy.Column(sqlalchemy.Text, nullable=True)
|
||||
message_id = sqlalchemy.Column(sqlalchemy.String(255), nullable=True, index=True) # Associated message ID
|
||||
|
||||
|
||||
class MonitoringEmbeddingCall(Base):
|
||||
"""Embedding call records"""
|
||||
|
||||
__tablename__ = 'monitoring_embedding_calls'
|
||||
|
||||
id = sqlalchemy.Column(sqlalchemy.String(255), primary_key=True)
|
||||
timestamp = sqlalchemy.Column(sqlalchemy.DateTime, nullable=False, index=True)
|
||||
model_name = sqlalchemy.Column(sqlalchemy.String(255), nullable=False)
|
||||
prompt_tokens = sqlalchemy.Column(sqlalchemy.Integer, nullable=False)
|
||||
total_tokens = sqlalchemy.Column(sqlalchemy.Integer, nullable=False)
|
||||
duration = sqlalchemy.Column(sqlalchemy.Integer, nullable=False) # milliseconds
|
||||
input_count = sqlalchemy.Column(sqlalchemy.Integer, nullable=False) # Number of input texts
|
||||
status = sqlalchemy.Column(sqlalchemy.String(50), nullable=False) # success, error
|
||||
error_message = sqlalchemy.Column(sqlalchemy.Text, nullable=True)
|
||||
# Optional context fields
|
||||
knowledge_base_id = sqlalchemy.Column(sqlalchemy.String(255), nullable=True, index=True)
|
||||
query_text = sqlalchemy.Column(sqlalchemy.Text, nullable=True) # For retrieval calls
|
||||
session_id = sqlalchemy.Column(sqlalchemy.String(255), nullable=True, index=True)
|
||||
message_id = sqlalchemy.Column(sqlalchemy.String(255), nullable=True, index=True)
|
||||
call_type = sqlalchemy.Column(sqlalchemy.String(50), nullable=True) # embedding, retrieve
|
||||
@@ -11,6 +11,7 @@ class LegacyPipeline(Base):
|
||||
uuid = sqlalchemy.Column(sqlalchemy.String(255), primary_key=True, unique=True)
|
||||
name = sqlalchemy.Column(sqlalchemy.String(255), nullable=False)
|
||||
description = sqlalchemy.Column(sqlalchemy.String(255), nullable=False)
|
||||
emoji = sqlalchemy.Column(sqlalchemy.String(10), nullable=True, default='⚙️')
|
||||
created_at = sqlalchemy.Column(sqlalchemy.DateTime, nullable=False, server_default=sqlalchemy.func.now())
|
||||
updated_at = sqlalchemy.Column(
|
||||
sqlalchemy.DateTime,
|
||||
|
||||
@@ -7,6 +7,7 @@ class KnowledgeBase(Base):
|
||||
uuid = sqlalchemy.Column(sqlalchemy.String(255), primary_key=True, unique=True)
|
||||
name = sqlalchemy.Column(sqlalchemy.String, index=True)
|
||||
description = sqlalchemy.Column(sqlalchemy.Text)
|
||||
emoji = sqlalchemy.Column(sqlalchemy.String(10), nullable=True, default='📚')
|
||||
created_at = sqlalchemy.Column(sqlalchemy.DateTime, default=sqlalchemy.func.now())
|
||||
updated_at = sqlalchemy.Column(sqlalchemy.DateTime, default=sqlalchemy.func.now(), onupdate=sqlalchemy.func.now())
|
||||
embedding_model_uuid = sqlalchemy.Column(sqlalchemy.String, default='')
|
||||
@@ -35,6 +36,7 @@ class ExternalKnowledgeBase(Base):
|
||||
uuid = sqlalchemy.Column(sqlalchemy.String(255), primary_key=True, unique=True)
|
||||
name = sqlalchemy.Column(sqlalchemy.String, index=True)
|
||||
description = sqlalchemy.Column(sqlalchemy.Text)
|
||||
emoji = sqlalchemy.Column(sqlalchemy.String(10), nullable=True, default='🔗')
|
||||
plugin_author = sqlalchemy.Column(sqlalchemy.String, nullable=False)
|
||||
plugin_name = sqlalchemy.Column(sqlalchemy.String, nullable=False)
|
||||
retriever_name = sqlalchemy.Column(sqlalchemy.String, nullable=False)
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import sqlalchemy
|
||||
from .. import migration
|
||||
|
||||
|
||||
@migration.migration_class(18)
|
||||
class DBMigrateAddEmojiSupport(migration.DBMigration):
|
||||
"""Add emoji field to knowledge_bases, external_knowledge_bases and legacy_pipelines tables"""
|
||||
|
||||
async def upgrade(self):
|
||||
"""Upgrade"""
|
||||
# Add emoji field to knowledge_bases
|
||||
await self._add_emoji_to_table('knowledge_bases', '📚')
|
||||
|
||||
# Add emoji field to external_knowledge_bases
|
||||
await self._add_emoji_to_table('external_knowledge_bases', '🔗')
|
||||
|
||||
# Add emoji field to legacy_pipelines
|
||||
await self._add_emoji_to_table('legacy_pipelines', '⚙️')
|
||||
|
||||
async def _add_emoji_to_table(self, table_name: str, default_emoji: str):
|
||||
"""Add emoji column to specified table if it doesn't exist"""
|
||||
# Get all column names from the table
|
||||
columns = []
|
||||
|
||||
if self.ap.persistence_mgr.db.name == 'postgresql':
|
||||
result = await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.text(
|
||||
f"SELECT column_name FROM information_schema.columns WHERE table_name = '{table_name}';"
|
||||
)
|
||||
)
|
||||
all_result = result.fetchall()
|
||||
columns = [row[0] for row in all_result]
|
||||
else:
|
||||
result = await self.ap.persistence_mgr.execute_async(sqlalchemy.text(f'PRAGMA table_info({table_name});'))
|
||||
all_result = result.fetchall()
|
||||
columns = [row[1] for row in all_result]
|
||||
|
||||
# Check and add emoji column
|
||||
if 'emoji' not in columns:
|
||||
if self.ap.persistence_mgr.db.name == 'postgresql':
|
||||
await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.text(f"ALTER TABLE {table_name} ADD COLUMN emoji VARCHAR(10) DEFAULT '{default_emoji}'")
|
||||
)
|
||||
else:
|
||||
# SQLite doesn't support DEFAULT with emoji directly in ALTER TABLE
|
||||
# Add column without default first
|
||||
await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.text(f'ALTER TABLE {table_name} ADD COLUMN emoji VARCHAR(10)')
|
||||
)
|
||||
|
||||
# Set default emoji value for existing records
|
||||
await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.text(f"UPDATE {table_name} SET emoji = '{default_emoji}' WHERE emoji IS NULL")
|
||||
)
|
||||
|
||||
async def downgrade(self):
|
||||
"""Downgrade"""
|
||||
pass
|
||||
270
src/langbot/pkg/pipeline/monitoring_helper.py
Normal file
270
src/langbot/pkg/pipeline/monitoring_helper.py
Normal file
@@ -0,0 +1,270 @@
|
||||
"""
|
||||
Monitoring helper for recording events during pipeline execution.
|
||||
This module provides convenient methods to record monitoring data
|
||||
without cluttering the main pipeline code.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import traceback
|
||||
import typing
|
||||
import time
|
||||
import json
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from ..core import app
|
||||
import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query
|
||||
|
||||
|
||||
class MonitoringHelper:
|
||||
"""Helper class for monitoring operations"""
|
||||
|
||||
@staticmethod
|
||||
async def record_query_start(
|
||||
ap: app.Application,
|
||||
query: pipeline_query.Query,
|
||||
bot_id: str,
|
||||
bot_name: str,
|
||||
pipeline_id: str,
|
||||
pipeline_name: str,
|
||||
runner_name: str | None = None,
|
||||
) -> str:
|
||||
"""Record the start of query processing, returns message_id"""
|
||||
try:
|
||||
# Check if session exists, if not, record session start
|
||||
session_id = f'{query.launcher_type}_{query.launcher_id}'
|
||||
|
||||
# Try to record message
|
||||
# Use JSON serialization to preserve message chain structure (including image URLs, etc.)
|
||||
if hasattr(query, 'message_chain') and hasattr(query.message_chain, 'model_dump'):
|
||||
message_content = json.dumps(query.message_chain.model_dump(), ensure_ascii=False)
|
||||
else:
|
||||
message_content = str(query)
|
||||
|
||||
# Variables will be updated in record_query_success after preproc stage sets them
|
||||
# Here we just record None, the full variables will be set when query completes
|
||||
|
||||
message_id = await ap.monitoring_service.record_message(
|
||||
bot_id=bot_id,
|
||||
bot_name=bot_name,
|
||||
pipeline_id=pipeline_id,
|
||||
pipeline_name=pipeline_name,
|
||||
message_content=message_content,
|
||||
session_id=session_id,
|
||||
status='pending',
|
||||
level='info',
|
||||
platform=query.launcher_type.value
|
||||
if hasattr(query.launcher_type, 'value')
|
||||
else str(query.launcher_type),
|
||||
user_id=query.sender_id,
|
||||
runner_name=runner_name,
|
||||
variables=None, # Will be updated in record_query_success
|
||||
)
|
||||
|
||||
# Update session activity or create new session if it doesn't exist
|
||||
# Always pass pipeline info to handle pipeline switches
|
||||
session_updated = await ap.monitoring_service.update_session_activity(
|
||||
session_id,
|
||||
pipeline_id=pipeline_id,
|
||||
pipeline_name=pipeline_name,
|
||||
)
|
||||
if not session_updated:
|
||||
# Session doesn't exist, create it
|
||||
await ap.monitoring_service.record_session_start(
|
||||
session_id=session_id,
|
||||
bot_id=bot_id,
|
||||
bot_name=bot_name,
|
||||
pipeline_id=pipeline_id,
|
||||
pipeline_name=pipeline_name,
|
||||
platform=query.launcher_type.value
|
||||
if hasattr(query.launcher_type, 'value')
|
||||
else str(query.launcher_type),
|
||||
user_id=query.sender_id,
|
||||
)
|
||||
|
||||
return message_id
|
||||
except Exception as e:
|
||||
ap.logger.error(f'Failed to record query start: {e}')
|
||||
return ''
|
||||
|
||||
@staticmethod
|
||||
async def record_query_success(
|
||||
ap: app.Application,
|
||||
message_id: str,
|
||||
query: pipeline_query.Query | None = None,
|
||||
):
|
||||
"""Record successful query processing by updating message status and variables"""
|
||||
try:
|
||||
if message_id:
|
||||
# Serialize query.variables (filtering out internal variables)
|
||||
query_variables_str = None
|
||||
if query and hasattr(query, 'variables') and query.variables:
|
||||
filtered_vars = {k: v for k, v in query.variables.items() if not k.startswith('_')}
|
||||
if filtered_vars:
|
||||
try:
|
||||
query_variables_str = json.dumps(filtered_vars, ensure_ascii=False, default=str)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
await ap.monitoring_service.update_message_status(
|
||||
message_id=message_id,
|
||||
status='success',
|
||||
variables=query_variables_str,
|
||||
)
|
||||
except Exception as e:
|
||||
ap.logger.error(f'Failed to record query success: {e}')
|
||||
|
||||
@staticmethod
|
||||
async def record_query_error(
|
||||
ap: app.Application,
|
||||
query: pipeline_query.Query,
|
||||
bot_id: str,
|
||||
bot_name: str,
|
||||
pipeline_id: str,
|
||||
pipeline_name: str,
|
||||
error: Exception,
|
||||
runner_name: str | None = None,
|
||||
) -> str:
|
||||
"""Record query processing error, returns message_id"""
|
||||
try:
|
||||
session_id = f'{query.launcher_type}_{query.launcher_id}'
|
||||
|
||||
# Record error message
|
||||
message_id = await ap.monitoring_service.record_message(
|
||||
bot_id=bot_id,
|
||||
bot_name=bot_name,
|
||||
pipeline_id=pipeline_id,
|
||||
pipeline_name=pipeline_name,
|
||||
message_content=f'Error: {str(error)}',
|
||||
session_id=session_id,
|
||||
status='error',
|
||||
level='error',
|
||||
platform=query.launcher_type.value
|
||||
if hasattr(query.launcher_type, 'value')
|
||||
else str(query.launcher_type),
|
||||
user_id=query.sender_id,
|
||||
runner_name=runner_name,
|
||||
)
|
||||
|
||||
# Record error log
|
||||
await ap.monitoring_service.record_error(
|
||||
bot_id=bot_id,
|
||||
bot_name=bot_name,
|
||||
pipeline_id=pipeline_id,
|
||||
pipeline_name=pipeline_name,
|
||||
error_type=type(error).__name__,
|
||||
error_message=str(error),
|
||||
session_id=session_id,
|
||||
stack_trace=traceback.format_exc(),
|
||||
message_id=message_id,
|
||||
)
|
||||
|
||||
return message_id
|
||||
except Exception as e:
|
||||
ap.logger.error(f'Failed to record query error: {e}')
|
||||
return ''
|
||||
|
||||
@staticmethod
|
||||
async def record_llm_call(
|
||||
ap: app.Application,
|
||||
query: pipeline_query.Query,
|
||||
bot_id: str,
|
||||
bot_name: str,
|
||||
pipeline_id: str,
|
||||
pipeline_name: str,
|
||||
model_name: str,
|
||||
input_tokens: int,
|
||||
output_tokens: int,
|
||||
duration_ms: int,
|
||||
status: str = 'success',
|
||||
cost: float | None = None,
|
||||
error_message: str | None = None,
|
||||
message_id: str | None = None,
|
||||
):
|
||||
"""Record LLM call"""
|
||||
try:
|
||||
session_id = f'{query.launcher_type}_{query.launcher_id}'
|
||||
|
||||
await ap.monitoring_service.record_llm_call(
|
||||
bot_id=bot_id,
|
||||
bot_name=bot_name,
|
||||
pipeline_id=pipeline_id,
|
||||
pipeline_name=pipeline_name,
|
||||
session_id=session_id,
|
||||
model_name=model_name,
|
||||
input_tokens=input_tokens,
|
||||
output_tokens=output_tokens,
|
||||
duration=duration_ms,
|
||||
status=status,
|
||||
cost=cost,
|
||||
error_message=error_message,
|
||||
message_id=message_id,
|
||||
)
|
||||
except Exception as e:
|
||||
ap.logger.error(f'Failed to record LLM call: {e}')
|
||||
|
||||
|
||||
class LLMCallMonitor:
|
||||
"""Context manager for monitoring LLM calls"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
ap: app.Application,
|
||||
query: pipeline_query.Query,
|
||||
bot_id: str,
|
||||
bot_name: str,
|
||||
pipeline_id: str,
|
||||
pipeline_name: str,
|
||||
model_name: str,
|
||||
):
|
||||
self.ap = ap
|
||||
self.query = query
|
||||
self.bot_id = bot_id
|
||||
self.bot_name = bot_name
|
||||
self.pipeline_id = pipeline_id
|
||||
self.pipeline_name = pipeline_name
|
||||
self.model_name = model_name
|
||||
self.start_time = None
|
||||
self.input_tokens = 0
|
||||
self.output_tokens = 0
|
||||
|
||||
async def __aenter__(self):
|
||||
self.start_time = time.time()
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
||||
duration_ms = int((time.time() - self.start_time) * 1000)
|
||||
|
||||
if exc_type is not None:
|
||||
# Error occurred
|
||||
await MonitoringHelper.record_llm_call(
|
||||
ap=self.ap,
|
||||
query=self.query,
|
||||
bot_id=self.bot_id,
|
||||
bot_name=self.bot_name,
|
||||
pipeline_id=self.pipeline_id,
|
||||
pipeline_name=self.pipeline_name,
|
||||
model_name=self.model_name,
|
||||
input_tokens=self.input_tokens,
|
||||
output_tokens=self.output_tokens,
|
||||
duration_ms=duration_ms,
|
||||
status='error',
|
||||
error_message=str(exc_val) if exc_val else None,
|
||||
)
|
||||
else:
|
||||
# Success
|
||||
await MonitoringHelper.record_llm_call(
|
||||
ap=self.ap,
|
||||
query=self.query,
|
||||
bot_id=self.bot_id,
|
||||
bot_name=self.bot_name,
|
||||
pipeline_id=self.pipeline_id,
|
||||
pipeline_name=self.pipeline_name,
|
||||
model_name=self.model_name,
|
||||
input_tokens=self.input_tokens,
|
||||
output_tokens=self.output_tokens,
|
||||
duration_ms=duration_ms,
|
||||
status='success',
|
||||
)
|
||||
|
||||
return False # Don't suppress exceptions
|
||||
@@ -115,6 +115,25 @@ class RuntimePipeline:
|
||||
# Store bound plugins and MCP servers in query for filtering
|
||||
query.variables['_pipeline_bound_plugins'] = self.bound_plugins
|
||||
query.variables['_pipeline_bound_mcp_servers'] = self.bound_mcp_servers
|
||||
|
||||
# Record query start for monitoring
|
||||
try:
|
||||
# Get bot name from bot_uuid
|
||||
bot_name = 'WebChat'
|
||||
if query.bot_uuid:
|
||||
try:
|
||||
bot = await self.ap.bot_service.get_bot(query.bot_uuid, include_secret=False)
|
||||
if bot:
|
||||
bot_name = bot.get('name', 'Unknown')
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Store for later use in process_query
|
||||
query.variables['_monitoring_bot_name'] = bot_name
|
||||
query.variables['_monitoring_pipeline_name'] = self.pipeline_entity.name
|
||||
except Exception as e:
|
||||
self.ap.logger.error(f'Failed to prepare monitoring data: {e}')
|
||||
|
||||
await self.process_query(query)
|
||||
|
||||
async def _check_output(self, query: pipeline_query.Query, result: pipeline_entities.StageProcessResult):
|
||||
@@ -131,7 +150,7 @@ class RuntimePipeline:
|
||||
query.message_event, platform_events.GroupMessage
|
||||
):
|
||||
result.user_notice.insert(0, platform_message.At(target=query.message_event.sender.id))
|
||||
if await query.adapter.is_stream_output_supported():
|
||||
if await query.adapter.is_stream_output_supported() and query.resp_messages:
|
||||
await query.adapter.reply_message_chunk(
|
||||
message_source=query.message_event,
|
||||
bot_message=query.resp_messages[-1],
|
||||
@@ -151,6 +170,37 @@ class RuntimePipeline:
|
||||
self.ap.logger.info(result.console_notice)
|
||||
if result.error_notice:
|
||||
self.ap.logger.error(result.error_notice)
|
||||
# Mark query as having error
|
||||
query.variables['_monitoring_has_error'] = True
|
||||
# Record error to monitoring system
|
||||
try:
|
||||
bot_name = query.variables.get('_monitoring_bot_name', 'Unknown')
|
||||
pipeline_name = query.variables.get('_monitoring_pipeline_name', 'Unknown')
|
||||
message_id = query.variables.get('_monitoring_message_id', '')
|
||||
session_id = f'{query.launcher_type}_{query.launcher_id}'
|
||||
|
||||
# Update message status to error
|
||||
if message_id:
|
||||
await self.ap.monitoring_service.update_message_status(
|
||||
message_id=message_id,
|
||||
status='error',
|
||||
level='error',
|
||||
)
|
||||
|
||||
# Record error log
|
||||
await self.ap.monitoring_service.record_error(
|
||||
bot_id=query.bot_uuid or 'unknown',
|
||||
bot_name=bot_name,
|
||||
pipeline_id=self.pipeline_entity.uuid,
|
||||
pipeline_name=pipeline_name,
|
||||
error_type='PipelineError',
|
||||
error_message=result.error_notice,
|
||||
session_id=session_id,
|
||||
stack_trace=result.debug_notice if result.debug_notice else None,
|
||||
message_id=message_id,
|
||||
)
|
||||
except Exception as e:
|
||||
self.ap.logger.error(f'Failed to record error to monitoring: {e}')
|
||||
|
||||
async def _execute_from_stage(
|
||||
self,
|
||||
@@ -221,6 +271,34 @@ class RuntimePipeline:
|
||||
|
||||
async def process_query(self, query: pipeline_query.Query):
|
||||
"""处理请求"""
|
||||
# Get monitoring metadata
|
||||
bot_name = query.variables.get('_monitoring_bot_name', 'Unknown')
|
||||
pipeline_name = query.variables.get('_monitoring_pipeline_name', 'Unknown')
|
||||
|
||||
# Get runner name from pipeline config
|
||||
runner_name = None
|
||||
if query.pipeline_config and 'ai' in query.pipeline_config and 'runner' in query.pipeline_config['ai']:
|
||||
runner_name = query.pipeline_config['ai']['runner'].get('runner')
|
||||
|
||||
# Record query start and store message_id
|
||||
message_id = ''
|
||||
try:
|
||||
from . import monitoring_helper
|
||||
|
||||
message_id = await monitoring_helper.MonitoringHelper.record_query_start(
|
||||
ap=self.ap,
|
||||
query=query,
|
||||
bot_id=query.bot_uuid or 'unknown',
|
||||
bot_name=bot_name,
|
||||
pipeline_id=self.pipeline_entity.uuid,
|
||||
pipeline_name=pipeline_name,
|
||||
runner_name=runner_name,
|
||||
)
|
||||
# Store message_id in query variables for LLM call monitoring
|
||||
query.variables['_monitoring_message_id'] = message_id
|
||||
except Exception as e:
|
||||
self.ap.logger.error(f'Failed to record query start: {e}')
|
||||
|
||||
try:
|
||||
# Get bound plugins for this pipeline
|
||||
bound_plugins = query.variables.get('_pipeline_bound_plugins', None)
|
||||
@@ -249,10 +327,40 @@ class RuntimePipeline:
|
||||
self.ap.logger.debug(f'Processing query {query.query_id}')
|
||||
|
||||
await self._execute_from_stage(0, query)
|
||||
|
||||
# Record query success only if no error occurred during processing
|
||||
if not query.variables.get('_monitoring_has_error', False):
|
||||
try:
|
||||
await monitoring_helper.MonitoringHelper.record_query_success(
|
||||
ap=self.ap,
|
||||
message_id=message_id,
|
||||
query=query,
|
||||
)
|
||||
except Exception as e:
|
||||
self.ap.logger.error(f'Failed to record query success: {e}')
|
||||
|
||||
except Exception as e:
|
||||
inst_name = query.current_stage_name if query.current_stage_name else 'unknown'
|
||||
self.ap.logger.error(f'Error processing query {query.query_id} stage={inst_name} : {e}')
|
||||
self.ap.logger.error(f'Traceback: {traceback.format_exc()}')
|
||||
|
||||
# Record query error
|
||||
try:
|
||||
from . import monitoring_helper
|
||||
|
||||
await monitoring_helper.MonitoringHelper.record_query_error(
|
||||
ap=self.ap,
|
||||
query=query,
|
||||
bot_id=query.bot_uuid or 'unknown',
|
||||
bot_name=bot_name,
|
||||
pipeline_id=self.pipeline_entity.uuid,
|
||||
pipeline_name=pipeline_name,
|
||||
error=e,
|
||||
runner_name=runner_name,
|
||||
)
|
||||
except Exception as me:
|
||||
self.ap.logger.error(f'Failed to record query error: {me}')
|
||||
|
||||
finally:
|
||||
self.ap.logger.debug(f'Query {query.query_id} processed')
|
||||
del self.ap.query_pool.cached_queries[query.query_id]
|
||||
|
||||
@@ -3,6 +3,8 @@ from __future__ import annotations
|
||||
import uuid
|
||||
import typing
|
||||
import traceback
|
||||
import time
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
from .. import handler
|
||||
@@ -10,7 +12,7 @@ from ... import entities
|
||||
from ....provider import runner as runner_module
|
||||
|
||||
import langbot_plugin.api.entities.events as events
|
||||
from ....utils import importutil
|
||||
from ....utils import importutil, constants
|
||||
from ....provider import runners
|
||||
import langbot_plugin.api.entities.builtin.provider.session as provider_session
|
||||
import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query
|
||||
@@ -84,6 +86,9 @@ class ChatMessageHandler(handler.MessageHandler):
|
||||
break
|
||||
else:
|
||||
raise ValueError(f'Request Runner not found: {query.pipeline_config["ai"]["runner"]["runner"]}')
|
||||
# Mark start time for telemetry
|
||||
start_ts = time.time()
|
||||
|
||||
if is_stream:
|
||||
resp_message_id = uuid.uuid4()
|
||||
chunk_count = 0 # Track streaming chunks to reduce excessive logging
|
||||
@@ -140,7 +145,8 @@ class ChatMessageHandler(handler.MessageHandler):
|
||||
|
||||
query.session.using_conversation.messages.extend(query.resp_messages)
|
||||
except Exception as e:
|
||||
self.ap.logger.error(f'Conversation({query.query_id}) Request Failed: {type(e).__name__} {str(e)}')
|
||||
error_info = f'{traceback.format_exc()}'
|
||||
self.ap.logger.error(f'Conversation({query.query_id}) Request Failed: {error_info}')
|
||||
traceback.print_exc()
|
||||
|
||||
hide_exception_info = query.pipeline_config['output']['misc']['hide-exception']
|
||||
@@ -153,5 +159,47 @@ class ChatMessageHandler(handler.MessageHandler):
|
||||
debug_notice=traceback.format_exc(),
|
||||
)
|
||||
finally:
|
||||
# TODO statistics
|
||||
pass
|
||||
# Telemetry reporting: collect minimal per-query execution info and send asynchronously
|
||||
try:
|
||||
end_ts = time.time()
|
||||
duration_ms = None
|
||||
if 'start_ts' in locals():
|
||||
duration_ms = int((end_ts - start_ts) * 1000)
|
||||
|
||||
adapter_name = query.adapter.__class__.__name__ if hasattr(query, 'adapter') else None
|
||||
runner_name = (
|
||||
query.pipeline_config.get('ai', {}).get('runner', {}).get('runner')
|
||||
if query.pipeline_config
|
||||
else None
|
||||
)
|
||||
|
||||
# Model name if using localagent
|
||||
model_name = None
|
||||
try:
|
||||
if runner_name == 'local-agent' and getattr(query, 'use_llm_model_uuid', None):
|
||||
m = await self.ap.model_mgr.get_model_by_uuid(query.use_llm_model_uuid)
|
||||
if m and getattr(m, 'model_entity', None):
|
||||
model_name = getattr(m.model_entity, 'name', None)
|
||||
except Exception:
|
||||
model_name = None
|
||||
|
||||
pipeline_plugins = query.variables.get('_pipeline_bound_plugins', None)
|
||||
|
||||
payload = {
|
||||
'query_id': query.query_id,
|
||||
'adapter': adapter_name,
|
||||
'runner': runner_name,
|
||||
'duration_ms': duration_ms,
|
||||
'model_name': model_name,
|
||||
'version': constants.semantic_version,
|
||||
'instance_id': constants.instance_id,
|
||||
'pipeline_plugins': pipeline_plugins,
|
||||
'error': locals().get('error_info', None),
|
||||
'timestamp': datetime.utcnow().isoformat(),
|
||||
}
|
||||
|
||||
# Send telemetry asynchronously and do not block pipeline via app's telemetry manager
|
||||
await self.ap.telemetry.start_send_task(payload)
|
||||
except Exception as ex:
|
||||
# Ensure telemetry issues do not affect normal flow
|
||||
self.ap.logger.warning(f'Failed to send telemetry: {ex}')
|
||||
|
||||
@@ -75,10 +75,17 @@ class RuntimeBot:
|
||||
|
||||
# Only add to query pool if no webhook requested to skip pipeline
|
||||
if not skip_pipeline:
|
||||
launcher_id = event.sender.id
|
||||
|
||||
if hasattr(adapter, 'get_launcher_id'):
|
||||
custom_launcher_id = adapter.get_launcher_id(event)
|
||||
if custom_launcher_id:
|
||||
launcher_id = custom_launcher_id
|
||||
|
||||
await self.ap.query_pool.add_query(
|
||||
bot_uuid=self.bot_entity.uuid,
|
||||
launcher_type=provider_session.LauncherTypes.PERSON,
|
||||
launcher_id=event.sender.id,
|
||||
launcher_id=launcher_id,
|
||||
sender_id=event.sender.id,
|
||||
message_event=event,
|
||||
message_chain=event.message_chain,
|
||||
@@ -86,7 +93,7 @@ class RuntimeBot:
|
||||
pipeline_uuid=self.bot_entity.use_pipeline_uuid,
|
||||
)
|
||||
else:
|
||||
await self.logger.info(f'Pipeline skipped for person message due to webhook response')
|
||||
await self.logger.info('Pipeline skipped for person message due to webhook response')
|
||||
|
||||
async def on_group_message(
|
||||
event: platform_events.GroupMessage,
|
||||
@@ -111,10 +118,17 @@ class RuntimeBot:
|
||||
|
||||
# Only add to query pool if no webhook requested to skip pipeline
|
||||
if not skip_pipeline:
|
||||
launcher_id = event.group.id
|
||||
|
||||
if hasattr(adapter, 'get_launcher_id'):
|
||||
custom_launcher_id = adapter.get_launcher_id(event)
|
||||
if custom_launcher_id:
|
||||
launcher_id = custom_launcher_id
|
||||
|
||||
await self.ap.query_pool.add_query(
|
||||
bot_uuid=self.bot_entity.uuid,
|
||||
launcher_type=provider_session.LauncherTypes.GROUP,
|
||||
launcher_id=event.group.id,
|
||||
launcher_id=launcher_id,
|
||||
sender_id=event.sender.id,
|
||||
message_event=event,
|
||||
message_chain=event.message_chain,
|
||||
@@ -122,7 +136,7 @@ class RuntimeBot:
|
||||
pipeline_uuid=self.bot_entity.use_pipeline_uuid,
|
||||
)
|
||||
else:
|
||||
await self.logger.info(f'Pipeline skipped for group message due to webhook response')
|
||||
await self.logger.info('Pipeline skipped for group message due to webhook response')
|
||||
|
||||
self.adapter.register_listener(platform_events.FriendMessage, on_friend_message)
|
||||
self.adapter.register_listener(platform_events.GroupMessage, on_group_message)
|
||||
|
||||
@@ -231,7 +231,10 @@ class DingTalkAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
|
||||
card_template_id = self.config['card_template_id']
|
||||
incoming_message = event.source_platform_object.incoming_message
|
||||
# message_id = incoming_message.message_id
|
||||
card_instance, card_instance_id = await self.bot.create_and_card(card_template_id, incoming_message)
|
||||
card_auto_layout = self.config.get('card_ auto_layout', False)
|
||||
card_instance, card_instance_id = await self.bot.create_and_card(
|
||||
card_template_id, incoming_message, card_auto_layout=card_auto_layout
|
||||
)
|
||||
self.card_instance_id_dict[message_id] = (card_instance, card_instance_id)
|
||||
return True
|
||||
|
||||
|
||||
@@ -56,6 +56,13 @@ spec:
|
||||
type: boolean
|
||||
required: true
|
||||
default: false
|
||||
- name: card_auto_layout
|
||||
label:
|
||||
en_US: Card Auto Layout
|
||||
zh_Hans: 卡片宽屏自动布局
|
||||
type: boolean
|
||||
required: false
|
||||
default: false
|
||||
- name: card_template_id
|
||||
label:
|
||||
en_US: card template id
|
||||
|
||||
@@ -244,7 +244,6 @@ class LarkMessageConverter(abstract_platform_adapter.AbstractMessageConverter):
|
||||
|
||||
lb_msg_list.append(platform_message.Source(id=message.message_id, time=msg_create_time))
|
||||
|
||||
|
||||
if message.message_type == 'text':
|
||||
element_list = []
|
||||
|
||||
@@ -310,7 +309,11 @@ class LarkMessageConverter(abstract_platform_adapter.AbstractMessageConverter):
|
||||
]
|
||||
elif message.message_type == 'audio':
|
||||
message_content['content'] = [
|
||||
{'tag': 'audio', 'file_key': message_content['file_key'], "duration": message_content.get('duration',0)}
|
||||
{
|
||||
'tag': 'audio',
|
||||
'file_key': message_content['file_key'],
|
||||
'duration': message_content.get('duration', 0),
|
||||
}
|
||||
]
|
||||
|
||||
for ele in message_content['content']:
|
||||
@@ -367,12 +370,9 @@ class LarkMessageConverter(abstract_platform_adapter.AbstractMessageConverter):
|
||||
audio_bytes = response.file.read()
|
||||
audio_base64 = base64.b64encode(audio_bytes).decode()
|
||||
|
||||
|
||||
# Get content type from response headers
|
||||
content_type = response.raw.headers.get('content-type', 'audio/mpeg')
|
||||
|
||||
|
||||
|
||||
mime_main = content_type.split(';')[0].strip()
|
||||
ext = mimetypes.guess_extension(mime_main) or '.bin'
|
||||
temp_dir = tempfile.gettempdir()
|
||||
@@ -418,7 +418,6 @@ class LarkMessageConverter(abstract_platform_adapter.AbstractMessageConverter):
|
||||
file_bytes = response.file.read()
|
||||
file_base64 = base64.b64encode(file_bytes).decode()
|
||||
|
||||
|
||||
file_format = response.raw.headers['content-type']
|
||||
|
||||
file_size = len(file_bytes)
|
||||
@@ -453,7 +452,6 @@ class LarkMessageConverter(abstract_platform_adapter.AbstractMessageConverter):
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
return platform_message.MessageChain(lb_msg_list)
|
||||
|
||||
|
||||
|
||||
BIN
src/langbot/pkg/platform/sources/satori.png
Normal file
BIN
src/langbot/pkg/platform/sources/satori.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
1090
src/langbot/pkg/platform/sources/satori.py
Normal file
1090
src/langbot/pkg/platform/sources/satori.py
Normal file
File diff suppressed because it is too large
Load Diff
65
src/langbot/pkg/platform/sources/satori.yaml
Normal file
65
src/langbot/pkg/platform/sources/satori.yaml
Normal file
@@ -0,0 +1,65 @@
|
||||
apiVersion: v1
|
||||
kind: MessagePlatformAdapter
|
||||
metadata:
|
||||
name: satori
|
||||
label:
|
||||
en_US: Satori
|
||||
zh_Hans: Satori
|
||||
description:
|
||||
en_US: SatoriAdapter
|
||||
zh_Hans: 古明地觉协议适配器
|
||||
icon: satori.png
|
||||
spec:
|
||||
config:
|
||||
- name: platform
|
||||
label:
|
||||
en_US: Platform
|
||||
zh_Hans: 平台名称
|
||||
type: string
|
||||
required: true
|
||||
default: "llonebot"
|
||||
description:
|
||||
en_US: The platform name (e.g., llonebot, discord, telegram)
|
||||
zh_Hans: 平台名称(如 llonebot, discord, telegram)
|
||||
- name: host
|
||||
label:
|
||||
en_US: Host
|
||||
zh_Hans: 主机地址
|
||||
type: string
|
||||
required: true
|
||||
default: "127.0.0.1"
|
||||
description:
|
||||
en_US: The host address of LLOneBot Satori server (e.g., 127.0.0.1, localhost, 192.168.1.100)
|
||||
zh_Hans: LLOneBot Satori服务器的主机地址(如 127.0.0.1, localhost, 192.168.1.100)
|
||||
- name: port
|
||||
label:
|
||||
en_US: Port
|
||||
zh_Hans: 监听端口
|
||||
type: integer
|
||||
required: true
|
||||
default: 5600
|
||||
- name: satori_api_base_url
|
||||
label:
|
||||
en_US: Satori API Endpoint
|
||||
zh_Hans: Satori API 终结点
|
||||
type: string
|
||||
required: true
|
||||
default: "http://localhost:5600/v1"
|
||||
- name: satori_endpoint
|
||||
label:
|
||||
en_US: Satori WebSocket Endpoint
|
||||
zh_Hans: Satori WebSocket 终结点
|
||||
type: string
|
||||
required: true
|
||||
default: "ws://localhost:5600/v1/events"
|
||||
- name: token
|
||||
label:
|
||||
en_US: Token
|
||||
zh_Hans: 令牌
|
||||
type: string
|
||||
required: true
|
||||
default: ""
|
||||
execution:
|
||||
python:
|
||||
path: ./satori.py
|
||||
attr: SatoriAdapter
|
||||
@@ -85,6 +85,26 @@ class TelegramMessageConverter(abstract_platform_adapter.AbstractMessageConverte
|
||||
)
|
||||
)
|
||||
|
||||
if message.voice:
|
||||
if message.caption:
|
||||
message_components.extend(parse_message_text(message.caption))
|
||||
|
||||
file = await message.voice.get_file()
|
||||
|
||||
file_bytes = None
|
||||
file_format = message.voice.mime_type or 'audio/ogg'
|
||||
|
||||
async with aiohttp.ClientSession(trust_env=True) as session:
|
||||
async with session.get(file.file_path) as response:
|
||||
file_bytes = await response.read()
|
||||
|
||||
message_components.append(
|
||||
platform_message.Voice(
|
||||
base64=f'data:{file_format};base64,{base64.b64encode(file_bytes).decode("utf-8")}',
|
||||
length=message.voice.duration,
|
||||
)
|
||||
)
|
||||
|
||||
return platform_message.MessageChain(message_components)
|
||||
|
||||
|
||||
@@ -159,7 +179,9 @@ class TelegramAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
|
||||
|
||||
application = ApplicationBuilder().token(config['token']).build()
|
||||
bot = application.bot
|
||||
application.add_handler(MessageHandler(filters.TEXT | (filters.COMMAND) | filters.PHOTO, telegram_callback))
|
||||
application.add_handler(
|
||||
MessageHandler(filters.TEXT | (filters.COMMAND) | filters.PHOTO | filters.VOICE, telegram_callback)
|
||||
)
|
||||
super().__init__(
|
||||
config=config,
|
||||
logger=logger,
|
||||
@@ -197,6 +219,10 @@ class TelegramAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
|
||||
}
|
||||
if self.config['markdown_card'] is True:
|
||||
args['parse_mode'] = 'MarkdownV2'
|
||||
|
||||
if message_source.source_platform_object.message.message_thread_id:
|
||||
args['message_thread_id'] = message_source.source_platform_object.message.message_thread_id
|
||||
|
||||
if quote_origin:
|
||||
args['reply_to_message_id'] = message_source.source_platform_object.message.id
|
||||
|
||||
@@ -216,8 +242,6 @@ class TelegramAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
|
||||
components = await TelegramMessageConverter.yiri2target(message, self.bot)
|
||||
args = {}
|
||||
message_id = message_source.source_platform_object.message.id
|
||||
if quote_origin:
|
||||
args['reply_to_message_id'] = message_source.source_platform_object.message.id
|
||||
|
||||
component = components[0]
|
||||
if message_id not in self.msg_stream_id: # 当消息回复第一次时,发送新消息
|
||||
@@ -233,6 +257,12 @@ class TelegramAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
|
||||
'chat_id': message_source.source_platform_object.effective_chat.id,
|
||||
'text': content,
|
||||
}
|
||||
if message_source.source_platform_object.message.message_thread_id:
|
||||
args['message_thread_id'] = message_source.source_platform_object.message.message_thread_id
|
||||
|
||||
if quote_origin:
|
||||
args['reply_to_message_id'] = message_source.source_platform_object.message.id
|
||||
|
||||
if self.config['markdown_card'] is True:
|
||||
args['parse_mode'] = 'MarkdownV2'
|
||||
|
||||
@@ -260,6 +290,24 @@ class TelegramAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
|
||||
# self.seq = 1 # 消息回复结束之后重置seq
|
||||
self.msg_stream_id.pop(message_id) # 消息回复结束之后删除流式消息id
|
||||
|
||||
def get_launcher_id(self, event: platform_events.MessageEvent) -> str | None:
|
||||
if not isinstance(event.source_platform_object, Update):
|
||||
return None
|
||||
|
||||
message = event.source_platform_object.message
|
||||
if not message:
|
||||
return None
|
||||
|
||||
# specifically handle telegram forum topic and private thread(not supported by official client yet but supported by bot api)
|
||||
if message.message_thread_id:
|
||||
# check if it is a group
|
||||
if isinstance(event, platform_events.GroupMessage):
|
||||
return f'{event.group.id}#{message.message_thread_id}'
|
||||
elif isinstance(event, platform_events.FriendMessage):
|
||||
return f'{event.sender.id}#{message.message_thread_id}'
|
||||
|
||||
return None
|
||||
|
||||
async def is_stream_output_supported(self) -> bool:
|
||||
is_stream = False
|
||||
if self.config.get('enable-stream-reply', None):
|
||||
|
||||
@@ -18,52 +18,52 @@ import langbot_plugin.api.entities.builtin.platform.entities as platform_entitie
|
||||
def split_string_by_bytes(text, limit=2048, encoding='utf-8'):
|
||||
"""
|
||||
Splits a string into a list of strings, where each part is at most 'limit' bytes.
|
||||
|
||||
|
||||
Args:
|
||||
text (str): The original string to split.
|
||||
limit (int): The maximum byte size for each split part.
|
||||
encoding (str): The encoding to use (default is 'utf-8').
|
||||
|
||||
|
||||
Returns:
|
||||
list: A list of split strings.
|
||||
"""
|
||||
# 1. Encode the entire string into bytes
|
||||
bytes_data = text.encode(encoding)
|
||||
total_len = len(bytes_data)
|
||||
|
||||
|
||||
parts = []
|
||||
start = 0
|
||||
|
||||
|
||||
while start < total_len:
|
||||
# 2. Determine the end index for the current chunk
|
||||
# It shouldn't exceed the total length
|
||||
end = min(start + limit, total_len)
|
||||
|
||||
|
||||
# 3. Slice the byte array
|
||||
chunk = bytes_data[start:end]
|
||||
|
||||
|
||||
# 4. Attempt to decode the chunk
|
||||
# Use errors='ignore' to drop any partial bytes at the end of the chunk
|
||||
# (e.g., if a 3-byte character was cut after the 2nd byte)
|
||||
part_str = chunk.decode(encoding, errors='ignore')
|
||||
|
||||
|
||||
# 5. Calculate the actual byte length of the successfully decoded string
|
||||
# This tells us exactly where the valid character boundary ended
|
||||
part_bytes = part_str.encode(encoding)
|
||||
part_len = len(part_bytes)
|
||||
|
||||
|
||||
# Safety check: Prevent infinite loop if limit is too small (e.g., limit=1 for a Chinese char)
|
||||
if part_len == 0 and end < total_len:
|
||||
# Force advance by 1 byte to consume the un-decodable byte or raise error
|
||||
# Here we just treat it as a part to avoid stuck loops, though it might be invalid
|
||||
start += 1
|
||||
start += 1
|
||||
continue
|
||||
|
||||
parts.append(part_str)
|
||||
|
||||
|
||||
# 6. Move the start pointer by the actual length consumed
|
||||
start += part_len
|
||||
|
||||
|
||||
return parts
|
||||
|
||||
|
||||
@@ -75,13 +75,15 @@ class WecomMessageConverter(abstract_platform_adapter.AbstractMessageConverter):
|
||||
for msg in message_chain:
|
||||
if type(msg) is platform_message.Plain:
|
||||
chunks = split_string_by_bytes(msg.text)
|
||||
content_list.extend([
|
||||
{
|
||||
'type': 'text',
|
||||
'content': chunk,
|
||||
}
|
||||
for chunk in chunks
|
||||
])
|
||||
content_list.extend(
|
||||
[
|
||||
{
|
||||
'type': 'text',
|
||||
'content': chunk,
|
||||
}
|
||||
for chunk in chunks
|
||||
]
|
||||
)
|
||||
elif type(msg) is platform_message.Image:
|
||||
content_list.append(
|
||||
{
|
||||
|
||||
@@ -56,7 +56,7 @@ class WebhookPusher:
|
||||
# Check if any webhook responded with skip_pipeline=true
|
||||
for result in results:
|
||||
if isinstance(result, dict) and result.get('skip_pipeline') is True:
|
||||
self.logger.info(f'Webhook responded with skip_pipeline=true, skipping pipeline for person message')
|
||||
self.logger.info('Webhook responded with skip_pipeline=true, skipping pipeline for person message')
|
||||
return True
|
||||
|
||||
return False
|
||||
@@ -103,7 +103,7 @@ class WebhookPusher:
|
||||
# Check if any webhook responded with skip_pipeline=true
|
||||
for result in results:
|
||||
if isinstance(result, dict) and result.get('skip_pipeline') is True:
|
||||
self.logger.info(f'Webhook responded with skip_pipeline=true, skipping pipeline for group message')
|
||||
self.logger.info('Webhook responded with skip_pipeline=true, skipping pipeline for group message')
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
@@ -324,7 +324,7 @@ class RuntimeConnectionHandler(handler.Handler):
|
||||
messages_obj = [provider_message.Message.model_validate(message) for message in messages]
|
||||
funcs_obj = [resource_tool.LLMTool.model_validate(func) for func in funcs]
|
||||
|
||||
result = await llm_model.provider.requester.invoke_llm(
|
||||
result = await llm_model.provider.invoke_llm(
|
||||
query=None,
|
||||
model=llm_model,
|
||||
messages=messages_obj,
|
||||
|
||||
@@ -9,6 +9,7 @@ from ...discover import engine
|
||||
from . import token
|
||||
from ...entity.persistence import model as persistence_model
|
||||
from ...entity.errors import provider as provider_errors
|
||||
from async_lru import alru_cache
|
||||
|
||||
|
||||
class ModelManager:
|
||||
@@ -148,6 +149,7 @@ class ModelManager:
|
||||
'prefered_ranking': space_model.featured_order,
|
||||
},
|
||||
preserve_uuid=True,
|
||||
auto_set_to_default_pipeline=False,
|
||||
)
|
||||
|
||||
elif space_model.category == 'embedding':
|
||||
@@ -349,6 +351,7 @@ class ModelManager:
|
||||
|
||||
await self.load_embedding_model_with_provider(model_entity, provider_entity)
|
||||
|
||||
@alru_cache(ttl=60 * 5)
|
||||
async def get_model_by_uuid(self, uuid: str) -> requester.RuntimeLLMModel:
|
||||
"""Get LLM model by uuid"""
|
||||
for model in self.llm_models:
|
||||
@@ -356,6 +359,7 @@ class ModelManager:
|
||||
return model
|
||||
raise ValueError(f'LLM model {uuid} not found')
|
||||
|
||||
@alru_cache(ttl=60 * 5)
|
||||
async def get_embedding_model_by_uuid(self, uuid: str) -> requester.RuntimeEmbeddingModel:
|
||||
"""Get embedding model by uuid"""
|
||||
for model in self.embedding_models:
|
||||
|
||||
@@ -2,6 +2,7 @@ from __future__ import annotations
|
||||
|
||||
import abc
|
||||
import typing
|
||||
import time
|
||||
|
||||
from ...core import app
|
||||
from ...entity.persistence import model as persistence_model
|
||||
@@ -33,6 +34,219 @@ class RuntimeProvider:
|
||||
self.token_mgr = token_mgr
|
||||
self.requester = requester
|
||||
|
||||
async def invoke_llm(
|
||||
self,
|
||||
query: pipeline_query.Query,
|
||||
model: RuntimeLLMModel,
|
||||
messages: typing.List[provider_message.Message],
|
||||
funcs: typing.List[resource_tool.LLMTool] = None,
|
||||
extra_args: dict[str, typing.Any] = {},
|
||||
remove_think: bool = False,
|
||||
) -> provider_message.Message:
|
||||
"""Bridge method for invoking LLM with monitoring"""
|
||||
# Start timing for monitoring
|
||||
start_time = time.time()
|
||||
input_tokens = 0
|
||||
output_tokens = 0
|
||||
status = 'success'
|
||||
error_message = None
|
||||
|
||||
try:
|
||||
# Call the underlying requester
|
||||
result = await self.requester.invoke_llm(
|
||||
query=query,
|
||||
model=model,
|
||||
messages=messages,
|
||||
funcs=funcs,
|
||||
extra_args=extra_args,
|
||||
remove_think=remove_think,
|
||||
)
|
||||
|
||||
# Try to extract token usage if the requester returns it
|
||||
# For requesters that return tuple (message, usage_info)
|
||||
if isinstance(result, tuple):
|
||||
msg, usage_info = result
|
||||
if usage_info:
|
||||
input_tokens = usage_info.get('input_tokens', 0)
|
||||
output_tokens = usage_info.get('output_tokens', 0)
|
||||
return msg
|
||||
else:
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
status = 'error'
|
||||
error_message = str(e)
|
||||
raise
|
||||
finally:
|
||||
# Record LLM call monitoring data (only if query is provided)
|
||||
if query is not None:
|
||||
duration_ms = int((time.time() - start_time) * 1000)
|
||||
|
||||
# Import monitoring helper
|
||||
try:
|
||||
from ...pipeline import monitoring_helper
|
||||
|
||||
# Get monitoring metadata from query variables
|
||||
if query.variables:
|
||||
bot_name = query.variables.get('_monitoring_bot_name', 'Unknown')
|
||||
pipeline_name = query.variables.get('_monitoring_pipeline_name', 'Unknown')
|
||||
message_id = query.variables.get('_monitoring_message_id')
|
||||
else:
|
||||
bot_name = 'Unknown'
|
||||
pipeline_name = 'Unknown'
|
||||
message_id = None
|
||||
|
||||
await monitoring_helper.MonitoringHelper.record_llm_call(
|
||||
ap=self.requester.ap,
|
||||
query=query,
|
||||
bot_id=query.bot_uuid or 'unknown',
|
||||
bot_name=bot_name,
|
||||
pipeline_id=query.pipeline_uuid or 'unknown',
|
||||
pipeline_name=pipeline_name,
|
||||
model_name=model.model_entity.name,
|
||||
input_tokens=input_tokens,
|
||||
output_tokens=output_tokens,
|
||||
duration_ms=duration_ms,
|
||||
status=status,
|
||||
error_message=error_message,
|
||||
message_id=message_id,
|
||||
)
|
||||
except Exception as monitor_err:
|
||||
self.requester.ap.logger.error(f'[Monitoring] Failed to record LLM call: {monitor_err}')
|
||||
|
||||
async def invoke_llm_stream(
|
||||
self,
|
||||
query: pipeline_query.Query,
|
||||
model: RuntimeLLMModel,
|
||||
messages: typing.List[provider_message.Message],
|
||||
funcs: typing.List[resource_tool.LLMTool] = None,
|
||||
extra_args: dict[str, typing.Any] = {},
|
||||
remove_think: bool = False,
|
||||
) -> provider_message.MessageChunk:
|
||||
"""Bridge method for invoking LLM stream with monitoring"""
|
||||
# Start timing for monitoring
|
||||
start_time = time.time()
|
||||
status = 'success'
|
||||
error_message = None
|
||||
# Note: Stream doesn't easily provide token counts, set to 0
|
||||
input_tokens = 0
|
||||
output_tokens = 0
|
||||
|
||||
try:
|
||||
# Stream the response
|
||||
async for chunk in self.requester.invoke_llm_stream(
|
||||
query=query,
|
||||
model=model,
|
||||
messages=messages,
|
||||
funcs=funcs,
|
||||
extra_args=extra_args,
|
||||
remove_think=remove_think,
|
||||
):
|
||||
yield chunk
|
||||
except Exception as e:
|
||||
status = 'error'
|
||||
error_message = str(e)
|
||||
raise
|
||||
finally:
|
||||
# Record LLM call monitoring data (only if query is provided)
|
||||
if query is not None:
|
||||
duration_ms = int((time.time() - start_time) * 1000)
|
||||
|
||||
# Import monitoring helper
|
||||
try:
|
||||
from ...pipeline import monitoring_helper
|
||||
|
||||
# Get monitoring metadata from query variables
|
||||
if query.variables:
|
||||
bot_name = query.variables.get('_monitoring_bot_name', 'Unknown')
|
||||
pipeline_name = query.variables.get('_monitoring_pipeline_name', 'Unknown')
|
||||
message_id = query.variables.get('_monitoring_message_id')
|
||||
else:
|
||||
bot_name = 'Unknown'
|
||||
pipeline_name = 'Unknown'
|
||||
message_id = None
|
||||
|
||||
await monitoring_helper.MonitoringHelper.record_llm_call(
|
||||
ap=self.requester.ap,
|
||||
query=query,
|
||||
bot_id=query.bot_uuid or 'unknown',
|
||||
bot_name=bot_name,
|
||||
pipeline_id=query.pipeline_uuid or 'unknown',
|
||||
pipeline_name=pipeline_name,
|
||||
model_name=model.model_entity.name,
|
||||
input_tokens=input_tokens,
|
||||
output_tokens=output_tokens,
|
||||
duration_ms=duration_ms,
|
||||
status=status,
|
||||
error_message=error_message,
|
||||
message_id=message_id,
|
||||
)
|
||||
except Exception as monitor_err:
|
||||
self.requester.ap.logger.error(f'[Monitoring] Failed to record LLM stream call: {monitor_err}')
|
||||
|
||||
async def invoke_embedding(
|
||||
self,
|
||||
model: RuntimeEmbeddingModel,
|
||||
input_text: typing.List[str],
|
||||
extra_args: dict[str, typing.Any] = {},
|
||||
knowledge_base_id: str | None = None,
|
||||
query_text: str | None = None,
|
||||
session_id: str | None = None,
|
||||
message_id: str | None = None,
|
||||
call_type: str | None = None,
|
||||
) -> typing.List[typing.List[float]]:
|
||||
"""Bridge method for invoking embedding with monitoring"""
|
||||
# Start timing for monitoring
|
||||
start_time = time.time()
|
||||
prompt_tokens = 0
|
||||
total_tokens = 0
|
||||
status = 'success'
|
||||
error_message = None
|
||||
|
||||
try:
|
||||
# Call the underlying requester
|
||||
result = await self.requester.invoke_embedding(
|
||||
model=model,
|
||||
input_text=input_text,
|
||||
extra_args=extra_args,
|
||||
)
|
||||
|
||||
# Handle both old format (list only) and new format (tuple with usage)
|
||||
if isinstance(result, tuple):
|
||||
embeddings, usage_info = result
|
||||
if usage_info:
|
||||
prompt_tokens = usage_info.get('prompt_tokens', 0)
|
||||
total_tokens = usage_info.get('total_tokens', 0)
|
||||
return embeddings
|
||||
else:
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
status = 'error'
|
||||
error_message = str(e)
|
||||
raise
|
||||
finally:
|
||||
# Record embedding call monitoring data
|
||||
duration_ms = int((time.time() - start_time) * 1000)
|
||||
|
||||
try:
|
||||
await self.requester.ap.monitoring_service.record_embedding_call(
|
||||
model_name=model.model_entity.name,
|
||||
prompt_tokens=prompt_tokens,
|
||||
total_tokens=total_tokens,
|
||||
duration=duration_ms,
|
||||
input_count=len(input_text),
|
||||
status=status,
|
||||
error_message=error_message,
|
||||
knowledge_base_id=knowledge_base_id,
|
||||
query_text=query_text,
|
||||
session_id=session_id,
|
||||
message_id=message_id,
|
||||
call_type=call_type,
|
||||
)
|
||||
except Exception as monitor_err:
|
||||
self.requester.ap.logger.error(f'[Monitoring] Failed to record embedding call: {monitor_err}')
|
||||
|
||||
|
||||
class RuntimeLLMModel:
|
||||
"""运行时模型"""
|
||||
@@ -141,7 +355,7 @@ class ProviderAPIRequester(metaclass=abc.ABCMeta):
|
||||
model: RuntimeEmbeddingModel,
|
||||
input_text: typing.List[str],
|
||||
extra_args: dict[str, typing.Any] = {},
|
||||
) -> typing.List[typing.List[float]]:
|
||||
) -> typing.Union[typing.List[typing.List[float]], tuple[typing.List[typing.List[float]], dict]]:
|
||||
"""调用 Embedding API
|
||||
|
||||
Args:
|
||||
@@ -151,5 +365,6 @@ class ProviderAPIRequester(metaclass=abc.ABCMeta):
|
||||
|
||||
Returns:
|
||||
typing.List[typing.List[float]]: 返回的 embedding 向量
|
||||
或者 tuple[typing.List[typing.List[float]], dict]: 返回 (embedding 向量, usage_info)
|
||||
"""
|
||||
pass
|
||||
|
||||
@@ -253,7 +253,7 @@ class OpenAIChatCompletions(requester.ProviderAPIRequester):
|
||||
use_funcs: list[resource_tool.LLMTool] = None,
|
||||
extra_args: dict[str, typing.Any] = {},
|
||||
remove_think: bool = False,
|
||||
) -> provider_message.Message:
|
||||
) -> tuple[provider_message.Message, dict]:
|
||||
self.client.api_key = use_model.provider.token_mgr.get_token()
|
||||
|
||||
args = {}
|
||||
@@ -285,7 +285,14 @@ class OpenAIChatCompletions(requester.ProviderAPIRequester):
|
||||
# 处理请求结果
|
||||
message = await self._make_msg(resp, remove_think)
|
||||
|
||||
return message
|
||||
# Extract token usage from response
|
||||
usage_info = {}
|
||||
if hasattr(resp, 'usage') and resp.usage:
|
||||
usage_info['input_tokens'] = resp.usage.prompt_tokens or 0
|
||||
usage_info['output_tokens'] = resp.usage.completion_tokens or 0
|
||||
usage_info['total_tokens'] = resp.usage.total_tokens or 0
|
||||
|
||||
return message, usage_info
|
||||
|
||||
async def invoke_llm(
|
||||
self,
|
||||
@@ -295,7 +302,8 @@ class OpenAIChatCompletions(requester.ProviderAPIRequester):
|
||||
funcs: typing.List[resource_tool.LLMTool] = None,
|
||||
extra_args: dict[str, typing.Any] = {},
|
||||
remove_think: bool = False,
|
||||
) -> provider_message.Message:
|
||||
) -> tuple[provider_message.Message, dict]:
|
||||
"""Invoke LLM and return message with usage info"""
|
||||
req_messages = [] # req_messages 仅用于类内,外部同步由 query.messages 进行
|
||||
for m in messages:
|
||||
msg_dict = m.dict(exclude_none=True)
|
||||
@@ -308,7 +316,7 @@ class OpenAIChatCompletions(requester.ProviderAPIRequester):
|
||||
req_messages.append(msg_dict)
|
||||
|
||||
try:
|
||||
msg = await self._closure(
|
||||
msg, usage_info = await self._closure(
|
||||
query=query,
|
||||
req_messages=req_messages,
|
||||
use_model=model,
|
||||
@@ -316,30 +324,38 @@ class OpenAIChatCompletions(requester.ProviderAPIRequester):
|
||||
extra_args=extra_args,
|
||||
remove_think=remove_think,
|
||||
)
|
||||
return msg
|
||||
return msg, usage_info
|
||||
except asyncio.TimeoutError:
|
||||
raise errors.RequesterError('请求超时')
|
||||
except openai.BadRequestError as e:
|
||||
if 'context_length_exceeded' in e.message:
|
||||
raise errors.RequesterError(f'上文过长,请重置会话: {e.message}')
|
||||
error_message = str(e.message) if hasattr(e, 'message') else str(e)
|
||||
if 'context_length_exceeded' in str(e):
|
||||
raise errors.RequesterError(f'上文过长,请重置会话: {error_message}')
|
||||
else:
|
||||
raise errors.RequesterError(f'请求参数错误: {e.message}')
|
||||
raise errors.RequesterError(f'请求参数错误: {error_message}')
|
||||
except openai.AuthenticationError as e:
|
||||
raise errors.RequesterError(f'无效的 api-key: {e.message}')
|
||||
error_message = str(e.message) if hasattr(e, 'message') else str(e)
|
||||
raise errors.RequesterError(f'无效的 api-key: {error_message}')
|
||||
except openai.NotFoundError as e:
|
||||
raise errors.RequesterError(f'请求路径错误: {e.message}')
|
||||
error_message = str(e.message) if hasattr(e, 'message') else str(e)
|
||||
raise errors.RequesterError(f'请求路径错误: {error_message}')
|
||||
except openai.RateLimitError as e:
|
||||
raise errors.RequesterError(f'请求过于频繁或余额不足: {e.message}')
|
||||
error_message = str(e.message) if hasattr(e, 'message') else str(e)
|
||||
raise errors.RequesterError(f'请求过于频繁或余额不足: {error_message}')
|
||||
except openai.APIConnectionError as e:
|
||||
error_message = f'连接错误: {str(e)}'
|
||||
raise errors.RequesterError(error_message)
|
||||
except openai.APIError as e:
|
||||
raise errors.RequesterError(f'请求错误: {e.message}')
|
||||
error_message = str(e.message) if hasattr(e, 'message') else str(e)
|
||||
raise errors.RequesterError(f'请求错误: {error_message}')
|
||||
|
||||
async def invoke_embedding(
|
||||
self,
|
||||
model: requester.RuntimeEmbeddingModel,
|
||||
input_text: list[str],
|
||||
extra_args: dict[str, typing.Any] = {},
|
||||
) -> list[list[float]]:
|
||||
"""调用 Embedding API"""
|
||||
) -> tuple[list[list[float]], dict]:
|
||||
"""调用 Embedding API, returns (embeddings, usage_info)"""
|
||||
self.client.api_key = model.provider.token_mgr.get_token()
|
||||
|
||||
args = {
|
||||
@@ -355,7 +371,13 @@ class OpenAIChatCompletions(requester.ProviderAPIRequester):
|
||||
try:
|
||||
resp = await self.client.embeddings.create(**args)
|
||||
|
||||
return [d.embedding for d in resp.data]
|
||||
# Extract usage info
|
||||
usage_info = {}
|
||||
if hasattr(resp, 'usage') and resp.usage:
|
||||
usage_info['prompt_tokens'] = resp.usage.prompt_tokens or 0
|
||||
usage_info['total_tokens'] = resp.usage.total_tokens or 0
|
||||
|
||||
return [d.embedding for d in resp.data], usage_info
|
||||
except asyncio.TimeoutError:
|
||||
raise errors.RequesterError('请求超时')
|
||||
except openai.BadRequestError as e:
|
||||
|
||||
@@ -25,7 +25,7 @@ class DeepseekChatCompletions(chatcmpl.OpenAIChatCompletions):
|
||||
use_funcs: list[resource_tool.LLMTool] = None,
|
||||
extra_args: dict[str, typing.Any] = {},
|
||||
remove_think: bool = False,
|
||||
) -> provider_message.Message:
|
||||
) -> tuple[provider_message.Message, dict]:
|
||||
self.client.api_key = use_model.provider.token_mgr.get_token()
|
||||
|
||||
args = {}
|
||||
@@ -43,7 +43,7 @@ class DeepseekChatCompletions(chatcmpl.OpenAIChatCompletions):
|
||||
# deepseek 不支持多模态,把content都转换成纯文字
|
||||
for m in messages:
|
||||
if 'content' in m and isinstance(m['content'], list):
|
||||
m['content'] = ' '.join([c['text'] for c in m['content']])
|
||||
m['content'] = ' '.join([c['text'] for c in m['content'] if 'text' in c])
|
||||
|
||||
args['messages'] = messages
|
||||
|
||||
@@ -57,4 +57,11 @@ class DeepseekChatCompletions(chatcmpl.OpenAIChatCompletions):
|
||||
# 处理请求结果
|
||||
message = await self._make_msg(resp, remove_think)
|
||||
|
||||
return message
|
||||
# Extract token usage from response
|
||||
usage_info = {}
|
||||
if hasattr(resp, 'usage') and resp.usage:
|
||||
usage_info['input_tokens'] = resp.usage.prompt_tokens or 0
|
||||
usage_info['output_tokens'] = resp.usage.completion_tokens or 0
|
||||
usage_info['total_tokens'] = resp.usage.total_tokens or 0
|
||||
|
||||
return message, usage_info
|
||||
|
||||
@@ -130,7 +130,7 @@ class ModelScopeChatCompletions(requester.ProviderAPIRequester):
|
||||
use_funcs: list[resource_tool.LLMTool] = None,
|
||||
extra_args: dict[str, typing.Any] = {},
|
||||
remove_think: bool = False,
|
||||
) -> provider_message.Message:
|
||||
) -> tuple[provider_message.Message, dict]:
|
||||
self.client.api_key = use_model.provider.token_mgr.get_token()
|
||||
|
||||
args = {}
|
||||
@@ -162,7 +162,10 @@ class ModelScopeChatCompletions(requester.ProviderAPIRequester):
|
||||
# 处理请求结果
|
||||
message = await self._make_msg(resp)
|
||||
|
||||
return message
|
||||
# ModelScope uses streaming, usage info not available
|
||||
usage_info = {}
|
||||
|
||||
return message, usage_info
|
||||
|
||||
async def _req_stream(
|
||||
self,
|
||||
|
||||
@@ -26,7 +26,7 @@ class MoonshotChatCompletions(chatcmpl.OpenAIChatCompletions):
|
||||
use_funcs: list[resource_tool.LLMTool] = None,
|
||||
extra_args: dict[str, typing.Any] = {},
|
||||
remove_think: bool = False,
|
||||
) -> provider_message.Message:
|
||||
) -> tuple[provider_message.Message, dict]:
|
||||
self.client.api_key = use_model.provider.token_mgr.get_token()
|
||||
|
||||
args = {}
|
||||
@@ -57,4 +57,11 @@ class MoonshotChatCompletions(chatcmpl.OpenAIChatCompletions):
|
||||
# 处理请求结果
|
||||
message = await self._make_msg(resp, remove_think)
|
||||
|
||||
return message
|
||||
# Extract token usage from response
|
||||
usage_info = {}
|
||||
if hasattr(resp, 'usage') and resp.usage:
|
||||
usage_info['input_tokens'] = resp.usage.prompt_tokens or 0
|
||||
usage_info['output_tokens'] = resp.usage.completion_tokens or 0
|
||||
usage_info['total_tokens'] = resp.usage.total_tokens or 0
|
||||
|
||||
return message, usage_info
|
||||
|
||||
@@ -51,9 +51,10 @@ class SeekDBEmbedding(requester.ProviderAPIRequester):
|
||||
await self.initialize()
|
||||
|
||||
if self._embedding_function is None:
|
||||
raise RuntimeError("SeekDB embedding function initialization failed")
|
||||
raise RuntimeError('SeekDB embedding function initialization failed')
|
||||
|
||||
return self._embedding_function(input_text)
|
||||
except Exception as e:
|
||||
from .. import errors
|
||||
|
||||
raise errors.RequesterError(f'SeekDB embedding failed: {str(e)}')
|
||||
|
||||
@@ -118,6 +118,7 @@ class DashScopeAPIRunner(runner.RequestRunner):
|
||||
stream=True, # 流式输出
|
||||
incremental_output=True, # 增量输出,使用流式输出需要开启增量输出
|
||||
session_id=query.session.using_conversation.uuid, # 会话ID用于,多轮对话
|
||||
enable_thinking=has_thoughts,
|
||||
has_thoughts=has_thoughts,
|
||||
# rag_options={ # 主要用于文件交互,暂不支持
|
||||
# "session_file_ids": ["FILE_ID1"], # FILE_ID1 替换为实际的临时文件ID,逗号隔开多个
|
||||
@@ -141,14 +142,14 @@ class DashScopeAPIRunner(runner.RequestRunner):
|
||||
# 获取流式传输的output
|
||||
stream_output = chunk.get('output', {})
|
||||
stream_think = stream_output.get('thoughts', [])
|
||||
if stream_think[0].get('thought'):
|
||||
if stream_think and stream_think[0].get('thought'):
|
||||
if not think_start:
|
||||
think_start = True
|
||||
pending_content += f'<think>\n{stream_think[0].get("thought")}'
|
||||
else:
|
||||
# 继续输出 reasoning_content
|
||||
pending_content += stream_think[0].get('thought')
|
||||
elif stream_think[0].get('thought') == '' and not think_end:
|
||||
elif (not stream_think or stream_think[0].get('thought') == '') and not think_end:
|
||||
think_end = True
|
||||
pending_content += '\n</think>\n'
|
||||
if stream_output.get('text') is not None:
|
||||
|
||||
@@ -289,12 +289,16 @@ class DifyServiceAPIRunner(runner.RequestRunner):
|
||||
yield msg
|
||||
if chunk['event'] == 'message_file':
|
||||
if chunk['type'] == 'image' and chunk['belongs_to'] == 'assistant':
|
||||
base_url = self.dify_client.base_url
|
||||
# 检查URL是否已经是完整的连接
|
||||
if chunk['url'].startswith('http://') or chunk['url'].startswith('https://'):
|
||||
image_url = chunk['url']
|
||||
else:
|
||||
base_url = self.dify_client.base_url
|
||||
|
||||
if base_url.endswith('/v1'):
|
||||
base_url = base_url[:-3]
|
||||
if base_url.endswith('/v1'):
|
||||
base_url = base_url[:-3]
|
||||
|
||||
image_url = base_url + chunk['url']
|
||||
image_url = base_url + chunk['url']
|
||||
|
||||
yield provider_message.Message(
|
||||
role='assistant',
|
||||
@@ -529,7 +533,7 @@ class DifyServiceAPIRunner(runner.RequestRunner):
|
||||
think_end = True
|
||||
elif think_end or not think_start:
|
||||
pending_agent_message += chunk['answer']
|
||||
if think_start:
|
||||
if think_start and not think_end:
|
||||
continue
|
||||
|
||||
else:
|
||||
@@ -559,12 +563,16 @@ class DifyServiceAPIRunner(runner.RequestRunner):
|
||||
if chunk['event'] == 'message_file':
|
||||
message_idx += 1
|
||||
if chunk['type'] == 'image' and chunk['belongs_to'] == 'assistant':
|
||||
base_url = self.dify_client.base_url
|
||||
# 检查URL是否已经是完整的连接
|
||||
if chunk['url'].startswith('http://') or chunk['url'].startswith('https://'):
|
||||
image_url = chunk['url']
|
||||
else:
|
||||
base_url = self.dify_client.base_url
|
||||
|
||||
if base_url.endswith('/v1'):
|
||||
base_url = base_url[:-3]
|
||||
if base_url.endswith('/v1'):
|
||||
base_url = base_url[:-3]
|
||||
|
||||
image_url = base_url + chunk['url']
|
||||
image_url = base_url + chunk['url']
|
||||
|
||||
yield provider_message.MessageChunk(
|
||||
role='assistant',
|
||||
|
||||
@@ -130,7 +130,7 @@ class LocalAgentRunner(runner.RequestRunner):
|
||||
if not is_stream:
|
||||
# 非流式输出,直接请求
|
||||
|
||||
msg = await use_llm_model.provider.requester.invoke_llm(
|
||||
msg = await use_llm_model.provider.invoke_llm(
|
||||
query,
|
||||
use_llm_model,
|
||||
req_messages,
|
||||
@@ -147,7 +147,7 @@ class LocalAgentRunner(runner.RequestRunner):
|
||||
accumulated_content = '' # 从开始累积的所有内容
|
||||
last_role = 'assistant'
|
||||
msg_sequence = 1
|
||||
async for msg in use_llm_model.provider.requester.invoke_llm_stream(
|
||||
async for msg in use_llm_model.provider.invoke_llm_stream(
|
||||
query,
|
||||
use_llm_model,
|
||||
req_messages,
|
||||
@@ -212,19 +212,34 @@ class LocalAgentRunner(runner.RequestRunner):
|
||||
try:
|
||||
func = tool_call.function
|
||||
|
||||
parameters = json.loads(func.arguments)
|
||||
if func.arguments:
|
||||
parameters = json.loads(func.arguments)
|
||||
else:
|
||||
parameters = {}
|
||||
|
||||
func_ret = await self.ap.tool_mgr.execute_func_call(func.name, parameters, query=query)
|
||||
|
||||
# Handle return value content
|
||||
tool_content = None
|
||||
if (
|
||||
isinstance(func_ret, list)
|
||||
and len(func_ret) > 0
|
||||
and isinstance(func_ret[0], provider_message.ContentElement)
|
||||
):
|
||||
tool_content = func_ret
|
||||
else:
|
||||
tool_content = json.dumps(func_ret, ensure_ascii=False)
|
||||
|
||||
if is_stream:
|
||||
msg = provider_message.MessageChunk(
|
||||
role='tool',
|
||||
content=json.dumps(func_ret, ensure_ascii=False),
|
||||
content=tool_content,
|
||||
tool_call_id=tool_call.id,
|
||||
)
|
||||
else:
|
||||
msg = provider_message.Message(
|
||||
role='tool',
|
||||
content=json.dumps(func_ret, ensure_ascii=False),
|
||||
content=tool_content,
|
||||
tool_call_id=tool_call.id,
|
||||
)
|
||||
|
||||
@@ -250,7 +265,7 @@ class LocalAgentRunner(runner.RequestRunner):
|
||||
last_role = 'assistant'
|
||||
msg_sequence = first_end_sequence
|
||||
|
||||
async for msg in use_llm_model.provider.requester.invoke_llm_stream(
|
||||
async for msg in use_llm_model.provider.invoke_llm_stream(
|
||||
query,
|
||||
use_llm_model,
|
||||
req_messages,
|
||||
@@ -306,7 +321,7 @@ class LocalAgentRunner(runner.RequestRunner):
|
||||
)
|
||||
else:
|
||||
# 处理完所有调用,再次请求
|
||||
msg = await use_llm_model.provider.requester.invoke_llm(
|
||||
msg = await use_llm_model.provider.invoke_llm(
|
||||
query,
|
||||
use_llm_model,
|
||||
req_messages,
|
||||
|
||||
@@ -68,15 +68,16 @@ class N8nServiceAPIRunner(runner.RequestRunner):
|
||||
|
||||
return plain_text
|
||||
|
||||
async def _process_stream_response(self, response: aiohttp.ClientResponse) -> typing.AsyncGenerator[
|
||||
provider_message.Message, None]:
|
||||
async def _process_stream_response(
|
||||
self, response: aiohttp.ClientResponse
|
||||
) -> typing.AsyncGenerator[provider_message.Message, None]:
|
||||
"""处理流式响应——支持部分 JSON 和多个 JSON 对象在同一 chunk 的情况"""
|
||||
full_content = ""
|
||||
full_content = ''
|
||||
chunk_idx = 0
|
||||
is_final = False
|
||||
message_idx = 0
|
||||
|
||||
buffer = ""
|
||||
buffer = ''
|
||||
decoder = json.JSONDecoder()
|
||||
|
||||
async for raw_chunk in response.content.iter_chunked(1024):
|
||||
@@ -129,7 +130,7 @@ class N8nServiceAPIRunner(runner.RequestRunner):
|
||||
preview = chunk_str[:200]
|
||||
except Exception:
|
||||
preview = '<unavailable>'
|
||||
self.ap.logger.warning(f"Failed to process chunk: {e}; chunk preview: {preview}")
|
||||
self.ap.logger.warning(f'Failed to process chunk: {e}; chunk preview: {preview}')
|
||||
|
||||
# 流结束后,尝试解析残余 buffer
|
||||
if buffer:
|
||||
@@ -151,7 +152,7 @@ class N8nServiceAPIRunner(runner.RequestRunner):
|
||||
)
|
||||
except Exception as e:
|
||||
preview = buffer[:200]
|
||||
self.ap.logger.warning(f"Failed to parse remaining buffer: {e}; buffer preview: {preview}")
|
||||
self.ap.logger.warning(f'Failed to parse remaining buffer: {e}; buffer preview: {preview}')
|
||||
|
||||
async def _call_webhook(self, query: pipeline_query.Query) -> typing.AsyncGenerator[provider_message.Message, None]:
|
||||
"""调用n8n webhook"""
|
||||
@@ -165,7 +166,7 @@ class N8nServiceAPIRunner(runner.RequestRunner):
|
||||
# 准备请求数据
|
||||
payload = {
|
||||
# 基本消息内容
|
||||
'chatInput' :plain_text, # 考虑到之前用户直接用的message model这里添加新键
|
||||
'chatInput': plain_text, # 考虑到之前用户直接用的message model这里添加新键
|
||||
'message': plain_text,
|
||||
'user_message_text': plain_text,
|
||||
'conversation_id': query.session.using_conversation.uuid,
|
||||
@@ -217,57 +218,49 @@ class N8nServiceAPIRunner(runner.RequestRunner):
|
||||
|
||||
# 调用webhook
|
||||
async with aiohttp.ClientSession() as session:
|
||||
if is_stream:
|
||||
# 流式请求
|
||||
async with session.post(
|
||||
self.webhook_url,
|
||||
json=payload,
|
||||
headers=headers,
|
||||
auth=auth,
|
||||
timeout=self.timeout
|
||||
) as response:
|
||||
if is_stream:
|
||||
# 流式请求
|
||||
async with session.post(
|
||||
self.webhook_url, json=payload, headers=headers, auth=auth, timeout=self.timeout
|
||||
) as response:
|
||||
if response.status != 200:
|
||||
error_text = await response.text()
|
||||
self.ap.logger.error(f'n8n webhook call failed: {response.status}, {error_text}')
|
||||
raise Exception(f'n8n webhook call failed: {response.status}, {error_text}')
|
||||
|
||||
# 处理流式响应
|
||||
async for chunk in self._process_stream_response(response):
|
||||
yield chunk
|
||||
else:
|
||||
async with session.post(
|
||||
self.webhook_url, json=payload, headers=headers, auth=auth, timeout=self.timeout
|
||||
) as response:
|
||||
try:
|
||||
async for chunk in self._process_stream_response(response):
|
||||
output_content = chunk.content if chunk.is_final else ''
|
||||
except:
|
||||
# 非流式请求(保持原有逻辑)
|
||||
if response.status != 200:
|
||||
error_text = await response.text()
|
||||
self.ap.logger.error(f'n8n webhook call failed: {response.status}, {error_text}')
|
||||
raise Exception(f'n8n webhook call failed: {response.status}, {error_text}')
|
||||
|
||||
# 处理流式响应
|
||||
async for chunk in self._process_stream_response(response):
|
||||
yield chunk
|
||||
else:
|
||||
async with session.post(
|
||||
self.webhook_url,
|
||||
json=payload,
|
||||
headers=headers,
|
||||
auth=auth,
|
||||
timeout=self.timeout
|
||||
) as response:
|
||||
try:
|
||||
async for chunk in self._process_stream_response(response):
|
||||
output_content = chunk.content if chunk.is_final else ''
|
||||
except:
|
||||
# 非流式请求(保持原有逻辑)
|
||||
if response.status != 200:
|
||||
error_text = await response.text()
|
||||
self.ap.logger.error(f'n8n webhook call failed: {response.status}, {error_text}')
|
||||
raise Exception(f'n8n webhook call failed: {response.status}, {error_text}')
|
||||
# 解析响应
|
||||
response_data = await response.json()
|
||||
self.ap.logger.debug(f'n8n webhook response: {response_data}')
|
||||
|
||||
# 解析响应
|
||||
response_data = await response.json()
|
||||
self.ap.logger.debug(f'n8n webhook response: {response_data}')
|
||||
# 从响应中提取输出
|
||||
if self.output_key in response_data:
|
||||
output_content = response_data[self.output_key]
|
||||
else:
|
||||
# 如果没有指定的输出键,则使用整个响应
|
||||
output_content = json.dumps(response_data, ensure_ascii=False)
|
||||
|
||||
# 从响应中提取输出
|
||||
if self.output_key in response_data:
|
||||
output_content = response_data[self.output_key]
|
||||
else:
|
||||
# 如果没有指定的输出键,则使用整个响应
|
||||
output_content = json.dumps(response_data, ensure_ascii=False)
|
||||
|
||||
# 返回消息
|
||||
yield provider_message.Message(
|
||||
role='assistant',
|
||||
content=output_content,
|
||||
)
|
||||
# 返回消息
|
||||
yield provider_message.Message(
|
||||
role='assistant',
|
||||
content=output_content,
|
||||
)
|
||||
except Exception as e:
|
||||
self.ap.logger.error(f'n8n webhook call exception: {str(e)}')
|
||||
raise N8nAPIError(f'n8n webhook call exception: {str(e)}')
|
||||
@@ -275,4 +268,4 @@ class N8nServiceAPIRunner(runner.RequestRunner):
|
||||
async def run(self, query: pipeline_query.Query) -> typing.AsyncGenerator[provider_message.Message, None]:
|
||||
"""运行请求"""
|
||||
async for msg in self._call_webhook(query):
|
||||
yield msg
|
||||
yield msg
|
||||
|
||||
@@ -7,14 +7,18 @@ import traceback
|
||||
from langbot_plugin.api.entities.events import pipeline_query
|
||||
import sqlalchemy
|
||||
import asyncio
|
||||
import httpx
|
||||
|
||||
import uuid as uuid_module
|
||||
from mcp import ClientSession, StdioServerParameters
|
||||
from mcp.client.stdio import stdio_client
|
||||
from mcp.client.sse import sse_client
|
||||
from mcp.client.streamable_http import streamable_http_client
|
||||
|
||||
from .. import loader
|
||||
from ....core import app
|
||||
import langbot_plugin.api.entities.builtin.resource.tool as resource_tool
|
||||
import langbot_plugin.api.entities.builtin.provider.message as provider_message
|
||||
from ....entity.persistence import mcp as persistence_mcp
|
||||
|
||||
|
||||
@@ -35,7 +39,7 @@ class RuntimeMCPSession:
|
||||
|
||||
server_config: dict
|
||||
|
||||
session: ClientSession
|
||||
session: ClientSession | None
|
||||
|
||||
exit_stack: AsyncExitStack
|
||||
|
||||
@@ -52,6 +56,8 @@ class RuntimeMCPSession:
|
||||
|
||||
_ready_event: asyncio.Event
|
||||
|
||||
error_message: str | None = None
|
||||
|
||||
def __init__(self, server_name: str, server_config: dict, enable: bool, ap: app.Application):
|
||||
self.server_name = server_name
|
||||
self.server_uuid = server_config.get('uuid', '')
|
||||
@@ -100,6 +106,24 @@ class RuntimeMCPSession:
|
||||
|
||||
await self.session.initialize()
|
||||
|
||||
async def _init_streamable_http_server(self):
|
||||
transport = await self.exit_stack.enter_async_context(
|
||||
streamable_http_client(
|
||||
self.server_config['url'],
|
||||
http_client=httpx.AsyncClient(
|
||||
headers=self.server_config.get('headers', {}),
|
||||
timeout=self.server_config.get('timeout', 10),
|
||||
follow_redirects=True,
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
read, write, _ = transport
|
||||
|
||||
self.session = await self.exit_stack.enter_async_context(ClientSession(read, write))
|
||||
|
||||
await self.session.initialize()
|
||||
|
||||
async def _lifecycle_loop(self):
|
||||
"""在后台任务中管理整个MCP会话的生命周期"""
|
||||
try:
|
||||
@@ -107,6 +131,8 @@ class RuntimeMCPSession:
|
||||
await self._init_stdio_python_server()
|
||||
elif self.server_config['mode'] == 'sse':
|
||||
await self._init_sse_server()
|
||||
elif self.server_config['mode'] == 'http':
|
||||
await self._init_streamable_http_server()
|
||||
else:
|
||||
raise ValueError(f'无法识别 MCP 服务器类型: {self.server_name}: {self.server_config}')
|
||||
|
||||
@@ -122,6 +148,7 @@ class RuntimeMCPSession:
|
||||
|
||||
except Exception as e:
|
||||
self.status = MCPSessionStatus.ERROR
|
||||
self.error_message = str(e)
|
||||
self.ap.logger.error(f'Error in MCP session lifecycle {self.server_name}: {e}\n{traceback.format_exc()}')
|
||||
# 即使出错也要设置ready事件,让start()方法知道初始化已完成
|
||||
self._ready_event.set()
|
||||
@@ -154,6 +181,9 @@ class RuntimeMCPSession:
|
||||
raise Exception('Connection failed, please check URL')
|
||||
|
||||
async def refresh(self):
|
||||
if not self.session:
|
||||
return
|
||||
|
||||
self.functions.clear()
|
||||
|
||||
tools = await self.session.list_tools()
|
||||
@@ -163,18 +193,36 @@ class RuntimeMCPSession:
|
||||
for tool in tools.tools:
|
||||
|
||||
async def func(*, _tool=tool, **kwargs):
|
||||
if not self.session:
|
||||
raise Exception('MCP session is not connected')
|
||||
|
||||
result = await self.session.call_tool(_tool.name, kwargs)
|
||||
if result.isError:
|
||||
raise Exception(result.content[0].text)
|
||||
return result.content[0].text
|
||||
error_texts = []
|
||||
for content in result.content:
|
||||
if content.type == 'text':
|
||||
error_texts.append(content.text)
|
||||
raise Exception('\n'.join(error_texts) if error_texts else 'Unknown error from MCP tool')
|
||||
|
||||
result_contents: list[provider_message.ContentElement] = []
|
||||
for content in result.content:
|
||||
if content.type == 'text':
|
||||
result_contents.append(provider_message.ContentElement.from_text(content.text))
|
||||
elif content.type == 'image':
|
||||
result_contents.append(provider_message.ContentElement.from_image_base64(content.image_base64))
|
||||
elif content.type == 'resource':
|
||||
# TODO: Handle resource content
|
||||
pass
|
||||
|
||||
return result_contents
|
||||
|
||||
func.__name__ = tool.name
|
||||
|
||||
self.functions.append(
|
||||
resource_tool.LLMTool(
|
||||
name=tool.name,
|
||||
human_desc=tool.description,
|
||||
description=tool.description,
|
||||
human_desc=tool.description or '',
|
||||
description=tool.description or '',
|
||||
parameters=tool.inputSchema,
|
||||
func=func,
|
||||
)
|
||||
@@ -186,6 +234,7 @@ class RuntimeMCPSession:
|
||||
def get_runtime_info_dict(self) -> dict:
|
||||
return {
|
||||
'status': self.status.value,
|
||||
'error_message': self.error_message,
|
||||
'tool_count': len(self.get_tools()),
|
||||
'tools': [
|
||||
{
|
||||
@@ -287,6 +336,11 @@ class MCPLoader(loader.ToolLoader):
|
||||
- enable: 是否启用
|
||||
- extra_args: 额外的配置参数 (可选)
|
||||
"""
|
||||
uuid_ = server_config.get('uuid')
|
||||
if not uuid_:
|
||||
self.ap.logger.warning('Server UUID is None for MCP server, maybe testing in the config page.')
|
||||
uuid_ = str(uuid_module.uuid4())
|
||||
server_config['uuid'] = uuid_
|
||||
|
||||
name = server_config['name']
|
||||
uuid = server_config['uuid']
|
||||
|
||||
@@ -32,12 +32,20 @@ class Embedder(BaseService):
|
||||
|
||||
await self.ap.persistence_mgr.execute_async(sqlalchemy.insert(persistence_rag.Chunk).values(chunk_dicts))
|
||||
|
||||
# get embeddings
|
||||
embeddings_list: list[list[float]] = await embedding_model.provider.requester.invoke_embedding(
|
||||
model=embedding_model,
|
||||
input_text=chunks,
|
||||
extra_args={}, # TODO: add extra args
|
||||
)
|
||||
# get embeddings (batch size limit: 64 for OpenAI)
|
||||
MAX_BATCH_SIZE = 64
|
||||
embeddings_list: list[list[float]] = []
|
||||
|
||||
for i in range(0, len(chunks), MAX_BATCH_SIZE):
|
||||
batch = chunks[i : i + MAX_BATCH_SIZE]
|
||||
batch_embeddings = await embedding_model.provider.invoke_embedding(
|
||||
model=embedding_model,
|
||||
input_text=batch,
|
||||
extra_args={}, # TODO: add extra args
|
||||
knowledge_base_id=kb_id,
|
||||
call_type='embedding',
|
||||
)
|
||||
embeddings_list.extend(batch_embeddings)
|
||||
|
||||
# save embeddings to vdb
|
||||
await self.ap.vector_db_mgr.vector_db.add_embeddings(kb_id, chunk_ids, embeddings_list, chunk_dicts)
|
||||
|
||||
@@ -19,10 +19,13 @@ class Retriever(base_service.BaseService):
|
||||
f"Retrieving for query: '{query[:10]}' with k={k} using {embedding_model.model_entity.uuid}"
|
||||
)
|
||||
|
||||
query_embedding: list[float] = await embedding_model.provider.requester.invoke_embedding(
|
||||
query_embedding: list[float] = await embedding_model.provider.invoke_embedding(
|
||||
model=embedding_model,
|
||||
input_text=[query],
|
||||
extra_args={}, # TODO: add extra args
|
||||
knowledge_base_id=kb_id,
|
||||
query_text=query,
|
||||
call_type='retrieve',
|
||||
)
|
||||
|
||||
vector_results = await self.ap.vector_db_mgr.vector_db.search(kb_id, query_embedding[0], k)
|
||||
|
||||
0
src/langbot/pkg/telemetry/__init__.py
Normal file
0
src/langbot/pkg/telemetry/__init__.py
Normal file
121
src/langbot/pkg/telemetry/telemetry.py
Normal file
121
src/langbot/pkg/telemetry/telemetry.py
Normal file
@@ -0,0 +1,121 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import httpx
|
||||
from ..core import app as core_app
|
||||
|
||||
|
||||
class TelemetryManager:
|
||||
"""TelemetryManager handles sending telemetry for a given application instance.
|
||||
|
||||
Usage:
|
||||
telemetry = TelemetryManager(ap)
|
||||
await telemetry.send({ ... })
|
||||
"""
|
||||
|
||||
send_tasks: list[asyncio.Task] = []
|
||||
|
||||
def __init__(self, ap: core_app.Application):
|
||||
self.ap = ap
|
||||
|
||||
self.telemetry_config = {}
|
||||
|
||||
async def initialize(self):
|
||||
self.telemetry_config = self.ap.instance_config.data.get('space', {})
|
||||
|
||||
async def start_send_task(self, payload: dict):
|
||||
task = asyncio.create_task(self.send(payload))
|
||||
self.send_tasks.append(task)
|
||||
|
||||
async def send(self, payload: dict):
|
||||
"""Send telemetry payload to configured telemetry server (non-blocking).
|
||||
|
||||
Expects ap.instance_config.data.telemetry to have:
|
||||
- enabled: bool
|
||||
- server: str (base URL, e.g. https://space.example.com)
|
||||
- timeout_seconds: optional int, overall request timeout (default 10)
|
||||
|
||||
Posts to {server.rstrip('/')}/api/v1/telemetry as JSON. Failures are logged but do not raise.
|
||||
"""
|
||||
|
||||
try:
|
||||
cfg = self.telemetry_config
|
||||
if not cfg:
|
||||
return
|
||||
if cfg.get('disable_telemetry', False):
|
||||
return
|
||||
server = cfg.get('url', '')
|
||||
if not server:
|
||||
return
|
||||
|
||||
# Normalize URL
|
||||
url = server.rstrip('/') + '/api/v1/telemetry'
|
||||
|
||||
try:
|
||||
# Sanitize payload so string fields are strings and not nulls
|
||||
sanitized = dict(payload)
|
||||
if 'query_id' in sanitized:
|
||||
try:
|
||||
sanitized['query_id'] = '' if sanitized['query_id'] is None else str(sanitized['query_id'])
|
||||
except Exception:
|
||||
sanitized['query_id'] = str(sanitized.get('query_id', ''))
|
||||
|
||||
for sfield in ('adapter', 'runner', 'model_name', 'version', 'error', 'timestamp'):
|
||||
v = sanitized.get(sfield)
|
||||
sanitized[sfield] = '' if v is None else str(v)
|
||||
|
||||
if 'duration_ms' in sanitized:
|
||||
try:
|
||||
sanitized['duration_ms'] = (
|
||||
int(sanitized['duration_ms']) if sanitized['duration_ms'] is not None else 0
|
||||
)
|
||||
except Exception:
|
||||
sanitized['duration_ms'] = 0
|
||||
|
||||
async with httpx.AsyncClient(timeout=httpx.Timeout(10)) as client:
|
||||
try:
|
||||
# Use asyncio.wait_for to ensure we always bound the total time
|
||||
resp = await asyncio.wait_for(client.post(url, json=sanitized), timeout=10 + 1)
|
||||
|
||||
if resp.status_code >= 400:
|
||||
self.ap.logger.warning(
|
||||
f'Telemetry post to {url} returned status {resp.status_code} - {resp.text}'
|
||||
)
|
||||
else:
|
||||
# Detect application-level errors inside HTTP 200 responses
|
||||
app_err = False
|
||||
try:
|
||||
j = resp.json()
|
||||
if isinstance(j, dict) and j.get('code') is not None and int(j.get('code')) >= 400:
|
||||
app_err = True
|
||||
self.ap.logger.warning(
|
||||
f'Telemetry post to {url} returned application error code {j.get("code")} - {j.get("msg")}'
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if app_err:
|
||||
self.ap.logger.warning(
|
||||
f'Telemetry post to {url} returned app-level error - response: {resp.text[:200]}'
|
||||
)
|
||||
else:
|
||||
self.ap.logger.debug(
|
||||
f'Telemetry posted to {url}, status {resp.status_code} - response: {resp.text[:200]}'
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
self.ap.logger.warning(f'Telemetry post to {url} timed out')
|
||||
except Exception as e:
|
||||
self.ap.logger.warning(f'Failed to post telemetry to {url}: {e}', exc_info=True)
|
||||
except Exception as e:
|
||||
try:
|
||||
self.ap.logger.warning(
|
||||
f'Failed to create HTTP client for telemetry or sanitize payload: {e}', exc_info=True
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
except Exception as e:
|
||||
# Never raise from telemetry; surface as warning for visibility
|
||||
try:
|
||||
self.ap.logger.warning(f'Unexpected telemetry error: {e}', exc_info=True)
|
||||
except Exception:
|
||||
pass
|
||||
@@ -2,7 +2,7 @@ import langbot
|
||||
|
||||
semantic_version = f'v{langbot.__version__}'
|
||||
|
||||
required_database_version = 17
|
||||
required_database_version = 18
|
||||
"""Tag the version of the database schema, used to check if the database needs to be migrated"""
|
||||
|
||||
debug_mode = False
|
||||
|
||||
@@ -37,7 +37,8 @@ class VectorDBManager:
|
||||
milvus_config = kb_config.get('milvus', {})
|
||||
uri = milvus_config.get('uri', './data/milvus.db')
|
||||
token = milvus_config.get('token')
|
||||
self.vector_db = MilvusVectorDatabase(self.ap, uri=uri, token=token)
|
||||
db_name = milvus_config.get('db_name', 'default')
|
||||
self.vector_db = MilvusVectorDatabase(self.ap, uri=uri, token=token, db_name=db_name)
|
||||
self.ap.logger.info('Initialized Milvus vector database backend.')
|
||||
|
||||
elif vdb_type == 'pgvector':
|
||||
@@ -54,12 +55,7 @@ class VectorDBManager:
|
||||
user = pgvector_config.get('user', 'postgres')
|
||||
password = pgvector_config.get('password', 'postgres')
|
||||
self.vector_db = PgVectorDatabase(
|
||||
self.ap,
|
||||
host=host,
|
||||
port=port,
|
||||
database=database,
|
||||
user=user,
|
||||
password=password
|
||||
self.ap, host=host, port=port, database=database, user=user, password=password
|
||||
)
|
||||
self.ap.logger.info('Initialized pgvector database backend.')
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
from __future__ import annotations
|
||||
import asyncio
|
||||
from typing import Any, Dict
|
||||
from pymilvus import MilvusClient, DataType
|
||||
from pymilvus import MilvusClient, DataType, CollectionSchema, FieldSchema
|
||||
from pymilvus.milvus_client.index import IndexParams
|
||||
from langbot.pkg.vector.vdb import VectorDatabase
|
||||
from langbot.pkg.core import app
|
||||
|
||||
@@ -9,7 +10,7 @@ from langbot.pkg.core import app
|
||||
class MilvusVectorDatabase(VectorDatabase):
|
||||
"""Milvus vector database implementation"""
|
||||
|
||||
def __init__(self, ap: app.Application, uri: str = "milvus.db", token: str = None):
|
||||
def __init__(self, ap: app.Application, uri: str = 'milvus.db', token: str = None, db_name: str = None):
|
||||
"""Initialize Milvus vector database
|
||||
|
||||
Args:
|
||||
@@ -21,77 +22,133 @@ class MilvusVectorDatabase(VectorDatabase):
|
||||
self.ap = ap
|
||||
self.uri = uri
|
||||
self.token = token
|
||||
self.db_name = db_name
|
||||
self.client = None
|
||||
self._collections = {}
|
||||
self._collections: set[str] = set()
|
||||
self._initialize_client()
|
||||
|
||||
def _initialize_client(self):
|
||||
"""Initialize Milvus client connection"""
|
||||
try:
|
||||
if self.token:
|
||||
self.client = MilvusClient(uri=self.uri, token=self.token)
|
||||
self.client = MilvusClient(uri=self.uri, token=self.token, db_name=self.db_name)
|
||||
else:
|
||||
self.client = MilvusClient(uri=self.uri)
|
||||
self.ap.logger.info(f"Connected to Milvus at {self.uri}")
|
||||
self.client = MilvusClient(uri=self.uri, db_name=self.db_name)
|
||||
self.ap.logger.info(f'Connected to Milvus at {self.uri}')
|
||||
except Exception as e:
|
||||
self.ap.logger.error(f"Failed to connect to Milvus: {e}")
|
||||
self.ap.logger.error(f'Failed to connect to Milvus: {e}')
|
||||
raise
|
||||
|
||||
async def get_or_create_collection(self, collection: str):
|
||||
"""Get or create a Milvus collection
|
||||
@staticmethod
|
||||
def _normalize_collection_name(collection: str) -> str:
|
||||
"""Normalize collection name to comply with Milvus naming requirements.
|
||||
|
||||
Milvus requirements:
|
||||
- First character must be an underscore or letter
|
||||
- Can only contain numbers, letters and underscores
|
||||
|
||||
Args:
|
||||
collection: Original collection name (e.g., UUID with hyphens)
|
||||
|
||||
Returns:
|
||||
Normalized collection name that complies with Milvus requirements
|
||||
"""
|
||||
# Replace hyphens with underscores
|
||||
normalized = collection.replace('-', '_')
|
||||
|
||||
# If first character is not a letter or underscore, prepend 'kb_'
|
||||
if normalized and not (normalized[0].isalpha() or normalized[0] == '_'):
|
||||
normalized = 'kb_' + normalized
|
||||
|
||||
return normalized
|
||||
|
||||
async def _ensure_vector_index(self, collection: str) -> None:
|
||||
"""Ensure the vector field has an index.
|
||||
|
||||
Args:
|
||||
collection: Normalized collection name
|
||||
"""
|
||||
index_params = IndexParams()
|
||||
index_params.add_index(
|
||||
field_name='vector',
|
||||
index_type='AUTOINDEX',
|
||||
metric_type='COSINE',
|
||||
)
|
||||
await asyncio.to_thread(self.client.create_index, collection_name=collection, index_params=index_params)
|
||||
|
||||
async def _get_or_create_collection_internal(self, collection: str, vector_size: int = None):
|
||||
"""Internal method to get or create a Milvus collection with proper configuration.
|
||||
|
||||
Args:
|
||||
collection: Collection name (corresponds to knowledge base UUID)
|
||||
vector_size: Dimension of the vectors (if None, defaults to 1536)
|
||||
"""
|
||||
# Normalize collection name for Milvus compatibility
|
||||
collection = self._normalize_collection_name(collection)
|
||||
|
||||
if collection in self._collections:
|
||||
return self._collections[collection]
|
||||
return collection
|
||||
|
||||
# Check if collection exists
|
||||
has_collection = await asyncio.to_thread(
|
||||
self.client.has_collection, collection_name=collection
|
||||
)
|
||||
has_collection = await asyncio.to_thread(self.client.has_collection, collection_name=collection)
|
||||
|
||||
if not has_collection:
|
||||
# Create collection with custom schema to support string IDs
|
||||
from pymilvus import CollectionSchema, FieldSchema, DataType
|
||||
# Default dimension if not specified (for backward compatibility)
|
||||
if vector_size is None:
|
||||
vector_size = 1536
|
||||
|
||||
fields = [
|
||||
FieldSchema(name="id", dtype=DataType.VARCHAR, is_primary=True, max_length=255),
|
||||
FieldSchema(name="vector", dtype=DataType.FLOAT_VECTOR, dim=1536),
|
||||
FieldSchema(name="text", dtype=DataType.VARCHAR, max_length=65535),
|
||||
FieldSchema(name="file_id", dtype=DataType.VARCHAR, max_length=255),
|
||||
FieldSchema(name="chunk_uuid", dtype=DataType.VARCHAR, max_length=255),
|
||||
FieldSchema(name='id', dtype=DataType.VARCHAR, is_primary=True, max_length=255),
|
||||
FieldSchema(name='vector', dtype=DataType.FLOAT_VECTOR, dim=vector_size),
|
||||
FieldSchema(name='text', dtype=DataType.VARCHAR, max_length=65535),
|
||||
FieldSchema(name='file_id', dtype=DataType.VARCHAR, max_length=255),
|
||||
FieldSchema(name='chunk_uuid', dtype=DataType.VARCHAR, max_length=255),
|
||||
]
|
||||
|
||||
schema = CollectionSchema(fields=fields, description="LangBot knowledge base vectors")
|
||||
schema = CollectionSchema(fields=fields, description='LangBot knowledge base vectors')
|
||||
|
||||
await asyncio.to_thread(
|
||||
self.client.create_collection,
|
||||
collection_name=collection,
|
||||
schema=schema,
|
||||
metric_type="COSINE",
|
||||
metric_type='COSINE',
|
||||
)
|
||||
|
||||
# Create index for vector field (required for loading/searching)
|
||||
index_params = {
|
||||
"metric_type": "COSINE",
|
||||
"index_type": "AUTOINDEX",
|
||||
"params": {}
|
||||
}
|
||||
await asyncio.to_thread(
|
||||
self.client.create_index,
|
||||
collection_name=collection,
|
||||
field_name="vector",
|
||||
index_params=index_params
|
||||
await self._ensure_vector_index(collection)
|
||||
self.ap.logger.info(
|
||||
f"Created Milvus collection '{collection}' with dimension={vector_size}, index=AUTOINDEX"
|
||||
)
|
||||
|
||||
self.ap.logger.info(f"Created Milvus collection '{collection}' with index")
|
||||
else:
|
||||
# Ensure index exists for existing collection
|
||||
await self._ensure_index_if_missing(collection)
|
||||
self.ap.logger.info(f"Milvus collection '{collection}' already exists")
|
||||
|
||||
self._collections[collection] = collection
|
||||
self._collections.add(collection)
|
||||
return collection
|
||||
|
||||
async def _ensure_index_if_missing(self, collection: str) -> None:
|
||||
"""Check if index exists for collection and create if missing.
|
||||
|
||||
Args:
|
||||
collection: Normalized collection name
|
||||
"""
|
||||
try:
|
||||
indexes = await asyncio.to_thread(self.client.list_indexes, collection_name=collection)
|
||||
if 'vector' not in indexes:
|
||||
await self._ensure_vector_index(collection)
|
||||
self.ap.logger.info(f"Created index for existing Milvus collection '{collection}'")
|
||||
except Exception as e:
|
||||
self.ap.logger.warning(f"Could not verify/create index for collection '{collection}': {e}")
|
||||
|
||||
async def get_or_create_collection(self, collection: str):
|
||||
"""Get or create a Milvus collection (without vector size - will use default).
|
||||
|
||||
Args:
|
||||
collection: Collection name (corresponds to knowledge base UUID)
|
||||
"""
|
||||
collection = self._normalize_collection_name(collection)
|
||||
return await self._get_or_create_collection_internal(collection)
|
||||
|
||||
async def add_embeddings(
|
||||
self,
|
||||
collection: str,
|
||||
@@ -107,45 +164,43 @@ class MilvusVectorDatabase(VectorDatabase):
|
||||
embeddings_list: List of embedding vectors
|
||||
metadatas: List of metadata dictionaries for each vector
|
||||
"""
|
||||
await self.get_or_create_collection(collection)
|
||||
collection = self._normalize_collection_name(collection)
|
||||
|
||||
if not embeddings_list:
|
||||
return
|
||||
|
||||
# Ensure collection exists with correct dimension
|
||||
vector_size = len(embeddings_list[0])
|
||||
await self._get_or_create_collection_internal(collection, vector_size)
|
||||
|
||||
# Prepare data in Milvus format
|
||||
data = []
|
||||
for i, vector_id in enumerate(ids):
|
||||
entry = {
|
||||
"id": vector_id,
|
||||
"vector": embeddings_list[i],
|
||||
'id': vector_id,
|
||||
'vector': embeddings_list[i],
|
||||
}
|
||||
# Add metadata fields
|
||||
if metadatas and i < len(metadatas):
|
||||
metadata = metadatas[i]
|
||||
# Add common metadata fields
|
||||
if "text" in metadata:
|
||||
entry["text"] = metadata["text"]
|
||||
if "file_id" in metadata:
|
||||
entry["file_id"] = metadata["file_id"]
|
||||
if "uuid" in metadata:
|
||||
entry["chunk_uuid"] = metadata["uuid"]
|
||||
if 'text' in metadata:
|
||||
entry['text'] = metadata['text']
|
||||
if 'file_id' in metadata:
|
||||
entry['file_id'] = metadata['file_id']
|
||||
if 'uuid' in metadata:
|
||||
entry['chunk_uuid'] = metadata['uuid']
|
||||
data.append(entry)
|
||||
|
||||
# Insert data into Milvus
|
||||
await asyncio.to_thread(
|
||||
self.client.insert,
|
||||
collection_name=collection,
|
||||
data=data
|
||||
)
|
||||
await asyncio.to_thread(self.client.insert, collection_name=collection, data=data)
|
||||
|
||||
# Load collection for searching (Milvus requires this)
|
||||
await asyncio.to_thread(
|
||||
self.client.load_collection,
|
||||
collection_name=collection
|
||||
)
|
||||
await asyncio.to_thread(self.client.load_collection, collection_name=collection)
|
||||
|
||||
self.ap.logger.info(f"Added {len(ids)} embeddings to Milvus collection '{collection}'")
|
||||
|
||||
async def search(
|
||||
self, collection: str, query_embedding: list[float], k: int = 5
|
||||
) -> Dict[str, Any]:
|
||||
async def search(self, collection: str, query_embedding: list[float], k: int = 5) -> Dict[str, Any]:
|
||||
"""Search for similar vectors in Milvus collection
|
||||
|
||||
Args:
|
||||
@@ -156,13 +211,11 @@ class MilvusVectorDatabase(VectorDatabase):
|
||||
Returns:
|
||||
Dictionary with search results in Chroma-compatible format
|
||||
"""
|
||||
collection = self._normalize_collection_name(collection)
|
||||
await self.get_or_create_collection(collection)
|
||||
|
||||
# Perform search
|
||||
search_params = {
|
||||
"metric_type": "COSINE",
|
||||
"params": {}
|
||||
}
|
||||
search_params = {'metric_type': 'COSINE', 'params': {}}
|
||||
|
||||
results = await asyncio.to_thread(
|
||||
self.client.search,
|
||||
@@ -170,7 +223,7 @@ class MilvusVectorDatabase(VectorDatabase):
|
||||
data=[query_embedding],
|
||||
limit=k,
|
||||
search_params=search_params,
|
||||
output_fields=["text", "file_id", "chunk_uuid"]
|
||||
output_fields=['text', 'file_id', 'chunk_uuid'],
|
||||
)
|
||||
|
||||
# Convert results to Chroma-compatible format
|
||||
@@ -181,30 +234,24 @@ class MilvusVectorDatabase(VectorDatabase):
|
||||
|
||||
if results and len(results) > 0:
|
||||
for hit in results[0]:
|
||||
ids.append(hit.get("id", ""))
|
||||
distances.append(hit.get("distance", 0.0))
|
||||
ids.append(hit.get('id', ''))
|
||||
distances.append(hit.get('distance', 0.0))
|
||||
|
||||
# Build metadata from entity fields
|
||||
entity = hit.get("entity", {})
|
||||
entity = hit.get('entity', {})
|
||||
metadata = {}
|
||||
if "text" in entity:
|
||||
metadata["text"] = entity["text"]
|
||||
if "file_id" in entity:
|
||||
metadata["file_id"] = entity["file_id"]
|
||||
if "chunk_uuid" in entity:
|
||||
metadata["uuid"] = entity["chunk_uuid"]
|
||||
if 'text' in entity:
|
||||
metadata['text'] = entity['text']
|
||||
if 'file_id' in entity:
|
||||
metadata['file_id'] = entity['file_id']
|
||||
if 'chunk_uuid' in entity:
|
||||
metadata['uuid'] = entity['chunk_uuid']
|
||||
metadatas.append(metadata)
|
||||
|
||||
# Return in Chroma-compatible format (nested lists)
|
||||
result = {
|
||||
"ids": [ids],
|
||||
"distances": [distances],
|
||||
"metadatas": [metadatas]
|
||||
}
|
||||
result = {'ids': [ids], 'distances': [distances], 'metadatas': [metadatas]}
|
||||
|
||||
self.ap.logger.info(
|
||||
f"Milvus search in '{collection}' returned {len(ids)} results"
|
||||
)
|
||||
self.ap.logger.info(f"Milvus search in '{collection}' returned {len(ids)} results")
|
||||
return result
|
||||
|
||||
async def delete_by_file_id(self, collection: str, file_id: str) -> None:
|
||||
@@ -214,17 +261,12 @@ class MilvusVectorDatabase(VectorDatabase):
|
||||
collection: Collection name
|
||||
file_id: File ID to filter deletion
|
||||
"""
|
||||
collection = self._normalize_collection_name(collection)
|
||||
await self.get_or_create_collection(collection)
|
||||
|
||||
# Delete entities matching the file_id
|
||||
await asyncio.to_thread(
|
||||
self.client.delete,
|
||||
collection_name=collection,
|
||||
filter=f'file_id == "{file_id}"'
|
||||
)
|
||||
self.ap.logger.info(
|
||||
f"Deleted embeddings from Milvus collection '{collection}' with file_id: {file_id}"
|
||||
)
|
||||
await asyncio.to_thread(self.client.delete, collection_name=collection, filter=f'file_id == "{file_id}"')
|
||||
self.ap.logger.info(f"Deleted embeddings from Milvus collection '{collection}' with file_id: {file_id}")
|
||||
|
||||
async def delete_collection(self, collection: str):
|
||||
"""Delete a Milvus collection
|
||||
@@ -232,18 +274,15 @@ class MilvusVectorDatabase(VectorDatabase):
|
||||
Args:
|
||||
collection: Collection name to delete
|
||||
"""
|
||||
if collection in self._collections:
|
||||
del self._collections[collection]
|
||||
collection = self._normalize_collection_name(collection)
|
||||
|
||||
self._collections.discard(collection)
|
||||
|
||||
# Check if collection exists before attempting deletion
|
||||
has_collection = await asyncio.to_thread(
|
||||
self.client.has_collection, collection_name=collection
|
||||
)
|
||||
has_collection = await asyncio.to_thread(self.client.has_collection, collection_name=collection)
|
||||
|
||||
if has_collection:
|
||||
await asyncio.to_thread(
|
||||
self.client.drop_collection, collection_name=collection
|
||||
)
|
||||
await asyncio.to_thread(self.client.drop_collection, collection_name=collection)
|
||||
self.ap.logger.info(f"Deleted Milvus collection '{collection}'")
|
||||
else:
|
||||
self.ap.logger.warning(f"Milvus collection '{collection}' not found")
|
||||
|
||||
@@ -1,19 +1,18 @@
|
||||
from __future__ import annotations
|
||||
import asyncio
|
||||
from typing import Any, Dict
|
||||
from sqlalchemy import create_engine, text, Column, String, Text
|
||||
from sqlalchemy.orm import declarative_base, sessionmaker, Session
|
||||
from sqlalchemy.orm import declarative_base
|
||||
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
|
||||
from pgvector.sqlalchemy import Vector
|
||||
from langbot.pkg.vector.vdb import VectorDatabase
|
||||
from langbot.pkg.core import app
|
||||
import uuid
|
||||
|
||||
Base = declarative_base()
|
||||
|
||||
|
||||
class PgVectorEntry(Base):
|
||||
"""SQLAlchemy model for pgvector entries"""
|
||||
|
||||
__tablename__ = 'langbot_vectors'
|
||||
|
||||
id = Column(String, primary_key=True)
|
||||
@@ -31,11 +30,11 @@ class PgVectorDatabase(VectorDatabase):
|
||||
self,
|
||||
ap: app.Application,
|
||||
connection_string: str = None,
|
||||
host: str = "localhost",
|
||||
host: str = 'localhost',
|
||||
port: int = 5432,
|
||||
database: str = "langbot",
|
||||
user: str = "postgres",
|
||||
password: str = "postgres"
|
||||
database: str = 'langbot',
|
||||
user: str = 'postgres',
|
||||
password: str = 'postgres',
|
||||
):
|
||||
"""Initialize pgvector database
|
||||
|
||||
@@ -54,14 +53,10 @@ class PgVectorDatabase(VectorDatabase):
|
||||
if connection_string:
|
||||
self.connection_string = connection_string
|
||||
else:
|
||||
self.connection_string = (
|
||||
f"postgresql+psycopg://{user}:{password}@{host}:{port}/{database}"
|
||||
)
|
||||
self.connection_string = f'postgresql+psycopg://{user}:{password}@{host}:{port}/{database}'
|
||||
|
||||
self.async_connection_string = self.connection_string.replace(
|
||||
"postgresql://", "postgresql+asyncpg://"
|
||||
).replace(
|
||||
"postgresql+psycopg://", "postgresql+asyncpg://"
|
||||
self.async_connection_string = self.connection_string.replace('postgresql://', 'postgresql+asyncpg://').replace(
|
||||
'postgresql+psycopg://', 'postgresql+asyncpg://'
|
||||
)
|
||||
|
||||
self.engine = None
|
||||
@@ -75,35 +70,25 @@ class PgVectorDatabase(VectorDatabase):
|
||||
"""Initialize database connection and create tables"""
|
||||
try:
|
||||
# Create async engine for async operations
|
||||
self.async_engine = create_async_engine(
|
||||
self.async_connection_string,
|
||||
echo=False,
|
||||
pool_pre_ping=True
|
||||
)
|
||||
self.AsyncSessionLocal = async_sessionmaker(
|
||||
self.async_engine,
|
||||
class_=AsyncSession,
|
||||
expire_on_commit=False
|
||||
)
|
||||
self.async_engine = create_async_engine(self.async_connection_string, echo=False, pool_pre_ping=True)
|
||||
self.AsyncSessionLocal = async_sessionmaker(self.async_engine, class_=AsyncSession, expire_on_commit=False)
|
||||
|
||||
# Create sync engine for table creation
|
||||
sync_connection_string = self.connection_string.replace(
|
||||
"postgresql+asyncpg://", "postgresql+psycopg://"
|
||||
)
|
||||
sync_connection_string = self.connection_string.replace('postgresql+asyncpg://', 'postgresql+psycopg://')
|
||||
self.engine = create_engine(sync_connection_string, echo=False)
|
||||
|
||||
# Create pgvector extension and tables
|
||||
with self.engine.connect() as conn:
|
||||
# Enable pgvector extension
|
||||
conn.execute(text("CREATE EXTENSION IF NOT EXISTS vector"))
|
||||
conn.execute(text('CREATE EXTENSION IF NOT EXISTS vector'))
|
||||
conn.commit()
|
||||
|
||||
# Create tables
|
||||
Base.metadata.create_all(self.engine)
|
||||
|
||||
self.ap.logger.info(f"Connected to PostgreSQL with pgvector")
|
||||
self.ap.logger.info('Connected to PostgreSQL with pgvector')
|
||||
except Exception as e:
|
||||
self.ap.logger.error(f"Failed to connect to PostgreSQL: {e}")
|
||||
self.ap.logger.error(f'Failed to connect to PostgreSQL: {e}')
|
||||
raise
|
||||
|
||||
async def get_or_create_collection(self, collection: str):
|
||||
@@ -144,24 +129,20 @@ class PgVectorDatabase(VectorDatabase):
|
||||
id=vector_id,
|
||||
collection=collection,
|
||||
embedding=embeddings_list[i],
|
||||
text=metadata.get("text", ""),
|
||||
file_id=metadata.get("file_id", ""),
|
||||
chunk_uuid=metadata.get("uuid", "")
|
||||
text=metadata.get('text', ''),
|
||||
file_id=metadata.get('file_id', ''),
|
||||
chunk_uuid=metadata.get('uuid', ''),
|
||||
)
|
||||
session.add(entry)
|
||||
|
||||
await session.commit()
|
||||
self.ap.logger.info(
|
||||
f"Added {len(ids)} embeddings to pgvector collection '{collection}'"
|
||||
)
|
||||
self.ap.logger.info(f"Added {len(ids)} embeddings to pgvector collection '{collection}'")
|
||||
except Exception as e:
|
||||
await session.rollback()
|
||||
self.ap.logger.error(f"Error adding embeddings to pgvector: {e}")
|
||||
self.ap.logger.error(f'Error adding embeddings to pgvector: {e}')
|
||||
raise
|
||||
|
||||
async def search(
|
||||
self, collection: str, query_embedding: list[float], k: int = 5
|
||||
) -> Dict[str, Any]:
|
||||
async def search(self, collection: str, query_embedding: list[float], k: int = 5) -> Dict[str, Any]:
|
||||
"""Search for similar vectors using cosine distance
|
||||
|
||||
Args:
|
||||
@@ -177,7 +158,7 @@ class PgVectorDatabase(VectorDatabase):
|
||||
async with self.AsyncSessionLocal() as session:
|
||||
try:
|
||||
# Use cosine distance for similarity search
|
||||
from sqlalchemy import select, func
|
||||
from sqlalchemy import select
|
||||
|
||||
# Query for similar vectors
|
||||
stmt = (
|
||||
@@ -186,7 +167,7 @@ class PgVectorDatabase(VectorDatabase):
|
||||
PgVectorEntry.text,
|
||||
PgVectorEntry.file_id,
|
||||
PgVectorEntry.chunk_uuid,
|
||||
PgVectorEntry.embedding.cosine_distance(query_embedding).label('distance')
|
||||
PgVectorEntry.embedding.cosine_distance(query_embedding).label('distance'),
|
||||
)
|
||||
.filter(PgVectorEntry.collection == collection)
|
||||
.order_by(PgVectorEntry.embedding.cosine_distance(query_embedding))
|
||||
@@ -204,25 +185,17 @@ class PgVectorDatabase(VectorDatabase):
|
||||
for row in rows:
|
||||
ids.append(row.id)
|
||||
distances.append(float(row.distance))
|
||||
metadatas.append({
|
||||
"text": row.text or "",
|
||||
"file_id": row.file_id or "",
|
||||
"uuid": row.chunk_uuid or ""
|
||||
})
|
||||
metadatas.append(
|
||||
{'text': row.text or '', 'file_id': row.file_id or '', 'uuid': row.chunk_uuid or ''}
|
||||
)
|
||||
|
||||
result_dict = {
|
||||
"ids": [ids],
|
||||
"distances": [distances],
|
||||
"metadatas": [metadatas]
|
||||
}
|
||||
result_dict = {'ids': [ids], 'distances': [distances], 'metadatas': [metadatas]}
|
||||
|
||||
self.ap.logger.info(
|
||||
f"pgvector search in '{collection}' returned {len(ids)} results"
|
||||
)
|
||||
self.ap.logger.info(f"pgvector search in '{collection}' returned {len(ids)} results")
|
||||
return result_dict
|
||||
|
||||
except Exception as e:
|
||||
self.ap.logger.error(f"Error searching pgvector: {e}")
|
||||
self.ap.logger.error(f'Error searching pgvector: {e}')
|
||||
raise
|
||||
|
||||
async def delete_by_file_id(self, collection: str, file_id: str) -> None:
|
||||
@@ -239,8 +212,7 @@ class PgVectorDatabase(VectorDatabase):
|
||||
from sqlalchemy import delete
|
||||
|
||||
stmt = delete(PgVectorEntry).where(
|
||||
PgVectorEntry.collection == collection,
|
||||
PgVectorEntry.file_id == file_id
|
||||
PgVectorEntry.collection == collection, PgVectorEntry.file_id == file_id
|
||||
)
|
||||
await session.execute(stmt)
|
||||
await session.commit()
|
||||
@@ -250,7 +222,7 @@ class PgVectorDatabase(VectorDatabase):
|
||||
)
|
||||
except Exception as e:
|
||||
await session.rollback()
|
||||
self.ap.logger.error(f"Error deleting from pgvector: {e}")
|
||||
self.ap.logger.error(f'Error deleting from pgvector: {e}')
|
||||
raise
|
||||
|
||||
async def delete_collection(self, collection: str):
|
||||
@@ -266,16 +238,14 @@ class PgVectorDatabase(VectorDatabase):
|
||||
try:
|
||||
from sqlalchemy import delete
|
||||
|
||||
stmt = delete(PgVectorEntry).where(
|
||||
PgVectorEntry.collection == collection
|
||||
)
|
||||
stmt = delete(PgVectorEntry).where(PgVectorEntry.collection == collection)
|
||||
await session.execute(stmt)
|
||||
await session.commit()
|
||||
|
||||
self.ap.logger.info(f"Deleted pgvector collection '{collection}'")
|
||||
except Exception as e:
|
||||
await session.rollback()
|
||||
self.ap.logger.error(f"Error deleting pgvector collection: {e}")
|
||||
self.ap.logger.error(f'Error deleting pgvector collection: {e}')
|
||||
raise
|
||||
|
||||
async def close(self):
|
||||
|
||||
@@ -3,10 +3,8 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
from typing import Any, Dict, List
|
||||
|
||||
import sqlalchemy
|
||||
|
||||
from langbot.pkg.core import app
|
||||
from langbot.pkg.entity.persistence import model as persistence_model
|
||||
from langbot.pkg.vector.vdb import VectorDatabase
|
||||
|
||||
try:
|
||||
@@ -87,14 +85,16 @@ class SeekDBVectorDatabase(VectorDatabase):
|
||||
self._collections: Dict[str, Any] = {}
|
||||
self._collection_configs: Dict[str, HNSWConfiguration] = {}
|
||||
|
||||
self._escape_table = str.maketrans({
|
||||
'\x00': '',
|
||||
'\\': '\\\\',
|
||||
'"': '\\"',
|
||||
'\n': '\\n',
|
||||
'\r': '\\r',
|
||||
'\t': '\\t',
|
||||
})
|
||||
self._escape_table = str.maketrans(
|
||||
{
|
||||
'\x00': '',
|
||||
'\\': '\\\\',
|
||||
'"': '\\"',
|
||||
'\n': '\\n',
|
||||
'\r': '\\r',
|
||||
'\t': '\\t',
|
||||
}
|
||||
)
|
||||
|
||||
async def _get_or_create_collection_internal(self, collection: str, vector_size: int = None) -> Any:
|
||||
"""Internal method to get or create a collection with proper configuration."""
|
||||
@@ -133,8 +133,10 @@ class SeekDBVectorDatabase(VectorDatabase):
|
||||
def _clean_metadata(self, meta: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""SeekDB metadata doesn't support \\ and ", insert will error 3104"""
|
||||
return {
|
||||
k: v.translate(self._escape_table) if isinstance(v, str)
|
||||
else v if v is None or isinstance(v, (int, float, bool))
|
||||
k: v.translate(self._escape_table)
|
||||
if isinstance(v, str)
|
||||
else v
|
||||
if v is None or isinstance(v, (int, float, bool))
|
||||
else str(v)
|
||||
for k, v in meta.items()
|
||||
if v is not None
|
||||
@@ -145,11 +147,7 @@ class SeekDBVectorDatabase(VectorDatabase):
|
||||
return await self._get_or_create_collection_internal(collection)
|
||||
|
||||
async def add_embeddings(
|
||||
self,
|
||||
collection: str,
|
||||
ids: List[str],
|
||||
embeddings_list: List[List[float]],
|
||||
metadatas: List[Dict[str, Any]]
|
||||
self, collection: str, ids: List[str], embeddings_list: List[List[float]], metadatas: List[Dict[str, Any]]
|
||||
) -> None:
|
||||
"""Add vector embeddings to the specified collection.
|
||||
|
||||
|
||||
@@ -15,8 +15,13 @@ proxy:
|
||||
http: ''
|
||||
https: ''
|
||||
system:
|
||||
edition: community
|
||||
recovery_key: ''
|
||||
allow_modify_login_info: true
|
||||
limitation:
|
||||
max_bots: -1
|
||||
max_pipelines: -1
|
||||
max_extensions: -1
|
||||
jwt:
|
||||
expire: 604800
|
||||
secret: ''
|
||||
@@ -51,6 +56,7 @@ vdb:
|
||||
milvus:
|
||||
uri: 'http://127.0.0.1:19530'
|
||||
token: ''
|
||||
db_name: ''
|
||||
pgvector:
|
||||
host: '127.0.0.1'
|
||||
port: 5433
|
||||
@@ -78,3 +84,4 @@ space:
|
||||
# OAuth authorization page URL (user will be redirected here)
|
||||
oauth_authorize_url: 'https://space.langbot.app/auth/authorize'
|
||||
disable_models_service: false
|
||||
disable_telemetry: false
|
||||
|
||||
@@ -9,27 +9,28 @@ from typing import Any
|
||||
|
||||
def _apply_env_overrides_to_config(cfg: dict) -> dict:
|
||||
"""Apply environment variable overrides to data/config.yaml
|
||||
|
||||
Environment variables should be uppercase and use __ (double underscore)
|
||||
|
||||
Environment variables should be uppercase and use __ (double underscore)
|
||||
to represent nested keys. For example:
|
||||
- CONCURRENCY__PIPELINE overrides concurrency.pipeline
|
||||
- PLUGIN__RUNTIME_WS_URL overrides plugin.runtime_ws_url
|
||||
|
||||
|
||||
Arrays and dict types are ignored.
|
||||
|
||||
|
||||
Args:
|
||||
cfg: Configuration dictionary
|
||||
|
||||
|
||||
Returns:
|
||||
Updated configuration dictionary
|
||||
"""
|
||||
|
||||
def convert_value(value: str, original_value: Any) -> Any:
|
||||
"""Convert string value to appropriate type based on original value
|
||||
|
||||
|
||||
Args:
|
||||
value: String value from environment variable
|
||||
original_value: Original value to infer type from
|
||||
|
||||
|
||||
Returns:
|
||||
Converted value (falls back to string if conversion fails)
|
||||
"""
|
||||
@@ -49,7 +50,7 @@ def _apply_env_overrides_to_config(cfg: dict) -> dict:
|
||||
return value
|
||||
else:
|
||||
return value
|
||||
|
||||
|
||||
# Process environment variables
|
||||
for env_key, env_value in os.environ.items():
|
||||
# Check if the environment variable is uppercase and contains __
|
||||
@@ -57,18 +58,18 @@ def _apply_env_overrides_to_config(cfg: dict) -> dict:
|
||||
continue
|
||||
if '__' not in env_key:
|
||||
continue
|
||||
|
||||
|
||||
# Convert environment variable name to config path
|
||||
# e.g., CONCURRENCY__PIPELINE -> ['concurrency', 'pipeline']
|
||||
keys = [key.lower() for key in env_key.split('__')]
|
||||
|
||||
|
||||
# Navigate to the target value and validate the path
|
||||
current = cfg
|
||||
|
||||
|
||||
for i, key in enumerate(keys):
|
||||
if not isinstance(current, dict) or key not in current:
|
||||
break
|
||||
|
||||
|
||||
if i == len(keys) - 1:
|
||||
# At the final key - check if it's a scalar value
|
||||
if isinstance(current[key], (dict, list)):
|
||||
@@ -81,248 +82,182 @@ def _apply_env_overrides_to_config(cfg: dict) -> dict:
|
||||
else:
|
||||
# Navigate deeper
|
||||
current = current[key]
|
||||
|
||||
|
||||
return cfg
|
||||
|
||||
|
||||
class TestEnvOverrides:
|
||||
"""Test environment variable override functionality"""
|
||||
|
||||
|
||||
def test_simple_string_override(self):
|
||||
"""Test overriding a simple string value"""
|
||||
cfg = {
|
||||
'api': {
|
||||
'port': 5300
|
||||
}
|
||||
}
|
||||
|
||||
cfg = {'api': {'port': 5300}}
|
||||
|
||||
# Set environment variable
|
||||
os.environ['API__PORT'] = '8080'
|
||||
|
||||
|
||||
result = _apply_env_overrides_to_config(cfg)
|
||||
|
||||
|
||||
assert result['api']['port'] == 8080
|
||||
|
||||
|
||||
# Cleanup
|
||||
del os.environ['API__PORT']
|
||||
|
||||
|
||||
def test_nested_key_override(self):
|
||||
"""Test overriding nested keys with __ delimiter"""
|
||||
cfg = {
|
||||
'concurrency': {
|
||||
'pipeline': 20,
|
||||
'session': 1
|
||||
}
|
||||
}
|
||||
|
||||
cfg = {'concurrency': {'pipeline': 20, 'session': 1}}
|
||||
|
||||
os.environ['CONCURRENCY__PIPELINE'] = '50'
|
||||
|
||||
|
||||
result = _apply_env_overrides_to_config(cfg)
|
||||
|
||||
|
||||
assert result['concurrency']['pipeline'] == 50
|
||||
assert result['concurrency']['session'] == 1 # Unchanged
|
||||
|
||||
|
||||
del os.environ['CONCURRENCY__PIPELINE']
|
||||
|
||||
|
||||
def test_deep_nested_override(self):
|
||||
"""Test overriding deeply nested keys"""
|
||||
cfg = {
|
||||
'system': {
|
||||
'jwt': {
|
||||
'expire': 604800,
|
||||
'secret': ''
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cfg = {'system': {'jwt': {'expire': 604800, 'secret': ''}}}
|
||||
|
||||
os.environ['SYSTEM__JWT__EXPIRE'] = '86400'
|
||||
os.environ['SYSTEM__JWT__SECRET'] = 'my_secret_key'
|
||||
|
||||
|
||||
result = _apply_env_overrides_to_config(cfg)
|
||||
|
||||
|
||||
assert result['system']['jwt']['expire'] == 86400
|
||||
assert result['system']['jwt']['secret'] == 'my_secret_key'
|
||||
|
||||
|
||||
del os.environ['SYSTEM__JWT__EXPIRE']
|
||||
del os.environ['SYSTEM__JWT__SECRET']
|
||||
|
||||
|
||||
def test_underscore_in_key(self):
|
||||
"""Test keys with underscores like runtime_ws_url"""
|
||||
cfg = {
|
||||
'plugin': {
|
||||
'enable': True,
|
||||
'runtime_ws_url': 'ws://localhost:5400/control/ws'
|
||||
}
|
||||
}
|
||||
|
||||
cfg = {'plugin': {'enable': True, 'runtime_ws_url': 'ws://localhost:5400/control/ws'}}
|
||||
|
||||
os.environ['PLUGIN__RUNTIME_WS_URL'] = 'ws://newhost:6000/ws'
|
||||
|
||||
|
||||
result = _apply_env_overrides_to_config(cfg)
|
||||
|
||||
|
||||
assert result['plugin']['runtime_ws_url'] == 'ws://newhost:6000/ws'
|
||||
|
||||
|
||||
del os.environ['PLUGIN__RUNTIME_WS_URL']
|
||||
|
||||
|
||||
def test_boolean_conversion(self):
|
||||
"""Test boolean value conversion"""
|
||||
cfg = {
|
||||
'plugin': {
|
||||
'enable': True,
|
||||
'enable_marketplace': False
|
||||
}
|
||||
}
|
||||
|
||||
cfg = {'plugin': {'enable': True, 'enable_marketplace': False}}
|
||||
|
||||
os.environ['PLUGIN__ENABLE'] = 'false'
|
||||
os.environ['PLUGIN__ENABLE_MARKETPLACE'] = 'true'
|
||||
|
||||
|
||||
result = _apply_env_overrides_to_config(cfg)
|
||||
|
||||
|
||||
assert result['plugin']['enable'] is False
|
||||
assert result['plugin']['enable_marketplace'] is True
|
||||
|
||||
|
||||
del os.environ['PLUGIN__ENABLE']
|
||||
del os.environ['PLUGIN__ENABLE_MARKETPLACE']
|
||||
|
||||
|
||||
def test_ignore_dict_type(self):
|
||||
"""Test that dict types are ignored"""
|
||||
cfg = {
|
||||
'database': {
|
||||
'use': 'sqlite',
|
||||
'sqlite': {
|
||||
'path': 'data/langbot.db'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cfg = {'database': {'use': 'sqlite', 'sqlite': {'path': 'data/langbot.db'}}}
|
||||
|
||||
# Try to override a dict value - should be ignored
|
||||
os.environ['DATABASE__SQLITE'] = 'new_value'
|
||||
|
||||
|
||||
result = _apply_env_overrides_to_config(cfg)
|
||||
|
||||
|
||||
# Should remain a dict, not overridden
|
||||
assert isinstance(result['database']['sqlite'], dict)
|
||||
assert result['database']['sqlite']['path'] == 'data/langbot.db'
|
||||
|
||||
|
||||
del os.environ['DATABASE__SQLITE']
|
||||
|
||||
|
||||
def test_ignore_list_type(self):
|
||||
"""Test that list/array types are ignored"""
|
||||
cfg = {
|
||||
'admins': ['admin1', 'admin2'],
|
||||
'command': {
|
||||
'enable': True,
|
||||
'prefix': ['!', '!']
|
||||
}
|
||||
}
|
||||
|
||||
cfg = {'admins': ['admin1', 'admin2'], 'command': {'enable': True, 'prefix': ['!', '!']}}
|
||||
|
||||
# Try to override list values - should be ignored
|
||||
os.environ['ADMINS'] = 'admin3'
|
||||
os.environ['COMMAND__PREFIX'] = '?'
|
||||
|
||||
|
||||
result = _apply_env_overrides_to_config(cfg)
|
||||
|
||||
|
||||
# Should remain lists, not overridden
|
||||
assert isinstance(result['admins'], list)
|
||||
assert result['admins'] == ['admin1', 'admin2']
|
||||
assert isinstance(result['command']['prefix'], list)
|
||||
assert result['command']['prefix'] == ['!', '!']
|
||||
|
||||
|
||||
del os.environ['ADMINS']
|
||||
del os.environ['COMMAND__PREFIX']
|
||||
|
||||
|
||||
def test_lowercase_env_var_ignored(self):
|
||||
"""Test that lowercase environment variables are ignored"""
|
||||
cfg = {
|
||||
'api': {
|
||||
'port': 5300
|
||||
}
|
||||
}
|
||||
|
||||
cfg = {'api': {'port': 5300}}
|
||||
|
||||
os.environ['api__port'] = '8080'
|
||||
|
||||
|
||||
result = _apply_env_overrides_to_config(cfg)
|
||||
|
||||
|
||||
# Should not be overridden
|
||||
assert result['api']['port'] == 5300
|
||||
|
||||
|
||||
del os.environ['api__port']
|
||||
|
||||
|
||||
def test_no_double_underscore_ignored(self):
|
||||
"""Test that env vars without __ are ignored"""
|
||||
cfg = {
|
||||
'api': {
|
||||
'port': 5300
|
||||
}
|
||||
}
|
||||
|
||||
cfg = {'api': {'port': 5300}}
|
||||
|
||||
os.environ['APIPORT'] = '8080'
|
||||
|
||||
|
||||
result = _apply_env_overrides_to_config(cfg)
|
||||
|
||||
|
||||
# Should not be overridden
|
||||
assert result['api']['port'] == 5300
|
||||
|
||||
|
||||
del os.environ['APIPORT']
|
||||
|
||||
|
||||
def test_nonexistent_key_ignored(self):
|
||||
"""Test that env vars for non-existent keys are ignored"""
|
||||
cfg = {
|
||||
'api': {
|
||||
'port': 5300
|
||||
}
|
||||
}
|
||||
|
||||
cfg = {'api': {'port': 5300}}
|
||||
|
||||
os.environ['API__NONEXISTENT'] = 'value'
|
||||
|
||||
|
||||
result = _apply_env_overrides_to_config(cfg)
|
||||
|
||||
|
||||
# Should not create new key
|
||||
assert 'nonexistent' not in result['api']
|
||||
|
||||
|
||||
del os.environ['API__NONEXISTENT']
|
||||
|
||||
|
||||
def test_integer_conversion(self):
|
||||
"""Test integer value conversion"""
|
||||
cfg = {
|
||||
'concurrency': {
|
||||
'pipeline': 20
|
||||
}
|
||||
}
|
||||
|
||||
cfg = {'concurrency': {'pipeline': 20}}
|
||||
|
||||
os.environ['CONCURRENCY__PIPELINE'] = '100'
|
||||
|
||||
|
||||
result = _apply_env_overrides_to_config(cfg)
|
||||
|
||||
|
||||
assert result['concurrency']['pipeline'] == 100
|
||||
assert isinstance(result['concurrency']['pipeline'], int)
|
||||
|
||||
|
||||
del os.environ['CONCURRENCY__PIPELINE']
|
||||
|
||||
|
||||
def test_multiple_overrides(self):
|
||||
"""Test multiple environment variable overrides at once"""
|
||||
cfg = {
|
||||
'api': {
|
||||
'port': 5300
|
||||
},
|
||||
'concurrency': {
|
||||
'pipeline': 20,
|
||||
'session': 1
|
||||
},
|
||||
'plugin': {
|
||||
'enable': False
|
||||
}
|
||||
}
|
||||
|
||||
cfg = {'api': {'port': 5300}, 'concurrency': {'pipeline': 20, 'session': 1}, 'plugin': {'enable': False}}
|
||||
|
||||
os.environ['API__PORT'] = '8080'
|
||||
os.environ['CONCURRENCY__PIPELINE'] = '50'
|
||||
os.environ['PLUGIN__ENABLE'] = 'true'
|
||||
|
||||
|
||||
result = _apply_env_overrides_to_config(cfg)
|
||||
|
||||
|
||||
assert result['api']['port'] == 8080
|
||||
assert result['concurrency']['pipeline'] == 50
|
||||
assert result['plugin']['enable'] is True
|
||||
|
||||
|
||||
del os.environ['API__PORT']
|
||||
del os.environ['CONCURRENCY__PIPELINE']
|
||||
del os.environ['PLUGIN__ENABLE']
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"""Test plugin list filtering by component kinds."""
|
||||
|
||||
from datetime import datetime
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
import pytest
|
||||
|
||||
@@ -31,16 +30,7 @@ async def test_plugin_list_filter_by_component_kinds():
|
||||
}
|
||||
}
|
||||
},
|
||||
'components': [
|
||||
{
|
||||
'manifest': {
|
||||
'manifest': {
|
||||
'kind': 'Tool',
|
||||
'metadata': {'name': 'tool1'}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
'components': [{'manifest': {'manifest': {'kind': 'Tool', 'metadata': {'name': 'tool1'}}}}],
|
||||
},
|
||||
{
|
||||
'debug': False,
|
||||
@@ -53,15 +43,8 @@ async def test_plugin_list_filter_by_component_kinds():
|
||||
}
|
||||
},
|
||||
'components': [
|
||||
{
|
||||
'manifest': {
|
||||
'manifest': {
|
||||
'kind': 'KnowledgeRetriever',
|
||||
'metadata': {'name': 'retriever1'}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
{'manifest': {'manifest': {'kind': 'KnowledgeRetriever', 'metadata': {'name': 'retriever1'}}}}
|
||||
],
|
||||
},
|
||||
{
|
||||
'debug': False,
|
||||
@@ -73,16 +56,7 @@ async def test_plugin_list_filter_by_component_kinds():
|
||||
}
|
||||
}
|
||||
},
|
||||
'components': [
|
||||
{
|
||||
'manifest': {
|
||||
'manifest': {
|
||||
'kind': 'Command',
|
||||
'metadata': {'name': 'cmd1'}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
'components': [{'manifest': {'manifest': {'kind': 'Command', 'metadata': {'name': 'cmd1'}}}}],
|
||||
},
|
||||
{
|
||||
'debug': False,
|
||||
@@ -94,16 +68,7 @@ async def test_plugin_list_filter_by_component_kinds():
|
||||
}
|
||||
}
|
||||
},
|
||||
'components': [
|
||||
{
|
||||
'manifest': {
|
||||
'manifest': {
|
||||
'kind': 'EventListener',
|
||||
'metadata': {'name': 'listener1'}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
'components': [{'manifest': {'manifest': {'kind': 'EventListener', 'metadata': {'name': 'listener1'}}}}],
|
||||
},
|
||||
{
|
||||
'debug': False,
|
||||
@@ -116,23 +81,9 @@ async def test_plugin_list_filter_by_component_kinds():
|
||||
}
|
||||
},
|
||||
'components': [
|
||||
{
|
||||
'manifest': {
|
||||
'manifest': {
|
||||
'kind': 'KnowledgeRetriever',
|
||||
'metadata': {'name': 'retriever2'}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
'manifest': {
|
||||
'manifest': {
|
||||
'kind': 'Tool',
|
||||
'metadata': {'name': 'tool2'}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
{'manifest': {'manifest': {'kind': 'KnowledgeRetriever', 'metadata': {'name': 'retriever2'}}}},
|
||||
{'manifest': {'manifest': {'kind': 'Tool', 'metadata': {'name': 'tool2'}}}},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
@@ -187,16 +138,7 @@ async def test_plugin_list_filter_no_filter():
|
||||
}
|
||||
}
|
||||
},
|
||||
'components': [
|
||||
{
|
||||
'manifest': {
|
||||
'manifest': {
|
||||
'kind': 'Tool',
|
||||
'metadata': {'name': 'tool1'}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
'components': [{'manifest': {'manifest': {'kind': 'Tool', 'metadata': {'name': 'tool1'}}}}],
|
||||
},
|
||||
{
|
||||
'debug': False,
|
||||
@@ -209,15 +151,8 @@ async def test_plugin_list_filter_no_filter():
|
||||
}
|
||||
},
|
||||
'components': [
|
||||
{
|
||||
'manifest': {
|
||||
'manifest': {
|
||||
'kind': 'KnowledgeRetriever',
|
||||
'metadata': {'name': 'retriever1'}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
{'manifest': {'manifest': {'kind': 'KnowledgeRetriever', 'metadata': {'name': 'retriever1'}}}}
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
@@ -267,15 +202,8 @@ async def test_plugin_list_filter_empty_result():
|
||||
}
|
||||
},
|
||||
'components': [
|
||||
{
|
||||
'manifest': {
|
||||
'manifest': {
|
||||
'kind': 'KnowledgeRetriever',
|
||||
'metadata': {'name': 'retriever1'}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
{'manifest': {'manifest': {'kind': 'KnowledgeRetriever', 'metadata': {'name': 'retriever1'}}}}
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
@@ -321,16 +249,7 @@ async def test_plugin_list_filter_plugin_without_components():
|
||||
}
|
||||
}
|
||||
},
|
||||
'components': [
|
||||
{
|
||||
'manifest': {
|
||||
'manifest': {
|
||||
'kind': 'Tool',
|
||||
'metadata': {'name': 'tool1'}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
'components': [{'manifest': {'manifest': {'kind': 'Tool', 'metadata': {'name': 'tool1'}}}}],
|
||||
},
|
||||
{
|
||||
'debug': False,
|
||||
@@ -342,7 +261,7 @@ async def test_plugin_list_filter_plugin_without_components():
|
||||
}
|
||||
}
|
||||
},
|
||||
'components': []
|
||||
'components': [],
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"*.{js,jsx,ts,tsx}": ["next lint --fix --file", "next lint --file"],
|
||||
"*.{js,jsx,ts,tsx}": ["eslint --fix"],
|
||||
"**/*": ["bash -c 'cd \"$(pwd)\" && next build"]
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@ const config = {
|
||||
singleQuote: true,
|
||||
// 大括号前后空格
|
||||
bracketSpacing: true,
|
||||
attributeVerticalAlignment: 'auto',
|
||||
trailingComma: 'all',
|
||||
};
|
||||
|
||||
|
||||
11210
web/package-lock.json
generated
Normal file
11210
web/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -6,7 +6,8 @@
|
||||
"dev": "next dev --turbopack",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "eslint . --fix",
|
||||
"lint-staged": "lint-staged"
|
||||
},
|
||||
"lint-staged": {
|
||||
@@ -43,16 +44,16 @@
|
||||
"@radix-ui/react-tooltip": "^1.2.7",
|
||||
"@tailwindcss/postcss": "^4.1.5",
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"axios": "^1.12.0",
|
||||
"axios": "^1.13.5",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"highlight.js": "^11.11.1",
|
||||
"i18next": "^25.1.2",
|
||||
"i18next-browser-languagedetector": "^8.1.0",
|
||||
"input-otp": "^1.4.2",
|
||||
"lodash": "^4.17.21",
|
||||
"lodash": "^4.17.23",
|
||||
"lucide-react": "^0.507.0",
|
||||
"next": "~15.5.9",
|
||||
"next": "~16.1.5",
|
||||
"next-themes": "^0.4.6",
|
||||
"postcss": "^8.5.3",
|
||||
"qrcode": "^1.5.4",
|
||||
@@ -63,6 +64,7 @@
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-photo-view": "^1.2.7",
|
||||
"react-syntax-highlighter": "^16.1.0",
|
||||
"recharts": "2.15.4",
|
||||
"rehype-autolink-headings": "^7.1.0",
|
||||
"rehype-highlight": "^7.0.2",
|
||||
"rehype-raw": "^7.0.0",
|
||||
|
||||
1568
web/pnpm-lock.yaml
generated
1568
web/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -19,6 +19,7 @@ import {
|
||||
CardDescription,
|
||||
} from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { LoadingSpinner } from '@/components/ui/loading-spinner';
|
||||
import langbotIcon from '@/app/assets/langbot-logo.webp';
|
||||
|
||||
function SpaceOAuthCallbackContent() {
|
||||
@@ -174,9 +175,7 @@ function SpaceOAuthCallbackContent() {
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col items-center space-y-4">
|
||||
{status === 'loading' && (
|
||||
<Loader2 className="h-12 w-12 animate-spin text-primary" />
|
||||
)}
|
||||
{status === 'loading' && <LoadingSpinner size="lg" text="" />}
|
||||
{status === 'confirm' && (
|
||||
<>
|
||||
<AlertTriangle className="h-12 w-12 text-yellow-500" />
|
||||
@@ -232,7 +231,7 @@ function LoadingFallback() {
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-50 dark:bg-neutral-900">
|
||||
<Card className="w-[400px] shadow-lg dark:shadow-white/10">
|
||||
<CardContent className="flex flex-col items-center py-12">
|
||||
<Loader2 className="h-12 w-12 animate-spin text-primary" />
|
||||
<LoadingSpinner size="lg" text="" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
@@ -6,12 +6,33 @@ import styles from './botLog.module.css';
|
||||
import { httpClient } from '@/app/infra/http/HttpClient';
|
||||
import { PhotoProvider } from 'react-photo-view';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Check } from 'lucide-react';
|
||||
import { Check, ChevronDown, ChevronRight } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
export function BotLogCard({ botLog }: { botLog: BotLog }) {
|
||||
const { t } = useTranslation();
|
||||
const baseURL = httpClient.getBaseUrl();
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
|
||||
// Fallback 复制方法,用于不支持 clipboard API 的环境
|
||||
function fallbackCopy(text: string) {
|
||||
const textArea = document.createElement('textarea');
|
||||
textArea.value = text;
|
||||
textArea.style.position = 'fixed';
|
||||
textArea.style.left = '-9999px';
|
||||
textArea.style.top = '-9999px';
|
||||
document.body.appendChild(textArea);
|
||||
textArea.focus();
|
||||
textArea.select();
|
||||
try {
|
||||
document.execCommand('copy');
|
||||
toast.success(t('common.copySuccess'));
|
||||
} catch {
|
||||
toast.error(t('common.copyFailed'));
|
||||
}
|
||||
document.body.removeChild(textArea);
|
||||
}
|
||||
|
||||
function formatTime(timestamp: number) {
|
||||
const now = new Date();
|
||||
@@ -63,6 +84,15 @@ export function BotLogCard({ botLog }: { botLog: BotLog }) {
|
||||
}
|
||||
}
|
||||
|
||||
// 截取文本的简短版本
|
||||
function getShortText(text: string, maxLength: number = 100) {
|
||||
if (text.length <= maxLength) return text;
|
||||
return text.substring(0, maxLength) + '...';
|
||||
}
|
||||
|
||||
// 判断是否需要展开按钮
|
||||
const needsExpand = botLog.text.length > 100 || botLog.images.length > 0;
|
||||
|
||||
return (
|
||||
<div className={`${styles.botLogCardContainer}`}>
|
||||
{/* 头部标签,时间 */}
|
||||
@@ -78,13 +108,24 @@ export function BotLogCard({ botLog }: { botLog: BotLog }) {
|
||||
{botLog.message_session_id && (
|
||||
<div
|
||||
className={`${styles.tag} ${styles.chatTag} relative`}
|
||||
onClick={() => {
|
||||
navigator.clipboard
|
||||
.writeText(botLog.message_session_id)
|
||||
.then(() => {
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
});
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
// 兼容性更好的复制方法
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||
navigator.clipboard
|
||||
.writeText(botLog.message_session_id)
|
||||
.then(() => {
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
toast.success(t('common.copySuccess'));
|
||||
})
|
||||
.catch(() => {
|
||||
// fallback
|
||||
fallbackCopy(botLog.message_session_id);
|
||||
});
|
||||
} else {
|
||||
fallbackCopy(botLog.message_session_id);
|
||||
}
|
||||
}}
|
||||
title={t('common.clickToCopy')}
|
||||
>
|
||||
@@ -125,12 +166,38 @@ export function BotLogCard({ botLog }: { botLog: BotLog }) {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className={`${styles.timestamp}`}>
|
||||
{formatTime(botLog.timestamp)}
|
||||
<div className="flex items-center gap-2">
|
||||
{needsExpand && (
|
||||
<button
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
className="flex items-center gap-1 text-xs text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300 transition-colors"
|
||||
>
|
||||
{expanded ? (
|
||||
<>
|
||||
<ChevronDown className="w-3 h-3" />
|
||||
{t('bots.collapse')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<ChevronRight className="w-3 h-3" />
|
||||
{t('bots.viewDetails')}
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
<div className={`${styles.timestamp}`}>
|
||||
{formatTime(botLog.timestamp)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`${styles.cardText}`}>{botLog.text}</div>
|
||||
{botLog.images.length > 0 && (
|
||||
|
||||
{/* 日志内容 - 简化显示 */}
|
||||
<div className={`${styles.cardText}`}>
|
||||
{expanded ? botLog.text : getShortText(botLog.text)}
|
||||
</div>
|
||||
|
||||
{/* 图片 - 只在展开时显示 */}
|
||||
{expanded && botLog.images.length > 0 && (
|
||||
<PhotoProvider>
|
||||
<div className={`flex flex-wrap gap-2 mt-3`}>
|
||||
{botLog.images.map((item) => (
|
||||
@@ -144,6 +211,13 @@ export function BotLogCard({ botLog }: { botLog: BotLog }) {
|
||||
</div>
|
||||
</PhotoProvider>
|
||||
)}
|
||||
|
||||
{/* 图片数量提示 - 未展开时显示 */}
|
||||
{!expanded && botLog.images.length > 0 && (
|
||||
<div className="mt-2 text-xs text-gray-500 dark:text-gray-400">
|
||||
📷 {botLog.images.length} {t('bots.imagesAttached')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -13,12 +13,14 @@ import {
|
||||
} from '@/components/ui/popover';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { ChevronDownIcon } from 'lucide-react';
|
||||
import { ChevronDownIcon, ExternalLink } from 'lucide-react';
|
||||
import { debounce } from 'lodash';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
export function BotLogListComponent({ botId }: { botId: string }) {
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
const manager = useRef(new BotLogManager(botId)).current;
|
||||
const [botLogList, setBotLogList] = useState<BotLog[]>([]);
|
||||
const [autoFlush, setAutoFlush] = useState(true);
|
||||
@@ -206,6 +208,15 @@ export function BotLogListComponent({ botId }: { botId: string }) {
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="ml-4 flex items-center gap-1"
|
||||
onClick={() => router.push(`/home/monitoring?botId=${botId}`)}
|
||||
>
|
||||
<ExternalLink className="h-4 w-4" />
|
||||
<span className="text-sm">{t('bots.viewDetailedLogs')}</span>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{filteredLogs.map((botLog) => {
|
||||
|
||||
@@ -12,6 +12,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import { extractI18nObject } from '@/i18n/I18nProvider';
|
||||
import BotDetailDialog from '@/app/home/bots/BotDetailDialog';
|
||||
import { CustomApiError } from '@/app/infra/entities/common';
|
||||
import { systemInfo } from '@/app/infra/http';
|
||||
|
||||
export default function BotConfigPage() {
|
||||
const { t } = useTranslation();
|
||||
@@ -60,6 +61,11 @@ export default function BotConfigPage() {
|
||||
}
|
||||
|
||||
function handleCreateBotClick() {
|
||||
const maxBots = systemInfo.limitation?.max_bots ?? -1;
|
||||
if (maxBots >= 0 && botList.length >= maxBots) {
|
||||
toast.error(t('limitation.maxBotsReached', { max: maxBots }));
|
||||
return;
|
||||
}
|
||||
setSelectedBotId('');
|
||||
setDetailDialogOpen(true);
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ import { Switch } from '@/components/ui/switch';
|
||||
import { ControllerRenderProps } from 'react-hook-form';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { httpClient, systemInfo } from '@/app/infra/http/HttpClient';
|
||||
import { httpClient, systemInfo, userInfo } from '@/app/infra/http';
|
||||
import {
|
||||
LLMModel,
|
||||
Bot,
|
||||
@@ -99,8 +99,11 @@ export default function DynamicFormItemComponent({
|
||||
.getProviderLLMModels()
|
||||
.then((resp) => {
|
||||
let models = resp.models;
|
||||
// Filter out space-chat-completions models when models service is disabled
|
||||
if (systemInfo.disable_models_service) {
|
||||
// Filter out space-chat-completions models when not logged in with space account or when models service is disabled
|
||||
if (
|
||||
systemInfo.disable_models_service ||
|
||||
userInfo?.account_type !== 'space'
|
||||
) {
|
||||
models = models.filter(
|
||||
(m) => m.provider?.requester !== 'space-chat-completions',
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import styles from './HomeSidebar.module.css';
|
||||
import { useEffect, useState, Suspense } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import {
|
||||
SidebarChild,
|
||||
SidebarChildVO,
|
||||
@@ -20,7 +20,6 @@ import {
|
||||
Lightbulb,
|
||||
LogOut,
|
||||
KeyRound,
|
||||
Loader2,
|
||||
} from 'lucide-react';
|
||||
import { useTheme } from 'next-themes';
|
||||
|
||||
@@ -59,7 +58,7 @@ function compareVersions(v1: string, v2: string): boolean {
|
||||
}
|
||||
|
||||
// TODO 侧边导航栏要加动画
|
||||
function HomeSidebarContent({
|
||||
export default function HomeSidebar({
|
||||
onSelectedChangeAction,
|
||||
}: {
|
||||
onSelectedChangeAction: (sidebarChild: SidebarChildVO) => void;
|
||||
@@ -228,6 +227,7 @@ function HomeSidebarContent({
|
||||
);
|
||||
if (routeSelectChild) {
|
||||
setSelectedChild(routeSelectChild);
|
||||
onSelectedChangeAction(routeSelectChild);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -483,25 +483,3 @@ function HomeSidebarContent({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarLoadingFallback() {
|
||||
return (
|
||||
<div className={`${styles.sidebarContainer}`}>
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<Loader2 className="h-6 w-6 animate-spin" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function HomeSidebar({
|
||||
onSelectedChangeAction,
|
||||
}: {
|
||||
onSelectedChangeAction: (sidebarChild: SidebarChildVO) => void;
|
||||
}) {
|
||||
return (
|
||||
<Suspense fallback={<SidebarLoadingFallback />}>
|
||||
<HomeSidebarContent onSelectedChangeAction={onSelectedChangeAction} />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -49,6 +49,26 @@ export const sidebarConfigList = [
|
||||
ja_JP: 'https://docs.langbot.app/ja/usage/pipelines/readme.html',
|
||||
},
|
||||
}),
|
||||
new SidebarChildVO({
|
||||
id: 'monitoring',
|
||||
name: t('monitoring.title'),
|
||||
icon: (
|
||||
<svg
|
||||
className={`${styles.sidebarChildIcon}`}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path d="M2 3.9934C2 3.44476 2.45531 3 2.9918 3H21.0082C21.556 3 22 3.44495 22 3.9934V20.0066C22 20.5552 21.5447 21 21.0082 21H2.9918C2.44405 21 2 20.5551 2 20.0066V3.9934ZM4 5V19H20V5H4ZM6 7H18V9H6V7ZM6 11H18V13H6V11ZM6 15H12V17H6V15Z"></path>
|
||||
</svg>
|
||||
),
|
||||
route: '/home/monitoring',
|
||||
description: t('monitoring.description'),
|
||||
helpLink: {
|
||||
en_US: 'https://docs.langbot.app/en/features/monitoring.html',
|
||||
zh_Hans: 'https://docs.langbot.app/zh/features/monitoring.html',
|
||||
},
|
||||
}),
|
||||
new SidebarChildVO({
|
||||
id: 'knowledge',
|
||||
name: t('knowledge.title'),
|
||||
|
||||
@@ -206,8 +206,23 @@ export default function ModelsDialog({
|
||||
}
|
||||
}
|
||||
|
||||
function handleSpaceLogin() {
|
||||
window.location.href = '/auth/space';
|
||||
async function handleSpaceLogin() {
|
||||
try {
|
||||
const token = localStorage.getItem('token');
|
||||
if (!token) {
|
||||
toast.error(t('common.error'));
|
||||
return;
|
||||
}
|
||||
const currentOrigin = window.location.origin;
|
||||
const redirectUri = `${currentOrigin}/auth/space/callback?mode=bind`;
|
||||
const response = await httpClient.getSpaceAuthorizeUrl(
|
||||
redirectUri,
|
||||
token,
|
||||
);
|
||||
window.location.href = response.authorize_url;
|
||||
} catch {
|
||||
toast.error(t('common.spaceLoginFailed'));
|
||||
}
|
||||
}
|
||||
|
||||
async function handleAddModel(
|
||||
|
||||
@@ -16,6 +16,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import { LLMModel, EmbeddingModel } from '@/app/infra/entities/api';
|
||||
import { ExtraArg, ModelType, TestResult } from '../types';
|
||||
import ExtraArgsEditor from './ExtraArgsEditor';
|
||||
import { userInfo } from '@/app/infra/http';
|
||||
|
||||
interface ModelItemProps {
|
||||
model: LLMModel | EmbeddingModel;
|
||||
@@ -113,10 +114,15 @@ export default function ModelItem({
|
||||
}
|
||||
};
|
||||
|
||||
// Check if popover should be disabled (space models when not logged in)
|
||||
const isPopoverDisabled =
|
||||
isLangBotModels && userInfo?.account_type !== 'space';
|
||||
|
||||
return (
|
||||
<Popover
|
||||
open={isEditOpen}
|
||||
open={isEditOpen && !isPopoverDisabled}
|
||||
onOpenChange={(open) => {
|
||||
if (isPopoverDisabled) return;
|
||||
if (open) {
|
||||
onOpenEditModel(model.uuid);
|
||||
} else {
|
||||
@@ -125,7 +131,13 @@ export default function ModelItem({
|
||||
}}
|
||||
>
|
||||
<PopoverTrigger asChild>
|
||||
<div className="flex items-center justify-between py-2 px-3 rounded-md border bg-background hover:bg-accent cursor-pointer">
|
||||
<div
|
||||
className={`flex items-center justify-between py-2 px-3 rounded-md border bg-background ${
|
||||
isPopoverDisabled
|
||||
? 'cursor-not-allowed opacity-60'
|
||||
: 'hover:bg-accent cursor-pointer'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<span className="text-sm font-medium">{model.name}</span>
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
|
||||
@@ -11,61 +11,46 @@ export default function ExternalKBCard({
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className={`${styles.cardContainer}`}>
|
||||
<div className="w-full h-full flex flex-row items-start gap-3">
|
||||
{/* Icon */}
|
||||
<img
|
||||
src={httpClient.getPluginIconURL(
|
||||
kbCardVO.pluginAuthor,
|
||||
kbCardVO.pluginName,
|
||||
)}
|
||||
alt="plugin icon"
|
||||
className="w-16 h-16 mt-1 rounded-[8%] flex-shrink-0"
|
||||
/>
|
||||
|
||||
{/* Info Column */}
|
||||
<div className="flex flex-col flex-1 min-w-0 h-full">
|
||||
{/* Top section: Name, Description and Plugin Info */}
|
||||
<div className="flex flex-col gap-0">
|
||||
{/* Name and Description */}
|
||||
<div className={`${styles.basicInfoNameContainer}`}>
|
||||
<div className={`${styles.basicInfoNameText} ${styles.bigText}`}>
|
||||
{kbCardVO.name}
|
||||
</div>
|
||||
<div className={`${styles.basicInfoDescriptionText}`}>
|
||||
{kbCardVO.description}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Plugin Info */}
|
||||
<div className="flex flex-row gap-2 items-center mt-1">
|
||||
<svg
|
||||
className="w-5 h-5 text-gray-500 dark:text-gray-400"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path d="M7 5C7 2.79086 8.79086 1 11 1C13.2091 1 15 2.79086 15 5H18C18.5523 5 19 5.44772 19 6V9C21.2091 9 23 10.7909 23 13C23 15.2091 21.2091 17 19 17V20C19 20.5523 18.5523 21 18 21H4C3.44772 21 3 20.5523 3 20V6C3 5.44772 3.44772 5 4 5H7ZM11 3C9.89543 3 9 3.89543 9 5C9 5.23554 9.0403 5.45952 9.11355 5.66675C9.22172 5.97282 9.17461 6.31235 8.98718 6.57739C8.79974 6.84243 8.49532 7 8.17071 7H5V19H17V15.8293C17 15.5047 17.1576 15.2003 17.4226 15.0128C17.6877 14.8254 18.0272 14.7783 18.3332 14.8865C18.5405 14.9597 18.7645 15 19 15C20.1046 15 21 14.1046 21 13C21 11.8954 20.1046 11 19 11C18.7645 11 18.5405 11.0403 18.3332 11.1135C18.0272 11.2217 17.6877 11.1746 17.4226 10.9872C17.1576 10.7997 17 10.4953 17 10.1707V7H13.8293C13.5047 7 13.2003 6.84243 13.0128 6.57739C12.8254 6.31235 12.7783 5.97282 12.8865 5.66675C12.9597 5.45952 13 5.23555 13 5C13 3.89543 12.1046 3 11 3Z"></path>
|
||||
</svg>
|
||||
<span className="text-sm font-medium text-gray-600 dark:text-gray-400">
|
||||
{kbCardVO.pluginAuthor} / {kbCardVO.pluginName}
|
||||
</span>
|
||||
<div className={`${styles.basicInfoContainer}`}>
|
||||
<div className={`${styles.iconBasicInfoContainer}`}>
|
||||
{/* Emoji with plugin icon badge */}
|
||||
<div className="relative">
|
||||
<div className={`${styles.iconEmoji}`}>
|
||||
{kbCardVO.emoji || '🔗'}
|
||||
</div>
|
||||
{/* Plugin icon badge at bottom right */}
|
||||
<img
|
||||
src={httpClient.getPluginIconURL(
|
||||
kbCardVO.pluginAuthor,
|
||||
kbCardVO.pluginName,
|
||||
)}
|
||||
alt="plugin icon"
|
||||
className="absolute -bottom-1 -right-1 w-5 h-5 rounded-[20%]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Bottom section: Update Time */}
|
||||
<div className="flex flex-row gap-2 items-center mt-auto">
|
||||
<svg
|
||||
className="w-5 h-5 text-gray-500 dark:text-gray-400"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path d="M12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22ZM12 20C16.4183 20 20 16.4183 20 12C20 7.58172 16.4183 4 12 4C7.58172 4 4 7.58172 4 12C4 16.4183 7.58172 20 12 20ZM13 12H17V14H11V7H13V12Z"></path>
|
||||
</svg>
|
||||
<span className="text-sm font-medium text-gray-600 dark:text-gray-400">
|
||||
{t('knowledge.updateTime')}
|
||||
{kbCardVO.lastUpdatedTimeAgo}
|
||||
</span>
|
||||
<div className={`${styles.basicInfoNameContainer}`}>
|
||||
<div className={`${styles.basicInfoNameText} ${styles.bigText}`}>
|
||||
{kbCardVO.name}
|
||||
</div>
|
||||
<div className={`${styles.basicInfoDescriptionText}`}>
|
||||
{kbCardVO.description}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={`${styles.basicInfoLastUpdatedTimeContainer}`}>
|
||||
<svg
|
||||
className={`${styles.basicInfoUpdateTimeIcon}`}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path d="M12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22ZM12 20C16.4183 20 20 16.4183 20 12C20 7.58172 16.4183 4 12 4C7.58172 4 4 7.58172 4 12C4 16.4183 7.58172 20 12 20ZM13 12H17V14H11V7H13V12Z"></path>
|
||||
</svg>
|
||||
<div className={`${styles.basicInfoUpdateTimeText}`}>
|
||||
{t('knowledge.updateTime')}
|
||||
{kbCardVO.lastUpdatedTimeAgo}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,7 @@ export class ExternalKBCardVO {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
emoji?: string;
|
||||
retrieverName: string;
|
||||
retrieverConfig: Record<string, unknown>;
|
||||
lastUpdatedTimeAgo: string;
|
||||
@@ -12,6 +13,7 @@ export class ExternalKBCardVO {
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
emoji,
|
||||
retrieverName,
|
||||
retrieverConfig,
|
||||
lastUpdatedTimeAgo,
|
||||
@@ -21,6 +23,7 @@ export class ExternalKBCardVO {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
emoji?: string;
|
||||
retrieverName: string;
|
||||
retrieverConfig: Record<string, unknown>;
|
||||
lastUpdatedTimeAgo: string;
|
||||
@@ -30,6 +33,7 @@ export class ExternalKBCardVO {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.description = description;
|
||||
this.emoji = emoji;
|
||||
this.retrieverName = retrieverName;
|
||||
this.retrieverConfig = retrieverConfig;
|
||||
this.lastUpdatedTimeAgo = lastUpdatedTimeAgo;
|
||||
|
||||
@@ -15,6 +15,7 @@ import { IDynamicFormItemSchema } from '@/app/infra/entities/form/dynamic';
|
||||
import DynamicFormComponent from '@/app/home/components/dynamic-form/DynamicFormComponent';
|
||||
import { httpClient } from '@/app/infra/http/HttpClient';
|
||||
import { ExternalKnowledgeBase } from '@/app/infra/entities/api';
|
||||
import EmojiPicker from '@/components/ui/emoji-picker';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -54,6 +55,7 @@ const getFormSchema = (t: (key: string) => string) =>
|
||||
z.object({
|
||||
name: z.string().min(1, { message: t('knowledge.nameRequired') }),
|
||||
description: z.string().optional(),
|
||||
emoji: z.string().optional(),
|
||||
plugin_author: z.string().min(1, { message: 'Please select a retriever' }),
|
||||
plugin_name: z.string().min(1, { message: 'Please select a retriever' }),
|
||||
retriever_name: z.string().min(1, { message: 'Please select a retriever' }),
|
||||
@@ -101,6 +103,7 @@ export default function ExternalKBForm({
|
||||
defaultValues: {
|
||||
name: '',
|
||||
description: '',
|
||||
emoji: '🔗',
|
||||
plugin_author: '',
|
||||
plugin_name: '',
|
||||
retriever_name: '',
|
||||
@@ -140,6 +143,7 @@ export default function ExternalKBForm({
|
||||
// Set form values
|
||||
form.setValue('name', kbConfig.name);
|
||||
form.setValue('description', kbConfig.description || '');
|
||||
form.setValue('emoji', kbConfig.emoji || '🔗');
|
||||
form.setValue('plugin_author', kbConfig.plugin_author);
|
||||
form.setValue('plugin_name', kbConfig.plugin_name);
|
||||
form.setValue('retriever_name', kbConfig.retriever_name);
|
||||
@@ -207,6 +211,7 @@ export default function ExternalKBForm({
|
||||
return {
|
||||
name: kb.name,
|
||||
description: kb.description,
|
||||
emoji: kb.emoji || '🔗',
|
||||
plugin_author: kb.plugin_author,
|
||||
plugin_name: kb.plugin_name,
|
||||
retriever_name: kb.retriever_name,
|
||||
@@ -276,6 +281,7 @@ export default function ExternalKBForm({
|
||||
const formData: ExternalKnowledgeBase = {
|
||||
name: form.getValues().name,
|
||||
description: form.getValues().description || '',
|
||||
emoji: form.getValues().emoji,
|
||||
plugin_author: form.getValues().plugin_author,
|
||||
plugin_name: form.getValues().plugin_name,
|
||||
retriever_name: form.getValues().retriever_name,
|
||||
@@ -390,23 +396,41 @@ export default function ExternalKBForm({
|
||||
className="space-y-8"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
{/* KB Name */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t('knowledge.kbName')}
|
||||
<span className="text-red-500">*</span>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{/* KB Name and Emoji in same row */}
|
||||
<div className="flex gap-4 items-start">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex-1">
|
||||
<FormLabel>
|
||||
{t('knowledge.kbName')}
|
||||
<span className="text-red-500">*</span>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="emoji"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('common.icon')}</FormLabel>
|
||||
<FormControl>
|
||||
<EmojiPicker
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* KB Description */}
|
||||
<FormField
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
background-color: #fff;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0px 2px 2px 0 rgba(0, 0, 0, 0.2);
|
||||
padding: 1.2rem;
|
||||
padding: 1rem;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
@@ -32,14 +32,41 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
gap: 0.4rem;
|
||||
gap: 0.5rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.iconEmoji {
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
border-radius: 0.5rem;
|
||||
background-color: #f5f5f5;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.75rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
:global(.dark) .iconEmoji {
|
||||
background-color: #2a2a2d;
|
||||
}
|
||||
|
||||
.iconBasicInfoContainer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 0.75rem;
|
||||
align-items: flex-start;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.basicInfoNameContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.2rem;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.basicInfoNameText {
|
||||
|
||||
@@ -7,12 +7,15 @@ export default function KBCard({ kbCardVO }: { kbCardVO: KnowledgeBaseVO }) {
|
||||
return (
|
||||
<div className={`${styles.cardContainer}`}>
|
||||
<div className={`${styles.basicInfoContainer}`}>
|
||||
<div className={`${styles.basicInfoNameContainer}`}>
|
||||
<div className={`${styles.basicInfoNameText} ${styles.bigText}`}>
|
||||
{kbCardVO.name}
|
||||
</div>
|
||||
<div className={`${styles.basicInfoDescriptionText}`}>
|
||||
{kbCardVO.description}
|
||||
<div className={`${styles.iconBasicInfoContainer}`}>
|
||||
<div className={`${styles.iconEmoji}`}>{kbCardVO.emoji || '📚'}</div>
|
||||
<div className={`${styles.basicInfoNameContainer}`}>
|
||||
<div className={`${styles.basicInfoNameText} ${styles.bigText}`}>
|
||||
{kbCardVO.name}
|
||||
</div>
|
||||
<div className={`${styles.basicInfoDescriptionText}`}>
|
||||
{kbCardVO.description}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ export interface IKnowledgeBaseVO {
|
||||
embeddingModelUUID: string;
|
||||
top_k: number;
|
||||
lastUpdatedTimeAgo: string;
|
||||
emoji?: string;
|
||||
}
|
||||
|
||||
export class KnowledgeBaseVO implements IKnowledgeBaseVO {
|
||||
@@ -14,6 +15,7 @@ export class KnowledgeBaseVO implements IKnowledgeBaseVO {
|
||||
embeddingModelUUID: string;
|
||||
top_k: number;
|
||||
lastUpdatedTimeAgo: string;
|
||||
emoji?: string;
|
||||
|
||||
constructor(props: IKnowledgeBaseVO) {
|
||||
this.id = props.id;
|
||||
@@ -22,5 +24,6 @@ export class KnowledgeBaseVO implements IKnowledgeBaseVO {
|
||||
this.embeddingModelUUID = props.embeddingModelUUID;
|
||||
this.top_k = props.top_k;
|
||||
this.lastUpdatedTimeAgo = props.lastUpdatedTimeAgo;
|
||||
this.emoji = props.emoji;
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user