落地并存测服全端:智能体 Graph/路由、后台切 8180、主账与部署脚本齐备,便于线上重发与验收。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
linmianxun
2026-09-13 21:15:58 +08:00
co-authored by Cursor
parent 0eb1adecc8
commit 8103b3b651
143 changed files with 8968 additions and 277 deletions
+17 -3
View File
@@ -1,6 +1,20 @@
"""
LLM 网关与队列:DeepSeek / 千问 mode schema、认领、重试。
LLM 网关与队列:DeepSeek / 千问 mode、入队、认领(Worker 侧后续接)。
禁止:在本包维护工单六态;禁止 Redis 全局 Worker 单例锁。
骨架:仅包占位。
禁止:在本包维护工单六态;禁止 Redis 全局 Worker 单例锁;禁止回调线程等完整 LLM。
"""
from agent.llm.gateway import LlmEnqueueResult, LlmGateway
from agent.llm.http_client import LlmHttpClient, LlmHttpResult
from agent.llm.mode_registry import invoke_mode_shell, list_mode_shells
from agent.llm.modes import LlmMode
__all__ = [
"LlmMode",
"LlmGateway",
"LlmEnqueueResult",
"LlmHttpClient",
"LlmHttpResult",
"invoke_mode_shell",
"list_mode_shells",
]
+127
View File
@@ -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,
}
+156
View File
@@ -0,0 +1,156 @@
"""
LLM 出站 HTTP 客户端(DeepSeek / 千问兼容 OpenAI Chat)。
本文件职责:带超时的 chat.completions 调用;缺 key / 未确认数据用途时明确失败。
禁止:在企微回调线程同步长等;正式长调用走 Worker 槽 + 队列。
"""
from __future__ import annotations
import logging
from dataclasses import dataclass
from typing import Any, Optional
import httpx
from agent.config import Settings, get_settings
from agent.llm.modes import LlmMode
logger = logging.getLogger(__name__)
@dataclass
class LlmHttpResult:
ok: bool
content: str = ""
raw: Optional[dict[str, Any]] = None
error: str = ""
provider: str = ""
tool_calls: list[dict[str, Any]] | None = None
@dataclass
class LlmHttpClient:
"""
兼容 OpenAI 的 Chat Completions 客户端。
DeepSeek / 千问 DashScope compatible-mode 共用此壳。
"""
settings: Settings
timeout_seconds: float = 60.0
@classmethod
def from_settings(cls, settings: Optional[Settings] = None) -> "LlmHttpClient":
return cls(settings=settings or get_settings())
def _provider_for_mode(self, mode: LlmMode | str) -> tuple[str, str, str]:
"""
返回 (provider, base_url, api_key)。
A/B 文本 → DeepSeek;附件/视觉 → 千问。
"""
mode_val = mode.value if isinstance(mode, LlmMode) else str(mode)
visionish = mode_val in {
LlmMode.ATTACHMENT_TEXT.value,
LlmMode.VISION.value,
LlmMode.SHEET_MAP.value,
}
if visionish:
return (
"qwen",
(self.settings.qwen_base_url or "").rstrip("/"),
self.settings.qwen_api_key or "",
)
return (
"deepseek",
(self.settings.deepseek_base_url or "").rstrip("/"),
self.settings.deepseek_api_key or "",
)
def _model_for(self, provider: str, mode: LlmMode | str) -> str:
mode_val = mode.value if isinstance(mode, LlmMode) else str(mode)
if provider == "qwen":
if mode_val == LlmMode.VISION.value:
return self.settings.qwen_vl_model or "qwen-vl-plus"
return self.settings.qwen_text_model or "qwen-plus"
return self.settings.deepseek_model or "deepseek-chat"
def chat(
self,
*,
mode: LlmMode | str,
messages: list[dict[str, Any]],
temperature: float = 0.0,
tools: Optional[list[dict[str, Any]]] = None,
tool_choice: Any = None,
) -> LlmHttpResult:
"""
发起一次 chat.completions(可带 tools)。
副作用:出站 HTTPS;超时由 timeout_seconds 限制。
未确认 LLM_DATA_USAGE_CONFIRMED 时拒绝调用(prompt/04)。
"""
if not self.settings.llm_data_usage_confirmed:
return LlmHttpResult(
ok=False,
error="llm_data_usage_not_confirmed",
provider="none",
)
provider, base, key = self._provider_for_mode(mode)
if not key or not base:
return LlmHttpResult(
ok=False,
error=f"{provider}_not_configured",
provider=provider,
)
model = self._model_for(provider, mode)
url = f"{base}/chat/completions"
headers = {
"Authorization": f"Bearer {key}",
"Content-Type": "application/json",
}
body: dict[str, Any] = {
"model": model,
"messages": messages,
"temperature": temperature,
}
if tools:
body["tools"] = tools
if tool_choice is not None:
body["tool_choice"] = tool_choice
try:
with httpx.Client(timeout=self.timeout_seconds) as client:
resp = client.post(url, headers=headers, json=body)
data = resp.json() if resp.content else {}
if resp.status_code >= 400:
logger.warning(
"llm.http 失败 provider=%s status=%s",
provider,
resp.status_code,
)
return LlmHttpResult(
ok=False,
error=f"http_{resp.status_code}",
raw=data if isinstance(data, dict) else None,
provider=provider,
)
choices = (data or {}).get("choices") or []
content = ""
tool_calls: list[dict[str, Any]] = []
if choices:
msg = (choices[0].get("message") or {})
content = str(msg.get("content") or "")
raw_calls = msg.get("tool_calls") or []
if isinstance(raw_calls, list):
tool_calls = [c for c in raw_calls if isinstance(c, dict)]
return LlmHttpResult(
ok=True,
content=content,
raw=data if isinstance(data, dict) else None,
provider=provider,
tool_calls=tool_calls or None,
)
except httpx.HTTPError as exc:
logger.warning("llm.http 异常 provider=%s err=%s", provider, exc)
return LlmHttpResult(ok=False, error=str(exc), provider=provider)
@@ -0,0 +1,30 @@
"""
千问:附件文本 mode 壳。
"""
from __future__ import annotations
from typing import Any
from agent.llm.modes import LlmMode
MODE = LlmMode.ATTACHMENT_TEXT
def mode_meta() -> dict[str, Any]:
return {
"mode": MODE.value,
"provider": "qwen",
"purpose": "attachment_text",
"implemented": False,
}
def invoke_shell(*, payload: dict[str, Any]) -> dict[str, Any]:
return {
"ok": False,
"stub": True,
"mode": MODE.value,
"error": "attachment_text_shell_not_implemented",
"echo_keys": sorted(payload.keys()),
}
@@ -0,0 +1,35 @@
"""
DeepSeek B:文本字段抽取 mode 壳。
本文件职责:声明 mode 元数据与空 invoke_shell;不写字段合同激活规则。
禁止:在本文件改主账六态。
"""
from __future__ import annotations
from typing import Any
from agent.llm.modes import LlmMode
MODE = LlmMode.EXTRACT_FIELDS
def mode_meta() -> dict[str, Any]:
"""返回本 mode 的框架元数据(无 prompt 正文业务)。"""
return {
"mode": MODE.value,
"provider": "deepseek",
"purpose": "text_field_extract",
"implemented": False,
}
def invoke_shell(*, payload: dict[str, Any]) -> dict[str, Any]:
"""空壳:不调模型,返回未实现。"""
return {
"ok": False,
"stub": True,
"mode": MODE.value,
"error": "extract_fields_shell_not_implemented",
"echo_keys": sorted(payload.keys()),
}
@@ -0,0 +1,30 @@
"""
群报价抽取 mode 壳。
"""
from __future__ import annotations
from typing import Any
from agent.llm.modes import LlmMode
MODE = LlmMode.GROUP_QUOTE
def mode_meta() -> dict[str, Any]:
return {
"mode": MODE.value,
"provider": "deepseek",
"purpose": "group_quote",
"implemented": False,
}
def invoke_shell(*, payload: dict[str, Any]) -> dict[str, Any]:
return {
"ok": False,
"stub": True,
"mode": MODE.value,
"error": "group_quote_shell_not_implemented",
"echo_keys": sorted(payload.keys()),
}
+34
View File
@@ -0,0 +1,34 @@
"""
LLM mode 空壳注册表:按 mode 名取 invoke_shell。
本文件只做分发到各 mode_* 模块;ordered_actions 走 routing/DeepSeekA。
"""
from __future__ import annotations
from typing import Any, Callable
from agent.llm import mode_attachment_text, mode_extract_fields, mode_group_quote, mode_sheet_map, mode_vision
from agent.llm.modes import LlmMode
_SHELLS: dict[str, Callable[..., dict[str, Any]]] = {
LlmMode.EXTRACT_FIELDS.value: mode_extract_fields.invoke_shell,
LlmMode.ATTACHMENT_TEXT.value: mode_attachment_text.invoke_shell,
LlmMode.VISION.value: mode_vision.invoke_shell,
LlmMode.GROUP_QUOTE.value: mode_group_quote.invoke_shell,
LlmMode.SHEET_MAP.value: mode_sheet_map.invoke_shell,
}
def invoke_mode_shell(mode: str | LlmMode, *, payload: dict[str, Any]) -> dict[str, Any]:
"""调用对应 mode 空壳;未知 mode 返回错误结构。"""
key = mode.value if isinstance(mode, LlmMode) else str(mode)
fn = _SHELLS.get(key)
if fn is None:
return {"ok": False, "stub": True, "mode": key, "error": "unknown_or_unregistered_mode"}
return fn(payload=payload)
def list_mode_shells() -> list[str]:
"""已注册空壳 mode 名。"""
return sorted(_SHELLS.keys())
+30
View File
@@ -0,0 +1,30 @@
"""
报价工作表映射 mode 壳。
"""
from __future__ import annotations
from typing import Any
from agent.llm.modes import LlmMode
MODE = LlmMode.SHEET_MAP
def mode_meta() -> dict[str, Any]:
return {
"mode": MODE.value,
"provider": "qwen",
"purpose": "sheet_map",
"implemented": False,
}
def invoke_shell(*, payload: dict[str, Any]) -> dict[str, Any]:
return {
"ok": False,
"stub": True,
"mode": MODE.value,
"error": "sheet_map_shell_not_implemented",
"echo_keys": sorted(payload.keys()),
}
+30
View File
@@ -0,0 +1,30 @@
"""
千问视觉:读图 mode 壳。
"""
from __future__ import annotations
from typing import Any
from agent.llm.modes import LlmMode
MODE = LlmMode.VISION
def mode_meta() -> dict[str, Any]:
return {
"mode": MODE.value,
"provider": "qwen",
"purpose": "vision",
"implemented": False,
}
def invoke_shell(*, payload: dict[str, Any]) -> dict[str, Any]:
return {
"ok": False,
"stub": True,
"mode": MODE.value,
"error": "vision_shell_not_implemented",
"echo_keys": sorted(payload.keys()),
}
+20
View File
@@ -0,0 +1,20 @@
"""
LLM 模式名(对齐 prompt/06)。
本文件只声明枚举;具体 prompt/schema 后续按 mode 分文件,禁止堆进上帝模块。
"""
from __future__ import annotations
from enum import Enum
class LlmMode(str, Enum):
"""允许入队的 LLM 模式。"""
ORDERED_ACTIONS = "ordered_actions" # DeepSeek A
EXTRACT_FIELDS = "extract_fields" # DeepSeek B
ATTACHMENT_TEXT = "attachment_text" # 千问读附件
VISION = "vision" # 千问读图
GROUP_QUOTE = "group_quote" # 群报价抽取
SHEET_MAP = "sheet_map" # 报价工作表映射