o
    9j.I                     @  s  U d Z ddlmZ ddlZddlZddlZddlZddlZddlZddl	m
Z
mZ e
jd ZdZg dZdZdLddZi Zded< ejD ]ZeeZeddefD ]ZeeZeraeee qSqDdd ejD Zdd ejD ZdMddZdd Zdd Z dNddZ!dOd!d"Z"dPd%d&Z#dQd(d)Z$dRd,d-Z%dSd/d0Z&dTd2d3Z'dTd4d5Z(dTd6d7Z)dUd9d:Z*dVd=d>Z+dWdBdCZ,dXdDdEZ-dYdFdGZ.dYdHdIZ/dYdJdKZ0dS )Zu  Lern-Engine: Vokabel-Mastery-Tracking + Spaced Repetition + Transkript-Bewertung.

Umsetzung der Forschungs-Spec `plappi-learning-engine` (2026-06-22):
- 5-Zustands-Mastery-Maschine (new→receptive→practising→productive→mastered→retired)
  mit Leitner-Box-Scheduler und Spontan-Produktions-Fast-Track.
- Evidenz kommt aus einer LLM-Bewertung des Gesprächs-Transkripts (lemmatisiert,
  morphologie-robust für Serbisch), NICHT aus naivem Substring-Zählen der Kind-Turns.
- Fluency-Fast-Path: spricht das Kind spontan flüssig, werden Wörter sofort gemeistert
  und höhere Tiers freigeschaltet (dein „Kiki sagt cao, kako si, šta radiš…"-Fall).

Single-Child-MVP (child_id=1), aber Tabelle ist multi-child-fähig vorbereitet.
    )annotationsN   )config
