Feat/2.0.0 (#1545)

This commit is contained in:
Wenru Li
2025-08-13 18:07:28 +08:00
committed by GitHub
1136 changed files with 1022761 additions and 19189 deletions
-51
View File
@@ -19,29 +19,6 @@ steps: # 定义流水线执行步骤,这些步骤将顺序执行
- git clone https://github.com/dataelement/bisheng.git .
- git checkout $DRONE_COMMIT
- name: package # 流水线名称
pull: if-not-exists
image: python:3.10-slim # 定义创建容器的Docker镜像
volumes: # 将容器内目录挂载到宿主机,仓库需要开启Trusted设置
- name: bisheng-cache
path: /app/build # 将应用打包好的Jar和执行脚本挂载出来
environment:
RELEASE_VERSION: 99.99.99
NEXUS_USER:
from_secret: NEXUS_USER
NEXUS_PASSWORD:
from_secret: NEXUS_PASSWORD
REPO:
from_secret: PY_NEXUS
commands: # 定义在Docker容器中执行的shell命令
- pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple
- pip install Cython
- pip install wheel
- pip install twine
- cd ./src/bisheng-langchain
- python setup.py bdist_wheel
- twine upload --verbose -u $NEXUS_USER -p $NEXUS_PASSWORD --repository-url $REPO dist/*.whl
- name: set poetry
pull: if-not-exists
image: golang
@@ -60,11 +37,9 @@ steps: # 定义流水线执行步骤,这些步骤将顺序执行
path: /app/build/
commands:
- cd ./src/backend
- cp -r /app/build/nltk_data ./
- echo $REPO
- REPO2=$(echo $REPO | sed 's/http:\\/\\///g')
- sed '/apt-get/ s|$| '"$PROXY"'|' Dockerfile
- sed -i 's/^bisheng_langchain.*/bisheng_langchain = "'$RELEASE_VERSION'"/g' pyproject.toml
- sed -i '6i\RUN pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple' Dockerfile
- sed -i '7i\RUN poetry source add --priority=supplemental foo http://'$NEXUS_PUBLIC':'$NEXUS_PUBLIC_PASSWORD'@'$REPO2'simple' Dockerfile
- sed -i '8i\RUN poetry source add --priority=primary qh https://pypi.tuna.tsinghua.edu.cn/simple' Dockerfile
@@ -218,34 +193,10 @@ steps: # 定义流水线执行步骤,这些步骤将顺序执行
- git clone https://github.com/dataelement/bisheng.git .
- git checkout $DRONE_COMMIT
- name: package # 流水线名称
pull: if-not-exists
image: python:3.10-slim # 定义创建容器的Docker镜像
volumes: # 将容器内目录挂载到宿主机,仓库需要开启Trusted设置
- name: bisheng-cache
path: /app/build # 将应用打包好的Jar和执行脚本挂载出来
environment:
RELEASE_VERSION: 99.99.90
NEXUS_USER:
from_secret: NEXUS_USER
NEXUS_PASSWORD:
from_secret: NEXUS_PASSWORD
REPO:
from_secret: PY_NEXUS
commands: # 定义在Docker容器中执行的shell命令
- pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple
- pip install Cython
- pip install wheel
- pip install twine
- cd ./src/bisheng-langchain
- python setup.py bdist_wheel
- twine upload --verbose -u $NEXUS_USER -p $NEXUS_PASSWORD --repository-url $REPO dist/*.whl
- name: set poetry
pull: if-not-exists
image: golang
environment:
RELEASE_VERSION: 99.99.90
NEXUS_PUBLIC:
from_secret: NEXUS_PUBLIC
NEXUS_PUBLIC_PASSWORD:
@@ -259,11 +210,9 @@ steps: # 定义流水线执行步骤,这些步骤将顺序执行
path: /app/build/
commands:
- cd ./src/backend
- cp -r /app/build/nltk_data ./
- echo $REPO
- REPO2=$(echo $REPO | sed 's/http:\\/\\///g')
- sed '/apt-get/ s|$| '"$PROXY"'|' Dockerfile
- sed -i 's/^bisheng_langchain.*/bisheng_langchain = "'$RELEASE_VERSION'"/g' pyproject.toml
- sed -i '6i\RUN pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple' Dockerfile
- sed -i '7i\RUN poetry source add --priority=supplemental foo http://'$NEXUS_PUBLIC':'$NEXUS_PUBLIC_PASSWORD'@'$REPO2'simple' Dockerfile
- sed -i '8i\RUN poetry source add --priority=primary qh https://pypi.tuna.tsinghua.edu.cn/simple' Dockerfile
-34
View File
@@ -14,41 +14,8 @@ concurrency:
cancel-in-progress: true
jobs:
build_bisheng_langchain:
runs-on: ubuntu-latest
#if: startsWith(github.event.ref, 'refs/tags')
steps:
- name: checkout
uses: actions/checkout@v2
- name: Get version
id: get_version
run: |
echo ::set-output name=VERSION::${GITHUB_REF/refs\/tags\//}
- name: Set Environment Variable
run: echo "RELEASE_VERSION=1.3.1" >> $GITHUB_ENV
# 构建 bisheng_langchain
- name: Set python version 3.10
uses: actions/setup-python@v1
with:
python-version: 3.10.*
- name: Build PyPi bisheng-langchain and push
id: pypi_build_bisheng_langchain
run: |
pip install Cython
pip install wheel
pip install twine
cd ./src/bisheng-langchain
python setup.py bdist_wheel
set +e
twine upload dist/* -u ${{ secrets.PYPI_USER }} -p ${{ secrets.PYPI_PASSWORD }} --repository pypi
set -e
build_bisheng_backend:
needs: build_bisheng_langchain
runs-on: ubuntu-latest
# if: startsWith(github.event.ref, 'refs/tags')
steps:
@@ -87,7 +54,6 @@ jobs:
docker buildx build --file ./src/backend/Dockerfile --platform linux/amd64 --provenance false --tag ${{ env.DOCKERHUB_REPO }}bisheng-backend:${{ steps.get_version.outputs.VERSION }}-amd64 --push ./src/backend/
build_backend_arm:
needs: build_bisheng_langchain
runs-on: ubuntu-22.04-arm
steps:
- name: checkout
+1 -1
View File
@@ -79,7 +79,7 @@ typings/
.node_repl_history
# Output of 'npm pack'
*.tgz
# *.tgz
# Yarn Integrity file
.yarn-integrity
+7 -3
View File
@@ -1,5 +1,7 @@
#!/bin/bash
export PYTHONPATH="./"
start_mode=${1:-api}
if [ $start_mode = "api" ]; then
@@ -8,10 +10,12 @@ if [ $start_mode = "api" ]; then
elif [ $start_mode = "worker" ]; then
echo "Starting Celery worker..."
# 处理知识库相关任务的worker
nohup celery -A bisheng.worker.main worker -l info -c 20 -P threads -Q knowledge_celery &
nohup celery -A bisheng.worker.main worker -l info -c 20 -P threads -Q knowledge_celery -n knowledge@%h &
# 工作流执行worker
celery -A bisheng.worker.main worker -l info -c 100 -P threads -Q workflow_celery
nohup celery -A bisheng.worker.main worker -l info -c 100 -P threads -Q workflow_celery -n workflow@%h &
python bisheng/linsight/worker.py --worker_num 4 --max_concurrency 5
else
echo "Invalid start mode. Use 'api' or 'celery'."
echo "Invalid start mode. Use 'api' or 'worker'."
exit 1
fi
+3 -3
View File
@@ -53,7 +53,7 @@ services:
backend:
container_name: bisheng-backend
image: dataelement/bisheng-backend:v1.3.1
image: dataelement/bisheng-backend:v2.0.0
ports:
- "7860:7860"
environment:
@@ -93,7 +93,7 @@ services:
backend_worker:
container_name: bisheng-backend-worker
image: dataelement/bisheng-backend:v1.3.1
image: dataelement/bisheng-backend:v2.0.0
environment:
TZ: Asia/Shanghai
BS_MILVUS_CONNECTION_ARGS: '{"host":"milvus","port":"19530","user":"","password":"","secure":false}'
@@ -125,7 +125,7 @@ services:
frontend:
container_name: bisheng-frontend
image: dataelement/bisheng-frontend:v1.3.1
image: dataelement/bisheng-frontend:v2.0.0
ports:
- "3001:3001"
environment:
+9 -22
View File
@@ -25,7 +25,14 @@ server {
add_header X-Frame-Options SAMEORIGIN;
}
location /api {
location /workspace/ {
alias /usr/share/nginx/html/client/;
index index.html index.htm;
try_files $uri $uri/ /workspace/index.html;
}
location ~ ^(/workspace)?/api(/|$) {
rewrite ^/workspace(/.*)$ $1 break;
proxy_pass http://backend:7860;
proxy_read_timeout 300s;
proxy_set_header Host $host;
@@ -34,27 +41,7 @@ server {
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
client_max_body_size 50m;
add_header Access-Control-Allow-Origin $host;
add_header X-Frame-Options SAMEORIGIN;
}
location /workspace/ {
alias /usr/share/nginx/html/client/;
index index.html index.htm;
try_files $uri $uri/ /workspace/index.html;
}
location /workspace/api {
rewrite ^/workspace(/.*)$ $1 break;
proxy_pass http://backend:7860;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
client_max_body_size 50m;
client_max_body_size 200m;
add_header Access-Control-Allow-Origin $host;
add_header X-Frame-Options SAMEORIGIN;
}
-1
View File
@@ -1,4 +1,3 @@
# 毕昇后端代码
* Dockerfile 使用 poetry 进行 Python 依赖管理
+1
View File
@@ -0,0 +1 @@
config.yaml
+1 -1
View File
@@ -6,7 +6,7 @@ from bisheng.processing.process import load_flow_from_json # noqa: E402
try:
# 通过ci去自动修改
__version__ = '1.3.1'
__version__ = '2.0.0'
except metadata.PackageNotFoundError:
# Case where package metadata is not available.
__version__ = ''
@@ -19,3 +19,5 @@
### 108 模型管理
### 109 知识库模块
### 110 灵思模块
@@ -0,0 +1,6 @@
from bisheng.api.errcode.base import BaseErrorCode
class SopFileError(BaseErrorCode):
Code: int = 11010
Msg: str = 'SOP文件格式不符合要求'
+6 -3
View File
@@ -1,14 +1,15 @@
# Router for base api
from fastapi import APIRouter
from bisheng.api.v1 import (assistant_router, audit_router, chat_router, component_router,
endpoints_router, evaluation_router, finetune_router, flows_router,
group_router, knowledge_router, llm_router, mark_router, qa_router,
report_router, server_router, skillcenter_router, tag_router,
user_router, validate_router, variable_router, workflow_router,
workstation_router)
workstation_router, linsight_router, tool_router, invite_code_router)
from bisheng.api.v2 import (assistant_router_rpc, chat_router_rpc, flow_router,
knowledge_router_rpc, rpc_router_rpc, workflow_router_rpc,
workstation_router_rpc)
from fastapi import APIRouter
router = APIRouter(prefix='/api/v1', )
router.include_router(chat_router)
@@ -33,7 +34,9 @@ router.include_router(llm_router)
router.include_router(workflow_router)
router.include_router(mark_router)
router.include_router(workstation_router)
router.include_router(linsight_router)
router.include_router(tool_router)
router.include_router(invite_code_router)
router_rpc = APIRouter(prefix='/api/v2', )
router_rpc.include_router(knowledge_router_rpc)
router_rpc.include_router(chat_router_rpc)
+1 -82
View File
@@ -7,7 +7,7 @@ from loguru import logger
from bisheng.api.errcode.assistant import (AssistantInitError, AssistantNameRepeatError,
AssistantNotEditError, AssistantNotExistsError, ToolTypeRepeatError,
ToolTypeNotExistsError, ToolTypeIsPresetError)
ToolTypeIsPresetError)
from bisheng.api.errcode.base import UnAuthorizedError, NotFoundError
from bisheng.api.services.assistant_agent import AssistantAgent
from bisheng.api.services.assistant_base import AssistantUtils
@@ -476,87 +476,6 @@ class AssistantService(BaseService, AssistantUtils):
GroupResourceDao.insert_group_batch(batch_resource)
return True
@classmethod
async def update_gpts_tools(cls, user: UserPayload, req: GptsToolsTypeRead) -> UnifiedResponseModel:
"""
更新工具类别,包括更新工具类别的名称和删除、新增工具类别的API
"""
# 尝试解析下openapi schema看下是否可以正常解析, 不能的话保存不允许保存
tool_service = ToolServices()
if req.is_preset == ToolPresetType.API.value:
await tool_service.parse_openapi_schema('', req.openapi_schema)
elif req.is_preset == ToolPresetType.MCP.value:
await tool_service.parse_mcp_schema(req.openapi_schema)
exist_tool_type = GptsToolsDao.get_one_tool_type(req.id)
if not exist_tool_type:
return ToolTypeNotExistsError.return_resp()
if req.name.__len__() > 1000 or req.name.__len__() == 0:
return resp_500(message="名字不符合规范:至少1个字符,不能超过1000个字符")
# 判断工具类别名称是否重复
tool_type = GptsToolsDao.get_one_tool_type_by_name(user.user_id, req.name)
if tool_type and tool_type.id != exist_tool_type.id:
return ToolTypeRepeatError.return_resp()
# 判断是否有更新权限
if not user.access_check(exist_tool_type.user_id, str(exist_tool_type.id), AccessType.GPTS_TOOL_WRITE):
return UnAuthorizedError.return_resp()
exist_tool_type.name = req.name
exist_tool_type.logo = req.logo
exist_tool_type.description = req.description
exist_tool_type.server_host = req.server_host
exist_tool_type.auth_method = req.auth_method
exist_tool_type.api_key = req.api_key
exist_tool_type.auth_type = req.auth_type
exist_tool_type.openapi_schema = req.openapi_schema
tool_extra = {"api_location": req.api_location, "parameter_name": req.parameter_name}
exist_tool_type.extra = json.dumps(tool_extra, ensure_ascii=False)
children_map = {}
for one in req.children:
save_key = GptsToolsDao.get_tool_key(exist_tool_type.id, one.tool_key)
save_key_prefix = save_key.split("_")[0]
if one.tool_key.startswith(save_key_prefix):
# 说明api和数据库的一致,没有通过openapiSchema重新解析
children_map[one.tool_key] = one
else:
children_map[save_key] = one
# 获取此类别下旧的API列表
old_tool_list = GptsToolsDao.get_list_by_type([exist_tool_type.id])
# 需要被删除的工具列表
delete_tool_id_list = []
# 需要被更新的工具列表
update_tool_list = []
for one in old_tool_list:
# 说明此工具 需要删除
if children_map.get(one.tool_key) is None:
delete_tool_id_list.append(one.id)
else:
# 说明此工具需要更新
new_tool_info = children_map.pop(one.tool_key)
one.name = new_tool_info.name
one.desc = new_tool_info.desc
one.extra = new_tool_info.extra
one.api_params = new_tool_info.api_params
update_tool_list.append(one)
add_children = []
for one in children_map.values():
one.id = None
one.user_id = user.user_id
one.is_preset = exist_tool_type.is_preset
one.is_delete = 0
add_children.append(one)
GptsToolsDao.update_tool_type(exist_tool_type, delete_tool_id_list,
add_children, update_tool_list)
children = GptsToolsDao.get_list_by_type([exist_tool_type.id])
res = GptsToolsTypeRead(**exist_tool_type.model_dump(), children=children)
return resp_200(data=res)
@classmethod
def delete_gpts_tools(cls, user: UserPayload, tool_type_id: int) -> UnifiedResponseModel:
""" 删除工具类别 """
@@ -210,6 +210,11 @@ class Etl4lmLoader(BasePDFLoader):
except requests.Timeout as e:
logger.error(f"Request to etl4lm API timed out: {e}")
raise Exception("etl4lm服务繁忙,请升级etl4lm服务的算力")
except Exception as e:
if str(e).find("Timeout") != -1:
logger.error(f"Request to etl4lm API timed out: {e}")
raise Exception("etl4lm服务繁忙,请升级etl4lm服务的算力")
raise e
if resp.status_code != 200:
raise Exception(
f"file partition {os.path.basename(self.file_name)} failed resp={resp.text}"
+22 -23
View File
@@ -1,37 +1,36 @@
import asyncio
import os
import io
import json
from typing import List
from bisheng.api.services.llm import LLMService
from bisheng.utils import generate_uuid
from fastapi import UploadFile, HTTPException
import pandas as pd
import os
from collections import defaultdict
from copy import deepcopy
from typing import List
from bisheng.api.services.user_service import UserPayload
from bisheng.api.v1.schemas import (UnifiedResponseModel, resp_200, StreamData, BuildStatus)
from bisheng.cache import InMemoryCache
from bisheng.database.models.flow import FlowDao
from bisheng.database.models.flow_version import FlowVersionDao
from bisheng.database.models.assistant import AssistantDao
from bisheng.api.services.flow import FlowService
from bisheng.database.models.evaluation import (Evaluation, EvaluationDao, ExecType, EvaluationTaskStatus)
from bisheng.database.models.user import UserDao
from bisheng.utils.minio_client import MinioClient
from fastapi.encoders import jsonable_encoder
from bisheng.utils.logger import logger
from bisheng.api.services.assistant_agent import AssistantAgent
import pandas as pd
from bisheng_ragas import evaluate
from bisheng_ragas.llms.langchain import LangchainLLM
from bisheng_ragas.metrics import AnswerCorrectnessBisheng
from datasets import Dataset
from bisheng_langchain.gpts.utils import import_by_type
from bisheng.cache.redis import redis_client
from fastapi import UploadFile, HTTPException
from fastapi.encoders import jsonable_encoder
from bisheng.api.services.assistant_agent import AssistantAgent
from bisheng.api.services.flow import FlowService
from bisheng.api.services.llm import LLMService
from bisheng.api.services.user_service import UserPayload
from bisheng.api.utils import build_flow, build_input_keys_response
from bisheng.api.v1.schemas import (UnifiedResponseModel, resp_200)
from bisheng.cache import InMemoryCache
from bisheng.cache.redis import redis_client
from bisheng.database.models.assistant import AssistantDao
from bisheng.database.models.evaluation import (Evaluation, EvaluationDao, ExecType, EvaluationTaskStatus)
from bisheng.database.models.flow import FlowDao
from bisheng.database.models.flow_version import FlowVersionDao
from bisheng.database.models.user import UserDao
from bisheng.graph.graph.base import Graph
from bisheng.utils import generate_uuid
from bisheng.utils.logger import logger
from bisheng.utils.minio_client import MinioClient
flow_data_store = redis_client
@@ -277,7 +276,7 @@ def add_evaluation_task(evaluation_id: int):
for index, one in enumerate(csv_data):
messages = asyncio.run(gpts_agent.run(one.get('question')))
if len(messages):
one["answer"] = messages[0].content
one["answer"] = messages[-1].content
current_progress += progress_increment
redis_client.set(redis_key, round(current_progress))
@@ -0,0 +1,52 @@
import random
import string
class VoucherGenerator:
def __init__(self, length=10):
self.length = length
# 排除相像的字母和数字: 'I', 'l', 'O', '0', '1'
self.characters = ''.join(set(string.ascii_letters + string.digits) - set('IlOo01'))
self.weights = [7, 9, 10, 5, 8, 4, 2, 1, 3] # 加权因子
self.check_digits = ['1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2'] # 校验码对应表
def generate_voucher(self):
voucher_base = ''.join(random.choices(self.characters, k=self.length - 1))
check_digit = self.calculate_check_digit(voucher_base)
return voucher_base + check_digit
def calculate_check_digit(self, voucher_base):
total = sum(self.weights[i] * (ord(char) - ord('A') if char.isalpha() else int(char)) for i, char in
enumerate(voucher_base))
remainder = total % 11
return self.check_digits[remainder]
def validate_voucher(self, voucher):
if len(voucher) != 10:
return False, "Invalid voucher length"
voucher_base = voucher[:-1]
provided_check_digit = voucher[-1]
calculated_check_digit = self.calculate_check_digit(voucher_base)
if provided_check_digit == calculated_check_digit:
return True, "Valid voucher"
else:
return False, "Invalid voucher"
# 示例用法
if __name__ == "__main__":
generator = VoucherGenerator()
voucher_code = generator.generate_voucher() # 生成一个唯一的兑换码
print(f"Generated voucher code: {voucher_code}")
# 验证兑换码
is_valid, info = generator.validate_voucher(voucher_code)
print(f"Is valid: {is_valid}, Info: {info}")
# 尝试验证一个无效的兑换码
invalid_voucher_code = 'ABCDEFGHJK967'
is_valid, info = generator.validate_voucher(invalid_voucher_code)
print(f"Is valid: {is_valid}, Info: {info}")
@@ -0,0 +1,112 @@
from loguru import logger
from bisheng.api.services.invite_code.code_validator import VoucherGenerator
from bisheng.api.services.user_service import UserPayload
from bisheng.database.models.invite_code import InviteCode, InviteCodeDao
from bisheng.utils import generate_uuid
class InviteCodeService:
@classmethod
async def use_invite_code(cls, user_id: int) -> bool:
"""
使用邀请码
:param user_id: 用户ID
:return: 邀请码使用结果
"""
logger.debug(f"use_invite_code {user_id}")
codes = await InviteCodeDao.get_user_bind_code(user_id)
for one in codes:
flag = await InviteCodeDao.use_invite_code(user_id, one.code)
if flag:
logger.debug(f"use_invite_code {user_id}, {one.code} success")
return True
return False
@classmethod
async def revoke_invite_code(cls, user_id: int) -> bool:
"""
撤销邀请码
:param user_id: 用户ID
:return: 邀请码撤销结果
"""
logger.debug(f"revoke_invite_code {user_id}")
codes = await InviteCodeDao.get_user_all_code(user_id)
for one in codes:
# 说明是崭新的邀请码,未被使用
if one.used <= 0:
continue
flag = await InviteCodeDao.revoke_invite_code_used(user_id, one.code)
if flag:
logger.debug(f"revoke_invite_code {user_id}, {one.code} success")
return True
return False
@classmethod
async def create_batch_invite_codes(cls, login_user: UserPayload, name: str, num: int, limit: int) -> list[str]:
"""
批量创建邀请码
:param login_user: 操作用户信息
:param name: 邀请码名称
:param num: 邀请码数量
:param limit: 每个邀请码的使用次数
:return: 创建的邀请码列表
"""
generator = VoucherGenerator()
code_list = []
batch_id = generate_uuid()
for i in range(num):
code_list.append(InviteCode(
code=generator.generate_voucher(),
batch_id=batch_id,
batch_name=name,
limit=limit,
created_id=login_user.user_id,
))
# 检查生成的邀请码是否重复
unique_codes = []
for code in code_list:
if code.code in unique_codes:
raise ValueError(f"Duplicate invite code found: {code.code}")
unique_codes.append(code.code)
# 调用数据库操作来保存邀请码
await InviteCodeDao.insert_invite_code(code_list)
return unique_codes
@classmethod
async def get_invite_code_num(cls, login_user: UserPayload) -> int:
"""
获取用户可用的邀请码的使用次数
:param login_user: 操作用户信息
:return: 邀请码使用次数
"""
nums = 0
codes = await InviteCodeDao.get_user_bind_code(login_user.user_id)
for one in codes:
nums += one.limit - one.used
return nums
@classmethod
async def bind_invite_code(cls, login_user: UserPayload, code: str) -> (bool, str):
"""
绑定邀请码
:param login_user: 操作用户信息
:param code: 邀请码
:return: 绑定结果
"""
generator = VoucherGenerator()
flag, _ = generator.validate_voucher(code)
if not flag:
return False, "您输入的邀请码无效"
codes = await InviteCodeDao.get_user_bind_code(login_user.user_id)
if codes:
return False, "已绑定其他邀请码"
flag = await InviteCodeDao.bind_invite_code(login_user.user_id, code)
return flag, "邀请码绑定成功" if flag else "您输入的邀请码无效"
@@ -71,7 +71,7 @@ from bisheng.worker.knowledge import file_worker
class KnowledgeService(KnowledgeUtils):
@classmethod
def get_knowledge(
async def get_knowledge(
cls,
request: Request,
login_user: UserPayload,
@@ -82,17 +82,15 @@ class KnowledgeService(KnowledgeUtils):
) -> (List[KnowledgeRead], int):
if not login_user.is_admin():
knowledge_id_extra = []
user_role = UserRoleDao.get_user_roles(login_user.user_id)
user_role = await UserRoleDao.aget_user_roles(login_user.user_id)
if user_role:
role_ids = [role.role_id for role in user_role]
role_access = RoleAccessDao.get_role_access(
role_ids, AccessType.KNOWLEDGE
)
role_access = await RoleAccessDao.aget_role_access(role_ids, AccessType.KNOWLEDGE)
if role_access:
knowledge_id_extra = [
int(access.third_id) for access in role_access
]
res = KnowledgeDao.get_user_knowledge(
res = await KnowledgeDao.aget_user_knowledge(
login_user.user_id,
knowledge_id_extra,
knowledge_type,
@@ -100,14 +98,14 @@ class KnowledgeService(KnowledgeUtils):
page,
limit,
)
total = KnowledgeDao.count_user_knowledge(
total = await KnowledgeDao.acount_user_knowledge(
login_user.user_id, knowledge_id_extra, knowledge_type, name
)
else:
res = KnowledgeDao.get_all_knowledge(
res = await KnowledgeDao.aget_all_knowledge(
name, knowledge_type, page=page, limit=limit
)
total = KnowledgeDao.count_all_knowledge(name, knowledge_type)
total = await KnowledgeDao.acount_all_knowledge(name, knowledge_type)
result = cls.convert_knowledge_read(login_user, res)
return result, total
@@ -2,7 +2,7 @@ import json
import os
import re
import time
from typing import Any, Dict, List, Optional, BinaryIO
from typing import Any, Dict, List, Optional, BinaryIO, Union
import requests
from bisheng_langchain.rag.extract_info import extract_title
@@ -319,7 +319,7 @@ def delete_knowledge_file_vectors(file_ids: List[int], clear_minio: bool = True)
def decide_vectorstores(
collection_name: str, vector_store: str, embedding: Embeddings
) -> VectorStore:
) -> Union[VectorStore, Any]:
"""vector db"""
param: dict = {"embedding": embedding}
@@ -646,8 +646,8 @@ def parse_document_title(title: str) -> str:
def read_chunk_text(
input_file,
file_name,
separator: List[str],
separator_rule: List[str],
separator: Optional[List[str]],
separator_rule: Optional[List[str]],
chunk_size: int,
chunk_overlap: int,
knowledge_id: Optional[int] = None,
@@ -656,6 +656,8 @@ def read_chunk_text(
force_ocr: int = 1,
filter_page_header_footer: int = 0,
excel_rule: ExcelRule = None,
no_summary: bool = False,
) -> (List[str], List[dict], str, Any): # type: ignore
"""
0chunks text
@@ -664,14 +666,17 @@ def read_chunk_text(
3: ocr bbox data: maybe None
"""
# 获取文档总结标题的llm
try:
llm = decide_knowledge_llm()
knowledge_llm = LLMService.get_knowledge_llm()
except Exception as e:
logger.exception("knowledge_llm_error:")
raise Exception(
f"文档知识库总结模型已失效,请前往模型管理-系统模型设置中进行配置。{str(e)}"
)
llm = None
if not no_summary:
try:
llm = decide_knowledge_llm()
knowledge_llm = LLMService.get_knowledge_llm()
except Exception as e:
logger.exception("knowledge_llm_error:")
raise Exception(
f"文档知识库总结模型已失效,请前往模型管理-系统模型设置中进行配置。{str(e)}"
)
text_splitter = ElemCharacterTextSplitter(
separators=separator,
separator_rule=separator_rule,
@@ -751,7 +756,7 @@ def read_chunk_text(
documents = loader.load()
elif file_extension_name in ["txt", "md"]:
loader = filetype_load_map[file_extension_name](file_path=input_file)
loader = filetype_load_map[file_extension_name](file_path=input_file, autodetect_encoding=True)
documents = loader.load()
else:
if etl_for_lm_url:
@@ -0,0 +1,73 @@
from starlette.websockets import WebSocket
from bisheng.linsight.state_message_manager import LinsightStateMessageManager, MessageData, MessageEventType
class MessageStreamHandle(object):
def __init__(self, websocket: 'WebSocket', session_version_id: str):
"""
初始化 MessageStreamHandle
:param websocket:
"""
self._websocket = websocket
self.session_version_id = session_version_id
self._state_message_manager: LinsightStateMessageManager = LinsightStateMessageManager(
session_version_id=session_version_id)
async def send_message(self, message_data: str) -> None:
"""
发送消息到 WebSocket
:param message_data: 要发送的消息内容
"""
await self._websocket.send_text(message_data)
async def receive_message(self) -> str:
"""
接收来自 WebSocket 的消息
:return:
"""
return await self._websocket.receive_text()
async def send_json(self, json_data: dict) -> None:
"""
发送 JSON 数据到 WebSocket
:param json_data: 要发送的 JSON 数据
"""
await self._websocket.send_json(json_data)
async def receive_json(self) -> dict:
"""
接收来自 WebSocket 的 JSON 数据
:return:
"""
return await self._websocket.receive_json()
# 处理 WebSocket 连接的生命周期事件
async def connect(self) -> None:
"""
连接到 WebSocket
"""
await self._websocket.accept()
while True:
try:
message = await self._state_message_manager.pop_message()
if message:
await self.send_json(message.model_dump())
if message.event_type in [MessageEventType.ERROR_MESSAGE, MessageEventType.TASK_TERMINATED,
MessageEventType.FINAL_RESULT]:
await self._websocket.close(code=1000, reason="Session finished or error occurred")
break
except Exception as e:
await self.send_json(
MessageData(event_type=MessageEventType.ERROR_MESSAGE, data={"error": str(e)}).model_dump())
await self._websocket.close(code=1000, reason=f"Error: {str(e)}")
break
async def disconnect(self) -> None:
"""
断开 WebSocket 连接
"""
await self._websocket.close(code=1000, reason="Client disconnected")
@@ -0,0 +1,583 @@
import io
import json
import uuid
from typing import List, Dict
import openpyxl
from fastapi import UploadFile
from langchain_core.documents import Document
from langchain_core.embeddings import Embeddings
from langchain_text_splitters import RecursiveCharacterTextSplitter
from loguru import logger
from bisheng.api.errcode.base import NotFoundError, ServerError
from bisheng.api.errcode.linsight import SopFileError
from bisheng.api.services.knowledge_imp import decide_vectorstores, extract_code_blocks
from bisheng.api.services.llm import LLMService
from bisheng.api.services.user_service import UserPayload
from bisheng.api.v1.schema.inspiration_schema import SOPManagementSchema, SOPManagementUpdateSchema
from bisheng.api.v1.schema.linsight_schema import SopRecordRead
from bisheng.api.v1.schemas import UnifiedResponseModel, resp_200
from bisheng.core.app_context import app_ctx
from bisheng.database.models.linsight_sop import LinsightSOP, LinsightSOPDao, LinsightSOPRecord
from bisheng.database.models.llm_server import LLMDao, LLMModelType
from bisheng.database.models.user import UserDao
from bisheng.interface.embeddings.custom import FakeEmbedding
from bisheng.interface.llms.custom import BishengLLM
from bisheng.settings import settings
from bisheng.utils import util
from bisheng.utils.embedding import decide_embeddings
from bisheng_langchain.rag.init_retrievers import KeywordRetriever, BaselineVectorRetriever
from bisheng_langchain.retrievers import EnsembleRetriever
from bisheng_langchain.vectorstores import ElasticKeywordsSearch, Milvus
class SOPManageService:
__doc__ = "灵思SOP管理服务"
collection_name = "col_linsight_sop"
@staticmethod
async def generate_sop_summary(sop_content: str, llm: BishengLLM = None) -> Dict[str, str]:
"""生成SOP摘要"""
default_summary = {"sop_title": "SOP名称", "sop_description": "SOP描述"}
try:
if llm is None:
workbench_conf = await LLMService.get_workbench_llm()
linsight_conf = settings.get_linsight_conf()
llm = BishengLLM(model_id=workbench_conf.task_model.id, temperature=linsight_conf.default_temperature)
prompt_service = app_ctx.get_prompt_loader()
prompt_obj = prompt_service.render_prompt(
namespace="sop",
prompt_name="gen_sop_summary",
sop_detail=sop_content
)
prompt = [
("system", prompt_obj.prompt.system),
("user", prompt_obj.prompt.user)
]
response = await llm.ainvoke(prompt)
if not response.content:
return default_summary
code_ret = extract_code_blocks(response.content)
if code_ret:
return json.loads(code_ret[0])
return json.loads(response.content)
except Exception as e:
logger.exception(f"生成SOP摘要失败: {e}")
return default_summary
@staticmethod
async def add_sop_record(sop_record: LinsightSOPRecord) -> LinsightSOPRecord:
"""
添加SOP记录
"""
if not sop_record.description:
sop_summary = await SOPManageService.generate_sop_summary(sop_record.content, None)
sop_record.description = sop_summary["sop_description"]
return await LinsightSOPDao.create_sop_record(sop_record)
@staticmethod
async def get_sop_record(keyword: str = None, sort: str = None, page: int = 1, page_size: int = 10) -> \
(List[SopRecordRead], int):
"""
根据关键词查询SOP记录
"""
user_ids = []
if keyword:
# 如果有关键词,先获取用户ID列表
user_ids = await UserDao.afilter_users(user_ids=[], keyword=keyword)
user_ids = [one.user_id for one in user_ids]
res = await LinsightSOPDao.filter_sop_record(keyword, user_ids, page, page_size, sort)
count = await LinsightSOPDao.count_sop_record(keyword, user_ids)
if not res:
return [], 0
all_users = await UserDao.afilter_users(user_ids=[one.user_id for one in res])
all_users = {
one.user_id: one.user_name for one in all_users
}
result = []
for one in res:
new_one = SopRecordRead.model_validate(one)
new_one.user_name = all_users.get(one.user_id, str(one.user_id))
result.append(new_one)
return result, count
@staticmethod
async def update_sop_record_score(session_version_id: str, score: int) -> None:
await LinsightSOPDao.update_sop_record_score(session_version_id, score)
@classmethod
async def sync_sop_record(cls, record_ids: list[int], override: bool = False, save_new: bool = False) \
-> list[str] | None:
"""
同步SOP记录
:param record_ids: SOP记录ID列表
:param override: 是否覆盖已有的SOP
:param save_new: 是否保存新的SOP
:return: 如果有重复的SOP记录,返回重复的记录名称列表,否则返回None
"""
sop_records = await LinsightSOPDao.get_sop_record_by_ids(record_ids)
return await cls._sync_sop_record(sop_records, override, save_new)
@staticmethod
async def _sync_sop_record(sop_records: list[LinsightSOPRecord], override: bool = False, save_new: bool = False) \
-> list[str] | None:
"""
如果有重复的SOP记录,返回重复的记录名称列表
"""
records_name_dict = {}
repeat_names = set()
name_set = set()
sop_list = []
oversize_records = []
new_records = []
for one in sop_records:
if len(one.content) > 50000:
oversize_records.append(one.name)
continue
new_records.append(one)
if one.name not in name_set:
records_name_dict[one.name] = one
name_set.add(one.name)
sop_records = new_records
if not sop_records and oversize_records:
raise ValueError(f"{''.join(oversize_records)}内容超长")
if name_set:
sop_list = await LinsightSOPDao.get_sops_by_names(list(name_set))
for one in sop_list:
repeat_names.add(one.name)
if override:
# 先更新已有的sop库
override_name_dict = {}
for one in sop_list:
if one_record := records_name_dict.get(one.name):
await SOPManageService.update_sop(SOPManagementUpdateSchema(
id=one.id,
name=one.name,
description=one_record.description,
content=one_record.content,
rating=one_record.rating,
))
override_name_dict[one.name] = True
# 再新增剩下的sop记录
for one in records_name_dict.values():
if one.name in override_name_dict:
continue
await SOPManageService.add_sop(SOPManagementSchema(
name=one.name,
description=one.description,
content=one.content,
rating=one.rating,
), one.user_id)
elif save_new:
for one in sop_records:
new_name = one.name
if new_name in repeat_names:
# 如果有重复的记录,添加后缀, 长度限制500个字符
new_name = f"{one.name}副本"
await SOPManageService.add_sop(SOPManagementSchema(
name=new_name,
description=one.description,
content=one.content,
rating=one.rating,
), one.user_id)
else:
# 说明有重复的记录,需要用户确认
if sop_list:
return list(repeat_names)
# 将记录插入到数据库中
for one in sop_records:
await SOPManageService.add_sop(SOPManagementSchema(
name=one.name,
description=one.description,
content=one.content,
rating=one.rating,
), one.user_id)
if oversize_records:
raise ValueError(f"{''.join(oversize_records)}内容超长")
return None
@classmethod
async def parse_sop_file(cls, file: UploadFile) -> (list, list):
"""
解析SOP文件
:param file: 文件路径
"""
if not file.size:
raise NotFoundError.http_exception(msg="未找到上传的指导手册文件")
error_rows = []
success_rows = []
wb = None
try:
wb = openpyxl.load_workbook(io.BytesIO(file.file.read()), read_only=True, data_only=True)
sheet = wb.active
max_rows = sheet.max_row
for i in range(2, max_rows + 1):
name = sheet.cell(row=i, column=1).value
description = sheet.cell(row=i, column=2).value
content = sheet.cell(row=i, column=3).value
error_msg = []
if not name:
error_msg.append("缺少名称")
if not content:
error_msg.append("缺少详细内容")
if len(str(name)) >= 500:
error_msg.append("名称长度超过500字符")
if len(str(content)) >= 50000:
error_msg.append("详细内容长度超过50000字符")
if description and len(str(description)) >= 1000:
error_msg.append("描述长度超过1000字符")
if error_msg:
error_msg = "".join(error_msg)
error_rows.append(f"• 第{i}行: {error_msg}")
else:
success_rows.append({
"name": str(name),
"description": str(description) if description is not None else "",
"content": str(content),
})
finally:
if wb:
wb.close()
return success_rows, error_rows
@classmethod
async def upload_sop_file(cls, login_user: UserPayload, file: UploadFile, ignore_error: bool, override: bool,
save_new: bool) \
-> list[str] | None:
"""
上传SOP文件
:param login_user: 登录用户信息
:param file: 文件路径
:param ignore_error: 是否忽略错误
:param override: 是否覆盖已有的SOP
:param save_new: 是否保存新的SOP
:return: 上传结果
"""
success_rows, error_rows = await cls.parse_sop_file(file)
error_msg = "\n".join(error_rows)
if (error_rows or len(success_rows) == 0) and not ignore_error:
raise SopFileError.http_exception(
msg=f"共计划导入{len(success_rows) + len(error_rows)}条指导手册,格式正确{len(success_rows)}条,错误{len(error_rows)}条:\n {error_msg}")
if not success_rows:
return None
records = [LinsightSOPRecord(**one, user_id=login_user.user_id) for one in success_rows]
return await cls._sync_sop_record(records, override=override, save_new=save_new)
@staticmethod
async def add_sop(sop_obj: SOPManagementSchema, user_id) -> UnifiedResponseModel | None:
"""
添加新的SOP
:param user_id:
:param sop_obj:
:return: 添加的SOP对象
"""
# 获取当前全局配置的embedding模型
workbench_conf = await LLMService.get_workbench_llm()
try:
emb_model_id = workbench_conf.embedding_model.id
if not emb_model_id:
raise ServerError.http_exception(msg="未配置知识库embedding模型,请从工作台配置中设置")
except AttributeError:
raise ServerError.http_exception(msg="工作台配置中未找到指导手册 embedding模型,请从工作台配置中设置")
# 校验embedding模型
embed_info = LLMDao.get_model_by_id(int(emb_model_id))
if not embed_info:
raise ServerError.http_exception(msg="知识库embedding模型不存在,请从工作台配置中设置")
if embed_info.model_type != LLMModelType.EMBEDDING.value:
raise ValueError("知识库embedding模型类型错误,请从工作台配置中设置")
vector_store_id = uuid.uuid4().hex
embeddings = decide_embeddings(emb_model_id)
try:
vector_client: Milvus = decide_vectorstores(
SOPManageService.collection_name, "Milvus", embeddings
)
es_client: ElasticKeywordsSearch = decide_vectorstores(
SOPManageService.collection_name, "ElasticKeywordsSearch", FakeEmbedding()
)
metadatas = [{"vector_store_id": vector_store_id}]
vector_client.add_texts([sop_obj.content[0:10000]], metadatas=metadatas)
es_client.add_texts([sop_obj.content], ids=[vector_store_id], metadatas=metadatas)
except Exception as e:
raise ServerError.http_exception(msg=f"添加指导手册失败,向向量存储添加数据失败: {str(e)}")
sop_dict = sop_obj.model_dump(exclude_unset=True)
sop_dict["vector_store_id"] = vector_store_id # 设置向量存储ID
# 这里可以添加数据库操作,将sop_obj保存到数据库中
sop_model = LinsightSOP(**sop_dict)
sop_model.user_id = user_id
sop_model = await LinsightSOPDao.create_sop(sop_model)
if not sop_model:
raise ServerError.http_exception(msg="添加指导手册失败")
return resp_200(data=sop_model)
@staticmethod
async def update_sop(sop_obj: SOPManagementUpdateSchema) -> UnifiedResponseModel | None:
"""
更新SOP
:param sop_obj:
:return: 更新后的SOP对象
"""
# 校验SOP是否存在
existing_sop = await LinsightSOPDao.get_sops_by_ids([sop_obj.id])
if not existing_sop:
raise NotFoundError.http_exception(msg="指导手册不存在")
if sop_obj.content != existing_sop[0].content:
# 获取当前全局配置的embedding模型
workbench_conf = await LLMService.get_workbench_llm()
try:
emb_model_id = workbench_conf.embedding_model.id
if not emb_model_id:
raise ServerError.http_exception(msg="未配置知识库embedding模型,请从工作台配置中设置")
except AttributeError:
raise ServerError.http_exception(msg="工作台配置中未找到指导手册 embedding模型,请从工作台配置中设置")
vector_store_id = existing_sop[0].vector_store_id
embeddings = decide_embeddings(emb_model_id)
# 更新向量存储
try:
vector_client: Milvus = decide_vectorstores(
SOPManageService.collection_name, "Milvus", embeddings
)
es_client: ElasticKeywordsSearch = decide_vectorstores(
SOPManageService.collection_name, "ElasticKeywordsSearch", FakeEmbedding()
)
vector_client.delete(expr=f"vector_store_id == '{vector_store_id}'")
es_client.delete([vector_store_id])
metadatas = [{"vector_store_id": vector_store_id}]
vector_client.add_texts([sop_obj.content[0:10000]], metadatas=metadatas)
es_client.add_texts([sop_obj.content], ids=[vector_store_id], metadatas=metadatas)
except Exception as e:
raise ServerError.http_exception(msg=f"更新指导手册失败,向向量存储更新数据失败: {str(e)}")
# 更新数据库中的SOP
sop_model = await LinsightSOPDao.update_sop(sop_obj)
return resp_200(data=sop_model)
@staticmethod
async def remove_sop(sop_ids: list[int], login_user: UserPayload) -> UnifiedResponseModel | None:
"""
删除SOP
:param login_user:
:param sop_ids: SOP唯一ID列表
:return: 删除结果
"""
if not sop_ids:
raise NotFoundError.http_exception(msg="指导手册 ID列表不能为空")
# 校验SOP是否存在
existing_sops = await LinsightSOPDao.get_sops_by_ids(sop_ids)
if not existing_sops:
return resp_200(data=True)
# 删除向量存储中的数据
try:
vector_store_ids = [sop.vector_store_id for sop in existing_sops]
vector_client: Milvus = decide_vectorstores(
SOPManageService.collection_name, "Milvus", FakeEmbedding()
)
es_client: ElasticKeywordsSearch = decide_vectorstores(
SOPManageService.collection_name, "ElasticKeywordsSearch", FakeEmbedding()
)
vector_client.delete(expr=f"vector_store_id in {vector_store_ids}")
es_client.delete(vector_store_ids)
except Exception as e:
raise ServerError.http_exception(msg=f"删除指导手册失败,向向量存储删除数据失败: {str(e)}")
# 删除数据库中的SOP
await LinsightSOPDao.remove_sop(sop_ids=sop_ids)
return resp_200(data=True)
# sop 库检索
@classmethod
async def search_sop(cls, query: str, k: int = 3) -> (List[Document], str | None):
"""
搜索SOP
:param k:
:param query: 搜索关键词
:return: 搜索结果
"""
# 获取当前全局配置的embedding模型
try:
vector_search = True
es_search = True
error_msg = None
workbench_conf = await LLMService.get_workbench_llm()
if workbench_conf.embedding_model is None or not workbench_conf.embedding_model.id:
vector_search = False
error_msg = "请联系管理员检查工作台向量检索模型状态"
else:
try:
emb_model_id = workbench_conf.embedding_model.id
embeddings = decide_embeddings(emb_model_id)
await embeddings.aembed_query("test")
except Exception as e:
logger.error(f"向量检索模型初始化失败: {str(e)}")
vector_search = False
error_msg = "请联系管理员检查工作台向量检索模型状态"
# 创建文本分割器
text_splitter = RecursiveCharacterTextSplitter()
retrievers = []
if vector_search and es_search:
emb_model_id = workbench_conf.embedding_model.id
embeddings = decide_embeddings(emb_model_id)
vector_client: Milvus = decide_vectorstores(
SOPManageService.collection_name, "Milvus", embeddings
)
es_client: ElasticKeywordsSearch = decide_vectorstores(
SOPManageService.collection_name, "ElasticKeywordsSearch", FakeEmbedding()
)
keyword_retriever = KeywordRetriever(keyword_store=es_client, search_kwargs={"k": 100},
text_splitter=text_splitter)
baseline_vector_retriever = BaselineVectorRetriever(vector_store=vector_client,
search_kwargs={"k": 100},
text_splitter=text_splitter)
retrievers = [keyword_retriever, baseline_vector_retriever]
elif es_search and not vector_search:
# 仅使用关键词检索
es_client: ElasticKeywordsSearch = decide_vectorstores(
SOPManageService.collection_name, "ElasticKeywordsSearch", FakeEmbedding()
)
keyword_retriever = KeywordRetriever(keyword_store=es_client, search_kwargs={"k": 100},
text_splitter=text_splitter)
retrievers = [keyword_retriever]
elif vector_search and not es_search:
# 仅使用向量检索
emb_model_id = workbench_conf.embedding_model.id
embeddings = decide_embeddings(emb_model_id)
vector_client: Milvus = decide_vectorstores(
SOPManageService.collection_name, "Milvus", embeddings
)
baseline_vector_retriever = BaselineVectorRetriever(vector_store=vector_client,
search_kwargs={"k": 100},
text_splitter=text_splitter)
retrievers = [baseline_vector_retriever]
else:
error_msg = "指导手册检索失败,向量检索与关键词检索均不可用"
return [], error_msg
retriever = EnsembleRetriever(retrievers=retrievers, weights=[0.5, 0.5] if len(retrievers) > 1 else [1.0])
# 执行检索
results = await retriever.ainvoke(input=query)
if not results:
return [], error_msg
vector_store_ids = [doc.metadata.get("vector_store_id") for doc in results if
doc.metadata.get("vector_store_id")]
# 根据vector_store_ids查询库中的sop
sop_models = await LinsightSOPDao.get_sop_by_vector_store_ids(vector_store_ids)
sop_model_vector_store_ids = [sop.vector_store_id for sop in sop_models]
# 过滤结果,确保只返回存在于数据库中的SOP
results = [doc for doc in results if doc.metadata.get("vector_store_id") in sop_model_vector_store_ids]
# 过滤完取前k条结果
results = results[:k]
return results, error_msg
except Exception as e:
logger.error(f"搜索指导手册失败: {str(e)}")
return [], f"指导手册检索失败: {str(e)}"
# 重建SOP VectorStore
@classmethod
async def rebuild_sop_vector_store_task(cls, embeddings: Embeddings):
"""
重建SOP向量存储
:return: 重建结果
"""
try:
# 获取所有SOP
all_sops = await LinsightSOPDao.get_all_sops()
if not all_sops:
logger.info("没有SOP数据需要重建向量存储")
return None
# 包装同步函数为异步函数
def sync_func(sops, emb):
"""
同步函数,用于重建SOP向量存储
:param emb:
:param sops:
:return:
"""
vector_client: Milvus = decide_vectorstores(
SOPManageService.collection_name, "Milvus", emb
)
# 删除现有的向量存储collection
if vector_client.col is not None:
logger.info("删除现有的SOP向量存储collection")
vector_client.col.drop()
vector_client.col = None
vector_client.fields = []
metadatas = [{"vector_store_id": sop.vector_store_id} for sop in sops]
contents = [sop.content for sop in sops]
batch_size = 16
for i in range(0, len(contents), batch_size):
batch_contents = contents[i:i + batch_size]
batch_metadatas = metadatas[i:i + batch_size]
# 添加新的SOP数据到向量存储
vector_client.add_texts(batch_contents, metadatas=batch_metadatas)
logger.info("SOP向量存储重建完成: {}".format(len(sops)))
# 使用run_async运行同步函数
await util.sync_func_to_async(sync_func)(all_sops, embeddings)
return None
except Exception as e:
logger.exception(f"重建SOP向量存储失败: {str(e)}")
return None
# if __name__ == '__main__':
# # 测试代码
# results, error_msg = asyncio.run(SOPManageService.search_sop(query="投标文件编写指南", k=3))
#
# print(results)
# print(error_msg)
@@ -0,0 +1,958 @@
import asyncio
import os
import uuid
from dataclasses import dataclass
from io import BytesIO
from typing import Dict, List, Optional, AsyncGenerator, Tuple, Any
from urllib.parse import unquote
from fastapi import UploadFile
from langchain_core.tools import BaseTool
from loguru import logger
from bisheng.api.services.assistant_agent import AssistantAgent
from bisheng.api.services.knowledge_imp import read_chunk_text, decide_vectorstores
from bisheng.api.services.linsight.sop_manage import SOPManageService
from bisheng.api.services.llm import LLMService
from bisheng.api.services.tool import ToolServices
from bisheng.api.services.user_service import UserPayload
from bisheng.api.services.workstation import WorkStationService
from bisheng.api.v1.schema.linsight_schema import LinsightQuestionSubmitSchema, BatchDownloadFilesSchema, \
SubmitFileSchema
from bisheng.cache.redis import redis_client
from bisheng.cache.utils import save_file_to_folder, CACHE_DIR
from bisheng.core.app_context import app_ctx
from bisheng.database.models import LinsightSessionVersion
from bisheng.database.models.flow import FlowType
from bisheng.database.models.knowledge import KnowledgeRead, KnowledgeTypeEnum
from bisheng.database.models.linsight_execute_task import LinsightExecuteTaskDao
from bisheng.database.models.linsight_session_version import LinsightSessionVersionDao, SessionVersionStatusEnum
from bisheng.database.models.linsight_sop import LinsightSOPRecord
from bisheng.database.models.session import MessageSessionDao, MessageSession
from bisheng.interface.embeddings.custom import FakeEmbedding
from bisheng.interface.llms.custom import BishengLLM
from bisheng.settings import settings
from bisheng.utils import util
from bisheng.utils.embedding import decide_embeddings
from bisheng.utils.minio_client import minio_client
from bisheng.utils.util import calculate_md5
from bisheng_langchain.linsight.const import ExecConfig
@dataclass
class TaskNode:
"""任务节点,用于构建任务树"""
task: Any # LinsightExecuteTask 对象
children: List['TaskNode'] = None
def __post_init__(self):
if self.children is None:
self.children = []
def to_dict(self) -> Dict:
"""将任务节点转换为字典格式"""
task_dict = self.task.model_dump()
task_dict['children'] = [child.to_dict() for child in self.children]
return task_dict
class LinsightWorkbenchImpl:
"""Linsight工作台实现类"""
# 类常量
COLLECTION_NAME_PREFIX = "col_linsight_file_"
FILE_INFO_REDIS_KEY_PREFIX = "linsight_file:"
CACHE_EXPIRATION_HOURS = 24
class LinsightError(Exception):
"""Linsight相关错误"""
pass
class SearchSOPError(Exception):
"""SOP检索错误"""
def __init__(self, message: str):
super().__init__(message)
self.message = message
class ToolsInitializationError(Exception):
"""工具初始化错误"""
pass
class BishengLLMError(Exception):
"""Bisheng LLM相关错误"""
pass
@classmethod
async def _get_llm(cls) -> (BishengLLM, Any):
# 获取并验证工作台配置
workbench_conf = await cls._get_workbench_config()
# 创建LLM实例
linsight_conf = settings.get_linsight_conf()
llm = BishengLLM(model_id=workbench_conf.task_model.id, temperature=linsight_conf.default_temperature)
return llm, workbench_conf
@classmethod
async def submit_user_question(cls, submit_obj: LinsightQuestionSubmitSchema,
login_user: UserPayload) -> tuple[MessageSession, LinsightSessionVersion]:
"""
提交用户问题并创建会话
Args:
submit_obj: 提交的问题对象
login_user: 登录用户信息
Returns:
tuple: (消息会话模型, 灵思会话版本模型)
Raises:
LinsightError: 当创建会话失败时
"""
try:
# 生成唯一会话ID
chat_id = uuid.uuid4().hex
# 创建消息会话
message_session = MessageSession(
chat_id=chat_id,
flow_id='',
flow_name='新对话',
flow_type=FlowType.LINSIGHT.value,
user_id=login_user.user_id
)
await MessageSessionDao.async_insert_one(message_session)
# 处理文件(如果存在)
processed_files = await cls._process_submitted_files(submit_obj.files, chat_id)
# 创建灵思会话版本
linsight_session_version = LinsightSessionVersion(
session_id=chat_id,
user_id=login_user.user_id,
question=submit_obj.question,
tools=submit_obj.tools,
org_knowledge_enabled=submit_obj.org_knowledge_enabled,
personal_knowledge_enabled=submit_obj.personal_knowledge_enabled,
files=processed_files
)
linsight_session_version = await LinsightSessionVersionDao.insert_one(linsight_session_version)
return message_session, linsight_session_version
except Exception as e:
logger.error(f"提交用户问题失败: {str(e)}")
raise cls.LinsightError(f"提交用户问题失败: {str(e)}")
@classmethod
async def _process_submitted_files(cls, files: Optional[List[SubmitFileSchema]], chat_id: str) -> Optional[List]:
"""
处理提交的文件
Args:
files: 文件列表
chat_id: 会话ID
Returns:
处理后的文件列表
"""
if not files:
return None
file_ids = []
for file in files:
if file.parsing_status != "completed":
raise cls.LinsightError(f"文件 {file.file_name} 解析状态不正确: {file.parsing_status}")
file_ids.append(file.file_id)
redis_keys = [f"{cls.FILE_INFO_REDIS_KEY_PREFIX}{file_id}" for file_id in file_ids]
processed_files = await redis_client.amget(redis_keys)
for file_info in processed_files:
if file_info:
await cls._copy_file_to_session_storage(file_info, chat_id)
return processed_files
@classmethod
async def _copy_file_to_session_storage(cls, file_info: Dict, chat_id: str) -> None:
"""
复制文件到会话存储
Args:
file_info: 文件信息
chat_id: 会话ID
"""
source_object_name = file_info.get("markdown_file_path")
if source_object_name:
original_filename = file_info.get("original_filename")
markdown_filename = f"{original_filename.rsplit('.', 1)[0]}.md"
new_object_name = f"linsight/{chat_id}/{source_object_name}"
minio_client.copy_object(
source_object_name=source_object_name,
target_object_name=new_object_name,
bucket_name=minio_client.tmp_bucket,
target_bucket_name=minio_client.bucket
)
file_info["markdown_file_path"] = new_object_name
file_info["markdown_filename"] = markdown_filename
@classmethod
async def task_title_generate(cls, question: str, chat_id: str,
login_user: UserPayload) -> Dict:
"""
生成任务标题
Args:
question: 用户问题
chat_id: 会话ID
login_user: 登录用户信息
Returns:
包含任务标题的字典
"""
try:
llm, _ = await cls._get_llm()
# 生成prompt
prompt = await cls._generate_title_prompt(question)
# 生成任务标题
task_title = await llm.ainvoke(prompt)
if not task_title.content:
raise ValueError("生成任务标题失败,请检查模型配置或输入内容")
# 更新会话标题
await cls._update_session_title(chat_id, task_title.content)
return {
"task_title": task_title.content,
"chat_id": chat_id,
"error_message": None
}
except Exception as e:
logger.error(f"生成任务标题失败: {str(e)}")
return {
"task_title": "新对话",
"chat_id": chat_id,
"error_message": str(e)
}
@classmethod
async def _get_workbench_config(cls):
"""获取并验证工作台配置"""
workbench_conf = await LLMService.get_workbench_llm()
if not workbench_conf or not workbench_conf.task_model:
raise cls.BishengLLMError("任务已终止,请联系管理员检查灵思任务执行模型状态")
return workbench_conf
@classmethod
async def _generate_title_prompt(cls, question: str) -> List[Tuple[str, str]]:
"""生成标题生成的prompt"""
prompt_service = app_ctx.get_prompt_loader()
prompt_obj = prompt_service.render_prompt(
namespace="gen_title",
prompt_name="linsight",
USER_GOAL=question
)
return [
("system", prompt_obj.prompt.system),
("user", prompt_obj.prompt.user)
]
@classmethod
async def _update_session_title(cls, chat_id: str, title: str) -> None:
"""更新会话标题"""
session = await MessageSessionDao.async_get_one(chat_id)
if session:
session.flow_name = title
await MessageSessionDao.async_insert_one(session)
@classmethod
async def get_linsight_session_version_list(cls, session_id: str) -> List[LinsightSessionVersion]:
"""
获取灵思会话版本列表
Args:
session_id: 会话ID
Returns:
灵思会话版本列表
"""
return await LinsightSessionVersionDao.get_session_versions_by_session_id(session_id)
@classmethod
async def modify_sop(cls, linsight_session_version_id: str, sop_content: str) -> Dict:
"""
修改灵思会话版本的SOP内容
Args:
linsight_session_version_id: 会话版本ID
sop_content: SOP内容
Returns:
操作结果
"""
try:
await LinsightSessionVersionDao.modify_sop_content(
linsight_session_version_id=linsight_session_version_id,
sop_content=sop_content
)
return {"success": True, "message": "modify sop content successfully"}
except Exception as e:
logger.error(f"修改SOP内容失败: {str(e)}")
return {"success": False, "message": str(e)}
@classmethod
async def generate_sop(cls, linsight_session_version_id: str,
previous_session_version_id: str,
feedback_content: Optional[str] = None,
reexecute: bool = False,
login_user: Optional[UserPayload] = None,
knowledge_list: List[KnowledgeRead] = None) -> AsyncGenerator[Dict, None]:
"""
生成SOP内容
Args:
linsight_session_version_id: 当前会话版本ID
previous_session_version_id: 上一个会话版本ID
feedback_content: 反馈内容
reexecute: 是否重新执行
login_user: 登录用户信息
knowledge_list: 知识库列表
Yields:
生成的SOP内容事件
"""
error_message = None
try:
# 获取工作台配置和会话版本
session_version = await cls._get_session_version(linsight_session_version_id)
if login_user.user_id != session_version.user_id:
yield {"event": "error", "data": "无权限操作该会话版本"}
return
try:
# 创建LLM和工具
llm, workbench_conf = await cls._get_llm()
except Exception as e:
logger.error(f"生成SOP内容失败: session_version_id={linsight_session_version_id}, error={str(e)}")
raise cls.BishengLLMError(str(e))
tools = await cls._prepare_tools(session_version, llm)
# 准备历史摘要
history_summary = await cls._prepare_history_summary(
reexecute, previous_session_version_id
)
# 创建代理并生成SOP
agent = await cls._create_linsight_agent(session_version, llm, tools, workbench_conf)
if previous_session_version_id:
session_version = await LinsightSessionVersionDao.get_by_id(previous_session_version_id)
content = ""
async for res in cls._generate_sop_content(
agent, session_version, feedback_content, history_summary, knowledge_list
):
if isinstance(res, cls.SearchSOPError):
yield {"event": "search_sop_error", "data": str(res.message)}
continue
content += res.content
yield {
"event": "generate_sop_content",
"data": res.model_dump_json()
}
# 更新SOP内容
await LinsightSessionVersionDao.modify_sop_content(
linsight_session_version_id=linsight_session_version_id,
sop_content=content
)
logger.info(f"生成SOP内容成功: session_version_id={linsight_session_version_id}")
except cls.ToolsInitializationError as e:
logger.exception(
f"初始化灵思工作台工具失败: session_version_id={linsight_session_version_id}, error={str(e)}")
error_message = f"初始化灵思工作台工具失败: {str(e)}"
except cls.BishengLLMError as e:
logger.exception(f"Bisheng LLM错误: session_version_id={linsight_session_version_id}, error={str(e)}")
error_message = str(e)
except Exception as e:
logger.exception(f"生成SOP内容失败: session_version_id={linsight_session_version_id}, error={str(e)}")
error_message = f"生成SOP内容失败: {str(e)}"
finally:
if error_message:
session_version = await LinsightSessionVersionDao.get_by_id(linsight_session_version_id)
if session_version:
session_version.sop = error_message
session_version.status = SessionVersionStatusEnum.SOP_GENERATION_FAILED
await LinsightSessionVersionDao.insert_one(session_version)
yield {"event": "error", "data": error_message}
@classmethod
async def _get_session_version(cls, session_version_id: str) -> LinsightSessionVersion:
"""获取会话版本"""
session_version = await LinsightSessionVersionDao.get_by_id(session_version_id)
if not session_version:
raise cls.LinsightError("灵思会话版本不存在")
return session_version
@classmethod
async def _prepare_tools(cls, session_version: LinsightSessionVersion,
llm: BishengLLM) -> List[BaseTool]:
"""准备工具列表"""
try:
tools = await cls.init_linsight_config_tools(session_version, llm)
root_path = os.path.join(CACHE_DIR, "linsight", session_version.id)
os.makedirs(root_path, exist_ok=True)
linsight_tools = await ToolServices.init_linsight_tools(root_path=root_path)
tools.extend(linsight_tools)
return tools
except Exception as e:
raise cls.ToolsInitializationError(f"初始化灵思工作台工具失败: {str(e)}")
@classmethod
async def prepare_file_list(cls, session_version: LinsightSessionVersion) -> List[str]:
"""准备文件列表"""
file_list = []
template_str = """@{filename}的文件储存信息:{{'文件储存在语义检索库中的id':'{file_id}','文件储存地址':'{markdown}'}}@"""
if not session_version.files:
return file_list
for file in session_version.files:
file_list.append(template_str.format(filename=file['original_filename'],
file_id=file['file_id'],
markdown=f"./{file['markdown_filename']}"))
return file_list
@classmethod
async def prepare_knowledge_list(cls, knowledge_list: list[KnowledgeRead]) -> List[str]:
res = []
if not knowledge_list:
return res
# 查询是否有个人知识库
template_str = """@{name}的储存信息:{{'知识库储存在语义检索库中的id':'{id}'}}@"""
for one in knowledge_list:
if one.type == KnowledgeTypeEnum.PRIVATE.value:
res.append(template_str.format(name="个人知识库", id=one.id))
else:
knowledge_str = template_str.format(name=one.name, id=one.id)
if one.description:
knowledge_str += f"{one.name}的描述是{one.description}"
res.append(knowledge_str)
return res
@classmethod
async def _prepare_history_summary(cls, reexecute: bool,
previous_session_version_id: str) -> List[str]:
"""准备历史摘要"""
history_summary = []
if reexecute and previous_session_version_id:
execute_tasks = await LinsightExecuteTaskDao.get_by_session_version_id(previous_session_version_id)
for task in execute_tasks:
if task.result:
answer = task.result.get("answer", "")
if answer:
history_summary.append(answer)
return history_summary
@classmethod
async def _create_linsight_agent(cls, session_version: LinsightSessionVersion,
llm: BishengLLM, tools: List[BaseTool],
workbench_conf):
"""创建Linsight代理"""
from bisheng_langchain.linsight.agent import LinsightAgent
root_path = os.path.join(CACHE_DIR, "linsight", session_version.id[:8])
linsight_conf = settings.get_linsight_conf()
exec_config = ExecConfig(**linsight_conf.model_dump(), debug_id=session_version.id)
return LinsightAgent(
file_dir=root_path,
query=session_version.question,
llm=llm,
tools=tools,
task_mode=workbench_conf.linsight_executor_mode,
exec_config=exec_config,
)
@classmethod
async def _generate_sop_content(cls, agent, session_version: LinsightSessionVersion,
feedback_content: Optional[str],
history_summary: List[str],
knowledge_list: List[KnowledgeRead] = None) -> AsyncGenerator:
"""生成SOP内容"""
file_list = await cls.prepare_file_list(session_version)
knowledge_list = await cls.prepare_knowledge_list(knowledge_list)
if feedback_content is None:
# 检索SOP模板
sop_template, search_sop_error_msg = await SOPManageService.search_sop(
query=session_version.question, k=3
)
if search_sop_error_msg:
logger.error(f"检索SOP模板失败: {search_sop_error_msg}")
yield cls.SearchSOPError(message=search_sop_error_msg)
sop_template = "\n\n".join([
f"例子:\n\n{sop.page_content}"
for sop in sop_template if sop.page_content
])
async for res in agent.generate_sop(sop=sop_template, file_list=file_list, knowledge_list=knowledge_list):
yield res
else:
sop_template = session_version.sop if session_version.sop else ""
async for res in agent.feedback_sop(
sop=sop_template,
feedback=feedback_content,
history_summary=history_summary if history_summary else None,
file_list=file_list,
knowledge_list=knowledge_list
):
yield res
@classmethod
async def get_execute_task_detail(cls, session_version_id: str,
login_user: Optional[UserPayload] = None):
"""
获取执行任务详情
Args:
session_version_id: 灵思会话版本ID
login_user: 登录用户信息
Returns:
执行任务详情列表
"""
execute_tasks = await LinsightExecuteTaskDao.get_by_session_version_id(session_version_id)
if not execute_tasks:
return []
# 1. 获取一级任务 parent_task_id 是 None 的任务
root_tasks = [task for task in execute_tasks if task.parent_task_id is None]
# 2. 根据previous_task_id与next_task_id排序一级任务
def sort_tasks_by_chain(tasks: List[Any]) -> List[Any]:
"""
根据任务链排序任务列表
previous_task_id是None则是第一个任务,next_task_id是None则是最后一个任务
"""
if not tasks:
return []
# 创建任务字典以便快速查找
task_dict = {task.id: task for task in tasks}
# 找到链的开始节点(previous_task_id 为 None
start_tasks = [task for task in tasks if task.previous_task_id is None]
sorted_tasks = []
for start_task in start_tasks:
# 从每个开始节点构建任务链
current_task = start_task
chain = []
while current_task is not None:
chain.append(current_task)
# 通过next_task_id找到下一个任务
next_task_id = current_task.next_task_id
current_task = task_dict.get(next_task_id) if next_task_id else None
sorted_tasks.extend(chain)
# 处理可能存在的孤立任务(既没有previous也没有next指向它们)
processed_ids = {task.id for task in sorted_tasks}
orphan_tasks = [task for task in tasks if task.id not in processed_ids]
sorted_tasks.extend(orphan_tasks)
return sorted_tasks
# 排序一级任务
sorted_root_tasks = sort_tasks_by_chain(root_tasks)
# 3. 构建任务树 使用 parent_task_id 将子任务与父任务关联起来
def build_task_tree(parent_tasks: List[Any], all_tasks: List[Any]) -> List[TaskNode]:
"""
构建任务树
"""
# 创建任务映射
task_map = {task.id: task for task in all_tasks}
# 按父任务ID分组子任务
children_map = {}
for task in all_tasks:
if task.parent_task_id:
if task.parent_task_id not in children_map:
children_map[task.parent_task_id] = []
children_map[task.parent_task_id].append(task)
def build_node(task: Any) -> TaskNode:
"""递归构建任务节点"""
node = TaskNode(task=task)
# 获取子任务
child_tasks = children_map.get(task.id, [])
# 对子任务进行排序
sorted_child_tasks = sort_tasks_by_chain(child_tasks)
# 递归构建子节点
for child_task in sorted_child_tasks:
child_node = build_node(child_task)
node.children.append(child_node)
return node
# 构建根节点列表
root_nodes = []
for parent_task in parent_tasks:
root_node = build_node(parent_task)
root_nodes.append(root_node)
return root_nodes
# 构建任务树
task_tree = build_task_tree(sorted_root_tasks, execute_tasks)
# 4. 返回任务树的根节点列表
result = [node.to_dict() for node in task_tree]
return result
@classmethod
async def upload_file(cls, file: UploadFile) -> Dict:
"""
上传文件到灵思工作台
Args:
file: 上传的文件
Returns:
文件信息字典
"""
# 生成文件信息
file_id = uuid.uuid4().hex[:8] # 生成8位唯一文件ID
# url 编码 decode 文件名
original_filename = unquote(file.filename)
file_extension = original_filename.split('.')[-1] if '.' in original_filename else ''
unique_filename = f"{file_id}.{file_extension}"
# 保存文件
file_path = await save_file_to_folder(file, 'linsight', unique_filename)
upload_result = {
"file_id": file_id,
"filename": unique_filename,
"original_filename": original_filename,
"file_path": file_path,
"parsing_status": "running",
}
# 缓存解析结果
await cls._cache_parse_result(file_id, upload_result)
return upload_result
@classmethod
async def parse_file(cls, upload_result: Dict) -> Dict:
"""
解析上传的文件
Args:
upload_result: 上传结果
Returns:
解析结果
"""
logger.info(f"开始解析文件: {upload_result}")
file_id = upload_result["file_id"]
original_filename = upload_result["original_filename"]
file_path = upload_result["file_path"]
try:
# 获取工作台配置
workbench_conf = await cls._get_workbench_config()
collection_name = f"{cls.COLLECTION_NAME_PREFIX}{workbench_conf.embedding_model.id}"
# 异步执行文件解析
parse_result = await util.sync_func_to_async(cls._parse_file_sync)(file_id, file_path, original_filename,
collection_name, workbench_conf)
# 缓存解析结果
await cls._cache_parse_result(file_id, parse_result)
logger.info(f"文件解析完成: {parse_result}")
except Exception as e:
logger.error(f"文件解析失败: file_id={file_id}, error={str(e)}")
parse_result = {
"file_id": file_id,
"original_filename": original_filename,
"parsing_status": "failed",
"error_message": str(e)
}
await cls._cache_parse_result(file_id, parse_result)
return parse_result
@classmethod
def _parse_file_sync(cls, file_id: str, file_path: str, original_filename: str,
collection_name: str, workbench_conf) -> Dict:
"""
同步解析文件
Args:
file_id: 文件ID
file_path: 文件路径
original_filename: 原始文件名
collection_name: 集合名称
workbench_conf: 工作台配置
Returns:
解析结果
"""
# 读取文件内容
try:
texts, _, parse_type, _ = read_chunk_text(
input_file=file_path,
file_name=original_filename,
separator=['\n\n', '\n'],
separator_rule=['after', 'after'],
chunk_size=1000,
chunk_overlap=100,
no_summary=True
)
# 生成markdown内容
markdown_content = "\n".join(texts)
markdown_bytes = markdown_content.encode('utf-8')
# 保存markdown文件
markdown_filename = f"{file_id}.md"
minio_client.upload_tmp(markdown_filename, markdown_bytes)
markdown_md5 = calculate_md5(markdown_bytes)
# 处理向量存储
cls._process_vector_storage(texts, file_id, collection_name, workbench_conf)
return {
"file_id": file_id,
"original_filename": original_filename,
"parsing_status": "completed",
"parse_type": parse_type,
"markdown_filename": markdown_filename,
"markdown_file_path": markdown_filename,
"markdown_file_md5": markdown_md5,
"embedding_model_id": workbench_conf.embedding_model.id,
"collection_name": collection_name
}
except Exception as e:
logger.error(f"文件解析失败: file_id={file_id}, error={str(e)}")
return {
"file_id": file_id,
"original_filename": original_filename,
"parsing_status": "failed",
"error_message": str(e)
}
@classmethod
def _process_vector_storage(cls, texts: List[str], file_id: str,
collection_name: str, workbench_conf) -> None:
"""处理向量存储"""
# 创建embeddings
embeddings = decide_embeddings(workbench_conf.embedding_model.id)
# 创建向量存储
vector_client = decide_vectorstores(collection_name, "Milvus", embeddings)
es_client = decide_vectorstores(collection_name, "ElasticKeywordsSearch", FakeEmbedding())
# 添加文本到向量存储
metadatas = [{"file_id": file_id} for _ in texts]
vector_client.add_texts(texts, metadatas=metadatas)
es_client.add_texts(texts, metadatas=metadatas)
@classmethod
async def _cache_parse_result(cls, file_id: str, parse_result: Dict) -> None:
"""缓存解析结果"""
key = f"{cls.FILE_INFO_REDIS_KEY_PREFIX}{file_id}"
await redis_client.aset(
key=key,
value=parse_result,
expiration=60 * 60 * cls.CACHE_EXPIRATION_HOURS
)
@classmethod
async def init_linsight_config_tools(cls, session_version: LinsightSessionVersion,
llm: BishengLLM) -> List[BaseTool]:
"""
初始化灵思配置的工具
Args:
session_version: 会话版本模型
llm: LLM实例
Returns:
工具列表
"""
tools = []
if not session_version.tools:
return tools
# 提取工具ID
tool_ids = cls._extract_tool_ids(session_version.tools)
# 获取工作台配置的工具ID
ws_config = await WorkStationService.aget_config()
config_tool_ids = cls._extract_tool_ids(ws_config.linsightConfig.tools or [])
# 过滤有效的工具ID
valid_tool_ids = [tid for tid in tool_ids if tid in config_tool_ids]
# 初始化工具
if valid_tool_ids:
tools.extend(await AssistantAgent.init_tools_by_tool_ids(valid_tool_ids, llm=llm))
return tools
@classmethod
def _extract_tool_ids(cls, tools: List[Dict]) -> List[int]:
"""
从工具配置中提取工具ID
Args:
tools: 工具配置列表
Returns:
工具ID列表
"""
tool_ids = []
for tool in tools:
if tool.get("children"):
tool_ids.extend(int(child.get("id")) for child in tool["children"] if child.get("id"))
return tool_ids
@classmethod
async def feedback_regenerate_sop_task(cls, session_version_model: LinsightSessionVersion,
feedback: str) -> None:
"""
根据反馈重新生成SOP任务
Args:
session_version_model: 灵思会话版本模型
feedback: 反馈内容
"""
try:
file_list = await cls.prepare_file_list(session_version_model)
# 创建LLM和工具
llm, workbench_conf = await cls._get_llm()
tools = await cls._prepare_tools(session_version_model, llm)
# 获取历史摘要
history_summary = await cls._get_history_summary(session_version_model.id)
# 创建代理并生成SOP
agent = await cls._create_linsight_agent(session_version_model, llm, tools, workbench_conf)
sop_content = ""
sop_template = session_version_model.sop or ''
async for res in agent.feedback_sop(
sop=sop_template,
feedback=feedback,
history_summary=history_summary if history_summary else None,
file_list=file_list
):
sop_content += res.content
# sop写到记录表里,这个sop不需要关联会话,因为不需要更新分数
await SOPManageService.add_sop_record(LinsightSOPRecord(
name=session_version_model.title,
description=None,
user_id=session_version_model.user_id,
content=sop_content,
))
except cls.ToolsInitializationError as e:
logger.exception(f"初始化灵思工作台工具失败: session_version_id={session_version_model.id}, error={str(e)}")
except Exception as e:
logger.exception(f"反馈重新生成SOP任务失败: session_version_id={session_version_model.id}, error={str(e)}")
@classmethod
async def _get_history_summary(cls, session_version_id: str) -> List[str]:
"""获取历史摘要"""
history_summary = []
execute_tasks = await LinsightExecuteTaskDao.get_by_session_version_id(session_version_id)
for task in execute_tasks:
if task.result:
answer = task.result.get("answer", "")
if answer:
history_summary.append(answer)
return history_summary
@classmethod
async def batch_download_files(cls, file_info_list: List[BatchDownloadFilesSchema]) -> bytes:
"""
批量下载文件
Args:
file_info_list: 文件信息列表
Returns:
包含文件下载信息的列表
"""
async def download_file(file_info: BatchDownloadFilesSchema) -> Tuple[str, bytes]:
"""下载单个文件"""
object_name = file_info.file_url
object_name = object_name.replace(f"/{minio_client.bucket}/", "")
try:
bytes_io = BytesIO()
file_byte = await util.sync_func_to_async(minio_client.get_object)(bucket_name=minio_client.bucket,
object_name=object_name)
bytes_io.write(file_byte)
bytes_io.seek(0)
return file_info.file_name, bytes_io.getvalue()
except Exception as e:
logger.error(f"下载文件失败 {object_name}: {e}")
return object_name, b''
# 批量下载文件
download_tasks = [download_file(file_info) for file_info in file_info_list]
results = await asyncio.gather(*download_tasks)
# 过滤掉下载失败的文件
successful_files = [res for res in results if res[1]]
if not successful_files:
raise ValueError("没有成功下载的文件,无法生成ZIP")
zip_bytes = util.bytes_to_zip(successful_files)
return zip_bytes
+47 -2
View File
@@ -1,7 +1,7 @@
import json
from typing import List, Optional
from fastapi import Request
from fastapi import Request, BackgroundTasks
from langchain_core.embeddings import Embeddings
from langchain_core.language_models import BaseChatModel
from loguru import logger
@@ -10,11 +10,12 @@ from bisheng.api.errcode.base import NotFoundError
from bisheng.api.errcode.llm import ServerExistError, ModelNameRepeatError, ServerAddError, ServerAddAllError
from bisheng.api.services.user_service import UserPayload
from bisheng.api.v1.schemas import LLMServerInfo, LLMModelInfo, KnowledgeLLMConfig, AssistantLLMConfig, \
EvaluationLLMConfig, AssistantLLMItem, LLMServerCreateReq
EvaluationLLMConfig, AssistantLLMItem, LLMServerCreateReq, WorkbenchModelConfig
from bisheng.database.models.config import ConfigDao, ConfigKeyEnum, Config
from bisheng.database.models.llm_server import LLMDao, LLMServer, LLMModel, LLMModelType
from bisheng.interface.importing import import_by_type
from bisheng.interface.initialize.loading import instantiate_llm, instantiate_embedding
from bisheng.utils.embedding import decide_embeddings
class LLMService:
@@ -406,3 +407,47 @@ class LLMService:
ret.append(LLMServerInfo(**one.dict(exclude={'config'}), models=model_dict[one.id]))
return ret
@classmethod
async def update_workbench_llm(cls, config_obj: WorkbenchModelConfig, background_tasks: BackgroundTasks):
"""
更新灵思模型配置
:param config_obj:
:return:
"""
config = await ConfigDao.aget_config(ConfigKeyEnum.LINSIGHT_LLM)
if not config:
config = Config(key=ConfigKeyEnum.LINSIGHT_LLM.value, value='{}')
if config_obj.embedding_model:
# 判断是否一致
config_old_obj = WorkbenchModelConfig(**json.loads(config.value)) if config else WorkbenchModelConfig()
if (config_obj.embedding_model.id and config_old_obj.embedding_model is None or
config_obj.embedding_model.id != config_old_obj.embedding_model.id):
embeddings = decide_embeddings(config_obj.embedding_model.id)
try:
await embeddings.aembed_query("test")
except Exception as e:
raise Exception(f"Embedding模型初始化失败: {str(e)}")
from bisheng.api.services.linsight.sop_manage import SOPManageService
background_tasks.add_task(SOPManageService.rebuild_sop_vector_store_task, embeddings)
config.value = json.dumps(config_obj.model_dump(), ensure_ascii=False)
await ConfigDao.async_insert_config(config)
return config_obj
@classmethod
async def get_workbench_llm(cls) -> WorkbenchModelConfig:
"""
获取工作台模型配置
:return:
"""
ret = {}
config = await ConfigDao.aget_config(ConfigKeyEnum.LINSIGHT_LLM)
if config:
ret = json.loads(config.value)
return WorkbenchModelConfig(**ret)
@@ -63,12 +63,30 @@ def unmerge_and_read_sheet(sheet_obj):
"""
if sheet_obj.max_row == 0 or sheet_obj.max_column == 0:
return []
max_row = sheet_obj.max_row
max_column = sheet_obj.max_column
data_grid = [
[None for _ in range(sheet_obj.max_column)] for _ in range(sheet_obj.max_row)
[None for _ in range(max_column)] for _ in range(max_row)
]
# 连续50行空行停止读取内容
empty_row_num = 0
max_empty_rows = 50
empty_row_end = 0
for r_idx, row in enumerate(sheet_obj.iter_rows()):
if empty_row_num > max_empty_rows:
break
row_empty = True
for c_idx, cell in enumerate(row):
data_grid[r_idx][c_idx] = cell.value
if cell.value:
row_empty = False
if row_empty:
empty_row_num += 1
empty_row_end = r_idx
else:
empty_row_num = 0
empty_row_end = 0
merged_cell_ranges = list(sheet_obj.merged_cells.ranges)
for merged_range in merged_cell_ranges:
@@ -77,6 +95,8 @@ def unmerge_and_read_sheet(sheet_obj):
for r in range(min_row, max_row + 1):
for c in range(min_col, max_col + 1):
data_grid[r - 1][c - 1] = top_left_cell_value
if empty_row_end and empty_row_end - max_empty_rows > 0:
data_grid = data_grid[:empty_row_end - max_empty_rows]
return data_grid
@@ -267,6 +287,7 @@ def excel_file_to_markdown(
logger.debug(f"\n 正在处理Excel工作表:'{sheet_name}'...")
sheet_obj = workbook[sheet_name]
unmerged_data_list_of_lists = unmerge_and_read_sheet(sheet_obj)
logger.debug(f"\n <read all data>Excel<UNK>'{sheet_name}'...{len(unmerged_data_list_of_lists)}")
# 使用新的判断函数
if is_list_of_lists_empty(unmerged_data_list_of_lists):
@@ -414,8 +435,8 @@ def handler(
if __name__ == "__main__":
# 定义测试参数
test_cache_dir = "/Users/tju/Desktop/"
test_file_name = "/Users/tju/Downloads/bug1.xlsx"
test_cache_dir = "/Users/zhangguoqing/Downloads/tmp"
test_file_name = "/Users/zhangguoqing/Downloads/124327.xlsx"
# 测试 append_header=True 且索引越界的情况
test_header_rows = [0, 0] # start_header_index 超出范围
test_data_rows = 2
-198
View File
@@ -1,198 +0,0 @@
import json
from typing import Optional
import yaml
from fastapi import Request
from loguru import logger
from pydantic import BaseModel, ConfigDict
from bisheng.api.errcode.base import ServerError
from bisheng.api.services.openapi import OpenApiSchema
from bisheng.api.services.user_service import UserPayload
from bisheng.api.utils import get_url_content
from bisheng.database.constants import ToolPresetType
from bisheng.database.models.gpts_tools import GptsToolsDao, GptsTools, GptsToolsType, GptsToolsTypeRead
from bisheng.mcp_manage.manager import ClientManager
from bisheng.utils import md5_hash
class ToolServices(BaseModel):
""" 工具服务类 """
model_config = ConfigDict(arbitrary_types_allowed=True)
request: Optional[Request] = None
login_user: Optional[UserPayload] = None
async def parse_openapi_schema(self, download_url: str, file_content: str) -> GptsToolsTypeRead:
if download_url:
try:
file_content = await get_url_content(download_url)
except Exception as e:
logger.exception(f'file {download_url} download error')
raise ServerError.http_exception(msg='url文件下载失败:' + str(e))
if not file_content:
raise ServerError.http_exception(msg='schema内容不能为空')
# 根据文件内容是否以`{`开头判断用什么解析方式
try:
if file_content.startswith('{'):
res = json.loads(file_content)
else:
res = yaml.safe_load(file_content)
except Exception as e:
logger.exception(f'openapi schema parse error {e}')
raise ServerError.http_exception(msg=f'openapi schema解析报错,请检查内容是否符合json或者yaml格式: {str(e)}')
# 解析openapi schema转为助手工具的格式
try:
schema = OpenApiSchema(res)
schema.parse_server()
if not schema.default_server.startswith(('http', 'https')):
raise ServerError.http_exception(msg=f'server中的url必须以http或者https开头: {schema.default_server}')
tool_type = GptsToolsTypeRead(name=schema.title,
description=schema.description,
is_preset=ToolPresetType.API.value,
server_host=schema.default_server,
openapi_schema=file_content,
api_location=schema.api_location,
parameter_name=schema.parameter_name,
auth_type=schema.auth_type,
auth_method=schema.auth_method,
children=[])
# 解析获取所有的api
schema.parse_paths()
for one in schema.apis:
tool_type.children.append(
GptsTools(
name=one['operationId'],
desc=one['description'],
tool_key=md5_hash(one['operationId']),
is_preset=0,
is_delete=0,
api_params=one['parameters'],
extra=json.dumps(one, ensure_ascii=False),
))
return tool_type
except Exception as e:
logger.exception(f'openapi schema parse error {e}')
raise ServerError.http_exception(msg='openapi schema解析失败:' + str(e))
async def parse_mcp_schema(self, file_content: str) -> GptsToolsTypeRead:
try:
result = json.loads(file_content)
mcp_servers = result['mcpServers']
except Exception as e:
logger.exception(f'mcp tool schema parse error {e}')
raise ServerError.http_exception(msg=f'mcp工具配置解析失败,请检查内容是否符合mcp配置格式: {str(e)}')
tool_type = None
for key, value in mcp_servers.items():
# 解析mcp服务配置
tool_type = GptsToolsTypeRead(name=value.get('name', ''),
server_host=value.get('url', ''),
description=value.get('description', ''),
is_preset=ToolPresetType.MCP.value,
openapi_schema=file_content,
children=[])
# 实例化mcp服务对象,获取工具列表
client = await ClientManager.connect_mcp_from_json(result)
tools = await client.list_tools()
for one in tools:
tool_type.children.append(GptsTools(
name=one.name,
desc=one.description,
tool_key=md5_hash(one.name),
is_preset=ToolPresetType.MCP.value,
api_params=ToolServices.convert_input_schema(one.inputSchema),
extra=one.model_dump_json(),
))
break
if tool_type is None:
raise ServerError.http_exception(msg='mcp服务配置解析失败,请检查配置里是否配置了mcpServers')
return tool_type
async def refresh_all_mcp(self) -> str:
""" return mcp server error msg """
# get user all mcp tool
tool_types = GptsToolsDao.get_user_tool_type(self.login_user.user_id, is_preset=ToolPresetType.MCP)
if not tool_types:
return ''
tools = GptsToolsDao.get_list_by_type(tool_type_ids=[one.id for one in tool_types])
tools_map = {}
for one in tools:
if one.type not in tools_map:
tools_map[one.type] = []
tools_map[one.type].append(one)
error_msg = ''
for one in tool_types:
try:
await self.refresh_mcp_tools(one, tools_map.get(one.id, []))
except Exception as e:
logger.exception(f'{one.name}刷新工具失败:')
error_msg += f'{one.name}工具获取失败,请重试\n'
return error_msg
async def refresh_mcp_tools(self, tool_type: GptsToolsType, old_tools: list[GptsTools]):
""" refresh mcp tools """
# 1. get all new tools
# 实例化mcp服务对象,获取工具列表
client = await ClientManager.connect_mcp_from_json(tool_type.openapi_schema)
tools = await client.list_tools()
new_tools = {}
for one in tools:
tool_key = GptsToolsDao.get_tool_key(tool_type.id, md5_hash(one.name))
new_tools[tool_key] = GptsTools(
name=one.name,
desc=one.description,
tool_key=tool_key,
is_preset=ToolPresetType.MCP.value,
api_params=self.convert_input_schema(one.inputSchema),
extra=one.model_dump_json(),
type=tool_type.id,
)
# 2. get need add or update or delete tool
need_delete_tool = [] # list[int]
need_update_tool = [] # list[GptsTools]
for one in old_tools:
if one.tool_key not in new_tools:
# 需要删除的工具
logger.info(f'delete mcp tool: {one.name}')
need_delete_tool.append(one.id)
else:
logger.info(f'update mcp tool: {one.name}')
one.name = new_tools[one.tool_key].name
one.desc = new_tools[one.tool_key].desc
one.tool_key = new_tools[one.tool_key].tool_key
one.api_params = new_tools[one.tool_key].api_params
one.extra = new_tools[one.tool_key].extra
need_update_tool.append(one)
del new_tools[one.tool_key]
need_add_tool = list(new_tools.values())
# 3. update db
if need_delete_tool:
GptsToolsDao.delete_tool_by_ids(need_delete_tool)
if need_update_tool:
GptsToolsDao.update_tool_list(need_update_tool)
if need_add_tool:
GptsToolsDao.update_tool_list(need_add_tool)
@classmethod
def convert_input_schema(cls, input_schema: dict):
""" 转换mcp工具的输入参数 为自定义工具的格式"""
required = input_schema.get('required', [])
properties = input_schema.get('properties', {})
res = []
for filed, field_info in properties.items():
res.append({
'in': "query",
'name': filed,
'description': field_info.get('description'),
'required': filed in required,
'schema': {
'type': field_info.get('type'),
}
})
return res
@@ -0,0 +1 @@
from .tool import ToolServices
@@ -0,0 +1,106 @@
import json
from typing import Optional, Type
from langchain_core.tools import BaseTool
from pydantic import BaseModel, Field
from bisheng.api.services.knowledge_imp import decide_vectorstores
from bisheng.database.models.knowledge import KnowledgeDao
from bisheng.database.models.linsight_session_version import LinsightSessionVersionDao
from bisheng.database.models.llm_server import LLMDao
from bisheng.interface.importing.utils import import_vectorstore
from bisheng.interface.initialize.loading import instantiate_vectorstore
from bisheng.utils.embedding import decide_embeddings
class ToolInput(BaseModel):
query: str = Field(..., description='需要检索的关键词')
knowledge_id: Optional[str] = Field(default=None, description='语义检索库id')
limit: Optional[int] = Field(default=2, description='返回结果的最大数量')
call_reason: str = Field(default='', description='调用该工具的原因,原因中不要使用id来描述文件或知识库')
class SearchKnowledgeBase(BaseTool):
name: str = "search_knowledge_base"
description: str = """在语义检索库中搜索相关内容。
用法:在你需要在知识库中进行语义搜索时,调用此工具。
Args:
query: 需要检索的关键词
knowledge_id: 语义检索库id
limit: 返回结果的最大数量,默认为2
Returns:
包含搜索结果(chunk的列表)的字典"""
args_schema: Type[BaseModel] = ToolInput
def _run(self, query: str, knowledge_id: Optional[str] = None,
**kwargs) -> str:
"""Use the tool."""
return "not supported in sync mode, please use async version"
async def _arun(self, query: str, knowledge_id: Optional[str] = None,
**kwargs) -> str:
limit = kwargs.get('limit', None) or 2
if not query:
raise ValueError("query 参数不能为空")
try:
knowledge_id = int(knowledge_id)
return await self.search_knowledge(query, knowledge_id, limit)
except ValueError:
return await self.search_linsight_file(query, knowledge_id, limit)
async def base_search(self, vector_client, query: str, k: int):
documents = await vector_client.asimilarity_search(query, k=k)
if not documents:
# "没有找到相关的知识内容"
return '{"状态": "无结果", "错误信息":"没有找到相关的知识内容"}'
result = {
"状态": "成功",
"结果": [one.page_content for one in documents]
}
result = json.dumps(result, ensure_ascii=False, indent=2)
return result
async def search_linsight_file(self, query: str, file_id: str, limit: int) -> str:
"""检索Linsight用户上传的文件"""
session_info = await LinsightSessionVersionDao.get_session_version_by_file_id(file_id=file_id)
if not session_info:
raise Exception("文件不存在或已被删除")
files = session_info.files
file_info = None
for one in files:
if one.get("file_id") == file_id:
file_info = one
break
if not file_info:
raise Exception("文件不存在或已被删除")
class_obj = import_vectorstore('Milvus')
embeddings = decide_embeddings(file_info.get("embedding_model_id"))
params = {
'collection_name': file_info.get("collection_name"),
'embedding': embeddings,
'metadata_expr': f'file_id in {[file_id]}'
}
milvus_client = instantiate_vectorstore('Milvus', class_object=class_obj, params=params)
return await self.base_search(milvus_client, query, limit)
async def search_knowledge(self, query: str, knowledge_id: int, limit: int) -> str:
knowledge_info = KnowledgeDao.query_by_id(knowledge_id)
if not knowledge_info:
raise Exception("知识库不存在或已被删除")
if not knowledge_info.model:
# "知识库未配置embedding模型"
raise Exception("知识库未配置embedding模型")
embed_info = LLMDao.get_model_by_id(int(knowledge_info.model))
if not embed_info:
# "知识库配置的embedding模型不存在或已被删除"
raise Exception("知识库配置的embedding模型不存在或已被删除")
embeddings = decide_embeddings(knowledge_info.model)
milvus_client = decide_vectorstores(
knowledge_info.collection_name, "Milvus", embeddings
)
return await self.base_search(milvus_client, query, limit)
@@ -0,0 +1,332 @@
import json
from typing import Optional, List
import yaml
from fastapi import Request
from langchain_core.tools import BaseTool
from loguru import logger
from pydantic import BaseModel, ConfigDict
from bisheng.api.errcode.assistant import ToolTypeNotExistsError, ToolTypeRepeatError
from bisheng.api.errcode.base import ServerError, UnAuthorizedError
from bisheng.api.services.openapi import OpenApiSchema
from bisheng.api.services.tool.langchain_tool.search_knowledge import SearchKnowledgeBase
from bisheng.api.services.user_service import UserPayload
from bisheng.api.utils import get_url_content
from bisheng.database.constants import ToolPresetType
from bisheng.database.models.gpts_tools import GptsToolsDao, GptsTools, GptsToolsType, GptsToolsTypeRead
from bisheng.database.models.role_access import AccessType
from bisheng.mcp_manage.manager import ClientManager
from bisheng.utils import md5_hash
from bisheng_langchain.gpts.load_tools import load_tools
class ToolServices(BaseModel):
""" 工具服务类 """
model_config = ConfigDict(arbitrary_types_allowed=True)
request: Optional[Request] = None
login_user: Optional[UserPayload] = None
async def parse_openapi_schema(self, download_url: str, file_content: str) -> GptsToolsTypeRead:
if download_url:
try:
file_content = await get_url_content(download_url)
except Exception as e:
logger.exception(f'file {download_url} download error')
raise ServerError.http_exception(msg='url文件下载失败:' + str(e))
if not file_content:
raise ServerError.http_exception(msg='schema内容不能为空')
# 根据文件内容是否以`{`开头判断用什么解析方式
try:
if file_content.startswith('{'):
res = json.loads(file_content)
else:
res = yaml.safe_load(file_content)
except Exception as e:
logger.exception(f'openapi schema parse error {e}')
raise ServerError.http_exception(msg=f'openapi schema解析报错,请检查内容是否符合json或者yaml格式: {str(e)}')
# 解析openapi schema转为助手工具的格式
try:
schema = OpenApiSchema(res)
schema.parse_server()
if not schema.default_server.startswith(('http', 'https')):
raise ServerError.http_exception(msg=f'server中的url必须以http或者https开头: {schema.default_server}')
tool_type = GptsToolsTypeRead(name=schema.title,
description=schema.description,
is_preset=ToolPresetType.API.value,
server_host=schema.default_server,
openapi_schema=file_content,
api_location=schema.api_location,
parameter_name=schema.parameter_name,
auth_type=schema.auth_type,
auth_method=schema.auth_method,
children=[])
# 解析获取所有的api
schema.parse_paths()
for one in schema.apis:
tool_type.children.append(
GptsTools(
name=one['operationId'],
desc=one['description'],
tool_key=md5_hash(one['operationId']),
is_preset=0,
is_delete=0,
api_params=one['parameters'],
extra=json.dumps(one, ensure_ascii=False),
))
return tool_type
except Exception as e:
logger.exception(f'openapi schema parse error {e}')
raise ServerError.http_exception(msg='openapi schema解析失败:' + str(e))
async def parse_mcp_schema(self, file_content: str) -> GptsToolsTypeRead:
try:
result = json.loads(file_content)
mcp_servers = result['mcpServers']
except Exception as e:
logger.exception(f'mcp tool schema parse error {e}')
raise ServerError.http_exception(msg=f'mcp工具配置解析失败,请检查内容是否符合mcp配置格式: {str(e)}')
tool_type = None
for key, value in mcp_servers.items():
# 解析mcp服务配置
tool_type = GptsToolsTypeRead(name=value.get('name', ''),
server_host=value.get('url', ''),
description=value.get('description', ''),
is_preset=ToolPresetType.MCP.value,
openapi_schema=file_content,
children=[])
# 实例化mcp服务对象,获取工具列表
client = await ClientManager.connect_mcp_from_json(result)
tools = await client.list_tools()
for one in tools:
tool_type.children.append(GptsTools(
name=one.name,
desc=one.description,
tool_key=md5_hash(one.name),
is_preset=ToolPresetType.MCP.value,
api_params=ToolServices.convert_input_schema(one.inputSchema),
extra=one.model_dump_json(),
))
break
if tool_type is None:
raise ServerError.http_exception(msg='mcp服务配置解析失败,请检查配置里是否配置了mcpServers')
return tool_type
@classmethod
async def _update_gpts_tools(cls, exist_tool_type: GptsToolsType, req: GptsToolsTypeRead) -> GptsToolsTypeRead:
exist_tool_type.name = req.name
exist_tool_type.logo = req.logo
exist_tool_type.description = req.description
exist_tool_type.server_host = req.server_host
exist_tool_type.auth_method = req.auth_method
exist_tool_type.api_key = req.api_key
exist_tool_type.auth_type = req.auth_type
exist_tool_type.openapi_schema = req.openapi_schema
tool_extra = {"api_location": req.api_location, "parameter_name": req.parameter_name}
exist_tool_type.extra = json.dumps(tool_extra, ensure_ascii=False)
children_map = {}
for one in req.children:
children_map[one.name] = one
# 获取此类别下旧的API列表
old_tool_list = GptsToolsDao.get_list_by_type([exist_tool_type.id])
# 需要被删除的工具列表
delete_tool_id_list = []
# 需要被更新的工具列表
update_tool_list = []
for one in old_tool_list:
# 说明此工具 需要删除
if children_map.get(one.name) is None:
delete_tool_id_list.append(one.id)
else:
# 说明此工具需要更新
new_tool_info = children_map.pop(one.name)
one.name = new_tool_info.name
one.desc = new_tool_info.desc
one.extra = new_tool_info.extra
one.api_params = new_tool_info.api_params
update_tool_list.append(one)
add_children = []
for one in children_map.values():
one.id = None
one.user_id = exist_tool_type.user_id
one.is_preset = exist_tool_type.is_preset
one.is_delete = 0
add_children.append(one)
GptsToolsDao.update_tool_type(exist_tool_type, delete_tool_id_list,
add_children, update_tool_list)
children = GptsToolsDao.get_list_by_type([exist_tool_type.id])
return GptsToolsTypeRead(**exist_tool_type.model_dump(), children=children)
@classmethod
async def update_gpts_tools(cls, user: UserPayload, req: GptsToolsTypeRead) -> GptsToolsTypeRead:
"""
更新工具类别,包括更新工具类别的名称和删除、新增工具类别的API
"""
# 尝试解析下openapi schema看下是否可以正常解析, 不能的话保存不允许保存
tool_service = ToolServices()
if req.is_preset == ToolPresetType.API.value:
await tool_service.parse_openapi_schema('', req.openapi_schema)
elif req.is_preset == ToolPresetType.MCP.value:
await tool_service.parse_mcp_schema(req.openapi_schema)
exist_tool_type = GptsToolsDao.get_one_tool_type(req.id)
if not exist_tool_type:
raise ToolTypeNotExistsError.http_exception()
if req.name.__len__() > 1000 or req.name.__len__() == 0:
raise ServerError.http_exception(msg="名字不符合规范:至少1个字符,不能超过1000个字符")
# 判断工具类别名称是否重复
tool_type = GptsToolsDao.get_one_tool_type_by_name(user.user_id, req.name)
if tool_type and tool_type.id != exist_tool_type.id:
raise ToolTypeRepeatError.http_exception()
# 判断是否有更新权限
if not user.access_check(exist_tool_type.user_id, str(exist_tool_type.id), AccessType.GPTS_TOOL_WRITE):
raise UnAuthorizedError.http_exception()
return await cls._update_gpts_tools(exist_tool_type, req)
async def refresh_all_mcp(self) -> str:
""" return mcp server error msg """
# get user all mcp tool
tool_types = GptsToolsDao.get_user_tool_type(self.login_user.user_id, is_preset=ToolPresetType.MCP)
if not tool_types:
return ''
tools = GptsToolsDao.get_list_by_type(tool_type_ids=[one.id for one in tool_types])
tools_map = {}
for one in tools:
if one.type not in tools_map:
tools_map[one.type] = []
tools_map[one.type].append(one)
error_msg = ''
for one in tool_types:
try:
await self.refresh_mcp_tools(one, tools_map.get(one.id, []))
except Exception as e:
logger.exception(f'{one.name}刷新工具失败:')
error_msg += f'{one.name}工具获取失败,请重试\n'
return error_msg
async def refresh_mcp_tools(self, tool_type: GptsToolsType, old_tools: list[GptsTools]):
""" refresh mcp tools """
# 1. get all new tools
# 实例化mcp服务对象,获取工具列表
client = await ClientManager.connect_mcp_from_json(tool_type.openapi_schema)
tools = await client.list_tools()
children = []
for one in tools:
children.append(GptsTools(
name=one.name,
desc=one.description,
is_preset=ToolPresetType.MCP.value,
api_params=self.convert_input_schema(one.inputSchema),
extra=one.model_dump_json(),
type=tool_type.id,
))
req = GptsToolsTypeRead(**tool_type.model_dump(), children=children)
await self._update_gpts_tools(tool_type, req)
@classmethod
def convert_input_schema(cls, input_schema: dict):
""" 转换mcp工具的输入参数 为自定义工具的格式"""
required = input_schema.get('required', [])
properties = input_schema.get('properties', {})
res = []
for filed, field_info in properties.items():
res.append({
'in': "query",
'name': filed,
'description': field_info.get('description'),
'required': filed in required,
'schema': {
'type': field_info.get('type'),
}
})
return res
@classmethod
async def init_linsight_tools(cls, root_path: str) -> List[BaseTool]:
""" 初始化Linsight 默认的工具, 特殊点在于本地文件工具初始化的参数不是固定的,而是再运行期间确定的 """
# 加载本地文件操作相关工具
local_file_tools = load_tools({
"list_files": {"root_path": root_path},
"get_file_details": {"root_path": root_path},
"search_files": {"root_path": root_path},
# "search_text_in_file": {"root_path": root_path},
"read_text_file": {"root_path": root_path},
"add_text_to_file": {"root_path": root_path},
"replace_file_lines": {"root_path": root_path},
})
knowledge_tools = [SearchKnowledgeBase()]
return knowledge_tools + local_file_tools
@classmethod
async def get_linsight_tools(cls) -> list[GptsToolsTypeRead]:
return [
GptsToolsTypeRead(
id=100000,
name="知识库和文件内容检索",
description="检索组织知识库、个人知识库以及本地上传文件的内容",
children=[
GptsTools(
id=100001,
name="知识库和文件内容检索",
desc="检索组织知识库、个人知识库以及本地上传文件的内容。",
tool_key="search_knowledge_base",
)
]
),
GptsToolsTypeRead(
id=200000,
name="文件操作",
description="本地文件系统的浏览、搜索与编辑工具集",
children=[
GptsTools(
id=200001,
name="获取所有文件和目录",
desc="列出指定目录下的所有文件和子目录。",
tool_key="list_files"
),
GptsTools(
id=200002,
name="获取文件详细信息",
desc="获取指定文件的文件名、文件大小、文件地址、字数、行数等详细信息。",
tool_key="get_file_details"
),
GptsTools(
id=200003,
name="搜索文件",
desc="在指定目录中搜索文件和子目录。",
tool_key="search_files"
),
GptsTools(
id=200004,
name="读取文件内容",
desc="读取本地文本文件的内容。",
tool_key="read_text_file"
),
GptsTools(
id=200005,
name="写入文件内容",
desc="将文本内容追加到文本文件,如果文件不存在,则创建文件",
tool_key="add_text_to_file"
),
GptsTools(
id=200006,
name="替换文件指定行范围内容",
desc="替换文件中的指定行范围。",
tool_key="replace_file_lines"
),
]
)
]
@@ -3,6 +3,10 @@ import json
from datetime import datetime
from typing import Optional, Any
from fastapi import BackgroundTasks, Request
from langchain_core.messages import AIMessage, HumanMessage
from loguru import logger
from openai import BaseModel
from pydantic import field_validator
from bisheng.api.services import knowledge_imp, llm
@@ -12,13 +16,10 @@ from bisheng.api.services.user_service import UserPayload
from bisheng.api.v1.schemas import KnowledgeFileOne, KnowledgeFileProcess, WorkstationConfig
from bisheng.database.constants import MessageCategory
from bisheng.database.models.config import Config, ConfigDao, ConfigKeyEnum
from bisheng.database.models.gpts_tools import GptsToolsDao
from bisheng.database.models.knowledge import KnowledgeCreate, KnowledgeDao, KnowledgeTypeEnum
from bisheng.database.models.message import ChatMessage, ChatMessageDao
from bisheng.database.models.session import MessageSession
from fastapi import BackgroundTasks, Request
from langchain_core.messages import AIMessage, HumanMessage
from loguru import logger
from openai import BaseModel
class WorkStationService(BaseService):
@@ -29,16 +30,44 @@ class WorkStationService(BaseService):
""" 更新workflow的默认模型配置 """
config = ConfigDao.get_config(ConfigKeyEnum.WORKSTATION)
if config:
config.value = json.dumps(data.dict())
config.value = data.model_dump_json()
else:
config = Config(key=ConfigKeyEnum.WORKSTATION.value, value=json.dumps(data.dict()))
ConfigDao.insert_config(config)
return data
@classmethod
def get_config(cls) -> WorkstationConfig | None:
""" 获取工作台的默认配置 """
config = ConfigDao.get_config(ConfigKeyEnum.WORKSTATION)
def sync_tool_info(cls, tools: list[dict]) -> list[dict]:
""" 同步工具信息 """
if not tools:
return []
tool_type_ids = [t.get("id") for t in tools]
tool_type_info = GptsToolsDao.get_all_tool_type(tool_type_ids)
exists_tool_type = {t.id: t for t in tool_type_info}
tool_info = GptsToolsDao.get_list_by_type(list(exists_tool_type.keys()))
exists_tool_info = {t.id: t for t in tool_info}
new_tools = []
for one in tools:
new_one = exists_tool_type.get(one.get("id"))
if not new_one:
continue
one["name"] = new_one.name
one["description"] = new_one.description
new_children = []
for item in one.get("children", []):
if not exists_tool_info.get(item.get("id")):
continue
item["name"] = exists_tool_info[item.get("id")].name
item["description"] = exists_tool_info[item.get("id")].desc
item["tool_key"] = exists_tool_info[item.get("id")].tool_key
new_children.append(item)
one["children"] = new_children
new_tools.append(one)
return new_tools
@classmethod
def parse_config(cls, config: Any) -> Optional[WorkstationConfig]:
if config:
ret = json.loads(config.value)
ret = WorkstationConfig(**ret)
@@ -51,16 +80,31 @@ class WorkStationService(BaseService):
if ret.webSearch and not ret.webSearch.params:
ret.webSearch.tool = 'bing'
ret.webSearch.params = {'api_key': ret.webSearch.bingKey, 'base_url': ret.webSearch.bingUrl}
if ret.linsightConfig:
# 判断工具是否被删除, 同步工具最新的信息名称和描述等
ret.linsightConfig.tools = cls.sync_tool_info(ret.linsightConfig.tools)
return ret
return None
@classmethod
def get_config(cls) -> WorkstationConfig | None:
""" 获取工作台的默认配置 """
config = ConfigDao.get_config(ConfigKeyEnum.WORKSTATION)
return cls.parse_config(config)
@classmethod
async def aget_config(cls) -> WorkstationConfig | None:
""" 异步获取工作台的默认配置 """
config = await ConfigDao.aget_config(ConfigKeyEnum.WORKSTATION)
return cls.parse_config(config)
@classmethod
async def uploadPersonalKnowledge(
cls,
request: Request,
login_user: UserPayload,
file_path,
background_tasks: BackgroundTasks,
cls,
request: Request,
login_user: UserPayload,
file_path,
background_tasks: BackgroundTasks,
):
# 查询是否有个人知识库
knowledge = KnowledgeDao.get_user_knowledge(login_user.user_id, None,
@@ -84,11 +128,11 @@ class WorkStationService(BaseService):
@classmethod
def queryKnowledgeList(
cls,
request: Request,
login_user: UserPayload,
page: int,
size: int,
cls,
request: Request,
login_user: UserPayload,
page: int,
size: int,
):
# 查询是否有个人知识库
knowledge = KnowledgeDao.get_user_knowledge(login_user.user_id, None,
+6
View File
@@ -6,7 +6,9 @@ from bisheng.api.v1.endpoints import router as endpoints_router
from bisheng.api.v1.evaluation import router as evaluation_router
from bisheng.api.v1.finetune import router as finetune_router
from bisheng.api.v1.flows import router as flows_router
from bisheng.api.v1.invite_code import router as invite_code_router
from bisheng.api.v1.knowledge import router as knowledge_router
from bisheng.api.v1.linsight import router as linsight_router
from bisheng.api.v1.llm import router as llm_router
from bisheng.api.v1.mark_task import router as mark_router
from bisheng.api.v1.qa import router as qa_router
@@ -14,6 +16,7 @@ from bisheng.api.v1.report import router as report_router
from bisheng.api.v1.server import router as server_router
from bisheng.api.v1.skillcenter import router as skillcenter_router
from bisheng.api.v1.tag import router as tag_router
from bisheng.api.v1.tool import router as tool_router
from bisheng.api.v1.user import router as user_router
from bisheng.api.v1.usergroup import router as group_router
from bisheng.api.v1.validate import router as validate_router
@@ -44,4 +47,7 @@ __all__ = [
'workflow_router',
'mark_router',
'workstation_router',
"linsight_router",
"tool_router",
"invite_code_router",
]
+7 -11
View File
@@ -1,9 +1,6 @@
import hashlib
import json
from typing import Dict, List, Optional
import yaml
from bisheng_langchain.gpts.tools.api_tools.openapi import OpenApiTools
from fastapi import (APIRouter, Body, Depends, HTTPException, Query, Request, WebSocket,
WebSocketException)
from fastapi import status as http_status
@@ -14,7 +11,6 @@ from bisheng.api.services.assistant import AssistantService
from bisheng.api.services.openapi import OpenApiSchema
from bisheng.api.services.tool import ToolServices
from bisheng.api.services.user_service import UserPayload, get_admin_user, get_login_user
from bisheng.api.utils import get_url_content, md5_hash
from bisheng.api.v1.schemas import (AssistantCreateReq, AssistantUpdateReq,
DeleteToolTypeReq, StreamData, TestToolReq,
resp_200, resp_500)
@@ -23,11 +19,11 @@ from bisheng.chat.manager import ChatManager
from bisheng.chat.types import WorkType
from bisheng.database.constants import ToolPresetType
from bisheng.database.models.assistant import Assistant
from bisheng.database.models.gpts_tools import GptsTools, GptsToolsTypeRead
from bisheng.mcp_manage.constant import McpClientType
from bisheng.database.models.gpts_tools import GptsToolsTypeRead
from bisheng.mcp_manage.manager import ClientManager
from bisheng.utils import generate_uuid
from bisheng.utils.logger import logger
from bisheng_langchain.gpts.tools.api_tools.openapi import OpenApiTools
router = APIRouter(prefix='/assistant', tags=['Assistant'])
chat_manager = ChatManager()
@@ -259,8 +255,8 @@ async def refresh_all_mcp_tools(request: Request, login_user: UserPayload = Depe
@router.post('/tool_list')
async def add_tool_type(*,
req: Dict = Body(default={}, description='openapi解析后的工具对象'),
login_user: UserPayload = Depends(get_login_user)):
req: Dict = Body(default={}, description='openapi解析后的工具对象'),
login_user: UserPayload = Depends(get_login_user)):
""" 新增自定义tool """
req = GptsToolsTypeRead(**req)
return await AssistantService.add_gpts_tools(login_user, req)
@@ -268,11 +264,11 @@ async def add_tool_type(*,
@router.put('/tool_list')
async def update_tool_type(*,
login_user: UserPayload = Depends(get_login_user),
req: Dict = Body(default={}, description='通过openapi 解析后的内容,包含类别的唯一ID')):
login_user: UserPayload = Depends(get_login_user),
req: Dict = Body(default={}, description='通过openapi 解析后的内容,包含类别的唯一ID')):
""" 更新自定义tool """
req = GptsToolsTypeRead(**req)
return await AssistantService.update_gpts_tools(login_user, req)
return resp_200(data=await ToolServices.update_gpts_tools(login_user, req))
@router.delete('/tool_list')
+3 -4
View File
@@ -437,10 +437,9 @@ def comment_resp(*, data: ChatInput):
@router.get('/chat/list')
def get_session_list(*,
page: Optional[int] = 1,
limit: Optional[int] = 10,
flow_type: Optional[int] = None,
def get_session_list(page: Optional[int] = Query(default=1, ge=1, le=1000),
limit: Optional[int] = Query(default=10, ge=1, le=100),
flow_type: Optional[List[int]] = Query(default=None, description='技能类型'),
login_user: UserPayload = Depends(get_login_user)):
res = MessageSessionDao.filter_session(user_ids=[login_user.user_id],
flow_type=flow_type,
+51
View File
@@ -0,0 +1,51 @@
from typing import List
from bisheng.api.services.dataset_service import DatasetService
from bisheng.api.services.user_service import UserPayload, get_login_user
from bisheng.api.v1.schema.dataset_param import CreateDatasetParam
from bisheng.api.v1.schemas import UnifiedResponseModel, resp_200
from bisheng.database.models.dataset import DatasetRead
from fastapi import APIRouter, Depends, Request
# build router
router = APIRouter(prefix='/dataset', tags=['FineTune'])
@router.get('/list', summary='获取数据集列表')
def list_dataset(*,
keyword: str = None,
page: int = 1,
limit: int = 10) -> UnifiedResponseModel[List[DatasetRead]]:
"""
获取数据集列表
"""
res, count = DatasetService.build_dataset_list(page, limit, keyword)
return resp_200(data={'list': res, 'total': count})
@router.post('/create', summary='创建数据集')
def create_dataset(
*,
request: Request,
data: CreateDatasetParam,
login_user: UserPayload = Depends(get_login_user),
) -> UnifiedResponseModel:
"""
创建数据集
"""
dataset = DatasetService.create_dataset(login_user.user_id, data)
return resp_200(data=dataset)
@router.delete('/del', summary='删除数据集')
def delete_dataset(
*,
request: Request,
dataset_id: int,
login_user: UserPayload = Depends(get_login_user),
) -> UnifiedResponseModel:
"""
创建数据集
"""
DatasetService.delete_dataset(dataset_id)
return resp_200()
+9 -1
View File
@@ -71,6 +71,7 @@ def get_env():
env['pro'] = settings.settings.get_system_login_method().bisheng_pro
env['version'] = __version__
env['enable_etl4lm'] = etl_for_lm_url is not None
return resp_200(env)
@@ -87,7 +88,14 @@ def save_config(data: dict, admin_user: UserPayload = Depends(get_admin_user)):
raise HTTPException(status_code=500, detail='配置不能为空')
try:
# 校验是否符合yaml格式
_ = yaml.safe_load(data.get('data'))
config = yaml.safe_load(data.get('data'))
# 判断 linsight_invitation_code 是不是boolean
if isinstance(config, dict) and 'linsight_invitation_code' in config.keys():
if config['linsight_invitation_code'] is not None and bool(config['linsight_invitation_code']) not in [True,
False]:
raise ValueError('linsight_invitation_code must be a boolean value')
db_config = ConfigDao.get_config(ConfigKeyEnum.INIT_DB)
db_config.value = data.get('data')
ConfigDao.insert_config(db_config)
+50
View File
@@ -0,0 +1,50 @@
from fastapi import APIRouter, Depends, Body, Request
from loguru import logger
from bisheng.api.services.invite_code.invite_code import InviteCodeService
from bisheng.api.services.user_service import UserPayload, get_admin_user, get_login_user
from bisheng.api.utils import get_request_ip
from bisheng.api.v1.schemas import resp_200, resp_500
router = APIRouter(prefix='/invite', tags=['InviteCode'])
@router.post('/code')
async def create_invite_code(request: Request, login_user: UserPayload = Depends(get_admin_user),
name: str = Body(..., description='批次名称'),
num: int = Body(..., description='当前批次的邀请码数量'),
limit: int = Body(..., description='当前批次邀请码的使用次数限制')):
"""
创建邀请码
"""
logger.debug(
f"create invite code user_id: {login_user.user_id}, ip: {get_request_ip(request)}, name: {name}, num: {num}, limit: {limit}")
codes = await InviteCodeService.create_batch_invite_codes(login_user, name, num, limit)
return resp_200(data={
"name": name,
"limit": limit,
"codes": codes
})
@router.post('/bind')
async def bind_invite_code(request: Request, login_user: UserPayload = Depends(get_login_user),
code: str = Body(..., embed=True, description='邀请码')):
"""
绑定邀请码
"""
result, error = await InviteCodeService.bind_invite_code(login_user, code)
logger.debug(f"bind_invite_code user_id:{login_user.user_id}, code:{code}, flag:{result}, error:{error}")
if result:
return resp_200(message=error)
else:
return resp_500(message=error)
@router.get('/code')
async def get_bind_code_num(request: Request, login_user: UserPayload = Depends(get_login_user)):
"""
获取用户绑定的有效的邀请码的可使用次数
"""
num = await InviteCodeService.get_invite_code_num(login_user)
return resp_200(data=num)
+26 -15
View File
@@ -46,10 +46,10 @@ async def upload_file(*, file: UploadFile = File(...)):
@router.post('/preview')
async def preview_file_chunk(*,
request: Request,
login_user: UserPayload = Depends(get_login_user),
req_data: KnowledgeFileProcess):
def preview_file_chunk(*,
request: Request,
login_user: UserPayload = Depends(get_login_user),
req_data: KnowledgeFileProcess):
""" 获取某个文件的分块预览内容 """
try:
parse_type, file_share_url, res, partitions = KnowledgeService.get_preview_file_chunk(
@@ -121,7 +121,7 @@ async def copy_knowledge(*,
""" 复制知识库. """
knowledge = KnowledgeDao.query_by_id(knowledge_id)
if not login_user.is_admin and knowledge.user_id != login_user.id:
if not login_user.is_admin and knowledge.user_id != login_user.user_id:
return UnAuthorizedError.return_resp()
knowledge_count = KnowledgeFileDao.count_file_by_filters(
@@ -135,18 +135,18 @@ async def copy_knowledge(*,
@router.get('', status_code=200)
def get_knowledge(*,
request: Request,
login_user: UserPayload = Depends(get_login_user),
name: str = None,
knowledge_type: int = Query(default=KnowledgeTypeEnum.NORMAL.value,
alias='type'),
page_size: Optional[int] = 10,
page_num: Optional[int] = 1):
async def get_knowledge(*,
request: Request,
login_user: UserPayload = Depends(get_login_user),
name: str = None,
knowledge_type: int = Query(default=KnowledgeTypeEnum.NORMAL.value,
alias='type'),
page_size: Optional[int] = 10,
page_num: Optional[int] = 1):
""" 读取所有知识库信息. """
knowledge_type = KnowledgeTypeEnum(knowledge_type)
res, total = KnowledgeService.get_knowledge(request, login_user, knowledge_type, name,
page_num, page_size)
res, total = await KnowledgeService.get_knowledge(request, login_user, knowledge_type, name,
page_num, page_size)
return resp_200(data={'data': res, 'total': total})
@@ -180,6 +180,17 @@ def delete_knowledge(*,
return resp_200(message='删除成功')
# 个人知识库信息获取
@router.get('/personal_knowledge_info', status_code=200)
def get_personal_knowledge_info(
login_user: UserPayload = Depends(get_login_user)):
""" 获取个人知识库信息. """
knowledge = KnowledgeDao.get_user_knowledge(login_user.user_id, None,
KnowledgeTypeEnum.PRIVATE)
return resp_200(data=knowledge)
@router.get('/file_list/{knowledge_id}', status_code=200)
def get_filelist(*,
request: Request,
+713
View File
@@ -0,0 +1,713 @@
import json
import os
from datetime import datetime
from typing import List, Literal, Optional
from urllib import parse
from fastapi import APIRouter, Depends, Body, Query, UploadFile, File, BackgroundTasks, Request, HTTPException
from fastapi_jwt_auth import AuthJWT
from loguru import logger
from sse_starlette import EventSourceResponse
from starlette.responses import StreamingResponse
from starlette.websockets import WebSocket
from bisheng.api.errcode.base import UnAuthorizedError, NotFoundError
from bisheng.api.services.invite_code.invite_code import InviteCodeService
from bisheng.api.services.knowledge import KnowledgeService
from bisheng.api.services.linsight.message_stream_handle import MessageStreamHandle
from bisheng.api.services.linsight.sop_manage import SOPManageService
from bisheng.api.services.linsight.workbench_impl import LinsightWorkbenchImpl
from bisheng.api.services.user_service import get_login_user, UserPayload, get_admin_user
from bisheng.api.v1.schema.base_schema import PageList
from bisheng.api.v1.schema.inspiration_schema import SOPManagementSchema, SOPManagementUpdateSchema
from bisheng.api.v1.schema.linsight_schema import LinsightQuestionSubmitSchema, BatchDownloadFilesSchema
from bisheng.api.v1.schemas import UnifiedResponseModel, resp_200, resp_500
from bisheng.cache.redis import redis_client
from bisheng.database.models.knowledge import KnowledgeTypeEnum, KnowledgeDao
from bisheng.database.models.linsight_session_version import LinsightSessionVersionDao, SessionVersionStatusEnum, \
LinsightSessionVersion
from bisheng.database.models.linsight_sop import LinsightSOPDao, LinsightSOPRecord
from bisheng.linsight.state_message_manager import LinsightStateMessageManager, MessageData, MessageEventType
from bisheng.settings import settings
router = APIRouter(prefix="/linsight", tags=["灵思"])
# 灵思上传文件
@router.post("/workbench/upload-file", summary="灵思上传文件", response_model=UnifiedResponseModel)
async def upload_file(
background_tasks: BackgroundTasks,
file: UploadFile = File(...),
login_user: UserPayload = Depends(get_login_user)) -> UnifiedResponseModel:
"""
灵思上传文件
:param background_tasks:
:param file: 上传的文件
:param login_user: 登录用户信息
:return: 上传结果
"""
try:
# 调用实现类处理文件上传
upload_result = await LinsightWorkbenchImpl.upload_file(file)
background_tasks.add_task(LinsightWorkbenchImpl.parse_file, upload_result)
result = {
"file_id": upload_result.get("file_id"),
"file_name": upload_result.get("original_filename"),
"parsing_status": upload_result.get("parsing_status"),
}
except Exception as e:
logger.error(f"文件上传失败: {str(e)}")
return resp_500(code=500, message=str(e))
# 返回上传结果
return resp_200(data=result, message="文件上传成功 并开始解析。请稍后查看解析状态。")
# 获取文件解析状态
@router.post("/workbench/file-parsing-status", summary="获取文件解析状态", response_model=UnifiedResponseModel)
async def get_file_parsing_status(
file_ids: List[str] = Body(..., description="文件ID列表", embed=True),
login_user: UserPayload = Depends(get_login_user)) -> UnifiedResponseModel:
"""
获取文件解析状态
:param file_ids:
:param login_user:
:return:
"""
try:
# 调用实现类获取文件解析状态
key_prefix = LinsightWorkbenchImpl.FILE_INFO_REDIS_KEY_PREFIX
if not file_ids:
return resp_500(code=400, message="文件ID列表不能为空")
file_ids = [f"{key_prefix}{file_id}" for file_id in file_ids]
# 使用 Redis 的 amget 方法批量获取文件解析状态
parsing_status = await redis_client.amget(file_ids)
return resp_200(data=parsing_status, message="文件解析状态获取成功")
except Exception as e:
logger.error(f"获取文件解析状态失败: {str(e)}")
return resp_500(code=500, message=str(e))
# 提交灵思用户问题请求
@router.post("/workbench/submit", summary="提交灵思用户问题请求")
async def submit_linsight_workbench(
submit_obj: LinsightQuestionSubmitSchema = Body(..., description="灵思用户问题提交对象"),
login_user: UserPayload = Depends(get_login_user)) -> EventSourceResponse:
"""
提交灵思用户问题请求
:param submit_obj:
:param login_user:
:return:
"""
logger.info(f"用户 {login_user.user_id} 提交灵思问题: {submit_obj.question}")
async def event_generator():
"""
事件生成器用于生成SSE事件
"""
try:
system_config = await settings.aget_all_config()
# 获取Linsight_invitation_code
linsight_invitation_code = system_config.get("linsight_invitation_code", False)
if linsight_invitation_code:
if await InviteCodeService.use_invite_code(user_id=login_user.user_id) is False:
yield {
"event": "error",
"data": "您的灵思使用次数已用完,请使用新的邀请码激活灵思功能。"
}
return
message_session_model, linsight_session_version_model = await LinsightWorkbenchImpl.submit_user_question(
submit_obj,
login_user)
response_data = {
"message_session": message_session_model.model_dump(),
"linsight_session_version": linsight_session_version_model.model_dump()
}
except Exception as e:
yield {
"event": "error",
"data": "提交灵思用户问题失败: " + str(e)
}
return
yield {
"event": "linsight_workbench_submit",
"data": json.dumps(response_data)
}
# 任务标题生成
title_data = await LinsightWorkbenchImpl.task_title_generate(question=submit_obj.question,
chat_id=message_session_model.chat_id,
login_user=login_user)
linsight_session_version_model.title = title_data.get("task_title")
await LinsightSessionVersionDao.insert_one(linsight_session_version_model)
yield {
"event": "linsight_workbench_title_generate",
"data": json.dumps(title_data)
}
return EventSourceResponse(event_generator())
# workbench 生成与重新规划灵思SOP
@router.post("/workbench/generate-sop", summary="生成与重新规划灵思SOP", response_model=UnifiedResponseModel)
async def generate_sop(
request: Request,
linsight_session_version_id: str = Body(..., description="灵思会话版本ID"),
previous_session_version_id: str = Body(None, description="上一个灵思会话版本ID"),
feedback_content: str = Body(None, description="用户反馈内容"),
reexecute: bool = Body(False, description="是否重新执行生成SOP"),
login_user: UserPayload = Depends(get_login_user)) -> EventSourceResponse:
"""
生成与重新规划灵思SOP
:param previous_session_version_id:
:param reexecute:
:param linsight_session_version_id:
:param feedback_content:
:param login_user:
:return:
"""
logger.info(f"开始生成与重新规划灵思SOP,灵思会话版本ID: {linsight_session_version_id} ")
session_version = await LinsightSessionVersionDao.get_by_id(linsight_session_version_id)
# 获取有权限的知识库列表
if not session_version:
raise NotFoundError.http_exception()
res = []
linsight_conf = settings.get_linsight_conf()
if session_version.org_knowledge_enabled and linsight_conf.max_knowledge_num > 0:
res, _ = await KnowledgeService.get_knowledge(request, login_user, KnowledgeTypeEnum.NORMAL, None, 1,
linsight_conf.max_knowledge_num)
if session_version.personal_knowledge_enabled:
knowledge = await KnowledgeDao.aget_user_knowledge(login_user.user_id, None,
KnowledgeTypeEnum.PRIVATE)
if knowledge:
res.extend(knowledge)
async def event_generator():
"""
事件生成器用于生成SSE事件
"""
# 生成SOP
sop_generate = LinsightWorkbenchImpl.generate_sop(
linsight_session_version_id=linsight_session_version_id,
previous_session_version_id=previous_session_version_id,
feedback_content=feedback_content,
reexecute=reexecute,
login_user=login_user,
knowledge_list=res
)
async for event in sop_generate:
yield event
# 结束
yield {
"event": "sop_generate_complete",
"data": json.dumps({"message": "SOP生成与重新规划完成"})
}
return EventSourceResponse(event_generator())
# workbench 修改sop
@router.post("/workbench/sop-modify", summary="修改灵思SOP", response_model=UnifiedResponseModel)
async def modify_sop(
sop_content: str = Body(..., description="SOP内容"),
linsight_session_version_id: str = Body(..., description="灵思会话版本ID"),
login_user: UserPayload = Depends(get_login_user)) -> UnifiedResponseModel:
"""
修改灵思SOP
:param sop_content:
:param linsight_session_version_id:
:param login_user:
:return:
"""
session_version_model = await LinsightSessionVersionDao.get_by_id(
linsight_session_version_id=linsight_session_version_id)
if not session_version_model:
return resp_500(code=404, message="灵思会话版本不存在")
if login_user.user_id != session_version_model.user_id:
return resp_500(code=403, message="无权限修改该灵思SOP")
modify_res = await LinsightWorkbenchImpl.modify_sop(linsight_session_version_id=linsight_session_version_id,
sop_content=sop_content)
return resp_200(modify_res)
# workbench 开始执行
@router.post("/workbench/start-execute", summary="开始执行灵思", response_model=UnifiedResponseModel)
async def start_execute_sop(
background_tasks: BackgroundTasks,
linsight_session_version_id: str = Body(..., description="灵思会话版本ID", embed=True),
login_user: UserPayload = Depends(get_login_user)) -> UnifiedResponseModel:
"""
开始执行灵思SOP
:param linsight_session_version_id:
:param login_user:
:return:
"""
session_version_model = await LinsightSessionVersionDao.get_by_id(
linsight_session_version_id=linsight_session_version_id)
if not session_version_model:
return resp_500(code=404, message="灵思会话版本不存在")
if login_user.user_id != session_version_model.user_id:
return resp_500(code=403, message="无权限执行该灵思SOP")
if session_version_model.status in [SessionVersionStatusEnum.COMPLETED, SessionVersionStatusEnum.TERMINATED,
SessionVersionStatusEnum.IN_PROGRESS]:
return resp_500(code=400, message="灵思会话版本已完成或正在执行,无法再次执行")
from bisheng.linsight.worker import LinsightQueue
try:
queue = LinsightQueue('queue', namespace="linsight", redis=redis_client)
await queue.put(data=linsight_session_version_id)
# 将sop写入到记录表
background_tasks.add_task(SOPManageService.add_sop_record, LinsightSOPRecord(
name=session_version_model.title,
description=None,
user_id=login_user.user_id,
content=session_version_model.sop,
linsight_version_id=session_version_model.id,
create_time=session_version_model.create_time,
))
except Exception as e:
logger.error(f"开始执行灵思任务失败: {str(e)}")
await InviteCodeService.revoke_invite_code(user_id=login_user.user_id)
return resp_500(code=500, message=str(e))
return resp_200(data=True, message="灵思执行任务已开始,执行结果将通过消息流返回")
# workbench 用户输入
@router.post("/workbench/user-input", summary="用户输入灵思", response_model=UnifiedResponseModel)
async def user_input(
session_version_id: str = Body(..., description="灵思会话版本ID"),
linsight_execute_task_id: str = Body(..., description="灵思执行任务ID"),
input_content: str = Body(..., description="用户输入内容"),
login_user: UserPayload = Depends(get_login_user)) -> UnifiedResponseModel:
"""
用户输入
:param session_version_id:
:param input_content:
:param linsight_execute_task_id:
:param login_user:
:return:
"""
session_version_model = await LinsightSessionVersionDao.get_by_id(
linsight_session_version_id=session_version_id)
if not session_version_model:
return resp_500(code=404, message="灵思会话版本不存在")
if login_user.user_id != session_version_model.user_id:
return resp_500(code=403, message="无权限输入该灵思SOP")
state_message_manager = LinsightStateMessageManager(session_version_id=session_version_id)
await state_message_manager.set_user_input(task_id=linsight_execute_task_id, user_input=input_content)
return resp_200(data=True, message="用户输入已提交")
# workbench 提交执行结果反馈
@router.post("/workbench/submit-feedback", summary="提交执行结果反馈", response_model=UnifiedResponseModel)
async def submit_feedback(
background_tasks: BackgroundTasks,
linsight_session_version_id: str = Body(..., description="灵思会话版本ID"),
feedback: str = Body(None, description="用户反馈意见"),
score: int = Body(0, ge=0, le=5, description="用户评分,1-5分"),
is_reexecute: bool = Body(False, description="是否重新执行"),
cancel_feedback: bool = Body(False, description="取消反馈"),
login_user: UserPayload = Depends(get_login_user)) -> UnifiedResponseModel:
"""
提交执行结果反馈
:param background_tasks:
:param cancel_feedback:
:param linsight_session_version_id:
:param feedback:
:param score:
:param is_reexecute:
:param login_user:
:return:
"""
session_version_model = await LinsightSessionVersionDao.get_by_id(
linsight_session_version_id=linsight_session_version_id)
if not session_version_model:
return resp_500(code=404, message="灵思会话版本不存在")
if login_user.user_id != session_version_model.user_id:
return resp_500(code=403, message="无权限提交该灵思的反馈")
if score is not None and 0 < score <= 5:
session_version_model.score = score
await SOPManageService.update_sop_record_score(session_version_model.id, score)
if feedback is not None:
session_version_model.execute_feedback = feedback
else:
session_version_model.execute_feedback = "用户未提供反馈"
# 如果是取消反馈
if cancel_feedback:
session_version_model.execute_feedback = "用户取消了反馈"
await LinsightSessionVersionDao.insert_one(session_version_model)
return resp_200(data=True, message="提交成功")
session_version_model = await LinsightSessionVersionDao.insert_one(session_version_model)
if is_reexecute:
# 重新执行灵思的逻辑
system_config = await settings.aget_all_config()
# 获取Linsight_invitation_code
linsight_invitation_code = system_config.get("linsight_invitation_code", False)
if linsight_invitation_code:
if await InviteCodeService.use_invite_code(user_id=login_user.user_id) is False:
return resp_500(code=400, message="您的灵思使用次数已用完,请使用新的邀请码激活灵思功能。")
# 灵思会话版本
linsight_session_version_model = LinsightSessionVersion(
session_id=session_version_model.session_id,
user_id=login_user.user_id,
question=session_version_model.question,
tools=session_version_model.tools,
org_knowledge_enabled=session_version_model.org_knowledge_enabled,
personal_knowledge_enabled=session_version_model.personal_knowledge_enabled,
files=session_version_model.files,
title=session_version_model.title
)
linsight_session_version_model = await LinsightSessionVersionDao.insert_one(linsight_session_version_model)
return resp_200(data=linsight_session_version_model.model_dump(),
message="提交成功。")
else:
if feedback is not None and feedback.strip() != "":
# 重新生成SOP记录到记录表
background_tasks.add_task(
LinsightWorkbenchImpl.feedback_regenerate_sop_task,
session_version_model,
feedback
)
pass
return resp_200(data=True, message="提交成功")
# workbench 终止执行
@router.post("/workbench/terminate-execute", summary="终止执行灵思", response_model=UnifiedResponseModel)
async def terminate_execute(
linsight_session_version_id: str = Body(..., description="灵思会话版本ID", embed=True),
login_user: UserPayload = Depends(get_login_user)) -> UnifiedResponseModel:
"""
终止执行灵思
:param linsight_session_version_id:
:param login_user:
:return:
"""
# 现终止执行灵思的逻辑
session_version_model = await LinsightSessionVersionDao.get_by_id(
linsight_session_version_id=linsight_session_version_id)
if not session_version_model:
return resp_500(code=404, message="灵思会话版本不存在")
if login_user.user_id != session_version_model.user_id:
return resp_500(code=403, message="无权限终止该灵思执行")
if session_version_model.status == SessionVersionStatusEnum.COMPLETED:
return resp_500(code=400, message="灵思会话版本已完成,无法终止执行")
if session_version_model.status == SessionVersionStatusEnum.TERMINATED:
return resp_500(code=400, message="灵思会话版本已终止执行")
from bisheng.linsight.worker import LinsightQueue
queue = LinsightQueue('queue', namespace="linsight", redis=redis_client)
try:
# 从队列中移除任务
await queue.remove(linsight_session_version_id)
except Exception as e:
logger.error(f"删除队列任务失败: {str(e)}")
# 更新状态为终止
session_version_model.status = SessionVersionStatusEnum.TERMINATED
state_message_manager = LinsightStateMessageManager(session_version_id=linsight_session_version_id)
await state_message_manager.set_session_version_info(session_version_model)
state_message_manager = LinsightStateMessageManager(session_version_id=session_version_model.id)
# 推送终止消息
await state_message_manager.push_message(
MessageData(
event_type=MessageEventType.TASK_TERMINATED,
data={
"message": "任务已被用户主动停止",
"session_id": session_version_model.id,
"terminated_at": datetime.now().isoformat()
}
)
)
return resp_200(data=True, message="灵思执行已终止")
# 获取当前会话所有灵思信息
@router.get("/workbench/session-version-list", summary="获取当前会话所有灵思信息", response_model=UnifiedResponseModel)
async def get_linsight_session_version_list(
session_id: str = Query(..., description="会话ID"),
login_user: UserPayload = Depends(get_login_user)) -> UnifiedResponseModel:
"""
获取当前会话所有灵思信息
:param session_id:
:param login_user:
:return:
"""
linsight_session_version_models = await LinsightWorkbenchImpl.get_linsight_session_version_list(session_id)
return resp_200([model.model_dump() for model in linsight_session_version_models])
# 获取执行任务详情
@router.get("/workbench/execute-task-detail", summary="获取执行任务详情", response_model=UnifiedResponseModel)
async def get_execute_task_detail(
session_version_id: str = Query(..., description="灵思会话版本ID"),
login_user: UserPayload = Depends(get_login_user)) -> UnifiedResponseModel:
"""
获取执行任务详情
:param session_version_id:
:param login_user:
:return:
"""
execute_task_models = await LinsightWorkbenchImpl.get_execute_task_detail(session_version_id)
return resp_200(execute_task_models)
# 建立灵思任务消息流 websocket
@router.websocket("/workbench/task-message-stream", name="task_message_stream")
async def task_message_stream(
websocket: WebSocket,
session_version_id: str = Query(..., description="灵思会话版本ID"),
Authorize: AuthJWT = Depends()):
"""
建立灵思任务消息流 websocket
:param Authorize:
:param websocket:
:param session_version_id:
:return:
"""
try:
Authorize.jwt_required(auth_from='websocket', websocket=websocket)
payload = Authorize.get_jwt_subject()
payload = json.loads(payload)
login_user = UserPayload(**payload)
message_handler = MessageStreamHandle(websocket=websocket, session_version_id=session_version_id)
await message_handler.connect()
except Exception as e:
await websocket.close(code=1000, reason=str(e))
return
# 批量下载任务文件
@router.post("/workbench/batch-download-files", summary="批量下载任务文件")
async def batch_download_files(
zip_name: str = Body(..., description="压缩包名称"),
file_info_list: List[BatchDownloadFilesSchema] = Body(..., description="文件信息列表"),
login_user: UserPayload = Depends(get_login_user)):
"""
批量下载任务文件
:param zip_name:
:param file_info_list:
:param login_user:
:return:
"""
try:
# 调用实现类处理批量下载
zip_bytes = await LinsightWorkbenchImpl.batch_download_files(file_info_list)
zip_name = zip_name if os.path.splitext(zip_name)[-1] == ".zip" else f"{zip_name}.zip"
# 转成 unicode 字符串
zip_name = parse.quote(zip_name)
return StreamingResponse(
iter([zip_bytes]),
media_type="application/zip",
headers={
"Content-Disposition": f"attachment; filename={zip_name}"
}
)
except Exception as e:
logger.error(f"批量下载文件失败: {str(e)}")
return resp_500(code=500, message=str(e))
# 获取队列排队状态
@router.get("/workbench/queue-status", summary="获取灵思队列排队状态", response_model=UnifiedResponseModel)
async def get_queue_status(
session_version_id: str = Query(..., description="灵思会话版本ID"),
login_user: UserPayload = Depends(get_login_user)) -> UnifiedResponseModel:
"""
获取灵思队列排队状态
:param session_version_id:
:param login_user:
:return:
"""
from bisheng.linsight.worker import LinsightQueue
queue = LinsightQueue('queue', namespace="linsight", redis=redis_client)
try:
index = await queue.index(session_version_id)
return resp_200(data={"index": index}, message="获取灵思队列排队状态成功")
except Exception as e:
logger.error(f"获取灵思队列排队状态失败: {str(e)}")
return resp_500(code=500, message=str(e))
@router.post("/sop/add", summary="添加灵思SOP", response_model=UnifiedResponseModel)
async def add_sop(
sop_obj: SOPManagementSchema = Body(..., description="SOP对象"),
login_user: UserPayload = Depends(get_login_user)) -> UnifiedResponseModel:
"""
添加灵思SOP
:return:
"""
if not login_user.is_admin():
return UnAuthorizedError.return_resp()
return await SOPManageService.add_sop(sop_obj, user_id=login_user.user_id)
@router.post("/sop/update", summary="更新灵思SOP", response_model=UnifiedResponseModel)
async def update_sop(
sop_obj: SOPManagementUpdateSchema = Body(..., description="SOP对象"),
login_user: UserPayload = Depends(get_admin_user)) -> UnifiedResponseModel:
"""
更新灵思SOP
:return:
"""
return await SOPManageService.update_sop(sop_obj)
@router.get("/sop/list", summary="获取灵思SOP列表", response_model=UnifiedResponseModel)
async def get_sop_list(
keywords: str = Query(None, description="搜索关键词"),
page: int = Query(1, ge=1, description="页码"),
page_size: int = Query(10, ge=1, le=100, description="每页数量"),
sort: Literal["asc", "desc"] = Query("desc", description="排序方式,asc或desc"),
login_user: UserPayload = Depends(get_login_user)) -> UnifiedResponseModel:
"""
获取灵思SOP列表
:return:
"""
if not login_user.is_admin():
return UnAuthorizedError.return_resp()
sop_pages = await LinsightSOPDao.get_sop_page(keywords=keywords, page=page, page_size=page_size, sort=sort)
return resp_200(data=sop_pages)
@router.get("/sop/record", summary="获取灵思SOP记录", response_model=UnifiedResponseModel)
async def get_sop_record(login_user: UserPayload = Depends(get_admin_user),
keyword: str = Query(None, description="搜索关键字"),
sort: str = Query(default='desc', description="排序方式,asc或desc"),
page: int = Query(1, ge=1, description="页码"),
page_size: int = Query(10, ge=1, le=100, description="每页数量")):
res, count = await SOPManageService.get_sop_record(keyword, sort, page, page_size)
return resp_200(PageList(total=count, list=res))
@router.post("/sop/record/sync", summary="同步sop记录到sop库", response_model=UnifiedResponseModel)
async def sync_sop_record(
login_user: UserPayload = Depends(get_admin_user),
record_ids: list[int] = Body(..., description="sop记录表里的唯一id"),
override: Optional[bool] = Body(default=False,
description="是否强制覆盖"),
save_new: Optional[bool] = Body(default=False,
description="是否另存为新sop")) -> UnifiedResponseModel:
"""
同步SOP记录到SOP库
"""
try:
repeat_name = await SOPManageService.sync_sop_record(record_ids, override, save_new)
return resp_200(data={
"repeat_name": repeat_name,
}, message="success")
except HTTPException as e:
raise e
except Exception as e:
logger.exception("sync_sop_record error")
return resp_500(code=500, message=f"指导手册 导入失败:{str(e)[:100]}") # 限制错误信息长度,避免过长
@router.post("/sop/upload", summary="批量导入SOP入库", response_model=UnifiedResponseModel)
async def upload_sop_file(
file: UploadFile = File(..., description="上传的SOP文件"),
override: Optional[bool] = Body(default=False, description="是否强制覆盖"),
save_new: Optional[bool] = Body(default=False, description="是否另存为新sop"),
ignore_error: Optional[bool] = Body(default=False, description="是否忽略文件找那个错误的记录"),
login_user: UserPayload = Depends(get_admin_user)) -> UnifiedResponseModel:
"""
批量导入SOP入库
"""
try:
# 调用实现类处理文件上传
repeat_name = await SOPManageService.upload_sop_file(login_user, file, ignore_error, override, save_new)
return resp_200(data={
"repeat_name": repeat_name,
}, message="指导手册文件上传成功")
except HTTPException as e:
raise e
except Exception as e:
logger.exception("SOP文件上传失败")
return resp_500(code=500, message=f"指导手册 导入失败:{str(e)[:100]}") # 限制错误信息长度,避免过长
@router.delete("/sop/remove", summary="删除灵思SOP", response_model=UnifiedResponseModel)
async def remove_sop(
sop_ids: List[int] = Body(..., description="SOP唯一ID列表", embed=True),
login_user: UserPayload = Depends(get_admin_user)) -> UnifiedResponseModel:
"""
删除灵思SOP
:return:
"""
return await SOPManageService.remove_sop(sop_ids, login_user)
+22 -2
View File
@@ -1,9 +1,9 @@
from fastapi import APIRouter, Request, Depends, Body, Query
from fastapi import APIRouter, Request, Depends, Body, Query, BackgroundTasks
from bisheng.api.services.llm import LLMService
from bisheng.api.services.user_service import UserPayload, get_login_user, get_admin_user
from bisheng.api.v1.schemas import resp_200, KnowledgeLLMConfig, \
AssistantLLMConfig, EvaluationLLMConfig, LLMServerCreateReq
AssistantLLMConfig, EvaluationLLMConfig, LLMServerCreateReq, WorkbenchModelConfig, UnifiedResponseModel, resp_500
router = APIRouter(prefix='/llm', tags=['LLM'])
@@ -50,6 +50,26 @@ def update_model_online(request: Request, login_user: UserPayload = Depends(get_
return resp_200(data=ret)
@router.get('/workbench', summary="获取工作台相关的模型配置", response_model=UnifiedResponseModel)
async def get_workbench_llm(request: Request, login_user: UserPayload = Depends(get_admin_user)):
""" 获取灵思相关的模型配置 """
ret = await LLMService.get_workbench_llm()
return resp_200(data=ret)
@router.post('/workbench', summary="更新工作台相关的模型配置", response_model=UnifiedResponseModel)
async def update_workbench_llm(
background_tasks: BackgroundTasks,
login_user: UserPayload = Depends(get_admin_user),
config_obj: WorkbenchModelConfig = Body(..., description="模型配置对象")):
""" 更新灵思相关的模型配置 """
try:
ret = await LLMService.update_workbench_llm(config_obj, background_tasks)
except Exception as e:
return resp_500(message=str(e))
return resp_200(data=ret)
@router.get('/knowledge')
def get_knowledge_llm(request: Request, login_user: UserPayload = Depends(get_login_user)):
ret = LLMService.get_knowledge_llm()
@@ -0,0 +1,11 @@
from ast import List
from typing import Optional
from pydantic import BaseModel
class CreateDatasetParam(BaseModel):
name: str
description: str
file_url: Optional[str]
qa_list: Optional[List[str]]
@@ -0,0 +1,23 @@
from typing import Optional
from pydantic import Field, BaseModel, field_validator
# SOP管理 Schema
class SOPManagementSchema(BaseModel):
"""SOP管理 Schema"""
name: str = Field(..., description="SOP名称")
description: str = Field(None, description="SOP描述")
content: str = Field(..., description="SOP内容")
rating: int = Field(0, ge=0, le=5, description="SOP评分,范围0-5")
linsight_session_id: Optional[str] = Field(default=None, description="Linsight会话ID")
@field_validator("name", mode="before")
def validate_name(cls, v):
# 限制SOP名称长度不超过500个字符
return v[:500]
class SOPManagementUpdateSchema(SOPManagementSchema):
"""SOP管理更新 Schema"""
id: int = Field(..., description="SOP唯一ID")
@@ -0,0 +1,54 @@
from typing import List, Dict, Optional
from pydantic import BaseModel, Field, field_validator
from bisheng.database.models.linsight_sop import LinsightSOPRecord
class ToolChildrenSchema(BaseModel):
id: int = Field(..., description="工具id")
name: Optional[str] = Field(None, description="工具名称")
tool_key: Optional[str] = Field(None, description="工具key")
desc: Optional[str] = Field(None, description="工具描述")
# 选择的toolSchema
class LinsightToolSchema(BaseModel):
id: int = Field(..., description="工具一级ID")
name: Optional[str] = Field(None, description="工具名称")
is_preset: int = Field(1, description="是否为预设工具")
desc: Optional[str] = Field(None, description="工具描述")
# child工具列表
children: Optional[List[ToolChildrenSchema]] = Field(..., description="子工具列表")
class SubmitFileSchema(BaseModel):
file_id: str = Field(..., description="文件唯一ID")
file_name: str = Field(..., description="文件名称")
parsing_status: str = Field(..., description="文件解析状态")
# 问题提交Schema
class LinsightQuestionSubmitSchema(BaseModel):
question: str = Field(..., description="用户提交的问题")
org_knowledge_enabled: bool = Field(False, description="是否启用组织知识库")
personal_knowledge_enabled: bool = Field(False, description="是否启用个人知识库")
files: List[SubmitFileSchema] = Field(None, description="上传的文件列表")
tools: List[LinsightToolSchema] = Field(None, description="可用的工具列表")
@field_validator("tools")
@classmethod
def validate_tools(cls, v: List[LinsightToolSchema]) -> List[Dict]:
if not v:
return []
# 将工具转换为字典格式
return [tool.model_dump() for tool in v]
class BatchDownloadFilesSchema(BaseModel):
file_name: str = Field(..., description="文件名称")
file_url: str = Field(..., description="文件下载链接")
class SopRecordRead(LinsightSOPRecord, table=False):
user_name: Optional[str] = Field(default=None, description="用户名称")
@@ -6,15 +6,19 @@ from pydantic import BaseModel, Field, field_validator
class WorkflowEventType(Enum):
NodeRun = 'node_run'
# 开场白
GuideWord = 'guide_word'
# 引导问题
GuideQuestion = 'guide_question'
# 告知用户,现在需要用户输入内容
UserInput = 'input'
# UserInput = 'user_input'
# 输出事件,返回预先定义的内容给用户
OutputMsg = 'output_msg'
# 输出的同时需要用户输入内容
OutputWithInput = 'output_with_input_msg'
# OutputWithInput = 'output_input_msg'
# 输出的同时需要用户选择内容
OutputWithChoose = 'output_with_choose_msg'
# OutputWithChoose = 'output_choose_msg'
# 流式输出事件,包含流式过程中、流式结束两个状态
StreamMsg = 'stream_msg'
Close = 'close'
Error = 'error'
+23 -1
View File
@@ -1,6 +1,6 @@
from datetime import datetime
from enum import Enum
from typing import Any, Dict, Generic, List, Optional, TypeVar, Union
from typing import Any, Dict, Generic, List, Optional, TypeVar, Union, Literal
from langchain.docstore.document import Document
from orjson import orjson
@@ -14,6 +14,7 @@ from bisheng.database.models.knowledge import KnowledgeRead
from bisheng.database.models.llm_server import LLMModelBase, LLMServerBase
from bisheng.database.models.message import ChatMessageRead
from bisheng.database.models.tag import Tag
from bisheng_langchain.linsight.const import TaskMode
class CaptchaInput(BaseModel):
@@ -451,6 +452,26 @@ class WSPrompt(BaseModel):
bingUrl: Optional[str] = None
class LinsightConfig(BaseModel):
"""
灵思管理配置
"""
input_placeholder: str = Field(..., description='输入框提示语')
tools: Optional[List[Dict]] = Field(None, description='灵思可选工具列表')
class WorkbenchModelConfig(BaseModel):
"""
灵思模型配置
"""
# 任务执行模型
task_model: Optional[WSModel] = Field(None, description='任务执行模型')
# 检索embedding模型
embedding_model: Optional[WSModel] = Field(None, description='embedding模型')
# 灵思执行模式
linsight_executor_mode: Optional[TaskMode] = Field(None, description='灵思执行模式')
class WorkstationConfig(BaseModel):
menuShow: bool = Field(default=True, description='是否显示左侧菜单栏')
maxTokens: Optional[int] = Field(default=1500, description='最大token数')
@@ -466,6 +487,7 @@ class WorkstationConfig(BaseModel):
knowledgeBase: Optional[WSPrompt] = None
fileUpload: Optional[WSPrompt] = None
systemPrompt: Optional[str] = None
linsightConfig: Optional[LinsightConfig] = Field(default=None, description='灵思配置')
class ExcelRule(BaseModel):
+15
View File
@@ -0,0 +1,15 @@
from fastapi import APIRouter
from bisheng.api.services.tool import ToolServices
from bisheng.api.v1.schemas import UnifiedResponseModel, resp_200
router = APIRouter(prefix='/tool', tags=['Tool'])
@router.get("/linsight/preset", summary="获取灵思预置工具列表", response_model=UnifiedResponseModel)
async def get_linsight_tools():
"""
获取灵思预置工具列表
"""
tools = await ToolServices.get_linsight_tools()
return resp_200(data=tools)
+27 -14
View File
@@ -11,19 +11,22 @@ from langchain_core.runnables import RunnableConfig
from loguru import logger
from bisheng.api.services import knowledge_imp
from bisheng.api.services.assistant_agent import AssistantAgent
from bisheng.api.services.knowledge import KnowledgeService
from bisheng.api.services.user_service import UserPayload, get_admin_user, get_login_user
from bisheng.api.services.workstation import (SSECallbackClient, WorkstationConversation,
WorkstationMessage, WorkStationService, SearchTool)
WorkstationMessage, WorkStationService)
from bisheng.api.v1.callback import AsyncStreamingLLMCallbackHandler
from bisheng.api.v1.schema.chat_schema import APIChatCompletion, SSEResponse, delta
from bisheng.api.v1.schemas import WorkstationConfig, resp_200, resp_500, WSPrompt, ExcelRule
from bisheng.api.v1.schemas import WorkstationConfig, resp_200, resp_500, WSPrompt, ExcelRule, UnifiedResponseModel
from bisheng.cache.redis import redis_client
from bisheng.cache.utils import file_download, save_download_file, save_uploaded_file
from bisheng.database.models.flow import FlowType
from bisheng.database.models.gpts_tools import GptsToolsDao
from bisheng.database.models.message import ChatMessage, ChatMessageDao
from bisheng.database.models.session import MessageSession, MessageSessionDao
from bisheng.interface.llms.custom import BishengLLM
from bisheng.settings import settings as bisheng_settings
router = APIRouter(prefix='/workstation', tags=['WorkStation'])
@@ -102,17 +105,27 @@ def final_message(conversation: MessageSession, title: str, requestMessage: Chat
return f'event: message\ndata: {msg}\n\n'
@router.get('/config')
@router.get('/config', summary='获取工作台配置', response_model=UnifiedResponseModel)
def get_config(
request: Request,
login_user: UserPayload = Depends(get_login_user),
):
login_user: UserPayload = Depends(get_login_user)):
""" 获取评价相关的模型配置 """
ret = WorkStationService.get_config()
etl4lm_settings = bisheng_settings.get_knowledge().get("etl4lm", {})
etl_for_lm_url = etl4lm_settings.get("url", None)
ret = ret.model_dump()
ret['enable_etl4lm'] = etl_for_lm_url is not None
linsight_invitation_code = bisheng_settings.get_all_config().get('linsight_invitation_code', None)
ret['linsight_invitation_code'] = linsight_invitation_code if linsight_invitation_code else False
ret['linsight_cache_dir'] = "./"
ret['waiting_list_url'] = bisheng_settings.get_linsight_conf().waiting_list_url
return resp_200(data=ret)
@router.post('/config')
@router.post('/config', summary='更新工作台配置', response_model=UnifiedResponseModel)
def update_config(
request: Request,
login_user: UserPayload = Depends(get_admin_user),
@@ -238,13 +251,13 @@ async def webSearch(query: str, web_search_config: WSPrompt):
"""
联网搜索
"""
if web_search_config.params:
tool = SearchTool.init_search_tool(web_search_config.tool, **web_search_config.params)
else:
# 兼容旧版的配置
tool = SearchTool.init_search_tool('bing', api_key=web_search_config.bingKey,
base_url=web_search_config.bingUrl)
return tool.invoke(query)
web_search_info = GptsToolsDao.get_tool_by_tool_key("web_search")
if not web_search_info:
raise Exception(f"No web_search tool found in database")
web_search_tool = await AssistantAgent.init_tools_by_tool_ids([web_search_info.id], None)
if not web_search_tool:
raise Exception(f"No web_search tool found in gpts tools")
return web_search_tool[0].invoke(input={"query": query})
def getFileContent(filepath):
@@ -372,7 +385,7 @@ async def chat_completions(
message.extra = json.dumps(extra, ensure_ascii=False)
ChatMessageDao.insert_one(message)
except Exception as e:
logger.error(f'Error in processing the prompt: {e}')
logger.exception(f'Error in processing the prompt')
error = True
final_res = 'Error in processing the prompt'
+18 -18
View File
@@ -2,26 +2,26 @@ import json
import os
from typing import Dict, List, Optional
from fastapi import (APIRouter, BackgroundTasks, Body, File, Form, HTTPException, Query, Request,
UploadFile)
from starlette.responses import FileResponse
from bisheng.api.services import knowledge_imp
from bisheng.api.services.knowledge import KnowledgeService
from bisheng.api.services.knowledge_imp import (decide_vectorstores, delete_es, delete_vector,
text_knowledge)
from bisheng.api.v1.schemas import (ChunkInput, KnowledgeFileOne, KnowledgeFileProcess,
UnifiedResponseModel, resp_200, resp_500, ExcelRule)
resp_200, resp_500, ExcelRule)
from bisheng.api.v2.schema.filelib import APIAddQAParam, APIAppendQAParam, QueryQAParam
from bisheng.api.v2.utils import get_default_operator
from bisheng.cache.utils import file_download, save_download_file
from bisheng.database.models.knowledge import (KnowledgeCreate, KnowledgeDao, KnowledgeTypeEnum,
KnowledgeUpdate)
from bisheng.database.models.knowledge_file import (KnowledgeFileRead, QAKnoweldgeDao, QAKnowledge,
QAKnowledgeUpsert)
from bisheng.database.models.knowledge_file import (QAKnoweldgeDao, QAKnowledgeUpsert)
from bisheng.database.models.message import ChatMessageDao
from bisheng.interface.embeddings.custom import FakeEmbedding
from bisheng.settings import settings
from bisheng.utils.logger import logger
from fastapi import (APIRouter, BackgroundTasks, Body, File, Form, HTTPException, Query, Request,
UploadFile)
from starlette.responses import FileResponse
# build router
router = APIRouter(prefix='/filelib', tags=['OpenAPI', 'Knowledge'])
@@ -44,18 +44,18 @@ def update_knowledge(*, request: Request, knowledge: KnowledgeUpdate):
@router.get('/', status_code=200)
def get_knowledge(*,
request: Request,
knowledge_type: int = Query(default=KnowledgeTypeEnum.NORMAL.value,
alias='type'),
name: str = None,
page_size: Optional[int] = 10,
page_num: Optional[int] = 1):
async def get_knowledge(*,
request: Request,
knowledge_type: int = Query(default=KnowledgeTypeEnum.NORMAL.value,
alias='type'),
name: str = None,
page_size: Optional[int] = 10,
page_num: Optional[int] = 1):
""" 读取所有知识库信息. """
knowledge_type = KnowledgeTypeEnum(knowledge_type)
login_user = get_default_operator()
res, total = KnowledgeService.get_knowledge(request, login_user, knowledge_type, name,
page_num, page_size)
res, total = await KnowledgeService.get_knowledge(request, login_user, knowledge_type, name,
page_num, page_size)
return resp_200(data={'data': res, 'total': total})
@@ -81,12 +81,12 @@ async def upload_file(
request: Request,
knowledge_id: int,
separator: Optional[List[str]] = Form(default=None,
description='切分文本规则, 不传则为默认'),
description='切分文本规则, 不传则为默认'),
separator_rule: Optional[List[str]] = Form(
default=None, description='切分规则前还是后进行切分;before/after'),
default=None, description='切分规则前还是后进行切分;before/after'),
chunk_size: Optional[int] = Form(default=None, description='切分文本长度,不传则为默认'),
chunk_overlap: Optional[int] = Form(default=None,
description='切分文本重叠长度,不传则为默认'),
description='切分文本重叠长度,不传则为默认'),
callback_url: Optional[str] = Form(default=None, description='回调地址'),
file_url: Optional[str] = Form(default=None, description='文件地址'),
file: Optional[UploadFile] = File(default=None, description='上传文件'),
+336 -34
View File
@@ -1,7 +1,10 @@
import pickle
from typing import Dict, Optional
import typing
from typing import Dict, Optional, Any, Coroutine
import redis
from redis.asyncio.client import Pipeline
from bisheng.settings import settings
from loguru import logger
from redis import ConnectionPool, RedisCluster
@@ -9,11 +12,14 @@ from redis.backoff import ExponentialBackoff
from redis.cluster import ClusterNode
from redis.retry import Retry
from redis.sentinel import Sentinel
from redis.asyncio.sentinel import Sentinel as AsyncSentinel
from redis.asyncio.cluster import RedisCluster as AsyncRedisCluster
from redis.asyncio import Redis as AsyncRedis
class RedisClient:
def __init__(self, url, max_connections=10):
def __init__(self, url, max_connections=100):
# # 哨兵模式
if isinstance(settings.redis_url, Dict):
redis_conf = dict(settings.redis_url)
@@ -31,18 +37,24 @@ class RedisClient:
self.connection = RedisCluster.from_url(cluster_url, **redis_conf,
retry=Retry(ExponentialBackoff(), 6),
cluster_error_retry_attempts=1)
self.async_connection: typing.Union[AsyncRedisCluster, AsyncRedis] = AsyncRedisCluster.from_url(
cluster_url, **redis_conf, retry=Retry(ExponentialBackoff(), 6), cluster_error_retry_attempts=1)
return
hosts = [eval(x) for x in redis_conf.pop('sentinel_hosts')]
password = redis_conf.pop('sentinel_password')
master = redis_conf.pop('sentinel_master')
sentinel = Sentinel(sentinels=hosts, socket_timeout=0.1, sentinel_kwargs={'password': password})
async_sentinel = AsyncSentinel(sentinels=hosts, socket_timeout=0.1, sentinel_kwargs={'password': password})
# 获取主节点的连接
self.connection = sentinel.master_for(master, socket_timeout=0.1, **redis_conf)
self.async_connection: AsyncRedis = async_sentinel.master_for(master, socket_timeout=0.1, **redis_conf)
else:
# 单机模式
self.pool = ConnectionPool.from_url(url, max_connections=max_connections)
self.async_pool = redis.asyncio.ConnectionPool.from_url(url, max_connections=max_connections)
self.connection = redis.StrictRedis(connection_pool=self.pool)
self.async_connection: AsyncRedis = redis.asyncio.Redis.from_pool(self.async_pool)
def set(self, key, value, expiration=3600):
try:
@@ -58,8 +70,21 @@ class RedisClient:
logger.error('pickle error, value={}', value)
except TypeError as exc:
raise TypeError('RedisCache only accepts values that can be pickled. ') from exc
finally:
self.close()
async def aset(self, key, value, expiration=3600):
try:
if pickled := pickle.dumps(value):
# await self.acluster_nodes(key)
if expiration:
result = await self.async_connection.setex(name=key, value=pickled, time=expiration)
else:
result = await self.async_connection.set(key, pickled)
if not result:
raise ValueError('RedisCache could not set the value.')
else:
logger.error('pickle error, value={}', value)
except TypeError as exc:
raise TypeError('RedisCache only accepts values that can be pickled. ') from exc
def setNx(self, key, value, expiration=3600):
try:
@@ -69,11 +94,115 @@ class RedisClient:
self.connection.expire(key, expiration)
if not result:
return False
return True
return True
except TypeError as exc:
raise TypeError('RedisCache only accepts values that can be pickled. ') from exc
finally:
self.close()
async def asetNx(self, key, value, expiration=3600):
try:
if pickled := pickle.dumps(value):
await self.acluster_nodes(key)
result = await self.async_connection.setnx(key, pickled)
await self.async_connection.expire(key, expiration)
if not result:
return False
return True
except TypeError as exc:
raise TypeError('RedisCache only accepts values that can be pickled. ') from exc
def setex(self, key, value, expiration=3600):
try:
if pickled := pickle.dumps(value):
self.cluster_nodes(key)
result = self.connection.setex(key, expiration, pickled)
if not result:
raise ValueError('RedisCache could not set the value.')
else:
logger.error('pickle error, value={}', value)
except TypeError as exc:
raise TypeError('RedisCache only accepts values that can be pickled. ') from exc
async def asetex(self, key, value, expiration=3600):
try:
if pickled := pickle.dumps(value):
await self.acluster_nodes(key)
result = await self.async_connection.setex(key, expiration, pickled)
if not result:
raise ValueError('RedisCache could not set the value.')
else:
logger.error('pickle error, value={}', value)
except TypeError as exc:
raise TypeError('RedisCache only accepts values that can be pickled. ') from exc
def mset(self, mapping: Dict[str, typing.Any], expiration: int = None) -> bool | None:
"""批量设置"""
try:
if not mapping:
return True
serialized_mapping = {k: pickle.dumps(v) for k, v in mapping.items() if v is not None}
result = self.connection.mset(serialized_mapping)
if expiration:
# 使用pipeline批量设置过期时间
pipe = self.connection.pipeline()
for key in mapping.keys():
pipe.expire(key, expiration)
pipe.execute()
return bool(result)
except TypeError as exc:
raise TypeError('RedisCache only accepts values that can be pickled. ') from exc
async def amset(self, mapping: Dict[str, typing.Any], expiration: int = None) -> bool | None:
"""异步批量设置"""
try:
if not mapping:
return True
serialized_mapping = {k: pickle.dumps(v) for k, v in mapping.items() if v is not None}
result = await self.async_connection.mset(serialized_mapping)
if expiration:
# 使用pipeline批量设置过期时间
pipe: Pipeline = self.async_connection.pipeline()
for key in mapping.keys():
await pipe.expire(key, expiration)
await pipe.execute()
return bool(result)
except TypeError as exc:
raise TypeError('RedisCache only accepts values that can be pickled. ') from exc
def mget(self, keys: typing.List[str]) -> typing.List[typing.Any] | None:
"""批量获取"""
try:
if not keys:
return []
values = self.connection.mget(keys)
return [pickle.loads(v) for v in values if v is not None]
except TypeError as exc:
raise TypeError('RedisCache only accepts values that can be pickled. ') from exc
async def amget(self, keys: typing.List[str]) -> typing.List[typing.Any] | None:
"""异步批量获取"""
try:
if not keys:
return []
values = await self.async_connection.mget(keys)
return [pickle.loads(v) for v in values if v is not None]
except TypeError as exc:
raise TypeError('RedisCache only accepts values that can be pickled. ') from exc
async def akeys(self, pattern: str) -> typing.List[str]:
"""异步获取匹配模式的所有键"""
try:
await self.acluster_nodes(pattern)
keys = await self.async_connection.keys(pattern)
return [key.decode('utf-8') for key in keys]
except Exception as e:
raise e
def hsetkey(self, name, key, value, expiration=3600):
try:
@@ -82,8 +211,18 @@ class RedisClient:
if expiration:
self.connection.expire(name, expiration)
return r
finally:
self.close()
except Exception as e:
raise e
async def ahsetkey(self, name, key, value, expiration=3600):
try:
await self.acluster_nodes(key)
r = await self.async_connection.hset(name, key, value)
if expiration:
await self.async_connection.expire(name, expiration)
return r
except Exception as e:
raise e
def hset(self, name,
key: Optional[str] = None,
@@ -97,37 +236,82 @@ class RedisClient:
if expiration:
self.connection.expire(name, expiration)
return r
finally:
self.close()
except Exception as e:
raise e
async def ahset(self, name,
key: Optional[str] = None,
value: Optional[str] = None,
mapping: Optional[dict] = None,
items: Optional[list] = None,
expiration: int = 3600):
try:
await self.acluster_nodes(name)
r = await self.async_connection.hset(name, key, value, mapping, items)
if expiration:
await self.async_connection.expire(name, expiration)
return r
except Exception as e:
raise e
def hget(self, name, key):
try:
self.cluster_nodes(name)
return self.connection.hget(name, key)
finally:
self.close()
except Exception as e:
raise e
async def ahget(self, name, key):
try:
await self.acluster_nodes(name)
return await self.async_connection.hget(name, key)
except Exception as e:
raise e
def hgetall(self, name):
try:
self.cluster_nodes(name)
return self.connection.hgetall(name)
finally:
self.close()
except Exception as e:
raise e
async def ahgetall(self, name):
try:
await self.acluster_nodes(name)
return await self.async_connection.hgetall(name)
except Exception as e:
raise e
def hdel(self, name, *keys):
try:
self.cluster_nodes(name)
return self.connection.hdel(name, *keys)
finally:
self.close()
except Exception as e:
raise e
async def ahdel(self, name, *keys):
try:
await self.acluster_nodes(name)
return await self.async_connection.hdel(name, *keys)
except Exception as e:
raise e
def get(self, key):
try:
self.cluster_nodes(key)
value = self.connection.get(key)
return pickle.loads(value) if value else None
finally:
self.close()
except Exception as e:
raise e
async def aget(self, key):
try:
await self.acluster_nodes(key)
value = await self.async_connection.get(key)
return pickle.loads(value) if value else None
except Exception as e:
# Handle the case where the value is None or not picklable
raise e
def incr(self, key, expiration=3600) -> int:
try:
@@ -136,22 +320,80 @@ class RedisClient:
if expiration:
self.connection.expire(key, expiration)
return value
finally:
self.close()
except Exception as e:
raise e
async def aincr(self, key, expiration=3600) -> int:
try:
await self.acluster_nodes(key)
value = await self.async_connection.incr(key)
if expiration:
await self.async_connection.expire(key, expiration)
return value
except Exception as e:
raise e
def expire_key(self, key, expiration: int):
try:
self.cluster_nodes(key)
self.connection.expire(key, expiration)
finally:
self.close()
except Exception as e:
raise e
async def aexpire_key(self, key, expiration: int):
try:
await self.acluster_nodes(key)
await self.async_connection.expire(key, expiration)
except Exception as e:
raise e
def delete(self, key):
try:
self.cluster_nodes(key)
return self.connection.delete(key)
finally:
self.close()
except Exception as e:
raise e
async def adelete(self, key):
try:
await self.acluster_nodes(key)
return await self.async_connection.delete(key)
except Exception as e:
raise e
async def alpush(self, key, value, expiration=3600):
try:
await self.acluster_nodes(key)
ret = await self.async_connection.lpush(key, value)
if expiration:
await self.aexpire_key(key, expiration)
return ret
except Exception as e:
raise e
async def ablpop(self, key, timeout=0):
try:
await self.acluster_nodes(key)
value = await self.async_connection.blpop(key, timeout)
return pickle.loads(value[1]) if value and value[1] else None
except Exception as e:
raise e
async def alrange(self, key, start=0, end=-1):
try:
await self.acluster_nodes(key)
values = await self.async_connection.lrange(key, start, end)
return [pickle.loads(v) for v in values if v is not None]
except Exception as e:
raise e
async def alrem(self, key, value):
try:
await self.acluster_nodes(key)
value = pickle.dumps(value) if not isinstance(value, bytes) else value
return await self.async_connection.lrem(key, 0, value)
except Exception as e:
raise e
def rpush(self, key, value, expiration=3600):
try:
@@ -160,33 +402,87 @@ class RedisClient:
if expiration:
self.expire_key(key, expiration)
return ret
finally:
self.close()
except Exception as e:
raise e
def lpop(self, key, count: int=None):
async def arpush(self, key, value, expiration=3600):
try:
await self.acluster_nodes(key)
value = pickle.dumps(value) if not isinstance(value, bytes) else value
ret = await self.async_connection.rpush(key, value)
if expiration:
await self.aexpire_key(key, expiration)
return ret
except Exception as e:
raise e
def lpop(self, key, count: int = None):
try:
self.cluster_nodes(key)
return self.connection.lpop(key, count)
finally:
self.close()
except Exception as e:
raise e
async def alpop(self, key, count: int = None):
try:
await self.acluster_nodes(key)
return await self.async_connection.lpop(key, count)
except Exception as e:
raise e
def publish(self, key, value):
try:
self.cluster_nodes(key)
return self.connection.publish(key, value)
finally:
self.close()
except Exception as e:
raise e
async def apublish(self, key, value):
try:
await self.acluster_nodes(key)
return await self.async_connection.publish(key, value)
except Exception as e:
raise e
def exists(self, key):
try:
self.cluster_nodes(key)
return self.connection.exists(key)
finally:
self.close()
except Exception as e:
raise e
async def aexists(self, key):
try:
await self.acluster_nodes(key)
return await self.async_connection.exists(key)
except Exception as e:
raise e
def close(self):
self.connection.close()
async def aclose(self):
"""Asynchronous close method for the Redis connection."""
if hasattr(self, 'async_connection') and self.async_connection:
await self.async_connection.close()
else:
logger.warning("No async connection to close.")
# ==================== Pipeline支持 ====================
def pipeline(self, transaction: bool = True) -> redis.client.Pipeline:
"""获取pipeline对象"""
return self.connection.pipeline(transaction=transaction)
def async_pipeline(self, transaction: bool = True) -> Pipeline:
"""获取异步pipeline对象"""
return self.async_connection.pipeline(transaction=transaction)
async def allen(self, key: str) -> int:
"""Check if the key is in the cache using the 'in' operator."""
await self.acluster_nodes(key)
return await self.async_connection.llen(key)
def __contains__(self, key):
"""Check if the key is in the cache."""
self.cluster_nodes(key)
@@ -213,6 +509,12 @@ class RedisClient:
target = self.connection.get_node_from_key(key)
self.connection.set_default_node(target)
async def acluster_nodes(self, key):
if isinstance(self.async_connection,
AsyncRedisCluster) and self.async_connection.get_default_node() is None:
target = self.async_connection.get_node_from_key(key)
self.async_connection.set_default_node(target)
# 示例用法
redis_client = RedisClient(settings.redis_url)
+36
View File
@@ -22,6 +22,7 @@ CACHE: Dict[str, Any] = {}
CACHE_DIR = user_cache_dir('bisheng', 'bisheng')
def create_cache_folder(func):
def wrapper(*args, **kwargs):
# Get the destination folder
@@ -34,6 +35,18 @@ def create_cache_folder(func):
return wrapper
def create_cache_folder_async(func):
async def wrapper(*args, **kwargs):
# Get the destination folder
cache_path = Path(CACHE_DIR) / PREFIX
# Create the destination folder if it doesn't exist
os.makedirs(cache_path, exist_ok=True)
return await func(*args, **kwargs)
return wrapper
def memoize_dict(maxsize=128):
cache = OrderedDict()
@@ -177,6 +190,29 @@ def upload_file_to_minio(file: UploadFile, object_name, bucket_name: str = tmp_b
return minio_client.get_share_link(object_name, bucket_name)
@create_cache_folder_async
async def save_file_to_folder(file: UploadFile, folder_name: str, file_name: str) -> str:
"""
保存上传的文件到folder_name文件夹
:param file:
:param folder_name:
:param file_name:
:return:
"""
cache_path = Path(CACHE_DIR)
folder_path = cache_path / folder_name
# Create the folder if it doesn't exist
os.makedirs(folder_path, exist_ok=True)
# Save the file to the specified folder
file_path = folder_path / file_name
with open(file_path, 'wb') as f:
content = await file.read()
f.write(content)
return str(file_path)
@create_cache_folder
def save_uploaded_file(file, folder_name, file_name, bucket_name: str = tmp_bucket):
"""
+67
View File
@@ -0,0 +1,67 @@
import asyncio
from typing import Optional, Dict, Any
from bisheng.prompts.prompt_loader import PromptLoader
from bisheng.utils.http_client import AsyncHttpClient
class AppContext:
def __init__(self):
# 缓存字典 用于存储以初始化的对象
self.cache: Dict[str, Any] = {}
async def get_http_client(self, loop: Optional[asyncio.AbstractEventLoop] = None) -> AsyncHttpClient:
"""
获取HTTP客户端如果未初始化则进行初始化
:return: AsyncHttpClient 实例
"""
key = "HTTP_CLIENT"
if key not in self.cache:
self.cache[key] = AsyncHttpClient()
await self.cache[key].get_aiohttp_client(loop=loop)
return self.cache[key]
def get_event_loop(self) -> asyncio.AbstractEventLoop:
"""
获取当前的事件循环如果没有则创建一个新的
:return: asyncio.AbstractEventLoop 实例
"""
key = "EVENT_LOOP"
if key not in self.cache:
self.cache[key] = asyncio.new_event_loop()
return self.cache[key]
# 获取 promptLoader
def get_prompt_loader(self) -> PromptLoader:
"""
获取 promptLoader如果未初始化则进行初始化
:return: promptLoader 实例
"""
key = "PROMPT_LOADER"
if key not in self.cache:
self.cache[key] = PromptLoader()
return self.cache[key]
app_ctx = AppContext()
# 需要优先加载的模块
async def init_app_context():
"""
初始化应用上下文
:param loop: 可选的事件循环
"""
loop = app_ctx.get_event_loop()
await app_ctx.get_http_client(loop=loop)
app_ctx.get_prompt_loader()
# 关闭应用上下文
async def close_app_context():
"""
关闭应用上下文释放资源
"""
# 清空缓存
app_ctx.cache.clear()
+59 -1
View File
@@ -1,4 +1,11 @@
from contextlib import contextmanager
import uuid
from contextlib import contextmanager, asynccontextmanager
from typing import Any, AsyncGenerator
from sqlalchemy import func
from sqlalchemy.ext.asyncio import async_sessionmaker
from sqlmodel.ext.asyncio.session import AsyncSession
from sqlmodel.sql.expression import SelectOfScalar
from bisheng.database.service import DatabaseService
from bisheng.settings import settings
@@ -20,3 +27,54 @@ def session_getter() -> Session:
raise
finally:
session.close()
@asynccontextmanager
async def async_session_getter() -> AsyncGenerator[AsyncSession, Any]:
"""轻量级异步session context"""
try:
async_session = async_sessionmaker(bind=db_service.async_engine, class_=AsyncSession, expire_on_commit=False)
async with async_session() as session:
yield session
except Exception as e:
logger.info('AsyncSession rollback because of exception:{}', e)
await session.rollback()
raise
finally:
await session.close()
def get_count(session: Session, q: SelectOfScalar) -> int:
"""
获取查询结果的数量
:param session:
:param q:
:return:
"""
count_q = q.with_only_columns(func.count()).order_by(None).select_from(q.get_final_froms()[0])
iterator = session.exec(count_q)
for count in iterator:
return count
return 0
async def async_get_count(session: AsyncSession, q: SelectOfScalar) -> int:
"""
获取异步查询结果的数量
:param session:
:param q:
:return:
"""
count_q = q.with_only_columns(func.count()).order_by(None).select_from(q.get_final_froms()[0])
iterator = await session.exec(count_q)
for count in iterator:
return count
return 0
def uuid_hex() -> str:
"""
生成一个UUID的十六进制字符串
:return: UUID的十六进制字符串
"""
return uuid.uuid4().hex
@@ -42,7 +42,17 @@
"create_time": "2024-03-29 14:39:37",
"update_time": "2024-03-29 14:39:37",
"id": 3,
"api_params": [{"name": "query", "description": "search query to look up", "in": "query", "required": true, "schema": {"type": "string"}}]
"api_params": [
{
"name": "query",
"description": "search query to look up",
"in": "query",
"required": true,
"schema": {
"type": "string"
}
}
]
},
{
"name": "Dalle3绘画",
@@ -56,7 +66,17 @@
"create_time": "2024-03-29 14:40:32",
"update_time": "2024-03-29 14:40:32",
"id": 4,
"api_params": [{"name": "query", "description": "Description about image.", "in": "query", "required": true, "schema": {"type": "string"}}]
"api_params": [
{
"name": "query",
"description": "Description about image.",
"in": "query",
"required": true,
"schema": {
"type": "string"
}
}
]
},
{
"name": "Bing web搜索",
@@ -70,7 +90,17 @@
"create_time": "2024-03-29 14:41:16",
"update_time": "2024-03-29 14:41:16",
"id": 5,
"api_params": [{"name": "query", "description": "query to look up in Bing search", "in": "query", "required": true, "schema": {"type": "string"}}]
"api_params": [
{
"name": "query",
"description": "query to look up in Bing search",
"in": "query",
"required": true,
"schema": {
"type": "string"
}
}
]
},
{
"name": "代码执行器",
@@ -84,7 +114,17 @@
"create_time": "2024-03-29 14:42:17",
"update_time": "2024-03-29 14:42:17",
"id": 6,
"api_params": [{"name": "python_code", "description": "The pure python script to be evaluated. \\nThe contents will be in main.py. \\nIt should not be in markdown format.", "in": "query", "required": true, "schema": {"type": "string"}}]
"api_params": [
{
"name": "python_code",
"description": "The pure python script to be evaluated. \\nThe contents will be in main.py. \\nIt should not be in markdown format.",
"in": "query",
"required": true,
"schema": {
"type": "string"
}
}
]
},
{
"name": "人员所有公司",
@@ -98,7 +138,26 @@
"create_time": "2024-03-29 14:44:34",
"update_time": "2024-03-29 14:44:34",
"id": 7,
"api_params": [{"name": "query", "description": "human who you want to search", "in": "query", "required": true, "schema": {"type": "string"}}, {"name": "name", "description": "company name which human worked", "in": "query", "required": true, "schema": {"type": "string"}}]
"api_params": [
{
"name": "query",
"description": "human who you want to search",
"in": "query",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "name",
"description": "company name which human worked",
"in": "query",
"required": true,
"schema": {
"type": "string"
}
}
]
},
{
"name": "搜索企业",
@@ -112,7 +171,17 @@
"create_time": "2024-03-29 14:50:07",
"update_time": "2024-03-29 14:50:07",
"id": 9,
"api_params": [{"name": "query", "description": "搜索关键字(公司名称、公司id、注册号或社会统一信用代码)", "in": "query", "required": true, "schema": {"type": "string"}}]
"api_params": [
{
"name": "query",
"description": "搜索关键字(公司名称、公司id、注册号或社会统一信用代码)",
"in": "query",
"required": true,
"schema": {
"type": "string"
}
}
]
},
{
"name": "企业基本信息",
@@ -126,7 +195,17 @@
"create_time": "2024-03-29 14:51:06",
"update_time": "2024-03-29 14:51:06",
"id": 10,
"api_params": [{"name": "query", "description": "搜索关键字(公司名称、公司id、注册号或社会统一信用代码)", "in": "query", "required": true, "schema": {"type": "string"}}]
"api_params": [
{
"name": "query",
"description": "搜索关键字(公司名称、公司id、注册号或社会统一信用代码)",
"in": "query",
"required": true,
"schema": {
"type": "string"
}
}
]
},
{
"name": "企业知识产权信息",
@@ -140,7 +219,17 @@
"create_time": "2024-03-29 14:51:33",
"update_time": "2024-03-29 14:51:33",
"id": 11,
"api_params": [{"name": "query", "description": "搜索关键字(公司名称、公司id、注册号或社会统一信用代码)", "in": "query", "required": true, "schema": {"type": "string"}}]
"api_params": [
{
"name": "query",
"description": "搜索关键字(公司名称、公司id、注册号或社会统一信用代码)",
"in": "query",
"required": true,
"schema": {
"type": "string"
}
}
]
},
{
"name": "企业司法风险",
@@ -154,7 +243,17 @@
"create_time": "2024-03-29 14:52:13",
"update_time": "2024-03-29 14:52:13",
"id": 12,
"api_params": [{"name": "query", "description": "搜索关键字(公司名称、公司id、注册号或社会统一信用代码)", "in": "query", "required": true, "schema": {"type": "string"}}]
"api_params": [
{
"name": "query",
"description": "搜索关键字(公司名称、公司id、注册号或社会统一信用代码)",
"in": "query",
"required": true,
"schema": {
"type": "string"
}
}
]
},
{
"name": "企业法律诉讼",
@@ -168,7 +267,17 @@
"create_time": "2024-03-29 14:52:36",
"update_time": "2024-03-29 14:52:36",
"id": 13,
"api_params": [{"name": "query", "description": "搜索关键字(公司名称、公司id、注册号或社会统一信用代码)", "in": "query", "required": true, "schema": {"type": "string"}}]
"api_params": [
{
"name": "query",
"description": "搜索关键字(公司名称、公司id、注册号或社会统一信用代码)",
"in": "query",
"required": true,
"schema": {
"type": "string"
}
}
]
},
{
"name": "企业工商信息",
@@ -182,7 +291,17 @@
"create_time": "2024-03-29 14:53:06",
"update_time": "2024-03-29 14:53:06",
"id": 14,
"api_params": [{"name": "query", "description": "搜索关键字(公司名称、公司id、注册号或社会统一信用代码)", "in": "query", "required": true, "schema": {"type": "string"}}]
"api_params": [
{
"name": "query",
"description": "搜索关键字(公司名称、公司id、注册号或社会统一信用代码)",
"in": "query",
"required": true,
"schema": {
"type": "string"
}
}
]
},
{
"name": "企业工商信息变更记录",
@@ -196,7 +315,17 @@
"create_time": "2024-03-29 14:54:02",
"update_time": "2024-03-29 14:54:02",
"id": 15,
"api_params": [{"name": "query", "description": "搜索关键字(公司名称、公司id、注册号或社会统一信用代码)", "in": "query", "required": true, "schema": {"type": "string"}}]
"api_params": [
{
"name": "query",
"description": "搜索关键字(公司名称、公司id、注册号或社会统一信用代码)",
"in": "query",
"required": true,
"schema": {
"type": "string"
}
}
]
},
{
"name": "企业股东",
@@ -210,7 +339,17 @@
"create_time": "2024-03-29 14:54:28",
"update_time": "2024-03-29 14:54:28",
"id": 16,
"api_params": [{"name": "query", "description": "搜索关键字(公司名称、公司id、注册号或社会统一信用代码)", "in": "query", "required": true, "schema": {"type": "string"}}]
"api_params": [
{
"name": "query",
"description": "搜索关键字(公司名称、公司id、注册号或社会统一信用代码)",
"in": "query",
"required": true,
"schema": {
"type": "string"
}
}
]
},
{
"name": "企业天眼风险",
@@ -224,7 +363,17 @@
"create_time": "2024-03-29 14:54:57",
"update_time": "2024-03-29 14:54:57",
"id": 17,
"api_params": [{"name": "query", "description": "搜索关键字(公司名称、公司id、注册号或社会统一信用代码)", "in": "query", "required": true, "schema": {"type": "string"}}]
"api_params": [
{
"name": "query",
"description": "搜索关键字(公司名称、公司id、注册号或社会统一信用代码)",
"in": "query",
"required": true,
"schema": {
"type": "string"
}
}
]
},
{
"name": "股票实时行情",
@@ -239,7 +388,35 @@
"create_time": "2024-03-29 14:56:15",
"update_time": "2024-03-29 14:56:15",
"id": 18,
"api_params": [{"name": "prefix", "description": "前缀。如果是\"stock_symbol\"传入的为股票代码,则需要传入s_;\n如果\"stock_symbol\"传入的为指数代码,则为空。", "in": "query", "required": true, "schema": {"type": "string"}}, {"name": "stock_exchange", "description": "交易所简写。股票上市的交易所,或者发布行情指数的交易所。可选项有\"sh\"(上海证券交易所)、\" sz\"( 深圳证券交易所)、\"bj\"( 北京证券交易所)", "in": "query", "required": true, "schema": {"type": "string"}}, {"name": "stock_symbol", "description": "6位数字的股票或者指数代码。\\n参考信息:\\n- 如果问题中未给出,可能需要上网查询。\\n- 上交所股票通常以 6 开头,深交所股票通常以 0、3 开头,北交所股票通常以 8 开头。\\n- 上交所行情指数通常以 000 开头,深交所指数通常以 399 开头。同一个指数可能会同时在两个交易所发布,例如沪深 300 有\"sh000300\"和\"sz399300\"两个代码。", "in": "query", "required": true, "schema": {"type": "string"}}]
"api_params": [
{
"name": "prefix",
"description": "前缀。如果是\"stock_symbol\"传入的为股票代码,则需要传入s_;\n如果\"stock_symbol\"传入的为指数代码,则为空。",
"in": "query",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "stock_exchange",
"description": "交易所简写。股票上市的交易所,或者发布行情指数的交易所。可选项有\"sh\"(上海证券交易所)、\" sz\"( 深圳证券交易所)、\"bj\"( 北京证券交易所)",
"in": "query",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "stock_symbol",
"description": "6位数字的股票或者指数代码。\\n参考信息:\\n- 如果问题中未给出,可能需要上网查询。\\n- 上交所股票通常以 6 开头,深交所股票通常以 0、3 开头,北交所股票通常以 8 开头。\\n- 上交所行情指数通常以 000 开头,深交所指数通常以 399 开头。同一个指数可能会同时在两个交易所发布,例如沪深 300 有\"sh000300\"和\"sz399300\"两个代码。",
"in": "query",
"required": true,
"schema": {
"type": "string"
}
}
]
},
{
"name": "股票历史行情",
@@ -254,7 +431,35 @@
"create_time": "2024-03-29 14:56:38",
"update_time": "2024-03-29 14:56:38",
"id": 19,
"api_params": [{"name": "date", "description": "需要查询的时间,按照”2024-03-26“格式,传入日期", "in": "query", "required": true, "schema": {"type": "string"}}, {"name": "stock_exchange", "description": "交易所简写。股票上市的交易所,或者发布行情指数的交易所。可选项有\"sh\"(上海证券交易所)、\" sz\"( 深圳证券交易所)、\"bj\"( 北京证券交易所)", "in": "query", "required": true, "schema": {"type": "string"}}, {"name": "stock_symbol", "description": "6位数字的股票或者指数代码。\\n参考信息:\\n- 如果问题中未给出,可能需要上网查询。\\n- 上交所股票通常以 6 开头,深交所股票通常以 0、3 开头,北交所股票通常以 8 开头。\\n- 上交所行情指数通常以 000 开头,深交所指数通常以 399 开头。同一个指数可能会同时在两个交易所发布,例如沪深 300 有\"sh000300\"和\"sz399300\"两个代码。", "in": "query", "required": true, "schema": {"type": "string"}}]
"api_params": [
{
"name": "date",
"description": "需要查询的时间,按照”2024-03-26“格式,传入日期",
"in": "query",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "stock_exchange",
"description": "交易所简写。股票上市的交易所,或者发布行情指数的交易所。可选项有\"sh\"(上海证券交易所)、\" sz\"( 深圳证券交易所)、\"bj\"( 北京证券交易所)",
"in": "query",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "stock_symbol",
"description": "6位数字的股票或者指数代码。\\n参考信息:\\n- 如果问题中未给出,可能需要上网查询。\\n- 上交所股票通常以 6 开头,深交所股票通常以 0、3 开头,北交所股票通常以 8 开头。\\n- 上交所行情指数通常以 000 开头,深交所指数通常以 399 开头。同一个指数可能会同时在两个交易所发布,例如沪深 300 有\"sh000300\"和\"sz399300\"两个代码。",
"in": "query",
"required": true,
"schema": {
"type": "string"
}
}
]
},
{
"name": "社会融资规模增量",
@@ -269,7 +474,26 @@
"create_time": "2024-03-29 14:59:06",
"update_time": "2024-03-29 14:59:06",
"id": 20,
"api_params": [{"name": "start_date", "description": "开始月份, 使用YYYY-MM-DD 方式表示", "in": "query", "required": true, "schema": {"type": "string"}}, {"name": "end_date", "description": "结束月份, 使用YYYY-MM-DD 方式表示", "in": "query", "required": true, "schema": {"type": "string"}}]
"api_params": [
{
"name": "start_date",
"description": "开始月份, 使用YYYY-MM-DD 方式表示",
"in": "query",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "end_date",
"description": "结束月份, 使用YYYY-MM-DD 方式表示",
"in": "query",
"required": true,
"schema": {
"type": "string"
}
}
]
},
{
"name": "GDP",
@@ -284,7 +508,26 @@
"create_time": "2024-03-29 14:59:38",
"update_time": "2024-03-29 14:59:38",
"id": 21,
"api_params": [{"name": "start_date", "description": "开始月份, 使用YYYY-MM-DD 方式表示", "in": "query", "required": true, "schema": {"type": "string"}}, {"name": "end_date", "description": "结束月份, 使用YYYY-MM-DD 方式表示", "in": "query", "required": true, "schema": {"type": "string"}}]
"api_params": [
{
"name": "start_date",
"description": "开始月份, 使用YYYY-MM-DD 方式表示",
"in": "query",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "end_date",
"description": "结束月份, 使用YYYY-MM-DD 方式表示",
"in": "query",
"required": true,
"schema": {
"type": "string"
}
}
]
},
{
"name": "CPI",
@@ -299,7 +542,26 @@
"create_time": "2024-03-29 15:00:00",
"update_time": "2024-03-29 15:00:00",
"id": 22,
"api_params": [{"name": "start_date", "description": "开始月份, 使用YYYY-MM-DD 方式表示", "in": "query", "required": true, "schema": {"type": "string"}}, {"name": "end_date", "description": "结束月份, 使用YYYY-MM-DD 方式表示", "in": "query", "required": true, "schema": {"type": "string"}}]
"api_params": [
{
"name": "start_date",
"description": "开始月份, 使用YYYY-MM-DD 方式表示",
"in": "query",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "end_date",
"description": "结束月份, 使用YYYY-MM-DD 方式表示",
"in": "query",
"required": true,
"schema": {
"type": "string"
}
}
]
},
{
"name": "PPI",
@@ -314,7 +576,26 @@
"create_time": "2024-03-29 15:00:24",
"update_time": "2024-03-29 15:00:24",
"id": 23,
"api_params": [{"name": "start_date", "description": "开始月份, 使用YYYY-MM-DD 方式表示", "in": "query", "required": true, "schema": {"type": "string"}}, {"name": "end_date", "description": "结束月份, 使用YYYY-MM-DD 方式表示", "in": "query", "required": true, "schema": {"type": "string"}}]
"api_params": [
{
"name": "start_date",
"description": "开始月份, 使用YYYY-MM-DD 方式表示",
"in": "query",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "end_date",
"description": "结束月份, 使用YYYY-MM-DD 方式表示",
"in": "query",
"required": true,
"schema": {
"type": "string"
}
}
]
},
{
"name": "货币供应量",
@@ -329,7 +610,26 @@
"create_time": "2024-03-29 15:00:54",
"update_time": "2024-03-29 15:00:54",
"id": 24,
"api_params": [{"name": "start_date", "description": "开始月份, 使用YYYY-MM-DD 方式表示", "in": "query", "required": true, "schema": {"type": "string"}}, {"name": "end_date", "description": "结束月份, 使用YYYY-MM-DD 方式表示", "in": "query", "required": true, "schema": {"type": "string"}}]
"api_params": [
{
"name": "start_date",
"description": "开始月份, 使用YYYY-MM-DD 方式表示",
"in": "query",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "end_date",
"description": "结束月份, 使用YYYY-MM-DD 方式表示",
"in": "query",
"required": true,
"schema": {
"type": "string"
}
}
]
},
{
"name": "社会消费品零售总额",
@@ -344,7 +644,26 @@
"create_time": "2024-03-29 15:01:47",
"update_time": "2024-03-29 15:01:47",
"id": 25,
"api_params": [{"name": "start_date", "description": "开始月份, 使用YYYY-MM-DD 方式表示", "in": "query", "required": true, "schema": {"type": "string"}}, {"name": "end_date", "description": "结束月份, 使用YYYY-MM-DD 方式表示", "in": "query", "required": true, "schema": {"type": "string"}}]
"api_params": [
{
"name": "start_date",
"description": "开始月份, 使用YYYY-MM-DD 方式表示",
"in": "query",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "end_date",
"description": "结束月份, 使用YYYY-MM-DD 方式表示",
"in": "query",
"required": true,
"schema": {
"type": "string"
}
}
]
},
{
"name": "PMI",
@@ -359,7 +678,26 @@
"create_time": "2024-04-15 12:01:11",
"update_time": "2024-04-15 12:01:11",
"id": 26,
"api_params": [{"name": "start_date", "description": "开始月份, 使用YYYY-MM-DD 方式表示", "in": "query", "required": true, "schema": {"type": "string"}}, {"name": "end_date", "description": "结束月份, 使用YYYY-MM-DD 方式表示", "in": "query", "required": true, "schema": {"type": "string"}}]
"api_params": [
{
"name": "start_date",
"description": "开始月份, 使用YYYY-MM-DD 方式表示",
"in": "query",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "end_date",
"description": "结束月份, 使用YYYY-MM-DD 方式表示",
"in": "query",
"required": true,
"schema": {
"type": "string"
}
}
]
},
{
"name": "中美国债收益率",
@@ -374,13 +712,51 @@
"create_time": "2024-04-15 12:03:27",
"update_time": "2024-04-15 12:03:27",
"id": 27,
"api_params": [{"name": "start_date", "description": "开始月份, 使用YYYY-MM-DD 方式表示", "in": "query", "required": true, "schema": {"type": "string"}}, {"name": "end_date", "description": "结束月份, 使用YYYY-MM-DD 方式表示", "in": "query", "required": true, "schema": {"type": "string"}}]
"api_params": [
{
"name": "start_date",
"description": "开始月份, 使用YYYY-MM-DD 方式表示",
"in": "query",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "end_date",
"description": "结束月份, 使用YYYY-MM-DD 方式表示",
"in": "query",
"required": true,
"schema": {
"type": "string"
}
}
]
},
{
"name": "发送钉钉群消息",
"logo": null,
"desc": "通过钉钉群机器人,快速将消息推送到指定钉钉群组中",
"api_params": [{"in": "query", "name": "url", "schema": {"type": "string"}, "required": true, "description": "自定义机器人的Wehhook地址"}, {"in": "query", "name": "message", "schema": {"type": "string"}, "required": true, "description": "发送的文本消息内容"}],
"api_params": [
{
"in": "query",
"name": "url",
"schema": {
"type": "string"
},
"required": true,
"description": "自定义机器人的Wehhook地址"
},
{
"in": "query",
"name": "message",
"schema": {
"type": "string"
},
"required": true,
"description": "发送的文本消息内容"
}
],
"tool_key": "ding_send_message",
"type": 9,
"extra": null,
@@ -395,7 +771,17 @@
"name": "单页面爬取|对应 Scrape 模式",
"logo": null,
"desc": "爬取并返回指定 URL 页面的内容,不会爬取子页面。",
"api_params": [{"in": "path", "name": "target_url", "schema": {"type": "string"}, "required": true, "description": "要爬取的网站 url"}],
"api_params": [
{
"in": "path",
"name": "target_url",
"schema": {
"type": "string"
},
"required": true,
"description": "要爬取的网站 url"
}
],
"tool_key": "fire_search_scrape",
"type": 10,
"extra": null,
@@ -410,7 +796,17 @@
"name": "获取单网页",
"logo": null,
"desc": "爬取指定URL(支持pdf),并将其转换为适合大模型处理的markdown格式",
"api_params": [{"in": "path", "name": "target_url", "schema": {"type": "string"}, "required": true, "description": "要获取的目标网页"}],
"api_params": [
{
"in": "path",
"name": "target_url",
"schema": {
"type": "string"
},
"required": true,
"description": "要获取的目标网页"
}
],
"tool_key": "jina_get_markdown",
"type": 11,
"is_preset": 1,
@@ -424,7 +820,26 @@
"name": "Stable diffusion",
"logo": null,
"desc": "使用Stable Diffusion模型,根据用户提示词生成图像",
"api_params": [{"in": "path", "name": "prompt", "schema": {"type": "string"}, "required": true, "description": "提示词生成图片描述词"}, {"in": "path", "name": "negative_prompt", "schema": {"type": "string"}, "required": false, "description": "不希望图片包含的内容"}],
"api_params": [
{
"in": "path",
"name": "prompt",
"schema": {
"type": "string"
},
"required": true,
"description": "提示词生成图片描述词"
},
{
"in": "path",
"name": "negative_prompt",
"schema": {
"type": "string"
},
"required": false,
"description": "不希望图片包含的内容"
}
],
"tool_key": "silicon_stable_diffusion",
"type": 12,
"is_preset": 1,
@@ -438,7 +853,17 @@
"name": "Flux",
"logo": null,
"desc": "使用Flux模型,根据用户提示词生成图像",
"api_params": [{"in": "path", "name": "prompt", "schema": {"type": "string"}, "required": true, "description": "提示词生成图片描述词(建议使用英文)"}],
"api_params": [
{
"in": "path",
"name": "prompt",
"schema": {
"type": "string"
},
"required": true,
"description": "提示词生成图片描述词(建议使用英文)"
}
],
"tool_key": "silicon_flux",
"type": 12,
"is_preset": 1,
@@ -452,7 +877,35 @@
"name": "向指定用户或者群聊发送消息",
"logo": null,
"desc": "向指定用户或者群聊发送消息",
"api_params": [{"in": "query", "name": "message", "schema": {"type": "string"}, "required": true, "description": "发送的文本消息内容"}, {"in": "query", "name": "receive_id", "schema": {"type": "string"}, "required": true, "description": "消息接收者的id"}, {"in": "query", "name": "receive_id_type", "schema": {"type": "string"}, "required": true, "description": "用户id类型,可选值:open_id(标识一个用户在某个应用中的身份);union_id(标识一个用户在某个应用开发商下的身份);user_id(标识一个用户在某个租户内的身份);email(以用户的真实邮箱来标识用户);chat_id(以群 ID 来标识群聊)"}],
"api_params": [
{
"in": "query",
"name": "message",
"schema": {
"type": "string"
},
"required": true,
"description": "发送的文本消息内容"
},
{
"in": "query",
"name": "receive_id",
"schema": {
"type": "string"
},
"required": true,
"description": "消息接收者的id"
},
{
"in": "query",
"name": "receive_id_type",
"schema": {
"type": "string"
},
"required": true,
"description": "用户id类型,可选值:open_id(标识一个用户在某个应用中的身份);union_id(标识一个用户在某个应用开发商下的身份);user_id(标识一个用户在某个租户内的身份);email(以用户的真实邮箱来标识用户);chat_id(以群 ID 来标识群聊)"
}
],
"tool_key": "feishu_send_message",
"type": 13,
"is_preset": 1,
@@ -466,7 +919,26 @@
"name": "发送企业微信群消息",
"logo": null,
"desc": "通过企业微信群机器人,快速将消息推送到指定企业微信群组中",
"api_params": [{"in": "query", "name": "url", "schema": {"type": "string"}, "required": true, "description": "微信机器人的webhook地址"}, {"in": "query", "name": "message", "schema": {"type": "string"}, "required": true, "description": "发送的消息内容"}],
"api_params": [
{
"in": "query",
"name": "url",
"schema": {
"type": "string"
},
"required": true,
"description": "微信机器人的webhook地址"
},
{
"in": "query",
"name": "message",
"schema": {
"type": "string"
},
"required": true,
"description": "发送的消息内容"
}
],
"tool_key": "wechat_send_message",
"type": 15,
"extra": "{}",
@@ -481,7 +953,35 @@
"name": "发送邮件",
"logo": null,
"desc": "给单个或多个邮箱发送邮件(多个邮箱账号使用\",\"分隔)",
"api_params": [{"in": "query", "name": "receiver", "schema": {"type": "string"}, "required": true, "description": "接受邮件的邮箱地址,确定邮件发送对象"}, {"in": "query", "name": "subject", "schema": {"type": "string"}, "required": false, "description": "邮件主题"}, {"in": "query", "name": "content", "schema": {"type": "string"}, "required": true, "description": "邮件的正文内容"}],
"api_params": [
{
"in": "query",
"name": "receiver",
"schema": {
"type": "string"
},
"required": true,
"description": "接受邮件的邮箱地址,确定邮件发送对象"
},
{
"in": "query",
"name": "subject",
"schema": {
"type": "string"
},
"required": false,
"description": "邮件主题"
},
{
"in": "query",
"name": "content",
"schema": {
"type": "string"
},
"required": true,
"description": "邮件的正文内容"
}
],
"tool_key": "email_send_email",
"type": 14,
"is_preset": 1,
@@ -495,7 +995,17 @@
"name": "深度爬取|对应 Crawl 模式",
"logo": null,
"desc": "爬取并返回指定 URL 以及所有可访问子页面的内容。",
"api_params": [{"in": "path", "name": "target_url", "schema": {"type": "string"}, "required": true, "description": "要爬取网站的起始 url。"}],
"api_params": [
{
"in": "path",
"name": "target_url",
"schema": {
"type": "string"
},
"required": true,
"description": "要爬取网站的起始 url。"
}
],
"tool_key": "fire_search_crawl",
"type": 10,
"is_preset": 1,
@@ -509,7 +1019,71 @@
"name": "获取指定单聊、群聊的历史消息",
"logo": null,
"desc": "支持在单聊或群聊中快速获取相关内容",
"api_params": [{"in": "query", "name": "container_id", "schema": {"type": "string"}, "required": true, "description": "单聊或群聊的id,或话题 id"}, {"in": "query", "name": "container_id_type", "schema": {"type": "string"}, "required": true, "description": "容器类型。 可选值有: chat:包含单聊(p2p)和群聊(group); thread:话题 。"}, {"in": "query", "name": "start_time", "schema": {"type": "string"}, "required": false, "description": "待查询历史信息的起始时间,秒级时间戳。 注意:thread 容器类型暂不支持获取指定时间范围内的消息。"}, {"in": "query", "name": "end_time", "schema": {"type": "string"}, "required": false, "description": "待查询历史信息的结束时间,秒级时间戳。注意:thread 容器类型暂不支持获取指定时间范围内的消息。"}, {"in": "query", "name": "page_size", "schema": {"type": "string"}, "required": false, "description": "分页大小,单次请求所返回的数据条目数,默认值20,取值范围1~50。"}, {"in": "query", "name": "sort_type", "schema": {"type": "string"}, "required": false, "description": "可选值有:ByCreateTimeAsc(按消息创建时间升序排列);ByCreateTimeDesc(按消息创建时间降序排列)"}, {"in": "query", "name": "page_token", "schema": {"type": "string"}, "required": false, "description": "分页标记,第一次请求不填,表示从头开始遍历;分页查询结果还有更多项时会同时返回新的 page_token,下次遍历可采用该 page_token 获取查询结果"}],
"api_params": [
{
"in": "query",
"name": "container_id",
"schema": {
"type": "string"
},
"required": true,
"description": "单聊或群聊的id,或话题 id"
},
{
"in": "query",
"name": "container_id_type",
"schema": {
"type": "string"
},
"required": true,
"description": "容器类型。 可选值有: chat:包含单聊(p2p)和群聊(group); thread:话题 。"
},
{
"in": "query",
"name": "start_time",
"schema": {
"type": "string"
},
"required": false,
"description": "待查询历史信息的起始时间,秒级时间戳。 注意:thread 容器类型暂不支持获取指定时间范围内的消息。"
},
{
"in": "query",
"name": "end_time",
"schema": {
"type": "string"
},
"required": false,
"description": "待查询历史信息的结束时间,秒级时间戳。注意:thread 容器类型暂不支持获取指定时间范围内的消息。"
},
{
"in": "query",
"name": "page_size",
"schema": {
"type": "string"
},
"required": false,
"description": "分页大小,单次请求所返回的数据条目数,默认值20,取值范围1~50。"
},
{
"in": "query",
"name": "sort_type",
"schema": {
"type": "string"
},
"required": false,
"description": "可选值有:ByCreateTimeAsc(按消息创建时间升序排列);ByCreateTimeDesc(按消息创建时间降序排列)"
},
{
"in": "query",
"name": "page_token",
"schema": {
"type": "string"
},
"required": false,
"description": "分页标记,第一次请求不填,表示从头开始遍历;分页查询结果还有更多项时会同时返回新的 page_token,下次遍历可采用该 page_token 获取查询结果"
}
],
"tool_key": "feishu_get_chat_messages",
"type": 13,
"is_preset": 1,
@@ -518,5 +1092,27 @@
"create_time": "2024-05-08 14:36:50",
"update_time": "2025-02-24 14:35:04",
"id": 37
},
{
"name": "联网搜索",
"logo": null,
"desc": "使用 query 进行联网检索并返回结果。",
"api_params": [
{
"name": "query",
"description": "Search query",
"in": "query",
"required": true,
"schema": {
"type": "string"
}
}
],
"tool_key": "web_search",
"type": 16,
"is_preset": 1,
"is_delete": 0,
"user_id": 1,
"id": 38
}
]
@@ -230,5 +230,19 @@
"create_time": "2024-04-30 10:26:40",
"update_time": "2025-01-10 21:22:37",
"openapi_schema": null
},
{
"id": 16,
"name": "联网搜索",
"logo": "",
"description": "搜索互联网信息,可配置使用不同的搜索引擎,目前支持 Bing、博查、Jina 深度搜索、SerpApi、Tavily。",
"server_host": "",
"auth_method": 0,
"api_key": "",
"auth_type": "basic",
"is_preset": 1,
"user_id": null,
"is_delete": 0,
"openapi_schema": null
}
]
]
+7 -6
View File
@@ -2,21 +2,22 @@ from typing import Dict, List
import yaml
from bisheng.database.models.config import Config
from bisheng.database.base import session_getter
from bisheng.database.base import session_getter, async_session_getter
from bisheng.settings import parse_key, read_from_conf
from bisheng.utils.logger import logger
from sqlmodel import select
def init_config():
async def init_config():
# 初始化config
# 首先通过yaml 获取配置文件所有的key
config_content = read_from_conf('initdb_config.yaml')
if not config_content:
return
with session_getter() as session:
config = session.exec(select(Config)).all()
async with async_session_getter() as session:
config = await session.exec(select(Config))
config = config.all()
db_keys = {conf.key: conf.value for conf in config}
all_config_key = 'initdb_config'
# 数据库内没有默认配置,将默认配置写入到数据库
@@ -27,10 +28,10 @@ def init_config():
try:
db_config = Config(key=all_config_key, value=new_config_content)
session.add(db_config)
session.commit()
await session.commit()
except Exception as e:
logger.exception(e)
session.rollback()
await session.rollback()
def merge_old_config(new_config: str, old_db_config: List[Config], old_db_keys: Dict[str, str]):
+47 -36
View File
@@ -9,7 +9,7 @@ from loguru import logger
from sqlmodel import select, update, text
from bisheng.database.init_config import init_config
from bisheng.database.base import session_getter, db_service
from bisheng.database.base import db_service, async_session_getter
from bisheng.settings import settings
from bisheng.cache.redis import redis_client
from bisheng.database.constants import AdminRole, DefaultRole
@@ -25,14 +25,15 @@ from bisheng.database.models.group import Group, DefaultGroup
from bisheng.database.models.role_access import RoleAccess, AccessType
def init_default_data():
async def init_default_data():
"""初始化数据库"""
if redis_client.setNx('init_default_data', '1'):
if await redis_client.asetNx('init_default_data', '1'):
try:
db_service.create_db_and_tables()
with session_getter() as session:
db_role = session.exec(select(Role).limit(1)).all()
await db_service.create_db_and_tables()
async with async_session_getter() as session:
db_role = await session.exec(select(Role).limit(1))
db_role = db_role.all()
if not db_role:
# 初始化系统配置, 管理员拥有所有权限
db_role = Role(id=AdminRole, role_name='系统管理员', remark='系统所有权限管理员',
@@ -47,16 +48,18 @@ def init_default_data():
RoleAccess(role_id=DefaultRole, type=AccessType.WEB_MENU.value, third_id='knowledge'),
RoleAccess(role_id=DefaultRole, type=AccessType.WEB_MENU.value, third_id='model'),
])
session.commit()
await session.commit()
# 添加默认用户组
group = session.exec(select(Group).limit(1)).all()
group = await session.exec(select(Group).limit(1))
group = group.all()
if not group:
group = Group(id=DefaultGroup, group_name='默认用户组', create_user=1, update_user=1)
session.add(group)
session.commit()
session.refresh(group)
await session.commit()
await session.refresh(group)
user = session.exec(select(User).limit(1)).all()
user = await session.exec(select(User).limit(1))
user = user.all()
if not user and settings.admin:
md5 = hashlib.md5()
md5.update(settings.admin.get('password').encode('utf-8'))
@@ -66,11 +69,12 @@ def init_default_data():
password=md5.hexdigest(),
)
session.add(user)
session.commit()
session.refresh(user)
UserRoleDao.set_admin_user(user.user_id)
await session.commit()
await session.refresh(user)
await UserRoleDao.set_admin_user(user.user_id)
component_db = session.exec(select(Component).limit(1)).all()
component_db = await session.exec(select(Component).limit(1))
component_db = component_db.all()
if not component_db:
db_components = []
json_items = json.loads(read_from_conf('data/component.json'))
@@ -79,18 +83,20 @@ def init_default_data():
db_component = Component(name=k, user_id=1, user_name='admin', data=v)
db_components.append(db_component)
session.add_all(db_components)
session.commit()
await session.commit()
# 初始化预置技能模板
templates = session.exec(select(Template).limit(1)).all()
templates = await session.exec(select(Template).limit(1))
templates = templates.all()
if not templates:
json_items = json.loads(read_from_conf('data/template.json'))
for item in json_items:
session.add(Template(**item))
session.commit()
await session.commit()
# 初始化预置工具列表
preset_tools = session.exec(select(GptsTools).limit(1)).all()
preset_tools = await session.exec(select(GptsTools).limit(1))
preset_tools = preset_tools.all()
if not preset_tools:
preset_tools = []
json_items = json.loads(read_from_conf('data/t_gpts_tools.json'))
@@ -98,9 +104,10 @@ def init_default_data():
preset_tool = GptsTools(**item)
preset_tools.append(preset_tool)
session.add_all(preset_tools)
session.commit()
await session.commit()
# 初始化预置工具类别
preset_tools_type = session.exec(select(GptsToolsType).limit(1)).all()
preset_tools_type = await session.exec(select(GptsToolsType).limit(1))
preset_tools_type = preset_tools_type.all()
if not preset_tools_type:
preset_tools_type = []
json_items = json.loads(read_from_conf('data/t_gpts_tools_type.json'))
@@ -108,21 +115,22 @@ def init_default_data():
preset_tool_type = GptsToolsType(**item)
preset_tools_type.append(preset_tool_type)
session.add_all(preset_tools_type)
session.commit()
await session.commit()
# 设置预置工具所属的类别, 需要和预置数据一致,所以id是固定的
for i in range(1, 7):
session.exec(update(GptsTools).where(GptsTools.id == i).values(type=i))
await session.exec(update(GptsTools).where(GptsTools.id == i).values(type=i))
# 属于天眼查类别下的工具
tyc_types: List[int] = list(range(7, 18))
session.exec(
await session.exec(
update(GptsTools).where(GptsTools.id.in_(tyc_types)).values(type=7))
# 属于金融类别下的工具
jr_types: List[int] = list(range(18, 28))
session.exec(
await session.exec(
update(GptsTools).where(GptsTools.id.in_(jr_types)).values(type=8))
session.commit()
await session.commit()
# 初始化配置可用于微调的基准模型
preset_models = session.exec(select(SftModel).limit(1)).all()
preset_models = await session.exec(select(SftModel).limit(1))
preset_models = preset_models.all()
if not preset_models:
preset_models = []
json_items = json.loads(read_from_conf('data/sft_model.json'))
@@ -130,25 +138,26 @@ def init_default_data():
preset_model = SftModel(**item)
preset_models.append(preset_model)
session.add_all(preset_models)
session.commit()
await session.commit()
# 初始化补充默认的技能版本表
flow_version = session.exec(select(FlowVersion).limit(1)).all()
flow_version = await session.exec(select(FlowVersion).limit(1))
flow_version = flow_version.all()
if not flow_version:
sql_query = text(
"INSERT INTO `flowversion` (`name`, `flow_id`, `data`, `user_id`, `is_current`, `is_delete`) \
select 'v0', `id` as flow_id, `data`, `user_id`, 1, 0 from `flow`;")
session.execute(sql_query)
session.commit()
await session.execute(sql_query)
await session.commit()
# 修改表单数据表
sql_query = text(
'UPDATE `t_variable_value` a SET a.version_id=(SELECT `id` from `flowversion` '
'WHERE flow_id=a.flow_id and is_current=1)'
)
session.execute(sql_query)
session.commit()
await session.execute(sql_query)
await session.commit()
# 初始化数据库config
init_config()
await init_config()
except Exception as exc:
# if the exception involves tables already existing
# we can ignore it
@@ -156,7 +165,7 @@ def init_default_data():
logger.exception(f'Error creating DB and tables: {exc}')
raise RuntimeError('Error creating DB and tables') from exc
finally:
redis_client.delete('init_default_data')
await redis_client.adelete('init_default_data')
def read_from_conf(file_path: str) -> str:
@@ -170,10 +179,12 @@ def read_from_conf(file_path: str) -> str:
return content
def upload_preset_minio_file():
""" 上传预置文件到minio, 为了和工作流模板配合 """
minio_client = MinioClient()
# 上传 「多助手并行+串行报告生成」 工作流模板需要的docx文件
template_data = read_from_conf('data/0254d1808a5247d2a3ee0d0011819acb.docx')
minio_client.upload_minio_data('workflow/report/0254d1808a5247d2a3ee0d0011819acb.docx', template_data,
len(template_data), 'application/vnd.openxmlformats-officedocument.wordprocessingml.document')
len(template_data),
'application/vnd.openxmlformats-officedocument.wordprocessingml.document')
@@ -0,0 +1,2 @@
from bisheng.database.models.linsight_execute_task import LinsightExecuteTask
from bisheng.database.models.linsight_session_version import LinsightSessionVersion
+20 -2
View File
@@ -5,8 +5,8 @@ from typing import Optional
from sqlalchemy import Column, DateTime, text, Text
from sqlmodel import Field, select
from bisheng.database.base import session_getter, async_session_getter
from bisheng.database.models.base import SQLModelSerializable
from bisheng.database.base import session_getter
class ConfigKeyEnum(Enum):
@@ -18,6 +18,7 @@ class ConfigKeyEnum(Enum):
EVALUATION_LLM = 'evaluation_llm' # 评测默认模型配置
WORKFLOW_LLM = 'workflow_llm' # 工作流默认模型配置
WORKSTATION = 'workstation' # 工作台默认模型配置
LINSIGHT_LLM = 'linsight_llm' # 灵思默认模型配置
class ConfigBase(SQLModelSerializable):
@@ -54,7 +55,16 @@ class ConfigDao(ConfigBase):
def get_config(cls, key: ConfigKeyEnum) -> Optional[Config]:
with session_getter() as session:
statement = select(Config).where(Config.key == key.value)
return session.exec(statement).first()
config = session.exec(statement).first()
return config
@classmethod
async def aget_config(cls, key: ConfigKeyEnum) -> Optional[Config]:
async with async_session_getter() as session:
statement = select(Config).where(Config.key == key.value)
config = await session.exec(statement)
config = config.first()
return config
@classmethod
def insert_config(cls, config: Config) -> Config:
@@ -63,3 +73,11 @@ class ConfigDao(ConfigBase):
session.commit()
session.refresh(config)
return config
@classmethod
async def async_insert_config(cls, config: Config) -> Config:
async with async_session_getter() as session:
session.add(config)
await session.commit()
await session.refresh(config)
return config
+1 -1
View File
@@ -19,7 +19,6 @@ from bisheng.utils import generate_uuid
# if TYPE_CHECKING:
class FlowStatus(Enum):
OFFLINE = 1
ONLINE = 2
@@ -30,6 +29,7 @@ class FlowType(Enum):
ASSISTANT = 5
WORKFLOW = 10
WORKSTATION = 15
LINSIGHT = 20 # 灵思模式
class FlowBase(SQLModelSerializable):
@@ -8,6 +8,7 @@ from sqlmodel import Field, or_, select, Text, update
from bisheng.database.base import session_getter
from bisheng.database.constants import ToolPresetType
from bisheng.database.models.base import SQLModelSerializable
from bisheng.utils import md5_hash, generate_uuid
class AuthMethod(Enum):
@@ -43,7 +44,7 @@ class GptsToolsBase(SQLModelSerializable):
class GptsToolsTypeBase(SQLModelSerializable):
id: Optional[int] = Field(default=None, index=True, primary_key=True)
name: str = Field(default='', sa_column=Column(String(length=1024), index=True), description="工具类别名字")
name: str = Field(default='', sa_column=Column(String(length=1024)), description="工具类别名字")
logo: Optional[str] = Field(default='', description="工具类别的logo文件地址")
extra: Optional[str] = Field(default='{}', sa_column=Column(Text),
description="工具类别的配置信息,用来存储工具类别所需的配置信息")
@@ -143,8 +144,8 @@ class GptsToolsDao(GptsToolsBase):
@classmethod
def get_list_by_ids(cls, tool_ids: List[int]) -> List[GptsTools]:
statement = select(GptsTools).where(GptsTools.id.in_(tool_ids)).where(GptsTools.is_delete == 0)
with session_getter() as session:
statement = select(GptsTools).where(GptsTools.id.in_(tool_ids))
return session.exec(statement).all()
@classmethod
@@ -292,7 +293,7 @@ class GptsToolsDao(GptsToolsBase):
# 插入工具列表
for one in children:
one.type = gpts_tool_type.id
one.tool_key = cls.get_tool_key(gpts_tool_type.id, one.tool_key)
one.tool_key = cls.get_tool_key(gpts_tool_type.id, one)
session.add_all(children)
session.commit()
res = GptsToolsTypeRead(**gpts_tool_type.model_dump(), children=children)
@@ -321,7 +322,7 @@ class GptsToolsDao(GptsToolsBase):
# 新增工具列表
for one in add_tool_list:
one.type = data.id
one.tool_key = cls.get_tool_key(data.id, one.tool_key)
one.tool_key = cls.get_tool_key(data.id, one)
session.add(one)
finally_children.append(one)
# 更新工具列表
@@ -352,11 +353,13 @@ class GptsToolsDao(GptsToolsBase):
session.commit()
@classmethod
def get_tool_key(cls, tool_type_id: int, tool_key: str) -> str:
def get_tool_key(cls, tool_type_id: int, gpt_tool: GptsTools) -> str:
"""
拼接自定义工具的tool_key
"""
return f"tool_type_{tool_type_id}_{tool_key}"
if gpt_tool.is_preset == ToolPresetType.MCP.value:
return f"{gpt_tool.name}_{generate_uuid()[:8]}"
return f"tool_type_{tool_type_id}_{md5_hash(gpt_tool.name)}"
@classmethod
def update_tools_extra(cls, tool_type_id: int, extra: str) -> bool:
@@ -0,0 +1,105 @@
from datetime import datetime
from typing import Optional, List
from sqlmodel import Field, Column, text, DateTime, select, update
from bisheng.database.base import async_session_getter
from bisheng.database.models.base import SQLModelSerializable
class InviteCodeBase(SQLModelSerializable):
"""
邀请码模型用于存储邀请码信息
"""
code: str = Field(..., index=True, unique=True, description='邀请码')
batch_id: str = Field(..., index=True, description='批次ID')
batch_name: str = Field(..., description='批次名称')
limit: int = Field(..., description='使用限制次数')
used: Optional[int] = Field(default=0, description='已使用次数')
bind_user: Optional[int] = Field(default=0, index=True, description='绑定的用户ID')
created_id: Optional[int] = Field(default=None, index=True, description='创建者ID')
create_time: Optional[datetime] = Field(default=None, sa_column=Column(
DateTime, nullable=False, index=True, server_default=text('CURRENT_TIMESTAMP')))
update_time: Optional[datetime] = Field(default=None, sa_column=Column(
DateTime, nullable=False, server_default=text('CURRENT_TIMESTAMP'), onupdate=text('CURRENT_TIMESTAMP')))
class InviteCode(InviteCodeBase, table=True):
id: Optional[int] = Field(default=None, index=True, primary_key=True, description='唯一ID')
class InviteCodeDao(InviteCodeBase):
"""
邀请码数据访问对象用于操作邀请码数据
"""
@classmethod
async def insert_invite_code(cls, invite_code: List[InviteCode]) -> List[InviteCode]:
async with async_session_getter() as session:
session.add_all(invite_code)
await session.commit()
return invite_code
@classmethod
async def get_user_bind_code(cls, bind_user: int) -> list[InviteCode]:
"""
获取用户绑定的有效的邀请码
"""
statement = select(InviteCode).where(InviteCode.bind_user == bind_user).where(
InviteCode.used < InviteCode.limit).order_by(InviteCode.id.asc())
async with async_session_getter() as session:
result = await session.exec(statement)
return result.all()
@classmethod
async def get_user_all_code(cls, bind_user: int) -> list[InviteCode]:
"""
获取用户绑定的所有邀请码
"""
statement = select(InviteCode).where(InviteCode.bind_user == bind_user).order_by(InviteCode.id.desc())
async with async_session_getter() as session:
result = await session.exec(statement)
return result.all()
@classmethod
async def bind_invite_code(cls, user_id: int, code: str) -> bool:
"""
绑定邀请码
"""
statement = update(InviteCode).where(InviteCode.code == code).where(InviteCode.bind_user == 0).values(
bind_user=user_id
)
async with async_session_getter() as session:
result = await session.exec(statement)
await session.commit()
if result.rowcount > 0:
return True
return False
@classmethod
async def use_invite_code(cls, user_id: int, code: str) -> bool:
statement = update(InviteCode).where(InviteCode.code == code).where(InviteCode.bind_user == user_id).values(
used=InviteCode.used + 1
).where(InviteCode.used < InviteCode.limit)
async with async_session_getter() as session:
result = await session.exec(statement)
await session.commit()
if result.rowcount > 0:
return True
return False
@classmethod
async def revoke_invite_code_used(cls, user_id: int, code: str) -> bool:
"""
撤销邀请码
"""
statement = update(InviteCode).where(InviteCode.code == code).where(InviteCode.bind_user == user_id).values(
used=InviteCode.used - 1
).where(InviteCode.used > 0)
async with async_session_getter() as session:
result = await session.exec(statement)
await session.commit()
if result.rowcount > 0:
return True
return False
@@ -6,7 +6,7 @@ from pydantic import BaseModel, field_validator
from sqlmodel import Column, DateTime, Field, delete, func, or_, select, text, update
from sqlmodel.sql.expression import Select, SelectOfScalar
from bisheng.database.base import session_getter
from bisheng.database.base import session_getter, async_session_getter
from bisheng.database.models.base import SQLModelSerializable
from bisheng.database.models.knowledge_file import KnowledgeFile, KnowledgeFileDao
from bisheng.database.models.role_access import AccessType, RoleAccessDao
@@ -17,7 +17,7 @@ from bisheng.database.models.user_role import UserRoleDao
class KnowledgeTypeEnum(Enum):
QA = 1
NORMAL = 0
PRIVATE = 2
PRIVATE = 2 # 工作台的个人知识库
class KnowledgeState(Enum):
@@ -158,6 +158,25 @@ class KnowledgeDao(KnowledgeBase):
with session_getter() as session:
return session.exec(statement).all()
@classmethod
async def aget_user_knowledge(cls,
user_id: int,
knowledge_id_extra: List[int] = None,
knowledge_type: KnowledgeTypeEnum = None,
name: str = None,
page: int = 0,
limit: int = 10,
filter_knowledge: List[int] = None) -> List[Knowledge]:
statement = select(Knowledge).where(Knowledge.state > 0)
statement = cls._user_knowledge_filters(statement, user_id, knowledge_id_extra,
knowledge_type, name, page, limit,
filter_knowledge)
statement = statement.order_by(Knowledge.update_time.desc())
async with async_session_getter() as session:
return (await session.exec(statement)).all()
@classmethod
def count_user_knowledge(cls,
user_id: int,
@@ -170,6 +189,18 @@ class KnowledgeDao(KnowledgeBase):
with session_getter() as session:
return session.scalar(statement)
@classmethod
async def acount_user_knowledge(cls,
user_id: int,
knowledge_id_extra: List[int] = None,
knowledge_type: KnowledgeTypeEnum = None,
name: str = None) -> int:
statement = select(func.count(Knowledge.id)).where(Knowledge.state > 0)
statement = cls._user_knowledge_filters(statement, user_id, knowledge_id_extra,
knowledge_type, name)
async with async_session_getter() as session:
return await session.scalar(statement)
@classmethod
def count_by_filter(cls, filters: List[Any]) -> int:
with session_getter() as session:
@@ -283,6 +314,23 @@ class KnowledgeDao(KnowledgeBase):
with session_getter() as session:
return session.exec(statement).all()
@classmethod
async def aget_all_knowledge(cls,
name: str = None,
knowledge_type: KnowledgeTypeEnum = None,
page: int = 0,
limit: int = 0) -> List[Knowledge]:
statement = select(Knowledge).where(Knowledge.state > 0)
statement = cls.generate_all_knowledge_filter(statement,
name=name,
knowledge_type=knowledge_type)
if page and limit:
statement = statement.offset((page - 1) * limit).limit(limit)
statement = statement.order_by(Knowledge.update_time.desc())
async with async_session_getter() as session:
return (await session.exec(statement)).all()
@classmethod
def count_all_knowledge(cls,
name: str = None,
@@ -294,6 +342,17 @@ class KnowledgeDao(KnowledgeBase):
with session_getter() as session:
return session.scalar(statement)
@classmethod
async def acount_all_knowledge(cls,
name: str = None,
knowledge_type: KnowledgeTypeEnum = None) -> int:
statement = select(func.count(Knowledge.id)).where(Knowledge.state > 0)
statement = cls.generate_all_knowledge_filter(statement,
name=name,
knowledge_type=knowledge_type)
async with async_session_getter() as session:
return await session.scalar(statement)
@classmethod
def update_knowledge_list(cls, knowledge_list: List[Knowledge]):
with session_getter() as session:
@@ -0,0 +1,156 @@
import uuid
from datetime import datetime
from enum import Enum
from typing import Optional, Dict, List
from uuid import UUID
from sqlalchemy import Enum as SQLEnum, Column, JSON, Text, DateTime, text, CHAR, ForeignKey
from sqlmodel import Field, select, col
from bisheng.database.base import async_session_getter, uuid_hex
from bisheng.database.models.base import SQLModelSerializable
class ExecuteTaskTypeEnum(str, Enum):
"""
灵思执行任务类型枚举
"""
# 单体任务
SINGLE = "single"
# 拥有子任务
COMPOSITE = "composite"
class ExecuteTaskStatusEnum(str, Enum):
"""
灵思执行任务状态枚举
"""
# 未开始
NOT_STARTED = "not_started"
# 进行中
IN_PROGRESS = "in_progress"
# 成功
SUCCESS = "success"
# 等待用户输入
WAITING_FOR_USER_INPUT = "waiting_for_user_input"
# 用户输入完成
USER_INPUT_COMPLETED = "user_input_completed"
# 失败
FAILED = "failed"
# 终止
TERMINATED = "terminated"
class LinsightExecuteTaskBase(SQLModelSerializable):
"""
灵思执行任务模型基类
"""
session_version_id: str = Field(..., description='会话版本ID',
sa_column=Column(CHAR(36), ForeignKey("linsight_session_version.id"),
nullable=False))
parent_task_id: Optional[str] = Field(None, description='父任务ID',
sa_column=Column(CHAR(36), ForeignKey("linsight_execute_task.id"),
nullable=True))
previous_task_id: Optional[str] = Field(None, description='上一个任务ID',
sa_column=Column(CHAR(36),
nullable=True))
next_task_id: Optional[str] = Field(None, description='下一个任务ID',
sa_column=Column(CHAR(36),
nullable=True))
task_type: ExecuteTaskTypeEnum = Field(..., description='任务类型',
sa_column=Column(SQLEnum(ExecuteTaskTypeEnum), nullable=False))
task_data: Optional[dict] = Field(None, description='任务数据', sa_type=JSON, nullable=True)
input_prompt: Optional[str] = Field(None, description='输入提示', sa_type=Text, nullable=True)
user_input: Optional[str] = Field(None, description='用户输入', sa_type=Text, nullable=True)
history: Optional[List[Dict]] = Field(None, description='执行步骤记录', sa_type=JSON, nullable=True)
status: ExecuteTaskStatusEnum = Field(ExecuteTaskStatusEnum.NOT_STARTED, description="任务状态",
sa_column=Column(SQLEnum(ExecuteTaskStatusEnum), nullable=False))
result: Optional[Dict] = Field(None, description='任务结果', sa_type=JSON, nullable=True)
class LinsightExecuteTask(LinsightExecuteTaskBase, table=True):
"""
灵思执行任务模型
"""
id: str = Field(default_factory=uuid_hex, description='任务ID',
sa_column=Column(CHAR(36), unique=True, nullable=False, primary_key=True))
create_time: datetime = Field(default_factory=datetime.now, description='创建时间',
sa_column=Column(DateTime, nullable=False, server_default=text('CURRENT_TIMESTAMP')))
update_time: Optional[datetime] = Field(default=None, sa_column=Column(
DateTime, nullable=True, server_default=text('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP')))
__tablename__ = "linsight_execute_task"
class LinsightExecuteTaskDao(object):
"""
灵思执行任务数据访问对象
"""
@classmethod
async def get_by_id(cls, task_id: str) -> Optional[LinsightExecuteTask]:
"""
根据任务ID获取任务
:param task_id: 任务ID
:return: 任务对象
"""
async with async_session_getter() as session:
statement = select(LinsightExecuteTask).where(LinsightExecuteTask.id == str(task_id))
task = await session.exec(statement)
return task.first()
@classmethod
async def get_by_session_version_id(cls, session_version_id: str, is_parent_task: bool = False) -> List[
LinsightExecuteTask]:
"""
根据会话版本ID获取所有任务
:param is_parent_task:
:param session_version_id: 会话版本ID
:return: 任务列表
"""
async with async_session_getter() as session:
statement = select(LinsightExecuteTask).where(
LinsightExecuteTask.session_version_id == str(session_version_id))
if is_parent_task:
statement = statement.where(col(LinsightExecuteTask.parent_task_id).is_(None))
tasks = await session.exec(statement)
return tasks.all()
@classmethod
async def batch_create_tasks(cls, tasks: List[LinsightExecuteTask]) -> List[LinsightExecuteTask]:
"""
批量创建任务
:param tasks: 任务列表
:return: 创建后的任务列表
"""
async with async_session_getter() as session:
session.add_all(tasks)
await session.commit()
return tasks
@classmethod
async def update_by_id(cls, task_id: str, **kwargs) -> Optional[LinsightExecuteTask]:
"""
根据任务ID更新任务
:param task_id: 任务ID
:param kwargs: 更新字段
:return: 更新后的任务对象
"""
async with async_session_getter() as session:
statement = select(LinsightExecuteTask).where(LinsightExecuteTask.id == task_id)
task = await session.exec(statement)
task = task.first()
if not task:
return None
for key, value in kwargs.items():
setattr(task, key, value)
session.add(task)
await session.commit()
await session.refresh(task)
return task
@@ -0,0 +1,160 @@
import logging
from datetime import datetime
from enum import Enum
from typing import List, Dict, Optional
from sqlalchemy import Column, Text, JSON, Boolean, Enum as SQLEnum, DateTime, text, ForeignKey, CHAR, func
from sqlmodel import Field, select, col, update
from bisheng.database.base import async_session_getter, uuid_hex
from bisheng.database.models.base import SQLModelSerializable
logger = logging.getLogger(__name__)
class SessionVersionStatusEnum(str, Enum):
"""
灵思会话版本状态枚举
"""
# 未执行
NOT_STARTED = "not_started"
# 进行中
IN_PROGRESS = "in_progress"
# 运行完成
COMPLETED = "completed"
# 运行失败
FAILED = "failed"
# SOP 生成失败
SOP_GENERATION_FAILED = "sop_generation_failed"
# 终止
TERMINATED = "terminated"
class LinsightSessionVersionBase(SQLModelSerializable):
"""
灵思会话版本模型基类
"""
session_id: str = Field(..., description='会话ID', sa_column=Column(CHAR(36),
ForeignKey("message_session.chat_id"),
nullable=False,
index=True))
user_id: int = Field(..., description='用户ID', foreign_key="user.user_id", nullable=False)
question: str = Field(..., description='用户问题', sa_type=Text, nullable=False)
title: Optional[str] = Field(None, description='会话标题', sa_type=Text, nullable=True)
tools: Optional[List[Dict]] = Field(None, description='可用的工具列表', sa_type=JSON, nullable=True)
# 个人知识库
personal_knowledge_enabled: bool = Field(False, description='是否启用个人知识库', sa_type=Boolean)
# 组织知识库
org_knowledge_enabled: bool = Field(False, description='是否启用组织知识库', sa_type=Boolean)
files: Optional[List[Dict]] = Field(None, description='上传的文件列表', sa_type=JSON, nullable=True)
sop: Optional[str] = Field(None, description='SOP内容', sa_type=Text, nullable=True)
output_result: Optional[Dict] = Field(None, description='输出结果', sa_type=JSON, nullable=True)
status: SessionVersionStatusEnum = Field(default=SessionVersionStatusEnum.NOT_STARTED, description='会话版本状态',
sa_column=Column(SQLEnum(SessionVersionStatusEnum), nullable=False))
score: Optional[int] = Field(None, description='会话评分', ge=1, le=5, nullable=True)
# 执行结果反馈信息
execute_feedback: Optional[str] = Field(None, description='执行结果反馈信息', sa_type=Text, nullable=True)
# 是否有重新执行
has_reexecute: bool = Field(default=False, description='是否有重新执行', sa_type=Boolean, nullable=False)
# 版本
version: datetime = Field(default_factory=datetime.now, description='会话版本创建时间', sa_type=DateTime)
class LinsightSessionVersion(LinsightSessionVersionBase, table=True):
"""
灵思会话版本模型
"""
id: str = Field(default_factory=uuid_hex, description='会话版本ID',
sa_column=Column(CHAR(36), unique=True, nullable=False, primary_key=True))
create_time: datetime = Field(default_factory=datetime.now, description='创建时间',
sa_column=Column(DateTime, nullable=False, server_default=text('CURRENT_TIMESTAMP')))
update_time: Optional[datetime] = Field(default=None, sa_column=Column(
DateTime, nullable=True, server_default=text('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP')))
__tablename__ = "linsight_session_version"
class LinsightSessionVersionDao(object):
"""
灵思会话版本数据访问对象
"""
@staticmethod
async def insert_one(session_version: LinsightSessionVersion) -> LinsightSessionVersion:
"""
插入一条灵思会话版本记录
:param session_version: 灵思会话版本对象
:return: 创建的灵思会话版本对象
"""
async with async_session_getter() as session:
session.add(session_version)
await session.commit()
await session.refresh(session_version)
return session_version
@staticmethod
async def get_by_id(linsight_session_version_id: str) -> Optional[LinsightSessionVersion]:
"""
根据灵思会话版本ID获取灵思会话版本
:param linsight_session_version_id: 灵思会话版本ID
:return: 灵思会话版本对象
"""
async with async_session_getter() as session:
statement = select(LinsightSessionVersion).where(
LinsightSessionVersion.id == str(linsight_session_version_id)) # 显式转 str
result = await session.exec(statement)
return result.first()
@staticmethod
async def get_session_versions_by_session_id(session_id: str) -> List[LinsightSessionVersion]:
"""
根据会话ID获取所有灵思会话版本
:param session_id: 会话ID
:return: 灵思会话版本列表
"""
async with async_session_getter() as session:
statement = select(LinsightSessionVersion).where(
LinsightSessionVersion.session_id == str(session_id)).order_by(
col(LinsightSessionVersion.version).desc())
return (await session.exec(statement)).all()
@staticmethod
async def modify_sop_content(linsight_session_version_id: str, sop_content: str):
"""
修改灵思会话版本的SOP内容
:param linsight_session_version_id:
:param sop_content:
:return:
"""
async with async_session_getter() as session:
stmt = (
update(LinsightSessionVersion)
.where(col(LinsightSessionVersion.id) == str(linsight_session_version_id)) # 显式转 str
.values(sop=sop_content)
)
result = await session.exec(stmt)
if result.rowcount == 0:
logger.warning(f"No session version found with ID: {linsight_session_version_id}")
await session.commit()
@staticmethod
async def get_session_version_by_file_id(file_id: str) -> Optional[LinsightSessionVersion]:
"""
根据文件ID获取灵思会话版本
:param file_id: 文件ID
:return: 灵思会话版本对象
"""
async with async_session_getter() as session:
statement = select(LinsightSessionVersion).where(
func.json_search(LinsightSessionVersion.files, 'all', file_id)
)
result = await session.exec(statement)
return result.first()
@@ -0,0 +1,281 @@
from datetime import datetime
from typing import Optional, Dict, Any, List, Literal
from loguru import logger
from sqlalchemy import update
from sqlalchemy.dialects.mysql import LONGTEXT
from sqlmodel import Field, select, delete, col, or_, func, Column, Text, DateTime, text, CHAR, ForeignKey
from bisheng.api.v1.schema.inspiration_schema import SOPManagementUpdateSchema
from bisheng.database.base import async_session_getter, async_get_count
from bisheng.database.models.base import SQLModelSerializable
class LinsightSOPBase(SQLModelSerializable):
"""
Inspiration SOP模型基类
"""
name: str = Field(..., description='SOP名称', sa_column=Column(Text, nullable=False))
description: Optional[str] = Field(default=None, description='SOP描述', sa_column=Column(Text))
user_id: int = Field(..., description='用户ID', foreign_key="user.user_id", nullable=False)
content: str = Field(..., description='SOP内容',
sa_column=Column(LONGTEXT, nullable=False, comment="SOP内容"))
rating: Optional[int] = Field(default=0, ge=0, le=5, description='SOP评分,范围0-5')
vector_store_id: Optional[str] = Field(..., description='向量存储ID',
sa_column=Column(CHAR(36), nullable=False, comment="向量存储ID"))
linsight_session_id: Optional[str] = Field(default=None, description='灵思会话ID',
sa_column=Column(CHAR(36),
ForeignKey("message_session.chat_id"),
nullable=True))
create_time: datetime = Field(default_factory=datetime.now, description='创建时间',
sa_column=Column(DateTime, nullable=False, server_default=text('CURRENT_TIMESTAMP')))
update_time: Optional[datetime] = Field(default=None, sa_column=Column(
DateTime, nullable=True, server_default=text('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP')))
class LinsightSOP(LinsightSOPBase, table=True):
"""
Inspiration SOP模型
"""
__tablename__ = "linsight_sop"
id: Optional[int] = Field(default=None, primary_key=True, description='SOP唯一ID')
class LinsightSOPRecord(SQLModelSerializable, table=True):
"""
灵思SOP运行记录表记录灵思执行过程中产生的sop
"""
__tablename__ = "linsight_sop_record"
id: Optional[int] = Field(default=None, primary_key=True, description='SOP记录唯一ID')
name: str = Field(..., description='SOP名称', sa_column=Column(Text, nullable=False))
description: Optional[str] = Field(default=None, description='SOP描述', sa_column=Column(Text))
user_id: int = Field(..., description='用户ID', foreign_key="user.user_id", nullable=False)
content: str = Field(..., description='SOP内容',
sa_column=Column(LONGTEXT, nullable=False, comment="SOP内容"))
rating: Optional[int] = Field(default=0, ge=0, le=5, description='SOP评分,范围0-5')
linsight_version_id: Optional[str] = Field(default=None, description='灵思会话版本id,同步评分')
create_time: datetime = Field(default_factory=datetime.now, description='创建时间',
sa_column=Column(DateTime, nullable=False, server_default=text('CURRENT_TIMESTAMP')))
update_time: Optional[datetime] = Field(default=None, sa_column=Column(
DateTime, nullable=True, server_default=text('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP')))
class LinsightSOPDao(LinsightSOPBase):
"""
Inspiration SOP数据访问对象
"""
@classmethod
async def create_sop(cls, sop: LinsightSOP) -> LinsightSOP:
async with async_session_getter() as session:
session.add(sop)
await session.commit()
await session.refresh(sop)
return sop
@classmethod
async def update_sop(cls, sop_obj: SOPManagementUpdateSchema) -> LinsightSOP:
async with async_session_getter() as session:
# 使用Update语句更新SOP
statement = select(LinsightSOP).where(LinsightSOP.id == sop_obj.id)
result = await session.exec(statement)
sop = result.first()
if not sop:
raise ValueError("SOP not found")
# 将sop_obj的字段值更新到sop实例中
for key, value in sop_obj.model_dump().items():
if hasattr(sop, key):
setattr(sop, key, value)
sop.update_time = datetime.now() # 更新修改时间
session.add(sop)
await session.commit()
await session.refresh(sop)
return sop
@classmethod
async def get_sop_page(cls, keywords: Optional[str] = None, sort: Literal["asc", "desc"] = "desc", page: int = 1,
page_size: int = 10) -> Dict[str, Any]:
"""
获取SOP分页列表
"""
statement = select(LinsightSOP)
if keywords:
statement = statement.where(
LinsightSOP.name.ilike(f'%{keywords}%') |
LinsightSOP.description.ilike(f'%{keywords}%') |
LinsightSOP.content.ilike(f'%{keywords}%')
)
# 根据 rating 和 create_time 排序
if sort == "asc":
statement = statement.order_by(col(LinsightSOP.rating).asc(), col(LinsightSOP.create_time).asc())
else:
statement = statement.order_by(col(LinsightSOP.rating).desc(), col(LinsightSOP.create_time).desc())
async with async_session_getter() as session:
total_count = await async_get_count(session, statement)
statement = statement.offset((page - 1) * page_size).limit(page_size)
result = (await session.exec(statement)).all()
return {
"total": total_count,
"current_page": page,
"page_size": page_size,
"items": [result.model_dump() for result in result]
}
@classmethod
async def get_sops_by_ids(cls, sop_ids: List[int]) -> List[LinsightSOP]:
"""
根据SOP ID列表获取SOP对象
"""
async with async_session_getter() as session:
statement = select(LinsightSOP).where(col(LinsightSOP.id).in_(sop_ids))
result = await session.exec(statement)
sop_list = result.all()
return sop_list
@classmethod
async def get_sops_by_names(cls, names: list[str]) -> List[LinsightSOP]:
"""
根据SOP名称列表获取SOP对象
"""
statement = select(LinsightSOP).where(col(LinsightSOP.name).in_(names))
async with async_session_getter() as session:
result = await session.exec(statement)
sop_list = result.all()
return sop_list
@classmethod
async def remove_sop(cls, sop_ids: List[int]) -> bool:
"""
删除SOP
"""
async with async_session_getter() as session:
delete_statement = delete(LinsightSOP).where(col(LinsightSOP.id).in_(sop_ids))
result = await session.exec(delete_statement)
await session.commit()
logger.info(f"Deleted {result.rowcount} SOP(s) with IDs: {sop_ids}")
return True
@classmethod
async def get_sop_by_session_id(cls, session_id: str) -> Optional[LinsightSOP]:
"""
根据灵思会话ID获取SOP
"""
async with async_session_getter() as session:
statement = select(LinsightSOP).where(LinsightSOP.linsight_session_id == session_id)
result = await session.exec(statement)
sop = result.first()
return sop if sop else None
@classmethod
async def get_sop_by_vector_store_ids(cls, vector_store_ids: List[str]) -> List[LinsightSOP]:
"""
根据向量存储ID列表获取SOP对象
"""
async with async_session_getter() as session:
statement = select(LinsightSOP).where(col(LinsightSOP.vector_store_id).in_(vector_store_ids))
result = await session.exec(statement)
sop_list = result.all()
return sop_list
@classmethod
async def get_all_sops(cls) -> List[LinsightSOP]:
"""
获取所有SOP
"""
async with async_session_getter() as session:
statement = select(LinsightSOP)
result = await session.exec(statement)
sop_list = result.all()
return sop_list
@classmethod
async def create_sop_record(cls, sop_record: LinsightSOPRecord) -> LinsightSOPRecord:
"""
插入一条SOP记录
"""
async with async_session_getter() as session:
session.add(sop_record)
await session.commit()
await session.refresh(sop_record)
return sop_record
@classmethod
async def _filter_sop_record_statement(cls, statement, keywords: str = None, user_ids: list[int] = None) -> select:
"""
构建SOP记录的查询语句
"""
or_params = []
if keywords:
or_params.extend([
LinsightSOPRecord.name.like(f'%{keywords}%'),
LinsightSOPRecord.description.like(f'%{keywords}%'),
LinsightSOPRecord.content.like(f'%{keywords}%')
])
if user_ids:
or_params.append(LinsightSOPRecord.user_id.in_(user_ids))
if or_params:
statement = statement.where(or_(*or_params))
return statement
@classmethod
async def filter_sop_record(cls, keywords: str = None, user_ids: list[int] = None, page: int = None,
page_size: int = None, sort: str = None) -> List[LinsightSOPRecord]:
"""
获取所有SOP记录, 关键字匹配namedescriptioncontentuser_ids为用户ID列表筛选条件之间是or的关系
"""
statement = select(LinsightSOPRecord)
statement = await cls._filter_sop_record_statement(statement, keywords, user_ids)
if page and page_size:
statement = statement.offset((page - 1) * page_size).limit(page_size)
if sort == "asc":
statement = statement.order_by(col(LinsightSOPRecord.create_time).asc())
else:
statement = statement.order_by(col(LinsightSOPRecord.create_time).desc())
async with async_session_getter() as session:
result = await session.exec(statement)
return result.all()
@classmethod
async def count_sop_record(cls, keywords: str = None, user_ids: list[int] = None) -> int:
"""
统计SOP记录数量
"""
statement = select(func.count(LinsightSOPRecord.id))
statement = await cls._filter_sop_record_statement(statement, keywords, user_ids)
async with async_session_getter() as session:
return await session.scalar(statement)
@classmethod
async def get_sop_record_by_ids(cls, ids: list[int]) -> List[LinsightSOPRecord]:
"""
根据SOP记录ID列表获取SOP记录对象
"""
statement = select(LinsightSOPRecord).where(col(LinsightSOPRecord.id).in_(ids))
async with async_session_getter() as session:
result = await session.exec(statement)
sop_record_list = result.all()
return sop_record_list
@classmethod
async def update_sop_record_score(cls, linsight_version_id: str, rating: int) -> bool:
"""
更新SOP记录的评分
"""
statement = update(LinsightSOPRecord).where(
col(LinsightSOPRecord.linsight_version_id) == linsight_version_id).values(rating=rating)
async with async_session_getter() as session:
await session.exec(statement)
await session.commit()
return True
@@ -6,7 +6,7 @@ from pydantic import BaseModel
from sqlalchemy import Column, DateTime, text
from sqlmodel import Field, select
from bisheng.database.base import session_getter
from bisheng.database.base import session_getter, async_session_getter
from bisheng.database.models.base import SQLModelSerializable
@@ -67,6 +67,15 @@ class RoleAccessDao(RoleAccessBase):
RoleAccess.type == access_type.value)).all()
return session.exec(select(RoleAccess).where(RoleAccess.role_id.in_(role_ids))).all()
@classmethod
async def aget_role_access(cls, role_ids: List[int], access_type: AccessType) -> List[RoleAccess]:
async with async_session_getter() as session:
if access_type:
return (await session.exec(
select(RoleAccess).where(RoleAccess.role_id.in_(role_ids),
RoleAccess.type == access_type.value))).all()
return (await session.exec(select(RoleAccess).where(RoleAccess.role_id.in_(role_ids)))).all()
@classmethod
def get_role_access_batch(cls, role_ids: List[int], access_type: List[AccessType]) -> List[RoleAccess]:
with session_getter() as session:
@@ -9,7 +9,7 @@ from bisheng.database.models.base import SQLModelSerializable
class ServerBase(SQLModelSerializable):
endpoint: str = Field(index=False, unique=True)
endpoint: str = Field(index=False)
sft_endpoint: str = Field(default='', index=False, description='Finetune服务地址')
server: str = Field(index=True)
remark: Optional[str] = Field(default=None, index=False)
+23 -5
View File
@@ -4,10 +4,11 @@ from typing import Optional, List
from sqlmodel import Field, Column, DateTime, text, select, func, update
from bisheng.database.base import session_getter
from bisheng.database.base import session_getter, async_session_getter
from bisheng.database.models.base import SQLModelSerializable
from bisheng.database.models.flow import FlowType
class SensitiveStatus(Enum):
PASS = 1 # 通过
VIOLATIONS = 2 # 违规
@@ -46,6 +47,14 @@ class MessageSessionDao(MessageSessionBase):
session.refresh(data)
return data
@classmethod
async def async_insert_one(cls, data: MessageSession) -> MessageSession:
async with async_session_getter() as session:
session.add(data)
await session.commit()
await session.refresh(data)
return data
@classmethod
def delete_session(cls, chat_id: str):
statement = update(MessageSession).where(MessageSession.chat_id == chat_id).values(is_delete=True)
@@ -59,6 +68,12 @@ class MessageSessionDao(MessageSessionBase):
with session_getter() as session:
return session.exec(statement).first()
@classmethod
async def async_get_one(cls, chat_id: str) -> MessageSession | None:
statement = select(MessageSession).where(MessageSession.chat_id == chat_id)
async with async_session_getter() as session:
return (await session.exec(statement)).first()
@classmethod
def generate_filter_session_statement(cls,
statement,
@@ -98,10 +113,13 @@ class MessageSessionDao(MessageSessionBase):
statement = statement.where(
MessageSession.sensitive_status.in_([one.value for one in sensitive_status]))
if flow_type:
statement = statement.where(MessageSession.flow_type == flow_type)
statement = statement.where(MessageSession.flow_type.in_(flow_type))
else:
# 过滤掉工作站的会话, 默认不带工作站
statement = statement.where(MessageSession.flow_type != FlowType.WORKSTATION.value)
# 过滤掉工作站的会话, 默认不带工作站 和 灵思
statement = statement.where(
MessageSession.flow_type != FlowType.WORKSTATION.value) # noqa
statement = statement.where(
MessageSession.flow_type != FlowType.LINSIGHT.value) # noqa
# 过滤掉被删除的会话
return statement
@@ -118,7 +136,7 @@ class MessageSessionDao(MessageSessionBase):
exclude_chats: List[str] = None,
page: int = 0,
limit: int = 0,
flow_type: int = None) -> List[MessageSession]:
flow_type: List[int] = None) -> List[MessageSession]:
statement = select(MessageSession)
statement = cls.generate_filter_session_statement(statement,
chat_ids,
+29 -7
View File
@@ -5,7 +5,7 @@ from pydantic import field_validator
from sqlalchemy import Column, DateTime, func, text
from sqlmodel import Field, select
from bisheng.database.base import session_getter
from bisheng.database.base import session_getter, async_session_getter
from bisheng.database.constants import AdminRole, DefaultRole
from bisheng.database.models.base import SQLModelSerializable
from bisheng.database.models.user_group import UserGroup
@@ -99,6 +99,17 @@ class UserDao(UserBase):
session.refresh(user)
return user
@classmethod
def _filter_users_statement(cls,
statement,
user_ids: List[int],
keyword: str = None):
if user_ids:
statement = statement.where(User.user_id.in_(user_ids))
if keyword:
statement = statement.where(User.user_name.like(f'%{keyword}%'))
return statement.order_by(User.user_id.desc())
@classmethod
def filter_users(cls,
user_ids: List[int],
@@ -106,19 +117,30 @@ class UserDao(UserBase):
page: int = 0,
limit: int = 0) -> (List[User], int):
statement = select(User)
statement = cls._filter_users_statement(statement, user_ids, keyword)
count_statement = select(func.count(User.user_id))
if user_ids:
statement = statement.where(User.user_id.in_(user_ids))
count_statement = count_statement.where(User.user_id.in_(user_ids))
if keyword:
statement = statement.where(User.user_name.like(f'%{keyword}%'))
count_statement = count_statement.where(User.user_name.like(f'%{keyword}%'))
count_statement = cls._filter_users_statement(count_statement, user_ids, keyword)
if page and limit:
statement = statement.offset((page - 1) * limit).limit(limit)
statement = statement.order_by(User.user_id.desc())
with session_getter() as session:
return session.exec(statement).all(), session.scalar(count_statement)
@classmethod
async def afilter_users(cls,
user_ids: List[int],
keyword: str = None,
page: int = 0,
limit: int = 0) -> List[User]:
statement = select(User)
statement = cls._filter_users_statement(statement, user_ids, keyword)
if page and limit:
statement = statement.offset((page - 1) * limit).limit(limit)
statement = statement.order_by(User.user_id.desc())
async with async_session_getter() as session:
result = await session.exec(statement)
return result.all()
@classmethod
def get_unique_user_by_name(cls, user_name: str) -> User | None:
with session_getter() as session:
@@ -5,7 +5,7 @@ from pydantic import BaseModel
from sqlalchemy import Column, DateTime, text, delete
from sqlmodel import Field, select
from bisheng.database.base import session_getter
from bisheng.database.base import session_getter, async_session_getter
from bisheng.database.constants import AdminRole
from bisheng.database.models.base import SQLModelSerializable
@@ -40,6 +40,12 @@ class UserRoleDao(UserRoleBase):
with session_getter() as session:
return session.exec(select(UserRole).where(UserRole.user_id == user_id)).all()
@classmethod
async def aget_user_roles(cls, user_id: int) -> List[UserRole]:
async with async_session_getter() as session:
result = await session.exec(select(UserRole).where(UserRole.user_id == user_id))
return result.all()
@classmethod
def get_roles_user(cls, role_ids: List[int], page: int = 0, limit: int = 0) -> List[UserRole]:
"""
@@ -61,15 +67,15 @@ class UserRoleDao(UserRoleBase):
return session.exec(statement).all()
@classmethod
def set_admin_user(cls, user_id: int) -> UserRole:
async def set_admin_user(cls, user_id: int) -> UserRole:
"""
设置用户为超级管理员
"""
with session_getter() as session:
async with async_session_getter() as session:
user_role = UserRole(user_id=user_id, role_id=AdminRole)
session.add(user_role)
session.commit()
session.refresh(user_role)
await session.commit()
await session.refresh(user_role)
return user_role
@classmethod
+67 -34
View File
@@ -1,5 +1,7 @@
from typing import TYPE_CHECKING
from sqlalchemy.ext.asyncio import create_async_engine
from bisheng.services.base import Service
from loguru import logger
from sqlalchemy.exc import OperationalError
@@ -19,15 +21,33 @@ class DatabaseService(Service):
# langflow_dir = Path(__file__).parent.parent.parent
# self.script_location = langflow_dir / "alembic"
# self.alembic_cfg_path = langflow_dir / "alembic.ini"
self.engine = self._create_engine()
def _create_engine(self) -> 'Engine':
"""Create the engine for the database."""
if self.database_url and self.database_url.startswith('sqlite'):
connect_args = {'check_same_thread': False}
else:
connect_args = {}
return create_engine(self.database_url, connect_args=connect_args, pool_size=100, max_overflow=20, pool_timeout=3, pool_pre_ping=True)
self.async_database_url = database_url.replace("pymysql", "aiomysql")
self.engine = self._create_engine()
self.async_engine = self._create_async_engine()
def _create_engine(self):
connect_args = {'check_same_thread': False} if self.database_url.startswith("sqlite") else {}
return create_engine(
self.database_url,
connect_args=connect_args,
pool_size=100,
max_overflow=20,
pool_timeout=3,
pool_pre_ping=True
)
def _create_async_engine(self):
connect_args = {'check_same_thread': False} if self.async_database_url.startswith("sqlite") else {}
return create_async_engine(
self.async_database_url,
connect_args=connect_args,
pool_size=100,
max_overflow=20,
pool_timeout=3,
pool_pre_ping=True
)
def __enter__(self):
self._session = Session(self.engine)
@@ -45,35 +65,48 @@ class DatabaseService(Service):
with Session(self.engine) as session:
yield session
def create_db_and_tables(self):
# from sqlalchemy import inspect
async def create_db_and_tables(self):
logger.debug("Creating database and tables (async)")
# inspector = inspect(self.engine)
# table_names = inspector.get_table_names()
# current_tables = ["flow", "user", "apikey"]
# if table_names and all(table in table_names for table in current_tables):
# logger.debug("Database and tables already exist")
# return
logger.debug('Creating database and tables')
for table in SQLModel.metadata.sorted_tables:
async with self.async_engine.begin() as conn:
try:
table.create(self.engine, checkfirst=True)
await conn.run_sync(SQLModel.metadata.create_all)
logger.debug("Tables created successfully")
except OperationalError as oe:
logger.warning(f'Table {table} already exists, skipping. Exception: {oe}')
logger.warning(f"Table creation skipped due to OperationalError: {oe}")
except Exception as exc:
logger.error(f'Error creating table {table}: {exc}')
raise RuntimeError(f'Error creating table {table}') from exc
# Now check if the required tables exist, if not, something went wrong.
# inspector = inspect(self.engine)
# table_names = inspector.get_table_names()
# for table in current_tables:
# if table not in table_names:
# logger.error("Something went wrong creating the database and tables.")
# logger.error("Please check your database settings.")
# raise RuntimeError("Something went wrong creating the database and tables.")
logger.error(f"Error creating tables: {exc}")
raise RuntimeError("Error creating tables") from exc
logger.debug('Database and tables created successfully')
# def create_db_and_tables(self):
# # from sqlalchemy import inspect
#
# # inspector = inspect(self.engine)
# # table_names = inspector.get_table_names()
# # current_tables = ["flow", "user", "apikey"]
#
# # if table_names and all(table in table_names for table in current_tables):
# # logger.debug("Database and tables already exist")
# # return
#
# logger.debug('Creating database and tables')
#
# for table in SQLModel.metadata.sorted_tables:
# try:
# table.create(self.engine, checkfirst=True)
# except OperationalError as oe:
# logger.warning(f'Table {table} already exists, skipping. Exception: {oe}')
# except Exception as exc:
# logger.error(f'Error creating table {table}: {exc}')
# raise RuntimeError(f'Error creating table {table}') from exc
# Now check if the required tables exist, if not, something went wrong.
# inspector = inspect(self.engine)
# table_names = inspector.get_table_names()
# for table in current_tables:
# if table not in table_names:
# logger.error("Something went wrong creating the database and tables.")
# logger.error("Please check your database settings.")
# raise RuntimeError("Something went wrong creating the database and tables.")
+15
View File
@@ -57,3 +57,18 @@ workflow:
max_steps: 50
# 等待用户输入的超时时间,单位分钟
timeout: 5
# 灵思模块相关配置
linsight:
# 历史记录中工具消息的最大token,超过后需要总结下历史记录
tool_buffer: 100000
# 单个任务最大执行步骤数,防止死循环
max_steps: 200
# 灵思任务执行过程中模型调用重试次数
retry_num: 3
# 灵思任务执行过程中模型调用重试间隔时间(秒)
retry_sleep: 5
# 生成SOP时,prompt里放的用户上传文件信息的数量
max_file_num: 5
# 生成SOP时,prompt里放的组织知识库的最大数量
max_knowledge_num: 20
@@ -1,13 +1,14 @@
from typing import List, Optional, Dict
import numpy as np
from langchain.embeddings.base import Embeddings
from loguru import logger
from pydantic import ConfigDict, Field, BaseModel
from bisheng.database.models.llm_server import (LLMDao, LLMModel, LLMModelType, LLMServer,
LLMServerType)
from bisheng.interface.importing import import_by_type
from bisheng.interface.utils import wrapper_bisheng_model_limit_check
from langchain.embeddings.base import Embeddings
from loguru import logger
from pydantic import ConfigDict, Field, BaseModel
class OpenAIProxyEmbedding(Embeddings):
@@ -144,10 +145,11 @@ class BishengEmbedding(BaseModel, Embeddings):
'model': params.get('model'),
}
elif server_info.type in [
LLMServerType.XINFERENCE.value, LLMServerType.LLAMACPP.value,
LLMServerType.VLLM.value
LLMServerType.XINFERENCE.value, LLMServerType.LLAMACPP.value,
LLMServerType.VLLM.value
]:
params['openai_api_key'] = params.pop('openai_api_key', None) or 'EMPTY'
params['batch_size'] = params.pop('batch_size', 1)
return params
@wrapper_bisheng_model_limit_check
@@ -187,7 +189,7 @@ class BishengEmbedding(BaseModel, Embeddings):
"""更新模型状态"""
# todo 接入到异步任务模块 累计5分钟更新一次
if self.model_info.status != status:
self.model_info.status = status
self.model_info.status = status
LLMDao.update_model_status(self.model_id, status, remark)
+3 -4
View File
@@ -58,6 +58,7 @@ def _get_azure_openai_params(params: dict, server_config: dict, model_config: di
'openai_api_key': server_config.get('openai_api_key'),
'openai_api_version': server_config.get('openai_api_version'),
'azure_deployment': params.pop('model'),
'stream_usage': True,
})
return params
@@ -67,6 +68,7 @@ def _get_qwen_params(params: dict, server_config: dict, model_config: dict) -> d
params['model_kwargs'] = {
'enable_search': model_config.get('enable_web_search', False),
'temperature': params.pop('temperature', 0.3),
'incremental_output': True, # 默认增量输出,tool call拼接流式内容call_id拼接重复的bug
}
if params.get('max_tokens'):
params['model_kwargs']['max_tokens'] = params.get('max_tokens')
@@ -147,7 +149,6 @@ class BishengLLM(BaseChatModel):
model_name: Optional[str] = Field(default='', description="后端服务保存的model名称")
streaming: bool = Field(default=True, description="是否使用流式输出", alias="stream")
temperature: float = Field(default=0.3, description="模型生成的温度")
top_p: float = Field(default=1, description="模型生成的top_p")
cache: bool = Field(default=False, description="是否使用缓存")
llm: Optional[BaseChatModel] = Field(default=None)
@@ -162,8 +163,7 @@ class BishengLLM(BaseChatModel):
self.model_name = kwargs.get('model_name')
self.streaming = kwargs.get('streaming', True)
self.temperature = kwargs.get('temperature', 0.3)
self.top_p = kwargs.get('top_p', 1)
self.cache = kwargs.get('cache', True)
self.cache = kwargs.get('cache', False)
# 是否忽略模型是否上线的检查
ignore_online = kwargs.get('ignore_online', False)
@@ -213,7 +213,6 @@ class BishengLLM(BaseChatModel):
'model': self.model_info.model_name,
'streaming': self.streaming,
'temperature': self.temperature,
'top_p': self.top_p,
'cache': self.cache
}
if model_config.get('max_tokens'):
@@ -1,10 +1,8 @@
from ast import literal_eval
from abc import ABC
from ast import literal_eval
from typing import TYPE_CHECKING, Any, Callable, Dict, Iterable, List, Optional, Tuple
import jieba
from bisheng_langchain.vectorstores.elastic_keywords_search import DEFAULT_PROMPT
from bisheng_langchain.vectorstores.milvus import DEFAULT_MILVUS_CONNECTION
from langchain.chains.llm import LLMChain
from langchain.docstore.document import Document
from langchain.embeddings.base import Embeddings
@@ -15,6 +13,9 @@ from langchain_core.language_models import BaseLLM
from langchain_core.prompts import PromptTemplate
from loguru import logger
from bisheng_langchain.vectorstores.elastic_keywords_search import DEFAULT_PROMPT
from bisheng_langchain.vectorstores.milvus import DEFAULT_MILVUS_CONNECTION
if TYPE_CHECKING:
from elasticsearch import Elasticsearch # noqa: F401
@@ -167,12 +168,12 @@ class MilvusWithPermissionCheck(MilvusLangchain):
connections.remove_connection(using)
def _init(
self,
embeddings: Optional[list] = None,
metadatas: Optional[list[dict]] = None,
partition_names: Optional[list] = None,
replica_number: int = 1,
timeout: Optional[float] = None,
self,
embeddings: Optional[list] = None,
metadatas: Optional[list[dict]] = None,
partition_names: Optional[list] = None,
replica_number: int = 1,
timeout: Optional[float] = None,
) -> None:
self._extract_fields(col_index=0)
self._create_search_params()
@@ -227,18 +228,18 @@ class MilvusWithPermissionCheck(MilvusLangchain):
@classmethod
def from_texts(
cls,
texts: List[str],
embedding: Embeddings,
metadatas: Optional[List[dict]] = None,
collection_name: list[str] = None,
connection_args: dict[str, Any] = DEFAULT_MILVUS_CONNECTION,
consistency_level: str = 'Session',
index_params: Optional[dict] = None,
search_params: Optional[dict] = None,
drop_old: bool = False,
no_embedding: bool = False,
**kwargs: Any,
cls,
texts: List[str],
embedding: Embeddings,
metadatas: Optional[List[dict]] = None,
collection_name: list[str] = None,
connection_args: dict[str, Any] = DEFAULT_MILVUS_CONNECTION,
consistency_level: str = 'Session',
index_params: Optional[dict] = None,
search_params: Optional[dict] = None,
drop_old: bool = False,
no_embedding: bool = False,
**kwargs: Any,
):
"""
no insert data into milvus, only search from milvus
@@ -256,13 +257,13 @@ class MilvusWithPermissionCheck(MilvusLangchain):
return vector_db
def similarity_search(
self,
query: str,
k: int = 4,
param: Optional[dict] = None,
expr: Optional[str] = None,
timeout: Optional[int] = None,
**kwargs: Any,
self,
query: str,
k: int = 4,
param: Optional[dict] = None,
expr: Optional[str] = None,
timeout: Optional[int] = None,
**kwargs: Any,
) -> List[Document]:
"""Perform a similarity search against the query string.
@@ -288,13 +289,13 @@ class MilvusWithPermissionCheck(MilvusLangchain):
return [doc for doc, _ in res]
def similarity_search_by_vector(
self,
embedding: List[float],
k: int = 4,
param: Optional[dict] = None,
expr: Optional[str] = None,
timeout: Optional[int] = None,
**kwargs: Any,
self,
embedding: List[float],
k: int = 4,
param: Optional[dict] = None,
expr: Optional[str] = None,
timeout: Optional[int] = None,
**kwargs: Any,
) -> List[Document]:
"""Perform a similarity search against the query string.
@@ -320,13 +321,13 @@ class MilvusWithPermissionCheck(MilvusLangchain):
return [doc for doc, _ in res]
def similarity_search_with_score(
self,
query: str,
k: int = 4,
param: Optional[dict] = None,
expr: Optional[str] = None,
timeout: Optional[int] = None,
**kwargs: Any,
self,
query: str,
k: int = 4,
param: Optional[dict] = None,
expr: Optional[str] = None,
timeout: Optional[int] = None,
**kwargs: Any,
) -> List[Tuple[Document, float]]:
"""Perform a search on a query string and return results with score.
@@ -367,14 +368,14 @@ class MilvusWithPermissionCheck(MilvusLangchain):
return res
def similarity_search_with_score_by_vector(
self,
embedding: List[float],
k: int = 4,
param: Optional[dict] = None,
query: Optional[str] = None,
expr: Optional[str] = None,
timeout: Optional[int] = None,
**kwargs: Any,
self,
embedding: List[float],
k: int = 4,
param: Optional[dict] = None,
query: Optional[str] = None,
expr: Optional[str] = None,
timeout: Optional[int] = None,
**kwargs: Any,
) -> List[Tuple[Document, float]]:
"""Perform a search on a query string and return results with score.
@@ -452,8 +453,7 @@ class MilvusWithPermissionCheck(MilvusLangchain):
@staticmethod
def _relevance_score_fn(distance: float) -> float:
"""Normalize the distance to a score on a scale [0, 1]."""
# Todo: normalize the es score on a scale [0, 1]
return 1 - distance
return 1 - distance / 2
def _select_relevance_score_fn(self) -> Callable[[float], float]:
return self._relevance_score_fn
@@ -465,13 +465,13 @@ class ElasticsearchWithPermissionCheck(VectorStore, ABC):
"""
def __init__(
self,
elasticsearch_url: str,
index_name: List[str],
drop_old: Optional[bool] = False,
*,
ssl_verify: Optional[Dict[str, Any]] = None,
llm_chain: Optional[LLMChain] = None,
self,
elasticsearch_url: str,
index_name: List[str],
drop_old: Optional[bool] = False,
*,
ssl_verify: Optional[Dict[str, Any]] = None,
llm_chain: Optional[LLMChain] = None,
):
"""Initialize with necessary components."""
try:
@@ -572,17 +572,17 @@ class ElasticsearchWithPermissionCheck(VectorStore, ABC):
@classmethod
def from_texts(
cls,
texts: List[str],
embedding: Embeddings,
metadatas: Optional[List[dict]] = None,
ids: Optional[List[str]] = None,
index_name: Optional[List[str]] = None,
refresh_indices: bool = True,
llm: Optional[BaseLLM] = None,
prompt: Optional[PromptTemplate] = DEFAULT_PROMPT,
drop_old: Optional[bool] = False,
**kwargs: Any,
cls,
texts: List[str],
embedding: Embeddings,
metadatas: Optional[List[dict]] = None,
ids: Optional[List[str]] = None,
index_name: Optional[List[str]] = None,
refresh_indices: bool = True,
llm: Optional[BaseLLM] = None,
prompt: Optional[PromptTemplate] = DEFAULT_PROMPT,
drop_old: Optional[bool] = False,
**kwargs: Any,
):
"""Construct ElasticKeywordsSearch wrapper from raw documents.
@@ -0,0 +1,435 @@
import asyncio
import pickle
from enum import Enum
from loguru import logger
from typing import List, Dict, Any, Optional
from pydantic import BaseModel, Field
from bisheng.cache.redis import redis_client
from bisheng.database.models import LinsightExecuteTask
from bisheng.database.models.linsight_execute_task import ExecuteTaskStatusEnum, LinsightExecuteTaskDao
from bisheng.database.models.linsight_session_version import LinsightSessionVersion, LinsightSessionVersionDao
from bisheng.utils.util import retry_async
from bisheng_langchain.linsight.event import ExecStep
class MessageEventType(str, Enum):
"""
消息事件类型枚举
"""
# 任务开始
TASK_START = "task_start"
# 生成任务
TASK_GENERATE = "task_generate"
# 任务状态更新
TASK_STATUS_UPDATE = "task_status_update"
# 用户输入
USER_INPUT = "user_input"
# 用户输入完成
USER_INPUT_COMPLETED = "user_input_completed"
# 任务执行步骤
TASK_EXECUTE_STEP = "task_execute_step"
# 任务结束
TASK_END = "task_end"
# 错误消息
ERROR_MESSAGE = "error_message"
# 最终结果
FINAL_RESULT = "final_result"
# 任务终止
TASK_TERMINATED = "task_terminated"
class MessageData(BaseModel):
"""消息数据模型"""
event_type: MessageEventType
data: Dict[str, Any]
timestamp: Optional[float] = Field(default_factory=lambda: asyncio.get_event_loop().time())
class LinsightStateMessageManager:
"""灵思状态与消息管理器"""
# 类常量
DEFAULT_EXPIRATION = 3600
DEFAULT_RETRY_ATTEMPTS = 3
DEFAULT_RETRY_DELAY = 1
def __init__(self, session_version_id: str):
"""
初始化灵思状态与消息管理器
Args:
session_version_id: 会话版本ID
"""
self._session_version_id = session_version_id
self._redis_client = redis_client
self._logger = logger
# Redis key管理
self._key_prefix = f"linsight_tasks:{self._session_version_id}:"
self._keys = {
'session_version_info': f"{self._key_prefix}session_version_info",
'messages': f"{self._key_prefix}messages",
'execution_tasks': f"{self._key_prefix}execution_tasks:"
}
async def _handle_redis_operation(self, operation, *args, **kwargs):
"""
统一的Redis操作错误处理
Args:
operation: Redis操作函数
*args: 位置参数
**kwargs: 关键字参数
Returns:
操作结果
Raises:
Exception: Redis操作失败时抛出异常
"""
try:
return await operation(*args, **kwargs)
except Exception as e:
self._logger.error(f"Redis operation failed: {e}")
raise
@retry_async(num_retries=DEFAULT_RETRY_ATTEMPTS, delay=DEFAULT_RETRY_DELAY)
async def push_message(self, message: MessageData) -> None:
"""
将消息推送到Redis列表中
Args:
message: 消息模型
"""
self._logger.info(f"Pushing message: {message.event_type}")
await self._handle_redis_operation(
self._redis_client.arpush,
self._keys['messages'],
message.model_dump()
)
self._logger.info(f"Message pushed: {message.event_type}")
@retry_async(num_retries=DEFAULT_RETRY_ATTEMPTS, delay=DEFAULT_RETRY_DELAY)
async def pop_message(self) -> Optional[MessageData]:
"""
从Redis列表中弹出一条消息
Returns:
消息模型或None
"""
try:
message_data = await self._redis_client.ablpop(self._keys['messages'])
if message_data:
return MessageData.model_validate(message_data)
return None
except Exception as e:
self._logger.error(f"Failed to pop message: {e}")
raise e
@retry_async(num_retries=DEFAULT_RETRY_ATTEMPTS, delay=DEFAULT_RETRY_DELAY)
async def set_session_version_info(self, session_version_model) -> None:
"""
设置会话版本信息
Args:
session_version_model: 会话版本模型
"""
# 使用事务确保数据一致性
async with self._redis_client.async_pipeline() as pipe:
try:
# 先写入数据库
await LinsightSessionVersionDao.insert_one(session_version_model)
# 再写入Redis
await pipe.set(
self._keys['session_version_info'],
pickle.dumps(session_version_model.model_dump()),
ex=self.DEFAULT_EXPIRATION
)
await pipe.execute()
self._logger.info(f"Session version info set: {self._session_version_id}")
except Exception as e:
self._logger.error(f"Failed to set session version info: {e}")
raise
@retry_async(num_retries=DEFAULT_RETRY_ATTEMPTS, delay=DEFAULT_RETRY_DELAY)
async def get_session_version_info(self) -> Optional[LinsightSessionVersion]:
"""
获取会话版本信息
Returns:
会话版本信息模型或None
"""
try:
info = await self._handle_redis_operation(
self._redis_client.aget,
self._keys['session_version_info']
)
if not info:
self._logger.warning(f"No session version info found for {self._session_version_id}")
session_version_model = await LinsightSessionVersionDao.get_by_id(self._session_version_id)
await self.set_session_version_info(session_version_model)
return session_version_model
return LinsightSessionVersion.model_validate(info)
except Exception as e:
self._logger.error(f"Failed to get session version info: {e}")
session_version_model = await LinsightSessionVersionDao.get_by_id(self._session_version_id)
await self.set_session_version_info(session_version_model)
return session_version_model
@retry_async(num_retries=DEFAULT_RETRY_ATTEMPTS, delay=DEFAULT_RETRY_DELAY)
async def set_execution_tasks(self, tasks: List[LinsightExecuteTask]) -> None:
"""
设置执行任务信息
Args:
tasks: 执行任务列表
"""
if not tasks:
self._logger.warning("No tasks provided to set_execution_tasks")
return
try:
# 批量写入Redis
tasks_mapping = {
f"{self._keys['execution_tasks']}{task.id}": task.model_dump()
for task in tasks
}
await self._redis_client.amset(tasks_mapping, expiration=self.DEFAULT_EXPIRATION)
except Exception as e:
self._logger.error(f"Failed to set execution tasks: {e}")
raise
@retry_async(num_retries=DEFAULT_RETRY_ATTEMPTS, delay=DEFAULT_RETRY_DELAY)
async def update_execution_task_status(
self,
task_id: str,
status: ExecuteTaskStatusEnum,
**kwargs
) -> Dict[str, Any]:
"""
更新执行任务状态
Args:
task_id: 任务ID
status: 新状态
**kwargs: 其他更新字段
Returns:
更新后的任务数据
"""
try:
# 先更新数据库
task_model = await LinsightExecuteTaskDao.update_by_id(
task_id,
status=status,
**kwargs
)
# 再更新Redis
task_key = f"{self._keys['execution_tasks']}{task_id}"
task_data = task_model.model_dump()
await self._redis_client.aset(
task_key,
task_data,
expiration=self.DEFAULT_EXPIRATION
)
self._logger.info(f"Updated task {task_id} status to {status}")
return task_data
except Exception as e:
self._logger.error(f"Failed to update task {task_id} status: {e}")
raise
@retry_async(num_retries=DEFAULT_RETRY_ATTEMPTS, delay=DEFAULT_RETRY_DELAY)
async def set_user_input(self, task_id: str, user_input: str) -> None:
"""
设置用户输入
Args:
task_id: 任务ID
user_input: 用户输入内容
"""
task_key = f"{self._keys['execution_tasks']}{task_id}"
try:
task_model = await self.get_execution_task(task_id)
if not task_model:
raise ValueError(f"Task with ID {task_id} not found in Redis or database.")
task_model.user_input = user_input
task_model.status = ExecuteTaskStatusEnum.USER_INPUT_COMPLETED
# 使用事务确保数据一致性
async with self._redis_client.async_pipeline() as pipe:
await pipe.set(task_key, pickle.dumps(task_model.model_dump()), ex=self.DEFAULT_EXPIRATION)
await pipe.execute()
# 更新数据库
await LinsightExecuteTaskDao.update_by_id(
task_id,
user_input=user_input,
status=ExecuteTaskStatusEnum.USER_INPUT_COMPLETED
)
self._logger.info(f"Set user input for task {task_id}")
except Exception as e:
self._logger.error(f"Failed to set user input for task {task_id}: {e}")
raise
@retry_async(num_retries=DEFAULT_RETRY_ATTEMPTS, delay=DEFAULT_RETRY_DELAY)
async def get_execution_task(self, task_id: str) -> Optional[LinsightExecuteTask]:
"""
获取执行任务信息
Args:
task_id: 任务ID
Returns:
执行任务模型或None
"""
task_key = f"{self._keys['execution_tasks']}{task_id}"
try:
task_data = await self._redis_client.aget(task_key)
if task_data:
return LinsightExecuteTask.model_validate(task_data)
# 如果Redis中没有数据,从数据库获取
task_model = await LinsightExecuteTaskDao.get_by_id(task_id)
await self.set_execution_tasks([task_model])
return task_model
except Exception as e:
self._logger.error(f"Failed to get execution task {task_id}: {e}")
return None
@retry_async(num_retries=DEFAULT_RETRY_ATTEMPTS, delay=DEFAULT_RETRY_DELAY)
async def add_execution_task_step(self, task_id: str, step: ExecStep) -> None:
"""
添加执行任务步骤
Args:
task_id: 任务ID
step: 执行步骤
"""
task_key = f"{self._keys['execution_tasks']}{task_id}"
try:
task_data = await self.get_execution_task(task_id)
if not task_data:
raise ValueError(f"Task with ID {task_id} not found in Redis.")
task_model = LinsightExecuteTask.model_validate(task_data)
# 初始化历史记录
if task_model.history is None:
task_model.history = []
# 添加新步骤
task_model.history.append(step.model_dump())
# 更新Redis和数据库
await self._redis_client.aset(
task_key,
task_model.model_dump(),
expiration=self.DEFAULT_EXPIRATION
)
await LinsightExecuteTaskDao.update_by_id(
task_id,
history=task_model.history
)
self._logger.info(f"Added step to task {task_id}")
except Exception as e:
self._logger.error(f"Failed to add step to task {task_id}: {e}")
raise
async def get_execution_tasks(self):
"""
获取所有执行任务
Returns:
执行任务列表
"""
try:
pattern = f"{self._keys['execution_tasks']}*"
task_keys = await self._redis_client.akeys(pattern)
if not task_keys:
return []
tasks_data = await self._redis_client.amget(task_keys)
tasks = [LinsightExecuteTask.model_validate(task) for task in tasks_data if task]
if not tasks:
tasks = await LinsightExecuteTaskDao.get_by_session_version_id(
session_version_id=self._session_version_id)
return tasks
except Exception as e:
self._logger.error(f"Failed to get execution tasks: {e}")
return []
async def cleanup_session_data(self) -> None:
"""
清理会话相关的Redis数据
"""
try:
pattern = f"{self._key_prefix}*"
keys = await self._redis_client.keys(pattern)
if keys:
await self._redis_client.delete(*keys)
self._logger.info(f"Cleaned up {len(keys)} keys for session {self._session_version_id}")
except Exception as e:
self._logger.error(f"Failed to cleanup session data: {e}")
raise
async def get_session_stats(self) -> Dict[str, Any]:
"""
获取会话统计信息
Returns:
包含会话统计信息的字典
"""
try:
stats = {
'session_version_id': self._session_version_id,
'message_count': await self._redis_client.llen(self._keys['messages']),
'has_session_info': await self._redis_client.exists(self._keys['session_version_info']),
'task_count': 0
}
# 计算任务数量
pattern = f"{self._keys['execution_tasks']}*"
task_keys = await self._redis_client.keys(pattern)
stats['task_count'] = len(task_keys)
return stats
except Exception as e:
self._logger.error(f"Failed to get session stats: {e}")
return {'error': str(e)}
+645
View File
@@ -0,0 +1,645 @@
import asyncio
import os
import shutil
from contextlib import asynccontextmanager
from datetime import datetime
from typing import Optional, List, Dict, Callable
from loguru import logger
from bisheng.api.services.invite_code.invite_code import InviteCodeService
from bisheng.api.services.linsight.workbench_impl import LinsightWorkbenchImpl
from bisheng.api.services.llm import LLMService
from bisheng.api.services.tool import ToolServices
from bisheng.cache.utils import create_cache_folder_async, CACHE_DIR
from bisheng.core.app_context import app_ctx
from bisheng.database.models import LinsightExecuteTask
from bisheng.database.models.linsight_execute_task import LinsightExecuteTaskDao, ExecuteTaskStatusEnum, \
ExecuteTaskTypeEnum
from bisheng.database.models.linsight_session_version import LinsightSessionVersionDao, SessionVersionStatusEnum, \
LinsightSessionVersion
from bisheng.interface.llms.custom import BishengLLM
from bisheng.linsight import utils as linsight_execute_utils
from bisheng.linsight.state_message_manager import LinsightStateMessageManager, MessageData, MessageEventType
from bisheng.settings import settings
from bisheng.utils.minio_client import minio_client
from bisheng_langchain.linsight.agent import LinsightAgent
from bisheng_langchain.linsight.const import TaskStatus, ExecConfig
from bisheng_langchain.linsight.event import NeedUserInput, GenerateSubTask, ExecStep, TaskStart, TaskEnd, BaseEvent
class TaskExecutionError(Exception):
"""任务执行异常"""
pass
class UserTerminationError(Exception):
"""用户主动终止异常"""
pass
# 任务已在进行中异常
class TaskAlreadyInProgressError(Exception):
"""任务已在进行中异常"""
pass
class LinsightWorkflowTask:
"""工作流任务执行器 - 负责管理整个任务的生命周期"""
USER_TERMINATION_CHECK_INTERVAL = 2
def __init__(self):
self._state_manager: Optional[LinsightStateMessageManager] = None
self._is_terminated = False
self._termination_task: Optional[asyncio.Task] = None
self._final_result: Optional[TaskEnd] = None
self.file_dir: Optional[str] = None
self.session_version_id: Optional[str] = None
self.step_event_extra_files: List[Dict] = [] # 用于存储步骤事件额外处理的文件信息
# ==================== 资源管理 ====================
@asynccontextmanager
async def _managed_execution(self):
"""管理执行资源的上下文管理器"""
self._state_manager = LinsightStateMessageManager(self.session_version_id)
session_model = await self._get_session_model(self.session_version_id)
# 检查会话状态
if await self._is_session_in_progress(session_model):
raise TaskAlreadyInProgressError("任务已在进行中")
try:
# 启动终止监控
await self._start_termination_monitor(session_model)
# 初始化文件目录
self.file_dir = await self._init_file_directory(session_model)
yield session_model
finally:
await self._cleanup_resources()
async def _cleanup_resources(self):
"""清理资源"""
try:
# 停止终止监控
await self._stop_termination_monitor()
# 清理文件目录
if self.file_dir and os.path.exists(self.file_dir):
shutil.rmtree(self.file_dir, ignore_errors=True)
except Exception as e:
logger.error(f"资源清理失败: {e}")
# ==================== 核心执行逻辑 ====================
async def async_run(self, session_version_id: str) -> None:
"""异步任务执行入口"""
self.session_version_id = session_version_id
with logger.contextualize(trace_id=self.session_version_id):
logger.info(f"开始执行任务: session_version_id={self.session_version_id}")
try:
async with self._managed_execution() as session_model:
await self._execute_workflow(session_model)
except UserTerminationError:
logger.info(f"任务被用户主动终止: session_version_id={self.session_version_id}")
except TaskAlreadyInProgressError:
logger.warning(f"任务已在进行中: session_version_id={self.session_version_id}")
except TaskExecutionError as e:
logger.error(f"任务执行失败: session_version_id={self.session_version_id}")
await self._handle_execution_error(e)
except Exception as e:
logger.error(f"未知错误: session_version_id={self.session_version_id}, error={e}")
await self._handle_execution_error(e)
async def _execute_workflow(self, session_model: LinsightSessionVersion):
"""执行工作流的核心逻辑"""
# 更新会话状态为进行中
await self._update_session_status(session_model, SessionVersionStatusEnum.IN_PROGRESS)
# 初始化执行组件
llm = await self._get_llm()
# 生成工具列表
tools = await self._generate_tools(session_model, llm)
linsight_tools = await ToolServices.init_linsight_tools(root_path=self.file_dir)
tools.extend(linsight_tools)
# 创建智能体
agent = await self._create_agent(session_model, llm, tools)
# 检查是否在初始化过程中被终止
self._check_termination()
# 生成并保存任务
task_info = await agent.generate_task(session_model.sop)
await self._save_task_info(session_model, task_info)
# 执行任务
success = await self._execute_agent_tasks(agent, task_info, session_model)
if success:
await self._handle_task_completion(session_model, llm)
else:
await self._handle_user_termination(session_model)
raise UserTerminationError("任务被用户终止")
# ==================== 会话和状态管理 ====================
async def _get_session_model(self, session_version_id: str) -> LinsightSessionVersion:
"""获取会话模型"""
try:
return await LinsightSessionVersionDao.get_by_id(session_version_id)
except Exception as e:
raise TaskExecutionError(f"获取会话模型失败: {e}")
async def _is_session_in_progress(self, session_model: LinsightSessionVersion) -> bool:
"""检查会话是否已在进行中"""
if session_model.status == SessionVersionStatusEnum.IN_PROGRESS:
logger.info(f"会话 {session_model.id} 已在进行中")
return True
return False
async def _update_session_status(self, session_model: LinsightSessionVersion, status: SessionVersionStatusEnum):
"""更新会话状态"""
session_model.status = status
await self._state_manager.set_session_version_info(session_model)
# ==================== 组件初始化 ====================
async def _get_llm(self) -> BishengLLM:
"""获取LLM实例"""
try:
workbench_conf = await LLMService.get_workbench_llm()
linsight_conf = settings.get_linsight_conf()
return BishengLLM(model_id=workbench_conf.task_model.id, temperature=linsight_conf.default_temperature)
except Exception as e:
raise TaskExecutionError("任务已终止,请联系管理员检查灵思任务执行模型状态")
@create_cache_folder_async
async def _init_file_directory(self, session_model: LinsightSessionVersion) -> str:
"""初始化文件目录"""
file_dir = os.path.join(CACHE_DIR, "linsight", session_model.id[:8])
file_dir = os.path.normpath(file_dir)
os.makedirs(file_dir, exist_ok=True)
if not session_model.files:
return file_dir
# 并发下载文件
download_tasks = [
self._download_file(file_info, file_dir)
for file_info in session_model.files
]
results = await asyncio.gather(*download_tasks, return_exceptions=True)
# 记录下载失败的文件
for i, result in enumerate(results):
if isinstance(result, Exception):
file_name = os.path.basename(session_model.files[i]["markdown_file_path"])
logger.error(f"文件下载失败 {file_name}: {result}")
return file_dir
async def _download_file(self, file_info: dict, target_dir: str) -> str:
"""下载单个文件"""
object_name = file_info["markdown_file_path"]
file_name = file_info.get("markdown_filename", os.path.basename(object_name))
file_path = os.path.join(target_dir, file_name)
try:
file_url = minio_client.get_share_link(object_name)
http_client = await app_ctx.get_http_client()
with open(file_path, "wb") as f:
async for chunk in http_client.stream(method="GET", url=str(file_url)):
f.write(chunk)
if not os.path.exists(file_path) or os.path.getsize(file_path) == 0:
raise ValueError(f"文件下载失败或为空: {object_name}")
return file_path
except Exception as e:
logger.error(f"下载文件失败 {object_name}: {e}")
raise
async def _generate_tools(self, session_model: LinsightSessionVersion, llm: BishengLLM) -> List:
"""生成工具列表"""
if not session_model.tools:
return []
return await LinsightWorkbenchImpl.init_linsight_config_tools(session_version=session_model, llm=llm)
async def _create_agent(self, session_model: LinsightSessionVersion, llm: BishengLLM, tools: List) -> LinsightAgent:
workbench_conf = await LLMService.get_workbench_llm()
linsight_conf = settings.get_linsight_conf()
exec_config = ExecConfig(**linsight_conf.model_dump(), debug_id=session_model.id)
"""创建智能体"""
return LinsightAgent(
llm=llm,
query=session_model.question,
tools=tools,
file_dir=self.file_dir,
task_mode=workbench_conf.linsight_executor_mode,
exec_config=exec_config,
)
# ==================== 任务执行 ====================
async def _save_task_info(self, session_model: LinsightSessionVersion, task_info: List[dict]):
"""保存任务信息"""
try:
tasks = []
sorted_data = sorted(task_info, key=lambda x: int(x['step_id'].split('_')[1]))
for index, task_info in enumerate(sorted_data):
previous_task_id = sorted_data[index - 1]["id"] if index > 0 else None
next_task_id = sorted_data[index + 1]["id"] if index < len(sorted_data) - 1 else None
task = LinsightExecuteTask(
id=task_info["id"],
parent_task_id=task_info.get("parent_id"),
session_version_id=session_model.id,
previous_task_id=previous_task_id,
next_task_id=next_task_id,
task_type=ExecuteTaskTypeEnum.COMPOSITE if task_info.get(
"node_loop") else ExecuteTaskTypeEnum.SINGLE,
task_data=task_info
)
tasks.append(task)
await LinsightExecuteTaskDao.batch_create_tasks(tasks)
await self._state_manager.set_execution_tasks(tasks)
# 推送生成任务消息
await self._state_manager.push_message(MessageData(
event_type=MessageEventType.TASK_GENERATE,
data={"tasks": [task.model_dump() for task in tasks]}
))
logger.info(f"Set {len(tasks)} execution tasks")
except Exception as e:
logger.error(f"保存任务信息失败: {e}")
raise TaskExecutionError(f"保存任务信息失败: {e}")
async def _execute_agent_tasks(self, agent, task_info: List[dict],
session_model) -> bool:
"""执行智能体任务 - 修改版本支持用户终止"""
async def agent_execution():
"""智能体执行任务"""
file_list = await LinsightWorkbenchImpl.prepare_file_list(session_model)
async for event in agent.ainvoke(task_info, session_model.sop, file_list=file_list):
await self._handle_event(agent, event, session_model)
return True
async def termination_monitor():
"""终止监控任务"""
while True:
await asyncio.sleep(0.5) # 每0.5秒检查一次
self._check_termination()
try:
# 创建两个并发任务
# 准备用户上传的文件
agent_task = asyncio.create_task(agent_execution())
monitor_task = asyncio.create_task(termination_monitor())
# 等待任何一个任务完成
done, pending = await asyncio.wait(
[agent_task, monitor_task],
return_when=asyncio.FIRST_COMPLETED
)
# 取消未完成的任务
for task in pending:
task.cancel()
try:
await task
except asyncio.CancelledError:
logger.debug("成功取消挂起的任务")
pass
# 检查完成的任务结果
for task in done:
if task.exception():
if isinstance(task.exception(), UserTerminationError):
logger.info("智能体任务被用户终止")
return False
else:
raise task.exception()
else:
# 如果是智能体任务正常完成
if task == agent_task:
logger.info("智能体任务正常完成")
return True
return True
except UserTerminationError:
logger.info("智能体任务被用户终止")
return False
except Exception as e:
# logger.exception(e)
raise TaskExecutionError(f"智能体任务执行失败: {e}")
# ==================== 事件处理 ====================
async def _handle_event(self, agent: LinsightAgent, event: BaseEvent, session_model: LinsightSessionVersion):
"""处理事件"""
event_handlers: Dict[type[BaseEvent], Callable] = {
GenerateSubTask: self._handle_generate_subtask,
TaskStart: self._handle_task_start,
TaskEnd: self._handle_task_end,
NeedUserInput: self._handle_need_user_input,
ExecStep: self._handle_exec_step,
}
handler = event_handlers.get(type(event))
if handler:
await handler(agent, event, session_model)
else:
logger.warning(f"未知事件类型: {type(event)}")
async def _handle_generate_subtask(self, agent: LinsightAgent, event: GenerateSubTask,
session_model: LinsightSessionVersion):
"""处理生成子任务事件"""
await self._save_task_info(session_model, event.subtask)
logger.debug(f"生成子任务: {event}")
async def _handle_task_start(self, agent: LinsightAgent, event: TaskStart, session_model: LinsightSessionVersion):
"""处理任务开始事件"""
task_data = await self._state_manager.update_execution_task_status(
task_id=event.task_id,
status=ExecuteTaskStatusEnum.IN_PROGRESS
)
await self._state_manager.push_message(
MessageData(event_type=MessageEventType.TASK_START, data=task_data)
)
async def _handle_task_end(self, agent: LinsightAgent, event: TaskEnd, session_model: LinsightSessionVersion):
"""处理任务结束事件"""
status = ExecuteTaskStatusEnum.SUCCESS if event.status == TaskStatus.SUCCESS.value else ExecuteTaskStatusEnum.FAILED
task_data = await self._state_manager.update_execution_task_status(
task_id=event.task_id,
status=status,
result={"answer": event.answer},
task_data=event.data
)
await self._state_manager.push_message(
MessageData(event_type=MessageEventType.TASK_END, data=task_data)
)
# 保存最终结果
self._final_result = event
async def _handle_need_user_input(self, agent: LinsightAgent, event: NeedUserInput,
session_model: LinsightSessionVersion):
"""处理需要用户输入事件"""
asyncio.create_task(self._wait_for_user_input(agent, event))
async def _handle_exec_step(self, agent: LinsightAgent, event: ExecStep, session_model: LinsightSessionVersion):
"""处理执行步骤事件"""
# 额外处理步骤事件
event = await linsight_execute_utils.handle_step_event_extra(event, self)
await self._state_manager.add_execution_task_step(event.task_id, step=event)
await self._state_manager.push_message(
MessageData(event_type=MessageEventType.TASK_EXECUTE_STEP, data=event.model_dump())
)
# ==================== 用户输入处理 ====================
async def _wait_for_user_input(self, agent: LinsightAgent, event: NeedUserInput):
"""等待用户输入"""
try:
# 更新状态为等待用户输入
await self._state_manager.update_execution_task_status(
event.task_id,
status=ExecuteTaskStatusEnum.WAITING_FOR_USER_INPUT,
input_prompt=event.call_reason
)
# 推送用户输入事件
await self._state_manager.push_message(
MessageData(event_type=MessageEventType.USER_INPUT, data=event.model_dump())
)
# 等待用户输入完成
task_model = await self._wait_for_input_completion(event.task_id)
if task_model is None:
logger.error(f"任务 {event.task_id} 在等待用户输入时未找到")
raise TaskExecutionError(f"任务 {event.task_id} 在等待用户输入时未找到任务信息")
# 推送输入完成事件
await self._state_manager.push_message(
MessageData(event_type=MessageEventType.USER_INPUT_COMPLETED, data=task_model.model_dump())
)
# 继续执行任务
await agent.continue_task(event.task_id, task_model.user_input)
except Exception as e:
raise TaskExecutionError(f"等待用户输入失败 task_id={event.task_id}: {e}")
async def _wait_for_input_completion(self, task_id: str) -> Optional[LinsightExecuteTask]:
"""等待用户输入完成"""
while True:
self._check_termination()
task_model = await self._state_manager.get_execution_task(task_id)
if task_model is None:
raise ValueError(f"任务 {task_id} 不存在")
if task_model.status == ExecuteTaskStatusEnum.USER_INPUT_COMPLETED:
return task_model
await asyncio.sleep(1)
# ==================== 终止检查 ====================
def _check_termination(self):
"""检查是否被终止"""
if self._is_terminated:
logger.info("检测到终止信号,准备终止智能体任务")
raise UserTerminationError("任务被用户终止")
async def _start_termination_monitor(self, session_model: LinsightSessionVersion):
"""启动终止监控"""
async def monitor():
while not self._is_terminated:
try:
if await self._check_user_termination():
self._is_terminated = True
break
await asyncio.sleep(self.USER_TERMINATION_CHECK_INTERVAL)
except Exception as e:
logger.error(f"终止监控异常: {e}")
await asyncio.sleep(self.USER_TERMINATION_CHECK_INTERVAL)
self._termination_task = asyncio.create_task(monitor())
async def _stop_termination_monitor(self):
"""停止终止监控"""
if self._termination_task and not self._termination_task.done():
self._termination_task.cancel()
try:
await self._termination_task
except asyncio.CancelledError:
pass
async def _check_user_termination(self) -> bool:
"""检查用户是否主动终止"""
if self._is_terminated:
return True
try:
current_session = await self._state_manager.get_session_version_info()
return (current_session and
current_session.status == SessionVersionStatusEnum.TERMINATED)
except Exception as e:
logger.error(f"检查用户终止状态失败: {e}")
return False
async def _handle_user_termination(self, session_model: LinsightSessionVersion):
"""处理用户主动终止"""
logger.info(f"处理用户终止 {session_model.id}")
session_model.status = SessionVersionStatusEnum.TERMINATED
session_model.output_result = {"answer": "任务已被用户主动停止"}
await self._state_manager.set_session_version_info(session_model)
# 设置所有任务为失败
await self._set_tasks_failed()
# 推送终止消息
await self._state_manager.push_message(
MessageData(
event_type=MessageEventType.TASK_TERMINATED,
data={
"message": "任务已被用户主动停止",
"session_id": session_model.id,
"terminated_at": datetime.now().isoformat()
}
)
)
# ==================== 任务完成处理 ====================
async def _handle_task_completion(self, session_model: LinsightSessionVersion, llm: BishengLLM):
"""处理任务完成"""
if not self._final_result:
logger.error("没有找到最终任务结果")
return
if self._final_result.status == TaskStatus.SUCCESS.value:
await self._handle_task_success(session_model, llm)
else:
await self._handle_task_failure(session_model, "任务执行失败")
async def _handle_task_success(self, session_model: LinsightSessionVersion, llm: BishengLLM):
"""处理任务成功"""
try:
# 读取文件目录文件详情
file_details = await linsight_execute_utils.read_file_directory(self.file_dir)
logger.debug(f"读取文件目录文件详情: {file_details}")
final_result_files = await linsight_execute_utils.get_final_result_file(
session_model=session_model,
file_details=file_details,
answer=self._final_result.answer
)
execution_tasks = await self._state_manager.get_execution_tasks()
all_from_session_files = await linsight_execute_utils.get_all_files_from_session(
execution_tasks=execution_tasks, file_details=file_details)
# 更新会话状态
session_model.status = SessionVersionStatusEnum.COMPLETED
session_model.output_result = {
"answer": self._final_result.answer,
"final_files": final_result_files,
"all_from_session_files": all_from_session_files
}
# 保存会话信息并推送消息
await self._state_manager.set_session_version_info(session_model)
await self._state_manager.push_message(
MessageData(
event_type=MessageEventType.FINAL_RESULT,
data=session_model.model_dump()
)
)
# TODO: 保存SOP 暂时关闭自动保存SOP功能
# await self._save_sop(session_model, llm)
logger.info(f"任务成功完成,处理了 {len(final_result_files)} 个文件")
except Exception as e:
logger.error(f"处理任务成功时发生错误: {e}")
raise TaskExecutionError(f"处理任务成功时发生错误: {e}")
# 修改所有的任务失败处理逻辑
async def _set_tasks_failed(self):
"""将所有任务设置为失败"""
try:
# 获取所有执行任务
execution_tasks = await self._state_manager.get_execution_tasks()
for task in execution_tasks:
# 更新每个任务状态为已终止
if task.status not in [ExecuteTaskStatusEnum.TERMINATED, ExecuteTaskStatusEnum.SUCCESS,
ExecuteTaskStatusEnum.FAILED]:
await self._state_manager.update_execution_task_status(task_id=task.id,
status=ExecuteTaskStatusEnum.TERMINATED)
except Exception as e:
logger.warning(f"设置任务失败时发生错误: {e}")
async def _handle_task_failure(self, session_model: LinsightSessionVersion, error_msg: str):
"""处理任务失败"""
session_model.status = SessionVersionStatusEnum.FAILED
session_model.output_result = {"error_message": error_msg}
await self._state_manager.set_session_version_info(session_model)
# 设置所有任务为失败
await self._set_tasks_failed()
await self._state_manager.push_message(
MessageData(event_type=MessageEventType.ERROR_MESSAGE, data={"error": error_msg})
)
system_config = await settings.aget_all_config()
# 获取Linsight_invitation_code
linsight_invitation_code = system_config.get("linsight_invitation_code", False)
if linsight_invitation_code:
await InviteCodeService.revoke_invite_code(user_id=session_model.user_id)
async def _handle_execution_error(self, error: Exception):
"""处理执行错误"""
try:
session_model = await LinsightSessionVersionDao.get_by_id(self.session_version_id)
await self._handle_task_failure(session_model, str(error))
except Exception as e:
logger.error(f"处理执行错误失败: session_version_id={self.session_version_id}, error={e}")
+274
View File
@@ -0,0 +1,274 @@
import asyncio
import os
import uuid
from typing import List, Dict, Any
from loguru import logger
from bisheng.database.models import LinsightSessionVersion, LinsightExecuteTask
from bisheng.utils import util
from bisheng.utils.minio_client import minio_client
from bisheng.utils.util import sync_func_to_async
from bisheng_langchain.linsight.event import ExecStep
# 灵思文件处理工具对应文件参数名
local_file_tool_dict = {
"add_text_to_file": "file_path",
"replace_file_lines": "file_path"
}
# 步骤事件额外处理工具对应参数名、
step_event_extra_tool_dict = {
"add_text_to_file": "file_path",
"replace_file_lines": "file_path",
"read_text_file": "file_path"
}
# 获取任务中的所有操作过的文件
async def get_all_files_from_session(execution_tasks: List[LinsightExecuteTask], file_details: List[Dict]) -> list[
Any] | \
list[
Exception | BaseException | None]:
"""
获取会话中所有操作过的文件
:param file_details:
:param execution_tasks: 执行任务列表
:return: 包含文件详情的列表
"""
# 过程文件列表
all_from_session_files = []
for task in execution_tasks:
if task.history is None or not task.history:
continue
for history in task.history:
history_name = history.get("name", "")
if history_name not in local_file_tool_dict.keys():
continue
file_path = history.get("params", {}).get(local_file_tool_dict[history_name], "")
if not file_path:
continue
file_name = os.path.basename(file_path)
# 从 file_details 中查找文件信息
file_info = next((f for f in file_details if f["file_name"] == file_name), None)
# 如果文件信息不存在,则跳过
if not file_info:
continue
all_from_session_files.append(file_info)
# 去重
seen = set()
all_from_session_files = [
file for file in all_from_session_files
if (file_tuple := (file["file_name"], file["file_path"], file["file_md5"])) not in seen and not seen.add(
file_tuple)
]
if not all_from_session_files:
logger.warning("没有找到会话中操作过的文件")
return []
# 上传文件到MinIO
async def upload_file_to_minio(file_info: Dict) -> dict | None:
"""上传文件到MinIO并返回文件信息"""
try:
object_name = f"linsight/session_files/{execution_tasks[0].session_version_id}/{file_info['file_name']}"
# Use async upload if available, otherwise wrap sync call
await sync_func_to_async(minio_client.upload_minio)(
bucket_name=minio_client.bucket,
object_name=object_name,
file_path=file_info["file_path"]
)
file_info["file_url"] = minio_client.clear_minio_share_host(minio_client.get_share_link(object_name))
return file_info
except Exception as e:
logger.error(f"上传文件到MinIO失败 {file_info['file_name']}: {e}")
return None
# 并行上传文件到MinIO
upload_tasks = [
upload_file_to_minio(file_info)
for file_info in all_from_session_files
]
upload_results = await asyncio.gather(*upload_tasks, return_exceptions=True)
# 过滤掉失败的上传结果
all_from_session_files = [
result for result in upload_results
if result is not None and not isinstance(result, Exception)
]
# 记录失败的上传
failed_uploads = [
result for result in upload_results
if isinstance(result, Exception)
]
if failed_uploads:
logger.warning(f"部分文件上传失败: {len(failed_uploads)} 个文件")
logger.debug(f"会话中操作过的文件数量: {len(all_from_session_files)},文件详情: {all_from_session_files}")
return all_from_session_files
# 读取文件目录文件详情
async def read_file_directory(file_dir: str) -> List[Dict[str, str]]:
"""读取文件目录中的文件详情"""
if not file_dir or not os.path.exists(file_dir):
return []
files = util.read_files_in_directory(file_dir)
file_details = []
for file in files:
file_md5 = await util.async_calculate_md5(file)
file_details.append({
"file_name": os.path.basename(file),
"file_path": file,
"file_md5": file_md5,
"file_id": uuid.uuid4().hex[:8] # 生成唯一的文件ID
})
return file_details
# 获取最终结果文件
async def get_final_result_file(session_model: LinsightSessionVersion, file_details, answer) -> List[Dict]:
"""
获取最终结果文件
:param file_details:
:param session_model: LinsightSessionVersion 模型实例
:param answer: 答案内容
:return: 包含最终结果文件信息的列表
"""
# 最终结果文件
final_result_files = []
for file_info in file_details:
file_name: str = file_info["file_name"]
# 判断文件名是否在answer字符串中
if file_name in answer:
# 如果文件名在答案中,添加到答案中
final_result_files.append({
"file_name": file_name,
"file_path": file_info["file_path"],
"file_md5": file_info["file_md5"],
"file_id": file_info["file_id"]
})
async def upload_file_to_minio(final_file_info: Dict) -> dict | None:
"""上传文件到MinIO并返回文件信息"""
try:
object_name = f"linsight/final_result/{session_model.id}/{final_file_info['file_name']}"
# Use async upload if available, otherwise wrap sync call
await sync_func_to_async(minio_client.upload_minio)(
bucket_name=minio_client.bucket,
object_name=object_name,
file_path=final_file_info["file_path"]
)
final_file_info["file_url"] = minio_client.clear_minio_share_host(minio_client.get_share_link(object_name))
return final_file_info
except Exception as e:
logger.error(f"上传文件到MinIO失败 {final_file_info['file_name']}: {e}")
return None
# 上传文件到MinIO (并行处理)
if final_result_files:
upload_tasks = [
upload_file_to_minio(final_file_info)
for final_file_info in final_result_files
]
upload_results = await asyncio.gather(*upload_tasks, return_exceptions=True)
# 过滤掉失败的上传结果
final_result_files = [
result for result in upload_results
if result is not None and not isinstance(result, Exception)
]
# 记录失败的上传
failed_uploads = [
result for result in upload_results
if isinstance(result, Exception)
]
if failed_uploads:
logger.warning(f"部分文件上传失败: {len(failed_uploads)} 个文件")
return final_result_files
# 步骤事件额外处理
async def handle_step_event_extra(event: ExecStep, task_exec_obj) -> ExecStep:
"""
处理步骤事件的额外逻辑
:param task_exec_obj:
:param event: 事件对象
"""
logger.debug(f"步骤事件额外处理,call_id: {event.call_id}, name: {event.name}, status: {event.status}")
try:
if event.status == "end" and event.name in step_event_extra_tool_dict.keys():
file_path = event.params.get(step_event_extra_tool_dict[event.name], "")
if not file_path:
return event
file_name = os.path.basename(file_path)
logger.debug(f"步骤事件额外处理,文件名: {file_name}")
# 文件路径处理
if not os.path.isabs(file_path):
# 相对路径,转换为绝对路径
file_path = os.path.join(task_exec_obj.file_dir, file_path)
file_path = os.path.normpath(file_path)
logger.debug(f"步骤事件额外处理,转换后的文件路径: {file_path}")
if not os.path.exists(file_path):
logger.error(f"步骤事件额外处理,文件不存在: {file_path}")
return event
file_md5 = await util.async_calculate_md5(file_path)
# 判断文件是否已经上传过
step_event_extra_files = task_exec_obj.step_event_extra_files
if step_event_extra_files:
existing_file = next((f for f in step_event_extra_files if f["file_md5"] == file_md5), None)
if existing_file:
logger.debug(f"步骤事件额外处理,文件已存在: {existing_file['file_name']}, file_md5: {file_md5}")
event.extra_info["file_info"] = {
"file_name": file_name,
"file_md5": existing_file["file_md5"],
"file_url": existing_file["file_url"]
}
return event
object_name = f"linsight/step_event/{task_exec_obj.session_version_id}/{uuid.uuid4().hex[:8]}.{file_name.split('.')[-1]}"
logger.debug(f"步骤事件额外处理,上传文件到MinIO: {object_name}")
# 上传文件到MinIO
await sync_func_to_async(minio_client.upload_minio)(
bucket_name=minio_client.bucket,
object_name=object_name,
file_path=file_path
)
event.extra_info["file_info"] = {
"file_name": file_name,
"file_md5": file_md5,
"file_url": minio_client.clear_minio_share_host(
minio_client.get_share_link(object_name, minio_client.bucket))
}
# 添加到步骤事件额外文件列表
task_exec_obj.step_event_extra_files.append(event.extra_info["file_info"])
except Exception as e:
logger.error(f"步骤事件额外处理异常: {e}")
# 发生异常时,返回原始事件,不做任何修改
return event
+184
View File
@@ -0,0 +1,184 @@
import argparse
import asyncio
import logging
from multiprocessing import Process, Manager
from multiprocessing.managers import ValueProxy
from typing import Optional, Union
from bisheng.cache.redis import RedisClient, redis_client
from bisheng.linsight.task_exec import LinsightWorkflowTask
from bisheng.settings import settings
from bisheng.utils.logger import configure
logger = logging.getLogger(__name__)
# LinsightQueue 队列
class LinsightQueue(object):
def __init__(self, name, namespace, redis):
self.__db: RedisClient = redis
self.key = '%s:%s' % (namespace, name)
async def qsize(self):
return await self.__db.allen(self.key) # 返回队列里面list内元素的数量
async def put(self, data, timeout=None):
await self.__db.arpush(self.key, data, expiration=timeout) # 添加新元素到队列最右方
async def get_wait(self, timeout=None):
# 返回队列第一个元素,如果为空则等待至有元素被加入队列(超时时间阈值为timeout,如果为None则一直等待)
item = await self.__db.ablpop(self.key, timeout=timeout)
return item
async def get_nowait(self):
# 直接返回队列第一个元素,如果队列为空返回的是None
item = await self.__db.alpop(self.key)
return item
# 获取某个任务数据在队列中的位置
async def index(self, data):
"""
获取某个任务数据在队列中的位置
:param data: 任务数据
:return: 任务数据在队列中的位置-1表示不在队列中
"""
items = await self.__db.alrange(self.key)
try:
index = items.index(data)
return index + 1 # 返回索引从1开始
except ValueError:
return 0
# 删除某个任务数据
async def remove(self, data):
"""
删除某个任务数据
:param data: 任务数据
:return: None
"""
await self.__db.alrem(self.key, data) # 从队列中删除指定数据
class ScheduleCenterProcess(Process):
def __init__(self, max_concurrency: ValueProxy = None):
"""
调度中心进程负责从 Redis 队列中获取任务并执行
:param max_concurrency:
"""
super().__init__()
self.daemon = True
self.queue: Optional[LinsightQueue] = None
# 信号量
self.semaphore: Optional[asyncio.Semaphore] = None
self.max_concurrency: Optional[Union[int, ValueProxy]] = max_concurrency
def handle_task_result(self, task: asyncio.Task):
try:
result = task.result() # 如果有异常,这里会抛出
except Exception as e:
logger.error(f"Task failed with exception: {e}")
finally:
# 释放信号量
if self.semaphore:
logger.info("Releasing semaphore after task completion.")
self.semaphore.release()
async def async_run(self):
"""
异步运行方法监听 Redis 队列并执行任务
:return:
"""
logger.info("ScheduleCenterProcess started...")
while True:
await self.semaphore.acquire() # 获取信号量,限制并发数
try:
session_version_id = await self.queue.get_wait()
if session_version_id is None:
logger.info("No session_version_id found in queue, waiting...")
self.semaphore.release()
continue
exec_task = LinsightWorkflowTask()
logger.info(f"Processing session_version_id: {session_version_id}")
task = asyncio.create_task(
exec_task.async_run(session_version_id)
)
task.add_done_callback(self.handle_task_result) # 添加回调处理任务结果
except Exception as e:
logger.error(f"Error in ScheduleCenterProcess: {e}")
if self.semaphore:
if self.semaphore._value < self.max_concurrency:
logger.info("Releasing semaphore due to error.")
self.semaphore.release()
continue
def run(self):
"""
运行进程
:return:
"""
configure(settings.logger_conf)
if self.max_concurrency is not None:
self.max_concurrency = self.max_concurrency.value # 获取 ValueProxy 的实际值
else:
self.max_concurrency = 32
logger.warning("No max_concurrency provided, using default value of 32.")
self.semaphore = asyncio.Semaphore(self.max_concurrency)
logger.info(f"Semaphore initialized with max concurrency: {self.semaphore._value}")
self.queue = LinsightQueue('queue', namespace="linsight", redis=redis_client)
for _ in range(10000):
try:
asyncio.run(self.async_run())
except Exception as e:
logger.error(f"Error in ScheduleCenterProcess run method: {e}")
def start_schedule_center_process(worker_num: int = 4, max_concurrency: ValueProxy = None):
"""
启动调度中心进程
:param max_concurrency:
:param worker_num: 启动的工作进程数量
:return:
"""
logger.info(f"Starting {worker_num} ScheduleCenterProcess workers...")
if worker_num <= 0:
logger.error("worker_num must be greater than 0")
return
processes = []
for _ in range(worker_num):
process = ScheduleCenterProcess(max_concurrency)
process.start()
logger.info(f"Started ScheduleCenterProcess with PID: {process.pid}")
processes.append(process)
logger.info(f"Started {len(processes)} ScheduleCenterProcess workers successfully.")
return processes
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('--worker_num', type=int, default=4, help='进程数量,默认为4')
# 单个进程的最大并发数
parser.add_argument('--max_concurrency', type=int, default=32, help='单个进程的最大并发数,默认为32')
args = parser.parse_args()
max_concurrency = Manager().Value('i', args.max_concurrency)
try:
processes = start_schedule_center_process(worker_num=args.worker_num,
max_concurrency=max_concurrency)
if processes:
for p in processes:
p.join() # 等待所有进程结束
except KeyboardInterrupt:
logger.info("ScheduleCenterProcess interrupted by user.")
logger.info("Stopping ScheduleCenterProcess workers...")
+12 -9
View File
@@ -2,14 +2,6 @@ from contextlib import asynccontextmanager
from pathlib import Path
from typing import Optional
from bisheng.api import router, router_rpc
from bisheng.database.init_data import init_default_data
from bisheng.interface.utils import setup_llm_caching
from bisheng.services.utils import initialize_services, teardown_services
from bisheng.settings import settings
from bisheng.utils.http_middleware import CustomMiddleware
from bisheng.utils.logger import configure
from bisheng.utils.threadpool import thread_pool
from fastapi import FastAPI, HTTPException, Request, status
from fastapi.exceptions import RequestValidationError
from fastapi.middleware.cors import CORSMiddleware
@@ -19,6 +11,16 @@ from fastapi_jwt_auth import AuthJWT
from fastapi_jwt_auth.exceptions import AuthJWTException
from loguru import logger
from bisheng.api import router, router_rpc
from bisheng.core.app_context import init_app_context
from bisheng.database.init_data import init_default_data
from bisheng.interface.utils import setup_llm_caching
from bisheng.services.utils import initialize_services, teardown_services
from bisheng.settings import settings
from bisheng.utils.http_middleware import CustomMiddleware
from bisheng.utils.logger import configure
from bisheng.utils.threadpool import thread_pool
def handle_http_exception(req: Request, exc: Exception) -> ORJSONResponse:
if isinstance(exc, HTTPException):
@@ -48,8 +50,9 @@ _EXCEPTION_HANDLERS = {
@asynccontextmanager
async def lifespan(app: FastAPI):
initialize_services()
await init_app_context()
setup_llm_caching()
init_default_data()
await init_default_data()
# LangfuseInstance.update()
yield
teardown_services()
@@ -0,0 +1,112 @@
import copy
import logging
import os
from enum import Enum
from string import Template
from typing import Union, Dict
import yaml
from pydantic import BaseModel, Field
logger = logging.getLogger(__name__)
class PromptTypeEnum(str, Enum):
"""
Prompt类型枚举
"""
PROMPT = "prompt"
CHATPROMPT = "chat_prompt"
class ChatPromptSchema(BaseModel):
system: str = Field(default='', description='系统Prompt内容')
user: str = Field(default='', description='用户Prompt内容')
# 标准Prompt Schema
class PromptSchema(BaseModel):
"""
标准Prompt Schema
"""
description: str = Field(default='', description='Prompt描述')
type: PromptTypeEnum = Field(default=PromptTypeEnum.PROMPT, description='Prompt类型')
prompt: Union[str, ChatPromptSchema] = Field(..., description='Prompt内容')
# 内置Prompt加载器
class PromptLoader(object):
def __init__(self):
"""
初始化Prompt加载器
"""
self.prompt_yaml_dir = os.path.join(os.path.dirname(__file__), 'yaml')
self.prompts_storage = {}
self._load_all()
# 解析prompts
@staticmethod
def _parse_prompt(prompts_data: dict) -> Dict[str, PromptSchema]:
"""
解析Prompt数据
:param prompts_data: Prompt数据
:return: 解析后的Prompt对象
"""
parsed_prompts = {}
for prompt_name, prompt_data in prompts_data.items():
if not isinstance(prompt_data, dict):
raise ValueError(f"Invalid prompt format for {prompt_name}: Expected a dictionary.")
prompt_schema = PromptSchema(**prompt_data)
parsed_prompts[prompt_name] = prompt_schema
return parsed_prompts
def _load_all(self):
for root, _, files in os.walk(self.prompt_yaml_dir):
for file in files:
if not file.endswith('.yaml') and not file.endswith('.yml'):
continue
file_path = os.path.join(root, file)
namespace = os.path.splitext(file)[0]
with open(file_path, 'r', encoding='utf-8') as f:
try:
prompt_data = yaml.safe_load(f)
if isinstance(prompt_data, dict):
self.prompts_storage[namespace] = self._parse_prompt(prompt_data.get('prompts', {}))
else:
raise ValueError(f"Invalid YAML format in {file_path}: Expected a dictionary.")
except yaml.YAMLError as e:
logger.error(f"Error parsing YAML file {file_path}: {e}")
continue
# 获取指定的Prompt
def get_prompt(self, namespace: str, prompt_name: str) -> PromptSchema:
"""
获取指定的Prompt
:param namespace: 命名空间
:param prompt_name: Prompt名称
:return: Prompt对象
"""
if namespace in self.prompts_storage:
return copy.deepcopy(self.prompts_storage[namespace].get(prompt_name, None))
else:
raise KeyError(f"Namespace '{namespace}' not found in prompts storage.")
def render_prompt(self, namespace: str, prompt_name: str, **kwargs) -> PromptSchema:
"""
渲染指定的Prompt
:param namespace: 命名空间
:param prompt_name: Prompt名称
:param kwargs: 渲染参数
:return: 渲染后的Prompt字符串
"""
prompt_obj = self.get_prompt(namespace, prompt_name)
if prompt_obj.type == PromptTypeEnum.PROMPT:
prompt_obj.prompt = Template(prompt_obj.prompt).safe_substitute(**kwargs)
elif prompt_obj.type == PromptTypeEnum.CHATPROMPT:
prompt_obj.prompt.system = Template(prompt_obj.prompt.system).safe_substitute(**kwargs)
prompt_obj.prompt.user = Template(prompt_obj.prompt.user).safe_substitute(**kwargs)
else:
raise ValueError(f"Unsupported prompt type: {prompt_obj.type}")
return prompt_obj
@@ -0,0 +1,16 @@
version: v1
description: 标题生成
prompts:
linsight:
description: "Linsight任务标题生成"
type: chat_prompt
prompt:
system: |
# 任务
你是一位资深信息架构师,请根据下面用户在会话中提供的「用户目标」,生成一个用于会话列表展示的对话标题(≤10 个汉字或英文单词)。
# 约束
仅输出标题内容,不要解释、不要添加多余文本或 Markdown。
user: |
用户目标:
${USER_GOAL}
+13
View File
@@ -0,0 +1,13 @@
version: v1
description: 标准问答(QA)生成器
prompts:
simple_qa:
description: "生成简单的问答"
type: chat_prompt
prompt:
system: |
你是一个问答生成器,基于用户提供的上下文信息,生成简洁明了的问答对。
user: |
基于以下上下文信息,生成一个问答对:
上下文信息:${context}
请回答以下问题:${question}
+21
View File
@@ -0,0 +1,21 @@
version: v1
description: SOP
prompts:
gen_sop_summary:
description: "根据SOP详情生成摘要信息"
type: chat_prompt
prompt:
system: |
# 任务
1. 你是一位资深信息架构师,请根据下面用户提供的 SOP 详细内容,生成用于在表格中展示的 SOP 描述字段,控制在 50 字以内。
2. 生成结果请严格使用 JSON 格式表示。
# JSON 格式模板
```json
{
"sop_description": "生成结果"
}
```
user: |
SOP 详细内容:
${sop_detail}
+16
View File
@@ -0,0 +1,16 @@
import subprocess
from multiprocessing import freeze_support
from celery.bin.worker import worker
from bisheng.worker import bisheng_celery
if __name__ == '__main__':
bisheng_celery.start(argv=['worker', '-l', 'info', '--pool=threads', '--concurrency=8', "-Q=workflow_celery"])
# celery_app.worker_main(
# argv=["worker", "--loglevel=info", "--logfile=./logs/celery.log", '--pool=threads', '--concurrency=4'])
# worker.main(celery_app)
# celery -A run_celery.celery_app worker -l info -c 16
# celery -A run_celery.celery_app beat # 计划任务 发布
# celery -A run_celery.celery_app worker -l info -P gevent # 调度执行任务
+41
View File
@@ -108,6 +108,19 @@ class CeleryConf(BaseModel):
return value
class LinsightConf(BaseModel):
debug: bool = Field(default=False, description='是否开启debug模式')
tool_buffer: int = Field(default=100000, description='工具执行历史记录的最大token,超过后需要总结下历史记录')
max_steps: int = Field(default=200, description='单个任务最大执行步骤数,防止死循环')
retry_num: int = Field(default=3, description='灵思任务执行过程中模型调用重试次数')
retry_sleep: int = Field(default=5, description='灵思任务执行过程中模型调用重试间隔时间(秒)')
max_file_num: int = Field(default=5, description='生成SOP时,prompt里放的用户上传文件信息的数量')
max_knowledge_num: int = Field(default=20, description='生成SOP时,prompt里放的知识库信息的数量')
waiting_list_url: str = Field(default=None, description='waiting list 跳转链接')
default_temperature: float = Field(default=0, description='模型请求时的默认温度')
retry_temperature: float = Field(default=1, description='react模式json解析失败后重试时模型温度')
class Settings(BaseModel):
model_config = ConfigDict(validate_assignment=True, arbitrary_types_allowed=True, extra='ignore')
@@ -144,6 +157,7 @@ class Settings(BaseModel):
gpts: dict = {}
openai_conf: dict = {}
minio_conf: dict = {}
linsight_conf: LinsightConf = LinsightConf()
logger_conf: LoggerConf = LoggerConf()
password_conf: PasswordConf = PasswordConf()
system_login_method: SystemLoginMethod = {}
@@ -257,6 +271,15 @@ class Settings(BaseModel):
all_config = self.get_all_config()
return WorkflowConf(**all_config.get('workflow', {}))
def get_linsight_conf(self) -> LinsightConf:
# 获取灵思相关的配置项
all_config = self.get_all_config()
conf = LinsightConf(debug=self.linsight_conf.debug)
linsight_conf = all_config.get('linsight', {})
for k, v in linsight_conf.items():
setattr(conf, k, v)
return conf
def get_from_db(self, key: str):
# 先获取所有的key
all_config = self.get_all_config()
@@ -281,6 +304,24 @@ class Settings(BaseModel):
else:
raise Exception('initdb_config not found, please check your system config')
async def aget_all_config(self):
from bisheng.database.base import async_session_getter
from bisheng.cache.redis import redis_client
from bisheng.database.models.config import Config
redis_key = 'config:initdb_config'
cache = await redis_client.aget(redis_key)
if cache:
return yaml.safe_load(cache)
else:
async with async_session_getter() as session:
initdb_config = (await session.exec(select(Config).where(Config.key == 'initdb_config'))).first()
if initdb_config:
await redis_client.aset(redis_key, initdb_config.value, 100)
return yaml.safe_load(initdb_config.value)
else:
raise Exception('initdb_config not found, please check your system config')
def update_from_yaml(self, file_path: str, dev: bool = False):
new_settings = load_settings_from_yaml(file_path)
self.chains = new_settings.chains or {}
+3
View File
@@ -0,0 +1,3 @@
from bisheng.worker.schedule_main import main
main()
+175
View File
@@ -0,0 +1,175 @@
import logging
from typing import Dict, Optional, Union, Any, AsyncGenerator, Literal
from aiohttp import ClientSession, ClientTimeout, TCPConnector
logger = logging.getLogger(__name__)
class AsyncHttpClient(object):
def __init__(self, timeout: int = 120, keep_alive: bool = True, limit_per_host: int = 100,
headers: Dict[str, str] = None, loop=None):
"""
Asynchronous HTTP client using aiohttp.
:param timeout: Timeout in seconds for the requests.
:param max_retries: Maximum number of retries for failed requests.
:param keep_alive: Whether to keep the connection alive.
:param limit_per_host: Maximum number of connections per host.
:param headers:
:param loop:
"""
self.headers = headers or self._get_default_headers()
self.timeout = timeout
self.keep_alive = keep_alive
self.limit_per_host = limit_per_host
self.loop = loop
self._aiohttp_client: Optional[ClientSession] = None
@staticmethod
def _get_default_headers() -> Dict[str, str]:
return {
"Accept": "application/json",
"Content-Type": "application/json",
"User-Agent": "aiohttp-client",
}
def add_custom_header(self, key: str, value: str) -> None:
self.headers[key] = value
def clear_custom_headers(self) -> None:
self.headers = self._get_default_headers()
async def get_aiohttp_client(self, loop=None) -> ClientSession:
loop = loop or self.loop
if self._aiohttp_client is None:
timeout = ClientTimeout(total=self.timeout)
connector = TCPConnector(keepalive_timeout=self.timeout, limit_per_host=self.limit_per_host, loop=loop)
self._aiohttp_client = ClientSession(timeout=timeout, connector=connector,
headers=self.headers, loop=loop)
return self._aiohttp_client
async def close_aiohttp_client(self) -> None:
if self._aiohttp_client:
await self._aiohttp_client.close()
self._aiohttp_client = None
async def _execute_request(
self,
method: str,
url: str,
headers: Dict[str, str],
params: Optional[Dict[str, Union[str, int]]] = None,
body: Optional[Union[Dict[str, Any], str, bytes, Any]] = None,
data_type: Literal["json", "text", "binary"] = "json",
destroy_session: bool = False,
clear_headers: bool = False
) -> Dict[str, Any]:
"""
Execute an HTTP request.
:param method: HTTP method (GET, POST, PUT, DELETE, etc.)
:param url: URL for the request.
:param headers: Headers for the request.
:param params: Query parameters for the request.
:param body: Body of the request.
:param data_type: Type of data to return (json, text, binary).
:param destroy_session: Whether to close the session after the request.
:param clear_headers: Whether to clear custom headers after the request.
:return: Response as a dictionary with status_code, body, and error.
"""
client = await self.get_aiohttp_client()
response: Dict[str, Any] = {"status_code": None, "body": None, "error": None}
headers = headers or self.headers
try:
async with client.request(
method=method,
url=url,
headers=headers,
params=params,
json=body if isinstance(body, dict) else None,
data=body if isinstance(body, str) or isinstance(body, bytes) else None,
) as http_response:
response["status_code"] = http_response.status
if data_type == "json":
response["body"] = await http_response.json()
elif data_type == "text":
response["body"] = await http_response.text()
elif data_type == "binary":
response["body"] = await http_response.read()
http_response.raise_for_status()
except Exception as error:
logger.error(f"Error during {method} request to {url}: {error}")
response["error"] = str(error)
response["status_code"] = 500
finally:
if destroy_session:
await self.close_aiohttp_client()
if clear_headers:
self.clear_custom_headers()
return response
async def get(self, url: str, params: Optional[Dict[str, Union[str, int]]] = None,
data_type: Literal["json", "text", "binary"] = "json", destroy_session: bool = False,
headers: Dict = None, clear_headers: bool = False) -> Dict[str, Any]:
return await self._execute_request("GET", url=url, headers=headers, params=params, data_type=data_type,
destroy_session=destroy_session, clear_headers=clear_headers)
async def post(self, url: str, body: Optional[Union[Dict[str, Any], str, bytes, Any]] = None,
data_type: Literal["json", "text", "binary"] = "json", destroy_session: bool = False,
headers: Dict = None, clear_headers: bool = False) -> Dict[str, Any]:
return await self._execute_request("POST", url=url, headers=headers, body=body, data_type=data_type,
destroy_session=destroy_session, clear_headers=clear_headers)
async def put(self, url: str, body: Optional[Union[Dict[str, Any], str, bytes, Any]] = None,
data_type: Literal["json", "text", "binary"] = "json", destroy_session: bool = False,
headers: Dict = None, clear_headers: bool = False) -> Dict[str, Any]:
return await self._execute_request("PUT", url=url, headers=headers, body=body, data_type=data_type,
destroy_session=destroy_session, clear_headers=clear_headers)
async def patch(self, url: str, body: Optional[Union[Dict[str, Any], str, bytes, Any]] = None,
data_type: Literal["json", "text", "binary"] = "json", destroy_session: bool = False,
headers: Dict = None, clear_headers: bool = False) -> Dict[str, Any]:
return await self._execute_request("PATCH", url=url, headers=headers, body=body, data_type=data_type,
destroy_session=destroy_session, clear_headers=clear_headers)
async def delete(self, url: str, params: Optional[Dict[str, Union[str, int]]] = None,
data_type: Literal["json", "text", "binary"] = "json", destroy_session: bool = False,
headers: Dict = None, clear_headers: bool = False) -> Dict[str, Any]:
return await self._execute_request("DELETE", url=url, headers=headers, params=params, data_type=data_type,
destroy_session=destroy_session, clear_headers=clear_headers)
async def stream(
self, method: Literal["GET", "POST", "PUT", "PATCH"],
url: str,
headers: Dict[str, str] = None,
params: Optional[Dict[str, Union[str, int]]] = None,
body: Optional[Union[Dict[str, Any], str]] = None, chunk_size: int = 1024 * 1024 * 10,
destroy_session: bool = False,
clear_headers: bool = False
) -> AsyncGenerator[bytes, None]:
"""
流式请求
"""
client = await self.get_aiohttp_client()
try:
async with client.request(method=method,
url=url,
headers=headers or self.headers,
params=params,
json=body if isinstance(body, dict) else None,
data=body if isinstance(body, str) else None, ) as response:
response.raise_for_status()
async for chunk in response.content.iter_chunked(chunk_size):
yield chunk
except Exception as error:
logger.error(f"Error during streaming from {url}: {error}")
raise error
finally:
if destroy_session:
await self.close_aiohttp_client()
if clear_headers:
self.clear_custom_headers()
@@ -3,7 +3,6 @@ import json
from typing import BinaryIO
import minio
from bisheng.settings import settings
from loguru import logger
from minio.commonconfig import Filter, CopySource
from minio.lifecycleconfig import LifecycleConfig, Rule, Expiration
+208 -9
View File
@@ -1,16 +1,23 @@
import asyncio
import functools
import hashlib
import importlib
import inspect
import io
import logging
import re
import time
import zipfile
from functools import wraps
from typing import Dict, Optional
from typing import Dict, Optional, Union, List, Tuple
from urllib.parse import urlparse
from pydantic import BaseModel
from bisheng.template.frontend_node.constants import FORCE_SHOW_FIELDS
from bisheng.utils import constants
from docstring_parser import parse # type: ignore
logger = logging.getLogger(__name__)
def build_template_from_function(name: str, type_to_loader_dict: Dict, add_function: bool = False):
classes = [item.__annotations__['return'].__name__ for item in type_to_loader_dict.values()]
@@ -94,11 +101,11 @@ def build_template_from_class(name: str, type_to_cls_dict: Dict, add_function: b
if name == 'self':
continue
variables[name] = {}
variables[name]['default'] = get_default_factory(module=_class.__base__.__module__, function=str(param.annotation))
variables[name]['default'] = get_default_factory(module=_class.__base__.__module__,
function=str(param.annotation))
variables[name]['annotation'] = str(param.annotation)
variables[name]['required'] = False
base_classes = get_base_classes(_class)
# Adding function to base classes to allow
# the output to be a function
@@ -113,10 +120,10 @@ def build_template_from_class(name: str, type_to_cls_dict: Dict, add_function: b
def build_template_from_method(
class_name: str,
method_name: str,
type_to_cls_dict: Dict,
add_function: bool = False,
class_name: str,
method_name: str,
type_to_cls_dict: Dict,
add_function: bool = False,
):
classes = [item.__name__ for item in type_to_cls_dict.values()]
@@ -348,6 +355,25 @@ def sync_to_async(func):
return async_wrapper
def run_async(coro, loop=None):
"""
运行异步函数
:param coro:
:param loop:
:return:
"""
if loop is None:
try:
loop = asyncio.get_running_loop()
return loop.run_until_complete(coro)
except RuntimeError:
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
return loop.run_until_complete(coro)
return loop.run_until_complete(coro)
def get_cache_key(flow_id: str, chat_id: str, vertex_id: str = None):
return f'{flow_id}_{chat_id}_{vertex_id}'
@@ -356,3 +382,176 @@ def _is_valid_url(url: str) -> bool:
"""Check if the url is valid."""
parsed = urlparse(url)
return bool(parsed.netloc) and bool(parsed.scheme)
# 重试装饰器 异步
def retry_async(num_retries=3, delay=0.5, return_exceptions=False):
def wrapper(func):
async def wrapped(*args, **kwargs):
for i in range(num_retries):
try:
return await func(*args, **kwargs)
except Exception as e:
logger.info(
f"Retrying {func.__name__} in {delay} seconds... Attempt {i + 1} of {num_retries}... error: {e}")
if i == num_retries - 1:
if return_exceptions:
# 返回异常的参数 将e.args拆分成元组
return e.args if len(e.args) > 1 else e.args[0]
logger.error(f"Failed to execute {func.__name__} after {num_retries} retries")
raise e
await asyncio.sleep(delay)
return None
return wrapped
return wrapper
# 重试装饰器
def retry_sync(num_retries=3, delay=0.5, return_exceptions=False):
def wrapper(func):
def wrapped(*args, **kwargs):
for i in range(num_retries):
try:
return func(*args, **kwargs)
except Exception as e:
logger.info(
f"Retrying {func.__name__} in {delay} seconds... Attempt {i + 1} of {num_retries}... error: {e}")
if i == num_retries - 1:
if return_exceptions:
# 返回异常的参数 将e.args拆分成元组
return e.args if len(e.args) > 1 else e.args[0]
logger.error(f"Failed to execute {func.__name__} after {num_retries} retries")
raise e
time.sleep(delay)
return None
return wrapped
return wrapper
def calculate_md5(file: Union[str, bytes]):
"""计算文档的 MD5 值。
Returns:
str: 文档的 MD5
"""
md5_hash = hashlib.md5()
if isinstance(file, bytes):
md5_hash.update(file)
return md5_hash.hexdigest()
else:
# 以二进制形式读取文件
with open(file, "rb") as f:
# 按块读取文件,避免大文件占用过多内存
for chunk in iter(lambda: f.read(4096), b""):
md5_hash.update(chunk)
return md5_hash.hexdigest()
async def async_calculate_md5(file: Union[str, bytes]):
"""异步计算文档的 MD5 值。
Returns:
str: 文档的 MD5
"""
import aiofiles
md5_hash = hashlib.md5()
if isinstance(file, bytes):
md5_hash.update(file)
return md5_hash.hexdigest()
else:
# 以二进制形式异步读取文件
async with aiofiles.open(file, "rb") as f:
# 按块异步读取文件,避免大文件占用过多内存
while True:
chunk = await f.read(4096)
if not chunk:
break
md5_hash.update(chunk)
return md5_hash.hexdigest()
# 读取目录下的所有文件
def read_files_in_directory(path: str):
"""
读取目录下的所有文件并返回文件名列表
Args:
path (str): 目录路径
Returns:
list: 文件名列表
"""
import os
if not os.path.exists(path):
logger.error(f"Path {path} does not exist.")
return []
files = []
for root, _, filenames in os.walk(path):
for filename in filenames:
files.append(os.path.join(root, filename))
return files
def sync_func_to_async(func, executor=None):
@functools.wraps(func)
async def wrapper(*args, **kwargs):
loop = asyncio.get_running_loop()
bound_func = functools.partial(func, *args, **kwargs)
return await loop.run_in_executor(executor, bound_func)
return wrapper
def bytes_to_zip(
files: List[Tuple[str, bytes]],
compress_level: int = 6
) -> bytes:
"""
将字节流数据打包成ZIP文件返回ZIP文件的字节流
参数:
files: 包含(文件名, 字节流)元组的列表
compress_level: 压缩级别(0-9)0表示不压缩9表示最高压缩率
返回:
生成的ZIP文件字节流
"""
try:
# 验证压缩级别
if not 0 <= compress_level <= 9:
raise ValueError("压缩级别必须在0到9之间")
# 创建内存中的字节流用于存储ZIP数据
zip_buffer = io.BytesIO()
# 创建ZIP文件并添加字节流数据
with zipfile.ZipFile(
zip_buffer,
'w',
zipfile.ZIP_DEFLATED,
compresslevel=compress_level
) as zipf:
for filename, data in files:
# 向ZIP文件中添加字节流数据
zipf.writestr(filename, data)
print(f"已添加: {filename} (大小: {len(data) / 1024:.2f} KB)")
# 将ZIP数据定位到起始位置并返回字节流
zip_buffer.seek(0)
zip_data = zip_buffer.getvalue()
logger.debug(f"\nZIP文件创建成功,总大小: {len(zip_data) / 1024:.2f} KB")
return zip_data
except Exception as e:
logger.error(f"打包过程出错: {str(e)}")
raise e
+1
View File
@@ -1,3 +1,4 @@
# register tasks
from bisheng.worker.test.test import *
from bisheng.worker.knowledge.file_worker import *
from bisheng.worker.workflow.tasks import *
+2 -1
View File
@@ -1,11 +1,12 @@
from celery import Celery
from bisheng.core.app_context import app_ctx
from bisheng.settings import settings
from bisheng.utils.logger import configure
from bisheng.interface.utils import setup_llm_caching
setup_llm_caching()
configure(settings.logger_conf)
# loop = app_ctx.get_event_loop()
bisheng_celery = Celery('bisheng', include=['bisheng.worker'])
bisheng_celery.config_from_object('bisheng.worker.config')
+3 -2
View File
@@ -2,7 +2,8 @@ from loguru import logger
from bisheng.worker.main import bisheng_celery
@bisheng_celery.task
def add(x,y):
def add(x, y):
logger.info(f"add {x} + {y}")
return x+y
return x + y

Some files were not shown because too many files have changed in this diff Show More