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