|
4 | 4 |
|
5 | 5 | namespace Cortex\Tests\Unit\Prompts\Factories; |
6 | 6 |
|
| 7 | +use Mockery; |
7 | 8 | use GuzzleHttp\Client; |
8 | 9 | use GuzzleHttp\HandlerStack; |
9 | 10 | use GuzzleHttp\Psr7\Response; |
10 | 11 | use GuzzleHttp\Handler\MockHandler; |
| 12 | +use Psr\SimpleCache\CacheInterface; |
11 | 13 | use Cortex\Exceptions\PromptException; |
12 | 14 | use Cortex\Prompts\Data\PromptMetadata; |
13 | 15 | use Cortex\JsonSchema\Types\ObjectSchema; |
|
22 | 24 |
|
23 | 25 | function createLangfuseFactory( |
24 | 26 | array $responses, |
| 27 | + ?CacheInterface $cache = null, |
| 28 | + int $cacheTtl = 3600, |
25 | 29 | ): LangfusePromptFactory { |
26 | 30 | return new LangfusePromptFactory( |
27 | 31 | username: 'username', |
28 | 32 | password: 'password', |
29 | 33 | httpClient: new Client([ |
30 | 34 | 'handler' => HandlerStack::create(new MockHandler($responses)), |
31 | 35 | ]), |
| 36 | + cache: $cache, |
| 37 | + cacheTtl: $cacheTtl, |
32 | 38 | ); |
33 | 39 | } |
34 | 40 |
|
@@ -450,3 +456,144 @@ function createLangfuseFactory( |
450 | 456 | expect($prompt->metadata->provider)->toBe('openai'); |
451 | 457 | expect($prompt->metadata->model)->toBe('gpt-3.5-turbo'); |
452 | 458 | }); |
| 459 | + |
| 460 | +test('it caches prompt responses and reuses them on subsequent calls', function (): void { |
| 461 | + $responseData = [ |
| 462 | + 'type' => 'text', |
| 463 | + 'name' => 'cached-prompt', |
| 464 | + 'version' => 1, |
| 465 | + 'config' => [ |
| 466 | + 'provider' => 'openai', |
| 467 | + 'model' => 'gpt-4', |
| 468 | + ], |
| 469 | + 'labels' => [], |
| 470 | + 'tags' => [], |
| 471 | + 'prompt' => 'This is a cached prompt', |
| 472 | + ]; |
| 473 | + |
| 474 | + /** @var CacheInterface&\Mockery\MockInterface $cache */ |
| 475 | + $cache = mock(CacheInterface::class); |
| 476 | + |
| 477 | + // First call: cache miss, then cache set |
| 478 | + $cache->shouldReceive('get') |
| 479 | + ->once() |
| 480 | + ->with(Mockery::type('string')) |
| 481 | + ->andReturn(null); |
| 482 | + |
| 483 | + $cache->shouldReceive('set') |
| 484 | + ->once() |
| 485 | + ->with(Mockery::type('string'), $responseData, 3600) |
| 486 | + ->andReturnTrue(); |
| 487 | + |
| 488 | + // Second call: cache hit |
| 489 | + $cache->shouldReceive('get') |
| 490 | + ->once() |
| 491 | + ->with(Mockery::type('string')) |
| 492 | + ->andReturn($responseData); |
| 493 | + |
| 494 | + $factory = createLangfuseFactory( |
| 495 | + [new Response(200, body: json_encode($responseData))], |
| 496 | + $cache, |
| 497 | + 3600, |
| 498 | + ); |
| 499 | + |
| 500 | + // First call - should hit HTTP client and cache the result |
| 501 | + $prompt1 = $factory->make('cached-prompt'); |
| 502 | + |
| 503 | + expect($prompt1)->toBeInstanceOf(TextPromptTemplate::class); |
| 504 | + expect($prompt1->text)->toBe('This is a cached prompt'); |
| 505 | + expect($prompt1->metadata->provider)->toBe('openai'); |
| 506 | + expect($prompt1->metadata->model)->toBe('gpt-4'); |
| 507 | + |
| 508 | + // Second call with same parameters - should use cache, not HTTP |
| 509 | + $prompt2 = $factory->make('cached-prompt'); |
| 510 | + |
| 511 | + expect($prompt2)->toBeInstanceOf(TextPromptTemplate::class); |
| 512 | + expect($prompt2->text)->toBe('This is a cached prompt'); |
| 513 | + expect($prompt2->metadata->provider)->toBe('openai'); |
| 514 | + expect($prompt2->metadata->model)->toBe('gpt-4'); |
| 515 | + |
| 516 | + // Verify both prompts are equivalent |
| 517 | + expect($prompt1->text)->toBe($prompt2->text); |
| 518 | + expect($prompt1->metadata->provider)->toBe($prompt2->metadata->provider); |
| 519 | + expect($prompt1->metadata->model)->toBe($prompt2->metadata->model); |
| 520 | +}); |
| 521 | + |
| 522 | +test('it generates correct cache keys for different prompt parameters', function (): void { |
| 523 | + $responseData = [ |
| 524 | + 'type' => 'text', |
| 525 | + 'name' => 'test-prompt', |
| 526 | + 'version' => 1, |
| 527 | + 'config' => [], |
| 528 | + 'labels' => [], |
| 529 | + 'tags' => [], |
| 530 | + 'prompt' => 'Test prompt', |
| 531 | + ]; |
| 532 | + |
| 533 | + $cacheKeys = []; |
| 534 | + /** @var CacheInterface&\Mockery\MockInterface $cache */ |
| 535 | + $cache = mock(CacheInterface::class); |
| 536 | + |
| 537 | + // Mock cache to capture the keys used and always return null (cache miss) |
| 538 | + $cache->shouldReceive('get') |
| 539 | + ->times(3) |
| 540 | + ->with(Mockery::on(function ($key) use (&$cacheKeys): bool { |
| 541 | + $cacheKeys[] = $key; |
| 542 | + |
| 543 | + return is_string($key); |
| 544 | + })) |
| 545 | + ->andReturn(null); |
| 546 | + |
| 547 | + $cache->shouldReceive('set') |
| 548 | + ->times(3) |
| 549 | + ->with(Mockery::type('string'), $responseData, Mockery::any()) |
| 550 | + ->andReturnTrue(); |
| 551 | + |
| 552 | + $factory = createLangfuseFactory([ |
| 553 | + new Response(200, body: json_encode($responseData)), |
| 554 | + new Response(200, body: json_encode($responseData)), |
| 555 | + new Response(200, body: json_encode($responseData)), |
| 556 | + ], $cache); |
| 557 | + |
| 558 | + // Call with different parameters to generate different cache keys |
| 559 | + $factory->make('test-prompt'); |
| 560 | + $factory->make('test-prompt', [ |
| 561 | + 'version' => 2, |
| 562 | + ]); |
| 563 | + $factory->make('test-prompt', [ |
| 564 | + 'label' => 'production', |
| 565 | + ]); |
| 566 | + |
| 567 | + expect($cacheKeys)->toHaveCount(3); |
| 568 | + |
| 569 | + // Verify all cache keys are different (different URLs generate different hashes) |
| 570 | + expect($cacheKeys[0])->not()->toBe($cacheKeys[1]); |
| 571 | + expect($cacheKeys[1])->not()->toBe($cacheKeys[2]); |
| 572 | + expect($cacheKeys[0])->not()->toBe($cacheKeys[2]); |
| 573 | + |
| 574 | + // Verify all keys start with the expected prefix |
| 575 | + foreach ($cacheKeys as $key) { |
| 576 | + expect($key)->toStartWith('cortex.prompts.langfuse.'); |
| 577 | + } |
| 578 | +}); |
| 579 | + |
| 580 | +test('it works without cache when none is provided', function (): void { |
| 581 | + $responseData = [ |
| 582 | + 'type' => 'text', |
| 583 | + 'name' => 'test-prompt', |
| 584 | + 'version' => 1, |
| 585 | + 'config' => [], |
| 586 | + 'labels' => [], |
| 587 | + 'tags' => [], |
| 588 | + 'prompt' => 'Test prompt without cache', |
| 589 | + ]; |
| 590 | + |
| 591 | + $factory = createLangfuseFactory([ |
| 592 | + new Response(200, body: json_encode($responseData)), |
| 593 | + ]); // No cache provided - should use PSR discovery |
| 594 | + |
| 595 | + $prompt = $factory->make('test-prompt'); |
| 596 | + |
| 597 | + expect($prompt)->toBeInstanceOf(TextPromptTemplate::class); |
| 598 | + expect($prompt->text)->toBe('Test prompt without cache'); |
| 599 | +}); |
0 commit comments