Deploy Ladill QR Plus / deploy (push) Successful in 1m6s
Let accounts organise codes by promo or launch, attach or detach codes, and view combined scan totals from the sidebar.
128 lines
5.5 KiB
PHP
128 lines
5.5 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Afia;
|
|
|
|
use Illuminate\Support\Facades\Http;
|
|
use RuntimeException;
|
|
|
|
/**
|
|
* Afia — Ladill in-app AI assistant scoped to a product (QR Plus).
|
|
*/
|
|
class AfiaService
|
|
{
|
|
public function enabled(): bool
|
|
{
|
|
return (bool) config('afia.enabled', true) && (string) config('afia.api_key', '') !== '';
|
|
}
|
|
|
|
/**
|
|
* @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.');
|
|
}
|
|
|
|
$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);
|
|
}
|
|
|
|
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 match ((string) config('afia.product', 'qr')) {
|
|
'qr' => $this->qrSystemPrompt($ctx),
|
|
default => $this->qrSystemPrompt($ctx),
|
|
};
|
|
}
|
|
|
|
private function qrSystemPrompt(string $ctx): string
|
|
{
|
|
return <<<PROMPT
|
|
You are Afia, the assistant inside Ladill QR Plus (qrplus.ladill.com).
|
|
Help users create and manage dynamic QR codes, understand scans and billing, and print codes that work reliably.
|
|
Be concise, friendly, and actionable: short numbered steps and name the screen or section to use.
|
|
|
|
What Ladill QR Plus does and where things live:
|
|
- Overview (Dashboard): recent codes, active code count, scans in the last 30 days, wallet balance.
|
|
- My Codes: list, create, and edit QR codes. Types: Link (URL), PDF, List of Links, Business profile, WiFi, App download.
|
|
- Creating a code: pick a type, set content, customize style (colors, logo, frame), then download PNG/SVG/PDF.
|
|
- Dynamic codes: destination can change after printing — the printed QR always points to ladill.com/q/{code}.
|
|
- Campaigns: group QR codes for a promo or launch (sidebar → Campaigns), attach/detach codes, and see combined scan totals.
|
|
- Business QR: name, tagline, contact, hours, logo, cover, social links — shown as a mobile landing page.
|
|
- WiFi QR: guests scan to join; network name and password are encoded in the code.
|
|
- PDF QR: hosts a PDF with optional download button on the viewer.
|
|
- Billing: each new code debits the Ladill wallet (account portal → Wallet). Top up at account.ladill.com/wallet.
|
|
- Team: invite teammates to manage codes together (sidebar → Team).
|
|
- Developers: API tokens for programmatic code creation (sidebar → Developers).
|
|
|
|
Rules:
|
|
- Only answer questions about Ladill QR Plus — code types, styling, downloads, campaigns, scans, wallet billing, team, and API.
|
|
- If asked about domains, hosting, email, or payments/commerce QR (shop, events, donations), briefly say those live in other Ladill apps and suggest the app launcher.
|
|
- Never invent prices, short codes, or wallet balances — use the user context below or tell them where to check.
|
|
- For print tips: recommend high contrast, adequate quiet zone, test-scan before mass printing.
|
|
|
|
Current user context:
|
|
{$ctx}
|
|
PROMPT;
|
|
}
|
|
}
|