Files
ladill-woo-manager/app/Services/Afia/AfiaService.php
T
isaaccladandCursor 26b1d5b433
Deploy Ladill Woo Manager / deploy (push) Successful in 1m19s
Wire Afia AI assistant for Woo Manager via platform relay.
The chat panel and POST /ai/chat route were missing after the Invoice scaffold; Afia now relays through the platform when no local OpenAI key is set.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-07 01:23:21 +00:00

170 lines
6.1 KiB
PHP

<?php
namespace App\Services\Afia;
use Illuminate\Support\Facades\Http;
use RuntimeException;
class AfiaService
{
public function enabled(): bool
{
if (! (bool) config('afia.enabled', true)) {
return false;
}
return $this->hasLocalKey() || $this->hasPlatformRelay();
}
/**
* @param array<int, array{role: string, text: string}> $history
* @param array<string, mixed> $context
*/
public function chat(string $message, array $history, array $context): string
{
if (! $this->enabled()) {
throw new RuntimeException('Afia is not configured.');
}
if ($this->hasLocalKey()) {
return $this->chatLocally($message, $history, $context);
}
return $this->chatViaPlatform($message, $history, $context);
}
/**
* @param array<int, array{role: string, text: string}> $history
* @param array<string, mixed> $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');
$messages = [['role' => 'system', 'content' => $this->systemPrompt($context)]];
foreach (array_slice($history, -8) as $turn) {
$role = ($turn['role'] ?? 'user') === 'assistant' ? 'assistant' : 'user';
$text = trim((string) ($turn['text'] ?? ''));
if ($text !== '') {
$messages[] = ['role' => $role, 'content' => $text];
}
}
$messages[] = ['role' => 'user', 'content' => $message];
return $provider === 'anthropic'
? $this->viaAnthropic($model, $apiKey, $messages)
: $this->viaOpenAi($model, $apiKey, $messages);
}
/**
* @param array<int, array{role: string, text: string}> $history
* @param array<string, mixed> $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', 'woo'),
'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)
->post('https://api.openai.com/v1/chat/completions', [
'model' => $model,
'temperature' => 0.3,
'max_tokens' => 600,
'messages' => $messages,
]);
if ($res->failed()) {
throw new RuntimeException('OpenAI request failed: '.$res->status());
}
return trim((string) $res->json('choices.0.message.content', ''));
}
private function viaAnthropic(string $model, string $apiKey, array $messages): string
{
$system = $messages[0]['content'] ?? '';
$turns = array_values(array_filter($messages, fn ($m) => $m['role'] !== 'system'));
$res = Http::withHeaders([
'x-api-key' => $apiKey,
'anthropic-version' => '2023-06-01',
])->acceptJson()->timeout(45)->post('https://api.anthropic.com/v1/messages', [
'model' => $model,
'max_tokens' => 600,
'system' => $system,
'messages' => array_map(fn ($m) => ['role' => $m['role'], 'content' => $m['content']], $turns),
]);
if ($res->failed()) {
throw new RuntimeException('Anthropic request failed: '.$res->status());
}
return trim((string) $res->json('content.0.text', ''));
}
/** @param array<string, mixed> $context */
private function systemPrompt(array $context): string
{
$ctx = collect($context)->map(fn ($v, $k) => "- {$k}: {$v}")->implode("\n");
return <<<PROMPT
You are Afia, the assistant inside Ladill Woo Manager (woo.ladill.com).
Help merchants connect WooCommerce stores, fulfill orders, sync products and categories, and manage catalog content. Be concise, friendly, and actionable.
What Ladill Woo Manager does:
- Connect a WordPress/WooCommerce store via the Ladill plugin (Connect with Ladill).
- Switch between multiple stores from the top bar (Pro plan).
- Orders: sync and update fulfillment status; payments stay in WooCommerce.
- Products & categories: sync from the store, edit, and push changes back when the plugin is connected.
- Stores page: view connected sites, switch active store, disconnect.
- Free plan: 1 store and 20 products. Pro unlocks multiple stores and unlimited products.
Rules:
- Only answer questions about Ladill Woo Manager and WooCommerce fulfillment via Ladill.
- If asked about invoicing, POS, or email, say those live in other Ladill apps.
- Never invent order numbers or product counts — use the user context below or say where to check.
Current user context:
{$ctx}
PROMPT;
}
}