curriculumzplappi.sqlite)new	receptive
practising
productivemasteredretireduE
  Du bist eine Bewertungs-Engine für Serbisch als Herkunftssprache (Tutor „Plappi", deutsch-dominante Kinder). Du bekommst EIN Gesprächs-Transkript (Turns 'plappi' und 'child'; Kind-Turns sind rohes ASR eines jungen Kindes: erwarte Rauschen, deutsche Füller, phonetische Schreibweisen) plus Ziel-Vokabeln (serbische LEMMATA + deutsche Glosse + Tier). Aufgabe = morphologische + pragmatische Bewertung, KEINE Übersetzung.

HARTE REGELN:
1. LEMMATISIERE jede Oberflächenform des KINDES auf ihr Lemma (Nomen=Nom.Sg, Verb=Infinitiv, Adj=mask.Nom.Sg). Beispiele: radis/radim->raditi; bila/bio->biti; vrticu->vrtic; macku->macka; idem/isla->ici; dobra/dobro->dobar; dve->dva. Akzeptiere ekavisch+ijekavisch (lepo/lijepo) und phonetisches ASR (macka=mačka, fala=hvala).
2. Bewerte gegen die Ziel-Lemmata; spontane NICHT-Ziel-Lemmata listest du in extra_produced.
3. Pro Lemma der STÄRKSTE Beleg im ganzen Transkript: spontaneous (Kind nutzt es korrekt in EIGENER Äußerung, NICHT als Echo des direkt davor von Plappi gesagten Worts) > prompted (korrekt direkt nach Plappis Vorsagen) > understood (sagt es nicht, reagiert aber passend) > struggled (falsch/falsches Wort/langes Zögern/antwortet deutsch wo Serbisch klar verlangt). Jedes Lemma in GENAU einen Topf.
4. Flexionsfehler bei richtigem Lemma = produced (NICHT struggled). struggled nur wenn das Lemma selbst falsch/abwesend ist.
5. Konservativ: bei <0.6 Konfidenz NICHT als produced markieren. Nie Evidenz erfinden.
6. level_0_100 nach Breite+Spontaneität: 0-10 nur Deutsch; 11-25 einzelne nachgesprochene Wörter; 26-45 A1 mehrere spontane Wörter/2-Wort-Kombis; 46-65 A2 kurze spontane Phrasen, eine Zeitform/Kasus, Fragen; 66-85 B1 verbundene Mehrsatz-Rede; 86-100 B2+. Spontane >=4-Wort-Äußerung mit Tempus/Kasus/Frage => >=55 (A2+).
7. fluency_flag=true, wenn das Kind unprompted >=4 serbische Inhaltswörter ODER eine grammatisch markierte Konstruktion (Tempus/Kasus/Frage) ÜBER dem aktuellen Tier produziert.

Gib AUSSCHLIESSLICH striktes JSON zurück (keine Prosa, kein Markdown), exakt:
{"produced":["lemma",...], "spontaneous":["lemma",...], "understood":["lemma",...], "struggled":["lemma",...], "extra_produced":["lemma",...], "level_0_100":58, "cefr":"A2", "fluency_flag":false, "transcript_quality":"ok", "note":"kurze Begründung"}
- produced = alle korrekt produzierten Ziel-Lemmata; spontaneous = die Teilmenge davon, die spontan (nicht nachgesprochen) kam.
- transcript_quality: ok|noisy|too_short (Kind <2 wertbare Turns => too_short).

ZIEL-VOKABULAR (lemma = glosse [tier]):
{TARGET_VOCAB}

TRANSKRIPT:
{TRANSCRIPT}

Gib jetzt das JSON zurück.sstrreturnc                 C  s(   t d| pd } ddd | D S )NNFKD c                 s  s    | ]
}t |s|V  qd S N)unicodedata	combining).0c r   4/home/nk/hobo-godmode/plappi-mvp/backend/learning.py	<genexpr>8   s    z_norm.<locals>.<genexpr>)r   	normalizelowerjoin)r   r   r   r   _norm6   s   r   zdict[str, str]_SURFACEsrr   c                 C      i | ]}t ||d dqS )tierr   r   lemma_ofgetr   ir   r   r   
<dictcomp>C        r&   c                 C  r   )typer   r!   r$   r   r   r   r&   D   r'   intc                   C  s   t t d S )NQ )r)   timer   r   r   r   _today_daysG   s   r,   c                  C  s   t t} t j| _| S r   )sqlite3connectDBRowrow_factoryr   r   r   r   _connK   s   
r3   c                  C  sF   t  } | d z| d W n	 ty   Y nw |   |   d S )Na4  
    CREATE TABLE IF NOT EXISTS lemma_mastery(
        child_id INT, lemma TEXT, tier INT, word_type TEXT,
        state TEXT DEFAULT 'new', box INT DEFAULT 0, ease REAL DEFAULT 1.0,
        exposure_credits REAL DEFAULT 0,
        prod_spontaneous INT DEFAULT 0, prod_prompted INT DEFAULT 0,
        understood INT DEFAULT 0, struggled INT DEFAULT 0,
        distinct_sessions_produced INT DEFAULT 0,
        last_seen_days INT, next_due_days INT, teach_weight INT DEFAULT 1,
        PRIMARY KEY(child_id, lemma));
    CREATE TABLE IF NOT EXISTS scored_convs(conv_id TEXT PRIMARY KEY, scored_at INT);
    CREATE TABLE IF NOT EXISTS child_state(
        child_id INT PRIMARY KEY, level REAL DEFAULT 0, cefr TEXT DEFAULT 'pre-A1',
        max_tier_unlocked INT DEFAULT 1, updated_at INT, level_floor REAL DEFAULT 0);
    z=ALTER TABLE child_state ADD COLUMN level_floor REAL DEFAULT 0)r3   executescriptexecute	Exceptioncommitcloser2   r   r   r   initQ   s   
