Add FastAPI backend with agent system

This commit is contained in:
2026-03-21 10:13:29 +08:00
parent ed6bab59fe
commit 6ffa07adde
82 changed files with 11138 additions and 0 deletions

View File

@@ -0,0 +1,22 @@
from app.agents.tools.search import (
search_knowledge, get_knowledge_graph_context,
build_knowledge_graph, hybrid_search,
)
from app.agents.tools.task import get_tasks, create_task, update_task_status
from app.agents.tools.forum import get_forum_posts, create_forum_post, scan_forum_for_instructions
ALL_TOOLS = [
# 知识库工具
search_knowledge,
get_knowledge_graph_context,
build_knowledge_graph,
hybrid_search,
# 任务工具
get_tasks,
create_task,
update_task_status,
# 论坛工具
get_forum_posts,
create_forum_post,
scan_forum_for_instructions,
]

View File

@@ -0,0 +1,134 @@
"""Agent 工具集 - 论坛相关"""
from langchain_core.tools import tool
from app.database import async_session
from app.models.forum import ForumPost, ForumReply
from app.agents.context import get_current_user
from sqlalchemy import select
import asyncio
def _run_async(coro, timeout: int = 30):
try:
loop = asyncio.get_running_loop()
future = loop.run_in_executor(__import__("concurrent.futures").ThreadPoolExecutor(), lambda: asyncio.run(coro))
return future.result(timeout=timeout)
except RuntimeError:
return asyncio.run(coro)
@tool
def get_forum_posts(category: str | None = None, limit: int = 10) -> str:
"""
获取论坛帖子列表。
Args:
category: 可选,筛选分类 (discussion/instruction/question)
limit: 返回数量默认10
Returns:
帖子列表
"""
uid = get_current_user()
async def _get():
async with async_session() as db:
from app.models.user import User
query = (
select(ForumPost)
.join(User, User.id == ForumPost.user_id)
.where(User.id == uid)
)
if category:
query = query.where(ForumPost.category == category)
query = query.order_by(ForumPost.created_at.desc()).limit(limit)
result = await db.execute(query)
posts = result.scalars().all()
if not posts:
return "暂无帖子"
lines = []
for p in posts:
exec_mark = " [已执行]" if p.is_executed else ""
lines.append(
f"- [{p.id[:8]}] [{p.category}] {p.title} | "
f"{p.content[:50]}...{exec_mark}"
)
return "\n".join(lines)
try:
return _run_async(_get())
except Exception as e:
return f"获取帖子失败: {str(e)}"
@tool
def create_forum_post(title: str, content: str, category: str = "discussion") -> str:
"""
在论坛发布新帖子。
Args:
title: 帖子标题
content: 帖子内容
category: 分类 (discussion/instruction/question)默认discussion
Returns:
创建结果
"""
uid = get_current_user()
async def _create():
async with async_session() as db:
post = ForumPost(
user_id=uid,
title=title,
content=content,
category=category,
)
db.add(post)
await db.commit()
await db.refresh(post)
return f"帖子发布成功: [{post.id[:8]}] {title}"
try:
return _run_async(_create())
except Exception as e:
return f"发布帖子失败: {str(e)}"
@tool
def scan_forum_for_instructions() -> str:
"""
扫描论坛中的指令类帖子,检查是否有待执行的指令。
Returns:
待执行指令的列表
"""
uid = get_current_user()
async def _scan():
async with async_session() as db:
from app.models.user import User
result = await db.execute(
select(ForumPost)
.join(User, User.id == ForumPost.user_id)
.where(ForumPost.user_id == uid)
.where(ForumPost.category == "instruction")
.where(ForumPost.is_executed == False)
.order_by(ForumPost.created_at.desc())
.limit(10)
)
posts = result.scalars().all()
if not posts:
return "暂无待执行的指令"
lines = ["待执行的指令:"]
for p in posts:
lines.append(f"- [{p.id[:8]}] {p.title}\n 内容: {p.content[:100]}...")
return "\n".join(lines)
try:
return _run_async(_scan())
except Exception as e:
return f"扫描论坛失败: {str(e)}"
__all__ = ["get_forum_posts", "create_forum_post", "scan_forum_for_instructions"]

