初始提交: Qihu-AIFlow v2.0.1 完整项目
- 前端 React+TypeScript (源码+构建产物) - 后端 Python FastAPI - 数据库迁移 001-015 - Docker部署配置 - 微信ClawBot API频道绑定 - 项目文档 README.md
This commit is contained in:
commit
78ff261b5a
199 files changed
+69126
No files matched your search
@@ -0,0 +1,781 @@
|
||||
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
|
||||
Reference in new issue
Block a user