diff --git a/examples/toolbox/brave.php b/examples/toolbox/brave.php index 43efa2ccb..633de0ada 100644 --- a/examples/toolbox/brave.php +++ b/examples/toolbox/brave.php @@ -12,23 +12,37 @@ use Symfony\AI\Agent\Agent; use Symfony\AI\Agent\Toolbox\AgentProcessor; use Symfony\AI\Agent\Toolbox\Tool\Brave; -use Symfony\AI\Agent\Toolbox\Tool\Crawler; +use Symfony\AI\Agent\Toolbox\Tool\Clock; +use Symfony\AI\Agent\Toolbox\Tool\Scraper; use Symfony\AI\Agent\Toolbox\Toolbox; use Symfony\AI\Platform\Bridge\OpenAi\PlatformFactory; use Symfony\AI\Platform\Message\Message; use Symfony\AI\Platform\Message\MessageBag; +use Symfony\Component\Clock\Clock as SymfonyClock; require_once dirname(__DIR__).'/bootstrap.php'; $platform = PlatformFactory::create(env('OPENAI_API_KEY'), http_client()); $brave = new Brave(http_client(), env('BRAVE_API_KEY')); -$crawler = new Crawler(http_client()); -$toolbox = new Toolbox([$brave, $crawler], logger: logger()); -$processor = new AgentProcessor($toolbox); -$agent = new Agent($platform, 'gpt-4o-mini', [$processor], [$processor]); +$clock = new Clock(new SymfonyClock()); +$crawler = new Scraper(http_client()); +$toolbox = new Toolbox([$brave, $clock, $crawler], logger: logger()); +$processor = new AgentProcessor($toolbox, includeSources: true); +$agent = new Agent($platform, 'gpt-4o', [$processor], [$processor]); -$messages = new MessageBag(Message::ofUser('What was the latest game result of Dallas Cowboys?')); -$result = $agent->call($messages); +$prompt = <<getContent().\PHP_EOL; +$result = $agent->call(new MessageBag(Message::ofUser($prompt))); + +echo $result->getContent().\PHP_EOL.\PHP_EOL; + +echo 'Used sources:'.\PHP_EOL; +foreach ($result->getMetadata()->get('sources', []) as $source) { + echo sprintf(' - %s (%s)', $source->getName(), $source->getReference()).\PHP_EOL; +} +echo \PHP_EOL; diff --git a/examples/toolbox/serpapi.php b/examples/toolbox/serpapi.php index 38b65677d..e64881f37 100644 --- a/examples/toolbox/serpapi.php +++ b/examples/toolbox/serpapi.php @@ -11,22 +11,38 @@ use Symfony\AI\Agent\Agent; use Symfony\AI\Agent\Toolbox\AgentProcessor; +use Symfony\AI\Agent\Toolbox\Tool\Clock; +use Symfony\AI\Agent\Toolbox\Tool\Scraper; use Symfony\AI\Agent\Toolbox\Tool\SerpApi; use Symfony\AI\Agent\Toolbox\Toolbox; use Symfony\AI\Platform\Bridge\OpenAi\PlatformFactory; use Symfony\AI\Platform\Message\Message; use Symfony\AI\Platform\Message\MessageBag; +use Symfony\Component\Clock\Clock as SymfonyClock; require_once dirname(__DIR__).'/bootstrap.php'; $platform = PlatformFactory::create(env('OPENAI_API_KEY'), http_client()); +$clock = new Clock(new SymfonyClock()); +$crawler = new Scraper(http_client()); $serpApi = new SerpApi(http_client(), env('SERP_API_KEY')); -$toolbox = new Toolbox([$serpApi], logger: logger()); -$processor = new AgentProcessor($toolbox); -$agent = new Agent($platform, 'gpt-4o-mini', [$processor], [$processor]); +$toolbox = new Toolbox([$clock, $crawler, $serpApi], logger: logger()); +$processor = new AgentProcessor($toolbox, includeSources: true); +$agent = new Agent($platform, 'gpt-4o', [$processor], [$processor]); -$messages = new MessageBag(Message::ofUser('Who is the current chancellor of Germany?')); -$result = $agent->call($messages); +$prompt = <<getContent().\PHP_EOL; +$result = $agent->call(new MessageBag(Message::ofUser($prompt))); + +echo $result->getContent().\PHP_EOL.\PHP_EOL; + +echo 'Used sources:'.\PHP_EOL; +foreach ($result->getMetadata()->get('sources', []) as $source) { + echo sprintf(' - %s (%s)', $source->getName(), $source->getReference()).\PHP_EOL; +} +echo \PHP_EOL; diff --git a/examples/toolbox/tavily.php b/examples/toolbox/tavily.php index c9f8c06a7..f31d4729b 100644 --- a/examples/toolbox/tavily.php +++ b/examples/toolbox/tavily.php @@ -11,22 +11,36 @@ use Symfony\AI\Agent\Agent; use Symfony\AI\Agent\Toolbox\AgentProcessor; +use Symfony\AI\Agent\Toolbox\Tool\Clock; use Symfony\AI\Agent\Toolbox\Tool\Tavily; use Symfony\AI\Agent\Toolbox\Toolbox; use Symfony\AI\Platform\Bridge\OpenAi\PlatformFactory; use Symfony\AI\Platform\Message\Message; use Symfony\AI\Platform\Message\MessageBag; +use Symfony\Component\Clock\Clock as SymfonyClock; require_once dirname(__DIR__).'/bootstrap.php'; $platform = PlatformFactory::create(env('OPENAI_API_KEY'), http_client()); +$clock = new Clock(new SymfonyClock()); $tavily = new Tavily(http_client(), env('TAVILY_API_KEY')); -$toolbox = new Toolbox([$tavily], logger: logger()); -$processor = new AgentProcessor($toolbox); -$agent = new Agent($platform, 'gpt-4o-mini', [$processor], [$processor]); +$toolbox = new Toolbox([$clock, $tavily], logger: logger()); +$processor = new AgentProcessor($toolbox, includeSources: true); +$agent = new Agent($platform, 'gpt-4o', [$processor], [$processor]); -$messages = new MessageBag(Message::ofUser('What was the latest game result of Dallas Cowboys?')); -$result = $agent->call($messages); +$prompt = <<getContent().\PHP_EOL; +$result = $agent->call(new MessageBag(Message::ofUser($prompt))); + +echo $result->getContent().\PHP_EOL.\PHP_EOL; + +echo 'Used sources:'.\PHP_EOL; +foreach ($result->getMetadata()->get('sources', []) as $source) { + echo sprintf(' - %s (%s)', $source->getName(), $source->getReference()).\PHP_EOL; +} +echo \PHP_EOL; diff --git a/src/agent/src/Toolbox/Source/SourceMap.php b/src/agent/src/Toolbox/Source/SourceMap.php index eccb83e80..0ff0cc909 100644 --- a/src/agent/src/Toolbox/Source/SourceMap.php +++ b/src/agent/src/Toolbox/Source/SourceMap.php @@ -11,7 +11,7 @@ namespace Symfony\AI\Agent\Toolbox\Source; -class SourceMap +final class SourceMap { /** * @var Source[] diff --git a/src/agent/src/Toolbox/Tool/Brave.php b/src/agent/src/Toolbox/Tool/Brave.php index f0c01d1e0..5b9e75961 100644 --- a/src/agent/src/Toolbox/Tool/Brave.php +++ b/src/agent/src/Toolbox/Tool/Brave.php @@ -12,6 +12,9 @@ namespace Symfony\AI\Agent\Toolbox\Tool; use Symfony\AI\Agent\Toolbox\Attribute\AsTool; +use Symfony\AI\Agent\Toolbox\Source\HasSourcesInterface; +use Symfony\AI\Agent\Toolbox\Source\HasSourcesTrait; +use Symfony\AI\Agent\Toolbox\Source\Source; use Symfony\AI\Platform\Contract\JsonSchema\Attribute\With; use Symfony\Contracts\HttpClient\HttpClientInterface; @@ -19,15 +22,17 @@ * @author Christopher Hertel */ #[AsTool('brave_search', 'Tool that searches the web using Brave Search')] -final readonly class Brave +final class Brave implements HasSourcesInterface { + use HasSourcesTrait; + /** * @param array $options See https://api-dashboard.search.brave.com/app/documentation/web-search/query#WebSearchAPIQueryParameters */ public function __construct( - private HttpClientInterface $httpClient, - #[\SensitiveParameter] private string $apiKey, - private array $options = [], + private readonly HttpClientInterface $httpClient, + #[\SensitiveParameter] private readonly string $apiKey, + private readonly array $options = [], ) { } @@ -61,6 +66,13 @@ public function __invoke( ]); $data = $result->toArray(); + $results = $data['web']['results'] ?? []; + + foreach ($results as $result) { + $this->addSource( + new Source($result['title'] ?? '', $result['url'] ?? '', $result['description'] ?? '') + ); + } return array_map(static function (array $result) { return ['title' => $result['title'], 'description' => $result['description'], 'url' => $result['url']]; diff --git a/src/agent/src/Toolbox/Tool/Clock.php b/src/agent/src/Toolbox/Tool/Clock.php index b6df32d0a..e59f3c3e6 100644 --- a/src/agent/src/Toolbox/Tool/Clock.php +++ b/src/agent/src/Toolbox/Tool/Clock.php @@ -12,6 +12,9 @@ namespace Symfony\AI\Agent\Toolbox\Tool; use Symfony\AI\Agent\Toolbox\Attribute\AsTool; +use Symfony\AI\Agent\Toolbox\Source\HasSourcesInterface; +use Symfony\AI\Agent\Toolbox\Source\HasSourcesTrait; +use Symfony\AI\Agent\Toolbox\Source\Source; use Symfony\Component\Clock\Clock as SymfonyClock; use Symfony\Component\Clock\ClockInterface; @@ -19,11 +22,13 @@ * @author Christopher Hertel */ #[AsTool('clock', description: 'Provides the current date and time.')] -final readonly class Clock +final class Clock implements HasSourcesInterface { + use HasSourcesTrait; + public function __construct( - private ClockInterface $clock = new SymfonyClock(), - private ?string $timezone = null, + private readonly ClockInterface $clock = new SymfonyClock(), + private readonly ?string $timezone = null, ) { } @@ -35,6 +40,10 @@ public function __invoke(): string $now = $now->setTimezone(new \DateTimeZone($this->timezone)); } + $this->addSource( + new Source('Current Time', 'Clock', $now->format('Y-m-d H:i:s')) + ); + return \sprintf( 'Current date is %s (YYYY-MM-DD) and the time is %s (HH:MM:SS).', $now->format('Y-m-d'), diff --git a/src/agent/src/Toolbox/Tool/Crawler.php b/src/agent/src/Toolbox/Tool/Crawler.php deleted file mode 100644 index 364dc6ea7..000000000 --- a/src/agent/src/Toolbox/Tool/Crawler.php +++ /dev/null @@ -1,42 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\AI\Agent\Toolbox\Tool; - -use Symfony\AI\Agent\Exception\RuntimeException; -use Symfony\AI\Agent\Toolbox\Attribute\AsTool; -use Symfony\Component\DomCrawler\Crawler as DomCrawler; -use Symfony\Contracts\HttpClient\HttpClientInterface; - -/** - * @author Christopher Hertel - */ -#[AsTool('crawler', 'A tool that crawls one page of a website and returns the visible text of it.')] -final readonly class Crawler -{ - public function __construct( - private HttpClientInterface $httpClient, - ) { - if (!class_exists(DomCrawler::class)) { - throw new RuntimeException('For using the Crawler tool, the symfony/dom-crawler package is required. Try running "composer require symfony/dom-crawler".'); - } - } - - /** - * @param string $url the URL of the page to crawl - */ - public function __invoke(string $url): string - { - $result = $this->httpClient->request('GET', $url); - - return (new DomCrawler($result->getContent()))->filter('body')->text(); - } -} diff --git a/src/agent/src/Toolbox/Tool/Scraper.php b/src/agent/src/Toolbox/Tool/Scraper.php new file mode 100644 index 000000000..f3504af14 --- /dev/null +++ b/src/agent/src/Toolbox/Tool/Scraper.php @@ -0,0 +1,58 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\AI\Agent\Toolbox\Tool; + +use Symfony\AI\Agent\Exception\RuntimeException; +use Symfony\AI\Agent\Toolbox\Attribute\AsTool; +use Symfony\AI\Agent\Toolbox\Source\HasSourcesInterface; +use Symfony\AI\Agent\Toolbox\Source\HasSourcesTrait; +use Symfony\AI\Agent\Toolbox\Source\Source; +use Symfony\Component\DomCrawler\Crawler as DomCrawler; +use Symfony\Contracts\HttpClient\HttpClientInterface; + +/** + * @author Christopher Hertel + */ +#[AsTool('scraper', 'Loads the visible text and title of a website by URL.')] +final class Scraper implements HasSourcesInterface +{ + use HasSourcesTrait; + + public function __construct( + private readonly HttpClientInterface $httpClient, + ) { + if (!class_exists(DomCrawler::class)) { + throw new RuntimeException('For using the Scraper tool, the symfony/dom-crawler package is required. Try running "composer require symfony/dom-crawler".'); + } + } + + /** + * @param string $url the URL of the page to load data from + * + * @return array{title: string, content: string} + */ + public function __invoke(string $url): array + { + $result = $this->httpClient->request('GET', $url); + $crawler = new DomCrawler($result->getContent()); + + $title = $crawler->filter('title')->text(); + $content = $crawler->filter('body')->text(); + + $this->addSource(new Source($title, $url, $content)); + + return [ + 'title' => $title, + 'content' => $content, + ]; + } +} diff --git a/src/agent/src/Toolbox/Tool/SerpApi.php b/src/agent/src/Toolbox/Tool/SerpApi.php index aa04e4424..08f9ea13b 100644 --- a/src/agent/src/Toolbox/Tool/SerpApi.php +++ b/src/agent/src/Toolbox/Tool/SerpApi.php @@ -12,24 +12,31 @@ namespace Symfony\AI\Agent\Toolbox\Tool; use Symfony\AI\Agent\Toolbox\Attribute\AsTool; +use Symfony\AI\Agent\Toolbox\Source\HasSourcesInterface; +use Symfony\AI\Agent\Toolbox\Source\HasSourcesTrait; +use Symfony\AI\Agent\Toolbox\Source\Source; use Symfony\Contracts\HttpClient\HttpClientInterface; /** * @author Christopher Hertel */ #[AsTool(name: 'serpapi', description: 'search for information on the internet')] -final readonly class SerpApi +final class SerpApi implements HasSourcesInterface { + use HasSourcesTrait; + public function __construct( - private HttpClientInterface $httpClient, - private string $apiKey, + private readonly HttpClientInterface $httpClient, + private readonly string $apiKey, ) { } /** * @param string $query The search query to use + * + * @return array{title: string, link: string, content: string}[] */ - public function __invoke(string $query): string + public function __invoke(string $query): array { $result = $this->httpClient->request('GET', 'https://serpapi.com/search', [ 'query' => [ @@ -38,14 +45,19 @@ public function __invoke(string $query): string ], ]); - return \sprintf('Results for "%s" are "%s".', $query, $this->extractBestResponse($result->toArray())); - } + $data = $result->toArray(); - /** - * @param array $results - */ - private function extractBestResponse(array $results): string - { - return implode('. ', array_map(fn ($story) => $story['title'], $results['organic_results'])); + $results = []; + foreach ($data['organic_results'] as $result) { + $results[] = [ + 'title' => $result['title'], + 'link' => $result['link'], + 'content' => $result['snippet'], + ]; + + $this->addSource(new Source($result['title'], $result['link'], $result['snippet'])); + } + + return $results; } } diff --git a/src/agent/src/Toolbox/Tool/Tavily.php b/src/agent/src/Toolbox/Tool/Tavily.php index 36405b42e..cdf150b77 100644 --- a/src/agent/src/Toolbox/Tool/Tavily.php +++ b/src/agent/src/Toolbox/Tool/Tavily.php @@ -12,6 +12,9 @@ namespace Symfony\AI\Agent\Toolbox\Tool; use Symfony\AI\Agent\Toolbox\Attribute\AsTool; +use Symfony\AI\Agent\Toolbox\Source\HasSourcesInterface; +use Symfony\AI\Agent\Toolbox\Source\HasSourcesTrait; +use Symfony\AI\Agent\Toolbox\Source\Source; use Symfony\Contracts\HttpClient\HttpClientInterface; /** @@ -21,15 +24,17 @@ */ #[AsTool('tavily_search', description: 'search for information on the internet', method: 'search')] #[AsTool('tavily_extract', description: 'fetch content from websites', method: 'extract')] -final readonly class Tavily +final class Tavily implements HasSourcesInterface { + use HasSourcesTrait; + /** * @param array $options */ public function __construct( - private HttpClientInterface $httpClient, - private string $apiKey, - private array $options = ['include_images' => false], + private readonly HttpClientInterface $httpClient, + private readonly string $apiKey, + private readonly array $options = ['include_images' => false], ) { } @@ -45,6 +50,14 @@ public function search(string $query): string ]), ]); + $data = $result->toArray(); + + foreach ($data['results'] ?? [] as $item) { + $this->addSource( + new Source($item['title'] ?? '', $item['url'] ?? '', $item['raw_content'] ?? '') + ); + } + return $result->getContent(); } @@ -60,6 +73,14 @@ public function extract(array $urls): string ], ]); + $data = $result->toArray(); + + foreach ($data['results'] ?? [] as $item) { + $this->addSource( + new Source($item['title'] ?? '', $item['url'] ?? '', $item['raw_content'] ?? '') + ); + } + return $result->getContent(); } }