# bb.photos — the photo library

The photo library is the durable store of images the agent should
remember: WhatsApp sends, camera captures the agent explicitly saved,
user uploads. It's separate from the workspace (the agent's notebook)
and from the perception pipeline's crops (ephemeral).

## When to use it

- User asks for a photo ("show me the beach photos from last summer").
- You want to pull up something tagged with a person ("any recent
  photos with Emily?").
- Curating the slideshow rotation.
- Attaching a specific photo to a reply — call `view()` so you can
  look at it and describe it accurately.

## When NOT to use it

- For ephemeral scratch images (agent-authored visualizations, debug
  snapshots). Those belong in `bb.workspace`.
- For raw perception state. Those live in perception/crops and never
  surface as library photos.

## API

### Search

```python
photos = bb.photos.search(
    query="snorlax plushie on kitchen table",   # free-text
    tags=["indoor", "kitchen"],                  # AND-filter
    people=["Erik"],                             # AND-filter
    limit=10,
)
for p in photos:
    print(p.id, p.description, p.tags)
```

Returns a list of `PhotoRecord` objects ordered by hybrid-retrieval
score (vector similarity + BM25). Filters are AND-combined. If no
query is given, results come back newest-first.

### Get one by id

```python
p = bb.photos.get("abc123def456")
print(p.description, p.tags, p.people)
```

Returns the full record: id, description, tags, people, dimensions,
source, created_at, etc. Raises `bb.photos.PhotosError` if missing.

### View — see the pixels

```python
bb.photos.view("abc123def456")
```

Attaches the photo's JPEG to the tool result as an image content block.
Same mechanism as `bb.workspace.view()` and `bb.camera.capture()` —
the agent sees the image this turn. Returns `{id, filename, kind:
"image", attached: True}`.

Use this before responding to "what's in that photo?" or when you need
the actual content, not just the description.

### View an unsaved file by path

```python
bb.photos.view_path("/var/lib/boxbot-sandbox/tmp/inbound/whatsapp/wamid.HBg.jpg")
```

Same idea as `view()`, but for image files that aren't in the photo
library yet. The most common case: an inbound WhatsApp image. The
user's message will start with `[image attached at <path>]` — pass
that exact path. The path must live under one of the allowlisted
roots (sandbox tmp, workspace, photos, perception crops); anything
else is refused.

### Ingest — save an image into the library

```python
photo_id = bb.photos.ingest(
    "/var/lib/boxbot-sandbox/tmp/inbound/whatsapp/wamid.HBg.jpg",
    source="whatsapp",
    sender="Erik",
    caption="my new pokémon",
)
```

Hands a local image file to the intake pipeline. The pipeline copies
the bytes into `data/photos/`, runs detection + tagging, and indexes
for search. The original file is deleted on success.

Use this when an inbound photo is worth keeping — family moments,
things the user asked you to remember, anything you'd want to surface
later. Don't ingest memes, throwaway shares, or anything ephemeral.
View it, respond, and let the inbound janitor reap it (7-day TTL).

`source` is mandatory and goes on the photo for filtering later;
`sender` and `caption` are optional but help. The caption seeds the
photo's description so search works even before the small-model
tagger fills it in.

### Show on the 7" screen

```python
# A single photo
bb.photos.show_on_screen(["abc123def456"])

# A rotating pick
results = bb.photos.search(query="Emily birthday")
bb.photos.show_on_screen([p.id for p in results[:5]])
```

Dispatches to the `picture` display on the physical screen. This is
for humans in the room — it does NOT attach to the tool result. Pair
with `view()` if you want to see what you're showing.

Currently stubs through when the display manager isn't wired up yet
(the call returns `{dispatched: False, reason: "display manager not
running"}`).

### Metadata updates

```python
bb.photos.update(photo_id, description="Dad's 60th, Apr 2026")
bb.photos.set_tags(photo_id, tags=["family", "party"])
bb.photos.set_person(photo_id, person_index=0, name="Erik")
```

### Slideshow

```python
bb.photos.add_to_slideshow(photo_id)
bb.photos.remove_from_slideshow(photo_id)
```

### Lifecycle

```python
bb.photos.delete(photo_id)   # soft delete, 30-day retention
bb.photos.restore(photo_id)
```

## Patterns

### "Show me that photo Emily sent last week"

```python
results = bb.photos.search(
    query="recent photos from Emily",
    people=["Emily"],
    limit=1,
)
if results:
    bb.photos.view(results[0].id)          # see it
    bb.photos.show_on_screen([results[0].id])  # and put it on the screen
else:
    print("no matching photo")
```

### Describing a photo the user is asking about

```python
p = bb.photos.get(photo_id)         # metadata
bb.photos.view(photo_id)            # pixels (attaches to tool result)
# Now you have the description + can cross-reference visual details.
```

### Inbound WhatsApp image — view and decide

```python
# The user's WhatsApp message arrived as:
#   "[image attached at /var/lib/boxbot-sandbox/tmp/inbound/whatsapp/wamid.HBg.jpg] check this out"
path = "/var/lib/boxbot-sandbox/tmp/inbound/whatsapp/wamid.HBg.jpg"

bb.photos.view_path(path)         # see it this turn

# Decide based on what you see + who sent it + the caption:
photo_id = bb.photos.ingest(
    path,
    source="whatsapp",
    sender="Erik",
    caption="check this out",
)
```

If you decide *not* to keep it, just don't call `ingest()` — the
janitor deletes staged inbound files older than 7 days.

### Curate the idle slideshow

```python
for tag in ("family", "vacation"):
    for p in bb.photos.search(tags=[tag], limit=20):
        bb.photos.add_to_slideshow(p.id)
```

## Known gaps

- `show_on_screen` is wired to the display manager but the `picture`
  display itself isn't fully implemented yet — until it lands, the
  call returns `dispatched: False`. `view()` (pixels to tool result)
  works now.
- Metadata mutation (`set_tags`, `delete`, etc.) currently acks the
  action but the main-process handlers are stubs. Read/view works;
  write doesn't yet.
