""" CuriosityEngine — proactive vocabulary gap analysis. Every N interactions (default: 5), sends the last 10 vocabulary entries to the LLM and asks it to identify one related agricultural / everyday term that is missing from the learner's vocabulary, then formulate a question asking the user how to say that word in their language. Usage in app_lab.py: _curiosity = CuriosityEngine(interval=5) # Inside _run_llm_and_tts, after the main LLM call: question = _curiosity.maybe_ask(_memory, _gemma) if question: history.append({"role": "assistant", "content": f"🌱 {question}"}) """ from __future__ import annotations import logging from typing import TYPE_CHECKING, Optional if TYPE_CHECKING: from src.memory.memory_manager import MemoryManager from src.llm.gemma_client import GemmaClient logger = logging.getLogger(__name__) _CURIOSITY_SYSTEM = """\ You are a language-learning assistant that notices gaps in a West African vocabulary list. Reply with a single valid JSON object and nothing else.\ """ _CURIOSITY_USER_TEMPLATE = """\ Here are the {n} most recent words I have learned so far: {vocab_list} Based on these words, what is ONE related agricultural or common everyday term \ I am likely missing? Formulate a short, warm question asking the user how to say \ that missing word in their language. Reply only with this JSON: {{ "word_suggestion": "", "question": "" }} """ class CuriosityEngine: """Fires a vocabulary-gap prompt every `interval` user interactions.""" def __init__(self, interval: int = 5) -> None: self._interval = interval self._interaction = 0 def maybe_ask( self, memory: "MemoryManager", gemma: "GemmaClient", ) -> Optional[str]: """ Increment the interaction counter. On every `interval`-th call, query the LLM for a missing vocabulary term and return the question string. Returns None on all other calls, or if vocabulary is too sparse, or if the LLM call fails. """ self._interaction += 1 if self._interaction % self._interval != 0: return None entries = memory.get_all() if len(entries) < 3: logger.debug("CuriosityEngine: vocabulary too sparse (%d entries)", len(entries)) return None recent = entries[-10:] lines = [ f" [{e.get('language','?')}] {e.get('word','')} = {e.get('translation','')}" for e in recent ] prompt = _CURIOSITY_USER_TEMPLATE.format( n=len(lines), vocab_list="\n".join(lines), ) try: # Pass the curiosity prompt as user text; empty vocab context to avoid # duplicating the word list inside the system prompt. result = gemma.chat(prompt, vocabulary_context="(see above)") question = result.get("question") or result.get("response") if question: word = result.get("word_suggestion", "") logger.info( "CuriosityEngine: suggesting '%s' — %s", word, question[:80], ) return question.strip() except Exception as exc: logger.warning("CuriosityEngine: LLM call failed: %s", exc) return None