├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── composer.json ├── ollama.http └── src ├── Chain ├── Chain.php ├── ChainAwareInterface.php ├── ChainAwareTrait.php ├── ChainInterface.php ├── Exception │ ├── ExceptionInterface.php │ ├── InvalidArgumentException.php │ ├── LogicException.php │ ├── MissingModelSupportException.php │ └── RuntimeException.php ├── Input.php ├── InputProcessor │ ├── ModelOverrideInputProcessor.php │ └── SystemPromptInputProcessor.php ├── InputProcessorInterface.php ├── Output.php ├── OutputProcessorInterface.php ├── StructuredOutput │ ├── ChainProcessor.php │ ├── ResponseFormatFactory.php │ └── ResponseFormatFactoryInterface.php └── Toolbox │ ├── Attribute │ └── AsTool.php │ ├── ChainProcessor.php │ ├── Event │ └── ToolCallsExecuted.php │ ├── Exception │ ├── ExceptionInterface.php │ ├── ToolConfigurationException.php │ ├── ToolException.php │ ├── ToolExecutionException.php │ └── ToolNotFoundException.php │ ├── FaultTolerantToolbox.php │ ├── StreamResponse.php │ ├── Tool │ ├── Brave.php │ ├── Chain.php │ ├── Clock.php │ ├── Crawler.php │ ├── OpenMeteo.php │ ├── SerpApi.php │ ├── SimilaritySearch.php │ ├── Tavily.php │ ├── Wikipedia.php │ └── YouTubeTranscriber.php │ ├── ToolCallResult.php │ ├── ToolFactory │ ├── AbstractToolFactory.php │ ├── ChainFactory.php │ ├── MemoryToolFactory.php │ └── ReflectionToolFactory.php │ ├── ToolFactoryInterface.php │ ├── ToolResultConverter.php │ ├── Toolbox.php │ └── ToolboxInterface.php ├── Platform ├── Bridge │ ├── Anthropic │ │ ├── Claude.php │ │ ├── Contract │ │ │ ├── AssistantMessageNormalizer.php │ │ │ ├── DocumentNormalizer.php │ │ │ ├── DocumentUrlNormalizer.php │ │ │ ├── ImageNormalizer.php │ │ │ ├── ImageUrlNormalizer.php │ │ │ ├── MessageBagNormalizer.php │ │ │ ├── ToolCallMessageNormalizer.php │ │ │ └── ToolNormalizer.php │ │ ├── ModelClient.php │ │ ├── PlatformFactory.php │ │ └── ResponseConverter.php │ ├── Azure │ │ ├── Meta │ │ │ ├── LlamaHandler.php │ │ │ └── PlatformFactory.php │ │ └── OpenAI │ │ │ ├── EmbeddingsModelClient.php │ │ │ ├── GPTModelClient.php │ │ │ ├── PlatformFactory.php │ │ │ └── WhisperModelClient.php │ ├── Bedrock │ │ ├── Anthropic │ │ │ └── ClaudeHandler.php │ │ ├── BedrockModelClient.php │ │ ├── Meta │ │ │ └── LlamaModelClient.php │ │ ├── Nova │ │ │ ├── Contract │ │ │ │ ├── AssistantMessageNormalizer.php │ │ │ │ ├── MessageBagNormalizer.php │ │ │ │ ├── ToolCallMessageNormalizer.php │ │ │ │ ├── ToolNormalizer.php │ │ │ │ └── UserMessageNormalizer.php │ │ │ ├── Nova.php │ │ │ └── NovaHandler.php │ │ ├── Platform.php │ │ └── PlatformFactory.php │ ├── Google │ │ ├── Contract │ │ │ ├── AssistantMessageNormalizer.php │ │ │ ├── MessageBagNormalizer.php │ │ │ └── UserMessageNormalizer.php │ │ ├── Gemini.php │ │ ├── ModelHandler.php │ │ └── PlatformFactory.php │ ├── HuggingFace │ │ ├── ApiClient.php │ │ ├── Contract │ │ │ ├── FileNormalizer.php │ │ │ └── MessageBagNormalizer.php │ │ ├── ModelClient.php │ │ ├── Output │ │ │ ├── Classification.php │ │ │ ├── ClassificationResult.php │ │ │ ├── DetectedObject.php │ │ │ ├── FillMaskResult.php │ │ │ ├── ImageSegment.php │ │ │ ├── ImageSegmentationResult.php │ │ │ ├── MaskFill.php │ │ │ ├── ObjectDetectionResult.php │ │ │ ├── QuestionAnsweringResult.php │ │ │ ├── SentenceSimilarityResult.php │ │ │ ├── TableQuestionAnsweringResult.php │ │ │ ├── Token.php │ │ │ ├── TokenClassificationResult.php │ │ │ └── ZeroShotClassificationResult.php │ │ ├── PlatformFactory.php │ │ ├── Provider.php │ │ ├── ResponseConverter.php │ │ └── Task.php │ ├── Meta │ │ ├── Contract │ │ │ └── MessageBagNormalizer.php │ │ ├── Llama.php │ │ └── LlamaPromptConverter.php │ ├── Mistral │ │ ├── Contract │ │ │ └── ToolNormalizer.php │ │ ├── Embeddings.php │ │ ├── Embeddings │ │ │ ├── ModelClient.php │ │ │ └── ResponseConverter.php │ │ ├── Llm │ │ │ ├── ModelClient.php │ │ │ └── ResponseConverter.php │ │ ├── Mistral.php │ │ └── PlatformFactory.php │ ├── Ollama │ │ ├── LlamaModelHandler.php │ │ └── PlatformFactory.php │ ├── OpenAI │ │ ├── DallE.php │ │ ├── DallE │ │ │ ├── Base64Image.php │ │ │ ├── ImageResponse.php │ │ │ ├── ModelClient.php │ │ │ └── UrlImage.php │ │ ├── Embeddings.php │ │ ├── Embeddings │ │ │ ├── ModelClient.php │ │ │ └── ResponseConverter.php │ │ ├── GPT.php │ │ ├── GPT │ │ │ ├── ModelClient.php │ │ │ └── ResponseConverter.php │ │ ├── PlatformFactory.php │ │ ├── TokenOutputProcessor.php │ │ ├── Whisper.php │ │ └── Whisper │ │ │ ├── AudioNormalizer.php │ │ │ ├── ModelClient.php │ │ │ └── ResponseConverter.php │ ├── OpenRouter │ │ ├── Client.php │ │ └── PlatformFactory.php │ ├── Replicate │ │ ├── Client.php │ │ ├── Contract │ │ │ └── LlamaMessageBagNormalizer.php │ │ ├── LlamaModelClient.php │ │ ├── LlamaResponseConverter.php │ │ └── PlatformFactory.php │ ├── TransformersPHP │ │ ├── Platform.php │ │ └── PlatformFactory.php │ └── Voyage │ │ ├── ModelHandler.php │ │ ├── PlatformFactory.php │ │ └── Voyage.php ├── Capability.php ├── Contract.php ├── Contract │ ├── JsonSchema │ │ ├── Attribute │ │ │ └── With.php │ │ ├── DescriptionParser.php │ │ └── Factory.php │ └── Normalizer │ │ ├── Message │ │ ├── AssistantMessageNormalizer.php │ │ ├── Content │ │ │ ├── AudioNormalizer.php │ │ │ ├── ImageNormalizer.php │ │ │ ├── ImageUrlNormalizer.php │ │ │ └── TextNormalizer.php │ │ ├── MessageBagNormalizer.php │ │ ├── SystemMessageNormalizer.php │ │ ├── ToolCallMessageNormalizer.php │ │ └── UserMessageNormalizer.php │ │ ├── ModelContractNormalizer.php │ │ ├── Response │ │ └── ToolCallNormalizer.php │ │ └── ToolNormalizer.php ├── Exception │ ├── ContentFilterException.php │ ├── ExceptionInterface.php │ ├── InvalidArgumentException.php │ └── RuntimeException.php ├── Message │ ├── AssistantMessage.php │ ├── Content │ │ ├── Audio.php │ │ ├── ContentInterface.php │ │ ├── Document.php │ │ ├── DocumentUrl.php │ │ ├── File.php │ │ ├── Image.php │ │ ├── ImageUrl.php │ │ └── Text.php │ ├── Message.php │ ├── MessageBag.php │ ├── MessageBagInterface.php │ ├── MessageInterface.php │ ├── Role.php │ ├── SystemMessage.php │ ├── ToolCallMessage.php │ └── UserMessage.php ├── Model.php ├── ModelClientInterface.php ├── Platform.php ├── PlatformInterface.php ├── Response │ ├── AsyncResponse.php │ ├── BaseResponse.php │ ├── BinaryResponse.php │ ├── Choice.php │ ├── ChoiceResponse.php │ ├── Exception │ │ └── RawResponseAlreadySetException.php │ ├── Metadata │ │ ├── Metadata.php │ │ └── MetadataAwareTrait.php │ ├── ObjectResponse.php │ ├── RawResponseAwareTrait.php │ ├── ResponseInterface.php │ ├── StreamResponse.php │ ├── TextResponse.php │ ├── ToolCall.php │ ├── ToolCallResponse.php │ └── VectorResponse.php ├── ResponseConverterInterface.php ├── Tool │ ├── ExecutionReference.php │ └── Tool.php └── Vector │ ├── NullVector.php │ ├── Vector.php │ └── VectorInterface.php └── Store ├── Bridge ├── Azure │ └── SearchStore.php ├── ChromaDB │ └── Store.php ├── MongoDB │ └── Store.php └── Pinecone │ └── Store.php ├── Document ├── Metadata.php ├── TextDocument.php └── VectorDocument.php ├── Embedder.php ├── Exception ├── ExceptionInterface.php ├── InvalidArgumentException.php └── RuntimeException.php ├── InitializableStoreInterface.php ├── StoreInterface.php └── VectorStoreInterface.php /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | You want to contribute to this project? Great! We are happy to welcome you to the repository. Please read the following 4 | guidelines to get started. It will help to make the contribution process easy and effective for everyone involved. 5 | 6 | ## Disclaimer 7 | 8 | Before you start contributing, please be aware that this project is not commercial and is maintained by volunteers in 9 | their free time to share code among themselves and with the community. We are happy to invest our time in this project 10 | and to share it with you. However, we cannot guarantee that we will be able to respond to your requests in a timely 11 | manner or that we will be able to implement your suggestions. 12 | 13 | ## Questions and Support 14 | 15 | If you have any questions or problems while using LLM Chain, please check the [README](README.md) file and also the 16 | [examples](examples) folder. If you can't find the answer there, feel free to open an 17 | [issue](https://github.com/php-llm/llm-chain/issues). 18 | 19 | ## Bug Reports 20 | 21 | If you open an issue to report a bug, please make sure to provide enough information to reproduce the bug. Ideally even 22 | provide a code snippet that reproduces the bug. This will help us to fix the bug faster. 23 | 24 | ## Feature Ideas 25 | 26 | Of course, we are happy to get your ideas for new features. And for sure, we are happy if you want to implement them. 27 | However, to make sure that you are not wasting your time, please open an issue first to discuss your idea in case it is 28 | a larger change. 29 | 30 | ## Pull Requests 31 | 32 | When you end up implementing a new feature or fixing a bug, please open a pull request. The pipeline will help you to 33 | check if your code is following the coding standards and if all tests are passing. To execute the tools locally, you 34 | can use the following commands: 35 | 36 | ```bash 37 | make ci-stable # execute all checks with stable dependencies 38 | make ci-lowest # execute all checks with lowest dependencies 39 | ``` 40 | 41 | Commit messages should follow the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) specification. 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2024 Christopher Hertel 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /ollama.http: -------------------------------------------------------------------------------- 1 | ### Llama 3.2 on Ollama 2 | POST http://localhost:11434/api/chat 3 | 4 | { 5 | "model": "llama3.2", 6 | "messages": [ 7 | { 8 | "role": "user", 9 | "content": "why is the sky blue?" 10 | } 11 | ], 12 | "stream": false 13 | } 14 | -------------------------------------------------------------------------------- /src/Chain/ChainAwareInterface.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | interface ChainAwareInterface 11 | { 12 | public function setChain(ChainInterface $chain): void; 13 | } 14 | -------------------------------------------------------------------------------- /src/Chain/ChainAwareTrait.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | trait ChainAwareTrait 11 | { 12 | private ChainInterface $chain; 13 | 14 | public function setChain(ChainInterface $chain): void 15 | { 16 | $this->chain = $chain; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Chain/ChainInterface.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | interface ChainInterface 14 | { 15 | /** 16 | * @param array $options 17 | */ 18 | public function call(MessageBagInterface $messages, array $options = []): ResponseInterface; 19 | } 20 | -------------------------------------------------------------------------------- /src/Chain/Exception/ExceptionInterface.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | interface ExceptionInterface extends \Throwable 11 | { 12 | } 13 | -------------------------------------------------------------------------------- /src/Chain/Exception/InvalidArgumentException.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface 11 | { 12 | } 13 | -------------------------------------------------------------------------------- /src/Chain/Exception/LogicException.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class LogicException extends \LogicException implements ExceptionInterface 11 | { 12 | } 13 | -------------------------------------------------------------------------------- /src/Chain/Exception/MissingModelSupportException.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | final class MissingModelSupportException extends RuntimeException 11 | { 12 | private function __construct(string $model, string $support) 13 | { 14 | parent::__construct(\sprintf('Model "%s" does not support "%s".', $model, $support)); 15 | } 16 | 17 | public static function forToolCalling(string $model): self 18 | { 19 | return new self($model, 'tool calling'); 20 | } 21 | 22 | public static function forAudioInput(string $model): self 23 | { 24 | return new self($model, 'audio input'); 25 | } 26 | 27 | public static function forImageInput(string $model): self 28 | { 29 | return new self($model, 'image input'); 30 | } 31 | 32 | public static function forStructuredOutput(string $model): self 33 | { 34 | return new self($model, 'structured output'); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Chain/Exception/RuntimeException.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class RuntimeException extends \RuntimeException implements ExceptionInterface 11 | { 12 | } 13 | -------------------------------------------------------------------------------- /src/Chain/Input.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | final class Input 14 | { 15 | /** 16 | * @param array $options 17 | */ 18 | public function __construct( 19 | public Model $model, 20 | public MessageBagInterface $messages, 21 | private array $options, 22 | ) { 23 | } 24 | 25 | /** 26 | * @return array 27 | */ 28 | public function getOptions(): array 29 | { 30 | return $this->options; 31 | } 32 | 33 | /** 34 | * @param array $options 35 | */ 36 | public function setOptions(array $options): void 37 | { 38 | $this->options = $options; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Chain/InputProcessor/ModelOverrideInputProcessor.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | final class ModelOverrideInputProcessor implements InputProcessorInterface 16 | { 17 | public function processInput(Input $input): void 18 | { 19 | $options = $input->getOptions(); 20 | 21 | if (!\array_key_exists('model', $options)) { 22 | return; 23 | } 24 | 25 | if (!$options['model'] instanceof Model) { 26 | throw new InvalidArgumentException(\sprintf('Option "model" must be an instance of %s.', Model::class)); 27 | } 28 | 29 | $input->model = $options['model']; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Chain/InputProcessor/SystemPromptInputProcessor.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | final readonly class SystemPromptInputProcessor implements InputProcessorInterface 19 | { 20 | /** 21 | * @param \Stringable|string $systemPrompt the system prompt to prepend to the input messages 22 | * @param ToolboxInterface|null $toolbox the tool box to be used to append the tool definitions to the system prompt 23 | */ 24 | public function __construct( 25 | private \Stringable|string $systemPrompt, 26 | private ?ToolboxInterface $toolbox = null, 27 | private LoggerInterface $logger = new NullLogger(), 28 | ) { 29 | } 30 | 31 | public function processInput(Input $input): void 32 | { 33 | $messages = $input->messages; 34 | 35 | if (null !== $messages->getSystemMessage()) { 36 | $this->logger->debug('Skipping system prompt injection since MessageBag already contains a system message.'); 37 | 38 | return; 39 | } 40 | 41 | $message = (string) $this->systemPrompt; 42 | 43 | if ($this->toolbox instanceof ToolboxInterface 44 | && [] !== $this->toolbox->getTools() 45 | ) { 46 | $this->logger->debug('Append tool definitions to system prompt.'); 47 | 48 | $tools = implode(\PHP_EOL.\PHP_EOL, array_map( 49 | fn (Tool $tool) => <<name} 51 | {$tool->description} 52 | TOOL, 53 | $this->toolbox->getTools() 54 | )); 55 | 56 | $message = <<systemPrompt} 58 | 59 | # Available tools 60 | 61 | {$tools} 62 | PROMPT; 63 | } 64 | 65 | $input->messages = $messages->prepend(Message::forSystem($message)); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Chain/InputProcessorInterface.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | interface InputProcessorInterface 11 | { 12 | public function processInput(Input $input): void; 13 | } 14 | -------------------------------------------------------------------------------- /src/Chain/Output.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | final class Output 15 | { 16 | /** 17 | * @param array $options 18 | */ 19 | public function __construct( 20 | public readonly Model $model, 21 | public ResponseInterface $response, 22 | public readonly MessageBagInterface $messages, 23 | public readonly array $options, 24 | ) { 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Chain/OutputProcessorInterface.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | interface OutputProcessorInterface 11 | { 12 | public function processOutput(Output $output): void; 13 | } 14 | -------------------------------------------------------------------------------- /src/Chain/StructuredOutput/ResponseFormatFactory.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | final readonly class ResponseFormatFactory implements ResponseFormatFactoryInterface 15 | { 16 | public function __construct( 17 | private Factory $schemaFactory = new Factory(), 18 | ) { 19 | } 20 | 21 | public function create(string $responseClass): array 22 | { 23 | return [ 24 | 'type' => 'json_schema', 25 | 'json_schema' => [ 26 | 'name' => u($responseClass)->afterLast('\\')->toString(), 27 | 'schema' => $this->schemaFactory->buildProperties($responseClass), 28 | 'strict' => true, 29 | ], 30 | ]; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Chain/StructuredOutput/ResponseFormatFactoryInterface.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | interface ResponseFormatFactoryInterface 11 | { 12 | /** 13 | * @param class-string $responseClass 14 | * 15 | * @return array{ 16 | * type: 'json_schema', 17 | * json_schema: array{ 18 | * name: string, 19 | * schema: array, 20 | * strict: true, 21 | * } 22 | * } 23 | */ 24 | public function create(string $responseClass): array; 25 | } 26 | -------------------------------------------------------------------------------- /src/Chain/Toolbox/Attribute/AsTool.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | #[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE)] 11 | final readonly class AsTool 12 | { 13 | public function __construct( 14 | public string $name, 15 | public string $description, 16 | public string $method = '__invoke', 17 | ) { 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Chain/Toolbox/Event/ToolCallsExecuted.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | final class ToolCallsExecuted 14 | { 15 | /** 16 | * @var ToolCallResult[] 17 | */ 18 | public readonly array $toolCallResults; 19 | public ResponseInterface $response; 20 | 21 | public function __construct(ToolCallResult ...$toolCallResults) 22 | { 23 | $this->toolCallResults = $toolCallResults; 24 | } 25 | 26 | public function hasResponse(): bool 27 | { 28 | return isset($this->response); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Chain/Toolbox/Exception/ExceptionInterface.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | interface ExceptionInterface extends BaseExceptionInterface 13 | { 14 | } 15 | -------------------------------------------------------------------------------- /src/Chain/Toolbox/Exception/ToolConfigurationException.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | final class ToolConfigurationException extends InvalidArgumentException implements ExceptionInterface 13 | { 14 | public static function invalidMethod(string $toolClass, string $methodName, \ReflectionException $previous): self 15 | { 16 | return new self(\sprintf('Method "%s" not found in tool "%s".', $methodName, $toolClass), previous: $previous); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Chain/Toolbox/Exception/ToolException.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | final class ToolException extends InvalidArgumentException implements ExceptionInterface 14 | { 15 | public static function invalidReference(mixed $reference): self 16 | { 17 | return new self(\sprintf('The reference "%s" is not a valid tool.', $reference)); 18 | } 19 | 20 | public static function missingAttribute(string $className): self 21 | { 22 | return new self(\sprintf('The class "%s" is not a tool, please add %s attribute.', $className, AsTool::class)); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Chain/Toolbox/Exception/ToolExecutionException.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | final class ToolExecutionException extends \RuntimeException implements ExceptionInterface 13 | { 14 | public ?ToolCall $toolCall = null; 15 | 16 | public static function executionFailed(ToolCall $toolCall, \Throwable $previous): self 17 | { 18 | $exception = new self(\sprintf('Execution of tool "%s" failed with error: %s', $toolCall->name, $previous->getMessage()), previous: $previous); 19 | $exception->toolCall = $toolCall; 20 | 21 | return $exception; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Chain/Toolbox/Exception/ToolNotFoundException.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | final class ToolNotFoundException extends \RuntimeException implements ExceptionInterface 14 | { 15 | public ?ToolCall $toolCall = null; 16 | 17 | public static function notFoundForToolCall(ToolCall $toolCall): self 18 | { 19 | $exception = new self(\sprintf('Tool not found for call: %s.', $toolCall->name)); 20 | $exception->toolCall = $toolCall; 21 | 22 | return $exception; 23 | } 24 | 25 | public static function notFoundForReference(ExecutionReference $reference): self 26 | { 27 | return new self(\sprintf('Tool not found for reference: %s::%s.', $reference->class, $reference->method)); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Chain/Toolbox/FaultTolerantToolbox.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | final readonly class FaultTolerantToolbox implements ToolboxInterface 18 | { 19 | public function __construct( 20 | private ToolboxInterface $innerToolbox, 21 | ) { 22 | } 23 | 24 | public function getTools(): array 25 | { 26 | return $this->innerToolbox->getTools(); 27 | } 28 | 29 | public function execute(ToolCall $toolCall): mixed 30 | { 31 | try { 32 | return $this->innerToolbox->execute($toolCall); 33 | } catch (ToolExecutionException $e) { 34 | return \sprintf('An error occurred while executing tool "%s".', $e->toolCall->name); 35 | } catch (ToolNotFoundException) { 36 | $names = array_map(fn (Tool $metadata) => $metadata->name, $this->getTools()); 37 | 38 | return \sprintf('Tool "%s" was not found, please use one of these: %s', $toolCall->name, implode(', ', $names)); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Chain/Toolbox/StreamResponse.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | final class StreamResponse extends BaseResponse 15 | { 16 | public function __construct( 17 | private readonly \Generator $generator, 18 | private readonly \Closure $handleToolCallsCallback, 19 | ) { 20 | } 21 | 22 | public function getContent(): \Generator 23 | { 24 | $streamedResponse = ''; 25 | foreach ($this->generator as $value) { 26 | if ($value instanceof ToolCallResponse) { 27 | yield from ($this->handleToolCallsCallback)($value, Message::ofAssistant($streamedResponse))->getContent(); 28 | 29 | break; 30 | } 31 | 32 | $streamedResponse .= $value; 33 | yield $value; 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Chain/Toolbox/Tool/Chain.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | final readonly class Chain 16 | { 17 | public function __construct( 18 | private ChainInterface $chain, 19 | ) { 20 | } 21 | 22 | /** 23 | * @param string $message the message to pass to the chain 24 | */ 25 | public function __invoke(string $message): string 26 | { 27 | $response = $this->chain->call(new MessageBag(Message::ofUser($message))); 28 | 29 | \assert($response instanceof TextResponse); 30 | 31 | return $response->getContent(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Chain/Toolbox/Tool/Clock.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | #[AsTool('clock', description: 'Provides the current date and time.')] 15 | final readonly class Clock 16 | { 17 | public function __construct( 18 | private ClockInterface $clock = new SymfonyClock(), 19 | ) { 20 | } 21 | 22 | public function __invoke(): string 23 | { 24 | return \sprintf( 25 | 'Current date is %s (YYYY-MM-DD) and the time is %s (HH:MM:SS).', 26 | $this->clock->now()->format('Y-m-d'), 27 | $this->clock->now()->format('H:i:s'), 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Chain/Toolbox/Tool/Crawler.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | #[AsTool('crawler', 'A tool that crawls one page of a website and returns the visible text of it.')] 16 | final readonly class Crawler 17 | { 18 | public function __construct( 19 | private HttpClientInterface $httpClient, 20 | ) { 21 | if (!class_exists(DomCrawler::class)) { 22 | throw new RuntimeException('The DomCrawler component is not installed. Please install it using "composer require symfony/dom-crawler".'); 23 | } 24 | } 25 | 26 | /** 27 | * @param string $url the URL of the page to crawl 28 | */ 29 | public function __invoke(string $url): string 30 | { 31 | $response = $this->httpClient->request('GET', $url); 32 | 33 | return (new DomCrawler($response->getContent()))->filter('body')->text(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Chain/Toolbox/Tool/SerpApi.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | #[AsTool(name: 'serpapi', description: 'search for information on the internet')] 14 | final readonly class SerpApi 15 | { 16 | public function __construct( 17 | private HttpClientInterface $httpClient, 18 | private string $apiKey, 19 | ) { 20 | } 21 | 22 | /** 23 | * @param string $query The search query to use 24 | */ 25 | public function __invoke(string $query): string 26 | { 27 | $response = $this->httpClient->request('GET', 'https://serpapi.com/search', [ 28 | 'query' => [ 29 | 'q' => $query, 30 | 'api_key' => $this->apiKey, 31 | ], 32 | ]); 33 | 34 | return \sprintf('Results for "%s" are "%s".', $query, $this->extractBestResponse($response->toArray())); 35 | } 36 | 37 | /** 38 | * @param array $results 39 | */ 40 | private function extractBestResponse(array $results): string 41 | { 42 | return implode('. ', array_map(fn ($story) => $story['title'], $results['organic_results'])); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Chain/Toolbox/Tool/SimilaritySearch.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | #[AsTool('similarity_search', description: 'Searches for documents similar to a query or sentence.')] 18 | final class SimilaritySearch 19 | { 20 | /** 21 | * @var VectorDocument[] 22 | */ 23 | public array $usedDocuments = []; 24 | 25 | public function __construct( 26 | private readonly PlatformInterface $platform, 27 | private readonly Model $model, 28 | private readonly VectorStoreInterface $vectorStore, 29 | ) { 30 | } 31 | 32 | /** 33 | * @param string $searchTerm string used for similarity search 34 | */ 35 | public function __invoke(string $searchTerm): string 36 | { 37 | /** @var Vector[] $vectors */ 38 | $vectors = $this->platform->request($this->model, $searchTerm)->getContent(); 39 | $this->usedDocuments = $this->vectorStore->query($vectors[0]); 40 | 41 | if (0 === \count($this->usedDocuments)) { 42 | return 'No results found'; 43 | } 44 | 45 | $result = 'Found documents with following information:'.\PHP_EOL; 46 | foreach ($this->usedDocuments as $document) { 47 | $result .= json_encode($document->metadata); 48 | } 49 | 50 | return $result; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Chain/Toolbox/Tool/Tavily.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | #[AsTool('tavily_search', description: 'search for information on the internet', method: 'search')] 16 | #[AsTool('tavily_extract', description: 'fetch content from websites', method: 'extract')] 17 | final readonly class Tavily 18 | { 19 | /** 20 | * @param array $options 21 | */ 22 | public function __construct( 23 | private HttpClientInterface $httpClient, 24 | private string $apiKey, 25 | private array $options = ['include_images' => false], 26 | ) { 27 | } 28 | 29 | /** 30 | * @param string $query The search query to use 31 | */ 32 | public function search(string $query): string 33 | { 34 | $response = $this->httpClient->request('POST', 'https://api.tavily.com/search', [ 35 | 'json' => array_merge($this->options, [ 36 | 'query' => $query, 37 | 'api_key' => $this->apiKey, 38 | ]), 39 | ]); 40 | 41 | return $response->getContent(); 42 | } 43 | 44 | /** 45 | * @param string[] $urls URLs to fetch information from 46 | */ 47 | public function extract(array $urls): string 48 | { 49 | $response = $this->httpClient->request('POST', 'https://api.tavily.com/extract', [ 50 | 'json' => [ 51 | 'urls' => $urls, 52 | 'api_key' => $this->apiKey, 53 | ], 54 | ]); 55 | 56 | return $response->getContent(); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Chain/Toolbox/ToolCallResult.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | final readonly class ToolCallResult 13 | { 14 | public function __construct( 15 | public ToolCall $toolCall, 16 | public mixed $result, 17 | ) { 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Chain/Toolbox/ToolFactory/AbstractToolFactory.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | abstract class AbstractToolFactory implements ToolFactoryInterface 18 | { 19 | public function __construct( 20 | private readonly Factory $factory = new Factory(), 21 | ) { 22 | } 23 | 24 | protected function convertAttribute(string $className, AsTool $attribute): Tool 25 | { 26 | try { 27 | return new Tool( 28 | new ExecutionReference($className, $attribute->method), 29 | $attribute->name, 30 | $attribute->description, 31 | $this->factory->buildParameters($className, $attribute->method) 32 | ); 33 | } catch (\ReflectionException $e) { 34 | throw ToolConfigurationException::invalidMethod($className, $attribute->method, $e); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Chain/Toolbox/ToolFactory/ChainFactory.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | final readonly class ChainFactory implements ToolFactoryInterface 14 | { 15 | /** 16 | * @var list 17 | */ 18 | private array $factories; 19 | 20 | /** 21 | * @param iterable $factories 22 | */ 23 | public function __construct(iterable $factories) 24 | { 25 | $this->factories = $factories instanceof \Traversable ? iterator_to_array($factories) : $factories; 26 | } 27 | 28 | public function getTool(string $reference): iterable 29 | { 30 | $invalid = 0; 31 | foreach ($this->factories as $factory) { 32 | try { 33 | yield from $factory->getTool($reference); 34 | } catch (ToolException) { 35 | ++$invalid; 36 | continue; 37 | } 38 | 39 | // If the factory does not throw an exception, we don't need to check the others 40 | return; 41 | } 42 | 43 | if ($invalid === \count($this->factories)) { 44 | throw ToolException::invalidReference($reference); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Chain/Toolbox/ToolFactory/MemoryToolFactory.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | final class MemoryToolFactory extends AbstractToolFactory 14 | { 15 | /** 16 | * @var array 17 | */ 18 | private array $tools = []; 19 | 20 | public function addTool(string|object $class, string $name, string $description, string $method = '__invoke'): self 21 | { 22 | $className = \is_object($class) ? $class::class : $class; 23 | $this->tools[$className][] = new AsTool($name, $description, $method); 24 | 25 | return $this; 26 | } 27 | 28 | /** 29 | * @param class-string $reference 30 | */ 31 | public function getTool(string $reference): iterable 32 | { 33 | if (!isset($this->tools[$reference])) { 34 | throw ToolException::invalidReference($reference); 35 | } 36 | 37 | foreach ($this->tools[$reference] as $tool) { 38 | yield $this->convertAttribute($reference, $tool); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Chain/Toolbox/ToolFactory/ReflectionToolFactory.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | final class ReflectionToolFactory extends AbstractToolFactory 16 | { 17 | /** 18 | * @param class-string $reference 19 | */ 20 | public function getTool(string $reference): iterable 21 | { 22 | if (!class_exists($reference)) { 23 | throw ToolException::invalidReference($reference); 24 | } 25 | 26 | $reflectionClass = new \ReflectionClass($reference); 27 | $attributes = $reflectionClass->getAttributes(AsTool::class); 28 | 29 | if (0 === \count($attributes)) { 30 | throw ToolException::missingAttribute($reference); 31 | } 32 | 33 | foreach ($attributes as $attribute) { 34 | yield $this->convertAttribute($reference, $attribute->newInstance()); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Chain/Toolbox/ToolFactoryInterface.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | interface ToolFactoryInterface 14 | { 15 | /** 16 | * @return iterable 17 | * 18 | * @throws ToolException if the metadata for the given reference is not found 19 | */ 20 | public function getTool(string $reference): iterable; 21 | } 22 | -------------------------------------------------------------------------------- /src/Chain/Toolbox/ToolResultConverter.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | final readonly class ToolResultConverter 11 | { 12 | /** 13 | * @param \JsonSerializable|\Stringable|array|float|string|null $result 14 | */ 15 | public function convert(\JsonSerializable|\Stringable|array|float|string|\DateTimeInterface|null $result): ?string 16 | { 17 | if (null === $result) { 18 | return null; 19 | } 20 | 21 | if ($result instanceof \JsonSerializable || \is_array($result)) { 22 | return json_encode($result, flags: \JSON_THROW_ON_ERROR); 23 | } 24 | 25 | if (\is_float($result) || $result instanceof \Stringable) { 26 | return (string) $result; 27 | } 28 | 29 | if ($result instanceof \DateTimeInterface) { 30 | return $result->format(\DATE_ATOM); 31 | } 32 | 33 | return $result; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Chain/Toolbox/ToolboxInterface.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | interface ToolboxInterface 16 | { 17 | /** 18 | * @return Tool[] 19 | */ 20 | public function getTools(): array; 21 | 22 | /** 23 | * @throws ToolExecutionException if the tool execution fails 24 | * @throws ToolNotFoundException if the tool is not found 25 | */ 26 | public function execute(ToolCall $toolCall): mixed; 27 | } 28 | -------------------------------------------------------------------------------- /src/Platform/Bridge/Anthropic/Claude.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class Claude extends Model 14 | { 15 | public const HAIKU_3 = 'claude-3-haiku-20240307'; 16 | public const HAIKU_35 = 'claude-3-5-haiku-20241022'; 17 | public const SONNET_3 = 'claude-3-sonnet-20240229'; 18 | public const SONNET_35 = 'claude-3-5-sonnet-20240620'; 19 | public const SONNET_35_V2 = 'claude-3-5-sonnet-20241022'; 20 | public const SONNET_37 = 'claude-3-7-sonnet-20250219'; 21 | public const OPUS_3 = 'claude-3-opus-20240229'; 22 | 23 | /** 24 | * @param array $options The default options for the model usage 25 | */ 26 | public function __construct( 27 | string $name = self::SONNET_37, 28 | array $options = ['temperature' => 1.0, 'max_tokens' => 1000], 29 | ) { 30 | $capabilities = [ 31 | Capability::INPUT_MESSAGES, 32 | Capability::INPUT_IMAGE, 33 | Capability::OUTPUT_TEXT, 34 | Capability::OUTPUT_STREAMING, 35 | Capability::TOOL_CALLING, 36 | ]; 37 | 38 | parent::__construct($name, $capabilities, $options); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Platform/Bridge/Anthropic/Contract/AssistantMessageNormalizer.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | final class AssistantMessageNormalizer extends ModelContractNormalizer implements NormalizerAwareInterface 19 | { 20 | use NormalizerAwareTrait; 21 | 22 | protected function supportedDataClass(): string 23 | { 24 | return AssistantMessage::class; 25 | } 26 | 27 | protected function supportsModel(Model $model): bool 28 | { 29 | return $model instanceof Claude; 30 | } 31 | 32 | /** 33 | * @param AssistantMessage $data 34 | * 35 | * @return array{ 36 | * role: 'assistant', 37 | * content: list 42 | * }> 43 | * } 44 | */ 45 | public function normalize(mixed $data, ?string $format = null, array $context = []): array 46 | { 47 | return [ 48 | 'role' => 'assistant', 49 | 'content' => array_map(static function (ToolCall $toolCall) { 50 | return [ 51 | 'type' => 'tool_use', 52 | 'id' => $toolCall->id, 53 | 'name' => $toolCall->name, 54 | 'input' => empty($toolCall->arguments) ? new \stdClass() : $toolCall->arguments, 55 | ]; 56 | }, $data->toolCalls), 57 | ]; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Platform/Bridge/Anthropic/Contract/DocumentNormalizer.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class DocumentNormalizer extends ModelContractNormalizer 14 | { 15 | protected function supportedDataClass(): string 16 | { 17 | return Document::class; 18 | } 19 | 20 | protected function supportsModel(Model $model): bool 21 | { 22 | return $model instanceof Claude; 23 | } 24 | 25 | /** 26 | * @param Document $data 27 | * 28 | * @return array{type: 'document', source: array{type: 'base64', media_type: string, data: string}} 29 | */ 30 | public function normalize(mixed $data, ?string $format = null, array $context = []): array 31 | { 32 | return [ 33 | 'type' => 'document', 34 | 'source' => [ 35 | 'type' => 'base64', 36 | 'media_type' => $data->getFormat(), 37 | 'data' => $data->asBase64(), 38 | ], 39 | ]; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Platform/Bridge/Anthropic/Contract/DocumentUrlNormalizer.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | final class DocumentUrlNormalizer extends ModelContractNormalizer 16 | { 17 | protected function supportedDataClass(): string 18 | { 19 | return DocumentUrl::class; 20 | } 21 | 22 | protected function supportsModel(Model $model): bool 23 | { 24 | return $model instanceof Claude; 25 | } 26 | 27 | /** 28 | * @param DocumentUrl $data 29 | * 30 | * @return array{type: 'document', source: array{type: 'url', url: string}} 31 | */ 32 | public function normalize(mixed $data, ?string $format = null, array $context = []): array 33 | { 34 | return [ 35 | 'type' => 'document', 36 | 'source' => [ 37 | 'type' => 'url', 38 | 'url' => $data->url, 39 | ], 40 | ]; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Platform/Bridge/Anthropic/Contract/ImageNormalizer.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | final class ImageNormalizer extends ModelContractNormalizer 18 | { 19 | protected function supportedDataClass(): string 20 | { 21 | return Image::class; 22 | } 23 | 24 | protected function supportsModel(Model $model): bool 25 | { 26 | return $model instanceof Claude; 27 | } 28 | 29 | /** 30 | * @param Image $data 31 | * 32 | * @return array{type: 'image', source: array{type: 'base64', media_type: string, data: string}} 33 | */ 34 | public function normalize(mixed $data, ?string $format = null, array $context = []): array 35 | { 36 | return [ 37 | 'type' => 'image', 38 | 'source' => [ 39 | 'type' => 'base64', 40 | 'media_type' => u($data->getFormat())->replace('jpg', 'jpeg')->toString(), 41 | 'data' => $data->asBase64(), 42 | ], 43 | ]; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Platform/Bridge/Anthropic/Contract/ImageUrlNormalizer.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | final class ImageUrlNormalizer extends ModelContractNormalizer 17 | { 18 | protected function supportedDataClass(): string 19 | { 20 | return ImageUrl::class; 21 | } 22 | 23 | protected function supportsModel(Model $model): bool 24 | { 25 | return $model instanceof Claude; 26 | } 27 | 28 | /** 29 | * @param ImageUrl $data 30 | * 31 | * @return array{type: 'image', source: array{type: 'url', url: string}} 32 | */ 33 | public function normalize(mixed $data, ?string $format = null, array $context = []): array 34 | { 35 | return [ 36 | 'type' => 'image', 37 | 'source' => [ 38 | 'type' => 'url', 39 | 'url' => $data->url, 40 | ], 41 | ]; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Platform/Bridge/Anthropic/Contract/MessageBagNormalizer.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | final class MessageBagNormalizer extends ModelContractNormalizer implements NormalizerAwareInterface 19 | { 20 | use NormalizerAwareTrait; 21 | 22 | protected function supportedDataClass(): string 23 | { 24 | return MessageBagInterface::class; 25 | } 26 | 27 | protected function supportsModel(Model $model): bool 28 | { 29 | return $model instanceof Claude; 30 | } 31 | 32 | /** 33 | * @param MessageBagInterface $data 34 | * 35 | * @return array{ 36 | * messages: array, 37 | * model?: string, 38 | * system?: string, 39 | * } 40 | */ 41 | public function normalize(mixed $data, ?string $format = null, array $context = []): array 42 | { 43 | $array = [ 44 | 'messages' => $this->normalizer->normalize($data->withoutSystemMessage()->getMessages(), $format, $context), 45 | ]; 46 | 47 | if (null !== $system = $data->getSystemMessage()) { 48 | $array['system'] = $system->content; 49 | } 50 | 51 | if (isset($context[Contract::CONTEXT_MODEL]) && $context[Contract::CONTEXT_MODEL] instanceof Model) { 52 | $array['model'] = $context[Contract::CONTEXT_MODEL]->getName(); 53 | } 54 | 55 | return $array; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Platform/Bridge/Anthropic/Contract/ToolCallMessageNormalizer.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | final class ToolCallMessageNormalizer extends ModelContractNormalizer implements NormalizerAwareInterface 18 | { 19 | use NormalizerAwareTrait; 20 | 21 | protected function supportedDataClass(): string 22 | { 23 | return ToolCallMessage::class; 24 | } 25 | 26 | protected function supportsModel(Model $model): bool 27 | { 28 | return $model instanceof Claude; 29 | } 30 | 31 | /** 32 | * @param ToolCallMessage $data 33 | * 34 | * @return array{ 35 | * role: 'user', 36 | * content: list 41 | * } 42 | */ 43 | public function normalize(mixed $data, ?string $format = null, array $context = []): array 44 | { 45 | return [ 46 | 'role' => 'user', 47 | 'content' => [ 48 | [ 49 | 'type' => 'tool_result', 50 | 'tool_use_id' => $data->toolCall->id, 51 | 'content' => $data->content, 52 | ], 53 | ], 54 | ]; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Platform/Bridge/Anthropic/Contract/ToolNormalizer.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | class ToolNormalizer extends ModelContractNormalizer 17 | { 18 | protected function supportedDataClass(): string 19 | { 20 | return Tool::class; 21 | } 22 | 23 | protected function supportsModel(Model $model): bool 24 | { 25 | return $model instanceof Claude; 26 | } 27 | 28 | /** 29 | * @param Tool $data 30 | * 31 | * @return array{ 32 | * name: string, 33 | * description: string, 34 | * input_schema: JsonSchema|array{type: 'object'} 35 | * } 36 | */ 37 | public function normalize(mixed $data, ?string $format = null, array $context = []): array 38 | { 39 | return [ 40 | 'name' => $data->name, 41 | 'description' => $data->description, 42 | 'input_schema' => $data->parameters ?? ['type' => 'object'], 43 | ]; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Platform/Bridge/Anthropic/ModelClient.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | final readonly class ModelClient implements ModelClientInterface 17 | { 18 | private EventSourceHttpClient $httpClient; 19 | 20 | public function __construct( 21 | HttpClientInterface $httpClient, 22 | #[\SensitiveParameter] private string $apiKey, 23 | private string $version = '2023-06-01', 24 | ) { 25 | $this->httpClient = $httpClient instanceof EventSourceHttpClient ? $httpClient : new EventSourceHttpClient($httpClient); 26 | } 27 | 28 | public function supports(Model $model): bool 29 | { 30 | return $model instanceof Claude; 31 | } 32 | 33 | public function request(Model $model, array|string $payload, array $options = []): ResponseInterface 34 | { 35 | if (isset($options['tools'])) { 36 | $options['tool_choice'] = ['type' => 'auto']; 37 | } 38 | 39 | return $this->httpClient->request('POST', 'https://api.anthropic.com/v1/messages', [ 40 | 'headers' => [ 41 | 'x-api-key' => $this->apiKey, 42 | 'anthropic-version' => $this->version, 43 | ], 44 | 'json' => array_merge($options, $payload), 45 | ]); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Platform/Bridge/Anthropic/PlatformFactory.php: -------------------------------------------------------------------------------- 1 | 22 | */ 23 | final readonly class PlatformFactory 24 | { 25 | public static function create( 26 | #[\SensitiveParameter] 27 | string $apiKey, 28 | string $version = '2023-06-01', 29 | ?HttpClientInterface $httpClient = null, 30 | ): Platform { 31 | $httpClient = $httpClient instanceof EventSourceHttpClient ? $httpClient : new EventSourceHttpClient($httpClient); 32 | 33 | return new Platform( 34 | [new ModelClient($httpClient, $apiKey, $version)], 35 | [new ResponseConverter()], 36 | Contract::create( 37 | new AssistantMessageNormalizer(), 38 | new DocumentNormalizer(), 39 | new DocumentUrlNormalizer(), 40 | new ImageNormalizer(), 41 | new ImageUrlNormalizer(), 42 | new MessageBagNormalizer(), 43 | new ToolCallMessageNormalizer(), 44 | new ToolNormalizer(), 45 | ) 46 | ); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Platform/Bridge/Azure/Meta/LlamaHandler.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | final readonly class LlamaHandler implements ModelClientInterface, ResponseConverterInterface 21 | { 22 | public function __construct( 23 | private HttpClientInterface $httpClient, 24 | private string $baseUrl, 25 | #[\SensitiveParameter] private string $apiKey, 26 | ) { 27 | } 28 | 29 | public function supports(Model $model): bool 30 | { 31 | return $model instanceof Llama; 32 | } 33 | 34 | public function request(Model $model, array|string $payload, array $options = []): ResponseInterface 35 | { 36 | $url = \sprintf('https://%s/chat/completions', $this->baseUrl); 37 | 38 | return $this->httpClient->request('POST', $url, [ 39 | 'headers' => [ 40 | 'Content-Type' => 'application/json', 41 | 'Authorization' => $this->apiKey, 42 | ], 43 | 'json' => array_merge($options, $payload), 44 | ]); 45 | } 46 | 47 | public function convert(ResponseInterface $response, array $options = []): LlmResponse 48 | { 49 | $data = $response->toArray(); 50 | 51 | if (!isset($data['choices'][0]['message']['content'])) { 52 | throw new RuntimeException('Response does not contain output'); 53 | } 54 | 55 | return new TextResponse($data['choices'][0]['message']['content']); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Platform/Bridge/Azure/Meta/PlatformFactory.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | final readonly class PlatformFactory 15 | { 16 | public static function create( 17 | string $baseUrl, 18 | #[\SensitiveParameter] 19 | string $apiKey, 20 | ?HttpClientInterface $httpClient = null, 21 | ): Platform { 22 | $modelClient = new LlamaHandler($httpClient ?? HttpClient::create(), $baseUrl, $apiKey); 23 | 24 | return new Platform([$modelClient], [$modelClient]); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Platform/Bridge/Azure/OpenAI/GPTModelClient.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | final readonly class GPTModelClient implements ModelClientInterface 19 | { 20 | private EventSourceHttpClient $httpClient; 21 | 22 | public function __construct( 23 | HttpClientInterface $httpClient, 24 | private string $baseUrl, 25 | private string $deployment, 26 | private string $apiVersion, 27 | #[\SensitiveParameter] private string $apiKey, 28 | ) { 29 | $this->httpClient = $httpClient instanceof EventSourceHttpClient ? $httpClient : new EventSourceHttpClient($httpClient); 30 | Assert::notStartsWith($baseUrl, 'http://', 'The base URL must not contain the protocol.'); 31 | Assert::notStartsWith($baseUrl, 'https://', 'The base URL must not contain the protocol.'); 32 | Assert::stringNotEmpty($deployment, 'The deployment must not be empty.'); 33 | Assert::stringNotEmpty($apiVersion, 'The API version must not be empty.'); 34 | Assert::stringNotEmpty($apiKey, 'The API key must not be empty.'); 35 | } 36 | 37 | public function supports(Model $model): bool 38 | { 39 | return $model instanceof GPT; 40 | } 41 | 42 | public function request(Model $model, object|array|string $payload, array $options = []): ResponseInterface 43 | { 44 | $url = \sprintf('https://%s/openai/deployments/%s/chat/completions', $this->baseUrl, $this->deployment); 45 | 46 | return $this->httpClient->request('POST', $url, [ 47 | 'headers' => [ 48 | 'api-key' => $this->apiKey, 49 | ], 50 | 'query' => ['api-version' => $this->apiVersion], 51 | 'json' => array_merge($options, $payload), 52 | ]); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Platform/Bridge/Azure/OpenAI/PlatformFactory.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | final readonly class PlatformFactory 19 | { 20 | public static function create( 21 | string $baseUrl, 22 | string $deployment, 23 | string $apiVersion, 24 | #[\SensitiveParameter] 25 | string $apiKey, 26 | ?HttpClientInterface $httpClient = null, 27 | ): Platform { 28 | $httpClient = $httpClient instanceof EventSourceHttpClient ? $httpClient : new EventSourceHttpClient($httpClient); 29 | $embeddingsResponseFactory = new EmbeddingsModelClient($httpClient, $baseUrl, $deployment, $apiVersion, $apiKey); 30 | $GPTResponseFactory = new GPTModelClient($httpClient, $baseUrl, $deployment, $apiVersion, $apiKey); 31 | $whisperResponseFactory = new WhisperModelClient($httpClient, $baseUrl, $deployment, $apiVersion, $apiKey); 32 | 33 | return new Platform( 34 | [$GPTResponseFactory, $embeddingsResponseFactory, $whisperResponseFactory], 35 | [new ResponseConverter(), new Embeddings\ResponseConverter(), new \PhpLlm\LlmChain\Platform\Bridge\OpenAI\Whisper\ResponseConverter()], 36 | Contract::create(new AudioNormalizer()), 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Platform/Bridge/Azure/OpenAI/WhisperModelClient.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | final readonly class WhisperModelClient implements ModelClientInterface 19 | { 20 | private EventSourceHttpClient $httpClient; 21 | 22 | public function __construct( 23 | HttpClientInterface $httpClient, 24 | private string $baseUrl, 25 | private string $deployment, 26 | private string $apiVersion, 27 | #[\SensitiveParameter] private string $apiKey, 28 | ) { 29 | $this->httpClient = $httpClient instanceof EventSourceHttpClient ? $httpClient : new EventSourceHttpClient($httpClient); 30 | Assert::notStartsWith($baseUrl, 'http://', 'The base URL must not contain the protocol.'); 31 | Assert::notStartsWith($baseUrl, 'https://', 'The base URL must not contain the protocol.'); 32 | Assert::stringNotEmpty($deployment, 'The deployment must not be empty.'); 33 | Assert::stringNotEmpty($apiVersion, 'The API version must not be empty.'); 34 | Assert::stringNotEmpty($apiKey, 'The API key must not be empty.'); 35 | } 36 | 37 | public function supports(Model $model): bool 38 | { 39 | return $model instanceof Whisper; 40 | } 41 | 42 | public function request(Model $model, array|string $payload, array $options = []): ResponseInterface 43 | { 44 | $url = \sprintf('https://%s/openai/deployments/%s/audio/translations', $this->baseUrl, $this->deployment); 45 | 46 | return $this->httpClient->request('POST', $url, [ 47 | 'headers' => [ 48 | 'api-key' => $this->apiKey, 49 | 'Content-Type' => 'multipart/form-data', 50 | ], 51 | 'query' => ['api-version' => $this->apiVersion], 52 | 'body' => array_merge($options, $payload), 53 | ]); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Platform/Bridge/Bedrock/BedrockModelClient.php: -------------------------------------------------------------------------------- 1 | |string $payload 19 | * @param array $options 20 | */ 21 | public function request(Model $model, array|string $payload, array $options = []): LlmResponse; 22 | } 23 | -------------------------------------------------------------------------------- /src/Platform/Bridge/Bedrock/Meta/LlamaModelClient.php: -------------------------------------------------------------------------------- 1 | bedrockRuntimeClient->invokeModel(new InvokeModelRequest([ 32 | 'modelId' => $this->getModelId($model), 33 | 'contentType' => 'application/json', 34 | 'body' => json_encode($payload, \JSON_THROW_ON_ERROR), 35 | ])); 36 | 37 | return $this->convert($response); 38 | } 39 | 40 | public function convert(InvokeModelResponse $bedrockResponse): LlmResponse 41 | { 42 | $responseBody = json_decode($bedrockResponse->getBody(), true, 512, \JSON_THROW_ON_ERROR); 43 | 44 | if (!isset($responseBody['generation'])) { 45 | throw new \RuntimeException('Response does not contain any content'); 46 | } 47 | 48 | return new TextResponse($responseBody['generation']); 49 | } 50 | 51 | private function getModelId(Model $model): string 52 | { 53 | $configuredRegion = $this->bedrockRuntimeClient->getConfiguration()->get('region'); 54 | $regionPrefix = substr((string) $configuredRegion, 0, 2); 55 | $modifiedModelName = str_replace('llama-3', 'llama3', $model->getName()); 56 | 57 | return $regionPrefix.'.meta.'.str_replace('.', '-', $modifiedModelName).'-v1:0'; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Platform/Bridge/Bedrock/Nova/Contract/AssistantMessageNormalizer.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | final class AssistantMessageNormalizer extends ModelContractNormalizer 17 | { 18 | protected function supportedDataClass(): string 19 | { 20 | return AssistantMessage::class; 21 | } 22 | 23 | protected function supportsModel(Model $model): bool 24 | { 25 | return $model instanceof Nova; 26 | } 27 | 28 | /** 29 | * @param AssistantMessage $data 30 | * 31 | * @return array{ 32 | * role: 'assistant', 33 | * content: array 41 | * } 42 | */ 43 | public function normalize(mixed $data, ?string $format = null, array $context = []): array 44 | { 45 | if ($data->hasToolCalls()) { 46 | return [ 47 | 'role' => 'assistant', 48 | 'content' => array_map(static function (ToolCall $toolCall) { 49 | return [ 50 | 'toolUse' => [ 51 | 'toolUseId' => $toolCall->id, 52 | 'name' => $toolCall->name, 53 | 'input' => empty($toolCall->arguments) ? new \stdClass() : $toolCall->arguments, 54 | ], 55 | ]; 56 | }, $data->toolCalls), 57 | ]; 58 | } 59 | 60 | return [ 61 | 'role' => 'assistant', 62 | 'content' => [['text' => $data->content]], 63 | ]; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Platform/Bridge/Bedrock/Nova/Contract/MessageBagNormalizer.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | final class MessageBagNormalizer extends ModelContractNormalizer implements NormalizerAwareInterface 18 | { 19 | use NormalizerAwareTrait; 20 | 21 | protected function supportedDataClass(): string 22 | { 23 | return MessageBagInterface::class; 24 | } 25 | 26 | protected function supportsModel(Model $model): bool 27 | { 28 | return $model instanceof Nova; 29 | } 30 | 31 | /** 32 | * @param MessageBagInterface $data 33 | * 34 | * @return array{ 35 | * messages: array>, 36 | * system?: array, 37 | * } 38 | */ 39 | public function normalize(mixed $data, ?string $format = null, array $context = []): array 40 | { 41 | $array = []; 42 | 43 | if ($data->getSystemMessage()) { 44 | $array['system'][]['text'] = $data->getSystemMessage()->content; 45 | } 46 | 47 | $array['messages'] = $this->normalizer->normalize($data->withoutSystemMessage()->getMessages(), $format, $context); 48 | 49 | return $array; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Platform/Bridge/Bedrock/Nova/Contract/ToolCallMessageNormalizer.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | final class ToolCallMessageNormalizer extends ModelContractNormalizer implements NormalizerAwareInterface 18 | { 19 | use NormalizerAwareTrait; 20 | 21 | protected function supportedDataClass(): string 22 | { 23 | return ToolCallMessage::class; 24 | } 25 | 26 | protected function supportsModel(Model $model): bool 27 | { 28 | return $model instanceof Nova; 29 | } 30 | 31 | /** 32 | * @param ToolCallMessage $data 33 | * 34 | * @return array{ 35 | * role: 'user', 36 | * content: array, 40 | * } 41 | * }> 42 | * } 43 | */ 44 | public function normalize(mixed $data, ?string $format = null, array $context = []): array 45 | { 46 | return [ 47 | 'role' => 'user', 48 | 'content' => [ 49 | [ 50 | 'toolResult' => [ 51 | 'toolUseId' => $data->toolCall->id, 52 | 'content' => [['json' => $data->content]], 53 | ], 54 | ], 55 | ], 56 | ]; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Platform/Bridge/Bedrock/Nova/Contract/ToolNormalizer.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | class ToolNormalizer extends ModelContractNormalizer 17 | { 18 | protected function supportedDataClass(): string 19 | { 20 | return Tool::class; 21 | } 22 | 23 | protected function supportsModel(Model $model): bool 24 | { 25 | return $model instanceof Nova; 26 | } 27 | 28 | /** 29 | * @param Tool $data 30 | * 31 | * @return array{ 32 | * toolSpec: array{ 33 | * name: string, 34 | * description: string, 35 | * inputSchema: array{ 36 | * json: JsonSchema|array{type: 'object'} 37 | * } 38 | * } 39 | * } 40 | */ 41 | public function normalize(mixed $data, ?string $format = null, array $context = []): array 42 | { 43 | return [ 44 | 'toolSpec' => [ 45 | 'name' => $data->name, 46 | 'description' => $data->description, 47 | 'inputSchema' => [ 48 | 'json' => $data->parameters ?? new \stdClass(), 49 | ], 50 | ], 51 | ]; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Platform/Bridge/Bedrock/Nova/Contract/UserMessageNormalizer.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | final class UserMessageNormalizer extends ModelContractNormalizer 21 | { 22 | protected function supportedDataClass(): string 23 | { 24 | return UserMessage::class; 25 | } 26 | 27 | protected function supportsModel(Model $model): bool 28 | { 29 | return $model instanceof Nova; 30 | } 31 | 32 | /** 33 | * @param UserMessage $data 34 | * 35 | * @return array{ 36 | * role: 'user', 37 | * content: array 44 | * } 45 | */ 46 | public function normalize(mixed $data, ?string $format = null, array $context = []): array 47 | { 48 | $array = ['role' => $data->getRole()->value]; 49 | 50 | foreach ($data->content as $value) { 51 | $contentPart = []; 52 | if ($value instanceof Text) { 53 | $contentPart['text'] = $value->text; 54 | } elseif ($value instanceof Image) { 55 | $contentPart['image']['format'] = u($value->getFormat())->replace('image/', '')->replace('jpg', 'jpeg')->toString(); 56 | $contentPart['image']['source']['bytes'] = $value->asBase64(); 57 | } else { 58 | throw new RuntimeException('Unsupported message type.'); 59 | } 60 | $array['content'][] = $contentPart; 61 | } 62 | 63 | return $array; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Platform/Bridge/Bedrock/Nova/Nova.php: -------------------------------------------------------------------------------- 1 | $options The default options for the model usage 22 | */ 23 | public function __construct( 24 | string $name = self::PRO, 25 | array $options = ['temperature' => 1.0, 'max_tokens' => 1000], 26 | ) { 27 | $capabilities = [ 28 | Capability::INPUT_MESSAGES, 29 | Capability::OUTPUT_TEXT, 30 | Capability::TOOL_CALLING, 31 | ]; 32 | 33 | if (self::MICRO !== $name) { 34 | $capabilities[] = Capability::INPUT_IMAGE; 35 | } 36 | 37 | parent::__construct($name, $capabilities, $options); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Platform/Bridge/Bedrock/PlatformFactory.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | final class AssistantMessageNormalizer extends ModelContractNormalizer implements NormalizerAwareInterface 18 | { 19 | use NormalizerAwareTrait; 20 | 21 | protected function supportedDataClass(): string 22 | { 23 | return AssistantMessage::class; 24 | } 25 | 26 | protected function supportsModel(Model $model): bool 27 | { 28 | return $model instanceof Gemini; 29 | } 30 | 31 | /** 32 | * @param AssistantMessage $data 33 | * 34 | * @return array{array{text: string}} 35 | */ 36 | public function normalize(mixed $data, ?string $format = null, array $context = []): array 37 | { 38 | return [ 39 | ['text' => $data->content], 40 | ]; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Platform/Bridge/Google/Contract/MessageBagNormalizer.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | final class MessageBagNormalizer extends ModelContractNormalizer implements NormalizerAwareInterface 19 | { 20 | use NormalizerAwareTrait; 21 | 22 | protected function supportedDataClass(): string 23 | { 24 | return MessageBagInterface::class; 25 | } 26 | 27 | protected function supportsModel(Model $model): bool 28 | { 29 | return $model instanceof Gemini; 30 | } 31 | 32 | /** 33 | * @param MessageBagInterface $data 34 | * 35 | * @return array{ 36 | * contents: list 39 | * }>, 40 | * system_instruction?: array{parts: array{text: string}} 41 | * } 42 | */ 43 | public function normalize(mixed $data, ?string $format = null, array $context = []): array 44 | { 45 | $array = ['contents' => []]; 46 | 47 | if (null !== $systemMessage = $data->getSystemMessage()) { 48 | $array['system_instruction'] = [ 49 | 'parts' => ['text' => $systemMessage->content], 50 | ]; 51 | } 52 | 53 | foreach ($data->withoutSystemMessage()->getMessages() as $message) { 54 | $array['contents'][] = [ 55 | 'role' => $message->getRole()->equals(Role::Assistant) ? 'model' : 'user', 56 | 'parts' => $this->normalizer->normalize($message, $format, $context), 57 | ]; 58 | } 59 | 60 | return $array; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Platform/Bridge/Google/Contract/UserMessageNormalizer.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | final class UserMessageNormalizer extends ModelContractNormalizer 18 | { 19 | protected function supportedDataClass(): string 20 | { 21 | return UserMessage::class; 22 | } 23 | 24 | protected function supportsModel(Model $model): bool 25 | { 26 | return $model instanceof Gemini; 27 | } 28 | 29 | /** 30 | * @param UserMessage $data 31 | * 32 | * @return list 33 | */ 34 | public function normalize(mixed $data, ?string $format = null, array $context = []): array 35 | { 36 | $parts = []; 37 | foreach ($data->content as $content) { 38 | if ($content instanceof Text) { 39 | $parts[] = ['text' => $content->text]; 40 | } 41 | if ($content instanceof Image) { 42 | $parts[] = ['inline_data' => [ 43 | 'mime_type' => $content->getFormat(), 44 | 'data' => $content->asBase64(), 45 | ]]; 46 | } 47 | } 48 | 49 | return $parts; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Platform/Bridge/Google/Gemini.php: -------------------------------------------------------------------------------- 1 | $options The default options for the model usage 23 | */ 24 | public function __construct(string $name = self::GEMINI_2_PRO, array $options = ['temperature' => 1.0]) 25 | { 26 | $capabilities = [ 27 | Capability::INPUT_MESSAGES, 28 | Capability::INPUT_IMAGE, 29 | Capability::OUTPUT_STREAMING, 30 | ]; 31 | 32 | parent::__construct($name, $capabilities, $options); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Platform/Bridge/Google/PlatformFactory.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | final class ApiClient 15 | { 16 | public function __construct( 17 | private ?HttpClientInterface $httpClient = null, 18 | ) { 19 | $this->httpClient = $httpClient ?? HttpClient::create(); 20 | } 21 | 22 | /** 23 | * @return Model[] 24 | */ 25 | public function models(?string $provider, ?string $task): array 26 | { 27 | $response = $this->httpClient->request('GET', 'https://huggingface.co/api/models', [ 28 | 'query' => [ 29 | 'inference_provider' => $provider, 30 | 'pipeline_tag' => $task, 31 | ], 32 | ]); 33 | 34 | return array_map(fn (array $model) => new Model($model['id']), $response->toArray()); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Platform/Bridge/HuggingFace/Contract/FileNormalizer.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class FileNormalizer extends ModelContractNormalizer 13 | { 14 | protected function supportedDataClass(): string 15 | { 16 | return File::class; 17 | } 18 | 19 | protected function supportsModel(Model $model): bool 20 | { 21 | return true; 22 | } 23 | 24 | /** 25 | * @param File $data 26 | * 27 | * @return array{ 28 | * headers: array<'Content-Type', string>, 29 | * body: string 30 | * } 31 | */ 32 | public function normalize(mixed $data, ?string $format = null, array $context = []): array 33 | { 34 | return [ 35 | 'headers' => ['Content-Type' => $data->getFormat()], 36 | 'body' => $data->asBinary(), 37 | ]; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Platform/Bridge/HuggingFace/Contract/MessageBagNormalizer.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | class MessageBagNormalizer extends ModelContractNormalizer implements NormalizerAwareInterface 15 | { 16 | use NormalizerAwareTrait; 17 | 18 | protected function supportedDataClass(): string 19 | { 20 | return MessageBagInterface::class; 21 | } 22 | 23 | protected function supportsModel(Model $model): bool 24 | { 25 | return true; 26 | } 27 | 28 | /** 29 | * @param MessageBagInterface $data 30 | * 31 | * @return array{ 32 | * headers: array<'Content-Type', 'application/json'>, 33 | * json: array{messages: array} 34 | * } 35 | */ 36 | public function normalize(mixed $data, ?string $format = null, array $context = []): array 37 | { 38 | return [ 39 | 'headers' => ['Content-Type' => 'application/json'], 40 | 'json' => [ 41 | 'messages' => $this->normalizer->normalize($data->getMessages(), $format, $context), 42 | ], 43 | ]; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Platform/Bridge/HuggingFace/Output/Classification.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | final readonly class Classification 11 | { 12 | public function __construct( 13 | public string $label, 14 | public float $score, 15 | ) { 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Platform/Bridge/HuggingFace/Output/ClassificationResult.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | final class ClassificationResult 11 | { 12 | /** 13 | * @param Classification[] $classifications 14 | */ 15 | public function __construct( 16 | public array $classifications, 17 | ) { 18 | } 19 | 20 | /** 21 | * @param array $data 22 | */ 23 | public static function fromArray(array $data): self 24 | { 25 | return new self( 26 | array_map(fn (array $item) => new Classification($item['label'], $item['score']), $data) 27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Platform/Bridge/HuggingFace/Output/DetectedObject.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | final readonly class DetectedObject 11 | { 12 | public function __construct( 13 | public string $label, 14 | public float $score, 15 | public float $xmin, 16 | public float $ymin, 17 | public float $xmax, 18 | public float $ymax, 19 | ) { 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Platform/Bridge/HuggingFace/Output/FillMaskResult.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | final class FillMaskResult 11 | { 12 | /** 13 | * @param MaskFill[] $fills 14 | */ 15 | public function __construct( 16 | public array $fills, 17 | ) { 18 | } 19 | 20 | /** 21 | * @param array $data 22 | */ 23 | public static function fromArray(array $data): self 24 | { 25 | return new self(array_map( 26 | fn (array $item) => new MaskFill( 27 | $item['token'], 28 | $item['token_str'], 29 | $item['sequence'], 30 | $item['score'], 31 | ), 32 | $data, 33 | )); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Platform/Bridge/HuggingFace/Output/ImageSegment.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | final readonly class ImageSegment 11 | { 12 | public function __construct( 13 | public string $label, 14 | public ?float $score, 15 | public string $mask, 16 | ) { 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Platform/Bridge/HuggingFace/Output/ImageSegmentationResult.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | final class ImageSegmentationResult 11 | { 12 | /** 13 | * @param ImageSegment[] $segments 14 | */ 15 | public function __construct( 16 | public array $segments, 17 | ) { 18 | } 19 | 20 | /** 21 | * @param array $data 22 | */ 23 | public static function fromArray(array $data): self 24 | { 25 | return new self( 26 | array_map(fn (array $item) => new ImageSegment($item['label'], $item['score'], $item['mask']), $data) 27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Platform/Bridge/HuggingFace/Output/MaskFill.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | final readonly class MaskFill 11 | { 12 | public function __construct( 13 | public int $token, 14 | public string $tokenStr, 15 | public string $sequence, 16 | public float $score, 17 | ) { 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Platform/Bridge/HuggingFace/Output/ObjectDetectionResult.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | final class ObjectDetectionResult 11 | { 12 | /** 13 | * @param DetectedObject[] $objects 14 | */ 15 | public function __construct( 16 | public array $objects, 17 | ) { 18 | } 19 | 20 | /** 21 | * @param array $data 22 | */ 23 | public static function fromArray(array $data): self 24 | { 25 | return new self(array_map( 26 | fn (array $item) => new DetectedObject( 27 | $item['label'], 28 | $item['score'], 29 | $item['box']['xmin'], 30 | $item['box']['ymin'], 31 | $item['box']['xmax'], 32 | $item['box']['ymax'], 33 | ), 34 | $data, 35 | )); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Platform/Bridge/HuggingFace/Output/QuestionAnsweringResult.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | final readonly class QuestionAnsweringResult 11 | { 12 | public function __construct( 13 | public string $answer, 14 | public int $startIndex, 15 | public int $endIndex, 16 | public float $score, 17 | ) { 18 | } 19 | 20 | /** 21 | * @param array{answer: string, start: int, end: int, score: float} $data 22 | */ 23 | public static function fromArray(array $data): self 24 | { 25 | return new self( 26 | $data['answer'], 27 | $data['start'], 28 | $data['end'], 29 | $data['score'], 30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Platform/Bridge/HuggingFace/Output/SentenceSimilarityResult.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | final readonly class SentenceSimilarityResult 11 | { 12 | /** 13 | * @param array $similarities 14 | */ 15 | public function __construct( 16 | public array $similarities, 17 | ) { 18 | } 19 | 20 | /** 21 | * @param array $data 22 | */ 23 | public static function fromArray(array $data): self 24 | { 25 | return new self($data); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Platform/Bridge/HuggingFace/Output/TableQuestionAnsweringResult.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | final readonly class TableQuestionAnsweringResult 11 | { 12 | /** 13 | * @param array $cells 14 | * @param array $aggregator 15 | */ 16 | public function __construct( 17 | public string $answer, 18 | public array $cells = [], 19 | public array $aggregator = [], 20 | ) { 21 | } 22 | 23 | /** 24 | * @param array{answer: string, cells?: array, aggregator?: array} $data 25 | */ 26 | public static function fromArray(array $data): self 27 | { 28 | return new self( 29 | $data['answer'], 30 | $data['cells'] ?? [], 31 | $data['aggregator'] ?? [], 32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Platform/Bridge/HuggingFace/Output/Token.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | final readonly class Token 11 | { 12 | public function __construct( 13 | public string $entityGroup, 14 | public float $score, 15 | public string $word, 16 | public int $start, 17 | public int $end, 18 | ) { 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Platform/Bridge/HuggingFace/Output/TokenClassificationResult.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | final class TokenClassificationResult 11 | { 12 | /** 13 | * @param Token[] $tokens 14 | */ 15 | public function __construct( 16 | public array $tokens, 17 | ) { 18 | } 19 | 20 | /** 21 | * @param array $data 22 | */ 23 | public static function fromArray(array $data): self 24 | { 25 | return new self(array_map( 26 | fn (array $item) => new Token( 27 | $item['entity_group'], 28 | $item['score'], 29 | $item['word'], 30 | $item['start'], 31 | $item['end'], 32 | ), 33 | $data, 34 | )); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Platform/Bridge/HuggingFace/Output/ZeroShotClassificationResult.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | final class ZeroShotClassificationResult 11 | { 12 | /** 13 | * @param array $labels 14 | * @param array $scores 15 | */ 16 | public function __construct( 17 | public array $labels, 18 | public array $scores, 19 | public ?string $sequence = null, 20 | ) { 21 | } 22 | 23 | /** 24 | * @param array{labels: array, scores: array, sequence?: string} $data 25 | */ 26 | public static function fromArray(array $data): self 27 | { 28 | return new self( 29 | $data['labels'], 30 | $data['scores'], 31 | $data['sequence'] ?? null, 32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Platform/Bridge/HuggingFace/PlatformFactory.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | final readonly class PlatformFactory 18 | { 19 | public static function create( 20 | #[\SensitiveParameter] 21 | string $apiKey, 22 | string $provider = Provider::HF_INFERENCE, 23 | ?HttpClientInterface $httpClient = null, 24 | ): Platform { 25 | $httpClient = $httpClient instanceof EventSourceHttpClient ? $httpClient : new EventSourceHttpClient($httpClient); 26 | 27 | return new Platform( 28 | [new ModelClient($httpClient, $provider, $apiKey)], 29 | [new ResponseConverter()], 30 | Contract::create( 31 | new FileNormalizer(), 32 | new MessageBagNormalizer(), 33 | ), 34 | ); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Platform/Bridge/HuggingFace/Provider.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | interface Provider 11 | { 12 | public const CEREBRAS = 'cerebras'; 13 | public const COHERE = 'cohere'; 14 | public const FAL_AI = 'fal-ai'; 15 | public const FIREWORKS = 'fireworks-ai'; 16 | public const HYPERBOLIC = 'hyperbolic'; 17 | public const HF_INFERENCE = 'hf-inference'; 18 | public const NEBIUS = 'nebius'; 19 | public const NOVITA = 'novita'; 20 | public const REPLICATE = 'replicate'; 21 | public const SAMBA_NOVA = 'sambanova'; 22 | public const TOGETHER = 'together'; 23 | } 24 | -------------------------------------------------------------------------------- /src/Platform/Bridge/HuggingFace/Task.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | interface Task 11 | { 12 | public const AUDIO_CLASSIFICATION = 'audio-classification'; 13 | public const AUTOMATIC_SPEECH_RECOGNITION = 'automatic-speech-recognition'; 14 | public const CHAT_COMPLETION = 'chat-completion'; 15 | public const FEATURE_EXTRACTION = 'feature-extraction'; 16 | public const FILL_MASK = 'fill-mask'; 17 | public const IMAGE_CLASSIFICATION = 'image-classification'; 18 | public const IMAGE_SEGMENTATION = 'image-segmentation'; 19 | public const IMAGE_TO_TEXT = 'image-to-text'; 20 | public const OBJECT_DETECTION = 'object-detection'; 21 | public const QUESTION_ANSWERING = 'question-answering'; 22 | public const SENTENCE_SIMILARITY = 'sentence-similarity'; 23 | public const SUMMARIZATION = 'summarization'; 24 | public const TABLE_QUESTION_ANSWERING = 'table-question-answering'; 25 | public const TEXT_CLASSIFICATION = 'text-classification'; 26 | public const TEXT_GENERATION = 'text-generation'; 27 | public const TEXT_TO_IMAGE = 'text-to-image'; 28 | public const TOKEN_CLASSIFICATION = 'token-classification'; 29 | public const TRANSLATION = 'translation'; 30 | public const ZERO_SHOT_CLASSIFICATION = 'zero-shot-classification'; 31 | } 32 | -------------------------------------------------------------------------------- /src/Platform/Bridge/Meta/Contract/MessageBagNormalizer.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | final class MessageBagNormalizer extends ModelContractNormalizer 17 | { 18 | public function __construct( 19 | private readonly LlamaPromptConverter $promptConverter = new LlamaPromptConverter(), 20 | ) { 21 | } 22 | 23 | protected function supportedDataClass(): string 24 | { 25 | return MessageBagInterface::class; 26 | } 27 | 28 | protected function supportsModel(Model $model): bool 29 | { 30 | return $model instanceof Llama; 31 | } 32 | 33 | /** 34 | * @param MessageBagInterface $data 35 | * 36 | * @return array{prompt: string} 37 | */ 38 | public function normalize(mixed $data, ?string $format = null, array $context = []): array 39 | { 40 | return [ 41 | 'prompt' => $this->promptConverter->convertToPrompt($data), 42 | ]; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Platform/Bridge/Meta/Llama.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class Llama extends Model 14 | { 15 | public const V3_3_70B_INSTRUCT = 'llama-3.3-70B-Instruct'; 16 | public const V3_2_90B_VISION_INSTRUCT = 'llama-3.2-90b-vision-instruct'; 17 | public const V3_2_11B_VISION_INSTRUCT = 'llama-3.2-11b-vision-instruct'; 18 | public const V3_2_3B = 'llama-3.2-3b'; 19 | public const V3_2_3B_INSTRUCT = 'llama-3.2-3b-instruct'; 20 | public const V3_2_1B = 'llama-3.2-1b'; 21 | public const V3_2_1B_INSTRUCT = 'llama-3.2-1b-instruct'; 22 | public const V3_1_405B_INSTRUCT = 'llama-3.1-405b-instruct'; 23 | public const V3_1_70B = 'llama-3.1-70b'; 24 | public const V3_1_70B_INSTRUCT = 'llama-3-70b-instruct'; 25 | public const V3_1_8B = 'llama-3.1-8b'; 26 | public const V3_1_8B_INSTRUCT = 'llama-3.1-8b-instruct'; 27 | public const V3_70B = 'llama-3-70b'; 28 | public const V3_8B_INSTRUCT = 'llama-3-8b-instruct'; 29 | public const V3_8B = 'llama-3-8b'; 30 | 31 | /** 32 | * @param array $options 33 | */ 34 | public function __construct(string $name = self::V3_1_405B_INSTRUCT, array $options = []) 35 | { 36 | $capabilities = [ 37 | Capability::INPUT_MESSAGES, 38 | Capability::OUTPUT_TEXT, 39 | ]; 40 | 41 | parent::__construct($name, $capabilities, $options); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Platform/Bridge/Mistral/Contract/ToolNormalizer.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class ToolNormalizer extends BaseToolNormalizer 11 | { 12 | public function normalize(mixed $data, ?string $format = null, array $context = []): array 13 | { 14 | $array = parent::normalize($data, $format, $context); 15 | 16 | $array['function']['parameters'] ??= ['type' => 'object']; 17 | 18 | return $array; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Platform/Bridge/Mistral/Embeddings.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | final class Embeddings extends Model 14 | { 15 | public const MISTRAL_EMBED = 'mistral-embed'; 16 | 17 | /** 18 | * @param array $options 19 | */ 20 | public function __construct( 21 | string $name = self::MISTRAL_EMBED, 22 | array $options = [], 23 | ) { 24 | parent::__construct($name, [Capability::INPUT_MULTIPLE], $options); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Platform/Bridge/Mistral/Embeddings/ModelClient.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | final readonly class ModelClient implements ModelClientInterface 18 | { 19 | private EventSourceHttpClient $httpClient; 20 | 21 | public function __construct( 22 | HttpClientInterface $httpClient, 23 | #[\SensitiveParameter] 24 | private string $apiKey, 25 | ) { 26 | $this->httpClient = $httpClient instanceof EventSourceHttpClient ? $httpClient : new EventSourceHttpClient($httpClient); 27 | } 28 | 29 | public function supports(Model $model): bool 30 | { 31 | return $model instanceof Embeddings; 32 | } 33 | 34 | public function request(Model $model, array|string $payload, array $options = []): ResponseInterface 35 | { 36 | return $this->httpClient->request('POST', 'https://api.mistral.ai/v1/embeddings', [ 37 | 'auth_bearer' => $this->apiKey, 38 | 'headers' => [ 39 | 'Content-Type' => 'application/json', 40 | ], 41 | 'json' => array_merge($options, [ 42 | 'model' => $model->getName(), 43 | 'input' => $payload, 44 | ]), 45 | ]); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Platform/Bridge/Mistral/Embeddings/ResponseConverter.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | final readonly class ResponseConverter implements ResponseConverterInterface 19 | { 20 | public function supports(Model $model): bool 21 | { 22 | return $model instanceof Embeddings; 23 | } 24 | 25 | public function convert(ResponseInterface $response, array $options = []): VectorResponse 26 | { 27 | $data = $response->toArray(false); 28 | 29 | if (200 !== $response->getStatusCode()) { 30 | throw new RuntimeException(\sprintf('Unexpected response code %d: %s', $response->getStatusCode(), $response->getContent(false))); 31 | } 32 | 33 | if (!isset($data['data'])) { 34 | throw new RuntimeException('Response does not contain data'); 35 | } 36 | 37 | return new VectorResponse( 38 | ...array_map( 39 | static fn (array $item): Vector => new Vector($item['embedding']), 40 | $data['data'] 41 | ), 42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Platform/Bridge/Mistral/Llm/ModelClient.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | final readonly class ModelClient implements ModelClientInterface 18 | { 19 | private EventSourceHttpClient $httpClient; 20 | 21 | public function __construct( 22 | HttpClientInterface $httpClient, 23 | #[\SensitiveParameter] 24 | private string $apiKey, 25 | ) { 26 | $this->httpClient = $httpClient instanceof EventSourceHttpClient ? $httpClient : new EventSourceHttpClient($httpClient); 27 | } 28 | 29 | public function supports(Model $model): bool 30 | { 31 | return $model instanceof Mistral; 32 | } 33 | 34 | public function request(Model $model, array|string $payload, array $options = []): ResponseInterface 35 | { 36 | return $this->httpClient->request('POST', 'https://api.mistral.ai/v1/chat/completions', [ 37 | 'auth_bearer' => $this->apiKey, 38 | 'headers' => [ 39 | 'Content-Type' => 'application/json', 40 | 'Accept' => 'application/json', 41 | ], 42 | 'json' => array_merge($options, $payload), 43 | ]); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Platform/Bridge/Mistral/Mistral.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | final class Mistral extends Model 14 | { 15 | public const CODESTRAL = 'codestral-latest'; 16 | public const CODESTRAL_MAMBA = 'open-codestral-mamba'; 17 | public const MISTRAL_LARGE = 'mistral-large-latest'; 18 | public const MISTRAL_SMALL = 'mistral-small-latest'; 19 | public const MISTRAL_NEMO = 'open-mistral-nemo'; 20 | public const MISTRAL_SABA = 'mistral-saba-latest'; 21 | public const MINISTRAL_3B = 'mistral-3b-latest'; 22 | public const MINISTRAL_8B = 'mistral-8b-latest'; 23 | public const PIXSTRAL_LARGE = 'pixstral-large-latest'; 24 | public const PIXSTRAL = 'pixstral-12b-latest'; 25 | 26 | /** 27 | * @param array $options 28 | */ 29 | public function __construct( 30 | string $name = self::MISTRAL_LARGE, 31 | array $options = [], 32 | ) { 33 | $capabilities = [ 34 | Capability::INPUT_MESSAGES, 35 | Capability::OUTPUT_TEXT, 36 | Capability::OUTPUT_STREAMING, 37 | Capability::OUTPUT_STRUCTURED, 38 | ]; 39 | 40 | if (\in_array($name, [self::PIXSTRAL, self::PIXSTRAL_LARGE, self::MISTRAL_SMALL], true)) { 41 | $capabilities[] = Capability::INPUT_IMAGE; 42 | } 43 | 44 | if (\in_array($name, [ 45 | self::CODESTRAL, 46 | self::MISTRAL_LARGE, 47 | self::MISTRAL_SMALL, 48 | self::MISTRAL_NEMO, 49 | self::MINISTRAL_3B, 50 | self::MINISTRAL_8B, 51 | self::PIXSTRAL, 52 | self::PIXSTRAL_LARGE, 53 | ], true)) { 54 | $capabilities[] = Capability::TOOL_CALLING; 55 | } 56 | 57 | parent::__construct($name, $capabilities, $options); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Platform/Bridge/Mistral/PlatformFactory.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | final class PlatformFactory 21 | { 22 | public static function create( 23 | #[\SensitiveParameter] 24 | string $apiKey, 25 | ?HttpClientInterface $httpClient = null, 26 | ): Platform { 27 | $httpClient = $httpClient instanceof EventSourceHttpClient ? $httpClient : new EventSourceHttpClient($httpClient); 28 | 29 | return new Platform( 30 | [new EmbeddingsModelClient($httpClient, $apiKey), new MistralModelClient($httpClient, $apiKey)], 31 | [new EmbeddingsResponseConverter(), new MistralResponseConverter()], 32 | Contract::create(new ToolNormalizer()), 33 | ); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Platform/Bridge/Ollama/LlamaModelHandler.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | final readonly class LlamaModelHandler implements ModelClientInterface, ResponseConverterInterface 21 | { 22 | public function __construct( 23 | private HttpClientInterface $httpClient, 24 | private string $hostUrl, 25 | ) { 26 | } 27 | 28 | public function supports(Model $model): bool 29 | { 30 | return $model instanceof Llama; 31 | } 32 | 33 | public function request(Model $model, array|string $payload, array $options = []): ResponseInterface 34 | { 35 | // Revert Ollama's default streaming behavior 36 | $options['stream'] ??= false; 37 | 38 | return $this->httpClient->request('POST', \sprintf('%s/api/chat', $this->hostUrl), [ 39 | 'headers' => ['Content-Type' => 'application/json'], 40 | 'json' => array_merge($options, $payload), 41 | ]); 42 | } 43 | 44 | public function convert(ResponseInterface $response, array $options = []): LlmResponse 45 | { 46 | $data = $response->toArray(); 47 | 48 | if (!isset($data['message'])) { 49 | throw new RuntimeException('Response does not contain message'); 50 | } 51 | 52 | if (!isset($data['message']['content'])) { 53 | throw new RuntimeException('Message does not contain content'); 54 | } 55 | 56 | return new TextResponse($data['message']['content']); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Platform/Bridge/Ollama/PlatformFactory.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | final class PlatformFactory 15 | { 16 | public static function create( 17 | string $hostUrl = 'http://localhost:11434', 18 | ?HttpClientInterface $httpClient = null, 19 | ): Platform { 20 | $httpClient = $httpClient instanceof EventSourceHttpClient ? $httpClient : new EventSourceHttpClient($httpClient); 21 | $handler = new LlamaModelHandler($httpClient, $hostUrl); 22 | 23 | return new Platform([$handler], [$handler]); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Platform/Bridge/OpenAI/DallE.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class DallE extends Model 14 | { 15 | public const DALL_E_2 = 'dall-e-2'; 16 | public const DALL_E_3 = 'dall-e-3'; 17 | 18 | /** @param array $options The default options for the model usage */ 19 | public function __construct(string $name = self::DALL_E_2, array $options = []) 20 | { 21 | $capabilities = [ 22 | Capability::INPUT_TEXT, 23 | Capability::OUTPUT_IMAGE, 24 | ]; 25 | 26 | parent::__construct($name, $capabilities, $options); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Platform/Bridge/OpenAI/DallE/Base64Image.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | final readonly class Base64Image 13 | { 14 | public function __construct( 15 | public string $encodedImage, 16 | ) { 17 | Assert::stringNotEmpty($encodedImage, 'The base64 encoded image generated must be given.'); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Platform/Bridge/OpenAI/DallE/ImageResponse.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class ImageResponse extends BaseResponse 13 | { 14 | /** @var list */ 15 | private readonly array $images; 16 | 17 | public function __construct( 18 | public ?string $revisedPrompt = null, // Only string on Dall-E 3 usage 19 | Base64Image|UrlImage ...$images, 20 | ) { 21 | $this->images = array_values($images); 22 | } 23 | 24 | /** 25 | * @return list 26 | */ 27 | public function getContent(): array 28 | { 29 | return $this->images; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Platform/Bridge/OpenAI/DallE/UrlImage.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | final readonly class UrlImage 13 | { 14 | public function __construct( 15 | public string $url, 16 | ) { 17 | Assert::stringNotEmpty($url, 'The image url must be given.'); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Platform/Bridge/OpenAI/Embeddings.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class Embeddings extends Model 13 | { 14 | public const TEXT_ADA_002 = 'text-embedding-ada-002'; 15 | public const TEXT_3_LARGE = 'text-embedding-3-large'; 16 | public const TEXT_3_SMALL = 'text-embedding-3-small'; 17 | 18 | /** 19 | * @param array $options 20 | */ 21 | public function __construct(string $name = self::TEXT_3_SMALL, array $options = []) 22 | { 23 | parent::__construct($name, [], $options); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Platform/Bridge/OpenAI/Embeddings/ModelClient.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | final readonly class ModelClient implements PlatformResponseFactory 18 | { 19 | public function __construct( 20 | private HttpClientInterface $httpClient, 21 | #[\SensitiveParameter] 22 | private string $apiKey, 23 | ) { 24 | Assert::stringNotEmpty($apiKey, 'The API key must not be empty.'); 25 | Assert::startsWith($apiKey, 'sk-', 'The API key must start with "sk-".'); 26 | } 27 | 28 | public function supports(Model $model): bool 29 | { 30 | return $model instanceof Embeddings; 31 | } 32 | 33 | public function request(Model $model, array|string $payload, array $options = []): ResponseInterface 34 | { 35 | return $this->httpClient->request('POST', 'https://api.openai.com/v1/embeddings', [ 36 | 'auth_bearer' => $this->apiKey, 37 | 'json' => array_merge($options, [ 38 | 'model' => $model->getName(), 39 | 'input' => $payload, 40 | ]), 41 | ]); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Platform/Bridge/OpenAI/Embeddings/ResponseConverter.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | final class ResponseConverter implements PlatformResponseConverter 19 | { 20 | public function supports(Model $model): bool 21 | { 22 | return $model instanceof Embeddings; 23 | } 24 | 25 | public function convert(ResponseInterface $response, array $options = []): VectorResponse 26 | { 27 | $data = $response->toArray(); 28 | 29 | if (!isset($data['data'])) { 30 | throw new RuntimeException('Response does not contain data'); 31 | } 32 | 33 | return new VectorResponse( 34 | ...array_map( 35 | static fn (array $item): Vector => new Vector($item['embedding']), 36 | $data['data'] 37 | ), 38 | ); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Platform/Bridge/OpenAI/GPT/ModelClient.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | final readonly class ModelClient implements PlatformResponseFactory 19 | { 20 | private EventSourceHttpClient $httpClient; 21 | 22 | public function __construct( 23 | HttpClientInterface $httpClient, 24 | #[\SensitiveParameter] 25 | private string $apiKey, 26 | ) { 27 | $this->httpClient = $httpClient instanceof EventSourceHttpClient ? $httpClient : new EventSourceHttpClient($httpClient); 28 | Assert::stringNotEmpty($apiKey, 'The API key must not be empty.'); 29 | Assert::startsWith($apiKey, 'sk-', 'The API key must start with "sk-".'); 30 | } 31 | 32 | public function supports(Model $model): bool 33 | { 34 | return $model instanceof GPT; 35 | } 36 | 37 | public function request(Model $model, array|string $payload, array $options = []): ResponseInterface 38 | { 39 | return $this->httpClient->request('POST', 'https://api.openai.com/v1/chat/completions', [ 40 | 'auth_bearer' => $this->apiKey, 41 | 'json' => array_merge($options, $payload), 42 | ]); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Platform/Bridge/OpenAI/PlatformFactory.php: -------------------------------------------------------------------------------- 1 | 22 | */ 23 | final readonly class PlatformFactory 24 | { 25 | public static function create( 26 | #[\SensitiveParameter] 27 | string $apiKey, 28 | ?HttpClientInterface $httpClient = null, 29 | ): Platform { 30 | $httpClient = $httpClient instanceof EventSourceHttpClient ? $httpClient : new EventSourceHttpClient($httpClient); 31 | 32 | $dallEModelClient = new DallEModelClient($httpClient, $apiKey); 33 | 34 | return new Platform( 35 | [ 36 | new GPTModelClient($httpClient, $apiKey), 37 | new EmbeddingsModelClient($httpClient, $apiKey), 38 | $dallEModelClient, 39 | new WhisperModelClient($httpClient, $apiKey), 40 | ], 41 | [ 42 | new GPTResponseConverter(), 43 | new EmbeddingsResponseConverter(), 44 | $dallEModelClient, 45 | new WhisperResponseConverter(), 46 | ], 47 | Contract::create(new AudioNormalizer()), 48 | ); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Platform/Bridge/OpenAI/TokenOutputProcessor.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | final class TokenOutputProcessor implements OutputProcessorInterface 15 | { 16 | public function processOutput(Output $output): void 17 | { 18 | if ($output->response instanceof StreamResponse) { 19 | // Streams have to be handled manually as the tokens are part of the streamed chunks 20 | return; 21 | } 22 | 23 | $rawResponse = $output->response->getRawResponse(); 24 | if (null === $rawResponse) { 25 | return; 26 | } 27 | 28 | $metadata = $output->response->getMetadata(); 29 | 30 | $metadata->add( 31 | 'remaining_tokens', 32 | (int) $rawResponse->getHeaders(false)['x-ratelimit-remaining-tokens'][0], 33 | ); 34 | 35 | $content = $rawResponse->toArray(false); 36 | 37 | if (!\array_key_exists('usage', $content)) { 38 | return; 39 | } 40 | 41 | $metadata->add('prompt_tokens', $content['usage']['prompt_tokens'] ?? null); 42 | $metadata->add('completion_tokens', $content['usage']['completion_tokens'] ?? null); 43 | $metadata->add('total_tokens', $content['usage']['total_tokens'] ?? null); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Platform/Bridge/OpenAI/Whisper.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class Whisper extends Model 14 | { 15 | public const WHISPER_1 = 'whisper-1'; 16 | 17 | /** 18 | * @param array $options 19 | */ 20 | public function __construct(string $name = self::WHISPER_1, array $options = []) 21 | { 22 | $capabilities = [ 23 | Capability::INPUT_AUDIO, 24 | Capability::OUTPUT_TEXT, 25 | ]; 26 | 27 | parent::__construct($name, $capabilities, $options); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Platform/Bridge/OpenAI/Whisper/AudioNormalizer.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | final class AudioNormalizer implements NormalizerInterface 16 | { 17 | public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool 18 | { 19 | return $data instanceof Audio && $context[Contract::CONTEXT_MODEL] instanceof Whisper; 20 | } 21 | 22 | public function getSupportedTypes(?string $format): array 23 | { 24 | return [ 25 | Audio::class => true, 26 | ]; 27 | } 28 | 29 | /** 30 | * @param Audio $data 31 | * 32 | * @return array{model: string, file: resource} 33 | */ 34 | public function normalize(mixed $data, ?string $format = null, array $context = []): array 35 | { 36 | return [ 37 | 'model' => $context[Contract::CONTEXT_MODEL]->getName(), 38 | 'file' => $data->asResource(), 39 | ]; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Platform/Bridge/OpenAI/Whisper/ModelClient.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | final readonly class ModelClient implements BaseModelClient 18 | { 19 | public function __construct( 20 | private HttpClientInterface $httpClient, 21 | #[\SensitiveParameter] 22 | private string $apiKey, 23 | ) { 24 | Assert::stringNotEmpty($apiKey, 'The API key must not be empty.'); 25 | } 26 | 27 | public function supports(Model $model): bool 28 | { 29 | return $model instanceof Whisper; 30 | } 31 | 32 | public function request(Model $model, array|string $payload, array $options = []): ResponseInterface 33 | { 34 | return $this->httpClient->request('POST', 'https://api.openai.com/v1/audio/transcriptions', [ 35 | 'auth_bearer' => $this->apiKey, 36 | 'headers' => ['Content-Type' => 'multipart/form-data'], 37 | 'body' => array_merge($options, $payload, ['model' => $model->getName()]), 38 | ]); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Platform/Bridge/OpenAI/Whisper/ResponseConverter.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | final class ResponseConverter implements BaseResponseConverter 18 | { 19 | public function supports(Model $model): bool 20 | { 21 | return $model instanceof Whisper; 22 | } 23 | 24 | public function convert(HttpResponse $response, array $options = []): LlmResponse 25 | { 26 | $data = $response->toArray(); 27 | 28 | return new TextResponse($data['text']); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Platform/Bridge/OpenRouter/PlatformFactory.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | final readonly class Client 15 | { 16 | public function __construct( 17 | private HttpClientInterface $httpClient, 18 | private ClockInterface $clock, 19 | #[\SensitiveParameter] private string $apiKey, 20 | ) { 21 | } 22 | 23 | /** 24 | * @param string $model The model name on Replicate, e.g. "meta/meta-llama-3.1-405b-instruct" 25 | * @param array $body 26 | */ 27 | public function request(string $model, string $endpoint, array $body): ResponseInterface 28 | { 29 | $url = \sprintf('https://api.replicate.com/v1/models/%s/%s', $model, $endpoint); 30 | 31 | $response = $this->httpClient->request('POST', $url, [ 32 | 'headers' => ['Content-Type' => 'application/json'], 33 | 'auth_bearer' => $this->apiKey, 34 | 'json' => ['input' => $body], 35 | ]); 36 | $data = $response->toArray(); 37 | 38 | while (!\in_array($data['status'], ['succeeded', 'failed', 'canceled'], true)) { 39 | $this->clock->sleep(1); // we need to wait until the prediction is ready 40 | 41 | $response = $this->getResponse($data['id']); 42 | $data = $response->toArray(); 43 | } 44 | 45 | return $response; 46 | } 47 | 48 | private function getResponse(string $id): ResponseInterface 49 | { 50 | $url = \sprintf('https://api.replicate.com/v1/predictions/%s', $id); 51 | 52 | return $this->httpClient->request('GET', $url, [ 53 | 'headers' => ['Content-Type' => 'application/json'], 54 | 'auth_bearer' => $this->apiKey, 55 | ]); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Platform/Bridge/Replicate/Contract/LlamaMessageBagNormalizer.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | final class LlamaMessageBagNormalizer extends ModelContractNormalizer 18 | { 19 | public function __construct( 20 | private readonly LlamaPromptConverter $promptConverter = new LlamaPromptConverter(), 21 | ) { 22 | } 23 | 24 | protected function supportedDataClass(): string 25 | { 26 | return MessageBagInterface::class; 27 | } 28 | 29 | protected function supportsModel(Model $model): bool 30 | { 31 | return $model instanceof Llama; 32 | } 33 | 34 | /** 35 | * @param MessageBagInterface $data 36 | * 37 | * @return array{system: string, prompt: string} 38 | */ 39 | public function normalize(mixed $data, ?string $format = null, array $context = []): array 40 | { 41 | return [ 42 | 'system' => $this->promptConverter->convertMessage($data->getSystemMessage() ?? new SystemMessage('')), 43 | 'prompt' => $this->promptConverter->convertToPrompt($data->withoutSystemMessage()), 44 | ]; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Platform/Bridge/Replicate/LlamaModelClient.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | final readonly class LlamaModelClient implements ModelClientInterface 17 | { 18 | public function __construct( 19 | private Client $client, 20 | ) { 21 | } 22 | 23 | public function supports(Model $model): bool 24 | { 25 | return $model instanceof Llama; 26 | } 27 | 28 | public function request(Model $model, array|string $payload, array $options = []): ResponseInterface 29 | { 30 | Assert::isInstanceOf($model, Llama::class); 31 | 32 | return $this->client->request(\sprintf('meta/meta-%s', $model->getName()), 'predictions', $payload); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Platform/Bridge/Replicate/LlamaResponseConverter.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | final readonly class LlamaResponseConverter implements ResponseConverterInterface 19 | { 20 | public function supports(Model $model): bool 21 | { 22 | return $model instanceof Llama; 23 | } 24 | 25 | public function convert(HttpResponse $response, array $options = []): LlmResponse 26 | { 27 | $data = $response->toArray(); 28 | 29 | if (!isset($data['output'])) { 30 | throw new RuntimeException('Response does not contain output'); 31 | } 32 | 33 | return new TextResponse(implode('', $data['output'])); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Platform/Bridge/Replicate/PlatformFactory.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | final class PlatformFactory 18 | { 19 | public static function create( 20 | #[\SensitiveParameter] 21 | string $apiKey, 22 | ?HttpClientInterface $httpClient = null, 23 | ): Platform { 24 | return new Platform( 25 | [new LlamaModelClient(new Client($httpClient ?? HttpClient::create(), new Clock(), $apiKey))], 26 | [new LlamaResponseConverter()], 27 | Contract::create(new LlamaMessageBagNormalizer()), 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Platform/Bridge/TransformersPHP/Platform.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | final class Platform implements PlatformInterface 21 | { 22 | public function request(Model $model, object|array|string $input, array $options = []): ResponseInterface 23 | { 24 | if (null === $task = $options['task'] ?? null) { 25 | throw new InvalidArgumentException('The task option is required.'); 26 | } 27 | 28 | $pipeline = pipeline( 29 | $options['task'], 30 | $model->getName(), 31 | $options['quantized'] ?? true, 32 | $options['config'] ?? null, 33 | $options['cacheDir'] ?? null, 34 | $options['revision'] ?? 'main', 35 | $options['modelFilename'] ?? null, 36 | ); 37 | 38 | $data = $pipeline($input); 39 | 40 | return match ($task) { 41 | Task::Text2TextGeneration => new TextResponse($data[0]['generated_text']), 42 | default => new ObjectResponse($data), 43 | }; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Platform/Bridge/TransformersPHP/PlatformFactory.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | final readonly class PlatformFactory 14 | { 15 | public static function create(): Platform 16 | { 17 | if (!class_exists(Transformers::class)) { 18 | throw new RuntimeException('TransformersPHP is not installed. Please install it using "composer require codewithkyrian/transformers".'); 19 | } 20 | 21 | return new Platform(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Platform/Bridge/Voyage/ModelHandler.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | final readonly class ModelHandler implements ModelClientInterface, ResponseConverterInterface 21 | { 22 | public function __construct( 23 | private HttpClientInterface $httpClient, 24 | #[\SensitiveParameter] private string $apiKey, 25 | ) { 26 | } 27 | 28 | public function supports(Model $model): bool 29 | { 30 | return $model instanceof Voyage; 31 | } 32 | 33 | public function request(Model $model, object|string|array $payload, array $options = []): ResponseInterface 34 | { 35 | return $this->httpClient->request('POST', 'https://api.voyageai.com/v1/embeddings', [ 36 | 'auth_bearer' => $this->apiKey, 37 | 'json' => [ 38 | 'model' => $model->getName(), 39 | 'input' => $payload, 40 | ], 41 | ]); 42 | } 43 | 44 | public function convert(ResponseInterface $response, array $options = []): LlmResponse 45 | { 46 | $response = $response->toArray(); 47 | 48 | if (!isset($response['data'])) { 49 | throw new RuntimeException('Response does not contain embedding data'); 50 | } 51 | 52 | $vectors = array_map(fn (array $data) => new Vector($data['embedding']), $response['data']); 53 | 54 | return new VectorResponse($vectors[0]); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Platform/Bridge/Voyage/PlatformFactory.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | final class PlatformFactory 15 | { 16 | public static function create( 17 | #[\SensitiveParameter] 18 | string $apiKey, 19 | ?HttpClientInterface $httpClient = null, 20 | ): Platform { 21 | $httpClient = $httpClient instanceof EventSourceHttpClient ? $httpClient : new EventSourceHttpClient($httpClient); 22 | $handler = new ModelHandler($httpClient, $apiKey); 23 | 24 | return new Platform([$handler], [$handler]); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Platform/Bridge/Voyage/Voyage.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class Voyage extends Model 14 | { 15 | public const V3 = 'voyage-3'; 16 | public const V3_LITE = 'voyage-3-lite'; 17 | public const FINANCE_2 = 'voyage-finance-2'; 18 | public const MULTILINGUAL_2 = 'voyage-multilingual-2'; 19 | public const LAW_2 = 'voyage-law-2'; 20 | public const CODE_2 = 'voyage-code-2'; 21 | 22 | /** 23 | * @param array $options 24 | */ 25 | public function __construct(string $name = self::V3, array $options = []) 26 | { 27 | parent::__construct($name, [Capability::INPUT_MULTIPLE], $options); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Platform/Capability.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class Capability 11 | { 12 | // INPUT 13 | public const INPUT_AUDIO = 'input-audio'; 14 | public const INPUT_IMAGE = 'input-image'; 15 | public const INPUT_MESSAGES = 'input-messages'; 16 | public const INPUT_MULTIPLE = 'input-multiple'; 17 | public const INPUT_PDF = 'input-pdf'; 18 | public const INPUT_TEXT = 'input-text'; 19 | 20 | // OUTPUT 21 | public const OUTPUT_AUDIO = 'output-audio'; 22 | public const OUTPUT_IMAGE = 'output-image'; 23 | public const OUTPUT_STREAMING = 'output-streaming'; 24 | public const OUTPUT_STRUCTURED = 'output-structured'; 25 | public const OUTPUT_TEXT = 'output-text'; 26 | 27 | // FUNCTIONALITY 28 | public const TOOL_CALLING = 'tool-calling'; 29 | } 30 | -------------------------------------------------------------------------------- /src/Platform/Contract/JsonSchema/DescriptionParser.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | final readonly class DescriptionParser 11 | { 12 | public function getDescription(\ReflectionProperty|\ReflectionParameter $reflector): string 13 | { 14 | if ($reflector instanceof \ReflectionProperty) { 15 | return $this->fromProperty($reflector); 16 | } 17 | 18 | return $this->fromParameter($reflector); 19 | } 20 | 21 | private function fromProperty(\ReflectionProperty $property): string 22 | { 23 | $comment = $property->getDocComment(); 24 | 25 | if (\is_string($comment) && preg_match('/@var\s+[a-zA-Z\\\\]+\s+((.*)(?=\*)|.*)/', $comment, $matches)) { 26 | return trim($matches[1]); 27 | } 28 | 29 | $class = $property->getDeclaringClass(); 30 | if ($class->hasMethod('__construct')) { 31 | return $this->fromParameter( 32 | new \ReflectionParameter([$class->getName(), '__construct'], $property->getName()) 33 | ); 34 | } 35 | 36 | return ''; 37 | } 38 | 39 | private function fromParameter(\ReflectionParameter $parameter): string 40 | { 41 | $comment = $parameter->getDeclaringFunction()->getDocComment(); 42 | if (!$comment) { 43 | return ''; 44 | } 45 | 46 | if (preg_match('/@param\s+\S+\s+\$'.preg_quote($parameter->getName(), '/').'\s+((.*)(?=\*)|.*)/', $comment, $matches)) { 47 | return trim($matches[1]); 48 | } 49 | 50 | return ''; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Platform/Contract/Normalizer/Message/AssistantMessageNormalizer.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | final class AssistantMessageNormalizer implements NormalizerInterface, NormalizerAwareInterface 16 | { 17 | use NormalizerAwareTrait; 18 | 19 | public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool 20 | { 21 | return $data instanceof AssistantMessage; 22 | } 23 | 24 | public function getSupportedTypes(?string $format): array 25 | { 26 | return [ 27 | AssistantMessage::class => true, 28 | ]; 29 | } 30 | 31 | /** 32 | * @param AssistantMessage $data 33 | * 34 | * @return array{role: 'assistant', content: string} 35 | */ 36 | public function normalize(mixed $data, ?string $format = null, array $context = []): array 37 | { 38 | $array = [ 39 | 'role' => $data->getRole()->value, 40 | ]; 41 | 42 | if (null !== $data->content) { 43 | $array['content'] = $data->content; 44 | } 45 | 46 | if ($data->hasToolCalls()) { 47 | $array['tool_calls'] = $this->normalizer->normalize($data->toolCalls, $format, $context); 48 | } 49 | 50 | return $array; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Platform/Contract/Normalizer/Message/Content/AudioNormalizer.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | final class AudioNormalizer implements NormalizerInterface 14 | { 15 | public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool 16 | { 17 | return $data instanceof Audio; 18 | } 19 | 20 | public function getSupportedTypes(?string $format): array 21 | { 22 | return [ 23 | Audio::class => true, 24 | ]; 25 | } 26 | 27 | /** 28 | * @param Audio $data 29 | * 30 | * @return array{type: 'input_audio', input_audio: array{ 31 | * data: string, 32 | * format: 'mp3'|'wav'|string, 33 | * }} 34 | */ 35 | public function normalize(mixed $data, ?string $format = null, array $context = []): array 36 | { 37 | return [ 38 | 'type' => 'input_audio', 39 | 'input_audio' => [ 40 | 'data' => $data->asBase64(), 41 | 'format' => match ($data->getFormat()) { 42 | 'audio/mpeg' => 'mp3', 43 | 'audio/wav' => 'wav', 44 | default => $data->getFormat(), 45 | }, 46 | ], 47 | ]; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Platform/Contract/Normalizer/Message/Content/ImageNormalizer.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | final class ImageNormalizer implements NormalizerInterface 14 | { 15 | public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool 16 | { 17 | return $data instanceof Image; 18 | } 19 | 20 | public function getSupportedTypes(?string $format): array 21 | { 22 | return [ 23 | Image::class => true, 24 | ]; 25 | } 26 | 27 | /** 28 | * @param Image $data 29 | * 30 | * @return array{type: 'image_url', image_url: array{url: string}} 31 | */ 32 | public function normalize(mixed $data, ?string $format = null, array $context = []): array 33 | { 34 | return [ 35 | 'type' => 'image_url', 36 | 'image_url' => ['url' => $data->asDataUrl()], 37 | ]; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Platform/Contract/Normalizer/Message/Content/ImageUrlNormalizer.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | final class ImageUrlNormalizer implements NormalizerInterface 14 | { 15 | public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool 16 | { 17 | return $data instanceof ImageUrl; 18 | } 19 | 20 | public function getSupportedTypes(?string $format): array 21 | { 22 | return [ 23 | ImageUrl::class => true, 24 | ]; 25 | } 26 | 27 | /** 28 | * @param ImageUrl $data 29 | * 30 | * @return array{type: 'image_url', image_url: array{url: string}} 31 | */ 32 | public function normalize(mixed $data, ?string $format = null, array $context = []): array 33 | { 34 | return [ 35 | 'type' => 'image_url', 36 | 'image_url' => ['url' => $data->url], 37 | ]; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Platform/Contract/Normalizer/Message/Content/TextNormalizer.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | final class TextNormalizer implements NormalizerInterface 14 | { 15 | public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool 16 | { 17 | return $data instanceof Text; 18 | } 19 | 20 | public function getSupportedTypes(?string $format): array 21 | { 22 | return [ 23 | Text::class => true, 24 | ]; 25 | } 26 | 27 | /** 28 | * @param Text $data 29 | * 30 | * @return array{type: 'text', text: string} 31 | */ 32 | public function normalize(mixed $data, ?string $format = null, array $context = []): array 33 | { 34 | return ['type' => 'text', 'text' => $data->text]; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Platform/Contract/Normalizer/Message/MessageBagNormalizer.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | final class MessageBagNormalizer implements NormalizerInterface, NormalizerAwareInterface 18 | { 19 | use NormalizerAwareTrait; 20 | 21 | public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool 22 | { 23 | return $data instanceof MessageBagInterface; 24 | } 25 | 26 | public function getSupportedTypes(?string $format): array 27 | { 28 | return [ 29 | MessageBagInterface::class => true, 30 | ]; 31 | } 32 | 33 | /** 34 | * @param MessageBagInterface $data 35 | * 36 | * @return array{ 37 | * messages: array, 38 | * model?: string, 39 | * } 40 | */ 41 | public function normalize(mixed $data, ?string $format = null, array $context = []): array 42 | { 43 | $array = [ 44 | 'messages' => $this->normalizer->normalize($data->getMessages(), $format, $context), 45 | ]; 46 | 47 | if (isset($context[Contract::CONTEXT_MODEL]) && $context[Contract::CONTEXT_MODEL] instanceof Model) { 48 | $array['model'] = $context[Contract::CONTEXT_MODEL]->getName(); 49 | } 50 | 51 | return $array; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Platform/Contract/Normalizer/Message/SystemMessageNormalizer.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | final class SystemMessageNormalizer implements NormalizerInterface 14 | { 15 | public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool 16 | { 17 | return $data instanceof SystemMessage; 18 | } 19 | 20 | public function getSupportedTypes(?string $format): array 21 | { 22 | return [ 23 | SystemMessage::class => true, 24 | ]; 25 | } 26 | 27 | /** 28 | * @param SystemMessage $data 29 | * 30 | * @return array{role: 'system', content: string} 31 | */ 32 | public function normalize(mixed $data, ?string $format = null, array $context = []): array 33 | { 34 | return [ 35 | 'role' => $data->getRole()->value, 36 | 'content' => $data->content, 37 | ]; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Platform/Contract/Normalizer/Message/ToolCallMessageNormalizer.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | final class ToolCallMessageNormalizer implements NormalizerInterface, NormalizerAwareInterface 16 | { 17 | use NormalizerAwareTrait; 18 | 19 | public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool 20 | { 21 | return $data instanceof ToolCallMessage; 22 | } 23 | 24 | public function getSupportedTypes(?string $format): array 25 | { 26 | return [ 27 | ToolCallMessage::class => true, 28 | ]; 29 | } 30 | 31 | /** 32 | * @return array{ 33 | * role: 'tool', 34 | * content: string, 35 | * tool_call_id: string, 36 | * } 37 | */ 38 | public function normalize(mixed $data, ?string $format = null, array $context = []): array 39 | { 40 | return [ 41 | 'role' => $data->getRole()->value, 42 | 'content' => $this->normalizer->normalize($data->content, $format, $context), 43 | 'tool_call_id' => $data->toolCall->id, 44 | ]; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Platform/Contract/Normalizer/Message/UserMessageNormalizer.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | final class UserMessageNormalizer implements NormalizerInterface, NormalizerAwareInterface 17 | { 18 | use NormalizerAwareTrait; 19 | 20 | public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool 21 | { 22 | return $data instanceof UserMessage; 23 | } 24 | 25 | public function getSupportedTypes(?string $format): array 26 | { 27 | return [ 28 | UserMessage::class => true, 29 | ]; 30 | } 31 | 32 | /** 33 | * @param UserMessage $data 34 | * 35 | * @return array{role: 'assistant', content: string} 36 | */ 37 | public function normalize(mixed $data, ?string $format = null, array $context = []): array 38 | { 39 | $array = ['role' => $data->getRole()->value]; 40 | 41 | if (1 === \count($data->content) && $data->content[0] instanceof Text) { 42 | $array['content'] = $data->content[0]->text; 43 | 44 | return $array; 45 | } 46 | 47 | $array['content'] = $this->normalizer->normalize($data->content, $format, $context); 48 | 49 | return $array; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Platform/Contract/Normalizer/ModelContractNormalizer.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | abstract class ModelContractNormalizer implements NormalizerInterface 13 | { 14 | /** 15 | * @return class-string 16 | */ 17 | abstract protected function supportedDataClass(): string; 18 | 19 | abstract protected function supportsModel(Model $model): bool; 20 | 21 | public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool 22 | { 23 | if (!is_a($data, $this->supportedDataClass(), true)) { 24 | return false; 25 | } 26 | 27 | if (isset($context[Contract::CONTEXT_MODEL]) && $context[Contract::CONTEXT_MODEL] instanceof Model) { 28 | return $this->supportsModel($context[Contract::CONTEXT_MODEL]); 29 | } 30 | 31 | return false; 32 | } 33 | 34 | public function getSupportedTypes(?string $format): array 35 | { 36 | return [ 37 | $this->supportedDataClass() => true, 38 | ]; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Platform/Contract/Normalizer/Response/ToolCallNormalizer.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | final class ToolCallNormalizer implements NormalizerInterface 14 | { 15 | public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool 16 | { 17 | return $data instanceof ToolCall; 18 | } 19 | 20 | public function getSupportedTypes(?string $format): array 21 | { 22 | return [ 23 | ToolCall::class => true, 24 | ]; 25 | } 26 | 27 | /** 28 | * @param ToolCall $data 29 | * 30 | * @return array{ 31 | * id: string, 32 | * type: 'function', 33 | * function: array{ 34 | * name: string, 35 | * arguments: string 36 | * } 37 | * } 38 | */ 39 | public function normalize(mixed $data, ?string $format = null, array $context = []): array 40 | { 41 | return [ 42 | 'id' => $data->id, 43 | 'type' => 'function', 44 | 'function' => [ 45 | 'name' => $data->name, 46 | 'arguments' => json_encode($data->arguments), 47 | ], 48 | ]; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Platform/Contract/Normalizer/ToolNormalizer.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | class ToolNormalizer implements NormalizerInterface 15 | { 16 | public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool 17 | { 18 | return $data instanceof Tool; 19 | } 20 | 21 | public function getSupportedTypes(?string $format): array 22 | { 23 | return [ 24 | Tool::class => true, 25 | ]; 26 | } 27 | 28 | /** 29 | * @param Tool $data 30 | * 31 | * @return array{ 32 | * type: 'function', 33 | * function: array{ 34 | * name: string, 35 | * description: string, 36 | * parameters?: JsonSchema 37 | * } 38 | * } 39 | */ 40 | public function normalize(mixed $data, ?string $format = null, array $context = []): array 41 | { 42 | $function = [ 43 | 'name' => $data->name, 44 | 'description' => $data->description, 45 | ]; 46 | 47 | if (isset($data->parameters)) { 48 | $function['parameters'] = $data->parameters; 49 | } 50 | 51 | return [ 52 | 'type' => 'function', 53 | 'function' => $function, 54 | ]; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Platform/Exception/ContentFilterException.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class ContentFilterException extends InvalidArgumentException 11 | { 12 | } 13 | -------------------------------------------------------------------------------- /src/Platform/Exception/ExceptionInterface.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | interface ExceptionInterface extends \Throwable 11 | { 12 | } 13 | -------------------------------------------------------------------------------- /src/Platform/Exception/InvalidArgumentException.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface 11 | { 12 | } 13 | -------------------------------------------------------------------------------- /src/Platform/Exception/RuntimeException.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class RuntimeException extends \RuntimeException implements ExceptionInterface 11 | { 12 | } 13 | -------------------------------------------------------------------------------- /src/Platform/Message/AssistantMessage.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | final readonly class AssistantMessage implements MessageInterface 13 | { 14 | /** 15 | * @param ?ToolCall[] $toolCalls 16 | */ 17 | public function __construct( 18 | public ?string $content = null, 19 | public ?array $toolCalls = null, 20 | ) { 21 | } 22 | 23 | public function getRole(): Role 24 | { 25 | return Role::Assistant; 26 | } 27 | 28 | public function hasToolCalls(): bool 29 | { 30 | return null !== $this->toolCalls && 0 !== \count($this->toolCalls); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Platform/Message/Content/Audio.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | final readonly class Audio extends File 11 | { 12 | } 13 | -------------------------------------------------------------------------------- /src/Platform/Message/Content/ContentInterface.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | interface ContentInterface 11 | { 12 | } 13 | -------------------------------------------------------------------------------- /src/Platform/Message/Content/Document.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | final readonly class Document extends File 11 | { 12 | } 13 | -------------------------------------------------------------------------------- /src/Platform/Message/Content/DocumentUrl.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | final readonly class DocumentUrl implements ContentInterface 11 | { 12 | public function __construct( 13 | public string $url, 14 | ) { 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Platform/Message/Content/Image.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | final readonly class Image extends File 11 | { 12 | } 13 | -------------------------------------------------------------------------------- /src/Platform/Message/Content/ImageUrl.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | final readonly class ImageUrl implements ContentInterface 11 | { 12 | public function __construct( 13 | public string $url, 14 | ) { 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Platform/Message/Content/Text.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | final readonly class Text implements ContentInterface 11 | { 12 | public function __construct( 13 | public string $text, 14 | ) { 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Platform/Message/Message.php: -------------------------------------------------------------------------------- 1 | 13 | * @author Denis Zunke 14 | */ 15 | final readonly class Message 16 | { 17 | // Disabled by default, just a bridge to the specific messages 18 | private function __construct() 19 | { 20 | } 21 | 22 | public static function forSystem(string $content): SystemMessage 23 | { 24 | return new SystemMessage($content); 25 | } 26 | 27 | /** 28 | * @param ?ToolCall[] $toolCalls 29 | */ 30 | public static function ofAssistant(?string $content = null, ?array $toolCalls = null): AssistantMessage 31 | { 32 | return new AssistantMessage($content, $toolCalls); 33 | } 34 | 35 | public static function ofUser(string|ContentInterface ...$content): UserMessage 36 | { 37 | $content = array_map( 38 | static fn (string|ContentInterface $entry) => \is_string($entry) ? new Text($entry) : $entry, 39 | $content, 40 | ); 41 | 42 | return new UserMessage(...$content); 43 | } 44 | 45 | public static function ofToolCall(ToolCall $toolCall, string $content): ToolCallMessage 46 | { 47 | return new ToolCallMessage($toolCall, $content); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Platform/Message/MessageBagInterface.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | interface MessageBagInterface extends \Countable 11 | { 12 | public function add(MessageInterface $message): void; 13 | 14 | /** 15 | * @return list 16 | */ 17 | public function getMessages(): array; 18 | 19 | public function getSystemMessage(): ?SystemMessage; 20 | 21 | public function with(MessageInterface $message): self; 22 | 23 | public function merge(self $messageBag): self; 24 | 25 | public function withoutSystemMessage(): self; 26 | 27 | public function prepend(MessageInterface $message): self; 28 | 29 | public function containsAudio(): bool; 30 | 31 | public function containsImage(): bool; 32 | } 33 | -------------------------------------------------------------------------------- /src/Platform/Message/MessageInterface.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | interface MessageInterface 11 | { 12 | public function getRole(): Role; 13 | } 14 | -------------------------------------------------------------------------------- /src/Platform/Message/Role.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | enum Role: string 13 | { 14 | use Comparable; 15 | 16 | case System = 'system'; 17 | case Assistant = 'assistant'; 18 | case User = 'user'; 19 | case ToolCall = 'tool'; 20 | } 21 | -------------------------------------------------------------------------------- /src/Platform/Message/SystemMessage.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | final readonly class SystemMessage implements MessageInterface 11 | { 12 | public function __construct(public string $content) 13 | { 14 | } 15 | 16 | public function getRole(): Role 17 | { 18 | return Role::System; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Platform/Message/ToolCallMessage.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | final readonly class ToolCallMessage implements MessageInterface 13 | { 14 | public function __construct( 15 | public ToolCall $toolCall, 16 | public string $content, 17 | ) { 18 | } 19 | 20 | public function getRole(): Role 21 | { 22 | return Role::ToolCall; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Platform/Message/UserMessage.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | final readonly class UserMessage implements MessageInterface 16 | { 17 | /** 18 | * @var list 19 | */ 20 | public array $content; 21 | 22 | public function __construct( 23 | ContentInterface ...$content, 24 | ) { 25 | $this->content = $content; 26 | } 27 | 28 | public function getRole(): Role 29 | { 30 | return Role::User; 31 | } 32 | 33 | public function hasAudioContent(): bool 34 | { 35 | foreach ($this->content as $content) { 36 | if ($content instanceof Audio) { 37 | return true; 38 | } 39 | } 40 | 41 | return false; 42 | } 43 | 44 | public function hasImageContent(): bool 45 | { 46 | foreach ($this->content as $content) { 47 | if ($content instanceof Image || $content instanceof ImageUrl) { 48 | return true; 49 | } 50 | } 51 | 52 | return false; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Platform/Model.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class Model 11 | { 12 | /** 13 | * @param string[] $capabilities 14 | * @param array $options 15 | */ 16 | public function __construct( 17 | private readonly string $name, 18 | private readonly array $capabilities = [], 19 | private readonly array $options = [], 20 | ) { 21 | } 22 | 23 | public function getName(): string 24 | { 25 | return $this->name; 26 | } 27 | 28 | /** 29 | * @return string[] 30 | */ 31 | public function getCapabilities(): array 32 | { 33 | return $this->capabilities; 34 | } 35 | 36 | public function supports(string $capability): bool 37 | { 38 | return \in_array($capability, $this->capabilities, true); 39 | } 40 | 41 | /** 42 | * @return array 43 | */ 44 | public function getOptions(): array 45 | { 46 | return $this->options; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Platform/ModelClientInterface.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | interface ModelClientInterface 13 | { 14 | public function supports(Model $model): bool; 15 | 16 | /** 17 | * @param array $payload 18 | * @param array $options 19 | */ 20 | public function request(Model $model, array|string $payload, array $options = []): ResponseInterface; 21 | } 22 | -------------------------------------------------------------------------------- /src/Platform/PlatformInterface.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | interface PlatformInterface 13 | { 14 | /** 15 | * @param array|string|object $input 16 | * @param array $options 17 | */ 18 | public function request(Model $model, array|string|object $input, array $options = []): ResponseInterface; 19 | } 20 | -------------------------------------------------------------------------------- /src/Platform/Response/BaseResponse.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | abstract class BaseResponse implements ResponseInterface 13 | { 14 | use MetadataAwareTrait; 15 | use RawResponseAwareTrait; 16 | } 17 | -------------------------------------------------------------------------------- /src/Platform/Response/BinaryResponse.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | final class BinaryResponse extends BaseResponse 13 | { 14 | public function __construct( 15 | public string $data, 16 | public ?string $mimeType = null, 17 | ) { 18 | } 19 | 20 | public function getContent(): string 21 | { 22 | return $this->data; 23 | } 24 | 25 | public function toBase64(): string 26 | { 27 | return base64_encode($this->data); 28 | } 29 | 30 | public function toDataUri(): string 31 | { 32 | if (null === $this->mimeType) { 33 | throw new RuntimeException('Mime type is not set.'); 34 | } 35 | 36 | return 'data:'.$this->mimeType.';base64,'.$this->toBase64(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Platform/Response/Choice.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | final readonly class Choice 11 | { 12 | /** 13 | * @param ToolCall[] $toolCalls 14 | */ 15 | public function __construct( 16 | private ?string $content = null, 17 | private array $toolCalls = [], 18 | ) { 19 | } 20 | 21 | public function getContent(): ?string 22 | { 23 | return $this->content; 24 | } 25 | 26 | public function hasContent(): bool 27 | { 28 | return null !== $this->content; 29 | } 30 | 31 | /** 32 | * @return ToolCall[] 33 | */ 34 | public function getToolCalls(): array 35 | { 36 | return $this->toolCalls; 37 | } 38 | 39 | public function hasToolCall(): bool 40 | { 41 | return 0 !== \count($this->toolCalls); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Platform/Response/ChoiceResponse.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | final class ChoiceResponse extends BaseResponse 13 | { 14 | /** 15 | * @var Choice[] 16 | */ 17 | private readonly array $choices; 18 | 19 | public function __construct(Choice ...$choices) 20 | { 21 | if (0 === \count($choices)) { 22 | throw new InvalidArgumentException('Response must have at least one choice.'); 23 | } 24 | 25 | $this->choices = $choices; 26 | } 27 | 28 | /** 29 | * @return Choice[] 30 | */ 31 | public function getContent(): array 32 | { 33 | return $this->choices; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Platform/Response/Exception/RawResponseAlreadySetException.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | final class RawResponseAlreadySetException extends RuntimeException 13 | { 14 | public function __construct() 15 | { 16 | parent::__construct('The raw response was already set.'); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Platform/Response/Metadata/MetadataAwareTrait.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | trait MetadataAwareTrait 11 | { 12 | private ?Metadata $metadata = null; 13 | 14 | public function getMetadata(): Metadata 15 | { 16 | return $this->metadata ??= new Metadata(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Platform/Response/ObjectResponse.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | final class ObjectResponse extends BaseResponse 11 | { 12 | /** 13 | * @param object|array $structuredOutput 14 | */ 15 | public function __construct( 16 | private readonly object|array $structuredOutput, 17 | ) { 18 | } 19 | 20 | /** 21 | * @return object|array 22 | */ 23 | public function getContent(): object|array 24 | { 25 | return $this->structuredOutput; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Platform/Response/RawResponseAwareTrait.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | trait RawResponseAwareTrait 14 | { 15 | protected ?SymfonyHttpResponse $rawResponse = null; 16 | 17 | public function setRawResponse(SymfonyHttpResponse $rawResponse): void 18 | { 19 | if (null !== $this->rawResponse) { 20 | throw new RawResponseAlreadySetException(); 21 | } 22 | 23 | $this->rawResponse = $rawResponse; 24 | } 25 | 26 | public function getRawResponse(): ?SymfonyHttpResponse 27 | { 28 | return $this->rawResponse; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Platform/Response/ResponseInterface.php: -------------------------------------------------------------------------------- 1 | 13 | * @author Denis Zunke 14 | */ 15 | interface ResponseInterface 16 | { 17 | /** 18 | * @return string|iterable|object|null 19 | */ 20 | public function getContent(): string|iterable|object|null; 21 | 22 | public function getMetadata(): Metadata; 23 | 24 | public function getRawResponse(): ?SymfonyHttpResponse; 25 | 26 | /** 27 | * @throws RawResponseAlreadySetException if the response is tried to be set more than once 28 | */ 29 | public function setRawResponse(SymfonyHttpResponse $rawResponse): void; 30 | } 31 | -------------------------------------------------------------------------------- /src/Platform/Response/StreamResponse.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | final class StreamResponse extends BaseResponse 11 | { 12 | public function __construct( 13 | private readonly \Generator $generator, 14 | ) { 15 | } 16 | 17 | public function getContent(): \Generator 18 | { 19 | yield from $this->generator; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Platform/Response/TextResponse.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | final class TextResponse extends BaseResponse 11 | { 12 | public function __construct( 13 | private readonly string $content, 14 | ) { 15 | } 16 | 17 | public function getContent(): string 18 | { 19 | return $this->content; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Platform/Response/ToolCall.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | final readonly class ToolCall implements \JsonSerializable 11 | { 12 | /** 13 | * @param array $arguments 14 | */ 15 | public function __construct( 16 | public string $id, 17 | public string $name, 18 | public array $arguments = [], 19 | ) { 20 | } 21 | 22 | /** 23 | * @return array{ 24 | * id: string, 25 | * type: 'function', 26 | * function: array{ 27 | * name: string, 28 | * arguments: string 29 | * } 30 | * } 31 | */ 32 | public function jsonSerialize(): array 33 | { 34 | return [ 35 | 'id' => $this->id, 36 | 'type' => 'function', 37 | 'function' => [ 38 | 'name' => $this->name, 39 | 'arguments' => json_encode($this->arguments), 40 | ], 41 | ]; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Platform/Response/ToolCallResponse.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | final class ToolCallResponse extends BaseResponse 13 | { 14 | /** 15 | * @var ToolCall[] 16 | */ 17 | private readonly array $toolCalls; 18 | 19 | public function __construct(ToolCall ...$toolCalls) 20 | { 21 | if (0 === \count($toolCalls)) { 22 | throw new InvalidArgumentException('Response must have at least one tool call.'); 23 | } 24 | 25 | $this->toolCalls = $toolCalls; 26 | } 27 | 28 | /** 29 | * @return ToolCall[] 30 | */ 31 | public function getContent(): array 32 | { 33 | return $this->toolCalls; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Platform/Response/VectorResponse.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | final class VectorResponse extends BaseResponse 13 | { 14 | /** 15 | * @var Vector[] 16 | */ 17 | private readonly array $vectors; 18 | 19 | public function __construct(Vector ...$vector) 20 | { 21 | $this->vectors = $vector; 22 | } 23 | 24 | /** 25 | * @return Vector[] 26 | */ 27 | public function getContent(): array 28 | { 29 | return $this->vectors; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Platform/ResponseConverterInterface.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | interface ResponseConverterInterface 14 | { 15 | public function supports(Model $model): bool; 16 | 17 | /** 18 | * @param array $options 19 | */ 20 | public function convert(HttpResponse $response, array $options = []): LlmResponse; 21 | } 22 | -------------------------------------------------------------------------------- /src/Platform/Tool/ExecutionReference.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | final class ExecutionReference 11 | { 12 | public function __construct( 13 | public string $class, 14 | public string $method = '__invoke', 15 | ) { 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Platform/Tool/Tool.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | final readonly class Tool 15 | { 16 | /** 17 | * @param JsonSchema|null $parameters 18 | */ 19 | public function __construct( 20 | public ExecutionReference $reference, 21 | public string $name, 22 | public string $description, 23 | public ?array $parameters = null, 24 | ) { 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Platform/Vector/NullVector.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | final class NullVector implements VectorInterface 13 | { 14 | public function getData(): array 15 | { 16 | throw new RuntimeException('getData() method cannot be called on a NullVector.'); 17 | } 18 | 19 | public function getDimensions(): int 20 | { 21 | throw new RuntimeException('getDimensions() method cannot be called on a NullVector.'); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Platform/Vector/Vector.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | final class Vector implements VectorInterface 13 | { 14 | /** 15 | * @param list $data 16 | */ 17 | public function __construct( 18 | private readonly array $data, 19 | private ?int $dimensions = null, 20 | ) { 21 | if (null !== $dimensions && $dimensions !== \count($data)) { 22 | throw new InvalidArgumentException('Vector must have '.$dimensions.' dimensions'); 23 | } 24 | 25 | if (0 === \count($data)) { 26 | throw new InvalidArgumentException('Vector must have at least one dimension'); 27 | } 28 | 29 | if (\is_int($dimensions) && \count($data) !== $dimensions) { 30 | throw new InvalidArgumentException('Vector must have '.$dimensions.' dimensions'); 31 | } 32 | 33 | if (null === $this->dimensions) { 34 | $this->dimensions = \count($data); 35 | } 36 | } 37 | 38 | /** 39 | * @return list 40 | */ 41 | public function getData(): array 42 | { 43 | return $this->data; 44 | } 45 | 46 | public function getDimensions(): int 47 | { 48 | return $this->dimensions; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Platform/Vector/VectorInterface.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | interface VectorInterface 11 | { 12 | /** 13 | * @return list 14 | */ 15 | public function getData(): array; 16 | 17 | public function getDimensions(): int; 18 | } 19 | -------------------------------------------------------------------------------- /src/Store/Bridge/ChromaDB/Store.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | final readonly class Store implements VectorStoreInterface 18 | { 19 | public function __construct( 20 | private Client $client, 21 | private string $collectionName, 22 | ) { 23 | } 24 | 25 | public function add(VectorDocument ...$documents): void 26 | { 27 | $ids = []; 28 | $vectors = []; 29 | $metadata = []; 30 | foreach ($documents as $document) { 31 | $ids[] = (string) $document->id; 32 | $vectors[] = $document->vector->getData(); 33 | $metadata[] = $document->metadata->getArrayCopy(); 34 | } 35 | 36 | $collection = $this->client->getOrCreateCollection($this->collectionName); 37 | $collection->add($ids, $vectors, $metadata); 38 | } 39 | 40 | public function query(Vector $vector, array $options = [], ?float $minScore = null): array 41 | { 42 | $collection = $this->client->getOrCreateCollection($this->collectionName); 43 | $queryResponse = $collection->query( 44 | queryEmbeddings: [$vector->getData()], 45 | nResults: 4, 46 | ); 47 | 48 | $documents = []; 49 | for ($i = 0; $i < \count($queryResponse->metadatas[0]); ++$i) { 50 | $documents[] = new VectorDocument( 51 | id: Uuid::fromString($queryResponse->ids[0][$i]), 52 | vector: new Vector($queryResponse->embeddings[0][$i]), 53 | metadata: new Metadata($queryResponse->metadatas[0][$i]), 54 | ); 55 | } 56 | 57 | return $documents; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Store/Document/Metadata.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * @author Christopher Hertel 11 | */ 12 | final class Metadata extends \ArrayObject 13 | { 14 | } 15 | -------------------------------------------------------------------------------- /src/Store/Document/TextDocument.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | final readonly class TextDocument 14 | { 15 | public function __construct( 16 | public Uuid $id, 17 | public string $content, 18 | public Metadata $metadata = new Metadata(), 19 | ) { 20 | Assert::stringNotEmpty(trim($this->content)); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Store/Document/VectorDocument.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | final readonly class VectorDocument 14 | { 15 | public function __construct( 16 | public Uuid $id, 17 | public VectorInterface $vector, 18 | public Metadata $metadata = new Metadata(), 19 | public ?float $score = null, 20 | ) { 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Store/Exception/ExceptionInterface.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | interface ExceptionInterface extends \Throwable 11 | { 12 | } 13 | -------------------------------------------------------------------------------- /src/Store/Exception/InvalidArgumentException.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface 11 | { 12 | } 13 | -------------------------------------------------------------------------------- /src/Store/Exception/RuntimeException.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class RuntimeException extends \RuntimeException implements ExceptionInterface 11 | { 12 | } 13 | -------------------------------------------------------------------------------- /src/Store/InitializableStoreInterface.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | interface InitializableStoreInterface extends StoreInterface 11 | { 12 | /** 13 | * @param array $options 14 | */ 15 | public function initialize(array $options = []): void; 16 | } 17 | -------------------------------------------------------------------------------- /src/Store/StoreInterface.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | interface StoreInterface 13 | { 14 | public function add(VectorDocument ...$documents): void; 15 | } 16 | -------------------------------------------------------------------------------- /src/Store/VectorStoreInterface.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | interface VectorStoreInterface extends StoreInterface 14 | { 15 | /** 16 | * @param array $options 17 | * 18 | * @return VectorDocument[] 19 | */ 20 | public function query(Vector $vector, array $options = [], ?float $minScore = null): array; 21 | } 22 | --------------------------------------------------------------------------------