Adam

 view release on metacpan or  search on metacpan

ex/ai-bot.pl  view on Meta::CPAN

# Environment:
#   ENGINE=Groq                 Engine class (default: Groq)
#   MODEL=llama-3.3-70b-versatile  Model name
#   API_KEY=gsk_...             API key (or LANGERTHA_<ENGINE>_API_KEY)
#   IRC_SERVER=irc.perl.org     IRC server (default: irc.perl.org)
#   IRC_NICKNAME=Bert           Bot nickname (default: random from a fun list)
#   OWNER=Getty                 Bot owner name for personality (default: $USER)
#   IRC_CHANNELS=#ai            Channels to join
#   DB_FILE=ai-bot.db           SQLite database path
#   MAX_LINE_LENGTH=400         Max IRC line length (default: 400)
#   BUFFER_DELAY=1.5            Seconds to buffer messages before processing (default: 1.5)
#   LINE_DELAY=1.5              Delay between outgoing IRC lines (default: 1.5)
#   IDLE_PING=1800              Seconds of silence before idle ping (default: 1800)
#   SYSTEM_PROMPT=...           Additional text appended to the system prompt

use strict;
use warnings;

my @BOT_NAMES = qw(
  Botsworth Clanky Sparky Fizz Gizmo Pixel Blip Rusty Ziggy Turbo
  Sprocket Widget Noodle Bleep Chomp Dingle Wobble Clunk Zippy Quirk

ex/ai-bot.pl  view on Meta::CPAN

has _mcp => ( is => 'rw', traits => ['NoGetopt'] );
has _raider => ( is => 'rw', traits => ['NoGetopt'] );
has _msg_buffer => (
  is => 'rw', traits => ['NoGetopt'],
  default => sub { {} },  # { channel => [messages] }
);
has _buffer_timers => (
  is => 'rw', traits => ['NoGetopt'],
  default => sub { {} },  # { channel => alarm_id }
);
has _processing => (
  is => 'rw', traits => ['NoGetopt'],
  default => 0,
);
has _pending_raid => (
  is => 'rw', traits => ['NoGetopt'],
  default => sub { undef },
);
has _rate_limit_wait => (
  is => 'rw', traits => ['NoGetopt'],
  default => 0,

ex/ai-bot.pl  view on Meta::CPAN

  return ref $channels ? $channels->[0] : $channels;
}

sub _buffer_message {
  my ($self, $channel, $nick, $msg) = @_;
  push @{$self->_msg_buffer->{$channel} ||= []}, { channel => $channel, nick => $nick, msg => $msg };
  # Per-channel timer: cancel previous, set new
  if (my $id = delete $self->_buffer_timers->{$channel}) {
    POE::Kernel->alarm_remove($id);
  }
  my $id = POE::Kernel->alarm_set( _process_buffer => time() + $BUFFER_DELAY, $channel );
  $self->_buffer_timers->{$channel} = $id;
}

event _process_buffer => sub {
  my ($self, $channel) = @_[OBJECT, ARG0];
  delete $self->_buffer_timers->{$channel};

  return if $self->_processing;
  my @messages = @{$self->_msg_buffer->{$channel} || []};
  return unless @messages;

  $self->_msg_buffer->{$channel} = [];
  $self->_processing(1);

  # Auto-recall: gather notes about active nicks
  my %seen_nicks;
  for my $m (@messages) {
    next if $m->{nick} eq 'system';
    $seen_nicks{$m->{nick}} = 1;
  }
  # Extract nicks mentioned in system messages (joins, PMs, etc.)
  for my $m (grep { $_->{nick} eq 'system' } @messages) {
    if ($m->{msg} =~ /^(\S+)\s+\(/) {

ex/ai-bot.pl  view on Meta::CPAN


  $self->_pending_raid({ input => $input, channel => $channel, messages => \@messages });
  $self->_do_raid;
};

sub _schedule_pending_buffers {
  my ($self) = @_;
  for my $ch (keys %{$self->_msg_buffer}) {
    next unless @{$self->_msg_buffer->{$ch} || []};
    next if $self->_buffer_timers->{$ch};  # already scheduled
    my $id = POE::Kernel->alarm_set( _process_buffer => time() + $BUFFER_DELAY, $ch );
    $self->_buffer_timers->{$ch} = $id;
  }
}

my @BRAINFREEZE = (
  '*brainfreeze*',
  '*buffering...*',
  '*hamster needs a breather*',
  '*neurons recharging*',
  '*getty forgot to pay the electricity bill again*',

ex/ai-bot.pl  view on Meta::CPAN


  # Reset rate limit state
  $self->_rate_limit_wait(0);
  $self->_pending_raid(undef);

  if ($@) {
    $self->error("Raider error: $@");
    # Show error only in main channel
    $self->_send_to_channel($self->_default_channel,
      "Something broke in my brain. Getty probably forgot to feed the hamster that powers my GPU.");
    $self->_processing(0);
    $self->_schedule_pending_buffers;
    return;
  }

  # Log rate limit info
  eval {
    my $engine = $self->_raider->active_engine;
    if ($engine->has_rate_limit) {
      my $rl = $engine->rate_limit;
      $self->info(sprintf "Rate limit: %s requests remaining, %s tokens remaining",
        $rl->requests_remaining // '?', $rl->tokens_remaining // '?');
    }
  };

  $self->_processing(0);

  # Check for silence
  if ($answer =~ /__SILENT__/) {
    $self->info("Bert chose to stay silent");
    $self->_schedule_pending_buffers;
    return;
  }

  # Clean up AI output
  $answer =~ s/^<\s*\@?\s*(\w+)\s*>:?\s*/$1: /mg;     # line start <@nick> → Nick:

ex/ai-bot.pl  view on Meta::CPAN

event _alarm_fired => sub {
  my ( $self, $channel, $reason ) = @_[ OBJECT, ARG0, ARG1 ];
  $self->info("Alarm fired: $reason");
  $self->_buffer_message($channel, 'system',
    "ALARM FIRED: $reason — You set this alarm earlier. Decide what to do now.");
};

event _idle_check => sub {
  my ($self) = $_[OBJECT];
  my $idle_secs = time() - $self->_last_activity;
  if ($idle_secs >= $IDLE_PING && !$self->_processing) {
    my $idle_mins = int($idle_secs / 60);
    $self->info("Idle ping after ${idle_mins}m");
    # Ping first channel only (idle is a global concept)
    my $channel = $self->_default_channel;
    $self->_buffer_message($channel, 'system',
      "No activity for $idle_mins minutes. You can say something if you want, or stay_silent.");
  }
  POE::Kernel->delay( _idle_check => $IDLE_PING );
};

lib/Moses.pm  view on Meta::CPAN

    # Run with POE (default)
    __PACKAGE__->run unless caller;

    # Or run with IO::Async (requires IO::Async::Loop::POE)
    # __PACKAGE__->async unless caller;

=head1 DESCRIPTION

Moses is declarative sugar for building IRC bots based on the L<Adam> IRC bot
framework. Moses is designed to minimize the amount of work you have to do to
make an IRC bot functional, and to make the process as declarative as possible.

Bots can run in two modes: the default L<POE> event loop via C<run()>, or an
L<IO::Async> mode via C<async()> that enables integration with
L<IO::Async>-based components such as L<Net::Async::MCP> or
L<Net::Async::HTTP>. The async mode requires L<IO::Async::Loop::POE>.

=head2 nickname

    nickname 'sample-bot';



( run in 0.941 second using v1.01-cache-2.11-cpan-39bf76dae61 )