feat: replace SerpApi web search with Mistral native web_search tool
MistralClient gains generateWithWebSearch() which uses Mistral's built-in web_search tool, handling the multi-turn tool-call loop server-side. SerpApiWebSearch and WebSearchInterface are removed — no longer needed. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
f3b018e048
commit
b9907d6c63
3 changed files with 53 additions and 58 deletions
|
|
@ -66,6 +66,59 @@ final class MistralClient implements OllamaClientInterface
|
||||||
return $data['choices'][0]['message']['content'];
|
return $data['choices'][0]['message']['content'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Chat completion with Mistral's built-in web_search tool enabled.
|
||||||
|
* Mistral executes searches server-side; we handle the multi-turn loop if needed.
|
||||||
|
*/
|
||||||
|
public function generateWithWebSearch(string $model, string $prompt): string
|
||||||
|
{
|
||||||
|
$messages = [['role' => 'user', 'content' => $prompt]];
|
||||||
|
|
||||||
|
$response = $this->httpClient->request('POST', $this->mistralBaseUrl.'/v1/chat/completions', [
|
||||||
|
'headers' => ['Authorization' => 'Bearer '.$this->mistralApiKey],
|
||||||
|
'json' => [
|
||||||
|
'model' => $model,
|
||||||
|
'tools' => [['type' => 'web_search']],
|
||||||
|
'messages' => $messages,
|
||||||
|
],
|
||||||
|
'timeout' => 120,
|
||||||
|
]);
|
||||||
|
|
||||||
|
/** @var array{choices: array{0: array{finish_reason: string, message: array{content: ?string, tool_calls?: list<array{id: string, function: array{name: string, arguments: string}}>}}}} $data */
|
||||||
|
$data = $response->toArray();
|
||||||
|
$choice = $data['choices'][0];
|
||||||
|
|
||||||
|
if ('tool_calls' !== $choice['finish_reason']) {
|
||||||
|
return (string) $choice['message']['content'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append assistant's tool call turn, then return empty tool results so
|
||||||
|
// Mistral's servers can complete the search and produce the final answer.
|
||||||
|
$messages[] = $choice['message'];
|
||||||
|
foreach ($choice['message']['tool_calls'] ?? [] as $toolCall) {
|
||||||
|
$messages[] = [
|
||||||
|
'role' => 'tool',
|
||||||
|
'tool_call_id' => $toolCall['id'],
|
||||||
|
'content' => '',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$final = $this->httpClient->request('POST', $this->mistralBaseUrl.'/v1/chat/completions', [
|
||||||
|
'headers' => ['Authorization' => 'Bearer '.$this->mistralApiKey],
|
||||||
|
'json' => [
|
||||||
|
'model' => $model,
|
||||||
|
'tools' => [['type' => 'web_search']],
|
||||||
|
'messages' => $messages,
|
||||||
|
],
|
||||||
|
'timeout' => 120,
|
||||||
|
]);
|
||||||
|
|
||||||
|
/** @var array{choices: array{0: array{message: array{content: string}}}} $finalData */
|
||||||
|
$finalData = $final->toArray();
|
||||||
|
|
||||||
|
return $finalData['choices'][0]['message']['content'];
|
||||||
|
}
|
||||||
|
|
||||||
private function guessMimeType(string $path): string
|
private function guessMimeType(string $path): string
|
||||||
{
|
{
|
||||||
return match (strtolower(pathinfo($path, PATHINFO_EXTENSION))) {
|
return match (strtolower(pathinfo($path, PATHINFO_EXTENSION))) {
|
||||||
|
|
|
||||||
|
|
@ -1,48 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Infrastructure\Search;
|
|
||||||
|
|
||||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
|
||||||
|
|
||||||
final class SerpApiWebSearch implements WebSearchInterface
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
private readonly HttpClientInterface $httpClient,
|
|
||||||
private readonly string $serpApiKey,
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
|
|
||||||
public function search(string $query): string
|
|
||||||
{
|
|
||||||
$response = $this->httpClient->request('GET', 'https://serpapi.com/search', [
|
|
||||||
'query' => [
|
|
||||||
'q' => $query,
|
|
||||||
'api_key' => $this->serpApiKey,
|
|
||||||
'num' => 5,
|
|
||||||
'hl' => 'de',
|
|
||||||
],
|
|
||||||
'timeout' => 15,
|
|
||||||
]);
|
|
||||||
|
|
||||||
/** @var array{organic_results?: list<array{title?: string, snippet?: string}>} $data */
|
|
||||||
$data = $response->toArray();
|
|
||||||
|
|
||||||
$results = $data['organic_results'] ?? [];
|
|
||||||
if ([] === $results) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
$texts = [];
|
|
||||||
foreach ($results as $result) {
|
|
||||||
$title = $result['title'] ?? '';
|
|
||||||
$snippet = $result['snippet'] ?? '';
|
|
||||||
if ('' !== $title || '' !== $snippet) {
|
|
||||||
$texts[] = $title."\n".$snippet;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return implode("\n\n", $texts);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Infrastructure\Search;
|
|
||||||
|
|
||||||
interface WebSearchInterface
|
|
||||||
{
|
|
||||||
public function search(string $query): string;
|
|
||||||
}
|
|
||||||
Loading…
Reference in a new issue