Skip to content

lang_tools.exercises.conversational_tutor

Conversational tutor exercise (from fala-comigo-ai-tutor).

This exercise is loosely coupled to UserWordProgress: each user turn is sent to a TutorResponseChain (defined in lang_tools.llm.tutor) that produces a correction block plus a conversation continuation. The chain itself is provided by the caller so this module stays free of an LLM dependency.

Classes:

Attributes:

  • TutorChain

    Callable that takes (user_text, history) and returns the tutor reply.

TutorChain module-attribute

TutorChain = Callable[
    [str, list[TutorMessage]], TutorMessage
]

Callable that takes (user_text, history) and returns the tutor reply.

ConversationalTutorExercise dataclass

ConversationalTutorExercise(
    chain: TutorChain | None = None,
    topic: str = "",
    **kwargs: object,
)

Bases: _BaseExercise

Conversational tutor session driven by a caller-supplied chain.

Attributes:

  • chain (TutorChain | None) –

    Callable matching the TutorChain signature.

  • topic (str) –

    Conversation topic shown to the user.

  • history (list[TutorMessage]) –

    Live conversation history; mutated in place.

Initialize with an optional chain and topic.

Parameters:

  • chain (TutorChain | None, default: None ) –

    Callable matching TutorChain. May be None if the exercise will be driven manually.

  • topic (str, default: '' ) –

    Conversation topic.

  • **kwargs (object, default: {} ) –

    Forwarded to _BaseExercise.

Methods:

  • start

    Open the session with an optional tutor greeting.

  • submit

    Send a user message and append the tutor reply.

Source code in src/lang_tools/exercises/conversational_tutor.py
def __init__(
    self,
    chain: TutorChain | None = None,
    topic: str = "",
    **kwargs: object,
) -> None:
    """Initialize with an optional chain and topic.

    Args:
        chain: Callable matching `TutorChain`. May be ``None`` if the
            exercise will be driven manually.
        topic: Conversation topic.
        **kwargs: Forwarded to `_BaseExercise`.
    """
    super().__init__(exercise_type="conversational_tutor", **kwargs)  # type: ignore[arg-type]
    self.chain = chain
    self.topic = topic
    self.history: list[TutorMessage] = []

start

start(
    greeting: TutorMessage | None = None,
) -> ExerciseRound

Open the session with an optional tutor greeting.

Parameters:

  • greeting (TutorMessage | None, default: None ) –

    Initial tutor message; appended to history if provided.

Returns:

  • ExerciseRound

    ExerciseRound whose prompt is the live history reference.

Source code in src/lang_tools/exercises/conversational_tutor.py
def start(self, greeting: TutorMessage | None = None) -> ExerciseRound:
    """Open the session with an optional tutor greeting.

    Args:
        greeting: Initial tutor message; appended to history if provided.

    Returns:
        `ExerciseRound` whose `prompt` is the live `history` reference.
    """
    self._ensure_started()
    if greeting is not None:
        self.history.append(greeting)
    return ExerciseRound(
        prompt={"topic": self.topic, "history": self.history},
        expected=None,
    )

submit

submit(
    round_: ExerciseRound, user_text: str
) -> RoundResult

Send a user message and append the tutor reply.

Parameters:

  • round_ (ExerciseRound) –

    The round returned by start.

  • user_text (str) –

    The user's target-language message.

Returns:

  • RoundResult

    RoundResult whose feedback carries the correction block (if any)

  • RoundResult

    and correct is True when no correction was returned.

Raises:

  • RuntimeError

    If chain is None when the user submits a message.

Source code in src/lang_tools/exercises/conversational_tutor.py
def submit(self, round_: ExerciseRound, user_text: str) -> RoundResult:
    """Send a user message and append the tutor reply.

    Args:
        round_: The round returned by `start`.
        user_text: The user's target-language message.

    Returns:
        `RoundResult` whose `feedback` carries the correction block (if any)
        and `correct` is True when no correction was returned.

    Raises:
        RuntimeError: If `chain` is None when the user submits a message.
    """
    del round_  # state lives on the exercise
    if self.chain is None:
        msg = "ConversationalTutorExercise.chain must be set before submit()."
        raise RuntimeError(msg)

    self.history.append(TutorMessage(role="user", content=user_text))
    reply = self.chain(user_text, list(self.history))
    self.history.append(reply)

    correct = not (reply.correction and reply.correction.strip())
    result = RoundResult(
        correct=correct,
        feedback=reply.correction,
    )
    self._bookkeep(result)
    return result

TutorMessage

Bases: BaseModel

One message in the tutor conversation history.

Attributes:

  • role (Literal['user', 'tutor']) –

    "user" or "tutor".

  • content (str) –

    Target-language text.

  • translation (str | None) –

    Optional user-language translation.

  • correction (str | None) –

    Optional correction block (tutor messages only).