落地并存测服全端:智能体 Graph/路由、后台切 8180、主账与部署脚本齐备,便于线上重发与验收。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
"""
|
||||
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,
|
||||
}
|
||||
Reference in New Issue
Block a user