Files
ZNJJ-api-server/src/core/fastgpt_client.py
T
Eric Wang 97de142e6d feat(storage): add selectable SQLite and PostgreSQL persistence
- Select one storage backend per instance via AGENT_STORAGE, retaining
  memory as the backward-compatible default. Manage the selected async
  repository, official checkpointer and connection pools in lifespan;
  fail startup on unavailable or uninitialized storage without fallback.
- Add SQLite and PostgreSQL adapters, schema initialization CLI, sample
  PostgreSQL compose configuration and a Windows-compatible serve entry.
  SQLite enforces single-instance file ownership; PostgreSQL uses a
  dedicated pool for session advisory locks plus version/epoch fencing.
- Make repository IO and commit callbacks awaitable. Persist business
  fields, a complete committed graph snapshot and turn result/version in
  one business transaction before success. Rebuild execution checkpoints
  from that snapshot, discarding uncommitted progress after interruption.
- Support optional Idempotency-Key on durable chat requests. Replay
  committed results and return 409 on changed request content; delete
  removes business/history data and replay records while retaining only
  an epoch/version tombstone to reject stale commits.
- Document migration, database switching boundaries, backup/deployment
  expectations and crash recovery. Ignore generated database files.

Validation: 315 tests passed with real SQLite and isolated PostgreSQL,
including abrupt child-process exits before/after business commit;
both backends passed CLI migration and actual HTTP server restart smoke.
Existing API tests were rechecked after isolating them from user storage.
An initial Windows CreateProcess permission failure did not recur in the
full diagnostic run; failure assertions were not skipped or relaxed.
2026-10-24 13:41:54 +08:00

80 lines
2.9 KiB
Python

"""后端与单一会话存储的生命周期、依赖注入。"""
from contextlib import AsyncExitStack, asynccontextmanager
from fastapi import FastAPI
from fastgpt_client import AsyncChatClient
from ..agent.graph import build_graph
from ..backends.chat import ChatBackend
from ..backends.fastgpt import FastGPTBackend
from ..backends.langgraph import LangGraphBackend
from ..services.app_service import AccidentAppService
from ..storage.checkpoints import delete_checkpoint
from .config import Config
_fastgpt_client: AsyncChatClient | None = None
_chat_backend: ChatBackend | None = None
_langgraph_graph = None
_app_service: AccidentAppService | None = None
FASTGPT_TIMEOUT_SECONDS = 60.0
FASTGPT_MAX_RETRIES = 3
@asynccontextmanager
async def lifespan(app: FastAPI):
global _chat_backend, _fastgpt_client, _langgraph_graph, _app_service
Config.validate()
llm = None
async with AsyncExitStack() as resources:
try:
if Config.AGENT_BACKEND == 'langgraph':
from ..agent.llm import build_llm_service
from ..storage.runtime import StorageSettings, open_storage
storage = await resources.enter_async_context(open_storage(StorageSettings.from_config(Config)))
_langgraph_graph = build_graph(storage.checkpointer)
llm = build_llm_service()
_chat_backend = LangGraphBackend(_langgraph_graph, llm=llm)
_app_service = AccidentAppService(_chat_backend, storage.repository, locks=storage.locks)
else:
_fastgpt_client = AsyncChatClient(
api_key=Config.FASTGPT_API_KEY, base_url=Config.FASTGPT_BASE_URL,
timeout=FASTGPT_TIMEOUT_SECONDS, max_retries=FASTGPT_MAX_RETRIES + 1,
retry_delay=1.0, enable_logging=Config.DEBUG,
)
await _fastgpt_client.__aenter__()
_chat_backend = FastGPTBackend(_fastgpt_client)
yield
finally:
_chat_backend = None
_langgraph_graph = None
_app_service = None
if llm is not None and hasattr(llm, 'aclose'):
await llm.aclose()
if _fastgpt_client:
await _fastgpt_client.__aexit__(None, None, None)
_fastgpt_client = None
async def delete_chat_session(thread_id: str) -> None:
await delete_checkpoint(_langgraph_graph, thread_id)
def get_fastgpt_client() -> AsyncChatClient:
if _fastgpt_client is None:
raise RuntimeError('FastGPT client not initialized')
return _fastgpt_client
def get_app_service() -> AccidentAppService:
if _app_service is None:
raise RuntimeError('Application service not initialized')
return _app_service
def get_chat_backend() -> ChatBackend:
if _chat_backend is None:
raise RuntimeError('Chat backend not initialized')
return _chat_backend