Build lightweight AI agent admin
This commit is contained in:
@@ -0,0 +1,419 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Data Source API - 数据源接口
|
||||
提供数据源的增删改查、执行、测试等功能
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
from typing import List, Any, Dict, Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Request
|
||||
from fastapi.responses import StreamingResponse
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.database import get_db
|
||||
from core.application.model import Application
|
||||
from app.config import settings
|
||||
from app.base_schema import PaginatedResponse, ResponseModel
|
||||
from core.data_source.model import DataSource
|
||||
from core.data_source.schema import (
|
||||
DataSourceCreate,
|
||||
DataSourceUpdate,
|
||||
DataSourceResponse,
|
||||
DataSourceSimpleOut,
|
||||
DataSourcePreviewRequest,
|
||||
DataSourceExecuteRequest,
|
||||
DataSourceTestRequest,
|
||||
DataSourceCopyRequest,
|
||||
DataSourceImportCheckIn,
|
||||
DataSourceImportCheckOut,
|
||||
DataSourceImportIn,
|
||||
AIGenerateSqlRequest,
|
||||
AIGenerateSqlResponse,
|
||||
)
|
||||
from core.data_source.service import DataSourceService
|
||||
from core.data_source.import_export import (
|
||||
DataSourceImportExportException,
|
||||
export_config as export_data_source_config,
|
||||
check_import as check_data_source_import,
|
||||
import_config as import_data_source_config,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(prefix="/data-source", tags=["数据源管理"])
|
||||
|
||||
|
||||
# ============ 静态路径接口(必须放在动态路径之前) ============
|
||||
|
||||
@router.get("", response_model=PaginatedResponse[DataSourceResponse], summary="获取数据源列表")
|
||||
async def list_data_source(
|
||||
page: int = Query(default=1, ge=1, description="页码"),
|
||||
page_size: int = Query(default=settings.PAGE_SIZE, ge=1, le=settings.PAGE_MAX_SIZE, alias="pageSize", description="每页数量"),
|
||||
application_id: Optional[str] = Query(default=None, alias="applicationId", description="所属应用ID"),
|
||||
name: str = Query(default=None, description="名称(模糊查询)"),
|
||||
code: str = Query(default=None, description="编码(模糊查询)"),
|
||||
source_type: str = Query(default=None, alias="sourceType", description="类型"),
|
||||
status: bool = Query(default=None, description="状态"),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""获取数据源列表(分页,自动应用数据权限)"""
|
||||
items, total = await DataSourceService.get_list_with_data_scope(
|
||||
db, page=page, page_size=page_size,
|
||||
application_id=application_id,
|
||||
name=name, code=code, source_type=source_type, status=status,
|
||||
)
|
||||
|
||||
# 批量查询应用名称
|
||||
app_ids = list({item.application_id for item in items if item.application_id})
|
||||
app_name_map = {}
|
||||
if app_ids:
|
||||
app_result = await db.execute(
|
||||
select(Application.id, Application.name).where(Application.id.in_(app_ids))
|
||||
)
|
||||
app_name_map = {row.id: row.name for row in app_result}
|
||||
|
||||
# 构建响应,附加 application_name
|
||||
result_items = []
|
||||
for item in items:
|
||||
item_dict = DataSourceResponse.model_validate(item).model_dump()
|
||||
item_dict["application_name"] = app_name_map.get(item.application_id, "")
|
||||
result_items.append(item_dict)
|
||||
|
||||
return PaginatedResponse(items=result_items, total=total)
|
||||
|
||||
|
||||
@router.post("", response_model=DataSourceResponse, summary="创建数据源")
|
||||
async def create_data_source(
|
||||
data: DataSourceCreate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""创建数据源"""
|
||||
# 检查编码是否已存在
|
||||
if await DataSourceService.check_code_exists(db, data.code):
|
||||
raise HTTPException(status_code=400, detail=f"编码已存在: {data.code}")
|
||||
|
||||
source = await DataSourceService.create(db, data.model_dump())
|
||||
await db.commit()
|
||||
logger.info(f"数据源已创建: {source.code}")
|
||||
return source
|
||||
|
||||
|
||||
@router.get("/get/all", response_model=List[DataSourceSimpleOut], summary="获取所有数据源")
|
||||
async def list_all_data_source(
|
||||
application_id: Optional[str] = Query(default=None, alias="applicationId", description="所属应用ID"),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""获取所有数据源(不分页,用于下拉选择)"""
|
||||
items = await DataSourceService.get_all(db, application_id=application_id)
|
||||
return items
|
||||
|
||||
|
||||
@router.post("/ai/generate-sql", response_model=AIGenerateSqlResponse, summary="AI 生成 SQL")
|
||||
async def ai_generate_sql(
|
||||
body: AIGenerateSqlRequest,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
使用 AI 生成 SQL 语句
|
||||
|
||||
根据用户的自然语言描述,结合选中的表结构和表关系,自动生成 SQL 查询语句。
|
||||
"""
|
||||
try:
|
||||
# 将表关系转换为字典格式
|
||||
table_relations = [rel.model_dump() for rel in body.table_relations] if body.table_relations else []
|
||||
|
||||
result = await DataSourceService.ai_generate_sql(
|
||||
db=db,
|
||||
user_question=body.user_question,
|
||||
db_connection=body.db_connection,
|
||||
database=body.database,
|
||||
schema_name=body.schema_name,
|
||||
selected_tables=body.selected_tables,
|
||||
table_fields=body.table_fields,
|
||||
table_relations=table_relations,
|
||||
include_table_relations=body.include_table_relations,
|
||||
model_id=body.model_id,
|
||||
)
|
||||
return result
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except Exception as e:
|
||||
logger.error(f"AI 生成 SQL 失败: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"生成失败: {str(e)}")
|
||||
|
||||
|
||||
@router.post("/test", summary="测试数据源配置")
|
||||
async def test_data_source(
|
||||
body: DataSourceTestRequest,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""测试数据源配置(不保存,直接执行)"""
|
||||
try:
|
||||
config = {
|
||||
'source_type': body.source_type,
|
||||
'api_url': body.api_url,
|
||||
'api_method': body.api_method,
|
||||
'api_headers': body.api_headers,
|
||||
'api_query_params': body.api_query_params,
|
||||
'api_body_type': body.api_body_type,
|
||||
'api_body': body.api_body,
|
||||
'api_content_type': body.api_content_type,
|
||||
'api_timeout': body.api_timeout,
|
||||
'api_data_path': body.api_data_path,
|
||||
'api_auth_type': body.api_auth_type,
|
||||
'api_auth_config': body.api_auth_config,
|
||||
'api_retry_count': body.api_retry_count,
|
||||
'api_retry_interval': body.api_retry_interval,
|
||||
'api_success_condition': body.api_success_condition,
|
||||
'api_proxy': body.api_proxy,
|
||||
'api_follow_redirects': body.api_follow_redirects,
|
||||
'api_verify_ssl': body.api_verify_ssl,
|
||||
'sql_content': body.sql_content,
|
||||
'db_connection': body.db_connection,
|
||||
'static_data': body.static_data,
|
||||
'params_def': body.params_def,
|
||||
'result_type': body.result_type,
|
||||
'tree_config': body.tree_config,
|
||||
'field_mapping': body.field_mapping,
|
||||
'chart_config': body.chart_config,
|
||||
}
|
||||
|
||||
data = await DataSourceService.execute_temp(db, config, body.params)
|
||||
|
||||
total = len(data) if isinstance(data, list) else 1
|
||||
|
||||
return {
|
||||
'data': data,
|
||||
'total': total,
|
||||
'limited': DataSourceService.MAX_ROWS_TEST,
|
||||
'success': True
|
||||
}
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except Exception as e:
|
||||
logger.error(f"数据源测试失败: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"测试失败: {str(e)}")
|
||||
|
||||
|
||||
@router.get("/check-code/{code}", summary="检查编码是否可用")
|
||||
async def check_code_available(
|
||||
code: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""检查数据源编码是否可用"""
|
||||
exists = await DataSourceService.check_code_exists(db, code)
|
||||
return {'available': not exists}
|
||||
|
||||
|
||||
@router.post("/import/check", response_model=DataSourceImportCheckOut, summary="导入预检查")
|
||||
async def check_import_data_source(
|
||||
data: DataSourceImportCheckIn,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""导入预检查:检查数据源编码是否冲突"""
|
||||
try:
|
||||
return await check_data_source_import(db, data.code)
|
||||
except DataSourceImportExportException as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/import", response_model=DataSourceResponse, summary="导入数据源配置")
|
||||
async def import_data_source(
|
||||
data: DataSourceImportIn,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""导入数据源配置"""
|
||||
try:
|
||||
source = await import_data_source_config(db, data.model_dump())
|
||||
await db.commit()
|
||||
return source
|
||||
except DataSourceImportExportException as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/code/{code}", response_model=DataSourceResponse, summary="根据编码获取数据源详情")
|
||||
async def get_data_source_by_code(
|
||||
code: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""根据编码获取数据源详情(包含参数定义)"""
|
||||
source = await DataSourceService.get_by_code(db, code)
|
||||
if not source:
|
||||
raise HTTPException(status_code=404, detail=f"数据源不存在: {code}")
|
||||
return source
|
||||
|
||||
|
||||
# ============ 执行接口 ============
|
||||
|
||||
@router.get("/execute/{code}", summary="执行数据源(GET)")
|
||||
async def execute_data_source_get(
|
||||
request: Request,
|
||||
code: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""根据编码执行数据源获取数据(GET 方式)"""
|
||||
# 获取所有查询参数
|
||||
params = dict(request.query_params)
|
||||
|
||||
try:
|
||||
data = await DataSourceService.execute(db, code, params)
|
||||
return {'data': data}
|
||||
except ValueError as e:
|
||||
if "不存在" in str(e):
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except Exception as e:
|
||||
logger.error(f"数据源执行失败: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"执行失败: {str(e)}")
|
||||
|
||||
|
||||
@router.post("/execute/{code}", summary="执行数据源(POST)")
|
||||
async def execute_data_source_post(
|
||||
code: str,
|
||||
body: DataSourceExecuteRequest,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""根据编码执行数据源获取数据(POST 方式)"""
|
||||
try:
|
||||
data = await DataSourceService.execute(db, code, body.params)
|
||||
return {'data': data}
|
||||
except ValueError as e:
|
||||
if "不存在" in str(e):
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except Exception as e:
|
||||
logger.error(f"数据源执行失败: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"执行失败: {str(e)}")
|
||||
|
||||
|
||||
# ============ 动态路径接口(必须放在静态路径之后) ============
|
||||
|
||||
@router.get("/{source_id}", response_model=DataSourceResponse, summary="获取数据源详情")
|
||||
async def get_data_source(
|
||||
source_id: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""获取数据源详情"""
|
||||
source = await DataSourceService.get_by_id(db, source_id)
|
||||
if not source:
|
||||
raise HTTPException(status_code=404, detail="数据源不存在")
|
||||
return source
|
||||
|
||||
|
||||
@router.get("/{source_id}/export", summary="导出数据源配置")
|
||||
async def export_data_source(
|
||||
source_id: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""导出数据源配置为 JSON"""
|
||||
try:
|
||||
config = await export_data_source_config(db, source_id)
|
||||
content = json.dumps(config, ensure_ascii=False, indent=2)
|
||||
return StreamingResponse(
|
||||
iter([content]),
|
||||
media_type="application/json",
|
||||
headers={
|
||||
"Content-Disposition": f'attachment; filename="{config["code"]}.json"'
|
||||
},
|
||||
)
|
||||
except DataSourceImportExportException as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
@router.put("/{source_id}", response_model=DataSourceResponse, summary="更新数据源")
|
||||
async def update_data_source(
|
||||
source_id: str,
|
||||
data: DataSourceUpdate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""更新数据源"""
|
||||
# 如果更新了编码,检查新编码是否已存在
|
||||
if data.code:
|
||||
if await DataSourceService.check_code_exists(db, data.code, exclude_id=source_id):
|
||||
raise HTTPException(status_code=400, detail=f"编码已存在: {data.code}")
|
||||
|
||||
source = await DataSourceService.update(db, source_id, data.model_dump(exclude_unset=True))
|
||||
if not source:
|
||||
raise HTTPException(status_code=404, detail="数据源不存在")
|
||||
await db.commit()
|
||||
logger.info(f"数据源已更新: {source.code}")
|
||||
return source
|
||||
|
||||
|
||||
@router.delete("/{source_id}", response_model=ResponseModel, summary="删除数据源")
|
||||
async def delete_data_source(
|
||||
source_id: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""删除数据源"""
|
||||
success = await DataSourceService.delete(db, source_id)
|
||||
if not success:
|
||||
raise HTTPException(status_code=404, detail="数据源不存在")
|
||||
await db.commit()
|
||||
return ResponseModel(message="删除成功")
|
||||
|
||||
|
||||
# ============ 预览和其他接口 ============
|
||||
|
||||
@router.post("/{source_id}/preview", summary="预览数据源数据")
|
||||
async def preview_data_source(
|
||||
source_id: str,
|
||||
body: DataSourcePreviewRequest,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""预览数据源数据(用于调试)"""
|
||||
try:
|
||||
data = await DataSourceService.execute_by_id(db, source_id, body.params)
|
||||
|
||||
# 限制返回数量
|
||||
if isinstance(data, list) and len(data) > body.limit:
|
||||
data = data[:body.limit]
|
||||
|
||||
return {
|
||||
'data': data,
|
||||
'total': len(data) if isinstance(data, list) else 1,
|
||||
'limited': body.limit
|
||||
}
|
||||
except ValueError as e:
|
||||
if "不存在" in str(e):
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except Exception as e:
|
||||
logger.error(f"数据源预览失败: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"预览失败: {str(e)}")
|
||||
|
||||
|
||||
@router.post("/{source_id}/copy", response_model=DataSourceResponse, summary="复制数据源")
|
||||
async def copy_data_source(
|
||||
source_id: str,
|
||||
body: DataSourceCopyRequest,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""复制数据源"""
|
||||
# 检查新编码是否已存在
|
||||
if await DataSourceService.check_code_exists(db, body.new_code):
|
||||
raise HTTPException(status_code=400, detail=f"编码已存在: {body.new_code}")
|
||||
|
||||
source = await DataSourceService.copy(db, source_id, body.new_code, body.new_name)
|
||||
if not source:
|
||||
raise HTTPException(status_code=404, detail="数据源不存在")
|
||||
await db.commit()
|
||||
logger.info(f"数据源已复制: {body.new_code}")
|
||||
return source
|
||||
|
||||
|
||||
@router.post("/{source_id}/clear-cache", response_model=ResponseModel, summary="清除数据源缓存")
|
||||
async def clear_data_source_cache(
|
||||
source_id: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""清除数据源缓存"""
|
||||
source = await DataSourceService.get_by_id(db, source_id)
|
||||
if not source:
|
||||
raise HTTPException(status_code=404, detail="数据源不存在")
|
||||
|
||||
await DataSourceService.clear_cache(source.code)
|
||||
return ResponseModel(message=f"缓存已清除: {source.code}")
|
||||
Reference in New Issue
Block a user