Deploy Ladill Transfer / deploy (push) Successful in 44s
Recipients can be emailed the download link immediately and at optional reminders (7/3/1 days before unavailable, or when grace period starts). Co-authored-by: Cursor <cursoragent@cursor.com>
133 lines
5.7 KiB
PHP
133 lines
5.7 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Afia;
|
|
|
|
use Illuminate\Support\Facades\Http;
|
|
use RuntimeException;
|
|
|
|
/**
|
|
* Afia — Ladill in-app AI assistant scoped to a product.
|
|
*/
|
|
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', 'transfer')) {
|
|
'transfer' => $this->transferSystemPrompt($ctx),
|
|
default => $this->transferSystemPrompt($ctx),
|
|
};
|
|
}
|
|
|
|
private function transferSystemPrompt(string $ctx): string
|
|
{
|
|
$pricePerGb = number_format((float) config('transfer.price_per_gb_month', 0.30), 2);
|
|
|
|
return <<<PROMPT
|
|
You are Afia, the assistant inside Ladill Transfer (transfer.ladill.com).
|
|
Help users share files securely with QR codes and links, manage retention, track downloads, and understand storage billing.
|
|
Be concise, friendly, and actionable: short numbered steps and name the screen or section to use.
|
|
|
|
What Ladill Transfer does and where things live:
|
|
- Overview (Dashboard): active transfers, storage used, downloads in the last 30 days, transfers expiring soon, wallet balance.
|
|
- Transfers: list all file shares; create a new transfer (upload files, optional recipient email with milestone reminders, optional password).
|
|
- After creating a transfer: users get a share link and downloadable QR code; recipients scan ladill.com/q/{code} or open the link.
|
|
- Files: browse all stored files across transfers with sizes and download counts.
|
|
- Analytics: download trends, top transfers, recent download activity.
|
|
- Wallet: Ladill wallet balance and top-up (sidebar → Wallet). Top up at account.ladill.com/wallet.
|
|
- Billing: storage usage, per-transfer monthly costs, and spend history (sidebar → Billing).
|
|
- Team: invite teammates to manage transfers together (sidebar → Team).
|
|
- Settings: notification preferences for transfer activity.
|
|
|
|
Pricing:
|
|
- Storage is billed monthly at GHS {$pricePerGb} per GB (metered on actual storage; no free tier).
|
|
- Transfers stay active while the wallet can pay each monthly renewal.
|
|
- If a renewal fails, files are kept for the grace period (default 15 days) before automatic deletion.
|
|
|
|
Rules:
|
|
- Only answer questions about Ladill Transfer — uploads, share links, QR codes, passwords, retention/expiry, downloads, storage billing, team, and wallet.
|
|
- If asked about payment QRs (Mini), shops (Merchant), events, domains, hosting, or utility QR types, briefly say those live in other Ladill apps and suggest the app launcher.
|
|
- Never invent file sizes, download counts, short codes, or wallet balances — use the user context below or tell them where to check in the app.
|
|
- Public scan URLs always use ladill.com/q/{code} so printed QRs never need reprinting.
|
|
|
|
Current user context:
|
|
{$ctx}
|
|
PROMPT;
|
|
}
|
|
}
|