Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1441 ariadna 1
<?php
2
// This file is part of Moodle - http://moodle.org/
3
//
4
// Moodle is free software: you can redistribute it and/or modify
5
// it under the terms of the GNU General Public License as published by
6
// the Free Software Foundation, either version 3 of the License, or
7
// (at your option) any later version.
8
//
9
// Moodle is distributed in the hope that it will be useful,
10
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
// GNU General Public License for more details.
13
//
14
// You should have received a copy of the GNU General Public License
15
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
16
 
17
namespace aiprovider_ollama;
18
 
19
use core_ai\aiactions\base;
20
use core_ai\provider;
21
use GuzzleHttp\Psr7\Response;
22
 
23
defined('MOODLE_INTERNAL') || die();
24
require_once(__DIR__ . '/testcase_helper_trait.php');
25
 
26
/**
27
 * Test Generate text provider class for Ollama provider methods.
28
 *
29
 * @package    aiprovider_ollama
30
 * @copyright  2024 Matt Porritt <matt.porritt@moodle.com>
31
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
32
 * @covers     \aiprovider_ollama\provider
33
 * @covers     \aiprovider_ollama\process_summarise_text
34
 * @covers     \aiprovider_ollama\abstract_processor
35
 */
36
final class process_summarise_text_test extends \advanced_testcase {
37
 
38
    use testcase_helper_trait;
39
 
40
    /** @var string A successful response in JSON format. */
41
    protected string $responsebodyjson;
42
 
43
    /** @var \core_ai\manager AI Manager. */
44
    private $manager;
45
 
46
    /** @var provider The provider that will process the action. */
47
    protected provider $provider;
48
 
49
    /** @var base The action to process. */
50
    protected base $action;
51
 
52
    /**
53
     * Set up the test.
54
     */
55
    protected function setUp(): void {
56
        parent::setUp();
57
        $this->resetAfterTest();
58
        // Load a response body from a file.
59
        $this->responsebodyjson = file_get_contents(self::get_fixture_path('aiprovider_ollama', 'text_request_success.json'));
60
        $this->manager = \core\di::get(\core_ai\manager::class);
61
        $this->provider = $this->create_provider(
62
            actionclass: \core_ai\aiactions\summarise_text::class,
63
            actionconfig: [
64
                'systeminstruction' => get_string('action_summarise_text_instruction', 'core_ai'),
65
            ],
66
        );
67
        $this->create_action();
68
    }
69
 
70
    /**
71
     * Create the action object.
72
     * @param int $userid The user id to use in the action.
73
     */
74
    private function create_action(int $userid = 1): void {
75
        $this->action = new \core_ai\aiactions\summarise_text(
76
            contextid: 1,
77
            userid: $userid,
78
            prompttext: 'This is a test prompt',
79
        );
80
    }
81
 
82
    /**
83
     * Test create_request_object
84
     */
85
    public function test_create_request_object(): void {
86
        $processor = new process_summarise_text($this->provider, $this->action);
87
 
88
        // We're working with a private method here, so we need to use reflection.
89
        $method = new \ReflectionMethod($processor, 'create_request_object');
90
        $request = $method->invoke($processor, 1);
91
 
92
        $body = (object) json_decode($request->getBody()->getContents());
93
 
94
        $this->assertEquals('This is a test prompt', $body->prompt);
95
        $this->assertEquals('llama3.2', $body->model);
96
    }
97
 
98
    /**
99
     * Test create_request_object with extra model settings.
100
     */
101
    public function test_create_request_object_with_model_settings(): void {
102
        $this->provider = $this->create_provider(
103
            actionclass: \core_ai\aiactions\summarise_text::class,
104
            actionconfig: [
105
                'systeminstruction' => get_string('action_summarise_text_instruction', 'core_ai'),
106
                'temperature' => '0.5',
107
                'mirostat' => '1',
108
                'seed' => '50',
109
            ],
110
        );
111
        $processor = new process_summarise_text($this->provider, $this->action);
112
 
113
        // We're working with a private method here, so we need to use reflection.
114
        $method = new \ReflectionMethod($processor, 'create_request_object');
115
        $request = $method->invoke($processor, 1);
116
 
117
        $body = (object) json_decode($request->getBody()->getContents());
118
 
119
        $this->assertEquals('llama3.2', $body->model);
120
        $this->assertEquals('0.5', $body->options->temperature);
121
        $this->assertEquals('1', $body->options->mirostat);
122
        $this->assertEquals('50', $body->options->seed);
123
 
124
        $this->provider = $this->create_provider(
125
            actionclass: \core_ai\aiactions\summarise_text::class,
126
            actionconfig: [
127
                'model' => 'my-custom-ollama',
128
                'systeminstruction' => get_string('action_summarise_text_instruction', 'core_ai'),
129
                'modelextraparams' => '{"temperature": 0.5,"mirostat": 1,"seed": "50"}',
130
            ],
131
        );
132
        $processor = new process_summarise_text($this->provider, $this->action);
133
 
134
        // We're working with a private method here, so we need to use reflection.
135
        $method = new \ReflectionMethod($processor, 'create_request_object');
136
        $request = $method->invoke($processor, 1);
137
 
138
        $body = (object) json_decode($request->getBody()->getContents());
139
 
140
        $this->assertEquals('my-custom-ollama', $body->model);
141
        $this->assertEquals('0.5', $body->options->temperature);
142
        $this->assertEquals('1', $body->options->mirostat);
143
        $this->assertEquals('50', $body->options->seed);
144
    }
145
 
146
    /**
147
     * Test the API error response handler method.
148
     *
149
     */
150
    public function test_handle_api_error(): void {
151
        $responses = [
152
            500 => new Response(500, ['Content-Type' => 'application/json']),
153
            503 => new Response(503, ['Content-Type' => 'application/json']),
154
            401 => new Response(401, ['Content-Type' => 'application/json'],
155
                '{"error": {"message": "Invalid Authentication"}}'),
156
            404 => new Response(404, ['Content-Type' => 'application/json'],
157
                '{"error": {"message": "You must be a member of an organization to use the API"}}'),
158
            429 => new Response(429, ['Content-Type' => 'application/json'],
159
                '{"error": {"message": "Rate limit reached for requests"}}'),
160
        ];
161
 
162
        $processor = new process_summarise_text($this->provider, $this->action);
163
        $method = new \ReflectionMethod($processor, 'handle_api_error');
164
 
165
        foreach ($responses as $status => $response) {
166
            $result = $method->invoke($processor, $response);
167
            $this->assertEquals($status, $result['errorcode']);
168
            if ($status == 500) {
169
                $this->assertEquals('Internal Server Error', $result['errormessage']);
170
            } else if ($status == 503) {
171
                $this->assertEquals('Service Unavailable', $result['errormessage']);
172
            } else {
173
                $this->assertStringContainsString($response->getBody()->getContents(), $result['errormessage']);
174
            }
175
        }
176
    }
177
 
178
    /**
179
     * Test the API success response handler method.
180
     */
181
    public function test_handle_api_success(): void {
182
        $response = new Response(
183
            200,
184
            ['Content-Type' => 'application/json'],
185
            $this->responsebodyjson,
186
        );
187
 
188
        // We're testing a private method, so we need to set up reflector magic.
189
        $processor = new process_summarise_text($this->provider, $this->action);
190
        $method = new \ReflectionMethod($processor, 'handle_api_success');
191
 
192
        $result = $method->invoke($processor, $response);
193
 
194
        $this->assertTrue($result['success']);
195
        $this->assertStringContainsString('World War II was a global conflict', $result['generatedcontent']);
196
        $this->assertEquals('stop', $result['finishreason']);
197
        $this->assertEquals('75', $result['prompttokens']);
198
        $this->assertEquals('123', $result['completiontokens']);
199
    }
200
 
201
    /**
202
     * Test query_ai_api for a successful call.
203
     */
204
    public function test_query_ai_api_success(): void {
205
        // Mock the http client to return a successful response.
206
        ['mock' => $mock] = $this->get_mocked_http_client();
207
 
208
        // The response from Ollama.
209
        $mock->append(new Response(
210
            200,
211
            ['Content-Type' => 'application/json'],
212
            $this->responsebodyjson,
213
        ));
214
 
215
        $processor = new process_summarise_text($this->provider, $this->action);
216
        $method = new \ReflectionMethod($processor, 'query_ai_api');
217
        $result = $method->invoke($processor);
218
 
219
        $this->assertTrue($result['success']);
220
        $this->assertStringContainsString('World War II was a global conflict', $result['generatedcontent']);
221
        $this->assertEquals('stop', $result['finishreason']);
222
        $this->assertEquals('75', $result['prompttokens']);
223
        $this->assertEquals('123', $result['completiontokens']);
224
    }
225
 
226
    /**
227
     * Test prepare_response success.
228
     */
229
    public function test_prepare_response_success(): void {
230
        $processor = new process_summarise_text($this->provider, $this->action);
231
 
232
        // We're working with a private method here, so we need to use reflection.
233
        $method = new \ReflectionMethod($processor, 'prepare_response');
234
 
235
        $response = [
236
            'success' => true,
237
            'id' => 'chatcmpl-9lkwPWOIiQEvI3nfcGofJcmS5lPYo',
238
            'fingerprint' => 'fp_c4e5b6fa31',
239
            'generatedcontent' => 'Sure, here is some sample text',
240
            'finishreason' => 'stop',
241
            'prompttokens' => '11',
242
            'completiontokens' => '568',
243
        ];
244
 
245
        $result = $method->invoke($processor, $response);
246
 
247
        $this->assertInstanceOf(\core_ai\aiactions\responses\response_base::class, $result);
248
        $this->assertTrue($result->get_success());
249
        $this->assertEquals('summarise_text', $result->get_actionname());
250
        $this->assertEquals($response['success'], $result->get_success());
251
        $this->assertEquals($response['generatedcontent'], $result->get_response_data()['generatedcontent']);
252
    }
253
 
254
    /**
255
     * Test prepare_response error.
256
     */
257
    public function test_prepare_response_error(): void {
258
        $processor = new process_summarise_text($this->provider, $this->action);
259
 
260
        // We're working with a private method here, so we need to use reflection.
261
        $method = new \ReflectionMethod($processor, 'prepare_response');
262
 
263
        $response = [
264
            'success' => false,
265
            'errorcode' => 500,
266
            'errormessage' => 'Internal server error.',
267
        ];
268
 
269
        $result = $method->invoke($processor, $response);
270
 
271
        $this->assertInstanceOf(\core_ai\aiactions\responses\response_base::class, $result);
272
        $this->assertFalse($result->get_success());
273
        $this->assertEquals('summarise_text', $result->get_actionname());
274
        $this->assertEquals($response['errorcode'], $result->get_errorcode());
275
        $this->assertEquals($response['errormessage'], $result->get_errormessage());
276
    }
277
 
278
    /**
279
     * Test process method.
280
     */
281
    public function test_process(): void {
282
        $this->resetAfterTest();
283
        // Log in user.
284
        $this->setUser($this->getDataGenerator()->create_user());
285
 
286
        // Mock the http client to return a successful response.
287
        ['mock' => $mock] = $this->get_mocked_http_client();
288
 
289
        // The response from Ollama.
290
        $mock->append(new Response(
291
            200,
292
            ['Content-Type' => 'application/json'],
293
            $this->responsebodyjson,
294
        ));
295
 
296
        $processor = new process_summarise_text($this->provider, $this->action);
297
        $result = $processor->process();
298
 
299
        $this->assertInstanceOf(\core_ai\aiactions\responses\response_base::class, $result);
300
        $this->assertTrue($result->get_success());
301
        $this->assertEquals('summarise_text', $result->get_actionname());
302
    }
303
 
304
    /**
305
     * Test process method with error.
306
     */
307
    public function test_process_error(): void {
308
        $this->resetAfterTest();
309
        // Log in user.
310
        $this->setUser($this->getDataGenerator()->create_user());
311
 
312
        // Mock the http client to return a successful response.
313
        ['mock' => $mock] = $this->get_mocked_http_client();
314
 
315
        // The response from Ollama.
316
        $mock->append(new Response(
317
            401,
318
            ['Content-Type' => 'application/json'],
319
            json_encode(['error' => ['message' => 'Invalid Authentication']]),
320
        ));
321
 
322
        $processor = new process_summarise_text($this->provider, $this->action);
323
        $result = $processor->process();
324
 
325
        $this->assertInstanceOf(\core_ai\aiactions\responses\response_base::class, $result);
326
        $this->assertFalse($result->get_success());
327
        $this->assertEquals('summarise_text', $result->get_actionname());
328
        $this->assertEquals(401, $result->get_errorcode());
329
        $this->assertEquals('Invalid Authentication', $result->get_errormessage());
330
    }
331
 
332
    /**
333
     * Test process method with user rate limiter.
334
     */
335
    public function test_process_with_user_rate_limiter(): void {
336
        $this->resetAfterTest();
337
        // Create users.
338
        $user1 = $this->getDataGenerator()->create_user();
339
        $user2 = $this->getDataGenerator()->create_user();
340
        // Log in user1.
341
        $this->setUser($user1);
342
        // Mock clock.
343
        $clock = $this->mock_clock_with_frozen();
344
 
345
        // Set the user rate limiter.
346
        $config = [
347
            'enableuserratelimit' => true,
348
            'userratelimit' => 1,
349
            'endpoint' => "http://localhost:11434/",
350
        ];
351
 
352
        $provider = $this->manager->create_provider_instance(
353
            classname: '\aiprovider_ollama\provider',
354
            name: 'dummy',
355
            config: $config,
356
            actionconfig: [
357
                \core_ai\aiactions\summarise_text::class => [
358
                    'settings' => [
359
                        'model' => 'llama3.2',
360
                        'systeminstruction' => get_string('action_summarise_text_instruction', 'core_ai'),
361
                    ],
362
                ],
363
            ],
364
        );
365
 
366
        // Mock the http client to return a successful response.
367
        ['mock' => $mock] = $this->get_mocked_http_client();
368
 
369
        // Case 1: User rate limit has not been reached.
370
        $this->create_action($user1->id);
371
        // The response from Ollama.
372
        $mock->append(new Response(
373
            200,
374
            ['Content-Type' => 'application/json'],
375
            $this->responsebodyjson,
376
        ));
377
        $processor = new process_summarise_text($provider, $this->action);
378
        $result = $processor->process();
379
        $this->assertTrue($result->get_success());
380
 
381
        // Case 2: User rate limit has been reached.
382
        $clock->bump(HOURSECS - 10);
383
        // The response from Ollama.
384
        $mock->append(new Response(
385
            200,
386
            ['Content-Type' => 'application/json'],
387
            $this->responsebodyjson,
388
        ));
389
        $this->create_action($user1->id);
390
        $processor = new process_summarise_text($provider, $this->action);
391
        $result = $processor->process();
392
        $this->assertEquals(429, $result->get_errorcode());
393
        $this->assertEquals('User rate limit exceeded', $result->get_errormessage());
394
        $this->assertFalse($result->get_success());
395
 
396
        // Case 3: User rate limit has not been reached for a different user.
397
        // Log in user2.
398
        $this->setUser($user2);
399
        $this->create_action($user2->id);
400
        // The response from Ollama.
401
        $mock->append(new Response(
402
            200,
403
            ['Content-Type' => 'application/json'],
404
            $this->responsebodyjson,
405
        ));
406
        $processor = new process_summarise_text($provider, $this->action);
407
        $result = $processor->process();
408
        $this->assertTrue($result->get_success());
409
 
410
        // Case 4: Time window has passed, user rate limit should be reset.
411
        $clock->bump(11);
412
        // Log in user1.
413
        $this->setUser($user1);
414
        // The response from Ollama.
415
        $mock->append(new Response(
416
            200,
417
            ['Content-Type' => 'application/json'],
418
            $this->responsebodyjson,
419
        ));
420
        $this->create_action($user1->id);
421
        $processor = new process_summarise_text($provider, $this->action);
422
        $result = $processor->process();
423
        $this->assertTrue($result->get_success());
424
    }
425
 
426
    /**
427
     * Test process method with global rate limiter.
428
     */
429
    public function test_process_with_global_rate_limiter(): void {
430
        $this->resetAfterTest();
431
        // Create users.
432
        $user1 = $this->getDataGenerator()->create_user();
433
        $user2 = $this->getDataGenerator()->create_user();
434
        // Log in user1.
435
        $this->setUser($user1);
436
        // Mock clock.
437
        $clock = $this->mock_clock_with_frozen();
438
 
439
        // Set the global rate limiter.
440
        $config = [
441
            'enableglobalratelimit' => true,
442
            'globalratelimit' => 1,
443
            'endpoint' => "http://localhost:11434/",
444
        ];
445
 
446
        $provider = $this->manager->create_provider_instance(
447
            classname: '\aiprovider_ollama\provider',
448
            name: 'dummy',
449
            config: $config,
450
            actionconfig: [
451
                \core_ai\aiactions\summarise_text::class => [
452
                    'settings' => [
453
                        'model' => 'llama3.2',
454
                        'systeminstruction' => get_string('action_summarise_text_instruction', 'core_ai'),
455
                    ],
456
                ],
457
            ],
458
        );
459
 
460
        // Mock the http client to return a successful response.
461
        ['mock' => $mock] = $this->get_mocked_http_client();
462
 
463
        // Case 1: Global rate limit has not been reached.
464
        $this->create_action($user1->id);
465
        // The response from Ollama.
466
        $mock->append(new Response(
467
            200,
468
            ['Content-Type' => 'application/json'],
469
            $this->responsebodyjson,
470
        ));
471
        $processor = new process_summarise_text($provider, $this->action);
472
        $result = $processor->process();
473
        $this->assertTrue($result->get_success());
474
 
475
        // Case 2: Global rate limit has been reached.
476
        $clock->bump(HOURSECS - 10);
477
        // The response from Ollama.
478
        $mock->append(new Response(
479
            200,
480
            ['Content-Type' => 'application/json'],
481
            $this->responsebodyjson,
482
        ));
483
        $this->create_action($user1->id);
484
        $processor = new process_summarise_text($provider, $this->action);
485
        $result = $processor->process();
486
        $this->assertEquals(429, $result->get_errorcode());
487
        $this->assertEquals('Global rate limit exceeded', $result->get_errormessage());
488
        $this->assertFalse($result->get_success());
489
 
490
        // Case 3: Global rate limit has been reached for a different user too.
491
        // Log in user2.
492
        $this->setUser($user2);
493
        $this->create_action($user2->id);
494
        // The response from Ollama.
495
        $mock->append(new Response(
496
            200,
497
            ['Content-Type' => 'application/json'],
498
            $this->responsebodyjson,
499
        ));
500
        $processor = new process_summarise_text($provider, $this->action);
501
        $result = $processor->process();
502
        $this->assertFalse($result->get_success());
503
 
504
        // Case 4: Time window has passed, global rate limit should be reset.
505
        $clock->bump(11);
506
        // Log in user1.
507
        $this->setUser($user1);
508
        // The response from Ollama.
509
        $mock->append(new Response(
510
            200,
511
            ['Content-Type' => 'application/json'],
512
            $this->responsebodyjson,
513
        ));
514
        $this->create_action($user1->id);
515
        $processor = new process_summarise_text($provider, $this->action);
516
        $result = $processor->process();
517
        $this->assertTrue($result->get_success());
518
    }
519
}