diff --git a/README.md b/README.md index 3429f0b3..6fccc9cf 100644 --- a/README.md +++ b/README.md @@ -41,20 +41,18 @@ Those models are provided by different **platforms**, like OpenAI, Azure, Replic #### Example Instantiation ```php -use PhpLlm\LlmChain\OpenAI\Model\Embeddings; -use PhpLlm\LlmChain\OpenAI\Model\Gpt; -use PhpLlm\LlmChain\OpenAI\Model\Gpt\Version; -use PhpLlm\LlmChain\OpenAI\Platform\OpenAI; -use Symfony\Component\HttpClient\HttpClient; +use PhpLlm\LlmChain\Bridge\OpenAI\Embeddings; +use PhpLlm\LlmChain\Bridge\OpenAI\GPT; +use PhpLlm\LlmChain\Bridge\OpenAI\PlatformFactory; // Platform: OpenAI -$platform = new OpenAI(HttpClient::create(), $_ENV['OPENAI_API_KEY']); +$platform = PlatformFactory::create($_ENV['OPENAI_API_KEY']); // Language Model: GPT (OpenAI) -$llm = new Gpt($platform, Version::gpt4oMini()); +$llm = new GPT(GPT::GPT_4O_MINI); // Embeddings Model: Embeddings (OpenAI) -$embeddings = new Embeddings($platform); +$embeddings = new Embeddings(); ``` #### Supported Models & Platforms @@ -62,6 +60,7 @@ $embeddings = new Embeddings($platform); * Language Models * [OpenAI's GPT](https://platform.openai.com/docs/models/overview) with [OpenAI](https://platform.openai.com/docs/overview) and [Azure](https://learn.microsoft.com/azure/ai-services/openai/concepts/models) as Platform * [Anthropic's Claude](https://www.anthropic.com/claude) with [Anthropic](https://www.anthropic.com/) as Platform + * [Meta's Llama](https://www.llama.com/) with [Ollama](https://ollama.com/) and [Replicate](https://replicate.com/) as Platform * Embeddings Models * [OpenAI's Text Embeddings](https://platform.openai.com/docs/guides/embeddings/embedding-models) with [OpenAI](https://platform.openai.com/docs/overview) and [Azure](https://learn.microsoft.com/azure/ai-services/openai/concepts/models) as Platform * [Voyage's Embeddings](https://docs.voyageai.com/docs/embeddings) with [Voyage](https://www.voyageai.com/) as Platform @@ -71,7 +70,7 @@ See [issue #28](https://github.com/php-llm/llm-chain/issues/28) for planned supp ### Chain & Messages The core feature of LLM Chain is to interact with language models via messages. This interaction is done by sending -a **MessageBag** to a **Chain**, which takes care of LLM invokation and response handling. +a **MessageBag** to a **Chain**, which takes care of LLM invocation and response handling. Messages can be of different types, most importantly `UserMessage`, `SystemMessage`, or `AssistantMessage`, and can also have different content types, like `Text` or `Image`. @@ -80,13 +79,13 @@ have different content types, like `Text` or `Image`. ```php use PhpLlm\LlmChain\Chain; -use PhpLlm\LlmChain\Message\MessageBag; -use PhpLlm\LlmChain\Message\SystemMessage; -use PhpLlm\LlmChain\Message\UserMessage; +use PhpLlm\LlmChain\Model\Message\MessageBag; +use PhpLlm\LlmChain\Model\Message\SystemMessage; +use PhpLlm\LlmChain\Model\Message\UserMessage; -// LLM instantiation +// Platform & LLM instantiation -$chain = new Chain($llm); +$chain = new Chain($platform, $llm); $messages = new MessageBag( new SystemMessage('You are a helpful chatbot answering questions about LLM Chain.'), new UserMessage('Hello, how are you?'), @@ -104,6 +103,8 @@ The `MessageInterface` and `Content` interface help to customize this process if 1. **OpenAI's GPT with Azure**: [chat-gpt-azure.php](examples/chat-gpt-azure.php) 1. **OpenAI's GPT**: [chat-gpt-openai.php](examples/chat-gpt-openai.php) 1. **OpenAI's o1**: [chat-o1-openai.php](examples/chat-o1-openai.php) +1. **Meta's Llama with Ollama**: [chat-llama-ollama.php](examples/chat-llama-ollama.php) +1. **Meta's Llama with Replicate**: [chat-llama-replicate.php](examples/chat-llama-replicate.php) ### Tools @@ -112,19 +113,21 @@ Tools are services that can be called by the LLM to provide additional features Tool calling can be enabled by registering the processors in the chain: ```php -use PhpLlm\LlmChain\ToolBox\ChainProcessor; -use PhpLlm\LlmChain\ToolBox\ToolAnalyzer; -use PhpLlm\LlmChain\ToolBox\ToolBox; +use PhpLlm\LlmChain\Chain\ToolBox\ChainProcessor; +use PhpLlm\LlmChain\Chain\ToolBox\ToolAnalyzer; +use PhpLlm\LlmChain\Chain\ToolBox\ToolBox; use Symfony\Component\Serializer\Encoder\JsonEncoder; use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; use Symfony\Component\Serializer\Serializer; +// Platform & LLM instantiation + $yourTool = new YourTool(); $toolBox = new ToolBox(new ToolAnalyzer(), [$yourTool]); $toolProcessor = new ChainProcessor($toolBox); -$chain = new Chain($llm, inputProcessor: [$toolProcessor], outputProcessor: [$toolProcessor]); +$chain = new Chain($platform, $llm, inputProcessor: [$toolProcessor], outputProcessor: [$toolProcessor]); ``` Custom tools can basically be any class, but must configure by the `#[AsTool]` attribute. @@ -159,15 +162,16 @@ For populating a vector store, LLM Chain provides the service `DocumentEmbedder` `EmbeddingsModel` and one of `StoreInterface`, and works with a collection of `Document` objects as input: ```php -use PhpLlm\LlmChain\DocumentEmbedder; -use PhpLlm\LlmChain\OpenAI\Model\Embeddings; -use PhpLlm\LlmChain\OpenAI\Platform\OpenAI; -use PhpLlm\LlmChain\Store\Pinecone\Store; +use PhpLlm\LlmChain\Embedder; +use PhpLlm\LlmChain\Bridge\OpenAI\Embeddings; +use PhpLlm\LlmChain\Bridge\OpenAI\PlatformFactory; +use PhpLlm\LlmChain\Bridge\Pinecone\Store; use Probots\Pinecone\Pinecone; use Symfony\Component\HttpClient\HttpClient; -$embedder = new DocumentEmbedder( - new Embeddings(new OpenAI(HttpClient::create(), $_ENV['OPENAI_API_KEY']);), +$embedder = new Embedder( + PlatformFactory::create($_ENV['OPENAI_API_KEY']), + new Embeddings(), new Store(Pinecone::client($_ENV['PINECONE_API_KEY'], $_ENV['PINECONE_HOST']), ); $embedder->embed($documents); @@ -196,20 +200,19 @@ In the end the chain is used in combination with a retrieval tool on top of the ```php use PhpLlm\LlmChain\Chain; -use PhpLlm\LlmChain\DocumentEmbedder; -use PhpLlm\LlmChain\Message\Message; -use PhpLlm\LlmChain\Message\MessageBag; -use PhpLlm\LlmChain\ToolBox\ChainProcessor; -use PhpLlm\LlmChain\ToolBox\Tool\SimilaritySearch; -use PhpLlm\LlmChain\ToolBox\ToolAnalyzer; -use PhpLlm\LlmChain\ToolBox\ToolBox; +use PhpLlm\LlmChain\Model\Message\Message; +use PhpLlm\LlmChain\Model\Message\MessageBag; +use PhpLlm\LlmChain\Chain\ToolBox\ChainProcessor; +use PhpLlm\LlmChain\Chain\ToolBox\Tool\SimilaritySearch; +use PhpLlm\LlmChain\Chain\ToolBox\ToolAnalyzer; +use PhpLlm\LlmChain\Chain\ToolBox\ToolBox; -// Initialize Platform and LLM +// Initialize Platform & Models $similaritySearch = new SimilaritySearch($embeddings, $store); $toolBox = new ToolBox(new ToolAnalyzer(), [$similaritySearch]); $processor = new ChainProcessor($toolBox); -$chain = new Chain(new Gpt($platform), [$processor], [$processor]); +$chain = new Chain($platform, $llm, [$processor], [$processor]); $messages = new MessageBag( Message::forSystem(<<getContent()); // returns an instance of `MathReasoning` class Also PHP array structures as `response_format` are supported, which also requires the chain processor mentioned above: ```php -use PhpLlm\LlmChain\Message\Message; -use PhpLlm\LlmChain\Message\MessageBag; +use PhpLlm\LlmChain\Model\Message\Message; +use PhpLlm\LlmChain\Model\Message\MessageBag; // Initialize Platform, LLM and Chain with processors and Clock tool @@ -380,9 +383,9 @@ needs to be used. Some LLMs also support images as input, which LLM Chain supports as `Content` type within the `UserMessage`: ```php -use PhpLlm\LlmChain\Message\Content\Image; -use PhpLlm\LlmChain\Message\Message; -use PhpLlm\LlmChain\Message\MessageBag; +use PhpLlm\LlmChain\Model\Message\Content\Image; +use PhpLlm\LlmChain\Model\Message\Message; +use PhpLlm\LlmChain\Model\Message\MessageBag; // Initialize Platoform, LLM & Chain @@ -411,16 +414,15 @@ therefore LLM Chain implements a `EmbeddingsModel` interface with various models The standalone usage results in an `Vector` instance: ```php -use PhpLlm\LlmChain\OpenAI\Model\Embeddings; -use PhpLlm\LlmChain\OpenAI\Model\Embeddings\Version; +use PhpLlm\LlmChain\Bridge\OpenAI\Embeddings; // Initialize Platform -$embeddings = new Embeddings($platform, Version::textEmbedding3Small()); +$embeddings = new Embeddings($platform, Embeddings::TEXT_3_SMALL); -$vector = $embeddings->create($textInput); +$vectors = $platform->request($embeddings, $textInput)->getContent(); -dump($vector->getData()); // Array of float values +dump($vectors[0]->getData()); // Array of float values ``` #### Code Examples @@ -436,9 +438,9 @@ interface. They are provided while instantiating the Chain instance: ```php use PhpLlm\LlmChain\Chain; -// Initialize LLM and processors +// Initialize Platform, LLM and processors -$chain = new Chain($llm, $inputProcessors, $outputProcessors); +$chain = new Chain($platform, $llm, $inputProcessors, $outputProcessors); ``` #### InputProcessor @@ -449,7 +451,7 @@ able to mutate both on top of the `Input` instance provided. ```php use PhpLlm\LlmChain\Chain\Input; use PhpLlm\LlmChain\Chain\InputProcessor; -use PhpLlm\LlmChain\Message\AssistantMessage +use PhpLlm\LlmChain\Model\Message\AssistantMessage final class MyProcessor implements InputProcessor { @@ -474,7 +476,7 @@ mutate or replace the given response: ```php use PhpLlm\LlmChain\Chain\Output; use PhpLlm\LlmChain\Chain\OutputProcessor; -use PhpLlm\LlmChain\Message\AssistantMessage +use PhpLlm\LlmChain\Model\Message\AssistantMessage final class MyProcessor implements OutputProcessor { @@ -499,7 +501,7 @@ use PhpLlm\LlmChain\Chain\ChainAwareProcessor; use PhpLlm\LlmChain\Chain\ChainAwareTrait; use PhpLlm\LlmChain\Chain\Output; use PhpLlm\LlmChain\Chain\OutputProcessor; -use PhpLlm\LlmChain\Message\AssistantMessage +use PhpLlm\LlmChain\Model\Message\AssistantMessage final class MyProcessor implements OutputProcessor, ChainAwareProcessor { diff --git a/composer.json b/composer.json index edc2d39d..07c5d4ce 100644 --- a/composer.json +++ b/composer.json @@ -28,6 +28,7 @@ "mongodb/mongodb": "^1.20", "php-cs-fixer/shim": "^3.64", "phpstan/phpstan": "^1.12", + "phpstan/phpstan-webmozart-assert": "^1.2", "phpunit/phpunit": "^11.3", "probots-io/pinecone-php": "^1.0", "rector/rector": "^1.2", diff --git a/examples/chat-claude-anthropic.php b/examples/chat-claude-anthropic.php index 7aa2516c..ed48d111 100755 --- a/examples/chat-claude-anthropic.php +++ b/examples/chat-claude-anthropic.php @@ -1,12 +1,11 @@ loadEnv(dirname(__DIR__).'/.env'); @@ -16,10 +15,10 @@ exit(1); } -$platform = new Anthropic(HttpClient::create(), $_ENV['ANTHROPIC_API_KEY']); -$llm = new Claude($platform); +$platform = PlatformFactory::create($_ENV['ANTHROPIC_API_KEY']); +$llm = new Claude(); -$chain = new Chain($llm); +$chain = new Chain($platform, $llm); $messages = new MessageBag( Message::forSystem('You are a pirate and you write funny.'), Message::ofUser('What is the Symfony framework?'), diff --git a/examples/chat-gpt-azure.php b/examples/chat-gpt-azure.php index 73e3c3a0..71094b25 100755 --- a/examples/chat-gpt-azure.php +++ b/examples/chat-gpt-azure.php @@ -1,12 +1,11 @@ loadEnv(dirname(__DIR__).'/.env'); @@ -17,15 +16,15 @@ exit(1); } -$platform = new Azure(HttpClient::create(), +$platform = PlatformFactory::create( $_ENV['AZURE_OPENAI_BASEURL'], $_ENV['AZURE_OPENAI_DEPLOYMENT'], $_ENV['AZURE_OPENAI_VERSION'], $_ENV['AZURE_OPENAI_KEY'], ); -$llm = new Gpt($platform, Gpt::GPT_4O_MINI); +$llm = new GPT(GPT::GPT_4O_MINI); -$chain = new Chain($llm); +$chain = new Chain($platform, $llm); $messages = new MessageBag( Message::forSystem('You are a pirate and you write funny.'), Message::ofUser('What is the Symfony framework?'), diff --git a/examples/chat-gpt-openai.php b/examples/chat-gpt-openai.php index ec2d14df..6062aa43 100755 --- a/examples/chat-gpt-openai.php +++ b/examples/chat-gpt-openai.php @@ -1,12 +1,11 @@ loadEnv(dirname(__DIR__).'/.env'); @@ -16,12 +15,12 @@ exit(1); } -$platform = new OpenAI(HttpClient::create(), $_ENV['OPENAI_API_KEY']); -$llm = new Gpt($platform, Gpt::GPT_4O_MINI, [ +$platform = PlatformFactory::create($_ENV['OPENAI_API_KEY']); +$llm = new GPT(GPT::GPT_4O_MINI, [ 'temperature' => 0.5, // default options for the model ]); -$chain = new Chain($llm); +$chain = new Chain($platform, $llm); $messages = new MessageBag( Message::forSystem('You are a pirate and you write funny.'), Message::ofUser('What is the Symfony framework?'), diff --git a/examples/chat-llama-ollama.php b/examples/chat-llama-ollama.php index 551b8f2d..feb95f76 100755 --- a/examples/chat-llama-ollama.php +++ b/examples/chat-llama-ollama.php @@ -1,12 +1,11 @@ loadEnv(dirname(__DIR__).'/.env'); @@ -16,10 +15,10 @@ exit(1); } -$platform = new Ollama(HttpClient::create(), $_ENV['OLLAMA_HOST_URL']); -$llm = new Llama($platform); +$platform = PlatformFactory::create($_ENV['OLLAMA_HOST_URL']); +$llm = new Llama('llama3.2'); -$chain = new Chain($llm); +$chain = new Chain($platform, $llm); $messages = new MessageBag( Message::forSystem('You are a helpful assistant.'), Message::ofUser('Tina has one brother and one sister. How many sisters do Tina\'s siblings have?'), diff --git a/examples/chat-llama-replicate.php b/examples/chat-llama-replicate.php index 491129af..c0579a3e 100755 --- a/examples/chat-llama-replicate.php +++ b/examples/chat-llama-replicate.php @@ -1,12 +1,11 @@ loadEnv(dirname(__DIR__).'/.env'); @@ -16,10 +15,10 @@ exit(1); } -$platform = new Replicate(HttpClient::create(), $_ENV['REPLICATE_API_KEY']); -$llm = new Llama($platform); +$platform = PlatformFactory::create($_ENV['REPLICATE_API_KEY']); +$llm = new Llama(); -$chain = new Chain($llm); +$chain = new Chain($platform, $llm); $messages = new MessageBag( Message::forSystem('You are a helpful assistant.'), Message::ofUser('Tina has one brother and one sister. How many sisters do Tina\'s siblings have?'), diff --git a/examples/chat-o1-openai.php b/examples/chat-o1-openai.php index a6b62181..b5987cd5 100755 --- a/examples/chat-o1-openai.php +++ b/examples/chat-o1-openai.php @@ -1,12 +1,11 @@ loadEnv(dirname(__DIR__).'/.env'); @@ -21,8 +20,8 @@ exit(134); } -$platform = new OpenAI(HttpClient::create(), $_ENV['OPENAI_API_KEY']); -$llm = new Gpt($platform, Gpt::O1_PREVIEW); +$platform = PlatformFactory::create($_ENV['OPENAI_API_KEY']); +$llm = new GPT(GPT::O1_PREVIEW); $prompt = <<call(new MessageBag(Message::ofUser($prompt))); +$response = (new Chain($platform, $llm))->call(new MessageBag(Message::ofUser($prompt))); echo $response->getContent().PHP_EOL; diff --git a/examples/embeddings-openai.php b/examples/embeddings-openai.php index 5c875677..01b5ec51 100755 --- a/examples/embeddings-openai.php +++ b/examples/embeddings-openai.php @@ -1,9 +1,9 @@ loadEnv(dirname(__DIR__).'/.env'); @@ -13,13 +13,15 @@ exit(1); } -$platform = new Platform(HttpClient::create(), $_ENV['OPENAI_API_KEY']); -$embeddings = new Embeddings($platform); +$platform = PlatformFactory::create($_ENV['OPENAI_API_KEY']); +$embeddings = new Embeddings(); -$vector = $embeddings->create(<<request($embeddings, <<getDimensions().PHP_EOL; +assert($response instanceof VectorResponse); + +echo 'Dimensions: '.$response->getContent()[0]->getDimensions().PHP_EOL; diff --git a/examples/embeddings-voyage.php b/examples/embeddings-voyage.php index 1e641b21..d01a6ce4 100755 --- a/examples/embeddings-voyage.php +++ b/examples/embeddings-voyage.php @@ -1,9 +1,9 @@ loadEnv(dirname(__DIR__).'/.env'); @@ -13,13 +13,15 @@ exit(1); } -$platform = new Platform(HttpClient::create(), $_ENV['VOYAGE_API_KEY']); -$embeddings = new Voyage($platform); +$platform = PlatformFactory::create($_ENV['VOYAGE_API_KEY']); +$embeddings = new Voyage(); -$vector = $embeddings->create(<<request($embeddings, <<getDimensions().PHP_EOL; +assert($response instanceof VectorResponse); + +echo 'Dimensions: '.$response->getContent()[0]->getDimensions().PHP_EOL; diff --git a/examples/image-describer-binary.php b/examples/image-describer-binary.php index 77aca01b..980c35bd 100755 --- a/examples/image-describer-binary.php +++ b/examples/image-describer-binary.php @@ -1,13 +1,12 @@ loadEnv(dirname(__DIR__).'/.env'); @@ -17,10 +16,10 @@ exit(1); } -$platform = new OpenAI(HttpClient::create(), $_ENV['OPENAI_API_KEY']); -$llm = new Gpt($platform, Gpt::GPT_4O_MINI); +$platform = PlatformFactory::create($_ENV['OPENAI_API_KEY']); +$llm = new GPT(GPT::GPT_4O_MINI); -$chain = new Chain($llm); +$chain = new Chain($platform, $llm); $messages = new MessageBag( Message::forSystem('You are an image analyzer bot that helps identify the content of images.'), Message::ofUser( diff --git a/examples/image-describer-url.php b/examples/image-describer-url.php index c51878ba..00038433 100755 --- a/examples/image-describer-url.php +++ b/examples/image-describer-url.php @@ -1,13 +1,12 @@ loadEnv(dirname(__DIR__).'/.env'); @@ -17,10 +16,10 @@ exit(1); } -$platform = new OpenAI(HttpClient::create(), $_ENV['OPENAI_API_KEY']); -$llm = new Gpt($platform, Gpt::GPT_4O_MINI); +$platform = PlatformFactory::create($_ENV['OPENAI_API_KEY']); +$llm = new GPT(GPT::GPT_4O_MINI); -$chain = new Chain($llm); +$chain = new Chain($platform, $llm); $messages = new MessageBag( Message::forSystem('You are an image analyzer bot that helps identify the content of images.'), Message::ofUser( diff --git a/examples/store-mongodb-similarity-search.php b/examples/store-mongodb-similarity-search.php index 90c35d68..78808da4 100755 --- a/examples/store-mongodb-similarity-search.php +++ b/examples/store-mongodb-similarity-search.php @@ -1,22 +1,21 @@ embed($documents); // initialize the index $store->initialize(); -$llm = new Gpt($platform, Gpt::GPT_4O_MINI); +$llm = new GPT(GPT::GPT_4O_MINI); -$similaritySearch = new SimilaritySearch($embeddings, $store); +$similaritySearch = new SimilaritySearch($platform, $embeddings, $store); $toolBox = new ToolBox(new ToolAnalyzer(), [$similaritySearch]); $processor = new ChainProcessor($toolBox); -$chain = new Chain($llm, [$processor], [$processor]); +$chain = new Chain($platform, $llm, [$processor], [$processor]); $messages = new MessageBag( Message::forSystem('Please answer all user questions only using SimilaritySearch function.'), diff --git a/examples/store-pinecone-similarity-search.php b/examples/store-pinecone-similarity-search.php index 5aa09144..5ecfb770 100755 --- a/examples/store-pinecone-similarity-search.php +++ b/examples/store-pinecone-similarity-search.php @@ -1,22 +1,21 @@ embed($documents); -$llm = new Gpt($platform, Gpt::GPT_4O_MINI); +$llm = new GPT(GPT::GPT_4O_MINI); -$similaritySearch = new SimilaritySearch($embeddings, $store); +$similaritySearch = new SimilaritySearch($platform, $embeddings, $store); $toolBox = new ToolBox(new ToolAnalyzer(), [$similaritySearch]); $processor = new ChainProcessor($toolBox); -$chain = new Chain($llm, [$processor], [$processor]); +$chain = new Chain($platform, $llm, [$processor], [$processor]); $messages = new MessageBag( Message::forSystem('Please answer all user questions only using SimilaritySearch function.'), diff --git a/examples/stream-claude-anthropic.php b/examples/stream-claude-anthropic.php index 9c6836ea..bc6573be 100644 --- a/examples/stream-claude-anthropic.php +++ b/examples/stream-claude-anthropic.php @@ -1,12 +1,11 @@ loadEnv(dirname(__DIR__).'/.env'); @@ -16,10 +15,10 @@ exit(1); } -$platform = new Anthropic(HttpClient::create(), $_ENV['ANTHROPIC_API_KEY']); -$llm = new Claude($platform); +$platform = PlatformFactory::create($_ENV['ANTHROPIC_API_KEY']); +$llm = new Claude(); -$chain = new Chain($llm); +$chain = new Chain($platform, $llm); $messages = new MessageBag( Message::forSystem('You are a thoughtful philosopher.'), Message::ofUser('What is the purpose of an ant?'), diff --git a/examples/stream-gpt-openai.php b/examples/stream-gpt-openai.php index c6565ea7..afe9b0a4 100644 --- a/examples/stream-gpt-openai.php +++ b/examples/stream-gpt-openai.php @@ -1,12 +1,11 @@ loadEnv(dirname(__DIR__).'/.env'); @@ -16,10 +15,10 @@ exit(1); } -$platform = new OpenAI(new EventSourceHttpClient(), $_ENV['OPENAI_API_KEY']); -$llm = new Gpt($platform, Gpt::GPT_4O_MINI); +$platform = PlatformFactory::create($_ENV['OPENAI_API_KEY']); +$llm = new GPT(GPT::GPT_4O_MINI); -$chain = new Chain($llm); +$chain = new Chain($platform, $llm); $messages = new MessageBag( Message::forSystem('You are a thoughtful philosopher.'), Message::ofUser('What is the purpose of an ant?'), diff --git a/examples/structured-output-clock.php b/examples/structured-output-clock.php index 475f4c6b..ac19feb4 100755 --- a/examples/structured-output-clock.php +++ b/examples/structured-output-clock.php @@ -1,19 +1,18 @@ call($messages, ['response_format' => [ diff --git a/examples/structured-output-math.php b/examples/structured-output-math.php index c833f2ef..65f372aa 100644 --- a/examples/structured-output-math.php +++ b/examples/structured-output-math.php @@ -1,15 +1,14 @@ loadEnv(dirname(__DIR__).'/.env'); @@ -21,13 +20,13 @@ exit(1); } -$platform = new OpenAI(HttpClient::create(), $_ENV['OPENAI_API_KEY']); -$llm = new Gpt($platform, Gpt::GPT_4O_MINI); +$platform = PlatformFactory::create($_ENV['OPENAI_API_KEY']); +$llm = new GPT(GPT::GPT_4O_MINI); $clock = new Clock(new SymfonyClock()); $toolBox = new ToolBox(new ToolAnalyzer(), [$clock]); $processor = new ChainProcessor($toolBox); -$chain = new Chain($llm, [$processor], [$processor]); +$chain = new Chain($platform, $llm, [$processor], [$processor]); $messages = new MessageBag(Message::ofUser('What date and time is it?')); $response = $chain->call($messages); diff --git a/examples/toolbox-serpapi.php b/examples/toolbox-serpapi.php index 94162e5e..e424f24f 100755 --- a/examples/toolbox-serpapi.php +++ b/examples/toolbox-serpapi.php @@ -1,14 +1,14 @@ call($messages); diff --git a/examples/toolbox-weather.php b/examples/toolbox-weather.php index 2729f785..fbcf6a16 100755 --- a/examples/toolbox-weather.php +++ b/examples/toolbox-weather.php @@ -1,14 +1,14 @@ call($messages); diff --git a/examples/toolbox-wikipedia.php b/examples/toolbox-wikipedia.php index 9af41672..09b2a010 100755 --- a/examples/toolbox-wikipedia.php +++ b/examples/toolbox-wikipedia.php @@ -1,14 +1,14 @@ call($messages); diff --git a/examples/toolbox-youtube.php b/examples/toolbox-youtube.php index c79a41bc..938030df 100755 --- a/examples/toolbox-youtube.php +++ b/examples/toolbox-youtube.php @@ -1,14 +1,14 @@ call($messages, [ diff --git a/phpstan.dist.neon b/phpstan.dist.neon index ae5c8505..f8cb0bcc 100644 --- a/phpstan.dist.neon +++ b/phpstan.dist.neon @@ -1,3 +1,6 @@ +includes: + - vendor/phpstan/phpstan-webmozart-assert/extension.neon + parameters: level: 6 paths: diff --git a/src/Bridge/Anthropic/Claude.php b/src/Bridge/Anthropic/Claude.php new file mode 100644 index 00000000..0c597920 --- /dev/null +++ b/src/Bridge/Anthropic/Claude.php @@ -0,0 +1,54 @@ + $options The default options for the model usage + */ + public function __construct( + private string $version = self::VERSION_35_SONNET, + private array $options = ['temperature' => 1.0, 'max_tokens' => 1000], + ) { + } + + public function getVersion(): string + { + return $this->version; + } + + public function getOptions(): array + { + return $this->options; + } + + public function supportsImageInput(): bool + { + return false; // it does, but implementation here is still open. + } + + public function supportsStreaming(): bool + { + return true; + } + + public function supportsStructuredOutput(): bool + { + return false; + } + + public function supportsToolCalling(): bool + { + return false; // it does, but implementation here is still open. + } +} diff --git a/src/Bridge/Anthropic/ModelHandler.php b/src/Bridge/Anthropic/ModelHandler.php new file mode 100644 index 00000000..088c921a --- /dev/null +++ b/src/Bridge/Anthropic/ModelHandler.php @@ -0,0 +1,84 @@ +httpClient = $httpClient instanceof EventSourceHttpClient ? $httpClient : new EventSourceHttpClient($httpClient); + } + + public function supports(Model $model, array|string|object $input): bool + { + return $model instanceof Claude && $input instanceof MessageBag; + } + + public function request(Model $model, object|array|string $input, array $options = []): ResponseInterface + { + Assert::isInstanceOf($input, MessageBag::class); + + $system = $input->getSystemMessage(); + $body = array_merge($options, [ + 'model' => $model->getVersion(), + 'system' => $system->content, + 'messages' => $input->withoutSystemMessage(), + ]); + + return $this->httpClient->request('POST', 'https://api.anthropic.com/v1/messages', [ + 'headers' => [ + 'x-api-key' => $this->apiKey, + 'anthropic-version' => $this->version, + ], + 'json' => $body, + ]); + } + + public function convert(ResponseInterface $response, array $options = []): LlmResponse + { + if ($options['stream'] ?? false) { + return new StreamResponse($this->convertStream($response)); + } + + $data = $response->toArray(); + + return new TextResponse($data['content'][0]['text']); + } + + private function convertStream(ResponseInterface $response): \Generator + { + foreach ((new EventSourceHttpClient())->stream($response) as $chunk) { + if (!$chunk instanceof ServerSentEvent || '[DONE]' === $chunk->getData()) { + continue; + } + + $data = $chunk->getArrayData(); + + if ('content_block_delta' != $data['type'] || !isset($data['delta']['text'])) { + continue; + } + + yield $data['delta']['text']; + } + } +} diff --git a/src/Bridge/Anthropic/PlatformFactory.php b/src/Bridge/Anthropic/PlatformFactory.php new file mode 100644 index 00000000..38e76868 --- /dev/null +++ b/src/Bridge/Anthropic/PlatformFactory.php @@ -0,0 +1,18 @@ +httpClient = $httpClient instanceof EventSourceHttpClient ? $httpClient : new EventSourceHttpClient($httpClient); + Assert::notStartsWith($baseUrl, 'http://', 'The base URL must not contain the protocol.'); + Assert::notStartsWith($baseUrl, 'https://', 'The base URL must not contain the protocol.'); + Assert::stringNotEmpty($deployment, 'The deployment must not be empty.'); + Assert::stringNotEmpty($apiVersion, 'The API version must not be empty.'); + Assert::stringNotEmpty($apiKey, 'The API key must not be empty.'); + } + + public function supports(Model $model, object|array|string $input): bool + { + return $model instanceof Embeddings; + } + + public function request(Model $model, object|array|string $input, array $options = []): ResponseInterface + { + $url = sprintf('https://%s/openai/deployments/%s/embeddings', $this->baseUrl, $this->deployment); + + return $this->httpClient->request('POST', $url, [ + 'headers' => [ + 'api-key' => $this->apiKey, + ], + 'query' => ['api-version' => $this->apiVersion], + 'json' => array_merge($options, [ + 'model' => $model->getVersion(), + 'input' => $input, + ]), + ]); + } +} diff --git a/src/Platform/OpenAI/Azure.php b/src/Bridge/Azure/OpenAI/GPTModelClient.php similarity index 64% rename from src/Platform/OpenAI/Azure.php rename to src/Bridge/Azure/OpenAI/GPTModelClient.php index b00409e8..8b32fecb 100644 --- a/src/Platform/OpenAI/Azure.php +++ b/src/Bridge/Azure/OpenAI/GPTModelClient.php @@ -2,14 +2,17 @@ declare(strict_types=1); -namespace PhpLlm\LlmChain\Platform\OpenAI; +namespace PhpLlm\LlmChain\Bridge\Azure\OpenAI; +use PhpLlm\LlmChain\Bridge\OpenAI\GPT; +use PhpLlm\LlmChain\Model\Model; +use PhpLlm\LlmChain\Platform\ModelClient; use Symfony\Component\HttpClient\EventSourceHttpClient; use Symfony\Contracts\HttpClient\HttpClientInterface; use Symfony\Contracts\HttpClient\ResponseInterface; use Webmozart\Assert\Assert; -final readonly class Azure extends AbstractPlatform implements Platform +final readonly class GPTModelClient implements ModelClient { private EventSourceHttpClient $httpClient; @@ -28,16 +31,24 @@ public function __construct( Assert::stringNotEmpty($apiKey, 'The API key must not be empty.'); } - protected function rawRequest(string $endpoint, array $body): ResponseInterface + public function supports(Model $model, object|array|string $input): bool { - $url = sprintf('https://%s/openai/deployments/%s/%s', $this->baseUrl, $this->deployment, $endpoint); + return $model instanceof GPT; + } + + public function request(Model $model, object|array|string $input, array $options = []): ResponseInterface + { + $url = sprintf('https://%s/openai/deployments/%s/chat/completions', $this->baseUrl, $this->deployment); return $this->httpClient->request('POST', $url, [ 'headers' => [ 'api-key' => $this->apiKey, ], 'query' => ['api-version' => $this->apiVersion], - 'json' => $body, + 'json' => array_merge($options, [ + 'model' => $model->getVersion(), + 'messages' => $input, + ]), ]); } } diff --git a/src/Bridge/Azure/OpenAI/PlatformFactory.php b/src/Bridge/Azure/OpenAI/PlatformFactory.php new file mode 100644 index 00000000..1cda68f1 --- /dev/null +++ b/src/Bridge/Azure/OpenAI/PlatformFactory.php @@ -0,0 +1,29 @@ + $options + */ + public function __construct( + private string $version = self::LLAMA_3_1_405B_INSTRUCT, + private array $options = [], + ) { + } + + public function getVersion(): string + { + return $this->version; + } + + public function getOptions(): array + { + return $this->options; + } + + public function supportsImageInput(): bool + { + return false; // it does, but implementation here is still open. + } + + public function supportsStreaming(): bool + { + return false; // it does, but implementation here is still open. + } + + public function supportsToolCalling(): bool + { + return false; // it does, but implementation here is still open. + } + + public function supportsStructuredOutput(): bool + { + return false; + } +} diff --git a/src/Model/Language/Llama.php b/src/Bridge/Meta/LlamaPromptConverter.php similarity index 52% rename from src/Model/Language/Llama.php rename to src/Bridge/Meta/LlamaPromptConverter.php index 9d87660d..195ad04a 100644 --- a/src/Model/Language/Llama.php +++ b/src/Bridge/Meta/LlamaPromptConverter.php @@ -2,47 +2,19 @@ declare(strict_types=1); -namespace PhpLlm\LlmChain\Model\Language; +namespace PhpLlm\LlmChain\Bridge\Meta; use PhpLlm\LlmChain\Exception\RuntimeException; -use PhpLlm\LlmChain\LanguageModel; -use PhpLlm\LlmChain\Message\AssistantMessage; -use PhpLlm\LlmChain\Message\Content\Image; -use PhpLlm\LlmChain\Message\Content\Text; -use PhpLlm\LlmChain\Message\MessageBag; -use PhpLlm\LlmChain\Message\SystemMessage; -use PhpLlm\LlmChain\Message\UserMessage; -use PhpLlm\LlmChain\Platform\Ollama; -use PhpLlm\LlmChain\Platform\Replicate; -use PhpLlm\LlmChain\Response\TextResponse; - -final readonly class Llama implements LanguageModel +use PhpLlm\LlmChain\Model\Message\AssistantMessage; +use PhpLlm\LlmChain\Model\Message\Content\Image; +use PhpLlm\LlmChain\Model\Message\Content\Text; +use PhpLlm\LlmChain\Model\Message\MessageBag; +use PhpLlm\LlmChain\Model\Message\SystemMessage; +use PhpLlm\LlmChain\Model\Message\UserMessage; + +final class LlamaPromptConverter { - public function __construct( - private Replicate|Ollama $platform, - ) { - } - - public function call(MessageBag $messages, array $options = []): TextResponse - { - if ($this->platform instanceof Replicate) { - $response = $this->platform->request('meta/meta-llama-3.1-405b-instruct', 'predictions', [ - 'system' => self::convertMessage($messages->getSystemMessage() ?? new SystemMessage('')), - 'prompt' => self::convertToPrompt($messages->withoutSystemMessage()), - ]); - - return new TextResponse(implode('', $response['output'])); - } - - $response = $this->platform->request('llama3.2', 'chat', ['messages' => $messages, 'stream' => false]); - - return new TextResponse($response['message']['content']); - } - - /** - * @todo make method private, just for testing, or create a MessageBag to LLama convert class :thinking: - */ - public static function convertToPrompt(MessageBag $messageBag): string + public function convertToPrompt(MessageBag $messageBag): string { $messages = []; @@ -56,10 +28,7 @@ public static function convertToPrompt(MessageBag $messageBag): string return trim(implode(PHP_EOL.PHP_EOL, $messages)).PHP_EOL.PHP_EOL.'<|start_header_id|>assistant<|end_header_id|>'; } - /** - * @todo make method private, just for testing - */ - public static function convertMessage(UserMessage|SystemMessage|AssistantMessage $message): string + public function convertMessage(UserMessage|SystemMessage|AssistantMessage $message): string { if ($message instanceof SystemMessage) { return trim(<<httpClient->request('POST', sprintf('%s/api/chat', $this->hostUrl), [ + 'headers' => ['Content-Type' => 'application/json'], + 'json' => [ + 'model' => $model->getVersion(), + 'messages' => $input, + 'stream' => false, + ], + ]); + } + + public function convert(ResponseInterface $response, array $options = []): LlmResponse + { + $data = $response->toArray(); + + return new TextResponse($data['message']['content']); + } +} diff --git a/src/Bridge/Ollama/PlatformFactory.php b/src/Bridge/Ollama/PlatformFactory.php new file mode 100644 index 00000000..472ef75e --- /dev/null +++ b/src/Bridge/Ollama/PlatformFactory.php @@ -0,0 +1,18 @@ + $options + */ + public function __construct( + private string $version = self::TEXT_3_SMALL, + private array $options = [], + ) { + } + + public function getVersion(): string + { + return $this->version; + } + + public function getOptions(): array + { + return $this->options; + } + + public function supportsMultipleInputs(): bool + { + return false; + } +} diff --git a/src/Bridge/OpenAI/Embeddings/ModelClient.php b/src/Bridge/OpenAI/Embeddings/ModelClient.php new file mode 100644 index 00000000..3ae64902 --- /dev/null +++ b/src/Bridge/OpenAI/Embeddings/ModelClient.php @@ -0,0 +1,40 @@ +httpClient->request('POST', 'https://api.openai.com/v1/embeddings', [ + 'auth_bearer' => $this->apiKey, + 'json' => array_merge($model->getOptions(), $options, [ + 'model' => $model->getVersion(), + 'input' => $input, + ]), + ]); + } +} diff --git a/src/Bridge/OpenAI/Embeddings/ResponseConverter.php b/src/Bridge/OpenAI/Embeddings/ResponseConverter.php new file mode 100644 index 00000000..659f62cd --- /dev/null +++ b/src/Bridge/OpenAI/Embeddings/ResponseConverter.php @@ -0,0 +1,27 @@ +toArray(); + + return new VectorResponse(new Vector($data['data'][0]['embedding'])); + } +} diff --git a/src/Bridge/OpenAI/GPT.php b/src/Bridge/OpenAI/GPT.php new file mode 100644 index 00000000..d4d64232 --- /dev/null +++ b/src/Bridge/OpenAI/GPT.php @@ -0,0 +1,67 @@ + $options The default options for the model usage + */ + public function __construct( + private readonly string $version = self::GPT_4O, + private readonly array $options = ['temperature' => 1.0], + private bool $supportsImageInput = false, + private bool $supportsStructuredOutput = false, + ) { + if (false === $this->supportsImageInput) { + $this->supportsImageInput = in_array($this->version, [self::GPT_4_TURBO, self::GPT_4O, self::GPT_4O_MINI, self::O1_MINI, self::O1_PREVIEW], true); + } + + if (false === $this->supportsStructuredOutput) { + $this->supportsStructuredOutput = in_array($this->version, [self::GPT_4O, self::GPT_4O_MINI], true); + } + } + + public function getVersion(): string + { + return $this->version; + } + + public function getOptions(): array + { + return $this->options; + } + + public function supportsImageInput(): bool + { + return $this->supportsImageInput; + } + + public function supportsStreaming(): bool + { + return true; + } + + public function supportsStructuredOutput(): bool + { + return $this->supportsStructuredOutput; + } + + public function supportsToolCalling(): bool + { + return true; + } +} diff --git a/src/Platform/OpenAI/OpenAI.php b/src/Bridge/OpenAI/GPT/ModelClient.php similarity index 51% rename from src/Platform/OpenAI/OpenAI.php rename to src/Bridge/OpenAI/GPT/ModelClient.php index 790fad75..dee1f80c 100644 --- a/src/Platform/OpenAI/OpenAI.php +++ b/src/Bridge/OpenAI/GPT/ModelClient.php @@ -2,14 +2,17 @@ declare(strict_types=1); -namespace PhpLlm\LlmChain\Platform\OpenAI; +namespace PhpLlm\LlmChain\Bridge\OpenAI\GPT; +use PhpLlm\LlmChain\Bridge\OpenAI\GPT; +use PhpLlm\LlmChain\Model\Model; +use PhpLlm\LlmChain\Platform\ModelClient as PlatformResponseFactory; use Symfony\Component\HttpClient\EventSourceHttpClient; use Symfony\Contracts\HttpClient\HttpClientInterface; use Symfony\Contracts\HttpClient\ResponseInterface; use Webmozart\Assert\Assert; -final readonly class OpenAI extends AbstractPlatform implements Platform +final readonly class ModelClient implements PlatformResponseFactory { private EventSourceHttpClient $httpClient; @@ -22,13 +25,19 @@ public function __construct( Assert::startsWith($apiKey, 'sk-', 'The API key must start with "sk-".'); } - protected function rawRequest(string $endpoint, array $body): ResponseInterface + public function supports(Model $model, array|string|object $input): bool { - $url = sprintf('https://api.openai.com/v1/%s', $endpoint); + return $model instanceof GPT; + } - return $this->httpClient->request('POST', $url, [ + public function request(Model $model, object|array|string $input, array $options = []): ResponseInterface + { + return $this->httpClient->request('POST', 'https://api.openai.com/v1/chat/completions', [ 'auth_bearer' => $this->apiKey, - 'json' => $body, + 'json' => array_merge($options, [ + 'model' => $model->getVersion(), + 'messages' => $input, + ]), ]); } } diff --git a/src/Model/Language/Gpt.php b/src/Bridge/OpenAI/GPT/ResponseConverter.php similarity index 54% rename from src/Model/Language/Gpt.php rename to src/Bridge/OpenAI/GPT/ResponseConverter.php index 425b318a..0dafa02a 100644 --- a/src/Model/Language/Gpt.php +++ b/src/Bridge/OpenAI/GPT/ResponseConverter.php @@ -2,77 +2,44 @@ declare(strict_types=1); -namespace PhpLlm\LlmChain\Model\Language; +namespace PhpLlm\LlmChain\Bridge\OpenAI\GPT; +use PhpLlm\LlmChain\Bridge\OpenAI\GPT; use PhpLlm\LlmChain\Exception\RuntimeException; -use PhpLlm\LlmChain\LanguageModel; -use PhpLlm\LlmChain\Message\MessageBag; -use PhpLlm\LlmChain\Platform\OpenAI\Platform; -use PhpLlm\LlmChain\Response\Choice; -use PhpLlm\LlmChain\Response\ChoiceResponse; -use PhpLlm\LlmChain\Response\ResponseInterface; -use PhpLlm\LlmChain\Response\StreamResponse; -use PhpLlm\LlmChain\Response\TextResponse; -use PhpLlm\LlmChain\Response\ToolCall; -use PhpLlm\LlmChain\Response\ToolCallResponse; - -final class Gpt implements LanguageModel +use PhpLlm\LlmChain\Model\Model; +use PhpLlm\LlmChain\Model\Response\Choice; +use PhpLlm\LlmChain\Model\Response\ChoiceResponse; +use PhpLlm\LlmChain\Model\Response\ResponseInterface as LlmResponse; +use PhpLlm\LlmChain\Model\Response\StreamResponse; +use PhpLlm\LlmChain\Model\Response\TextResponse; +use PhpLlm\LlmChain\Model\Response\ToolCall; +use PhpLlm\LlmChain\Model\Response\ToolCallResponse; +use PhpLlm\LlmChain\Platform\ResponseConverter as PlatformResponseConverter; +use Symfony\Component\HttpClient\Chunk\ServerSentEvent; +use Symfony\Component\HttpClient\EventSourceHttpClient; +use Symfony\Contracts\HttpClient\ResponseInterface as HttpResponse; + +final class ResponseConverter implements PlatformResponseConverter { - public const GPT_35_TURBO = 'gpt-3.5-turbo'; - public const GPT_35_TURBO_INSTRUCT = 'gpt-3.5-turbo-instruct'; - public const GPT_4 = 'gpt-4'; - public const GPT_4_TURBO = 'gpt-4-turbo'; - public const GPT_4O = 'gpt-4o'; - public const GPT_4O_MINI = 'gpt-4o-mini'; - public const O1_MINI = 'o1-mini'; - public const O1_PREVIEW = 'o1-preview'; - - /** - * @param array $options The default options for the model usage - */ - public function __construct( - private readonly Platform $platform, - private readonly string $version = self::GPT_4O, - private readonly array $options = ['temperature' => 1.0], - private bool $supportsImageInput = false, - private bool $supportsStructuredOutput = false, - ) { - if (false === $this->supportsImageInput) { - $this->supportsImageInput = in_array($this->version, [self::GPT_4_TURBO, self::GPT_4O, self::GPT_4O_MINI, self::O1_MINI, self::O1_PREVIEW], true); - } - - if (false === $this->supportsStructuredOutput) { - $this->supportsStructuredOutput = in_array($this->version, [self::GPT_4O, self::GPT_4O_MINI], true); - } + public function supports(Model $model, array|string|object $input): bool + { + return $model instanceof GPT; } - /** - * @param array $options The options to be used for this specific call. - * Can overwrite default options. - */ - public function call(MessageBag $messages, array $options = []): ResponseInterface + public function convert(HttpResponse $response, array $options = []): LlmResponse { - $body = array_merge($this->options, $options, [ - 'model' => $this->version, - 'messages' => $messages, - ]); - - $response = $this->platform->request('chat/completions', $body); - - if ($response instanceof \Generator) { - if ($this->streamIsToolCall($response)) { - return new ToolCallResponse(...$this->convertStreamToToolCalls($response)); - } else { - return new StreamResponse($this->convertStream($response)); - } + if ($options['stream'] ?? false) { + return $this->convertStream($response); } - if (!isset($response['choices'])) { + $data = $response->toArray(); + + if (!isset($data['choices'])) { throw new RuntimeException('Response does not contain choices'); } /** @var Choice[] $choices */ - $choices = array_map([$this, 'convertChoice'], $response['choices']); + $choices = array_map([$this, 'convertChoice'], $data['choices']); if (1 !== count($choices)) { return new ChoiceResponse(...$choices); @@ -85,19 +52,26 @@ public function call(MessageBag $messages, array $options = []): ResponseInterfa return new TextResponse($choices[0]->getContent()); } - public function supportsToolCalling(): bool + private function convertStream(HttpResponse $response): ToolCallResponse|StreamResponse { - return true; - } + $stream = $this->streamResponse($response); - public function supportsImageInput(): bool - { - return $this->supportsImageInput; + if ($this->streamIsToolCall($stream)) { + return new ToolCallResponse(...$this->convertStreamToToolCalls($stream)); + } else { + return new StreamResponse($this->convertStreamContent($stream)); + } } - public function supportsStructuredOutput(): bool + private function streamResponse(HttpResponse $response): \Generator { - return $this->supportsStructuredOutput; + foreach ((new EventSourceHttpClient())->stream($response) as $chunk) { + if (!$chunk instanceof ServerSentEvent || '[DONE]' === $chunk->getData()) { + continue; + } + + yield $chunk->getArrayData(); + } } private function streamIsToolCall(\Generator $response): bool @@ -107,17 +81,6 @@ private function streamIsToolCall(\Generator $response): bool return isset($data['choices'][0]['delta']['tool_calls']); } - private function convertStream(\Generator $generator): \Generator - { - foreach ($generator as $data) { - if (!isset($data['choices'][0]['delta']['content'])) { - continue; - } - - yield $data['choices'][0]['delta']['content']; - } - } - /** * @return ToolCall[] */ @@ -147,6 +110,17 @@ private function convertStreamToToolCalls(\Generator $response): array return array_map([$this, 'convertToolCall'], $toolCalls); } + private function convertStreamContent(\Generator $generator): \Generator + { + foreach ($generator as $data) { + if (!isset($data['choices'][0]['delta']['content'])) { + continue; + } + + yield $data['choices'][0]['delta']['content']; + } + } + /** * @param array{ * index: integer, diff --git a/src/Bridge/OpenAI/PlatformFactory.php b/src/Bridge/OpenAI/PlatformFactory.php new file mode 100644 index 00000000..d09e1f11 --- /dev/null +++ b/src/Bridge/OpenAI/PlatformFactory.php @@ -0,0 +1,25 @@ + $body - * - * @return array */ - public function request(string $model, string $endpoint, array $body): array + public function request(string $model, string $endpoint, array $body): ResponseInterface { $url = sprintf('https://api.replicate.com/v1/models/%s/%s', $model, $endpoint); @@ -28,29 +29,26 @@ public function request(string $model, string $endpoint, array $body): array 'headers' => ['Content-Type' => 'application/json'], 'auth_bearer' => $this->apiKey, 'json' => ['input' => $body], - ])->toArray(); + ]); + $data = $response->toArray(); - while (!in_array($response['status'], ['succeeded', 'failed', 'canceled'], true)) { - sleep(1); + while (!in_array($data['status'], ['succeeded', 'failed', 'canceled'], true)) { + $this->clock->sleep(1); // we need to wait until the prediction is ready - $response = $this->getResponse($response['id']); + $response = $this->getResponse($data['id']); + $data = $response->toArray(); } return $response; } - /** - * @return array - */ - private function getResponse(string $id): array + private function getResponse(string $id): ResponseInterface { $url = sprintf('https://api.replicate.com/v1/predictions/%s', $id); - $response = $this->httpClient->request('GET', $url, [ + return $this->httpClient->request('GET', $url, [ 'headers' => ['Content-Type' => 'application/json'], 'auth_bearer' => $this->apiKey, ]); - - return $response->toArray(); } } diff --git a/src/Bridge/Replicate/LlamaModelClient.php b/src/Bridge/Replicate/LlamaModelClient.php new file mode 100644 index 00000000..4a779fa0 --- /dev/null +++ b/src/Bridge/Replicate/LlamaModelClient.php @@ -0,0 +1,39 @@ +client->request(sprintf('meta/meta-%s', $model->getVersion()), 'predictions', [ + 'system' => $this->promptConverter->convertMessage($input->getSystemMessage() ?? new SystemMessage('')), + 'prompt' => $this->promptConverter->convertToPrompt($input->withoutSystemMessage()), + ]); + } +} diff --git a/src/Bridge/Replicate/LlamaResponseConverter.php b/src/Bridge/Replicate/LlamaResponseConverter.php new file mode 100644 index 00000000..511048ac --- /dev/null +++ b/src/Bridge/Replicate/LlamaResponseConverter.php @@ -0,0 +1,28 @@ +toArray(); + + return new TextResponse(implode('', $data['output'])); + } +} diff --git a/src/Bridge/Replicate/PlatformFactory.php b/src/Bridge/Replicate/PlatformFactory.php new file mode 100644 index 00000000..e5323e5c --- /dev/null +++ b/src/Bridge/Replicate/PlatformFactory.php @@ -0,0 +1,21 @@ +httpClient->request('POST', 'https://api.voyageai.com/v1/embeddings', [ + 'auth_bearer' => $this->apiKey, + 'json' => [ + 'model' => $model->getVersion(), + 'input' => $input, + ], + ]); + } + + public function convert(ResponseInterface $response, array $options = []): LlmResponse + { + $response = $response->toArray(); + + $vectors = array_map(fn (array $data) => new Vector($data['embedding']), $response['data']); + + return new VectorResponse($vectors[0]); + } +} diff --git a/src/Bridge/Voyage/PlatformFactory.php b/src/Bridge/Voyage/PlatformFactory.php new file mode 100644 index 00000000..c3ef9fb0 --- /dev/null +++ b/src/Bridge/Voyage/PlatformFactory.php @@ -0,0 +1,18 @@ + $options + */ + public function __construct( + private string $version = self::VERSION_V3, + private array $options = [], + ) { + } + + public function getVersion(): string + { + return $this->version; + } + + public function getOptions(): array + { + return $this->options; + } + + public function supportsMultipleInputs(): bool + { + return true; + } +} diff --git a/src/Chain.php b/src/Chain.php index 4c27c036..e3721f14 100644 --- a/src/Chain.php +++ b/src/Chain.php @@ -11,8 +11,9 @@ use PhpLlm\LlmChain\Chain\OutputProcessor; use PhpLlm\LlmChain\Exception\InvalidArgumentException; use PhpLlm\LlmChain\Exception\MissingModelSupport; -use PhpLlm\LlmChain\Message\MessageBag; -use PhpLlm\LlmChain\Response\ResponseInterface; +use PhpLlm\LlmChain\Model\LanguageModel; +use PhpLlm\LlmChain\Model\Message\MessageBag; +use PhpLlm\LlmChain\Model\Response\ResponseInterface; final readonly class Chain implements ChainInterface { @@ -31,6 +32,7 @@ * @param OutputProcessor[] $outputProcessor */ public function __construct( + private Platform $platform, private LanguageModel $llm, iterable $inputProcessor = [], iterable $outputProcessor = [], @@ -51,7 +53,7 @@ public function call(MessageBag $messages, array $options = []): ResponseInterfa throw MissingModelSupport::forImageInput($this->llm::class); } - $response = $this->llm->call($messages, $options = $input->getOptions()); + $response = $this->platform->request($this->llm, $messages, $options = $input->getOptions()); $output = new Output($this->llm, $response, $messages, $options); array_map(fn (OutputProcessor $processor) => $processor->processOutput($output), $this->outputProcessor); diff --git a/src/Chain/Input.php b/src/Chain/Input.php index 9109e8fe..ecfb73aa 100644 --- a/src/Chain/Input.php +++ b/src/Chain/Input.php @@ -4,8 +4,8 @@ namespace PhpLlm\LlmChain\Chain; -use PhpLlm\LlmChain\LanguageModel; -use PhpLlm\LlmChain\Message\MessageBag; +use PhpLlm\LlmChain\Model\LanguageModel; +use PhpLlm\LlmChain\Model\Message\MessageBag; final class Input { diff --git a/src/Chain/Output.php b/src/Chain/Output.php index a5877bf6..438eb5f0 100644 --- a/src/Chain/Output.php +++ b/src/Chain/Output.php @@ -4,9 +4,9 @@ namespace PhpLlm\LlmChain\Chain; -use PhpLlm\LlmChain\LanguageModel; -use PhpLlm\LlmChain\Message\MessageBag; -use PhpLlm\LlmChain\Response\ResponseInterface; +use PhpLlm\LlmChain\Model\LanguageModel; +use PhpLlm\LlmChain\Model\Message\MessageBag; +use PhpLlm\LlmChain\Model\Response\ResponseInterface; final class Output { diff --git a/src/StructuredOutput/ChainProcessor.php b/src/Chain/StructuredOutput/ChainProcessor.php similarity index 95% rename from src/StructuredOutput/ChainProcessor.php rename to src/Chain/StructuredOutput/ChainProcessor.php index 901c99bd..ba52b23b 100644 --- a/src/StructuredOutput/ChainProcessor.php +++ b/src/Chain/StructuredOutput/ChainProcessor.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace PhpLlm\LlmChain\StructuredOutput; +namespace PhpLlm\LlmChain\Chain\StructuredOutput; use PhpLlm\LlmChain\Chain\Input; use PhpLlm\LlmChain\Chain\InputProcessor; @@ -10,7 +10,7 @@ use PhpLlm\LlmChain\Chain\OutputProcessor; use PhpLlm\LlmChain\Exception\InvalidArgumentException; use PhpLlm\LlmChain\Exception\MissingModelSupport; -use PhpLlm\LlmChain\Response\StructuredResponse; +use PhpLlm\LlmChain\Model\Response\StructuredResponse; use Symfony\Component\Serializer\SerializerInterface; final class ChainProcessor implements InputProcessor, OutputProcessor diff --git a/src/StructuredOutput/ResponseFormatFactory.php b/src/Chain/StructuredOutput/ResponseFormatFactory.php similarity index 92% rename from src/StructuredOutput/ResponseFormatFactory.php rename to src/Chain/StructuredOutput/ResponseFormatFactory.php index 24a79fa6..328c954d 100644 --- a/src/StructuredOutput/ResponseFormatFactory.php +++ b/src/Chain/StructuredOutput/ResponseFormatFactory.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace PhpLlm\LlmChain\StructuredOutput; +namespace PhpLlm\LlmChain\Chain\StructuredOutput; use function Symfony\Component\String\u; diff --git a/src/StructuredOutput/ResponseFormatFactoryInterface.php b/src/Chain/StructuredOutput/ResponseFormatFactoryInterface.php similarity index 89% rename from src/StructuredOutput/ResponseFormatFactoryInterface.php rename to src/Chain/StructuredOutput/ResponseFormatFactoryInterface.php index f533eec7..7c13d121 100644 --- a/src/StructuredOutput/ResponseFormatFactoryInterface.php +++ b/src/Chain/StructuredOutput/ResponseFormatFactoryInterface.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace PhpLlm\LlmChain\StructuredOutput; +namespace PhpLlm\LlmChain\Chain\StructuredOutput; interface ResponseFormatFactoryInterface { diff --git a/src/StructuredOutput/SchemaFactory.php b/src/Chain/StructuredOutput/SchemaFactory.php similarity index 98% rename from src/StructuredOutput/SchemaFactory.php rename to src/Chain/StructuredOutput/SchemaFactory.php index 96f1c683..57e2c156 100644 --- a/src/StructuredOutput/SchemaFactory.php +++ b/src/Chain/StructuredOutput/SchemaFactory.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace PhpLlm\LlmChain\StructuredOutput; +namespace PhpLlm\LlmChain\Chain\StructuredOutput; use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor; use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor; diff --git a/src/ToolBox/Attribute/AsTool.php b/src/Chain/ToolBox/Attribute/AsTool.php similarity index 85% rename from src/ToolBox/Attribute/AsTool.php rename to src/Chain/ToolBox/Attribute/AsTool.php index 2c0f652c..9af84a26 100644 --- a/src/ToolBox/Attribute/AsTool.php +++ b/src/Chain/ToolBox/Attribute/AsTool.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace PhpLlm\LlmChain\ToolBox\Attribute; +namespace PhpLlm\LlmChain\Chain\ToolBox\Attribute; #[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE)] final readonly class AsTool diff --git a/src/ToolBox/Attribute/ToolParameter.php b/src/Chain/ToolBox/Attribute/ToolParameter.php similarity index 98% rename from src/ToolBox/Attribute/ToolParameter.php rename to src/Chain/ToolBox/Attribute/ToolParameter.php index efe493f3..f6bb35f0 100644 --- a/src/ToolBox/Attribute/ToolParameter.php +++ b/src/Chain/ToolBox/Attribute/ToolParameter.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace PhpLlm\LlmChain\ToolBox\Attribute; +namespace PhpLlm\LlmChain\Chain\ToolBox\Attribute; use Webmozart\Assert\Assert; diff --git a/src/ToolBox/ChainProcessor.php b/src/Chain/ToolBox/ChainProcessor.php similarity index 91% rename from src/ToolBox/ChainProcessor.php rename to src/Chain/ToolBox/ChainProcessor.php index 3faf91cf..15293d9c 100644 --- a/src/ToolBox/ChainProcessor.php +++ b/src/Chain/ToolBox/ChainProcessor.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace PhpLlm\LlmChain\ToolBox; +namespace PhpLlm\LlmChain\Chain\ToolBox; use PhpLlm\LlmChain\Chain\ChainAwareProcessor; use PhpLlm\LlmChain\Chain\ChainAwareTrait; @@ -11,8 +11,8 @@ use PhpLlm\LlmChain\Chain\Output; use PhpLlm\LlmChain\Chain\OutputProcessor; use PhpLlm\LlmChain\Exception\MissingModelSupport; -use PhpLlm\LlmChain\Message\Message; -use PhpLlm\LlmChain\Response\ToolCallResponse; +use PhpLlm\LlmChain\Model\Message\Message; +use PhpLlm\LlmChain\Model\Response\ToolCallResponse; final class ChainProcessor implements InputProcessor, OutputProcessor, ChainAwareProcessor { diff --git a/src/ToolBox/Metadata.php b/src/Chain/ToolBox/Metadata.php similarity index 96% rename from src/ToolBox/Metadata.php rename to src/Chain/ToolBox/Metadata.php index a4884f6b..caca3306 100644 --- a/src/ToolBox/Metadata.php +++ b/src/Chain/ToolBox/Metadata.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace PhpLlm\LlmChain\ToolBox; +namespace PhpLlm\LlmChain\Chain\ToolBox; /** * @phpstan-import-type ParameterDefinition from ParameterAnalyzer diff --git a/src/ToolBox/ParameterAnalyzer.php b/src/Chain/ToolBox/ParameterAnalyzer.php similarity index 96% rename from src/ToolBox/ParameterAnalyzer.php rename to src/Chain/ToolBox/ParameterAnalyzer.php index 21402191..13882233 100644 --- a/src/ToolBox/ParameterAnalyzer.php +++ b/src/Chain/ToolBox/ParameterAnalyzer.php @@ -2,9 +2,9 @@ declare(strict_types=1); -namespace PhpLlm\LlmChain\ToolBox; +namespace PhpLlm\LlmChain\Chain\ToolBox; -use PhpLlm\LlmChain\ToolBox\Attribute\ToolParameter; +use PhpLlm\LlmChain\Chain\ToolBox\Attribute\ToolParameter; /** * @phpstan-type ParameterDefinition array{ diff --git a/src/ToolBox/Tool/Clock.php b/src/Chain/ToolBox/Tool/Clock.php similarity index 84% rename from src/ToolBox/Tool/Clock.php rename to src/Chain/ToolBox/Tool/Clock.php index ad356ff9..fee7197a 100644 --- a/src/ToolBox/Tool/Clock.php +++ b/src/Chain/ToolBox/Tool/Clock.php @@ -2,9 +2,9 @@ declare(strict_types=1); -namespace PhpLlm\LlmChain\ToolBox\Tool; +namespace PhpLlm\LlmChain\Chain\ToolBox\Tool; -use PhpLlm\LlmChain\ToolBox\Attribute\AsTool; +use PhpLlm\LlmChain\Chain\ToolBox\Attribute\AsTool; use Symfony\Component\Clock\ClockInterface; #[AsTool('clock', description: 'Provides the current date and time.')] diff --git a/src/ToolBox/Tool/OpenMeteo.php b/src/Chain/ToolBox/Tool/OpenMeteo.php similarity index 89% rename from src/ToolBox/Tool/OpenMeteo.php rename to src/Chain/ToolBox/Tool/OpenMeteo.php index 453c8992..b741769e 100644 --- a/src/ToolBox/Tool/OpenMeteo.php +++ b/src/Chain/ToolBox/Tool/OpenMeteo.php @@ -2,9 +2,9 @@ declare(strict_types=1); -namespace PhpLlm\LlmChain\ToolBox\Tool; +namespace PhpLlm\LlmChain\Chain\ToolBox\Tool; -use PhpLlm\LlmChain\ToolBox\Attribute\AsTool; +use PhpLlm\LlmChain\Chain\ToolBox\Attribute\AsTool; use Symfony\Contracts\HttpClient\HttpClientInterface; #[AsTool(name: 'weather', description: 'get the current weather for a location')] diff --git a/src/ToolBox/Tool/SerpApi.php b/src/Chain/ToolBox/Tool/SerpApi.php similarity index 91% rename from src/ToolBox/Tool/SerpApi.php rename to src/Chain/ToolBox/Tool/SerpApi.php index 9229e3d1..41a2bf16 100644 --- a/src/ToolBox/Tool/SerpApi.php +++ b/src/Chain/ToolBox/Tool/SerpApi.php @@ -2,9 +2,9 @@ declare(strict_types=1); -namespace PhpLlm\LlmChain\ToolBox\Tool; +namespace PhpLlm\LlmChain\Chain\ToolBox\Tool; -use PhpLlm\LlmChain\ToolBox\Attribute\AsTool; +use PhpLlm\LlmChain\Chain\ToolBox\Attribute\AsTool; use Symfony\Contracts\HttpClient\HttpClientInterface; #[AsTool(name: 'serpapi', description: 'search for information on the internet')] diff --git a/src/ToolBox/Tool/SimilaritySearch.php b/src/Chain/ToolBox/Tool/SimilaritySearch.php similarity index 72% rename from src/ToolBox/Tool/SimilaritySearch.php rename to src/Chain/ToolBox/Tool/SimilaritySearch.php index 0248a302..880b9b9d 100644 --- a/src/ToolBox/Tool/SimilaritySearch.php +++ b/src/Chain/ToolBox/Tool/SimilaritySearch.php @@ -2,12 +2,14 @@ declare(strict_types=1); -namespace PhpLlm\LlmChain\ToolBox\Tool; +namespace PhpLlm\LlmChain\Chain\ToolBox\Tool; +use PhpLlm\LlmChain\Chain\ToolBox\Attribute\AsTool; +use PhpLlm\LlmChain\Document\Vector; use PhpLlm\LlmChain\Document\VectorDocument; -use PhpLlm\LlmChain\EmbeddingsModel; +use PhpLlm\LlmChain\Model\EmbeddingsModel; +use PhpLlm\LlmChain\Platform; use PhpLlm\LlmChain\Store\VectorStoreInterface; -use PhpLlm\LlmChain\ToolBox\Attribute\AsTool; #[AsTool('similarity_search', description: 'Searches for documents similar to a query or sentence.')] final class SimilaritySearch @@ -18,6 +20,7 @@ final class SimilaritySearch public array $usedDocuments = []; public function __construct( + private readonly Platform $platform, private readonly EmbeddingsModel $embeddings, private readonly VectorStoreInterface $vectorStore, ) { @@ -28,8 +31,9 @@ public function __construct( */ public function __invoke(string $searchTerm): string { - $vector = $this->embeddings->create($searchTerm); - $this->usedDocuments = $this->vectorStore->query($vector); + /** @var Vector[] $vectors */ + $vectors = $this->platform->request($this->embeddings, $searchTerm)->getContent(); + $this->usedDocuments = $this->vectorStore->query($vectors[0]); if (0 === count($this->usedDocuments)) { return 'No results found'; diff --git a/src/ToolBox/Tool/Wikipedia.php b/src/Chain/ToolBox/Tool/Wikipedia.php similarity index 95% rename from src/ToolBox/Tool/Wikipedia.php rename to src/Chain/ToolBox/Tool/Wikipedia.php index 19bb0082..119371aa 100644 --- a/src/ToolBox/Tool/Wikipedia.php +++ b/src/Chain/ToolBox/Tool/Wikipedia.php @@ -2,9 +2,9 @@ declare(strict_types=1); -namespace PhpLlm\LlmChain\ToolBox\Tool; +namespace PhpLlm\LlmChain\Chain\ToolBox\Tool; -use PhpLlm\LlmChain\ToolBox\Attribute\AsTool; +use PhpLlm\LlmChain\Chain\ToolBox\Attribute\AsTool; use Symfony\Contracts\HttpClient\HttpClientInterface; #[AsTool('wikipedia_search', description: 'Searches Wikipedia for a given query', method: 'search')] diff --git a/src/ToolBox/Tool/YouTubeTranscriber.php b/src/Chain/ToolBox/Tool/YouTubeTranscriber.php similarity index 96% rename from src/ToolBox/Tool/YouTubeTranscriber.php rename to src/Chain/ToolBox/Tool/YouTubeTranscriber.php index 88c6989b..0b634884 100644 --- a/src/ToolBox/Tool/YouTubeTranscriber.php +++ b/src/Chain/ToolBox/Tool/YouTubeTranscriber.php @@ -2,11 +2,11 @@ declare(strict_types=1); -namespace PhpLlm\LlmChain\ToolBox\Tool; +namespace PhpLlm\LlmChain\Chain\ToolBox\Tool; +use PhpLlm\LlmChain\Chain\ToolBox\Attribute\AsTool; use PhpLlm\LlmChain\Exception\LogicException; use PhpLlm\LlmChain\Exception\RuntimeException; -use PhpLlm\LlmChain\ToolBox\Attribute\AsTool; use Symfony\Component\CssSelector\CssSelectorConverter; use Symfony\Component\DomCrawler\Crawler; use Symfony\Contracts\HttpClient\HttpClientInterface; diff --git a/src/ToolBox/ToolAnalyzer.php b/src/Chain/ToolBox/ToolAnalyzer.php similarity index 92% rename from src/ToolBox/ToolAnalyzer.php rename to src/Chain/ToolBox/ToolAnalyzer.php index 4b3a8413..1d02e216 100644 --- a/src/ToolBox/ToolAnalyzer.php +++ b/src/Chain/ToolBox/ToolAnalyzer.php @@ -2,10 +2,10 @@ declare(strict_types=1); -namespace PhpLlm\LlmChain\ToolBox; +namespace PhpLlm\LlmChain\Chain\ToolBox; +use PhpLlm\LlmChain\Chain\ToolBox\Attribute\AsTool; use PhpLlm\LlmChain\Exception\InvalidToolImplementation; -use PhpLlm\LlmChain\ToolBox\Attribute\AsTool; final readonly class ToolAnalyzer { diff --git a/src/ToolBox/ToolBox.php b/src/Chain/ToolBox/ToolBox.php similarity index 93% rename from src/ToolBox/ToolBox.php rename to src/Chain/ToolBox/ToolBox.php index dc87109e..bc3f852e 100644 --- a/src/ToolBox/ToolBox.php +++ b/src/Chain/ToolBox/ToolBox.php @@ -2,10 +2,10 @@ declare(strict_types=1); -namespace PhpLlm\LlmChain\ToolBox; +namespace PhpLlm\LlmChain\Chain\ToolBox; use PhpLlm\LlmChain\Exception\RuntimeException; -use PhpLlm\LlmChain\Response\ToolCall; +use PhpLlm\LlmChain\Model\Response\ToolCall; final class ToolBox implements ToolBoxInterface { diff --git a/src/ToolBox/ToolBoxInterface.php b/src/Chain/ToolBox/ToolBoxInterface.php similarity index 70% rename from src/ToolBox/ToolBoxInterface.php rename to src/Chain/ToolBox/ToolBoxInterface.php index 3ec7f73e..ed847653 100644 --- a/src/ToolBox/ToolBoxInterface.php +++ b/src/Chain/ToolBox/ToolBoxInterface.php @@ -2,9 +2,9 @@ declare(strict_types=1); -namespace PhpLlm\LlmChain\ToolBox; +namespace PhpLlm\LlmChain\Chain\ToolBox; -use PhpLlm\LlmChain\Response\ToolCall; +use PhpLlm\LlmChain\Model\Response\ToolCall; interface ToolBoxInterface { diff --git a/src/ChainInterface.php b/src/ChainInterface.php index c8815e1d..c152fb9c 100644 --- a/src/ChainInterface.php +++ b/src/ChainInterface.php @@ -4,8 +4,8 @@ namespace PhpLlm\LlmChain; -use PhpLlm\LlmChain\Message\MessageBag; -use PhpLlm\LlmChain\Response\ResponseInterface; +use PhpLlm\LlmChain\Model\Message\MessageBag; +use PhpLlm\LlmChain\Model\Response\ResponseInterface; interface ChainInterface { diff --git a/src/DocumentEmbedder.php b/src/Embedder.php similarity index 51% rename from src/DocumentEmbedder.php rename to src/Embedder.php index a9474ddb..814299c2 100644 --- a/src/DocumentEmbedder.php +++ b/src/Embedder.php @@ -6,17 +6,19 @@ use PhpLlm\LlmChain\Document\TextDocument; use PhpLlm\LlmChain\Document\VectorDocument; +use PhpLlm\LlmChain\Model\EmbeddingsModel; use PhpLlm\LlmChain\Store\StoreInterface; use Psr\Log\LoggerInterface; use Psr\Log\NullLogger; use Symfony\Component\Clock\Clock; use Symfony\Component\Clock\ClockInterface; -final readonly class DocumentEmbedder +final readonly class Embedder { private ClockInterface $clock; public function __construct( + private Platform $platform, private EmbeddingsModel $embeddings, private StoreInterface $store, ?ClockInterface $clock = null, @@ -43,18 +45,42 @@ public function embed(TextDocument|array $documents, int $chunkSize = 0, int $sl $chunks = 0 !== $chunkSize ? array_chunk($documents, $chunkSize) : [$documents]; foreach ($chunks as $chunk) { - $vectors = $this->embeddings->multiCreate(array_map(fn (TextDocument $document) => $document->content, $chunk)); + $this->store->add(...$this->createVectorDocuments($chunk)); - $vectorDocuments = []; - foreach ($chunk as $i => $document) { - $vectorDocuments[] = new VectorDocument($document->id, $vectors[$i], $document->metadata); + if (0 !== $sleep) { + $this->clock->sleep($sleep); } + } + } + + /** + * @param TextDocument[] $documents + * + * @return VectorDocument[] + */ + private function createVectorDocuments(array $documents): array + { + if ($this->embeddings->supportsMultipleInputs()) { + $response = $this->platform->request($this->embeddings, array_map(fn (TextDocument $document) => $document->content, $documents)); - $this->store->add(...$vectorDocuments); + $vectors = $response->getContent(); + } else { + $responses = []; + foreach ($documents as $document) { + $responses[] = $this->platform->request($this->embeddings, $document->content); + } - if (0 !== $sleep) { - $this->clock->sleep($sleep); + $vectors = []; + foreach ($responses as $response) { + $vectors = array_merge($vectors, $response->getContent()); } } + + $vectorDocuments = []; + foreach ($documents as $i => $document) { + $vectorDocuments[] = new VectorDocument($document->id, $vectors[$i], $document->metadata); + } + + return $vectorDocuments; } } diff --git a/src/EmbeddingsModel.php b/src/EmbeddingsModel.php deleted file mode 100644 index 859e37a2..00000000 --- a/src/EmbeddingsModel.php +++ /dev/null @@ -1,23 +0,0 @@ - $options - */ - public function create(string $text, array $options = []): Vector; - - /** - * @param list $texts - * @param array $options - * - * @return Vector[] - */ - public function multiCreate(array $texts, array $options = []): array; -} diff --git a/src/Exception/InvalidToolImplementation.php b/src/Exception/InvalidToolImplementation.php index 4035a22b..703633a0 100644 --- a/src/Exception/InvalidToolImplementation.php +++ b/src/Exception/InvalidToolImplementation.php @@ -4,7 +4,7 @@ namespace PhpLlm\LlmChain\Exception; -use PhpLlm\LlmChain\ToolBox\Attribute\AsTool; +use PhpLlm\LlmChain\Chain\ToolBox\Attribute\AsTool; final class InvalidToolImplementation extends InvalidArgumentException { diff --git a/src/LanguageModel.php b/src/LanguageModel.php deleted file mode 100644 index 16e8ac3e..00000000 --- a/src/LanguageModel.php +++ /dev/null @@ -1,22 +0,0 @@ - $options - */ - public function call(MessageBag $messages, array $options = []): ResponseInterface; - - public function supportsToolCalling(): bool; - - public function supportsImageInput(): bool; - - public function supportsStructuredOutput(): bool; -} diff --git a/src/Model/Embeddings/OpenAI.php b/src/Model/Embeddings/OpenAI.php deleted file mode 100644 index ad9b37fd..00000000 --- a/src/Model/Embeddings/OpenAI.php +++ /dev/null @@ -1,60 +0,0 @@ -platform->request('embeddings', $this->createBody($text)); - - return $this->extractVector($response); - } - - public function multiCreate(array $texts, array $options = []): array - { - $bodies = array_map([$this, 'createBody'], $texts); - - $vectors = []; - foreach ($this->platform->multiRequest('embeddings', $bodies) as $response) { - $vectors[] = $this->extractVector($response); - } - - return $vectors; - } - - /** - * @return array{model: non-empty-string, input: string} - */ - private function createBody(string $text): array - { - return [ - 'model' => $this->version, - 'input' => $text, - ]; - } - - /** - * @param array $data - */ - private function extractVector(array $data): Vector - { - return new Vector($data['data'][0]['embedding']); - } -} diff --git a/src/Model/Embeddings/Voyage.php b/src/Model/Embeddings/Voyage.php deleted file mode 100644 index 2cae5262..00000000 --- a/src/Model/Embeddings/Voyage.php +++ /dev/null @@ -1,42 +0,0 @@ -multiCreate([$text], $options); - - return $vectors[0]; - } - - public function multiCreate(array $texts, array $options = []): array - { - $response = $this->platform->request(array_merge($options, [ - 'model' => $this->version, - 'input' => $texts, - ])); - - return array_map(fn (array $data) => new Vector($data['embedding']), $response['data']); - } -} diff --git a/src/Model/EmbeddingsModel.php b/src/Model/EmbeddingsModel.php new file mode 100644 index 00000000..3f354410 --- /dev/null +++ b/src/Model/EmbeddingsModel.php @@ -0,0 +1,10 @@ + $options The default options for the model usage - */ - public function __construct( - private Anthropic $platform, - private string $version = self::VERSION_35_SONNET, - private array $options = ['temperature' => 1.0, 'max_tokens' => 1000], - ) { - } - - /** - * @param array $options The options to be used for this specific call. - * Can overwrite default options. - */ - public function call(MessageBag $messages, array $options = []): ResponseInterface - { - $system = $messages->getSystemMessage(); - $body = array_merge($this->options, $options, [ - 'model' => $this->version, - 'system' => $system->content, - 'messages' => $messages->withoutSystemMessage(), - ]); - - $response = $this->platform->request($body); - - if ($response instanceof \Generator) { - return new StreamResponse($this->convertStream($response)); - } - - return new TextResponse($response['content'][0]['text']); - } - - public function supportsToolCalling(): bool - { - return false; // it does, but implementation here is still open. - } - - public function supportsImageInput(): bool - { - return false; // it does, but implementation here is still open. - } - - public function supportsStructuredOutput(): bool - { - return false; - } - - private function convertStream(\Generator $generator): \Generator - { - foreach ($generator as $data) { - if ('content_block_delta' != $data['type'] || !isset($data['delta']['text'])) { - continue; - } - - yield $data['delta']['text']; - } - } -} diff --git a/src/Model/LanguageModel.php b/src/Model/LanguageModel.php new file mode 100644 index 00000000..d9794368 --- /dev/null +++ b/src/Model/LanguageModel.php @@ -0,0 +1,16 @@ + diff --git a/src/Message/MessageInterface.php b/src/Model/Message/MessageInterface.php similarity index 75% rename from src/Message/MessageInterface.php rename to src/Model/Message/MessageInterface.php index a6f73d0e..efae114e 100644 --- a/src/Message/MessageInterface.php +++ b/src/Model/Message/MessageInterface.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace PhpLlm\LlmChain\Message; +namespace PhpLlm\LlmChain\Model\Message; interface MessageInterface extends \JsonSerializable { diff --git a/src/Message/Role.php b/src/Model/Message/Role.php similarity index 80% rename from src/Message/Role.php rename to src/Model/Message/Role.php index 8eca4e4d..d3ce4850 100644 --- a/src/Message/Role.php +++ b/src/Model/Message/Role.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace PhpLlm\LlmChain\Message; +namespace PhpLlm\LlmChain\Model\Message; enum Role: string { diff --git a/src/Message/SystemMessage.php b/src/Model/Message/SystemMessage.php similarity index 92% rename from src/Message/SystemMessage.php rename to src/Model/Message/SystemMessage.php index afd3252a..b914c3bd 100644 --- a/src/Message/SystemMessage.php +++ b/src/Model/Message/SystemMessage.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace PhpLlm\LlmChain\Message; +namespace PhpLlm\LlmChain\Model\Message; final readonly class SystemMessage implements MessageInterface { diff --git a/src/Message/ToolCallMessage.php b/src/Model/Message/ToolCallMessage.php similarity index 88% rename from src/Message/ToolCallMessage.php rename to src/Model/Message/ToolCallMessage.php index 2595f12c..20a97679 100644 --- a/src/Message/ToolCallMessage.php +++ b/src/Model/Message/ToolCallMessage.php @@ -2,9 +2,9 @@ declare(strict_types=1); -namespace PhpLlm\LlmChain\Message; +namespace PhpLlm\LlmChain\Model\Message; -use PhpLlm\LlmChain\Response\ToolCall; +use PhpLlm\LlmChain\Model\Response\ToolCall; final readonly class ToolCallMessage implements MessageInterface { diff --git a/src/Message/UserMessage.php b/src/Model/Message/UserMessage.php similarity index 84% rename from src/Message/UserMessage.php rename to src/Model/Message/UserMessage.php index f28931f5..eea38bcf 100644 --- a/src/Message/UserMessage.php +++ b/src/Model/Message/UserMessage.php @@ -2,11 +2,11 @@ declare(strict_types=1); -namespace PhpLlm\LlmChain\Message; +namespace PhpLlm\LlmChain\Model\Message; -use PhpLlm\LlmChain\Message\Content\Content; -use PhpLlm\LlmChain\Message\Content\Image; -use PhpLlm\LlmChain\Message\Content\Text; +use PhpLlm\LlmChain\Model\Message\Content\Content; +use PhpLlm\LlmChain\Model\Message\Content\Image; +use PhpLlm\LlmChain\Model\Message\Content\Text; final readonly class UserMessage implements MessageInterface { diff --git a/src/Model/Model.php b/src/Model/Model.php new file mode 100644 index 00000000..95d27c4c --- /dev/null +++ b/src/Model/Model.php @@ -0,0 +1,15 @@ + + */ + public function getOptions(): array; +} diff --git a/src/Response/Choice.php b/src/Model/Response/Choice.php similarity index 93% rename from src/Response/Choice.php rename to src/Model/Response/Choice.php index 3e4e7056..d7a62941 100644 --- a/src/Response/Choice.php +++ b/src/Model/Response/Choice.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace PhpLlm\LlmChain\Response; +namespace PhpLlm\LlmChain\Model\Response; final readonly class Choice { diff --git a/src/Response/ChoiceResponse.php b/src/Model/Response/ChoiceResponse.php similarity index 93% rename from src/Response/ChoiceResponse.php rename to src/Model/Response/ChoiceResponse.php index 0ae05e01..3f0a6d1b 100644 --- a/src/Response/ChoiceResponse.php +++ b/src/Model/Response/ChoiceResponse.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace PhpLlm\LlmChain\Response; +namespace PhpLlm\LlmChain\Model\Response; use PhpLlm\LlmChain\Exception\InvalidArgumentException; diff --git a/src/Response/ResponseInterface.php b/src/Model/Response/ResponseInterface.php similarity index 82% rename from src/Response/ResponseInterface.php rename to src/Model/Response/ResponseInterface.php index fa1fbaa4..8b799bc6 100644 --- a/src/Response/ResponseInterface.php +++ b/src/Model/Response/ResponseInterface.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace PhpLlm\LlmChain\Response; +namespace PhpLlm\LlmChain\Model\Response; interface ResponseInterface { diff --git a/src/Response/StreamResponse.php b/src/Model/Response/StreamResponse.php similarity index 87% rename from src/Response/StreamResponse.php rename to src/Model/Response/StreamResponse.php index 499bca3e..b9618f54 100644 --- a/src/Response/StreamResponse.php +++ b/src/Model/Response/StreamResponse.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace PhpLlm\LlmChain\Response; +namespace PhpLlm\LlmChain\Model\Response; final readonly class StreamResponse implements ResponseInterface { diff --git a/src/Response/StructuredResponse.php b/src/Model/Response/StructuredResponse.php similarity index 91% rename from src/Response/StructuredResponse.php rename to src/Model/Response/StructuredResponse.php index 4e77fbce..25918ca7 100644 --- a/src/Response/StructuredResponse.php +++ b/src/Model/Response/StructuredResponse.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace PhpLlm\LlmChain\Response; +namespace PhpLlm\LlmChain\Model\Response; final readonly class StructuredResponse implements ResponseInterface { diff --git a/src/Response/TextResponse.php b/src/Model/Response/TextResponse.php similarity index 86% rename from src/Response/TextResponse.php rename to src/Model/Response/TextResponse.php index 86c90d02..1c289e31 100644 --- a/src/Response/TextResponse.php +++ b/src/Model/Response/TextResponse.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace PhpLlm\LlmChain\Response; +namespace PhpLlm\LlmChain\Model\Response; final readonly class TextResponse implements ResponseInterface { diff --git a/src/Response/ToolCall.php b/src/Model/Response/ToolCall.php similarity index 94% rename from src/Response/ToolCall.php rename to src/Model/Response/ToolCall.php index 431c86dd..a6743bff 100644 --- a/src/Response/ToolCall.php +++ b/src/Model/Response/ToolCall.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace PhpLlm\LlmChain\Response; +namespace PhpLlm\LlmChain\Model\Response; final readonly class ToolCall implements \JsonSerializable { diff --git a/src/Response/ToolCallResponse.php b/src/Model/Response/ToolCallResponse.php similarity index 93% rename from src/Response/ToolCallResponse.php rename to src/Model/Response/ToolCallResponse.php index 23caa2e9..40bff942 100644 --- a/src/Response/ToolCallResponse.php +++ b/src/Model/Response/ToolCallResponse.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace PhpLlm\LlmChain\Response; +namespace PhpLlm\LlmChain\Model\Response; use PhpLlm\LlmChain\Exception\InvalidArgumentException; diff --git a/src/Model/Response/VectorResponse.php b/src/Model/Response/VectorResponse.php new file mode 100644 index 00000000..f14201eb --- /dev/null +++ b/src/Model/Response/VectorResponse.php @@ -0,0 +1,28 @@ +vectors = $vector; + } + + /** + * @return Vector[] + */ + public function getContent(): array + { + return $this->vectors; + } +} diff --git a/src/Platform.php b/src/Platform.php new file mode 100644 index 00000000..0e027b20 --- /dev/null +++ b/src/Platform.php @@ -0,0 +1,89 @@ + $modelClients + * @param iterable $responseConverter + */ + public function __construct(iterable $modelClients, iterable $responseConverter) + { + $this->modelClients = $modelClients instanceof \Traversable ? iterator_to_array($modelClients) : $modelClients; + $this->responseConverter = $responseConverter instanceof \Traversable ? iterator_to_array($responseConverter) : $responseConverter; + } + + /** + * @param array|string|object $input + * @param array $options + */ + public function request(Model $model, array|string|object $input, array $options = []): ResponseInterface + { + $options = array_merge($model->getOptions(), $options); + + try { + $response = $this->doRequest($model, $input, $options); + + return $this->convertResponse($model, $input, $response, $options); + } catch (ClientExceptionInterface $e) { + $message = $e->getMessage(); + + throw new InvalidArgumentException('' === $message ? 'Invalid request to model or platform' : $message, 0, $e); + } catch (HttpExceptionInterface $e) { + throw new RuntimeException('Failed to request model', 0, $e); + } + } + + /** + * @param array|string|object $input + * @param array $options + */ + private function doRequest(Model $model, array|string|object $input, array $options = []): HttpResponse + { + foreach ($this->modelClients as $modelClient) { + if ($modelClient->supports($model, $input)) { + return $modelClient->request($model, $input, $options); + } + } + + throw new RuntimeException('No response factory found for the given model'); + } + + /** + * @param array|string|object $input + * @param array $options + */ + private function convertResponse(Model $model, object|array|string $input, HttpResponse $response, array $options): ResponseInterface + { + foreach ($this->responseConverter as $responseConverter) { + if ($responseConverter->supports($model, $input)) { + return $responseConverter->convert($response, $options); + } + } + + throw new RuntimeException('No response converter found for the given model'); + } +} diff --git a/src/Platform/Anthropic.php b/src/Platform/Anthropic.php deleted file mode 100644 index b639903b..00000000 --- a/src/Platform/Anthropic.php +++ /dev/null @@ -1,56 +0,0 @@ -httpClient = $httpClient instanceof EventSourceHttpClient ? $httpClient : new EventSourceHttpClient($httpClient); - } - - /** - * @param array $body - * - * @return array - */ - public function request(array $body): iterable - { - $response = $this->httpClient->request('POST', 'https://api.anthropic.com/v1/messages', [ - 'headers' => [ - 'x-api-key' => $this->apiKey, - 'anthropic-version' => $this->version, - ], - 'json' => $body, - ]); - - if ($body['stream'] ?? false) { - return $this->stream($response); - } - - return $response->toArray(); - } - - private function stream(ResponseInterface $response): \Generator - { - foreach ((new EventSourceHttpClient())->stream($response) as $chunk) { - if (!$chunk instanceof ServerSentEvent || '[DONE]' === $chunk->getData()) { - continue; - } - - yield $chunk->getArrayData(); - } - } -} diff --git a/src/Platform/ModelClient.php b/src/Platform/ModelClient.php new file mode 100644 index 00000000..09881fc1 --- /dev/null +++ b/src/Platform/ModelClient.php @@ -0,0 +1,22 @@ +|string|object $input + */ + public function supports(Model $model, array|string|object $input): bool; + + /** + * @param array|string|object $input + * @param array $options + */ + public function request(Model $model, array|string|object $input, array $options = []): ResponseInterface; +} diff --git a/src/Platform/Ollama.php b/src/Platform/Ollama.php deleted file mode 100644 index d642b422..00000000 --- a/src/Platform/Ollama.php +++ /dev/null @@ -1,36 +0,0 @@ - $body - * - * @return array - */ - public function request(string $model, string $endpoint, array $body): array - { - $url = sprintf('%s/api/%s', $this->hostUrl, $endpoint); - - $response = $this->httpClient->request('POST', $url, [ - 'headers' => ['Content-Type' => 'application/json'], - 'json' => array_merge($body, [ - 'model' => $model, - ]), - ]); - - return $response->toArray(); - } -} diff --git a/src/Platform/OpenAI/AbstractPlatform.php b/src/Platform/OpenAI/AbstractPlatform.php deleted file mode 100644 index 496ee8b9..00000000 --- a/src/Platform/OpenAI/AbstractPlatform.php +++ /dev/null @@ -1,58 +0,0 @@ -rawRequest($endpoint, $body); - - if ($body['stream'] ?? false) { - return $this->stream($response); - } - - try { - return $response->toArray(); - } catch (ClientException $e) { - dump($e->getResponse()->getContent(false)); - throw new RuntimeException('Failed to make request', 0, $e); - } - } - - public function multiRequest(string $endpoint, array $bodies): \Generator - { - $responses = []; - foreach ($bodies as $body) { - $responses[] = $this->rawRequest($endpoint, $body); - } - - foreach ($responses as $response) { - yield $response->toArray(); - } - } - - private function stream(ResponseInterface $response): \Generator - { - foreach ((new EventSourceHttpClient())->stream($response) as $chunk) { - if (!$chunk instanceof ServerSentEvent || '[DONE]' === $chunk->getData()) { - continue; - } - - yield $chunk->getArrayData(); - } - } - - /** - * @param array $body - */ - abstract protected function rawRequest(string $endpoint, array $body): ResponseInterface; -} diff --git a/src/Platform/OpenAI/Platform.php b/src/Platform/OpenAI/Platform.php deleted file mode 100644 index dd12e656..00000000 --- a/src/Platform/OpenAI/Platform.php +++ /dev/null @@ -1,22 +0,0 @@ - $body - * - * @return array - */ - public function request(string $endpoint, array $body): iterable; - - /** - * @param array> $bodies - * - * @return \Generator> - */ - public function multiRequest(string $endpoint, array $bodies): \Generator; -} diff --git a/src/Platform/ResponseConverter.php b/src/Platform/ResponseConverter.php new file mode 100644 index 00000000..a6a03262 --- /dev/null +++ b/src/Platform/ResponseConverter.php @@ -0,0 +1,22 @@ +|string|object $input + */ + public function supports(Model $model, array|string|object $input): bool; + + /** + * @param array $options + */ + public function convert(HttpResponse $response, array $options = []): LlmResponse; +} diff --git a/src/Platform/Voyage.php b/src/Platform/Voyage.php deleted file mode 100644 index 0671596e..00000000 --- a/src/Platform/Voyage.php +++ /dev/null @@ -1,31 +0,0 @@ - $body - * - * @return array - */ - public function request(array $body): array - { - $response = $this->httpClient->request('POST', 'https://api.voyageai.com/v1/embeddings', [ - 'auth_bearer' => $this->apiKey, - 'json' => $body, - ]); - - return $response->toArray(); - } -} diff --git a/tests/Model/Language/LlamaTest.php b/tests/Bridge/Meta/LlamaPromptConverterTest.php similarity index 78% rename from tests/Model/Language/LlamaTest.php rename to tests/Bridge/Meta/LlamaPromptConverterTest.php index 94ff87fa..8db2d986 100644 --- a/tests/Model/Language/LlamaTest.php +++ b/tests/Bridge/Meta/LlamaPromptConverterTest.php @@ -2,32 +2,29 @@ declare(strict_types=1); -namespace PhpLlm\LlmChain\Tests\Model\Language; - -use PhpLlm\LlmChain\Message\AssistantMessage; -use PhpLlm\LlmChain\Message\Content\Image; -use PhpLlm\LlmChain\Message\Message; -use PhpLlm\LlmChain\Message\MessageBag; -use PhpLlm\LlmChain\Message\SystemMessage; -use PhpLlm\LlmChain\Message\UserMessage; -use PhpLlm\LlmChain\Model\Language\Llama; -use PhpLlm\LlmChain\Platform\Ollama; +namespace PhpLlm\LlmChain\Tests\Bridge\Meta; + +use PhpLlm\LlmChain\Bridge\Meta\LlamaPromptConverter; +use PhpLlm\LlmChain\Model\Message\AssistantMessage; +use PhpLlm\LlmChain\Model\Message\Content\Image; +use PhpLlm\LlmChain\Model\Message\Message; +use PhpLlm\LlmChain\Model\Message\MessageBag; +use PhpLlm\LlmChain\Model\Message\SystemMessage; +use PhpLlm\LlmChain\Model\Message\UserMessage; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Small; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; -use Symfony\Component\HttpClient\MockHttpClient; -#[CoversClass(Llama::class)] +#[CoversClass(LlamaPromptConverter::class)] #[Small] -final class LlamaTest extends TestCase +final class LlamaPromptConverterTest extends TestCase { #[Test] public function convertMessages(): void { - $messageBag = new MessageBag( - ); + $messageBag = new MessageBag(); foreach (self::provideMessages() as $message) { $messageBag->append($message[1]); } @@ -58,7 +55,7 @@ public function convertMessages(): void <|start_header_id|>assistant<|end_header_id|> EXPECTED, - (new Llama(new Ollama(new MockHttpClient(), 'http://example.com')))->convertToPrompt($messageBag) + (new LlamaPromptConverter())->convertToPrompt($messageBag) ); } @@ -68,7 +65,7 @@ public function convertMessage(string $expected, UserMessage|SystemMessage|Assis { self::assertSame( $expected, - (new Llama(new Ollama(new MockHttpClient(), 'http://example.com')))->convertMessage($message) + (new LlamaPromptConverter())->convertMessage($message) ); } diff --git a/tests/StructuredOutput/ChainProcessorTest.php b/tests/Chain/StructuredOutput/ChainProcessorTest.php similarity index 92% rename from tests/StructuredOutput/ChainProcessorTest.php rename to tests/Chain/StructuredOutput/ChainProcessorTest.php index dbadb488..287339b4 100644 --- a/tests/StructuredOutput/ChainProcessorTest.php +++ b/tests/Chain/StructuredOutput/ChainProcessorTest.php @@ -2,17 +2,17 @@ declare(strict_types=1); -namespace PhpLlm\LlmChain\Tests\StructuredOutput; +namespace PhpLlm\LlmChain\Tests\Chain\StructuredOutput; use PhpLlm\LlmChain\Chain\Input; use PhpLlm\LlmChain\Chain\Output; +use PhpLlm\LlmChain\Chain\StructuredOutput\ChainProcessor; use PhpLlm\LlmChain\Exception\MissingModelSupport; -use PhpLlm\LlmChain\LanguageModel; -use PhpLlm\LlmChain\Message\MessageBag; -use PhpLlm\LlmChain\Response\Choice; -use PhpLlm\LlmChain\Response\StructuredResponse; -use PhpLlm\LlmChain\Response\TextResponse; -use PhpLlm\LlmChain\StructuredOutput\ChainProcessor; +use PhpLlm\LlmChain\Model\LanguageModel; +use PhpLlm\LlmChain\Model\Message\MessageBag; +use PhpLlm\LlmChain\Model\Response\Choice; +use PhpLlm\LlmChain\Model\Response\StructuredResponse; +use PhpLlm\LlmChain\Model\Response\TextResponse; use PhpLlm\LlmChain\Tests\Double\ConfigurableResponseFormatFactory; use PhpLlm\LlmChain\Tests\Fixture\SomeStructure; use PHPUnit\Framework\Attributes\CoversClass; diff --git a/tests/StructuredOutput/ResponseFormatFactoryTest.php b/tests/Chain/StructuredOutput/ResponseFormatFactoryTest.php similarity index 89% rename from tests/StructuredOutput/ResponseFormatFactoryTest.php rename to tests/Chain/StructuredOutput/ResponseFormatFactoryTest.php index bd9f736d..3f3bcb8a 100644 --- a/tests/StructuredOutput/ResponseFormatFactoryTest.php +++ b/tests/Chain/StructuredOutput/ResponseFormatFactoryTest.php @@ -2,10 +2,10 @@ declare(strict_types=1); -namespace PhpLlm\LlmChain\Tests\StructuredOutput; +namespace PhpLlm\LlmChain\Tests\Chain\StructuredOutput; -use PhpLlm\LlmChain\StructuredOutput\ResponseFormatFactory; -use PhpLlm\LlmChain\StructuredOutput\SchemaFactory; +use PhpLlm\LlmChain\Chain\StructuredOutput\ResponseFormatFactory; +use PhpLlm\LlmChain\Chain\StructuredOutput\SchemaFactory; use PhpLlm\LlmChain\Tests\Fixture\StructuredOutput\User; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Test; diff --git a/tests/StructuredOutput/SchemaFactoryTest.php b/tests/Chain/StructuredOutput/SchemaFactoryTest.php similarity index 96% rename from tests/StructuredOutput/SchemaFactoryTest.php rename to tests/Chain/StructuredOutput/SchemaFactoryTest.php index 7069f8c9..0db30a34 100644 --- a/tests/StructuredOutput/SchemaFactoryTest.php +++ b/tests/Chain/StructuredOutput/SchemaFactoryTest.php @@ -2,9 +2,9 @@ declare(strict_types=1); -namespace PhpLlm\LlmChain\Tests\StructuredOutput; +namespace PhpLlm\LlmChain\Tests\Chain\StructuredOutput; -use PhpLlm\LlmChain\StructuredOutput\SchemaFactory; +use PhpLlm\LlmChain\Chain\StructuredOutput\SchemaFactory; use PhpLlm\LlmChain\Tests\Fixture\StructuredOutput\MathReasoning; use PhpLlm\LlmChain\Tests\Fixture\StructuredOutput\Step; use PhpLlm\LlmChain\Tests\Fixture\StructuredOutput\User; diff --git a/tests/ToolBox/Attribute/AsToolTest.php b/tests/Chain/ToolBox/Attribute/AsToolTest.php similarity index 82% rename from tests/ToolBox/Attribute/AsToolTest.php rename to tests/Chain/ToolBox/Attribute/AsToolTest.php index b8bdc511..384638dc 100644 --- a/tests/ToolBox/Attribute/AsToolTest.php +++ b/tests/Chain/ToolBox/Attribute/AsToolTest.php @@ -2,9 +2,9 @@ declare(strict_types=1); -namespace PhpLlm\LlmChain\Tests\ToolBox\Attribute; +namespace PhpLlm\LlmChain\Tests\Chain\ToolBox\Attribute; -use PhpLlm\LlmChain\ToolBox\Attribute\AsTool; +use PhpLlm\LlmChain\Chain\ToolBox\Attribute\AsTool; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; diff --git a/tests/ToolBox/Attribute/ToolParameterTest.php b/tests/Chain/ToolBox/Attribute/ToolParameterTest.php similarity index 98% rename from tests/ToolBox/Attribute/ToolParameterTest.php rename to tests/Chain/ToolBox/Attribute/ToolParameterTest.php index 2615cec8..f366d7a3 100644 --- a/tests/ToolBox/Attribute/ToolParameterTest.php +++ b/tests/Chain/ToolBox/Attribute/ToolParameterTest.php @@ -2,9 +2,9 @@ declare(strict_types=1); -namespace PhpLlm\LlmChain\Tests\ToolBox\Attribute; +namespace PhpLlm\LlmChain\Tests\Chain\ToolBox\Attribute; -use PhpLlm\LlmChain\ToolBox\Attribute\ToolParameter; +use PhpLlm\LlmChain\Chain\ToolBox\Attribute\ToolParameter; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; diff --git a/tests/ToolBox/ChainProcessorTest.php b/tests/Chain/ToolBox/ChainProcessorTest.php similarity index 90% rename from tests/ToolBox/ChainProcessorTest.php rename to tests/Chain/ToolBox/ChainProcessorTest.php index d88cf9a0..55ca2aa0 100644 --- a/tests/ToolBox/ChainProcessorTest.php +++ b/tests/Chain/ToolBox/ChainProcessorTest.php @@ -2,14 +2,14 @@ declare(strict_types=1); -namespace PhpLlm\LlmChain\Tests\ToolBox; +namespace PhpLlm\LlmChain\Tests\Chain\ToolBox; use PhpLlm\LlmChain\Chain\Input; +use PhpLlm\LlmChain\Chain\ToolBox\ChainProcessor; +use PhpLlm\LlmChain\Chain\ToolBox\ToolBoxInterface; use PhpLlm\LlmChain\Exception\MissingModelSupport; -use PhpLlm\LlmChain\LanguageModel; -use PhpLlm\LlmChain\Message\MessageBag; -use PhpLlm\LlmChain\ToolBox\ChainProcessor; -use PhpLlm\LlmChain\ToolBox\ToolBoxInterface; +use PhpLlm\LlmChain\Model\LanguageModel; +use PhpLlm\LlmChain\Model\Message\MessageBag; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\UsesClass; diff --git a/tests/ToolBox/ParameterAnalyzerTest.php b/tests/Chain/ToolBox/ParameterAnalyzerTest.php similarity index 96% rename from tests/ToolBox/ParameterAnalyzerTest.php rename to tests/Chain/ToolBox/ParameterAnalyzerTest.php index a6a61f73..2adcc63b 100644 --- a/tests/ToolBox/ParameterAnalyzerTest.php +++ b/tests/Chain/ToolBox/ParameterAnalyzerTest.php @@ -2,16 +2,16 @@ declare(strict_types=1); -namespace PhpLlm\LlmChain\Tests\ToolBox; +namespace PhpLlm\LlmChain\Tests\Chain\ToolBox; +use PhpLlm\LlmChain\Chain\ToolBox\Attribute\AsTool; +use PhpLlm\LlmChain\Chain\ToolBox\Attribute\ToolParameter; +use PhpLlm\LlmChain\Chain\ToolBox\Metadata; +use PhpLlm\LlmChain\Chain\ToolBox\ParameterAnalyzer; use PhpLlm\LlmChain\Tests\Fixture\Tool\ToolNoParams; use PhpLlm\LlmChain\Tests\Fixture\Tool\ToolOptionalParam; use PhpLlm\LlmChain\Tests\Fixture\Tool\ToolRequiredParams; use PhpLlm\LlmChain\Tests\Fixture\Tool\ToolWithToolParameterAttribute; -use PhpLlm\LlmChain\ToolBox\Attribute\AsTool; -use PhpLlm\LlmChain\ToolBox\Attribute\ToolParameter; -use PhpLlm\LlmChain\ToolBox\Metadata; -use PhpLlm\LlmChain\ToolBox\ParameterAnalyzer; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Test; diff --git a/tests/ToolBox/ToolAnalyzerTest.php b/tests/Chain/ToolBox/ToolAnalyzerTest.php similarity index 94% rename from tests/ToolBox/ToolAnalyzerTest.php rename to tests/Chain/ToolBox/ToolAnalyzerTest.php index 63fc9660..30314e7f 100644 --- a/tests/ToolBox/ToolAnalyzerTest.php +++ b/tests/Chain/ToolBox/ToolAnalyzerTest.php @@ -2,16 +2,16 @@ declare(strict_types=1); -namespace PhpLlm\LlmChain\Tests\ToolBox; +namespace PhpLlm\LlmChain\Tests\Chain\ToolBox; +use PhpLlm\LlmChain\Chain\ToolBox\Attribute\AsTool; +use PhpLlm\LlmChain\Chain\ToolBox\Metadata; +use PhpLlm\LlmChain\Chain\ToolBox\ParameterAnalyzer; +use PhpLlm\LlmChain\Chain\ToolBox\ToolAnalyzer; use PhpLlm\LlmChain\Exception\InvalidToolImplementation; use PhpLlm\LlmChain\Tests\Fixture\Tool\ToolMultiple; use PhpLlm\LlmChain\Tests\Fixture\Tool\ToolRequiredParams; use PhpLlm\LlmChain\Tests\Fixture\Tool\ToolWrong; -use PhpLlm\LlmChain\ToolBox\Attribute\AsTool; -use PhpLlm\LlmChain\ToolBox\Metadata; -use PhpLlm\LlmChain\ToolBox\ParameterAnalyzer; -use PhpLlm\LlmChain\ToolBox\ToolAnalyzer; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\UsesClass; diff --git a/tests/ToolBox/ToolBoxTest.php b/tests/Chain/ToolBox/ToolBoxTest.php similarity index 91% rename from tests/ToolBox/ToolBoxTest.php rename to tests/Chain/ToolBox/ToolBoxTest.php index 10dd5b5e..a02b3444 100644 --- a/tests/ToolBox/ToolBoxTest.php +++ b/tests/Chain/ToolBox/ToolBoxTest.php @@ -2,17 +2,17 @@ declare(strict_types=1); -namespace PhpLlm\LlmChain\Tests\ToolBox; +namespace PhpLlm\LlmChain\Tests\Chain\ToolBox; -use PhpLlm\LlmChain\Response\ToolCall; +use PhpLlm\LlmChain\Chain\ToolBox\Attribute\AsTool; +use PhpLlm\LlmChain\Chain\ToolBox\Metadata; +use PhpLlm\LlmChain\Chain\ToolBox\ParameterAnalyzer; +use PhpLlm\LlmChain\Chain\ToolBox\ToolAnalyzer; +use PhpLlm\LlmChain\Chain\ToolBox\ToolBox; +use PhpLlm\LlmChain\Model\Response\ToolCall; use PhpLlm\LlmChain\Tests\Fixture\Tool\ToolNoParams; use PhpLlm\LlmChain\Tests\Fixture\Tool\ToolOptionalParam; use PhpLlm\LlmChain\Tests\Fixture\Tool\ToolRequiredParams; -use PhpLlm\LlmChain\ToolBox\Attribute\AsTool; -use PhpLlm\LlmChain\ToolBox\Metadata; -use PhpLlm\LlmChain\ToolBox\ParameterAnalyzer; -use PhpLlm\LlmChain\ToolBox\ToolAnalyzer; -use PhpLlm\LlmChain\ToolBox\ToolBox; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\UsesClass; diff --git a/tests/Double/ConfigurableResponseFormatFactory.php b/tests/Double/ConfigurableResponseFormatFactory.php index f62c2872..a7327341 100644 --- a/tests/Double/ConfigurableResponseFormatFactory.php +++ b/tests/Double/ConfigurableResponseFormatFactory.php @@ -4,7 +4,7 @@ namespace PhpLlm\LlmChain\Tests\Double; -use PhpLlm\LlmChain\StructuredOutput\ResponseFormatFactoryInterface; +use PhpLlm\LlmChain\Chain\StructuredOutput\ResponseFormatFactoryInterface; final readonly class ConfigurableResponseFormatFactory implements ResponseFormatFactoryInterface { diff --git a/tests/Double/PlatformTestHandler.php b/tests/Double/PlatformTestHandler.php new file mode 100644 index 00000000..605c1351 --- /dev/null +++ b/tests/Double/PlatformTestHandler.php @@ -0,0 +1,50 @@ +createCalls; + + return new MockResponse(); + } + + public function convert(HttpResponse $response, array $options = []): LlmResponse + { + return $this->create ?? new VectorResponse(new Vector([1, 2, 3])); + } +} diff --git a/tests/Double/TestEmbeddingsModel.php b/tests/Double/TestEmbeddingsModel.php deleted file mode 100644 index d8b4ff40..00000000 --- a/tests/Double/TestEmbeddingsModel.php +++ /dev/null @@ -1,36 +0,0 @@ -createCalls; - - return $this->create ?? new Vector([1, 2, 3]); - } - - public function multiCreate(array $texts, array $options = []): array - { - ++$this->multiCreateCalls; - - return $this->multiCreate; - } -} diff --git a/tests/DocumentEmbedderTest.php b/tests/EmbedderTest.php similarity index 76% rename from tests/DocumentEmbedderTest.php rename to tests/EmbedderTest.php index 67e8f883..79f04c54 100644 --- a/tests/DocumentEmbedderTest.php +++ b/tests/EmbedderTest.php @@ -4,35 +4,38 @@ namespace PhpLlm\LlmChain\Tests; +use PhpLlm\LlmChain\Bridge\OpenAI\Embeddings; use PhpLlm\LlmChain\Document\Metadata; use PhpLlm\LlmChain\Document\TextDocument; use PhpLlm\LlmChain\Document\Vector; use PhpLlm\LlmChain\Document\VectorDocument; -use PhpLlm\LlmChain\DocumentEmbedder; -use PhpLlm\LlmChain\Tests\Double\TestEmbeddingsModel; +use PhpLlm\LlmChain\Embedder; +use PhpLlm\LlmChain\Model\Response\VectorResponse; +use PhpLlm\LlmChain\Tests\Double\PlatformTestHandler; use PhpLlm\LlmChain\Tests\Double\TestStore; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\UsesClass; use PHPUnit\Framework\TestCase; use Psr\Log\LoggerInterface; -use Psr\Log\NullLogger; use Symfony\Component\Clock\MockClock; use Symfony\Component\Uid\Uuid; -#[CoversClass(DocumentEmbedder::class)] +#[CoversClass(Embedder::class)] #[UsesClass(TextDocument::class)] #[UsesClass(Vector::class)] #[UsesClass(VectorDocument::class)] -final class DocumentEmbedderTest extends TestCase +final class EmbedderTest extends TestCase { #[Test] public function embedSingleDocument(): void { $document = new TextDocument($id = Uuid::v4(), 'Test content'); + $vector = new Vector([0.1, 0.2, 0.3]); - $embedder = new DocumentEmbedder( - new TestEmbeddingsModel(multiCreate: [$vector = new Vector([0.1, 0.2, 0.3])]), + $embedder = new Embedder( + PlatformTestHandler::createPlatform(new VectorResponse($vector)), + new Embeddings(), $store = new TestStore(), new MockClock(), ); @@ -51,8 +54,9 @@ public function embedEmptyDocumentList(): void $logger = $this->createMock(LoggerInterface::class); $logger->expects($this->once())->method('debug')->with('No documents to embed'); - $embedder = new DocumentEmbedder( - new TestEmbeddingsModel(), + $embedder = new Embedder( + PlatformTestHandler::createPlatform(), + new Embeddings(), $store = new TestStore(), new MockClock(), $logger, @@ -68,12 +72,13 @@ public function embedDocumentWithMetadata(): void { $metadata = new Metadata(['key' => 'value']); $document = new TextDocument($id = Uuid::v4(), 'Test content', $metadata); + $vector = new Vector([0.1, 0.2, 0.3]); - $embedder = new DocumentEmbedder( - new TestEmbeddingsModel(multiCreate: [$vector = new Vector([0.1, 0.2, 0.3])]), + $embedder = new Embedder( + PlatformTestHandler::createPlatform(new VectorResponse($vector)), + new Embeddings(), $store = new TestStore(), new MockClock(), - new NullLogger(), ); $embedder->embed($document); @@ -95,11 +100,11 @@ public function embedWithSleep(): void $document1 = new TextDocument(Uuid::v4(), 'Test content 1'); $document2 = new TextDocument(Uuid::v4(), 'Test content 2'); - $embedder = new DocumentEmbedder( - new TestEmbeddingsModel(multiCreate: [$vector1, $vector2]), + $embedder = new Embedder( + PlatformTestHandler::createPlatform(new VectorResponse($vector1, $vector2)), + new Embeddings(), $store = new TestStore(), $clock = new MockClock('2024-01-01 00:00:00'), - new NullLogger(), ); $embedder->embed( diff --git a/tests/Fixture/Tool/ToolMultiple.php b/tests/Fixture/Tool/ToolMultiple.php index 392ac6b1..acfe6cc7 100644 --- a/tests/Fixture/Tool/ToolMultiple.php +++ b/tests/Fixture/Tool/ToolMultiple.php @@ -4,7 +4,7 @@ namespace PhpLlm\LlmChain\Tests\Fixture\Tool; -use PhpLlm\LlmChain\ToolBox\Attribute\AsTool; +use PhpLlm\LlmChain\Chain\ToolBox\Attribute\AsTool; #[AsTool('tool_hello_world', 'Function to say hello', method: 'hello')] #[AsTool('tool_required_params', 'Function to say a number', method: 'bar')] diff --git a/tests/Fixture/Tool/ToolNoParams.php b/tests/Fixture/Tool/ToolNoParams.php index 977f915c..5a5189d2 100644 --- a/tests/Fixture/Tool/ToolNoParams.php +++ b/tests/Fixture/Tool/ToolNoParams.php @@ -4,7 +4,7 @@ namespace PhpLlm\LlmChain\Tests\Fixture\Tool; -use PhpLlm\LlmChain\ToolBox\Attribute\AsTool; +use PhpLlm\LlmChain\Chain\ToolBox\Attribute\AsTool; #[AsTool('tool_no_params', 'A tool without parameters')] final class ToolNoParams diff --git a/tests/Fixture/Tool/ToolOptionalParam.php b/tests/Fixture/Tool/ToolOptionalParam.php index 8ccf5b95..7f8969d0 100644 --- a/tests/Fixture/Tool/ToolOptionalParam.php +++ b/tests/Fixture/Tool/ToolOptionalParam.php @@ -4,7 +4,7 @@ namespace PhpLlm\LlmChain\Tests\Fixture\Tool; -use PhpLlm\LlmChain\ToolBox\Attribute\AsTool; +use PhpLlm\LlmChain\Chain\ToolBox\Attribute\AsTool; #[AsTool('tool_optional_param', 'A tool with one optional parameter', method: 'bar')] final class ToolOptionalParam diff --git a/tests/Fixture/Tool/ToolRequiredParams.php b/tests/Fixture/Tool/ToolRequiredParams.php index 585c2d5a..89d5d5a8 100644 --- a/tests/Fixture/Tool/ToolRequiredParams.php +++ b/tests/Fixture/Tool/ToolRequiredParams.php @@ -4,7 +4,7 @@ namespace PhpLlm\LlmChain\Tests\Fixture\Tool; -use PhpLlm\LlmChain\ToolBox\Attribute\AsTool; +use PhpLlm\LlmChain\Chain\ToolBox\Attribute\AsTool; #[AsTool('tool_required_params', 'A tool with required parameters', method: 'bar')] final class ToolRequiredParams diff --git a/tests/Fixture/Tool/ToolWithToolParameterAttribute.php b/tests/Fixture/Tool/ToolWithToolParameterAttribute.php index b740bd8c..03f2ec64 100644 --- a/tests/Fixture/Tool/ToolWithToolParameterAttribute.php +++ b/tests/Fixture/Tool/ToolWithToolParameterAttribute.php @@ -4,8 +4,8 @@ namespace PhpLlm\LlmChain\Tests\Fixture\Tool; -use PhpLlm\LlmChain\ToolBox\Attribute\AsTool; -use PhpLlm\LlmChain\ToolBox\Attribute\ToolParameter; +use PhpLlm\LlmChain\Chain\ToolBox\Attribute\AsTool; +use PhpLlm\LlmChain\Chain\ToolBox\Attribute\ToolParameter; #[AsTool('tool_with_ToolParameter_attribute', 'A tool which has a parameter with described with #[ToolParameter] attribute')] final class ToolWithToolParameterAttribute diff --git a/tests/Message/AssistantMessageTest.php b/tests/Model/Message/AssistantMessageTest.php similarity index 91% rename from tests/Message/AssistantMessageTest.php rename to tests/Model/Message/AssistantMessageTest.php index 24dbfb11..0fc32bcb 100644 --- a/tests/Message/AssistantMessageTest.php +++ b/tests/Model/Message/AssistantMessageTest.php @@ -2,11 +2,11 @@ declare(strict_types=1); -namespace PhpLlm\LlmChain\Tests\Message; +namespace PhpLlm\LlmChain\Tests\Model\Message; -use PhpLlm\LlmChain\Message\AssistantMessage; -use PhpLlm\LlmChain\Message\Role; -use PhpLlm\LlmChain\Response\ToolCall; +use PhpLlm\LlmChain\Model\Message\AssistantMessage; +use PhpLlm\LlmChain\Model\Message\Role; +use PhpLlm\LlmChain\Model\Response\ToolCall; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Small; diff --git a/tests/Message/Content/ImageTest.php b/tests/Model/Message/Content/ImageTest.php similarity index 89% rename from tests/Message/Content/ImageTest.php rename to tests/Model/Message/Content/ImageTest.php index 6da809c2..1e9b537e 100644 --- a/tests/Message/Content/ImageTest.php +++ b/tests/Model/Message/Content/ImageTest.php @@ -2,9 +2,9 @@ declare(strict_types=1); -namespace PhpLlm\LlmChain\Tests\Message\Content; +namespace PhpLlm\LlmChain\Tests\Model\Message\Content; -use PhpLlm\LlmChain\Message\Content\Image; +use PhpLlm\LlmChain\Model\Message\Content\Image; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Small; use PHPUnit\Framework\Attributes\Test; @@ -33,7 +33,7 @@ public function constructWithValidDataUrl(): void #[Test] public function withValidFile(): void { - $image = new Image(dirname(__DIR__, 2).'/Fixture/image.png'); + $image = new Image(dirname(__DIR__, 3).'/Fixture/image.png'); self::assertStringStartsWith('data:image/png;base64,', $image->url); } diff --git a/tests/Message/Content/TextTest.php b/tests/Model/Message/Content/TextTest.php similarity index 85% rename from tests/Message/Content/TextTest.php rename to tests/Model/Message/Content/TextTest.php index 893ca201..7ce538cf 100644 --- a/tests/Message/Content/TextTest.php +++ b/tests/Model/Message/Content/TextTest.php @@ -2,9 +2,9 @@ declare(strict_types=1); -namespace PhpLlm\LlmChain\Tests\Message\Content; +namespace PhpLlm\LlmChain\Tests\Model\Message\Content; -use PhpLlm\LlmChain\Message\Content\Text; +use PhpLlm\LlmChain\Model\Message\Content\Text; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Small; use PHPUnit\Framework\Attributes\Test; diff --git a/tests/Message/MessageBagTest.php b/tests/Model/Message/MessageBagTest.php similarity index 93% rename from tests/Message/MessageBagTest.php rename to tests/Model/Message/MessageBagTest.php index ef864c54..143911b8 100644 --- a/tests/Message/MessageBagTest.php +++ b/tests/Model/Message/MessageBagTest.php @@ -2,15 +2,15 @@ declare(strict_types=1); -namespace PhpLlm\LlmChain\Tests\Message; - -use PhpLlm\LlmChain\Message\AssistantMessage; -use PhpLlm\LlmChain\Message\Content\Image; -use PhpLlm\LlmChain\Message\Content\Text; -use PhpLlm\LlmChain\Message\Message; -use PhpLlm\LlmChain\Message\MessageBag; -use PhpLlm\LlmChain\Message\SystemMessage; -use PhpLlm\LlmChain\Message\UserMessage; +namespace PhpLlm\LlmChain\Tests\Model\Message; + +use PhpLlm\LlmChain\Model\Message\AssistantMessage; +use PhpLlm\LlmChain\Model\Message\Content\Image; +use PhpLlm\LlmChain\Model\Message\Content\Text; +use PhpLlm\LlmChain\Model\Message\Message; +use PhpLlm\LlmChain\Model\Message\MessageBag; +use PhpLlm\LlmChain\Model\Message\SystemMessage; +use PhpLlm\LlmChain\Model\Message\UserMessage; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Small; use PHPUnit\Framework\Attributes\Test; diff --git a/tests/Message/MessageTest.php b/tests/Model/Message/MessageTest.php similarity index 87% rename from tests/Message/MessageTest.php rename to tests/Model/Message/MessageTest.php index 8dfc8e51..97b1f3f5 100644 --- a/tests/Message/MessageTest.php +++ b/tests/Model/Message/MessageTest.php @@ -2,17 +2,17 @@ declare(strict_types=1); -namespace PhpLlm\LlmChain\Tests\Message; - -use PhpLlm\LlmChain\Message\AssistantMessage; -use PhpLlm\LlmChain\Message\Content\Image; -use PhpLlm\LlmChain\Message\Content\Text; -use PhpLlm\LlmChain\Message\Message; -use PhpLlm\LlmChain\Message\Role; -use PhpLlm\LlmChain\Message\SystemMessage; -use PhpLlm\LlmChain\Message\ToolCallMessage; -use PhpLlm\LlmChain\Message\UserMessage; -use PhpLlm\LlmChain\Response\ToolCall; +namespace PhpLlm\LlmChain\Tests\Model\Message; + +use PhpLlm\LlmChain\Model\Message\AssistantMessage; +use PhpLlm\LlmChain\Model\Message\Content\Image; +use PhpLlm\LlmChain\Model\Message\Content\Text; +use PhpLlm\LlmChain\Model\Message\Message; +use PhpLlm\LlmChain\Model\Message\Role; +use PhpLlm\LlmChain\Model\Message\SystemMessage; +use PhpLlm\LlmChain\Model\Message\ToolCallMessage; +use PhpLlm\LlmChain\Model\Message\UserMessage; +use PhpLlm\LlmChain\Model\Response\ToolCall; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Small; use PHPUnit\Framework\Attributes\Test; diff --git a/tests/Message/SystemMessageTest.php b/tests/Model/Message/SystemMessageTest.php similarity index 84% rename from tests/Message/SystemMessageTest.php rename to tests/Model/Message/SystemMessageTest.php index 1e5bdef2..aa5fd381 100644 --- a/tests/Message/SystemMessageTest.php +++ b/tests/Model/Message/SystemMessageTest.php @@ -2,10 +2,10 @@ declare(strict_types=1); -namespace PhpLlm\LlmChain\Tests\Message; +namespace PhpLlm\LlmChain\Tests\Model\Message; -use PhpLlm\LlmChain\Message\Role; -use PhpLlm\LlmChain\Message\SystemMessage; +use PhpLlm\LlmChain\Model\Message\Role; +use PhpLlm\LlmChain\Model\Message\SystemMessage; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Small; use PHPUnit\Framework\Attributes\Test; diff --git a/tests/Message/ToolCallMessageTest.php b/tests/Model/Message/ToolCallMessageTest.php similarity index 83% rename from tests/Message/ToolCallMessageTest.php rename to tests/Model/Message/ToolCallMessageTest.php index 4b933a20..a6558163 100644 --- a/tests/Message/ToolCallMessageTest.php +++ b/tests/Model/Message/ToolCallMessageTest.php @@ -2,11 +2,11 @@ declare(strict_types=1); -namespace PhpLlm\LlmChain\Tests\Message; +namespace PhpLlm\LlmChain\Tests\Model\Message; -use PhpLlm\LlmChain\Message\Role; -use PhpLlm\LlmChain\Message\ToolCallMessage; -use PhpLlm\LlmChain\Response\ToolCall; +use PhpLlm\LlmChain\Model\Message\Role; +use PhpLlm\LlmChain\Model\Message\ToolCallMessage; +use PhpLlm\LlmChain\Model\Response\ToolCall; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Small; use PHPUnit\Framework\Attributes\Test; diff --git a/tests/Message/UserMessageTest.php b/tests/Model/Message/UserMessageTest.php similarity index 92% rename from tests/Message/UserMessageTest.php rename to tests/Model/Message/UserMessageTest.php index 075b24c7..571b6bc3 100644 --- a/tests/Message/UserMessageTest.php +++ b/tests/Model/Message/UserMessageTest.php @@ -2,12 +2,12 @@ declare(strict_types=1); -namespace PhpLlm\LlmChain\Tests\Message; +namespace PhpLlm\LlmChain\Tests\Model\Message; -use PhpLlm\LlmChain\Message\Content\Image; -use PhpLlm\LlmChain\Message\Content\Text; -use PhpLlm\LlmChain\Message\Role; -use PhpLlm\LlmChain\Message\UserMessage; +use PhpLlm\LlmChain\Model\Message\Content\Image; +use PhpLlm\LlmChain\Model\Message\Content\Text; +use PhpLlm\LlmChain\Model\Message\Role; +use PhpLlm\LlmChain\Model\Message\UserMessage; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Small; diff --git a/tests/Response/ChoiceResponseTest.php b/tests/Model/Response/ChoiceResponseTest.php similarity index 89% rename from tests/Response/ChoiceResponseTest.php rename to tests/Model/Response/ChoiceResponseTest.php index 036bca42..ea5600f7 100644 --- a/tests/Response/ChoiceResponseTest.php +++ b/tests/Model/Response/ChoiceResponseTest.php @@ -2,11 +2,11 @@ declare(strict_types=1); -namespace PhpLlm\LlmChain\Tests\Response; +namespace PhpLlm\LlmChain\Tests\Model\Response; use PhpLlm\LlmChain\Exception\InvalidArgumentException; -use PhpLlm\LlmChain\Response\Choice; -use PhpLlm\LlmChain\Response\ChoiceResponse; +use PhpLlm\LlmChain\Model\Response\Choice; +use PhpLlm\LlmChain\Model\Response\ChoiceResponse; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Small; use PHPUnit\Framework\Attributes\Test; diff --git a/tests/Response/ChoiceTest.php b/tests/Model/Response/ChoiceTest.php similarity index 92% rename from tests/Response/ChoiceTest.php rename to tests/Model/Response/ChoiceTest.php index 1cb20181..d3f53948 100644 --- a/tests/Response/ChoiceTest.php +++ b/tests/Model/Response/ChoiceTest.php @@ -2,10 +2,10 @@ declare(strict_types=1); -namespace PhpLlm\LlmChain\Tests\Response; +namespace PhpLlm\LlmChain\Tests\Model\Response; -use PhpLlm\LlmChain\Response\Choice; -use PhpLlm\LlmChain\Response\ToolCall; +use PhpLlm\LlmChain\Model\Response\Choice; +use PhpLlm\LlmChain\Model\Response\ToolCall; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Small; use PHPUnit\Framework\Attributes\Test; diff --git a/tests/Response/StreamResponseTest.php b/tests/Model/Response/StreamResponseTest.php similarity index 88% rename from tests/Response/StreamResponseTest.php rename to tests/Model/Response/StreamResponseTest.php index 40153b0c..fd208483 100644 --- a/tests/Response/StreamResponseTest.php +++ b/tests/Model/Response/StreamResponseTest.php @@ -2,9 +2,9 @@ declare(strict_types=1); -namespace PhpLlm\LlmChain\Tests\Response; +namespace PhpLlm\LlmChain\Tests\Model\Response; -use PhpLlm\LlmChain\Response\StreamResponse; +use PhpLlm\LlmChain\Model\Response\StreamResponse; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Small; use PHPUnit\Framework\Attributes\Test; diff --git a/tests/Response/StructuredResponseTest.php b/tests/Model/Response/StructuredResponseTest.php similarity index 88% rename from tests/Response/StructuredResponseTest.php rename to tests/Model/Response/StructuredResponseTest.php index 0f944811..578ea8e5 100644 --- a/tests/Response/StructuredResponseTest.php +++ b/tests/Model/Response/StructuredResponseTest.php @@ -2,9 +2,9 @@ declare(strict_types=1); -namespace PhpLlm\LlmChain\Tests\Response; +namespace PhpLlm\LlmChain\Tests\Model\Response; -use PhpLlm\LlmChain\Response\StructuredResponse; +use PhpLlm\LlmChain\Model\Response\StructuredResponse; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Small; use PHPUnit\Framework\Attributes\Test; diff --git a/tests/Response/TextResponseTest.php b/tests/Model/Response/TextResponseTest.php similarity index 82% rename from tests/Response/TextResponseTest.php rename to tests/Model/Response/TextResponseTest.php index ffacb0e8..7dce5313 100644 --- a/tests/Response/TextResponseTest.php +++ b/tests/Model/Response/TextResponseTest.php @@ -2,9 +2,9 @@ declare(strict_types=1); -namespace PhpLlm\LlmChain\Tests\Response; +namespace PhpLlm\LlmChain\Tests\Model\Response; -use PhpLlm\LlmChain\Response\TextResponse; +use PhpLlm\LlmChain\Model\Response\TextResponse; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Small; use PHPUnit\Framework\Attributes\Test; diff --git a/tests/Response/TollCallResponseTest.php b/tests/Model/Response/TollCallResponseTest.php similarity index 86% rename from tests/Response/TollCallResponseTest.php rename to tests/Model/Response/TollCallResponseTest.php index c8ae9295..22be08a0 100644 --- a/tests/Response/TollCallResponseTest.php +++ b/tests/Model/Response/TollCallResponseTest.php @@ -2,11 +2,11 @@ declare(strict_types=1); -namespace PhpLlm\LlmChain\Tests\Response; +namespace PhpLlm\LlmChain\Tests\Model\Response; use PhpLlm\LlmChain\Exception\InvalidArgumentException; -use PhpLlm\LlmChain\Response\ToolCall; -use PhpLlm\LlmChain\Response\ToolCallResponse; +use PhpLlm\LlmChain\Model\Response\ToolCall; +use PhpLlm\LlmChain\Model\Response\ToolCallResponse; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Small; use PHPUnit\Framework\Attributes\Test; diff --git a/tests/Response/ToolCallTest.php b/tests/Model/Response/ToolCallTest.php similarity index 91% rename from tests/Response/ToolCallTest.php rename to tests/Model/Response/ToolCallTest.php index a37f3627..0479ac83 100644 --- a/tests/Response/ToolCallTest.php +++ b/tests/Model/Response/ToolCallTest.php @@ -2,9 +2,9 @@ declare(strict_types=1); -namespace PhpLlm\LlmChain\Tests\Response; +namespace PhpLlm\LlmChain\Tests\Model\Response; -use PhpLlm\LlmChain\Response\ToolCall; +use PhpLlm\LlmChain\Model\Response\ToolCall; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Small; use PHPUnit\Framework\Attributes\Test;