makeCtx(); // Seed a part that the AI will "find" via the tool. Part::create([ 'name' => 'Filtru ulei MANN', 'article' => 'W811/80', 'brand' => 'MANN', 'category' => 'Filtre', 'qty' => 5, 'min_qty' => 2, 'unit' => 'buc', 'sell_price' => 75, 'buy_price' => 50, 'is_active' => true, ]); // First API response: Claude requests the tool. // Second: Claude returns the final answer. Http::fakeSequence('api.anthropic.com/*') ->push([ 'content' => [ ['type' => 'text', 'text' => 'Caut piesa…'], ['type' => 'tool_use', 'id' => 'toolu_1', 'name' => 'find_parts', 'input' => ['query' => 'MANN']], ], 'stop_reason' => 'tool_use', 'usage' => ['input_tokens' => 100, 'output_tokens' => 20], 'model' => 'claude-sonnet-4-5', ]) ->push([ 'content' => [['type' => 'text', 'text' => 'Am găsit Filtru ulei MANN, stoc 5 buc la 75 MDL.']], 'stop_reason' => 'end_turn', 'usage' => ['input_tokens' => 120, 'output_tokens' => 35], 'model' => 'claude-sonnet-4-5', ]); $chat = AiChat::create([ 'user_id' => $ctx['user']->id, 'provider' => 'claude', ]); $reply = app(AiAssistantService::class)->ask($chat, 'Ai filtru de ulei MANN?'); $this->assertEquals('assistant', $reply->role); $this->assertStringContainsString('MANN', $reply->content); $this->assertStringContainsString('75', $reply->content); $this->assertEquals('find_parts', $reply->meta['tools'][0]['name']); $this->assertEquals(220, $reply->meta['tokens_in']); // 100 + 120 cumulated $this->assertEquals(55, $reply->meta['tokens_out']); // Verify both API rounds happened with tools defined. $rounds = 0; Http::assertSent(function ($req) use (&$rounds) { if (! str_contains($req->url(), 'api.anthropic.com')) return false; $body = json_decode($req->body(), true); $this->assertArrayHasKey('tools', $body); $this->assertCount(5, $body['tools']); $rounds++; return true; }); $this->assertEquals(2, $rounds); } public function test_executor_finds_parts_by_brand(): void { $this->makeCtx(); Part::create(['name' => 'X', 'brand' => 'MANN', 'qty' => 1, 'unit' => 'buc', 'sell_price' => 10, 'buy_price' => 5, 'is_active' => true]); Part::create(['name' => 'Y', 'brand' => 'Bosch', 'qty' => 1, 'unit' => 'buc', 'sell_price' => 10, 'buy_price' => 5, 'is_active' => true]); $r = app(AiToolExecutor::class)->execute('find_parts', ['query' => 'MANN']); $this->assertEquals(1, $r['count']); $this->assertEquals('X', $r['rows'][0]['name']); } public function test_executor_unknown_tool_returns_error(): void { $this->makeCtx(); $r = app(AiToolExecutor::class)->execute('does_not_exist', []); $this->assertArrayHasKey('error', $r); } private function makeCtx(): array { $plan = Plan::firstOrCreate(['slug' => 'test'], ['name' => 'T', 'price' => 0, 'features' => []]); $company = Company::create([ 'plan_id' => $plan->id, 'slug' => 'tool-' . uniqid(), 'name' => 'Tool Co', 'city' => 'Chișinău', 'status' => 'active', 'settings' => ['ai' => ['default_provider' => 'claude', 'claude_key' => 'sk-fake']], ]); app(TenantManager::class)->setCurrent($company); $user = User::create([ 'company_id' => $company->id, 'name' => 'Op', 'email' => 'op-' . uniqid() . '@example.com', 'password' => bcrypt('x'), 'role' => 'admin', 'status' => 'active', ]); $this->actingAs($user); return compact('company', 'user'); } }