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_generate_text
32
 * @covers     \aiprovider_openai\abstract_processor
33
 */
34
final class process_generate_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\generate_text::class,
61
            actionconfig: [
62
                'systeminstruction' => get_string('action_generate_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\generate_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_generate_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('This is a test prompt', $body->messages[1]->content);
93
        $this->assertEquals('user', $body->messages[1]->role);
94
    }
95
 
96
    /**
97
     * Test create_request_object with extra model settings.
98
     */
99
    public function test_create_request_object_with_model_settings(): void {
100
        $this->provider = $this->create_provider(
101
            actionclass: \core_ai\aiactions\generate_text::class,
102
            actionconfig: [
103
                'systeminstruction' => get_string('action_generate_text_instruction', 'core_ai'),
104
                'temperature' => '0.5',
105
                'max_completion_tokens' => '100',
106
            ],
107
        );
108
        $processor = new process_generate_text($this->provider, $this->action);
109
 
110
        // We're working with a private method here, so we need to use reflection.
111
        $method = new \ReflectionMethod($processor, 'create_request_object');
112
        $request = $method->invoke($processor, 1);
113
 
114
        $body = (object) json_decode($request->getBody()->getContents());
115
 
116
        $this->assertEquals('gpt-4o', $body->model);
117
        $this->assertEquals('0.5', $body->temperature);
118
        $this->assertEquals('100', $body->max_completion_tokens);
119
 
120
        $this->provider = $this->create_provider(
121
            actionclass: \core_ai\aiactions\generate_text::class,
122
            actionconfig: [
123
                'model' => 'my-custom-gpt',
124
                'systeminstruction' => get_string('action_generate_text_instruction', 'core_ai'),
125
                'modelextraparams' => '{"temperature": 0.5,"max_completion_tokens": 100}',
126
            ],
127
        );
128
        $processor = new process_generate_text($this->provider, $this->action);
129
 
130
        // We're working with a private method here, so we need to use reflection.
131
        $method = new \ReflectionMethod($processor, 'create_request_object');
132
        $request = $method->invoke($processor, 1);
133
 
134
        $body = (object) json_decode($request->getBody()->getContents());
135
 
136
        $this->assertEquals('my-custom-gpt', $body->model);
137
        $this->assertEquals('0.5', $body->temperature);
138
        $this->assertEquals('100', $body->max_completion_tokens);
139
    }
140
 
141
    /**
142
     * Test the API error response handler method.
143
     */
144
    public function test_handle_api_error(): void {
145
        $responses = [
146
            500 => new Response(500, ['Content-Type' => 'application/json']),
147
            503 => new Response(503, ['Content-Type' => 'application/json']),
148
            401 => new Response(
149
                401,
150
                ['Content-Type' => 'application/json'],
151
                json_encode(['error' => ['message' => 'Invalid Authentication']]),
152
            ),
153
            404 => new Response(
154
                404,
155
                ['Content-Type' => 'application/json'],
156
                json_encode(['error' => ['message' => 'You must be a member of an organization to use the API']]),
157
            ),
158
            429 => new Response(
159
                429,
160
                ['Content-Type' => 'application/json'],
161
                json_encode(['error' => ['message' => 'Rate limit reached for requests']]),
162
            ),
163
        ];
164
 
165
        $processor = new process_generate_text($this->provider, $this->action);
166
        $method = new \ReflectionMethod($processor, 'handle_api_error');
167
 
168
        foreach ($responses as $status => $response) {
169
            $result = $method->invoke($processor, $response);
170
            $this->assertEquals($status, $result['errorcode']);
171
            if ($status == 500) {
172
                $this->assertEquals('Internal Server Error', $result['errormessage']);
173
            } else if ($status == 503) {
174
                $this->assertEquals('Service Unavailable', $result['errormessage']);
175
            } else {
176
                $this->assertStringContainsString($response->getBody()->getContents(), $result['errormessage']);
177
            }
178
        }
179
    }
180
 
181
    /**
182
     * Test the API success response handler method.
183
     */
184
    public function test_handle_api_success(): void {
185
        $response = new Response(
186
            200,
187
            ['Content-Type' => 'application/json'],
188
            $this->responsebodyjson,
189
        );
190
 
191
        // We're testing a private method, so we need to setup reflector magic.
192
        $processor = new process_generate_text($this->provider, $this->action);
193
        $method = new \ReflectionMethod($processor, 'handle_api_success');
194
 
195
        $result = $method->invoke($processor, $response);
196
 
197
        $this->assertTrue($result['success']);
198
        $this->assertEquals('chatcmpl-9lkwPWOIiQEvI3nfcGofJcmS5lPYo', $result['id']);
199
        $this->assertEquals('fp_c4e5b6fa31', $result['fingerprint']);
200
        $this->assertStringContainsString('Sure, here is some sample text', $result['generatedcontent']);
201
        $this->assertEquals('stop', $result['finishreason']);
202
        $this->assertEquals('11', $result['prompttokens']);
203
        $this->assertEquals('568', $result['completiontokens']);
204
        $this->assertEquals('gpt-4o-2024-05-13', $result['model']);
205
    }
206
 
207
    /**
208
     * Test query_ai_api for a successful call.
209
     */
210
    public function test_query_ai_api_success(): void {
211
        // Mock the http client to return a successful response.
212
        ['mock' => $mock] = $this->get_mocked_http_client();
213
 
214
        // The response from OpenAI.
215
        $mock->append(new Response(
216
            200,
217
            ['Content-Type' => 'application/json'],
218
            $this->responsebodyjson,
219
        ));
220
 
221
        $processor = new process_generate_text($this->provider, $this->action);
222
        $method = new \ReflectionMethod($processor, 'query_ai_api');
223
        $result = $method->invoke($processor);
224
 
225
        $this->assertTrue($result['success']);
226
        $this->assertEquals('chatcmpl-9lkwPWOIiQEvI3nfcGofJcmS5lPYo', $result['id']);
227
        $this->assertEquals('fp_c4e5b6fa31', $result['fingerprint']);
228
        $this->assertStringContainsString('Sure, here is some sample text', $result['generatedcontent']);
229
        $this->assertEquals('stop', $result['finishreason']);
230
        $this->assertEquals('11', $result['prompttokens']);
231
        $this->assertEquals('568', $result['completiontokens']);
232
        $this->assertEquals('gpt-4o-2024-05-13', $result['model']);
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
            'model' => 'gpt-4o',
253
        ];
254
 
255
        $result = $method->invoke($processor, $response);
256
 
257
        $this->assertInstanceOf(\core_ai\aiactions\responses\response_base::class, $result);
258
        $this->assertTrue($result->get_success());
259
        $this->assertEquals('generate_text', $result->get_actionname());
260
        $this->assertEquals($response['success'], $result->get_success());
261
        $this->assertEquals($response['generatedcontent'], $result->get_response_data()['generatedcontent']);
262
        $this->assertEquals($response['model'], $result->get_response_data()['model']);
263
    }
