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
|
MESSENGER_TRANSPORT_DSN=redis://:${REDIS_PASSWORD}@redis:6379/messages
|
||||||
|
|
||||||
MAILER_DSN=smtp://localhost
|
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:
|
App\Application\Storage\StorageManagerInterface:
|
||||||
alias: App\Infrastructure\Storage\LocalStorageManager
|
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