add doctor feedback

This commit is contained in:
Danel Kungla
2025-10-28 16:09:06 +02:00
parent b5b01648fc
commit 8bc6089a7f
28 changed files with 820 additions and 95 deletions

View File

@@ -1,6 +1,11 @@
'use server';
import { AccountWithParams } from '@/packages/features/accounts/src/types/accounts';
import {
AnalysisResponse,
Patient,
} from '@/packages/features/doctor/src/lib/server/schema/doctor-analysis-detail-view.schema';
import { getLogger } from '@/packages/shared/src/logger';
import { getSupabaseServerClient } from '@/packages/supabase/src/clients/server-client';
import OpenAI from 'openai';
@@ -13,26 +18,16 @@ import {
} from '../../_components/ai/types';
import { OrderAnalysisCard } from '../../_components/order-analyses-cards';
async function getLatestResponseTime(items?: AnalysisResponses) {
if (!items?.length) return null;
let latest = null;
for (const it of items) {
const d = new Date(it.response_time);
const t = d.getTime();
if (!Number.isNaN(t) && (latest === null || t > latest.getTime())) {
latest = d;
}
}
return latest;
}
export async function updateLifeStyle({
account,
analysisResponses,
isDoctorView = false,
aiResponseTimestamp,
}: {
account: AccountWithParams;
analysisResponses?: AnalysisResponses;
isDoctorView?: boolean;
aiResponseTimestamp: string;
}): Promise<ILifeStyleResponse> {
const LIFE_STYLE_PROMPT_ID = process.env.PROMPT_ID_LIFE_STYLE;
if (!LIFE_STYLE_PROMPT_ID || !account?.personal_code) {
@@ -65,11 +60,6 @@ export async function updateLifeStyle({
?.find((ar) => ar.analysis_name_lab === 'Vitamiin D (25-OH)')
?.response_value.toString() || 'unknown';
const latestResponseTime = await getLatestResponseTime(analysisResponses);
const latestISO = latestResponseTime
? new Date(latestResponseTime).toISOString()
: new Date('2025').toISOString();
try {
const response = await openAIClient.responses.create({
store: false,
@@ -106,8 +96,9 @@ export async function updateLifeStyle({
vitamind,
is_smoker: isSmoker.toString(),
}),
latest_data_change: latestISO,
latest_data_change: aiResponseTimestamp,
response: response.output_text,
is_visible_to_customer: !isDoctorView,
});
const json = JSON.parse(response.output_text);
@@ -126,10 +117,12 @@ export async function updateRecommendations({
analyses,
analysisResponses,
account,
aiResponseTimestamp,
}: {
analyses: OrderAnalysisCard[];
analysisResponses?: AnalysisResponses;
account: AccountWithParams;
aiResponseTimestamp: string;
}) {
const RECOMMENDATIONS_PROMPT_IT =
process.env.PROMPT_ID_ANALYSIS_RECOMMENDATIONS;
@@ -164,10 +157,6 @@ export async function updateRecommendations({
description,
title,
}));
const latestResponseTime = await getLatestResponseTime(analysisResponses);
const latestISO = latestResponseTime
? new Date(latestResponseTime).toISOString()
: new Date('2025').toISOString();
try {
const response = await openAIClient.responses.create({
@@ -198,8 +187,9 @@ export async function updateRecommendations({
age,
weight,
}),
latest_data_change: latestISO,
latest_data_change: aiResponseTimestamp,
response: response.output_text,
is_visible_to_customer: false,
});
const json = JSON.parse(response.output_text);
@@ -210,3 +200,125 @@ export async function updateRecommendations({
return [];
}
}
export async function generateDoctorFeedback({
patient,
analysisResponses,
aiResponseTimestamp,
}: {
patient: Patient;
analysisResponses: AnalysisResponse[];
aiResponseTimestamp: string;
}): Promise<string> {
const DOCTOR_FEEDBACK_PROMPT_ID = process.env.PROMPT_ID_DOCTOR_FEEDBACK;
if (!DOCTOR_FEEDBACK_PROMPT_ID) {
console.error('No secrets for doctor feedback');
return '';
}
const openAIClient = new OpenAI();
const supabaseClient = getSupabaseServerClient();
const formattedAnalysisResponses = analysisResponses?.map(
({
analysis_name,
response_value,
norm_upper,
norm_lower,
norm_status,
}) => ({
name: analysis_name,
value: response_value,
normUpper: norm_upper,
normLower: norm_lower,
normStatus: norm_status,
}),
);
try {
const response = await openAIClient.responses.create({
store: false,
prompt: {
id: DOCTOR_FEEDBACK_PROMPT_ID,
variables: {
analysesresults: JSON.stringify(formattedAnalysisResponses),
},
},
});
await supabaseClient
.schema('medreport')
.from('ai_responses')
.insert({
account_id: patient.userId,
prompt_name: PROMPT_NAME.FEEDBACK,
prompt_id: DOCTOR_FEEDBACK_PROMPT_ID,
input: JSON.stringify({
analysesresults: formattedAnalysisResponses,
}),
latest_data_change: aiResponseTimestamp,
response: response.output_text,
});
return response.output_text;
} catch (error) {
console.error('Error getting doctor feedback: ', error);
return '';
}
}
export async function confirmPatientAIResponses(
patientId: string,
aiResponseTimestamp: string,
recommendations: string[],
isRecommendationsEdited: boolean,
) {
const logger = await getLogger();
const supabaseClient = getSupabaseServerClient();
const { error } = await supabaseClient
.schema('medreport')
.from('ai_responses')
.update({
is_visible_to_customer: true,
})
.eq('latest_data_change', aiResponseTimestamp)
.eq('account_id', patientId)
.eq('prompt_name', PROMPT_NAME.LIFE_STYLE);
if (error) {
logger.error(
{ error, patientId, aiResponseTimestamp },
'Failed updating life style',
);
}
const { error: _error } = await supabaseClient
.schema('medreport')
.from('ai_responses')
.update({
is_visible_to_customer: true,
...(isRecommendationsEdited && {
response: JSON.stringify({
why: 'This was edited by doctor',
recommended: recommendations,
}),
}),
})
.eq('latest_data_change', aiResponseTimestamp)
.eq('account_id', patientId)
.eq('prompt_name', PROMPT_NAME.ANALYSIS_RECOMMENDATIONS);
if (_error) {
logger.error(
{
error,
aiResponseTimestamp,
patientId,
isRecommendationsEdited,
},
'Failed updating analysis recommendations',
);
}
}

View File

@@ -1,6 +1,7 @@
import { cache } from 'react';
import { AccountWithParams } from '@/packages/features/accounts/src/types/accounts';
import { getLogger } from '@/packages/shared/src/logger';
import { getSupabaseServerClient } from '@/packages/supabase/src/clients/server-client';
import {
@@ -11,28 +12,52 @@ import {
import { updateLifeStyle } from './ai-actions';
const failedResponse = {
lifestyle: [],
summary: null,
response: {
lifestyle: [],
summary: null,
},
dateCreated: new Date().toISOString(),
};
async function lifeStyleLoader(
account: AccountWithParams,
analysisResponses?: AnalysisResponses,
): Promise<ILifeStyleResponse> {
async function lifeStyleLoader({
account,
analysisResponses,
isDoctorView = false,
aiResponseTimestamp,
}: {
account: AccountWithParams | null;
analysisResponses?: AnalysisResponses;
isDoctorView?: boolean;
aiResponseTimestamp: string;
}): Promise<{ response: ILifeStyleResponse; dateCreated: string }> {
const logger = await getLogger();
if (!account?.personal_code) {
return failedResponse;
}
const supabaseClient = getSupabaseServerClient();
const { data, error } = await supabaseClient
const query = supabaseClient
.schema('medreport')
.from('ai_responses')
.select('response')
.select('response, latest_data_change')
.eq('account_id', account.id)
.eq('prompt_name', PROMPT_NAME.LIFE_STYLE)
.order('latest_data_change', { ascending: false, nullsFirst: false })
.limit(1)
.maybeSingle();
.eq('prompt_name', PROMPT_NAME.LIFE_STYLE);
if (isDoctorView) {
logger.info(
{ aiResponseTimestamp, accountId: account.id },
'Attempting to receive life style row',
);
query.eq('latest_data_change', aiResponseTimestamp);
} else {
query
.eq('is_visible_to_customer', true)
.order('latest_data_change', { ascending: false, nullsFirst: false });
}
const { data, error } = await query.limit(1).maybeSingle();
logger.info({ data: !!data }, 'Existing life style row');
if (error) {
console.error('Error fetching AI response from DB: ', error);
@@ -40,9 +65,19 @@ async function lifeStyleLoader(
}
if (data?.response) {
return JSON.parse(data.response as string);
return {
response: JSON.parse(data.response as string),
dateCreated: data.latest_data_change,
};
} else {
return await updateLifeStyle({ account, analysisResponses });
const newLifeStyle = await updateLifeStyle({
account,
analysisResponses,
isDoctorView,
aiResponseTimestamp,
});
return { response: newLifeStyle, dateCreated: aiResponseTimestamp };
}
}
export const loadLifeStyle = cache(lifeStyleLoader);

View File

@@ -1,29 +1,57 @@
import { cache } from 'react';
import { AccountWithParams } from '@/packages/features/accounts/src/types/accounts';
import { getLogger } from '@/packages/shared/src/logger';
import { getSupabaseServerClient } from '@/packages/supabase/src/clients/server-client';
import { PROMPT_NAME } from '../../_components/ai/types';
import { AnalysisResponses, PROMPT_NAME } from '../../_components/ai/types';
import { OrderAnalysisCard } from '../../_components/order-analyses-cards';
import { updateRecommendations } from './ai-actions';
export const loadRecommendations = cache(recommendationsLoader);
async function recommendationsLoader(
account: AccountWithParams | null,
): Promise<string[]> {
async function recommendationsLoader({
account,
isDoctorView = false,
analyses,
analysisResponses,
aiResponseTimestamp,
}: {
account: AccountWithParams | null;
isDoctorView?: boolean;
analyses: OrderAnalysisCard[];
analysisResponses?: AnalysisResponses;
aiResponseTimestamp: string;
}): Promise<string[]> {
const logger = await getLogger();
if (!account?.personal_code) {
return [];
}
const supabaseClient = getSupabaseServerClient();
const { data, error } = await supabaseClient
const query = supabaseClient
.schema('medreport')
.from('ai_responses')
.select('*')
.eq('account_id', account.id)
.eq('prompt_name', PROMPT_NAME.ANALYSIS_RECOMMENDATIONS)
.order('latest_data_change', { ascending: false, nullsFirst: false })
.limit(1)
.maybeSingle();
.eq('prompt_name', PROMPT_NAME.ANALYSIS_RECOMMENDATIONS);
logger.info(
{ accountId: account.id, isDoctorView, aiResponseTimestamp },
'Attempting to receive existing recommendations',
);
if (isDoctorView) {
query.eq('latest_data_change', aiResponseTimestamp);
} else {
query
.eq('is_visible_to_customer', true)
.order('latest_data_change', { ascending: false, nullsFirst: false });
}
const { data, error } = await query.limit(1).maybeSingle();
logger.info({ data: data }, 'Existing recommendations');
if (error) {
console.error('Error fetching AI response from DB: ', error);
@@ -33,6 +61,14 @@ async function recommendationsLoader(
if (data?.response) {
return JSON.parse(data.response as string).recommended;
} else {
if (isDoctorView) {
return await updateRecommendations({
account,
analyses,
analysisResponses,
aiResponseTimestamp,
});
}
return [];
}
}