🧮 Building a Math Trivia Game Agent with Mistral AI and Maxim

🧮 Building a Math Trivia Game Agent with Mistral AI and Maxim

Ever wanted to create an intelligent game that can generate questions, check answers, and adapt to different difficulty levels? In this tutorial, we'll build a Math Trivia Game using Mistral AI's language model and Maxim for observability. Our agent will be able to generate arithmetic and algebra questions, provide hints, and track scores - all through natural conversation!

What We're Building

Our Math Trivia Game agent will feature:

  • Dynamic question generation (arithmetic and algebra)
  • Three difficulty levels (easy, medium, hard)
  • Answer checking and scoring
  • Hint system
  • Conversational interface
  • Full observability with Maxim logging

Prerequisites

Before we start, make sure you have:

  • Python 3.8+
  • Mistral API key
  • Maxim API key and repository ID
  • Basic understanding of Python and APIs

Resources

  1. If you wish to follow a video to learn about Maxim’s One Line Integration for Mistral for Observability, you can follow this link -

2.Project Code is available in this Google Colab Notebook - https://colab.research.google.com/drive/1oV2liqbMqbCNMHzvlj1cXFNHEbnqKw46?usp=sharing

  1. Maxim Docs (Mistral <> Maxim) - https://www.getmaxim.ai/docs/sdk/python/integrations/mistral/mistral

Step 1: Setting Up Dependencies and Imports

Let's start by importing all the necessary libraries and setting up our foundation:

import os
import json
import uuid
import random
from typing import Dict, Any, List
from mistralai import Mistral
from maxim import Maxim
from maxim.logger.mistral import MaximMistralClient

What's happening here:

  • os and json for environment variables and data handling
  • uuid for generating unique question IDs
  • random for creating varied questions
  • typing for better code documentation
  • mistralai for the AI language model
  • maxim libraries for observability and logging

Step 2: Initialize Logging and Game State

Next, we'll set up our logging system and create a global game state:

# Initialize Maxim logger
logger = Maxim().logger()

# Game state storage
game_state = {
    "score": 0,
    "current_question": None,
    "questions_asked": [],
    "total_questions": 0
}

Why this matters:

  • Maxim provides observability into our AI interactions
  • The game state tracks everything: score, current question, history, and statistics
  • Using a global state keeps our game session persistent across function calls

Step 3: Building the Question Generator

This is the heart of our game - the function that creates math questions dynamically:

def generate_math_question(difficulty: str, question_type: str) -> Dict[str, Any]:
    """Dynamically generate a math question based on difficulty and type"""
    question_id = str(uuid.uuid4())

    def generate_arithmetic_question(level: str):
        if level == "easy":
            # Single digit operations
            a, b = random.randint(1, 9), random.randint(1, 9)
            op = random.choice(['+', '-', '×', '÷'])

            if op == '+':
                question = f"What is {a} + {b}?"
                answer = str(a + b)
            elif op == '-':
                # Ensure positive result
                a, b = max(a, b), min(a, b)
                question = f"What is {a} - {b}?"
                answer = str(a - b)
            elif op == '×':
                question = f"What is {a} × {b}?"
                answer = str(a * b)
            else:  # ÷
                # Ensure clean division
                result = random.randint(2, 9)
                divisor = random.randint(2, 9)
                dividend = result * divisor
                question = f"What is {dividend} ÷ {divisor}?"
                answer = str(result)

Key design decisions:

  • Each question gets a unique UUID for tracking
  • For subtraction, we ensure positive results by swapping numbers if needed
  • For division, we work backwards (result × divisor = dividend) to avoid fractions
  • Questions are generated as strings for easy display

The function continues with medium and hard arithmetic questions, using larger numbers and more complex operations.

Step 4: Adding Algebra Question Generation

Inside the same function, we add algebra capabilities:

def generate_algebra_question(level: str):
    if level == "easy":
        # Simple linear equations: x + a = b or ax = b
        if random.choice([True, False]):
            # x + a = b
            x = random.randint(1, 10)
            a = random.randint(1, 10)
            b = x + a
            question = f"If x + {a} = {b}, what is x?"
            answer = str(x)
        else:
            # ax = b
            x = random.randint(1, 10)
            a = random.randint(2, 5)
            b = a * x
            question = f"If {a}x = {b}, what is x?"
            answer = str(x)

Smart approach:

  • We start with the answer (x) and work backwards to create the equation
  • This ensures every equation has a clean, integer solution
  • Easy questions focus on basic algebraic concepts
  • Medium and hard questions introduce multi-step solving

Step 5: Completing the Question Generator

The function wraps up by storing the question and returning the necessary data:

