My agentic slop goes here. Not intended for anyone else!

more

Changed files
+307 -79
stack
+91 -28
stack/zulip/examples/README_ECHO_BOT.md
···
-
# Zulip Echo Bot
-
A simple echo bot that demonstrates the basic functionality of the Zulip bot framework. The bot responds to direct messages and mentions by echoing back the message content.
## Features
-
- Responds to direct messages
-
- Responds to @mentions in channels
-
- Avoids infinite loops by ignoring its own messages
-
- Simple help command
## Prerequisites
···
## Running the Bot
```bash
-
# Run from the project root
dune exec echo_bot
-
# Or run the built executable directly
-
./_build/default/zulip/examples/echo_bot.exe
```
-
You should see output like:
```
-
Starting Zulip Echo Bot...
-
=============================
-
Loaded authentication for: echo-bot@your-domain.zulipchat.com
-
Server: https://your-domain.zulipchat.com
-
Echo bot is running!
-
Send a direct message or mention @echobot in a channel.
-
Press Ctrl+C to stop.
```
## Testing the Bot
···
1. In any channel, type: `@echobot Hello everyone!`
2. The bot should respond: `Echo from [Your Name]: Hello everyone!`
-
### Help Command
-
Send `help` to the bot to get usage information.
## How It Works
The echo bot:
-
1. Connects to Zulip using the real-time events API
-
2. Listens for messages where it's mentioned or direct messaged
-
3. Extracts the message content and sender information
-
4. Sends back an echo of the message
-
5. Ignores its own messages to prevent loops
## Code Structure
-
- **Bot Handler Module**: Implements the `Bot_handler.S` signature with message processing logic
-
- **Message Processing**: Extracts fields from incoming JSON messages
-
- **Response Generation**: Creates appropriate responses based on message type
-
- **Identity Management**: Uses bot identity to avoid self-responses
## Customization
···
+
# Zulip Echo Bot with Verbose Logging
+
An enhanced echo bot that demonstrates the Zulip bot framework with comprehensive logging and CLI configuration. The bot responds to direct messages and mentions by echoing back the message content, with detailed logging at every step.
## Features
+
- **Responds to direct messages and @mentions in channels**
+
- **Comprehensive logging** with multiple verbosity levels
+
- **Command-line interface** with help and configuration options
+
- **Detailed message tracing** for debugging
+
- **Structured logging** using the OCaml logs library
+
- **Built-in commands**: help, ping, and echo
+
- **Avoids infinite loops** by ignoring its own messages
## Prerequisites
···
## Running the Bot
+
### Basic Usage
+
```bash
+
# Show help and available options
+
dune exec echo_bot -- --help
+
+
# Run with default settings (info level logging)
dune exec echo_bot
+
# Run with verbose logging (shows all info messages)
+
dune exec echo_bot -- -v
+
+
# Run with debug logging (shows everything)
+
dune exec echo_bot -- -vv
+
+
# Use custom config file
+
dune exec echo_bot -- -c /path/to/bot.zuliprc
+
+
# Combine options
+
dune exec echo_bot -- -vv -c ~/my-bot.zuliprc
```
+
### Example Output with Different Verbosity Levels
+
+
**Default (no flags) - Info level:**
```
+
echo_bot: [INFO] Starting Zulip Echo Bot
+
echo_bot: [INFO] Log level: Info
+
echo_bot: [INFO] =============================
+
echo_bot: [INFO] Echo bot is running!
+
echo_bot: [INFO] Send a direct message or mention @echobot in a channel.
+
echo_bot: [INFO] Commands: 'help', 'ping', or any message to echo
+
echo_bot: [INFO] Press Ctrl+C to stop.
+
```
+
+
**Verbose (-v) - Info level with more details:**
+
```
+
echo_bot: [INFO] Starting Zulip Echo Bot
+
echo_bot: [INFO] Log level: Info
+
echo_bot: [INFO] =============================
+
+
echo_bot: [INFO] Loaded authentication for: echo-bot@your-domain.zulipchat.com
+
echo_bot: [INFO] Server: https://your-domain.zulipchat.com
+
echo_bot: [INFO] Bot identity created: Echo Bot (echo-bot@your-domain.zulipchat.com)
+
echo_bot: [INFO] Echo bot is running!
+
echo_bot: [INFO] Processing message with 12 fields
+
echo_bot: [INFO] Message metadata: type=private, sender=John Doe (john@example.com), id=12345
+
echo_bot: [INFO] Processing message from John Doe (ID: 123): Hello bot!
+
echo_bot: [INFO] Sending private reply: Echo from John Doe: Hello bot!
+
```
+
**Debug (-vv) - Full debug output:**
+
```
+
echo_bot: [INFO] Starting Zulip Echo Bot
+
echo_bot: [DEBUG] Creating Zulip client
+
echo_bot: [DEBUG] Creating bot storage for echo-bot@your-domain.zulipchat.com
+
echo_bot: [DEBUG] Creating bot handler
+
echo_bot: [DEBUG] Creating bot runner
+
echo_bot: [DEBUG] Received message for processing
+
echo_bot: [DEBUG] Extracted field content: Hello bot!
+
echo_bot: [DEBUG] Extracted field sender_email: john@example.com
+
echo_bot: [DEBUG] Extracted field sender_full_name: John Doe
+
echo_bot: [DEBUG] Extracted field type: private
+
echo_bot: [DEBUG] Extracted field sender_id: 123
+
echo_bot: [DEBUG] Extracted field id: 12345
+
echo_bot: [DEBUG] No bot mention to remove
+
echo_bot: [DEBUG] Generated response: Echo from John Doe: Hello bot!
```
## Testing the Bot
···
1. In any channel, type: `@echobot Hello everyone!`
2. The bot should respond: `Echo from [Your Name]: Hello everyone!`
+
### Special Commands
+
+
- **`help`** - Get usage information
+
- **`ping`** - Bot responds with "Pong! 🏓"
+
- Any other text - Bot echoes it back
## How It Works
The echo bot:
+
1. **Initializes logging** based on CLI verbosity flags
+
2. **Connects to Zulip** using the real-time events API
+
3. **Listens for messages** where it's mentioned or direct messaged
+
4. **Logs message details** at various verbosity levels
+
5. **Extracts fields** with debug logging for each field
+
6. **Processes content** and removes bot mentions
+
7. **Generates responses** with appropriate logging
+
8. **Sends back echo** with type-appropriate reply
+
9. **Ignores own messages** to prevent loops
## Code Structure
+
- **Logging Setup**: Creates a custom log source `echo_bot` for structured logging
+
- **Bot Handler Module**: Implements the `Bot_handler.S` signature with comprehensive logging
+
- **Field Extraction**: Helper functions with debug logging for each field
+
- **Message Processing**: Detailed logging of message metadata and content
+
- **Response Generation**: Logs response creation and type determination
+
- **CLI Interface**: Cmdliner-based argument parsing with help text
+
- **Verbosity Control**: Maps CLI flags to log levels (Info/Debug)
+
- **Error Handling**: Catches and logs exceptions with backtraces
## Customization
+1 -1
stack/zulip/examples/dune
···
(public_name echo_bot)
(name echo_bot)
(package zulip_bot)
-
(libraries zulip zulip_bot eio_main))
(executable
(public_name atom_feed_bot)
···
(public_name echo_bot)
(name echo_bot)
(package zulip_bot)
+
(libraries zulip zulip_bot eio_main cmdliner logs logs.fmt))
(executable
(public_name atom_feed_bot)
+215 -50
stack/zulip/examples/echo_bot.ml
···
-
(* Simple Echo Bot for Zulip
-
Responds to direct messages and mentions by echoing back the message *)
open Zulip_bot
module Echo_bot_handler : Bot_handler.S = struct
-
let initialize _ = Ok ()
let usage () =
"Echo Bot - I repeat everything you say to me!"
···
Send me a direct message or mention me in a channel."
let handle_message ~config:_ ~storage:_ ~identity ~message ~env:_ =
(* Parse the incoming message *)
let extract_field fields name =
match List.assoc_opt name fields with
-
| Some (`String s) -> Some s
-
| _ -> None
in
let extract_int_field fields name =
match List.assoc_opt name fields with
-
| Some (`Float f) -> Some (int_of_float f)
-
| _ -> None
in
match message with
| `O fields ->
(* Extract message details *)
let content = extract_field fields "content" in
let sender_email = extract_field fields "sender_email" in
let sender_full_name = extract_field fields "sender_full_name" in
let message_type = extract_field fields "type" in
-
let _sender_id = extract_int_field fields "sender_id" in
(* Check if this is our own message to avoid loops *)
let is_own_message = match sender_email with
-
| Some email -> email = (Bot_handler.Identity.email identity)
| None -> false
in
-
if is_own_message then
Ok Bot_handler.Response.None
-
else
(* Process the message content *)
-
let response_content = match content, sender_full_name with
-
| Some msg, Some name ->
(* Remove bot mention if present *)
let cleaned_msg =
-
let mention = "@**" ^ Bot_handler.Identity.mention_name identity ^ "**" in
-
let msg =
-
if String.starts_with ~prefix:mention msg then
-
String.sub msg (String.length mention) (String.length msg - String.length mention)
-
else msg
-
in
-
String.trim msg
in
(* Create echo response *)
-
if cleaned_msg = "" then
-
Printf.sprintf "Hello %s! Send me a message and I'll echo it back!" name
-
else if String.lowercase_ascii cleaned_msg = "help" then
-
Printf.sprintf "Hi %s! I'm an echo bot. Whatever you say to me, I'll repeat back. \
-
Try sending me any message!" name
-
else
-
Printf.sprintf "Echo from %s: %s" name cleaned_msg
-
| Some msg, None ->
Printf.sprintf "Echo: %s" msg
-
| _ ->
"I couldn't understand that message."
in
(* Determine response type based on original message type *)
let response = match message_type with
| Some "private" ->
-
(* For private messages, reply directly *)
Bot_handler.Response.Reply response_content
| Some "stream" ->
-
(* For stream messages, reply in the same topic *)
Bot_handler.Response.Reply response_content
-
| _ ->
Bot_handler.Response.None
in
Ok response
-
| _ ->
Ok Bot_handler.Response.None
end
-
let run_echo_bot env =
-
Printf.printf "Starting Zulip Echo Bot...\n";
-
Printf.printf "=============================\n\n";
(* Load authentication from .zuliprc file *)
-
let auth = match Zulip.Auth.from_zuliprc () with
| Ok a ->
-
Printf.printf "Loaded authentication for: %s\n" (Zulip.Auth.email a);
-
Printf.printf "Server: %s\n\n" (Zulip.Auth.server_url a);
a
| Error e ->
-
Printf.eprintf "Failed to load .zuliprc: %s\n" (Zulip.Error.message e);
-
Printf.eprintf "\nPlease create a ~/.zuliprc file with:\n";
-
Printf.eprintf "[api]\n";
-
Printf.eprintf "email=bot@example.com\n";
-
Printf.eprintf "key=your-api-key\n";
-
Printf.eprintf "site=https://your-domain.zulipchat.com\n";
exit 1
in
Eio.Switch.run @@ fun sw ->
let client = Zulip.Client.create ~sw env auth in
(* Create bot configuration *)
let config = Bot_config.create [] in
let bot_email = Zulip.Auth.email auth in
let storage = Bot_storage.create client ~bot_email in
let identity = Bot_handler.Identity.create
~full_name:"Echo Bot"
~email:bot_email
~mention_name:"echobot"
in
(* Create and run the bot *)
let handler = Bot_handler.create
(module Echo_bot_handler)
~config ~storage ~identity
in
let runner = Bot_runner.create ~env ~client ~handler in
-
Printf.printf "Echo bot is running!\n";
-
Printf.printf "Send a direct message or mention @echobot in a channel.\n";
-
Printf.printf "Press Ctrl+C to stop.\n\n";
(* Run in real-time mode *)
-
Bot_runner.run_realtime runner
let () =
-
Eio_main.run run_echo_bot
···
+
(* Enhanced Echo Bot for Zulip with Logging and CLI
+
Responds to direct messages and mentions by echoing back the message
+
Features verbose logging and command-line configuration *)
open Zulip_bot
+
+
(* Set up logging *)
+
let src = Logs.Src.create "echo_bot" ~doc:"Zulip Echo Bot"
+
module Log = (val Logs.src_log src : Logs.LOG)
module Echo_bot_handler : Bot_handler.S = struct
+
let initialize _config =
+
Log.info (fun m -> m "Initializing echo bot handler");
+
Log.debug (fun m -> m "Bot handler initialized");
+
Ok ()
let usage () =
"Echo Bot - I repeat everything you say to me!"
···
Send me a direct message or mention me in a channel."
let handle_message ~config:_ ~storage:_ ~identity ~message ~env:_ =
+
Log.debug (fun m -> m "Received message for processing");
+
(* Parse the incoming message *)
let extract_field fields name =
match List.assoc_opt name fields with
+
| Some (`String s) ->
+
Log.debug (fun m -> m "Extracted field %s: %s" name s);
+
Some s
+
| _ ->
+
Log.debug (fun m -> m "Field %s not found or not a string" name);
+
None
in
let extract_int_field fields name =
match List.assoc_opt name fields with
+
| Some (`Float f) ->
+
let i = int_of_float f in
+
Log.debug (fun m -> m "Extracted field %s: %d" name i);
+
Some i
+
| _ ->
+
Log.debug (fun m -> m "Field %s not found or not a number" name);
+
None
in
match message with
| `O fields ->
+
Log.info (fun m -> m "Processing message with %d fields" (List.length fields));
+
(* Extract message details *)
let content = extract_field fields "content" in
let sender_email = extract_field fields "sender_email" in
let sender_full_name = extract_field fields "sender_full_name" in
let message_type = extract_field fields "type" in
+
let sender_id = extract_int_field fields "sender_id" in
+
let message_id = extract_int_field fields "id" in
+
+
(* Log message metadata *)
+
Log.info (fun m ->
+
m "Message metadata: type=%s, sender=%s (%s), id=%s"
+
(Option.value message_type ~default:"unknown")
+
(Option.value sender_full_name ~default:"unknown")
+
(Option.value sender_email ~default:"unknown")
+
(Option.fold ~none:"none" ~some:string_of_int message_id));
(* Check if this is our own message to avoid loops *)
+
let bot_email = Bot_handler.Identity.email identity in
let is_own_message = match sender_email with
+
| Some email ->
+
let is_own = email = bot_email in
+
if is_own then
+
Log.debug (fun m -> m "Ignoring own message from %s" email);
+
is_own
| None -> false
in
+
if is_own_message then (
+
Log.debug (fun m -> m "Skipping response to own message");
Ok Bot_handler.Response.None
+
) else
(* Process the message content *)
+
let response_content = match content, sender_full_name, sender_id with
+
| Some msg, Some name, Some id ->
+
Log.info (fun m -> m "Processing message from %s (ID: %d): %s" name id msg);
+
(* Remove bot mention if present *)
+
let mention = "@**" ^ Bot_handler.Identity.mention_name identity ^ "**" in
let cleaned_msg =
+
if String.starts_with ~prefix:mention msg then (
+
let cleaned = String.sub msg
+
(String.length mention)
+
(String.length msg - String.length mention) in
+
Log.debug (fun m -> m "Removed bot mention, cleaned message: %s" cleaned);
+
String.trim cleaned
+
) else (
+
Log.debug (fun m -> m "No bot mention to remove");
+
String.trim msg
+
)
in
(* Create echo response *)
+
let response =
+
if cleaned_msg = "" then
+
Printf.sprintf "Hello %s! Send me a message and I'll echo it back!" name
+
else if String.lowercase_ascii cleaned_msg = "help" then
+
Printf.sprintf "Hi %s! I'm an echo bot. Whatever you say to me, I'll repeat back. \
+
Try sending me any message!" name
+
else if String.lowercase_ascii cleaned_msg = "ping" then (
+
Log.info (fun m -> m "Responding to ping from %s" name);
+
Printf.sprintf "Pong! 🏓 (from %s)" name
+
)
+
else
+
Printf.sprintf "Echo from %s: %s" name cleaned_msg
+
in
+
+
Log.debug (fun m -> m "Generated response: %s" response);
+
response
+
+
| Some msg, None, _ ->
+
Log.warn (fun m -> m "Message without sender name: %s" msg);
Printf.sprintf "Echo: %s" msg
+
+
| None, _, _ ->
+
Log.warn (fun m -> m "Received message without content");
"I couldn't understand that message."
+
+
| _ ->
+
Log.warn (fun m -> m "Incomplete message data");
+
"I couldn't process that message properly."
in
(* Determine response type based on original message type *)
let response = match message_type with
| Some "private" ->
+
Log.info (fun m -> m "Sending private reply: %s" response_content);
Bot_handler.Response.Reply response_content
| Some "stream" ->
+
Log.info (fun m -> m "Sending stream reply: %s" response_content);
Bot_handler.Response.Reply response_content
+
| Some other ->
+
Log.warn (fun m -> m "Unknown message type: %s" other);
+
Bot_handler.Response.None
+
| None ->
+
Log.warn (fun m -> m "No message type specified");
Bot_handler.Response.None
in
Ok response
+
+
| other ->
+
Log.err (fun m -> m "Received non-object message: %s"
+
(match other with
+
| `A _ -> "array"
+
| `Bool _ -> "bool"
+
| `Float _ -> "float"
+
| `String _ -> "string"
+
| `Null -> "null"
+
| _ -> "unknown"));
Ok Bot_handler.Response.None
end
+
let run_echo_bot config_file verbosity env =
+
(* Set up logging based on verbosity *)
+
Logs.set_reporter (Logs_fmt.reporter ());
+
let log_level = match verbosity with
+
| 0 -> Logs.Info
+
| 1 -> Logs.Debug
+
| _ -> Logs.Debug (* Cap at debug level *)
+
in
+
Logs.set_level (Some log_level);
+
+
(* Also set levels for related modules if they exist *)
+
Logs.Src.set_level src (Some log_level);
+
+
Log.app (fun m -> m "Starting Zulip Echo Bot");
+
Log.app (fun m -> m "Log level: %s" (Logs.level_to_string (Some log_level)));
+
Log.app (fun m -> m "=============================\n");
(* Load authentication from .zuliprc file *)
+
let auth = match Zulip.Auth.from_zuliprc ?path:config_file () with
| Ok a ->
+
Log.info (fun m -> m "Loaded authentication for: %s" (Zulip.Auth.email a));
+
Log.info (fun m -> m "Server: %s" (Zulip.Auth.server_url a));
a
| Error e ->
+
Log.err (fun m -> m "Failed to load .zuliprc: %s" (Zulip.Error.message e));
+
Log.app (fun m -> m "\nPlease create a ~/.zuliprc file with:");
+
Log.app (fun m -> m "[api]");
+
Log.app (fun m -> m "email=bot@example.com");
+
Log.app (fun m -> m "key=your-api-key");
+
Log.app (fun m -> m "site=https://your-domain.zulipchat.com");
exit 1
in
Eio.Switch.run @@ fun sw ->
+
Log.debug (fun m -> m "Creating Zulip client");
let client = Zulip.Client.create ~sw env auth in
(* Create bot configuration *)
let config = Bot_config.create [] in
let bot_email = Zulip.Auth.email auth in
+
+
Log.debug (fun m -> m "Creating bot storage for %s" bot_email);
let storage = Bot_storage.create client ~bot_email in
+
let identity = Bot_handler.Identity.create
~full_name:"Echo Bot"
~email:bot_email
~mention_name:"echobot"
in
+
Log.info (fun m -> m "Bot identity created: %s (%s)"
+
(Bot_handler.Identity.full_name identity)
+
(Bot_handler.Identity.email identity));
(* Create and run the bot *)
+
Log.debug (fun m -> m "Creating bot handler");
let handler = Bot_handler.create
(module Echo_bot_handler)
~config ~storage ~identity
in
+
Log.debug (fun m -> m "Creating bot runner");
let runner = Bot_runner.create ~env ~client ~handler in
+
Log.app (fun m -> m "Echo bot is running!");
+
Log.app (fun m -> m "Send a direct message or mention @echobot in a channel.");
+
Log.app (fun m -> m "Commands: 'help', 'ping', or any message to echo");
+
Log.app (fun m -> m "Press Ctrl+C to stop.\n");
(* Run in real-time mode *)
+
Log.info (fun m -> m "Starting real-time event loop");
+
try
+
Bot_runner.run_realtime runner;
+
Log.info (fun m -> m "Bot runner exited normally")
+
with
+
| Sys.Break ->
+
Log.info (fun m -> m "Received interrupt signal, shutting down");
+
Bot_runner.shutdown runner
+
| exn ->
+
Log.err (fun m -> m "Bot crashed with exception: %s" (Printexc.to_string exn));
+
Log.debug (fun m -> m "Backtrace: %s" (Printexc.get_backtrace ()));
+
raise exn
+
+
(* Command-line interface *)
+
open Cmdliner
+
+
let config_file =
+
let doc = "Path to .zuliprc configuration file" in
+
Arg.(value & opt (some string) None & info ["c"; "config"] ~docv:"FILE" ~doc)
+
+
let verbosity =
+
let doc = "Increase verbosity. Use multiple times for more verbose output." in
+
Arg.(value & flag_all & info ["v"; "verbose"] ~doc)
+
+
let verbosity_term =
+
Term.(const List.length $ verbosity)
+
+
let bot_cmd env =
+
let doc = "Zulip Echo Bot with verbose logging" in
+
let man = [
+
`S Manpage.s_description;
+
`P "A simple echo bot for Zulip that responds to messages by echoing them back. \
+
Features verbose logging for debugging and development.";
+
`S "CONFIGURATION";
+
`P "The bot reads configuration from a .zuliprc file (default: ~/.zuliprc).";
+
`P "The file should contain:";
+
`Pre "[api]\n\
+
email=bot@example.com\n\
+
key=your-api-key\n\
+
site=https://your-domain.zulipchat.com";
+
`S "LOGGING";
+
`P "Use -v for info level logging, -vv for debug level logging.";
+
`P "Log messages include:";
+
`P "- Message metadata (sender, type, ID)";
+
`P "- Message processing steps";
+
`P "- Bot responses";
+
`P "- Error conditions";
+
`S "COMMANDS";
+
`P "The bot responds to:";
+
`P "- 'help' - Show usage information";
+
`P "- 'ping' - Respond with 'Pong!'";
+
`P "- Any other message - Echo it back";
+
`S Manpage.s_examples;
+
`P "Run with default configuration:";
+
`Pre " echo_bot";
+
`P "Run with verbose logging:";
+
`Pre " echo_bot -v";
+
`P "Run with debug logging:";
+
`Pre " echo_bot -vv";
+
`P "Run with custom config file:";
+
`Pre " echo_bot -c /path/to/.zuliprc";
+
`P "Run with maximum verbosity and custom config:";
+
`Pre " echo_bot -vv -c ~/my-bot.zuliprc";
+
`S Manpage.s_bugs;
+
`P "Report bugs at https://github.com/your-org/zulip-ocaml/issues";
+
`S Manpage.s_see_also;
+
`P "zulip(1), zulip-bot(1)";
+
] in
+
let info = Cmd.info "echo_bot" ~version:"1.0.0" ~doc ~man in
+
Cmd.v info Term.(const (run_echo_bot) $ config_file $ verbosity_term $ const env)
let () =
+
Eio_main.run @@ fun env ->
+
exit (Cmd.eval (bot_cmd env))