package WebService::Ollama;

use 5.006;
use strict;
use warnings;

our $VERSION = '0.07';

use Moo;

use WebService::Ollama::UA;

has base_url => (
	is => 'ro',
	required => 1,
	lazy => 1,
);

has model => (
	is => 'ro',
);

has ua => (
	is => 'ro',
	default => sub {
		return WebService::Ollama::UA->new(
			base_url => $_[0]->base_url
		);
	}
);

sub version {
	my ($self, %args) = @_;

	return $self->ua->get(url => '/api/version');
}

sub create_model {
	my ($self, %args) = @_;

	if (! $args{model}) {
		die "No model defined for create_model";
	}

	return $self->ua->post(
		url => '/api/create',
		data => \%args
	);
}

sub copy_model {
	my ($self, %args) = @_;

	if (! $args{source}) {
		die "No source defined for copy_model";
	}


	if (! $args{destination}) {
		die "No destination defined for copy_model";
	}

	return $self->ua->post(
		url => '/api/create',
		data => \%args
	);
}

sub delete_model {
	my ($self, %args) = @_;

	if (! $args{model}) {
		die "No model defined for create_model";
	}

	return $self->ua->delete(
		url => '/api/delete',
		data => \%args
	);
}


sub available_models {
	my ($self, %args) = @_;
	
	return $self->ua->get(url => '/api/tags');
}

sub running_models {
	my ($self, %args) = @_;
	
	return $self->ua->get(url => '/api/ps');
}

sub load_completion_model {
	my ($self, %args) = @_;

	$args{model} //= $self->model;

	if (! $args{model}) {
		die "No model defined for load_completion_model";
	}

	return $self->ua->post(
		url => '/api/generate',
		data => \%args
	);
}

sub unload_completion_model {
	my ($self, %args) = @_;

	$args{model} //= $self->model;

	if (! $args{model}) {
		die "No model defined for unload_completion_model";
	}

	$args{keep_alive} = 0;

	return $self->ua->post(
		url => '/api/generate',
		data => \%args
	);
}

sub completion {
	my ($self, %args) = @_;

	$args{model} //= $self->model;

	if (! $args{model}) {
		die "No model defined for completion";
	}

	if (! $args{prompt}) {
		die "No prompt defined for completion";
	}

	$args{stream} = $args{stream} ? \1 : \0;

	if ( defined $args{image_files} ) {
		$args{images} = [];
		push @{$args{images}}, @{ $self->ua->base64_images(delete $args{image_files}) };
	}

	return $self->ua->post(
		url => '/api/generate',
		data => \%args
	);
}

sub load_chat_model {
	my ($self, %args) = @_;

	$args{model} //= $self->model;

	if (! $args{model}) {
		die "No model defined for load_chat_model";
	}

	$args{messages} = [];

	return $self->ua->post(
		url => '/api/chat',
		data => \%args
	);
}

sub unload_chat_model {
	my ($self, %args) = @_;

	$args{model} //= $self->model;

	if (! $args{model}) {
		die "No model defined for unload_chat_model";
	}

	$args{keep_alive} = 0;
	$args{messages} = [];

	return $self->ua->post(
		url => '/api/generate',
		data => \%args
	);
}

sub chat {
	my ($self, %args) = @_;

	$args{model} //= $self->model;

	if (! $args{model}) {
		die "No model defined for chat";
	}

	if (! $args{messages}) {
		die "No messsages defined for chat";
	}

	$args{stream} = $args{stream} ? \1 : \0;

	for my $message (@{$args{messages}}) {
		if ($message->{image_files}) {
			$message->{images} = [];
			push @{$message->{images}}, @{ $self->ua->base64_images(delete $message->{image_files}) };
		}
	}

	return $self->ua->post(
		url => '/api/chat',
		data => \%args
	);
}

sub embed {
	my ($self, %args) = @_;

	$args{model} //= $self->model;

	if (! $args{model}) {
		die "No model defined for embed";
	}

	return $self->ua->post(
		url => '/api/embed',
		data => \%args
	);
}

1;

__END__

=head1 NAME

WebService::Ollama - ollama client