# Generate question based on type
if question_type == "arithmetic":
    question_text, correct_answer = generate_arithmetic_question(difficulty)
else:  # algebra
    question_text, correct_answer = generate_algebra_question(difficulty)

# Store question in game state
question_data = {
    "question_id": question_id,
    "question": question_text,
    "correct_answer": correct_answer,
    "difficulty": difficulty,
    "type": question_type
}

game_state["current_question"] = question_data
game_state["questions_asked"].append(question_data)
game_state["total_questions"] += 1

return {
    "question_id": question_id,
    "question": question_text,
    "difficulty": difficulty,
    "type": question_type
}

Why we do this:

  • Store complete question data for answer checking
  • Keep a history of all questions asked
  • Return only what the AI needs to present the question
  • Track total questions for statistics

Step 6: Building the Answer Checker

Now we need a function that can verify if the user's answer is correct:

def check_answer(question_id: str, user_answer: str) -> Dict[str, Any]:
    """Check if the user's answer is correct"""
    current_q = game_state.get("current_question")

    if not current_q or current_q["question_id"] != question_id:
        return {
            "error": "Question not found or expired",
            "correct": False,
            "score": game_state["score"]
        }

    correct_answer = current_q["correct_answer"].strip().lower()
    user_answer_clean = user_answer.strip().lower()

    is_correct = correct_answer == user_answer_clean

    if is_correct:
        game_state["score"] += 1

    return {
        "correct": is_correct,
        "correct_answer": current_q["correct_answer"],
        "user_answer": user_answer,
        "score": game_state["score"]
    }

Important features:

  • Validates that the question ID matches the current question
  • Normalizes answers by stripping whitespace and converting to lowercase
  • Updates the score only when the answer is correct
  • Returns comprehensive feedback for the AI to use

Step 7: Adding Helper Functions

We need two more utility functions - one for hints and one for scoring:

def get_hint(question_id: str) -> Dict[str, Any]:
    """Provide a hint for the current question"""
    current_q = game_state.get("current_question")

    if not current_q or current_q["question_id"] != question_id:
        return {"error": "Question not found"}

    # Generate hints based on question type
    hints = {
        "arithmetic": "Break down the calculation step by step.",
        "algebra": "Isolate the variable by performing the same operation on both sides of the equation."
    }

    hint = hints.get(current_q["type"], "Think about the mathematical operations needed.")

    return {
        "hint": hint,
        "question_id": question_id
    }

def get_score() -> Dict[str, Any]:
    """Get the current game score"""
    return {
        "score": game_state["score"],
        "total_questions": game_state["total_questions"],
        "accuracy": game_state["score"] / max(1, game_state["total_questions"]) * 100
    }

Step 8: Defining Tools for Mistral AI

Now we need to tell Mistral AI what tools are available and how to use them:

TOOLS = [
    {
        "type": "function",
        "function": {
            "name": "generate_math_question",
            "description": "Generate a new math question based on difficulty and type",
            "parameters": {
                "type": "object",
                "properties": {
                    "difficulty": {
                        "type": "string",
                        "enum": ["easy", "medium", "hard"],
                        "description": "Difficulty level of the question"
                    },
                    "question_type": {
                        "type": "string",
                        "enum": ["arithmetic", "algebra"],
                        "description": "Type of math question"
                    }
                },
                "required": ["difficulty", "question_type"]
            }
        }
    },
    # ... similar definitions for check_answer, get_hint, get_score
]

This is crucial because:

  • Mistral AI needs to understand what functions are available
  • The schema defines exactly what parameters each function expects
  • Descriptions help the AI choose the right tool for each situation
  • Enums restrict choices to valid options

Step 9: Tool Execution Handler

We need a function that can execute the tools when Mistral AI calls them:

TOOL_FUNCTIONS = {
    "generate_math_question": generate_math_question,
    "check_answer": check_answer,
    "get_hint": get_hint,
    "get_score": get_score
}

def execute_tool_call(tool_call) -> str:
    """Execute a tool call and return the result"""
    function_name = tool_call.function.name
    function_args = json.loads(tool_call.function.arguments)

    if function_name in TOOL_FUNCTIONS:
        try:
            result = TOOL_FUNCTIONS[function_name](**function_args)
            return json.dumps(result)
        except Exception as e:
            return json.dumps({"error": f"Tool execution failed: {str(e)}"})
    else:
        return json.dumps({"error": f"Unknown tool: {function_name}"})

What this does:

  • Maps function names to actual Python functions
  • Parses JSON arguments from Mistral's tool calls
  • Executes the function with proper error handling
  • Returns results as JSON strings

Step 10: Building the Main Game Loop