264
 
265
    /**
266
     * Test prepare_response error.
267
     */
268
    public function test_prepare_response_error(): void {
269
        $processor = new process_generate_text($this->provider, $this->action);
270
 
271
        // We're working with a private method here, so we need to use reflection.
272
        $method = new \ReflectionMethod($processor, 'prepare_response');
273
 
274
        $response = [
275
            'success' => false,
276
            'errorcode' => 500,
277
            'errormessage' => 'Internal server error.',
278
        ];
279
 
280
        $result = $method->invoke($processor, $response);
281
 
282
        $this->assertInstanceOf(\core_ai\aiactions\responses\response_base::class, $result);
283
        $this->assertFalse($result->get_success());
284
        $this->assertEquals('generate_text', $result->get_actionname());
285
        $this->assertEquals($response['errorcode'], $result->get_errorcode());
286
        $this->assertEquals($response['errormessage'], $result->get_errormessage());
287
    }
288
 
289
    /**
290
     * Test process method.
291
     */
292
    public function test_process(): void {
293
        // Log in user.
294
        $this->setUser($this->getDataGenerator()->create_user());
295
 
296
        // Mock the http client to return a successful response.
297
        ['mock' => $mock] = $this->get_mocked_http_client();
298
 
299
        // The response from OpenAI.
300
        $mock->append(new Response(
301
            200,
302
            ['Content-Type' => 'application/json'],
303
            $this->responsebodyjson,
304
        ));
305
 
306
        $processor = new process_generate_text($this->provider, $this->action);
307
        $result = $processor->process();
308
 
309
        $this->assertInstanceOf(\core_ai\aiactions\responses\response_base::class, $result);
310
        $this->assertTrue($result->get_success());
311
        $this->assertEquals('generate_text', $result->get_actionname());
312
    }
313
 
314
    /**
315
     * Test process method with error.
316
     */
