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
This commit is contained in:
DevSoftTech Server 2026-03-18 06:18:52 +05:30
parent 4adc6d79d9
commit 47d7db6d8b
8 changed files with 93 additions and 229 deletions

View File

@ -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<int, string> $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;
}

View File

@ -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<string, mixed>
*/
private function geminiHttpOptions(): array
private function aiHttpOptions(): array
{
$options = [
'timeout' => 20,

View File

@ -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<int, array<string, mixed>> $rows
*
* @return array<int, array<string, mixed>>
*/
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<string, mixed>
*/

View File

@ -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",

View File

@ -60,7 +60,7 @@
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label">Gemini Daily Limit (guardrail)</label>
<label class="form-label">AI Daily Limit (guardrail)</label>
<input type="number" min="1" max="10000" class="form-control" id="diagnosis_ai_daily_limit" value="<?= esc($diagnosis_ai_daily_limit ?? '20') ?>">
<div class="form-text">Used for warning levels at 70% and 90%.</div>
</div>
@ -75,7 +75,7 @@
<span class="badge bg-secondary" id="ai_usage_level">Unknown</span>
</div>
<div class="row g-2">
<div class="col-md-3"><div class="small text-muted">Gemini Calls</div><div id="ai_usage_gemini" class="fw-bold">-</div></div>
<div class="col-md-3"><div class="small text-muted">AI Calls</div><div id="ai_usage_ai" class="fw-bold">-</div></div>
<div class="col-md-3"><div class="small text-muted">Fallback Calls</div><div id="ai_usage_fallback" class="fw-bold">-</div></div>
<div class="col-md-3"><div class="small text-muted">Total Calls</div><div id="ai_usage_total" class="fw-bold">-</div></div>
<div class="col-md-3"><div class="small text-muted">Last Hour</div><div id="ai_usage_hour" class="fw-bold">-</div></div>
@ -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.');
});
}

View File

@ -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 + '.');
});
});

View File

@ -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.');

View File

@ -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') {