184 lines
6.0 KiB
Python
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,
|
|
)
|