Skip to content

Commit f982e57

Browse files
committed
Simplify model configuration by defaulting to platform-specific model classes
This PR revolutionizes model configuration in the Symfony AI bundle by simplifying the way models are configured and eliminating the need for explicit class definitions in most use cases. The changes make model configuration more intuitive and maintainable while providing full backwards compatibility. ## Key Changes ### 1. Bridge-Specific ModelCatalogs with Auto-Discovery - **Added platform-specific ModelCatalog classes** for OpenAI, Anthropic, Mistral, Perplexity, Gemini, and Cerebras - **Automatic model class inference** based on platform and capabilities - **Eliminates need for explicit class configuration** in most scenarios - Each platform now provides its own model catalog with predefined capabilities ### 2. Enhanced Model Configuration Syntax - **Direct model string support**: `model: 'gpt-4o-mini?temperature=0.5&max_tokens=1000'` - **Query parameter parsing** embedded directly in model names - **Traditional name/options structure** still supported for backwards compatibility - **Validation prevents conflicting configurations** (query string + options object) ### 3. Core Platform Infrastructure Updates - **New `PlatformInterface::getModelCatalog()` method** for accessing platform model catalogs - **Enhanced `AbstractModelCatalog`** with validation for empty model names - **New `AutoDiscoveryModelCatalog`** for automatic capability detection - **Updated platform factories** to create bridge-specific model catalogs ### 4. Bundle Integration Improvements - **Updated AI Bundle configuration** (`config/options.php`) to support direct model strings - **Enhanced bundle class** (`AiBundle.php`) to handle new model configuration patterns - **Removed hardcoded model class assumptions** throughout the bundle ### 5. Testing & Examples - **Updated 40+ test files** to use Model objects instead of raw strings - **Enhanced InputProcessor tests** to validate new configuration patterns - **Added comprehensive test coverage** for all new ModelCatalog implementations - **Updated agent tests** to use proper Model instances ## Before/After Configuration Examples ### Before (Old Configuration) ```yaml ai: model: openai: custom-gpt-4: class: OpenAi\Gpt capabilities: - input_messages - output_text - tool_calling ``` ### After (New Configuration) ```yaml # Using direct model string with query parameters ai: agent: my_agent: platform: openai model: 'gpt-4o-mini?temperature=0.5&max_tokens=1000' # Using traditional name/options structure ai: agent: my_agent: platform: openai model: name: gpt-4o-mini options: temperature: 0.5 max_tokens: 1000 # Custom model with capabilities (using enum syntax) ai: model: openai: custom-gpt-4: capabilities: - !php/const Symfony\AI\Platform\Capability::INPUT_MESSAGES - !php/const Symfony\AI\Platform\Capability::OUTPUT_TEXT - !php/const Symfony\AI\Platform\Capability::TOOL_CALLING agent: my_agent: platform: openai model: 'custom-gpt-4?temperature=0.3' ``` ## Benefits 1. **Dramatically Simplified Configuration**: No more need to specify model classes for standard platform models 2. **Enhanced Developer Experience**: Intuitive query parameter syntax for model options 3. **Type Safety**: Enum constants ensure valid capability values 4. **Automatic Model Discovery**: Platform-specific catalogs automatically infer appropriate model classes 5. **Validation**: Built-in prevention of conflicting configuration patterns 6. **Full Backwards Compatibility**: Existing configurations continue to work unchanged ## Technical Implementation - **ModelCatalog Interface**: Each platform now implements its own model catalog - **Query String Parser**: Parses model options from URL-style query parameters - **Capability Auto-Detection**: Automatically determines model capabilities based on platform standards - **Configuration Validation**: Prevents invalid combinations of configuration approaches - **Seamless Migration**: Existing model configurations work without modification This change significantly reduces configuration complexity while maintaining the full power and flexibility of the Symfony AI platform integration.
1 parent 83058fb commit f982e57

File tree

187 files changed

+14216
-1787
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

187 files changed

+14216
-1787
lines changed

