TAssist

M

Muhammad Sohail

Guest

Building TAssist: My Journey Creating an AI-Powered Teaching Assistant for Google Classroom​


How I built a full-stack application to revolutionize assignment evaluation using FastAPI, Next.js, and AI models

The Problem That Started It All​


As a teaching assistant struggling with the overwhelming task of evaluating hundreds of coding assignments, I witnessed firsthand how much time and effort goes into providing meaningful feedback. The manual process was not only time-consuming but often led to inconsistent grading and delayed feedback for students. I was lazy not to take manual vivas which would take 2-3 days so I decided to build TAssist so I can check assignments with a single click.

What TAssist Does​


TAssist is a modern web application that:

  • Automatically evaluates student coding assignments using AI models
  • Integrates seamlessly with Google Classroom
  • Provides detailed feedback based on assignment rubrics and instructions
  • Supports multiple file formats including code files, PDFs, and Jupyter notebooks
  • Offers granular control - evaluate entire submissions or specific files

The Tech Stack That Made It Possible​

Backend: FastAPI + Python​

  • FastAPI for the robust REST API
  • Google API Client for Classroom integration
  • Firebase Admin SDK for data management
  • Redis for caching evaluation results
  • OpenAI/DeepSeek models for AI evaluation
  • PyMuPDF & python-docx for file parsing

Frontend: Next.js + TypeScript​

  • Next.js 15 with the App Router
  • TypeScript for type safety
  • TailwindCSS for modern styling
  • Framer Motion for smooth animations
  • React Context API for state management

The Architecture Deep Dive​

1. Authentication Flow with Google OAuth​


The authentication system was one of the most challenging parts. I needed to implement a secure OAuth flow that could handle Google Classroom permissions:


Code:
# backend/routes/auth.py
@router.get("/auth")
async def authenticate():
    creds = None

    # Load existing credentials if available
    if os.path.exists('token.json'):
        creds = Credentials.from_authorized_user_file('token.json', SCOPES)

    # Refresh or re-authenticate if needed
    if not creds or not creds.valid:
        if creds and creds.expired and creds.refresh_token:
            creds.refresh(Request())
        else:
            flow = InstalledAppFlow.from_client_secrets_file('credentials.json', SCOPES)
            creds = flow.run_local_server(
                port=9090,
                access_type='offline',
                prompt='consent'  # Forces refresh token
            )
    return {
        "token": creds.token,
        "refresh_token": creds.refresh_token,
        "token_uri": creds.token_uri,
        "client_id": creds.client_id,
        "client_secret": creds.client_secret
    }

2. Smart File Parsing System​


One of TAssist's standout features is its ability to parse various file formats from student submissions. Here's how I built the ZIP file parser:


Code:
# backend/parse_students_zip.py
import zipfile
import json
import fitz  # PyMuPDF
from docx import Document

def parse_zip_contents(zip_stream):
    extracted_text = ""

    with zipfile.ZipFile(zip_stream, 'r') as archive:
        for file_name in archive.namelist():
            if should_skip(file_name):
                continue

            with archive.open(file_name) as file:
                content = ""

                if is_supported_text_file(file_name):
                    content = file.read().decode('utf-8', errors='ignore')
                elif file_name.endswith('.pdf'):
                    content = extract_text_from_pdf(io.BytesIO(file.read()))
                elif file_name.endswith('.docx'):
                    content = extract_text_from_docx(io.BytesIO(file.read()))
                elif file_name.endswith('.ipynb'):
                    content = extract_text_from_ipynb(io.TextIOWrapper(file, encoding='utf-8'))

                if content:
                    extracted_text += f"\n--- {file_name} ---\n{content.strip()}\n"

    return extracted_text

def extract_text_from_ipynb(file_stream):
    """Extract and format Jupyter Notebook content"""
    try:
        notebook = json.load(file_stream)
        cells = notebook.get("cells", [])
        parts = []

        for i, cell in enumerate(cells):
            cell_type = cell.get("cell_type", "unknown")
            source = ''.join(cell.get("source", []))

            if cell_type == "markdown":
                parts.append(f"[Markdown Cell {i+1}]\n{source}")
            elif cell_type == "code":
                parts.append(f"[Code Cell {i+1}]\n{source}")

        return '\n\n'.join(parts)
    except Exception as e:
        return "[Error reading .ipynb file]"

