PHP Classes

File: docs/proposals/AIBots.md

Recommend this page to a friend!
  Packages of Matthew Asham   Binkterm PHP   docs/proposals/AIBots.md   Download  
File: docs/proposals/AIBots.md
Role: Auxiliary data
Content type: text/markdown
Description: Auxiliary data
Class: Binkterm PHP
Bulletin board system based on the Web
Author: By
Last change:
Date: 5 days ago
Size: 18,196 bytes
 

Contents

Class file image Download

AI Bots System ? Design Proposal

> Draft ? This document was generated by AI and may not have been reviewed for accuracy.

Table of Contents

  1. Overview
  2. Goals & Non-Goals
  3. Data Model
  4. System User Association
  5. Activity Architecture
  6. Local Chat Activity
  7. Bot Daemon
  8. Weekly Budget Enforcement
  9. Admin Interface
  10. Database Migrations
  11. Future Activities
  12. Security & Safety

Overview

AI Bots are system-user accounts backed by a configured AI provider that can participate in BBS activities on behalf of a sysop-defined persona. Each bot has a system prompt that shapes its personality and knowledge, a set of enabled activities that define what the bot reacts to, and a weekly spending budget that prevents runaway API costs.

The first supported activity is local chat: a bot can receive direct messages from users and respond when mentioned by name in a chat room. This is fully reactive ? the bot responds to live messages through the existing PostgreSQL LISTEN/NOTIFY pipeline rather than polling.

Goals & Non-Goals

Goals - Allow sysops to create multiple AI bots with distinct personas - Make bot response latency comparable to a live user (seconds, not minutes) - Reuse existing AI infrastructure (src/AI/AiService.php, ai_requests table) - Design activity handlers so new activities (netmail, echomail digest, etc.) can be added without restructuring the core bot system - Give sysops clear cost visibility per bot per week

Non-Goals - Bot-to-bot conversations (out of scope for v1) - Per-message credit charging to regular users - Proactive (unsolicited) message generation on a schedule (only reactive for local chat)

Data Model

ai_bots table

| Column | Type | Notes | |---|---|---| | id | SERIAL PRIMARY KEY | | | user_id | INTEGER NOT NULL | FK ? users.id; the system user account for this bot | | name | VARCHAR(100) NOT NULL | Display name shown in admin UI (may differ from username) | | description | TEXT | Short sysop note about this bot's purpose | | system_prompt | TEXT NOT NULL | Sent as the system role message to the AI provider | | provider | VARCHAR(50) | openai or anthropic; NULL = use site default | | model | VARCHAR(100) | Model override; NULL = use provider default | | weekly_budget_usd | NUMERIC(10,4) NOT NULL DEFAULT 1.00 | Max spend per week; resets Sunday 00:00 UTC | | context_messages | SMALLINT NOT NULL DEFAULT 10 | How many prior chat messages to include as context | | is_active | BOOLEAN NOT NULL DEFAULT TRUE | Master on/off switch | | created_at | TIMESTAMP NOT NULL DEFAULT NOW() | | | updated_at | TIMESTAMP NOT NULL DEFAULT NOW() | |

ai_bot_activities table

| Column | Type | Notes | |---|---|---| | id | SERIAL PRIMARY KEY | | | bot_id | INTEGER NOT NULL | FK ? ai_bots.id ON DELETE CASCADE | | activity_type | VARCHAR(50) NOT NULL | e.g. local_chat, netmail (future) | | is_enabled | BOOLEAN NOT NULL DEFAULT TRUE | | | config_json | JSONB | Activity-specific settings (see per-activity docs) | | created_at | TIMESTAMP NOT NULL DEFAULT NOW() | | | UNIQUE(bot_id, activity_type) | | One record per activity per bot |

Weekly cost query

A nullable bot_id column is added to the existing ai_requests table (see migration v1.11.0.76). All AI calls made on behalf of a bot populate this column with the bot's ai_bots.id. This makes per-bot cost accounting independent of which system user the bot is associated with ? multiple bots can share a user account or a user can have many bots without the cost query breaking.

Current-week cost is calculated on demand:

