feat(agent): align prompts and runtime with workflow@20261020
- archive workflow/20261020 export (diff vs 20261016: collection prompt
only - state-decides-body rule, route when info complete)
- prompts: collection.md updated; 6 photo/confirm prompts archived with
source nodeIds; render_prompt replaces {{$vars}} (Asia/Shanghai clock,
person fields, explicit 'not provided' for missing prefills)
- llm: message history passed as user/assistant roles (chatNode history=50);
optional LLM_TEMPERATURE; FakeLLMService supports history-aware responders
- collect: context via system supplement, raw user input as user message
(Text Editor#2 parity), shared validated_reply (one call, one repair)
- welcome: Assigned Reply#3 original text, stored in dialogue history
- transitions: 2004 plate-confirm entries (2003->2004/2005, 2004->2005)
- verified: prompt files byte-identical to workflow JSON (parametrized test)
This commit is contained in:
+86
-10
@@ -29,8 +29,11 @@ class LLMAPIError(LLMError):
|
||||
class LLMService(Protocol):
|
||||
"""节点依赖的最小 LLM 接口。"""
|
||||
|
||||
async def complete(self, system_prompt: str, user_message: str) -> str:
|
||||
"""返回模型文本输出(含 <state> 前缀,由调用方解析校验)。"""
|
||||
async def complete(
|
||||
self, system_prompt: str, user_message: str, *,
|
||||
history: list[dict[str, str]] | None = None,
|
||||
) -> str:
|
||||
"""返回模型文本;history 保留 user/assistant 角色,与工作流对齐。"""
|
||||
...
|
||||
|
||||
|
||||
@@ -44,15 +47,29 @@ class FakeLLMService:
|
||||
|
||||
def __init__(self, responder: Responder):
|
||||
self._responder = responder
|
||||
self.calls: list[dict[str, str]] = []
|
||||
self.calls: list[dict] = []
|
||||
# 兼容两参 responder;三参 responder 可读取对话历史。
|
||||
import inspect
|
||||
|
||||
async def complete(self, system_prompt: str, user_message: str) -> str:
|
||||
self._accepts_history = (
|
||||
len(inspect.signature(responder).parameters) >= 3
|
||||
)
|
||||
|
||||
async def complete(
|
||||
self, system_prompt: str, user_message: str, *,
|
||||
history: list[dict[str, str]] | None = None,
|
||||
) -> str:
|
||||
self.calls.append(
|
||||
{"system_prompt": system_prompt, "user_message": user_message}
|
||||
{"system_prompt": system_prompt, "user_message": user_message,
|
||||
"history": [dict(message) for message in (history or [])]}
|
||||
)
|
||||
import asyncio
|
||||
|
||||
result = self._responder(system_prompt, user_message)
|
||||
result = (
|
||||
self._responder(system_prompt, user_message, history or [])
|
||||
if self._accepts_history
|
||||
else self._responder(system_prompt, user_message)
|
||||
)
|
||||
if asyncio.iscoroutine(result):
|
||||
result = await result
|
||||
return result
|
||||
@@ -74,12 +91,14 @@ class OpenAILLMService:
|
||||
base_url: str = "",
|
||||
timeout_seconds: float = 30.0,
|
||||
max_retries: int = 2,
|
||||
temperature: float | None = None,
|
||||
):
|
||||
self._model = model
|
||||
self._api_key = api_key
|
||||
self._base_url = base_url
|
||||
self._timeout_seconds = timeout_seconds
|
||||
self._max_retries = max_retries
|
||||
self._temperature = temperature
|
||||
self._client = None
|
||||
|
||||
def _get_client(self):
|
||||
@@ -94,7 +113,10 @@ class OpenAILLMService:
|
||||
self._client = AsyncOpenAI(**kwargs)
|
||||
return self._client
|
||||
|
||||
async def complete(self, system_prompt: str, user_message: str) -> str:
|
||||
async def complete(
|
||||
self, system_prompt: str, user_message: str, *,
|
||||
history: list[dict[str, str]] | None = None,
|
||||
) -> str:
|
||||
import asyncio
|
||||
|
||||
from openai import APIConnectionError, APITimeoutError, RateLimitError
|
||||
@@ -102,15 +124,17 @@ class OpenAILLMService:
|
||||
client = self._get_client()
|
||||
messages = [
|
||||
{"role": "system", "content": system_prompt},
|
||||
*[dict(message) for message in (history or [])],
|
||||
{"role": "user", "content": user_message},
|
||||
]
|
||||
kwargs: dict = {"model": self._model, "messages": messages}
|
||||
if self._temperature is not None:
|
||||
kwargs["temperature"] = self._temperature
|
||||
retries = 0
|
||||
while True:
|
||||
try:
|
||||
response = await asyncio.wait_for(
|
||||
client.chat.completions.create(
|
||||
model=self._model, messages=messages
|
||||
),
|
||||
client.chat.completions.create(**kwargs),
|
||||
timeout=self._timeout_seconds,
|
||||
)
|
||||
except (APITimeoutError, asyncio.TimeoutError) as exc:
|
||||
@@ -145,6 +169,57 @@ def _default_fake_responder(system_prompt: str, user_message: str) -> str:
|
||||
return f"<state>1002</state>已收到:{user_message}"
|
||||
|
||||
|
||||
def workflow_fake_responder(system_prompt: str, user_message: str, history=None) -> str:
|
||||
"""模拟工作流提示词行为的假 LLM(离线测试/冒烟用)。
|
||||
|
||||
从注入的运行时上下文(system 末尾的 context_prompt)读取当前
|
||||
阶段与 clientMode;按工作流提示词语义响应客户端事件:
|
||||
会话标记 1001→1002、【拍摄完成】沿链推进、失败一次→0005、
|
||||
第二次无回复→0004;普通输入保持当前阶段。正文字面回显输入。
|
||||
"""
|
||||
import re
|
||||
|
||||
from .events import ClientEvent, parse_client_event
|
||||
|
||||
stage_match = re.search(r'"stage_code":\s*"(\d{4})"', system_prompt)
|
||||
current = stage_match.group(1) if stage_match else "1001"
|
||||
mode = "browser_addon" if '"client_mode": "browser_addon"' in system_prompt else "direct"
|
||||
|
||||
event = parse_client_event(user_message)
|
||||
if event in (ClientEvent.SESSION_START, ClientEvent.CONTINUE) and current == "1001":
|
||||
return f"<state>1002</state>已收到:{user_message}"
|
||||
if event is ClientEvent.PHOTO_DONE:
|
||||
chain = {
|
||||
"2000": "2001", "2001": "2002", "2002": "2003", "2003": "2005",
|
||||
"2010": "2011", "2011": "2012", "2012": "2013",
|
||||
"2013": "2014", "2014": "2015",
|
||||
}
|
||||
target = chain.get(current)
|
||||
if target is None:
|
||||
target = "0000" if mode == "browser_addon" else ("3001" if current == "2005" else "3002")
|
||||
return f"<state>{target}</state>已收到:{user_message}"
|
||||
if event is ClientEvent.PHOTO_FAILED_3X:
|
||||
return "<state>0005</state>多次拍摄识别仍未成功,正在为您转接人工处理。"
|
||||
if event is ClientEvent.NO_REPLY:
|
||||
trailing = 0
|
||||
for message in reversed(history or []):
|
||||
if message.get("role") != "user":
|
||||
continue
|
||||
if parse_client_event(str(message.get("content", ""))) is ClientEvent.NO_REPLY:
|
||||
trailing += 1
|
||||
else:
|
||||
break
|
||||
if trailing >= 1:
|
||||
return "<state>0004</state>长时间未收到回应,为您转接人工。"
|
||||
return f"<state>{current}</state>请问您还在吗?"
|
||||
return f"<state>{current}</state>已收到:{user_message}"
|
||||
|
||||
|
||||
def workflow_fake_llm() -> "FakeLLMService":
|
||||
"""离线假 LLM:行为对齐工作流提示词,供冒烟与无模型测试。"""
|
||||
return FakeLLMService(workflow_fake_responder)
|
||||
|
||||
|
||||
def build_llm_service(provider: str | None = None) -> LLMService:
|
||||
"""按配置构造 LLM 服务;provider 缺省读 Config.AGENT_LLM_PROVIDER。"""
|
||||
from ..core.config import Config
|
||||
@@ -157,6 +232,7 @@ def build_llm_service(provider: str | None = None) -> LLMService:
|
||||
base_url=Config.LLM_BASE_URL,
|
||||
timeout_seconds=Config.LLM_TIMEOUT_SECONDS,
|
||||
max_retries=Config.LLM_MAX_RETRIES,
|
||||
temperature=Config.LLM_TEMPERATURE,
|
||||
)
|
||||
if provider == "fake":
|
||||
return FakeLLMService(_default_fake_responder)
|
||||
|
||||
Reference in New Issue
Block a user