Skip to content

Commit

Permalink
80 8 offensive language in discussions (#98)
Browse files Browse the repository at this point in the history
* refactor: move Firestore utility functions to server module and update imports

* feat: add warning fields to conversation and group schemas, update related types and functions

* feat: update conversation and group schemas to include nullable warning fields, enhance moderation checks in discussions

* fix: update session ID in test script and refine moderation handling in discussion API

* feat: add moderation field to group schema and update discussion handling

* feat: integrate content moderation check in discussion addition endpoint

* feat: add warning field for moderation and off-topic checks in group creation

* feat: simplify warning structure by removing off-topic field in group creation
  • Loading branch information
TakalaWang authored Dec 25, 2024
1 parent b2664f2 commit fbd1b7b
Show file tree
Hide file tree
Showing 17 changed files with 236 additions and 223 deletions.
2 changes: 1 addition & 1 deletion scripts/createTestStudent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { GroupSchema } from '../src/lib/schema/group';
import { ProfileSchema } from '../src/lib/schema/profile';
import { adminDb } from '../src/lib/server/firebase'; // 假設有一個 Firebase store 模組

const sessionId = 'HtxeyVYERenrLEjfyo5D';
const sessionId = 'tWwNvzhumqTFS4YMF6Cu';
const numberOfStudents = 40;
const numberOfGroups = 8;

Expand Down
8 changes: 7 additions & 1 deletion src/lib/schema/conversation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,13 @@ export const ConversationSchema = z.object({
z.object({
role: z.enum(['system', 'user', 'assistant']),
content: z.string(),
audio: z.string().nullable() // to find the raw file
audio: z.string().nullable(), // to find the raw file
warning: z
.object({
moderation: z.boolean().default(false),
offTopic: z.boolean().default(false)
})
.nullable()
})
),
warning: z.object({
Expand Down
8 changes: 5 additions & 3 deletions src/lib/schema/group.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,16 @@ export const GroupSchema = z.object({
z.object({
content: z.string(),
id: z.string().nullable(),
speaker: z.string(), // i am not sure if this is going to be used
audio: z.string().nullable() // to find the raw file
speaker: z.string(),
audio: z.string().nullable(), // to find the raw file
moderation: z.boolean().default(false)
})
),
updatedAt: z.date().nullable(),
status: z.enum(['discussion', 'summarize', 'end']).default('discussion'),
summary: z.string().nullable(), // lock on stage 2 finalize transaction
keywords: z.record(z.string(), z.number().min(1).max(5))
keywords: z.record(z.string(), z.number().min(1).max(5)),
moderation: z.boolean().default(false)
});

export interface GroupDiscussionMessage {
Expand Down
173 changes: 173 additions & 0 deletions src/lib/server/firebase.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { env } from '$env/dynamic/private';
import type { Conversation } from '$lib/schema/conversation';
import type { Group } from '$lib/schema/group';
import type { Session } from '$lib/schema/session';
import type { LLMChatMessage } from '$lib/server/types';
import { error } from '@sveltejs/kit';
import { cert, getApps, initializeApp, type ServiceAccount } from 'firebase-admin/app';
import { getAuth } from 'firebase-admin/auth';
import { getFirestore } from 'firebase-admin/firestore';
Expand All @@ -22,3 +27,171 @@ if (!getApps().length) {

export const adminAuth = getAuth();
export const adminDb = getFirestore();

export async function createConversation(
id: string,
group_number: string,
userId: string,
task: string,
subtasks: string[],
history: LLMChatMessage[],
resources: { name: string; content: string }[]
) {
const conversationsRef = adminDb
.collection('sessions')
.doc(id)
.collection('groups')
.doc(group_number)
.collection('conversations');

const existingConversations = await conversationsRef.get();
if (!existingConversations.empty) {
return existingConversations.docs[0].id;
}

const conversationRef = conversationsRef.doc();
await conversationRef.set({
userId: userId,
task: task,
subtasks: subtasks,
resources: resources,
history: history,
subtaskCompleted: new Array(subtasks.length).fill(false),
warning: { moderation: false, off_topic: 0 }
});

return conversationRef.id;
}
export function getConversationRef(id: string, group_number: string, conv_id: string) {
return adminDb
.collection('sessions')
.doc(id)
.collection('groups')
.doc(group_number)
.collection('conversations')
.doc(conv_id);
}

export function getConversationsRef(id: string, group_number: string) {
return adminDb
.collection('sessions')
.doc(id)
.collection('groups')
.doc(group_number)
.collection('conversations');
}

export async function getConversationData(
conversation_ref: FirebaseFirestore.DocumentReference<FirebaseFirestore.DocumentData>
): Promise<Conversation> {
const conversation = await conversation_ref.get();
if (!conversation.exists) {
throw error(404, 'Conversation not found');
}

return conversation.data() as Conversation;
}

export async function getConversationsData(
conversations_ref: FirebaseFirestore.CollectionReference<
FirebaseFirestore.DocumentData,
FirebaseFirestore.DocumentData
>
) {
const conversations = await conversations_ref.get();
if (conversations.empty) {
throw error(404, 'Conversations not found');
}

return conversations.docs.map((doc) => doc.data() as Conversation);
}

export function getGroupRef(id: string, group_number: string) {
return adminDb.collection('sessions').doc(id).collection('groups').doc(group_number);
}

export function getGroupsRef(id: string) {
return adminDb.collection('sessions').doc(id).collection('groups');
}

export async function getGroupData(
group_ref: FirebaseFirestore.DocumentReference<FirebaseFirestore.DocumentData>
): Promise<Group> {
const group = await group_ref.get();
if (!group.exists) {
throw error(404, 'Group not found');
}

return group.data() as Group;
}

export async function getGroupsData(
groups_ref: FirebaseFirestore.CollectionReference<FirebaseFirestore.DocumentData>
): Promise<Group[]> {
const groups = await groups_ref.get();
if (groups.empty) {
throw error(404, 'Groups not found');
}

return groups.docs.map((doc) => doc.data() as Group);
}

export function getSessionRef(id: string) {
return adminDb.collection('sessions').doc(id);
}

export async function getSessionData(
session_ref: FirebaseFirestore.DocumentReference<FirebaseFirestore.DocumentData>
): Promise<Session> {
const session = await session_ref.get();
if (!session.exists) {
throw error(404, 'Session not found');
}

return session.data() as Session;
}

export async function getConversationsFromAllParticipantsData(
id: string
): Promise<Array<Conversation & { groupId: string; conversationId: string }>> {
// 先獲取所有群組
const groupsRef = getGroupsRef(id);
const groups = await groupsRef.get();

if (groups.empty) {
throw error(404, 'No groups found');
}

// 獲取每個群組中的所有對話
const conversationsPromises = groups.docs.map(async (groupDoc) => {
const conversationsRef = getConversationsRef(id, groupDoc.id);
const conversations = await conversationsRef.get();

return conversations.docs.map((doc) => ({
...(doc.data() as Conversation),
groupId: groupDoc.id,
conversationId: doc.id
}));
});

const allConversations = await Promise.all(conversationsPromises);
const flattenedConversations = allConversations.flat();

if (flattenedConversations.length === 0) {
throw error(404, 'No conversations found');
}

return flattenedConversations;
}

export async function checkRemoveParticipantPermission(
sessionId: string,
userId: string,
participantToRemove: string
): Promise<boolean> {
const sessionRef = getSessionRef(sessionId);
const session = await getSessionData(sessionRef);

// 檢查是否為 session host 或是要被移除的參與者本人
return session.host === userId || userId === participantToRemove;
}
17 changes: 7 additions & 10 deletions src/lib/server/llm.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { env } from '$env/dynamic/private';
import type { Resource } from '$lib/schema/resource';
import type { LLMChatMessage, StudentSpeak } from '$lib/utils/types';
import type { Discussion, LLMChatMessage } from '$lib/server/types';
import fs from 'fs/promises';
import { OpenAI } from 'openai';
import { zodResponseFormat } from 'openai/helpers/zod';
Expand All @@ -19,7 +19,7 @@ const openai = new OpenAI({
baseURL: env.OPENAI_BASE_URL
});

async function isHarmfulContent(
export async function isHarmfulContent(
content: string
): Promise<{ success: boolean; harmful: boolean; error?: string }> {
console.log('Checking content for harmful content:', { contentLength: content.length });
Expand Down Expand Up @@ -399,16 +399,13 @@ export async function summarizeConcepts(
}
}

export async function summarizeGroupOpinions(student_opinion: StudentSpeak[]): Promise<{
success: boolean;
summary: string;
keywords: Record<string, number>;
error?: string;
}> {
export async function summarizeGroupOpinions(
student_opinion: Discussion[]
): Promise<{ success: boolean; summary: string; keywords: string[]; error?: string }> {
try {
const formatted_opinions = student_opinion
.filter((opinion) => opinion.role !== '摘要小幫手')
.map((opinion) => `${opinion.role}: ${opinion.content}`)
.filter((opinion) => opinion.speaker !== '摘要小幫手')
.map((opinion) => `${opinion.speaker}: ${opinion.content}`)
.join('\n');

const system_prompt = GROUP_OPINION_SUMMARY_PROMPT.replace(
Expand Down
13 changes: 8 additions & 5 deletions src/lib/utils/types.ts → src/lib/server/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,20 @@ export interface DBChatMessage {
role: 'user' | 'assistant' | 'system';
content: string;
audio: string | null;
warning: {
moderation: boolean;
offTopic: boolean;
} | null;
}

export interface Discussion {
id: string | null;
content: string;
speaker: string;
}

export interface StudentSpeak {
role: string;
content: string;
warning: {
moderation: boolean;
offTopic: boolean;
};
}

export interface SummaryData {
Expand Down
Loading

0 comments on commit fbd1b7b

Please sign in to comment.