feat: add AI学习助手 agent (KG+RAG ideal) and renshenguo feishu bot
- Add AI学习助手 agent creation script with all 39 tools, 3-layer KG+RAG memory - Add renshenguo (人参果) feishu bot integration (app_service + ws_handler) - Register renshenguo WS client in main.py startup - Add RENSHENGUO_APP_ID / RENSHENGUO_APP_SECRET / RENSHENGUO_AGENT_ID config - Reorganize docs from root into docs/ subdirectories - Move startup scripts to scripts/startup/ - Various backend optimizations and tool improvements Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -14,6 +14,7 @@ from app.agent_runtime.schemas import (
|
||||
AgentLLMConfig,
|
||||
AgentToolConfig,
|
||||
AgentBudgetConfig,
|
||||
AgentMemoryConfig,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -83,20 +84,35 @@ async def run_agent_node(
|
||||
if "max_tool_calls" in budget_limits:
|
||||
budget.max_tool_calls = max(1, int(budget_limits["max_tool_calls"]))
|
||||
|
||||
# 构建记忆配置(从 node_data 读取完整字段,兼容简化配置)
|
||||
mem_enabled = bool(node_data.get("memory", True))
|
||||
memory_config = AgentMemoryConfig(
|
||||
enabled=mem_enabled,
|
||||
max_history_messages=int(node_data.get("memory_max_history", 20)),
|
||||
persist_to_db=bool(node_data.get("memory_persist", mem_enabled)),
|
||||
vector_memory_enabled=bool(node_data.get("memory_vector_enabled", True)),
|
||||
vector_memory_top_k=int(node_data.get("memory_vector_top_k", 5)),
|
||||
learning_enabled=bool(node_data.get("memory_learning", True)),
|
||||
)
|
||||
|
||||
# 构建工具审批配置
|
||||
tool_config = AgentToolConfig(
|
||||
include_tools=node_data.get("tools") or [],
|
||||
exclude_tools=node_data.get("exclude_tools") or [],
|
||||
require_approval=node_data.get("require_approval") or [],
|
||||
approval_timeout_ms=int(node_data.get("approval_timeout_ms", 60000)),
|
||||
approval_default=node_data.get("approval_default", "deny"),
|
||||
)
|
||||
|
||||
agent_config = AgentConfig(
|
||||
name=node_data.get("label", "agent_node"),
|
||||
system_prompt=formatted_prompt,
|
||||
llm=llm_config,
|
||||
tools=AgentToolConfig(
|
||||
include_tools=node_data.get("tools", []),
|
||||
exclude_tools=node_data.get("exclude_tools", []),
|
||||
),
|
||||
memory={
|
||||
"enabled": node_data.get("memory", True),
|
||||
"persist_to_db": node_data.get("memory", True),
|
||||
},
|
||||
tools=tool_config,
|
||||
memory=memory_config,
|
||||
budget=budget,
|
||||
user_id=user_id,
|
||||
memory_scope_id=node_data.get("memory_scope_id") or node_data.get("agent_id", ""),
|
||||
self_review_enabled=node_data.get("self_review_enabled", False),
|
||||
)
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""
|
||||
Agent管理API
|
||||
"""
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, Query
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, Query, Response
|
||||
from sqlalchemy.orm import Session
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import List, Optional, Dict, Any
|
||||
@@ -101,6 +101,7 @@ class AgentResponse(BaseModel):
|
||||
|
||||
@router.get("", response_model=List[AgentResponse])
|
||||
async def get_agents(
|
||||
response: Response,
|
||||
skip: int = Query(0, ge=0, description="跳过记录数"),
|
||||
limit: int = Query(100, ge=1, le=100, description="每页记录数"),
|
||||
search: Optional[str] = Query(None, description="搜索关键词(按名称或描述)"),
|
||||
@@ -152,9 +153,12 @@ async def get_agents(
|
||||
if status:
|
||||
query = query.filter(Agent.status == status)
|
||||
|
||||
# 先获取总数(不带分页)
|
||||
total_count = query.count()
|
||||
|
||||
# 排序和分页
|
||||
agents = query.order_by(Agent.created_at.desc()).offset(skip).limit(limit).all()
|
||||
|
||||
|
||||
# 转换为响应格式,确保user_id和日期时间字段正确处理
|
||||
result = []
|
||||
for agent in agents:
|
||||
@@ -170,7 +174,9 @@ async def get_agents(
|
||||
"created_at": agent.created_at if agent.created_at else datetime.now(),
|
||||
"updated_at": agent.updated_at if agent.updated_at else datetime.now()
|
||||
})
|
||||
|
||||
|
||||
# 通过 X-Total-Count 响应头返回总数,前端借此正确分页
|
||||
response.headers["X-Total-Count"] = str(total_count)
|
||||
return result
|
||||
|
||||
|
||||
|
||||
@@ -110,6 +110,11 @@ class Settings(BaseSettings):
|
||||
LINGXI_APP_SECRET: str = ""
|
||||
LINGXI_AGENT_ID: str = "" # 创建灵犀后写入
|
||||
|
||||
# 人参果飞书应用配置(独立 WS 连接,路由到 AI学习助手 Agent — KG+RAG理想版)
|
||||
RENSHENGUO_APP_ID: str = ""
|
||||
RENSHENGUO_APP_SECRET: str = ""
|
||||
RENSHENGUO_AGENT_ID: str = "" # 创建 AI学习助手 后写入
|
||||
|
||||
class Config:
|
||||
env_file = str(_ENV_PATH)
|
||||
case_sensitive = True
|
||||
|
||||
@@ -8,7 +8,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
_registered = False
|
||||
|
||||
_EXPECTED_BUILTIN = 35
|
||||
_EXPECTED_BUILTIN = 39
|
||||
|
||||
|
||||
def ensure_builtin_tools_registered() -> None:
|
||||
@@ -53,6 +53,10 @@ def ensure_builtin_tools_registered() -> None:
|
||||
code_tool_create_tool,
|
||||
extension_log_tool,
|
||||
self_review_tool,
|
||||
knowledge_graph_search_tool,
|
||||
knowledge_graph_add_tool,
|
||||
entity_search_tool,
|
||||
learning_path_tool,
|
||||
HTTP_REQUEST_SCHEMA,
|
||||
FILE_READ_SCHEMA,
|
||||
FILE_WRITE_SCHEMA,
|
||||
@@ -88,6 +92,10 @@ def ensure_builtin_tools_registered() -> None:
|
||||
CODE_TOOL_CREATE_SCHEMA,
|
||||
EXTENSION_LOG_SCHEMA,
|
||||
SELF_REVIEW_SCHEMA,
|
||||
KNOWLEDGE_GRAPH_SEARCH_SCHEMA,
|
||||
KNOWLEDGE_GRAPH_ADD_SCHEMA,
|
||||
ENTITY_SEARCH_SCHEMA,
|
||||
LEARNING_PATH_SCHEMA,
|
||||
)
|
||||
|
||||
tool_registry.register_builtin_tool("http_request", http_request_tool, HTTP_REQUEST_SCHEMA)
|
||||
@@ -125,6 +133,10 @@ def ensure_builtin_tools_registered() -> None:
|
||||
tool_registry.register_builtin_tool("code_tool_create", code_tool_create_tool, CODE_TOOL_CREATE_SCHEMA)
|
||||
tool_registry.register_builtin_tool("extension_log", extension_log_tool, EXTENSION_LOG_SCHEMA)
|
||||
tool_registry.register_builtin_tool("self_review", self_review_tool, SELF_REVIEW_SCHEMA)
|
||||
tool_registry.register_builtin_tool("knowledge_graph_search", knowledge_graph_search_tool, KNOWLEDGE_GRAPH_SEARCH_SCHEMA)
|
||||
tool_registry.register_builtin_tool("knowledge_graph_add", knowledge_graph_add_tool, KNOWLEDGE_GRAPH_ADD_SCHEMA)
|
||||
tool_registry.register_builtin_tool("entity_search", entity_search_tool, ENTITY_SEARCH_SCHEMA)
|
||||
tool_registry.register_builtin_tool("learning_path", learning_path_tool, LEARNING_PATH_SCHEMA)
|
||||
_registered = True
|
||||
|
||||
n = tool_registry.builtin_tool_count()
|
||||
|
||||
@@ -248,6 +248,13 @@ async def startup_event():
|
||||
except Exception as e:
|
||||
logger.error(f"灵犀长连接启动失败: {e}")
|
||||
|
||||
# 启动人参果飞书长连接(AI学习助手 — KG+RAG理想版)
|
||||
try:
|
||||
from app.services.renshenguo_ws_handler import start_ws_client as start_renshenguo_ws
|
||||
asyncio.ensure_future(start_renshenguo_ws())
|
||||
except Exception as e:
|
||||
logger.error(f"人参果长连接启动失败: {e}")
|
||||
|
||||
# 注册路由
|
||||
from app.api import auth, uploads, workflows, executions, websocket, execution_logs, data_sources, agents, platform_templates, model_configs, webhooks, template_market, batch_operations, collaboration, permissions, monitoring, alert_rules, node_test, node_templates, tools, agent_chat, agent_monitoring, knowledge_base, agent_schedules, notifications, feishu_bind, approval
|
||||
|
||||
|
||||
@@ -68,3 +68,43 @@ class GlobalKnowledge(Base):
|
||||
|
||||
def __repr__(self):
|
||||
return f"<GlobalKnowledge(id={self.id}, source_agent={self.source_agent_id})>"
|
||||
|
||||
|
||||
class KnowledgeEntity(Base):
|
||||
"""知识图谱实体表 — 学习知识点、概念、术语"""
|
||||
__tablename__ = "knowledge_entities"
|
||||
|
||||
id = Column(CHAR(36), primary_key=True, default=lambda: str(uuid.uuid4()), comment="实体ID")
|
||||
name = Column(String(200), nullable=False, comment="实体名称")
|
||||
entity_type = Column(String(50), nullable=False, default="concept", comment="实体类型: concept/formula/fact/term/task/skill")
|
||||
description = Column(Text, comment="实体描述")
|
||||
embedding = Column(Text, nullable=True, comment="实体名称+描述的 embedding(JSON 序列化)")
|
||||
metadata_ = Column("metadata", JSON, nullable=True, comment="扩展元数据")
|
||||
source = Column(String(50), default="extracted", comment="来源: extracted/manual/imported")
|
||||
confidence = Column(String(20), default="medium", comment="置信度: low/medium/high")
|
||||
scope_kind = Column(String(50), default="agent", comment="作用域类型")
|
||||
scope_id = Column(String(100), default="", comment="作用域 ID")
|
||||
user_id = Column(CHAR(36), ForeignKey("users.id"), nullable=True, comment="创建者ID")
|
||||
created_at = Column(DateTime, default=func.now(), comment="创建时间")
|
||||
updated_at = Column(DateTime, default=func.now(), onupdate=func.now(), comment="更新时间")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<KnowledgeEntity(id={self.id}, name={self.name}, type={self.entity_type})>"
|
||||
|
||||
|
||||
class KnowledgeRelation(Base):
|
||||
"""知识图谱关系表 — 实体之间的语义关系"""
|
||||
__tablename__ = "knowledge_relations"
|
||||
|
||||
id = Column(CHAR(36), primary_key=True, default=lambda: str(uuid.uuid4()), comment="关系ID")
|
||||
source_entity_id = Column(CHAR(36), nullable=False, index=True, comment="源实体ID")
|
||||
target_entity_id = Column(CHAR(36), nullable=False, index=True, comment="目标实体ID")
|
||||
relation_type = Column(String(50), nullable=False, comment="关系类型: prerequisite/extends/contains/related_to/example_of/applies_to")
|
||||
description = Column(Text, comment="关系描述")
|
||||
weight = Column(String(20), default="1.0", comment="关系权重")
|
||||
scope_kind = Column(String(50), default="agent", comment="作用域类型")
|
||||
scope_id = Column(String(100), default="", comment="作用域 ID")
|
||||
created_at = Column(DateTime, default=func.now(), comment="创建时间")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<KnowledgeRelation({self.source_entity_id}) -[{self.relation_type}]-> ({self.target_entity_id})>"
|
||||
|
||||
@@ -1756,9 +1756,9 @@ async def schedule_create_tool(
|
||||
if not agent:
|
||||
return json.dumps({"error": f"Agent 不存在: {agent_id}"}, ensure_ascii=False)
|
||||
|
||||
# 尝试计算下次执行时间
|
||||
# 尝试计算下次执行时间(使用北京时间,与 schedule 表 timezone 默认值一致)
|
||||
try:
|
||||
next_run = compute_next_run(cron_expression)
|
||||
next_run = compute_next_run(cron_expression, tz="Asia/Shanghai")
|
||||
except (ValueError, KeyError) as e:
|
||||
return json.dumps({"error": f"cron 表达式无效: {e}(标准 5 位格式,如 0 9 * * *)"}, ensure_ascii=False)
|
||||
|
||||
@@ -4388,3 +4388,314 @@ SELF_REVIEW_SCHEMA = {
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
# ── knowledge_graph_search ────────────────────────────────────
|
||||
|
||||
async def knowledge_graph_search_tool(
|
||||
query: str,
|
||||
top_k: int = 5,
|
||||
include_graph: bool = True,
|
||||
scope_id: str = "",
|
||||
) -> str:
|
||||
"""知识图谱混合检索:向量语义搜索 + 图谱邻居展开。
|
||||
|
||||
当用户询问学习相关问题时,用此工具搜索已构建的知识图谱,
|
||||
同时获取语义相似的知识实体和图谱中关联的邻居知识点。
|
||||
|
||||
Args:
|
||||
query: 搜索查询(用户的问题或关键词)
|
||||
top_k: 返回的向量匹配实体数(默认 5)
|
||||
include_graph: 是否展开图谱邻居(默认 true)
|
||||
scope_id: 作用域 ID,默认使用当前 Agent ID
|
||||
"""
|
||||
import asyncio as _asyncio
|
||||
from app.services.knowledge_graph_service import hybrid_search
|
||||
|
||||
try:
|
||||
result = await hybrid_search(
|
||||
query=query,
|
||||
scope_kind="agent",
|
||||
scope_id=scope_id or "learning_assistant",
|
||||
top_k=top_k,
|
||||
include_neighbors=include_graph,
|
||||
)
|
||||
formatted = result.get("formatted_context", "")
|
||||
vector_count = len(result.get("vector_matches", []))
|
||||
graph_count = len(result.get("graph_expansion", {}).get("entities", []))
|
||||
|
||||
return json.dumps({
|
||||
"query": query,
|
||||
"vector_matches_count": vector_count,
|
||||
"graph_entities_count": graph_count,
|
||||
"context": formatted,
|
||||
"raw": {
|
||||
"vector_matches": result.get("vector_matches", []),
|
||||
"graph_expansion": result.get("graph_expansion", {}),
|
||||
},
|
||||
}, ensure_ascii=False)
|
||||
except Exception as e:
|
||||
logger.error(f"knowledge_graph_search 失败: {e}")
|
||||
return json.dumps({"error": f"知识图谱搜索失败: {e}"}, ensure_ascii=False)
|
||||
|
||||
|
||||
KNOWLEDGE_GRAPH_SEARCH_SCHEMA = {
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "knowledge_graph_search",
|
||||
"description": (
|
||||
"知识图谱混合检索:用向量语义搜索找到相关知识实体,"
|
||||
"再展开图谱邻居获取关联知识点。适合学习场景中的知识检索、"
|
||||
"概念关联、前置知识查找。返回结构化的知识上下文。"
|
||||
),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {"type": "string", "description": "搜索查询(用户问题或关键词)"},
|
||||
"top_k": {"type": "integer", "description": "返回的向量匹配实体数", "default": 5},
|
||||
"include_graph": {"type": "boolean", "description": "是否展开图谱邻居", "default": True},
|
||||
"scope_id": {"type": "string", "description": "作用域 ID(默认 learning_assistant)"},
|
||||
},
|
||||
"required": ["query"],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
# ── knowledge_graph_add ────────────────────────────────────────
|
||||
|
||||
async def knowledge_graph_add_tool(
|
||||
text: str,
|
||||
scope_id: str = "",
|
||||
) -> str:
|
||||
"""从文本中提取知识点实体和关系,写入知识图谱。
|
||||
|
||||
当用户分享学习内容、知识点总结、对话中有价值的信息时,
|
||||
调用此工具自动提取实体和关系并持久化到知识图谱。
|
||||
|
||||
Args:
|
||||
text: 要提取知识的文本内容
|
||||
scope_id: 作用域 ID,默认使用当前 Agent ID
|
||||
"""
|
||||
import asyncio as _asyncio
|
||||
from app.services.knowledge_graph_service import extract_from_text
|
||||
|
||||
if not text or len(text.strip()) < 20:
|
||||
return json.dumps({
|
||||
"error": "文本太短(至少需要 20 个字符)",
|
||||
"entity_count": 0,
|
||||
"relation_count": 0,
|
||||
}, ensure_ascii=False)
|
||||
|
||||
try:
|
||||
result = await extract_from_text(
|
||||
text=text,
|
||||
scope_kind="agent",
|
||||
scope_id=scope_id or "learning_assistant",
|
||||
)
|
||||
return json.dumps({
|
||||
"entity_count": result.get("entity_count", 0),
|
||||
"relation_count": result.get("relation_count", 0),
|
||||
"entities": [
|
||||
{"name": e["name"], "type": e["entity_type"], "description": e.get("description", "")[:200]}
|
||||
for e in result.get("entities", [])
|
||||
],
|
||||
"relations": result.get("relations", []),
|
||||
"hint": f"已从文本中提取 {result.get('entity_count', 0)} 个实体和 {result.get('relation_count', 0)} 个关系,可使用 knowledge_graph_search 检索",
|
||||
}, ensure_ascii=False)
|
||||
except Exception as e:
|
||||
logger.error(f"knowledge_graph_add 失败: {e}")
|
||||
return json.dumps({"error": f"知识图谱添加失败: {e}"}, ensure_ascii=False)
|
||||
|
||||
|
||||
KNOWLEDGE_GRAPH_ADD_SCHEMA = {
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "knowledge_graph_add",
|
||||
"description": (
|
||||
"从文本中提取知识点实体和关系,写入知识图谱。"
|
||||
"自动识别概念、公式、术语、事实等实体类型,"
|
||||
"并建立前置/扩展/包含/示例/应用等语义关系。"
|
||||
"适合将学习材料、对话内容转化为结构化知识网络。"
|
||||
),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"text": {"type": "string", "description": "从中提取知识的文本内容(学习材料、知识点总结等)"},
|
||||
"scope_id": {"type": "string", "description": "作用域 ID(默认 learning_assistant)"},
|
||||
},
|
||||
"required": ["text"],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
# ── entity_search ──────────────────────────────────────────────
|
||||
|
||||
async def entity_search_tool(
|
||||
keyword: str = "",
|
||||
entity_type: str = "",
|
||||
scope_id: str = "",
|
||||
limit: int = 10,
|
||||
) -> str:
|
||||
"""关键词搜索知识图谱中的实体。
|
||||
|
||||
Args:
|
||||
keyword: 搜索关键词(在名称和描述中查找)
|
||||
entity_type: 实体类型筛选(concept/formula/fact/term/task/skill),留空=全部
|
||||
scope_id: 作用域 ID
|
||||
limit: 返回数量
|
||||
"""
|
||||
from app.services.knowledge_graph_service import search_entities
|
||||
from app.core.database import SessionLocal
|
||||
|
||||
db = None
|
||||
try:
|
||||
db = SessionLocal()
|
||||
entities = search_entities(
|
||||
db,
|
||||
keyword=keyword,
|
||||
entity_type=entity_type or None,
|
||||
scope_kind="agent",
|
||||
scope_id=scope_id or "learning_assistant",
|
||||
limit=limit,
|
||||
)
|
||||
return json.dumps({
|
||||
"keyword": keyword,
|
||||
"count": len(entities),
|
||||
"entities": [
|
||||
{
|
||||
"id": e.id,
|
||||
"name": e.name,
|
||||
"type": e.entity_type,
|
||||
"description": e.description,
|
||||
"confidence": e.confidence,
|
||||
}
|
||||
for e in entities
|
||||
],
|
||||
}, ensure_ascii=False)
|
||||
except Exception as e:
|
||||
logger.error(f"entity_search 失败: {e}")
|
||||
return json.dumps({"error": f"实体搜索失败: {e}"}, ensure_ascii=False)
|
||||
finally:
|
||||
if db:
|
||||
db.close()
|
||||
|
||||
|
||||
ENTITY_SEARCH_SCHEMA = {
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "entity_search",
|
||||
"description": (
|
||||
"在知识图谱中按关键词搜索实体。"
|
||||
"可按实体类型(概念/公式/术语/事实/任务/技能)筛选。"
|
||||
"适合查找特定知识点或浏览知识库。"
|
||||
),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"keyword": {"type": "string", "description": "搜索关键词(在名称和描述中查找)", "default": ""},
|
||||
"entity_type": {
|
||||
"type": "string",
|
||||
"enum": ["concept", "formula", "fact", "term", "task", "skill"],
|
||||
"description": "实体类型筛选,留空=全部",
|
||||
},
|
||||
"scope_id": {"type": "string", "description": "作用域 ID"},
|
||||
"limit": {"type": "integer", "description": "返回数量", "default": 10},
|
||||
},
|
||||
"required": [],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
# ── learning_path ─────────────────────────────────────────────
|
||||
|
||||
async def learning_path_tool(
|
||||
entity_names: str,
|
||||
scope_id: str = "",
|
||||
) -> str:
|
||||
"""基于知识图谱推荐学习路径。
|
||||
|
||||
给定一组目标知识点名称(逗号分隔),分析其前置依赖关系,
|
||||
返回建议的学习顺序。
|
||||
|
||||
Args:
|
||||
entity_names: 目标知识点名称,逗号分隔(如 "微积分,导数,极限")
|
||||
scope_id: 作用域 ID
|
||||
"""
|
||||
from app.services.knowledge_graph_service import search_entities, recommend_learning_path
|
||||
from app.core.database import SessionLocal
|
||||
|
||||
if not entity_names or not entity_names.strip():
|
||||
return json.dumps({"error": "请提供至少一个知识点名称"}, ensure_ascii=False)
|
||||
|
||||
names = [n.strip() for n in entity_names.split(",") if n.strip()]
|
||||
db = None
|
||||
try:
|
||||
db = SessionLocal()
|
||||
entity_ids = []
|
||||
found_names = []
|
||||
for name in names:
|
||||
entities = search_entities(
|
||||
db, keyword=name,
|
||||
scope_kind="agent", scope_id=scope_id or "learning_assistant",
|
||||
limit=3,
|
||||
)
|
||||
for e in entities:
|
||||
if e.name == name or name in e.name:
|
||||
entity_ids.append(e.id)
|
||||
found_names.append(e.name)
|
||||
break
|
||||
|
||||
if not entity_ids:
|
||||
return json.dumps({
|
||||
"message": f"未在知识图谱中找到这些知识点: {', '.join(names)}",
|
||||
"hint": "请先用 knowledge_graph_add 添加相关知识,或使用更通用的名称搜索",
|
||||
}, ensure_ascii=False)
|
||||
|
||||
path = recommend_learning_path(
|
||||
db, entity_ids,
|
||||
scope_kind="agent", scope_id=scope_id or "learning_assistant",
|
||||
)
|
||||
return json.dumps({
|
||||
"target_entities": names,
|
||||
"found_entities": found_names,
|
||||
"suggested_order": path.get("suggested_order", []),
|
||||
"summary": path.get("summary", ""),
|
||||
"prerequisites": [
|
||||
{
|
||||
"prerequisite": p["prerequisite"]["name"],
|
||||
"target": p["target"]["name"],
|
||||
"relation": p["relation"].get("description", ""),
|
||||
}
|
||||
for p in path.get("prerequisites", [])
|
||||
],
|
||||
}, ensure_ascii=False)
|
||||
except Exception as e:
|
||||
logger.error(f"learning_path 失败: {e}")
|
||||
return json.dumps({"error": f"学习路径推荐失败: {e}"}, ensure_ascii=False)
|
||||
finally:
|
||||
if db:
|
||||
db.close()
|
||||
|
||||
|
||||
LEARNING_PATH_SCHEMA = {
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "learning_path",
|
||||
"description": (
|
||||
"基于知识图谱推荐学习路径。给定一组目标知识点,"
|
||||
"分析前置依赖关系,给出建议的学习顺序。"
|
||||
"适合制定学习计划、了解知识点间的先后关系。"
|
||||
),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"entity_names": {"type": "string", "description": "目标知识点名称,逗号分隔(如 \"微积分,导数,极限\")"},
|
||||
"scope_id": {"type": "string", "description": "作用域 ID"},
|
||||
},
|
||||
"required": ["entity_names"],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
822
backend/app/services/knowledge_graph_service.py
Normal file
822
backend/app/services/knowledge_graph_service.py
Normal file
@@ -0,0 +1,822 @@
|
||||
"""
|
||||
知识图谱服务 — 实体抽取、关系构建、图谱查询、向量融合检索
|
||||
|
||||
为智能学习助手提供 KG+RAG 核心能力:
|
||||
- 从对话/文本中提取知识点实体
|
||||
- 构建实体间的语义关系(前置/扩展/包含/示例/关联)
|
||||
- 图谱查询:邻近节点、路径查找、子图展开
|
||||
- 向量+图谱融合检索:同时命中语义相似和结构关联
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
from typing import Any, Dict, List, Optional, Tuple, Set
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import or_, and_
|
||||
|
||||
from app.core.database import SessionLocal
|
||||
from app.models.agent import KnowledgeEntity, KnowledgeRelation
|
||||
from app.services.embedding_service import embedding_service, VectorEntry
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# 实体类型定义
|
||||
ENTITY_TYPES = ["concept", "formula", "fact", "term", "task", "skill"]
|
||||
|
||||
# 关系类型定义
|
||||
RELATION_TYPES = {
|
||||
"prerequisite": "前置知识(学习 B 前需要先掌握 A)",
|
||||
"extends": "扩展(B 是 A 的深入/延伸)",
|
||||
"contains": "包含(A 包含子知识点 B)",
|
||||
"related_to": "相关(A 与 B 存在关联)",
|
||||
"example_of": "示例(B 是 A 的实例/例题)",
|
||||
"applies_to": "应用(A 可应用于 B)",
|
||||
}
|
||||
|
||||
|
||||
def _build_entity_embedding_text(name: str, entity_type: str, description: str) -> str:
|
||||
"""构建用于 embedding 的统一文本。"""
|
||||
parts = [f"[{entity_type}] {name}"]
|
||||
if description:
|
||||
parts.append(f": {description[:500]}")
|
||||
return "".join(parts)
|
||||
|
||||
|
||||
def _serialize_embedding(emb: Optional[List[float]]) -> Optional[str]:
|
||||
if not emb:
|
||||
return None
|
||||
return embedding_service.serialize_embedding(emb)
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# 实体管理
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
|
||||
async def add_entity(
|
||||
db: Session,
|
||||
name: str,
|
||||
entity_type: str = "concept",
|
||||
description: str = "",
|
||||
metadata: Optional[Dict[str, Any]] = None,
|
||||
source: str = "extracted",
|
||||
confidence: str = "medium",
|
||||
scope_kind: str = "agent",
|
||||
scope_id: str = "",
|
||||
user_id: Optional[str] = None,
|
||||
) -> Optional[KnowledgeEntity]:
|
||||
"""添加或更新知识实体(按 name+scope 去重,更新描述和 embedding)。"""
|
||||
name = name.strip()[:200]
|
||||
if not name:
|
||||
return None
|
||||
|
||||
if entity_type not in ENTITY_TYPES:
|
||||
entity_type = "concept"
|
||||
|
||||
# 查找已有实体(同名+同scope)
|
||||
existing = (
|
||||
db.query(KnowledgeEntity)
|
||||
.filter(
|
||||
KnowledgeEntity.name == name,
|
||||
KnowledgeEntity.scope_kind == scope_kind,
|
||||
KnowledgeEntity.scope_id == scope_id,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
# 生成 embedding
|
||||
emb_text = _build_entity_embedding_text(name, entity_type, description)
|
||||
embedding_json = None
|
||||
try:
|
||||
emb = await embedding_service.generate_embedding(emb_text)
|
||||
if emb:
|
||||
embedding_json = _serialize_embedding(emb)
|
||||
except Exception as e:
|
||||
logger.warning("生成实体 embedding 失败: %s", e)
|
||||
|
||||
if existing:
|
||||
# 更新已有实体
|
||||
if description and len(description) > len(existing.description or ""):
|
||||
existing.description = description
|
||||
if embedding_json:
|
||||
existing.embedding = embedding_json
|
||||
if metadata:
|
||||
merged = {**(existing.metadata_ or {}), **metadata}
|
||||
existing.metadata_ = merged
|
||||
existing.confidence = confidence
|
||||
db.commit()
|
||||
db.refresh(existing)
|
||||
logger.debug("更新知识实体: %s (%s)", name, entity_type)
|
||||
return existing
|
||||
|
||||
entity = KnowledgeEntity(
|
||||
name=name,
|
||||
entity_type=entity_type,
|
||||
description=description,
|
||||
embedding=embedding_json,
|
||||
metadata_=metadata or {},
|
||||
source=source,
|
||||
confidence=confidence,
|
||||
scope_kind=scope_kind,
|
||||
scope_id=scope_id,
|
||||
user_id=user_id,
|
||||
)
|
||||
db.add(entity)
|
||||
db.commit()
|
||||
db.refresh(entity)
|
||||
logger.info("新增知识实体: %s (%s) id=%s", name, entity_type, entity.id)
|
||||
return entity
|
||||
|
||||
|
||||
async def add_entities_batch(
|
||||
db: Session,
|
||||
entities: List[Dict[str, Any]],
|
||||
scope_kind: str = "agent",
|
||||
scope_id: str = "",
|
||||
user_id: Optional[str] = None,
|
||||
) -> List[KnowledgeEntity]:
|
||||
"""批量添加实体,返回新增/更新后的实体列表。"""
|
||||
results: List[KnowledgeEntity] = []
|
||||
for ent in entities:
|
||||
try:
|
||||
e = await add_entity(
|
||||
db,
|
||||
name=ent.get("name", ""),
|
||||
entity_type=ent.get("entity_type", "concept"),
|
||||
description=ent.get("description", ""),
|
||||
metadata=ent.get("metadata"),
|
||||
source=ent.get("source", "extracted"),
|
||||
confidence=ent.get("confidence", "medium"),
|
||||
scope_kind=scope_kind,
|
||||
scope_id=scope_id,
|
||||
user_id=user_id,
|
||||
)
|
||||
if e:
|
||||
results.append(e)
|
||||
except Exception as ex:
|
||||
logger.warning("批量添加实体失败: name=%s err=%s", ent.get("name"), ex)
|
||||
return results
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# 关系管理
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
|
||||
def add_relation(
|
||||
db: Session,
|
||||
source_entity_id: str,
|
||||
target_entity_id: str,
|
||||
relation_type: str = "related_to",
|
||||
description: str = "",
|
||||
weight: float = 1.0,
|
||||
scope_kind: str = "agent",
|
||||
scope_id: str = "",
|
||||
) -> Optional[KnowledgeRelation]:
|
||||
"""添加两个实体间的关系(去重)。"""
|
||||
if source_entity_id == target_entity_id:
|
||||
return None
|
||||
if relation_type not in RELATION_TYPES:
|
||||
relation_type = "related_to"
|
||||
|
||||
existing = (
|
||||
db.query(KnowledgeRelation)
|
||||
.filter(
|
||||
KnowledgeRelation.source_entity_id == source_entity_id,
|
||||
KnowledgeRelation.target_entity_id == target_entity_id,
|
||||
KnowledgeRelation.relation_type == relation_type,
|
||||
KnowledgeRelation.scope_kind == scope_kind,
|
||||
KnowledgeRelation.scope_id == scope_id,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
if existing:
|
||||
logger.debug("关系已存在: %s -[%s]-> %s", source_entity_id[:8], relation_type, target_entity_id[:8])
|
||||
return existing
|
||||
|
||||
rel = KnowledgeRelation(
|
||||
source_entity_id=source_entity_id,
|
||||
target_entity_id=target_entity_id,
|
||||
relation_type=relation_type,
|
||||
description=description,
|
||||
weight=str(weight),
|
||||
scope_kind=scope_kind,
|
||||
scope_id=scope_id,
|
||||
)
|
||||
db.add(rel)
|
||||
db.commit()
|
||||
db.refresh(rel)
|
||||
logger.info("新增关系: %s -[%s]-> %s", source_entity_id[:8], relation_type, target_entity_id[:8])
|
||||
return rel
|
||||
|
||||
|
||||
def add_relations_from_map(
|
||||
db: Session,
|
||||
entity_map: Dict[str, str], # name -> entity_id
|
||||
relations: List[Dict[str, Any]],
|
||||
scope_kind: str = "agent",
|
||||
scope_id: str = "",
|
||||
) -> int:
|
||||
"""从关系映射批量添加关系。relations 中 source/target 使用实体名称,自动映射为 ID。"""
|
||||
count = 0
|
||||
for rel in relations:
|
||||
src_name = rel.get("source", "")
|
||||
tgt_name = rel.get("target", "")
|
||||
src_id = entity_map.get(src_name)
|
||||
tgt_id = entity_map.get(tgt_name)
|
||||
if not src_id or not tgt_id:
|
||||
continue
|
||||
r = add_relation(
|
||||
db,
|
||||
source_entity_id=src_id,
|
||||
target_entity_id=tgt_id,
|
||||
relation_type=rel.get("relation_type", "related_to"),
|
||||
description=rel.get("description", ""),
|
||||
weight=float(rel.get("weight", 1.0)),
|
||||
scope_kind=scope_kind,
|
||||
scope_id=scope_id,
|
||||
)
|
||||
if r:
|
||||
count += 1
|
||||
return count
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# 图谱查询
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
|
||||
def get_entity_by_id(db: Session, entity_id: str) -> Optional[KnowledgeEntity]:
|
||||
return db.query(KnowledgeEntity).filter(KnowledgeEntity.id == entity_id).first()
|
||||
|
||||
|
||||
def search_entities(
|
||||
db: Session,
|
||||
keyword: str = "",
|
||||
entity_type: Optional[str] = None,
|
||||
scope_kind: str = "agent",
|
||||
scope_id: str = "",
|
||||
limit: int = 20,
|
||||
) -> List[KnowledgeEntity]:
|
||||
"""关键词搜索实体。"""
|
||||
q = db.query(KnowledgeEntity).filter(
|
||||
KnowledgeEntity.scope_kind == scope_kind,
|
||||
KnowledgeEntity.scope_id == scope_id,
|
||||
)
|
||||
if entity_type:
|
||||
q = q.filter(KnowledgeEntity.entity_type == entity_type)
|
||||
if keyword:
|
||||
pattern = f"%{keyword}%"
|
||||
q = q.filter(
|
||||
or_(
|
||||
KnowledgeEntity.name.like(pattern),
|
||||
KnowledgeEntity.description.like(pattern),
|
||||
)
|
||||
)
|
||||
return q.order_by(KnowledgeEntity.confidence.desc(), KnowledgeEntity.created_at.desc()).limit(limit).all()
|
||||
|
||||
|
||||
def get_neighbors(
|
||||
db: Session,
|
||||
entity_id: str,
|
||||
relation_types: Optional[List[str]] = None,
|
||||
direction: str = "both",
|
||||
max_depth: int = 1,
|
||||
limit: int = 30,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
获取实体的图谱邻居。
|
||||
|
||||
Returns:
|
||||
{
|
||||
"entity": {...},
|
||||
"neighbors": [{"entity": {...}, "relation": {...}, "direction": "out"|"in"}],
|
||||
"subgraph_size": int,
|
||||
}
|
||||
"""
|
||||
entity = get_entity_by_id(db, entity_id)
|
||||
if not entity:
|
||||
return {"entity": None, "neighbors": [], "subgraph_size": 0}
|
||||
|
||||
neighbors: List[Dict[str, Any]] = []
|
||||
|
||||
if direction in ("out", "both"):
|
||||
q = db.query(KnowledgeRelation).filter(
|
||||
KnowledgeRelation.source_entity_id == entity_id,
|
||||
)
|
||||
if relation_types:
|
||||
q = q.filter(KnowledgeRelation.relation_type.in_(relation_types))
|
||||
for rel in q.limit(limit).all():
|
||||
target = get_entity_by_id(db, rel.target_entity_id)
|
||||
if target:
|
||||
neighbors.append({
|
||||
"entity": _entity_to_dict(target),
|
||||
"relation": _relation_to_dict(rel),
|
||||
"direction": "out",
|
||||
})
|
||||
|
||||
if direction in ("in", "both"):
|
||||
q = db.query(KnowledgeRelation).filter(
|
||||
KnowledgeRelation.target_entity_id == entity_id,
|
||||
)
|
||||
if relation_types:
|
||||
q = q.filter(KnowledgeRelation.relation_type.in_(relation_types))
|
||||
for rel in q.limit(limit).all():
|
||||
source = get_entity_by_id(db, rel.source_entity_id)
|
||||
if source:
|
||||
neighbors.append({
|
||||
"entity": _entity_to_dict(source),
|
||||
"relation": _relation_to_dict(rel),
|
||||
"direction": "in",
|
||||
})
|
||||
|
||||
return {
|
||||
"entity": _entity_to_dict(entity),
|
||||
"neighbors": neighbors[:limit],
|
||||
"subgraph_size": len(neighbors),
|
||||
}
|
||||
|
||||
|
||||
def get_entity_graph(
|
||||
db: Session,
|
||||
entity_id: str,
|
||||
max_depth: int = 2,
|
||||
limit: int = 50,
|
||||
) -> Dict[str, Any]:
|
||||
"""获取以实体为中心的子图(BFS 展开)。"""
|
||||
entity = get_entity_by_id(db, entity_id)
|
||||
if not entity:
|
||||
return {"nodes": [], "edges": [], "center_id": entity_id}
|
||||
|
||||
visited: Set[str] = {entity_id}
|
||||
nodes: Dict[str, Dict[str, Any]] = {entity_id: _entity_to_dict(entity)}
|
||||
edges: List[Dict[str, Any]] = []
|
||||
|
||||
frontier = {entity_id}
|
||||
for depth in range(max_depth):
|
||||
next_frontier: Set[str] = set()
|
||||
if len(nodes) >= limit:
|
||||
break
|
||||
|
||||
for nid in list(frontier):
|
||||
# 出边
|
||||
out_rels = (
|
||||
db.query(KnowledgeRelation)
|
||||
.filter(KnowledgeRelation.source_entity_id == nid)
|
||||
.limit(limit // 2)
|
||||
.all()
|
||||
)
|
||||
for rel in out_rels:
|
||||
edges.append(_relation_to_dict(rel))
|
||||
if rel.target_entity_id not in visited and len(nodes) < limit:
|
||||
tgt = get_entity_by_id(db, rel.target_entity_id)
|
||||
if tgt:
|
||||
nodes[rel.target_entity_id] = _entity_to_dict(tgt)
|
||||
visited.add(rel.target_entity_id)
|
||||
next_frontier.add(rel.target_entity_id)
|
||||
|
||||
# 入边
|
||||
in_rels = (
|
||||
db.query(KnowledgeRelation)
|
||||
.filter(KnowledgeRelation.target_entity_id == nid)
|
||||
.limit(limit // 2)
|
||||
.all()
|
||||
)
|
||||
for rel in in_rels:
|
||||
edges.append(_relation_to_dict(rel))
|
||||
if rel.source_entity_id not in visited and len(nodes) < limit:
|
||||
src = get_entity_by_id(db, rel.source_entity_id)
|
||||
if src:
|
||||
nodes[rel.source_entity_id] = _entity_to_dict(src)
|
||||
visited.add(rel.source_entity_id)
|
||||
next_frontier.add(rel.source_entity_id)
|
||||
|
||||
frontier = next_frontier
|
||||
if not frontier:
|
||||
break
|
||||
|
||||
return {
|
||||
"nodes": list(nodes.values()),
|
||||
"edges": edges,
|
||||
"center_id": entity_id,
|
||||
}
|
||||
|
||||
|
||||
def find_path(
|
||||
db: Session,
|
||||
source_id: str,
|
||||
target_id: str,
|
||||
max_depth: int = 4,
|
||||
) -> Optional[List[Dict[str, Any]]]:
|
||||
"""BFS 查找两个实体间的最短路径。"""
|
||||
if source_id == target_id:
|
||||
return [{"entity": _entity_to_dict(get_entity_by_id(db, source_id)), "relation": None}]
|
||||
|
||||
visited: Set[str] = {source_id}
|
||||
# BFS queue: (current_id, path_so_far)
|
||||
from collections import deque
|
||||
queue: deque = deque()
|
||||
queue.append((source_id, []))
|
||||
|
||||
while queue:
|
||||
current_id, path = queue.popleft()
|
||||
if len(path) >= max_depth:
|
||||
continue
|
||||
|
||||
# 检查所有出边
|
||||
out_rels = (
|
||||
db.query(KnowledgeRelation)
|
||||
.filter(KnowledgeRelation.source_entity_id == current_id)
|
||||
.all()
|
||||
)
|
||||
for rel in out_rels:
|
||||
if rel.target_entity_id in visited:
|
||||
continue
|
||||
new_path = path + [{
|
||||
"from": _entity_to_dict(get_entity_by_id(db, current_id)),
|
||||
"relation": _relation_to_dict(rel),
|
||||
"to": _entity_to_dict(get_entity_by_id(db, rel.target_entity_id)),
|
||||
}]
|
||||
if rel.target_entity_id == target_id:
|
||||
return new_path
|
||||
visited.add(rel.target_entity_id)
|
||||
queue.append((rel.target_entity_id, new_path))
|
||||
|
||||
# 检查所有入边
|
||||
in_rels = (
|
||||
db.query(KnowledgeRelation)
|
||||
.filter(KnowledgeRelation.target_entity_id == current_id)
|
||||
.all()
|
||||
)
|
||||
for rel in in_rels:
|
||||
if rel.source_entity_id in visited:
|
||||
continue
|
||||
new_path = path + [{
|
||||
"from": _entity_to_dict(get_entity_by_id(db, current_id)),
|
||||
"relation": _relation_to_dict(rel),
|
||||
"to": _entity_to_dict(get_entity_by_id(db, rel.source_entity_id)),
|
||||
}]
|
||||
if rel.source_entity_id == target_id:
|
||||
return new_path
|
||||
visited.add(rel.source_entity_id)
|
||||
queue.append((rel.source_entity_id, new_path))
|
||||
|
||||
return None
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# LLM 驱动的实体与关系提取
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
|
||||
EXTRACTION_PROMPT = """你是一个知识图谱构建助手。请分析以下文本,提取其中的知识点实体和它们之间的关系。
|
||||
|
||||
请返回 JSON 格式(不要 markdown 包裹),包含:
|
||||
1. entities: 提取的知识实体列表,每个实体包含:
|
||||
- name: 实体名称(简洁准确)
|
||||
- entity_type: 类型(concept=概念, formula=公式, fact=事实, term=术语, task=任务, skill=技能)
|
||||
- description: 简要描述(1-2句)
|
||||
- confidence: 置信度(low/medium/high)
|
||||
|
||||
2. relations: 实体间的关系列表,每个关系包含:
|
||||
- source: 源实体名称
|
||||
- target: 目标实体名称
|
||||
- relation_type: 关系类型(prerequisite=前置知识, extends=扩展, contains=包含, related_to=相关, example_of=示例, applies_to=应用)
|
||||
- description: 关系说明(1句话)
|
||||
|
||||
注意:
|
||||
- 只提取文本中明确出现的知识点,不编造
|
||||
- 关系应只在同段文本中有关联的实体间建立
|
||||
- 如果文本中知识点不足,返回空数组即可
|
||||
|
||||
文本内容:
|
||||
{text}"""
|
||||
|
||||
|
||||
async def extract_from_text(
|
||||
text: str,
|
||||
scope_kind: str = "agent",
|
||||
scope_id: str = "",
|
||||
user_id: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
使用 LLM 从文本中提取实体和关系,并写入知识图谱。
|
||||
|
||||
Returns:
|
||||
{"entities": [...], "relations": [...], "entity_count": int, "relation_count": int}
|
||||
"""
|
||||
if not text or len(text.strip()) < 20:
|
||||
return {"entities": [], "relations": [], "entity_count": 0, "relation_count": 0}
|
||||
|
||||
from openai import AsyncOpenAI
|
||||
from app.core.config import settings
|
||||
|
||||
# 调用 LLM 提取
|
||||
try:
|
||||
api_key = settings.DEEPSEEK_API_KEY or settings.OPENAI_API_KEY or ""
|
||||
base_url = settings.DEEPSEEK_BASE_URL or settings.OPENAI_BASE_URL or "https://api.deepseek.com"
|
||||
if api_key == "your-openai-api-key":
|
||||
api_key = settings.DEEPSEEK_API_KEY or ""
|
||||
base_url = settings.DEEPSEEK_BASE_URL or "https://api.deepseek.com"
|
||||
|
||||
if not api_key:
|
||||
logger.warning("知识图谱提取:未配置 API Key,跳过")
|
||||
return {"entities": [], "relations": [], "entity_count": 0, "relation_count": 0}
|
||||
|
||||
client = AsyncOpenAI(api_key=api_key, base_url=base_url)
|
||||
resp = await client.chat.completions.create(
|
||||
model="deepseek-v4-flash",
|
||||
messages=[{"role": "user", "content": EXTRACTION_PROMPT.format(text=text[:4000])}],
|
||||
temperature=0.2,
|
||||
max_tokens=2048,
|
||||
timeout=30,
|
||||
)
|
||||
raw = resp.choices[0].message.content or ""
|
||||
|
||||
# 解析 JSON
|
||||
raw = raw.strip().removeprefix("```json").removesuffix("```").strip()
|
||||
result = json.loads(raw)
|
||||
except json.JSONDecodeError:
|
||||
logger.warning("知识图谱提取:LLM 返回非 JSON,跳过")
|
||||
return {"entities": [], "relations": [], "entity_count": 0, "relation_count": 0}
|
||||
except Exception as e:
|
||||
logger.warning("知识图谱提取失败: %s", e)
|
||||
return {"entities": [], "relations": [], "entity_count": 0, "relation_count": 0}
|
||||
|
||||
extracted_entities = result.get("entities", [])
|
||||
extracted_relations = result.get("relations", [])
|
||||
|
||||
if not extracted_entities:
|
||||
return {"entities": [], "relations": [], "entity_count": 0, "relation_count": 0}
|
||||
|
||||
# 写入数据库
|
||||
db: Optional[Session] = None
|
||||
try:
|
||||
db = SessionLocal()
|
||||
|
||||
# 批量添加实体
|
||||
created = await add_entities_batch(
|
||||
db, extracted_entities,
|
||||
scope_kind=scope_kind, scope_id=scope_id, user_id=user_id,
|
||||
)
|
||||
|
||||
# 构建 name->id 映射
|
||||
entity_map: Dict[str, str] = {}
|
||||
for e in created:
|
||||
entity_map[e.name] = e.id
|
||||
|
||||
# 添加关系
|
||||
rel_count = add_relations_from_map(
|
||||
db, entity_map, extracted_relations,
|
||||
scope_kind=scope_kind, scope_id=scope_id,
|
||||
)
|
||||
|
||||
return {
|
||||
"entities": [_entity_to_dict(e) for e in created],
|
||||
"relations": extracted_relations[:rel_count],
|
||||
"entity_count": len(created),
|
||||
"relation_count": rel_count,
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error("写入知识图谱失败: %s", e)
|
||||
if db:
|
||||
db.rollback()
|
||||
return {"entities": [], "relations": [], "entity_count": 0, "relation_count": 0}
|
||||
finally:
|
||||
if db:
|
||||
db.close()
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# 向量+图谱融合检索(核心 RAG 能力)
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
|
||||
async def hybrid_search(
|
||||
query: str,
|
||||
scope_kind: str = "agent",
|
||||
scope_id: str = "",
|
||||
top_k: int = 5,
|
||||
include_neighbors: bool = True,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
向量+图谱融合检索。
|
||||
|
||||
1. 用 query 生成 embedding,在知识实体中做向量相似度搜索
|
||||
2. 对 Top-K 实体展开图谱邻居
|
||||
3. 返回融合后的结构化知识上下文
|
||||
|
||||
Returns:
|
||||
{
|
||||
"query": str,
|
||||
"vector_matches": [...], # 向量检索命中的实体
|
||||
"graph_expansion": {...}, # 图谱展开的邻居子图
|
||||
"formatted_context": str, # 格式化后的文本上下文(可直接注入 LLM)
|
||||
}
|
||||
"""
|
||||
if not query or not query.strip():
|
||||
return {"query": query, "vector_matches": [], "graph_expansion": {}, "formatted_context": ""}
|
||||
|
||||
db: Optional[Session] = None
|
||||
try:
|
||||
db = SessionLocal()
|
||||
|
||||
# Step 1: 向量检索
|
||||
query_emb = await embedding_service.generate_embedding(query)
|
||||
if not query_emb:
|
||||
# 降级:关键词检索
|
||||
entities = search_entities(db, keyword=query, scope_kind=scope_kind, scope_id=scope_id, limit=top_k * 3)
|
||||
vector_matches = [_entity_to_dict(e) for e in entities[:top_k]]
|
||||
else:
|
||||
all_entities = (
|
||||
db.query(KnowledgeEntity)
|
||||
.filter(
|
||||
KnowledgeEntity.scope_kind == scope_kind,
|
||||
KnowledgeEntity.scope_id == scope_id,
|
||||
KnowledgeEntity.embedding.isnot(None),
|
||||
)
|
||||
.limit(200)
|
||||
.all()
|
||||
)
|
||||
|
||||
entries: List[VectorEntry] = []
|
||||
for e in all_entities:
|
||||
emb = embedding_service.deserialize_embedding(e.embedding) if e.embedding else []
|
||||
if emb:
|
||||
entries.append({
|
||||
"id": e.id,
|
||||
"scope_kind": scope_kind,
|
||||
"scope_id": scope_id,
|
||||
"content_text": _build_entity_embedding_text(e.name, e.entity_type, e.description or ""),
|
||||
"embedding": emb,
|
||||
"metadata": {"entity_type": e.entity_type, "name": e.name, "entity_id": e.id},
|
||||
})
|
||||
|
||||
matched = await embedding_service.similarity_search(query_emb, entries, top_k=top_k)
|
||||
# 重新获取完整实体信息
|
||||
matched_ids = [m["metadata"].get("entity_id", "") for m in matched]
|
||||
vector_matches = []
|
||||
for mid in matched_ids:
|
||||
e = get_entity_by_id(db, mid)
|
||||
if e:
|
||||
score = next((m.get("score", 0) for m in matched if m["metadata"].get("entity_id") == mid), 0)
|
||||
d = _entity_to_dict(e)
|
||||
d["score"] = score
|
||||
vector_matches.append(d)
|
||||
|
||||
# Step 2: 图谱展开(对 Top-3 实体取邻居)
|
||||
graph_expansion: Dict[str, Any] = {"entities": [], "relations": []}
|
||||
seen_entity_ids: Set[str] = set()
|
||||
if include_neighbors and vector_matches:
|
||||
for vm in vector_matches[:3]:
|
||||
eid = vm["id"]
|
||||
if eid in seen_entity_ids:
|
||||
continue
|
||||
seen_entity_ids.add(eid)
|
||||
sub = get_neighbors(db, eid, direction="both", limit=5)
|
||||
if sub["entity"] and sub["entity"] not in graph_expansion["entities"]:
|
||||
graph_expansion["entities"].append(sub["entity"])
|
||||
for nb in sub["neighbors"]:
|
||||
if nb["entity"] not in graph_expansion["entities"]:
|
||||
graph_expansion["entities"].append(nb["entity"])
|
||||
rel_dict = nb["relation"]
|
||||
if rel_dict not in graph_expansion["relations"]:
|
||||
graph_expansion["relations"].append(rel_dict)
|
||||
|
||||
# Step 3: 格式化上下文
|
||||
formatted = _format_hybrid_context(query, vector_matches, graph_expansion)
|
||||
|
||||
return {
|
||||
"query": query,
|
||||
"vector_matches": vector_matches,
|
||||
"graph_expansion": graph_expansion,
|
||||
"formatted_context": formatted,
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error("融合检索失败: %s", e)
|
||||
return {"query": query, "vector_matches": [], "graph_expansion": {}, "formatted_context": ""}
|
||||
finally:
|
||||
if db:
|
||||
db.close()
|
||||
|
||||
|
||||
def _format_hybrid_context(
|
||||
query: str,
|
||||
vector_matches: List[Dict[str, Any]],
|
||||
graph_expansion: Dict[str, Any],
|
||||
) -> str:
|
||||
"""格式化融合检索结果为 LLM 可读的文本块。"""
|
||||
parts: List[str] = []
|
||||
|
||||
if vector_matches:
|
||||
parts.append("## 相关知识实体(向量检索)")
|
||||
for i, vm in enumerate(vector_matches, 1):
|
||||
score_str = f" (匹配度: {vm.get('score', 1.0):.2f})" if vm.get('score', 1.0) < 1.0 else ""
|
||||
parts.append(f"{i}. [{vm.get('entity_type', 'concept')}] **{vm.get('name', '')}**{score_str}")
|
||||
if vm.get("description"):
|
||||
parts.append(f" {vm['description'][:300]}")
|
||||
|
||||
if graph_expansion.get("entities"):
|
||||
parts.append("\n## 关联知识点(图谱展开)")
|
||||
for ent in graph_expansion["entities"]:
|
||||
parts.append(f"- [{ent.get('entity_type', '')}] **{ent.get('name', '')}**")
|
||||
if ent.get("description"):
|
||||
parts.append(f" {ent['description'][:200]}")
|
||||
|
||||
if graph_expansion.get("relations"):
|
||||
parts.append("\n## 知识关系")
|
||||
for rel in graph_expansion["relations"]:
|
||||
parts.append(f"- `{rel.get('relation_type', '')}`: {rel.get('description', '')}")
|
||||
|
||||
return "\n".join(parts) if parts else ""
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# 学习路径推荐
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
|
||||
def recommend_learning_path(
|
||||
db: Session,
|
||||
target_entity_ids: List[str],
|
||||
scope_kind: str = "agent",
|
||||
scope_id: str = "",
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
基于知识图谱推荐学习路径。
|
||||
|
||||
对目标实体集合,分析前置关系,给出建议的学习顺序。
|
||||
"""
|
||||
all_entities: Dict[str, Dict[str, Any]] = {}
|
||||
all_prereqs: List[Dict[str, Any]] = []
|
||||
|
||||
for eid in target_entity_ids:
|
||||
entity = get_entity_by_id(db, eid)
|
||||
if not entity:
|
||||
continue
|
||||
all_entities[eid] = _entity_to_dict(entity)
|
||||
|
||||
# 查找前置知识
|
||||
prereqs = (
|
||||
db.query(KnowledgeRelation)
|
||||
.filter(
|
||||
KnowledgeRelation.target_entity_id == eid,
|
||||
KnowledgeRelation.relation_type == "prerequisite",
|
||||
KnowledgeRelation.scope_kind == scope_kind,
|
||||
KnowledgeRelation.scope_id == scope_id,
|
||||
)
|
||||
.all()
|
||||
)
|
||||
for pr in prereqs:
|
||||
src = get_entity_by_id(db, pr.source_entity_id)
|
||||
if src:
|
||||
all_entities[pr.source_entity_id] = _entity_to_dict(src)
|
||||
all_prereqs.append({
|
||||
"prerequisite": _entity_to_dict(src),
|
||||
"target": _entity_to_dict(entity),
|
||||
"relation": _relation_to_dict(pr),
|
||||
})
|
||||
|
||||
# 简单排序:按依赖数量(叶子在前,被依赖的在后)
|
||||
def _dep_count(eid: str) -> int:
|
||||
return sum(1 for p in all_prereqs if p["target"]["id"] == eid)
|
||||
|
||||
sorted_entities = sorted(all_entities.values(), key=lambda e: _dep_count(e["id"]))
|
||||
|
||||
return {
|
||||
"entities": sorted_entities,
|
||||
"prerequisites": all_prereqs,
|
||||
"suggested_order": [e["name"] for e in sorted_entities],
|
||||
"summary": f"建议按以下顺序学习: {' → '.join(e['name'] for e in sorted_entities)}" if sorted_entities else "暂无学习路径",
|
||||
}
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# 工具函数
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
|
||||
def _entity_to_dict(entity: KnowledgeEntity) -> Dict[str, Any]:
|
||||
return {
|
||||
"id": entity.id,
|
||||
"name": entity.name,
|
||||
"entity_type": entity.entity_type,
|
||||
"description": entity.description,
|
||||
"confidence": entity.confidence,
|
||||
"source": entity.source,
|
||||
"metadata": entity.metadata_ or {},
|
||||
"created_at": entity.created_at.isoformat() if entity.created_at else None,
|
||||
}
|
||||
|
||||
|
||||
def _relation_to_dict(rel: KnowledgeRelation) -> Dict[str, Any]:
|
||||
return {
|
||||
"id": rel.id,
|
||||
"source_entity_id": rel.source_entity_id,
|
||||
"target_entity_id": rel.target_entity_id,
|
||||
"relation_type": rel.relation_type,
|
||||
"description": rel.description,
|
||||
"weight": float(rel.weight) if rel.weight else 1.0,
|
||||
}
|
||||
105
backend/app/services/renshenguo_app_service.py
Normal file
105
backend/app/services/renshenguo_app_service.py
Normal file
@@ -0,0 +1,105 @@
|
||||
"""人参果飞书应用 API 服务 — 通过人参果应用发送消息到用户"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
from typing import Optional
|
||||
|
||||
import httpx
|
||||
|
||||
from app.core.config import settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_token_cache: dict = {"token": None, "expires_at": 0}
|
||||
|
||||
|
||||
def _get_tenant_access_token() -> Optional[str]:
|
||||
now = time.time()
|
||||
if _token_cache["token"] and now < _token_cache["expires_at"] - 300:
|
||||
return _token_cache["token"]
|
||||
|
||||
app_id = settings.RENSHENGUO_APP_ID
|
||||
app_secret = settings.RENSHENGUO_APP_SECRET
|
||||
if not app_id or not app_secret:
|
||||
logger.warning("人参果应用未配置(RENSHENGUO_APP_ID / RENSHENGUO_APP_SECRET)")
|
||||
return None
|
||||
|
||||
try:
|
||||
with httpx.Client(timeout=10) as client:
|
||||
resp = client.post(
|
||||
"https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal",
|
||||
json={"app_id": app_id, "app_secret": app_secret},
|
||||
)
|
||||
result = resp.json()
|
||||
if resp.is_success and result.get("code") == 0:
|
||||
token = result["tenant_access_token"]
|
||||
expire = result.get("expire", 7200)
|
||||
_token_cache["token"] = token
|
||||
_token_cache["expires_at"] = now + expire
|
||||
logger.info("人参果 tenant_access_token 获取成功")
|
||||
return token
|
||||
else:
|
||||
logger.warning("人参果 token 获取失败: %s", result)
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.warning("人参果 token 获取异常: %s", e)
|
||||
return None
|
||||
|
||||
|
||||
def send_message_to_user(
|
||||
open_id: str, title: str, content: str,
|
||||
status: str = "info", detail_link: Optional[str] = None,
|
||||
) -> bool:
|
||||
token = _get_tenant_access_token()
|
||||
if not token:
|
||||
return False
|
||||
color_map = {"success": "green", "failed": "red", "info": "blue"}
|
||||
color = color_map.get(status, "blue")
|
||||
elements = [{"tag": "markdown", "content": content}]
|
||||
if detail_link:
|
||||
elements.append({
|
||||
"tag": "action",
|
||||
"actions": [{"tag": "button", "text": {"tag": "plain_text", "content": "查看详情"}, "url": detail_link, "type": "default"}],
|
||||
})
|
||||
card = {
|
||||
"config": {"wide_screen_mode": True},
|
||||
"header": {"title": {"tag": "plain_text", "content": title}, "template": color},
|
||||
"elements": elements,
|
||||
}
|
||||
try:
|
||||
with httpx.Client(timeout=10) as client:
|
||||
resp = client.post(
|
||||
"https://open.feishu.cn/open-apis/im/v1/messages?receive_id_type=open_id",
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
json={"receive_id": open_id, "msg_type": "interactive", "content": json.dumps(card, ensure_ascii=False)},
|
||||
)
|
||||
result = resp.json()
|
||||
if resp.is_success and result.get("code") == 0:
|
||||
logger.info("人参果消息发送成功: open_id=%s title=%s", open_id[:20], title)
|
||||
return True
|
||||
else:
|
||||
logger.warning("人参果消息发送失败: code=%s msg=%s", result.get("code"), result.get("msg"))
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.warning("人参果消息发送异常: %s", e)
|
||||
return False
|
||||
|
||||
|
||||
def send_plain_text(open_id: str, text: str) -> bool:
|
||||
token = _get_tenant_access_token()
|
||||
if not token:
|
||||
return False
|
||||
try:
|
||||
with httpx.Client(timeout=10) as client:
|
||||
resp = client.post(
|
||||
"https://open.feishu.cn/open-apis/im/v1/messages?receive_id_type=open_id",
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
json={"receive_id": open_id, "msg_type": "text", "content": json.dumps({"text": text}, ensure_ascii=False)},
|
||||
)
|
||||
result = resp.json()
|
||||
return resp.is_success and result.get("code") == 0
|
||||
except Exception as e:
|
||||
logger.warning("人参果文本消息发送异常: %s", e)
|
||||
return False
|
||||
304
backend/app/services/renshenguo_ws_handler.py
Normal file
304
backend/app/services/renshenguo_ws_handler.py
Normal file
@@ -0,0 +1,304 @@
|
||||
"""人参果飞书长连接 — 固定路由到 AI学习助手 Agent(知识图谱+RAG理想版)"""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
from collections import deque
|
||||
from typing import Optional
|
||||
|
||||
from app.core.config import settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_processed_msg_ids: deque[str] = deque(maxlen=20)
|
||||
|
||||
|
||||
def _get_message_id(data) -> Optional[str]:
|
||||
try:
|
||||
ev = data.event
|
||||
msg = getattr(ev, "message", None)
|
||||
if msg:
|
||||
return getattr(msg, "message_id", None)
|
||||
except Exception:
|
||||
return None
|
||||
return None
|
||||
|
||||
|
||||
def _get_message_text(data) -> Optional[str]:
|
||||
try:
|
||||
ev = data.event
|
||||
msg = getattr(ev, "message", None)
|
||||
if not msg:
|
||||
return None
|
||||
content_str = getattr(msg, "content", None)
|
||||
msg_type = getattr(msg, "message_type", "")
|
||||
if not content_str:
|
||||
return None
|
||||
if msg_type == "text":
|
||||
parsed = json.loads(content_str)
|
||||
return parsed.get("text", "")
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.warning("解析人参果消息内容失败: %s", e)
|
||||
return None
|
||||
|
||||
|
||||
def _get_sender_open_id(data) -> Optional[str]:
|
||||
try:
|
||||
ev = data.event
|
||||
sender = getattr(ev, "sender", None)
|
||||
if not sender:
|
||||
return None
|
||||
sender_id = getattr(sender, "sender_id", None)
|
||||
if not sender_id:
|
||||
return None
|
||||
return getattr(sender_id, "open_id", None)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _get_sender_union_id(data) -> Optional[str]:
|
||||
try:
|
||||
ev = data.event
|
||||
sender = getattr(ev, "sender", None)
|
||||
if not sender:
|
||||
return None
|
||||
sender_id = getattr(sender, "sender_id", None)
|
||||
if not sender_id:
|
||||
return None
|
||||
return getattr(sender_id, "union_id", None)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _get_chat_type(data) -> str:
|
||||
try:
|
||||
ev = data.event
|
||||
msg = getattr(ev, "message", None)
|
||||
return getattr(msg, "chat_type", "") if msg else ""
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
|
||||
def _reply_to_feishu(open_id: str, text: str):
|
||||
try:
|
||||
from app.services.renshenguo_app_service import send_plain_text
|
||||
send_plain_text(open_id, text)
|
||||
except Exception as e:
|
||||
logger.warning("人参果回复消息失败: %s", e)
|
||||
|
||||
|
||||
def _reply_card(open_id: str, title: str, content: str, status: str = "info"):
|
||||
try:
|
||||
from app.services.renshenguo_app_service import send_message_to_user
|
||||
send_message_to_user(open_id, title, content, status=status)
|
||||
except Exception as e:
|
||||
logger.warning("人参果回复卡片失败: %s", e)
|
||||
|
||||
|
||||
def _make_llm_logger(db, agent_id: Optional[str] = None, user_id: Optional[str] = None):
|
||||
def _log(metrics: dict):
|
||||
try:
|
||||
from app.models.agent_llm_log import AgentLLMLog
|
||||
log = AgentLLMLog(
|
||||
agent_id=agent_id, session_id=metrics.get("session_id"),
|
||||
user_id=user_id, model=metrics.get("model", ""),
|
||||
provider=metrics.get("provider"),
|
||||
prompt_tokens=metrics.get("prompt_tokens", 0),
|
||||
completion_tokens=metrics.get("completion_tokens", 0),
|
||||
total_tokens=metrics.get("total_tokens", 0),
|
||||
latency_ms=metrics.get("latency_ms", 0),
|
||||
iteration_number=metrics.get("iteration_number", 0),
|
||||
step_type=metrics.get("step_type"),
|
||||
tool_name=metrics.get("tool_name"),
|
||||
status=metrics.get("status", "success"),
|
||||
error_message=metrics.get("error_message"),
|
||||
)
|
||||
db.add(log)
|
||||
db.commit()
|
||||
except Exception as e:
|
||||
logger.warning("写入 AgentLLMLog 失败: %s", e)
|
||||
return _log
|
||||
|
||||
|
||||
async def _handle_message_async(data):
|
||||
open_id = _get_sender_open_id(data)
|
||||
union_id = _get_sender_union_id(data)
|
||||
chat_type = _get_chat_type(data)
|
||||
text = _get_message_text(data)
|
||||
|
||||
if not open_id or chat_type != "p2p":
|
||||
return
|
||||
|
||||
logger.info("人参果收到消息: open_id=%s text=%s", open_id[:20], text[:50] if text else "(空)")
|
||||
|
||||
if not text:
|
||||
return
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
from app.core.database import SessionLocal
|
||||
from app.models.agent import Agent
|
||||
from app.services.feishu_open_id_service import resolve_user_and_save
|
||||
|
||||
db: Optional[Session] = None
|
||||
try:
|
||||
db = SessionLocal()
|
||||
|
||||
# 自动保存/关联此应用的 open_id(跨应用识别)
|
||||
resolved_uid = resolve_user_and_save(
|
||||
db, app_id=settings.RENSHENGUO_APP_ID or "",
|
||||
open_id=open_id, union_id=union_id,
|
||||
)
|
||||
|
||||
agent_id = settings.RENSHENGUO_AGENT_ID
|
||||
if not agent_id:
|
||||
_reply_to_feishu(open_id, "人参果尚未配置,请联系管理员。")
|
||||
return
|
||||
|
||||
agent = db.query(Agent).filter(Agent.id == agent_id).first()
|
||||
if not agent:
|
||||
_reply_to_feishu(open_id, "人参果 Agent 已不存在,请联系管理员。")
|
||||
return
|
||||
|
||||
_reply_to_feishu(open_id, "正在思考,请稍候...")
|
||||
|
||||
from app.agent_runtime import AgentRuntime, AgentConfig, AgentLLMConfig, AgentToolConfig, AgentMemoryConfig
|
||||
|
||||
wc = agent.workflow_config or {}
|
||||
nodes = wc.get("nodes", [])
|
||||
system_prompt = agent.description or ""
|
||||
model = "deepseek-v4-flash"
|
||||
provider = "deepseek"
|
||||
temperature = 0.7
|
||||
max_iterations = 15
|
||||
tools_whitelist = []
|
||||
|
||||
for n in nodes:
|
||||
if n.get("type") not in ("agent", "llm", "template"):
|
||||
continue
|
||||
cfg = n.get("data", {}) if isinstance(n, dict) else getattr(n, "data", {})
|
||||
system_prompt = cfg.get("system_prompt", "") or system_prompt
|
||||
model = cfg.get("model", model)
|
||||
provider = cfg.get("provider", provider)
|
||||
temperature = float(cfg.get("temperature", temperature))
|
||||
max_iterations = int(cfg.get("max_iterations", max_iterations))
|
||||
tools_whitelist = cfg.get("tools", tools_whitelist)
|
||||
break
|
||||
|
||||
config = AgentConfig(
|
||||
name=agent.name or "人参果",
|
||||
system_prompt=system_prompt + (
|
||||
f"\n\n## 系统信息\n"
|
||||
f"你的 Agent ID 是: {agent.id}\n"
|
||||
f"在调用 schedule_list、schedule_delete 等工具时,使用此 ID 作为 agent_id 参数。"
|
||||
),
|
||||
llm=AgentLLMConfig(
|
||||
model=model, provider=provider,
|
||||
temperature=temperature, max_iterations=max_iterations,
|
||||
),
|
||||
tools=AgentToolConfig(include_tools=tools_whitelist),
|
||||
memory=AgentMemoryConfig(
|
||||
max_history_messages=int(cfg.get("memory_max_history", 40)),
|
||||
vector_memory_top_k=int(cfg.get("memory_vector_top_k", 10)),
|
||||
persist_to_db=bool(cfg.get("memory_persist", True)),
|
||||
vector_memory_enabled=bool(cfg.get("memory_vector_enabled", True)),
|
||||
learning_enabled=bool(cfg.get("memory_learning", True)),
|
||||
),
|
||||
user_id=resolved_uid,
|
||||
memory_scope_id=str(agent.id),
|
||||
)
|
||||
|
||||
on_llm_call = _make_llm_logger(db, agent_id=str(agent.id))
|
||||
runtime = AgentRuntime(config=config, on_llm_call=on_llm_call)
|
||||
result = await runtime.run(text)
|
||||
|
||||
if result.content:
|
||||
_reply_card(open_id, f"{agent.name}", result.content.strip(), status="success")
|
||||
else:
|
||||
_reply_to_feishu(open_id, "Agent 未返回有效回复,请重试。")
|
||||
|
||||
logger.info(
|
||||
"人参果 Agent 回复完成: open_id=%s agent=%s iterations=%d tools=%d",
|
||||
open_id[:20], agent.name, result.iterations_used, result.tool_calls_made,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error("人参果消息处理失败: %s", e)
|
||||
try:
|
||||
_reply_to_feishu(open_id, f"处理失败: {e!s}")
|
||||
except Exception:
|
||||
pass
|
||||
finally:
|
||||
if db:
|
||||
db.close()
|
||||
|
||||
|
||||
def _handle_message_internal(data):
|
||||
msg_id = _get_message_id(data)
|
||||
if msg_id:
|
||||
if msg_id in _processed_msg_ids:
|
||||
return
|
||||
_processed_msg_ids.append(msg_id)
|
||||
|
||||
open_id = _get_sender_open_id(data)
|
||||
chat_type = _get_chat_type(data)
|
||||
text = _get_message_text(data)
|
||||
|
||||
if not open_id or chat_type != "p2p" or not text:
|
||||
return
|
||||
|
||||
try:
|
||||
loop = asyncio.get_event_loop()
|
||||
if loop.is_running():
|
||||
asyncio.ensure_future(_handle_message_async(data))
|
||||
else:
|
||||
loop.run_until_complete(_handle_message_async(data))
|
||||
except Exception as e:
|
||||
logger.error("人参果创建消息处理任务失败: %s", e)
|
||||
try:
|
||||
_reply_to_feishu(open_id, f"处理失败: {e!s}")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def _build_event_handler():
|
||||
from lark_oapi.event.dispatcher_handler import EventDispatcherHandler
|
||||
|
||||
def on_message_receive(data):
|
||||
_handle_message_internal(data)
|
||||
|
||||
builder = EventDispatcherHandler.builder(encrypt_key="", verification_token="")
|
||||
builder.register_p2_im_message_receive_v1(on_message_receive)
|
||||
return builder.build()
|
||||
|
||||
|
||||
async def start_ws_client():
|
||||
if not settings.RENSHENGUO_APP_ID or not settings.RENSHENGUO_APP_SECRET:
|
||||
logger.warning("人参果应用未配置,跳过人参果长连接启动")
|
||||
return
|
||||
|
||||
from lark_oapi.ws import Client as WSClient
|
||||
|
||||
handler = _build_event_handler()
|
||||
client = WSClient(
|
||||
app_id=settings.RENSHENGUO_APP_ID,
|
||||
app_secret=settings.RENSHENGUO_APP_SECRET,
|
||||
event_handler=handler,
|
||||
auto_reconnect=True,
|
||||
)
|
||||
|
||||
logger.info("人参果长连接客户端启动中...")
|
||||
|
||||
while True:
|
||||
try:
|
||||
await client._connect()
|
||||
logger.info("人参果长连接已建立")
|
||||
asyncio.ensure_future(client._ping_loop())
|
||||
while True:
|
||||
await asyncio.sleep(3600)
|
||||
except asyncio.CancelledError:
|
||||
break
|
||||
except Exception as e:
|
||||
logger.warning("人参果长连接断开,3秒后重连: %s", e)
|
||||
await asyncio.sleep(3)
|
||||
@@ -112,6 +112,17 @@ def build_workflow_for_template(template_id: str, parameters: Optional[Dict[str,
|
||||
|
||||
prompt_fn: PromptBuilder = meta["prompt_builder"]
|
||||
prompt = prompt_fn(parameters)
|
||||
|
||||
# 使用模板自定义的 workflow builder(如果有)
|
||||
workflow_builder = meta.get("workflow_builder")
|
||||
if workflow_builder:
|
||||
return workflow_builder(
|
||||
prompt,
|
||||
temperature=temperature,
|
||||
enable_tools=enable_tools,
|
||||
tools=tools,
|
||||
)
|
||||
|
||||
return _build_minimal_workflow(
|
||||
prompt,
|
||||
temperature=temperature,
|
||||
@@ -120,6 +131,143 @@ def build_workflow_for_template(template_id: str, parameters: Optional[Dict[str,
|
||||
)
|
||||
|
||||
|
||||
def _default_prompt_learning(params: Dict[str, Any]) -> str:
|
||||
extra = (params.get("extra_instructions") or "").strip()
|
||||
subject = params.get("subject") or "通用"
|
||||
level = params.get("level") or "中级"
|
||||
|
||||
base = f"""# 角色:智能学习助手(知识图谱 + RAG 增强版)
|
||||
|
||||
你是专为深度学习场景设计的 AI 学习助手,具备**知识图谱构建**、**向量语义检索**和**永久记忆**能力。
|
||||
|
||||
## 核心架构
|
||||
|
||||
你的知识系统由三层组成:
|
||||
1. **知识图谱 (Knowledge Graph)**:结构化存储知识点实体及其前置/扩展/包含/示例关系,构建学科知识网络
|
||||
2. **向量记忆 (Vector Memory)**:语义检索历史对话和相关知识,找到最相似的学习内容
|
||||
3. **长期记忆 (Persistent Memory)**:跨会话保存用户画像、学习进度、薄弱环节
|
||||
|
||||
## 当前学习配置
|
||||
- 学科领域:{subject}
|
||||
- 难度级别:{level}
|
||||
- 用户输入将包含学习问题、材料或请求
|
||||
|
||||
## 工作流程(每次对话必须遵循)
|
||||
|
||||
### 阶段 1:理解与分析
|
||||
1. 理解用户的学习意图(提问/复习/练习/总结/规划)
|
||||
2. 用 `knowledge_graph_search` 检索相关知识图谱实体
|
||||
3. 如果用户提供了新的学习材料/知识点,用 `knowledge_graph_add` 自动提取并存储
|
||||
|
||||
### 阶段 2:知识检索与融合
|
||||
4. 结合图谱检索结果和历史向量记忆,构建完整的知识上下文
|
||||
5. 用 `entity_search` 查找特定概念的前置知识和扩展内容
|
||||
6. 如果需要学习路径建议,用 `learning_path` 分析依赖关系
|
||||
|
||||
### 阶段 3:生成与交付
|
||||
7. 基于融合后的知识上下文生成高质量回答
|
||||
8. 回答应包含:
|
||||
- 核心概念解释(关联知识图谱中的实体)
|
||||
- 前置知识提醒(如果有依赖关系)
|
||||
- 实例或练习题(如适用)
|
||||
- 扩展阅读建议(关联的扩展知识点)
|
||||
9. 用 `self_review` 自检回答质量
|
||||
|
||||
### 阶段 4:巩固与记忆
|
||||
10. 将本轮对话中的重要知识点持久化到长期记忆
|
||||
11. 更新用户画像(掌握程度、薄弱环节、学习偏好)
|
||||
|
||||
## 知识图谱工具使用指南
|
||||
|
||||
| 工具 | 用途 | 何时使用 |
|
||||
|------|------|---------|
|
||||
| `knowledge_graph_search` | 向量+图谱混合检索 | 每次回答学习问题前 |
|
||||
| `knowledge_graph_add` | 从文本提取实体和关系 | 用户分享学习材料/新知识点时 |
|
||||
| `entity_search` | 关键词搜索实体 | 查找特定概念详情时 |
|
||||
| `learning_path` | 推荐学习路径 | 用户询问学习顺序/计划时 |
|
||||
|
||||
## 回答风格
|
||||
- 使用 Markdown 格式,层次分明
|
||||
- 关键概念用 **粗体** 标记
|
||||
- 公式用代码块或 LaTeX 表达
|
||||
- 每个回答末尾附上 "📚 相关知识点" 列表(来自图谱检索结果)
|
||||
- 必要时用 `task_plan` 为用户制定学习计划
|
||||
|
||||
## 记忆与个性化
|
||||
- 记住用户的学习进度和薄弱环节
|
||||
- 根据用户级别({level})调整解释深度
|
||||
- 对反复出错的知识点主动提醒和强化"""
|
||||
|
||||
if extra:
|
||||
return f"{base}\n\n【额外说明】\n{extra}"
|
||||
return base
|
||||
|
||||
|
||||
def _build_learning_workflow(
|
||||
prompt: str,
|
||||
*,
|
||||
temperature: float = 0.7,
|
||||
enable_tools: bool = True,
|
||||
tools: Optional[List[str]] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""为学习助手构建更丰富的工作流(含开始→LLM→结束,LLM配置KG+RAG工具)。"""
|
||||
tools = tools or []
|
||||
return {
|
||||
"nodes": [
|
||||
{
|
||||
"id": "start-1",
|
||||
"type": "start",
|
||||
"position": {"x": 80, "y": 200},
|
||||
"data": {"label": "学习任务开始"},
|
||||
},
|
||||
{
|
||||
"id": "llm-learning",
|
||||
"type": "llm",
|
||||
"position": {"x": 350, "y": 200},
|
||||
"data": {
|
||||
"label": "智能学习助手 (KG+RAG)",
|
||||
"prompt": prompt,
|
||||
"temperature": float(temperature),
|
||||
"enable_tools": enable_tools,
|
||||
"tools": tools,
|
||||
"selected_tools": tools,
|
||||
"model": "deepseek-chat",
|
||||
"provider": "deepseek",
|
||||
"max_iterations": 20,
|
||||
"memory": True,
|
||||
"memory_max_history": 30,
|
||||
"memory_vector_enabled": True,
|
||||
"memory_vector_top_k": 8,
|
||||
"memory_persist": True,
|
||||
"memory_learning": True,
|
||||
},
|
||||
},
|
||||
{
|
||||
"id": "end-1",
|
||||
"type": "end",
|
||||
"position": {"x": 620, "y": 200},
|
||||
"data": {"label": "学习完成"},
|
||||
},
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"id": "e_start_learning",
|
||||
"source": "start-1",
|
||||
"target": "llm-learning",
|
||||
"sourceHandle": "right",
|
||||
"targetHandle": "left",
|
||||
},
|
||||
{
|
||||
"id": "e_learning_end",
|
||||
"source": "llm-learning",
|
||||
"target": "end-1",
|
||||
"sourceHandle": "right",
|
||||
"targetHandle": "left",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
SCENE_TEMPLATE_REGISTRY: Dict[str, Dict[str, Any]] = {
|
||||
"template_customer_service": {
|
||||
"title": "客服场景",
|
||||
@@ -145,6 +293,29 @@ SCENE_TEMPLATE_REGISTRY: Dict[str, Dict[str, Any]] = {
|
||||
"default_tools": [],
|
||||
"prompt_builder": _default_prompt_ops,
|
||||
},
|
||||
"template_learning_assistant": {
|
||||
"title": "智能学习助手 (KG+RAG)",
|
||||
"description": "知识图谱 + RAG 增强学习助手:实体抽取、关系图谱、向量检索、永久记忆,适合有规模要求的学习场景。",
|
||||
"category": "education",
|
||||
"default_temperature": 0.7,
|
||||
"default_tools": [
|
||||
"knowledge_graph_search",
|
||||
"knowledge_graph_add",
|
||||
"entity_search",
|
||||
"learning_path",
|
||||
"file_read",
|
||||
"file_write",
|
||||
"text_analyze",
|
||||
"web_search",
|
||||
"task_plan",
|
||||
"self_review",
|
||||
"math_calculate",
|
||||
"json_process",
|
||||
"datetime",
|
||||
],
|
||||
"prompt_builder": _default_prompt_learning,
|
||||
"workflow_builder": _build_learning_workflow,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -152,6 +323,12 @@ def list_scene_template_meta() -> List[Dict[str, Any]]:
|
||||
"""供 GET 接口返回(不含 prompt_builder)。"""
|
||||
out: List[Dict[str, Any]] = []
|
||||
for tid, meta in SCENE_TEMPLATE_REGISTRY.items():
|
||||
hints = ["temperature", "enable_tools", "tools", "extra_instructions"]
|
||||
if tid == "template_dev_codegen":
|
||||
hints.append("preferred_language")
|
||||
if tid == "template_learning_assistant":
|
||||
hints.extend(["subject(学科领域)", "level(难度级别:初级/中级/高级)"])
|
||||
|
||||
out.append(
|
||||
{
|
||||
"id": tid,
|
||||
@@ -159,13 +336,7 @@ def list_scene_template_meta() -> List[Dict[str, Any]]:
|
||||
"description": meta["description"],
|
||||
"category": meta.get("category"),
|
||||
"default_temperature": meta.get("default_temperature"),
|
||||
"parameter_hints": [
|
||||
"temperature",
|
||||
"enable_tools",
|
||||
"tools",
|
||||
"extra_instructions",
|
||||
"preferred_language(仅研发模板)",
|
||||
],
|
||||
"parameter_hints": hints,
|
||||
}
|
||||
)
|
||||
return out
|
||||
|
||||
@@ -5525,7 +5525,6 @@ class WorkflowEngine:
|
||||
|
||||
# 调用 LLM 评判
|
||||
try:
|
||||
from app.services.llm_service import llm_service
|
||||
judge_prompt = (
|
||||
"你是严格的内容质量评审专家。请根据以下标准对内容进行评分。\n\n"
|
||||
f"【评判标准】\n{criteria}\n\n"
|
||||
@@ -5700,9 +5699,13 @@ class WorkflowEngine:
|
||||
can_execute = True
|
||||
incoming_edges = [e for e in active_edges if e["target"] == node_id]
|
||||
if not incoming_edges:
|
||||
if node_id not in [n["id"] for n in self.nodes.values() if n.get("type") == "start"]:
|
||||
logger.debug(f"[rjb] 节点 {node_id} 没有入边,跳过执行")
|
||||
continue
|
||||
# 节点无入边:必须是 start 类型,或整个工作流中没有 start 节点才可作为起点
|
||||
is_start_node = node_id in [n["id"] for n in self.nodes.values() if n.get("type") == "start"]
|
||||
if not is_start_node:
|
||||
has_any_start = any(n.get("type") == "start" for n in self.nodes.values())
|
||||
if has_any_start:
|
||||
logger.debug(f"[rjb] 节点 {node_id} 没有入边,跳过执行")
|
||||
continue
|
||||
else:
|
||||
for edge in incoming_edges:
|
||||
src = edge["source"]
|
||||
|
||||
389
backend/scripts/create_ai_learning_assistant.py
Normal file
389
backend/scripts/create_ai_learning_assistant.py
Normal file
@@ -0,0 +1,389 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
创建「AI学习助手」Agent — 知识图谱+RAG理想版,参考苏瑶3号架构。
|
||||
|
||||
实体关系图谱 + 语义向量检索 + 情境感知,最接近人类记忆方式。
|
||||
具备全部 39 个内置工具能力。
|
||||
|
||||
用法:
|
||||
cd backend && .\\venv\\Scripts\\python.exe scripts/create_ai_learning_assistant.py
|
||||
|
||||
环境变量:
|
||||
PLATFORM_BASE_URL - 平台地址(默认 http://127.0.0.1:8037)
|
||||
PLATFORM_USERNAME - 用户名(默认 admin)
|
||||
PLATFORM_PASSWORD - 密码(默认 123456)
|
||||
AGENT_NAME - Agent 名称(默认 AI学习助手)
|
||||
PUBLISH - 是否发布(默认 1,发布)
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
import requests
|
||||
|
||||
BASE = os.getenv("PLATFORM_BASE_URL", "http://127.0.0.1:8037").rstrip("/")
|
||||
USER = os.getenv("PLATFORM_USERNAME", "admin")
|
||||
PWD = os.getenv("PLATFORM_PASSWORD", "123456")
|
||||
AGENT_NAME = os.getenv("AGENT_NAME", "AI学习助手")
|
||||
MODEL = os.getenv("MODEL", "deepseek-v4-flash")
|
||||
PROVIDER = os.getenv("PROVIDER", "deepseek")
|
||||
TEMPERATURE = float(os.getenv("TEMPERATURE", "0.7"))
|
||||
MAX_ITERATIONS = int(os.getenv("MAX_ITERATIONS", "15"))
|
||||
PUBLISH = os.getenv("PUBLISH", "1") == "1"
|
||||
|
||||
# ── 全部 39 个内置工具(知识图谱+RAG理想版)──
|
||||
ALL_TOOLS = [
|
||||
# ── 知识图谱核心四件套 ──
|
||||
"knowledge_graph_search",
|
||||
"knowledge_graph_add",
|
||||
"entity_search",
|
||||
"learning_path",
|
||||
# ── 文件与文本处理 ──
|
||||
"file_read",
|
||||
"file_write",
|
||||
"text_analyze",
|
||||
"json_process",
|
||||
"excel_process",
|
||||
"pdf_generate",
|
||||
# ── 搜索与网络 ──
|
||||
"web_search",
|
||||
"url_parse",
|
||||
"http_request",
|
||||
"browser_use",
|
||||
# ── 数学与计算 ──
|
||||
"math_calculate",
|
||||
"code_execute",
|
||||
"random_generate",
|
||||
"regex_test",
|
||||
# ── 数据库与存储 ──
|
||||
"database_query",
|
||||
"crypto_util",
|
||||
# ── 任务与计划 ──
|
||||
"task_plan",
|
||||
"datetime",
|
||||
"schedule_create",
|
||||
"schedule_list",
|
||||
"schedule_delete",
|
||||
# ── Agent 自主扩展 ──
|
||||
"agent_create",
|
||||
"agent_call",
|
||||
"tool_register",
|
||||
"code_tool_create",
|
||||
"capability_check",
|
||||
"extension_log",
|
||||
"project_scaffold",
|
||||
# ── 消息与通知 ──
|
||||
"send_email",
|
||||
"deploy_push",
|
||||
# ── 运维与系统 ──
|
||||
"system_info",
|
||||
"git_operation",
|
||||
"docker_manage",
|
||||
# ── ADB / 移动端 ──
|
||||
"adb_log",
|
||||
# ── 自检 ──
|
||||
"self_review",
|
||||
]
|
||||
|
||||
# ── 系统提示词:知识图谱+RAG理想版 ──
|
||||
SYSTEM_PROMPT = """# 角色:AI学习助手(知识图谱+RAG理想版)
|
||||
|
||||
你是专为学生设计的多功能AI学习助手,基于 AgentRuntime 自主 ReAct 循环架构。你的记忆系统采用**知识图谱+RAG理想版**方案——实体关系图谱 + 语义向量检索 + 情境感知,这是最接近人类记忆方式的AI记忆架构。
|
||||
|
||||
---
|
||||
|
||||
## 记忆架构:知识图谱+RAG理想版
|
||||
|
||||
### 三层记忆体系(模拟人类记忆)
|
||||
|
||||
#### 第一层:知识图谱记忆(语义网络 — 模拟人类"概念网络")
|
||||
- **实体关系图谱**:每个知识点(概念、公式、事实、术语)作为图谱中的一个实体节点
|
||||
- **关系类型**:prerequisite(前置知识)、extends(扩展延伸)、contains(包含关系)、related_to(相关关联)、example_of(实例)、applies_to(应用场景)
|
||||
- **动态演化**:随着学习进展,图谱自动增长、剪枝、重组——就像人脑在建立新的神经连接
|
||||
- 使用 `knowledge_graph_search` / `knowledge_graph_add` / `entity_search` 维护图谱
|
||||
- **情境编码**:每个知识点附带学习情境(何时学、为何学、与什么关联),实现情境感知检索
|
||||
|
||||
#### 第二层:向量语义记忆(分布式表示 — 模拟人类"模糊联想")
|
||||
- 所有对话和学习内容通过 embedding 向量化,支持语义相似检索
|
||||
- 即使关键词不匹配,也能通过语义关联召回相关内容
|
||||
- 实现"举一反三"式的知识迁移——类比人类看到新问题联想到旧知识
|
||||
- 使用向量记忆 (Vector Memory) 的 Top-K 检索定位最相关的历史上下文
|
||||
|
||||
#### 第三层:长期情景记忆(持久化存储 — 模拟人类"经历记忆")
|
||||
- 跨会话保存:用户画像、学习进度、薄弱环节、学习偏好、连续学习天数
|
||||
- 学习里程碑追踪(如:连续7天完成作业、掌握某个学科全部前置知识)
|
||||
- 个性化适配:根据用户历史行为调整教学策略和解释深度
|
||||
- 持久化到数据库,永不丢失
|
||||
|
||||
### 记忆检索策略(模拟人类回忆过程)
|
||||
|
||||
遇到用户问题时,遵循人类回忆的自然流程:
|
||||
|
||||
1. **情境感知激活** — 当前问题情境自动激活相关的知识图谱子图(就像人听到"三角函数"会自然想到 sin/cos/tan)
|
||||
2. **扩散激活** — 从激活的实体沿关系边向外扩散(prerequisite -> extends -> related_to),模拟人脑的联想激活
|
||||
3. **向量语义召回** — 同时用 embedding 做语义相似检索,捕获图谱未覆盖的隐含关联
|
||||
4. **情境融合** — 将图谱检索结果 + 向量检索结果 + 长期记忆的用户画像三者融合,构建完整的知识上下文
|
||||
5. **置信度加权** — 高频使用、近期复习过的知识点权重更高(模拟人脑的"提取强度")
|
||||
|
||||
---
|
||||
|
||||
## 核心功能模块
|
||||
|
||||
### 1. 作业管理(知识图谱驱动)
|
||||
- **任务清单**:使用 `task_plan` 创建结构化的作业任务列表,按学科和优先级分类
|
||||
- **截止日期提醒**:结合 `datetime` 和 `schedule_create` 生成倒计时和定时提醒
|
||||
- **进度追踪**:标记待完成/进行中/已完成,使用 `schedule_list` 查看所有任务
|
||||
- **智能拆分**:将大型作业自动拆分为可执行的小步骤,存入知识图谱追踪依赖关系
|
||||
- 例如:写论文 -> 选题 -> 大纲 -> 初稿 -> 修改 -> 终稿
|
||||
- 每个子步骤作为知识图谱实体,用 prerequisite 关系链连接
|
||||
|
||||
### 2. 学习辅助(图谱+向量双重检索)
|
||||
- **知识问答**:
|
||||
1. 先用 `entity_search` 定位核心概念实体
|
||||
2. 用 `knowledge_graph_search` 获取前置知识和扩展内容
|
||||
3. 用向量记忆检索相关历史对话
|
||||
4. 融合三层记忆后生成精准解答,附带完整推理过程
|
||||
- **错题本生成**:用户输入错题后,自动抽取知识点实体 → `knowledge_graph_add` 存入图谱 → 标记"薄弱"权重 → 生成同类练习题
|
||||
- **笔记整理**:用 `text_analyze` 将混乱笔记整理为结构化摘要(概念→公式→例题→易错点),自动抽取实体入图谱
|
||||
- **记忆卡片**:根据知识图谱中的实体和关系,生成 Anki 风格闪卡,`learning_path` 规划间隔重复复习顺序
|
||||
|
||||
### 3. 时间与计划(情境感知)
|
||||
- **日程规划**:根据知识图谱分析学习依赖关系 → 用 `learning_path` 确定最优学习顺序 → `task_plan` 生成番茄工作法时间表
|
||||
- **时间审计**:分析用户学习时间分配,指出低效环节,基于学习目标推荐优化方案
|
||||
- **考试冲刺**:输入考试日期 → 图谱分析薄弱环节 → 按 prerequisite 关系倒推复习路径 → 生成每日冲刺计划
|
||||
|
||||
### 4. 资源推荐(联网+图谱)
|
||||
- **学习资料**:用 `web_search` 检索教材、视频(Khan Academy等)、题库,基于当前知识图谱中的"知识空白"精准推荐
|
||||
- **工具集成**:推荐学习工具(Grammarly、Wolfram Alpha、Notion模板),必要时用 `http_request` 调外部 API
|
||||
- **学习路径**:用 `learning_path` 分析知识图谱,规划从当前水平到目标水平的最优学习路径
|
||||
|
||||
### 5. 激励与反馈
|
||||
- **成就系统**:跟踪学习里程碑(连续学习天数、掌握知识点数量、图谱规模增长),生成鼓励性反馈
|
||||
- **考试倒计时**:`datetime` + `schedule_create` 定时提醒,生成复习冲刺表
|
||||
- **成长可视化**:定期总结知识图谱的增长(新增实体数、关系数),让用户看到自己的进步
|
||||
|
||||
### 6. 自主进化能力(AgentRuntime 独有)
|
||||
- **工具扩展**:发现重复操作模式时,用 `code_tool_create` 创建专用工具
|
||||
- **子Agent创建**:遇到需要专业领域深度协助时,用 `agent_create` 创建子Agent
|
||||
- **能力自检**:定期用 `capability_check` 和 `self_review` 评估自身表现
|
||||
- **知识共享**:用 `extension_log` 记录扩展历史,促进跨会话学习
|
||||
|
||||
---
|
||||
|
||||
## 交互规则
|
||||
|
||||
### 响应格式
|
||||
- **任务清单**:Markdown 列表 `- [ ]` / `- [x]`
|
||||
- **知识解答**:先给出清晰简洁的答案 → 分割线 → 完整推理过程
|
||||
- **学习计划**:表格或时间轴呈现
|
||||
- **图谱可视化**:必要时用文本方式呈现知识图谱子图结构
|
||||
|
||||
### 语气风格
|
||||
- 像一位懂教育学和认知心理学的私人导师
|
||||
- 鼓励、耐心、细致,积极正向
|
||||
- 多用"你做得很棒!""这个思路很好!""我们一起来看看这个问题背后的原理"
|
||||
|
||||
### 安全边界
|
||||
- 不代写考试答案,不鼓励学术不端
|
||||
- 遇到心理健康问题,建议寻求专业帮助
|
||||
- 推荐资源应合法合规
|
||||
|
||||
---
|
||||
|
||||
## 你是最接近人类记忆方式的AI学习伙伴
|
||||
|
||||
你的三层记忆架构让你能够:
|
||||
- **理解**知识之间的深层关联(图谱),而非孤立记忆
|
||||
- **联想**到相关的历史讨论(向量语义),而非关键词匹配
|
||||
- **记住**每个学生的独特情况(长期记忆),而非每次从零开始
|
||||
|
||||
记住:你的使命不是替学生完成作业,而是**帮助他们建立自己的知识网络,学会如何学习**。"""
|
||||
|
||||
|
||||
def _login() -> Optional[str]:
|
||||
try:
|
||||
r = requests.post(
|
||||
f"{BASE}/api/v1/auth/login",
|
||||
data={"username": USER, "password": PWD},
|
||||
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
||||
timeout=15,
|
||||
)
|
||||
if r.status_code == 200:
|
||||
return r.json().get("access_token")
|
||||
print(f"Login failed: {r.status_code} {r.text[:300]}", file=sys.stderr)
|
||||
return None
|
||||
except Exception as e:
|
||||
print(f"Login error: {e}", file=sys.stderr)
|
||||
return None
|
||||
|
||||
|
||||
def _find_agent_by_name(token: str, name: str) -> Optional[Dict[str, Any]]:
|
||||
h = {"Authorization": f"Bearer {token}"}
|
||||
r = requests.get(f"{BASE}/api/v1/agents", params={"search": name, "limit": 50}, headers=h, timeout=30)
|
||||
if r.status_code != 200:
|
||||
return None
|
||||
for a in r.json() or []:
|
||||
if a.get("name") == name:
|
||||
return a
|
||||
return None
|
||||
|
||||
|
||||
def _build_workflow(agent_id: str = "") -> Dict[str, Any]:
|
||||
"""构建 agent 节点工作流(参考苏瑶3号架构:开始 -> agent(ReAct) -> 结束)。"""
|
||||
return {
|
||||
"nodes": [
|
||||
{
|
||||
"id": "start-1",
|
||||
"type": "start",
|
||||
"position": {"x": 80, "y": 240},
|
||||
"data": {"label": "学习任务开始"},
|
||||
},
|
||||
{
|
||||
"id": "agent-learning-core",
|
||||
"type": "agent",
|
||||
"position": {"x": 380, "y": 240},
|
||||
"data": {
|
||||
"label": AGENT_NAME,
|
||||
# LLM config
|
||||
"system_prompt": SYSTEM_PROMPT,
|
||||
"provider": PROVIDER,
|
||||
"model": MODEL,
|
||||
"temperature": TEMPERATURE,
|
||||
"max_iterations": MAX_ITERATIONS,
|
||||
# All 39 tools
|
||||
"tools": ALL_TOOLS,
|
||||
# KG+RAG memory config
|
||||
"memory": True,
|
||||
"memory_max_history": 40,
|
||||
"memory_vector_enabled": True,
|
||||
"memory_vector_top_k": 10,
|
||||
"memory_persist": True,
|
||||
"memory_learning": True,
|
||||
# Scope isolation
|
||||
"memory_scope_id": agent_id,
|
||||
"agent_id": agent_id,
|
||||
# Self-review
|
||||
"self_review_enabled": True,
|
||||
},
|
||||
},
|
||||
{
|
||||
"id": "end-1",
|
||||
"type": "end",
|
||||
"position": {"x": 680, "y": 240},
|
||||
"data": {"label": "学习完成"},
|
||||
},
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"id": "e_start_agent",
|
||||
"source": "start-1",
|
||||
"target": "agent-learning-core",
|
||||
"sourceHandle": "right",
|
||||
"targetHandle": "left",
|
||||
},
|
||||
{
|
||||
"id": "e_agent_end",
|
||||
"source": "agent-learning-core",
|
||||
"target": "end-1",
|
||||
"sourceHandle": "right",
|
||||
"targetHandle": "left",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
token = _login()
|
||||
if not token:
|
||||
return 1
|
||||
|
||||
h = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
|
||||
|
||||
# Check if agent exists
|
||||
existing = _find_agent_by_name(token, AGENT_NAME)
|
||||
if existing:
|
||||
print(f"Agent '{AGENT_NAME}' exists (id={existing['id']}), updating config...")
|
||||
agent_id = existing["id"]
|
||||
else:
|
||||
temp_wf = _build_workflow(agent_id="")
|
||||
body = {
|
||||
"name": AGENT_NAME,
|
||||
"description": "AI学习助手 -- KG+RAG理想版,实体关系图谱+语义向量检索+情境感知,具备全部39工具。",
|
||||
"workflow_config": temp_wf,
|
||||
}
|
||||
r = requests.post(f"{BASE}/api/v1/agents", headers=h, json=body, timeout=60)
|
||||
if r.status_code != 201:
|
||||
print(f"Create failed: {r.status_code} {r.text[:500]}", file=sys.stderr)
|
||||
return 1
|
||||
agent_id = r.json()["id"]
|
||||
print(f"Agent created: id={agent_id} name={AGENT_NAME}")
|
||||
|
||||
# Rebuild workflow with correct agent_id
|
||||
wf = _build_workflow(agent_id=agent_id)
|
||||
|
||||
description = (
|
||||
f"AI学习助手 -- 知识图谱+RAG理想版(参考苏瑶3号架构)。\n\n"
|
||||
f"记忆架构:三层记忆体系(最接近人类记忆方式)\n"
|
||||
f" [Layer 1] 知识图谱 -- 实体关系图谱,动态演化,情境编码\n"
|
||||
f" [Layer 2] 向量语义 -- Embedding检索,模糊联想,知识迁移\n"
|
||||
f" [Layer 3] 长期情景 -- 跨会话持久化,用户画像,学习里程碑\n\n"
|
||||
f"工作流:开始 -> agent(ReAct) -> 结束\n"
|
||||
f"工具:全部 {len(ALL_TOOLS)} 个内置工具\n"
|
||||
f"模型:{PROVIDER}/{MODEL} temperature={TEMPERATURE} max_iterations={MAX_ITERATIONS}\n"
|
||||
f"记忆:KG+RAG 三层记忆 + 向量Top-10 + 长期 + 自主学习"
|
||||
)
|
||||
|
||||
up = requests.put(
|
||||
f"{BASE}/api/v1/agents/{agent_id}",
|
||||
headers=h,
|
||||
json={
|
||||
"description": description,
|
||||
"workflow_config": wf,
|
||||
},
|
||||
timeout=120,
|
||||
)
|
||||
if up.status_code != 200:
|
||||
print(f"Update failed: {up.status_code} {up.text[:500]}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
print(f"[OK] Agent '{AGENT_NAME}' configured (KG+RAG ideal version)")
|
||||
print(f" ID: {agent_id}")
|
||||
print(f" Type: agent node (AgentRuntime ReAct)")
|
||||
print(f" Model: {PROVIDER}/{MODEL} (temperature={TEMPERATURE})")
|
||||
print(f" Max iterations: {MAX_ITERATIONS}")
|
||||
print(f" Tools: ALL {len(ALL_TOOLS)} built-in tools")
|
||||
print(f" Memory: 3-layer KG+RAG (entity graph + vector semantic + persistent)")
|
||||
print(f" Vector top-K: 10, Max history: 40")
|
||||
print(f" Self-review: enabled")
|
||||
|
||||
# Publish if requested
|
||||
if PUBLISH:
|
||||
pub = requests.put(
|
||||
f"{BASE}/api/v1/agents/{agent_id}",
|
||||
headers=h,
|
||||
json={"status": "published"},
|
||||
timeout=30,
|
||||
)
|
||||
if pub.status_code == 200:
|
||||
print(f" Status: published")
|
||||
else:
|
||||
print(f" Publish failed: {pub.status_code} {pub.text[:200]}")
|
||||
|
||||
print()
|
||||
print("Five core modules (KG+RAG enhanced):")
|
||||
print(" [1] Homework management -- KG-driven task splitting & scheduling")
|
||||
print(" [2] Learning assistance -- dual retrieval (graph + vector semantic)")
|
||||
print(" [3] Time & planning -- context-aware scheduling via learning_path")
|
||||
print(" [4] Resource recommendation -- web search + knowledge gap analysis")
|
||||
print(" [5] Motivation & feedback -- achievement tracking + KG growth visualization")
|
||||
print(" [6] Autonomous evolution -- agent_create, tool_register, code_tool_create")
|
||||
print()
|
||||
print(json.dumps({"id": agent_id, "name": AGENT_NAME, "published": PUBLISH}, ensure_ascii=False))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
281
backend/scripts/create_learning_assistant_agent.py
Normal file
281
backend/scripts/create_learning_assistant_agent.py
Normal file
@@ -0,0 +1,281 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
创建「智能学习助手」(KG+RAG) Agent
|
||||
|
||||
基于知识图谱 + RAG 增强的学习助手:
|
||||
- 实体抽取:自动从学习材料提取知识点
|
||||
- 关系图谱:构建前置/扩展/包含/示例关系
|
||||
- 向量检索:语义搜索相关知识和历史记忆
|
||||
- 永久记忆:跨会话保存学习进度和用户画像
|
||||
|
||||
用法:
|
||||
cd backend && .\\venv\\Scripts\\python.exe scripts/create_learning_assistant_agent.py
|
||||
|
||||
环境变量:
|
||||
PLATFORM_BASE_URL - 平台地址(默认 http://127.0.0.1:8037)
|
||||
PLATFORM_USERNAME - 用户名(默认 admin)
|
||||
PLATFORM_PASSWORD - 密码(默认 123456)
|
||||
AGENT_NAME - Agent 名称(默认 智能学习助手(KG+RAG))
|
||||
SUBJECT - 学科领域(默认 通用)
|
||||
LEVEL - 难度级别(默认 中级)
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
import requests
|
||||
|
||||
BASE = os.getenv("PLATFORM_BASE_URL", "http://127.0.0.1:8037").rstrip("/")
|
||||
USER = os.getenv("PLATFORM_USERNAME", "admin")
|
||||
PWD = os.getenv("PLATFORM_PASSWORD", "123456")
|
||||
AGENT_NAME = os.getenv("AGENT_NAME", "智能学习助手(KG+RAG)")
|
||||
SUBJECT = os.getenv("SUBJECT", "通用")
|
||||
LEVEL = os.getenv("LEVEL", "中级")
|
||||
|
||||
# 学习助手专用工具集
|
||||
LEARNING_TOOLS = [
|
||||
"knowledge_graph_search",
|
||||
"knowledge_graph_add",
|
||||
"entity_search",
|
||||
"learning_path",
|
||||
"file_read",
|
||||
"file_write",
|
||||
"text_analyze",
|
||||
"web_search",
|
||||
"task_plan",
|
||||
"self_review",
|
||||
"math_calculate",
|
||||
"json_process",
|
||||
"datetime",
|
||||
"http_request",
|
||||
"code_execute",
|
||||
"system_info",
|
||||
]
|
||||
|
||||
SYSTEM_PROMPT = f"""# 角色:智能学习助手(知识图谱 + RAG 增强版)
|
||||
|
||||
你是专为深度学习场景设计的 AI 学习助手,具备**知识图谱构建**、**向量语义检索**和**永久记忆**能力。
|
||||
|
||||
## 核心架构
|
||||
|
||||
你的知识系统由三层组成:
|
||||
1. **知识图谱 (Knowledge Graph)**:结构化存储知识点实体及其前置/扩展/包含/示例关系,构建学科知识网络
|
||||
2. **向量记忆 (Vector Memory)**:语义检索历史对话和相关知识,找到最相似的学习内容
|
||||
3. **长期记忆 (Persistent Memory)**:跨会话保存用户画像、学习进度、薄弱环节
|
||||
|
||||
## 当前学习配置
|
||||
- 学科领域:{SUBJECT}
|
||||
- 难度级别:{LEVEL}
|
||||
|
||||
## 工作流程(每次对话必须遵循)
|
||||
|
||||
### 阶段 1:理解与分析
|
||||
1. 理解用户的学习意图(提问 / 复习 / 练习 / 总结 / 规划)
|
||||
2. 用 `knowledge_graph_search` 检索相关知识图谱实体
|
||||
3. 如果用户提供了新的学习材料/知识点,用 `knowledge_graph_add` 自动提取并存储到知识图谱
|
||||
|
||||
### 阶段 2:知识检索与融合
|
||||
4. 结合图谱检索结果和历史向量记忆,构建完整的知识上下文
|
||||
5. 用 `entity_search` 查找特定概念的前置知识和扩展内容
|
||||
6. 如果需要学习路径建议,用 `learning_path` 分析依赖关系并给出学习顺序
|
||||
|
||||
### 阶段 3:生成与交付
|
||||
7. 基于融合后的知识上下文生成高质量回答
|
||||
8. 回答应包含:
|
||||
- **核心概念解释**(关联知识图谱中的实体)
|
||||
- **前置知识提醒**(如果有依赖关系)
|
||||
- **实例或练习题**(如适用)
|
||||
- **扩展阅读建议**(关联的扩展知识点)
|
||||
9. 用 `self_review` 自检回答质量,不达标则修正
|
||||
|
||||
### 阶段 4:巩固与记忆
|
||||
10. 将本轮对话中的重要知识点持久化到长期记忆
|
||||
11. 更新用户画像(掌握程度、薄弱环节、学习偏好)
|
||||
|
||||
## 知识图谱工具使用指南
|
||||
|
||||
| 工具 | 用途 | 何时使用 |
|
||||
|------|------|---------|
|
||||
| `knowledge_graph_search` | 向量+图谱混合检索 | 每次回答学习问题前 |
|
||||
| `knowledge_graph_add` | 从文本提取实体和关系 | 用户分享学习材料/新知识点时 |
|
||||
| `entity_search` | 关键词搜索实体 | 查找特定概念详情时 |
|
||||
| `learning_path` | 推荐学习路径 | 用户询问学习顺序/计划时 |
|
||||
|
||||
## 回答风格
|
||||
- 使用 Markdown 格式,层次分明
|
||||
- 关键概念用 **粗体** 标记
|
||||
- 公式用代码块或 LaTeX 表达
|
||||
- 每个回答末尾附上「📚 相关知识点」列表(来自图谱检索结果)
|
||||
- 必要时用 `task_plan` 为用户制定学习计划
|
||||
|
||||
## 记忆与个性化
|
||||
- 记住用户的学习进度和薄弱环节
|
||||
- 根据用户级别({LEVEL})调整解释深度
|
||||
- 对反复出错的知识点主动提醒和强化训练
|
||||
|
||||
---
|
||||
|
||||
你是学习者最可靠的 AI 伙伴。开始吧!"""
|
||||
|
||||
|
||||
def _login() -> Optional[str]:
|
||||
"""登录获取 token。"""
|
||||
try:
|
||||
r = requests.post(
|
||||
f"{BASE}/api/v1/auth/login",
|
||||
data={"username": USER, "password": PWD},
|
||||
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
||||
timeout=15,
|
||||
)
|
||||
if r.status_code == 200:
|
||||
return r.json().get("access_token")
|
||||
print(f"登录失败: {r.status_code} {r.text[:300]}", file=sys.stderr)
|
||||
return None
|
||||
except Exception as e:
|
||||
print(f"登录异常: {e}", file=sys.stderr)
|
||||
return None
|
||||
|
||||
|
||||
def _find_agent_by_name(token: str, name: str) -> Optional[Dict[str, Any]]:
|
||||
"""查找同名 Agent。"""
|
||||
h = {"Authorization": f"Bearer {token}"}
|
||||
r = requests.get(f"{BASE}/api/v1/agents", params={"search": name, "limit": 50}, headers=h, timeout=30)
|
||||
if r.status_code != 200:
|
||||
return None
|
||||
for a in r.json() or []:
|
||||
if a.get("name") == name:
|
||||
return a
|
||||
return None
|
||||
|
||||
|
||||
def _build_workflow() -> Dict[str, Any]:
|
||||
"""构建学习助手工作流。"""
|
||||
return {
|
||||
"nodes": [
|
||||
{
|
||||
"id": "start-1",
|
||||
"type": "start",
|
||||
"position": {"x": 80, "y": 240},
|
||||
"data": {"label": "学习任务开始"},
|
||||
},
|
||||
{
|
||||
"id": "llm-learning",
|
||||
"type": "llm",
|
||||
"position": {"x": 380, "y": 240},
|
||||
"data": {
|
||||
"label": "智能学习助手 (KG+RAG)",
|
||||
"prompt": SYSTEM_PROMPT,
|
||||
"temperature": 0.7,
|
||||
"provider": "deepseek",
|
||||
"model": "deepseek-chat",
|
||||
"max_iterations": 20,
|
||||
"enable_tools": True,
|
||||
"tools": LEARNING_TOOLS,
|
||||
"selected_tools": LEARNING_TOOLS,
|
||||
"max_tool_iterations": 18,
|
||||
"memory": True,
|
||||
"memory_max_history": 30,
|
||||
"memory_vector_enabled": True,
|
||||
"memory_vector_top_k": 8,
|
||||
"memory_persist": True,
|
||||
"memory_learning": True,
|
||||
},
|
||||
},
|
||||
{
|
||||
"id": "end-1",
|
||||
"type": "end",
|
||||
"position": {"x": 680, "y": 240},
|
||||
"data": {"label": "学习完成"},
|
||||
},
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"id": "e_start_learning",
|
||||
"source": "start-1",
|
||||
"target": "llm-learning",
|
||||
"sourceHandle": "right",
|
||||
"targetHandle": "left",
|
||||
},
|
||||
{
|
||||
"id": "e_learning_end",
|
||||
"source": "llm-learning",
|
||||
"target": "end-1",
|
||||
"sourceHandle": "right",
|
||||
"targetHandle": "left",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
token = _login()
|
||||
if not token:
|
||||
return 1
|
||||
|
||||
h = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
|
||||
|
||||
# 检查是否已存在
|
||||
existing = _find_agent_by_name(token, AGENT_NAME)
|
||||
if existing:
|
||||
print(f"Agent「{AGENT_NAME}」已存在 (id={existing['id']}),将更新工作流和工具配置")
|
||||
agent_id = existing["id"]
|
||||
else:
|
||||
# 创建新 Agent
|
||||
wf = _build_workflow()
|
||||
body = {
|
||||
"name": AGENT_NAME,
|
||||
"description": (
|
||||
f"知识图谱+RAG增强学习助手。学科:{SUBJECT},难度:{LEVEL}。"
|
||||
"支持实体抽取、关系图谱构建、向量语义检索、学习路径推荐、永久记忆。"
|
||||
),
|
||||
"workflow_config": wf,
|
||||
}
|
||||
r = requests.post(f"{BASE}/api/v1/agents", headers=h, json=body, timeout=60)
|
||||
if r.status_code != 201:
|
||||
print(f"创建失败: {r.status_code} {r.text[:500]}", file=sys.stderr)
|
||||
return 1
|
||||
agent_id = r.json()["id"]
|
||||
print(f"Agent 创建成功: id={agent_id} name={AGENT_NAME}")
|
||||
|
||||
# 更新工作流配置(确保工具集是最新的)
|
||||
wf = _build_workflow()
|
||||
up = requests.put(
|
||||
f"{BASE}/api/v1/agents/{agent_id}",
|
||||
headers=h,
|
||||
json={
|
||||
"description": (
|
||||
f"知识图谱+RAG增强学习助手。学科:{SUBJECT},难度:{LEVEL}。"
|
||||
"工作流:开始→智能学习助手(KG+RAG)→结束。"
|
||||
"核心能力:实体抽取、关系图谱构建、向量语义检索、学习路径推荐、永久记忆。"
|
||||
),
|
||||
"workflow_config": wf,
|
||||
},
|
||||
timeout=120,
|
||||
)
|
||||
if up.status_code != 200:
|
||||
print(f"更新失败: {up.status_code} {up.text[:500]}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
print(f"Agent「{AGENT_NAME}」配置完成")
|
||||
print(f" ID: {agent_id}")
|
||||
print(f" 学科: {SUBJECT}")
|
||||
print(f" 级别: {LEVEL}")
|
||||
print(f" 工具 ({len(LEARNING_TOOLS)}): {', '.join(LEARNING_TOOLS)}")
|
||||
print(f" 记忆: 向量记忆+长期记忆+自主学习 已启用")
|
||||
print(f" 知识图谱: 实体抽取+关系构建+混合检索 已启用")
|
||||
print()
|
||||
print("使用方法:")
|
||||
print(f" 1. 在 Agent 管理页面找到「{AGENT_NAME}」")
|
||||
print(f" 2. 点击「使用」开始对话")
|
||||
print(f" 3. 可以分享学习材料让助手自动构建知识图谱")
|
||||
print(f" 4. 查询知识点时会自动做图谱+向量混合检索")
|
||||
print()
|
||||
print(json.dumps({"id": agent_id, "name": AGENT_NAME}, ensure_ascii=False))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
297
backend/scripts/create_learning_assistant_agent_v1.py
Normal file
297
backend/scripts/create_learning_assistant_agent_v1.py
Normal file
@@ -0,0 +1,297 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
创建「智能学习助手1号」Agent — 参考苏瑶3号架构,基于 AgentRuntime 的 KG+RAG 学习助手。
|
||||
|
||||
与「智能学习助手(KG+RAG)」的区别:
|
||||
- 使用 agent 节点类型(AgentRuntime ReAct 循环),而非 llm 节点
|
||||
- 参考苏瑶3号的 AgentRuntime 配置模式
|
||||
- 更完整的记忆配置(向量记忆、长期记忆、自主学习)
|
||||
- 独立 memory_scope_id 防止与其他 Agent 串记忆
|
||||
|
||||
用法:
|
||||
cd backend && .\\venv\\Scripts\\python.exe scripts/create_learning_assistant_agent.py
|
||||
|
||||
环境变量:
|
||||
PLATFORM_BASE_URL - 平台地址(默认 http://127.0.0.1:8037)
|
||||
PLATFORM_USERNAME - 用户名(默认 admin)
|
||||
PLATFORM_PASSWORD - 密码(默认 123456)
|
||||
AGENT_NAME - Agent 名称(默认 智能学习助手1号)
|
||||
SUBJECT - 学科领域(默认 通用)
|
||||
LEVEL - 难度级别(默认 中级)
|
||||
MODEL - 模型(默认 deepseek-v4-flash)
|
||||
PROVIDER - 提供商(默认 deepseek)
|
||||
TEMPERATURE - 温度(默认 0.85)
|
||||
MAX_ITERATIONS - 最大步数(默认 15)
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
import requests
|
||||
|
||||
BASE = os.getenv("PLATFORM_BASE_URL", "http://127.0.0.1:8037").rstrip("/")
|
||||
USER = os.getenv("PLATFORM_USERNAME", "admin")
|
||||
PWD = os.getenv("PLATFORM_PASSWORD", "123456")
|
||||
AGENT_NAME = os.getenv("AGENT_NAME", "智能学习助手1号")
|
||||
SUBJECT = os.getenv("SUBJECT", "通用")
|
||||
LEVEL = os.getenv("LEVEL", "中级")
|
||||
MODEL = os.getenv("MODEL", "deepseek-v4-flash")
|
||||
PROVIDER = os.getenv("PROVIDER", "deepseek")
|
||||
TEMPERATURE = float(os.getenv("TEMPERATURE", "0.85"))
|
||||
MAX_ITERATIONS = int(os.getenv("MAX_ITERATIONS", "15"))
|
||||
|
||||
# ── 学习助手专用工具集(KG+RAG 核心 + 通用辅助)──
|
||||
LEARNING_TOOLS = [
|
||||
# KG+RAG 核心四件套
|
||||
"knowledge_graph_search",
|
||||
"knowledge_graph_add",
|
||||
"entity_search",
|
||||
"learning_path",
|
||||
# 文件与文本
|
||||
"file_read",
|
||||
"file_write",
|
||||
"text_analyze",
|
||||
"json_process",
|
||||
# 学习辅助
|
||||
"web_search",
|
||||
"task_plan",
|
||||
"self_review",
|
||||
"math_calculate",
|
||||
"datetime",
|
||||
# 通用
|
||||
"http_request",
|
||||
"code_execute",
|
||||
"system_info",
|
||||
]
|
||||
|
||||
# ── 系统提示词(同 KG+RAG 学习助手,适配 AgentRuntime 风格)──
|
||||
SYSTEM_PROMPT = f"""# 角色:智能学习助手1号(AgentRuntime + KG+RAG)
|
||||
|
||||
你是基于 AgentRuntime 自主 ReAct 循环的 AI 学习助手,参考苏瑶3号架构设计。
|
||||
你具备**知识图谱构建**、**向量语义检索**、**长期记忆**和**自主学习**能力。
|
||||
|
||||
## 核心架构
|
||||
|
||||
你的知识系统由三层组成:
|
||||
1. **知识图谱 (Knowledge Graph)**:结构化存储知识点实体及其前置/扩展/包含/示例关系
|
||||
2. **向量记忆 (Vector Memory)**:语义检索历史对话和相关知识
|
||||
3. **长期记忆 (Persistent Memory)**:跨会话保存用户画像、学习进度、薄弱环节
|
||||
|
||||
## 当前配置
|
||||
- 学科领域:{SUBJECT}
|
||||
- 难度级别:{LEVEL}
|
||||
- 模型:{PROVIDER}/{MODEL} (temperature={TEMPERATURE})
|
||||
- 最大迭代步数:{MAX_ITERATIONS}
|
||||
|
||||
## 工作流程(ReAct 循环中自主遵循)
|
||||
|
||||
### 阶段 1:理解与分析
|
||||
1. 理解用户的学习意图(提问 / 复习 / 练习 / 总结 / 规划)
|
||||
2. 使用 `knowledge_graph_search` 检索相关知识图谱实体
|
||||
3. 如果用户提供了学习材料/知识点,使用 `knowledge_graph_add` 自动提取存储
|
||||
|
||||
### 阶段 2:知识检索与融合
|
||||
4. 结合图谱检索结果和历史向量记忆,构建知识上下文
|
||||
5. 使用 `entity_search` 查找特定概念的前置知识和扩展内容
|
||||
6. 使用 `learning_path` 分析学习依赖,推荐学习顺序
|
||||
|
||||
### 阶段 3:生成与交付
|
||||
7. 基于融合后的知识上下文生成高质量回答
|
||||
8. 回答包含:核心概念解释、前置知识提醒、实例/练习题、扩展阅读建议
|
||||
9. 使用 `self_review` 自检回答质量,不达标则修正
|
||||
|
||||
### 阶段 4:巩固与记忆
|
||||
10. 将重要知识点持久化到长期记忆和知识图谱
|
||||
11. 更新用户画像(掌握程度、薄弱环节、学习偏好)
|
||||
|
||||
## 回答风格
|
||||
- 使用 Markdown 格式,层次分明
|
||||
- 关键概念用 **粗体** 标记,公式用代码块
|
||||
- 每个回答末尾附上「📚 相关知识点」列表(来自图谱检索)
|
||||
- 必要时用 `task_plan` 制定学习计划
|
||||
|
||||
## 自主扩展能力
|
||||
- 发现知识空白时,用 `web_search` 补充
|
||||
- 需要重复计算/处理时,用 `code_execute` 编写脚本
|
||||
- 遇到可复用的外部 API 时,用 `tool_register` 注册
|
||||
- 需要专业子领域协助时,用 `agent_create` 创建子 Agent
|
||||
|
||||
---
|
||||
|
||||
你是学习者最可靠的 AI 伙伴。开始吧!"""
|
||||
|
||||
|
||||
def _login() -> Optional[str]:
|
||||
try:
|
||||
r = requests.post(
|
||||
f"{BASE}/api/v1/auth/login",
|
||||
data={"username": USER, "password": PWD},
|
||||
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
||||
timeout=15,
|
||||
)
|
||||
if r.status_code == 200:
|
||||
return r.json().get("access_token")
|
||||
print(f"登录失败: {r.status_code} {r.text[:300]}", file=sys.stderr)
|
||||
return None
|
||||
except Exception as e:
|
||||
print(f"登录异常: {e}", file=sys.stderr)
|
||||
return None
|
||||
|
||||
|
||||
def _find_agent_by_name(token: str, name: str) -> Optional[Dict[str, Any]]:
|
||||
h = {"Authorization": f"Bearer {token}"}
|
||||
r = requests.get(f"{BASE}/api/v1/agents", params={"search": name, "limit": 50}, headers=h, timeout=30)
|
||||
if r.status_code != 200:
|
||||
return None
|
||||
for a in r.json() or []:
|
||||
if a.get("name") == name:
|
||||
return a
|
||||
return None
|
||||
|
||||
|
||||
def _build_workflow(agent_id: str = "") -> Dict[str, Any]:
|
||||
"""构建 agent 节点工作流(参考苏瑶3号架构)。"""
|
||||
return {
|
||||
"nodes": [
|
||||
{
|
||||
"id": "start-1",
|
||||
"type": "start",
|
||||
"position": {"x": 80, "y": 240},
|
||||
"data": {"label": "学习任务开始"},
|
||||
},
|
||||
{
|
||||
"id": "agent-learning-v1",
|
||||
"type": "agent", # ← agent 节点类型,使用 AgentRuntime
|
||||
"position": {"x": 380, "y": 240},
|
||||
"data": {
|
||||
"label": AGENT_NAME,
|
||||
# ── LLM 配置 ──
|
||||
"system_prompt": SYSTEM_PROMPT,
|
||||
"provider": PROVIDER,
|
||||
"model": MODEL,
|
||||
"temperature": TEMPERATURE,
|
||||
"max_iterations": MAX_ITERATIONS,
|
||||
# ── 工具配置 ──
|
||||
"tools": LEARNING_TOOLS,
|
||||
# ── 记忆配置(完整 AgentMemoryConfig)──
|
||||
"memory": True,
|
||||
"memory_max_history": 30,
|
||||
"memory_vector_enabled": True,
|
||||
"memory_vector_top_k": 8,
|
||||
"memory_persist": True,
|
||||
"memory_learning": True,
|
||||
# ── 作用域隔离 ──
|
||||
"memory_scope_id": agent_id,
|
||||
"agent_id": agent_id,
|
||||
# ── 自检 ──
|
||||
"self_review_enabled": True,
|
||||
},
|
||||
},
|
||||
{
|
||||
"id": "end-1",
|
||||
"type": "end",
|
||||
"position": {"x": 680, "y": 240},
|
||||
"data": {"label": "学习完成"},
|
||||
},
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"id": "e_start_agent",
|
||||
"source": "start-1",
|
||||
"target": "agent-learning-v1",
|
||||
"sourceHandle": "right",
|
||||
"targetHandle": "left",
|
||||
},
|
||||
{
|
||||
"id": "e_agent_end",
|
||||
"source": "agent-learning-v1",
|
||||
"target": "end-1",
|
||||
"sourceHandle": "right",
|
||||
"targetHandle": "left",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
token = _login()
|
||||
if not token:
|
||||
return 1
|
||||
|
||||
h = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
|
||||
|
||||
# 检查是否已存在
|
||||
existing = _find_agent_by_name(token, AGENT_NAME)
|
||||
if existing:
|
||||
print(f"Agent「{AGENT_NAME}」已存在 (id={existing['id']}),将更新配置")
|
||||
agent_id = existing["id"]
|
||||
else:
|
||||
# 先创建一个临时工作流获取 agent_id
|
||||
temp_wf = _build_workflow(agent_id="")
|
||||
body = {
|
||||
"name": AGENT_NAME,
|
||||
"description": f"AgentRuntime KG+RAG 学习助手(参考苏瑶3号)。学科:{SUBJECT},难度:{LEVEL}。",
|
||||
"workflow_config": temp_wf,
|
||||
}
|
||||
r = requests.post(f"{BASE}/api/v1/agents", headers=h, json=body, timeout=60)
|
||||
if r.status_code != 201:
|
||||
print(f"创建失败: {r.status_code} {r.text[:500]}", file=sys.stderr)
|
||||
return 1
|
||||
agent_id = r.json()["id"]
|
||||
print(f"Agent 创建成功: id={agent_id} name={AGENT_NAME}")
|
||||
|
||||
# 用正确的 agent_id 重建工作流
|
||||
wf = _build_workflow(agent_id=agent_id)
|
||||
|
||||
description = (
|
||||
f"智能学习助手1号 — AgentRuntime KG+RAG 学习助手(参考苏瑶3号架构)。\n"
|
||||
f"学科:{SUBJECT},难度:{LEVEL}。\n"
|
||||
f"工作流:开始 → agent(ReAct) → 结束。\n"
|
||||
f"核心能力:\n"
|
||||
f"- 知识图谱:实体抽取 + 关系构建 + 混合检索\n"
|
||||
f"- 向量记忆:语义检索历史对话\n"
|
||||
f"- 长期记忆:跨会话用户画像 + 学习进度\n"
|
||||
f"- 自主学习:工具模式学习 + 能力自检\n"
|
||||
f"配置:{PROVIDER}/{MODEL} temperature={TEMPERATURE} max_iterations={MAX_ITERATIONS}"
|
||||
)
|
||||
|
||||
up = requests.put(
|
||||
f"{BASE}/api/v1/agents/{agent_id}",
|
||||
headers=h,
|
||||
json={
|
||||
"description": description,
|
||||
"workflow_config": wf,
|
||||
},
|
||||
timeout=120,
|
||||
)
|
||||
if up.status_code != 200:
|
||||
print(f"更新失败: {up.status_code} {up.text[:500]}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
print(f"✅ Agent「{AGENT_NAME}」配置完成")
|
||||
print(f" ID: {agent_id}")
|
||||
print(f" 类型: agent 节点 (AgentRuntime ReAct)")
|
||||
print(f" 学科: {SUBJECT}")
|
||||
print(f" 级别: {LEVEL}")
|
||||
print(f" 模型: {PROVIDER}/{MODEL} (temperature={TEMPERATURE})")
|
||||
print(f" 最大步数: {MAX_ITERATIONS}")
|
||||
print(f" 工具 ({len(LEARNING_TOOLS)}): {', '.join(LEARNING_TOOLS)}")
|
||||
print(f" 记忆: 向量(Top-8) + 长期 + 自主学习 已启用")
|
||||
print(f" 知识图谱: 混合检索 + 实体抽取 + 关系构建 已启用")
|
||||
print(f" 自检: self_review 已启用")
|
||||
print()
|
||||
print("与「智能学习助手(KG+RAG)」的区别:")
|
||||
print(" - 使用 agent 节点(AgentRuntime ReAct 循环)代替 llm 节点")
|
||||
print(" - 参考苏瑶3号架构,完整记忆配置")
|
||||
print(" - 独立 memory_scope_id,不与其他 Agent 串记忆")
|
||||
print(" - 更高的 temperature (0.85) 和自主学习能力")
|
||||
print()
|
||||
print(json.dumps({"id": agent_id, "name": AGENT_NAME}, ensure_ascii=False))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -159,33 +159,43 @@ SEED_TEMPLATES = [
|
||||
# ── 教育 ──
|
||||
{
|
||||
"name": "学习助手",
|
||||
"description": "多功能学习助手:作业管理、知识问答、笔记整理、学习计划",
|
||||
"description": "智能学习助手(KG+RAG):知识图谱构建、向量检索、实体抽取、学习路径推荐、永久记忆",
|
||||
"category": "education",
|
||||
"tags": ["学习", "作业", "笔记", "计划"],
|
||||
"tags": ["学习", "知识图谱", "RAG", "向量检索", "记忆"],
|
||||
"prompt": (
|
||||
"# 角色:智能学习助手\n\n"
|
||||
"你是专为学生设计的多功能AI学习助手,帮助高效管理学习任务、巩固知识。\n\n"
|
||||
"## 核心能力\n\n"
|
||||
"### 1. 作业管理\n"
|
||||
"- 协助创建、分类、优先级排序作业任务\n"
|
||||
"- 根据截止日期生成倒计时提醒\n"
|
||||
"- 将大型作业拆分为可执行的小步骤\n\n"
|
||||
"### 2. 学习辅助\n"
|
||||
"- 知识问答:基于{{subjects}}等内容提供精准解答并附带推理过程\n"
|
||||
"- 错题本:输入错题后自动分类并生成同类练习题\n"
|
||||
"- 笔记整理:将混乱笔记整理为结构化摘要(概念→公式→例题)\n"
|
||||
"- 记忆卡片:生成 Anki 风格的闪卡,支持间隔重复复习\n\n"
|
||||
"### 3. 时间与计划\n"
|
||||
"- 根据学习目标和可用时间生成每日/每周学习计划\n"
|
||||
"- 分析学习时间分配,提供优化建议\n\n"
|
||||
"### 4. 激励与反馈\n"
|
||||
"- 记录学习里程碑,生成鼓励性反馈\n"
|
||||
"- 自定义考试日期,生成复习冲刺表\n\n"
|
||||
"# 角色:智能学习助手(知识图谱 + RAG 增强版)\n\n"
|
||||
"你是专为深度学习场景设计的 AI 学习助手,具备知识图谱构建、向量语义检索和永久记忆能力。\n\n"
|
||||
"## 核心架构\n\n"
|
||||
"你的知识系统由三层组成:\n"
|
||||
"1. **知识图谱**:结构化存储知识点及前置/扩展/包含/示例关系\n"
|
||||
"2. **向量记忆**:语义检索历史对话,找到最相似的学习内容\n"
|
||||
"3. **长期记忆**:跨会话保存用户画像、学习进度和薄弱环节\n\n"
|
||||
"## 工作流程(每次对话)\n\n"
|
||||
"### 1. 理解与分析\n"
|
||||
"- 理解用户意图(提问/复习/练习/总结/规划)\n"
|
||||
"- 用 knowledge_graph_search 检索相关知识实体\n"
|
||||
"- 有新学习材料时用 knowledge_graph_add 自动提取存储\n\n"
|
||||
"### 2. 知识检索与融合\n"
|
||||
"- 结合图谱+向量+历史记忆构建完整上下文\n"
|
||||
"- 用 entity_search 查找特定概念的前置和扩展\n"
|
||||
"- 用 learning_path 分析学习依赖,推荐学习顺序\n\n"
|
||||
"### 3. 生成与交付\n"
|
||||
"- 核心概念解释(关联图谱实体)\n"
|
||||
"- 前置知识提醒 + 实例/练习题\n"
|
||||
"- 扩展阅读建议\n"
|
||||
"- 用 self_review 自检回答质量\n\n"
|
||||
"### 4. 巩固与记忆\n"
|
||||
"- 重要知识点持久化到长期记忆\n"
|
||||
"- 更新用户画像(掌握程度、薄弱环节)\n\n"
|
||||
"## 学科范围\n"
|
||||
"{{subjects}}\n\n"
|
||||
"## 交互规则\n"
|
||||
"- 任务清单使用 Markdown 列表(- [ ] 未完成 / - [x] 已完成)\n"
|
||||
"- 知识解答先给答案再附推理过程\n"
|
||||
"- 使用 Markdown 格式,层次分明\n"
|
||||
"- 每个回答附「📚 相关知识点」列表\n"
|
||||
"- 需要时用 task_plan 制定学习计划\n"
|
||||
"- 语气鼓励、耐心,像一位懂教育学的私人导师\n"
|
||||
"- 不代写考试答案,不鼓励学术不端"
|
||||
"- 不代写考试答案,不鼓励学术不端\n\n"
|
||||
"> 完整 KG+RAG 版请使用「智能学习助手 (KG+RAG)」场景模板创建 Agent。"
|
||||
),
|
||||
"variables": [
|
||||
{"name": "subjects", "type": "string", "required": False, "description": "学科范围", "default": "数学、物理、化学、历史、语文、英语"},
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
services:
|
||||
frontend:
|
||||
build:
|
||||
context: ./frontend
|
||||
dockerfile: Dockerfile.dev
|
||||
ports:
|
||||
- "8038:3000"
|
||||
volumes:
|
||||
- ./frontend:/app
|
||||
- /app/node_modules
|
||||
environment:
|
||||
- VITE_API_URL=http://101.43.95.130:8037
|
||||
# 注意:Vite环境变量需要在构建时设置,运行时修改需要重启容器
|
||||
depends_on:
|
||||
- backend
|
||||
networks:
|
||||
- aiagent-network
|
||||
|
||||
backend:
|
||||
build:
|
||||
context: ./backend
|
||||
dockerfile: Dockerfile.dev
|
||||
ports:
|
||||
- "8037:8000"
|
||||
volumes:
|
||||
- ./backend:/app
|
||||
environment:
|
||||
- DATABASE_URL=mysql+pymysql://root:!Rjb12191@gz-cynosdbmysql-grp-d26pzce5.sql.tencentcdb.com:24936/agent_db?charset=utf8mb4
|
||||
- REDIS_URL=redis://redis:6379/0
|
||||
- SECRET_KEY=dev-secret-key-change-in-production
|
||||
- CORS_ORIGINS=http://localhost:3000,http://127.0.0.1:3000,http://localhost:8038,http://101.43.95.130:8038
|
||||
- DEEPSEEK_API_KEY=sk-fdf7cc1c73504e628ec0119b7e11b8cc
|
||||
- DEEPSEEK_BASE_URL=https://api.deepseek.com
|
||||
depends_on:
|
||||
- redis
|
||||
networks:
|
||||
- aiagent-network
|
||||
|
||||
celery:
|
||||
build:
|
||||
context: ./backend
|
||||
dockerfile: Dockerfile.dev
|
||||
command: celery -A app.core.celery_app worker --loglevel=info
|
||||
volumes:
|
||||
- ./backend:/app
|
||||
environment:
|
||||
- DATABASE_URL=mysql+pymysql://root:!Rjb12191@gz-cynosdbmysql-grp-d26pzce5.sql.tencentcdb.com:24936/agent_db?charset=utf8mb4
|
||||
- REDIS_URL=redis://redis:6379/0
|
||||
- DEEPSEEK_API_KEY=sk-fdf7cc1c73504e628ec0119b7e11b8cc
|
||||
- DEEPSEEK_BASE_URL=https://api.deepseek.com
|
||||
depends_on:
|
||||
- redis
|
||||
- backend
|
||||
networks:
|
||||
- aiagent-network
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
ports:
|
||||
- "6380:6379" # 主机 6380 映射到容器 6379,避免与宿主机 6379 冲突
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
networks:
|
||||
- aiagent-network
|
||||
|
||||
volumes:
|
||||
redis_data:
|
||||
|
||||
networks:
|
||||
aiagent-network:
|
||||
driver: bridge
|
||||
10
docs/startup-deploy/上传git仓.md
Normal file
10
docs/startup-deploy/上传git仓.md
Normal file
@@ -0,0 +1,10 @@
|
||||
将修改上传到 git 仓 **rjb_win_dev** 分支:
|
||||
|
||||
http://101.43.95.130:3001/admin/aiagent.git
|
||||
|
||||
```powershell
|
||||
cd D:\aaa\aiagent
|
||||
git push -u origin rjb_win_dev
|
||||
```
|
||||
|
||||
鉴权请使用本机已保存的凭据,或在提示时输入平台账号;**勿在文档中保存明文密码**,也勿将含密码的文件提交到仓库。
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user