# Ask the agent

Notebook: https://cow-tools.sophia-labs.com/notebooks/ask-the-agent/ask-the-agent.ipynb · Page: https://cow-tools.sophia-labs.com/notebooks/ask-the-agent/index.html · Data: Uruguay MGAP/SNIG DICOSE 2024–2025 (graph `uruguay-cows` at https://cow-tools.sophia-labs.com/mcp).

> Shipped unexecuted: it talks to a model, so it runs on your own OpenRouter key (BYOK, the X-Model-Key header). Set OPENROUTER_API_KEY and run it; the site's build spends no model money.

## The notebook

The demo hosts personal agents over its graphs. You open a session, talk to
it, leave, and come back to the same conversation later. The protocol is
Vehicle's **ride protocol** (`sophia.vehicle-ride.v2`): plain HTTP plus a
long-poll (or SSE) event stream, the same one the chat box on the site uses.

Reading the data and the tools is free. A conversation costs model money, so
it needs one of two things: **your own OpenRouter key**, sent with every
request in the `X-Model-Key` header (held in memory for the session, never
stored), or a Sophia account with credit. This notebook uses your key.

And because this is cow-tools, we do not take the agent's word for it: the
last cell checks its answer against the cube.

```python
# Setup: your OpenRouter key from the environment (never paste it into a notebook).
import json
import os
import time
import urllib.request
import uuid

import cowtools_client as cw

KEY = os.environ.get("OPENROUTER_API_KEY")
if not KEY:
    raise SystemExit("set OPENROUTER_API_KEY to run this notebook (https://openrouter.ai/keys)")
RIDE = f"{cw.BASE_URL}/services/vehicle/g/{cw.COWS}"
CLIENT = f"notebook-{uuid.uuid4().hex[:8]}"


def ride(method: str, path: str, body: dict | None = None) -> dict:
    headers = {"Content-Type": "application/json", "X-Model-Key": KEY, "X-Vehicle-Client-Id": CLIENT}
    if cw.TOKEN:
        headers["Authorization"] = f"Bearer {cw.TOKEN}"
    req = urllib.request.Request(RIDE + path, method=method, headers=headers,
                                 data=json.dumps(body).encode() if body is not None else None)
    with urllib.request.urlopen(req, timeout=60) as r:
        frame = json.loads(r.read())
    if frame.get("type") == "error":
        raise cw.CowToolsError(f"{frame.get('code')}: {frame.get('message')}")
    return frame
```

## Open a session

A session is bound to the graph in its path (`/g/uruguay-cows/`). The
`requestId` makes each call idempotent: send it twice, it happens once.

```python
created = ride("POST", "/sessions", {"requestId": f"req-{uuid.uuid4()}", "title": "cow-tools notebook",
                                     "model": "openrouter/openai/gpt-6-luna"})
session = created["value"]["session"]["sessionId"]
print("session:", session)
```

## Ask, then watch the events

`prompt` starts a run. `watch` long-polls the event log from a cursor until
the run settles; every event carries a sequence number, so a client that
drops off can resume where it left.

```python
question = ("How many dairy cows (milking plus dry) did Florida declare on 30 June 2025, by the PD_PF count? "
            "Answer with the number first, then the query you used.")
ride("POST", f"/sessions/{session}/commands", {"requestId": f"req-{uuid.uuid4()}", "op": "prompt", "text": question})

since, replies, cost, done = 0, [], 0.0, False
deadline = time.time() + 180
while not done and time.time() < deadline:
    frame = ride("GET", f"/sessions/{session}/events?since={since}&waitMs=25000")
    for ev in frame["value"]["events"]:
        since = max(since, ev["seq"])
        p = ev["payload"]
        if ev["type"] == "tool.started":
            print("tool:", p.get("toolName") or p.get("name"))
        if ev["type"] == "message.completed" and p.get("role") == "assistant":
            replies.append(p.get("text", ""))
            cost += p.get("usage", {}).get("costUsd", 0)
        if ev["type"] in ("run.settled", "run.error"):
            done = True
answer = replies[-1] if replies else ""
print(answer)
print(f"model cost: ${cost:.4f}")
```

## Come back later

The session outlives this notebook. `inspect` returns its summary and
transcript; `capsule` a compact record. Note the session id and you can pick
the conversation up from any client.

```python
snapshot = ride("GET", f"/sessions/{session}")
print(json.dumps(snapshot["value"].get("summary", snapshot["value"]), indent=1)[:800])
```

## Check the agent

```python
truth = cw.sparql_value("""
SELECT (SUM(?pf) AS ?cows)
WHERE {
  GRAPH ?g {
    ?obs a cow:AnimalCategoryStockObservation ; cow:year "2025"^^xsd:gYear ;
         cow:department <urn:sophia:cow:department/7> ; cow:species <urn:sophia:cow:species/11> ;
         cow:animalCategory ?c ; cow:animalsCountPf ?pf .
    VALUES ?c { <urn:sophia:cow:animal-category/11/2> <urn:sophia:cow:animal-category/11/3> }
  }
}
""")
said = [int(x.replace(",", "")) for x in __import__("re").findall(r"\b\d{1,3}(?:,\d{3})+\b|\b\d{4,}\b", answer)]
cw.check("the agent's first number is the cube's", said[0] if said else None, int(truth),
         source="SPARQL over uruguay-cows, categories 11/2 and 11/3, PD_PF")
cw.finding("headline", f"The agent answered {said[0] if said else 'nothing'}; the cube says {int(truth):,}.")
cw.save()
```
