diff --git a/README.md b/README.md index e23bd2618..ebc69ce13 100644 --- a/README.md +++ b/README.md @@ -34,11 +34,3 @@ Help Symfony by [sponsoring](https://symfony.com/sponsor) its development! ## Contributing Thank you for considering contributing to Symfony AI! You can find the [contribution guide here](CONTRIBUTING.md). - -## Fixture Licenses - -For testing multi-modal features, the repository contains binary media content, with the following owners and licenses: - -* `tests/Fixture/image.jpg`: Chris F., Creative Commons, see [pexels.com](https://www.pexels.com/photo/blauer-und-gruner-elefant-mit-licht-1680755/) -* `tests/Fixture/audio.mp3`: davidbain, Creative Commons, see [freesound.org](https://freesound.org/people/davidbain/sounds/136777/) -* `tests/Fixture/document.pdf`: Chem8240ja, Public Domain, see [Wikipedia](https://en.m.wikipedia.org/wiki/File:Re_example.pdf) diff --git a/examples/huggingface/README.md b/examples/huggingface/README.md new file mode 100644 index 000000000..c9baeed7a --- /dev/null +++ b/examples/huggingface/README.md @@ -0,0 +1,128 @@ +# Symfony Hugging Face Examples + +This directory contains various examples of how to use the Symfony AI with [Hugging Face](https://huggingface.co/) +and sits on top of the [Hugging Face Inference API](https://huggingface.co/inference-api). + +The Hugging Face Hub provides access to a wide range of pre-trained open source models for various AI tasks, which you +can directly use via Symfony AI's Hugging Face Platform Bridge. + +## Getting Started + +Hugging Face offers a free tier for their Inference API, which you can use to get started. Therefore, you need to create +an account on [Hugging Face](https://huggingface.co/join), generate an +[access token](https://huggingface.co/settings/tokens), and add it to your `.env.local` file in the root of the +examples' directory as `HUGGINGFACE_KEY`. + +```bash +echo 'HUGGINGFACE_KEY=hf_your_access_key' >> .env.local +``` + +Different to other platforms, Hugging Face provides close to 50.000 models for various AI tasks, which enables you to +easily try out different, specialized models for your use case. Common use cases can be found in this example directory. + +## Running the Examples + +You can run an example by executing the following command: + +```bash +# Run all example with runner: +./runner huggingface + +# Or run a specific example standalone, e.g., object detection: +php huggingface/object-detection.php +``` + +## Available Models + +When running the examples, you might experience that some models are not available, and you encounter an error like: + +``` +Model, provider or task not found (404). +``` + +This can happen due to pre-selected models in the examples not being available anymore or not being "warmed up" on +Hugging Face's side. You can change the model used in the examples by updating the model name in the example script. + +To find available models for a specific task, you can check out the [Hugging Face Model Hub](https://huggingface.co/models) +and filter by the desired task, or you can use the `huggingface/_model-listing.php` script. + +### Listing Available Models + +List _all_ models: + +```bash +php huggingface/_model.php ai:huggingface:model-list +``` +(This is limited to 1000 results by default.) + +Limit models to a specific _task_, e.g., object-detection: + +```bash +php huggingface/_model.php ai:huggingface:model-list --task=object-detection +``` + +Limit models to a specific _provider_, e.g., "hf-inference": + +```bash +# Single provider: +php huggingface/_model.php ai:huggingface:model-list --provider=hf-inference + +# Multiple providers: +php huggingface/_model.php ai:huggingface:model-list --provider=sambanova,novita +``` + +Search for models matching a specific term, e.g., "gpt": + +```bash +php huggingface/_model.php ai:huggingface:model-list --search=gpt +``` + +Limit models to currently warm models: + +```bash +php huggingface/_model.php ai:huggingface:model-list --warm +``` + +You can combine task and provider filters, task and warm filters, but not provider and warm filters. + +```bash +# Combine provider and task: +php huggingface/_model.php ai:huggingface:model-list --provider=hf-inference --task=object-detection + +# Combine task and warm: +php huggingface/_model.php ai:huggingface:model-list --task=object-detection --warm + +# Search for warm gpt model for text-generation: +php huggingface/_model.php ai:huggingface:model-list --warm --task=text-generation --search=gpt +``` + +### Model Information + +To get detailed information about a specific model, you can use the `huggingface/_model-info.php` script: + +```bash +php huggingface/_model.php ai:huggingface:model-info google/vit-base-patch16-224 + +Hugging Face Model Information +============================== + + Model: google/vit-base-patch16-224 + ----------- ----------------------------- + ID google/vit-base-patch16-224 + Downloads 2985836 + Likes 889 + Task image-classification + Warm yes + ----------- ----------------------------- + + Inference Provider: + ----------------- ----------------------------- + Provider hf-inference + Status live + Provider ID google/vit-base-patch16-224 + Task image-classification + Is Model Author no + ----------------- ----------------------------- +``` + +Important to understand is what you can use a model for and its availability on different providers. diff --git a/examples/huggingface/_model-listing.php b/examples/huggingface/_model-listing.php deleted file mode 100644 index e9b314026..000000000 --- a/examples/huggingface/_model-listing.php +++ /dev/null @@ -1,48 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -use Symfony\AI\Platform\Bridge\HuggingFace\ApiClient; -use Symfony\AI\Platform\Model; -use Symfony\Component\Console\Command\Command; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Input\InputOption; -use Symfony\Component\Console\Output\OutputInterface; -use Symfony\Component\Console\SingleCommandApplication; -use Symfony\Component\Console\Style\SymfonyStyle; - -require_once dirname(__DIR__).'/bootstrap.php'; - -$app = (new SingleCommandApplication('HuggingFace Model Listing')) - ->setDescription('Lists all available models on HuggingFace') - ->addOption('provider', 'p', InputOption::VALUE_REQUIRED, 'Name of the inference provider to filter models by') - ->addOption('task', 't', InputOption::VALUE_REQUIRED, 'Name of the task to filter models by') - ->setCode(function (InputInterface $input, OutputInterface $output) { - $io = new SymfonyStyle($input, $output); - $io->title('HuggingFace Model Listing'); - - $provider = $input->getOption('provider'); - $task = $input->getOption('task'); - - $models = (new ApiClient())->models($provider, $task); - - if (0 === count($models)) { - $io->error('No models found for the given provider and task.'); - - return Command::FAILURE; - } - - $io->listing( - array_map(fn (Model $model) => $model->getName(), $models) - ); - - return Command::SUCCESS; - }) - ->run(); diff --git a/examples/huggingface/_model.php b/examples/huggingface/_model.php new file mode 100644 index 000000000..b1639fd81 --- /dev/null +++ b/examples/huggingface/_model.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Symfony\AI\Platform\Bridge\HuggingFace\ApiClient; +use Symfony\AI\Platform\Bridge\HuggingFace\Command\ModelInfoCommand; +use Symfony\AI\Platform\Bridge\HuggingFace\Command\ModelListCommand; +use Symfony\Component\Console\Application; + +require_once dirname(__DIR__).'/bootstrap.php'; + +$apiClient = new ApiClient(http_client()); + +$app = new Application('Hugging Face Model Commands'); +$app->addCommands([ + new ModelListCommand($apiClient), + new ModelInfoCommand($apiClient), +]); + +$app->run(); diff --git a/examples/huggingface/object-detection.php b/examples/huggingface/object-detection.php index 86512ef32..b9242c6d9 100644 --- a/examples/huggingface/object-detection.php +++ b/examples/huggingface/object-detection.php @@ -17,7 +17,7 @@ $platform = PlatformFactory::create(env('HUGGINGFACE_KEY'), httpClient: http_client()); -$image = Image::fromFile(dirname(__DIR__, 2).'/fixtures/image.jpg'); +$image = Image::fromFile(dirname(__DIR__, 2).'/fixtures/accordion.jpg'); $result = $platform->invoke('facebook/detr-resnet-50', $image, [ 'task' => Task::OBJECT_DETECTION, ]); diff --git a/examples/huggingface/table-question-answering.php b/examples/huggingface/table-question-answering.php index bd9e1c9e8..56a12736b 100644 --- a/examples/huggingface/table-question-answering.php +++ b/examples/huggingface/table-question-answering.php @@ -19,12 +19,12 @@ $input = [ 'query' => 'select year where city = beijing', 'table' => [ - 'year' => [1896, 1900, 1904, 2004, 2008, 2012], + 'year' => ['1896', '1900', '1904', '2004', '2008', '2012'], 'city' => ['athens', 'paris', 'st. louis', 'athens', 'beijing', 'london'], ], ]; -$result = $platform->invoke('microsoft/tapex-base', $input, [ +$result = $platform->invoke('google/tapas-base-finetuned-wtq', $input, [ 'task' => Task::TABLE_QUESTION_ANSWERING, ]); diff --git a/examples/huggingface/text-generation.php b/examples/huggingface/text-generation.php index 1eb8343dc..4d218bb1e 100644 --- a/examples/huggingface/text-generation.php +++ b/examples/huggingface/text-generation.php @@ -16,7 +16,7 @@ $platform = PlatformFactory::create(env('HUGGINGFACE_KEY'), httpClient: http_client()); -$result = $platform->invoke('gpt2', 'The quick brown fox jumps over the lazy', [ +$result = $platform->invoke('katanemo/Arch-Router-1.5B', 'The quick brown fox jumps over the lazy', [ 'task' => Task::TEXT_GENERATION, ]); diff --git a/fixtures/README.md b/fixtures/README.md new file mode 100644 index 000000000..c0089ccc2 --- /dev/null +++ b/fixtures/README.md @@ -0,0 +1,8 @@ +# Fixture Licenses + +For testing multi-modal features, the repository contains binary media content, with the following owners and licenses: + +* `tests/Fixture/accordion.jpg`: Jefferson Lucena, Creative Commons, see [pexels.com](https://www.pexels.com/photo/man-playing-accordion-10153219/) +* `tests/Fixture/audio.mp3`: davidbain, Creative Commons, see [freesound.org](https://freesound.org/people/davidbain/sounds/136777/) +* `tests/Fixture/document.pdf`: Chem8240ja, Public Domain, see [Wikipedia](https://en.m.wikipedia.org/wiki/File:Re_example.pdf) +* `tests/Fixture/image.jpg`: Chris F., Creative Commons, see [pexels.com](https://www.pexels.com/photo/blauer-und-gruner-elefant-mit-licht-1680755/) diff --git a/fixtures/accordion.jpg b/fixtures/accordion.jpg new file mode 100644 index 000000000..bd1ba6800 Binary files /dev/null and b/fixtures/accordion.jpg differ diff --git a/src/platform/src/Bridge/HuggingFace/ApiClient.php b/src/platform/src/Bridge/HuggingFace/ApiClient.php index 97f0f384d..e683ce892 100644 --- a/src/platform/src/Bridge/HuggingFace/ApiClient.php +++ b/src/platform/src/Bridge/HuggingFace/ApiClient.php @@ -11,6 +11,7 @@ namespace Symfony\AI\Platform\Bridge\HuggingFace; +use Symfony\AI\Platform\Exception\RuntimeException; use Symfony\AI\Platform\Model; use Symfony\Component\HttpClient\HttpClient; use Symfony\Contracts\HttpClient\HttpClientInterface; @@ -27,17 +28,78 @@ public function __construct( } /** + * @return array{ + * id: string, + * downloads: int, + * likes: int, + * pipeline_tag: string|null, + * inference: string|null, + * inferenceProviderMapping: array|null, + * } + */ + public function getModel(string $modelId): array + { + $result = $this->httpClient->request('GET', 'https://huggingface.co/api/models/'.$modelId, [ + 'query' => [ + 'expand' => ['downloads', 'likes', 'pipeline_tag', 'inference', 'inferenceProviderMapping'], + ], + ]); + + $data = $result->toArray(false); + + if (isset($data['error'])) { + throw new RuntimeException(\sprintf('Error fetching model info for "%s": "%s"', $modelId, $data['error'])); + } + + return $data; + } + + /** + * @param ?string $provider Filter by inference provider (see Provider::*) + * @param ?string $task Filter by task (see Task::*) + * @param ?string $search Search term to filter models by + * @param bool $warm Filter for models with warm inference available + * * @return Model[] */ - public function models(?string $provider, ?string $task): array + public function getModels(?string $provider = null, ?string $task = null, ?string $search = null, bool $warm = false): array { $result = $this->httpClient->request('GET', 'https://huggingface.co/api/models', [ 'query' => [ 'inference_provider' => $provider, 'pipeline_tag' => $task, + 'search' => $search, + ...$warm ? ['inference' => 'warm'] : [], ], ]); - return array_map(fn (array $model) => new Model($model['id']), $result->toArray()); + $data = $result->toArray(false); + + if (isset($data['error'])) { + throw new RuntimeException(\sprintf('Error fetching models: "%s"', $data['error'])); + } + + return array_map($this->convertToModel(...), $data); + } + + /** + * @param array{ + * id: string, + * pipeline_tag?: string, + * } $data + */ + private function convertToModel(array $data): Model + { + return new Model( + $data['id'], + options: [ + 'tags' => isset($data['pipeline_tag']) ? [$data['pipeline_tag']] : [], + ], + ); } } diff --git a/src/platform/src/Bridge/HuggingFace/Command/ModelInfoCommand.php b/src/platform/src/Bridge/HuggingFace/Command/ModelInfoCommand.php new file mode 100644 index 000000000..56f36b5f0 --- /dev/null +++ b/src/platform/src/Bridge/HuggingFace/Command/ModelInfoCommand.php @@ -0,0 +1,69 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\AI\Platform\Bridge\HuggingFace\Command; + +use Symfony\AI\Platform\Bridge\HuggingFace\ApiClient; +use Symfony\Component\Console\Attribute\Argument; +use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Style\SymfonyStyle; + +#[AsCommand('ai:huggingface:model-info', 'Retrieves inference information about a model on Hugging Face')] +final class ModelInfoCommand +{ + public function __construct( + private readonly ApiClient $apiClient, + ) { + } + + public function __invoke( + SymfonyStyle $io, + #[Argument('Name of the model to get information about')] + string $model, + ): int { + $io->title('Hugging Face Model Information'); + + $info = $this->apiClient->getModel($model); + + $io->text(\sprintf('Model: %s', $model)); + + $io->horizontalTable( + ['ID', 'Downloads', 'Likes', 'Task', 'Warm'], + [[ + $info['id'], + $info['downloads'], + $info['likes'], + $info['pipeline_tag'], + ('warm' === ($info['inference'] ?? null)) ? 'yes' : 'no', + ]] + ); + + $io->text('Inference Provider:'); + if (!isset($info['inferenceProviderMapping']) || [] === $info['inferenceProviderMapping']) { + $io->text('No inference provider information available for this model.'); + $io->newLine(); + } else { + $io->horizontalTable( + ['Provider', 'Status', 'Provider ID', 'Task', 'Is Model Author'], + array_map(fn (string $provider, array $data) => [ + $provider, + $data['status'], + $data['providerId'], + $data['task'], + $data['isModelAuthor'] ? 'yes' : 'no', + ], array_keys($info['inferenceProviderMapping']), $info['inferenceProviderMapping']) + ); + } + + return Command::SUCCESS; + } +} diff --git a/src/platform/src/Bridge/HuggingFace/Command/ModelListCommand.php b/src/platform/src/Bridge/HuggingFace/Command/ModelListCommand.php new file mode 100644 index 000000000..d1030f678 --- /dev/null +++ b/src/platform/src/Bridge/HuggingFace/Command/ModelListCommand.php @@ -0,0 +1,60 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\AI\Platform\Bridge\HuggingFace\Command; + +use Symfony\AI\Platform\Bridge\HuggingFace\ApiClient; +use Symfony\AI\Platform\Model; +use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Attribute\Option; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Style\SymfonyStyle; + +#[AsCommand('ai:huggingface:model-list', 'Lists all available models on Hugging Face')] +final class ModelListCommand +{ + public function __construct( + private readonly ApiClient $apiClient, + ) { + } + + public function __invoke( + SymfonyStyle $io, + #[Option('Name of the inference provider to filter models by', 'provider', 'p')] + ?string $provider = null, + #[Option('Name of the task to filter models by', 'task', 't')] + ?string $task = null, + #[Option('Search term to filter models by', 'search', 's')] + ?string $search = null, + #[Option('Only list models that are "warm" (i.e. ready for inference without cold start)', 'warm', 'w')] + bool $warm = false, + ): int { + $io->title('Hugging Face Model Listing'); + + $models = $this->apiClient->getModels($provider, $task, $search, $warm); + + if (0 === \count($models)) { + $io->error('No models found for the given filters.'); + + return Command::FAILURE; + } + + $formatModel = function (Model $model) { + return \sprintf('%s [%s]', $model->getName(), implode(', ', $model->getOptions()['tags'] ?? [])); + }; + + $io->listing(array_map($formatModel, $models)); + + $io->success(\sprintf('Found %d model(s).', \count($models))); + + return Command::SUCCESS; + } +} diff --git a/src/platform/src/Bridge/HuggingFace/ModelClient.php b/src/platform/src/Bridge/HuggingFace/ModelClient.php index 10b2240fb..cb38a4cf6 100644 --- a/src/platform/src/Bridge/HuggingFace/ModelClient.php +++ b/src/platform/src/Bridge/HuggingFace/ModelClient.php @@ -42,20 +42,20 @@ public function supports(Model $model): bool */ public function request(Model $model, array|string $payload, array $options = []): RawHttpResult { - // Extract task from options if provided + $provider = $options['provider'] ?? $this->provider; $task = $options['task'] ?? null; - unset($options['task']); + unset($options['task'], $options['provider']); - return new RawHttpResult($this->httpClient->request('POST', $this->getUrl($model, $task), [ + return new RawHttpResult($this->httpClient->request('POST', $this->getUrl($model, $provider, $task), [ 'auth_bearer' => $this->apiKey, ...$this->getPayload($payload, $options), ])); } - private function getUrl(Model $model, ?string $task): string + private function getUrl(Model $model, string $provider, ?string $task): string { $endpoint = Task::FEATURE_EXTRACTION === $task ? 'pipeline/feature-extraction' : 'models'; - $url = \sprintf('https://router.huggingface.co/%s/%s/%s', $this->provider, $endpoint, $model->getName()); + $url = \sprintf('https://router.huggingface.co/%s/%s/%s', $provider, $endpoint, $model->getName()); if (Task::CHAT_COMPLETION === $task) { $url .= '/v1/chat/completions'; diff --git a/src/platform/src/Bridge/HuggingFace/Output/TableQuestionAnsweringResult.php b/src/platform/src/Bridge/HuggingFace/Output/TableQuestionAnsweringResult.php index e6d024d75..ec7a6687e 100644 --- a/src/platform/src/Bridge/HuggingFace/Output/TableQuestionAnsweringResult.php +++ b/src/platform/src/Bridge/HuggingFace/Output/TableQuestionAnsweringResult.php @@ -17,25 +17,33 @@ final class TableQuestionAnsweringResult { /** - * @param array $cells - * @param array $aggregator + * @param array{0: int, 1: int}[] $coordinates + * @param array $cells + * @param array|string|null $aggregator */ public function __construct( public readonly string $answer, + public readonly array $coordinates = [], public readonly array $cells = [], - public readonly array $aggregator = [], + public readonly array|string|null $aggregator = null, ) { } /** - * @param array{answer: string, cells?: array, aggregator?: array} $data + * @param array{ + * answer: string, + * coordinates?: array{0: int, 1: int}[], + * cells?: array, + * aggregator?: array + * } $data */ public static function fromArray(array $data): self { return new self( $data['answer'], + $data['coordinates'] ?? [], $data['cells'] ?? [], - $data['aggregator'] ?? [], + $data['aggregator'] ?? null, ); } } diff --git a/src/platform/src/Bridge/HuggingFace/Provider.php b/src/platform/src/Bridge/HuggingFace/Provider.php index 2a24ea9fd..2538b4926 100644 --- a/src/platform/src/Bridge/HuggingFace/Provider.php +++ b/src/platform/src/Bridge/HuggingFace/Provider.php @@ -12,6 +12,9 @@ namespace Symfony\AI\Platform\Bridge\HuggingFace; /** + * Based on the list of supported providers at + * https://huggingface.co/docs/inference-providers/index. + * * @author Christopher Hertel */ interface Provider @@ -19,12 +22,19 @@ interface Provider public const CEREBRAS = 'cerebras'; public const COHERE = 'cohere'; public const FAL_AI = 'fal-ai'; + public const FEATHERLESS_AI = 'featherless-ai'; public const FIREWORKS = 'fireworks-ai'; - public const HYPERBOLIC = 'hyperbolic'; + public const GROQ = 'groq'; public const HF_INFERENCE = 'hf-inference'; + public const HYPERBOLIC = 'hyperbolic'; public const NEBIUS = 'nebius'; public const NOVITA = 'novita'; + public const NSCALE = 'nscale'; + public const PUBLIC_AI = 'publicai'; public const REPLICATE = 'replicate'; public const SAMBA_NOVA = 'sambanova'; + public const SCALEWAY = 'scaleway'; public const TOGETHER = 'together'; + public const WAVE_SPEED_AI = 'wavespeed'; + public const Z_AI = 'zai-org'; } diff --git a/src/platform/src/Bridge/HuggingFace/README.md b/src/platform/src/Bridge/HuggingFace/README.md new file mode 100644 index 000000000..20270a484 --- /dev/null +++ b/src/platform/src/Bridge/HuggingFace/README.md @@ -0,0 +1,196 @@ +# HuggingFace Bridge + +The HuggingFace bridge provides integration with the [HuggingFace Inference API](https://huggingface.co/docs/inference-api/index), +enabling access to thousands of pre-trained models for various AI tasks including natural language processing, computer +vision, audio processing, and more. + +## Features + +- **Multi-Provider Support**: Access models through multiple inference providers (HuggingFace Inference, Cerebras, Cohere, Groq, Together, and others) +- **Comprehensive Task Support**: Support for 40+ different AI tasks including: + - Chat completion and text generation + - Image classification, object detection, and segmentation + - Automatic speech recognition and audio classification + - Text classification, translation, summarization + - Feature extraction and embeddings + - And many more... +- **Model Discovery**: Built-in API client to discover and query available models +- **Flexible Input/Output**: Support for text, images, audio, and binary data +- **Type-Safe Results**: Structured result objects for each task type + +## Installation + +The bridge is included in the `symfony/ai-platform` package. Ensure you have the required dependencies: + +```bash +composer require symfony/ai-platform +``` + +## Quick Start + +### Initialize the Platform + +```php +use Symfony\AI\Platform\Bridge\HuggingFace\PlatformFactory; +use Symfony\AI\Platform\Bridge\HuggingFace\Provider; + +$platform = PlatformFactory::create( + apiKey: 'hf_your_api_key_here', + provider: Provider::CEREBRAS, // or other providers + httpClient: $httpClient // optional, uses default if not provided +); +``` + +### Chat Completion Example + +```php +use Symfony\AI\Platform\Bridge\HuggingFace\Task; +use Symfony\AI\Platform\Message\Message; +use Symfony\AI\Platform\Message\MessageBag; + +$messages = new MessageBag( + Message::ofUser('Hello, how are you doing today?') +); + +$result = $platform->invoke('HuggingFaceH4/zephyr-7b-beta', $messages, [ + 'task' => Task::CHAT_COMPLETION, +]); + +echo $result->asText(); +``` + +### Image Classification Example + +```php +use Symfony\AI\Platform\Bridge\HuggingFace\Task; +use Symfony\AI\Platform\Message\Content\Image; + +$image = Image::fromFile('/path/to/image.jpg'); + +$result = $platform->invoke('google/vit-base-patch16-224', $image, [ + 'task' => Task::IMAGE_CLASSIFICATION, +]); + +$classifications = $result->asObject(); +foreach ($classifications as $classification) { + echo $classification->label . ': ' . $classification->score . PHP_EOL; +} +``` + +### Feature Extraction (Embeddings) + +```php +$result = $platform->invoke('sentence-transformers/all-MiniLM-L6-v2', 'Hello world', [ + 'task' => Task::FEATURE_EXTRACTION, +]); + +$vector = $result->asVector(); +``` + +## Providers + +The bridge supports multiple inference providers. For a complete list of available providers and their constants, see +the [`Provider` interface](./Provider.php). + +Specify a provider when creating the platform: + +```php +use Symfony\AI\Platform\Bridge\HuggingFace\Provider; + +$platform = PlatformFactory::create( + apiKey: 'your_api_key', + provider: Provider::GROQ, +); +``` + +## Supported Tasks + +The bridge supports 40+ AI tasks across multiple categories including: + +- **Natural Language Processing**: Chat completion, text generation, classification, translation, summarization, embeddings, and more +- **Computer Vision**: Image classification, object detection, segmentation, depth estimation, and more +- **Audio**: Speech recognition, audio classification, text-to-speech, and more +- **Multimodal**: Visual question answering, document understanding, and more + +For the complete list of supported tasks and their constants, see the [`Task` interface](./Task.php). + +## Model Discovery + +The bridge includes commands to discover available models without using inference credits. They help to research and use +models, and can be registered in a Symfony Console application. + +### Command-Line Interface + +The HuggingFace bridge provides two console commands for model discovery: + +#### List Models + +List available models with optional filtering: + +```bash +ai:huggingface:model-list [options] +``` + +Options: +- `--provider, -p`: Filter by inference provider (e.g., `inference`) +- `--task, -t`: Filter by task type (e.g., `text-generation`) +- `--search, -s`: Search term to filter models +- `--warm, -w`: Only list models with warm inference (ready without cold start) + +Examples: +```bash +# List all text generation models +ai:huggingface:model-list --task=text-generation + +# List warm models for a specific provider +ai:huggingface:model-list --provider=hf-inference --warm + +# Search for specific models +ai:huggingface:model-list --search=llama +``` + +#### Get Model Information + +Retrieve detailed information about a specific model: + +```bash +ai:huggingface:model-info +``` + +Examples: +```bash +# Get information about a specific model +ai:huggingface:model-info meta-llama/Llama-2-7b-chat +ai:huggingface:model-info gpt2 +``` + +Output includes: +- Model ID, downloads, and community likes +- Task type (pipeline tag) +- Inference status (warm/cold) +- Inference provider mappings and availability + +## Options and Configuration + +When invoking the platform, you can pass task-specific options: + +```php +$result = $platform->invoke('model/id', $input, [ + 'task' => Task::TEXT_GENERATION, + 'temperature' => 0.7, + 'top_p' => 0.9, + 'top_k' => 50, + 'max_new_tokens' => 256, + 'repetition_penalty' => 1.2, + // ... other provider-specific parameters +]); +``` + +You can also override the provider per request: + +```php +$result = $platform->invoke('model/id', $input, [ + 'task' => Task::CHAT_COMPLETION, + 'provider' => Provider::GROQ, // Override default provider +]); +``` diff --git a/src/platform/src/Bridge/HuggingFace/ResultConverter.php b/src/platform/src/Bridge/HuggingFace/ResultConverter.php index ac63843ea..0af31d68e 100644 --- a/src/platform/src/Bridge/HuggingFace/ResultConverter.php +++ b/src/platform/src/Bridge/HuggingFace/ResultConverter.php @@ -56,7 +56,7 @@ public function convert(RawResultInterface|RawHttpResult $result, array $options $headers = $httpResponse->getHeaders(false); $contentType = $headers['content-type'][0] ?? null; - $content = 'application/json' === $contentType ? $httpResponse->toArray(false) : $httpResponse->getContent(false); + $content = str_contains($contentType, 'application/json') ? $httpResponse->toArray(false) : $httpResponse->getContent(false); if (str_starts_with((string) $httpResponse->getStatusCode(), '4')) { $message = match (true) { diff --git a/src/platform/src/Bridge/HuggingFace/Task.php b/src/platform/src/Bridge/HuggingFace/Task.php index e2be0049c..ef6ad42a5 100644 --- a/src/platform/src/Bridge/HuggingFace/Task.php +++ b/src/platform/src/Bridge/HuggingFace/Task.php @@ -12,27 +12,74 @@ namespace Symfony\AI\Platform\Bridge\HuggingFace; /** + * Based on the facets listed at https://huggingface.co/models. + * * @author Christopher Hertel */ interface Task { - public const AUDIO_CLASSIFICATION = 'audio-classification'; - public const AUTOMATIC_SPEECH_RECOGNITION = 'automatic-speech-recognition'; - public const CHAT_COMPLETION = 'chat-completion'; - public const FEATURE_EXTRACTION = 'feature-extraction'; - public const FILL_MASK = 'fill-mask'; + // Multimodal + public const AUDIO_TEXT_TO_TEXT = 'audio-text-to-text'; + public const IMAGE_TEXT_TO_TEXT = 'image-text-to-text'; + public const VISUAL_QUESTION_ANSWERING = 'visual-question-answering'; + public const DOCUMENT_QUESTION_ANSWERING = 'document-question-answering'; + public const VIDEO_TEXT_TO_TEXT = 'video-text-to-text'; + public const VISUAL_DOCUMENT_RETRIEVAL = 'visual-document-retrieval'; + public const ANY_TO_ANY = 'any-to-any'; + + // Computer Vision + public const DEPTH_ESTIMATION = 'depth-estimation'; public const IMAGE_CLASSIFICATION = 'image-classification'; + public const OBJECT_DETECTION = 'object-detection'; public const IMAGE_SEGMENTATION = 'image-segmentation'; + public const TEXT_TO_IMAGE = 'text-to-image'; public const IMAGE_TO_TEXT = 'image-to-text'; - public const OBJECT_DETECTION = 'object-detection'; - public const QUESTION_ANSWERING = 'question-answering'; - public const SENTENCE_SIMILARITY = 'sentence-similarity'; - public const SUMMARIZATION = 'summarization'; - public const TABLE_QUESTION_ANSWERING = 'table-question-answering'; + public const IMAGE_TO_IMAGE = 'image-to-image'; + public const IMAGE_TO_VIDEO = 'image-to-video'; + public const UNCONDITIONAL_IMAGE_GENERATION = 'unconditional-image-generation'; + public const VIDEO_CLASSIFICATION = 'video-classification'; + public const TEXT_TO_VIDEO = 'text-to-video'; + public const ZERO_SHOT_IMAGE_CLASSIFICATION = 'zero-shot-image-classification'; + public const MASK_GENERATION = 'mask-generation'; + public const ZERO_SHOT_OBJECT_DETECTION = 'zero-shot-object-detection'; + public const TEXT_TO_3D = 'text-to-3d'; + public const IMAGE_TO_3D = 'image-to-3d'; + public const IMAGE_FEATURE_EXTRACTION = 'image-feature-extraction'; + public const KEYPOINT_DETECTION = 'keypoint-detection'; + public const VIDEO_TO_VIDEO = 'video-to-video'; + + // Natural Language Processing public const TEXT_CLASSIFICATION = 'text-classification'; - public const TEXT_GENERATION = 'text-generation'; - public const TEXT_TO_IMAGE = 'text-to-image'; public const TOKEN_CLASSIFICATION = 'token-classification'; - public const TRANSLATION = 'translation'; + public const TABLE_QUESTION_ANSWERING = 'table-question-answering'; + public const QUESTION_ANSWERING = 'question-answering'; public const ZERO_SHOT_CLASSIFICATION = 'zero-shot-classification'; + public const TRANSLATION = 'translation'; + public const SUMMARIZATION = 'summarization'; + public const FEATURE_EXTRACTION = 'feature-extraction'; + public const TEXT_GENERATION = 'text-generation'; + public const FILL_MASK = 'fill-mask'; + public const SENTENCE_SIMILARITY = 'sentence-similarity'; + public const TEXT_RANKING = 'text-ranking'; + + // Audio + public const TEXT_TO_SPEECH = 'text-to-speech'; + public const TEXT_TO_AUDIO = 'text-to-audio'; + public const AUTOMATIC_SPEECH_RECOGNITION = 'automatic-speech-recognition'; + public const AUDIO_TO_AUDIO = 'audio-to-audio'; + public const AUDIO_CLASSIFICATION = 'audio-classification'; + public const VOICE_ACTIVITY_DETECTION = 'voice-activity-detection'; + + // Tabular + public const TABULAR_CLASSIFICATION = 'tabular-classification'; + public const TABULAR_REGRESSION = 'tabular-regression'; + public const TIME_SERIES_FORECASTING = 'time-series-forecasting'; + + // Reinforcement Learning + public const REINFORCEMENT_LEARNING = 'reinforcement-learning'; + public const ROBOTICS = 'robotics'; + + // Other + public const GRAPH_MACHINE_LEARNING = 'graph-machine-learning'; + public const CHAT_COMPLETION = 'chat-completion'; } diff --git a/src/platform/tests/Bridge/HuggingFace/ApiClientTest.php b/src/platform/tests/Bridge/HuggingFace/ApiClientTest.php index 9a58cccba..4ef902cfa 100644 --- a/src/platform/tests/Bridge/HuggingFace/ApiClientTest.php +++ b/src/platform/tests/Bridge/HuggingFace/ApiClientTest.php @@ -35,7 +35,7 @@ public function testModelsWithProviderAndTask() $httpClient = new MockHttpClient(new JsonMockResponse($responseData)); $apiClient = new ApiClient($httpClient); - $models = $apiClient->models('test-provider', 'text-generation'); + $models = $apiClient->getModels('test-provider', 'text-generation'); $this->assertCount(3, $models); $this->assertInstanceOf(Model::class, $models[0]); @@ -55,7 +55,7 @@ public function testModelsWithNullProviderAndTask() $httpClient = new MockHttpClient(new JsonMockResponse($responseData)); $apiClient = new ApiClient($httpClient); - $models = $apiClient->models(null, null); + $models = $apiClient->getModels(); $this->assertCount(2, $models); $this->assertInstanceOf(Model::class, $models[0]); @@ -71,7 +71,7 @@ public function testModelsWithEmptyResponse() $httpClient = new MockHttpClient(new JsonMockResponse($responseData)); $apiClient = new ApiClient($httpClient); - $models = $apiClient->models('test-provider', 'text-generation'); + $models = $apiClient->getModels('test-provider', 'text-generation'); $this->assertCount(0, $models); } @@ -90,7 +90,7 @@ public function testModelsRequestParameters() }); $apiClient = new ApiClient($httpClient); - $apiClient->models('test-provider', 'text-generation'); + $apiClient->getModels('test-provider', 'text-generation'); } #[TestDox('Sends correct HTTP request with null provider and task parameters')] @@ -107,6 +107,6 @@ public function testModelsRequestParametersWithNullValues() }); $apiClient = new ApiClient($httpClient); - $apiClient->models(null, null); + $apiClient->getModels(); } } diff --git a/src/platform/tests/Bridge/HuggingFace/Output/TableQuestionAnsweringResultTest.php b/src/platform/tests/Bridge/HuggingFace/Output/TableQuestionAnsweringResultTest.php index 84c2d2236..5f0d3e92c 100644 --- a/src/platform/tests/Bridge/HuggingFace/Output/TableQuestionAnsweringResultTest.php +++ b/src/platform/tests/Bridge/HuggingFace/Output/TableQuestionAnsweringResultTest.php @@ -27,34 +27,38 @@ public function testConstruction() $result = new TableQuestionAnsweringResult('Paris'); $this->assertSame('Paris', $result->answer); + $this->assertSame([], $result->coordinates); $this->assertSame([], $result->cells); - $this->assertSame([], $result->aggregator); + $this->assertNull($result->aggregator); } #[TestDox('Construction with all parameters creates valid instance')] public function testConstructionWithAllParameters() { + $coordinates = [[0, 1]]; $cells = ['cell1', 'cell2', 42]; $aggregator = ['SUM', 'AVERAGE']; - $result = new TableQuestionAnsweringResult('Total is 100', $cells, $aggregator); + $result = new TableQuestionAnsweringResult('Total is 100', $coordinates, $cells, $aggregator); $this->assertSame('Total is 100', $result->answer); + $this->assertSame($coordinates, $result->coordinates); $this->assertSame($cells, $result->cells); $this->assertSame($aggregator, $result->aggregator); } #[TestDox('Constructor accepts various parameter combinations')] - #[TestWith(['Yes', [], []])] - #[TestWith(['No', ['A1'], ['COUNT']])] - #[TestWith(['42.5', ['A1', 'B1', 42], ['SUM', 'AVERAGE']])] - #[TestWith(['', [], []])] - #[TestWith(['Complex answer with multiple words', [1, 2, 3], ['NONE']])] - public function testConstructorWithDifferentValues(string $answer, array $cells, array $aggregator) + #[TestWith(['Yes', [], [], []])] + #[TestWith(['No', [[0, 1]], ['A1'], ['COUNT']])] + #[TestWith(['42.5', [[0, 1]], ['A1', 'B1', 42], ['SUM', 'AVERAGE']])] + #[TestWith(['', [[0, 1]], [], []])] + #[TestWith(['Complex answer with multiple words', [[0, 1]], [1, 2, 3], ['NONE']])] + public function testConstructorWithDifferentValues(string $answer, array $coordinates, array $cells, array $aggregator) { - $result = new TableQuestionAnsweringResult($answer, $cells, $aggregator); + $result = new TableQuestionAnsweringResult($answer, $coordinates, $cells, $aggregator); $this->assertSame($answer, $result->answer); + $this->assertSame($coordinates, $result->coordinates); $this->assertSame($cells, $result->cells); $this->assertSame($aggregator, $result->aggregator); } @@ -67,8 +71,9 @@ public function testFromArrayWithRequiredField() $result = TableQuestionAnsweringResult::fromArray($data); $this->assertSame('Berlin', $result->answer); + $this->assertSame([], $result->coordinates); $this->assertSame([], $result->cells); - $this->assertSame([], $result->aggregator); + $this->assertNull($result->aggregator); } #[TestDox('fromArray creates instance with all fields')] @@ -76,6 +81,7 @@ public function testFromArrayWithAllFields() { $data = [ 'answer' => 'The result is 150', + 'coordinates' => [[0, 0], [1, 1]], 'cells' => ['A1', 'B2', 100, 50], 'aggregator' => ['SUM'], ]; @@ -83,11 +89,13 @@ public function testFromArrayWithAllFields() $result = TableQuestionAnsweringResult::fromArray($data); $this->assertSame('The result is 150', $result->answer); + $this->assertSame([[0, 0], [1, 1]], $result->coordinates); $this->assertSame(['A1', 'B2', 100, 50], $result->cells); $this->assertSame(['SUM'], $result->aggregator); } #[TestDox('fromArray handles optional fields with default values')] + #[TestWith([['answer' => 'Test', 'coordinates' => [[0, 0], [1, 1]]]])] #[TestWith([['answer' => 'Test', 'cells' => ['A1', 'B1']]])] #[TestWith([['answer' => 'Test', 'aggregator' => ['COUNT']]])] #[TestWith([['answer' => 'Test', 'cells' => [1, 2], 'aggregator' => ['SUM', 'AVG']]])] @@ -96,8 +104,9 @@ public function testFromArrayWithOptionalFields(array $data) $result = TableQuestionAnsweringResult::fromArray($data); $this->assertSame($data['answer'], $result->answer); + $this->assertSame($data['coordinates'] ?? [], $result->coordinates); $this->assertSame($data['cells'] ?? [], $result->cells); - $this->assertSame($data['aggregator'] ?? [], $result->aggregator); + $this->assertSame($data['aggregator'] ?? null, $result->aggregator); } #[TestDox('fromArray handles various cell data types')] @@ -137,13 +146,15 @@ public function testFromArrayWithVariousAggregatorFormats(array $data) #[TestDox('Empty arrays are handled correctly')] public function testEmptyArrays() { - $result1 = new TableQuestionAnsweringResult('answer', [], []); + $result1 = new TableQuestionAnsweringResult('answer', [], [], []); $result2 = TableQuestionAnsweringResult::fromArray(['answer' => 'test']); + $this->assertSame([], $result1->coordinates); $this->assertSame([], $result1->cells); $this->assertSame([], $result1->aggregator); + $this->assertSame([], $result2->coordinates); $this->assertSame([], $result2->cells); - $this->assertSame([], $result2->aggregator); + $this->assertNull($result2->aggregator); } #[TestDox('Large cell arrays are handled correctly')] @@ -155,7 +166,7 @@ public function testLargeCellArrays() $largeCells[] = $i; } - $result = new TableQuestionAnsweringResult('Large table result', $largeCells, ['COUNT']); + $result = new TableQuestionAnsweringResult('Large table result', [], $largeCells, ['COUNT']); $this->assertCount(200, $result->cells); $this->assertSame('cell_0', $result->cells[0]); diff --git a/src/platform/tests/Bridge/HuggingFace/PlatformFactoryTest.php b/src/platform/tests/Bridge/HuggingFace/PlatformFactoryTest.php index d797b66eb..559681211 100644 --- a/src/platform/tests/Bridge/HuggingFace/PlatformFactoryTest.php +++ b/src/platform/tests/Bridge/HuggingFace/PlatformFactoryTest.php @@ -54,14 +54,21 @@ public function testCreateWithEventSourceHttpClient() #[TestWith([Provider::CEREBRAS])] #[TestWith([Provider::COHERE])] #[TestWith([Provider::FAL_AI])] + #[TestWith([Provider::FEATHERLESS_AI])] #[TestWith([Provider::FIREWORKS])] - #[TestWith([Provider::HYPERBOLIC])] + #[TestWith([Provider::GROQ])] #[TestWith([Provider::HF_INFERENCE])] + #[TestWith([Provider::HYPERBOLIC])] #[TestWith([Provider::NEBIUS])] #[TestWith([Provider::NOVITA])] + #[TestWith([Provider::NSCALE])] + #[TestWith([Provider::PUBLIC_AI])] #[TestWith([Provider::REPLICATE])] #[TestWith([Provider::SAMBA_NOVA])] + #[TestWith([Provider::SCALEWAY])] #[TestWith([Provider::TOGETHER])] + #[TestWith([Provider::WAVE_SPEED_AI])] + #[TestWith([Provider::Z_AI])] public function testCreateWithDifferentProviders(string $provider) { $platform = PlatformFactory::create('test-api-key', $provider);