Warning: session_start(): Session cannot be started after headers have already been sent in /home/tvrreohg/public_html/manga.php on line 13
load('documents');
$session->load(['messages' => fn ($query) => $query->latest()->take(12)]);
$history = $session->messages
->sortBy('created_at')
->map(fn ($chat) => strtoupper($chat->role).": {$chat->content}")
->implode("\n\n");
$prompt = <<phase}
Case:
Title: {$case->title}
Category: {$case->category}
Summary: {$case->summary}
Timeline: {$case->timeline}
People involved: {$case->people_involved}
Questions: {$case->questions}
Concerns: {$case->concerns}
Uploaded documents with extracted text:
{$this->documentContext($case)}
Recent conversation:
{$history}
Client message:
{$message}
PROMPT;
return $this->generate($prompt, $case);
}
public function packet(CaseRecord $case): array
{
$disclaimer = self::DISCLAIMER;
$case->load('documents');
$prompt = <<title}
Category: {$case->category}
Summary: {$case->summary}
Timeline: {$case->timeline}
People involved: {$case->people_involved}
Questions: {$case->questions}
Concerns: {$case->concerns}
Uploaded documents:
{$this->documentContext($case)}
PROMPT;
return $this->generate($prompt, $case);
}
public function explainDocument(CaseDocument $document): array
{
$disclaimer = self::DISCLAIMER;
$text = $document->extracted_text ?: 'No readable text was extracted from this document. If it is an image or scan, describe only what can be safely inferred from the file name and category.';
$prompt = <<original_name}
Document category: {$document->category}
Extracted text:
{$text}
PROMPT;
return $this->generate($prompt, $document->caseRecord);
}
private function documentContext(CaseRecord $case): string
{
return $case->documents
->map(function ($document) {
$text = filled($document->extracted_text)
? mb_substr($document->extracted_text, 0, 12000)
: 'No extracted text available.';
return "DOCUMENT: {$document->original_name}\nCategory: {$document->category}\nPlain summary: {$document->plain_language_summary}\nText: {$text}";
})
->implode("\n\n");
}
private function generate(string $prompt, ?CaseRecord $case = null): array
{
$key = config('services.gemini.key');
$model = config('services.gemini.model');
if (! $key) {
throw new RuntimeException('Gemini is not configured. Add GEMINI_API_KEY to the environment.');
}
$parts = [['text' => mb_substr($prompt, 0, 180000)]];
$imageBytes = 0;
if ($case) {
foreach ($case->documents->filter(fn ($document) => str_starts_with((string) $document->mime_type, 'image/'))->take(3) as $image) {
if (! Storage::disk($image->disk)->exists($image->path) || $image->size > 5 * 1024 * 1024) {
continue;
}
$imageBytes += $image->size;
if ($imageBytes > 8 * 1024 * 1024) {
break;
}
$parts[] = [
'inline_data' => [
'mime_type' => $image->mime_type,
'data' => base64_encode(Storage::disk($image->disk)->get($image->path)),
],
];
}
}
try {
$response = Http::withHeaders(['x-goog-api-key' => $key])
->acceptJson()
->timeout(120)
->post("https://generativelanguage.googleapis.com/v1beta/models/{$model}:generateContent", [
'contents' => [[
'role' => 'user',
'parts' => $parts,
]],
'generationConfig' => [
'temperature' => 0.2,
'maxOutputTokens' => 2400,
],
])
->throw()
->json();
} catch (RequestException $exception) {
$status = $exception->response->status();
$providerStatus = $exception->response->json('error.status');
$message = match (true) {
in_array($status, [401, 403], true) || $providerStatus === 'PERMISSION_DENIED' => 'The Gemini API key is invalid or does not have access to this model.',
$status === 429 || $providerStatus === 'RESOURCE_EXHAUSTED' => 'The Gemini API rate or free-tier limit has been reached. Please try again later.',
default => 'The AI service could not complete this request. Please try again later.',
};
throw new RuntimeException($message, previous: $exception);
}
$text = collect(data_get($response, 'candidates.0.content.parts', []))
->pluck('text')
->filter()
->implode("\n");
if (! $text) {
$blockReason = data_get($response, 'promptFeedback.blockReason');
throw new RuntimeException($blockReason
? 'Gemini could not process this request because of its content safety settings.'
: 'The AI service returned an empty response.');
}
return [
'content' => $text,
'model' => data_get($response, 'modelVersion', $model),
'input_tokens' => data_get($response, 'usageMetadata.promptTokenCount'),
'output_tokens' => data_get($response, 'usageMetadata.candidatesTokenCount'),
];
}
}