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