diff --git a/examples/chat-claude-anthropic.php b/examples/chat-claude-anthropic.php index ed48d111..2a9c1386 100755 --- a/examples/chat-claude-anthropic.php +++ b/examples/chat-claude-anthropic.php @@ -23,6 +23,6 @@ Message::forSystem('You are a pirate and you write funny.'), Message::ofUser('What is the Symfony framework?'), ); -$response = $chain->call($messages); +$response = $chain->process($messages); echo $response->getContent().PHP_EOL; diff --git a/examples/chat-gpt-azure.php b/examples/chat-gpt-azure.php index 71094b25..436205bb 100755 --- a/examples/chat-gpt-azure.php +++ b/examples/chat-gpt-azure.php @@ -29,6 +29,6 @@ Message::forSystem('You are a pirate and you write funny.'), Message::ofUser('What is the Symfony framework?'), ); -$response = $chain->call($messages); +$response = $chain->process($messages); echo $response->getContent().PHP_EOL; diff --git a/examples/chat-gpt-openai.php b/examples/chat-gpt-openai.php index 6062aa43..e8916350 100755 --- a/examples/chat-gpt-openai.php +++ b/examples/chat-gpt-openai.php @@ -25,7 +25,7 @@ Message::forSystem('You are a pirate and you write funny.'), Message::ofUser('What is the Symfony framework?'), ); -$response = $chain->call($messages, [ +$response = $chain->process($messages, [ 'max_tokens' => 500, // specific options just for this call ]); diff --git a/examples/chat-llama-ollama.php b/examples/chat-llama-ollama.php index feb95f76..4531946d 100755 --- a/examples/chat-llama-ollama.php +++ b/examples/chat-llama-ollama.php @@ -23,6 +23,6 @@ Message::forSystem('You are a helpful assistant.'), Message::ofUser('Tina has one brother and one sister. How many sisters do Tina\'s siblings have?'), ); -$response = $chain->call($messages); +$response = $chain->process($messages); echo $response->getContent().PHP_EOL; diff --git a/examples/chat-llama-replicate.php b/examples/chat-llama-replicate.php index c0579a3e..fdf63c1a 100755 --- a/examples/chat-llama-replicate.php +++ b/examples/chat-llama-replicate.php @@ -23,6 +23,6 @@ Message::forSystem('You are a helpful assistant.'), Message::ofUser('Tina has one brother and one sister. How many sisters do Tina\'s siblings have?'), ); -$response = $chain->call($messages); +$response = $chain->process($messages); echo $response->getContent().PHP_EOL; diff --git a/examples/chat-o1-openai.php b/examples/chat-o1-openai.php index b5987cd5..d0535d48 100755 --- a/examples/chat-o1-openai.php +++ b/examples/chat-o1-openai.php @@ -32,6 +32,6 @@ at the beginning and end, not throughout the code. PROMPT; -$response = (new Chain($platform, $llm))->call(new MessageBag(Message::ofUser($prompt))); +$response = (new Chain($platform, $llm))->process(new MessageBag(Message::ofUser($prompt))); echo $response->getContent().PHP_EOL; diff --git a/examples/image-describer-binary.php b/examples/image-describer-binary.php index 980c35bd..41deff4d 100755 --- a/examples/image-describer-binary.php +++ b/examples/image-describer-binary.php @@ -27,6 +27,6 @@ new Image(dirname(__DIR__).'/tests/Fixture/image.png'), ), ); -$response = $chain->call($messages); +$response = $chain->process($messages); echo $response->getContent().PHP_EOL; diff --git a/examples/image-describer-url.php b/examples/image-describer-url.php index 00038433..7250562f 100755 --- a/examples/image-describer-url.php +++ b/examples/image-describer-url.php @@ -27,6 +27,6 @@ new Image('https://upload.wikimedia.org/wikipedia/commons/thumb/3/31/Webysther_20160423_-_Elephpant.svg/350px-Webysther_20160423_-_Elephpant.svg.png'), ), ); -$response = $chain->call($messages); +$response = $chain->process($messages); echo $response->getContent().PHP_EOL; diff --git a/examples/store-mongodb-similarity-search.php b/examples/store-mongodb-similarity-search.php index 78808da4..fac1e1f7 100755 --- a/examples/store-mongodb-similarity-search.php +++ b/examples/store-mongodb-similarity-search.php @@ -63,13 +63,13 @@ $similaritySearch = new SimilaritySearch($platform, $embeddings, $store); $toolBox = new ToolBox(new ToolAnalyzer(), [$similaritySearch]); -$processor = new ChainProcessor($toolBox); -$chain = new Chain($platform, $llm, [$processor], [$processor]); +$processor = (new ChainProcessor())->withToolBox($toolBox); +$chain = new Chain($platform, $llm); $messages = new MessageBag( Message::forSystem('Please answer all user questions only using SimilaritySearch function.'), Message::ofUser('Which movie fits the theme of the mafia?') ); -$response = $chain->call($messages); +$response = $chain->process(messages: $messages, chainProcessor: $processor); echo $response->getContent().PHP_EOL; diff --git a/examples/store-pinecone-similarity-search.php b/examples/store-pinecone-similarity-search.php index 5ecfb770..03f4b7dd 100755 --- a/examples/store-pinecone-similarity-search.php +++ b/examples/store-pinecone-similarity-search.php @@ -54,13 +54,13 @@ $similaritySearch = new SimilaritySearch($platform, $embeddings, $store); $toolBox = new ToolBox(new ToolAnalyzer(), [$similaritySearch]); -$processor = new ChainProcessor($toolBox); -$chain = new Chain($platform, $llm, [$processor], [$processor]); +$processor = (new ChainProcessor())->withToolBox($toolBox); +$chain = new Chain($platform, $llm); $messages = new MessageBag( Message::forSystem('Please answer all user questions only using SimilaritySearch function.'), Message::ofUser('Which movie fits the theme of the mafia?') ); -$response = $chain->call($messages); +$response = $chain->process(messages: $messages, chainProcessor: $processor); echo $response->getContent().PHP_EOL; diff --git a/examples/stream-claude-anthropic.php b/examples/stream-claude-anthropic.php index bc6573be..2728eb50 100644 --- a/examples/stream-claude-anthropic.php +++ b/examples/stream-claude-anthropic.php @@ -23,7 +23,7 @@ Message::forSystem('You are a thoughtful philosopher.'), Message::ofUser('What is the purpose of an ant?'), ); -$response = $chain->call($messages, [ +$response = $chain->process($messages, [ 'stream' => true, // enable streaming of response text ]); diff --git a/examples/stream-gpt-openai.php b/examples/stream-gpt-openai.php index afe9b0a4..22b847e8 100644 --- a/examples/stream-gpt-openai.php +++ b/examples/stream-gpt-openai.php @@ -23,7 +23,7 @@ Message::forSystem('You are a thoughtful philosopher.'), Message::ofUser('What is the purpose of an ant?'), ); -$response = $chain->call($messages, [ +$response = $chain->process($messages, [ 'stream' => true, // enable streaming of response text ]); diff --git a/examples/structured-output-clock.php b/examples/structured-output-clock.php index ac19feb4..1ec5d80c 100755 --- a/examples/structured-output-clock.php +++ b/examples/structured-output-clock.php @@ -30,13 +30,15 @@ $clock = new Clock(new SymfonyClock()); $toolBox = new ToolBox(new ToolAnalyzer(), [$clock]); -$toolProcessor = new ToolProcessor($toolBox); +$toolProcessor = (new ToolProcessor())->withToolBox($toolBox); $serializer = new Serializer([new ObjectNormalizer()], [new JsonEncoder()]); $structuredOutputProcessor = new StructuredOutputProcessor(new ResponseFormatFactory(), $serializer); -$chain = new Chain($platform, $llm, [$toolProcessor, $structuredOutputProcessor], [$toolProcessor, $structuredOutputProcessor]); +$structuredOutputProcessor->setOutputProcessors([$toolProcessor, $structuredOutputProcessor]); +$structuredOutputProcessor->setInputProcessors([$toolProcessor, $structuredOutputProcessor]); +$chain = new Chain($platform, $llm); $messages = new MessageBag(Message::ofUser('What date and time is it?')); -$response = $chain->call($messages, ['response_format' => [ +$response = $chain->process($messages, ['response_format' => [ 'type' => 'json_schema', 'json_schema' => [ 'name' => 'clock', @@ -51,6 +53,6 @@ 'additionalProperties' => false, ], ], -]]); +]], $structuredOutputProcessor); dump($response->getContent()); diff --git a/examples/structured-output-math.php b/examples/structured-output-math.php index 65f372aa..3c027971 100644 --- a/examples/structured-output-math.php +++ b/examples/structured-output-math.php @@ -26,11 +26,11 @@ $serializer = new Serializer([new ObjectNormalizer()], [new JsonEncoder()]); $processor = new ChainProcessor(new ResponseFormatFactory(), $serializer); -$chain = new Chain($platform, $llm, [$processor], [$processor]); +$chain = new Chain($platform, $llm); $messages = new MessageBag( Message::forSystem('You are a helpful math tutor. Guide the user through the solution step by step.'), Message::ofUser('how can I solve 8x + 7 = -23'), ); -$response = $chain->call($messages, ['output_structure' => MathReasoning::class]); +$response = $chain->process($messages, ['output_structure' => MathReasoning::class], $processor); dump($response->getContent()); diff --git a/examples/toolbox-clock.php b/examples/toolbox-clock.php index 490b17a5..945fe26d 100755 --- a/examples/toolbox-clock.php +++ b/examples/toolbox-clock.php @@ -25,10 +25,10 @@ $clock = new Clock(new SymfonyClock()); $toolBox = new ToolBox(new ToolAnalyzer(), [$clock]); -$processor = new ChainProcessor($toolBox); -$chain = new Chain($platform, $llm, [$processor], [$processor]); +$processor = (new ChainProcessor())->withToolBox($toolBox); +$chain = new Chain($platform, $llm); $messages = new MessageBag(Message::ofUser('What date and time is it?')); -$response = $chain->call($messages); +$response = $chain->process($messages, [], $processor); echo $response->getContent().PHP_EOL; diff --git a/examples/toolbox-serpapi.php b/examples/toolbox-serpapi.php index e424f24f..ae2311cb 100755 --- a/examples/toolbox-serpapi.php +++ b/examples/toolbox-serpapi.php @@ -24,10 +24,10 @@ $serpApi = new SerpApi(HttpClient::create(), $_ENV['SERP_API_KEY']); $toolBox = new ToolBox(new ToolAnalyzer(), [$serpApi]); -$processor = new ChainProcessor($toolBox); -$chain = new Chain($platform, $llm, [$processor], [$processor]); +$processor = (new ChainProcessor())->withToolBox($toolBox); +$chain = new Chain($platform, $llm); $messages = new MessageBag(Message::ofUser('Who is the current chancellor of Germany?')); -$response = $chain->call($messages); +$response = $chain->process(messages: $messages, chainProcessor: $processor); echo $response->getContent().PHP_EOL; diff --git a/examples/toolbox-weather.php b/examples/toolbox-weather.php index fbcf6a16..0da84e9c 100755 --- a/examples/toolbox-weather.php +++ b/examples/toolbox-weather.php @@ -25,10 +25,10 @@ $wikipedia = new OpenMeteo(HttpClient::create()); $toolBox = new ToolBox(new ToolAnalyzer(), [$wikipedia]); -$processor = new ChainProcessor($toolBox); -$chain = new Chain($platform, $llm, [$processor], [$processor]); +$processor = (new ChainProcessor())->withToolBox($toolBox); +$chain = new Chain($platform, $llm); $messages = new MessageBag(Message::ofUser('How is the weather currently in Berlin?')); -$response = $chain->call($messages); +$response = $chain->process($messages, [], $processor); echo $response->getContent().PHP_EOL; diff --git a/examples/toolbox-wikipedia.php b/examples/toolbox-wikipedia.php index 09b2a010..d2b97d82 100755 --- a/examples/toolbox-wikipedia.php +++ b/examples/toolbox-wikipedia.php @@ -25,10 +25,10 @@ $wikipedia = new Wikipedia(HttpClient::create()); $toolBox = new ToolBox(new ToolAnalyzer(), [$wikipedia]); -$processor = new ChainProcessor($toolBox); -$chain = new Chain($platform, $llm, [$processor], [$processor]); +$processor = (new ChainProcessor())->withToolBox($toolBox); +$chain = new Chain($platform, $llm); $messages = new MessageBag(Message::ofUser('Who is the current chancellor of Germany?')); -$response = $chain->call($messages); +$response = $chain->process(messages: $messages, chainProcessor: $processor); echo $response->getContent().PHP_EOL; diff --git a/examples/toolbox-youtube.php b/examples/toolbox-youtube.php index 938030df..023c91ce 100755 --- a/examples/toolbox-youtube.php +++ b/examples/toolbox-youtube.php @@ -25,13 +25,13 @@ $transcriber = new YouTubeTranscriber(HttpClient::create()); $toolBox = new ToolBox(new ToolAnalyzer(), [$transcriber]); -$processor = new ChainProcessor($toolBox); -$chain = new Chain($platform, $llm, [$processor], [$processor]); +$processor = (new ChainProcessor())->withToolBox($toolBox); +$chain = new Chain($platform, $llm); $messages = new MessageBag(Message::ofUser('Please summarize this video for me: https://www.youtube.com/watch?v=6uXW-ulpj0s')); -$response = $chain->call($messages, [ +$response = $chain->process($messages, [ 'stream' => true, // enable streaming of response text -]); +], $processor); foreach ($response->getContent() as $word) { echo $word; diff --git a/src/Chain.php b/src/Chain.php index f649218e..5d7e9f34 100644 --- a/src/Chain.php +++ b/src/Chain.php @@ -9,7 +9,6 @@ use PhpLlm\LlmChain\Chain\InputProcessor; use PhpLlm\LlmChain\Chain\Output; use PhpLlm\LlmChain\Chain\OutputProcessor; -use PhpLlm\LlmChain\Exception\InvalidArgumentException; use PhpLlm\LlmChain\Exception\MissingModelSupport; use PhpLlm\LlmChain\Model\LanguageModel; use PhpLlm\LlmChain\Model\Message\MessageBag; @@ -18,37 +17,23 @@ final readonly class Chain implements ChainInterface { - /** - * @var InputProcessor[] - */ - private array $inputProcessor; - - /** - * @var OutputProcessor[] - */ - private array $outputProcessor; - - /** - * @param InputProcessor[] $inputProcessor - * @param OutputProcessor[] $outputProcessor - */ public function __construct( private PlatformInterface $platform, private LanguageModel $llm, - iterable $inputProcessor = [], - iterable $outputProcessor = [], ) { - $this->inputProcessor = $this->initializeProcessors($inputProcessor, InputProcessor::class); - $this->outputProcessor = $this->initializeProcessors($outputProcessor, OutputProcessor::class); } /** * @param array $options */ - public function call(MessageBag $messages, array $options = []): ResponseInterface + public function process(MessageBag $messages, array $options = [], ?ChainAwareProcessor $chainProcessor = null): ResponseInterface { + $chainProcessor?->setChain($this); + $input = new Input($this->llm, $messages, $options); - array_map(fn (InputProcessor $processor) => $processor->processInput($input), $this->inputProcessor); + if ($chainProcessor) { + array_map(fn (InputProcessor $processor) => $processor->processInput($input), $chainProcessor->getInputProcessors()); + } if ($messages->containsImage() && !$this->llm->supportsImageInput()) { throw MissingModelSupport::forImageInput($this->llm::class); @@ -61,28 +46,11 @@ public function call(MessageBag $messages, array $options = []): ResponseInterfa } $output = new Output($this->llm, $response, $messages, $options); - array_map(fn (OutputProcessor $processor) => $processor->processOutput($output), $this->outputProcessor); - return $output->response; - } - - /** - * @param InputProcessor[]|OutputProcessor[] $processors - * - * @return InputProcessor[]|OutputProcessor[] - */ - private function initializeProcessors(iterable $processors, string $interface): array - { - foreach ($processors as $processor) { - if (!$processor instanceof $interface) { - throw new InvalidArgumentException(sprintf('Processor %s must implement %s interface.', $processor::class, $interface)); - } - - if ($processor instanceof ChainAwareProcessor) { - $processor->setChain($this); - } + if ($chainProcessor) { + array_map(fn (OutputProcessor $processor) => $processor->processOutput($output), $chainProcessor->getOutputProcessors()); } - return $processors instanceof \Traversable ? iterator_to_array($processors) : $processors; + return $output->response; } } diff --git a/src/Chain/ChainAwareProcessor.php b/src/Chain/ChainAwareProcessor.php index 4ce94122..8a7e5612 100644 --- a/src/Chain/ChainAwareProcessor.php +++ b/src/Chain/ChainAwareProcessor.php @@ -4,9 +4,33 @@ namespace PhpLlm\LlmChain\Chain; -use PhpLlm\LlmChain\Chain; +use PhpLlm\LlmChain\ChainInterface; interface ChainAwareProcessor { - public function setChain(Chain $chain): void; + public function setChain(ChainInterface $chain): void; + + public function addOutputProcessor(OutputProcessor $outputProcessor): self; + + public function addInputProcessor(InputProcessor $outputProcessor): self; + + /** + * @return array + */ + public function getInputProcessors(): array; + + /** + * @return array + */ + public function getOutputProcessors(): array; + + /** + * @param array $inputProcessors + */ + public function setInputProcessors(array $inputProcessors): self; + + /** + * @param array $outputProcessors + */ + public function setOutputProcessors(array $outputProcessors): self; } diff --git a/src/Chain/ChainAwareTrait.php b/src/Chain/ChainAwareTrait.php index 25be91a3..ad3e13ea 100644 --- a/src/Chain/ChainAwareTrait.php +++ b/src/Chain/ChainAwareTrait.php @@ -4,14 +4,90 @@ namespace PhpLlm\LlmChain\Chain; -use PhpLlm\LlmChain\Chain; +use PhpLlm\LlmChain\Chain\ToolBox\ToolBoxInterface; +use PhpLlm\LlmChain\ChainInterface; +use PhpLlm\LlmChain\Exception\InvalidArgumentException; trait ChainAwareTrait { - private Chain $chain; + private ChainInterface $chain; - public function setChain(Chain $chain): void + private ToolBoxInterface $toolBox; + + /** + * @var InputProcessor[] + */ + private array $inputProcessors = []; + + /** + * @var OutputProcessor[] + */ + private array $outputProcessors = []; + + public function setChain(ChainInterface $chain): void { $this->chain = $chain; } + + public function getInputProcessors(): array + { + return $this->inputProcessors; + } + + public function getOutputProcessors(): array + { + return $this->outputProcessors; + } + + public function addOutputProcessor(OutputProcessor $outputProcessor): self + { + if (!in_array($outputProcessor, $this->outputProcessors)) { + $this->outputProcessors[] = $outputProcessor; + } + + return $this; + } + + public function addInputProcessor(InputProcessor $outputProcessor): self + { + if (!in_array($outputProcessor, $this->inputProcessors)) { + $this->inputProcessors[] = $outputProcessor; + } + + return $this; + } + + public function setOutputProcessors(iterable $outputProcessors): self + { + foreach ($outputProcessors as $processor) { + if (!$processor instanceof OutputProcessor) { + throw new InvalidArgumentException(sprintf('Processor %s must implement %s interface.', $processor::class, OutputProcessor::class)); + } + + $this->addOutputProcessor($processor); + } + + return $this; + } + + public function setInputProcessors(iterable $inputProcessors): self + { + foreach ($inputProcessors as $processor) { + if (!$processor instanceof InputProcessor) { + throw new InvalidArgumentException(sprintf('Processor %s must implement %s interface.', $processor::class, InputProcessor::class)); + } + + $this->addInputProcessor($processor); + } + + return $this; + } + + public function withToolBox(ToolBoxInterface $toolBox): self + { + $chain = clone $this; + $chain->toolBox = $toolBox; + + return $chain; + } } diff --git a/src/Chain/StructuredOutput/ChainProcessor.php b/src/Chain/StructuredOutput/ChainProcessor.php index ba52b23b..d9b4513d 100644 --- a/src/Chain/StructuredOutput/ChainProcessor.php +++ b/src/Chain/StructuredOutput/ChainProcessor.php @@ -4,6 +4,8 @@ namespace PhpLlm\LlmChain\Chain\StructuredOutput; +use PhpLlm\LlmChain\Chain\ChainAwareProcessor; +use PhpLlm\LlmChain\Chain\ChainAwareTrait; use PhpLlm\LlmChain\Chain\Input; use PhpLlm\LlmChain\Chain\InputProcessor; use PhpLlm\LlmChain\Chain\Output; @@ -13,8 +15,10 @@ use PhpLlm\LlmChain\Model\Response\StructuredResponse; use Symfony\Component\Serializer\SerializerInterface; -final class ChainProcessor implements InputProcessor, OutputProcessor +final class ChainProcessor implements InputProcessor, OutputProcessor, ChainAwareProcessor { + use ChainAwareTrait; + private string $outputStructure; public function __construct( diff --git a/src/Chain/ToolBox/ChainProcessor.php b/src/Chain/ToolBox/ChainProcessor.php index 15293d9c..b35a5a53 100644 --- a/src/Chain/ToolBox/ChainProcessor.php +++ b/src/Chain/ToolBox/ChainProcessor.php @@ -18,11 +18,6 @@ final class ChainProcessor implements InputProcessor, OutputProcessor, ChainAwar { use ChainAwareTrait; - public function __construct( - private ToolBoxInterface $toolBox, - ) { - } - public function processInput(Input $input): void { if (!$input->llm->supportsToolCalling()) { @@ -52,7 +47,7 @@ public function processOutput(Output $output): void $messages[] = Message::ofToolCall($toolCall, $result); } - $output->response = $this->chain->call($messages, $output->options); + $output->response = $this->chain->process($messages, $output->options, $this); } } } diff --git a/src/ChainInterface.php b/src/ChainInterface.php index c152fb9c..114b8b9f 100644 --- a/src/ChainInterface.php +++ b/src/ChainInterface.php @@ -4,6 +4,7 @@ namespace PhpLlm\LlmChain; +use PhpLlm\LlmChain\Chain\ChainAwareProcessor; use PhpLlm\LlmChain\Model\Message\MessageBag; use PhpLlm\LlmChain\Model\Response\ResponseInterface; @@ -12,5 +13,5 @@ interface ChainInterface /** * @param array $options */ - public function call(MessageBag $messages, array $options = []): ResponseInterface; + public function process(MessageBag $messages, array $options = [], ?ChainAwareProcessor $chainProcessor = null): ResponseInterface; } diff --git a/tests/Chain/ToolBox/ChainProcessorTest.php b/tests/Chain/ToolBox/ChainProcessorTest.php index 55ca2aa0..e9fffd1d 100644 --- a/tests/Chain/ToolBox/ChainProcessorTest.php +++ b/tests/Chain/ToolBox/ChainProcessorTest.php @@ -30,7 +30,7 @@ public function processInputWithoutRegisteredToolsWillResultInNoOptionChange(): $llm = $this->createMock(LanguageModel::class); $llm->method('supportsToolCalling')->willReturn(true); - $chainProcessor = new ChainProcessor($toolBox); + $chainProcessor = (new ChainProcessor())->withToolBox($toolBox); $input = new Input($llm, new MessageBag(), []); $chainProcessor->processInput($input); @@ -47,7 +47,7 @@ public function processInputWithRegisteredToolsWillResultInOptionChange(): void $llm = $this->createMock(LanguageModel::class); $llm->method('supportsToolCalling')->willReturn(true); - $chainProcessor = new ChainProcessor($toolBox); + $chainProcessor = (new ChainProcessor())->withToolBox($toolBox); $input = new Input($llm, new MessageBag(), []); $chainProcessor->processInput($input); @@ -63,7 +63,7 @@ public function processInputWithUnsupportedToolCallingWillThrowException(): void $llm = $this->createMock(LanguageModel::class); $llm->method('supportsToolCalling')->willReturn(false); - $chainProcessor = new ChainProcessor($this->createStub(ToolBoxInterface::class)); + $chainProcessor = new ChainProcessor(); $input = new Input($llm, new MessageBag(), []); $chainProcessor->processInput($input); diff --git a/tests/Fixture/Tool/ToolWrong.php b/tests/Fixture/Tool/ToolWithoutAttribute.php similarity index 91% rename from tests/Fixture/Tool/ToolWrong.php rename to tests/Fixture/Tool/ToolWithoutAttribute.php index f1964a70..7ab6d315 100644 --- a/tests/Fixture/Tool/ToolWrong.php +++ b/tests/Fixture/Tool/ToolWithoutAttribute.php @@ -4,7 +4,7 @@ namespace PhpLlm\LlmChain\Tests\Fixture\Tool; -final class ToolWrong +final class ToolWithoutAttribute { /** * @param string $text The text given to the tool