Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
name: CI

on:
push:
branches: [master]
pull_request:
branches: [master]

concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true

jobs:
backend:
runs-on: ubuntu-latest
defaults:
run:
working-directory: backend
steps:
- uses: actions/checkout@v5

- name: Install uv
uses: astral-sh/setup-uv@v10.0.1
with:
version: "0.12.14"
python-version: "3.12"
enable-cache: true
working-directory: backend

# 跳过 parsers 组(unstructured + torch/CUDA,数 GB):测试不需要它
- name: Install dependencies
run: uv sync --frozen --extra dev --no-group parsers

# 必须 --no-sync:否则 uv run 会重新同步默认组,把 parsers 装回来
- name: Lint
run: uv run --no-sync ruff check .

- name: Test
run: uv run --no-sync pytest -q

- name: Assert heavy deps excluded
run: uv run --no-sync python -c "import importlib.util, sys; sys.exit(0 if importlib.util.find_spec('torch') is None and importlib.util.find_spec('unstructured') is None else 1)"

frontend:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5

- name: Setup Node
uses: actions/setup-node@v6
with:
node-version: 22
cache: npm
cache-dependency-path: front/package-lock.json

- name: Install dependencies
working-directory: front
run: npm ci

- name: Lint
working-directory: front
run: npm run lint

- name: Build
working-directory: front
run: npm run build
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
</a>
<a href="https://github.com/RMA-MUN/RAGNotebook/network/members">
<img src="https://img.shields.io/github/forks/RMA-MUN/RAGNotebook?style=flat-square&label=Forks&color=green" alt="Forks">
</a>
<a href="https://github.com/RMA-MUN/RAGNotebook/actions/workflows/ci.yml">
<img src="https://github.com/RMA-MUN/RAGNotebook/actions/workflows/ci.yml/badge.svg" alt="CI">
</a>
<img src="https://img.shields.io/badge/python-v3.12.4-blue.svg" alt="Python">
</div>
Expand Down Expand Up @@ -148,9 +151,13 @@ cd RAGNotebook
##### 后端依赖
```bash
cd backend
uv sync
uv sync --extra dev
```

> 说明:文档解析依赖(`unstructured` + torch,数 GB)放在 `parsers`
> 依赖组中,**默认随 `uv sync` 一起安装**,本地/Docker 行为不变;
> CI 用 `uv sync --extra dev --no-group parsers` 跳过它以保持快速。