SELECT COALESCE(SUM(estimated_cost_usd), 0)
FROM   ai_requests
WHERE  bot_id = :bot_id
  AND  created_at >= (
           date_trunc('week', NOW() AT TIME ZONE 'UTC')
           -- date_trunc gives Monday; shift back 1 day for Sunday anchor,
           -- then forward 1 day if today IS Sunday to avoid going to last Sunday
           - INTERVAL '1 day'
           + CASE WHEN EXTRACT(DOW FROM NOW() AT TIME ZONE 'UTC') = 0
                  THEN INTERVAL '7 days' ELSE INTERVAL '0 days' END
       )
  AND  status = 'success';

A helper method AiBot::getCurrentWeekCostUsd() will encapsulate this query.

System User Association

Every bot is linked to a users record with is_system = TRUE. This means:

  • The bot has a real username and can be @mentioned, receive direct messages, and appear in user lists
  • The bot cannot log in through normal authentication
  • Messages sent by the bot are attributed to its user account, making them visible to all participants exactly like a regular user's messages
  • The bot's AI usage is tracked under its `bot_id` in `ai_requests`, making per-bot cost queries accurate even when multiple bots share a system user

When a sysop creates a bot via the admin UI, the system: 1. Creates (or links an existing) system user account 2. Creates the ai_bots record pointing to that user 3. Creates default ai_bot_activities rows for each enabled activity

Bot usernames should follow a naming convention (e.g., prefixed with _bot_ or chosen freely by the sysop) but must still be unique and not contain spaces, per the existing username policy.

Activity Architecture

Activities are the things a bot does. The system distinguishes two interaction models:

| Model | Description | Examples | |---|---|---| | Reactive | Triggered by an event arriving in real time | Local chat DM, @mention | | Scheduled | Triggered on a timer; the handler polls a source | Netmail inbox scan (future) |

PHP Interface

namespace BinktermPHP\AiBot;

interface BotActivityHandler
{
    / Canonical identifier stored in ai_bot_activities.activity_type */
    public function getActivityType(): string;

    / Human-readable label shown in the admin UI */
    public function getLabel(): string;

    / True = daemon registers this handler with the event loop.
        False = daemon calls handle() on a cron interval. */
    public function isReactive(): bool;

    /
     * Called when the activity should process an event (reactive)
     * or perform its periodic work (scheduled).
     *
     * @param AiBot        $bot   The bot that owns this activity
     * @param ActivityEvent $event Encapsulates the triggering data
     */
    public function handle(AiBot $bot, ActivityEvent $event): void;
}

ActivityEvent

namespace BinktermPHP\AiBot;

class ActivityEvent
{
    public function __construct(
        public readonly string $type,       // 'chat_direct' | 'chat_mention' | 'netmail' ...
        public readonly array  $payload,    // event-specific data
    ) {}
}

Daemon integration

The bot daemon (scripts/ai_bot_daemon.php) maintains a registry of activity handlers. On startup it loads all enabled bots and registers their active activity handlers. Reactive handlers subscribe to PostgreSQL NOTIFY channels. Scheduled handlers are invoked by an internal timer within the daemon's event loop.

Local Chat Activity

Activity type string: local_chat

Triggers: 1. A direct message is sent with to_user_id = bot.user_id 2. A room message is posted containing @<BotUsername> (case-insensitive)

config_json fields (all optional):

{
  "respond_in_dm": true,
  "respond_in_rooms": true,
  "allowed_room_ids": [],
  "blocked_user_ids": []
}

  • `respond_in_dm` ? whether to respond to direct messages (default: true)
  • `respond_in_rooms` ? whether to respond to @mentions in rooms (default: true)
  • `allowed_room_ids` ? if non-empty, only respond in these rooms; empty = all rooms
  • `blocked_user_ids` ? ignore messages from these user IDs

Message processing flow

INSERT INTO chat_messages (trigger fires)
        ?
        ?
trg_chat_message_notify inserts into sse_events
  payload: { id, type, room_id, from_user_id, to_user_id, body }
  user_id: NULL (room) or to_user_id (DM)
pg_notify('binkstream', sse_event_id)
        ?
        ?
ai_bot_daemon LISTEN wakes via stream_select()
        ?
        ?
Fetch sse_events row ? extract payload
        ?
   ?????????????????????????????????????????????????
   ? DM: payload.to_user_id matches a bot.user_id? ?
   ? Room: payload.body contains @BotUsername?      ?
   ?????????????????????????????????????????????????
        ? yes ? identify which bot(s) should respond
        ?
Check bot.is_active AND weekly budget not exceeded
        ?
        ?
Build context window:
  - Fetch last N messages (ai_bots.context_messages)
    from the same room or DM thread via chat_messages
  - Format as provider message array
        ?
        ?