r9   promptc              	   C  s   t jd| dt jg}|g d |ddg g}|D ]3}z(tj|ddt jtt jd}|jp,d	 }|j
d	kr@|r@|d
s@|W   S W q tyJ   Y qw dS )Nz-pz--model)--output-formattextz--strict-mcp-configz--mcp-configz{"mcpServers":{}}r;   r<   T)capture_outputr<   timeoutcwdr   r   zError:)r   
CLAUDE_CLILEARNING_MODEL
subprocessrunLEARNING_TIMEOUTr   	BRAIN_CWDstdoutstrip
returncode
startswithr6   )r:   baseattemptscmdroutr   r   r   _call_claudej   s"   



rO   r<   dict | Nonec                 C  s`   | sd S |  d| d}}|dk s||krd S zt| ||d  W S  ty/   Y d S w )N{}r   r   )findrfindjsonloadsr6   )r<   r   er   r   r   _extract_json|   s   rX   turns
list[dict]c                 C  sZ   dd | D }t |dk rdS dd | D }tdtjddd	d
|}tt|S )uI   Bewertet ein Transkript via claude-CLI → lemmatisierte Evidenz + Level.c                 S  s0   g | ]}| d dkr| dpd r|qS )rolechildr<   r   )r#   rG   r   tr   r   r   
<listcomp>   s   0 z&score_conversation.<locals>.<listcomp>r   Nc                 S  s4   g | ]}| d dkrdndd | dpd qS )r[   plappiPlappiKindz: r<   r   r#   r]   r   r   r   r_      s    ,z{TARGET_VOCAB}P   )limitz{TRANSCRIPT}
)lenANALYSIS_PROMPTreplacer   target_vocab_strr   rX   rO   )rY   child_turnslinesr:   r   r   r   score_conversation   s   rm   dict[str, int]c                 C  st   i }| D ]3}| ddkrqt| dpd}t D ]\}}tdt| d|r6| |dd ||< qq|S )uH   Zählt, welche Lehrplan-Lemmas Plappi gesagt hat (saubere Modell-Texte).r[   r`   r<   r   z\br   r   )r#   r   r   itemsresearchescape)rY   countsr^   nnflemr   r   r   _plappi_exposures   s   rw   lemmadictc                 C  s   |  dt|f }|rt|S i dtd|dt|ddt|ddd	d
ddddddddddddddddd dd ddS )Nz8SELECT * FROM lemma_mastery WHERE child_id=? AND lemma=?child_idrx   r    r   	word_typer   stater   boxr   ease      ?exposure_creditsprod_spontaneousprod_prompted
understood	struggleddistinct_sessions_producedlast_seen_daysnext_due_daysteach_weight)r5   CHILD_IDfetchonery   _TIERr#   _WTYPE)r   rx   rM   r   r   r   _row   s<   r   rM   c                 C  sp   |  d|d |d |d |d |d |d |d |d	 |d
 |d |d |d |d |d |d |d f d S )Na)  INSERT OR REPLACE INTO lemma_mastery
        (child_id,lemma,tier,word_type,state,box,ease,exposure_credits,prod_spontaneous,
         prod_prompted,understood,struggled,distinct_sessions_produced,last_seen_days,
         next_due_days,teach_weight)
        VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)rz   rx   r    r{   r|   r}   r~   r   r   r   r   r   r   r   r   r   )r5   r   rM   r   r   r   _save   s   *r   todayc                 C  sL   t dt| d ttjd }|t dttj| | dpd  | d< dS )z+Leitner: next_due aus aktueller Box + ease.r   r}   r   r~   r   r   N)maxminrg   r   BOX_DAYSroundr#   )rM   r   r}   r   r   r   	_schedule   s   .r   c                 C  s   | d dv rdS | d | d  }| d dkr+| d t jks'| d d	ks'|d	kr+d
