0.1.0
This commit is contained in:
@@ -1,10 +1,11 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
from . import chat, health, luckysheet_ws, tasks
|
||||
from . import chat, health, luckysheet_ws, tasks, lightrag
|
||||
|
||||
api_router = APIRouter(prefix="/api/v1")
|
||||
api_router.include_router(tasks.router, tags=["tasks"])
|
||||
api_router.include_router(chat.router, tags=["chat"])
|
||||
api_router.include_router(lightrag.router, tags=["lightrag"])
|
||||
|
||||
root_router = APIRouter()
|
||||
root_router.include_router(health.router, tags=["health"])
|
||||
|
||||
@@ -1,26 +1,67 @@
|
||||
import asyncio
|
||||
from fastapi import APIRouter
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import AsyncIterator, Optional
|
||||
|
||||
from fastapi import APIRouter, HTTPException, status
|
||||
from fastapi.responses import StreamingResponse
|
||||
|
||||
from app.deps import AuthDep
|
||||
from app.services.lightrag_service import lightrag_service
|
||||
from app.services.supabase_rest import supabase_rest
|
||||
|
||||
router = APIRouter(prefix="/chat")
|
||||
|
||||
|
||||
@router.get("")
|
||||
async def chat(query: str, document_id: str, auth: AuthDep) -> StreamingResponse: # noqa: ARG001
|
||||
async def chat(
|
||||
query: str,
|
||||
auth: AuthDep,
|
||||
document_id: Optional[str] = None,
|
||||
) -> StreamingResponse:
|
||||
"""
|
||||
SSE 流式占位。后续会调用 LightRAG + OpenAI。
|
||||
当前直接返回 mock 文字,确保前端链路可用。
|
||||
基于 LightRAG 的 SSE 流式回答。
|
||||
query: 必填问题
|
||||
document_id: 可选,指定所属 workspace
|
||||
"""
|
||||
if not query or not query.strip():
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="query 不能为空")
|
||||
|
||||
async def event_stream() -> asyncio.AsyncGenerator[str, None]:
|
||||
reply = await lightrag_service.query(query_text=query, user_id="placeholder-user")
|
||||
chunks = [reply[: len(reply) // 2 or 1], reply[len(reply) // 2 or 1 :]]
|
||||
for chunk in chunks:
|
||||
yield f"data: {chunk}\n\n"
|
||||
await asyncio.sleep(0.1)
|
||||
workspace_id: Optional[str] = None
|
||||
if document_id:
|
||||
document = supabase_rest.select_one(
|
||||
"documents", {"id": document_id, "user_id": auth.user_id}
|
||||
)
|
||||
if not document:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Document not found")
|
||||
workspace_id = (
|
||||
str(document.get("workspace_id")) if document.get("workspace_id") else None
|
||||
)
|
||||
|
||||
health = await lightrag_service.run_healthcheck()
|
||||
if not health.get("ok"):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail=health.get("error", "LightRAG 未就绪"),
|
||||
)
|
||||
|
||||
lightrag_result = await lightrag_service.stream_answer(
|
||||
query_text=query, user_id=auth.user_id, workspace_id=workspace_id
|
||||
)
|
||||
|
||||
async def event_stream() -> AsyncIterator[str]:
|
||||
if lightrag_result.get("is_streaming") and lightrag_result.get("iterator"):
|
||||
iterator = lightrag_result["iterator"]
|
||||
async for chunk in iterator:
|
||||
payload = json.dumps({"type": "chunk", "content": chunk})
|
||||
yield f"data: {payload}\n\n"
|
||||
else:
|
||||
payload = json.dumps(
|
||||
{"type": "chunk", "content": lightrag_result.get("content", "")}
|
||||
)
|
||||
yield f"data: {payload}\n\n"
|
||||
references = lightrag_result.get("references", [])
|
||||
yield f"data: {json.dumps({'type': 'references', 'data': references})}\n\n"
|
||||
yield "data: [DONE]\n\n"
|
||||
|
||||
return StreamingResponse(event_stream(), media_type="text/event-stream")
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter, HTTPException, status
|
||||
from pydantic import BaseModel
|
||||
|
||||
from app.deps import AuthDep
|
||||
from app.schemas.tasks import TaskStatusResponse
|
||||
from app.services.lightrag_service import lightrag_service
|
||||
from app.services.supabase_rest import supabase_rest
|
||||
from app.services.task_tracker import task_tracker
|
||||
from app.workers.tasks import lightrag_index_pipeline
|
||||
|
||||
router = APIRouter(prefix="/lightrag")
|
||||
|
||||
|
||||
class IndexRequest(BaseModel):
|
||||
document_id: str
|
||||
|
||||
|
||||
@router.post("/index", response_model=TaskStatusResponse)
|
||||
async def enqueue_lightrag_index(payload: IndexRequest, auth: AuthDep) -> TaskStatusResponse:
|
||||
document = supabase_rest.select_one(
|
||||
"documents", {"id": payload.document_id, "user_id": auth.user_id}
|
||||
)
|
||||
if not document:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Document not found")
|
||||
|
||||
raw_text = str(document.get("raw_text") or "").strip()
|
||||
if not raw_text:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="文档尚未生成 raw_text,无法索引",
|
||||
)
|
||||
|
||||
supabase_rest.update(
|
||||
"documents", {"id": payload.document_id}, {"index_status": "pending"}
|
||||
)
|
||||
task = task_tracker.create_task(
|
||||
user_id=auth.user_id, document_id=payload.document_id, task_type="index"
|
||||
)
|
||||
lightrag_index_pipeline.delay(task.task_id, payload.document_id, auth.user_id)
|
||||
return task
|
||||
|
||||
|
||||
@router.get("/health")
|
||||
async def lightrag_health():
|
||||
result = await lightrag_service.run_healthcheck()
|
||||
if not result.get("ok"):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail=result.get("error", "LightRAG 未就绪"),
|
||||
)
|
||||
return result
|
||||
@@ -102,8 +102,9 @@ def _decode_ws_payload(raw_message: str) -> Optional[dict]:
|
||||
|
||||
async def _fetch_supabase_user(access_token: str) -> Optional[dict]:
|
||||
base_url = settings.supabase_url.rstrip("/")
|
||||
apikey = settings.supabase_anon_key or settings.supabase_service_role_key
|
||||
headers = {
|
||||
"apikey": settings.supabase_service_role_key,
|
||||
"apikey": apikey,
|
||||
"Authorization": f"Bearer {access_token}",
|
||||
}
|
||||
try:
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
from fastapi import APIRouter, HTTPException, status
|
||||
|
||||
from app.deps import AuthDep
|
||||
from app.schemas.tasks import OcrTaskRequest, TaskStatusResponse
|
||||
from app.schemas.tasks import MediaOcrTaskRequest, OcrTaskRequest, TaskStatusResponse
|
||||
from app.services.supabase_rest import supabase_rest
|
||||
from app.services.task_tracker import task_tracker
|
||||
from app.workers.tasks import ocr_pipeline
|
||||
from app.workers.tasks import media_ocr_pipeline, ocr_pipeline
|
||||
from app.workers.utils import dispatch_task
|
||||
|
||||
router = APIRouter(prefix="/tasks")
|
||||
|
||||
@@ -12,7 +14,8 @@ router = APIRouter(prefix="/tasks")
|
||||
async def enqueue_ocr_task(payload: OcrTaskRequest, auth: AuthDep) -> TaskStatusResponse:
|
||||
"""记录任务并投递 Celery,阶段 0 返回占位任务。"""
|
||||
task = task_tracker.create_task(user_id=auth.user_id, document_id=payload.document_id, task_type="ocr")
|
||||
ocr_pipeline.delay(
|
||||
dispatch_task(
|
||||
ocr_pipeline,
|
||||
task_id=task.task_id,
|
||||
document_id=payload.document_id,
|
||||
file_url=str(payload.file_url),
|
||||
@@ -21,6 +24,31 @@ async def enqueue_ocr_task(payload: OcrTaskRequest, auth: AuthDep) -> TaskStatus
|
||||
return task
|
||||
|
||||
|
||||
@router.post("/media-ocr")
|
||||
async def enqueue_media_ocr(payload: MediaOcrTaskRequest, auth: AuthDep) -> dict:
|
||||
"""触发媒体资产 OCR,占位实现先写入模拟文本。"""
|
||||
asset = supabase_rest.select_one("media_assets", {"id": payload.asset_id})
|
||||
if not asset:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Media asset not found")
|
||||
|
||||
document_id = asset.get("document_id")
|
||||
if not document_id:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Media asset missing document")
|
||||
|
||||
document = supabase_rest.select_one("documents", {"id": document_id})
|
||||
if not document or document.get("user_id") != auth.user_id:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Forbidden")
|
||||
|
||||
supabase_rest.update("media_assets", {"id": payload.asset_id}, {"ocr_status": "processing"})
|
||||
dispatch_task(
|
||||
media_ocr_pipeline,
|
||||
asset_id=payload.asset_id,
|
||||
user_id=auth.user_id,
|
||||
document_id=document_id,
|
||||
)
|
||||
return {"asset_id": payload.asset_id, "status": "queued"}
|
||||
|
||||
|
||||
@router.get("/{task_id}", response_model=TaskStatusResponse)
|
||||
async def get_task_status(task_id: str, auth: AuthDep) -> TaskStatusResponse:
|
||||
task = task_tracker.get_task(task_id=task_id, user_id=auth.user_id)
|
||||
|
||||
Reference in New Issue
Block a user