317
    public function test_process_error(): void {
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 OpenAI.
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
        // Create users.
346
        $user1 = $this->getDataGenerator()->create_user();
347
        $user2 = $this->getDataGenerator()->create_user();
348
        // Log in user1.
349
        $this->setUser($user1);
350
        // Mock clock.
351
        $clock = $this->mock_clock_with_frozen();
352
 
353
        // Set the user rate limiter.
354
        $config = [
355
            'apikey' => '123',
356
            'enableuserratelimit' => true,
357
            'userratelimit' => 1,
358
        ];
359
        $provider = $this->manager->create_provider_instance(
360
            classname: '\aiprovider_openai\provider',
361
            name: 'dummy',
362
            config: $config,
363
            actionconfig: [
364
                \core_ai\aiactions\generate_text::class => [
365
                    'settings' => [
366
                        'model' => 'gpt-4o',
367
                        'endpoint' => "https://api.openai.com/v1/chat/completions",
368
                        'systeminstruction' => get_string('action_generate_text_instruction', 'core_ai'),
369
                    ],
370
                ],
371
            ],
372
        );
373
 
374
        // Mock the http client to return a successful response.
375
        ['mock' => $mock] = $this->get_mocked_http_client();
376
 
377
        // Case 1: User rate limit has not been reached.
378
        $this->create_action($user1->id);
379
        // The response from OpenAI.
380
        $mock->append(new Response(
381
            200,
382
            ['Content-Type' => 'application/json'],
383
            $this->responsebodyjson,
384
        ));
385
        $processor = new process_generate_text($this->provider, $this->action);
386
        $result = $processor->process();
387
        $this->assertTrue($result->get_success());
388
 
389
        // Case 2: User rate limit has been reached.
390
        $clock->bump(HOURSECS - 10);
391
        // The response from OpenAI.
392
        $mock->append(new Response(
393
            200,
394
            ['Content-Type' => 'application/json'],
395
            $this->responsebodyjson,
396
        ));
397
        $this->create_action($user1->id);
398
        $processor = new process_generate_text($provider, $this->action);
399
        $result = $processor->process();
400
        $this->assertEquals(429, $result->get_errorcode());
401
        $this->assertEquals('User rate limit exceeded', $result->get_errormessage());
402
        $this->assertFalse($result->get_success());
403
 
404
        // Case 3: User rate limit has not been reached for a different user.
405
        // Log in user2.
406
        $this->setUser($user2);
407
        $this->create_action($user2->id);
408
        // The response from OpenAI.
409
        $mock->append(new Response(
410
            200,
411
            ['Content-Type' => 'application/json'],
412
            $this->responsebodyjson,
413
        ));
414
        $processor = new process_generate_text($provider, $this->action);
415
        $result = $processor->process();
416
        $this->assertTrue($result->get_success());
417
 
418
        // Case 4: Time window has passed, user rate limit should be reset.
419
        $clock->bump(11);
420
        // Log in user1.
421
        $this->setUser($user1);
422
        // The response from OpenAI.
423
        $mock->append(new Response(
424
            200,
425
            ['Content-Type' => 'application/json'],
426
            $this->responsebodyjson,
427
        ));
428
        $this->provider = $this->create_provider(\core_ai\aiactions\generate_text::class);
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
        // Create users.
440
        $user1 = $this->getDataGenerator()->create_user();
441
        $user2 = $this->getDataGenerator()->create_user();
442
        // Log in user1.
443
        $this->setUser($user1);
444
        // Mock clock.
445
        $clock = $this->mock_clock_with_frozen();
446
 
447
        // Set the global rate limiter.
448
        $config = [
449
            'apikey' => '123',
450
            'enableglobalratelimit' => true,
451
            'globalratelimit' => 1,
452
        ];
453
        $provider = $this->manager->create_provider_instance(
454
            classname: '\aiprovider_openai\provider',
455
            name: 'dummy',
456
            config: $config,
457
            actionconfig: [
458
                \core_ai\aiactions\generate_text::class => [
459
                    'settings' => [
460
                        'model' => 'gpt-4o',
461
                        'endpoint' => "https://api.openai.com/v1/chat/completions",
462
                        'systeminstruction' => get_string('action_generate_text_instruction', 'core_ai'),
463
                    ],
464
                ],
465
            ],
466
        );
467
 
468
        // Mock the http client to return a successful response.
469
        ['mock' => $mock] = $this->get_mocked_http_client();
470
 
471
        // Case 1: Global rate limit has not been reached.
472
        $this->create_action($user1->id);
473
        // The response from OpenAI.
474
        $mock->append(new Response(
475
            200,
476
            ['Content-Type' => 'application/json'],
477
            $this->responsebodyjson,
478
        ));
479
        $processor = new process_generate_text($provider, $this->action);
480
        $result = $processor->process();
481
        $this->assertTrue($result->get_success());
482
 
483
        // Case 2: Global rate limit has been reached.
484
        $clock->bump(HOURSECS - 10);
485
        // The response from OpenAI.
486
        $mock->append(new Response(
487
            200,
488
            ['Content-Type' => 'application/json'],
489
            $this->responsebodyjson,
490
        ));
491
        $this->create_action($user1->id);
492
        $processor = new process_generate_text($provider, $this->action);
493
        $result = $processor->process();
494
        $this->assertEquals(429, $result->get_errorcode());
495
        $this->assertEquals('Global rate limit exceeded', $result->get_errormessage());
496
        $this->assertFalse($result->get_success());
497
 
498
        // Case 3: Global rate limit has been reached for a different user too.
499
        // Log in user2.
500
        $this->setUser($user2);
501
        $this->create_action($user2->id);
502
        // The response from OpenAI.
503
        $mock->append(new Response(
504
            200,
505
            ['Content-Type' => 'application/json'],
506
            $this->responsebodyjson,
507
        ));
508
        $processor = new process_generate_text($provider, $this->action);
509
        $result = $processor->process();
510
        $this->assertFalse($result->get_success());
511
 
512
        // Case 4: Time window has passed, global rate limit should be reset.
513
        $clock->bump(11);
514
        // Log in user1.
515
        $this->setUser($user1);
516
        // The response from OpenAI.
517
        $mock->append(new Response(
518
            200,
519
            ['Content-Type' => 'application/json'],
520
            $this->responsebodyjson,
521
        ));
522
        $this->provider = $this->create_provider(\core_ai\aiactions\generate_text::class);
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
}