From 7f51392857929aa7966a2fe96c11dd632113b498 Mon Sep 17 00:00:00 2001 From: isaacclad Date: Sun, 28 Jun 2026 07:45:39 +0000 Subject: [PATCH] Route Afia through the platform AI relay when no local API key is set. Frontdesk now calls ladill.com/api/afia/chat using IDENTITY_API_KEY_FRONTDESK so production does not need a separate OpenAI key in the frontdesk .env. Co-authored-by: Cursor --- .env.example | 6 +++ app/Services/Afia/AfiaService.php | 64 ++++++++++++++++++++++++++++- config/afia.php | 4 ++ resources/js/app.js | 6 ++- tests/Feature/FrontdeskAfiaTest.php | 30 +++++++++++++- 5 files changed, 106 insertions(+), 4 deletions(-) diff --git a/.env.example b/.env.example index 263d131..a88bfeb 100644 --- a/.env.example +++ b/.env.example @@ -31,6 +31,12 @@ BILLING_API_KEY_FRONTDESK= IDENTITY_API_URL=https://ladill.com/api IDENTITY_API_KEY_FRONTDESK= +# Afia AI assistant (uses platform relay via IDENTITY_API_KEY when AFIA_API_KEY is unset) +AFIA_ENABLED=true +# AFIA_API_KEY= +# AFIA_PLATFORM_API_URL=https://ladill.com/api +# AFIA_PLATFORM_API_KEY= + # Platform events webhook (user/org lifecycle from the monolith) SERVICE_EVENTS_INBOUND_SECRET= diff --git a/app/Services/Afia/AfiaService.php b/app/Services/Afia/AfiaService.php index 9c5893b..09c6fbb 100644 --- a/app/Services/Afia/AfiaService.php +++ b/app/Services/Afia/AfiaService.php @@ -12,7 +12,11 @@ class AfiaService { public function enabled(): bool { - return (bool) config('afia.enabled', true) && (string) config('afia.api_key', '') !== ''; + if (! (bool) config('afia.enabled', true)) { + return false; + } + + return $this->hasLocalKey() || $this->hasPlatformRelay(); } /** @@ -25,6 +29,19 @@ class AfiaService throw new RuntimeException('Afia is not configured.'); } + if ($this->hasLocalKey()) { + return $this->chatLocally($message, $history, $context); + } + + return $this->chatViaPlatform($message, $history, $context); + } + + /** + * @param array $history + * @param array $context + */ + private function chatLocally(string $message, array $history, array $context): string + { $provider = (string) config('afia.provider', 'openai'); $model = (string) config('afia.model', 'gpt-4o-mini'); $apiKey = (string) config('afia.api_key'); @@ -44,6 +61,49 @@ class AfiaService : $this->viaOpenAi($model, $apiKey, $messages); } + /** + * @param array $history + * @param array $context + */ + private function chatViaPlatform(string $message, array $history, array $context): string + { + $base = rtrim((string) config('afia.platform_api_url', ''), '/'); + $token = (string) config('afia.platform_api_key', ''); + + $res = Http::withToken($token)->acceptJson()->timeout(50)->post($base.'/afia/chat', [ + 'product' => (string) config('afia.product', 'frontdesk'), + 'message' => $message, + 'history' => $history, + 'system_prompt' => $this->systemPrompt($context), + ]); + + if ($res->status() === 503) { + throw new RuntimeException('Afia is not configured on the platform.'); + } + + if ($res->failed()) { + throw new RuntimeException('Platform Afia relay failed: '.$res->status()); + } + + $reply = trim((string) $res->json('reply', '')); + if ($reply === '') { + throw new RuntimeException('Platform Afia relay returned an empty response.'); + } + + return $reply; + } + + private function hasLocalKey(): bool + { + return (string) config('afia.api_key', '') !== ''; + } + + private function hasPlatformRelay(): bool + { + return rtrim((string) config('afia.platform_api_url', ''), '/') !== '' + && (string) config('afia.platform_api_key', '') !== ''; + } + private function viaOpenAi(string $model, string $apiKey, array $messages): string { $res = Http::withToken($apiKey)->acceptJson()->timeout(45) @@ -63,7 +123,7 @@ class AfiaService private function viaAnthropic(string $model, string $apiKey, array $messages): string { - $system = $messages[0]['content']; + $system = $messages[0]['content'] ?? ''; $turns = array_values(array_filter($messages, fn ($m) => $m['role'] !== 'system')); $res = Http::withHeaders([ diff --git a/config/afia.php b/config/afia.php index ae3789c..3d2af5c 100644 --- a/config/afia.php +++ b/config/afia.php @@ -6,5 +6,9 @@ return [ 'enabled' => (bool) env('AFIA_ENABLED', true), 'provider' => env('AFIA_PROVIDER', 'openai'), // openai | anthropic 'model' => env('AFIA_MODEL', 'gpt-4o-mini'), + // Optional direct provider key. When empty, Afia uses the platform relay below. 'api_key' => env('AFIA_API_KEY', env('OPENAI_API_KEY')), + // Platform relay — uses the monolith's configured AI keys (same pattern as Billing). + 'platform_api_url' => env('AFIA_PLATFORM_API_URL', env('IDENTITY_API_URL', 'https://ladill.com/api')), + 'platform_api_key' => env('AFIA_PLATFORM_API_KEY', env('IDENTITY_API_KEY_FRONTDESK')), ]; diff --git a/resources/js/app.js b/resources/js/app.js index 1410c37..1af3fdd 100644 --- a/resources/js/app.js +++ b/resources/js/app.js @@ -110,7 +110,11 @@ Alpine.data('afia', (config = {}) => ({ body: JSON.stringify({ message: text, history }), }); const data = await res.json(); - this.messages.push({ role: 'assistant', text: data.reply || data.message || 'Sorry, I could not respond.' }); + if (! res.ok) { + this.messages.push({ role: 'assistant', text: data.message || data.reply || 'Sorry, I could not respond.' }); + } else { + this.messages.push({ role: 'assistant', text: data.reply || data.message || 'Sorry, I could not respond.' }); + } } catch (e) { this.messages.push({ role: 'assistant', text: 'Network error — please try again.' }); } diff --git a/tests/Feature/FrontdeskAfiaTest.php b/tests/Feature/FrontdeskAfiaTest.php index 528f6d9..b2aeb7e 100644 --- a/tests/Feature/FrontdeskAfiaTest.php +++ b/tests/Feature/FrontdeskAfiaTest.php @@ -8,6 +8,7 @@ use App\Models\Organization; use App\Models\User; use App\Services\Afia\AfiaService; use Illuminate\Foundation\Testing\RefreshDatabase; +use Illuminate\Support\Facades\Http; use Tests\TestCase; class FrontdeskAfiaTest extends TestCase @@ -75,7 +76,11 @@ class FrontdeskAfiaTest extends TestCase public function test_afia_chat_returns_service_unavailable_when_disabled(): void { - config(['afia.enabled' => false, 'afia.api_key' => '']); + config([ + 'afia.enabled' => false, + 'afia.api_key' => '', + 'afia.platform_api_key' => '', + ]); $this->actingAs($this->user) ->postJson(route('frontdesk.ai.chat'), [ @@ -83,4 +88,27 @@ class FrontdeskAfiaTest extends TestCase ]) ->assertStatus(503); } + + public function test_afia_chat_uses_platform_relay_when_local_key_missing(): void + { + config([ + 'afia.enabled' => true, + 'afia.api_key' => '', + 'afia.platform_api_url' => 'https://platform.test/api', + 'afia.platform_api_key' => 'frontdesk-relay-key', + ]); + + Http::fake([ + 'platform.test/api/afia/chat' => Http::response([ + 'reply' => 'Open Devices and register a Visitor Kiosk.', + ]), + ]); + + $this->actingAs($this->user) + ->postJson(route('frontdesk.ai.chat'), [ + 'message' => 'How do I set up a kiosk?', + ]) + ->assertOk() + ->assertJsonPath('reply', 'Open Devices and register a Visitor Kiosk.'); + } }