diff --git a/.env b/.env index 843e42d..2ea519e 100644 --- a/.env +++ b/.env @@ -16,6 +16,8 @@ OLLAMA_BASE_URL=http://172.18.0.1:11434 OLLAMA_VISION_MODEL=llava OLLAMA_TEXT_MODEL=llama3.2 +TAVILY_API_KEY= + MISTRAL_BASE_URL=https://api.mistral.ai MISTRAL_API_KEY= # Vision requires a Pixtral model, e.g. pixtral-12b-2409 diff --git a/config/services.yaml b/config/services.yaml index 3f95432..953c650 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -82,6 +82,13 @@ services: arguments: $model: '%env(AI_TEXT_MODEL)%' + App\Infrastructure\Search\WebSearchInterface: + alias: App\Infrastructure\Search\TavilyWebSearch + + App\Infrastructure\Search\TavilyWebSearch: + arguments: + $apiKey: '%env(TAVILY_API_KEY)%' + App\Infrastructure\AI\Agent\JsonCodingAgent: arguments: $model: '%env(AI_TEXT_MODEL)%' diff --git a/migrations/Version20260520020000.php b/migrations/Version20260520020000.php new file mode 100644 index 0000000..f406508 --- /dev/null +++ b/migrations/Version20260520020000.php @@ -0,0 +1,51 @@ +addSql( + "UPDATE app.prompt_templates SET body = :body WHERE key = 'specs_research'", + ['body' => $body], + ); + } + + public function down(Schema $schema): void + { + $body = <<<'PROMPT' +List all known technical specifications for the {{articleType}}: "{{subject}}". +Include: processor, RAM, storage variants, display size and resolution, GPU, battery capacity, +ports, connectivity, weight, dimensions, OS, and any other relevant specs. +If you know this device, be specific and complete. If it is unknown, say so explicitly. +PROMPT; + + $this->addSql( + "UPDATE app.prompt_templates SET body = :body WHERE key = 'specs_research'", + ['body' => $body], + ); + } +} diff --git a/src/Infrastructure/AI/Agent/SpecsResearchAgent.php b/src/Infrastructure/AI/Agent/SpecsResearchAgent.php index 9337669..8fc0ec8 100644 --- a/src/Infrastructure/AI/Agent/SpecsResearchAgent.php +++ b/src/Infrastructure/AI/Agent/SpecsResearchAgent.php @@ -6,12 +6,14 @@ namespace App\Infrastructure\AI\Agent; use App\Infrastructure\AI\OllamaClientInterface; use App\Infrastructure\AI\PromptTemplateService; +use App\Infrastructure\Search\WebSearchInterface; final class SpecsResearchAgent { public function __construct( private readonly OllamaClientInterface $client, private readonly PromptTemplateService $prompts, + private readonly WebSearchInterface $search, private readonly string $model, ) { } @@ -20,12 +22,15 @@ final class SpecsResearchAgent { $subject = trim(($manufacturer !== '' ? $manufacturer.' ' : '').$modelName); + $searchResults = $this->search->search("{$subject} {$articleTypeName} specifications"); + $prompt = $this->prompts->render('specs_research', [ 'articleType' => $articleTypeName, 'subject' => $subject, + 'searchResults' => $searchResults !== '' ? $searchResults : 'No web results available.', ]); - $result = $this->client->generateWithWebSearch($this->model, $prompt); + $result = $this->client->generate($this->model, $prompt); if ('' === trim($result)) { throw new \RuntimeException("No specifications found for model: {$modelName}"); diff --git a/src/Infrastructure/AI/PromptTemplateService.php b/src/Infrastructure/AI/PromptTemplateService.php index 27b940a..5fadb06 100644 --- a/src/Infrastructure/AI/PromptTemplateService.php +++ b/src/Infrastructure/AI/PromptTemplateService.php @@ -11,10 +11,15 @@ final class PromptTemplateService /** @var array */ private const array DEFAULTS = [ 'specs_research' => <<<'PROMPT' -List all known technical specifications for the {{articleType}}: "{{subject}}". -Include: processor, RAM, storage variants, display size and resolution, GPU, battery capacity, +You are a hardware specifications expert. Extract the technical specifications for the {{articleType}}: "{{subject}}". + +Web search results: +{{searchResults}} + +Based on the search results above, list all technical specifications including: +processor, RAM, storage variants, display size and resolution, GPU, battery capacity, ports, connectivity, weight, dimensions, OS, and any other relevant specs. -If you know this device, be specific and complete. If it is unknown, say so explicitly. +Be specific and accurate. If a spec is not found in the search results, omit it rather than guessing. PROMPT, 'ebay_title' => <<<'PROMPT' @@ -89,7 +94,7 @@ PROMPT, public static function knownKeys(): array { return [ - 'specs_research' => ['articleType', 'subject'], + 'specs_research' => ['articleType', 'subject', 'searchResults'], 'ebay_title' => ['typeName', 'deviceLabel', 'condition', 'specsSection'], 'ebay_description' => ['typeName', 'deviceLabel', 'condition', 'conditionNotes', 'specsSection'], 'vision_analyze' => [], diff --git a/src/Infrastructure/Search/TavilyWebSearch.php b/src/Infrastructure/Search/TavilyWebSearch.php new file mode 100644 index 0000000..5e318e6 --- /dev/null +++ b/src/Infrastructure/Search/TavilyWebSearch.php @@ -0,0 +1,45 @@ +httpClient->request('POST', self::API_URL, [ + 'json' => [ + 'api_key' => $this->apiKey, + 'query' => $query, + 'max_results' => 5, + 'include_raw_content' => false, + ], + 'timeout' => 30, + ]); + + /** @var array{results: list} $data */ + $data = $response->toArray(); + + if (empty($data['results'])) { + return ''; + } + + $parts = []; + foreach ($data['results'] as $result) { + $parts[] = "### {$result['title']}\n{$result['content']}"; + } + + return implode("\n\n", $parts); + } +} diff --git a/src/Infrastructure/Search/WebSearchInterface.php b/src/Infrastructure/Search/WebSearchInterface.php new file mode 100644 index 0000000..9f80cd4 --- /dev/null +++ b/src/Infrastructure/Search/WebSearchInterface.php @@ -0,0 +1,13 @@ +