3. AI-Powered Evaluation Engine​


The heart of TAssist is its evaluation system. I implemented a robust AI evaluation engine with failover support:


Code:
# backend/evaluate_assignment.py
from openai import OpenAI


def evaluate_with_failover(prompt):
    for api_key in API_KEYS:
        client = OpenAI(
            base_url="https://openrouter.ai/api/v1",
            api_key=api_key
        )

        try:
            response = client.chat.completions.create(
                model="deepseek/deepseek-chat-v3-0324:free",
                messages=[
                    {"role": "system", "content": "You are an AI academic evaluator for programming assignments."},
                    {"role": "user", "content": prompt}
                ],
                extra_body={
                    "temperature": 0.7,
                    "max_tokens": 1024,
                    "provider": {"order": ["Chutes"]}
                }
            )

            return response.choices[0].message.content.strip()
        except Exception as e:
            print(f"[WARN] Failed with key '{api_key[:15]}...': {e}")
            continue

    return "❌ All API keys failed for DeepSeek model."

def evaluate_assignment(summary, assignment_text, rubric_text):
    evaluation_prompt = f"""
    You are an academic evaluator. Based on the provided assignment instructions and submitted content, 
    assess the student's work and assign a grade out of 100 with detailed feedback.

    ### Assignment Instructions (Summarized in JSON):
    {summary}

    ### Rubric (Marks Breakdown):
    {rubric_text}

    ### Student Submission Content:
    {assignment_text}

    ### Your Response Format:
    {{ 
        "score": <Total marks>,
        "breakdown": "<Marks breakdown according to rubric>",
        "feedback": "<Detailed evaluation with technical feedback>"
    }}

    Evaluate fairly and provide specific technical feedback on code functionality and implementation.
    """

    return evaluate_with_failover(evaluation_prompt)

The Challenges I Faced and How I Solved Them πŸ§—β€β™‚οΈ

1. Google API Rate Limits


Problem: Google Classroom API has strict rate limits, especially when fetching student names for large classes.

Solution: I implemented concurrent request handling with semaphores and proper error handling:


Code:
# backend/fetch_submissions.py
MAX_CONCURRENT_REQUESTS = 50

async def fetch_all_names(user_ids, headers):
    sem = asyncio.Semaphore(MAX_CONCURRENT_REQUESTS)

    async with aiohttp.ClientSession() as session:
        tasks = [fetch_name(session, headers, user_id, sem) for user_id in user_ids]
        results = await asyncio.gather(*tasks, return_exceptions=True)

    return {user_id: name for user_id, name in results if isinstance(name, str)}

2. Complex File Structure Parsing


Problem: Student submissions come in various formats - ZIP files containing multiple code files, PDFs, notebooks, etc.

Solution: I built a comprehensive file parser that handles multiple formats and nested structures while maintaining context about file origins.

3. State Management in React


Problem: Managing complex evaluation states, file selections, and real-time updates across components.

Solution: Used React Context API with TypeScript for type-safe state management:


Code:
// frontend/context/AssignmentContext.tsx
interface AssignmentContextType {
  details: AssignmentDetails | null;
  setDetails: (details: AssignmentDetails) => void;
  evaluations: Record<string, EvaluationResult>;
  setEvaluations: (evaluations: Record<string, EvaluationResult>) => void;
}

export const AssignmentProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
  const [details, setDetails] = useState<AssignmentDetails | null>(null);
  const [evaluations, setEvaluations] = useState<Record<string, EvaluationResult>>({});

  return (
    <AssignmentContext.Provider value={{ details, setDetails, evaluations, setEvaluations }}>
      {children}
    </AssignmentContext.Provider>
  );
};

Key Features That Make TAssist Special​

1. Intelligent Instruction Parsing


TAssist automatically detects assignment types (coding vs. subjective) and adapts its evaluation approach:


