初始提交

This commit is contained in:
焦龙言
2026-07-24 09:43:10 +08:00
commit b5b4721456
793 changed files with 116305 additions and 0 deletions
@@ -0,0 +1,164 @@
from typing import Annotated
from fastapi import Body, Path, Request, Response
from fastapi.responses import StreamingResponse
from sqlalchemy.ext.asyncio import AsyncSession
from common.annotation.cache_annotation import ApiCache, ApiCacheEvict
from common.annotation.log_annotation import Log
from common.annotation.rate_limit_annotation import ApiRateLimit, ApiRateLimitPreset
from common.aspect.db_seesion import DBSessionDependency
from common.aspect.pre_auth import CurrentUserDependency, PreAuthDependency
from common.constant import ApiGroup, ApiNamespace
from common.enums import BusinessType
from common.router import APIRouterPro
from common.vo import DataResponseModel, ResponseBaseModel
from module_admin.entity.vo.user_vo import CurrentUserModel
from module_ai.entity.vo.ai_chat_vo import (
AiChatConfigModel,
AiChatRequestModel,
AiChatSessionBaseModel,
AiChatSessionModel,
)
from module_ai.service.ai_chat_service import AiChatService
from utils.log_util import logger
from utils.response_util import ResponseUtil
ai_chat_controller = APIRouterPro(
prefix='/ai/chat', order_num=19, tags=['AI管理-AI对话'], dependencies=[PreAuthDependency()]
)
@ai_chat_controller.post(
'/send',
summary='发送对话消息',
description='流式返回对话结果',
response_class=StreamingResponse,
responses={
200: {
'description': '流式返回对话结果',
'content': {
'text/event-stream': {},
},
}
},
)
@ApiRateLimit(namespace=ApiNamespace.AI_CHAT_SEND, preset=ApiRateLimitPreset.USER_INTERACTIVE_HIGH_FREQ)
async def send_chat_message(
request: Request,
chat_req: AiChatRequestModel,
query_db: Annotated[AsyncSession, DBSessionDependency()],
current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
) -> StreamingResponse:
user_id = current_user.user.user_id if current_user and current_user.user else 1
chat_stream = AiChatService.chat_services(query_db, chat_req, user_id)
logger.info(f'用户{user_id}发送对话消息成功')
return StreamingResponse(content=chat_stream, media_type='text/event-stream')
@ai_chat_controller.get(
'/config',
summary='获取用户对话配置',
description='获取当前用户的AI对话配置',
response_model=DataResponseModel[AiChatConfigModel],
)
@ApiCache(namespace=ApiNamespace.AI_CHAT_CONFIG)
async def get_user_chat_config(
request: Request,
query_db: Annotated[AsyncSession, DBSessionDependency()],
current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
) -> Response:
user_id = current_user.user.user_id
ai_chat_config_detail_result = await AiChatService.ai_chat_config_detail_services(query_db, user_id)
logger.info(f'获取user_id为{user_id}的对话配置成功')
return ResponseUtil.success(data=ai_chat_config_detail_result)
@ai_chat_controller.put(
'/config',
summary='保存用户对话配置',
description='保存当前用户的AI对话配置',
response_model=DataResponseModel[AiChatConfigModel],
)
@ApiCacheEvict(namespaces=ApiGroup.AI_CHAT_CONFIG_MUTATION)
@Log(title='AI对话配置管理', business_type=BusinessType.INSERT)
async def save_user_chat_config(
request: Request,
ai_chat_config: AiChatConfigModel,
query_db: Annotated[AsyncSession, DBSessionDependency()],
current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
) -> Response:
user_id = current_user.user.user_id if current_user and current_user.user else 1
save_ai_chat_config_result = await AiChatService.save_ai_chat_config_services(query_db, user_id, ai_chat_config)
logger.info(save_ai_chat_config_result.message)
return ResponseUtil.success(msg=save_ai_chat_config_result.message)
@ai_chat_controller.get(
'/session/list',
summary='获取会话列表',
description='获取用户的会话列表',
response_model=DataResponseModel[list[AiChatSessionBaseModel]],
)
async def get_chat_session_list(
request: Request,
current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
) -> Response:
result = await AiChatService.get_chat_session_list_services(current_user.user.user_id)
logger.info('获取成功')
return ResponseUtil.success(data=result)
@ai_chat_controller.delete(
'/session/{session_id}',
summary='删除会话',
description='删除指定会话',
response_model=ResponseBaseModel,
)
@Log(title='AI对话会话管理', business_type=BusinessType.DELETE)
async def delete_chat_session(
request: Request,
session_id: Annotated[str, Path(description='会话ID')],
query_db: Annotated[AsyncSession, DBSessionDependency()],
) -> Response:
delete_chat_session_result = await AiChatService.delete_chat_session_services(session_id)
logger.info(delete_chat_session_result.message)
return ResponseUtil.success(msg=delete_chat_session_result.message)
@ai_chat_controller.get(
'/session/{session_id}',
summary='获取会话消息详情',
description='获取指定会话的消息详情',
response_model=DataResponseModel[AiChatSessionModel],
)
async def get_chat_session_detail(
request: Request,
session_id: Annotated[str, Path(description='会话ID')],
) -> Response:
chat_session_detail_result = await AiChatService.get_chat_session_detail_services(session_id)
logger.info(f'获取session_id为{session_id}的信息成功')
return ResponseUtil.success(data=chat_session_detail_result)
@ai_chat_controller.post(
'/cancel',
summary='取消对话',
description='取消正在进行的对话',
response_model=ResponseBaseModel,
)
@ApiRateLimit(namespace=ApiNamespace.AI_CHAT_CANCEL, preset=ApiRateLimitPreset.USER_INTERACTIVE_HIGH_FREQ)
async def cancel_chat_run(
request: Request,
run_id: Annotated[str, Body(embed=True, description='运行ID', alias='runId')],
) -> Response:
cancel_result = await AiChatService.cancel_run_services(run_id)
logger.info(cancel_result.message)
return ResponseUtil.success(msg=cancel_result.message)
@@ -0,0 +1,188 @@
from datetime import datetime
from typing import Annotated
from fastapi import Path, Query, Request, Response
from pydantic_validation_decorator import ValidateFields
from sqlalchemy import ColumnElement
from sqlalchemy.ext.asyncio import AsyncSession
from common.annotation.cache_annotation import ApiCache, ApiCacheEvict
from common.annotation.log_annotation import Log, RequestLogFieldRoot
from common.aspect.data_scope import DataScopeDependency
from common.aspect.db_seesion import DBSessionDependency
from common.aspect.interface_auth import UserInterfaceAuthDependency
from common.aspect.pre_auth import CurrentUserDependency, PreAuthDependency
from common.constant import ApiGroup, ApiNamespace
from common.enums import BusinessType
from common.router import APIRouterPro
from common.vo import DataResponseModel, PageResponseModel, ResponseBaseModel
from module_admin.entity.vo.user_vo import CurrentUserModel
from module_ai.entity.do.ai_model_do import AiModels
from module_ai.entity.vo.ai_model_vo import AiModelModel, AiModelPageQueryModel, DeleteAiModelModel
from module_ai.service.ai_model_service import AiModelService
from utils.log_util import logger
from utils.response_util import ResponseUtil
ai_model_controller = APIRouterPro(
prefix='/ai/model', order_num=18, tags=['AI管理-模型管理'], dependencies=[PreAuthDependency()]
)
@ai_model_controller.get(
'/list',
summary='获取AI模型分页列表接口',
description='用于获取AI模型分页列表',
response_model=PageResponseModel[AiModelModel],
dependencies=[UserInterfaceAuthDependency('ai:model:list')],
)
@ApiCache(namespace=ApiNamespace.AI_MODEL_LIST)
async def get_ai_model_list(
request: Request,
ai_model_page_query: Annotated[AiModelPageQueryModel, Query()],
query_db: Annotated[AsyncSession, DBSessionDependency()],
data_scope_sql: Annotated[ColumnElement, DataScopeDependency(AiModels)],
) -> Response:
# 获取分页数据
result = await AiModelService.get_ai_model_list_services(
query_db, ai_model_page_query, data_scope_sql, is_page=True
)
logger.info('获取成功')
return ResponseUtil.success(model_content=result)
@ai_model_controller.get(
'/all',
summary='获取AI模型不分页列表接口',
description='用于获取AI模型不分页列表',
response_model=DataResponseModel[AiModelModel],
)
@ApiCache(namespace=ApiNamespace.AI_MODEL_ALL)
async def get_ai_model_all(
request: Request,
query_db: Annotated[AsyncSession, DBSessionDependency()],
data_scope_sql: Annotated[ColumnElement, DataScopeDependency(AiModels)],
) -> Response:
# 获取不分页数据
ai_model_page_query = AiModelPageQueryModel(status='0')
result = await AiModelService.get_ai_model_list_services(
query_db, ai_model_page_query, data_scope_sql, is_page=False
)
logger.info('获取成功')
return ResponseUtil.success(data=result)
@ai_model_controller.post(
'',
summary='新增AI模型接口',
description='用于新增AI模型',
response_model=ResponseBaseModel,
dependencies=[UserInterfaceAuthDependency('ai:model:add')],
)
@ValidateFields(validate_model='add_ai_model')
@ApiCacheEvict(namespaces=ApiGroup.AI_MODEL_MUTATION)
@Log(
title='AI模型管理',
business_type=BusinessType.INSERT,
request_log_mode='exclude',
request_exclude_fields=(RequestLogFieldRoot.JSON_BODY.field('api_key'),),
)
async def add_ai_model(
request: Request,
add_ai_model: AiModelModel,
query_db: Annotated[AsyncSession, DBSessionDependency()],
current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
) -> Response:
add_ai_model.user_id = current_user.user.user_id
add_ai_model.dept_id = current_user.user.dept_id
add_ai_model.create_by = current_user.user.user_name
add_ai_model.create_time = datetime.now()
add_ai_model.update_by = current_user.user.user_name
add_ai_model.update_time = datetime.now()
add_ai_model_result = await AiModelService.add_ai_model_services(query_db, add_ai_model)
logger.info(add_ai_model_result.message)
return ResponseUtil.success(msg=add_ai_model_result.message)
@ai_model_controller.put(
'',
summary='编辑AI模型接口',
description='用于编辑AI模型',
response_model=ResponseBaseModel,
dependencies=[UserInterfaceAuthDependency('ai:model:edit')],
)
@ValidateFields(validate_model='edit_ai_model')
@ApiCacheEvict(namespaces=ApiGroup.AI_MODEL_MUTATION)
@Log(
title='AI模型管理',
business_type=BusinessType.UPDATE,
request_log_mode='exclude',
request_exclude_fields=(RequestLogFieldRoot.JSON_BODY.field('api_key'),),
)
async def edit_ai_model(
request: Request,
edit_ai_model: AiModelModel,
query_db: Annotated[AsyncSession, DBSessionDependency()],
current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
data_scope_sql: Annotated[ColumnElement, DataScopeDependency(AiModels)],
) -> Response:
if not current_user.user.admin:
await AiModelService.check_ai_model_data_scope_services(query_db, edit_ai_model.model_id, data_scope_sql)
edit_ai_model.update_by = current_user.user.user_name
edit_ai_model.update_time = datetime.now()
edit_ai_model_result = await AiModelService.edit_ai_model_services(query_db, edit_ai_model)
logger.info(edit_ai_model_result.message)
return ResponseUtil.success(msg=edit_ai_model_result.message)
@ai_model_controller.delete(
'/{model_ids}',
summary='删除AI模型接口',
description='用于删除AI模型',
response_model=ResponseBaseModel,
dependencies=[UserInterfaceAuthDependency('ai:model:remove')],
)
@ApiCacheEvict(namespaces=ApiGroup.AI_MODEL_MUTATION)
@Log(title='AI模型管理', business_type=BusinessType.DELETE)
async def delete_ai_model(
request: Request,
model_ids: Annotated[str, Path(description='需要删除的模型ID')],
query_db: Annotated[AsyncSession, DBSessionDependency()],
current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
data_scope_sql: Annotated[ColumnElement, DataScopeDependency(AiModels)],
) -> Response:
model_id_list = model_ids.split(',')
for model_id in model_id_list:
if not current_user.user.admin:
await AiModelService.check_ai_model_data_scope_services(query_db, int(model_id), data_scope_sql)
delete_ai_model = DeleteAiModelModel(modelIds=model_ids)
delete_ai_model_result = await AiModelService.delete_ai_model_services(query_db, delete_ai_model)
logger.info(delete_ai_model_result.message)
return ResponseUtil.success(msg=delete_ai_model_result.message)
@ai_model_controller.get(
'/{model_id}',
summary='获取AI模型详情接口',
description='用于获取指定AI模型的详细信息',
response_model=DataResponseModel[AiModelModel],
dependencies=[UserInterfaceAuthDependency('ai:model:query')],
)
@ApiCache(namespace=ApiNamespace.AI_MODEL_DETAIL)
async def get_ai_model_detail(
request: Request,
model_id: Annotated[int, Path(description='模型ID')],
query_db: Annotated[AsyncSession, DBSessionDependency()],
current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
data_scope_sql: Annotated[ColumnElement, DataScopeDependency(AiModels)],
) -> Response:
if not current_user.user.admin:
await AiModelService.check_ai_model_data_scope_services(query_db, model_id, data_scope_sql)
ai_model_detail_result = await AiModelService.ai_model_detail_services(query_db, model_id)
logger.info(f'获取model_id为{model_id}的信息成功')
return ResponseUtil.success(data=ai_model_detail_result)
@@ -0,0 +1,52 @@
from sqlalchemy import select, update
from sqlalchemy.ext.asyncio import AsyncSession
from module_ai.entity.do.ai_chat_do import AiChatConfig
from module_ai.entity.vo.ai_chat_vo import AiChatConfigModel
class AiChatConfigDao:
"""
AI对话配置数据库操作层
"""
@classmethod
async def get_chat_config_detail_by_user_id(cls, db: AsyncSession, user_id: int) -> AiChatConfig | None:
"""
根据用户ID获取配置
:param db: orm对象
:param user_id: 用户ID
:return: 配置对象
"""
ai_chat_config = (
(await db.execute(select(AiChatConfig).where(AiChatConfig.user_id == user_id))).scalars().first()
)
return ai_chat_config
@classmethod
async def add_chat_config_dao(cls, db: AsyncSession, chat_config: AiChatConfigModel) -> AiChatConfig:
"""
新增对话配置数据库操作
:param db: orm对象
:param chat_config: 对话配置对象
:return: 配置对象
"""
db_chat_config = AiChatConfig(**chat_config.model_dump(exclude_unset=True))
db.add(db_chat_config)
await db.flush()
return db_chat_config
@classmethod
async def edit_chat_config_dao(cls, db: AsyncSession, chat_config: dict) -> None:
"""
编辑对话配置数据库操作
:param db: orm对象
:param chat_config: 需要更新的对话配置字典
:return:
"""
await db.execute(update(AiChatConfig), [chat_config])
@@ -0,0 +1,96 @@
from typing import Any
from sqlalchemy import ColumnElement, delete, select, update
from sqlalchemy.ext.asyncio import AsyncSession
from common.vo import PageModel
from module_ai.entity.do.ai_model_do import AiModels
from module_ai.entity.vo.ai_model_vo import AiModelModel, AiModelPageQueryModel
from utils.page_util import PageUtil
class AiModelDao:
"""
AI模型管理数据库操作层
"""
@classmethod
async def get_ai_model_detail_by_id(cls, db: AsyncSession, model_id: int) -> AiModels | None:
"""
根据AI模型id获取AI模型详细信息
:param db: orm对象
:param model_id: AI模型id
:return: AI模型信息对象
"""
ai_model_info = (await db.execute(select(AiModels).where(AiModels.model_id == model_id))).scalars().first()
return ai_model_info
@classmethod
async def get_ai_model_list(
cls, db: AsyncSession, query_object: AiModelPageQueryModel, data_scope_sql: ColumnElement, is_page: bool = False
) -> PageModel | list[dict[str, Any]]:
"""
根据查询参数获取AI模型列表信息
:param db: orm对象
:param query_object: 查询参数对象
:param data_scope_sql: 数据权限对应的查询sql语句
:param is_page: 是否开启分页
:return: AI模型列表信息对象
"""
query = (
select(AiModels)
.where(
AiModels.model_id == query_object.model_id if query_object.model_id else True,
AiModels.model_name.like(f'%{query_object.model_name}%') if query_object.model_name else True,
AiModels.model_code.like(f'%{query_object.model_code}%') if query_object.model_code else True,
AiModels.provider == query_object.provider if query_object.provider else True,
AiModels.status == query_object.status if query_object.status else True,
data_scope_sql,
)
.order_by(AiModels.model_sort)
)
ai_model_list: PageModel | list[dict[str, Any]] = await PageUtil.paginate(
db, query, query_object.page_num, query_object.page_size, is_page
)
return ai_model_list
@classmethod
async def add_ai_model_dao(cls, db: AsyncSession, ai_model: AiModelModel) -> AiModels:
"""
新增AI模型数据库操作
:param db: orm对象
:param ai_model: AI模型对象
:return: AI模型信息对象
"""
db_model = AiModels(**ai_model.model_dump(exclude_unset=True))
db.add(db_model)
await db.flush()
return db_model
@classmethod
async def edit_ai_model_dao(cls, db: AsyncSession, ai_model: dict) -> None:
"""
编辑AI模型数据库操作
:param db: orm对象
:param ai_model: 需要更新的AI模型字典
:return:
"""
await db.execute(update(AiModels), [ai_model])
@classmethod
async def delete_ai_model_dao(cls, db: AsyncSession, ai_model: AiModelModel) -> None:
"""
删除AI模型数据库操作
:param db: orm对象
:param ai_model: AI模型对象
:return:
"""
await db.execute(delete(AiModels).where(AiModels.model_id.in_([ai_model.model_id])))
@@ -0,0 +1,26 @@
from datetime import datetime
from sqlalchemy import CHAR, BigInteger, Column, DateTime, Float, Integer, Text
from config.database import Base
class AiChatConfig(Base):
"""
AI对话配置表
"""
__tablename__ = 'ai_chat_config'
__table_args__ = {'comment': 'AI对话配置表'}
chat_config_id = Column(BigInteger, primary_key=True, nullable=False, autoincrement=True, comment='配置主键')
user_id = Column(BigInteger, nullable=False, unique=True, comment='用户ID')
temperature = Column(Float, nullable=True, comment='默认温度')
add_history_to_context = Column(CHAR(1), server_default='0', comment='是否添加历史记录(0是, 1否)')
num_history_runs = Column(Integer, nullable=True, comment='历史记录条数')
system_prompt = Column(Text, nullable=True, comment='系统提示词')
metrics_default_visible = Column(CHAR(1), server_default='0', comment='默认显示指标(0是, 1否)')
vision_enabled = Column(CHAR(1), server_default='1', comment='是否开启视觉(0是, 1否)')
image_max_size_mb = Column(Integer, nullable=True, comment='图片最大大小(MB)')
create_time = Column(DateTime, nullable=True, default=datetime.now, comment='创建时间')
update_time = Column(DateTime, nullable=True, default=datetime.now, comment='更新时间')
@@ -0,0 +1,62 @@
from datetime import datetime
from sqlalchemy import CHAR, BigInteger, Column, DateTime, Float, Integer, String
from config.database import Base
from config.env import DataBaseConfig
from utils.common_util import SqlalchemyUtil
class AiModels(Base):
"""
AI模型表
"""
__tablename__ = 'ai_models'
__table_args__ = {'comment': 'AI模型表'}
model_id = Column(BigInteger, primary_key=True, nullable=False, autoincrement=True, comment='模型主键')
model_code = Column(String(100), nullable=False, comment='模型编码')
model_name = Column(
String(100),
nullable=True,
server_default=SqlalchemyUtil.get_server_default_null(DataBaseConfig.db_type),
comment='模型名称',
)
provider = Column(String(50), nullable=False, comment='提供商')
model_sort = Column(Integer, nullable=False, comment='显示顺序')
api_key = Column(
String(255),
nullable=True,
server_default=SqlalchemyUtil.get_server_default_null(DataBaseConfig.db_type),
comment='API Key',
)
base_url = Column(
String(255),
nullable=True,
server_default=SqlalchemyUtil.get_server_default_null(DataBaseConfig.db_type),
comment='Base URL',
)
model_type = Column(
String(50),
nullable=True,
server_default=SqlalchemyUtil.get_server_default_null(DataBaseConfig.db_type),
comment='模型类型',
)
max_tokens = Column(Integer, nullable=True, comment='最大输出token')
temperature = Column(Float, nullable=True, comment='默认温度')
support_reasoning = Column(CHAR(1), server_default='N', comment='是否支持推理')
support_images = Column(CHAR(1), server_default='N', comment='是否支持图片')
status = Column(CHAR(1), server_default='0', comment='模型状态')
user_id = Column(BigInteger, nullable=True, comment='用户ID')
dept_id = Column(BigInteger, nullable=True, comment='部门ID')
create_by = Column(String(64), nullable=True, server_default="''", comment='创建者')
create_time = Column(DateTime, nullable=True, default=datetime.now(), comment='创建时间')
update_by = Column(String(64), nullable=True, server_default="''", comment='更新者')
update_time = Column(DateTime, nullable=True, default=datetime.now(), comment='更新时间')
remark = Column(
String(500),
nullable=True,
server_default=SqlalchemyUtil.get_server_default_null(DataBaseConfig.db_type),
comment='备注',
)
@@ -0,0 +1,159 @@
from datetime import datetime
from typing import Literal
from pydantic import BaseModel, ConfigDict, Field
from pydantic.alias_generators import to_camel
class AiChatRequestModel(BaseModel):
"""
AI对话请求模型
"""
model_config = ConfigDict(alias_generator=to_camel)
session_id: str | None = Field(default=None, description='会话ID')
model_id: int = Field(description='模型ID')
message: str = Field(description='用户消息')
is_reasoning: bool | None = Field(default=None, description='本次是否开启深度思考')
images: list[str] | None = Field(default=None, description='图片URL列表')
class AiChatConfigModel(BaseModel):
"""
AI对话配置模型
"""
model_config = ConfigDict(alias_generator=to_camel)
chat_config_id: int | None = Field(default=None, description='配置主键')
user_id: int | None = Field(default=None, description='用户ID')
temperature: float | None = Field(default=None, description='默认温度')
add_history_to_context: Literal['0', '1'] | None = Field(default=None, description='是否添加历史记录')
num_history_runs: int | None = Field(default=3, description='历史记录条数')
system_prompt: str | None = Field(default=None, description='系统提示词')
metrics_default_visible: Literal['0', '1'] | None = Field(default=None, description='默认显示指标')
vision_enabled: Literal['0', '1'] | None = Field(default=None, description='是否开启视觉')
image_max_size_mb: int | None = Field(default=None, description='图片最大大小(MB)')
create_time: datetime | None = Field(default=None, description='创建时间')
update_time: datetime | None = Field(default=None, description='更新时间')
class AiChatSessionBaseModel(BaseModel):
"""
AI对话会话基础模型
"""
model_config = ConfigDict(alias_generator=to_camel)
session_id: str = Field(description='会话ID')
session_title: str | None = Field(default=None, description='会话标题')
session_type: str | None = Field(default=None, description='会话类型')
user_id: str | None = Field(default=None, description='用户ID')
created_at: datetime | None = Field(default=None, description='创建时间')
updated_at: datetime | None = Field(default=None, description='更新时间')
class ModelInfoModel(BaseModel):
"""
对话会话数据模型-模型信息模型
"""
model_config = ConfigDict(alias_generator=to_camel)
id: str | None = Field(default=None, description='模型ID')
name: str | None = Field(default=None, description='模型名称')
provider: str | None = Field(default=None, description='模型提供者')
temperature: float | None = Field(default=None, description='模型温度')
class AgentDataModel(BaseModel):
"""
对话会话数据模型-智能体数据模型
"""
model_config = ConfigDict(alias_generator=to_camel)
model: ModelInfoModel | None = Field(default=None, description='模型信息')
agent_id: str | None = Field(default=None, description='智能体ID')
class SessionMetricsModel(BaseModel):
"""
对话会话数据模型-会话指标模型
"""
model_config = ConfigDict(alias_generator=to_camel)
cost: float | None = Field(default=None, description='成本(元)')
timer: float | None = Field(default=None, description='运行时长(秒)')
duration: float | None = Field(default=None, description='运行时长(秒)')
input_tokens: int | None = Field(default=None, description='输入Token数量')
total_tokens: int | None = Field(default=None, description='总Token数量')
output_tokens: int | None = Field(default=None, description='输出Token数量')
provider_metrics: dict | None = Field(default=None, description='提供者指标')
reasoning_tokens: int | None = Field(default=None, description='推理Token数量')
cache_read_tokens: int | None = Field(default=None, description='缓存读取Token数量')
additional_metrics: dict | None = Field(default=None, description='其他指标')
audio_input_tokens: int | None = Field(default=None, description='音频输入Token数量')
audio_total_tokens: int | None = Field(default=None, description='音频总Token数量')
cache_write_tokens: int | None = Field(default=None, description='缓存写入Token数量')
audio_output_tokens: int | None = Field(default=None, description='音频输出Token数量')
time_to_first_token: float | None = Field(default=None, description='到第一个Token的时间(秒)')
class SessionDataModel(BaseModel):
"""
对话会话数据模型
"""
model_config = ConfigDict(alias_generator=to_camel)
session_state: dict | None = Field(default=None, description='会话状态')
session_metrics: SessionMetricsModel | None = Field(default=None, description='会话指标')
class MessageMetrics(BaseModel):
"""
对话消息模型-消息指标模型
"""
model_config = ConfigDict(alias_generator=to_camel)
input_tokens: int | None = Field(default=None, description='输入Token数量')
total_tokens: int | None = Field(default=None, description='总Token数量')
output_tokens: int | None = Field(default=None, description='输出Token数量')
reasoning_tokens: int | None = Field(default=None, description='推理Token数量')
duration: float | None = Field(default=None, description='运行时长(秒)')
time_to_first_token: float | None = Field(default=None, description='到第一个Token的时间(秒)')
class ChatMessageModel(BaseModel):
"""
对话消息模型
"""
model_config = ConfigDict(alias_generator=to_camel)
id: str | None = Field(default=None, description='消息ID')
role: str | None = Field(default=None, description='角色')
content: str | None = Field(default=None, description='内容')
images: list[str] | None = Field(default=None, description='图片列表')
metrics: MessageMetrics | None = Field(default=None, description='Token使用统计')
created_at: datetime | None = Field(default=None, description='创建时间')
from_history: bool | None = Field(default=None, description='是否来自历史记录')
reasoning_content: str | None = Field(default=None, description='推理/思考内容')
stop_after_tool_call: bool | None = Field(default=None, description='是否在工具调用后停止')
class AiChatSessionModel(AiChatSessionBaseModel):
"""
AI对话会话模型
"""
model_config = ConfigDict(alias_generator=to_camel)
agent_id: str | None = Field(default=None, description='智能体ID')
session_data: SessionDataModel | None = Field(default=None, description='会话数据')
agent_data: AgentDataModel | None = Field(default=None, description='智能体数据')
messages: list[ChatMessageModel] | None = Field(default=None, description='消息列表')
@@ -0,0 +1,64 @@
from datetime import datetime
from typing import Literal
from pydantic import BaseModel, ConfigDict, Field
from pydantic.alias_generators import to_camel
from pydantic_validation_decorator import NotBlank, Size
class AiModelModel(BaseModel):
"""
AI模型表对应pydantic模型
"""
model_config = ConfigDict(alias_generator=to_camel, from_attributes=True)
model_id: int | None = Field(default=None, description='模型主键')
model_code: str | None = Field(default=None, description='模型编码')
model_name: str | None = Field(default=None, description='模型名称')
provider: str | None = Field(default=None, description='提供商')
model_sort: int | None = Field(default=None, description='显示顺序')
api_key: str | None = Field(default=None, description='API Key')
base_url: str | None = Field(default=None, description='Base URL')
max_tokens: int | None = Field(default=None, description='最大输出token')
temperature: float | None = Field(default=None, description='默认温度')
support_reasoning: Literal['Y', 'N'] | None = Field(default=None, description='是否支持推理(深度思考)')
support_images: Literal['Y', 'N'] | None = Field(default=None, description='是否支持图片')
model_type: str | None = Field(default=None, description='模型类型')
status: Literal['0', '1'] | None = Field(default=None, description='模型状态')
user_id: int | None = Field(default=None, description='用户ID')
dept_id: int | None = Field(default=None, description='部门ID')
create_by: str | None = Field(default=None, description='创建者')
create_time: datetime | None = Field(default=None, description='创建时间')
update_by: str | None = Field(default=None, description='更新者')
update_time: datetime | None = Field(default=None, description='更新时间')
remark: str | None = Field(default=None, description='备注')
@NotBlank(field_name='model_code', message='模型编码不能为空')
@Size(field_name='model_code', min_length=0, max_length=100, message='模型编码长度不能超过100个字符')
def get_model_code(self) -> str | None:
return self.model_code
@NotBlank(field_name='base_url', message='Base URL不能为空')
@Size(field_name='base_url', min_length=0, max_length=255, message='Base URL长度不能超过255个字符')
def get_base_url(self) -> str | None:
return self.base_url
class AiModelPageQueryModel(AiModelModel):
"""
AI模型管理分页查询模型
"""
page_num: int = Field(default=1, description='当前页码')
page_size: int = Field(default=10, description='每页记录数')
class DeleteAiModelModel(BaseModel):
"""
删除AI模型模型
"""
model_config = ConfigDict(alias_generator=to_camel)
model_ids: str = Field(description='需要删除的模型主键')
@@ -0,0 +1,483 @@
import json
import os
import uuid
from collections.abc import AsyncGenerator, AsyncIterator
from datetime import datetime
from typing import TYPE_CHECKING, Any
from agno.agent import Agent
from agno.db.base import SessionType
from agno.media import Image
from agno.run.agent import RunEvent, RunOutput, RunOutputEvent
from agno.run.cancel import acancel_run
from sqlalchemy.ext.asyncio import AsyncSession
from common.vo import CrudResponseModel
from config.env import UploadConfig
from exceptions.exception import ServiceException
from module_ai.dao.ai_chat_dao import AiChatConfigDao
from module_ai.dao.ai_model_dao import AiModelDao
from module_ai.entity.do.ai_chat_do import AiChatConfig
from module_ai.entity.vo.ai_chat_vo import (
AgentDataModel,
AiChatConfigModel,
AiChatRequestModel,
AiChatSessionBaseModel,
AiChatSessionModel,
ChatMessageModel,
MessageMetrics,
SessionDataModel,
SessionMetricsModel,
)
from module_ai.entity.vo.ai_model_vo import AiModelModel
from utils.ai_util import AiUtil
from utils.common_util import CamelCaseUtil
from utils.crypto_util import CryptoUtil
if TYPE_CHECKING:
from agno.models.message import Message
from agno.run.team import TeamRunOutput
from agno.run.workflow import WorkflowRunOutput
from agno.session import Session
class AiChatService:
"""
AI对话服务层
"""
@classmethod
def _resolve_temperature(cls, user_config: AiChatConfigModel, model_config: AiModelModel) -> float:
"""
解析温度配置,优先级为 用户配置 > 模型配置
:param user_config: 用户配置对象
:param model_config: 模型配置对象
:return: 解析后的温度值
"""
temperature = user_config.temperature or model_config.temperature
return temperature
@classmethod
def _resolve_is_reasoning(cls, chat_req: AiChatRequestModel, model_config: AiModelModel) -> bool:
"""
解析深度思考开关,结合请求参数与模型配置确定最终是否开启
:param chat_req: 对话请求对象
:param model_config: 模型配置对象
:return: 是否开启深度思考
"""
if model_config.support_reasoning != 'Y':
return False
return bool(chat_req.is_reasoning)
@classmethod
def _resolve_history_config(cls, user_config: AiChatConfigModel) -> tuple[bool, int]:
"""
解析历史消息配置,确定是否附带历史以及轮数
:param user_config: 用户配置对象
:return: (是否附带历史, 历史轮数)
"""
add_history = user_config.add_history_to_context == '0'
num_history = user_config.num_history_runs or 3
return bool(add_history), int(num_history)
@classmethod
def _build_agent(
cls,
model_config: AiModelModel,
temperature: float,
system_prompt: str | None,
user_id: int,
session_id: str,
add_history: bool,
num_history: int,
) -> Agent:
"""
构建对话Agent对象
:param model_config: 模型配置对象
:param temperature: 对话温度
:param system_prompt: 系统提示词
:param user_id: 用户ID
:param session_id: 会话ID
:param add_history: 是否附带历史消息
:param num_history: 历史消息轮数
:return: Agent对象
"""
real_api_key = CryptoUtil.decrypt(model_config.api_key)
model = AiUtil.get_model_from_factory(
provider=model_config.provider,
model_code=model_config.model_code,
model_name=model_config.model_name,
api_key=real_api_key,
base_url=model_config.base_url,
temperature=temperature,
max_tokens=model_config.max_tokens,
)
storage = AiUtil.get_storage_engine()
return Agent(
model=model,
id='chat-agent',
description=system_prompt or 'You are a helpful AI assistant.',
db=storage,
user_id=str(user_id),
session_id=session_id,
add_history_to_context=add_history,
num_history_runs=num_history,
markdown=True,
)
@classmethod
def _build_run_kwargs(
cls,
chat_req: AiChatRequestModel,
user_config: AiChatConfigModel,
) -> dict[str, Any]:
"""
构造Agent运行参数
:param chat_req: 对话请求对象
:param user_config: 用户配置对象
:return: 运行参数字典
"""
run_kwargs: dict[str, Any] = {'stream': True, 'stream_events': True}
if not chat_req.images or not user_config.vision_enabled:
return run_kwargs
processed_images: list[Image] = []
for img in chat_req.images:
if img and img.startswith(UploadConfig.UPLOAD_PREFIX):
relative_path = img[len(UploadConfig.UPLOAD_PREFIX) :]
if relative_path.startswith('/'):
relative_path = relative_path[1:]
file_path = os.path.join(UploadConfig.UPLOAD_PATH, relative_path)
abs_path = os.path.abspath(file_path)
if os.path.exists(abs_path):
processed_images.append(Image(filepath=abs_path))
run_kwargs['images'] = processed_images
return run_kwargs
@classmethod
def _convert_images_to_upload_paths(cls, images: list[Image] | None) -> list[str] | None:
"""
将Agno Image对象列表转换为前端可访问的上传路径列表
:param images: Image对象列表
:return: 上传路径列表
"""
if not images:
return None
result = []
for img in images:
# 如果是本地文件路径
if hasattr(img, 'filepath') and img.filepath:
try:
# 使用 abspath 确保路径标准化
abs_filepath = os.path.abspath(img.filepath)
abs_upload_path = os.path.abspath(UploadConfig.UPLOAD_PATH)
if abs_filepath.startswith(abs_upload_path):
relative_path = os.path.relpath(abs_filepath, abs_upload_path)
# 转换路径分隔符为URL格式
url_path = relative_path.replace(os.sep, '/')
# 拼接前缀
full_url = f'{UploadConfig.UPLOAD_PREFIX}/{url_path}'.replace('//', '/')
result.append(full_url)
else:
result.append(img.filepath)
except Exception:
result.append(img.filepath)
# 如果是URL
elif hasattr(img, 'url') and img.url:
result.append(img.url)
return result if result else None
@classmethod
async def _stream_agent(
cls,
agent: Agent,
chat_req: AiChatRequestModel,
run_kwargs: dict[str, Any],
is_reasoning: bool,
session_id: str,
) -> AsyncGenerator[str, None]:
"""
将Agent输出流式转换为前端SSE消息
:param agent: Agent实例
:param chat_req: 对话请求对象
:param run_kwargs: 运行参数字典
:param is_reasoning: 是否输出推理内容
:param session_id: 会话ID
:return: SSE消息生成器
"""
full_response = ''
full_reasoning = ''
try:
yield json.dumps({'session_id': session_id, 'type': 'meta'}) + '\n'
response_stream: AsyncIterator[RunOutputEvent] = agent.arun(chat_req.message, **run_kwargs)
async for chunk in response_stream:
content = None
reasoning = None
if chunk.event == RunEvent.run_started and chunk.run_id:
yield json.dumps({'run_id': chunk.run_id, 'type': 'run_info'}) + '\n'
if chunk.event == RunEvent.run_content:
content = chunk.content
if hasattr(chunk, 'reasoning_content') and chunk.reasoning_content:
reasoning = chunk.reasoning_content
if reasoning and is_reasoning:
full_reasoning += reasoning
yield json.dumps({'content': reasoning, 'type': 'reasoning'}) + '\n'
if chunk.event == RunEvent.run_completed and chunk.metrics:
yield (
json.dumps(
{'metrics': CamelCaseUtil.transform_result(chunk.metrics.to_dict()), 'type': 'metrics'}
)
+ '\n'
)
if content:
full_response += content
yield json.dumps({'content': content, 'type': 'content'}) + '\n'
except Exception as e:
yield json.dumps({'error': str(e), 'type': 'error'}) + '\n'
@classmethod
async def chat_services(
cls, query_db: AsyncSession, chat_req: AiChatRequestModel, user_id: int
) -> AsyncGenerator[str, None]:
"""
流式对话
:param query_db: orm对象
:param chat_req: 对话请求对象
:param user_id: 用户ID
:return: 对话响应流
"""
ai_model = await AiModelDao.get_ai_model_detail_by_id(query_db, chat_req.model_id)
model_config = AiModelModel(**CamelCaseUtil.transform_result(ai_model)) if ai_model else AiModelModel()
if not model_config:
raise ServiceException(message='模型不存在')
user_config = await cls.ai_chat_config_detail_services(query_db, user_id)
session_id = chat_req.session_id
if not session_id:
session_id = str(uuid.uuid4())
temperature = cls._resolve_temperature(user_config, model_config)
is_reasoning = cls._resolve_is_reasoning(chat_req, model_config)
add_history, num_history = cls._resolve_history_config(user_config)
system_prompt = user_config.system_prompt
agent = cls._build_agent(
model_config=model_config,
temperature=temperature,
system_prompt=system_prompt,
user_id=user_id,
session_id=session_id,
add_history=add_history,
num_history=num_history,
)
run_kwargs = cls._build_run_kwargs(chat_req, user_config)
async for chunk in cls._stream_agent(
agent=agent,
chat_req=chat_req,
run_kwargs=run_kwargs,
is_reasoning=is_reasoning,
session_id=session_id,
):
yield chunk
@classmethod
async def ai_chat_config_detail_services(cls, query_db: AsyncSession, user_id: int) -> AiChatConfigModel:
"""
获取用户配置
:param query_db: orm对象
:param user_id: 用户ID
:return: 配置模型
"""
chat_config = await AiChatConfigDao.get_chat_config_detail_by_user_id(query_db, user_id)
result = AiChatConfigModel(**CamelCaseUtil.transform_result(chat_config)) if chat_config else AiChatConfig()
return result
@classmethod
async def save_ai_chat_config_services(
cls, query_db: AsyncSession, user_id: int, page_object: AiChatConfigModel
) -> CrudResponseModel:
"""
保存用户配置
:param query_db: orm对象
:param user_id: 用户ID
:param page_object: AI对话配置对象
:return: 更新后的配置模型
"""
chat_config = await AiChatConfigDao.get_chat_config_detail_by_user_id(query_db, user_id)
if page_object.user_id is None:
page_object.user_id = user_id
try:
if chat_config:
if chat_config.chat_config_id != page_object.chat_config_id:
raise ServiceException(message='只允许修改当前用户的配置')
page_object.update_time = datetime.now()
edit_ai_chat_config = page_object.model_dump(exclude_unset=True)
await AiChatConfigDao.edit_chat_config_dao(query_db, edit_ai_chat_config)
else:
page_object.create_time = datetime.now()
await AiChatConfigDao.add_chat_config_dao(query_db, page_object)
await query_db.commit()
except Exception as e:
await query_db.rollback()
raise e
return CrudResponseModel(is_success=True, message='保存成功')
@classmethod
async def get_chat_session_list_services(cls, user_id: int) -> list[AiChatSessionBaseModel]:
"""
获取用户会话列表
:param user_id: 用户ID
:return: 用户会话列表
"""
# 获取Agno会话列表
storage = AiUtil.get_storage_engine()
sessions: list[Session] = await storage.get_sessions(
user_id=str(user_id),
component_id='chat-agent',
session_type=SessionType.AGENT,
)
result = []
for s in sessions:
created_at = datetime.fromtimestamp(s.created_at) if s.created_at else None
updated_at = datetime.fromtimestamp(s.updated_at) if s.updated_at else None
title_limit = 20
session_title = s.runs[0].input.input_content[:title_limit] + '...' if s.runs else ''
result.append(
AiChatSessionBaseModel(
sessionId=s.session_id,
sessionTitle=session_title if len(session_title) <= title_limit else session_title[:title_limit],
userId=s.user_id,
createdAt=created_at,
updatedAt=updated_at,
)
)
return result
@classmethod
async def delete_chat_session_services(cls, session_id: str) -> CrudResponseModel:
"""
删除会话
:param session_id: 会话ID
:return: 删除结果
"""
storage = AiUtil.get_storage_engine()
delete_result = await storage.delete_session(session_id=session_id)
if not delete_result:
raise ServiceException(message='删除会话失败')
return CrudResponseModel(is_success=True, message='删除成功')
@classmethod
async def get_chat_session_detail_services(cls, session_id: str) -> AiChatSessionModel:
"""
获取会话消息详情
:param session_id: 会话ID
:return: 会话消息详情
"""
storage = AiUtil.get_storage_engine()
session: Session | None = await storage.get_session(session_id=session_id, session_type=SessionType.AGENT)
if not session:
raise ServiceException(message='会话不存在')
session_data: dict[str, Any] = session.session_data
agent_data: dict[str, Any] = session.agent_data
runs: list[RunOutput | TeamRunOutput | WorkflowRunOutput] = session.runs
messages: list[Message] = session.get_messages(skip_roles=['system'])
run_metrics_map = {}
if runs:
for run in runs:
if run.model_provider_data and (provider_id := run.model_provider_data.get('id')):
run_metrics_map[provider_id] = run.metrics
chat_messages = []
for m in messages:
if hasattr(m, 'provider_data') and m.provider_data:
provider_id = m.provider_data.get('id')
if provider_id and provider_id in run_metrics_map:
m.metrics = run_metrics_map[provider_id]
metrics_model = None
if getattr(m, 'metrics', None) and hasattr(m.metrics, 'to_dict'):
metrics_dict = m.metrics.to_dict()
if metrics_dict:
metrics_model = MessageMetrics(**CamelCaseUtil.transform_result(metrics_dict))
chat_messages.append(
ChatMessageModel(
id=m.id,
role=m.role,
content=m.content,
images=cls._convert_images_to_upload_paths(m.images),
metrics=metrics_model,
createdAt=datetime.fromtimestamp(m.created_at) if m.created_at else None,
reasoningContent=m.reasoning_content,
fromHistory=m.from_history,
stopAfterToolCall=m.stop_after_tool_call,
)
)
session_detail = AiChatSessionModel(
sessionId=session.session_id,
sessionTitle=session.runs[0].input.input_content[:20] + '...' if session.runs else '',
userId=session.user_id,
createdAt=datetime.fromtimestamp(session.created_at) if session.created_at else None,
updatedAt=datetime.fromtimestamp(session.updated_at) if session.updated_at else None,
agentId=session.agent_id,
sessionData=SessionDataModel(
sessionState=session_data.get('session_state'),
sessionMetrics=SessionMetricsModel(
**CamelCaseUtil.transform_result(session_data.get('session_metrics'))
),
),
agentData=AgentDataModel(**CamelCaseUtil.transform_result(agent_data)),
messages=chat_messages,
)
return session_detail
@classmethod
async def cancel_run_services(cls, run_id: str) -> CrudResponseModel:
"""
取消运行
:param run_id: 运行ID
:return: 取消结果
"""
cancel_result = await acancel_run(run_id)
if not cancel_result:
raise ServiceException(message='取消运行失败')
return CrudResponseModel(is_success=True, message='取消成功')
@@ -0,0 +1,155 @@
from typing import Any
from sqlalchemy import ColumnElement
from sqlalchemy.ext.asyncio import AsyncSession
from common.vo import CrudResponseModel, PageModel
from exceptions.exception import ServiceException
from module_ai.dao.ai_model_dao import AiModelDao
from module_ai.entity.vo.ai_model_vo import AiModelModel, AiModelPageQueryModel, DeleteAiModelModel
from utils.common_util import CamelCaseUtil
from utils.crypto_util import CryptoUtil
class AiModelService:
"""
AI模型管理服务层
"""
@classmethod
async def get_ai_model_list_services(
cls,
query_db: AsyncSession,
query_object: AiModelPageQueryModel,
data_scope_sql: ColumnElement,
is_page: bool = False,
) -> PageModel | list[dict[str, Any]]:
"""
获取AI模型列表信息service
:param query_db: orm对象
:param query_object: 查询参数对象
:param data_scope_sql: 数据权限对应的查询sql语句
:param is_page: 是否开启分页
:return: AI模型列表信息对象
"""
ai_model_list_result = await AiModelDao.get_ai_model_list(query_db, query_object, data_scope_sql, is_page)
rows = ai_model_list_result.rows if isinstance(ai_model_list_result, PageModel) else ai_model_list_result
for row in rows:
if 'apiKey' in row:
row['apiKey'] = '********' * 3
return ai_model_list_result
@classmethod
async def check_ai_model_data_scope_services(
cls,
query_db: AsyncSession,
model_id: int,
data_scope_sql: ColumnElement,
) -> CrudResponseModel:
"""
校验用户是否有AI模型数据权限service
:param query_db: orm对象
:param model_id: 模型主键
:param data_scope_sql: 数据权限对应的查询sql语句
:return: 校验结果
"""
ai_models = await AiModelDao.get_ai_model_list(
query_db, AiModelModel(modelId=model_id), data_scope_sql, is_page=False
)
if ai_models:
return CrudResponseModel(is_success=True, message='校验通过')
raise ServiceException(message='没有权限访问AI模型数据')
@classmethod
async def add_ai_model_services(cls, query_db: AsyncSession, page_object: AiModelModel) -> CrudResponseModel:
"""
新增AI模型信息service
:param request: Request对象
:param query_db: orm对象
:param page_object: 新增AI模型对象
:return: 新增AI模型校验结果
"""
try:
if page_object.api_key:
page_object.api_key = CryptoUtil.encrypt(page_object.api_key)
await AiModelDao.add_ai_model_dao(query_db, page_object)
await query_db.commit()
return CrudResponseModel(is_success=True, message='新增成功')
except Exception as e:
await query_db.rollback()
raise e
@classmethod
async def edit_ai_model_services(cls, query_db: AsyncSession, page_object: AiModelModel) -> CrudResponseModel:
"""
编辑AI模型信息service
:param query_db: orm对象
:param page_object: 编辑AI模型对象
:return: 编辑AI模型校验结果
"""
edit_ai_model = page_object.model_dump(exclude_unset=True)
if page_object.api_key:
if page_object.api_key == '********' * 3:
if 'api_key' in edit_ai_model:
del edit_ai_model['api_key']
else:
edit_ai_model['api_key'] = CryptoUtil.encrypt(page_object.api_key)
ai_model_info = await cls.ai_model_detail_services(query_db, page_object.model_id)
if ai_model_info.model_id:
try:
await AiModelDao.edit_ai_model_dao(query_db, edit_ai_model)
await query_db.commit()
return CrudResponseModel(is_success=True, message='修改成功')
except Exception as e:
await query_db.rollback()
raise e
else:
raise ServiceException(message='AI模型不存在')
@classmethod
async def delete_ai_model_services(
cls, query_db: AsyncSession, page_object: DeleteAiModelModel
) -> CrudResponseModel:
"""
删除AI模型信息service
:param query_db: orm对象
:param page_object: 删除AI模型对象
:return: 删除AI模型校验结果
"""
if page_object.model_ids:
model_id_list = page_object.model_ids.split(',')
try:
for model_id in model_id_list:
await AiModelDao.delete_ai_model_dao(query_db, AiModelModel(modelId=model_id))
await query_db.commit()
return CrudResponseModel(is_success=True, message='删除成功')
except Exception as e:
await query_db.rollback()
raise e
else:
raise ServiceException(message='传入AI模型id为空')
@classmethod
async def ai_model_detail_services(cls, query_db: AsyncSession, model_id: int) -> AiModelModel:
"""
获取AI模型详细信息service
:param query_db: orm对象
:param model_id: AI模型id
:return: AI模型id对应的信息
"""
ai_model = await AiModelDao.get_ai_model_detail_by_id(query_db, model_id=model_id)
result = AiModelModel(**CamelCaseUtil.transform_result(ai_model)) if ai_model else AiModelModel()
if result.api_key:
result.api_key = '********' * 3
return result