feat: add MistralClient as switchable alternative to OllamaClient
Implements OllamaClientInterface against the Mistral Cloud API (/v1/chat/completions). Switch by toggling the alias in services.yaml and pointing AI_TEXT_MODEL / AI_VISION_MODEL env vars at the MISTRAL_* or OLLAMA_* counterparts. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
6bf001b0c0
commit
d51efa057b
3 changed files with 221 additions and 1 deletions
31
.env
31
.env
|
|
@ -12,4 +12,33 @@ REDIS_URL=redis://:${REDIS_PASSWORD}@redis:6379
|
|||
MESSENGER_TRANSPORT_DSN=redis://:${REDIS_PASSWORD}@redis:6379/messages
|
||||
|
||||
MAILER_DSN=smtp://localhost
|
||||
OLLAMA_URL=http://localhost:11434
|
||||
OLLAMA_BASE_URL=http://172.18.0.1:11434
|
||||
OLLAMA_VISION_MODEL=llava
|
||||
OLLAMA_TEXT_MODEL=llama3.2
|
||||
|
||||
MISTRAL_BASE_URL=https://api.mistral.ai
|
||||
MISTRAL_API_KEY=
|
||||
# Vision requires a Pixtral model, e.g. pixtral-12b-2409
|
||||
MISTRAL_VISION_MODEL=pixtral-12b-2409
|
||||
MISTRAL_TEXT_MODEL=mistral-large-latest
|
||||
|
||||
# Active backend — point these at OLLAMA_* or MISTRAL_* vars
|
||||
AI_TEXT_MODEL=${OLLAMA_TEXT_MODEL}
|
||||
AI_VISION_MODEL=${OLLAMA_VISION_MODEL}
|
||||
SERP_API_KEY=
|
||||
|
||||
EBAY_CLIENT_ID=
|
||||
EBAY_CLIENT_SECRET=
|
||||
EBAY_MARKETPLACE_ID=EBAY_DE
|
||||
EBAY_API_BASE_URL=https://api.ebay.com
|
||||
EBAY_OAUTH_BASE_URL=https://api.ebay.com
|
||||
EBAY_VERIFICATION_TOKEN=
|
||||
EBAY_ENDPOINT_URL=https://your-domain.com/webhooks/ebay
|
||||
|
||||
FRAPPE_ERP_BASE_URL=https://erp.example.com
|
||||
FRAPPE_ERP_API_KEY=changeme
|
||||
FRAPPE_ERP_API_SECRET=changeme
|
||||
FRAPPE_GENERIC_ITEM_CODE=REFURB-HW
|
||||
|
||||
SUPPLIER_EMAIL=lieferant@example.com
|
||||
SENDER_EMAIL=noreply@superseller3000.de
|
||||
|
|
|
|||
|
|
@ -46,3 +46,115 @@ services:
|
|||
|
||||
App\Application\Storage\StorageManagerInterface:
|
||||
alias: App\Infrastructure\Storage\LocalStorageManager
|
||||
|
||||
App\Domain\Auth\Repository\UserRepositoryInterface:
|
||||
alias: App\Infrastructure\Persistence\Repository\DoctrineUserRepository
|
||||
|
||||
App\Domain\Auth\Repository\ApiKeyRepositoryInterface:
|
||||
alias: App\Infrastructure\Persistence\Repository\DoctrineApiKeyRepository
|
||||
|
||||
# Switch between Ollama and Mistral by changing the alias target below
|
||||
App\Infrastructure\AI\OllamaClientInterface:
|
||||
alias: App\Infrastructure\AI\OllamaClient
|
||||
# alias: App\Infrastructure\AI\MistralClient
|
||||
|
||||
App\Infrastructure\AI\OllamaClient:
|
||||
arguments:
|
||||
$ollamaBaseUrl: '%env(OLLAMA_BASE_URL)%'
|
||||
|
||||
App\Infrastructure\AI\MistralClient:
|
||||
arguments:
|
||||
$mistralApiKey: '%env(MISTRAL_API_KEY)%'
|
||||
$mistralBaseUrl: '%env(MISTRAL_BASE_URL)%'
|
||||
|
||||
App\Infrastructure\Search\WebSearchInterface:
|
||||
alias: App\Infrastructure\Search\SerpApiWebSearch
|
||||
|
||||
App\Infrastructure\Search\SerpApiWebSearch:
|
||||
arguments:
|
||||
$serpApiKey: '%env(SERP_API_KEY)%'
|
||||
|
||||
App\Infrastructure\AI\Agent\OllamaVisionAgent:
|
||||
arguments:
|
||||
$model: '%env(AI_VISION_MODEL)%'
|
||||
|
||||
App\Infrastructure\AI\Agent\SpecsResearchAgent:
|
||||
arguments:
|
||||
$model: '%env(AI_TEXT_MODEL)%'
|
||||
|
||||
App\Infrastructure\AI\Agent\JsonCodingAgent:
|
||||
arguments:
|
||||
$model: '%env(AI_TEXT_MODEL)%'
|
||||
|
||||
App\Infrastructure\AI\Agent\EbayTextAgent:
|
||||
arguments:
|
||||
$model: '%env(AI_TEXT_MODEL)%'
|
||||
|
||||
App\Domain\Pipeline\Repository\AIPipelineJobRepositoryInterface:
|
||||
alias: App\Infrastructure\Persistence\Repository\DoctrineAIPipelineJobRepository
|
||||
|
||||
App\Infrastructure\Console\BackupCommand:
|
||||
arguments:
|
||||
$backupDir: '%kernel.project_dir%/var/backups'
|
||||
$databaseUrl: '%env(DATABASE_URL)%'
|
||||
|
||||
App\Application\Channel\ChannelAdapterRegistry:
|
||||
arguments:
|
||||
$adapters: !tagged_iterator app.channel_adapter
|
||||
|
||||
App\Infrastructure\Channel\Ebay\EbayAdapter:
|
||||
tags: ['app.channel_adapter']
|
||||
|
||||
App\Infrastructure\Channel\Ebay\EbayOAuthClient:
|
||||
arguments:
|
||||
$clientId: '%env(EBAY_CLIENT_ID)%'
|
||||
$clientSecret: '%env(EBAY_CLIENT_SECRET)%'
|
||||
$oauthBaseUrl: '%env(EBAY_OAUTH_BASE_URL)%'
|
||||
$cache: '@cache.app'
|
||||
|
||||
App\Infrastructure\Channel\Ebay\EbayInventoryApiClient:
|
||||
arguments:
|
||||
$apiBaseUrl: '%env(EBAY_API_BASE_URL)%'
|
||||
$marketplaceId: '%env(EBAY_MARKETPLACE_ID)%'
|
||||
|
||||
App\Infrastructure\Channel\Ebay\EbayTaxonomyService:
|
||||
arguments:
|
||||
$apiBaseUrl: '%env(EBAY_API_BASE_URL)%'
|
||||
$marketplaceId: '%env(EBAY_MARKETPLACE_ID)%'
|
||||
$cache: '@cache.app'
|
||||
|
||||
App\Infrastructure\Channel\Ebay\EbayWebhookVerifier:
|
||||
arguments:
|
||||
$verificationToken: '%env(EBAY_VERIFICATION_TOKEN)%'
|
||||
$endpointUrl: '%env(EBAY_ENDPOINT_URL)%'
|
||||
|
||||
App\Domain\Order\Repository\InvoiceRepositoryInterface:
|
||||
alias: App\Infrastructure\Persistence\Repository\DoctrineInvoiceRepository
|
||||
|
||||
App\Infrastructure\Channel\Frappe\FrappeHttpClient:
|
||||
arguments:
|
||||
$baseUrl: '%env(FRAPPE_ERP_BASE_URL)%'
|
||||
$apiKey: '%env(FRAPPE_ERP_API_KEY)%'
|
||||
$apiSecret: '%env(FRAPPE_ERP_API_SECRET)%'
|
||||
|
||||
App\Infrastructure\Channel\Frappe\FrappeErpAdapter:
|
||||
arguments:
|
||||
$genericItemCode: '%env(FRAPPE_GENERIC_ITEM_CODE)%'
|
||||
|
||||
App\Application\Order\ErpAdapterInterface:
|
||||
alias: App\Infrastructure\Channel\Frappe\FrappeErpAdapter
|
||||
|
||||
App\Application\Order\CustomerResolverInterface:
|
||||
alias: App\Infrastructure\Order\CustomerResolver
|
||||
|
||||
App\Application\Order\InvoiceMailerInterface:
|
||||
alias: App\Infrastructure\Mail\SymfonyInvoiceMailer
|
||||
|
||||
App\Infrastructure\Mail\SymfonyInvoiceMailer:
|
||||
arguments:
|
||||
$supplierEmail: '%env(SUPPLIER_EMAIL)%'
|
||||
$senderEmail: '%env(SENDER_EMAIL)%'
|
||||
|
||||
App\Infrastructure\Channel\Ebay\EbayFulfillmentApiClient:
|
||||
arguments:
|
||||
$apiBaseUrl: '%env(EBAY_API_BASE_URL)%'
|
||||
|
|
|
|||
79
src/Infrastructure/AI/MistralClient.php
Normal file
79
src/Infrastructure/AI/MistralClient.php
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Infrastructure\AI;
|
||||
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
|
||||
final class MistralClient implements OllamaClientInterface
|
||||
{
|
||||
public function __construct(
|
||||
private readonly HttpClientInterface $httpClient,
|
||||
private readonly string $mistralApiKey,
|
||||
private readonly string $mistralBaseUrl,
|
||||
) {
|
||||
}
|
||||
|
||||
public function generate(string $model, string $prompt): string
|
||||
{
|
||||
$response = $this->httpClient->request('POST', $this->mistralBaseUrl.'/v1/chat/completions', [
|
||||
'headers' => [
|
||||
'Authorization' => 'Bearer '.$this->mistralApiKey,
|
||||
],
|
||||
'json' => [
|
||||
'model' => $model,
|
||||
'messages' => [
|
||||
['role' => 'user', 'content' => $prompt],
|
||||
],
|
||||
],
|
||||
'timeout' => 120,
|
||||
]);
|
||||
|
||||
/** @var array{choices: array{0: array{message: array{content: string}}}} $data */
|
||||
$data = $response->toArray();
|
||||
|
||||
return $data['choices'][0]['message']['content'];
|
||||
}
|
||||
|
||||
public function generateWithImage(string $model, string $prompt, string $imagePath): string
|
||||
{
|
||||
$imageData = base64_encode((string) file_get_contents($imagePath));
|
||||
$mimeType = $this->guessMimeType($imagePath);
|
||||
|
||||
$response = $this->httpClient->request('POST', $this->mistralBaseUrl.'/v1/chat/completions', [
|
||||
'headers' => [
|
||||
'Authorization' => 'Bearer '.$this->mistralApiKey,
|
||||
],
|
||||
'json' => [
|
||||
'model' => $model,
|
||||
'messages' => [
|
||||
[
|
||||
'role' => 'user',
|
||||
'content' => [
|
||||
['type' => 'text', 'text' => $prompt],
|
||||
['type' => 'image_url', 'image_url' => ['url' => 'data:'.$mimeType.';base64,'.$imageData]],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
'timeout' => 180,
|
||||
]);
|
||||
|
||||
/** @var array{choices: array{0: array{message: array{content: string}}}} $data */
|
||||
$data = $response->toArray();
|
||||
|
||||
return $data['choices'][0]['message']['content'];
|
||||
}
|
||||
|
||||
private function guessMimeType(string $path): string
|
||||
{
|
||||
return match (strtolower(pathinfo($path, PATHINFO_EXTENSION))) {
|
||||
'jpg', 'jpeg' => 'image/jpeg',
|
||||
'png' => 'image/png',
|
||||
'gif' => 'image/gif',
|
||||
'webp' => 'image/webp',
|
||||
default => 'image/jpeg',
|
||||
};
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue