2026-04-28 04:32:52 -07:00
|
|
|
"""Lightweight middleware that loads the anonymous-session document into state.
|
|
|
|
|
|
|
|
|
|
Anonymous chats receive a single uploaded document via Redis (no DB row,
|
|
|
|
|
read-only). This middleware loads it once on the first turn into
|
|
|
|
|
``state['kb_anon_doc']`` so:
|
|
|
|
|
|
|
|
|
|
* :class:`KnowledgeTreeMiddleware` can render the synthetic ``/documents``
|
|
|
|
|
view without touching the DB.
|
|
|
|
|
* :class:`KnowledgePriorityMiddleware` skips hybrid search and emits a
|
|
|
|
|
degenerate priority list.
|
|
|
|
|
* :class:`KBPostgresBackend` (``als_info`` / ``aread`` / ``_load_file_data``)
|
|
|
|
|
recognises the synthetic path.
|
|
|
|
|
|
|
|
|
|
The middleware is a no-op when ``anon_session_id`` is not provided or when
|
|
|
|
|
the document is already cached in state.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import json
|
|
|
|
|
import logging
|
|
|
|
|
from typing import Any
|
|
|
|
|
|
|
|
|
|
from langchain.agents.middleware import AgentMiddleware, AgentState
|
|
|
|
|
from langgraph.runtime import Runtime
|
|
|
|
|
|
2026-06-05 10:54:15 +02:00
|
|
|
from app.agents.multi_agent_chat.shared.state.filesystem_state import (
|
|
|
|
|
SurfSenseFilesystemState,
|
|
|
|
|
)
|
2026-06-04 12:34:28 +02:00
|
|
|
from app.agents.shared.path_resolver import DOCUMENTS_ROOT, safe_filename
|
2026-04-28 04:32:52 -07:00
|
|
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class AnonymousDocumentMiddleware(AgentMiddleware): # type: ignore[type-arg]
|
|
|
|
|
"""Load the anonymous user's uploaded document from Redis into state."""
|
|
|
|
|
|
|
|
|
|
tools = ()
|
|
|
|
|
state_schema = SurfSenseFilesystemState
|
|
|
|
|
|
|
|
|
|
def __init__(self, *, anon_session_id: str | None) -> None:
|
|
|
|
|
self.anon_session_id = anon_session_id
|
|
|
|
|
|
|
|
|
|
async def abefore_agent( # type: ignore[override]
|
|
|
|
|
self,
|
|
|
|
|
state: AgentState,
|
|
|
|
|
runtime: Runtime[Any],
|
|
|
|
|
) -> dict[str, Any] | None:
|
|
|
|
|
del runtime
|
|
|
|
|
if not self.anon_session_id:
|
|
|
|
|
return None
|
|
|
|
|
if state.get("kb_anon_doc"):
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
anon_doc = await self._load_anon_document()
|
|
|
|
|
if anon_doc is None:
|
|
|
|
|
return None
|
|
|
|
|
return {"kb_anon_doc": anon_doc}
|
|
|
|
|
|
|
|
|
|
async def _load_anon_document(self) -> dict[str, Any] | None:
|
|
|
|
|
"""Read ``anon:doc:<session_id>`` from Redis."""
|
|
|
|
|
try:
|
|
|
|
|
import redis.asyncio as aioredis # local import to keep cold paths cheap
|
|
|
|
|
|
|
|
|
|
from app.config import config
|
|
|
|
|
|
|
|
|
|
redis_client = aioredis.from_url(
|
|
|
|
|
config.REDIS_APP_URL, decode_responses=True
|
|
|
|
|
)
|
|
|
|
|
try:
|
|
|
|
|
redis_key = f"anon:doc:{self.anon_session_id}"
|
|
|
|
|
data = await redis_client.get(redis_key)
|
|
|
|
|
if not data:
|
|
|
|
|
return None
|
|
|
|
|
payload = json.loads(data)
|
|
|
|
|
finally:
|
|
|
|
|
await redis_client.aclose()
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
logger.warning("Failed to load anonymous document from Redis: %s", exc)
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
title = str(payload.get("filename") or "uploaded_document")
|
|
|
|
|
content = str(payload.get("content") or "")
|
|
|
|
|
path = f"{DOCUMENTS_ROOT}/{safe_filename(title)}"
|
|
|
|
|
return {
|
|
|
|
|
"path": path,
|
|
|
|
|
"title": title,
|
|
|
|
|
"content": content,
|
|
|
|
|
"chunks": [{"chunk_id": -1, "content": content}] if content else [],
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
__all__ = ["AnonymousDocumentMiddleware"]
|