'system', 'content' => $system],
+ ['role' => 'user', 'content' => $preRedacted],
+ ];
+ $chatOptions = ['temperature' => 0.1, 'max_tokens' => 8000, 'json' => true, 'timeout' => 90];
+
try {
- $response = $this->azure->chat([
- ['role' => 'system', 'content' => $system],
- ['role' => 'user', 'content' => $preRedacted],
- ], [
- 'temperature' => 0.1,
- 'max_tokens' => 8000,
- 'json' => true,
- 'timeout' => 90,
- ]);
+ if ($engine === 'gpu') {
+ $response = $this->callGpuLlm($messages, $chatOptions);
+ $deployLabel = 'GPU (cuttlefish)';
+ } elseif ($engine === 'azure_full') {
+ $response = $this->azure->withDeployment('gpt-4o')->chat($messages, $chatOptions);
+ $deployLabel = 'gpt-4o';
+ } else {
+ $response = $this->azure->chat($messages, $chatOptions);
+ $deployLabel = $this->azure->chatDeployment();
+ }
$content = (string)($response['choices'][0]['message']['content'] ?? '');
$json = $this->azure->decodeJsonObject($content);
@@ -869,7 +963,7 @@ PROMPT;
return [
'skipped' => false,
'entities' => is_array($json['redactions']) ? $json['redactions'] : [],
- 'deployment' => $this->azure->chatDeployment(),
+ 'deployment' => $deployLabel,
];
} catch (Throwable $e) {
error_log('DBN tools LLM redaction pass failed: ' . $e->getMessage());
@@ -877,6 +971,150 @@ PROMPT;
}
}
+ private function callGpuLlm(array $messages, array $options = []): array
+ {
+ $url = 'http://10.0.1.10:4000/v1/chat/completions';
+ $apiKey = 'sk-bnl-litellm-26xR9mK4qvN3wL8sTj7pB2d';
+ $model = 'qwen2.5:14b';
+ $timeout = (int)($options['timeout'] ?? 90);
+
+ $payload = [
+ 'model' => $model,
+ 'messages' => $messages,
+ 'temperature' => $options['temperature'] ?? 0.1,
+ 'max_tokens' => $options['max_tokens'] ?? 8000,
+ ];
+ if (!empty($options['json'])) {
+ $payload['response_format'] = ['type' => 'json_object'];
+ }
+
+ $body = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
+ $headers = [
+ 'Content-Type: application/json',
+ 'Authorization: Bearer ' . $apiKey,
+ ];
+
+ if (function_exists('curl_init')) {
+ $ch = curl_init($url);
+ curl_setopt_array($ch, [
+ CURLOPT_RETURNTRANSFER => true,
+ CURLOPT_POST => true,
+ CURLOPT_POSTFIELDS => $body,
+ CURLOPT_HTTPHEADER => $headers,
+ CURLOPT_TIMEOUT => $timeout,
+ ]);
+ $response = curl_exec($ch);
+ $code = (int)curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
+ $err = curl_error($ch);
+ curl_close($ch);
+
+ if ($response === false) {
+ throw new RuntimeException('GPU LiteLLM request failed: ' . $err);
+ }
+ } else {
+ $ctx = stream_context_create(['http' => [
+ 'method' => 'POST',
+ 'header' => implode("\r\n", $headers),
+ 'content' => $body,
+ 'timeout' => $timeout,
+ 'ignore_errors' => true,
+ ]]);
+ $response = @file_get_contents($url, false, $ctx);
+ $code = 0;
+ if (isset($http_response_header[0]) && preg_match('/\s(\d{3})\s/', $http_response_header[0], $m)) {
+ $code = (int)$m[1];
+ }
+ if ($response === false) {
+ throw new RuntimeException('GPU LiteLLM request failed.');
+ }
+ }
+
+ $decoded = json_decode($response, true);
+ if (!is_array($decoded)) {
+ throw new RuntimeException('GPU LiteLLM returned non-JSON response.');
+ }
+ if ($code < 200 || $code >= 300) {
+ $msg = $decoded['error']['message'] ?? ('HTTP ' . $code);
+ throw new RuntimeException('GPU LiteLLM error: ' . $msg);
+ }
+ return $decoded;
+ }
+
+ private function applyGenericTags(string $text): string
+ {
+ // Collapse contextual role tags (e.g. [FATHER], [JUDGE: Andersen], [CHILD_1]) → [PERSON]
+ $text = preg_replace('/\[(?:FATHER|MOTHER|CHILD(?:_\d+)?|GRANDPARENT|SIBLING|ATTORNEY|JUDGE(?::\s*[^\]]+)?|CASEWORKER(?::\s*[^\]]+)?|EXPERT_WITNESS(?::\s*[^\]]+)?|PERSON(?:_\d+)?)\]/u', '[PERSON]', $text) ?? $text;
+ return $text;
+ }
+
+ private function applyPseudonymization(string $text, array $allCounts): string
+ {
+ $norwegianNames = [
+ 'Ola Nordmann', 'Per Hansen', 'Kari Larsen', 'Anne Berg', 'Erik Dahl',
+ 'Ingrid Holm', 'Lars Moen', 'Silje Bakke', 'Tor Haugen', 'Eva Strand',
+ ];
+ $nameCursor = 0;
+ $phoneBase = 1;
+ $emailCursor = 0;
+ $addrCursor = 1;
+ $orgCursor = 1;
+ $personMap = [];
+
+ // Replace named role tags (keeping consistent mapping per unique tag)
+ $text = preg_replace_callback(
+ '/\[(FATHER|MOTHER|CHILD(?:_\d+)?|GRANDPARENT|SIBLING|ATTORNEY|JUDGE(?::\s*[^\]]+)?|CASEWORKER(?::\s*[^\]]+)?|EXPERT_WITNESS(?::\s*[^\]]+)?|PERSON(?:_\d+)?)\]/u',
+ function (array $m) use (&$nameCursor, &$personMap, $norwegianNames): string {
+ $key = $m[1];
+ if (!isset($personMap[$key])) {
+ $personMap[$key] = $norwegianNames[$nameCursor % count($norwegianNames)];
+ $nameCursor++;
+ }
+ return $personMap[$key];
+ },
+ $text
+ ) ?? $text;
+
+ $text = preg_replace_callback('/\[PHONE\]/', function () use (&$phoneBase): string {
+ return sprintf('+47 400 00 %03d', $phoneBase++);
+ }, $text) ?? $text;
+
+ $text = preg_replace_callback('/\[EMAIL\]/', function () use (&$emailCursor): string {
+ $letter = chr(ord('a') + ($emailCursor % 26));
+ $emailCursor++;
+ return "person.{$letter}@example.no";
+ }, $text) ?? $text;
+
+ $text = preg_replace_callback('/\[ADDRESS\]/', function () use (&$addrCursor): string {
+ return "Eksempelveien {$addrCursor}, 0001 Oslo";
+ }, $text) ?? $text;
+
+ $text = preg_replace_callback('/\[ORG\]/', function () use (&$orgCursor): string {
+ return "Eksempel AS ({$orgCursor})";
+ }, $text) ?? $text;
+
+ $text = preg_replace_callback('/\[FNR\]/', function (): string {
+ return '010100XXXXX';
+ }, $text) ?? $text;
+
+ $text = preg_replace_callback('/\[(?:SE_PERSONNUMMER|FR_INSEE|UK_NI|SSN|NAT_ID|DOC_NO|ECHR_APP_NO)\]/', function (): string {
+ return '[ID-REDACTED]';
+ }, $text) ?? $text;
+
+ $text = preg_replace_callback('/\[PLACE\]/', function (): string {
+ return 'Eksempelby';
+ }, $text) ?? $text;
+
+ $text = preg_replace_callback('/\[DOB\]/', function (): string {
+ return '01.01.0000';
+ }, $text) ?? $text;
+
+ $text = preg_replace_callback('/\[IBAN\]/', function (): string {
+ return 'NO00 0000 00 00000';
+ }, $text) ?? $text;
+
+ return $text;
+ }
+
private function uncertaintySummary(mixed $uncertainty): string
{
if (is_array($uncertainty)) {
diff --git a/redact.php b/redact.php
index 8429b27..81fdd5c 100644
--- a/redact.php
+++ b/redact.php
@@ -6,5 +6,124 @@ $toolKind = 'Redaction Assistant';
$toolBadge = 'deterministic first';
require_once __DIR__ . '/includes/layout.php';
?>
-
+
+
+
+
+
Ready
+
Paste text or upload a file, configure redaction options, then run.
+
+
+
+
+
+
+
+
+
+
+
+
+