Add LLM token
This commit is contained in:
@@ -3,10 +3,14 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import AsyncGenerator
|
||||
import os
|
||||
import re
|
||||
import tempfile
|
||||
from typing import AsyncGenerator, Optional
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from fastapi import APIRouter, Depends, File, UploadFile
|
||||
from fastapi.responses import StreamingResponse
|
||||
from loguru import logger
|
||||
|
||||
from app.api.dependencies.auth import get_current_user
|
||||
from app.config.settings import settings
|
||||
@@ -15,6 +19,8 @@ from app.schemas.rag import RagChatRequest, QuickQuestionsResponse, QuickQuestio
|
||||
from app.shared.async_utils import iter_in_thread
|
||||
from app.shared.bootstrap import get_agent_conversation_service
|
||||
|
||||
# Maximum characters of document text injected as LLM context (≈ 6 000 tokens).
|
||||
_MAX_CONTEXT_CHARS = 8_000
|
||||
|
||||
router = APIRouter(prefix="/rag", tags=["RAG问答"])
|
||||
|
||||
@@ -28,17 +34,90 @@ _DEFAULT_QUICK_QUESTIONS = [
|
||||
]
|
||||
|
||||
|
||||
def _extract_text_from_bytes(content: bytes, filename: str) -> str:
|
||||
"""Extract plain text from an uploaded file using the document parser.
|
||||
|
||||
Tries the configured parser first; falls back to raw UTF-8 decode for
|
||||
plain-text formats (.txt, .md). Returns at most _MAX_CONTEXT_CHARS characters
|
||||
so the text fits comfortably inside the LLM context window.
|
||||
"""
|
||||
suffix = os.path.splitext(filename or "doc.pdf")[1] or ".pdf"
|
||||
# Fast path: plain-text files don't need a parser
|
||||
if suffix.lower() in {".txt", ".md", ".csv"}:
|
||||
try:
|
||||
return content.decode("utf-8", errors="replace")[:_MAX_CONTEXT_CHARS]
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
tmp_path = ""
|
||||
try:
|
||||
with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as tmp:
|
||||
tmp.write(content)
|
||||
tmp_path = tmp.name
|
||||
from app.shared.bootstrap import get_document_command_service
|
||||
svc = get_document_command_service()
|
||||
parsed = svc.parser.parse(file_path=tmp_path, doc_id="ctx_extract", doc_name=filename)
|
||||
if parsed.raw_text:
|
||||
return parsed.raw_text[:_MAX_CONTEXT_CHARS]
|
||||
# Fallback: join semantic blocks
|
||||
return "\n".join(
|
||||
b.get("text", "") for b in parsed.semantic_blocks if b.get("text")
|
||||
)[:_MAX_CONTEXT_CHARS]
|
||||
except Exception as exc:
|
||||
logger.warning("Context text extraction failed for {}: {}", filename, exc)
|
||||
return ""
|
||||
finally:
|
||||
if tmp_path:
|
||||
try:
|
||||
os.unlink(tmp_path)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
@router.post("/upload-context")
|
||||
async def upload_context(
|
||||
file: UploadFile = File(...),
|
||||
current_user: UserClaims = Depends(get_current_user),
|
||||
):
|
||||
"""Extract text from an uploaded document and return it as conversation context.
|
||||
|
||||
The client stores the returned text and includes it in subsequent /rag/chat
|
||||
requests via the context_text field — the LLM receives the document content
|
||||
directly without requiring vector-store indexing.
|
||||
"""
|
||||
content = await file.read()
|
||||
filename = file.filename or "document"
|
||||
text = await __import__("asyncio").to_thread(_extract_text_from_bytes, content, filename)
|
||||
if not text.strip():
|
||||
from fastapi import HTTPException
|
||||
raise HTTPException(status_code=422, detail="Could not extract text from the uploaded file.")
|
||||
return {
|
||||
"filename": filename,
|
||||
"text": text,
|
||||
"char_count": len(text),
|
||||
"truncated": len(text) >= _MAX_CONTEXT_CHARS,
|
||||
}
|
||||
|
||||
|
||||
@router.post("/chat")
|
||||
async def rag_chat(
|
||||
request: RagChatRequest,
|
||||
current_user: UserClaims = Depends(get_current_user),
|
||||
):
|
||||
"""Stream RAG Q&A using the real agent service."""
|
||||
"""Stream RAG Q&A using the real agent service.
|
||||
|
||||
When request.context_text is provided the document text is passed directly
|
||||
to the answer generator as a dedicated document context section — RAG
|
||||
retrieval still runs on the user's original question (not the document text)
|
||||
so embedding quality is preserved for regulation chunk matching.
|
||||
"""
|
||||
session_id, event_stream = get_agent_conversation_service().stream_chat(
|
||||
query=request.query,
|
||||
session_id=request.session_id,
|
||||
filters=request.filters,
|
||||
top_k=request.top_k or settings.rag_top_k,
|
||||
context_text=request.context_text,
|
||||
context_filename=request.context_filename,
|
||||
)
|
||||
|
||||
async def generate() -> AsyncGenerator[str, None]:
|
||||
|
||||
Reference in New Issue
Block a user