Skip to content

Commit

Permalink
[AI Tutor] CT-253: Teachers can view flagged chats (#57669)
Browse files Browse the repository at this point in the history
* controller method to fetch single student's AiTutorInteractions

* add endpoint to interactionsApi

* refactor api routes for ai tutor interactions

* refactor to more RESTful single route

* remove puts

* fix querystring logic and autocorrect ruby linting

* refactor ai tutor constants

* resolve type issues

* populate dropdown

* add filters for status, student, and time

* add enum for time filters

* missed usage of enum

* loosen type checking

* use AITutorInteractionStatusValue over string

* remove errant semicolon

---------

Co-authored-by: Erin Bond <erin.bond@code.org>
  • Loading branch information
ebeastlake and Erin007 committed Apr 9, 2024
1 parent 0fcdaa8 commit 9a4ae51
Show file tree
Hide file tree
Showing 25 changed files with 310 additions and 110 deletions.
3 changes: 2 additions & 1 deletion apps/script/generateSharedConstants.rb
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,9 @@ def main
CHILD_ACCOUNT_COMPLIANCE_STATES
CENSUS_CONSTANTS
DANCE_SONG_MANIFEST_FILENAME
AI_TUTOR_INTERACTION_SAVE_STATUS
AI_TUTOR_INTERACTION_STATUS
AI_TUTOR_TYPES
PII_TYPES
FEATURED_PROJECT_STATUS
FEATURED_PROJECT_CONSTANTS
LMS_LINKS
Expand Down
5 changes: 4 additions & 1 deletion apps/src/aiTutor/constants.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import {Role, Status} from '@cdo/apps/aiTutor/types';
import {
Role,
AITutorInteractionStatus as Status,
} from '@cdo/apps/aiTutor/types';

export const compilationSystemPrompt =
'You are a tutor in a high school computer science class. Students in the class are studying Java and they would like to know in age-appropriate, clear language why their code does not compile. Do not write any code.';
Expand Down
18 changes: 10 additions & 8 deletions apps/src/aiTutor/redux/aiTutorRedux.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,20 @@ import {
} from '@cdo/apps/aiTutor/constants';
import {savePromptAndResponse} from '../interactionsApi';
import {
TutorType,
Role,
Status,
AITutorInteractionStatus as Status,
ChatCompletionMessage,
Level,
ChatContext,
AITutorTypesValue,
AITutorTypes as TutorTypes,
AITutorInteractionStatusValue,
} from '../types';

const registerReducers = require('@cdo/apps/redux').registerReducers;

export interface AITutorState {
selectedTutorType: TutorType | undefined;
selectedTutorType: AITutorTypesValue | undefined;
level: Level | undefined;
scriptId: number | undefined;
aiResponse: string | undefined;
Expand Down Expand Up @@ -58,9 +60,9 @@ export const askAITutor = createAsyncThunk(
};

const tutorType = chatContext.tutorType;
const generalChat = tutorType === TutorType.GENERAL_CHAT;
const compilation = tutorType === TutorType.COMPILATION;
const validation = tutorType === TutorType.VALIDATION;
const generalChat = tutorType === TutorTypes.GENERAL_CHAT;
const compilation = tutorType === TutorTypes.COMPILATION;
const validation = tutorType === TutorTypes.VALIDATION;

let systemPrompt;
if (validation) {
Expand Down Expand Up @@ -129,7 +131,7 @@ const aiTutorSlice = createSlice({
reducers: {
setSelectedTutorType: (
state,
action: PayloadAction<TutorType | undefined>
action: PayloadAction<AITutorTypesValue | undefined>
) => {
state.selectedTutorType = action.payload;
},
Expand Down Expand Up @@ -159,7 +161,7 @@ const aiTutorSlice = createSlice({
},
updateChatMessageStatus: (
state,
action: PayloadAction<{id: number; status: Status}>
action: PayloadAction<{id: number; status: AITutorInteractionStatusValue}>
) => {
const {id, status} = action.payload;
const chatMessage = state.chatMessages.find(msg => msg.id === id);
Expand Down
66 changes: 36 additions & 30 deletions apps/src/aiTutor/types.ts
Original file line number Diff line number Diff line change
@@ -1,63 +1,76 @@
import {
AiTutorInteractionSaveStatus,
AiTutorTypes,
AiTutorInteractionStatus as AITutorInteractionStatus,
AiTutorTypes as AITutorTypes,
PiiTypes as PII,
} from '@cdo/apps/util/sharedConstants';

export type ChatCompletionMessage = {
// TODO: Update this once https://codedotorg.atlassian.net/browse/CT-471 is resolved
export type AITutorTypesValue = string;
export type AITutorInteractionStatusValue = string;

export {AITutorInteractionStatus, AITutorTypes, PII};

export interface ChatCompletionMessage {
id: number;
role: Role;
chatMessageText: string;
status: Status;
status: string;
timestamp?: string;
};
}

export type AITutorInteraction = {
export interface AITutorInteraction {
userId?: number;
levelId?: number;
scriptId?: number;
type: TutorType | undefined;
type: AITutorTypesValue | undefined;
isProjectBacked?: boolean;
prompt: string;
status: string;
status: AITutorInteractionStatusValue;
aiResponse?: string;
};
}

export type StudentChatRow = {
id: number;
studentName: string;
type: TutorType;
prompt: string;
status: string;
export interface StudentChatRow {
aiModelVersion: string;
aiResponse?: string;
createdAt: string;
};
id: number;
levelId?: number;
projectId?: string;
prompt: string;
scriptId?: number;
status: AITutorInteractionStatusValue;
studentName: string;
type: AITutorTypesValue;
updatedAt?: string;
userId: number;
}

export type StudentServerData = {
export interface StudentServerData {
id: number;
name: string;
ai_tutor_access_denied: boolean;
};
}

export type StudentAccessData = {
export interface StudentAccessData {
id: number;
name: string;
aiTutorAccessDenied: boolean;
};
}

export type Level = {
export interface Level {
id: number;
type: string;
hasValidation: boolean;
isProjectBacked: boolean;
aiTutorAvailable: boolean;
isAssessment: boolean;
};
}

export interface ChatContext {
// studentInput is the last user message for general chat
// or the student's code for compilation and validaiton.
studentInput: string;
tutorType: TutorType | undefined;
tutorType: AITutorTypesValue | undefined;
}

export enum Role {
Expand All @@ -67,10 +80,3 @@ export enum Role {
// only used in Aichat, but our types are currently tangled up :)
MODEL_UPDATE = 'update',
}
export type Status =
(typeof AiTutorInteractionSaveStatus)[keyof typeof AiTutorInteractionSaveStatus];
export const Status = AiTutorInteractionSaveStatus;
export const PII = [Status.EMAIL, Status.ADDRESS, Status.PHONE];

export type TutorType = (typeof AiTutorTypes)[keyof typeof AiTutorTypes];
export const TutorType = AiTutorTypes;
22 changes: 11 additions & 11 deletions apps/src/aichat/chatApi.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
import {
Role,
Status,
AITutorInteractionStatus as Status,
AITutorInteractionStatusValue,
AITutorTypesValue,
ChatCompletionMessage,
PII,
} from '@cdo/apps/aiTutor/types';
import HttpClient from '@cdo/apps/util/HttpClient';
import {CHAT_COMPLETION_URL} from './constants';
import Lab2Registry from '../lab2/Lab2Registry';
import {TutorType} from '../aiTutor/types';

/**
* This function sends a POST request to the chat completion backend controller.
*/
export async function postOpenaiChatCompletion(
messagesToSend: OpenaiChatCompletionMessage[],
levelId?: number,
tutorType?: TutorType
tutorType?: AITutorTypesValue
): Promise<OpenaiChatCompletionMessage | null> {
const payload = levelId
? {levelId: levelId, messages: messagesToSend, type: tutorType}
Expand Down Expand Up @@ -54,7 +54,7 @@ export async function getChatCompletionMessage(
newMessage: string,
chatMessages: ChatCompletionMessage[],
levelId?: number,
tutorType?: TutorType
tutorType?: AITutorTypesValue
): Promise<ChatCompletionResponse> {
const messagesToSend = [
{role: Role.SYSTEM, content: systemPrompt},
Expand All @@ -77,16 +77,16 @@ export async function getChatCompletionMessage(
// For now, response will be null if there was an error.
if (!response) {
return {status: Status.ERROR, id: userMessageId};
} else if (response.status === Status.PROFANITY) {
} else if (response?.status === Status.PROFANITY_VIOLATION) {
return {
status: Status.PROFANITY,
status: Status.PROFANITY_VIOLATION,
id: userMessageId,
assistantResponse:
"I can't respond because your message is inappropriate. Please don't use profanity.",
};
} else if (response && response.status && PII.includes(response.status)) {
} else if (response?.status === Status.PII_VIOLATION) {
return {
status: Status.PERSONAL,
status: Status.PII_VIOLATION,
id: userMessageId,
assistantResponse: `I can't respond because your message is inappropriate. Please don't include personal information like your ${response.status}.`,
};
Expand All @@ -99,12 +99,12 @@ export async function getChatCompletionMessage(
}

type OpenaiChatCompletionMessage = {
status?: Status;
status?: AITutorInteractionStatusValue;
role: Role;
content: string;
};
type ChatCompletionResponse = {
status: Status;
status: AITutorInteractionStatusValue;
id: number;
assistantResponse?: string;
};
6 changes: 5 additions & 1 deletion apps/src/aichat/constants.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
export const CHAT_COMPLETION_URL = '/openai/chat_completion';
import {ChatCompletionMessage, Role, Status} from './types';
import {
ChatCompletionMessage,
Role,
AITutorInteractionStatus as Status,
} from './types';

const initialChatMessages: ChatCompletionMessage[] = [
{
Expand Down
5 changes: 3 additions & 2 deletions apps/src/aichat/redux/aichatRedux.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ import {
ChatCompletionMessage,
AichatLevelProperties,
Role,
Status,
AITutorInteractionStatus as Status,
AITutorInteractionStatusType,
AiCustomizations,
ModelCardInfo,
Visibility,
Expand Down Expand Up @@ -215,7 +216,7 @@ const aichatSlice = createSlice({
},
updateChatMessageStatus: (
state,
action: PayloadAction<{id: number; status: Status}>
action: PayloadAction<{id: number; status: AITutorInteractionStatusType}>
) => {
const {id, status} = action.payload;
const chatMessage = state.chatMessages.find(msg => msg.id === id);
Expand Down
17 changes: 10 additions & 7 deletions apps/src/aichat/types.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
import {LevelProperties} from '@cdo/apps/lab2/types';
import {AiTutorInteractionSaveStatus} from '@cdo/apps/util/sharedConstants';
import {
AiTutorInteractionStatus as AITutorInteractionStatus,
PiiTypes as PII,
} from '@cdo/apps/util/sharedConstants';

// TODO: Update this once https://codedotorg.atlassian.net/browse/CT-471 is resolved
export type AITutorInteractionStatusType = string;

export {PII, AITutorInteractionStatus};

export type ChatCompletionMessage = {
id: number;
role: Role;
chatMessageText: string;
status: Status;
status: AITutorInteractionStatusType;
timestamp?: string;
};

Expand All @@ -16,11 +24,6 @@ export enum Role {
MODEL_UPDATE = 'update',
}

export type Status =
(typeof AiTutorInteractionSaveStatus)[keyof typeof AiTutorInteractionSaveStatus];
export const Status = AiTutorInteractionSaveStatus;
export const PII = [Status.EMAIL, Status.ADDRESS, Status.PHONE];

export enum ViewMode {
EDIT = 'edit-mode',
PRESENTATION = 'presentation-mode',
Expand Down
6 changes: 3 additions & 3 deletions apps/src/aichat/views/ChatMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
AichatLevelProperties,
ChatCompletionMessage,
Role,
Status,
AITutorInteractionStatus as Status,
} from '../types';
import aichatI18n from '../locale';
import moduleStyles from './chatMessage.module.scss';
Expand All @@ -39,7 +39,7 @@ const displayUserMessage = (status: string, chatMessageText: string) => {
{chatMessageText}
</div>
);
} else if (status === Status.INAPPROPRIATE) {
} else if (status === Status.PROFANITY_VIOLATION) {
return (
<div
className={classNames(
Expand All @@ -50,7 +50,7 @@ const displayUserMessage = (status: string, chatMessageText: string) => {
{INAPPROPRIATE_MESSAGE}
</div>
);
} else if (status === Status.PERSONAL) {
} else if (status === Status.PII_VIOLATION) {
return (
<div
className={classNames(
Expand Down
8 changes: 4 additions & 4 deletions apps/src/code-studio/components/aiTutor/aiTutor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React, {useCallback, useEffect} from 'react';
import style from './ai-tutor.module.scss';
import ChatWorkspace from './chatWorkspace';
import {useAppDispatch, useAppSelector} from '@cdo/apps/util/reduxHooks';
import {TutorType} from '@cdo/apps/aiTutor/types';
import {AITutorTypes as TutorTypes} from '@cdo/apps/aiTutor/types';
import {
compilationError,
compilationErrorFirst,
Expand Down Expand Up @@ -74,13 +74,13 @@ const AITutor: React.FunctionComponent = () => {
}, [dispatch]);

useEffect(() => {
if (tutorType === TutorType.COMPILATION) {
if (tutorType === TutorTypes.COMPILATION) {
setCompilationChatMessages();
}
if (tutorType === TutorType.GENERAL_CHAT) {
if (tutorType === TutorTypes.GENERAL_CHAT) {
setGeneralChatMessages();
}
if (tutorType === TutorType.VALIDATION) {
if (tutorType === TutorTypes.VALIDATION) {
setValidationChatMessages();
}
}, [
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import React from 'react';
import style from './chat-messages-table.module.scss';
import {StudentChatRow, TutorType} from '@cdo/apps/aiTutor/types';
import {
StudentChatRow,
AITutorTypes as TutorTypes,
} from '@cdo/apps/aiTutor/types';
import {
genericCompilation,
genericValidation,
Expand All @@ -18,9 +21,9 @@ const AITutorChatMessagesTableRow: React.FunctionComponent<
};

const getPrompt = (chatMessage: StudentChatRow) => {
if (chatMessage.type === TutorType.COMPILATION) {
if (chatMessage.type === TutorTypes.COMPILATION) {
return genericCompilation;
} else if (chatMessage.type === TutorType.VALIDATION) {
} else if (chatMessage.type === TutorTypes.VALIDATION) {
return genericValidation;
} else {
return chatMessage.prompt;
Expand Down

0 comments on commit 9a4ae51

Please sign in to comment.