Call AiService::generateText(
    provider, model, system_prompt, messages[],
    feature: 'ai_bot', metadata: {bot_id: N}
)  ? records usage in ai_requests with bot_id
        ?
        ?
INSERT INTO chat_messages
  (from_user_id = bot.user_id, same room/DM thread)
        ?
        ?
Trigger fires again ? sse_events ? SSE delivers to clients

Context window

For room messages, context is the last context_messages rows from chat_messages where room_id = <room>, ordered by created_at DESC. For direct messages, context is the conversation between the user and the bot.

Each prior message is formatted as a user or assistant role message depending on whether user_id matches the bot.

Rate limiting within budget

Before calling the AI provider, LocalChatActivityHandler calls AiBot::isUnderBudget(). If the bot has reached its weekly budget, it posts a single canned reply informing the user ("I've reached my weekly limit and will be back Sunday.") and logs a warning. No AI call is made.

Bot Daemon

File: scripts/ai_bot_daemon.php

Delivery mechanism

Chat messages are delivered to the daemon via the existing sse_events infrastructure ? no changes to the chat send endpoint or the database trigger are required.

When a row is inserted into chat_messages, the trg_chat_message_notify trigger: 1. Inserts a row into sse_events with event_type = 'chat_message' and a JSON payload containing id, type, room_id, from_user_id, to_user_id, and body 2. Calls pg_notify('binkstream', evt_id::text) where evt_id is the new sse_events row ID

The daemon subscribes to this channel using a persistent native PostgreSQL connection (the pg_* family of functions, separate from the PDO connection used for application queries). The event loop:

  1. Opens a native `pg_connect()` and issues `LISTEN binkstream`
  2. Uses `stream_select([pg_socket($conn)], ...)` with a short timeout to wait for notifications without busy-looping
  3. On wake: calls `pg_get_notify($conn)` to retrieve the `sse_events` row ID from the notification payload
  4. Queries `sse_events WHERE id = $evtId AND event_type = 'chat_message'` to fetch the full payload
  5. Dispatches to `LocalChatActivityHandler` for any active bots that should respond

On startup the daemon records the current MAX(id) from sse_events as its starting cursor, so it does not replay historical messages.

Required modifications to existing code

The only addition needed outside the new src/AiBot/ namespace is:

  • data/logs/ai_bot_daemon.log must be added to the `UDP_ALLOWED_LOG_FILES` allowlist in `src/Admin/AdminDaemonServer.php` (per the project convention for all new log files)
  • The admin daemon must be extended with start/stop/status commands for `ai_bot_daemon.php`, following the same pattern used for `mrc_daemon.php`

No changes to routes/api-routes.php, sse_events, the chat trigger, or BinkStream are required.

Daemon structure

scripts/ai_bot_daemon.php   ? daemon entrypoint
src/AiBot/
  AiBot.php                 ? model + budget helpers
  AiBotRepository.php       ? DB queries for bots + activities
  ActivityEvent.php
  BotActivityHandler.php    ? interface
  LocalChatActivityHandler.php

The daemon is started/stopped the same way as other daemons (via the admin daemon or systemd unit).

Weekly Budget Enforcement

  • The budget window is Sunday 00:00 UTC ? Saturday 23:59:59 UTC
  • Current spend is the sum of `ai_requests.estimated_cost_usd` for `bot_id = bot.id` within the window where `status = 'success'`
  • The daemon caches this sum per bot and refreshes it after each successful AI call (avoiding a DB hit on every message)
  • If `current_spend >= weekly_budget_usd` the bot is silenced for the remainder of the week; it resumes automatically after Sunday 00:00 UTC
  • The admin UI displays `$current_spend / $weekly_budget` so sysops can monitor usage

Admin Interface

Bot list page: GET /admin/ai-bots

Follows the standard admin list pattern used by Chat Rooms and Users.

Template: templates/admin/ai_bots.twig

Table columns:

| Column | Notes | |---|---| | Name | Bot's display name (links to edit) | | Username | The system user account | | Description | Short sysop note | | Weekly Spend | $0.0012 / $1.00 ? colored green/amber/red based on % used | | Status | Active / Inactive badge | | Actions | Edit · Delete |

