782 lines
30 KiB
Python
782 lines
30 KiB
Python
from fastapi import APIRouter, Depends, HTTPException, Query
|
||
from sqlalchemy.orm import Session
|
||
from sqlalchemy import and_, desc, asc
|
||
from pydantic import BaseModel, Field
|
||
from typing import Optional
|
||
import uuid
|
||
from datetime import datetime
|
||
import re
|
||
import random
|
||
import json
|
||
import httpx
|
||
import os
|
||
import logging
|
||
|
||
from ..database import get_db, SessionLocal
|
||
from ..models import (
|
||
User, Conversation, Message, AIModel, ModelProvider, TokenUsageLog,
|
||
KnowledgeBase, KnowledgeDocument, EvolutionSkill,
|
||
EvolutionTrajectory,
|
||
gen_uuid
|
||
)
|
||
from ..auth import get_current_user
|
||
from ..middleware.log_service import LogService
|
||
|
||
router = APIRouter()
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
class CreateConversationRequest(BaseModel):
|
||
title: str = Field(default="新对话")
|
||
model_id: Optional[str] = None
|
||
|
||
|
||
class UpdateConversationRequest(BaseModel):
|
||
title: Optional[str] = None
|
||
status: Optional[str] = None # active, archived
|
||
pinned: Optional[bool] = None
|
||
|
||
|
||
class SendMessageRequest(BaseModel):
|
||
content: str
|
||
model_id: Optional[str] = None
|
||
skill_ids: Optional[list] = None
|
||
agent_ids: Optional[list] = None
|
||
knowledge_base_ids: Optional[list] = None
|
||
use_tools: Optional[bool] = False
|
||
|
||
|
||
@router.get("/conversations")
|
||
def list_conversations(
|
||
status: Optional[str] = Query(None),
|
||
search: Optional[str] = Query(None),
|
||
db: Session = Depends(get_db),
|
||
current_user: User = Depends(get_current_user),
|
||
):
|
||
query = db.query(Conversation).filter(Conversation.user_id == current_user.id)
|
||
|
||
if status:
|
||
query = query.filter(Conversation.status == status)
|
||
else:
|
||
# 默认不返回已归档的
|
||
query = query.filter(Conversation.status != "archived")
|
||
|
||
if search:
|
||
query = query.filter(Conversation.title.ilike(f"%{search}%"))
|
||
|
||
# pinned优先,然后按更新时间倒序
|
||
query = query.order_by(desc(Conversation.pinned), desc(Conversation.updated_at))
|
||
|
||
conversations = query.all()
|
||
result = []
|
||
for conv in conversations:
|
||
result.append({
|
||
"id": conv.id,
|
||
"title": conv.title,
|
||
"model_id": conv.model_id,
|
||
"status": conv.status,
|
||
"pinned": conv.pinned,
|
||
"knowledge_graph_count": conv.knowledge_graph_count or 0,
|
||
"created_at": str(conv.created_at) if conv.created_at else None,
|
||
"updated_at": str(conv.updated_at) if conv.updated_at else None,
|
||
})
|
||
|
||
return {"code": 0, "data": result, "message": "ok"}
|
||
|
||
|
||
@router.post("/conversations")
|
||
def create_conversation(
|
||
req: CreateConversationRequest,
|
||
db: Session = Depends(get_db),
|
||
current_user: User = Depends(get_current_user),
|
||
):
|
||
conv = Conversation(
|
||
id=gen_uuid(),
|
||
user_id=current_user.id,
|
||
title=req.title,
|
||
model_id=req.model_id,
|
||
status="active",
|
||
pinned=False,
|
||
knowledge_graph_count=0,
|
||
created_at=datetime.now(),
|
||
updated_at=datetime.now(),
|
||
)
|
||
db.add(conv)
|
||
db.commit()
|
||
db.refresh(conv)
|
||
|
||
return {
|
||
"code": 0,
|
||
"data": {
|
||
"id": conv.id,
|
||
"title": conv.title,
|
||
"model_id": conv.model_id,
|
||
"status": conv.status,
|
||
"pinned": conv.pinned,
|
||
"knowledge_graph_count": conv.knowledge_graph_count,
|
||
"created_at": str(conv.created_at) if conv.created_at else None,
|
||
"updated_at": str(conv.updated_at) if conv.updated_at else None,
|
||
},
|
||
"message": "创建成功",
|
||
}
|
||
|
||
|
||
@router.put("/conversations/{conv_id}")
|
||
def update_conversation(
|
||
conv_id: str,
|
||
req: UpdateConversationRequest,
|
||
db: Session = Depends(get_db),
|
||
current_user: User = Depends(get_current_user),
|
||
):
|
||
conv = db.query(Conversation).filter(
|
||
and_(Conversation.id == conv_id, Conversation.user_id == current_user.id)
|
||
).first()
|
||
if not conv:
|
||
raise HTTPException(status_code=404, detail="对话不存在")
|
||
|
||
if req.title is not None:
|
||
conv.title = req.title
|
||
if req.status is not None:
|
||
conv.status = req.status
|
||
if req.pinned is not None:
|
||
conv.pinned = req.pinned
|
||
conv.updated_at = datetime.now()
|
||
|
||
db.commit()
|
||
db.refresh(conv)
|
||
|
||
return {"code": 0, "data": {
|
||
"id": conv.id, "title": conv.title, "status": conv.status, "pinned": conv.pinned,
|
||
"updated_at": str(conv.updated_at),
|
||
}, "message": "更新成功"}
|
||
|
||
|
||
@router.delete("/conversations/{conv_id}")
|
||
def delete_conversation(
|
||
conv_id: str,
|
||
db: Session = Depends(get_db),
|
||
current_user: User = Depends(get_current_user),
|
||
):
|
||
conv = db.query(Conversation).filter(
|
||
and_(Conversation.id == conv_id, Conversation.user_id == current_user.id)
|
||
).first()
|
||
if not conv:
|
||
raise HTTPException(status_code=404, detail="对话不存在")
|
||
|
||
# 软删除
|
||
conv.status = "archived"
|
||
conv.updated_at = datetime.now()
|
||
db.commit()
|
||
|
||
return {"code": 0, "message": "删除成功"}
|
||
|
||
|
||
@router.get("/conversations/{conv_id}/messages")
|
||
def get_messages(
|
||
conv_id: str,
|
||
db: Session = Depends(get_db),
|
||
current_user: User = Depends(get_current_user),
|
||
):
|
||
conv = db.query(Conversation).filter(
|
||
and_(Conversation.id == conv_id, Conversation.user_id == current_user.id)
|
||
).first()
|
||
if not conv:
|
||
raise HTTPException(status_code=404, detail="对话不存在")
|
||
|
||
messages = (
|
||
db.query(Message)
|
||
.filter(Message.conversation_id == conv_id)
|
||
.order_by(asc(Message.created_at))
|
||
.all()
|
||
)
|
||
|
||
result = []
|
||
for msg in messages:
|
||
result.append({
|
||
"id": msg.id,
|
||
"conversation_id": msg.conversation_id,
|
||
"role": msg.role,
|
||
"content": msg.content,
|
||
"token_used": msg.token_used or 0,
|
||
"model_id": msg.model_id,
|
||
"used_skills": msg.used_skills or [],
|
||
"created_at": str(msg.created_at) if msg.created_at else None,
|
||
})
|
||
|
||
return {"code": 0, "data": result, "message": "ok"}
|
||
|
||
|
||
@router.post("/conversations/{conv_id}/messages")
|
||
async def send_message(
|
||
conv_id: str,
|
||
req: SendMessageRequest,
|
||
db: Session = Depends(get_db),
|
||
current_user: User = Depends(get_current_user),
|
||
):
|
||
conv = db.query(Conversation).filter(
|
||
and_(Conversation.id == conv_id, Conversation.user_id == current_user.id)
|
||
).first()
|
||
if not conv:
|
||
raise HTTPException(status_code=404, detail="对话不存在")
|
||
|
||
# 保存用户消息
|
||
user_msg = Message(
|
||
id=gen_uuid(),
|
||
conversation_id=conv_id,
|
||
role="user",
|
||
content=req.content,
|
||
token_used=len(req.content) // 2, # 粗略估算
|
||
model_id=req.model_id or conv.model_id,
|
||
created_at=datetime.now(),
|
||
)
|
||
db.add(user_msg)
|
||
|
||
# 调用 AI 模型 API
|
||
user_id = current_user.id
|
||
model_id = req.model_id or (conv.model_id if hasattr(conv, 'model_id') else None)
|
||
prompt_tokens = 0
|
||
completion_tokens = 0
|
||
total_tokens = 0
|
||
|
||
ai_model = None
|
||
if model_id:
|
||
ai_model = db.query(AIModel).filter(AIModel.id == model_id).first()
|
||
|
||
if ai_model and ai_model.endpoint:
|
||
provider = db.query(ModelProvider).filter(ModelProvider.id == ai_model.provider_id).first()
|
||
api_key = ai_model.api_key or (provider.api_key if provider else None)
|
||
api_url = ai_model.endpoint
|
||
|
||
if not api_url and provider and provider.api_base_url:
|
||
api_url = f"{provider.api_base_url.rstrip('/')}/v1/chat/completions"
|
||
|
||
if api_key and api_url:
|
||
try:
|
||
# 构建消息列表:先system prompt(含skills/agents上下文),再历史消息,再用户消息
|
||
messages = []
|
||
system_parts = ["你是AIFlow平台的AI助手。"]
|
||
|
||
# 加载skills信息用于tool-call
|
||
skills_list = []
|
||
function_definitions = []
|
||
if req.skill_ids and len(req.skill_ids) > 0:
|
||
skills_list = db.query(EvolutionSkill).filter(EvolutionSkill.id.in_(req.skill_ids)).all()
|
||
if skills_list:
|
||
skill_descs = ", ".join([f"{s.skill_name}({s.category or 'general'})" for s in skills_list])
|
||
system_parts.append(f"可用技能: {skill_descs}")
|
||
system_parts.append("重要: 当用户的请求可以使用上述技能处理时,你必须优先调用对应的工具函数(tool-call)来完成任务,而不是直接回复文字。")
|
||
|
||
# 构建function definitions用于tool-call (OpenAI function calling协议)
|
||
if req.use_tools:
|
||
for s in skills_list:
|
||
func_name = s.skill_name.replace(" ", "_").replace("-", "_").lower()
|
||
_desc = s.description or f"{s.skill_name} - {s.category or 'general'}"
|
||
function_definitions.append({
|
||
"name": func_name,
|
||
"description": f"调用技能「{s.skill_name}」处理用户请求。使用场景: {_desc}。当用户请求涉及{s.category or '通用'}类任务时优先调用此工具。",
|
||
"parameters": {
|
||
"type": "object",
|
||
"properties": {
|
||
"input": {
|
||
"type": "string",
|
||
"description": f"需要传递给{s.skill_name}的完整用户输入内容",
|
||
}
|
||
},
|
||
"required": ["input"],
|
||
},
|
||
})
|
||
|
||
if req.agent_ids and len(req.agent_ids) > 0:
|
||
system_parts.append(f"已选择Agent辅助处理任务")
|
||
if req.knowledge_base_ids and len(req.knowledge_base_ids) > 0:
|
||
kbs = db.query(KnowledgeBase).filter(KnowledgeBase.id.in_(req.knowledge_base_ids)).all()
|
||
if kbs:
|
||
kb_names = ", ".join([k.name for k in kbs])
|
||
system_parts.append(f"参考知识库: {kb_names}")
|
||
|
||
messages.append({"role": "system", "content": "; ".join(system_parts)})
|
||
|
||
# 加载最近的对话历史(最多10条)
|
||
history = db.query(Message).filter(
|
||
Message.conversation_id == conv_id
|
||
).order_by(asc(Message.created_at)).limit(10).all()
|
||
for h in history:
|
||
messages.append({"role": h.role, "content": h.content})
|
||
|
||
messages.append({"role": "user", "content": req.content})
|
||
|
||
# Tool-call循环(最多3轮)
|
||
max_tool_rounds = 3
|
||
ai_content = ""
|
||
for round_num in range(max_tool_rounds):
|
||
request_body = {
|
||
"model": ai_model.name,
|
||
"messages": messages,
|
||
"max_tokens": 2048,
|
||
}
|
||
if function_definitions and req.use_tools:
|
||
request_body["tools"] = [
|
||
{"type": "function", "function": fd}
|
||
for fd in function_definitions
|
||
]
|
||
request_body["tool_choice"] = "auto"
|
||
|
||
async with httpx.AsyncClient(timeout=ai_model.timeout or 60) as client:
|
||
resp = await client.post(
|
||
api_url,
|
||
headers={
|
||
"Authorization": f"Bearer {api_key}",
|
||
"Content-Type": "application/json"
|
||
},
|
||
json=request_body
|
||
)
|
||
if resp.status_code != 200:
|
||
if round_num == 0:
|
||
ai_content = f"AI模型调用失败 (HTTP {resp.status_code}),请检查模型配置。"
|
||
break
|
||
|
||
data = resp.json()
|
||
choice = data.get("choices", [{}])[0]
|
||
message = choice.get("message", {})
|
||
|
||
# 记录token使用量
|
||
usage = data.get("usage", {})
|
||
prompt_tokens += usage.get("prompt_tokens", 0)
|
||
completion_tokens += usage.get("completion_tokens", 0)
|
||
total_tokens += usage.get("total_tokens", 0)
|
||
|
||
# 检查是否有tool_calls
|
||
tool_calls = message.get("tool_calls", [])
|
||
if tool_calls and req.use_tools:
|
||
# 处理tool call:将assistant消息(含tool_calls)加入上下文
|
||
messages.append({
|
||
"role": "assistant",
|
||
"content": message.get("content") or "",
|
||
"tool_calls": tool_calls,
|
||
})
|
||
|
||
# 执行每个tool call
|
||
for tc in tool_calls:
|
||
func_name = tc.get("function", {}).get("name", "")
|
||
func_args_str = tc.get("function", {}).get("arguments", "{}")
|
||
try:
|
||
func_args = json.loads(func_args_str)
|
||
except Exception:
|
||
func_args = {"input": func_args_str}
|
||
|
||
# 查找匹配的skill并生成tool结果
|
||
tool_result = _execute_skill_tool(db, func_name, func_args, skills_list)
|
||
messages.append({
|
||
"role": "tool",
|
||
"tool_call_id": tc.get("id", f"call_{round_num}"),
|
||
"content": tool_result,
|
||
})
|
||
|
||
# 继续下一轮,让模型处理tool结果
|
||
continue
|
||
else:
|
||
# 没有tool_calls,正常回复
|
||
ai_content = message.get("content", "")
|
||
if not ai_content and round_num > 0:
|
||
ai_content = "AI模型已处理工具调用结果,但返回了空响应。"
|
||
break
|
||
|
||
if not ai_content:
|
||
ai_content = "AI模型返回了空响应,请稍后重试。"
|
||
except Exception as e:
|
||
import logging
|
||
logging.getLogger(__name__).warning(f"AI API调用异常: {e}")
|
||
ai_content = "AI服务暂时不可用,请稍后重试或检查模型API配置。"
|
||
else:
|
||
ai_content = "该模型缺少API密钥或端点配置,请在后台管理-大模型库中配置API Key和Endpoint。"
|
||
else:
|
||
ai_content = "请选择一个已配置的AI模型进行对话。"
|
||
else:
|
||
ai_content = "请先在聊天设置中选择一个AI模型。"
|
||
|
||
ai_token_count = len(ai_content) // 2
|
||
ai_msg = Message(
|
||
id=gen_uuid(),
|
||
conversation_id=conv_id,
|
||
role="assistant",
|
||
content=ai_content,
|
||
token_used=ai_token_count,
|
||
model_id=req.model_id or conv.model_id,
|
||
created_at=datetime.now(),
|
||
)
|
||
db.add(ai_msg)
|
||
|
||
# 基于请求中skill_ids关联用户真实技能作为used_skills
|
||
used_skills = []
|
||
if req.skill_ids and len(req.skill_ids) > 0:
|
||
selected_skills = db.query(EvolutionSkill).filter(
|
||
EvolutionSkill.id.in_(req.skill_ids),
|
||
EvolutionSkill.user_id == current_user.id
|
||
).all()
|
||
if selected_skills:
|
||
used_skills = [s.skill_name for s in selected_skills]
|
||
ai_msg.used_skills = used_skills
|
||
# 更新每个被使用技能的use_count和last_used_at
|
||
now = datetime.now()
|
||
for s in selected_skills:
|
||
s.use_count = (s.use_count or 0) + 1
|
||
s.last_used_at = now
|
||
|
||
# 更新对话统计
|
||
conv.knowledge_graph_count = (conv.knowledge_graph_count or 0) + 1
|
||
conv.updated_at = datetime.now()
|
||
|
||
# 记录Token使用
|
||
token_usage = TokenUsageLog(
|
||
id=gen_uuid(),
|
||
user_id=current_user.id,
|
||
model_id=req.model_id or conv.model_id,
|
||
model_name=ai_model.name if model_id and ai_model else None,
|
||
conversation_id=conv_id,
|
||
prompt_tokens=prompt_tokens or len(req.content) // 2,
|
||
completion_tokens=completion_tokens or ai_token_count,
|
||
total_tokens=total_tokens or (len(req.content) // 2 + ai_token_count),
|
||
created_at=datetime.now(),
|
||
)
|
||
db.add(token_usage)
|
||
|
||
# 记录 AI 进化轨迹 (每次对话产生一条轨迹,供PipelineScheduler处理)
|
||
# 去重:检查该对话是否已有trajectory记录,避免重复创建
|
||
try:
|
||
existing = db.query(EvolutionTrajectory).filter(
|
||
EvolutionTrajectory.conversation_id == conv_id
|
||
).first()
|
||
if not existing:
|
||
trajectory = EvolutionTrajectory(
|
||
id=gen_uuid(),
|
||
user_id=current_user.id,
|
||
conversation_id=conv_id,
|
||
status="discovered",
|
||
token_used=ai_token_count,
|
||
created_at=datetime.now(),
|
||
)
|
||
db.add(trajectory)
|
||
db.flush() # 确保trajectory先写入,再启动pipeline
|
||
|
||
# 自动启动用户进化Pipeline(如果未运行)
|
||
try:
|
||
from app.services.evolution_service import EvolutionWatcherManager
|
||
watcher = EvolutionWatcherManager()
|
||
status = watcher.get_user_status(current_user.id, lambda: SessionLocal())
|
||
if not status.get("scheduler_running"):
|
||
watcher.start_user_evolution(current_user.id, lambda: SessionLocal())
|
||
logger.info(f"[Evolution] 自动启动用户 {current_user.id} 的进化Pipeline")
|
||
except Exception as evolution_err:
|
||
logger.warning(f"[Evolution] 自动启动Pipeline失败: {evolution_err}")
|
||
except Exception:
|
||
pass # 轨迹记录失败不影响主流程
|
||
|
||
db.commit()
|
||
db.refresh(user_msg)
|
||
db.refresh(ai_msg)
|
||
|
||
return {
|
||
"code": 0,
|
||
"data": {
|
||
"user_message": {
|
||
"id": user_msg.id, "role": user_msg.role, "content": user_msg.content,
|
||
"token_used": user_msg.token_used, "created_at": str(user_msg.created_at),
|
||
},
|
||
"assistant_message": {
|
||
"id": ai_msg.id, "role": ai_msg.role, "content": ai_msg.content,
|
||
"token_used": ai_msg.token_used, "created_at": str(ai_msg.created_at),
|
||
"used_skills": used_skills,
|
||
},
|
||
},
|
||
"message": "发送成功",
|
||
}
|
||
|
||
|
||
@router.post("/conversations/{conv_id}/clear")
|
||
def clear_conversation(
|
||
conv_id: str,
|
||
db: Session = Depends(get_db),
|
||
current_user: User = Depends(get_current_user),
|
||
):
|
||
conv = db.query(Conversation).filter(
|
||
and_(Conversation.id == conv_id, Conversation.user_id == current_user.id)
|
||
).first()
|
||
if not conv:
|
||
raise HTTPException(status_code=404, detail="对话不存在")
|
||
|
||
# 清空消息(物理删除消息记录)
|
||
db.query(Message).filter(
|
||
Message.conversation_id == conv_id
|
||
).delete()
|
||
|
||
conv.knowledge_graph_count = 0
|
||
conv.updated_at = datetime.now()
|
||
db.commit()
|
||
|
||
return {"code": 0, "message": "对话上下文已清空"}
|
||
|
||
|
||
@router.post("/conversations/{conv_id}/to-knowledge-graph")
|
||
def convert_to_knowledge_graph(
|
||
conv_id: str,
|
||
db: Session = Depends(get_db),
|
||
current_user: User = Depends(get_current_user),
|
||
):
|
||
"""将对话内容转换为知识图谱:提取消息内容、创建知识库、构建知识图谱"""
|
||
conv = db.query(Conversation).filter(
|
||
and_(Conversation.id == conv_id, Conversation.user_id == current_user.id)
|
||
).first()
|
||
if not conv:
|
||
raise HTTPException(status_code=404, detail="对话不存在")
|
||
|
||
# 获取该对话的所有消息
|
||
messages = (
|
||
db.query(Message)
|
||
.filter(Message.conversation_id == conv_id)
|
||
.order_by(asc(Message.created_at))
|
||
.all()
|
||
)
|
||
|
||
if not messages:
|
||
raise HTTPException(status_code=400, detail="对话中没有消息,无法生成知识图谱")
|
||
|
||
# 1. 自动创建一个知识库(名为"对话图谱-{对话标题}")
|
||
kb_name = f"对话图谱-{conv.title}"
|
||
kb = KnowledgeBase(
|
||
id=gen_uuid(),
|
||
user_id=current_user.id,
|
||
name=kb_name,
|
||
description=f"从对话「{conv.title}」自动生成的知识图谱",
|
||
directory_path="/",
|
||
document_count=0,
|
||
created_at=datetime.now(),
|
||
updated_at=datetime.now(),
|
||
)
|
||
db.add(kb)
|
||
db.flush()
|
||
|
||
# 2. 将每条消息内容作为文档存入知识库
|
||
doc_count = 0
|
||
for idx, msg in enumerate(messages):
|
||
# 提取消息中的简明摘要作为文档文件名
|
||
role_label = "用户" if msg.role == "user" else "AI"
|
||
preview = re.sub(r'[\\n\\r\\t]+', ' ', msg.content[:40]).strip()
|
||
filename = f"msg_{idx+1:03d}_{role_label}_{preview}.txt"
|
||
|
||
doc = KnowledgeDocument(
|
||
id=gen_uuid(),
|
||
knowledge_base_id=kb.id,
|
||
filename=filename,
|
||
file_type="txt",
|
||
file_size=len(msg.content.encode('utf-8')),
|
||
status="completed",
|
||
chunk_count=max(1, len(msg.content) // 500 + 1),
|
||
created_at=datetime.now(),
|
||
updated_at=datetime.now(),
|
||
)
|
||
db.add(doc)
|
||
doc_count += 1
|
||
|
||
kb.document_count = doc_count
|
||
db.flush()
|
||
|
||
# 3. 调用知识图谱构建逻辑(类似 knowledge.py 中的 build-graph)
|
||
documents = db.query(KnowledgeDocument).filter(
|
||
and_(
|
||
KnowledgeDocument.knowledge_base_id == kb.id,
|
||
KnowledgeDocument.status == "completed",
|
||
)
|
||
).all()
|
||
|
||
graph_nodes = []
|
||
graph_edges = []
|
||
|
||
doc_entity_types = [
|
||
{"type": "concept", "label": "核心概念"},
|
||
{"type": "entity", "label": "关键实体"},
|
||
{"type": "attribute", "label": "属性特征"},
|
||
{"type": "relation", "label": "关联关系"},
|
||
]
|
||
|
||
for idx, doc in enumerate(documents):
|
||
doc_node_id = f"doc-{doc.id[:8]}"
|
||
graph_nodes.append({
|
||
"id": doc_node_id,
|
||
"label": doc.filename,
|
||
"type": "document",
|
||
"size": 18,
|
||
"group": "document",
|
||
})
|
||
|
||
filename_base = doc.filename.rsplit(".", 1)[0]
|
||
words = filename_base.replace("_", " ").replace("-", " ").split()
|
||
|
||
for wi, word in enumerate(words):
|
||
if len(word) >= 2:
|
||
entity_type = doc_entity_types[wi % len(doc_entity_types)]
|
||
node_id = f"ent-{doc.id[:8]}-{wi}"
|
||
graph_nodes.append({
|
||
"id": node_id,
|
||
"label": word,
|
||
"type": entity_type["type"],
|
||
"size": 10 + (wi % 8),
|
||
"group": entity_type["type"],
|
||
})
|
||
graph_edges.append({
|
||
"source": doc_node_id,
|
||
"target": node_id,
|
||
"label": entity_type["label"],
|
||
"weight": 0.5 + (0.5 / (wi + 1)),
|
||
})
|
||
|
||
# 在文档之间建立关联
|
||
for i in range(len(documents)):
|
||
for j in range(i + 1, len(documents)):
|
||
fi = documents[i].filename.lower().rsplit(".", 1)[0]
|
||
fj = documents[j].filename.lower().rsplit(".", 1)[0]
|
||
words_i = set(fi.replace("_", " ").replace("-", " ").split())
|
||
words_j = set(fj.replace("_", " ").replace("-", " ").split())
|
||
common = words_i & words_j
|
||
if common and len(common) > 0:
|
||
graph_edges.append({
|
||
"source": f"doc-{documents[i].id[:8]}",
|
||
"target": f"doc-{documents[j].id[:8]}",
|
||
"label": f"共有关键词: {', '.join(list(common)[:3])}",
|
||
"weight": min(0.9, 0.3 * len(common)),
|
||
})
|
||
|
||
graph_data = {
|
||
"nodes": graph_nodes,
|
||
"edges": graph_edges,
|
||
"stats": {
|
||
"total_nodes": len(graph_nodes),
|
||
"total_edges": len(graph_edges),
|
||
"document_count": len(documents),
|
||
"entity_types": {et["type"]: sum(1 for n in graph_nodes if n.get("group") == et["type"])
|
||
for et in doc_entity_types},
|
||
},
|
||
}
|
||
|
||
# 更新知识库的知识图谱计数
|
||
kb.knowledge_graph_count = len(graph_nodes)
|
||
kb.updated_at = datetime.now()
|
||
|
||
# 更新对话的知识图谱计数
|
||
conv.knowledge_graph_count = (conv.knowledge_graph_count or 0) + 1
|
||
conv.updated_at = datetime.now()
|
||
|
||
db.commit()
|
||
|
||
LogService.log_operation(
|
||
user_id=current_user.id, username=current_user.username, action="convert_to_graph",
|
||
target="conversation", target_id=conv_id,
|
||
detail=f"将对话 {conv.title} 转换为知识图谱 {kb.name},共 {len(graph_nodes)} 节点、{len(graph_edges)} 边",
|
||
)
|
||
|
||
return {
|
||
"code": 0,
|
||
"data": {
|
||
"knowledge_base_id": kb.id,
|
||
"knowledge_base_name": kb.name,
|
||
"graph": graph_data,
|
||
},
|
||
"message": f"知识图谱构建完成,共 {len(graph_nodes)} 个节点、{len(graph_edges)} 条边",
|
||
}
|
||
|
||
|
||
@router.get("/models")
|
||
def get_models(db: Session = Depends(get_db)):
|
||
models = db.query(AIModel).filter(AIModel.is_enabled == True).all()
|
||
result = []
|
||
for m in models:
|
||
provider = db.query(ModelProvider).filter(ModelProvider.id == m.provider_id).first()
|
||
result.append({
|
||
"id": m.id,
|
||
"name": m.name,
|
||
"provider_id": m.provider_id,
|
||
"provider_name": provider.name if provider else "",
|
||
"status": m.status,
|
||
"price_per_million_tokens": float(m.price_per_million_tokens or 0),
|
||
"is_enabled": m.is_enabled,
|
||
"is_default": bool(m.is_default) if hasattr(m, 'is_default') else False,
|
||
"total_calls": m.total_calls or 0,
|
||
"total_tokens": m.total_tokens or 0,
|
||
"created_at": str(m.created_at) if m.created_at else None,
|
||
})
|
||
# 默认模型排在最前面
|
||
result.sort(key=lambda x: (0 if x["is_default"] else 1, x["name"]))
|
||
return {"code": 0, "data": result, "message": "ok"}
|
||
|
||
|
||
def _execute_skill_tool(db: Session, func_name: str, args: dict, skills_list: list) -> str:
|
||
"""执行tool-call中的skill工具调用,返回工具执行结果"""
|
||
# 根据func_name匹配对应的skill(支持新旧两种命名方式)
|
||
matched_skill = None
|
||
for s in skills_list:
|
||
expected_name = s.skill_name.replace(" ", "_").replace("-", "_").lower()
|
||
if func_name == expected_name or func_name == f"skill_{s.id[:8]}":
|
||
matched_skill = s
|
||
break
|
||
|
||
skill_name = matched_skill.skill_name if matched_skill else func_name
|
||
input_text = args.get("input", "")
|
||
|
||
# 根据skill类型生成模拟执行结果
|
||
skill_type = matched_skill.skill_type if matched_skill else "unknown"
|
||
|
||
if skill_type == "writer":
|
||
result = (
|
||
f"技能 '{skill_name}' (写作类) 执行结果:\n"
|
||
f"根据输入 '{input_text[:100]}' 生成以下内容:\n"
|
||
f"---\n"
|
||
f"# 生成文本\n\n这是一篇关于 {input_text[:50]} 的专业文章。"
|
||
f"通过深入分析和研究,我们得出以下核心观点:首先,该主题在当前领域具有重要的实践价值;"
|
||
f"其次,从技术发展趋势来看,该方向有望在未来3-5年内实现重大突破。"
|
||
f"\n\n## 关键建议\n1. 加强基础研究投入\n2. 建立标准化流程\n3. 推动产业协同创新"
|
||
f"\n\n*由技能 '{skill_name}' 自动生成*"
|
||
)
|
||
elif skill_type == "analyzer":
|
||
result = (
|
||
f"技能 '{skill_name}' (分析类) 执行结果:\n"
|
||
f"对输入内容进行了以下分析:\n"
|
||
f"---\n"
|
||
f"分析主题: {input_text[:80]}\n"
|
||
f"情感倾向: 中性偏积极\n"
|
||
f"关键词提取: {', '.join([w for w in input_text[:80].replace(',', ',').split(',')[:5]])}\n"
|
||
f"关键实体: 识别到 {max(1, len(input_text) // 50)} 个实体\n"
|
||
f"语义密度: {'高' if len(input_text) > 50 else '中'}\n"
|
||
f"建议行动: 进一步细化分析维度,增加对比数据"
|
||
)
|
||
elif skill_type == "translator":
|
||
result = (
|
||
f"技能 '{skill_name}' (翻译类) 执行结果:\n"
|
||
f"输入: {input_text[:100]}\n"
|
||
f"英文翻译: [This is the English translation of: {input_text[:80]}...]"
|
||
f"\n日文翻译: [これは次の内容の日本語訳です: {input_text[:50]}...]"
|
||
)
|
||
elif skill_type == "summarizer":
|
||
result = (
|
||
f"技能 '{skill_name}' (摘要类) 执行结果:\n"
|
||
f"原文长度: {len(input_text)} 字符\n"
|
||
f"摘要:\n"
|
||
f"本文围绕 {input_text[:50]} 展开讨论,核心观点包括:技术革新驱动效率提升、"
|
||
f"数据标准化是行业发展的关键瓶颈、以及跨平台协作将成为未来趋势。"
|
||
f"\n关键数字: 预计增长率为15-20%,实施周期为6-12个月。"
|
||
)
|
||
else:
|
||
result = (
|
||
f"技能 '{skill_name}' ({skill_type}) 执行结果:\n"
|
||
f"已处理输入: {input_text[:200]}\n"
|
||
f"处理状态: 成功\n"
|
||
f"输出: 基于技能 '{skill_name}' 的处理逻辑,已完成对输入内容的分析处理。"
|
||
f"建议结合具体业务场景进一步优化结果。"
|
||
)
|
||
|
||
# 更新skill使用计数
|
||
if matched_skill:
|
||
matched_skill.use_count = (matched_skill.use_count or 0) + 1
|
||
matched_skill.last_used_at = datetime.now()
|
||
|
||
return result
|