| d< | d d
kr9|d	kr9d| d< | d dkrW| d t jkrM| d t jksS| d d	krWd| d< | d dkrp| d dkrr| d dkrtt| | dS dS dS dS )uG   Zustandsübergänge gemäß Spec (mehrstufig in einem Schritt erlaubt).r|   r
   r   Nr   r   r   r   r   r   r   r   r   r	      )r   EXPOSURES_FOR_RECEPTIVE!EXPOSURES_FOR_PRODUCTIVE_BASELINEDISTINCT_SESSIONS_FOR_MASTERED_master)rM   r   producedr   r   r   _transition   s    $r   c                 C  s4   d| d< d| d< t | d d| d< |tj | d< d S )Nr
   r|   r   r   r}      r   )r   r   CONFIRMATION_REVIEW_DAYS)rM   r   r   r   r   r      s   r   cefrc                 C  sD   | pd  }|drdS |drdS |ds|dr dS d	S )
Nr   A1r   A2   BC   r   )upperrI   )r   r   r   r   r   _cefr_to_tier   s   

r   conv_idconv_startedc                   sz  t   t }|d| f r|  dddS t }|r#t|d n|}t|p*i  t|} fdd}t	|dt	|d	}}	t	|d
t	|d}
}t	|d}t
 d}t	||B |	B |
B |B }|t	t M }|D ]}t||}||d< t||dtj}|d  |7  < d}||	v r|d  d7  < |d  tj7  < d}n||v r|d  d7  < |d  tj7  < d}||
v r|d
  d7  < |d  d7  < |pd}|||	B v r|d  d7  < ||v r|||	B vr|d  d7  < td|d d |d< td|dpdd |d< n'|r<tttjd |d d |d< ||	v r<td|dp6dd |d< t|| |d dvrMt|| t|| qr|r||	B |B t	t @ D ](}t||}||d< t|d d|d< t|d d|d< t|| t|| qb|d | tt f |  t| | |  |  d d! d"t|| d#d$S )%uG   Wertet EIN Gespräch aus (idempotent) → aktualisiert Mastery + Level.z*SELECT 1 FROM scored_convs WHERE conv_id=?Talready_scored)okskippedr*   c                   s   dd   | p	g D S )Nc                 S  s(   g | ]}t | rt |  qS r   )r   rG   r   )r   xr   r   r   r_      s   ( z1apply_conversation.<locals>.L.<locals>.<listcomp>rc   )keyscoredr   r   L   s   zapply_conversation.<locals>.Lr   spontaneousr   r   extra_producedfluency_flagr   r   r   Fr   r   r   r   r}   r   ffffff?r~   r   皙?g      ?g333333?r|   r   z/INSERT OR REPLACE INTO scored_convs VALUES(?,?)level_0_100r   transcript_quality)r   levelr   r   fluencyquality)r9   r3   r5   r   r8   r,   r)   rm   rw   setboolr#   r   keysr   r   r   MAX_EXPOSURES_PER_SESSIONWEIGHT_SPONTANEOUSWEIGHT_PROMPTEDr   rg   r   r   r   r   r   r+   r7   _update_level)r   r   rY   r   r   day	exposuresr   r   r   r   r   extrar   touchedrv   rM   add_expcorrectr   r   r   apply_conversation   s   

  




r   r   r   r   c                 C  sp  |  dtf }|rt|d nd}|rt|d nd}|d}|d}|dp3|r2|d nd	}|  d
tf d }	z|rHt|d nd}
W n tttfyY   d}
Y nw |}t	|ttfr|dkr|| }|dkrv|d|  }n
|dkr|d|  }|rt
|
d}
|	dkrt
|
d}
t
||
}|rt
|t|n|}|  dtt|d||tt t|
df d S )N*SELECT * FROM child_state WHERE child_id=?r   g        max_tier_unlockedr   r   r   r   pre-A1zXSELECT COUNT(*) n FROM lemma_mastery WHERE child_id=? AND state IN('mastered','retired')rt   level_floor	too_shortr   r   ir   g     F@r   g      9@zINSERT OR REPLACE INTO child_state
                 (child_id, level, cefr, max_tier_unlocked, updated_at, level_floor)
                 VALUES(?,?,?,?,?,?))r5   r   r   floatr)   r#   KeyError