PR_CUSTOM_MODEL_EXAMPLE.md

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
# Custom Model Configuration Examples
2+
3+
## Before (Old Configuration)
4+
5+
```yaml
6+
ai:
7+
model:
8+
openai:
9+
custom-gpt-4:
10+
class: OpenAi\Gpt
11+
capabilities:
12+
- input_messages
13+
- output_text
14+
- output_streaming
15+
- tool_calling
16+
- output_structured
17+
```
18+
19+
## After (New Configuration)
20+
21+
### Using Direct Model String with Query Parameters
22+
23+
```yaml
24+
ai:
25+
agent:
26+
my_agent:
27+
platform: openai
28+
model: 'gpt-4o-mini?temperature=0.5&max_tokens=1000'
29+
```
30+
31+
### Using Traditional Name/Options Structure
32+
33+
```yaml
34+
ai:
35+
agent:
36+
my_agent:
37+
platform: openai
38+
model:
39+
name: gpt-4o-mini
40+
options:
41+
temperature: 0.5
42+
max_tokens: 1000
43+
```
44+
45+
### Custom Model with Capabilities (Using Enum Syntax)
46+
47+
```yaml
48+
ai:
49+
model:
50+
openai:
51+
custom-gpt-4:
52+
capabilities:
53+
- !php/const Symfony\AI\Platform\Capability::INPUT_MESSAGES
54+
- !php/const Symfony\AI\Platform\Capability::OUTPUT_TEXT
55+
- !php/const Symfony\AI\Platform\Capability::OUTPUT_STREAMING
56+
- !php/const Symfony\AI\Platform\Capability::TOOL_CALLING
57+
- !php/const Symfony\AI\Platform\Capability::OUTPUT_STRUCTURED
58+
59+
agent:
60+
my_agent:
61+
platform: openai
62+
model: 'custom-gpt-4?temperature=0.3'
63+
```
64+
65+
### Vectorizer Configuration
66+
67+
```yaml
68+
ai:
69+
vectorizer:
70+
my_vectorizer:
71+
platform: openai
72+
model: 'text-embedding-3-small?dimensions=512'
73+
```
74+
75+
## Key Improvements
76+
77+
1. **Shorter Syntax**: Direct model strings eliminate nested configuration
78+
2. **Query Parameters**: Model options can be embedded directly in the model name
79+
3. **Type Safety**: Enum constants ensure valid capability values
80+
4. **Auto-Discovery**: Model classes are automatically inferred from capabilities
81+
5. **Validation**: Conflicting configuration (query string + options) is prevented

src/agent/src/Agent.php

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
use Symfony\AI\Agent\Exception\MissingModelSupportException;
1818
use Symfony\AI\Agent\Exception\RuntimeException;
1919
use Symfony\AI\Platform\Capability;
20-
use Symfony\AI\Platform\Message\MessageBagInterface;
20+
use Symfony\AI\Platform\Message\MessageBag;
2121
use Symfony\AI\Platform\Model;
2222
use Symfony\AI\Platform\PlatformInterface;
2323
use Symfony\AI\Platform\Result\ResultInterface;
@@ -45,21 +45,37 @@
4545
*/
4646
public function __construct(
4747
private PlatformInterface $platform,
48-
private Model $model,
48+
/** @var non-empty-string */
49+
private string $model,
4950
iterable $inputProcessors = [],
5051
iterable $outputProcessors = [],
52+
private string $name = 'agent',
5153
private LoggerInterface $logger = new NullLogger(),
5254
) {
5355
$this->inputProcessors = $this->initializeProcessors($inputProcessors, InputProcessorInterface::class);
5456
$this->outputProcessors = $this->initializeProcessors($outputProcessors, OutputProcessorInterface::class);
5557
}
5658

59+
public function getModel(): Model
60+
{
61+
return $this->platform->getModel($this->model);
62+
}
63+
64+
public function getName(): string
65+
{
66+
return $this->name;
67+
}
68+
5769
/**
5870
* @param array<string, mixed> $options
71+
*
72+
* @throws MissingModelSupportException When the model doesn't support audio or image inputs present in the messages
73+
* @throws InvalidArgumentException When the platform returns a client error (4xx) indicating invalid request parameters
74+
* @throws RuntimeException When the platform returns a server error (5xx) or network failure occurs
5975
*/
60-
public function call(MessageBagInterface $messages, array $options = []): ResultInterface
76+
public function call(MessageBag $messages, array $options = []): ResultInterface
6177
{
62-
$input = new Input($this->model, $messages, $options);
78+
$input = new Input($this->getModel(), $messages, $options);
6379
array_map(fn (InputProcessorInterface $processor) => $processor->processInput($input), $this->inputProcessors);
6480

6581
$model = $input->model;
@@ -75,7 +91,7 @@ public function call(MessageBagInterface $messages, array $options = []): Result
7591
}
7692

7793
try {
78-
$result = $this->platform->invoke($model, $messages, $options)->getResult();
94+
$result = $this->platform->invoke($this->model, $messages, $options)->getResult();
7995
} catch (ClientExceptionInterface $e) {
8096
$message = $e->getMessage();
8197
$content = $e->getResponse()->toArray(false);
@@ -84,7 +100,7 @@ public function call(MessageBagInterface $messages, array $options = []): Result
84100

85101
throw new InvalidArgumentException('' === $message ? 'Invalid request to model or platform' : $message, previous: $e);
86102
} catch (HttpExceptionInterface $e) {
87-
throw new RuntimeException('Failed to request model', previous: $e);
103+
throw new RuntimeException('Failed to request model.', previous: $e);
88104
}
89105

