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_generate_text
34
 * @covers     \aiprovider_ollama\abstract_processor
35
 */
36
final class process_generate_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\generate_text::class,
63
            actionconfig: [
64
                'systeminstruction' => get_string('action_generate_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\generate_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_generate_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\generate_text::class,
104
            actionconfig: [
105
                'systeminstruction' => get_string('action_generate_text_instruction', 'core_ai'),
106
                'temperature' => '0.5',
107
                'mirostat' => '1',
108
                'seed' => '50',
109
            ],
110
        );
111
        $processor = new process_generate_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\generate_text::class,
126
            actionconfig: [
127
                'model' => 'my-custom-ollama',
128
                'systeminstruction' => get_string('action_generate_text_instruction', 'core_ai'),
129
                'modelextraparams' => '{"temperature": 0.5,"mirostat": 1,"seed": "50"}',
130
            ],
131
        );
132
        $processor = new process_generate_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
    public function test_handle_api_error(): void {
150
        $responses = [
151
            500 => new Response(500, ['Content-Type' => 'application/json']),
152
            503 => new Response(503, ['Content-Type' => 'application/json']),
153
            401 => new Response(
154
                401,
155
                ['Content-Type' => 'application/json'],
156
                json_encode(['error' => ['message' => 'Invalid Authentication']]),
157
            ),
158
            404 => new Response(
159
                404,
160
                ['Content-Type' => 'application/json'],
161
                json_encode(['error' => ['message' => 'You must be a member of an organization to use the API']]),
162
            ),
163
            429 => new Response(
164
                429,
165
                ['Content-Type' => 'application/json'],
166
                json_encode(['error' => ['message' => 'Rate limit reached for requests']]),
167
            ),
168
        ];
169
 
170
        $processor = new process_generate_text($this->provider, $this->action);
171
        $method = new \ReflectionMethod($processor, 'handle_api_error');
172
 
173
        foreach ($responses as $status => $response) {
174
            $result = $method->invoke($processor, $response);
175
            $this->assertEquals($status, $result['errorcode']);
176
            if ($status == 500) {
177
                $this->assertEquals('Internal Server Error', $result['errormessage']);
178
            } else if ($status == 503) {
179
                $this->assertEquals('Service Unavailable', $result['errormessage']);
180
            } else {
181
                $this->assertStringContainsString($response->getBody()->getContents(), $result['errormessage']);
182
            }
183
        }
184
    }
185
 
186
    /**
187
     * Test the API success response handler method.
188
     */
189
    public function test_handle_api_success(): void {
190
        $response = new Response(
191
            200,
192
            ['Content-Type' => 'application/json'],
193
            $this->responsebodyjson,
194
        );
195
 
196
        // We're testing a private method, so we need to setup reflector magic.
197
        $processor = new process_generate_text($this->provider, $this->action);
198
        $method = new \ReflectionMethod($processor, 'handle_api_success');
199
 
200
        $result = $method->invoke($processor, $response);
201
 
202
        $this->assertTrue($result['success']);
203
        $this->assertStringContainsString('World War II was a global conflict', $result['generatedcontent']);
204
        $this->assertEquals('stop', $result['finishreason']);
205
        $this->assertEquals('75', $result['prompttokens']);
206
        $this->assertEquals('123', $result['completiontokens']);
207
 
208
    }
209
 
210
    /**
211
     * Test query_ai_api for a successful call.
212
     */
213
    public function test_query_ai_api_success(): void {
214
        // Mock the http client to return a successful response.
215
        ['mock' => $mock] = $this->get_mocked_http_client();
216
 
217
        // The response from Ollama.
218
        $mock->append(new Response(
219
            200,
220
            ['Content-Type' => 'application/json'],
221
            $this->responsebodyjson,
222
        ));
223
 
224
        $processor = new process_generate_text($this->provider, $this->action);
225
        $method = new \ReflectionMethod($processor, 'query_ai_api');
226
        $result = $method->invoke($processor);
227
 
228
        $this->assertTrue($result['success']);
229
        $this->assertStringContainsString('World War II was a global conflict', $result['generatedcontent']);
230
        $this->assertEquals('stop', $result['finishreason']);
231
        $this->assertEquals('75', $result['prompttokens']);
232
        $this->assertEquals('123', $result['completiontokens']);
233
    }
234
 
235
    /**
236
     * Test prepare_response success.
237
     */
238
    public function test_prepare_response_success(): void {
239
        $processor = new process_generate_text($this->provider, $this->action);
240
 
241
        // We're working with a private method here, so we need to use reflection.
242
        $method = new \ReflectionMethod($processor, 'prepare_response');
243
 
244
        $response = [
245
            'success' => true,
246
            'id' => 'chatcmpl-9lkwPWOIiQEvI3nfcGofJcmS5lPYo',
247
            'fingerprint' => 'fp_c4e5b6fa31',
248
            'generatedcontent' => 'Sure, here is some sample text',
249
            'finishreason' => 'stop',
250
            'prompttokens' => '11',
251
            'completiontokens' => '568',
252
        ];
253
 
254
        $result = $method->invoke($processor, $response);
255
 
256
        $this->assertInstanceOf(\core_ai\aiactions\responses\response_base::class, $result);
257
        $this->assertTrue($result->get_success());
258
        $this->assertEquals('generate_text', $result->get_actionname());
259
        $this->assertEquals($response['success'], $result->get_success());
260
        $this->assertEquals($response['generatedcontent'], $result->get_response_data()['generatedcontent']);
261
    }