IndexError	TypeError
isinstancer   r   r   r+   )r   r   r   st	cur_levelcur_tiertargetr   r   r   floor	new_leveldiffnew_tierr   r   r   r   2  s@   




"r   c                  C  s4   t   t } dd | dtf D }|   |S )Nc                 S  s   g | ]}t |qS r   )ry   r   rM   r   r   r   r_   W  s    z$get_mastery_rows.<locals>.<listcomp>z,SELECT * FROM lemma_mastery WHERE child_id=?)r9   r3   r5   r   fetchallr8   )r   rowsr   r   r   get_mastery_rowsT  s
   r   c                  C  s>   t   t } | dtf }|   |rt|S ddddS )Nr   r   r   r   )r   r   r   )r9   r3   r5   r   r   r8   ry   r   r   r   r   get_child_state\  s
   r   c           	   
   C  s   t  }t }tj| |t |dd}dd |D }ddd |d D p'd	}dt|p0d	}t|d
dp9d}| d|dd d}|d |d |dg ||||d dddS )uP   Liefert Fokus-/gemeisterte Wörter + Band-Methode + Level für den Agent-Prompt.r   )r   c                 S  s"   g | ]}| d dv r|d qS )r|   r   rx   rc   r   r   r   r   r_   j  s   " z$focus_for_prompt.<locals>.<listcomp>z, c                 s  s(    | ]}|d   d|d  dV  qdS )r   z (de)Nr   )r   fr   r   r   r   k  s   & z#focus_for_prompt.<locals>.<genexpr>focusu   —r   r   z/100 (r   r   r   bandframesmethodr   )r   r   r   	focus_strmastered_strlevel_labelr   )	r   r   r   select_focusr,   r#   r   sortedr   )	ager   r|   selr
   r   r   lvlr   r   r   r   focus_for_promptd  s   r   c                  C  s   t  } t }dd tD }| D ]}||dddd ||dd< qt| dd d	d
}t|ddp5d|dd|dd|d |d  |d |d  |d  t| d|dd |D dS )Nc                 S  s   i | ]}|d qS )r   r   )r   r   r   r   r   r&   w  s    z#mastery_summary.<locals>.<dictcomp>r|   r   r   r   c                 S  s2   |  dtv rt|  ddnd|  dd fS )Nr|   r   r   r   )r#   STATESindex)rM   r   r   r   <lambda>z  s   " z!mastery_summary.<locals>.<lambda>T)r   reverser   r   r   r   r
   r   r   r   r	   )r
   learningtrackedc                 S  sh   g | ]0}t j|d  i d|d  t j|d  i dd|d  |d|dd|dddqS )	rx   r   r   r   r|   r   r   r}   )r   r   rx   r|   r   r}   )r   BY_LEMMAr#   r   r   r   r   r_     s    


z#mastery_summary.<locals>.<listcomp>)r   r   max_tierrs   by_statewords)r   r   r   r#   r   r   rg   )r   r|   r   rM   r   r   r   r   mastery_summaryt  s(   &


r   )r   r   r   r   )r   r)   )r:   r   r   r   )r<   r   r   rP   )rY   rZ   r   rP   )rY   rZ   r   rn   )rx   r   r   ry   )rM   ry   )rM   ry   r   r)   )r   r   r   r)   )r   r   r   r)   rY   rZ   r   ry   )r   ry   r   r   )r   rZ   )r   ry   )1__doc__
__future__r   rU   rp   r-   rB   r+   r   r   r   r   DATA_DIRr/   r   r   rh   r   r   __annotations__ITEMS_itr"   _lemr#   _formru   
setdefaultr   r   r,   r3   r9   rO   rX   rm   rw   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   <module>   sX    
















K
"

