"""NL2SQL Supervisor 的意图和会话并发控制。""" from __future__ import annotations from uuid import uuid4 class UnsupportedIntent(ValueError): """当前版本不支持的查询意图。""" class SessionBusyError(RuntimeError): """同一会话已有查询运行。""" def detect_intent(question: str) -> str: """根据有限关键词识别当前版本的任务类型。""" text = question.lower() if "etl" in text or "数据任务" in text: return "etl_build" if "口径" in text or "caliber" in text: return "caliber_maintain" if "血缘" in text or "lineage" in text: return "lineage_query" return "query" def ensure_query_intent(question: str) -> None: """M1 只允许 query 意图进入 SQL 主链路。""" intent = detect_intent(question) if intent != "query": raise UnsupportedIntent(f"当前不支持 {intent} 意图") class SessionLock: """基于 Redis NX 的同会话互斥锁。""" def __init__(self, redis, session_id: str, *, ttl: int = 120): self.redis = redis self.key = f"nl2sql:session:{session_id}:lock" self.token = uuid4().hex self.ttl = ttl self.acquired = False async def acquire(self) -> None: if not await self.redis.set(self.key, self.token, nx=True, ex=self.ttl): raise SessionBusyError("同一会话已有查询运行") self.acquired = True async def release(self) -> None: if not self.acquired: return release_script = """ if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) end return 0 """ if hasattr(self.redis, "eval"): await self.redis.eval(release_script, 1, self.key, self.token) else: current = await self.redis.get(self.key) if current in {self.token, self.token.encode()}: await self.redis.delete(self.key) self.acquired = False