ai-autoreply.md 6.0 KB


type: workflow title: AI auto-reply workflow description: End-to-end trace of the per-chat AI autoreply — enabling it, the ReplyGate rules, building the AI context, the vision path with its courtesy fallback, and sending the reply. tags: [ai, autoreply, workflow, vision, reply-gate] verified:

  • by: openwiki/0.7.2 at: 2026-10-19T18:58:46.219Z sources:
  • id: openwiki-source-9fec9d1af4 resource: repo://src/app.rs
  • id: openwiki-source-043e6252ba resource: repo://src/autoreply.rs generated: { by: "claude-code", at: "2026-10-19T18:58:46.219Z" } ---

AI auto-reply workflow

The autoreply answers incoming WhatsApp messages with an AI-generated text, per chat and only when the user turned it on. Decision rules and context building are pure functions in src/autoreply.rs (no I/O, src/autoreply.rs:1-4); orchestration lives in App; the HTTP call is done by the AI client.

WaEvent::Message
  -> History::save_message (new only)
  -> App::maybe_autoreply
       ReplyGate::should_reply? --no--> stop
       ReplyGate::record_reply
       History::recent_messages(20) -> build_turns
       image + vision model? -> vision request (fallback: text request)
                       else -> text request
  -> AppEvent::AiReply
  -> autoreply still enabled? --no--> discard
  -> WaHandle::send_text -> AppEvent::Sent -> stored with is_ai = true

1. Enabling per chat

Ctrl+A toggles the flag of the open chat (toggle_autoreply, src/app.rs:477-491). Turning it off is always allowed; turning it on requires a configured AI (API key and system_prompt, see Configuration) and a private chat (can_enable_autoreply, src/app.rs:743-751). The flag is persisted in the history store (set_autoreply, default off).

A chat is private when its JID server is s.whatsapp.net or lid; groups, broadcasts, newsletters and malformed JIDs are not (is_private_chat, src/autoreply.rs:48-58).

2. Trigger

Only messages that History::save_message reports as new reach maybe_autoreply (src/app.rs:251-284), so duplicates and history-sync re-deliveries of stored messages never trigger. maybe_autoreply needs an AI client and a system prompt, refreshes the own JIDs if unknown and reads the chat flag.

3. ReplyGate rules

ReplyGate::should_reply(message, enabled, now) (src/autoreply.rs:130-137) returns true only when all hold:

Rule Detail
Enabled per-chat flag is on
Sender allowed not sent by the user — except human-written messages in the self-chat ("Messaggi a te stesso"); AI messages never trigger (src/autoreply.rs:115-117)
Private chat is_private_chat
No backlog timestamp >= started_at, the process start time
Has content text not blank and not [media]/[messaggio non supportato]; voice notes and images do trigger
Cooldown at least COOLDOWN_SECONDS = 30 s since the last reply in that chat (src/autoreply.rs:155-159)

The self-chat is detected by comparing the chat JID with the account's own PN/LID JIDs, ignoring a :N device suffix (src/autoreply.rs:94-107, src/autoreply.rs:168-178).

record_reply is called before the AI request is spawned, so a second message arriving during the request cannot cause a double reply (src/autoreply.rs:145-147).

4. Building the context

build_turns(system_prompt, messages, context) (src/autoreply.rs:268-280):

  • first a System turn with the configured prompt;
  • then the last CONTEXT_MESSAGES = 20 messages (src/autoreply.rs:15), skipping blank texts; the last one is the triggering message;
  • roles (turn_role, src/autoreply.rs:187-190): in normal chats own messages are Assistant; in the self-chat, where every message is from_me, only is_ai messages are Assistant;
  • only User turns are rewritten (src/autoreply.rs:229-254):
    • voice note: the triggering one becomes an instruction to reply courteously that it cannot be listened to and ask to write it; older ones become a short history note;
    • image: the triggering one becomes "reply to the attached image" when vision is used, otherwise an instruction to say it cannot be seen and ask for a description; older ones become a history note; a caption is appended as Didascalia: ... (src/autoreply.rs:209-219);
    • any other text is kept as is.

TurnContext { self_chat, vision } (src/autoreply.rs:193-199) carries the two switches.

5. Vision path and fallback

The image is kept only when the triggering message is a live image and a vision model is configured (vision_applies). In that case spawn_reply (src/app.rs:293-316) builds two turn lists — with vision: true and, as fallback, vision: false — and runs one background task that:

  1. downloads the image into memory via the WhatsApp client;
  2. calls complete_with_image with the vision model;
  3. on any failure, retries with plain complete using the fallback turns (the courtesy "cannot see it" reply).

If both fail, the error combines both messages (<vision error>; Fallback cortesia fallito: <error>, src/app.rs:653-676). Without a usable image, a plain text request is spawned.

6. Sending

AppEvent::AiReply is handled by send_ai_reply_if_enabled (src/app.rs:338-359), which re-reads the chat flag: if autoreply was turned off while the request was running, the reply is discarded with status Risposta AI scartata: autoreply disattivato. Otherwise the text is sent with spawn_send(..., is_ai = true); on success handle_sent (src/app.rs:365-389) stores it as an own message with is_ai = true, which keeps it from re-triggering the self-chat. Errors appear in the status line (Errore AI: ..., Invio fallito: ...).

Tests

The rules and turn building are covered by 41 tests in src/autoreply.rs:282+; enable rules and vision applicability by src/app.rs:762+. See Unit tests.