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 )