---
name: devplacebot
description: Build persona bots for devplace.net that answer mentions and direct messages through the OpenAI-compatible gateway, generate images, and react with emoji. Use when asked to create, extend, or debug a devplace bot like bot_dolphin_place.py.
---
<!-- retoor <retoor@molodetz.nl> -->
# Devplacebot

A devplace bot is a single polling process with three collaborators:
`DevPlaceClient` (REST), `Gateway` (LLM), `Store` (dedupe state). The
reference implementation is `bot_dolphin_place.py`; the template is
`scripts/bot_template.py`. Endpoint details: `references/api.md`.

## Architecture

| Class | Responsibility |
|---|---|
| `Store` | dataset + SQLite tables `seen_notifications`, `answered_messages`, `reacted_targets`; every table keyed by uid |
| `DevPlaceClient` | `requests.Session` with `X-API-KEY`; `get`/`post` raise on HTTP error and return JSON |
| `Gateway` | `OpenAI` client with `base_url=<base>/openai/v1`, same API key, `X-App-Reference` header |
| `Bot` | two polls in one loop, each wrapped in a safe runner that logs and continues |

Configuration comes from the environment: `DEVPLACE_API_KEY`
(required), `DEVPLACE_BASE_URL` (default `https://devplace.net`).
Model name is `molodetz`.

## Loop

A monotonic scheduler runs `poll_mentions` every 30 s and
`poll_messages` every 10 s with a 1 s sleep. No threads. Every poll and
every per-item handler has its own try/except with `LOG.exception`, so
one bad item never stops the bot or the rest of the batch.

## Mentions

1. `GET /notifications` returns `notification_groups[].entries[]`; the
   record is `entry["notification"]` or the entry itself.
2. Skip when `uid` is missing or already in `seen_notifications`.
3. Skip and mark seen when `read` is true.
4. Only handle types `mention`, `reply`, `comment`.
5. Mark seen before handling, mark read on the server in `finally`.
6. Resolve `target_url` with the slug regexes for `/posts/`, `/gists/`,
   `/projects/`, `/news/`, then fetch the detail by slug. The item is
   `detail[target_type]` or the detail itself.
7. Content falls back through `content`, `description`, `source_code`,
   `title`.
8. Prompt the model with the notification `message` plus the content,
   post the reply with `POST /comments/create` (`target_uid`,
   `target_type`).
9. React once per target, recorded in `reacted_targets`.

## Direct messages

1. `GET /messages` returns `conversations[]`; only those with
   `unread` and `other_user.uid`.
2. `GET /messages?with_uid=<uid>` returns `messages[]`, each
   `{is_mine, message: {uid, content}}`.
3. Answer only when the last incoming message uid is not in
   `answered_messages`; mark it answered before calling the model.
4. History is the last 20 messages, `is_mine` mapped to `assistant`,
   others to `user`, empty content dropped.
5. Append a line to the system prompt stating this is a private chat.
6. Reply with `POST /messages/send` (`receiver_uid`).

## Persona

The persona is the `INSTRUCTIONS` system prompt. Keep these sections:

- identity and behavioural rules
- disabled features, to suppress unwanted tool talk
- conversational style
- image protocol (below)
- formatting restrictions, for example no markdown tables

## Image protocol

The model requests an image by ending its reply with exactly one line
`IMAGE: <visual description>`. `split_image_request` inspects only the
last line, case-insensitive, and returns `(reply, prompt)`. The prompt
goes to `POST /uploads/generate-image`; the returned `uid` is sent as
`attachment_uids` on the comment or message. The system prompt must
state that the line is added only on explicit request and is never
mentioned to the user. Image failure is logged and the text reply still
goes out.

## Reactions

A separate completion with the persona prompt asks for exactly one emoji
or `NONE`. Accept only the first line, reject empty, `NONE`, or longer
than 8 characters. Send via `POST /reactions/<type>/<uid>` with the
`X-Requested-With: fetch` header.

## Delivery semantics

State is marked before the side effect. A failure therefore drops one
reply instead of looping and spamming; this is deliberate. Keep that
order. Do not mark after success unless duplicate replies are
acceptable.

## Extending

- New target type: add a slug regex, a client detail method, and a row
  in the resolver table; no other change.
- New behaviour: add a poll method, register it in the scheduler with
  its own interval, wrap it in the safe runner.
- Multiple personas: one process per persona, separate database file
  and `APP_REFERENCE`.
- Rate: keep intervals at or above the defaults; the API is shared.

## Checklist

- `databases/` directory is created before `dataset.connect`
- no secrets in code; the key is read from the environment
- every `requests` call has a timeout and `raise_for_status`
- the bot never replies to its own messages or notifications
- logs carry uid of the failing item
- README lists the environment variables and the run command
