# shop


<!-- WARNING: THIS FILE WAS AUTOGENERATED! DO NOT EDIT! -->

## A cart, as an interface

This notebook lets the agent shop: search a store, put products in the
trolley and read the trolley back. It is written around one real job, a
weekly grocery order split across two Australian stores, and it shows
how to add a whole new kind of capability to Ramabana without touching
the harness.

The host is the agent’s connection to the outside world, and every host
capability turns into tools for every agent built on it. Shopping does
not belong there. An agent editing a repository needs no cart, and most
hosts have no browser to run one. So the cart arrives as an extension
instead: a Python file in the config folder whose `setup(reg)` function
registers the cart’s tools with `reg.tool`. Without that file there is
no cart.

[`Cart`](https://vedicreader.github.io/ramabana/shop.html#cart) is the
interface every cart implements: `open` a store, `find` products, `add`
one, and read back the `lines` and the `total`.
[`FossickCart`](https://vedicreader.github.io/ramabana/shop.html#fossickcart)
drives a real logged-in Chrome.
[`FakeCart`](https://vedicreader.github.io/ramabana/shop.html#fakecart)
is an in-memory stand-in. The tests and the worked example below use
[`FakeCart`](https://vedicreader.github.io/ramabana/shop.html#fakecart),
so building the docs never fills a real trolley. A failed cart action
raises
[`CartError`](https://vedicreader.github.io/ramabana/shop.html#carterror).

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/shop.py#L28"
target="_blank" style="float:right; font-size:smaller">source</a>

### Cart

``` python
def Cart(
    *args, **kwargs
):
```

*One shopping session: find things, put them in, read the trolley back.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/shop.py#L23"
target="_blank" style="float:right; font-size:smaller">source</a>

### CartError

``` python
def CartError(
    *args, **kwargs
):
```

*A cart action failed: no such product, no add control, nothing
matched.*

## The real one

[`FossickCart`](https://vedicreader.github.io/ramabana/shop.html#fossickcart)
drives a logged-in Chrome tab through `fossick.shop`. It starts the
browser session on first use, on the debugging port `port`, and reuses
it after that.

`fossick.shop` does the hard part:

- it injects its helper scripts into the page
- it finds the page’s add-to-cart control
- on Shopify stores, it posts to `/cart/add.js` directly
- it watches the cart for a sign that the add worked

[`FossickCart`](https://vedicreader.github.io/ramabana/shop.html#fossickcart)
is a thin adapter from the
[`Cart`](https://vedicreader.github.io/ramabana/shop.html#cart) methods
to those calls. `stores()` lists the stores fossick knows the search and
trolley pages of, from its `SITES` table. Both stores in the example are
in that table, so their urls are known rather than guessed. Coles blocks
bots that come from datacentre addresses and Ceres is a members’ shop,
so the cart drives a browser you have logged into instead of fetching
pages.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/shop.py#L57"
target="_blank" style="float:right; font-size:smaller">source</a>

### FossickCart

``` python
def FossickCart(
    port:int=9223, tout:int=180, headless:NoneType=None
):
```

*Real Chrome trolley via `fossick.shop`, opened on first use and
reused.*

## The double

[`FakeCart`](https://vedicreader.github.io/ramabana/shop.html#fakecart)
is a shop with a fixed catalogue, `CATALOGUE` unless you pass one, and a
list for a trolley. It picks the store from the catalogue by matching
the current url. It matches products the way fossick does. A number
means that row of the last search. A title is matched exactly first,
then as a substring, against the last search and then the whole store,
and the shortest matching title wins. When nothing matches it raises
[`CartError`](https://vedicreader.github.io/ramabana/shop.html#carterror)
listing what the store stocks, rather than guess. A model can recover
from “no product matching ‘caviar’” followed by a list of titles. It
cannot recover from a silent wrong add.

The cells below search for milk, add two of it and a loaf of sourdough,
and read the total.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/shop.py#L102"
target="_blank" style="float:right; font-size:smaller">source</a>

### FakeCart

``` python
def FakeCart(
    catalogue:NoneType=None, url:str='https://www.coles.com.au'
):
```

*A browserless cart with a fixed catalogue, for tests and docs.*

``` python
cart = FakeCart()
cart.find('milk')
```

``` python
cart.add('milk', qty=2)
cart.add('Sourdough Loaf 680g')
cart.total()
```

A failed match names the options instead of guessing, so the model can
try again instead of buying the wrong thing.

``` python
test_fail(lambda: cart.add('caviar'), contains='no product matching')
test_fail(lambda: cart.add(99), contains='no product #99')
```

## The tools

`cart_tools(cart)` turns any
[`Cart`](https://vedicreader.github.io/ramabana/shop.html#cart) into six
tools for the model: `cart_stores`, `cart_open`, `cart_find`,
`cart_add`, `cart_show` and `cart_remove`. `cart_add` reports `added`
only when the trolley changed, `DID NOT ADD` when it did not, and
`UNVERIFIED` when the cart could not tell.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/shop.py#L167"
target="_blank" style="float:right; font-size:smaller">source</a>

### cart_tools

``` python
def cart_tools(
    cart
):
```

*Return the six tools that let a model shop with `cart`.*

Registering them takes one extension file in the config folder. Nothing
in the harness changes:

``` python
# ~/.config/ramabana/extensions/shopping.py
from ramabana.shop import FossickCart, cart_tools

def setup(reg):
    for t in cart_tools(FossickCart()): reg.tool(t)
```

The cell below does the same with a `Registry` and a
[`FakeCart`](https://vedicreader.github.io/ramabana/shop.html#fakecart).

``` python
from ramabana.tools import Registry, WRITE_TOOLS
reg = Registry()
for t in cart_tools(FakeCart()): reg.tool(t)
[t.__name__ for t in reg.tools]
```

    ['cart_stores',
     'cart_open',
     'cart_find',
     'cart_add',
     'cart_show',
     'cart_remove']

`cart_add` and `cart_remove` are in `WRITE_TOOLS`, the set of tools the
approval gate guards, so an
[`Approvals`](https://vedicreader.github.io/ramabana/agent.html#approvals)
gate asks before either runs, as it would before a file write. Spending
money deserves a confirmation at least as much.

``` python
test_eq({'cart_add', 'cart_remove'} <= WRITE_TOOLS, True)
```

## The weekly shop

This is the whole job end to end: a reminder set once, noticed later,
and acted on.

Only the model’s decisions and the trolley are fakes. The vault, the
watch and the poll are real, and every tool call below runs the actual
tool.
[`ScriptedBackend`](https://vedicreader.github.io/ramabana/testing.html#scriptedbackend),
from the testing notebook, chooses which tool to call and streams the
words, and the harness does everything else. The host is a
[`VaultHost`](https://vedicreader.github.io/ramabana/vault.html#vaulthost)
over a temporary vault, with
[`no_model`](https://vedicreader.github.io/ramabana/testing.html#no_model)
as its graph chat so no model loads.

``` python
import time
from ramabana import Agent
from ramabana.vault import VaultHost
from ramabana.testing import ScriptedBackend, Step, no_model

tmp = Path(tempfile.mkdtemp())
host = VaultHost(roots=[tmp], vault=tmp/'vault.db', index=False, web=False, graph_chat=no_model())
host.open_vault(wait=True)
```

    Warning: You are sending unauthenticated requests to the HF Hub. Please set a HF_TOKEN to enable higher rate limits and faster downloads.

    <ramabana.vault.VaultHost at 0x7f31a5cf6270>

**Once, weeks ago.** Store the shopping list as a note and set a weekly
reminder against it. `start` is when the reminder first comes due.
Setting it a second in the past makes the rest of the page the morning
the reminder fires, with no fake clock.

``` python
host.remember('Weekly Coles order: milk, sourdough, eggs, baby spinach, olive oil. '
              'Plus a seasonal fruit box from Ceres Fair Food.',
              title='the weekly shop', tags=['groceries'])

due = host.watch('Put the weekly groceries in the Coles trolley, and add a Ceres fruit box.',
                 action='remind', every='1w', note='weekly shop', start=time.time() - 1)
int(due['every'])
```

No one has polled yet, so the vault shows the reminder as due.

``` python
test_eq(len(host.watches(due_only=True)), 1)
```

**The morning it fires.** The agent gets the host’s own tools plus the
cart’s. `gpt-mini` resolves to `openai/gpt-5.6-luna`, fast and cheap,
which suits a run of many small steps that each can be checked. The
script never calls it: the steps below are played by
[`ScriptedBackend`](https://vedicreader.github.io/ramabana/testing.html#scriptedbackend).

``` python
shop = FakeCart()
agent = Agent(host, model='gpt-mini', extensions=False)
tools = {t.__name__: t for t in agent.tools} | {t.__name__: t for t in cart_tools(shop)}
agent.routing.turn
```

``` python
script = [
    Step('There is a weekly shop. Pulling the list.',
         tool=('memory_search', {'query': 'weekly Coles order list', 'limit': 2})),
    Step('Opening Coles.', tool=('cart_open', {'url': 'https://www.coles.com.au'})),
    *[s for item in ['milk', 'sourdough', 'eggs', 'baby spinach', 'olive oil']
      for s in (Step(f'Looking for {item}.', tool=('cart_find', {'query': item})),
                Step(f'Adding {item}.', tool=('cart_add', {'item': item})))],
    Step('Now the fruit box.', tool=('cart_open', {'url': 'https://members.ceresfairfood.org.au'})),
    Step('', tool=('cart_find', {'query': 'fruit box'})),
    Step('', tool=('cart_add', {'item': 'Seasonal Fruit Box - Medium'})),
    Step('Reading the trolley back.', tool=('cart_show', {})),
    Step('Done: five staples in the Coles trolley and a medium seasonal fruit box from Ceres.'),
]

be = ScriptedBackend(steps=script, token_delay=0)
be.refresh('', list(tools.values()))
agent.poll_watches(force=True).join()
print(be.send('anything outstanding this week?'))
```

The harness polls, not the model: there is no `poll_watches` tool. In a
session
[`Agent.poll_watches`](https://vedicreader.github.io/ramabana/agent.html#agent.poll_watches)
runs at the start of each turn. Here the script is sent to the backend
directly, so the cell calls it by hand first. The reminder that fired
became a vault note, and the first `memory_search` finds it:

``` python
[h['content'] for h in be.hist if h['role'] == 'tool'][0]
```

Check the trolley, not the transcript. `cart_add` compares the trolley
before and after each add, so a reported add is one the cart confirmed.

``` python
shop.total()
```

The reminder has rescheduled itself for next week, so the job repeats
with no new setup.

``` python
w = host.watches()[0]
test_eq(w['runs'], 1)
test_eq(w['last_status'], 'ok')
test_eq(w['next_run'] > due['next_run'], True)
```

### Against the real shops

The same run on a browser you are already logged into. The cart is
[`FossickCart`](https://vedicreader.github.io/ramabana/shop.html#fossickcart),
and the agent asks before every write:

``` python
from ramabana import Agent
from ramabana.agent import Approvals
from ramabana.shop import FossickCart, cart_tools
from ramabana.tools import WRITE_TOOLS
from ramabana.vault import VaultHost

host  = VaultHost(roots=['~/notes'])              # the shared ~/.vishalakshi/vault.db
agent = Agent(host, model='gpt-mini', approvals=Approvals(WRITE_TOOLS, mode='ask'))
for t in cart_tools(FossickCart()): agent.add_tool(t)

agent.ask('anything outstanding this week?')
```

Two rules for a real trolley. Keep the approval gate on: `cart_add` is
in `WRITE_TOOLS`, so you see every add before it happens. And leave the
checkout alone. `fossick.shop` has `shop_fill` for the payment form, and
this module does not wrap it. A trolley can be emptied again. A placed
order cannot.