Code:
# backend/summarize_instructions.py
def summarize_instructions(pdf_text: str):
    assignment_type = detect_assignment_type(pdf_text)

    if assignment_type == "coding":
        prompt = CODING_ASSIGNMENT_PROMPT
    else:
        prompt = SUBJECTIVE_ASSIGNMENT_PROMPT

    return evaluate_with_failover(prompt + "\n\nAssignment Instructions:\n" + pdf_text)

2. Granular File Selection


Teachers can choose to evaluate specific files rather than entire submissions:


Code:
const toggleFileSelection = (user_id: string, file_id: string) => {
  setSelectedFiles(prev => {
    const currentFiles = prev[user_id] || [];
    const newFiles = currentFiles.includes(file_id)
      ? currentFiles.filter(id => id !== file_id)
      : [...currentFiles, file_id];

    return { ...prev, [user_id]: newFiles };
  });
};

3. Redis-Powered Caching


Evaluation results are cached to avoid re-processing and provide instant access to previous evaluations:


Code:
# backend/redis_client.py
def store_evaluation(coursework_id: str, evaluation_data: dict):
    try:
        redis_client.setex(
            f"evaluation:{coursework_id}", 
            3600,  # 1 hour TTL
            json.dumps(evaluation_data)
        )
    except Exception as e:
        print(f"Failed to store evaluation: {e}")

What I Learned Building TAssist​

1. API Integration at Scale


Working with Google's APIs taught me about proper authentication flows, rate limiting, and handling edge cases in third-party integrations.

2. AI Prompt Engineering


Crafting effective prompts for consistent, reliable evaluation results was an art form. I learned to be specific about output formats and provide clear evaluation criteria.

Evaluated Assignment

3. File Processing Challenges


Handling diverse file formats while maintaining performance taught me about streaming, memory management, and format-specific parsing libraries.

4. User Experience Design


Building an interface that teachers would actually want to use required understanding their workflow and pain points deeply.

Courses Page

The Impact and Future​


TAssist has the potential to:

  • Save hours of manual grading time for educators
  • Provide consistent evaluation criteria across all submissions
  • Offer immediate feedback to students
  • Scale evaluation for large classes efficiently

Future Enhancements I'm Planning:​

  1. Support for more programming languages and assignment types
  2. Integration with additional LMS platforms (Canvas, Moodle)
  3. Advanced analytics for class performance insights
  4. Plagiarism detection capabilities
  5. Mobile app for on-the-go evaluation review

Technical Lessons and Best Practices​

1. Error Handling is Critical


With multiple external APIs and AI services, robust error handling and graceful degradation are essential:


Code:
try:
    evaluation_result = evaluate_assignment(summary, assignment_text, rubric_text)
    store_evaluation(coursework_id, result_data)
    return {"evaluation_result": json_to_readable_string(evaluation_result)}
except Exception as e:
    print(f" Unhandled exception: {e}")
    raise HTTPException(status_code=500, detail=f"Error evaluating assignment: {str(e)}")

2. Type Safety Saves Time


Using TypeScript throughout the frontend prevented countless runtime errors:


Code:
interface Submission {
  user_id: string;
  name: string;
  state: string;
  is_empty: boolean;
  files?: FileInfo[];
}

3. Performance Optimization


Implementing proper caching, lazy loading, and concurrent processing made the difference between a slow tool and a delightful experience.

Conclusion: Building with Passion​


Building TAssist has been one of the most rewarding projects I've worked on. I feel that automating something meaningful is truly exciting. The codebase has many problems but it can always be improved.

Want to Try TAssist?​


The project is currently in development, but I'm always looking for feedback from educators and developers. If you're interested in testing it out or contributing to the project, feel free to reach out!

Tech Stack Summary:

  • Backend: FastAPI, Google APIs, OpenAI, Redis, Firebase
  • Frontend: Next.js 15, TypeScript, TailwindCSS, Framer Motion
  • Infrastructure: OAuth2, REST APIs, File Processing, AI Integration

What's your experience with educational technology? Have you built similar tools or faced challenges in the education space? I'd love to hear your thoughts and experiences in the comments below!

Tags: #AI #Education #FastAPI #NextJS #GoogleAPI #FullStack #Python #TypeScript #OAuth #WebDevelopment

Continue reading...
 


Join 𝕋𝕄𝕋 on Telegram
Channel PREVIEW:
Back
Top