DEV Community

Cover image for TAssist
Muhammad Sohail
Muhammad Sohail

Posted on

TAssist

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:

# 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
    }
Enter fullscreen mode Exit fullscreen mode

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:

# 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]"
Enter fullscreen mode Exit fullscreen mode

3. AI-Powered Evaluation Engine

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

# 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)
Enter fullscreen mode Exit fullscreen mode

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:

# 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)}
Enter fullscreen mode Exit fullscreen mode

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:

// 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>
  );
};
Enter fullscreen mode Exit fullscreen mode

Key Features That Make TAssist Special

1. Intelligent Instruction Parsing

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

# 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)
Enter fullscreen mode Exit fullscreen mode

2. Granular File Selection

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

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 };
  });
};
Enter fullscreen mode Exit fullscreen mode

3. Redis-Powered Caching

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

# 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}")
Enter fullscreen mode Exit fullscreen mode

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:

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)}")
Enter fullscreen mode Exit fullscreen mode

2. Type Safety Saves Time

Using TypeScript throughout the frontend prevented countless runtime errors:

interface Submission {
  user_id: string;
  name: string;
  state: string;
  is_empty: boolean;
  files?: FileInfo[];
}
Enter fullscreen mode Exit fullscreen mode

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

Top comments (0)