View File

@@ -0,0 +1,159 @@
"""
Agent 工具集 - 知识库 & 图谱相关
这些工具在 LangChain ToolNode 中被调用。
由于 LangChain 工具系统是同步的,内部用 run_in_executor 处理 async 逻辑。
"""
from langchain_core.tools import tool
from concurrent.futures import ThreadPoolExecutor
from app.database import async_session
from app.agents.context import get_current_user
import asyncio
_executor = ThreadPoolExecutor(max_workers=4)
def _run_async(coro, timeout: int = 30):
"""在同步上下文中运行 async 代码"""
try:
loop = asyncio.get_running_loop()
future = loop.run_in_executor(_executor, lambda: asyncio.run(coro))
return future.result(timeout=timeout)
except RuntimeError:
return asyncio.run(coro)
@tool
def search_knowledge(query: str, top_k: int = 5) -> str:
"""
搜索用户的私人知识库。根据查询返回最相关的文档片段,支持语义检索。
Args:
query: 搜索查询
top_k: 返回结果数量默认5条
Returns:
包含相关文档片段和来源信息的格式化文本
"""
from app.services.knowledge_service import KnowledgeService
uid = get_current_user()
async def _search():
async with async_session() as db:
service = KnowledgeService(db, user_id=uid)
results = await service.retrieve(query, user_id=uid, top_k=top_k)
if not results:
return "未找到相关知识。知识库可能为空,或尝试用其他关键词搜索。"
texts = []
for i, r in enumerate(results, 1):
prev = f"\n上一段: {r.prev_chunk[:100]}..." if r.prev_chunk else ""
next_ = f"\n下一段: {r.next_chunk[:100]}..." if r.next_chunk else ""
texts.append(
f"[{i}] 来源: {r.document_title}\n"
f"相关度: {r.score:.2f}\n"
f"{prev}{next_}\n"
f"内容: {r.content[:300]}{'...' if len(r.content) > 300 else ''}"
)
return "\n\n---\n\n".join(texts)
try:
return _run_async(_search(), timeout=30)
except Exception as e:
return f"知识检索失败: {str(e)}"
@tool
def get_knowledge_graph_context(entity: str | None = None) -> str:
"""
获取用户知识图谱的上下文信息。
Args:
entity: 可选,指定要查询的实体名称。如果为空则返回整体图谱摘要。
Returns:
知识图谱节点和关系的描述
"""
from app.services.graph_service import GraphService
uid = get_current_user()
async def _get():
async with async_session() as db:
service = GraphService(db)
if entity:
return await service.get_entity_context(entity, uid)
return await service.get_graph_summary(uid)
try:
return _run_async(_get(), timeout=30)
except Exception as e:
return f"图谱查询失败: {str(e)}"
@tool
def build_knowledge_graph(document_ids: list[str] | None = None) -> str:
"""
从文档构建/更新知识图谱。
Args:
document_ids: 可选指定要处理的文档ID列表。如果为空则处理所有文档。
Returns:
构建结果摘要
"""
from app.services.graph_service import GraphService
uid = get_current_user()
async def _build():
async with async_session() as db:
service = GraphService(db)
await service.build_graph(user_id=uid, document_ids=document_ids)
return "知识图谱构建完成"
try:
return _run_async(_build(), timeout=120)
except Exception as e:
return f"图谱构建失败: {str(e)}"
@tool
def hybrid_search(query: str, top_k: int = 5) -> str:
"""
混合搜索,结合向量语义检索和关键词匹配,返回最相关结果。
Args:
query: 搜索查询
top_k: 返回结果数量默认5条
Returns:
混合检索结果
"""
from app.services.knowledge_service import KnowledgeService
uid = get_current_user()
async def _search():
async with async_session() as db:
service = KnowledgeService(db, user_id=uid)
results = await service.hybrid_search(query, user_id=uid, top_k=top_k)
if not results:
return "未找到相关知识。"
texts = []
for i, r in enumerate(results, 1):
texts.append(
f"[{i}] {r.document_title} (相关度: {r.score:.2f})\n"
f"{r.content[:200]}{'...' if len(r.content) > 200 else ''}"
)
return "\n\n---\n\n".join(texts)
try:
return _run_async(_search(), timeout=30)
except Exception as e:
return f"混合搜索失败: {str(e)}"
__all__ = [
"search_knowledge",
"get_knowledge_graph_context",
"build_knowledge_graph",
"hybrid_search",
]

