Skip to content

Commit

Permalink
Merge pull request #121 from RickCarlino/prod
Browse files Browse the repository at this point in the history
v4.1.0
  • Loading branch information
RickCarlino authored Feb 2, 2025
2 parents e54a7d0 + c09f490 commit ddf7d51
Show file tree
Hide file tree
Showing 25 changed files with 471 additions and 185 deletions.
17 changes: 10 additions & 7 deletions koala/decks/backfill-decks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,16 @@ export async function backfillDecks(userID: string) {
});

// Step 2: Group the cards by `langCode`
const cardsByLangCode = cards.reduce((acc, card) => {
if (!acc[card.langCode]) {
acc[card.langCode] = [];
}
acc[card.langCode].push(card);
return acc;
}, {} as Record<string, typeof cards>);
const cardsByLangCode = cards.reduce(
(acc, card) => {
if (!acc[card.langCode]) {
acc[card.langCode] = [];
}
acc[card.langCode].push(card);
return acc;
},
{} as Record<string, typeof cards>,
);

// Step 3: Iterate through each language group and backfill
for (const [langCode, cards] of Object.entries(cardsByLangCode)) {
Expand Down
34 changes: 30 additions & 4 deletions koala/equivalence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,34 @@ const PASS = {
result: "pass",
userMessage: "OK",
} as const;
const SYSTEM_PROMPT = `
**System Prompt:**
You are a translation equivalence evaluator. Your sole task is to determine whether a student’s translation of an English sentence into a target language conveys the same meaning as the original sentence. Do not assess grammar, style, or fluency unless these issues alter the overall meaning. Your evaluation should be based solely on semantic equivalence. Follow these guidelines:
1. **Core Meaning:**
- Focus on whether the student’s translation preserves the essential information, intent, and context of the English sentence.
- Ignore variations in syntax, word order, or phrasing as long as the meaning remains the same.
2. **Linguistic Nuance:**
- Recognize that the target language may naturally omit or express elements differently (e.g., pronouns in languages like Korean or differences in tense usage).
- Do not penalize omissions or changes that are linguistically appropriate for the target language and do not alter the core meaning.
3. **Lexical Variation:**
- Accept synonyms, idiomatic expressions, or alternate phrasings that accurately reflect the original meaning.
- Be cautious if key vocabulary is mistranslated or if essential details are missing, as this can change the meaning.
4. **Evaluation Outcome:**
- Respond with “YES” if the translation accurately and fully conveys the meaning of the English sentence, or “NO” if it does not.
Your evaluation should be fair and lenient enough to allow natural language variation while ensuring that any translation marked as equivalent does not reinforce an incorrect meaning.
`;

const buildPrompt = (props: GrammarCorrectionProps): string =>
[
`### SENTENCE A: "${props.userInput}".`,
`### SENTENCE B: "${props.term}".`,
`### Target Sentence: "${props.term}".`,
`### Example Acceptable answer: "${props.definition}".`,
`### User Input: "${props.userInput}".`,
"(YES/NO) Does Sentence A more-or-less mean the same thing as Sentence B?",
"Meanings do not need to be 100% exact, just mostly the same.",
].join("\n");
Expand All @@ -40,7 +63,10 @@ export const equivalence: QuizEvaluator = async (input) => {

const check = async () => {
const response = await openai.beta.chat.completions.parse({
messages: [{ role: "user", content: prompt }],
messages: [
{ role: "system", content: SYSTEM_PROMPT },
{ role: "user", content: prompt },
],
model,
max_tokens: 125,
temperature: 0.1,
Expand All @@ -51,7 +77,7 @@ export const equivalence: QuizEvaluator = async (input) => {
};

const gradeResponses = await Promise.all([check(), check()]);
const passing = gradeResponses.find((response) => response?.evaluation === "yes");
const passing = gradeResponses.map((x) => x?.evaluation).includes("yes");
if (passing) {
return PASS;
} else {
Expand Down
66 changes: 55 additions & 11 deletions koala/fetch-lesson.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,31 @@
import { prismaClient } from "@/koala/prisma-client";
import { Quiz, Card } from "@prisma/client";
import { unique } from "radash";
import { shuffle, unique } from "radash";
import { getUserSettings } from "./auth-helpers";
import { autoPromoteCards } from "./autopromote";
import { errorReport } from "./error-report";
import { maybeGetCardImageUrl } from "./image";
import { LessonType } from "./shared-types";
import { generateLessonAudio } from "./speech";
import { Card, Quiz } from "@prisma/client";

type GetLessonInputParams = {
userId: string;
deckId: number;
now: number;
take: number;
};

type LocalQuiz = Quiz & { Card: Card };
type CardKeys = "imageBlobId" | "definition" | "langCode" | "gender" | "term";
type LocalCard = Pick<Card, CardKeys>;
type QuizKeys =
| "id"
| "repetitions"
| "lastReview"
| "difficulty"
| "stability"
| "quizType"
| "cardId"
| "lapses";
type LocalQuiz = Pick<Quiz, QuizKeys> & { Card: LocalCard };

function interleave<T>(max: number, ...lists: T[][]): T[] {
const result: T[] = [];
Expand Down Expand Up @@ -123,6 +133,34 @@ async function fetchNewCards(
});
}

async function getCardsWithFailures(
userId: string,
take: number,
): Promise<LocalQuiz[]> {
const cards = await prismaClient.card.findMany({
take,
where: {
userId: userId,
lastFailure: { not: 0 },
flagged: { not: true },
},
orderBy: { lastFailure: "asc" },
});
return cards.map((Card): LocalQuiz => {
return {
id: -1 * Math.round(Math.random() * 1000000),
repetitions: 999,
lastReview: 999,
difficulty: 999,
stability: 999,
quizType: "review",
cardId: Card.id,
lapses: 999,
Card,
};
});
}

export async function getLessons(p: GetLessonInputParams) {
const { userId, deckId, now, take } = p;
await autoPromoteCards(userId);
Expand Down Expand Up @@ -174,12 +212,18 @@ export async function getLessons(p: GetLessonInputParams) {
speakingDueOld,
speakingDueNew,
);
const allCards = unique([...newCards, ...oldCards], (q) => q.id);
const failedCards = await getCardsWithFailures(userId, take);
const allCards = unique(
[...failedCards, ...newCards, ...oldCards],
(q) => q.id,
);
const uniqueByCardId = unique(allCards, (q) => q.cardId);
return uniqueByCardId.slice(0, take).map((quiz) => {
const isListening = quiz.quizType === "listening";
const isNew = (quiz.repetitions || 0) < 1;
const quizType = isListening && isNew ? "dictation" : quiz.quizType;
return buildQuizPayload({ ...quiz, quizType }, speedPercent);
});
return shuffle(uniqueByCardId)
.slice(0, take)
.map((quiz) => {
const isListening = quiz.quizType === "listening";
const isNew = (quiz.repetitions || 0) < 1;
const quizType = isListening && isNew ? "dictation" : quiz.quizType;
return buildQuizPayload({ ...quiz, quizType }, speedPercent);
});
}
2 changes: 1 addition & 1 deletion koala/get-lang-name.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@ import { LangCode, supportedLanguages } from "./shared-types";

export const getLangName = (lang: string) => {
const key = lang.slice(0, 2).toLowerCase() as LangCode;
return supportedLanguages[key] || 'target language';
return supportedLanguages[key] || "target language";
};
Loading

0 comments on commit ddf7d51

Please sign in to comment.