""" LangGraph Checkpointer 配置模块 支持多种持久化后端: 1. MemorySaver - 内存存储(仅用于测试) 2. PostgresSaver - PostgreSQL 数据库(生产环境) 使用 message_id 和 chat_id 作为 thread_id 来恢复工作流状态 """ import os import asyncio from typing import Optional from langgraph.checkpoint.memory import MemorySaver from config import POSTGRES_CONNECTION_STRING CHECKPOINTER_TYPE = "postgres" def get_thread_id(chat_id: str, message_id: str) -> str: """ 生成唯一的 thread_id Args: chat_id: 聊天会话 ID message_id: 消息 ID Returns: 组合后的唯一 thread_id """ return f"{chat_id}:{message_id}" def parse_thread_id(thread_id: str) -> tuple[str, str]: """ 解析 thread_id 获取 chat_id 和 message_id Args: thread_id: 组合的 thread_id Returns: (chat_id, message_id) 元组 """ parts = thread_id.split(":", 1) if len(parts) == 2: return parts[0], parts[1] return parts[0], "" async def verify_checkpointer_tables_exist() -> bool: """ 验证 checkpointer 所需的数据库表是否存在且结构正确 Returns: True 如果所有必需的表都存在且结构正确,否则 False """ try: from psycopg_pool import AsyncConnectionPool except ImportError: return False try: async with AsyncConnectionPool( POSTGRES_CONNECTION_STRING, kwargs={"autocommit": True} ) as pool: async with pool.connection() as conn: async with conn.cursor() as cur: # 检查 checkpoints 表 await cur.execute(""" SELECT EXISTS ( SELECT FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'checkpoints' ) """) checkpoints_exists = (await cur.fetchone())[0] # 检查 checkpoint_writes 表 await cur.execute(""" SELECT EXISTS ( SELECT FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'checkpoint_writes' ) """) writes_exists = (await cur.fetchone())[0] # 检查 checkpoint_blobs 表 await cur.execute(""" SELECT EXISTS ( SELECT FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'checkpoint_blobs' ) """) blobs_exists = (await cur.fetchone())[0] if not (checkpoints_exists and writes_exists and blobs_exists): return False # 检查 checkpoints 表的 checkpoint 字段是否为 JSONB 类型 await cur.execute(""" SELECT data_type FROM information_schema.columns WHERE table_name = 'checkpoints' AND column_name = 'checkpoint' """) result = await cur.fetchone() if not result or result[0] != 'jsonb': print(f"[Checkpointer] checkpoints.checkpoint column type is {result[0] if result else 'NULL'}, expected 'jsonb'") return False # 检查 checkpoint_writes 表是否有 task_path 列 await cur.execute(""" SELECT EXISTS ( SELECT FROM information_schema.columns WHERE table_name = 'checkpoint_writes' AND column_name = 'task_path' ) """) task_path_exists = (await cur.fetchone())[0] if not task_path_exists: print(f"[Checkpointer] checkpoint_writes.task_path column does not exist") return False # 检查 checkpoint_writes 表的主键是否正确 await cur.execute(""" SELECT COUNT(*) FROM information_schema.table_constraints tc JOIN information_schema.key_column_usage kcu ON tc.constraint_name = kcu.constraint_name WHERE tc.table_name = 'checkpoint_writes' AND tc.constraint_type = 'PRIMARY KEY' """) pk_count = (await cur.fetchone())[0] if pk_count != 5: # 应该有 5 个主键列 print(f"[Checkpointer] checkpoint_writes has {pk_count} primary key columns, expected 5") return False return True except Exception as e: print(f"[Checkpointer] Error verifying tables: {e}") return False async def init_checkpointer_db(): """ 初始化 PostgreSQL checkpointer 数据库表 使用 AsyncPostgresSaver 的 setup 方法创建正确的表结构 """ if CHECKPOINTER_TYPE != "postgres": return try: from langgraph.checkpoint.postgres.aio import AsyncPostgresSaver from psycopg_pool import AsyncConnectionPool import psycopg except ImportError as e: print(f"[Checkpointer] ERROR: Missing required package: {e}") print(f"[Checkpointer] Please install: pip install langgraph-checkpoint-postgres psycopg-pool") raise ImportError( "langgraph-checkpoint-postgres or psycopg-pool not installed. " "Run: pip install langgraph-checkpoint-postgres psycopg-pool" ) from e # 首先检查表是否已存在 tables_exist = await verify_checkpointer_tables_exist() if tables_exist: print(f"[Checkpointer] Database tables already exist, skipping initialization") return print(f"[Checkpointer] Initializing PostgreSQL database tables...") # 尝试使用 AsyncPostgresSaver 的 setup 方法 try: async with AsyncConnectionPool( POSTGRES_CONNECTION_STRING, kwargs={"autocommit": True} ) as pool: checkpointer = AsyncPostgresSaver(pool) await checkpointer.setup() print(f"[Checkpointer] AsyncPostgresSaver.setup() executed") except Exception as e: print(f"[Checkpointer] AsyncPostgresSaver.setup() failed: {e}") # 验证表是否真的被创建 tables_exist_after = await verify_checkpointer_tables_exist() if not tables_exist_after: print(f"[Checkpointer] Tables not created by setup(), using direct SQL...") try: await create_checkpointer_tables_directly() except Exception as e2: print(f"[Checkpointer] Direct SQL initialization also failed: {e2}") raise else: print(f"[Checkpointer] PostgreSQL database tables verified successfully") async def create_checkpointer_tables_directly(): """ 直接通过 SQL 语句创建 checkpointer 所需的数据库表 这是备用方案,当 AsyncPostgresSaver.setup() 失败时使用 注意:LangGraph AsyncPostgresSaver 使用 JSONB 类型存储数据 表名必须是: checkpoints, checkpoint_writes, checkpoint_blobs """ try: from psycopg_pool import AsyncConnectionPool except ImportError as e: print(f"[Checkpointer] ERROR: Missing psycopg-pool: {e}") raise print(f"[Checkpointer] Creating checkpointer tables using direct SQL...") async with AsyncConnectionPool( POSTGRES_CONNECTION_STRING, kwargs={"autocommit": True} ) as pool: async with pool.connection() as conn: async with conn.cursor() as cur: # 先删除旧表(如果存在错误的表结构) await cur.execute("DROP TABLE IF EXISTS checkpoints CASCADE") await cur.execute("DROP TABLE IF EXISTS writes CASCADE") await cur.execute("DROP TABLE IF EXISTS checkpoint_blobs CASCADE") await cur.execute("DROP TABLE IF EXISTS checkpoint_writes CASCADE") await cur.execute("DROP TABLE IF EXISTS blobs CASCADE") await cur.execute("DROP TABLE IF EXISTS checkpoint_migrations CASCADE") print(f"[Checkpointer] Dropped existing tables") # 创建 checkpoint_migrations 表(用于跟踪迁移版本) await cur.execute(""" CREATE TABLE IF NOT EXISTS checkpoint_migrations ( v INTEGER PRIMARY KEY ) """) # 创建 checkpoints 表 - 使用 JSONB 类型 await cur.execute(""" CREATE TABLE IF NOT EXISTS checkpoints ( thread_id TEXT NOT NULL, checkpoint_ns TEXT NOT NULL DEFAULT '', checkpoint_id TEXT NOT NULL, parent_checkpoint_id TEXT, type TEXT, checkpoint JSONB NOT NULL DEFAULT '{}', metadata JSONB NOT NULL DEFAULT '{}', PRIMARY KEY (thread_id, checkpoint_ns, checkpoint_id) ) """) # 创建 checkpoint_writes 表 - 使用 BYTEA 类型 await cur.execute(""" CREATE TABLE IF NOT EXISTS checkpoint_writes ( thread_id TEXT NOT NULL, checkpoint_ns TEXT NOT NULL DEFAULT '', checkpoint_id TEXT NOT NULL, task_id TEXT NOT NULL, task_path TEXT NOT NULL DEFAULT '', idx INTEGER NOT NULL, channel TEXT NOT NULL, type TEXT, blob BYTEA, PRIMARY KEY (thread_id, checkpoint_ns, checkpoint_id, task_id, idx) ) """) # 创建 checkpoint_blobs 表 - 使用 BYTEA 类型存储二进制数据 await cur.execute(""" CREATE TABLE IF NOT EXISTS checkpoint_blobs ( thread_id TEXT NOT NULL, checkpoint_ns TEXT NOT NULL DEFAULT '', channel TEXT NOT NULL, version TEXT NOT NULL, type TEXT NOT NULL, blob BYTEA, PRIMARY KEY (thread_id, checkpoint_ns, channel, version) ) """) # 创建索引以提高查询性能 await cur.execute(""" CREATE INDEX IF NOT EXISTS checkpoints_thread_id_idx ON checkpoints(thread_id) """) await cur.execute(""" CREATE INDEX IF NOT EXISTS checkpoint_writes_thread_id_idx ON checkpoint_writes(thread_id) """) await cur.execute(""" CREATE INDEX IF NOT EXISTS checkpoint_blobs_thread_id_idx ON checkpoint_blobs(thread_id) """) # 插入迁移版本记录 await cur.execute(""" INSERT INTO checkpoint_migrations (v) VALUES (1) ON CONFLICT DO NOTHING """) print(f"[Checkpointer] Checkpointer tables created successfully via direct SQL") def create_checkpointer(checkpointer_type: str = None): """ 创建同步 checkpointer 实例 Args: checkpointer_type: checkpointer 类型,可选 "memory", "postgres" Returns: checkpointer 实例 """ checkpointer_type = checkpointer_type or CHECKPOINTER_TYPE if checkpointer_type == "memory": return MemorySaver() elif checkpointer_type == "postgres": try: from langgraph.checkpoint.postgres import PostgresSaver from psycopg import Connection if not POSTGRES_CONNECTION_STRING: raise ValueError("POSTGRES_CONNECTION_STRING 环境变量未设置") conn = Connection.connect(POSTGRES_CONNECTION_STRING) return PostgresSaver(conn) except ImportError: print("[WARNING] psycopg 或 langgraph.checkpoint.postgres 未安装,回退到 memory") return MemorySaver() else: print(f"[WARNING] 未知的 checkpointer 类型: {checkpointer_type},使用 memory") return MemorySaver() def create_async_checkpointer(checkpointer_type: str = None): """ 创建异步 checkpointer 实例 Args: checkpointer_type: checkpointer 类型 Returns: 异步 checkpointer 实例 """ checkpointer_type = checkpointer_type or CHECKPOINTER_TYPE print(f"[Checkpointer] 创建异步 checkpointer, 类型配置: {checkpointer_type}") if checkpointer_type == "memory": print("[Checkpointer] 使用 MemorySaver") return MemorySaver() elif checkpointer_type == "postgres": try: from langgraph.checkpoint.postgres.aio import AsyncPostgresSaver from psycopg_pool import AsyncConnectionPool if not POSTGRES_CONNECTION_STRING: raise ValueError("POSTGRES_CONNECTION_STRING 环境变量未设置") pool = AsyncConnectionPool(POSTGRES_CONNECTION_STRING) return AsyncPostgresSaver(pool) except ImportError: print("[WARNING] psycopg_pool 或 langgraph.checkpoint.postgres 未安装,回退到 memory") return MemorySaver() else: print(f"[WARNING] 未知的 checkpointer 类型: {checkpointer_type},使用 memory") return MemorySaver() async def get_or_create_async_checkpointer(checkpointer_type: str = None): """ 获取或创建异步 checkpointer 实例(正确处理上下文管理器) Args: checkpointer_type: checkpointer 类型 Returns: 已初始化的异步 checkpointer 实例 """ checkpointer = create_async_checkpointer(checkpointer_type) if hasattr(checkpointer, '__aenter__'): checkpointer = await checkpointer.__aenter__() return checkpointer class CheckpointerManager: """ Checkpointer 管理器 - 单例模式 用于管理多个工作流的 checkpointer 实例 """ _instance = None _sync_checkpointer = None _async_conn_pool = None _async_checkpointer = None _initialized = False def __new__(cls): if cls._instance is None: cls._instance = super().__new__(cls) return cls._instance def get_checkpointer(self): """获取同步 checkpointer 实例""" if self._sync_checkpointer is None: self._sync_checkpointer = create_checkpointer() return self._sync_checkpointer async def get_async_checkpointer(self): """ 获取异步 checkpointer 实例 """ if self._async_checkpointer is None: if CHECKPOINTER_TYPE == "postgres": try: from langgraph.checkpoint.postgres.aio import AsyncPostgresSaver from psycopg_pool import AsyncConnectionPool if not POSTGRES_CONNECTION_STRING: raise ValueError("POSTGRES_CONNECTION_STRING not set") self._async_conn_pool = AsyncConnectionPool( POSTGRES_CONNECTION_STRING, open=False ) await self._async_conn_pool.open() self._async_checkpointer = AsyncPostgresSaver(self._async_conn_pool) # 检查表是否存在 tables_exist = await verify_checkpointer_tables_exist() if not tables_exist: # 尝试使用 setup() 方法 try: await self._async_checkpointer.setup() print(f"[Checkpointer] AsyncPostgresSaver.setup() executed") except Exception as setup_e: print(f"[Checkpointer] AsyncPostgresSaver.setup() failed: {setup_e}") # 再次验证表是否被创建 tables_exist_after = await verify_checkpointer_tables_exist() if not tables_exist_after: print(f"[Checkpointer] Tables not created by setup(), using direct SQL...") await create_checkpointer_tables_directly() else: print(f"[Checkpointer] Database tables already exist") print(f"[Checkpointer] AsyncPostgresSaver created, connection: {POSTGRES_CONNECTION_STRING.split('@')[1] if '@' in POSTGRES_CONNECTION_STRING else POSTGRES_CONNECTION_STRING}") except ImportError as e: print(f"[WARNING] psycopg_pool or langgraph.checkpoint.postgres not installed: {e}, fallback to memory") self._async_checkpointer = MemorySaver() except Exception as e: print(f"[Checkpointer] ERROR: Failed to create AsyncPostgresSaver: {e}") raise elif CHECKPOINTER_TYPE == "memory": self._async_checkpointer = MemorySaver() print("[Checkpointer] Using MemorySaver") else: self._async_checkpointer = MemorySaver() print(f"[Checkpointer] Unknown type {CHECKPOINTER_TYPE}, using MemorySaver") print(f"[Checkpointer] Async checkpointer type: {type(self._async_checkpointer).__name__}") return self._async_checkpointer async def setup(self): """ 初始化 checkpointer(创建数据库表等) """ if not self._initialized: await init_checkpointer_db() try: from tools.fault_record_db import init_fault_records_table await init_fault_records_table() except Exception as e: print(f"[Checkpointer] 初始化 fault_records 表失败: {e}") self._initialized = True def reset(self): """重置 checkpointer(主要用于测试)""" self._sync_checkpointer = None self._async_checkpointer = None self._async_conn_pool = None self._initialized = False checkpointer_manager = CheckpointerManager() if __name__ == "__main__": print("Checkpointer 配置测试:") print(f" 类型: {CHECKPOINTER_TYPE}") print(f" PostgreSQL 连接: {'已配置' if POSTGRES_CONNECTION_STRING else '未配置'}")