##### 前端依赖
```bash
cd front
Expand Down
10 changes: 4 additions & 6 deletions backend/app/agent/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,25 +10,24 @@
from langchain_core.tools import BaseTool
from langgraph.types import Command

from app.core.settings import settings

from app.agent.agent_middleware import DEFAULT_APPROVAL_TOOLS
from app.agent.agent_rag_tool import init_rag_guard, search_rag
from app.agent.agent_tools import (
create_note_tool,
get_note_stats_tool,
get_related_notes_tool,
get_thinking_callback_from_context,
get_today_reviews_tool,
get_user_info_tools,
get_thinking_callback_from_context,
mark_reviewed_tool,
search_notes_tool,
set_current_user_id,
set_thinking_callback,
what_time_is_now,
)
from app.agent.agent_middleware import DEFAULT_APPROVAL_TOOLS, get_middleware
from app.agent.agent_rag_tool import init_rag_guard, search_rag
from app.agent.checkpoint.mysql_saver import MySQLCheckpointSaver
from app.core.logger_handler import logger
from app.core.settings import settings
from app.services import session_manager as sm
from app.utils.prompt_loader import load_prompt

Expand Down Expand Up @@ -669,7 +668,6 @@ async def run_agent():
yield f"data: {json.dumps({'type': 'done'}, ensure_ascii=False)}\n\n"
return

response = agent_result_holder.get("response") or "抱歉,我无法理解您的请求。"
yield f"data: {json.dumps({'type': 'done', 'session_id': session_id}, ensure_ascii=False)}\n\n"
except Exception as e:
logger.error(f"【Agent流式响应】处理请求失败: {e}", exc_info=True)
Expand Down
3 changes: 2 additions & 1 deletion backend/app/agent/agent_middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ def decorator(func):
from app.agent.agent_tools import get_thinking_callback_from_context
from app.core.logger_handler import logger


def _describe_create_note(tool_call, state, runtime) -> str:
"""创建笔记审批摘要:只带标题、正文长度与开头预览,避免把全文塞进审批负载。"""
args = (tool_call.get("args") if isinstance(tool_call, dict) else {}) or {}
Expand Down Expand Up @@ -218,4 +219,4 @@ def get_middleware(approval_tools: dict | None = None):
)
else:
logger.warning("HumanInTheLoopMiddleware 不可用,审批中间件已禁用(降级模式)。")
return middleware
return middleware
2 changes: 1 addition & 1 deletion backend/app/cache/redis_decorator.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,4 +164,4 @@ async def wrapper(*args, **kwargs):
# 使用RedisCache获取或设置缓存
return await RedisCache.get_or_set(key, func, *args, expire=expire, **kwargs)
return wrapper
return decorator
return decorator
2 changes: 1 addition & 1 deletion backend/app/core/failed_response.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from starlette import status

# 项目配置唯一读取点在 app/core/settings.py,此处 re-export 兼容既有导入路径
from app.core.settings import Settings, settings
from app.core.settings import settings

# 生产环境强制关闭 DEBUG_MODE,双保险防止泄露
DEBUG_MODE = settings.DEBUG_MODE if settings.ENV != "prod" else False
Expand Down
6 changes: 3 additions & 3 deletions backend/app/db/db_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,7 @@ async def init_db():
# 确保所有 Model 已导入,注册到 Base.metadata
from app.models import (
agent_checkpoint, # noqa: F401
chat_history, graph, note, note_template, review_record, user_model,
)
)

async with async_engine.begin() as conn:
# 先删除旧表,然后创建新表
Expand All @@ -101,9 +100,10 @@ async def get_db():


async def seed_test_user():
from sqlalchemy import select

from app.models.user_model import User, UserStatusChoice
from app.utils.auth_utils import hash_password
from sqlalchemy import select

async with AsyncSessionLocal() as session:
result = await session.execute(select(User).where(User.username == "admin"))
Expand Down
2 changes: 1 addition & 1 deletion backend/app/graph/extraction/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
"""图谱抽取子包:双链解析 + LLM 实体抽取。"""
"""图谱抽取子包:双链解析 + LLM 实体抽取。"""
4 changes: 2 additions & 2 deletions backend/app/graph/extraction/entity_extractor.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import re

from app.core.logger_handler import logger
from app.graph.schemas.graph import ExtractResult, ExtractedEntity, ExtractedRelation
from app.graph.schemas.graph import ExtractedEntity, ExtractedRelation, ExtractResult
from app.utils.prompt_loader import load_prompt


Expand Down Expand Up @@ -62,4 +62,4 @@ async def extract_entities(title: str, content: str, chat_model) -> ExtractResul
except Exception as e2:
logger.error(f"实体抽取失败 title={title}: {e2}")
result = _fallback_extract(content or "", "")
return result
return result
2 changes: 1 addition & 1 deletion backend/app/graph/extraction/link_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,4 @@ def parse_links(content: str) -> list[str]:
if target and target not in seen:
seen.add(target)
result.append(target)
return result
return result
2 changes: 1 addition & 1 deletion backend/app/graph/routers/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
"""图谱 API 路由子包。"""
"""图谱 API 路由子包。"""
22 changes: 9 additions & 13 deletions backend/app/graph/routers/graph_router.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,24 @@
import functools
import json

from fastapi import Depends, HTTPException
from fastapi import Depends, HTTPException, Query
from fastapi.responses import StreamingResponse
from fastapi.routing import APIRouter

from app.core.logger_handler import logger
from app.graph.services.event_bus import event_bus
from app.graph.storage.neo4j_client import GraphUnavailableError
from app.utils.auth_utils import get_current_user_id

graph_router = APIRouter(prefix="/api/graph", tags=["graph"])


from fastapi import Depends, Query
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession

from app.core.logger_handler import logger
from app.core.success_response import success_response
from app.db.db_config import get_db
from app.graph.schemas.graph import EntityIn, MergeRequest, RelationIn, TypeIn
from app.graph.services.event_bus import event_bus
from app.graph.services.graph_service import manual_re_extract
from app.graph.storage import get_graph_store
from app.graph.storage.neo4j_client import GraphUnavailableError
from app.models.note import Note
from app.utils.auth_utils import get_current_user_id

graph_router = APIRouter(prefix="/api/graph", tags=["graph"])


def _graph_gate(fn):
Expand Down Expand Up @@ -83,7 +79,7 @@ async def neighbors(entity_id: str, depth: int = Query(1, ge=1, le=3),
async def entity_notes(entity_id: str, user_id: str = Depends(get_current_user_id),
db: AsyncSession = Depends(get_db)):
links = await get_graph_store(db).get_entity_notes(user_id, entity_id)
return success_response(data=[l.model_dump() for l in links])
return success_response(data=[link.model_dump() for link in links])


@graph_router.get("/notes/{note_id}/related")
Expand Down Expand Up @@ -274,4 +270,4 @@ async def gen():
finally:
await event_bus.unsubscribe(user_id, q)

return StreamingResponse(gen(), media_type="text/event-stream")
return StreamingResponse(gen(), media_type="text/event-stream")
2 changes: 1 addition & 1 deletion backend/app/graph/services/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
"""图谱服务子包。"""
"""图谱服务子包。"""
2 changes: 1 addition & 1 deletion backend/app/graph/services/event_bus.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,4 @@ async def publish(self, user_id: str, event: dict) -> None:
q.put_nowait(event)


event_bus = GraphEventBus()
event_bus = GraphEventBus()
2 changes: 1 addition & 1 deletion backend/app/graph/storage/graph_store.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,4 +151,4 @@ async def sweep_orphan_entities(self, user_id: str, candidate_entity_ids: list[s

@abstractmethod
async def clear_all_docs(self, user_id: str) -> None:
"""清空用户全部文档图谱(Doc 节点/doc Chunk/doc 溯源关系),并清扫孤儿实体。"""
"""清空用户全部文档图谱(Doc 节点/doc Chunk/doc 溯源关系),并清扫孤儿实体。"""
1 change: 0 additions & 1 deletion backend/app/rag/agentic_rag/evidence.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

from app.rag.agentic_rag.schemas import Evidence


_SOURCE_LABELS = {
"note": "笔记",
"knowledge_base": "知识库",
Expand Down
3 changes: 0 additions & 3 deletions backend/app/rag/agentic_rag/planner.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,10 @@
import re
from typing import Any

from pydantic import ValidationError

from app.core.logger_handler import logger
from app.core.settings import settings
from app.rag.agentic_rag.schemas import RetrievalPlan, RetrievalStep


FRESHNESS_TERMS = (
"最新",
"现在",
Expand Down
1 change: 0 additions & 1 deletion backend/app/rag/agentic_rag/query_entity_extractor.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,6 @@ async def extract(self, query: str) -> list[str]:
t0 = time.perf_counter()
used_llm = True
try:
from langchain_core.messages import HumanMessage
prompt = self.prompt_template.replace("{query}", query)
response = await model.ainvoke(prompt)
content = getattr(response, "content", response)
Expand Down
1 change: 0 additions & 1 deletion backend/app/rag/agentic_rag/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

from pydantic import BaseModel, Field


EvidenceSource = Literal["note", "knowledge_base", "graph", "web"]
RetrievalTool = Literal[
"search_notes",
Expand Down
1 change: 0 additions & 1 deletion backend/app/rag/agentic_rag/web_search.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
from app.core.settings import settings
from app.rag.agentic_rag.schemas import Evidence


HttpClientFactory = Callable[[], httpx.AsyncClient]


Expand Down
2 changes: 1 addition & 1 deletion backend/app/rag/vector_store.py
Original file line number Diff line number Diff line change
Expand Up @@ -362,4 +362,4 @@ async def main():
docs = await store.get_user_documents()
logger.debug(f"知识库文档数量: {len(docs)}")

asyncio.run(main())
asyncio.run(main())
1 change: 0 additions & 1 deletion backend/app/router/chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
read_pending_interrupt,
)
from app.agent.agent_rag_tool import build_pre_searched_queries
from app.agent.checkpoint.mysql_saver import MySQLCheckpointSaver
from app.core.logger_handler import logger
from app.core.rate_limit import rate_limit
from app.core.success_response import success_response
Expand Down
1 change: 0 additions & 1 deletion backend/app/router/chat_service.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
"""Chat 业务服务层:会话管理、Agent 问答、重排序(旧 RagService 链路已退役,检索统一走 Agentic RAG)。"""
import uuid
from typing import Any

from fastapi import HTTPException
Expand Down
2 changes: 1 addition & 1 deletion backend/app/router/note_router.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,8 +106,8 @@ async def batch_download_notes(
"""
批量下载笔记为 ZIP 压缩包(内含 .md 文件)。
"""
from urllib.parse import quote
from datetime import datetime
from urllib.parse import quote

zip_bytes = await init_manager.note_service.batch_export_zip(db, user_id, payload.ids)
date_str = datetime.now().strftime("%Y%m%d")
Expand Down
7 changes: 4 additions & 3 deletions backend/app/services/note_template_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
笔记模板服务 —— CRUD + 内置模板初始化。
"""

import uuid
import logging
import uuid

from sqlalchemy import select, func
from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession

from app.models.note_template import NoteTemplate
Expand Down Expand Up @@ -56,7 +56,8 @@
"icon": "ListTodo",
"category": "project",
"title": "",
"content": "## 项目概述\n\n\n## 目标\n- [ ] \n\n## 里程碑\n| 阶段 | 内容 | 截止日期 | 状态 |\n|------|------|----------|------|\n| 1 | | | 待开始 |\n\n## 备注\n\n",
"content": "## 项目概述\n\n\n## 目标\n- [ ] \n\n## 里程碑\n| 阶段 | 内容 | 截止日期 | 状态 |\n"
"|------|------|----------|------|\n| 1 | | | 待开始 |\n\n## 备注\n\n",
"tags": ["项目"],
},
{
Expand Down
1 change: 0 additions & 1 deletion backend/app/utils/auth_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import uuid
from typing import Any

from dotenv import load_dotenv
from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from jose import JWTError, jwt
Expand Down
Loading
Loading