mirror of
https://github.com/dataelement/bisheng.git
synced 2026-08-29 01:22:31 +08:00
Feat/2.0.0 (#1545)
This commit is contained in:
-51
@@ -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
|
||||
|
||||
@@ -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
@@ -79,7 +79,7 @@ typings/
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
# *.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,4 +1,3 @@
|
||||
# 毕昇后端代码
|
||||
|
||||
* Dockerfile 使用 poetry 进行 Python 依赖管理
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
config.yaml
|
||||
@@ -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文件格式不符合要求'
|
||||
@@ -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)
|
||||
|
||||
@@ -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}"
|
||||
|
||||
@@ -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
|
||||
"""
|
||||
0:chunks 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
|
||||
@@ -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
|
||||
|
||||
@@ -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,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",
|
||||
]
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
@@ -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'
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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='上传文件'),
|
||||
|
||||
Vendored
+336
-34
@@ -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)
|
||||
|
||||
Vendored
+36
@@ -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):
|
||||
"""
|
||||
|
||||
@@ -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()
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
]
|
||||
|
||||
@@ -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]):
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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记录, 关键字匹配name、description、content。user_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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.")
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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)}
|
||||
@@ -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}")
|
||||
@@ -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
|
||||
@@ -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...")
|
||||
@@ -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}
|
||||
@@ -0,0 +1,13 @@
|
||||
version: v1
|
||||
description: 标准问答(QA)生成器
|
||||
prompts:
|
||||
simple_qa:
|
||||
description: "生成简单的问答"
|
||||
type: chat_prompt
|
||||
prompt:
|
||||
system: |
|
||||
你是一个问答生成器,基于用户提供的上下文信息,生成简洁明了的问答对。
|
||||
user: |
|
||||
基于以下上下文信息,生成一个问答对:
|
||||
上下文信息:${context}
|
||||
请回答以下问题:${question}
|
||||
@@ -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}
|
||||
@@ -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 # 调度执行任务
|
||||
@@ -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 {}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
from bisheng.worker.schedule_main import main
|
||||
|
||||
main()
|
||||
@@ -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
|
||||
|
||||
@@ -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,3 +1,4 @@
|
||||
# register tasks
|
||||
from bisheng.worker.test.test import *
|
||||
from bisheng.worker.knowledge.file_worker import *
|
||||
from bisheng.worker.workflow.tasks import *
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user