初始提交
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user