90106
$output = new Output($model, $result, $messages, $options);
@@ -103,7 +119,7 @@ private function initializeProcessors(iterable $processors, string $interface):
103119
{
104120
foreach ($processors as $processor) {
105121
if (!$processor instanceof $interface) {
106-
throw new InvalidArgumentException(\sprintf('Processor %s must implement %s interface.', $processor::class, $interface));
122+
throw new InvalidArgumentException(\sprintf('Processor "%s" must implement "%s".', $processor::class, $interface));
107123
}
108124

109125
if ($processor instanceof AgentAwareInterface) {

src/agent/tests/InputProcessor/ModelOverrideInputProcessorTest.php

Lines changed: 21 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -13,60 +13,56 @@
1313

1414
use PHPUnit\Framework\Attributes\CoversClass;
1515
use PHPUnit\Framework\Attributes\Small;
16-
use PHPUnit\Framework\Attributes\Test;
1716
use PHPUnit\Framework\Attributes\UsesClass;
1817
use PHPUnit\Framework\TestCase;
1918
use Symfony\AI\Agent\Exception\InvalidArgumentException;
2019
use Symfony\AI\Agent\Input;
2120
use Symfony\AI\Agent\InputProcessor\ModelOverrideInputProcessor;
22-
use Symfony\AI\Platform\Bridge\Anthropic\Claude;
23-
use Symfony\AI\Platform\Bridge\OpenAI\Embeddings;
24-
use Symfony\AI\Platform\Bridge\OpenAI\GPT;
21+
use Symfony\AI\Platform\Capability;
2522
use Symfony\AI\Platform\Message\MessageBag;
23+
use Symfony\AI\Platform\Model;
2624

2725
#[CoversClass(ModelOverrideInputProcessor::class)]
28-
#[UsesClass(GPT::class)]
29-
#[UsesClass(Claude::class)]
3026
#[UsesClass(Input::class)]
3127
#[UsesClass(MessageBag::class)]
32-
#[UsesClass(Embeddings::class)]
28+
#[UsesClass(Model::class)]
3329
#[Small]
3430
final class ModelOverrideInputProcessorTest extends TestCase
3531
{
36-
#[Test]
37-
public function processInputWithValidModelOption(): void
32+
public function testProcessInputWithValidModelOption()
3833
{
39-
$gpt = new GPT();
40-
$claude = new Claude();
41-
$input = new Input($gpt, new MessageBag(), ['model' => $claude]);
34+
$originalModel = new Model('gpt-4o-mini', [Capability::INPUT_TEXT, Capability::OUTPUT_TEXT]);
35+
$overrideModel = new Model('claude-3-5-sonnet-20241022', [Capability::INPUT_TEXT, Capability::OUTPUT_TEXT]);
36+
37+
$input = new Input($originalModel, new MessageBag(), ['model' => $overrideModel]);
4238

4339
$processor = new ModelOverrideInputProcessor();
4440
$processor->processInput($input);
4541

46-
self::assertSame($claude, $input->model);
42+
$this->assertSame($overrideModel, $input->model);
43+
$this->assertSame('claude-3-5-sonnet-20241022', $input->model->getName());
4744
}
4845

49-
#[Test]
50-
public function processInputWithoutModelOption(): void
46+
public function testProcessInputWithoutModelOption()
5147
{
52-
$gpt = new GPT();
53-
$input = new Input($gpt, new MessageBag(), []);
48+
$originalModel = new Model('gpt-4o-mini', [Capability::INPUT_TEXT, Capability::OUTPUT_TEXT]);
49+
50+
$input = new Input($originalModel, new MessageBag());
5451

5552
$processor = new ModelOverrideInputProcessor();
5653
$processor->processInput($input);
5754

58-
self::assertSame($gpt, $input->model);
55+
$this->assertSame($originalModel, $input->model);
56+
$this->assertSame('gpt-4o-mini', $input->model->getName());
5957
}
6058

61-
#[Test]
62-
public function processInputWithInvalidModelOption(): void
59+
public function testProcessInputWithInvalidModelOption()
6360
{
64-
self::expectException(InvalidArgumentException::class);
65-
self::expectExceptionMessage('Option "model" must be an instance of Symfony\AI\Platform\Model.');
61+
$this->expectException(InvalidArgumentException::class);
62+
$this->expectExceptionMessage('Option "model" must be an instance of "Symfony\AI\Platform\Model".');
6663

67-
$gpt = new GPT();
68-
$model = new MessageBag();
69-
$input = new Input($gpt, new MessageBag(), ['model' => $model]);
64+
$originalModel = new Model('gpt-4o-mini', [Capability::INPUT_TEXT, Capability::OUTPUT_TEXT]);
65+
$input = new Input($originalModel, new MessageBag(), ['model' => new MessageBag()]);
7066

7167
$processor = new ModelOverrideInputProcessor();
7268
$processor->processInput($input);

0 commit comments

Comments
 (0)