File Coverage

blib/lib/WebService/Ollama.pm
Criterion Covered Total %
statement 37 149 24.8
branch 4 58 6.9
condition 8 50 16.0
subroutine 10 24 41.6
pod 16 16 100.0
total 75 297 25.2


line stmt bran cond sub pod time code
1             package WebService::Ollama;
2              
3 3     3   414353 use 5.006;
  3         14  
4 3     3   19 use strict;
  3         7  
  3         152  
5 3     3   18 use warnings;
  3         7  
  3         292  
6              
7             our $VERSION = '0.08';
8              
9 3     3   2271 use Moo;
  3         30199  
  3         27  
10 3     3   6048 use Exporter 'import';
  3         6  
  3         124  
11              
12 3     3   1883 use WebService::Ollama::UA;
  3         13  
  3         165  
13 3     3   29 use JSON::Lines;
  3         7  
  3         33  
14              
15             our @EXPORT_OK = qw(ollama);
16              
17             # Singleton instance for functional API
18             my $_instance;
19              
20             sub ollama {
21 1     1 1 259599 my ($method, @args) = @_;
22            
23             # Initialize singleton on first call
24             $_instance //= __PACKAGE__->new(
25             base_url => $ENV{OLLAMA_URL} // 'http://localhost:11434',
26 1   50     57 model => $ENV{OLLAMA_MODEL} // 'llama3.2',
      50        
      33        
27             );
28            
29             # If first arg is a hashref, it's options for new instance
30 1 50       14 if (ref($method) eq 'HASH') {
31 1         29 $_instance = __PACKAGE__->new(%$method);
32 1         21 return $_instance;
33             }
34            
35             # Call method on singleton
36 0         0 return $_instance->$method(@args);
37             }
38              
39             has json => (
40             is => 'ro',
41             default => sub {
42             JSON::Lines->new( utf8 => 1 );
43             }
44             );
45              
46             has base_url => (
47             is => 'ro',
48             required => 1,
49             lazy => 1,
50             );
51              
52             has model => (
53             is => 'ro',
54             );
55              
56             has ua => (
57             is => 'ro',
58             lazy => 1,
59             default => sub {
60             return WebService::Ollama::UA->new(
61             base_url => $_[0]->base_url
62             );
63             }
64             );
65              
66             has tools => (
67             is => 'ro',
68             default => sub { {} },
69             );
70              
71             sub register_tool {
72 4     4 1 113 my ($self, %args) = @_;
73              
74 4 50       19 my $name = $args{name} or die "Tool name required";
75 4   100     21 my $description = $args{description} // '';
76 4   100     21 my $parameters = $args{parameters} // { type => 'object', properties => {} };
77 4 50       13 my $handler = $args{handler} or die "Tool handler required";
78              
79 4         30 $self->tools->{$name} = {
80             name => $name,
81             description => $description,
82             parameters => $parameters,
83             handler => $handler,
84             };
85              
86 4         15 return $self;
87             }
88              
89             sub _format_tools_for_api {
90 2     2   1088 my ($self, $tools) = @_;
91              
92 2   33     19 $tools //= $self->tools;
93 2 50       8 return [] unless keys %$tools;
94              
95             return [
96             map {
97 2         8 {
98             type => 'function',
99             function => {
100             name => $_->{name},
101             description => $_->{description},
102             parameters => $_->{parameters},
103             },
104             }
105 3         23 } values %$tools
106             ];
107             }
108              
109             sub version {
110 0     0 1   my ($self, %args) = @_;
111              
112 0           return $self->ua->get(url => '/api/version');
113             }
114              
115             sub create_model {
116 0     0 1   my ($self, %args) = @_;
117              
118 0 0         if (! $args{model}) {
119 0           die "No model defined for create_model";
120             }
121              
122 0           return $self->ua->post(
123             url => '/api/create',
124             data => \%args
125             );
126             }
127              
128             sub copy_model {
129 0     0 1   my ($self, %args) = @_;
130              
131 0 0         if (! $args{source}) {
132 0           die "No source defined for copy_model";
133             }
134              
135              
136 0 0         if (! $args{destination}) {
137 0           die "No destination defined for copy_model";
138             }
139              
140 0           return $self->ua->post(
141             url => '/api/create',
142             data => \%args
143             );
144             }
145              
146             sub delete_model {
147 0     0 1   my ($self, %args) = @_;
148              
149 0 0         if (! $args{model}) {
150 0           die "No model defined for create_model";
151             }
152              
153 0           return $self->ua->delete(
154             url => '/api/delete',
155             data => \%args
156             );
157             }
158              
159              
160             sub available_models {
161 0     0 1   my ($self, %args) = @_;
162            
163 0           return $self->ua->get(url => '/api/tags');
164             }
165              
166             sub running_models {
167 0     0 1   my ($self, %args) = @_;
168            
169 0           return $self->ua->get(url => '/api/ps');
170             }
171              
172             sub load_completion_model {
173 0     0 1   my ($self, %args) = @_;
174              
175 0   0       $args{model} //= $self->model;
176              
177 0 0         if (! $args{model}) {
178 0           die "No model defined for load_completion_model";
179             }
180              
181 0           return $self->ua->post(
182             url => '/api/generate',
183             data => \%args
184             );
185             }
186              
187             sub unload_completion_model {
188 0     0 1   my ($self, %args) = @_;
189              
190 0   0       $args{model} //= $self->model;
191              
192 0 0         if (! $args{model}) {
193 0           die "No model defined for unload_completion_model";
194             }
195              
196 0           $args{keep_alive} = 0;
197              
198 0           return $self->ua->post(
199             url => '/api/generate',
200             data => \%args
201             );
202             }
203              
204             sub completion {
205 0     0 1   my ($self, %args) = @_;
206              
207 0   0       $args{model} //= $self->model;
208              
209 0 0         if (! $args{model}) {
210 0           die "No model defined for completion";
211             }
212              
213 0 0         if (! $args{prompt}) {
214 0           die "No prompt defined for completion";
215             }
216              
217 0 0         $args{stream} = $args{stream} ? \1 : \0;
218              
219 0 0         if ( defined $args{image_files} ) {
220 0           $args{images} = [];
221 0           push @{$args{images}}, @{ $self->ua->base64_images(delete $args{image_files}) };
  0            
  0            
222             }
223              
224 0           return $self->ua->post(
225             url => '/api/generate',
226             data => \%args
227             );
228             }
229              
230             sub load_chat_model {
231 0     0 1   my ($self, %args) = @_;
232              
233 0   0       $args{model} //= $self->model;
234              
235 0 0         if (! $args{model}) {
236 0           die "No model defined for load_chat_model";
237             }
238              
239 0           $args{messages} = [];
240              
241 0           return $self->ua->post(
242             url => '/api/chat',
243             data => \%args
244             );
245             }
246              
247             sub unload_chat_model {
248 0     0 1   my ($self, %args) = @_;
249              
250 0   0       $args{model} //= $self->model;
251              
252 0 0         if (! $args{model}) {
253 0           die "No model defined for unload_chat_model";
254             }
255              
256 0           $args{keep_alive} = 0;
257 0           $args{messages} = [];
258              
259 0           return $self->ua->post(
260             url => '/api/generate',
261             data => \%args
262             );
263             }
264              
265             sub chat {
266 0     0 1   my ($self, %args) = @_;
267              
268 0   0       $args{model} //= $self->model;
269              
270 0 0         if (! $args{model}) {
271 0           die "No model defined for chat";
272             }
273              
274 0 0         if (! $args{messages}) {
275 0           die "No messsages defined for chat";
276             }
277              
278 0 0         $args{stream} = $args{stream} ? \1 : \0;
279              
280 0           for my $message (@{$args{messages}}) {
  0            
281 0 0         if ($message->{image_files}) {
282 0           $message->{images} = [];
283 0           push @{$message->{images}}, @{ $self->ua->base64_images(delete $message->{image_files}) };
  0            
  0            
284             }
285             }
286              
287 0           return $self->ua->post(
288             url => '/api/chat',
289             data => \%args
290             );
291             }
292              
293             sub embed {
294 0     0 1   my ($self, %args) = @_;
295              
296 0   0       $args{model} //= $self->model;
297              
298 0 0         if (! $args{model}) {
299 0           die "No model defined for embed";
300             }
301              
302 0           return $self->ua->post(
303             url => '/api/embed',
304             data => \%args
305             );
306             }
307              
308             # ============================================
309             # TOOL EXECUTION
310             # ============================================
311              
312             sub chat_with_tools {
313 0     0 1   my ($self, %args) = @_;
314              
315 0   0       $args{model} //= $self->model;
316              
317 0 0         if (! $args{model}) {
318 0           die "No model defined for chat_with_tools";
319             }
320              
321 0 0         if (! $args{messages}) {
322 0           die "No messages defined for chat_with_tools";
323             }
324              
325 0   0       my $max_iterations = $args{max_iterations} // 10;
326 0           my @messages = @{$args{messages}};
  0            
327 0           my @all_responses;
328              
329             # Add tools to request
330 0   0       my $tools = $args{tools} // $self->_format_tools_for_api;
331              
332 0           for my $iteration (1 .. $max_iterations) {
333             my $response = $self->chat(
334             model => $args{model},
335 0           messages => \@messages,
336             tools => $tools,
337             stream => 0,
338             );
339              
340 0           push @all_responses, $response;
341              
342             # Check if model wants to call tools
343 0           my $tool_calls = $response->extract_tool_calls;
344              
345 0 0         if (!@$tool_calls) {
346             # No tool calls - return final response
347 0           last;
348             }
349              
350             # Add assistant message with tool calls
351             push @messages, {
352             role => 'assistant',
353 0   0       content => $response->message->{content} // '',
354             tool_calls => $tool_calls,
355             };
356              
357             # Execute each tool and add results
358 0           for my $tool_call (@$tool_calls) {
359 0           my $function = $tool_call->{function};
360 0           my $tool_name = $function->{name};
361 0           my $tool_args = $function->{arguments};
362              
363             # Parse arguments if string
364 0 0         if (!ref($tool_args)) {
365 0   0       $tool_args = eval { $self->json->decode($tool_args)->[0] } // {};
  0            
366             }
367              
368 0           my $result;
369 0 0         if (my $handler = $self->tools->{$tool_name}{handler}) {
370 0           $result = eval { $handler->($tool_args) };
  0            
371 0 0         if ($@) {
372 0           $result = "Error executing tool: $@";
373             }
374             } else {
375 0           $result = "Unknown tool: $tool_name";
376             }
377              
378             # Add tool result message
379             push @messages, {
380             role => 'tool',
381             content => ref($result) ? $self->json->encode([$result]) : "$result",
382 0 0 0       tool_call_id => $tool_call->{id} // $tool_name,
383             };
384             }
385             }
386              
387             # Return last response (or all if needed)
388 0 0         return wantarray ? @all_responses : $all_responses[-1];
389             }
390              
391             1;
392              
393             __END__