agent-Specialization/server/goal_flow.py

252 lines
9.1 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.

"""目标模式Goal Mode在 Web 任务主循环中的编排逻辑。
将启动、提示词注入、turn 结束后的审核/续命/停止判定集中在此,
尽量减少对 chat_flow_task_main.py 的侵入。
"""
from __future__ import annotations
from typing import Any, Callable, Dict, Optional
from modules.goal_state_manager import (
GoalStateManager,
REASON_IDLE_NO_TOOL,
REASON_MAX_TOKENS,
REASON_MAX_TURNS,
REASON_USER_CANCEL,
)
from modules.goal_review_agent import GoalReviewAgent
from modules.personalization_manager import load_personalization_config
from .chat_flow_task_support import inject_runtime_user_message
# ① 注入的目标模式提示词(开头一次 + 每次压缩后重注入)
GOAL_MODE_PROMPT = (
"【目标模式已开启】以上是本次的目标。在目标达成前,请持续推进,"
"不要把控制权交还给我、也不要停下来等我确认。请用实际的工具操作推进工作,"
"避免只停留在计划或反复询问。每当你认为工作告一段落停下时,会有独立的审核方"
"对照目标检查是否真正达成;若未达成,你会收到具体的下一步指示并继续。"
)
# 续命消息前缀
CONTINUE_PREFIX = "审核智能体对于你的工作结束给出了以下内容:"
def _load_token_total(web_terminal) -> int:
"""读取工作区累计 total_tokens。"""
try:
totals = web_terminal.context_manager._load_token_totals()
return int(totals.get("total_tokens") or 0)
except Exception:
return 0
def snapshot_token_baseline(web_terminal) -> Dict[str, int]:
try:
totals = web_terminal.context_manager._load_token_totals()
return {
"input_tokens": int(totals.get("input_tokens") or 0),
"output_tokens": int(totals.get("output_tokens") or 0),
"total_tokens": int(totals.get("total_tokens") or 0),
}
except Exception:
return {"input_tokens": 0, "output_tokens": 0, "total_tokens": 0}
def maybe_start_goal(
*,
web_terminal,
workspace,
conversation_id: Optional[str],
goal_text: str,
current_tool_calls: int,
) -> bool:
"""启动一个新目标。
本函数语义是“本次用户显式开启目标模式”而不是“续接已有目标”。
因此必须覆盖工作区级 goal_state.json 中的旧目标状态(包括 running/done/stopped、
review_history、token 基线、开始时间等),避免新对话复用上一轮目标。
"""
gsm = GoalStateManager(workspace.data_dir)
cfg = load_personalization_config(workspace.data_dir)
review_mode = cfg.get("goal_review_mode") or "readonly"
end_conditions = cfg.get("goal_end_conditions") or ["max_turns"]
max_turns = cfg.get("goal_max_turns") if "max_turns" in end_conditions else None
max_tokens = cfg.get("goal_max_tokens") if "max_tokens" in end_conditions else None
gsm.start(
goal=goal_text,
review_mode=review_mode,
max_turns=max_turns,
max_tokens=max_tokens,
conversation_id=conversation_id,
token_baseline=snapshot_token_baseline(web_terminal),
tool_call_baseline=current_tool_calls,
)
return True
def inject_goal_prompt(
*,
web_terminal,
messages,
sender: Optional[Callable[[str, Dict[str, Any]], None]],
conversation_id: Optional[str],
) -> None:
"""注入 ① 号目标模式提示词(开头 / 压缩后重注入共用)。"""
inject_runtime_user_message(
web_terminal=web_terminal,
messages=messages,
text=GOAL_MODE_PROMPT,
source="goal_prompt",
sender=sender,
conversation_id=conversation_id,
inline=False,
persist=True,
)
def goal_is_active(workspace) -> bool:
return GoalStateManager(workspace.data_dir).is_active()
def _emit_snapshot(sender, event: str, gsm: GoalStateManager, *, total_tokens: int, tool_calls: int, extra: Optional[Dict] = None):
if not callable(sender):
return
snap = gsm.progress_snapshot(current_total_tokens=total_tokens, current_tool_calls=tool_calls)
if extra:
snap.update(extra)
try:
sender(event, snap)
except Exception:
pass
def emit_goal_progress(
*,
web_terminal,
workspace,
sender: Optional[Callable[[str, Dict[str, Any]], None]],
total_tool_calls: int = 0,
extra: Optional[Dict[str, Any]] = None,
) -> None:
"""向前端广播当前目标模式进度,用于刷新后恢复与进度弹窗动态数字。"""
gsm = GoalStateManager(workspace.data_dir)
if not gsm.state:
return
total_tokens = _load_token_total(web_terminal)
_emit_snapshot(
sender,
"goal_progress",
gsm,
total_tokens=total_tokens,
tool_calls=total_tool_calls,
extra=extra,
)
async def handle_goal_after_turn(
*,
web_terminal,
workspace,
messages,
sender: Optional[Callable[[str, Dict[str, Any]], None]],
conversation_id: Optional[str],
assistant_content: str,
made_tool_call: bool,
total_tool_calls: int,
) -> Dict[str, Any]:
"""主模型某轮结束(无 tool_calls后的目标处理。
返回 {'action': 'inactive'|'stop'|'done'|'continue', ...}
- inactive当前无活动目标调用方照常结束本次任务。
- stop目标因空转/边界停止,调用方照常结束(事件已发)。
- done目标达成调用方照常结束事件已发
- continue已注入续命消息调用方应 continue 回主循环开下一轮。
"""
gsm = GoalStateManager(workspace.data_dir)
if not gsm.is_active():
return {"action": "inactive"}
total_tokens = _load_token_total(web_terminal)
_emit_snapshot(sender, "goal_progress", gsm, total_tokens=total_tokens, tool_calls=total_tool_calls)
# 1) 空转保护:本目标轮主模型一次工具都没调过 → 直接停止
if not made_tool_call:
gsm.mark_stopped(REASON_IDLE_NO_TOOL)
_emit_snapshot(sender, "goal_stopped", gsm, total_tokens=total_tokens, tool_calls=total_tool_calls)
return {"action": "stop", "reason": REASON_IDLE_NO_TOOL}
# 2) 计入一轮,检查结束边界
gsm.increment_turn()
_emit_snapshot(sender, "goal_progress", gsm, total_tokens=total_tokens, tool_calls=total_tool_calls)
if gsm.reached_max_turns():
gsm.mark_stopped(REASON_MAX_TURNS)
_emit_snapshot(sender, "goal_stopped", gsm, total_tokens=total_tokens, tool_calls=total_tool_calls)
return {"action": "stop", "reason": REASON_MAX_TURNS}
if gsm.reached_max_tokens(total_tokens):
gsm.mark_stopped(REASON_MAX_TOKENS)
_emit_snapshot(sender, "goal_stopped", gsm, total_tokens=total_tokens, tool_calls=total_tool_calls)
return {"action": "stop", "reason": REASON_MAX_TOKENS}
# 3) 调用审核智能体
payload_text = gsm.build_review_payload_text(assistant_content)
if callable(sender):
try:
sender("goal_review_progress", {"progress": {"stage": "start", "message": "开始审核"}})
except Exception:
pass
try:
agent = GoalReviewAgent(web_terminal=web_terminal)
def _progress(progress: Dict[str, Any]) -> None:
if callable(sender):
try:
sender("goal_review_progress", {"progress": progress})
except Exception:
pass
result = await agent.review(
payload_text=payload_text,
review_mode=gsm.get_review_mode(),
progress_cb=_progress,
)
except Exception as exc:
# 审核异常 → 保守续命,但仍受边界约束
result = {"status": "continue", "message": f"目标审核出现异常({exc}),请继续核对目标并推进未完成的部分。"}
status = (result or {}).get("status")
message = (result or {}).get("message") or ""
if status == "done":
gsm.mark_done(message)
_emit_snapshot(
sender, "goal_completed", gsm,
total_tokens=total_tokens, tool_calls=total_tool_calls,
extra={"summary": message},
)
return {"action": "done"}
# continue记录审核历史 + 注入续命消息
gsm.append_review(assistant_content, message)
inject_runtime_user_message(
web_terminal=web_terminal,
messages=messages,
text=f"{CONTINUE_PREFIX}{message}",
source="goal_review",
sender=sender,
conversation_id=conversation_id,
inline=False,
persist=True,
)
return {"action": "continue", "message": message}
def stop_goal_user_cancel(*, web_terminal, workspace, sender, total_tool_calls: int = 0) -> bool:
"""用户停止任务时,若有活动目标则一并停止。返回是否有活动目标被停止。"""
gsm = GoalStateManager(workspace.data_dir)
if not gsm.is_active():
return False
total_tokens = _load_token_total(web_terminal)
gsm.mark_stopped(REASON_USER_CANCEL)
_emit_snapshot(sender, "goal_stopped", gsm, total_tokens=total_tokens, tool_calls=total_tool_calls)
return True