Files
inquiry_robot/inquiry-agent/agent/llm/gateway.py
T

128 lines
3.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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,
}