""" LLM 网关骨架:入队 Redis Stream,不在回调线程等完整结果。 本文件职责:校验 mode、短幂等、XADD;真实模型 HTTP 后续按 mode 实现。 禁止:wait_result 堵在请求线程;禁止本包维护工单六态。 """ from __future__ import annotations import logging from dataclasses import dataclass from typing import Any, Optional from agent.config import Settings, get_settings from agent.llm.modes import LlmMode from agent.redis_coord import get_redis_runtime, try_acquire_idempotency from agent.redis_coord.runtime import RedisRuntime logger = logging.getLogger(__name__) @dataclass class LlmEnqueueResult: """入队结果。""" accepted: bool entry_id: str = "" reason: str = "" @dataclass class LlmGateway: """ LLM 网关。 调用:HTTP/Handler 只 enqueue;Worker 槽 claim 后调 invoke_stub/未来真调用。 """ settings: Settings redis: RedisRuntime @classmethod def from_settings(cls, settings: Optional[Settings] = None) -> "LlmGateway": cfg = settings or get_settings() return cls(settings=cfg, redis=get_redis_runtime(cfg)) def enqueue( self, *, mode: LlmMode | str, payload: dict[str, Any], idempotency_key: str = "", ) -> LlmEnqueueResult: """ 将任务写入 LLM Stream。 重复 idempotency_key(TTL 内)返回 accepted=False。 副作用:可能写 Redis;不调外部模型。 """ mode_val = mode.value if isinstance(mode, LlmMode) else str(mode) try: LlmMode(mode_val) except ValueError: return LlmEnqueueResult(accepted=False, reason=f"unknown_mode:{mode_val}") if idempotency_key: ok = try_acquire_idempotency( self.redis.client, scope=f"llm:{mode_val}", idempotency_key=idempotency_key, ttl_seconds=300, ) if not ok: return LlmEnqueueResult(accepted=False, reason="duplicate_idempotency_key") body = {"mode": mode_val, "payload": payload} entry_id = self.redis.llm_stream.enqueue( payload=body, idempotency_key=idempotency_key, ) logger.info("llm.enqueue mode=%s id=%s", mode_val, entry_id) return LlmEnqueueResult(accepted=True, entry_id=entry_id) def invoke_stub(self, *, mode: LlmMode | str, payload: dict[str, Any]) -> dict[str, Any]: """ 模型调用占位:返回固定结构,不访问外网。 正式实现按 mode 调 DeepSeek/千问;本方法仅供骨架与单测。 """ mode_val = mode.value if isinstance(mode, LlmMode) else str(mode) if mode_val != LlmMode.ORDERED_ACTIONS.value: from agent.llm.mode_registry import invoke_mode_shell return invoke_mode_shell(mode_val, payload=payload) return { "ok": True, "stub": True, "mode": mode_val, "echo_keys": sorted(payload.keys()), "note": "骨架不调用真实模型", } def invoke( self, *, mode: LlmMode | str, messages: list[dict[str, Any]], allow_network: bool = False, ) -> dict[str, Any]: """ Worker 槽内调用模型。 allow_network=False:走 stub(默认,防误打费)。 allow_network=True:走 LlmHttpClient(需已配 API Key)。 """ if not allow_network: return self.invoke_stub(mode=mode, payload={"messages": len(messages)}) from agent.llm.http_client import LlmHttpClient result = LlmHttpClient.from_settings(self.settings).chat(mode=mode, messages=messages) return { "ok": result.ok, "stub": False, "provider": result.provider, "content": result.content, "error": result.error, }