Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,21 @@ Both exports have this structure:

Problems raise `ExportError` with a readable message, for example when the project or session doesn't exist or you can't read it, a background job fails, or the server doesn't support exports yet.

## Agent reference

The server describes its own API and data model (resources, fields, relationships, JSON schemas, guides) at
`/reference/`. It is written for AI agents, public, and needs no token:

```python
print(client.agent_reference()) # index of all pages
print(client.agent_reference("resources/session.md")) # one resource
print(client.agent_reference("guides/query-planning.md")) # a guide
```

```bash
brainstem reference rules.md
```

## Command-line Interface

After installation a `brainstem` command is available in your shell.
Expand Down
26 changes: 26 additions & 0 deletions brainstem_api_tools/brainstem_api_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -657,6 +657,32 @@ def _check_export_response(self, resp: Response, model: str) -> None:
raise ExportError(resp.json().get("detail", resp.reason))
resp.raise_for_status()

# ------------------------------------------------------------------
# Agent reference
# ------------------------------------------------------------------

def agent_reference(self, page: str = "index.md") -> str:
"""Return one page of the server's agent reference as text.

The server describes its own API and data model at ``/reference/`` (Markdown
pages such as ``rules.md``, ``resources/session.md`` or
``guides/query-planning.md``, and JSON documents under ``data/``). It is
public, so this works with an anonymous client too.

Parameters
----------
page : Page path relative to the reference root. Defaults to the index.
"""
url = self._base_url + "reference/" + page.lstrip("/")
resp = self._session.get(url, timeout=self.DEFAULT_TIMEOUT)
if resp.status_code == 404:
raise ValueError(
f"No agent reference page '{page}' on this server. The page does not exist, "
"or the server predates the agent reference."
)
resp.raise_for_status()
return resp.text

def __enter__(self):
return self

Expand Down
20 changes: 20 additions & 0 deletions brainstem_api_tools/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,16 @@ def _build_parser() -> argparse.ArgumentParser:
p_delete.add_argument("--portal", default="private")
p_delete.add_argument("--id", required=True, help="UUID of the record to delete.")

# ---- reference --------------------------------------------------
p_reference = sub.add_parser(
"reference", parents=[common],
help="Print a page of the server's agent reference (API and data model description).",
)
p_reference.add_argument(
"page", nargs="?", default="index.md",
help="Page path, e.g. rules.md, resources/session.md, guides/query-planning.md (default: index.md).",
)

return parser


Expand All @@ -151,6 +161,16 @@ def main():
print("No cached token found.")
return

if args.command == "reference":
# The reference is public: never start a login for it.
client = BrainstemClient(anonymous=True, url=getattr(args, "url", None))
try:
print(client.agent_reference(args.page))
except (ValueError, requests.HTTPError) as exc:
print(f"Error: {exc}", file=sys.stderr)
sys.exit(1)
return

client = BrainstemClient(
token=args.token,
headless=getattr(args, "headless", False),
Expand Down
49 changes: 49 additions & 0 deletions tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -1154,6 +1154,23 @@ def _run_cli(self, argv, client_mock):
patch("sys.argv", argv):
cli_module.main()

def test_cli_reference_prints_page(self, capsys):
client = MagicMock()
client.agent_reference.return_value = "# rules"
with patch("brainstem_api_tools.cli.BrainstemClient", return_value=client) as factory, \
patch("sys.argv", ["brainstem", "reference", "rules.md"]):
import brainstem_api_tools.cli as cli_module
cli_module.main()
assert factory.call_args.kwargs["anonymous"] is True
client.agent_reference.assert_called_once_with("rules.md")
assert "# rules" in capsys.readouterr().out

def test_cli_reference_defaults_to_index(self, capsys):
client = MagicMock()
client.agent_reference.return_value = "# index"
self._run_cli(["brainstem", "--token", TOKEN, "reference"], client)
client.agent_reference.assert_called_once_with("index.md")

def test_cli_load_calls_load(self, capsys):
resp = mock_response(200, {"sessions": []})
client = MagicMock()
Expand Down Expand Up @@ -1293,3 +1310,35 @@ def test_cli_offset_not_multiple_of_page_size_exits(self, capsys):
cli_module.main()
assert "multiple of limit" in capsys.readouterr().err
client._session.get.assert_not_called()


# ---------------------------------------------------------------------------
# Agent reference
# ---------------------------------------------------------------------------

class TestAgentReference:
def test_fetches_page(self):
client = make_client()
resp = MagicMock(status_code=200, text="# rules")
with patch.object(client._session, "get", return_value=resp) as get:
assert client.agent_reference("rules.md") == "# rules"
assert get.call_args[0][0] == "https://www.brainstem.org/reference/rules.md"

def test_default_page_is_index(self):
client = make_client()
with patch.object(client._session, "get", return_value=MagicMock(status_code=200, text="x")) as get:
client.agent_reference()
assert get.call_args[0][0].endswith("/reference/index.md")

def test_unknown_page_raises_value_error(self):
client = make_client()
with patch.object(client._session, "get", return_value=MagicMock(status_code=404)):
with pytest.raises(ValueError, match="No agent reference page"):
client.agent_reference("nope.md")

def test_anonymous_client_reads_the_public_reference(self):
client = BrainstemClient(anonymous=True)
with patch.object(client._session, "get", return_value=MagicMock(status_code=200, text="# index")) as get:
assert client.agent_reference() == "# index"
assert "Authorization" not in client._session.headers
get.assert_called_once()
Loading