From 47d7db6d8b08e1f09c078c7d4e0f43e03e4f065d Mon Sep 17 00:00:00 2001 From: DevSoftTech Server Date: Wed, 18 Mar 2026 06:18:52 +0530 Subject: [PATCH] refactor: remove Gemini integration and seed leakage paths - Remove Gemini runtime fallback from OPD AI text generation and complaint drafting - Remove legacy Gemini scan extraction path - Update UI/provider labels to Azure/AI generic naming - Make AI usage guardrail metrics provider-neutral (ai_today) - Add seed export sanitization to exclude sensitive hospital_setting and audit entries - Remove Gemini API audit entry from master seed JSON --- app/Controllers/Opd_prescription.php | 215 ++---------------- app/Controllers/Setting/AiSettings.php | 20 +- app/Controllers/Setup.php | 56 ++++- app/Database/master_seed_data.json | 14 +- app/Views/Setting/Admin/ai_settings.php | 8 +- app/Views/billing/opd_advice_workspace.php | 2 +- app/Views/billing/opd_medicince_workspace.php | 2 +- .../diagnosis/imaging_ai_result_modal.php | 5 +- 8 files changed, 93 insertions(+), 229 deletions(-) diff --git a/app/Controllers/Opd_prescription.php b/app/Controllers/Opd_prescription.php index d69d451..f8a18ce 100644 --- a/app/Controllers/Opd_prescription.php +++ b/app/Controllers/Opd_prescription.php @@ -480,11 +480,11 @@ class Opd_prescription extends BaseController $prompt = $this->buildClinicalPrompt($mode, $text); $this->lastAiProvider = 'none'; - $draft = $this->generateClinicalTextWithGemini($prompt); + $draft = $this->generateClinicalTextWithAi($prompt); if ($draft !== null && trim($draft) !== '') { - $provider = $this->lastAiProvider === 'azure' ? 'azure' : 'gemini'; - $providerLabel = $provider === 'azure' ? 'Azure OpenAI' : 'Gemini'; + $provider = $this->lastAiProvider === 'azure' ? 'azure' : 'ai'; + $providerLabel = $provider === 'azure' ? 'Azure OpenAI' : 'AI'; return $this->response->setJSON([ 'update' => 1, 'draft_text' => trim($draft), @@ -571,7 +571,7 @@ class Opd_prescription extends BaseController $prompt = $this->buildFullClinicalDraftPrompt($input); $this->lastAiProvider = 'none'; - $draftText = $this->generateClinicalTextWithGemini($prompt); + $draftText = $this->generateClinicalTextWithAi($prompt); $drafts = $this->extractFullDraftJson($draftText); if ($drafts === null) { @@ -579,8 +579,8 @@ class Opd_prescription extends BaseController $aiMode = 'fallback'; $message = 'AI unavailable. Template draft prepared from entered data.'; } else { - $aiMode = $this->lastAiProvider === 'azure' ? 'azure' : 'gemini'; - $message = 'Full clinical draft prepared using ' . ($aiMode === 'azure' ? 'Azure OpenAI' : 'Gemini') . '.'; + $aiMode = $this->lastAiProvider === 'azure' ? 'azure' : 'ai'; + $message = 'Full clinical draft prepared using ' . ($aiMode === 'azure' ? 'Azure OpenAI' : 'AI') . '.'; } return $this->response->setJSON([ @@ -2208,7 +2208,7 @@ class Opd_prescription extends BaseController . "Use short plain text values. If unsure for a field, keep empty string."; $this->lastAiProvider = 'none'; - $aiText = $this->generateClinicalTextWithGemini($prompt, 280); + $aiText = $this->generateClinicalTextWithAi($prompt, 280); if ($aiText === null || trim($aiText) === '') { $fallback = $this->inferMedicineDetailsFromBrandName($name, $formulation); if (($fallback['genericname'] ?? '') !== '' || ($fallback['salt_name'] ?? '') !== '') { @@ -2229,7 +2229,7 @@ class Opd_prescription extends BaseController ]); } - return $this->response->setJSON(['update' => 0, 'error_text' => 'AI unavailable and no local match found. Check AI settings/API key.']); + return $this->response->setJSON(['update' => 0, 'error_text' => 'AI unavailable and no local match found. Check AI settings.']); } $parsed = $this->parseJsonFromAiResponse($aiText); @@ -2605,11 +2605,11 @@ class Opd_prescription extends BaseController . "English Advice: {$sourceText}"; $this->lastAiProvider = 'none'; - $aiText = $this->generateClinicalTextWithGemini($prompt, 220); + $aiText = $this->generateClinicalTextWithAi($prompt, 220); if ($aiText === null || trim($aiText) === '') { return $this->response->setJSON([ 'update' => 0, - 'error_text' => 'AI unavailable. Check AI settings/API key.', + 'error_text' => 'AI unavailable. Check AI settings.', 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); @@ -3014,13 +3014,13 @@ class Opd_prescription extends BaseController $finalComplaints = array_values($normalized); $this->lastAiProvider = 'none'; - $geminiDraft = $this->generateComplaintDraftWithGemini($finalComplaints, $currentText); - if ($geminiDraft !== null && trim($geminiDraft) !== '') { - $provider = $this->lastAiProvider === 'azure' ? 'azure' : 'gemini'; - $providerLabel = $provider === 'azure' ? 'Azure OpenAI' : 'Gemini'; + $aiDraft = $this->generateComplaintDraftWithAi($finalComplaints, $currentText); + if ($aiDraft !== null && trim($aiDraft) !== '') { + $provider = $this->lastAiProvider === 'azure' ? 'azure' : 'ai'; + $providerLabel = $provider === 'azure' ? 'Azure OpenAI' : 'AI'; return $this->response->setJSON([ 'update' => 1, - 'draft_text' => trim($geminiDraft), + 'draft_text' => trim($aiDraft), 'error_text' => 'AI draft prepared using ' . $providerLabel, 'ai_mode' => $provider, 'csrfName' => csrf_token(), @@ -5373,34 +5373,6 @@ class Opd_prescription extends BaseController return array_values($out); } - private function getGeminiApiKey(): string - { - foreach (['GEMINI_API_KEY', 'AI_GEMINI_API_KEY', 'APP_GEMINI_API_KEY', 'H_GEMINI_API_KEY'] as $constName) { - if (defined($constName)) { - $value = trim((string) constant($constName)); - if ($value !== '') { - return $value; - } - } - } - - if ($this->db->tableExists('hospital_setting')) { - $row = $this->db->table('hospital_setting') - ->select('s_value') - ->whereIn('s_name', ['GEMINI_API_KEY', 'AI_GEMINI_API_KEY', 'APP_GEMINI_API_KEY', 'H_GEMINI_API_KEY']) - ->where('s_value !=', '') - ->orderBy('s_name', 'ASC') - ->get(1) - ->getRowArray(); - - if (! empty($row['s_value'])) { - return trim((string) $row['s_value']); - } - } - - return ''; - } - /** * @return array{endpoint:string,api_key:string,deployment:string,api_version:string} */ @@ -5475,7 +5447,7 @@ class Opd_prescription extends BaseController /** * @param array $complaints */ - private function generateComplaintDraftWithGemini(array $complaints, string $currentText): ?string + private function generateComplaintDraftWithAi(array $complaints, string $currentText): ?string { if (empty($complaints)) { return null; @@ -5487,7 +5459,7 @@ Input complaints: " . implode(', ', $complaints) . " Additional raw doctor notes: " . $currentText . " Output only one plain text sentence starting with 'Chief complaints:' and no markdown."; - return $this->generateClinicalTextWithGemini($prompt, 180); + return $this->generateClinicalTextWithAi($prompt, 180); } private function generateClinicalTextWithAzureOpenAi(string $prompt, int $maxOutputTokens = 300): ?string @@ -5665,70 +5637,9 @@ Input: " . $text; return implode(' ', $sentences); } - private function generateClinicalTextWithGemini(string $prompt, int $maxOutputTokens = 300): ?string + private function generateClinicalTextWithAi(string $prompt, int $maxOutputTokens = 300): ?string { - $azureText = $this->generateClinicalTextWithAzureOpenAi($prompt, $maxOutputTokens); - if ($azureText !== null && trim($azureText) !== '') { - return $azureText; - } - - $apiKey = $this->getGeminiApiKey(); - if ($apiKey === '' || trim($prompt) === '') { - return null; - } - - try { - $httpOptions = [ - 'timeout' => 20, - 'http_errors' => false, - ]; - if (defined('ENVIRONMENT') && in_array((string) ENVIRONMENT, ['development', 'testing'], true)) { - $httpOptions['verify'] = false; - } - - $client = service('curlrequest', $httpOptions); - $payload = [ - 'contents' => [[ - 'parts' => [[ - 'text' => $prompt, - ]], - ]], - 'generationConfig' => [ - 'temperature' => 0.2, - 'topP' => 0.9, - 'maxOutputTokens' => max(64, $maxOutputTokens), - ], - ]; - - foreach (['gemini-2.0-flash', 'gemini-1.5-flash', 'gemini-1.5-flash-latest'] as $modelName) { - $url = 'https://generativelanguage.googleapis.com/v1beta/models/' . $modelName . ':generateContent?key=' . urlencode($apiKey); - $response = $client->post($url, [ - 'headers' => ['Content-Type' => 'application/json'], - 'json' => $payload, - ]); - - if ($response->getStatusCode() < 200 || $response->getStatusCode() >= 300) { - continue; - } - - $decoded = json_decode((string) $response->getBody(), true); - $parts = $decoded['candidates'][0]['content']['parts'] ?? []; - $text = ''; - foreach ($parts as $part) { - $text .= (string) ($part['text'] ?? ''); - } - - $text = trim($text); - if ($text !== '') { - $this->lastAiProvider = 'gemini'; - return preg_replace('/\s+/', ' ', $text) ?: $text; - } - } - } catch (\Throwable $e) { - return null; - } - - return null; + return $this->generateClinicalTextWithAzureOpenAi($prompt, $maxOutputTokens); } /** @@ -6331,91 +6242,9 @@ OPD SNAPSHOT JSON: " . $payload; private function extractClinicalTextFromScan(string $filePath, string $scanType = ''): ?string { - $apiKey = $this->getGeminiApiKey(); - if ($apiKey === '') { - return null; - } - - $ext = strtolower(pathinfo($filePath, PATHINFO_EXTENSION)); - $mimeMap = [ - 'jpg' => 'image/jpeg', - 'jpeg' => 'image/jpeg', - 'png' => 'image/png', - 'webp' => 'image/webp', - 'pdf' => 'application/pdf', - ]; - $mime = $mimeMap[$ext] ?? ''; - if ($mime === '') { - return null; - } - - $raw = @file_get_contents($filePath); - if ($raw === false || $raw === '') { - return null; - } - - if (strlen($raw) > 8 * 1024 * 1024) { - return null; - } - - $typeHint = $scanType !== '' ? strtoupper($scanType) : 'GENERAL'; - $prompt = "You are a medical report transcription assistant.\n" - . "Extract readable text from this " . $typeHint . " scan.\n" - . "Return concise structured plain text with sections: Findings, Impression, Values(if present).\n" - . "Do not invent data. If unreadable, mention unreadable fields."; - - try { - $client = service('curlrequest', [ - 'timeout' => 30, - 'http_errors' => false, - ]); - - $payload = [ - 'contents' => [[ - 'parts' => [ - ['text' => $prompt], - [ - 'inlineData' => [ - 'mimeType' => $mime, - 'data' => base64_encode($raw), - ], - ], - ], - ]], - 'generationConfig' => [ - 'temperature' => 0.1, - 'topP' => 0.8, - 'maxOutputTokens' => 600, - ], - ]; - - foreach (['gemini-2.0-flash', 'gemini-1.5-flash'] as $modelName) { - $url = 'https://generativelanguage.googleapis.com/v1beta/models/' . $modelName . ':generateContent?key=' . urlencode($apiKey); - $response = $client->post($url, [ - 'headers' => ['Content-Type' => 'application/json'], - 'json' => $payload, - ]); - - if ($response->getStatusCode() < 200 || $response->getStatusCode() >= 300) { - continue; - } - - $decoded = json_decode((string) $response->getBody(), true); - $parts = $decoded['candidates'][0]['content']['parts'] ?? []; - $text = ''; - foreach ($parts as $part) { - $text .= (string) ($part['text'] ?? ''); - } - - $text = trim($text); - if ($text !== '') { - return preg_replace('/\s+/', ' ', $text) ?: $text; - } - } - } catch (\Throwable $e) { - return null; - } - + // Legacy external scan extraction has been removed from this module. + // Keep returning null so existing fallback paths continue to work. + unset($filePath, $scanType); return null; } diff --git a/app/Controllers/Setting/AiSettings.php b/app/Controllers/Setting/AiSettings.php index cb1eabb..8462ade 100644 --- a/app/Controllers/Setting/AiSettings.php +++ b/app/Controllers/Setting/AiSettings.php @@ -125,7 +125,7 @@ class AiSettings extends BaseController 'error_text' => 'Usage table unavailable', 'usage' => [ 'total_today' => 0, - 'gemini_today' => 0, + 'ai_today' => 0, 'fallback_today' => 0, 'last_hour' => 0, 'daily_limit' => 20, @@ -154,18 +154,14 @@ class AiSettings extends BaseController ->where('created_at <=', $end) ->countAllResults(); - $geminiToday = (int) $this->db->table('lab_ai_extraction_batches') - ->where('created_at >=', $start) - ->where('created_at <=', $end) - ->where("LOWER(ai_provider) = 'gemini'", null, false) - ->countAllResults(); - $fallbackToday = (int) $this->db->table('lab_ai_extraction_batches') ->where('created_at >=', $start) ->where('created_at <=', $end) ->where("LOWER(ai_provider) IN ('local-xray-fallback','regex-fallback')", null, false) ->countAllResults(); + $aiToday = max(0, $totalToday - $fallbackToday); + $lastHour = (int) $this->db->table('lab_ai_extraction_batches') ->where('created_at >=', $hourStart) ->where('created_at <=', $end) @@ -192,7 +188,7 @@ class AiSettings extends BaseController ]; } - $ratio = (int) round(($geminiToday / max(1, $dailyLimit)) * 100); + $ratio = (int) round(($aiToday / max(1, $dailyLimit)) * 100); $level = 'ok'; if ($ratio >= 100) { $level = 'critical'; @@ -206,7 +202,7 @@ class AiSettings extends BaseController 'update' => 1, 'usage' => [ 'total_today' => $totalToday, - 'gemini_today' => $geminiToday, + 'ai_today' => $aiToday, 'fallback_today' => $fallbackToday, 'last_hour' => $lastHour, 'daily_limit' => $dailyLimit, @@ -249,7 +245,7 @@ class AiSettings extends BaseController $headers['Authorization'] = 'Bearer ' . $token; } - $client = service('curlrequest', $this->geminiHttpOptions()); + $client = service('curlrequest', $this->aiHttpOptions()); $response = $client->post(rtrim($endpoint, '/'), [ 'headers' => $headers, 'json' => [ @@ -326,7 +322,7 @@ class AiSettings extends BaseController $headers['Authorization'] = 'Bearer ' . $token; } - $client = service('curlrequest', $this->geminiHttpOptions()); + $client = service('curlrequest', $this->aiHttpOptions()); $url = rtrim($baseUrl, '/') . '/translate?text=test&target_lang=en'; $response = $client->get($url, [ 'headers' => $headers, @@ -364,7 +360,7 @@ class AiSettings extends BaseController /** * @return array */ - private function geminiHttpOptions(): array + private function aiHttpOptions(): array { $options = [ 'timeout' => 20, diff --git a/app/Controllers/Setup.php b/app/Controllers/Setup.php index 06fe8a7..6aa92c0 100644 --- a/app/Controllers/Setup.php +++ b/app/Controllers/Setup.php @@ -295,7 +295,8 @@ class Setup extends BaseController try { $rows = $this->db->table($resolvedTable)->get()->getResultArray(); - $seedPayload['tables'][$resolvedTable] = is_array($rows) ? $rows : []; + $rows = is_array($rows) ? $rows : []; + $seedPayload['tables'][$resolvedTable] = $this->sanitizeSeedRows($resolvedTable, $rows); } catch (\Throwable $e) { log_message('error', 'Seed export table read failed for {table}: {message}', ['table' => $resolvedTable, 'message' => $e->getMessage()]); $seedPayload['tables'][$resolvedTable] = []; @@ -1103,6 +1104,59 @@ class Setup extends BaseController return array_values($out); } + /** + * @param array> $rows + * + * @return array> + */ + private function sanitizeSeedRows(string $tableName, array $rows): array + { + $table = strtolower(trim($tableName)); + if ($rows === []) { + return $rows; + } + + if ($table === 'hospital_setting') { + return array_values(array_filter($rows, function (array $row): bool { + $name = strtoupper(trim((string) ($row['s_name'] ?? ''))); + return ! $this->isSensitiveSettingName($name); + })); + } + + if ($table === 'clinical_audit_trail') { + return array_values(array_filter($rows, function (array $row): bool { + if (strtolower(trim((string) ($row['module'] ?? ''))) !== 'hospital_setting') { + return true; + } + + $recordId = strtoupper(trim((string) ($row['record_id'] ?? ''))); + $fieldName = strtoupper(trim((string) ($row['field_name'] ?? ''))); + return ! ($this->isSensitiveSettingName($recordId) || $this->isSensitiveSettingName($fieldName)); + })); + } + + return $rows; + } + + private function isSensitiveSettingName(string $name): bool + { + if ($name === '') { + return false; + } + + if (str_contains($name, 'API_KEY') || str_contains($name, 'TOKEN') || str_contains($name, 'SECRET')) { + return true; + } + + foreach (['GEMINI_', 'AZURE_', 'OPENAI_'] as $prefix) { + if (str_starts_with($name, $prefix)) { + return true; + } + } + + return false; + } + /** * @return array */ diff --git a/app/Database/master_seed_data.json b/app/Database/master_seed_data.json index 758670b..3da4bf2 100644 --- a/app/Database/master_seed_data.json +++ b/app/Database/master_seed_data.json @@ -1569,19 +1569,7 @@ "created_at": "2026-02-21 13:36:59", "updated_at": "2026-02-21 13:36:59" }, - { - "id": "7", - "module": "hospital_setting", - "record_id": "GEMINI_API_KEY", - "field_name": "GEMINI_API_KEY", - "old_value": "\"\"", - "new_value": "\"REDACTED_GEMINI_API_KEY\"", - "user_id": "1", - "action_at": "2026-02-21 19:34:24", - "hash": "dfc6370231d5fec4c562914546b5aea6fa79e1a8fe86d84ba9ba31c121cdcfcb", - "created_at": "2026-02-21 14:04:24", - "updated_at": "2026-02-21 14:04:24" - }, + { "id": "8", "module": "hospital_setting", diff --git a/app/Views/Setting/Admin/ai_settings.php b/app/Views/Setting/Admin/ai_settings.php index 85bbddd..f4b0814 100644 --- a/app/Views/Setting/Admin/ai_settings.php +++ b/app/Views/Setting/Admin/ai_settings.php @@ -60,7 +60,7 @@
- +
Used for warning levels at 70% and 90%.
@@ -75,7 +75,7 @@ Unknown
-
Gemini Calls
-
+
AI Calls
-
Fallback Calls
-
Total Calls
-
Last Hour
-
@@ -204,7 +204,7 @@ return; } - $('#ai_usage_gemini').text((usage.gemini_today || 0) + ' / ' + (usage.daily_limit || 0)); + $('#ai_usage_ai').text((usage.ai_today || 0) + ' / ' + (usage.daily_limit || 0)); $('#ai_usage_fallback').text(usage.fallback_today || 0); $('#ai_usage_total').text(usage.total_today || 0); $('#ai_usage_hour').text(usage.last_hour || 0); @@ -224,7 +224,7 @@ } $('#ai_usage_level').removeClass().addClass('badge ' + levelClass).text(levelText); - $('#ai_usage_hint').text('Gemini utilization: ' + (usage.ratio || 0) + '%. Thresholds: 70% warning, 90% high.'); + $('#ai_usage_hint').text('AI utilization: ' + (usage.ratio || 0) + '%. Thresholds: 70% warning, 90% high.'); }); } diff --git a/app/Views/billing/opd_advice_workspace.php b/app/Views/billing/opd_advice_workspace.php index 6a139ea..471597a 100644 --- a/app/Views/billing/opd_advice_workspace.php +++ b/app/Views/billing/opd_advice_workspace.php @@ -227,7 +227,7 @@ $('#advice_hi').val((data.translated_text || '').toString()); var langName = (data.target_language_name || '').toString(); var provider = (data.provider || '').toString(); - var providerLabel = provider === 'azure' ? 'Azure OpenAI' : (provider === 'gemini' ? 'Gemini' : 'AI'); + var providerLabel = provider === 'azure' ? 'Azure OpenAI' : 'AI'; setMsg('ok', 'Translated to ' + (langName || 'selected language') + ' using ' + providerLabel + '.'); }); }); diff --git a/app/Views/billing/opd_medicince_workspace.php b/app/Views/billing/opd_medicince_workspace.php index 12a6749..3202209 100644 --- a/app/Views/billing/opd_medicince_workspace.php +++ b/app/Views/billing/opd_medicince_workspace.php @@ -608,7 +608,7 @@ return; } - if (isNewMedicine && (provider === 'gemini' || provider === 'azure' || provider === 'local-brand-fallback')) { + if (isNewMedicine && (provider === 'azure' || provider === 'local-brand-fallback')) { if (window.confirm('Details prepared for a new medicine. Save this new master entry now?')) { saveMedicine(buildMedicinePayload(), function() { setMsg('ok', 'New medicine saved with suggested generic/salt details.'); diff --git a/app/Views/diagnosis/imaging_ai_result_modal.php b/app/Views/diagnosis/imaging_ai_result_modal.php index 65b6f17..e7d0d71 100644 --- a/app/Views/diagnosis/imaging_ai_result_modal.php +++ b/app/Views/diagnosis/imaging_ai_result_modal.php @@ -12,10 +12,7 @@ $impressionInputId = 'ai_impression_' . $batchId; $providerKey = strtolower($provider); $providerLabel = $provider !== '' ? $provider : 'ai-server'; $providerBadgeClass = 'bg-secondary'; -if ($providerKey === 'gemini') { - $providerLabel = 'Gemini'; - $providerBadgeClass = 'bg-primary'; -} elseif ($providerKey === 'local-xray-fallback') { +if ($providerKey === 'local-xray-fallback') { $providerLabel = 'Local X-ray Fallback'; $providerBadgeClass = 'bg-warning text-dark'; } elseif ($providerKey === 'regex-fallback') {