AI-Anthropic

 view release on metacpan or  search on metacpan

lib/AI/Anthropic.pm  view on Meta::CPAN

        model       => 'claude-sonnet-4-20250514',  # optional
        max_tokens  => 4096,                 # optional
        timeout     => 120,                  # optional, seconds
    );

=cut

sub new {
    my ($class, %args) = @_;
    
    my $api_key = $args{api_key} // $ENV{ANTHROPIC_API_KEY}
        or croak "API key required. Set api_key parameter or ANTHROPIC_API_KEY environment variable";
    
    my $self = {
        api_key     => $api_key,
        model       => $args{model}      // DEFAULT_MODEL,
        max_tokens  => $args{max_tokens} // 4096,
        timeout     => $args{timeout}    // 120,
        api_base    => $args{api_base}   // API_BASE,
        _http       => HTTP::Tiny->new(
            timeout => $args{timeout} // 120,
        ),
        _json       => JSON::PP->new->utf8->allow_nonref,
    };
    
    return bless $self, $class;
}

=head2 message

Simple interface for single message:

    my $response = $claude->message("Your question here");
    my $response = $claude->message("Your question", system => "You are helpful");
    
    print $response->text;

=cut

sub message {
    my ($self, $content, %opts) = @_;
    
    croak "Message content required" unless defined $content;
    
    return $self->chat(
        messages => [ { role => 'user', content => $content } ],
        %opts,
    );
}

=head2 chat

Full chat interface:

    my $response = $claude->chat(
        messages    => \@messages,       # required
        system      => $system_prompt,   # optional
        model       => $model,           # optional, overrides default
        max_tokens  => $max_tokens,      # optional
        temperature => 0.7,              # optional, 0.0-1.0
        stream      => \&callback,       # optional, for streaming
        tools       => \@tools,          # optional, for function calling
    );

=cut

sub chat {
    my ($self, %args) = @_;
    
    my $messages = $args{messages}
        or croak "messages parameter required";
    
    # Build request body
    my $body = {
        model      => $args{model}      // $self->{model},
        max_tokens => $args{max_tokens} // $self->{max_tokens},
        messages   => $self->_normalize_messages($messages),
    };
    
    # Optional parameters
    $body->{system}      = $args{system}      if defined $args{system};
    $body->{temperature} = $args{temperature} if defined $args{temperature};
    $body->{tools}       = $args{tools}       if defined $args{tools};
    $body->{tool_choice} = $args{tool_choice} if defined $args{tool_choice};
    
    # Streaming or regular request
    if (my $stream_cb = $args{stream}) {
        return $self->_stream_request($body, $stream_cb);
    } else {
        return $self->_request($body);
    }
}

=head2 models

Returns list of available models:

    my @models = $claude->models;

=cut

sub models {
    return (
        'claude-opus-4-20250514',
        'claude-sonnet-4-20250514',
        'claude-sonnet-4-5-20250929',
        'claude-haiku-4-5-20251001',
        'claude-3-5-sonnet-20241022',
        'claude-3-5-haiku-20241022',
        'claude-3-opus-20240229',
        'claude-3-sonnet-20240229',
        'claude-3-haiku-20240307',
    );
}

# ============================================
# Private methods
# ============================================

sub _normalize_messages {
    my ($self, $messages) = @_;

lib/AI/Anthropic.pm  view on Meta::CPAN

    open my $fh, '<:raw', $path
        or croak "Cannot open image file '$path': $!";
    local $/;
    my $data = <$fh>;
    close $fh;
    
    # Detect media type
    my $media_type = 'image/png';
    if ($path =~ /\.jpe?g$/i) {
        $media_type = 'image/jpeg';
    } elsif ($path =~ /\.gif$/i) {
        $media_type = 'image/gif';
    } elsif ($path =~ /\.webp$/i) {
        $media_type = 'image/webp';
    }
    
    return {
        type   => 'image',
        source => {
            type       => 'base64',
            media_type => $media_type,
            data       => encode_base64($data, ''),
        },
    };
}

sub _image_from_url {
    my ($self, $url) = @_;
    
    return {
        type   => 'image',
        source => {
            type => 'url',
            url  => $url,
        },
    };
}

sub _request {
    my ($self, $body) = @_;
    
    my $response = $self->{_http}->post(
        $self->{api_base} . '/v1/messages',
        {
            headers => $self->_headers,
            content => $self->{_json}->encode($body),
        }
    );
    
    return $self->_handle_response($response);
}

sub _stream_request {
    my ($self, $body, $callback) = @_;
    
    $body->{stream} = \1;  # JSON true
    
    my $full_text = '';
    my $response_data;
    
    # HTTP::Tiny doesn't support streaming well, so we use a data callback
    my $response = $self->{_http}->post(
        $self->{api_base} . '/v1/messages',
        {
            headers      => $self->_headers,
            content      => $self->{_json}->encode($body),
            data_callback => sub {
                my ($chunk, $res) = @_;
                
                # Parse SSE events
                for my $line (split /\n/, $chunk) {
                    next unless $line =~ /^data: (.+)/;
                    my $data = $1;
                    next if $data eq '[DONE]';
                    
                    eval {
                        my $event = $self->{_json}->decode($data);
                        
                        if ($event->{type} eq 'content_block_delta') {
                            my $text = $event->{delta}{text} // '';
                            $full_text .= $text;
                            $callback->($text) if $callback;
                        } elsif ($event->{type} eq 'message_stop') {
                            $response_data = $event;
                        }
                    };
                }
            },
        }
    );
    
    unless ($response->{success}) {
        return $self->_handle_response($response);
    }
    
    # Return a response object with the full text
    return AI::Anthropic::Response->new(
        text         => $full_text,
        raw_response => $response_data,
    );
}

sub _headers {
    my ($self) = @_;
    
    return {
        'Content-Type'      => 'application/json',
        'x-api-key'         => $self->{api_key},
        'anthropic-version' => API_VERSION,
    };
}

sub _handle_response {
    my ($self, $response) = @_;
    
    my $data;
    eval {
        $data = $self->{_json}->decode($response->{content});
    };
    
    unless ($response->{success}) {



( run in 1.504 second using v1.01-cache-2.11-cpan-140bd7fdf52 )