Files

184 lines
6.0 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,
}
tool_descriptions = {
"tavily_search": (
"Search the live public web through the controlled Tavily service. "
"Use this for current facts, source discovery, and URL verification. "
"Do not use execute, curl, httpx, or raw sandbox networking instead."
),
"web_search": (
"Search the live public web through the configured MCP search provider. "
"Use this for current facts and source verification, not local memory recall."
),
}
manifest: tuple[dict[str, Any], ...] = tuple(
{
"name": name,
"description": tool_descriptions.get(
name, "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,
config: Any = None,
) -> 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(config if config is not None else load_config())
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,
)