935 lines
36 KiB
Python
935 lines
36 KiB
Python
"""
|
||
智能体服务
|
||
"""
|
||
import logging
|
||
import time
|
||
from typing import Any, Dict, List, Optional
|
||
|
||
from sqlalchemy import select
|
||
from sqlalchemy.ext.asyncio import AsyncSession
|
||
|
||
from ai_platform.models import (
|
||
Agent, AgentConversation, AgentMessage, AIWorkflow, LLMModel,
|
||
)
|
||
from .llm_service import LLMService
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
class AgentService:
|
||
"""
|
||
智能体服务
|
||
|
||
管理智能体的对话和推理
|
||
"""
|
||
|
||
def __init__(self, db: Optional[AsyncSession] = None):
|
||
self._db = db
|
||
self.llm_service = LLMService(db)
|
||
|
||
async def chat(
|
||
self,
|
||
agent: Agent,
|
||
conversation: AgentConversation,
|
||
user_message: str,
|
||
application_id: str = None,
|
||
form_code: str = None,
|
||
attachments: List[Dict] = None,
|
||
):
|
||
"""
|
||
与智能体对话(流式)
|
||
|
||
Args:
|
||
agent: 智能体
|
||
conversation: 对话
|
||
user_message: 用户消息
|
||
application_id: 子应用 ID(用于表单创建等场景)
|
||
form_code: 表单编码(从表单列表调用时传入)
|
||
attachments: 附件列表 [{id, type, name, url, mime_type, size}]
|
||
|
||
Yields:
|
||
事件流
|
||
"""
|
||
if agent.mode == 'autonomous':
|
||
async for event in self._chat_autonomous(agent, conversation, user_message, attachments):
|
||
yield event
|
||
elif agent.mode == 'dialog_flow':
|
||
async for event in self._chat_dialog_flow(agent, conversation, user_message, application_id, form_code, attachments):
|
||
yield event
|
||
else:
|
||
yield {'type': 'error', 'content': f'不支持的模式: {agent.mode}'}
|
||
|
||
async def _chat_autonomous(
|
||
self,
|
||
agent: Agent,
|
||
conversation: AgentConversation,
|
||
user_message: str,
|
||
attachments: List[Dict] = None,
|
||
):
|
||
"""
|
||
自主规划模式对话
|
||
|
||
优先使用原生 Function Calling,如果模型不支持则回退到 ReAct 模式
|
||
|
||
Yields:
|
||
流式事件
|
||
"""
|
||
# 检查模型是否支持 Function Calling
|
||
model_id = await self._resolve_agent_model_id(agent)
|
||
supports_function_call = await self._check_model_supports_function_call(model_id)
|
||
|
||
if supports_function_call:
|
||
async for event in self._chat_autonomous_function_calling(agent, conversation, user_message, attachments):
|
||
yield event
|
||
else:
|
||
async for event in self._chat_autonomous_react(agent, conversation, user_message, attachments):
|
||
yield event
|
||
|
||
async def _check_model_supports_function_call(self, model_id) -> bool:
|
||
"""检查模型是否支持 Function Calling"""
|
||
if not self._db or not model_id:
|
||
return False
|
||
|
||
result = await self._db.execute(
|
||
select(LLMModel).where(LLMModel.id == model_id, LLMModel.is_deleted == False)
|
||
)
|
||
model = result.scalar_one_or_none()
|
||
return model.supports_function_call if model else False
|
||
|
||
async def _resolve_agent_model_id(self, agent: Agent) -> Optional[str]:
|
||
if not self._db:
|
||
return str(agent.model_id) if agent.model_id else None
|
||
|
||
if agent.model_id:
|
||
result = await self._db.execute(
|
||
select(LLMModel).where(
|
||
LLMModel.id == agent.model_id,
|
||
LLMModel.is_deleted == False,
|
||
LLMModel.is_active == True,
|
||
)
|
||
)
|
||
if result.scalar_one_or_none():
|
||
return str(agent.model_id)
|
||
|
||
result = await self._db.execute(
|
||
select(LLMModel)
|
||
.where(
|
||
LLMModel.is_deleted == False,
|
||
LLMModel.is_active == True,
|
||
LLMModel.model_type == "chat",
|
||
)
|
||
.order_by(LLMModel.sort.desc(), LLMModel.sys_create_datetime.desc())
|
||
)
|
||
model = result.scalars().first()
|
||
if not model:
|
||
return None
|
||
|
||
agent.model_id = model.id
|
||
await self._db.flush()
|
||
logger.info(
|
||
"Agent %s has no active model, fallback to default chat model %s",
|
||
getattr(agent, "code", agent.id),
|
||
model.id,
|
||
)
|
||
return str(model.id)
|
||
|
||
async def _chat_autonomous_function_calling(
|
||
self,
|
||
agent: Agent,
|
||
conversation: AgentConversation,
|
||
user_message: str,
|
||
attachments: List[Dict] = None,
|
||
):
|
||
"""
|
||
自主规划模式对话(支持多模态附件)
|
||
|
||
Yields:
|
||
流式事件
|
||
"""
|
||
from ai_platform.providers.base import LLMMessage
|
||
|
||
start_time = time.time()
|
||
|
||
if not self._db:
|
||
yield {'type': 'error', 'content': '数据库会话未初始化'}
|
||
return
|
||
|
||
# 创建用户消息(包含附件)
|
||
# 附件格式: [{file_id, type, name, mime_type, size}]
|
||
stored_attachments = []
|
||
if attachments:
|
||
for att in attachments:
|
||
stored_attachments.append({
|
||
'file_id': att.get('file_id'),
|
||
'type': att.get('type', 'file'),
|
||
'name': att.get('name', ''),
|
||
'mime_type': att.get('mime_type', ''),
|
||
'size': att.get('size', 0),
|
||
})
|
||
|
||
user_msg = AgentMessage(
|
||
conversation_id=conversation.id,
|
||
role='user',
|
||
content=user_message,
|
||
attachments=stored_attachments,
|
||
status='completed',
|
||
)
|
||
self._db.add(user_msg)
|
||
await self._db.flush()
|
||
|
||
# 创建助手消息(pending 状态)
|
||
assistant_msg = AgentMessage(
|
||
conversation_id=conversation.id,
|
||
role='assistant',
|
||
content='',
|
||
status='pending',
|
||
)
|
||
self._db.add(assistant_msg)
|
||
await self._db.flush()
|
||
|
||
yield {
|
||
'type': 'start',
|
||
'conversation_id': str(conversation.id),
|
||
'message_id': str(assistant_msg.id),
|
||
}
|
||
|
||
try:
|
||
# 构建系统提示词
|
||
system_prompt = self._build_system_prompt_simple(agent)
|
||
|
||
# 标注直接回复检查(高相似度时跳过 LLM,直接返回标注答案)
|
||
annotation_reply = await self._check_annotation_direct_reply(agent, user_message)
|
||
if annotation_reply:
|
||
final_answer = annotation_reply['answer']
|
||
elapsed_time = int((time.time() - start_time) * 1000)
|
||
assistant_msg.content = final_answer
|
||
assistant_msg.status = 'completed'
|
||
assistant_msg.elapsed_time = elapsed_time
|
||
|
||
yield {
|
||
'type': 'annotation_reply',
|
||
'content': f"标注直接回复 (相似度: {annotation_reply['score']:.2f})",
|
||
}
|
||
yield {
|
||
'type': 'answer',
|
||
'content': final_answer,
|
||
}
|
||
|
||
conversation.message_count = (conversation.message_count or 0) + 2
|
||
if conversation.message_count == 2 and (not conversation.title or conversation.title == "新建对话"):
|
||
self._generate_title(conversation, user_message)
|
||
agent.message_count = (agent.message_count or 0) + 2
|
||
await self._db.commit()
|
||
|
||
yield {
|
||
'type': 'complete',
|
||
'message_id': str(assistant_msg.id),
|
||
'conversation_id': str(conversation.id),
|
||
'tokens_used': 0,
|
||
'elapsed_time': elapsed_time,
|
||
}
|
||
return
|
||
|
||
# 知识库检索增强(RAG)
|
||
knowledge_context = await self._retrieve_knowledge_context(agent, user_message)
|
||
if knowledge_context:
|
||
system_prompt = self._inject_knowledge_context(system_prompt, knowledge_context)
|
||
yield {
|
||
'type': 'knowledge_retrieval',
|
||
'content': knowledge_context['summary'],
|
||
'result_count': knowledge_context['result_count'],
|
||
}
|
||
|
||
# 获取对话历史
|
||
history = await self._get_conversation_history(conversation, limit=10)
|
||
|
||
# 构建当前用户消息内容(支持多模态)
|
||
user_content = LLMMessage.create_multimodal_content(user_message, attachments)
|
||
history.append({'role': 'user', 'content': user_content})
|
||
|
||
# 构建消息
|
||
messages = [{'role': 'system', 'content': system_prompt}] + history
|
||
|
||
# 检查是否启用流式输出
|
||
enable_streaming = getattr(agent, 'enable_streaming', True)
|
||
model_id = await self._resolve_agent_model_id(agent)
|
||
if not model_id:
|
||
raise ValueError('未找到可用的 chat 模型,请先在模型配置中启用一个模型')
|
||
|
||
total_tokens = 0
|
||
final_answer = ''
|
||
|
||
if enable_streaming:
|
||
# 流式输出
|
||
accumulated_content = ''
|
||
async for chunk in self.llm_service.chat_stream(
|
||
model_id=model_id,
|
||
messages=messages,
|
||
temperature=agent.temperature or 0.7,
|
||
max_tokens=agent.max_tokens or 2048,
|
||
):
|
||
if chunk.content:
|
||
accumulated_content += chunk.content
|
||
yield {
|
||
'type': 'llm_chunk',
|
||
'content': chunk.content,
|
||
'accumulated_content': accumulated_content,
|
||
}
|
||
|
||
if chunk.is_finished:
|
||
total_tokens = chunk.total_tokens
|
||
final_answer = accumulated_content
|
||
else:
|
||
# 非流式输出
|
||
response = await self.llm_service.chat_async(
|
||
model_id=model_id,
|
||
messages=messages,
|
||
temperature=agent.temperature or 0.7,
|
||
max_tokens=agent.max_tokens or 2048,
|
||
)
|
||
|
||
total_tokens = response.total_tokens
|
||
final_answer = response.content
|
||
|
||
yield {
|
||
'type': 'answer',
|
||
'content': final_answer,
|
||
}
|
||
|
||
# 更新助手消息
|
||
elapsed_time = int((time.time() - start_time) * 1000)
|
||
assistant_msg.content = final_answer
|
||
assistant_msg.status = 'completed'
|
||
assistant_msg.total_tokens = total_tokens
|
||
assistant_msg.elapsed_time = elapsed_time
|
||
|
||
# 更新对话统计
|
||
conversation.message_count = (conversation.message_count or 0) + 2
|
||
conversation.total_tokens = (conversation.total_tokens or 0) + total_tokens
|
||
|
||
# 自动生成标题(第一次对话时,且标题为默认值)
|
||
if conversation.message_count == 2 and (not conversation.title or conversation.title == "新建对话"):
|
||
self._generate_title(conversation, user_message)
|
||
|
||
# 更新智能体统计
|
||
agent.message_count = (agent.message_count or 0) + 2
|
||
agent.total_tokens = (agent.total_tokens or 0) + total_tokens
|
||
|
||
await self._db.commit()
|
||
|
||
yield {
|
||
'type': 'complete',
|
||
'message_id': str(assistant_msg.id),
|
||
'conversation_id': str(conversation.id),
|
||
'tokens_used': total_tokens,
|
||
'elapsed_time': elapsed_time,
|
||
}
|
||
|
||
except Exception as e:
|
||
logger.exception(f'Agent chat error: {e}')
|
||
assistant_msg.status = 'failed'
|
||
assistant_msg.error_message = str(e)
|
||
await self._db.commit()
|
||
yield {
|
||
'type': 'error',
|
||
'content': str(e),
|
||
}
|
||
|
||
async def _chat_autonomous_react(
|
||
self,
|
||
agent: Agent,
|
||
conversation: AgentConversation,
|
||
user_message: str,
|
||
attachments: List[Dict] = None,
|
||
):
|
||
"""
|
||
ReAct 模式(简化版,不使用工具,直接复用 function calling 逻辑)
|
||
|
||
Yields:
|
||
流式事件
|
||
"""
|
||
# 直接复用简化后的 function calling 逻辑
|
||
async for event in self._chat_autonomous_function_calling(agent, conversation, user_message, attachments):
|
||
yield event
|
||
|
||
async def _chat_dialog_flow(
|
||
self,
|
||
agent: Agent,
|
||
conversation: AgentConversation,
|
||
user_message: str,
|
||
application_id: str = None,
|
||
form_code: str = None,
|
||
attachments: List[Dict] = None,
|
||
):
|
||
"""
|
||
对话流模式对话
|
||
|
||
直接复用工作流服务的执行逻辑,透传工作流事件
|
||
|
||
Args:
|
||
agent: 智能体
|
||
conversation: 对话
|
||
user_message: 用户消息
|
||
application_id: 子应用 ID(用于表单创建等场景)
|
||
form_code: 表单编码(从表单列表调用时传入)
|
||
attachments: 附件列表
|
||
|
||
Yields:
|
||
流式事件
|
||
"""
|
||
start_time = time.time()
|
||
|
||
if not self._db:
|
||
yield {'type': 'error', 'content': '数据库会话未初始化'}
|
||
return
|
||
|
||
# 创建用户消息(包含附件)
|
||
# 附件格式: [{file_id, type, name, mime_type, size}]
|
||
stored_attachments = []
|
||
if attachments:
|
||
for att in attachments:
|
||
stored_attachments.append({
|
||
'file_id': att.get('file_id'),
|
||
'type': att.get('type', 'file'),
|
||
'name': att.get('name', ''),
|
||
'mime_type': att.get('mime_type', ''),
|
||
'size': att.get('size', 0),
|
||
})
|
||
|
||
user_msg = AgentMessage(
|
||
conversation_id=conversation.id,
|
||
role='user',
|
||
content=user_message,
|
||
attachments=stored_attachments,
|
||
status='completed',
|
||
)
|
||
self._db.add(user_msg)
|
||
await self._db.flush()
|
||
|
||
# 创建助手消息
|
||
assistant_msg = AgentMessage(
|
||
conversation_id=conversation.id,
|
||
role='assistant',
|
||
content='',
|
||
status='pending',
|
||
)
|
||
self._db.add(assistant_msg)
|
||
await self._db.flush()
|
||
|
||
yield {
|
||
'type': 'start',
|
||
'conversation_id': str(conversation.id),
|
||
'message_id': str(assistant_msg.id),
|
||
}
|
||
|
||
try:
|
||
from ai_platform.services.workflow_service import AIWorkflowService
|
||
|
||
workflow_service = AIWorkflowService(self._db)
|
||
|
||
# 检查是否有进行中的工作流运行
|
||
workflow_run_id = None
|
||
if conversation.extra_data:
|
||
workflow_run_id = conversation.extra_data.get('workflow_run_id')
|
||
|
||
final_content = ''
|
||
is_waiting = False
|
||
total_tokens = 0
|
||
current_run_id = workflow_run_id
|
||
|
||
# 获取对话历史(如果启用了记忆功能)
|
||
conversation_history = []
|
||
if agent.enable_memory:
|
||
conversation_history = await self._get_conversation_history(
|
||
conversation,
|
||
limit=agent.memory_window or 10
|
||
)
|
||
|
||
if workflow_run_id:
|
||
# 恢复工作流执行 - 使用异步方法
|
||
# 将附件内容(文件解析内容或图片OCR)整合到 user_input 中
|
||
enhanced_user_input = user_message
|
||
if attachments:
|
||
attachment_texts = []
|
||
for att in attachments:
|
||
text_content = att.get('text_content', '')
|
||
if text_content:
|
||
file_name = att.get('name', 'unknown')
|
||
attachment_texts.append(f"\n[附件: {file_name}]\n{text_content}")
|
||
|
||
if attachment_texts:
|
||
enhanced_user_input = user_message + '\n' + '\n'.join(attachment_texts)
|
||
|
||
async for event in workflow_service.resume_workflow_stream_async(
|
||
run_id=workflow_run_id,
|
||
user_input=enhanced_user_input,
|
||
conversation_history=conversation_history,
|
||
):
|
||
event_type = event.get('type')
|
||
|
||
if event_type == 'answer':
|
||
final_content = event.get('content', '')
|
||
elif event_type == 'llm_chunk':
|
||
accumulated = event.get('accumulated_content', '')
|
||
if accumulated:
|
||
final_content = accumulated
|
||
elif event_type == 'node_complete':
|
||
output = (event.get('outputs') or {}).get('output', '')
|
||
if output:
|
||
final_content = output
|
||
elif event_type == 'waiting_input':
|
||
is_waiting = True
|
||
elif event_type == 'complete':
|
||
total_tokens = event.get('total_tokens', 0)
|
||
output = (event.get('outputs') or {}).get('output', '')
|
||
if output:
|
||
final_content = output
|
||
|
||
yield event
|
||
else:
|
||
# 新对话,启动工作流 - 使用异步方法
|
||
if not agent.workflow_id:
|
||
yield {'type': 'error', 'content': '智能体未配置工作流'}
|
||
return
|
||
|
||
# 构建工作流输入变量
|
||
# 将附件内容(文件解析内容或图片OCR)整合到 user_input 中
|
||
enhanced_user_input = user_message
|
||
if attachments:
|
||
attachment_texts = []
|
||
for att in attachments:
|
||
text_content = att.get('text_content', '')
|
||
if text_content:
|
||
file_name = att.get('name', 'unknown')
|
||
attachment_texts.append(f"\n[附件: {file_name}]\n{text_content}")
|
||
|
||
if attachment_texts:
|
||
enhanced_user_input = user_message + '\n' + '\n'.join(attachment_texts)
|
||
|
||
workflow_inputs = await self._build_workflow_inputs(agent, enhanced_user_input)
|
||
if application_id:
|
||
workflow_inputs['application_id'] = application_id
|
||
if form_code:
|
||
workflow_inputs['form_code'] = form_code
|
||
|
||
# 调试日志
|
||
import logging
|
||
logger = logging.getLogger(__name__)
|
||
logger.info(f"[AgentService] form_code parameter: {form_code}")
|
||
logger.info(f"[AgentService] workflow_inputs: {workflow_inputs}")
|
||
|
||
async for event in workflow_service.run_workflow_stream_async(
|
||
workflow_id=str(agent.workflow_id),
|
||
inputs=workflow_inputs,
|
||
conversation_id=str(conversation.id),
|
||
conversation_history=conversation_history,
|
||
trigger_type='agent',
|
||
):
|
||
event_type = event.get('type')
|
||
|
||
if event_type == 'start':
|
||
current_run_id = event.get('run_id')
|
||
elif event_type == 'answer':
|
||
final_content = event.get('content', '')
|
||
elif event_type == 'llm_chunk':
|
||
accumulated = event.get('accumulated_content', '')
|
||
if accumulated:
|
||
final_content = accumulated
|
||
elif event_type == 'node_complete':
|
||
output = (event.get('outputs') or {}).get('output', '')
|
||
if output:
|
||
final_content = output
|
||
elif event_type == 'waiting_input':
|
||
is_waiting = True
|
||
# 保存工作流运行 ID 到对话元数据
|
||
if current_run_id:
|
||
conversation.extra_data = conversation.extra_data or {}
|
||
conversation.extra_data['workflow_run_id'] = current_run_id
|
||
elif event_type == 'complete':
|
||
total_tokens = event.get('total_tokens', 0)
|
||
output = (event.get('outputs') or {}).get('output', '')
|
||
if output:
|
||
final_content = output
|
||
|
||
yield event
|
||
|
||
# 更新助手消息
|
||
elapsed_time = int((time.time() - start_time) * 1000)
|
||
|
||
if final_content:
|
||
assistant_msg.content = final_content
|
||
|
||
assistant_msg.status = 'completed'
|
||
assistant_msg.elapsed_time = elapsed_time
|
||
assistant_msg.total_tokens = total_tokens
|
||
|
||
# 更新对话统计
|
||
conversation.message_count = (conversation.message_count or 0) + 2
|
||
conversation.total_tokens = (conversation.total_tokens or 0) + total_tokens
|
||
|
||
# 自动生成标题(第一次对话时,且标题为默认值)
|
||
if conversation.message_count == 2 and (not conversation.title or conversation.title == "新建对话"):
|
||
self._generate_title(conversation, user_message)
|
||
|
||
# 如果工作流完成(非等待状态),清除对话元数据
|
||
if not is_waiting:
|
||
if conversation.extra_data:
|
||
conversation.extra_data.pop('workflow_run_id', None)
|
||
|
||
await self._db.commit()
|
||
|
||
yield {
|
||
'type': 'complete',
|
||
'elapsed_time': elapsed_time,
|
||
'tokens_used': total_tokens,
|
||
}
|
||
|
||
except Exception as e:
|
||
logger.exception(f'Dialog flow error: {e}')
|
||
assistant_msg.status = 'failed'
|
||
assistant_msg.error_message = str(e)
|
||
await self._db.commit()
|
||
yield {
|
||
'type': 'error',
|
||
'content': str(e),
|
||
}
|
||
|
||
async def _build_workflow_inputs(self, agent: Agent, user_input: str) -> Dict[str, Any]:
|
||
workflow_inputs: Dict[str, Any] = {'user_input': user_input}
|
||
if not self._db or not agent.workflow_id:
|
||
return workflow_inputs
|
||
|
||
result = await self._db.execute(
|
||
select(AIWorkflow).where(
|
||
AIWorkflow.id == agent.workflow_id,
|
||
AIWorkflow.is_deleted == False,
|
||
)
|
||
)
|
||
workflow = result.scalar_one_or_none()
|
||
if not workflow:
|
||
return workflow_inputs
|
||
|
||
input_variables = workflow.input_variables or []
|
||
if any((item or {}).get('name') == 'request' for item in input_variables):
|
||
workflow_inputs.setdefault('request', user_input)
|
||
if any((item or {}).get('name') == 'issue_id' for item in input_variables):
|
||
workflow_inputs.setdefault('issue_id', self._extract_issue_id(user_input))
|
||
return workflow_inputs
|
||
|
||
@staticmethod
|
||
def _extract_issue_id(text: str) -> str:
|
||
import re
|
||
|
||
match = re.search(r'\b[A-Z][A-Z0-9]+-\d+\b', text or '', re.IGNORECASE)
|
||
return match.group(0).upper() if match else 'MANUAL-1'
|
||
|
||
def _build_system_prompt_simple(self, agent: Agent) -> str:
|
||
"""构建简化的系统提示词"""
|
||
# 基础提示词
|
||
base_prompt = agent.system_prompt
|
||
|
||
# 如果没有系统提示词,从人设生成
|
||
if not base_prompt and agent.persona:
|
||
base_prompt = self._generate_prompt_from_persona(agent.persona)
|
||
|
||
if not base_prompt:
|
||
base_prompt = "你是一个智能助手,可以帮助用户完成各种任务。回答要简洁明了,使用中文。"
|
||
|
||
return base_prompt
|
||
|
||
def _generate_prompt_from_persona(self, persona: Dict[str, Any]) -> str:
|
||
"""从人设配置生成提示词"""
|
||
parts = []
|
||
|
||
if persona.get('role'):
|
||
parts.append(persona['role'])
|
||
|
||
if persona.get('personality'):
|
||
personalities = persona['personality']
|
||
if isinstance(personalities, list):
|
||
parts.append(f"你的性格特点是:{', '.join(personalities)}。")
|
||
|
||
if persona.get('skills'):
|
||
skills = persona['skills']
|
||
if isinstance(skills, list):
|
||
parts.append(f"你擅长:{', '.join(skills)}。")
|
||
|
||
if persona.get('background'):
|
||
parts.append(persona['background'])
|
||
|
||
if persona.get('constraints'):
|
||
constraints = persona['constraints']
|
||
if isinstance(constraints, list):
|
||
parts.append("注意事项:\n" + "\n".join(f"- {c}" for c in constraints))
|
||
|
||
return "\n\n".join(parts)
|
||
|
||
async def _get_conversation_history(
|
||
self,
|
||
conversation: AgentConversation,
|
||
limit: int = 10,
|
||
) -> List[Dict[str, str]]:
|
||
"""获取对话历史"""
|
||
if not self._db:
|
||
return []
|
||
|
||
result = await self._db.execute(
|
||
select(AgentMessage).where(
|
||
AgentMessage.conversation_id == conversation.id,
|
||
AgentMessage.is_deleted == False,
|
||
AgentMessage.role.in_(['user', 'assistant']),
|
||
AgentMessage.status == 'completed'
|
||
).order_by(AgentMessage.sys_create_datetime.desc()).limit(limit)
|
||
)
|
||
messages = result.scalars().all()
|
||
|
||
history = []
|
||
for msg in reversed(list(messages)):
|
||
history.append({
|
||
'role': msg.role,
|
||
'content': msg.content,
|
||
})
|
||
|
||
return history
|
||
|
||
def _generate_title(self, conversation, first_message: str):
|
||
"""自动生成对话标题"""
|
||
# 简单截取前 20 个字符作为标题
|
||
title = first_message[:20]
|
||
if len(first_message) > 20:
|
||
title += '...'
|
||
conversation.title = title
|
||
|
||
async def _check_annotation_direct_reply(
|
||
self,
|
||
agent: Agent,
|
||
query: str,
|
||
) -> Optional[Dict[str, Any]]:
|
||
"""
|
||
检查标注直接回复(Dify 风格)
|
||
|
||
当启用标注回复且匹配到高相似度标注时,直接返回标注答案,跳过 LLM 调用。
|
||
|
||
Returns:
|
||
{'answer': str, 'score': float, 'question': str} 或 None
|
||
"""
|
||
knowledge_base_ids = getattr(agent, 'knowledge_base_ids', None) or []
|
||
if not knowledge_base_ids or not self._db:
|
||
return None
|
||
|
||
knowledge_config = getattr(agent, 'knowledge_config', None) or {}
|
||
annotation_reply_enabled = knowledge_config.get('annotation_reply_enabled', False)
|
||
if not annotation_reply_enabled:
|
||
return None
|
||
|
||
annotation_threshold = knowledge_config.get('annotation_threshold', 0.9)
|
||
|
||
try:
|
||
from ai_platform.knowledge.services.retrieval_service import RetrievalService
|
||
from ai_platform.knowledge.models import KnowledgeBase
|
||
from sqlalchemy import select
|
||
|
||
# 获取第一个知识库的 embedding 配置
|
||
kb_result = await self._db.execute(
|
||
select(KnowledgeBase).where(
|
||
KnowledgeBase.id.in_(knowledge_base_ids),
|
||
KnowledgeBase.is_deleted == False,
|
||
)
|
||
)
|
||
first_kb = kb_result.scalars().first()
|
||
if not first_kb or not first_kb.embedding_model_id:
|
||
return None
|
||
|
||
service = RetrievalService(self._db)
|
||
annotation_results = await service._match_annotations(
|
||
query=query,
|
||
knowledge_base_ids=knowledge_base_ids,
|
||
embedding_model_id=first_kb.embedding_model_id,
|
||
score_threshold=annotation_threshold,
|
||
dimensions=first_kb.embedding_dimensions,
|
||
max_results=1,
|
||
)
|
||
|
||
if annotation_results:
|
||
best = annotation_results[0]
|
||
metadata = getattr(best, 'metadata', {}) or {}
|
||
logger.info(
|
||
f'标注直接回复命中: score={best.score}, '
|
||
f'question={metadata.get("question", "")}, answer={best.content[:50]}'
|
||
)
|
||
|
||
# 记录检索日志
|
||
try:
|
||
from ai_platform.knowledge.models import KnowledgeRetrievalLog
|
||
log = KnowledgeRetrievalLog(
|
||
query=query,
|
||
knowledge_base_ids=knowledge_base_ids,
|
||
retrieval_mode='annotation',
|
||
top_k=1,
|
||
score_threshold=annotation_threshold,
|
||
result_count=1,
|
||
results=[{'segment_id': best.segment_id, 'score': best.score, 'kb_id': best.knowledge_base_id}],
|
||
rerank_applied='false',
|
||
elapsed_time=0,
|
||
source='agent_annotation',
|
||
)
|
||
self._db.add(log)
|
||
await self._db.flush()
|
||
except Exception as e:
|
||
logger.warning(f'记录标注回复日志失败: {e}')
|
||
|
||
return {
|
||
'answer': best.content,
|
||
'score': best.score,
|
||
'question': metadata.get('question', ''),
|
||
}
|
||
|
||
except Exception as e:
|
||
logger.warning(f'标注直接回复检查失败: {e}')
|
||
|
||
return None
|
||
|
||
async def _retrieve_knowledge_context(
|
||
self,
|
||
agent: Agent,
|
||
query: str,
|
||
) -> Optional[Dict[str, Any]]:
|
||
"""
|
||
检索知识库上下文(RAG)
|
||
|
||
Args:
|
||
agent: 智能体(包含 knowledge_base_ids 和 knowledge_config)
|
||
query: 用户查询文本
|
||
|
||
Returns:
|
||
知识库上下文字典,无结果时返回 None
|
||
"""
|
||
knowledge_base_ids = getattr(agent, 'knowledge_base_ids', None) or []
|
||
if not knowledge_base_ids or not self._db:
|
||
logger.info(f'RAG 跳过: knowledge_base_ids={knowledge_base_ids}, db={bool(self._db)}')
|
||
return None
|
||
|
||
knowledge_config = getattr(agent, 'knowledge_config', None) or {}
|
||
logger.info(f'RAG 开始: kb_ids={knowledge_base_ids}, config={knowledge_config}, query={query[:100]}')
|
||
top_k = knowledge_config.get('top_k', 5)
|
||
score_threshold = knowledge_config.get('score_threshold', 0.5)
|
||
retrieval_mode = knowledge_config.get('retrieval_mode', None)
|
||
rerank_enabled = knowledge_config.get('rerank_enabled', None)
|
||
rerank_model_id = knowledge_config.get('rerank_model_id', None)
|
||
|
||
try:
|
||
from ai_platform.knowledge.services.retrieval_service import RetrievalService
|
||
|
||
start_time = time.time()
|
||
service = RetrievalService(self._db)
|
||
results = await service.retrieve(
|
||
query=query,
|
||
knowledge_base_ids=knowledge_base_ids,
|
||
top_k=top_k,
|
||
score_threshold=score_threshold,
|
||
retrieval_mode=retrieval_mode,
|
||
rerank_enabled=rerank_enabled,
|
||
rerank_model_id=rerank_model_id,
|
||
)
|
||
|
||
elapsed = int((time.time() - start_time) * 1000)
|
||
|
||
logger.info(f'RAG 检索结果: {len(results)} 条')
|
||
for r in results:
|
||
logger.info(f' - segment_id={r.segment_id}, score={r.score}, doc={getattr(r, "document_name", "")}, type={getattr(r, "metadata", {}).get("type", "segment")}')
|
||
|
||
# 记录检索日志(与召回测试共用同一张日志表)
|
||
try:
|
||
from ai_platform.knowledge.models import KnowledgeRetrievalLog
|
||
log = KnowledgeRetrievalLog(
|
||
query=query,
|
||
knowledge_base_ids=knowledge_base_ids,
|
||
retrieval_mode=retrieval_mode or 'hybrid',
|
||
top_k=top_k,
|
||
score_threshold=score_threshold,
|
||
result_count=len(results),
|
||
results=[
|
||
{'segment_id': r.segment_id, 'score': r.score, 'kb_id': r.knowledge_base_id}
|
||
for r in results
|
||
],
|
||
rerank_applied='true' if rerank_enabled else 'false',
|
||
elapsed_time=elapsed,
|
||
source='agent',
|
||
)
|
||
self._db.add(log)
|
||
await self._db.flush()
|
||
except Exception as e:
|
||
logger.warning(f'记录 Agent 检索日志失败: {e}')
|
||
|
||
if not results:
|
||
return None
|
||
|
||
# 构建上下文文本
|
||
context_parts = []
|
||
for i, r in enumerate(results, 1):
|
||
score = getattr(r, 'score', 0)
|
||
content = getattr(r, 'content', '')
|
||
metadata = getattr(r, 'metadata', {}) or {}
|
||
|
||
if metadata.get('type') == 'annotation':
|
||
# 标注类型:显示为 Q&A 对,让 LLM 明确知道这是预设的标准答案
|
||
question = metadata.get('question', '')
|
||
context_parts.append(
|
||
f"[{i}] [标准问答] (相似度: {score:.2f})\n"
|
||
f"问题: {question}\n"
|
||
f"标准答案: {content}"
|
||
)
|
||
else:
|
||
source = getattr(r, 'document_name', '') or ''
|
||
context_parts.append(
|
||
f"[{i}] (来源: {source}, 相似度: {score:.2f})\n{content}"
|
||
)
|
||
|
||
context_text = '\n\n'.join(context_parts)
|
||
summary = f"检索到 {len(results)} 条相关结果"
|
||
|
||
return {
|
||
'context': context_text,
|
||
'result_count': len(results),
|
||
'summary': summary,
|
||
'results': [
|
||
{
|
||
'segment_id': getattr(r, 'segment_id', ''),
|
||
'document_name': getattr(r, 'document_name', ''),
|
||
'score': getattr(r, 'score', 0),
|
||
}
|
||
for r in results
|
||
],
|
||
}
|
||
|
||
except Exception as e:
|
||
logger.warning(f'知识库检索失败: {e}')
|
||
return None
|
||
|
||
@staticmethod
|
||
def _inject_knowledge_context(
|
||
system_prompt: str,
|
||
knowledge_context: Dict[str, Any],
|
||
) -> str:
|
||
"""
|
||
将知识库检索结果注入系统提示词
|
||
|
||
参考 Dify 的 RAG 注入方式:在 system prompt 末尾追加参考资料段落,
|
||
并指示 LLM 基于资料回答,资料中无相关信息时如实告知。
|
||
"""
|
||
context_text = knowledge_context.get('context', '')
|
||
if not context_text:
|
||
return system_prompt
|
||
|
||
rag_instruction = (
|
||
"\n\n"
|
||
"# 参考资料\n"
|
||
"以下是从知识库中检索到的与用户问题相关的参考资料。\n"
|
||
"- 标记为[标准问答]的条目是预设的权威问答对,当用户问题与其匹配时,请直接使用标准答案回答,不要自行发挥。\n"
|
||
"- 其他条目为文档参考内容,请基于这些资料回答用户的问题。\n"
|
||
"- 如果参考资料中没有相关信息,请基于你自身的知识回答,并说明该回答未基于知识库。\n\n"
|
||
f"{context_text}"
|
||
)
|
||
|
||
return system_prompt + rag_instruction
|