This is where everything comes together:

def run_math_trivia_game():
    """Main game loop"""
    print("🎮 Welcome to Math Trivia Game with Mistral + Maxim!")
    print("Type 'quit' to exit the game.")
    print("-" * 50)

    # Initialize Mistral client with Maxim observability
    with MaximMistralClient(Mistral(
        api_key=os.getenv("MISTRAL_API_KEY", ""),
    ), logger) as mistral:

        # Initial system message
        messages = [
            {
                "role": "system",
                "content": """You are a Math Trivia Game host. Your job is to:
1. Generate math questions using the generate_math_question tool
2. Check user answers using the check_answer tool
3. Provide hints when requested using the get_hint tool
4. Show scores using the get_score tool
5. Keep the game engaging and fun!

Start by generating an easy arithmetic question to begin the game."""
            }
        ]

Key setup elements:

  • MaximMistralClient wraps the regular Mistral client for observability
  • System message defines the AI's role and available tools
  • Message history starts with clear instructions

Step 11: Handling AI Responses and Tool Calls

The game loop handles the conversation flow:

# Generate initial question
response = mistral.chat.complete(
    model="mistral-small-latest",
    messages=messages,
    tools=TOOLS,
    tool_choice="auto"
)

# Handle initial tool calls
if response.choices[0].message.tool_calls:
    messages.append(response.choices[0].message)

    for tool_call in response.choices[0].message.tool_calls:
        tool_result = execute_tool_call(tool_call)
        messages.append({
            "role": "tool",
            "tool_call_id": tool_call.id,
            "content": tool_result
        })

    # Get the follow-up response
    response = mistral.chat.complete(
        model="mistral-small-latest",
        messages=messages,
        tools=TOOLS,
        tool_choice="auto"
    )

The conversation flow:

  1. AI decides to use a tool based on the system message
  2. We execute the tool and add results to the conversation
  3. AI gets another chance to respond with the tool results
  4. This creates a natural conversation where the AI can use tools seamlessly

Step 12: The Interactive Game Loop

Finally, we handle user input and maintain the conversation:

# Main game loop
while True:
    user_input = input("\\n👤 You: ").strip()

    if user_input.lower() in ['quit', 'exit', 'stop']:
        print("🎮 Thanks for playing! Final score:")
        final_score = get_score()
        print(f"   Score: {final_score['score']}/{final_score['total_questions']}")
        print(f"   Accuracy: {final_score['accuracy']:.1f}%")
        break

    # Add user message
    messages.append({
        "role": "user",
        "content": user_input
    })

    # Get response from Mistral
    response = mistral.chat.complete(
        model="mistral-small-latest",
        messages=messages,
        tools=TOOLS,
        tool_choice="auto"
    )

    # Handle tool calls (similar to above)
    # ... tool execution code ...

    # Display response
    print(f"🤖 Game Host: {response.choices[0].message.content}")

Step 13: Environment Setup and Error Handling

Don't forget proper setup and error handling:

if __name__ == "__main__":
    # Check for required environment variables
    required_vars = ["MISTRAL_API_KEY", "MAXIM_API_KEY", "MAXIM_LOG_REPO_ID"]
    missing_vars = [var for var in required_vars if not os.getenv(var)]

    if missing_vars:
        print(f"❌ Missing required environment variables: {', '.join(missing_vars)}")
        print("\\nPlease set:")
        for var in missing_vars:
            print(f"export {var}=your_value_here")
        exit(1)

    try:
        run_math_trivia_game()
    except Exception as e:
        print(f"❌ Error running game: {e}")
        print("\\nMake sure you have installed required packages:")
        print("pip install mistralai maxim-py")

Running Your Math Trivia Game

Run the game:

python math_trivia_game.py

Set environment variables:

export MISTRAL_API_KEY=your_mistral_key
export MAXIM_API_KEY=your_maxim_key
export MAXIM_LOG_REPO_ID=your_repo_id

Install dependencies:

pip install mistralai maxim-py
0:00
/0:28

What Makes This Special

This isn't just a simple quiz app - it's a sophisticated AI agent that:

  • Learns: Conversation history helps the AI provide better responses
  • Observes: Maxim logging gives you insights into AI behavior
  • Scales: Easy to add new question types or game features

Conclusion

You've built a complete AI-powered math game that demonstrates key concepts in AI agent development: tool usage, state management, conversational flow, and observability. The combination of Mistral AI's language capabilities with structured tools creates an engaging, intelligent game experience.

The beauty of this approach is that the AI naturally manages the game flow, decides when to use tools, and maintains an engaging conversation - all while you maintain complete control over the core game logic through your tool functions.