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