feat: agent tool definitions (browser, flows, doc) and registry
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,27 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Callable
|
||||||
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
|
from auto_reverse.tools.browser_tools import browser_tools
|
||||||
|
from auto_reverse.tools.doc_tools import doc_tools
|
||||||
|
from auto_reverse.tools.flows_tools import flows_tools
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from auto_reverse.doc.engine import DocEngine
|
||||||
|
from auto_reverse.store import FlowStore
|
||||||
|
|
||||||
|
Handler = Callable[[dict[str, Any]], dict[str, Any]]
|
||||||
|
Registry = dict[str, tuple[dict[str, Any], Handler]]
|
||||||
|
|
||||||
|
|
||||||
|
def build_registry(browser: Any, store: FlowStore, engine: DocEngine) -> Registry:
|
||||||
|
registry: Registry = {}
|
||||||
|
registry.update(browser_tools(browser))
|
||||||
|
registry.update(flows_tools(store))
|
||||||
|
registry.update(doc_tools(store, engine))
|
||||||
|
return registry
|
||||||
|
|
||||||
|
|
||||||
|
def tool_schemas(registry: Registry) -> list[dict[str, Any]]:
|
||||||
|
return [schema for schema, _ in registry.values()]
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Callable
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
Handler = Callable[[dict[str, Any]], dict[str, Any]]
|
||||||
|
|
||||||
|
|
||||||
|
def browser_tools(browser: Any) -> dict[str, tuple[dict[str, Any], Handler]]:
|
||||||
|
return {
|
||||||
|
"browser_navigate": (
|
||||||
|
{
|
||||||
|
"name": "browser_navigate",
|
||||||
|
"description": "Navigate the browser to a URL and return a page snapshot.",
|
||||||
|
"input_schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {"url": {"type": "string"}},
|
||||||
|
"required": ["url"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
lambda inp: browser.navigate(inp["url"]),
|
||||||
|
),
|
||||||
|
"browser_click": (
|
||||||
|
{
|
||||||
|
"name": "browser_click",
|
||||||
|
"description": "Click an element by CSS selector; returns a new snapshot.",
|
||||||
|
"input_schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {"selector": {"type": "string"}},
|
||||||
|
"required": ["selector"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
lambda inp: browser.click(inp["selector"]),
|
||||||
|
),
|
||||||
|
"browser_type": (
|
||||||
|
{
|
||||||
|
"name": "browser_type",
|
||||||
|
"description": "Fill a form field (CSS selector) with text.",
|
||||||
|
"input_schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"selector": {"type": "string"},
|
||||||
|
"text": {"type": "string"},
|
||||||
|
},
|
||||||
|
"required": ["selector", "text"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
lambda inp: browser.type_text(inp["selector"], inp["text"]),
|
||||||
|
),
|
||||||
|
"browser_snapshot": (
|
||||||
|
{
|
||||||
|
"name": "browser_snapshot",
|
||||||
|
"description": "Return the current page snapshot without acting.",
|
||||||
|
"input_schema": {"type": "object", "properties": {}},
|
||||||
|
},
|
||||||
|
lambda inp: browser.snapshot(),
|
||||||
|
),
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Callable
|
||||||
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from auto_reverse.doc.engine import DocEngine
|
||||||
|
from auto_reverse.store import FlowStore
|
||||||
|
|
||||||
|
Handler = Callable[[dict[str, Any]], dict[str, Any]]
|
||||||
|
|
||||||
|
|
||||||
|
def doc_tools(store: FlowStore, engine: DocEngine) -> dict[str, tuple[dict[str, Any], Handler]]:
|
||||||
|
def document(inp: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
path = inp["path_template"]
|
||||||
|
for rec in store.endpoints():
|
||||||
|
if rec.signature.path_template == path:
|
||||||
|
if inp.get("summary"):
|
||||||
|
rec.summary = inp["summary"]
|
||||||
|
if inp.get("description"):
|
||||||
|
rec.description = inp["description"]
|
||||||
|
if inp.get("tag"):
|
||||||
|
rec.tag = inp["tag"]
|
||||||
|
engine.document(rec.signature)
|
||||||
|
return {"documented": path}
|
||||||
|
return {"error": f"no endpoint matching {path}"}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"doc_document": (
|
||||||
|
{
|
||||||
|
"name": "doc_document",
|
||||||
|
"description": (
|
||||||
|
"Enrich and (re)write docs for an endpoint by path template, "
|
||||||
|
"optionally setting a human summary/description/tag."
|
||||||
|
),
|
||||||
|
"input_schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"path_template": {"type": "string"},
|
||||||
|
"summary": {"type": "string"},
|
||||||
|
"description": {"type": "string"},
|
||||||
|
"tag": {"type": "string"},
|
||||||
|
},
|
||||||
|
"required": ["path_template"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
document,
|
||||||
|
),
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Callable
|
||||||
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from auto_reverse.models import EndpointRecord
|
||||||
|
from auto_reverse.store import FlowStore
|
||||||
|
|
||||||
|
Handler = Callable[[dict[str, Any]], dict[str, Any]]
|
||||||
|
|
||||||
|
|
||||||
|
def _record_view(rec: EndpointRecord) -> dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"method": rec.signature.method,
|
||||||
|
"path": rec.signature.path_template,
|
||||||
|
"status": rec.signature.status_class,
|
||||||
|
"samples": rec.sample_count,
|
||||||
|
"documented": rec.documented,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def flows_tools(store: FlowStore) -> dict[str, tuple[dict[str, Any], Handler]]:
|
||||||
|
def search(inp: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
query = inp.get("query", "")
|
||||||
|
records = store.search(query) if query else store.endpoints()
|
||||||
|
return {"endpoints": [_record_view(r) for r in records]}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"flows_search": (
|
||||||
|
{
|
||||||
|
"name": "flows_search",
|
||||||
|
"description": "List/search discovered API endpoints captured so far.",
|
||||||
|
"input_schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {"query": {"type": "string"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
search,
|
||||||
|
),
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
from auto_reverse.doc.engine import DocEngine
|
||||||
|
from auto_reverse.models import CapturedFlow
|
||||||
|
from auto_reverse.store import FlowStore, ScopeFilter
|
||||||
|
from auto_reverse.tools import build_registry
|
||||||
|
|
||||||
|
|
||||||
|
class FakeBrowser:
|
||||||
|
def navigate(self, url):
|
||||||
|
return {"url": url, "title": "T", "elements": []}
|
||||||
|
|
||||||
|
def click(self, selector):
|
||||||
|
return {"url": "u", "title": "T", "elements": []}
|
||||||
|
|
||||||
|
def type_text(self, selector, text):
|
||||||
|
return {"url": "u", "title": "T", "elements": []}
|
||||||
|
|
||||||
|
def snapshot(self):
|
||||||
|
return {"url": "u", "title": "T", "elements": []}
|
||||||
|
|
||||||
|
|
||||||
|
def _store_with_endpoint(tmp_path):
|
||||||
|
store = FlowStore(ScopeFilter(target_hosts={"ex.com"}))
|
||||||
|
store.ingest(CapturedFlow(
|
||||||
|
method="GET", host="ex.com", path="/api/users", query={}, req_headers={},
|
||||||
|
req_body=None, status=200,
|
||||||
|
resp_headers={"content-type": "application/json"}, resp_body=b"[]",
|
||||||
|
timestamp=0.0,
|
||||||
|
))
|
||||||
|
engine = DocEngine(store, out_dir=tmp_path, title="x", use_llm=False)
|
||||||
|
return store, engine
|
||||||
|
|
||||||
|
|
||||||
|
def test_registry_has_expected_tools(tmp_path):
|
||||||
|
store, engine = _store_with_endpoint(tmp_path)
|
||||||
|
reg = build_registry(FakeBrowser(), store, engine)
|
||||||
|
names = {schema["name"] for schema, _ in reg.values()}
|
||||||
|
assert {"browser_navigate", "browser_click", "flows_search", "doc_document"} <= names
|
||||||
|
|
||||||
|
|
||||||
|
def test_flows_search_handler_returns_matches(tmp_path):
|
||||||
|
store, engine = _store_with_endpoint(tmp_path)
|
||||||
|
reg = build_registry(FakeBrowser(), store, engine)
|
||||||
|
_, handler = reg["flows_search"]
|
||||||
|
result = handler({"query": "users"})
|
||||||
|
assert any("/api/users" in ep["path"] for ep in result["endpoints"])
|
||||||
|
|
||||||
|
|
||||||
|
def test_browser_navigate_handler(tmp_path):
|
||||||
|
store, engine = _store_with_endpoint(tmp_path)
|
||||||
|
reg = build_registry(FakeBrowser(), store, engine)
|
||||||
|
_, handler = reg["browser_navigate"]
|
||||||
|
result = handler({"url": "http://x"})
|
||||||
|
assert result["url"] == "http://x"
|
||||||
Reference in New Issue
Block a user