5a581c78a2
Build / build (push) Has been cancelled
Docker / build (push) Has been cancelled
Lint / ruff (push) Has been cancelled
Test / pytest (ubuntu-latest, 3.11) (push) Has been cancelled
Test / pytest (ubuntu-latest, 3.12) (push) Has been cancelled
Test / pytest (windows-latest, 3.11) (push) Has been cancelled
Test / pytest (windows-latest, 3.12) (push) Has been cancelled
Introduce provider, model, and invocation contracts with encrypted configuration persistence. Add web runtime fencing, route fallback, recovery middleware, workspace scoping, and comprehensive tests.
171 lines
5.4 KiB
Python
171 lines
5.4 KiB
Python
"""Web-specific agent construction owned by EvoScientist.
|
|
|
|
Gateway supplies a workspace/checkpointer host context only. Model routes,
|
|
provider clients, and the profile semantics remain entirely inside the Evo
|
|
runtime.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import copy
|
|
import hashlib
|
|
import json
|
|
import os
|
|
from collections.abc import Awaitable, Callable
|
|
from typing import Any
|
|
|
|
from langchain.agents.middleware.types import AgentMiddleware, ToolCallRequest
|
|
from langchain_core.messages import ToolMessage
|
|
from langgraph.types import Command
|
|
|
|
from .config.settings import load_config
|
|
from .llm.contracts import (
|
|
AgentExecutionProfile,
|
|
AgentModelSet,
|
|
EvoRuntimeError,
|
|
WebHostContext,
|
|
)
|
|
|
|
|
|
def web_tool_registry_manifest() -> tuple[tuple[dict[str, Any], ...], str]:
|
|
"""Return the current bounded Web profile and main-agent MCP tools."""
|
|
|
|
names = (
|
|
"think_tool",
|
|
"execute",
|
|
"read_file",
|
|
"write_file",
|
|
"edit_file",
|
|
"ls",
|
|
"glob",
|
|
"grep",
|
|
"write_todos",
|
|
"web_search",
|
|
"parse_documents",
|
|
"use_skill",
|
|
)
|
|
if os.environ.get("TAVILY_API_KEY"):
|
|
names = (*names, "tavily_search")
|
|
schema = {
|
|
"type": "object",
|
|
"additionalProperties": True,
|
|
"maxProperties": 32,
|
|
}
|
|
manifest: tuple[dict[str, Any], ...] = tuple(
|
|
{
|
|
"name": name,
|
|
"description": "EvoScientist Web runtime tool",
|
|
"schema": schema,
|
|
}
|
|
for name in names
|
|
)
|
|
from .EvoScientist import _load_mcp_config_once, _load_mcp_tools_cached
|
|
|
|
mcp_tools = _load_mcp_tools_cached().get("main", [])
|
|
dynamic = []
|
|
for tool in mcp_tools:
|
|
args_schema = getattr(tool, "args_schema", None)
|
|
if hasattr(args_schema, "model_json_schema"):
|
|
args_schema = args_schema.model_json_schema()
|
|
dynamic.append(
|
|
{
|
|
"name": str(getattr(tool, "name", type(tool).__name__)),
|
|
"description": str(getattr(tool, "description", "")),
|
|
"schema": args_schema or {},
|
|
}
|
|
)
|
|
static_names = {item["name"] for item in manifest}
|
|
dynamic_names = [item["name"] for item in dynamic]
|
|
if static_names & set(dynamic_names) or len(dynamic_names) != len(
|
|
set(dynamic_names)
|
|
):
|
|
raise EvoRuntimeError("TOOL_REGISTRY_CONFLICT")
|
|
manifest = tuple(sorted((*manifest, *dynamic), key=lambda item: item["name"]))
|
|
mcp_config_signature, _mcp_config = _load_mcp_config_once()
|
|
mcp_config_revision = hashlib.sha256(
|
|
mcp_config_signature.encode("utf-8")
|
|
).hexdigest()
|
|
encoded = json.dumps(
|
|
{
|
|
"tools": manifest,
|
|
"mcp_config_revision": mcp_config_revision,
|
|
"builtin_revision": "evoscientist-web-tools-v1",
|
|
},
|
|
sort_keys=True,
|
|
separators=(",", ":"),
|
|
).encode()
|
|
return manifest, f"sha256:{hashlib.sha256(encoded).hexdigest()}"
|
|
|
|
|
|
class _ToolRegistryFenceMiddleware(AgentMiddleware):
|
|
name = "web_tool_registry_fence"
|
|
|
|
def __init__(self, expected_revision: str) -> None:
|
|
super().__init__()
|
|
self.expected_revision = expected_revision
|
|
|
|
def _require_current(self) -> None:
|
|
_manifest, revision = web_tool_registry_manifest()
|
|
if revision != self.expected_revision:
|
|
raise EvoRuntimeError("TOOL_REGISTRY_STALE")
|
|
|
|
def wrap_tool_call(
|
|
self,
|
|
request: ToolCallRequest,
|
|
handler: Callable[[ToolCallRequest], ToolMessage | Command[Any]],
|
|
) -> ToolMessage | Command[Any]:
|
|
self._require_current()
|
|
return handler(request)
|
|
|
|
async def awrap_tool_call(
|
|
self,
|
|
request: ToolCallRequest,
|
|
handler: Callable[
|
|
[ToolCallRequest], Awaitable[ToolMessage | Command[Any]]
|
|
],
|
|
) -> ToolMessage | Command[Any]:
|
|
self._require_current()
|
|
return await handler(request)
|
|
|
|
|
|
def create_web_agent(
|
|
*, snapshot: Any, host: WebHostContext, model_set: AgentModelSet
|
|
) -> Any:
|
|
"""Create a `web_v3` agent without importing Gateway types."""
|
|
|
|
from .EvoScientist import create_cli_agent
|
|
from .middleware.evo_route_fallback import EvoRouteFallbackMiddleware
|
|
|
|
config = copy.copy(load_config())
|
|
config.auto_approve = True
|
|
config.auto_mode = True
|
|
config.enable_ask_user = False
|
|
config.enable_async_subagents = False
|
|
config.enable_scheduler = False
|
|
config.memory_workers_enabled = False
|
|
route_middleware = EvoRouteFallbackMiddleware(
|
|
model_set.main_fallbacks,
|
|
route_health=model_set.route_health,
|
|
capacity=model_set.capacity,
|
|
)
|
|
tool_registry_fence = _ToolRegistryFenceMiddleware(
|
|
host.tool_registry_revision
|
|
)
|
|
return create_cli_agent(
|
|
workspace_dir=host.workspace_dir,
|
|
memory_dir=host.memory_dir,
|
|
workspace_backend=host.workspace_backend,
|
|
checkpointer=host.checkpointer,
|
|
config=config,
|
|
chat_model=model_set.main_agent,
|
|
on_mcp_progress=host.on_mcp_progress,
|
|
tool_selector_threshold=host.tool_selector_threshold,
|
|
memory_max_inline_profile_chars=host.memory_max_inline_profile_chars,
|
|
enable_subagents=False,
|
|
enable_background_execution=False,
|
|
main_agent_outer_middlewares=[tool_registry_fence],
|
|
main_agent_route_middleware=route_middleware,
|
|
execution_profile=AgentExecutionProfile.web_v3(),
|
|
agent_model_set=model_set,
|
|
)
|