View File

@@ -0,0 +1,142 @@
"""Agent 工具集 - 任务相关"""
from langchain_core.tools import tool
from app.database import async_session
from app.models.task import Task
from app.agents.context import get_current_user
from sqlalchemy import select
import asyncio
_executor = None
def _run_async(coro, timeout: int = 30):
try:
loop = asyncio.get_running_loop()
future = loop.run_in_executor(_executor or __import__("concurrent.futures").ThreadPoolExecutor(), lambda: asyncio.run(coro))
return future.result(timeout=timeout)
except RuntimeError:
return asyncio.run(coro)
@tool
def get_tasks(status: str | None = None, limit: int = 20) -> str:
"""
获取用户当前的任务列表。
Args:
status: 可选,筛选任务状态 (todo/in_progress/done/blocked)
limit: 返回数量默认20
Returns:
任务列表
"""
uid = get_current_user()
async def _get():
async with async_session() as db:
from app.models.user import User
query = (
select(Task)
.join(User, User.id == Task.user_id)
.where(User.id == uid)
)
if status:
query = query.where(Task.status == status)
query = query.order_by(Task.priority.desc(), Task.updated_at.desc()).limit(limit)
result = await db.execute(query)
tasks = result.scalars().all()
if not tasks:
return "暂无任务"
lines = []
for t in tasks:
lines.append(
f"- [{t.id[:8]}] {t.title} | "
f"状态:{t.status} | 优先级:{t.priority} | 截止:{t.due_date or ''}"
)
return "\n".join(lines)
try:
return _run_async(_get())
except Exception as e:
return f"获取任务失败: {str(e)}"
@tool
def create_task(title: str, description: str = "", priority: int = 2, due_date: str | None = None) -> str:
"""
创建新任务。
Args:
title: 任务标题(必填)
description: 任务描述
priority: 优先级 1-4数字越大优先级越高默认2
due_date: 截止日期,格式 YYYY-MM-DD
Returns:
创建结果
"""
uid = get_current_user()
async def _create():
async with async_session() as db:
task = Task(
user_id=uid,
title=title,
description=description,
priority=priority,
due_date=due_date,
status="todo",
)
db.add(task)
await db.commit()
await db.refresh(task)
return f"任务创建成功: [{task.id[:8]}] {title}"
try:
return _run_async(_create())
except Exception as e:
return f"创建任务失败: {str(e)}"
@tool
def update_task_status(task_id: str, status: str) -> str:
"""
更新任务状态。
Args:
task_id: 任务ID完整ID或前8位
status: 新状态 (todo/in_progress/done/blocked)
Returns:
更新结果
"""
uid = get_current_user()
async def _update():
async with async_session() as db:
from app.models.user import User
query = (
select(Task)
.join(User, User.id == Task.user_id)
.where(User.id == uid)
)
if len(task_id) == 8:
query = query.where(Task.id.like(f"{task_id}%"))
else:
query = query.where(Task.id == task_id)
result = await db.execute(query)
task = result.scalar_one_or_none()
if not task:
return f"任务不存在: {task_id}"
task.status = status
await db.commit()
return f"任务状态已更新: {task.title} -> {status}"
try:
return _run_async(_update())
except Exception as e:
return f"更新任务失败: {str(e)}"
__all__ = ["get_tasks", "create_task", "update_task_status"]