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.

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.

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:
- Support for more programming languages and assignment types
- Integration with additional LMS platforms (Canvas, Moodle)
- Advanced analytics for class performance insights
- Plagiarism detection capabilities
- 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...