feat(engine): 添加运行时上下文支持并重构工具迭代限制
添加 RuntimeContext 类用于捕获模型运行时的日期时间信息, 包括UTC时间、本地时间和时区信息,并在系统提示中显示这些信息。 同时增加最大上下文消息数和工具迭代次数的配置选项, 将验证服务从引擎加载器中移除,并更新相关的数据结构和接口。 BREAKING CHANGE: 移除了验证服务,相关字段被替换为证据状态和接受状态。 - 添加 RuntimeContext 类和相关渲染方法 - 增加 max_context_messages 和 max_tool_iterations 配置 - 移除 ValidationService 相关代码 - 更新消息记录中的验证状态字段 - 添加原始工具调用检测和回退处理
This commit is contained in:
@ -44,6 +44,8 @@ from .files import (
|
||||
workspace_file_path,
|
||||
)
|
||||
from .schemas import (
|
||||
WebChatAcceptanceRequest,
|
||||
WebChatAcceptanceResponse,
|
||||
WebChatFeedbackRequest,
|
||||
WebChatFeedbackResponse,
|
||||
WebChatRequest,
|
||||
@ -155,6 +157,13 @@ except ModuleNotFoundError: # pragma: no cover - fallback for skeleton-only env
|
||||
return decorator
|
||||
|
||||
|
||||
RAW_TOOL_CALL_DISPLAY_FALLBACK = (
|
||||
"The run reached the configured tool-call limit before producing a reliable final answer. "
|
||||
"The model attempted another tool call instead of answering, so the raw tool call was suppressed. "
|
||||
"Please request a revision to continue the task."
|
||||
)
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def _app_lifespan(
|
||||
app: FastAPI,
|
||||
@ -365,6 +374,7 @@ def create_app(
|
||||
"workspace_exists": loaded.workspace.exists(),
|
||||
"model": config.default_model or agent_service.profile.default_model,
|
||||
"max_tokens": agent_service.profile.max_tokens,
|
||||
"max_context_messages": agent_service.profile.max_context_messages,
|
||||
"temperature": agent_service.profile.temperature,
|
||||
"max_tool_iterations": agent_service.profile.max_tool_iterations,
|
||||
"providers": providers_status,
|
||||
@ -1719,7 +1729,8 @@ def create_app(
|
||||
usage=result.usage,
|
||||
task_id=result.task_id,
|
||||
task_status=result.task_status,
|
||||
validation_result=result.validation_result,
|
||||
evidence_status="recorded" if result.task_id else None,
|
||||
validation_result=None,
|
||||
)
|
||||
|
||||
fallback_target = _model_dump(payload.fallback_target)
|
||||
@ -1769,7 +1780,8 @@ def create_app(
|
||||
usage=result.usage,
|
||||
task_id=result.task_id,
|
||||
task_status=result.task_status,
|
||||
validation_result=result.validation_result,
|
||||
evidence_status="recorded" if result.task_id else None,
|
||||
validation_result=None,
|
||||
)
|
||||
|
||||
@app.websocket("/ws/{session_id:path}")
|
||||
@ -1882,6 +1894,30 @@ def create_app(
|
||||
}
|
||||
)
|
||||
|
||||
@app.post(
|
||||
"/api/chat/acceptance",
|
||||
response_model=WebChatAcceptanceResponse,
|
||||
responses={
|
||||
400: {"model": WebErrorResponse},
|
||||
404: {"model": WebErrorResponse},
|
||||
},
|
||||
)
|
||||
async def chat_acceptance(request: Request, payload: WebChatAcceptanceRequest) -> WebChatAcceptanceResponse:
|
||||
agent_service = get_agent_service(request)
|
||||
try:
|
||||
result = await agent_service.submit_acceptance(
|
||||
session_id=payload.session_id,
|
||||
run_id=payload.run_id,
|
||||
acceptance_type=payload.acceptance_type,
|
||||
comment=payload.comment,
|
||||
)
|
||||
except ValueError as exc:
|
||||
detail = str(exc)
|
||||
status_code = 404 if "No internal task" in detail else 400
|
||||
raise HTTPException(status_code=status_code, detail=detail) from exc
|
||||
|
||||
return WebChatAcceptanceResponse(**result)
|
||||
|
||||
@app.post(
|
||||
"/api/chat/feedback",
|
||||
response_model=WebChatFeedbackResponse,
|
||||
@ -1893,10 +1929,10 @@ def create_app(
|
||||
async def chat_feedback(request: Request, payload: WebChatFeedbackRequest) -> WebChatFeedbackResponse:
|
||||
agent_service = get_agent_service(request)
|
||||
try:
|
||||
result = await agent_service.submit_feedback(
|
||||
result = await agent_service.submit_acceptance(
|
||||
session_id=payload.session_id,
|
||||
run_id=payload.run_id,
|
||||
feedback_type=payload.feedback_type,
|
||||
acceptance_type=payload.feedback_type,
|
||||
comment=payload.comment,
|
||||
)
|
||||
except ValueError as exc:
|
||||
@ -1915,15 +1951,21 @@ def _session_detail(session_manager: Any, session_id: str, session: dict[str, An
|
||||
role = event.get("role")
|
||||
if role not in {"user", "assistant"}:
|
||||
continue
|
||||
content = event.get("content") or ""
|
||||
comparable_content = str(content).replace("\u200b", "").replace("\u200c", "").replace("\u200d", "").replace("\ufeff", "")
|
||||
if role == "assistant" and not comparable_content.strip():
|
||||
continue
|
||||
content = _sanitize_user_visible_assistant_content(role=role, content=content)
|
||||
messages.append(
|
||||
{
|
||||
"role": role,
|
||||
"content": event.get("content") or "",
|
||||
"content": content,
|
||||
"timestamp": _iso_from_timestamp(event.get("timestamp")),
|
||||
"run_id": event.get("run_id"),
|
||||
"task_id": event.get("task_id"),
|
||||
"task_status": event.get("task_status"),
|
||||
"validation_status": event.get("validation_status"),
|
||||
"evidence_status": event.get("evidence_status"),
|
||||
"acceptance_state": event.get("acceptance_state"),
|
||||
"feedback_state": event.get("feedback_state"),
|
||||
"feedback_error": event.get("feedback_error"),
|
||||
"message_type": event.get("message_type"),
|
||||
@ -2142,6 +2184,7 @@ def _task_run_views(task: Any, events: list[Any], session_manager: Any, run_memo
|
||||
content = (record.content or "").strip()
|
||||
if not content:
|
||||
continue
|
||||
content = _sanitize_user_visible_assistant_content(role=record.role, content=content)
|
||||
messages.append(
|
||||
{
|
||||
"role": record.role,
|
||||
@ -2150,7 +2193,6 @@ def _task_run_views(task: Any, events: list[Any], session_manager: Any, run_memo
|
||||
"tool_name": record.tool_name,
|
||||
}
|
||||
)
|
||||
validation = run_record.validation_result if run_record is not None else None
|
||||
views.append(
|
||||
{
|
||||
"run_id": run_id,
|
||||
@ -2163,7 +2205,8 @@ def _task_run_views(task: Any, events: list[Any], session_manager: Any, run_memo
|
||||
"attempt_index": run_record.attempt_index if run_record is not None else None,
|
||||
"task_text": run_record.task_text if run_record is not None else "",
|
||||
"messages": messages,
|
||||
"validation_result": validation,
|
||||
"evidence_status": "recorded",
|
||||
"validation_result": None,
|
||||
}
|
||||
)
|
||||
return views
|
||||
@ -2428,12 +2471,6 @@ def _model_dump(value: Any) -> dict[str, Any] | None:
|
||||
return dict(value)
|
||||
|
||||
|
||||
def _validation_status(validation_result: dict[str, Any] | None) -> str:
|
||||
if validation_result is None:
|
||||
return "unknown"
|
||||
return "passed" if validation_result.get("accepted") is True else "failed"
|
||||
|
||||
|
||||
def _websocket_input_metadata(payload: dict[str, Any]) -> dict[str, Any]:
|
||||
metadata = payload.get("metadata") if isinstance(payload.get("metadata"), dict) else {}
|
||||
result: dict[str, Any] = dict(metadata)
|
||||
@ -2467,13 +2504,15 @@ def _int_or_none(value: Any) -> int | None:
|
||||
|
||||
|
||||
def _websocket_message_payload(result: Any, *, input_payload: dict[str, Any]) -> dict[str, Any]:
|
||||
validation_result = getattr(result, "validation_result", None)
|
||||
task_id = getattr(result, "task_id", None)
|
||||
task_status = getattr(result, "task_status", None)
|
||||
return {
|
||||
"type": "message",
|
||||
"role": "assistant",
|
||||
"content": getattr(result, "output_text", "") or "",
|
||||
"content": _sanitize_user_visible_assistant_content(
|
||||
role="assistant",
|
||||
content=getattr(result, "output_text", "") or "",
|
||||
),
|
||||
"session_id": getattr(result, "session_id", None),
|
||||
"run_id": getattr(result, "run_id", None),
|
||||
"finish_reason": getattr(result, "finish_reason", None),
|
||||
@ -2483,17 +2522,39 @@ def _websocket_message_payload(result: Any, *, input_payload: dict[str, Any]) ->
|
||||
"usage": dict(getattr(result, "usage", {}) or {}),
|
||||
"task_id": task_id,
|
||||
"task_status": task_status,
|
||||
"validation_result": validation_result,
|
||||
"validation_status": _validation_status(validation_result),
|
||||
"evidence_status": "recorded" if task_id else None,
|
||||
"validation_result": None,
|
||||
"metadata": {
|
||||
"task_id": task_id,
|
||||
"task_status": task_status,
|
||||
"validation_result": validation_result,
|
||||
"evidence_status": "recorded" if task_id else None,
|
||||
"input_metadata": _websocket_input_metadata(input_payload),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _sanitize_user_visible_assistant_content(*, role: str, content: str) -> str:
|
||||
if role != "assistant":
|
||||
return content
|
||||
if _looks_like_raw_tool_call(content):
|
||||
return RAW_TOOL_CALL_DISPLAY_FALLBACK
|
||||
return content
|
||||
|
||||
|
||||
def _looks_like_raw_tool_call(content: str | None) -> bool:
|
||||
if not content:
|
||||
return False
|
||||
stripped = content.strip()
|
||||
lowered = stripped.lower()
|
||||
return (
|
||||
lowered.startswith("<tool_call")
|
||||
and lowered.endswith("</tool_call>")
|
||||
) or (
|
||||
lowered.startswith("<function=")
|
||||
and lowered.endswith("</function>")
|
||||
)
|
||||
|
||||
|
||||
def _provider_enabled(provider_name: str, provider_cfg: Any) -> bool:
|
||||
if provider_cfg is None or provider_name == "custom":
|
||||
return False
|
||||
@ -2980,6 +3041,7 @@ def _write_config_json(path: Path, data: dict[str, Any]) -> None:
|
||||
def _reload_agent_config(agent_service: AgentService, config_path: Path) -> None:
|
||||
config = load_config(config_path=config_path)
|
||||
agent_service.loader.config = config
|
||||
agent_service._apply_configured_profile_defaults() # noqa: SLF001
|
||||
loop = getattr(agent_service, "_loop", None)
|
||||
loaded = getattr(loop, "loaded", None) if loop is not None else None
|
||||
if loaded is not None:
|
||||
|
||||
Reference in New Issue
Block a user