Files

303 lines
12 KiB
Python

import json
import re
import urllib
from typing import Optional
import requests
from fastapi import FastAPI
from sqlmodel import Session, select
from starlette.middleware.cors import CORSMiddleware
from apps.datasource.models.datasource import CoreDatasource
from apps.datasource.utils.utils import aes_encrypt
from apps.system.models.system_model import AssistantModel
from apps.system.schemas.auth import CacheName, CacheNamespace
from apps.system.schemas.system_schema import AssistantHeader, AssistantOutDsSchema, UserInfoDTO
from common.core.config import settings
from common.core.db import engine
from common.core.sqlbot_cache import cache
from common.utils.aes_crypto import simple_aes_decrypt
from common.utils.utils import SQLBotLogUtil, get_domain_list, string_to_numeric_hash
from common.core.deps import Trans
from common.core.response_middleware import ResponseMiddleware
def _update_cors_middleware_instance(app: FastAPI, updated_origins: list[str]):
"""遍历 middleware 栈,找到 CORSMiddleware 实例并更新其 allow_origins。
仅修改 middleware.kwargs 不会影响已构建的中间件实例,
需要直接更新实例的 allow_origins 属性。
"""
stack = getattr(app, 'middleware_stack', None)
while stack is not None and hasattr(stack, 'app'):
if isinstance(stack, CORSMiddleware):
stack.allow_origins = updated_origins
return
stack = stack.app
@cache(namespace=CacheNamespace.EMBEDDED_INFO, cacheName=CacheName.ASSISTANT_INFO, keyExpression="assistant_id")
async def get_assistant_info(*, session: Session, assistant_id: int) -> AssistantModel | None:
db_model = session.get(AssistantModel, assistant_id)
return db_model
def get_assistant_user(*, id: int):
return UserInfoDTO(id=id, account="sqlbot-inner-assistant", oid=1, name="sqlbot-inner-assistant",
email="[email protected]")
def get_assistant_ds(session: Session, llm_service) -> list[dict]:
assistant: AssistantHeader = llm_service.current_assistant
type = assistant.type
if type == 0 or type == 2:
configuration = assistant.configuration
if configuration:
config: dict[any] = json.loads(configuration)
oid: int = int(config['oid'])
stmt = select(CoreDatasource.id, CoreDatasource.name, CoreDatasource.description).where(
CoreDatasource.oid == oid)
if not assistant.online:
public_list: list[int] = config.get('public_list') or None
if public_list:
stmt = stmt.where(CoreDatasource.id.in_(public_list))
else:
return []
""" private_list: list[int] = config.get('private_list') or None
if private_list:
stmt = stmt.where(~CoreDatasource.id.in_(private_list)) """
db_ds_list = session.exec(stmt)
result_list = [
{
"id": ds.id,
"name": ds.name,
"description": ds.description
}
for ds in db_ds_list
]
# filter private ds if offline
return result_list
out_ds_instance: AssistantOutDs = AssistantOutDsFactory.get_instance(assistant)
llm_service.out_ds_instance = out_ds_instance
dslist = out_ds_instance.get_simple_ds_list()
# format?
return dslist
def init_dynamic_cors(app: FastAPI):
try:
with Session(engine) as session:
list_result = session.exec(select(AssistantModel).order_by(AssistantModel.create_time)).all()
seen = set()
unique_domains = []
for item in list_result:
if item.domain:
for domain in get_domain_list(item.domain):
domain = domain.strip()
if domain and domain not in seen:
seen.add(domain)
unique_domains.append(domain)
cors_middleware = None
response_middleware = None
for middleware in app.user_middleware:
if not cors_middleware and middleware.cls == CORSMiddleware:
cors_middleware = middleware
if not response_middleware and middleware.cls == ResponseMiddleware:
response_middleware = middleware
if cors_middleware and response_middleware:
break
updated_origins = list(set(settings.all_cors_origins + unique_domains))
if cors_middleware:
cors_middleware.kwargs['allow_origins'] = updated_origins
_update_cors_middleware_instance(app, updated_origins)
if response_middleware:
for instance in ResponseMiddleware.instances:
instance.update_allow_origins(updated_origins)
except Exception as e:
return False, e
class AssistantOutDs:
assistant: AssistantHeader
ds_list: Optional[list[AssistantOutDsSchema]] = None
certificate: Optional[str] = None
request_origin: Optional[str] = None
def __init__(self, assistant: AssistantHeader):
self.assistant = assistant
self.ds_list = None
self.certificate = assistant.certificate
self.request_origin = assistant.request_origin
self.get_ds_from_api()
# @cache(namespace=CacheNamespace.EMBEDDED_INFO, cacheName=CacheName.ASSISTANT_DS, keyExpression="current_user.id")
def get_ds_from_api(self):
config: dict[any] = json.loads(self.assistant.configuration)
endpoint: str = config['endpoint']
endpoint = self.get_complete_endpoint(endpoint=endpoint)
if not endpoint:
raise Exception(
f"Failed to get datasource list from {config['endpoint']}, error: [Assistant domain or endpoint miss]")
certificateList: list[any] = json.loads(self.certificate)
header = {}
cookies = {}
param = {}
for item in certificateList:
if item['target'] == 'header':
header[item['key']] = item['value']
if item['target'] == 'cookie':
cookies[item['key']] = item['value']
if item['target'] == 'param':
param[item['key']] = item['value']
timeout = int(config.get('timeout')) if config.get('timeout') else 10
res = requests.get(url=endpoint, params=param, headers=header, cookies=cookies, timeout=timeout)
if res.status_code == 200:
result_json: dict[any] = json.loads(res.text)
if result_json.get('code') == 0 or result_json.get('code') == 200:
temp_list = result_json.get('data', [])
temp_ds_list = [
self.convert2schema(item, config)
for item in temp_list
]
self.ds_list = temp_ds_list
return self.ds_list
else:
raise Exception(f"Failed to get datasource list from {endpoint}, error: {result_json.get('message')}")
else:
SQLBotLogUtil.error(f"Failed to get datasource list from {endpoint}, response: {res}")
raise Exception(f"Failed to get datasource list from {endpoint}, response: {res}")
def get_first_element(self, text: str):
parts = re.split(r'[,;]', text.strip())
first_domain = parts[0].strip()
return first_domain
def get_complete_endpoint(self, endpoint: str) -> str | None:
if endpoint.startswith("http://") or endpoint.startswith("https://"):
return endpoint
domain_text = self.assistant.domain
if not domain_text:
return None
if ',' in domain_text or ';' in domain_text:
return (
self.request_origin.strip('/') if self.request_origin else self.get_first_element(domain_text).strip(
'/')) + endpoint
else:
return f"{domain_text}{endpoint}"
def get_simple_ds_list(self):
if self.ds_list:
return [{'id': ds.id, 'name': ds.name, 'description': ds.comment} for ds in self.ds_list]
else:
raise Exception("Datasource list is not found.")
def get_db_schema(self, ds_id: int, question: str = '', embedding: bool = True,
table_list: list[str] = None) -> tuple[str, list]:
ds = self.get_ds(ds_id)
schema_str = ""
db_name = ds.db_schema if ds.db_schema is not None and ds.db_schema != "" else ds.dataBase
schema_str += f"【DB_ID】 {db_name}\n【Schema】\n"
tables = []
table_name_list = []
i = 0
for table in ds.tables:
# 如果传入了 table_list,则只处理在列表中的表
if table_list is not None and table.name not in table_list:
continue
i += 1
schema_table = ''
schema_table += f"# Table: {db_name}.{table.name}" if ds.type != "mysql" and ds.type != "es" else f"# Table: {table.name}"
table_comment = table.comment
if table_comment == '':
schema_table += '\n[\n'
else:
schema_table += f", {table_comment}\n[\n"
field_list = []
for field in table.fields:
field_comment = field.comment
if field_comment == '':
field_list.append(f"({field.name}:{field.type})")
else:
field_list.append(f"({field.name}:{field.type}, {field_comment})")
schema_table += ",\n".join(field_list)
schema_table += '\n]\n'
t_obj = {"id": i, "schema_table": schema_table}
tables.append(t_obj)
table_name_list.append(table.name)
# do table embedding
# if embedding and tables and settings.TABLE_EMBEDDING_ENABLED:
# tables = get_table_embedding(tables, question)
if tables:
for s in tables:
schema_str += s.get('schema_table')
return schema_str, table_name_list
def get_ds(self, ds_id: int, trans: Trans = None):
if self.ds_list:
for ds in self.ds_list:
if ds.id == ds_id:
return ds
else:
raise Exception("Datasource list is not found.")
raise Exception(f"Datasource id {ds_id} is not found." if trans is None else trans(
'i18n_data_training.datasource_id_not_found', key=ds_id))
def convert2schema(self, ds_dict: dict, config: dict[any]) -> AssistantOutDsSchema:
id_marker: str = ''
attr_list = ['name', 'type', 'host', 'port', 'user', 'dataBase', 'schema', 'mode', 'lowVersion']
if config.get('encrypt', False):
key = config.get('aes_key', None)
iv = config.get('aes_iv', None)
aes_attrs = ['host', 'user', 'password', 'dataBase', 'db_schema', 'schema', 'mode', 'lowVersion']
for attr in aes_attrs:
if attr in ds_dict and ds_dict[attr]:
try:
ds_dict[attr] = simple_aes_decrypt(ds_dict[attr], key, iv)
except Exception as e:
raise Exception(
f"Failed to encrypt {attr} for datasource {ds_dict.get('name')}, error: {str(e)}")
id = ds_dict.get('id', None)
if not id:
for attr in attr_list:
if attr in ds_dict:
id_marker += str(ds_dict.get(attr, '')) + '--sqlbot--'
id = string_to_numeric_hash(id_marker)
db_schema = ds_dict.get('schema', ds_dict.get('db_schema', ''))
ds_dict.pop("schema", None)
return AssistantOutDsSchema(**{**ds_dict, "id": id, "db_schema": db_schema})
class AssistantOutDsFactory:
@staticmethod
def get_instance(assistant: AssistantHeader) -> AssistantOutDs:
return AssistantOutDs(assistant)
def get_out_ds_conf(ds: AssistantOutDsSchema, timeout: int = 30) -> str:
conf = {
"host": ds.host or '',
"port": ds.port or 0,
"username": ds.user or '',
"password": ds.password or '',
"database": ds.dataBase or '',
"driver": '',
"extraJdbc": ds.extraParams or '',
"dbSchema": ds.db_schema or '',
"timeout": timeout or 30,
"mode": ds.mode or '',
"lowVersion": ds.lowVersion or False,
}
conf["extraJdbc"] = ''
return aes_encrypt(json.dumps(conf))