feat: replace Mistral web_search with Tavily for specs research
- Add WebSearchInterface + TavilyWebSearch (POST /search, max 5 results)
- SpecsResearchAgent now fetches search results first, injects them as
{{searchResults}} context into the prompt, then calls plain generate()
— no dependency on model-specific web_search tool support
- Update specs_research prompt template (PHP default + DB migration) to
use the new {{searchResults}} variable
- Wire TAVILY_API_KEY env var; register TavilyWebSearch in services.yaml
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
00dc232426
commit
cfb5cc4ad0
7 changed files with 133 additions and 5 deletions
2
.env
2
.env
|
|
@ -16,6 +16,8 @@ OLLAMA_BASE_URL=http://172.18.0.1:11434
|
||||||
OLLAMA_VISION_MODEL=llava
|
OLLAMA_VISION_MODEL=llava
|
||||||
OLLAMA_TEXT_MODEL=llama3.2
|
OLLAMA_TEXT_MODEL=llama3.2
|
||||||
|
|
||||||
|
TAVILY_API_KEY=
|
||||||
|
|
||||||
MISTRAL_BASE_URL=https://api.mistral.ai
|
MISTRAL_BASE_URL=https://api.mistral.ai
|
||||||
MISTRAL_API_KEY=
|
MISTRAL_API_KEY=
|
||||||
# Vision requires a Pixtral model, e.g. pixtral-12b-2409
|
# Vision requires a Pixtral model, e.g. pixtral-12b-2409
|
||||||
|
|
|
||||||
|
|
@ -82,6 +82,13 @@ services:
|
||||||
arguments:
|
arguments:
|
||||||
$model: '%env(AI_TEXT_MODEL)%'
|
$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:
|
App\Infrastructure\AI\Agent\JsonCodingAgent:
|
||||||
arguments:
|
arguments:
|
||||||
$model: '%env(AI_TEXT_MODEL)%'
|
$model: '%env(AI_TEXT_MODEL)%'
|
||||||
|
|
|
||||||
51
migrations/Version20260520020000.php
Normal file
51
migrations/Version20260520020000.php
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
final class Version20260520020000 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'Update specs_research prompt to include {{searchResults}} from Tavily';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
$body = <<<'PROMPT'
|
||||||
|
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.
|
||||||
|
Be specific and accurate. If a spec is not found in the search results, omit it rather than guessing.
|
||||||
|
PROMPT;
|
||||||
|
|
||||||
|
$this->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],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -6,12 +6,14 @@ namespace App\Infrastructure\AI\Agent;
|
||||||
|
|
||||||
use App\Infrastructure\AI\OllamaClientInterface;
|
use App\Infrastructure\AI\OllamaClientInterface;
|
||||||
use App\Infrastructure\AI\PromptTemplateService;
|
use App\Infrastructure\AI\PromptTemplateService;
|
||||||
|
use App\Infrastructure\Search\WebSearchInterface;
|
||||||
|
|
||||||
final class SpecsResearchAgent
|
final class SpecsResearchAgent
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly OllamaClientInterface $client,
|
private readonly OllamaClientInterface $client,
|
||||||
private readonly PromptTemplateService $prompts,
|
private readonly PromptTemplateService $prompts,
|
||||||
|
private readonly WebSearchInterface $search,
|
||||||
private readonly string $model,
|
private readonly string $model,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
@ -20,12 +22,15 @@ final class SpecsResearchAgent
|
||||||
{
|
{
|
||||||
$subject = trim(($manufacturer !== '' ? $manufacturer.' ' : '').$modelName);
|
$subject = trim(($manufacturer !== '' ? $manufacturer.' ' : '').$modelName);
|
||||||
|
|
||||||
|
$searchResults = $this->search->search("{$subject} {$articleTypeName} specifications");
|
||||||
|
|
||||||
$prompt = $this->prompts->render('specs_research', [
|
$prompt = $this->prompts->render('specs_research', [
|
||||||
'articleType' => $articleTypeName,
|
'articleType' => $articleTypeName,
|
||||||
'subject' => $subject,
|
'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)) {
|
if ('' === trim($result)) {
|
||||||
throw new \RuntimeException("No specifications found for model: {$modelName}");
|
throw new \RuntimeException("No specifications found for model: {$modelName}");
|
||||||
|
|
|
||||||
|
|
@ -11,10 +11,15 @@ final class PromptTemplateService
|
||||||
/** @var array<string, string> */
|
/** @var array<string, string> */
|
||||||
private const array DEFAULTS = [
|
private const array DEFAULTS = [
|
||||||
'specs_research' => <<<'PROMPT'
|
'specs_research' => <<<'PROMPT'
|
||||||
List all known technical specifications for the {{articleType}}: "{{subject}}".
|
You are a hardware specifications expert. Extract the technical specifications for the {{articleType}}: "{{subject}}".
|
||||||
Include: processor, RAM, storage variants, display size and resolution, GPU, battery capacity,
|
|
||||||
|
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.
|
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,
|
PROMPT,
|
||||||
|
|
||||||
'ebay_title' => <<<'PROMPT'
|
'ebay_title' => <<<'PROMPT'
|
||||||
|
|
@ -89,7 +94,7 @@ PROMPT,
|
||||||
public static function knownKeys(): array
|
public static function knownKeys(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'specs_research' => ['articleType', 'subject'],
|
'specs_research' => ['articleType', 'subject', 'searchResults'],
|
||||||
'ebay_title' => ['typeName', 'deviceLabel', 'condition', 'specsSection'],
|
'ebay_title' => ['typeName', 'deviceLabel', 'condition', 'specsSection'],
|
||||||
'ebay_description' => ['typeName', 'deviceLabel', 'condition', 'conditionNotes', 'specsSection'],
|
'ebay_description' => ['typeName', 'deviceLabel', 'condition', 'conditionNotes', 'specsSection'],
|
||||||
'vision_analyze' => [],
|
'vision_analyze' => [],
|
||||||
|
|
|
||||||
45
src/Infrastructure/Search/TavilyWebSearch.php
Normal file
45
src/Infrastructure/Search/TavilyWebSearch.php
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Infrastructure\Search;
|
||||||
|
|
||||||
|
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||||
|
|
||||||
|
final class TavilyWebSearch implements WebSearchInterface
|
||||||
|
{
|
||||||
|
private const string API_URL = 'https://api.tavily.com/search';
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private readonly HttpClientInterface $httpClient,
|
||||||
|
private readonly string $apiKey,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function search(string $query): string
|
||||||
|
{
|
||||||
|
$response = $this->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<array{title: string, url: string, content: string}>} $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);
|
||||||
|
}
|
||||||
|
}
|
||||||
13
src/Infrastructure/Search/WebSearchInterface.php
Normal file
13
src/Infrastructure/Search/WebSearchInterface.php
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Infrastructure\Search;
|
||||||
|
|
||||||
|
interface WebSearchInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run a web search and return the results as plain text suitable for use as AI context.
|
||||||
|
*/
|
||||||
|
public function search(string $query): string;
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue