flashcards.go
2348 bytes
1package adapters
2
3import (
4 "context"
5 "encoding/csv"
6 "fmt"
7 "os"
8 "sync"
9
10 "git.theedgeofrage.com/TheEdgeOfRage/kaiwari/internal/llm"
11)
12
13// flashcardsPath is where generated vocabulary cards are persisted, next to the
14// raw model log. It is a plain append-only CSV for an external flashcard app.
15const flashcardsPath = "flashcards.csv"
16
17const flashcardHeader = "kana,romaji,english,explanation"
18
19// FlashcardModel adapts an llm.Client to game.FlashcardModel. It asks the model
20// to generate a sentence from instructions and turn it into raw CSV, validates
21// the rows in Go, and appends them to the persistent local deck.
22type FlashcardModel struct {
23 Client *llm.Client
24}
25
26func (m *FlashcardModel) Generate(ctx context.Context, instructions string) (int, error) {
27 ctx, cancel := context.WithTimeout(ctx, sideTaskTimeout)
28 defer cancel()
29 raw, err := m.Client.Generate(ctx, llm.BuildFlashcardMessages(instructions))
30 if err != nil {
31 return 0, err
32 }
33 dumpRaw("flashcards", "", raw)
34 rows, err := llm.ParseFlashcards(raw)
35 if err != nil {
36 return 0, err
37 }
38 if len(rows) == 0 {
39 return 0, nil
40 }
41 if err := appendFlashcards(flashcardsPath, rows); err != nil {
42 return 0, err
43 }
44 return len(rows), nil
45}
46
47// flashcardsMu serializes deck appends so concurrent sessions cannot
48// double-write the header or interleave rows.
49var flashcardsMu sync.Mutex
50
51// appendFlashcards appends valid rows to the deck file, creating it with the
52// header when it does not exist yet. Existing rows are never rewritten.
53func appendFlashcards(path string, rows []llm.FlashcardRow) error {
54 flashcardsMu.Lock()
55 defer flashcardsMu.Unlock()
56
57 f, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
58 if err != nil {
59 return fmt.Errorf("open flashcards: %w", err)
60 }
61
62 err = func() error {
63 fi, err := f.Stat()
64 if err != nil {
65 return fmt.Errorf("stat flashcards: %w", err)
66 }
67 if fi.Size() == 0 {
68 if _, err := f.WriteString(flashcardHeader + "\n"); err != nil {
69 return fmt.Errorf("write flashcards header: %w", err)
70 }
71 }
72 w := csv.NewWriter(f)
73 for _, r := range rows {
74 if err := w.Write([]string{r.Kana, r.Romaji, r.English, r.Explanation}); err != nil {
75 return fmt.Errorf("write flashcard row: %w", err)
76 }
77 }
78 w.Flush()
79 return w.Error()
80 }()
81 if cerr := f.Close(); err == nil {
82 err = cerr
83 }
84 return err
85}