262
 
263
    /**
264
     * Test prepare_response error.
265
     */
266
    public function test_prepare_response_error(): void {
267
        $processor = new process_generate_text($this->provider, $this->action);
268
 
269
        // We're working with a private method here, so we need to use reflection.
270
        $method = new \ReflectionMethod($processor, 'prepare_response');
271
 
272
        $response = [
273
            'success' => false,
274
            'errorcode' => 500,
275
            'errormessage' => 'Internal server error.',
276
        ];
277
 
278
        $result = $method->invoke($processor, $response);
279
 
280
        $this->assertInstanceOf(\core_ai\aiactions\responses\response_base::class, $result);
281
        $this->assertFalse($result->get_success());
282
        $this->assertEquals('generate_text', $result->get_actionname());
283
        $this->assertEquals($response['errorcode'], $result->get_errorcode());
284
        $this->assertEquals($response['errormessage'], $result->get_errormessage());
285
    }
286
 
287
    /**
288
     * Test process method.
289
     */
290
    public function test_process(): void {
291
        $this->resetAfterTest();
292
        // Log in user.
293
        $this->setUser($this->getDataGenerator()->create_user());
294
 
295
        // Mock the http client to return a successful response.
296
        ['mock' => $mock] = $this->get_mocked_http_client();
297
 
298
        // The response from Ollama.
299
        $mock->append(new Response(
300
            200,
301
            ['Content-Type' => 'application/json'],
302
            $this->responsebodyjson,
303
        ));
304
 
305
        $processor = new process_generate_text($this->provider, $this->action);
306
        $result = $processor->process();
307
 
308
        $this->assertInstanceOf(\core_ai\aiactions\responses\response_base::class, $result);
309
        $this->assertTrue($result->get_success());
310
        $this->assertEquals('generate_text', $result->get_actionname());
311
    }
312
 
313
    /**
314
     * Test process method with error.
315
     */
316
    public function test_process_error(): void {
317
        $this->resetAfterTest();
318
        // Log in user.
319
        $this->setUser($this->getDataGenerator()->create_user());
320
 
321
        // Mock the http client to return a successful response.
322
        ['mock' => $mock] = $this->get_mocked_http_client();
323
 
324
        // The response from Ollama.
325
        $mock->append(new Response(
326
            401,
327
            ['Content-Type' => 'application/json'],
328
            json_encode(['error' => ['message' => 'Invalid Authentication']]),
329
        ));
330
 
331
        $processor = new process_generate_text($this->provider, $this->action);
332
        $result = $processor->process();
333
 
334
        $this->assertInstanceOf(\core_ai\aiactions\responses\response_base::class, $result);
335
        $this->assertFalse($result->get_success());
336
        $this->assertEquals('generate_text', $result->get_actionname());
337
        $this->assertEquals(401, $result->get_errorcode());
338
        $this->assertEquals('Invalid Authentication', $result->get_errormessage());
339
    }
340
 
341
    /**
342
     * Test process method with user rate limiter.
343
     */
344
    public function test_process_with_user_rate_limiter(): void {
345
        $this->resetAfterTest();
346
        // Create users.
347
        $user1 = $this->getDataGenerator()->create_user();
348
        $user2 = $this->getDataGenerator()->create_user();
349
        // Log in user1.
350
        $this->setUser($user1);
351
        // Mock clock.
352
        $clock = $this->mock_clock_with_frozen();
353
 
354
        // Set the user rate limiter.
355
        $config = [
356
            'enableuserratelimit' => true,
357
            'userratelimit' => 1,
358
            'endpoint' => "http://localhost:11434/",
359
        ];
360
 
361
        $provider = $this->manager->create_provider_instance(
362
            classname: '\aiprovider_ollama\provider',
363
            name: 'dummy',
364
            config: $config,
365
            actionconfig: [
366
                \core_ai\aiactions\generate_text::class => [
367
                    'settings' => [
368
                        'model' => 'llama3.2',
369
                        'systeminstruction' => get_string('action_generate_text_instruction', 'core_ai'),
370
                    ],
371
                ],
372
            ],
373
        );
374
 
375
        // Mock the http client to return a successful response.
376
        ['mock' => $mock] = $this->get_mocked_http_client();
377
 
378
        // Case 1: User rate limit has not been reached.
379
        $this->create_action($user1->id);
380
        // The response from Ollama.
381
        $mock->append(new Response(
382
            200,
383
            ['Content-Type' => 'application/json'],
384
            $this->responsebodyjson,
385
        ));
386
        $processor = new process_generate_text($provider, $this->action);
387
        $result = $processor->process();
388
        $this->assertTrue($result->get_success());
389
 
390
        // Case 2: User rate limit has been reached.
391
        $clock->bump(HOURSECS - 10);
392
        // The response from Ollama.
393
        $mock->append(new Response(
394
            200,
395
            ['Content-Type' => 'application/json'],
396
            $this->responsebodyjson,
397
        ));
398
        $this->create_action($user1->id);
399
        $processor = new process_generate_text($provider, $this->action);
400
        $result = $processor->process();
401
        $this->assertEquals(429, $result->get_errorcode());
402
        $this->assertEquals('User rate limit exceeded', $result->get_errormessage());
403
        $this->assertFalse($result->get_success());
404
 
405
        // Case 3: User rate limit has not been reached for a different user.
406
        // Log in user2.
407
        $this->setUser($user2);
408
        $this->create_action($user2->id);
409
        // The response from Ollama.
410
        $mock->append(new Response(
411
            200,
412
            ['Content-Type' => 'application/json'],
413
            $this->responsebodyjson,
414
        ));
415
        $processor = new process_generate_text($provider, $this->action);
416
        $result = $processor->process();
417
        $this->assertTrue($result->get_success());
418
 
419
        // Case 4: Time window has passed, user rate limit should be reset.
420
        $clock->bump(11);
421
        // Log in user1.
422
        $this->setUser($user1);
423
        // The response from Ollama.
424
        $mock->append(new Response(
425
            200,
426
            ['Content-Type' => 'application/json'],
427
            $this->responsebodyjson,
428
        ));
429
        $this->create_action($user1->id);
430
        $processor = new process_generate_text($provider, $this->action);
431
        $result = $processor->process();
432
        $this->assertTrue($result->get_success());
433
    }
