128 lines
3.9 KiB
Python
128 lines
3.9 KiB
Python
"""
|
||
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,
|
||
}
|