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'); $startTime = microtime(true); $language = 'en'; $creditDeducted = false; $ftUid = 0; $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 { // ── Parse input (JSON or multipart) ───────────────────────────────────────── $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) > 200000) { 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'); } } // ── Normalise + validate ──────────────────────────────────────────────────── $language = dbnToolsNormalizeLanguage($input['language'] ?? 'en'); $allowedBodies = ['barnehage','school_1_10','sfo','nav','bufdir','barnevernet', 'kommune_other','statsforvalter','trygderetten','tingrett','other']; $allowedOutput = ['email','formal','filing','call_prep']; $allowedTone = ['cooperative','neutral','firm','adversarial','warm']; $allowedMode = ['reply','initiate']; $intake = [ 'mode' => in_array($input['mode'] ?? '', $allowedMode, true) ? $input['mode'] : 'initiate', 'recipient_body' => in_array($input['recipient_body'] ?? '', $allowedBodies, true) ? $input['recipient_body'] : 'other', 'output_type' => in_array($input['output_type'] ?? '', $allowedOutput, true) ? $input['output_type'] : 'email', 'tone' => in_array($input['tone'] ?? '', $allowedTone, true) ? $input['tone'] : 'neutral', 'language' => $language, 'case_ref' => mb_substr(trim((string)($input['case_ref'] ?? '')), 0, 120, 'UTF-8'), 'where' => mb_substr(trim((string)($input['where'] ?? '')), 0, 200, 'UTF-8'), 'parties_text' => mb_substr(trim((string)($input['parties_text'] ?? '')), 0, 2000, 'UTF-8'), 'narrative' => mb_substr(trim((string)($input['narrative'] ?? '')), 0, 8000, 'UTF-8'), 'goal' => mb_substr(trim((string)($input['goal'] ?? '')), 0, 600, 'UTF-8'), 'deadlines' => is_array($input['deadlines'] ?? null) ? array_slice(array_values(array_filter(array_map( fn($d) => mb_substr(trim((string)$d), 0, 60, 'UTF-8'), $input['deadlines'] ))), 0, 6) : [], 'clarifications' => is_array($input['clarifications'] ?? null) ? $input['clarifications'] : [], ]; $forceDraft = !empty($input['force_draft']); $hasClarif = !empty($intake['clarifications']); // ── Extract uploaded files (Reply mode) ───────────────────────────────────── $receivedText = ''; $attachmentsText = ''; $attachmentNames = []; if (!empty($_FILES['files']) && is_array($_FILES['files']['tmp_name'] ?? null)) { $count = count($_FILES['files']['tmp_name']); if ($count > 4) { throw new DbnToolsHttpException('At most 4 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); $attachmentNames[] = $extracted['filename']; if ($i === 0) { $receivedText = $extracted['text']; } else { $attachmentsText .= "\n\n--- " . $extracted['filename'] . " ---\n\n" . $extracted['text']; } $emit('progress', [ 'detail' => DbnKorrespondAgent::L('file_read', $language, [ 'name' => $extracted['filename'], 'chars' => $extracted['chars'], ]), ]); } } $intake['received_text'] = $receivedText; $intake['attachments_text'] = $attachmentsText; if ($intake['mode'] === 'reply' && $receivedText === '' && $intake['narrative'] === '') { throw new DbnToolsHttpException( 'For reply mode, upload the received letter or paste its text.', 422, 'reply_no_input' ); } if ($intake['mode'] === 'initiate' && $intake['narrative'] === '') { throw new DbnToolsHttpException( 'Describe the situation in "What happened" before drafting.', 422, 'initiate_no_narrative' ); } $emit('start', [ 'mode' => $intake['mode'], 'body' => $intake['recipient_body'], 'output_type' => $intake['output_type'], 'language' => $language, 'attachments' => $attachmentNames, ]); // ── Pass 1: classify + gap-check ──────────────────────────────────────────── $emit('progress', ['detail' => DbnKorrespondAgent::L('analyzing', $language)]); $agent = new DbnKorrespondAgent(); $classify = $agent->classify($intake); $emit('classify', ['result' => [ 'summary' => $classify['summary'], 'parties' => $classify['parties'], 'decision_or_action' => $classify['decision_or_action'], 'deadlines' => $classify['deadlines'], 'applicable_acts' => $classify['applicable_acts'], 'jurisdiction' => $classify['jurisdiction'], 'suggested_goal' => $classify['suggested_goal'], ]]); $missing = $classify['missing_facts'] ?? []; if (!empty($missing) && !$forceDraft && !$hasClarif) { // Gate: emit clarify and stop — NO credit deduct $emit('clarify', ['questions' => $missing]); $emit('end', ['stopped_at' => 'clarify']); exit; } // ── Deduct credit now (Pass 2 starts) ─────────────────────────────────────── $ftUid = dbnToolsFreeTierCheck('korrespond'); $ftRemaining = dbnToolsFreeTierDeduct($ftUid, 'korrespond'); $creditDeducted = true; if ($ftRemaining >= 0) { header('X-Credits-Remaining: ' . $ftRemaining); } // ── Pass 2: retrieve law → draft → self-check → translate ────────────────── $result = $agent->generate($intake, $classify, $emit); $result['ok'] = true; $result['latency_ms'] = (int)round((microtime(true) - $startTime) * 1000); dbnToolsLogMetadata([ 'tool' => 'korrespond', 'language' => $language, 'ok' => true, 'latency_ms' => $result['latency_ms'], 'source_count' => is_array($result['cited_law'] ?? null) ? count($result['cited_law']) : 0, 'deployment' => 'gpt-4o', ]); $emit('final', ['result' => $result]); } catch (DbnToolsHttpException $e) { $latency = (int)round((microtime(true) - $startTime) * 1000); dbnToolsLogMetadata([ 'tool' => 'korrespond', '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('Korrespond fatal: ' . $e->getMessage()); $latency = (int)round((microtime(true) - $startTime) * 1000); dbnToolsLogMetadata([ 'tool' => 'korrespond', 'language' => $language, 'ok' => false, 'latency_ms' => $latency, 'error_code' => 'internal_error', ]); $emit('error', ['code' => 'internal_error', 'message' => 'Korrespond could not complete this request.']); }