0) { @ob_end_clean(); } ob_implicit_flush(true); header('Content-Type: application/x-ndjson; charset=utf-8'); header('Cache-Control: no-store'); header('X-Accel-Buffering: no'); $language = 'en'; $startTime = microtime(true); $emit = function (string $event, array $payload = []) use ($startTime): void { $payload['event'] = $event; $payload['t_ms'] = (int)round((microtime(true) - $startTime) * 1000); echo json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . "\n"; @flush(); }; try { $isMultipart = stripos((string)($_SERVER['CONTENT_TYPE'] ?? ''), 'multipart/form-data') !== false; if ($isMultipart) { $payloadRaw = (string)($_POST['payload'] ?? ''); if ($payloadRaw === '') { throw new DbnToolsHttpException('Multipart request missing payload.', 422, 'missing_payload'); } $input = json_decode($payloadRaw, true); if (!is_array($input)) { throw new DbnToolsHttpException('Invalid payload JSON.', 422, 'invalid_payload_json'); } } else { $raw = file_get_contents('php://input'); if ($raw === false || strlen($raw) > 120000) { throw new DbnToolsHttpException('Request body unreadable or too large.', 413, 'body_too_large'); } $input = json_decode((string)$raw, true); if (!is_array($input)) { throw new DbnToolsHttpException('Request body must be valid JSON.', 400, 'invalid_json'); } } $language = dbnToolsNormalizeLanguage($input['language'] ?? 'en'); $advocateRole = trim((string)($input['advocate_role'] ?? '')); if (isset($input['tier'])) { $run = ToolModels::resolveTier(dbnToolsFreeTierUid(), 'barnevernet', (string)$input['tier']); $engine = $run['engine']; $tierCredits = $run['credits']; $tierMeta = ['tier' => $run['tier'], 'engine' => $engine]; if ($ftUid > 0) { $gate = FreeTier::checkAmount($ftUid, 'barnevernet', $tierCredits); if (empty($gate['ok'])) { $emit('error', ['code' => $gate['reason'] ?? 'no_credits', 'message' => 'Insufficient credits for the selected tier.']); exit; } } } else { $engine = ToolModels::engineForUser($ftUid, (string)($input['engine'] ?? 'azure_mini')); $tierCredits = null; $tierMeta = []; } $sliceInput = $input['slices'] ?? []; $controls = is_array($input['controls'] ?? null) ? $input['controls'] : []; $additionalNotes = mb_substr(dbnToolsInjectDocContent($input, trim((string)($input['additional_notes'] ?? ''))), 0, 8000, 'UTF-8'); if (mb_strlen($advocateRole, 'UTF-8') > 200) { throw new DbnToolsHttpException('advocate_role is too long.', 422, 'advocate_role_too_long'); } if (mb_strlen($additionalNotes, 'UTF-8') > 8000) { throw new DbnToolsHttpException('additional_notes is too long.', 422, 'notes_too_long'); } $emit('progress', ['detail' => 'Reading upload(s)…']); $uploadedFiles = []; if (!empty($_FILES['files']) && is_array($_FILES['files']['tmp_name'] ?? null)) { $count = count($_FILES['files']['tmp_name']); if ($count > 5) { throw new DbnToolsHttpException('At most 5 files can be uploaded per request.', 413, 'too_many_files'); } for ($i = 0; $i < $count; $i++) { $file = [ 'name' => $_FILES['files']['name'][$i] ?? '', 'type' => $_FILES['files']['type'][$i] ?? '', 'tmp_name' => $_FILES['files']['tmp_name'][$i] ?? '', 'error' => $_FILES['files']['error'][$i] ?? UPLOAD_ERR_NO_FILE, 'size' => $_FILES['files']['size'][$i] ?? 0, ]; $extracted = dbnToolsExtractUploadedFile($file); $uploadedFiles[] = [ 'filename' => $extracted['filename'], 'text' => $extracted['text'], 'chars' => $extracted['chars'], 'truncated' => $extracted['truncated'], ]; $emit('progress', [ 'detail' => sprintf('Extracted %s (%d chars%s)', $extracted['filename'], $extracted['chars'], !empty($extracted['truncated']) ? ', truncated' : '' ), ]); } } if (empty($uploadedFiles)) { throw new DbnToolsHttpException( 'Upload at least one BVJ document (PDF, DOCX, or TXT) before running the analyzer.', 422, 'no_uploads' ); } $emit('start', [ 'engine' => $engine, 'language' => $language, 'file_count' => count($uploadedFiles), ]); // Optional: append user's case-context chunks to the last uploaded document text, // so the agent reads them as supplementary background. $useMyCase = !empty($input['use_my_case']); if ($useMyCase && !empty($uploadedFiles)) { $retrievalQuery = mb_substr((string)$uploadedFiles[0]['text'], 0, 2000, 'UTF-8'); $caseBlock = dbnToolsCaseContext(true, $retrievalQuery, 5); if ($caseBlock !== '') { $uploadedFiles[0]['text'] .= "\n\n" . $caseBlock; } } $personaSlug = (isset($input['profile']) && is_string($input['profile']) && trim($input['profile']) !== '') ? trim($input['profile']) : null; $result = (new DbnBvjAnalyzerAgent()) ->withPersona($personaSlug) ->run( $uploadedFiles, $advocateRole, $engine, $language, is_array($sliceInput) ? $sliceInput : [], $controls, $additionalNotes, $emit ); $result['ok'] = true; $result['latency_ms'] = (int)round((microtime(true) - $startTime) * 1000); dbnToolsLogMetadata([ 'tool' => 'bvj_analyzer', 'language' => $language, 'ok' => true, 'latency_ms' => $result['latency_ms'], 'chunk_count' => (int)($result['trace_metadata']['chunk_count'] ?? 0), 'source_count' => (int)($result['trace_metadata']['source_count'] ?? 0), 'deployment' => $result['trace_metadata']['deployment'] ?? null, 'advocate_role' => $advocateRole !== '' ? $advocateRole : null, 'bvj_doc_type' => $result['doc_meta']['doc_type'] ?? null, ]); $ftRemaining = $tierCredits === null ? dbnToolsFreeTierDeduct($ftUid, 'barnevernet') : dbnToolsFreeTierDeductAmount($ftUid, 'barnevernet', $tierCredits, $tierMeta); if ($ftRemaining >= 0) { $result['balance'] = $ftRemaining; } $emit('final', ['result' => $result]); } catch (DbnToolsHttpException $e) { $latency = (int)round((microtime(true) - $startTime) * 1000); dbnToolsLogMetadata([ 'tool' => 'bvj_analyzer', 'language' => $language, 'ok' => false, 'latency_ms' => $latency, 'error_code' => $e->errorCode, ]); $emit('error', ['code' => $e->errorCode, 'message' => $e->getMessage(), 'status' => $e->status]); } catch (Throwable $e) { error_log('DBN BVJ analyzer fatal: ' . $e->getMessage()); $latency = (int)round((microtime(true) - $startTime) * 1000); dbnToolsLogMetadata([ 'tool' => 'bvj_analyzer', 'language' => $language, 'ok' => false, 'latency_ms' => $latency, 'error_code' => 'internal_error', ]); $emit('error', ['code' => 'internal_error', 'message' => 'The analyzer could not complete this request.']); }