=head1 VERSION

Version 0.07

=cut

=head1 SYNOPSIS

	my $ollama = WebService::Ollama->new(
		base_url => 'http://localhost:11434',
		model => 'llama3.2'
	);

	$ollama->load_completion_model;

	my $string = "";

	my $why = $ollama->completion(
		prompt => 'Why is the sky blue?',
		stream => 1,
		stream_cb => sub {
			my ($res) = @_;
			$string .= $res->response;
		}
	); # returns all chunked responses as an array

	$ollama->unload_completion_model;

=head1 SUBROUTINES/METHODS

=head2 version

Retrieve the Ollama version

	$ollama->version;

=head2 create_model

Create a model from: another model, a safetensors directory or a GGUF file.

=head3 Parameters

=over

=item model

name of the model to create

=item from

(optional) name of an existing model to create the new model from

=item files

(optional) a dictionary of file names to SHA256 digests of blobs to create the model from

=item adapters

(optional) a dictionary of file names to SHA256 digests of blobs for LORA adapters

=item template

(optional) the prompt template for the model

=item license

(optional) a string or list of strings containing the license or licenses for the model

=item system

(optional) a string containing the system prompt for the model

=item parameters

(optional) a dictionary of parameters for the model (see Modelfile for a list of parameters)

=item messages

(optional) a list of message objects used to create a conversation

=item stream

(optional) if false the response will be returned as a single response object, rather than a stream of objects

=item stream_cb

(optional) cb to handle stream data

=item quantize

(optional) quantize a non-quantized (e.g. float16) model

=back

	$ollama->create_model(
		model => 'mario',
		from => 'llama3.2',
		system => 'You are Mario from Super Mario Bros.'
	);


	my $mario_story = $ollama->chat(
		model => 'mario',
		messages => [
			{
				role => 'user',
				content => 'Hello, Tell me a story.',
			}
		],
	);

=head2 copy_model

Copy a model. Creates a model with another name from an existing model.

=head3 Parameters

=over

=item source

source of model to be copied from.

=item destination

destination of model to be copied to.

=back

	$ollama->copy_model(
		source => 'llama3.2',
		destination => 'llama3-backup'
	);


=head2 delete_model

Delete a model and its data.

=head3 Parameters

=over

=item model

model name to delete

=back

	$ollama->delete_model(
		model => 'mario'
	);


=head2 available_models

List models that are available locally.

	$ollama->available_models;

=head2 running_models

List models that are currently loaded into memory.

	$ollama->running_models;

=head2 load_completion_model

Load a model into memory

	$ollama->load_completion_model;

	$ollama->load_completion_model(model => 'llava');

=head2 unload_completion_model

Unload a model from memory

	$ollama->unload_completion_model;

	$ollama->unload_completion_model(model => 'llava');

=head2 completion

Generate a response for a given prompt with a provided model. This is a streaming endpoint, so there will be a series of responses. The final response object will include statistics and additional data from the request.

=head3 Parameters

=over

=item model

(required) the model name

=item prompt

the prompt to generate a response for

=item suffix

the text after the model response

=item images

(optional) a list of base64-encoded images (for multimodal models such as llava)

=item image_files

(optional) a list of image files

=back

=head3 Advanced parameters (optional):

=over

=item format

the format to return a response in. Format can be json or a JSON schema

=item options

additional model parameters listed in the documentation for the Modelfile such as temperature

=item system

system message to (overrides what is defined in the Modelfile)

=item template

the prompt template to use (overrides what is defined in the Modelfile)

=item stream

if false the response will be returned as a single response object, rather than a stream of objects

=item stream_cb

(optional) cb to handle stream data

=item raw

 if true no formatting will be applied to the prompt. You may choose to use the raw parameter if you are specifying a full templated prompt in your request to the API

=item keep_alive

controls how long the model will stay loaded into memory following the request (default: 5m)

=item context (deprecated)

the context parameter returned from a previous request to /generate, this can be used to keep a short conversational memory

