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:
renjianbo
2026-05-06 01:37:13 +08:00
parent f33bc461ff
commit eabf90c496
171 changed files with 4906 additions and 445 deletions

View File

@@ -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),
)

View File

@@ -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

View File

@@ -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

View File

@@ -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()

View File

@@ -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

View File

@@ -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="实体名称+描述的 embeddingJSON 序列化)")
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})>"

View File

@@ -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"],
},
},
}

View 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,
}

View 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

View 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)

View File

@@ -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

View File

@@ -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"]

View 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())

View 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())

View 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())

View File

@@ -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": "数学、物理、化学、历史、语文、英语"},

View File

@@ -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

View 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