Create / Edit modal fields: - Bot Name (text) - Username (text; auto-suggested from name; read-only after creation) - Description (text) - System Prompt (textarea, large) - AI Provider (select: site default / OpenAI / Anthropic) - Model (text, optional override) - Weekly Budget (USD, numeric) - Context Messages (numeric, 1?50) - Active (toggle) - Activities section: list of checkboxes for each available activity type; each with a "Configure" expander for activity-specific settings

API routes (admin)

| Method | Path | Purpose | |---|---|---| | GET | /admin/api/ai-bots | List all bots with current-week spend | | POST | /admin/api/ai-bots | Create bot + system user | | GET | /admin/api/ai-bots/:id | Fetch single bot | | PUT | /admin/api/ai-bots/:id | Update bot | | DELETE | /admin/api/ai-bots/:id | Delete bot (and system user if no other records) | | GET | /admin/api/ai-bots/:id/spend | Current week spend detail |

Database Migrations

v1.11.0.74_ai_bots.sql

CREATE TABLE ai_bots (
    id               SERIAL PRIMARY KEY,
    user_id          INTEGER NOT NULL REFERENCES users(id),
    name             VARCHAR(100) NOT NULL,
    description      TEXT,
    system_prompt    TEXT NOT NULL DEFAULT '',
    provider         VARCHAR(50),
    model            VARCHAR(100),
    weekly_budget_usd NUMERIC(10,4) NOT NULL DEFAULT 1.00,
    context_messages SMALLINT NOT NULL DEFAULT 10,
    is_active        BOOLEAN NOT NULL DEFAULT TRUE,
    created_at       TIMESTAMP NOT NULL DEFAULT NOW(),
    updated_at       TIMESTAMP NOT NULL DEFAULT NOW()
);

CREATE INDEX idx_ai_bots_user_id ON ai_bots(user_id);

v1.11.0.75_ai_bot_activities.sql

CREATE TABLE ai_bot_activities (
    id            SERIAL PRIMARY KEY,
    bot_id        INTEGER NOT NULL REFERENCES ai_bots(id) ON DELETE CASCADE,
    activity_type VARCHAR(50) NOT NULL,
    is_enabled    BOOLEAN NOT NULL DEFAULT TRUE,
    config_json   JSONB,
    created_at    TIMESTAMP NOT NULL DEFAULT NOW(),
    UNIQUE(bot_id, activity_type)
);

CREATE INDEX idx_ai_bot_activities_bot_id ON ai_bot_activities(bot_id);

v1.11.0.76_ai_requests_bot_id.sql

Adds a bot_id foreign key to the existing ai_requests table so that per-bot cost queries are independent of user associations.

ALTER TABLE ai_requests
    ADD COLUMN bot_id INTEGER REFERENCES ai_bots(id) ON DELETE SET NULL;

CREATE INDEX idx_ai_requests_bot_id ON ai_requests(bot_id)
    WHERE bot_id IS NOT NULL;

Future Activities

The activity handler architecture is designed so new activity types slot in without changing the daemon's core loop.

Netmail activity (example)

Activity type: netmail Model: Scheduled (not reactive) Behaviour: Every N minutes, scan the bot user's netmail inbox for unread messages. For each unread message, call the AI with the message body and the bot's system prompt, then send a reply netmail and mark the original as read.

config_json:

{
  "poll_interval_minutes": 5,
  "max_messages_per_run": 5,
  "auto_reply": true
}

Echomail digest activity (example)

Activity type: echomail_digest Model: Scheduled Behaviour: Periodically summarise recent posts in a configured echoarea and post the summary to a configured chat room or send it as netmail to subscribed users.

Security & Safety

  • Bot users cannot log in: the system user flag prevents normal authentication. Even if a bot's username is known, it cannot be impersonated through the login flow.
  • Budget cap is server-enforced: the budget check happens inside the daemon, not in JavaScript. API cost cannot be bypassed by a client.
  • System prompt confidentiality: the system prompt is an admin-only field, never exposed to regular users via any API.
  • Message attribution is transparent: bot messages appear under the bot's username, not an anonymous or impersonated identity. Users can see they are talking to a bot.
  • No DM to bot initiated by bot: bots only respond; they never initiate a conversation unsolicited.
  • Blocked users list: sysops can block specific user IDs per bot to prevent abuse or infinite loops between bots.
  • Bot-to-bot loop prevention: before replying, the handler checks whether the triggering message's `user_id` belongs to another bot (i.e., `is_system = true` and has an `ai_bots` record). If so, the message is ignored to prevent feedback loops.