פרידה ולקחים מ Spanish Panda
לפני כמה שנים התחלתי לעבוד על פרויקט צד שיעזור לי לזכור מילים בשפות זרות. חשבתי שיהיה נחמד אם יהיה לי מילון זמין שגם יוכל לזכור את המילים שחיפשתי ואחרי כמה ימים להמשיך להזכיר לי את אותן מילים עד שאני כבר אזכור טוב את משמעותן.
השבוע הורדתי את השרת שהריץ אותו אחרי שהבנתי טוב יותר מה באמת אני צריך ובניתי פיתרון מדויק בהרבה. זה הסיפור בקצרה וכמה לקחים.
1. הכל התחיל בטכנולוגיה
את המוטיבציה לכתוב מילון קיבלתי כשהתחלתי ללמוד על בסיסי נתונים גרפיים. חשבתי שחיבור בין מילים בשפה זה שימוש ממש מושלם לבסיס נתונים גרפי ומאוד אהבתי את הממשק של neo4j וככה חשבתי לפתור שתי בעיות בפרויקט צד אחד: גם אוכל ללמוד neo4j וגם לזכור מילים בשפה זרה. בשביל הזמינות ובשביל לחסוך עבודה על הממשק החלטתי שזה יהיה בוט לטלגרם, וכך נולדה הפנדה הספרדית.
באותו זמן ה AI היה עוד בחיתולים ואי אפשר היה להשתמש ב ChatGPT כדי לתרגם. גוגל טרנסלייט גם החזיר תוצאות בינוניות ולכן השקעתי המון עבודה בלמצוא ולטייב תרגומים ולחבר תוצאות מכמה מילונים. זה היה כיף אבל גם מיותר לגמרי כי עד שסיימתי את הפיתוח כבר היה AI וכל בעיות התרגום שלי נפתרו בשיחה עם קלוד, אבל לא בטוח שאפשר היה לדעת איך דברים יתפתחו באותו זמן.
מבחינת בסיס הנתונים העבודה עם neo4j נראתה מבטיחה בהתחלה אבל מהר מאוד האבסטרקציה נשברה. הבעיה עם neo4j עבורי היתה שהשאילתות היו כתובות בשפה מיוחדת שלהם והיה קשה לבנות שאילתות בסיסיות ולהשתמש בהן כדי לבנות שאילתות מורכבות יותר. מצאתי שאני משכפל הרבה מקוד השאילתות וכך המערכת הפכה לקשה עד בלתי אפשרית לתחזוקה.
בגלל שאחת המטרות של הפרויקט היתה ללמוד טכנולוגיה, המחסום נראה כמו הזדמנות ועברתי לשכתב את המערכת על בסיס נתונים גרפי אחר שנקרא JanusGraph. הוא כתוב ב Java וזו נראתה הזדמנות טובה ללמוד סקאלה וכך נכנסתי להרפתקאה חדשה של לימוד סקאלה, JanusGraph ושפת שאילתות שנקראת גרמלין.
2. המוצר היה רק תירוץ
סקאלה היתה בשבילי אהבה ממבט ראשון. שפה זורמת, אבסטרקציות טובות, קלה להרחבה ועם מערכת טיפוסים חזקה. הבעיה עם סקאלה זה האקוסיסטם אבל זה סיפור לפוסט אחר.
בגלל שזה היה כיף המשכתי להוסיף פיצ'רים לבוט בעיקר בשביל לשחק עם הכלים: יצרתי חידוני אוצר מילים שהבוט שלח, בניתי תשתית גנרית כדי שאפשר יהיה בקלות לבנות בוטים מקבילים לפלטפורמות אחרות כמו ווטסאפ או סלאק, בניתי מנגנון של Spaced Repetition שעושה מאמץ לשלוח למשתמשים בדיוק את המילים שהם צריכים לתרגל, חיברתי חיפוש תמונה ואפשרות להגדיר אוספים שונים של מילים וגם הסברים ואטימיולוגיה לכל מילה שמחפשים ומערכת לניהול אוצר המילים ששמור בבוט ואפשרות לייצא ולייבא את המילים, כמובן שאפשרות לבחור את השפה של הבוט וקבצי הודעות בכל השפות ועוד המון דברים שאני אפילו לא זוכר. כל פעם כשהיה קצת זמן הוספתי עוד פיצ'ר קטן, סך הכל הפרויקט עמד על קצת מעל 10 אלף שורות.
3. עד שהבנתי שאני בעצם לא משתמש בכל הפיצ'רים האלה
השימוש בבוט היה קצת פחות מהנה מהפיתוח שלו. ככל ששמרתי יותר מילים גיליתי שהשיטה של בחני אוצר מילים לא ממש עובדת עבורי. לא משנה כמה פעמים אני מצליח לענות על המילה בבוחן, זה לא עוזר לי להיזכר בה בשיחה אמיתית.
לאט לאט הבנתי שאני משתמש בו רק בתור מילון בטלגרם, ואני חייב להודות שבתור מילון בטלגרם הוא כן היה נחמד, אבל בשביל מילון בטלגרם לא צריך 10 אלף שורות קוד, לא צריך בסיס נתונים גרפי וגם לא שרת. אפשר להסתפק ב val.town.
וככה נפרדתי מכל הטכנולוגיה המתוחכמת והחלפתי אותה בסקריפט הבא, שרץ בחינם לגמרי על val.town (אפילו לא צריך לשלם על הטוקנים ל OpenAI כי גם זה עליהם):
import { OpenAI } from "https://esm.town/v/std/openai";
import { telegramSendMessage } from "https://esm.town/v/vtdocs/telegramSendMessage?v=5";
import {
Bot,
webhookCallback,
} from "https://deno.land/x/grammy@v1.35.0/mod.ts";
const bot = new Bot(Deno.env.get("SPANISH_PANDA_BOT_TOKEN")!);
const SECRET_TOKEN = Deno.env.get("SPANISH_PANDA_BOT_TOKEN")!.split(":")[1];
let isEndpointSet = false;
bot.command("start", (ctx) => ctx.reply("Welcome! Up and running."));
bot.on("message", async (ctx) => {
const text = await ctx.message.text
console.log(`received: ${text}`)
if (text) {
const response = await translateToSpanishWithOpenAI(text);
console.log(`translated to: ${response}`);
ctx.reply(response);
}
});
const handleUpdate = webhookCallback(
bot,
"std/http",
undefined,
undefined,
SECRET_TOKEN
);
async function translateToSpanishWithOpenAI(text: string) {
const openai = new OpenAI();
const completion = await openai.chat.completions.create({
messages: [
{
role: 'system',
content: 'You are a Spanish/English dictionary but you also know all the languages in the world. When you receive a word or phrase in Spanish respond with its English translation. When you receive a word or phrase in another language translate it to Spanish'
},
{
role: "user",
content: "hola"
},
{ role: "assistant", content: `hello` },
{
role: "user",
content: "יש לי חבר"
},
{
role: "assistant",
content:
`tengo un amigo`,
},
{
role: "user",
content: text
},
],
model: "gpt-4",
});
return completion.choices[0].message.content || "Translation Error";
}
export default async (req: Request): Promise<Response> => {
// Set webhook if it is not set yet
// Do this in the HTTP handler so we get the endpoint url from req.url
// This is a no-op if nothing's changed
if (!isEndpointSet) {
await bot.api.setWebhook(req.url, {
secret_token: SECRET_TOKEN,
});
isEndpointSet = true;
}
if (req.method === "POST") {
return await handleUpdate(req);
}
return new Response("TODO GET", { status: 200 });
};
פחות מ 100 שורות קוד, שאומנם עושות הרבה פחות אבל מבחינה פרקטית זה כל מה שאני צריך.
4. מסקנות ומחשבות קדימה
חלק גדול מהכיף בפרויקט צד הוא המשחק עם טכנולוגיה חדשה ופה היה לי הרבה מזה: סקאלה, גרמלין, neo4j, ג'נוס, מילונים, תרגומים ובוטים לטלגרם. אני מקווה שגם בפרויקט הבא שאכתוב אשבור את השיניים ואלמד כלים חדשים.
תקופה ארוכה חשבתי לסדר את הפרויקט ולפרסם אותו בקוד פתוח. איכשהו דווקא לזה לא הגעתי כי תמיד היה נראה יותר כיף לבנות עוד פיצ'ר. אולי זה עוד יקרה אבל אני חושב שבפרויקט צד הבא כדאי להתחיל מלפתוח עמוד גיטהאב מסודר ותיעוד ולבנות את זה כקוד פתוח נקי מההתחלה.
התחלתי את העבודה על הבוט לפני שנתיים או שלוש. מעניין מאוד לחשוב איך העולם השתנה וכמה הייתי צריך להתאמץ בשביל דברים שהיום הם טריוויאליים לגמרי.