434
 
435
    /**
436
     * Test process method with global rate limiter.
437
     */
438
    public function test_process_with_global_rate_limiter(): void {
439
        $this->resetAfterTest();
440
        // Create users.
441
        $user1 = $this->getDataGenerator()->create_user();
442
        $user2 = $this->getDataGenerator()->create_user();
443
        // Log in user1.
444
        $this->setUser($user1);
445
        // Mock clock.
446
        $clock = $this->mock_clock_with_frozen();
447
 
448
        // Set the global rate limiter.
449
        $config = [
450
            'enableglobalratelimit' => true,
451
            'globalratelimit' => 1,
452
            'endpoint' => "http://localhost:11434/",
453
        ];
454
 
455
        $provider = $this->manager->create_provider_instance(
456
            classname: '\aiprovider_ollama\provider',
457
            name: 'dummy',
458
            config: $config,
459
            actionconfig: [
460
                \core_ai\aiactions\generate_text::class => [
461
                    'settings' => [
462
                        'model' => 'llama3.2',
463
                        'systeminstruction' => get_string('action_generate_text_instruction', 'core_ai'),
464
                    ],
465
                ],
466
            ],
467
        );
468
 
469
        // Mock the http client to return a successful response.
470
        ['mock' => $mock] = $this->get_mocked_http_client();
471
 
472
        // Case 1: Global rate limit has not been reached.
473
        $this->create_action($user1->id);
474
        // The response from Ollama.
475
        $mock->append(new Response(
476
            200,
477
            ['Content-Type' => 'application/json'],
478
            $this->responsebodyjson,
479
        ));
480
        $processor = new process_generate_text($provider, $this->action);
481
        $result = $processor->process();
482
        $this->assertTrue($result->get_success());
483
 
484
        // Case 2: Global rate limit has been reached.
485
        $clock->bump(HOURSECS - 10);
486
        // The response from Ollama.
487
        $mock->append(new Response(
488
            200,
489
            ['Content-Type' => 'application/json'],
490
            $this->responsebodyjson,
491
        ));
492
        $this->create_action($user1->id);
493
        $processor = new process_generate_text($provider, $this->action);
494
        $result = $processor->process();
495
        $this->assertEquals(429, $result->get_errorcode());
496
        $this->assertEquals('Global rate limit exceeded', $result->get_errormessage());
497
        $this->assertFalse($result->get_success());
498
 
499
        // Case 3: Global rate limit has been reached for a different user too.
500
        // Log in user2.
501
        $this->setUser($user2);
502
        $this->create_action($user2->id);
503
        // The response from Ollama.
504
        $mock->append(new Response(
505
            200,
506
            ['Content-Type' => 'application/json'],
507
            $this->responsebodyjson,
508
        ));
509
        $processor = new process_generate_text($provider, $this->action);
510
        $result = $processor->process();
511
        $this->assertFalse($result->get_success());
512
 
513
        // Case 4: Time window has passed, global rate limit should be reset.
514
        $clock->bump(11);
515
        // Log in user1.
516
        $this->setUser($user1);
517
        // The response from Ollama.
518
        $mock->append(new Response(
519
            200,
520
            ['Content-Type' => 'application/json'],
521
            $this->responsebodyjson,
522
        ));
523
        $this->create_action($user1->id);
524
        $processor = new process_generate_text($provider, $this->action);
525
        $result = $processor->process();
526
        $this->assertTrue($result->get_success());
527
    }
528
}