=back

	my $image = $ollama->completion(
		model => 'llava',
		prompt => 'What is in this image?',
		image_files => [
			"t/pingu.png"
		]
	); 

	my $json = $ollama->completion(
		prompt => "What color is the sky at different times of the day? Respond using JSON",
		format => "json",
	)->json_response;

	my $json2 = $ollama->completion(
		prompt => "Ollama is 22 years old and is busy saving the world. Respond using JSON",
		format => {
			type => "object",
			properties => {
				age => {
					"type" => "integer"
				},
				available => {
					"type" => "boolean"
				}
			},
			required => [
				"age",
				"available"
			]
		}
	)->json_response;

=head2 load_chat_model

Load a model into memory

	$ollama->load_chat_model;

	$ollama->load_chat_model(model => 'llava');


=head2 unload_chat_model

Unload a model from memory

	$ollama->unload_chat_model;

	$ollama->unload_chat_model(model => 'llava');

=head2 chat

Generate the next message in a chat with a provided model. 

=head3 Parameters

=over

=item model

(required) the model name

=item messages

the messages of the chat, this can be used to keep a chat memory

The message object has the following fields:

=over 

=item role
	
the role of the message, either system, user, assistant, or tool

=item content

the content of the message

=item images

(optional) a list of images to include in the message (for multimodal models such as llava)

=item tool_calls

(optional): a list of tools in JSON that the model wants to use

=back

=item tools

list of tools in JSON for the model to use if supported

=item format

the format to return a response in. Format can be json or a JSON schema.

=item options

additional model parameters listed in the documentation for the Modelfile such as temperature

=item stream

if false the response will be returned as a single response object, rather than a stream of objects

=item keep_alive

controls how long the model will stay loaded into memory following the request (default: 5m)

=back

	my $completion = $ollama->chat(
		messages => [
			{
				role => 'user',
				content => 'Why is the sky blue?',
			}
		],
	);


	my $image = $ollama->chat(
		model => 'llava',
		messages => [
			{
				role => 'user',
				content => 'What is in this image?',
				image_files => [
					"t/pingu.png"
				]
			}
		]
	);

	my $json = $ollama->chat(
		messages => [
			{
				role => "user",
				"content" => "Ollama is 22 years old and is busy saving the world. Respond using JSON",
			}
		],
		format => {
			type => "object",
			properties => {
				age => {
					"type" => "integer"
				},
				available => {
					"type" => "boolean"
				}
			},
			required => [
				"age",
				"available"
			]
		}
	)->json_response;


=head2 embed

Generate embeddings from a model

=head3 Parameters

=over 

=item model

name of model to generate embeddings from

=item input

text or list of text to generate embeddings for

=item truncate

(optional) truncates the end of each input to fit within context length. Returns error if false and context length is exceeded. Defaults to true

=item options

(optional) additional model parameters listed in the documentation for the Modelfile such as temperature

=item keep_alive

(optional) controls how long the model will stay loaded into memory following the request (default: 5m)

=back

	my $embeddings = $ollama->embed(
		model => "nomic-embed-text",
		input => "Why is the sky blue?"
	);

=head1 AUTHOR

LNATION, C<< <email at lnation.org> >>

=head1 BUGS

Please report any bugs or feature requests to C<bug-webservice-ollama at rt.cpan.org>, or through
the web interface at L<https://rt.cpan.org/NoAuth/ReportBug.html?Queue=WebService-Ollama>.  I will be notified, and then you'll
automatically be notified of progress on your bug as I make changes.

=head1 SUPPORT

You can find documentation for this module with the perldoc command.

    perldoc WebService::Ollama


You can also look for information at:

=over 4

=item * RT: CPAN's request tracker (report bugs here)

L<https://rt.cpan.org/NoAuth/Bugs.html?Dist=WebService-Ollama>

=item * CPAN Ratings

L<https://cpanratings.perl.org/d/WebService-Ollama>

=item * Search CPAN

L<https://metacpan.org/release/WebService-Ollama>

=back


=head1 ACKNOWLEDGEMENTS


=head1 LICENSE AND COPYRIGHT

This software is Copyright (c) 2025 by LNATION.

This is free software, licensed under:

  The Artistic License 2.0 (GPL Compatible)

=cut

1; # End of WebService::Ollama