EvaluMe — Text analysis by agents
# How We Built EvaluMe: A Privacy-First AI Writing Coach on Google Cloud Run
From a teacher’s frustration to a multi-agent assessment platform powered by Gemini
— -
## The Spark: A Problem in Our Living Room
It started where many good ideas do: at the intersection of daily life and professional frustration. My partner, a Portuguese and English teacher, would spend evenings reviewing handwritten essays from students who lacked access to consistent feedback. Meanwhile, our teenage daughter — privileged to have that expertise at home — would casually ask for a quick review before submitting her own work.
That contrast gnawed at us. Millions of students worldwide face high-stakes writing assessments without a safety net. Entrance exams like Brazil’s ENEM (taken by over 5 million students annually), TOEFL, IELTS, and numerous academic programs demand excellence, yet timely, quality feedback remains a luxury.
We also encountered another issue: even as AI tools proliferate, most students aren’t prompt engineers. They shouldn’t have to be. The gap between having access to powerful LLMs and actually benefiting from them pedagogically is massive.
EvaluMe was our answer: a global, formative writing assessment platform that turns a photo of handwritten text into structured, pedagogically sound feedback — without storing a single byte of user data.
— -
## The Tech Stack: Why Each Piece Mattered
### Frontend: React + Vite + TypeScript
We needed a fast, modern, and developer-friendly stack that could handle image uploads, provide real-time progress updates, and maintain clean UI states. Vite was a revelation — what Create React App promised but never delivered in terms of speed. TypeScript saved us countless debugging hours when managing complex analysis result types.
```tsx
// Snippet: Handling progressive agent updates
const [analysis, setAnalysis] = useState<AnalysisState>({ status: ‘idle’ });
useEffect(() => {
if (analysis.status === ‘processing’) {
// Poll for results from parallel agents
const interval = setInterval(async () => {
const response = await fetch(`/api/status/${jobId}`);
const data = await response.json();
if (data.linguistic && data.argumentative) {
setAnalysis({ status: ‘complete’, results: data });
}
}, 1000);
return () => clearInterval(interval);
}
}, [analysis.status]);
```
The choice was pragmatic: Vite’s HMR let us iterate UI changes in under 100ms, crucial during a time-pressured hackathon. The component-based architecture made it easy to isolate the analysis display logic, which varies significantly between linguistic and argumentative feedback.
### Backend: FastAPI + Python
FastAPI was non-negotiable. When your core value proposition is ”upload image, get AI analysis,” the API layer must be:
1. Self-documenting (automatic OpenAPI spec)
2. Type-safe (Pydantic models for request/response validation)
3. Async-native (critical for parallel agent calls)
We structured it as a thin orchestration layer:
```python
# Core orchestration logic
@app.post(“/upload”)
async def analyze_writing(file: UploadFile, topic: str):
# 1. Extract text from image
extracted_text = await gemini_client.extract_text(file)
# 2. Launch parallel agents
async with asyncio.TaskGroup() as tg:
linguistic_task = tg.create_task(
linguistic_agent.analyze(extracted_text)
)
argumentative_task = tg.create_task(
argumentative_agent.analyze(extracted_text, topic)
)
# 3. Aggregate and return
return {
“linguistic”: linguistic_task.result(),
“argumentative”: argumentative_task.result()
}
```
This `TaskGroup` pattern was a game-changer. Parallel agent execution cut total processing time from ~8 seconds to ~3.5 seconds — the difference between a demo that feels instant and one that loses the user’s attention.
### The AI Brain: Gemini 2.5 Flash + Pro
We didn’t just “use Gemini.” We orchestrated it.
#### Text Extraction: Gemini 2.5 Flash
We tested dedicated OCR services, but for hackathon velocity, Gemini 2.5 Flash proved to be stunningly capable. Handwritten Portuguese, English, even messy cursive — it handled diverse inputs with surprising grace. One API call, zero integration headaches.
```python
# Simplified extraction prompt
prompt = f”””
Extract all text from this handwritten image.
Return ONLY the clean text, preserving paragraphs.
If text is unclear, mark it with [illegible].
“””
response = model.generate_content([image, prompt])
```
#### Multi-Agent System: Parallel Specialists
Here’s where we got clever. Rather than one giant prompt trying to do everything, we created two specialized agents using the Google Agent Development Kit (ADK):
Agent 1: The Linguistic Purist
- Model: Gemini 2.5 Flash
- Job: Grammar, cohesion, clarity, syntax
- Prompt Engineering: Rubric-based scoring (0–10 scale) with ENEM-inspired criteria
Agent 2: The Argumentative Architect
- Model: Gemini 2.5 Pro (higher reasoning capabilities)
- Job: Thesis strength, logical flow, evidence quality, conclusion effectiveness
- Context: Explicitly fed the user’s topic for relevance scoring
This separation did three things:
1. Improved quality: Specialists beat generalists
2. Enabled parallelization: Agents run simultaneously
3. Simplified debugging: Isolated prompt iteration
— -
## The Creation Path: From AI Studio to Production
### Phase 1: The 30-Minute Validation
We didn’t write a line of backend code for the first two days. Instead, we prototyped the entire analysis flow in Google AI Studio.
Drag image → Write prompt → See result.
That loop let us validate:
- Can Gemini read messy handwriting? ✅
- Can it score objectively? ✅
- Can it provide actionable feedback? ✅
When we saw consistent, pedagogically sound outputs, we knew the idea had legs. AI Studio saved us ~20 hours of blind backend development.
### Phase 2: Cloud Run Deployment (The “Hello World”)
Next, we containerized the simplest possible version: a single endpoint that took an image and returned raw Gemini output. The goal? Prove we could deploy.
```dockerfile
# Dockerfile template that worked first try
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install — no-cache-dir -r requirements.txt
COPY . .
CMD [“uvicorn”, “main:app”, “ — host”, “0.0.0.0”, “ — port”, “8080”]
```
`gcloud run deploy` and we had a live API. 10 minutes from Docker to URL. That speed is why Cloud Run dominates hackathons.
### Phase 3: Frontend Decoupling with Gemini CLI
The AI Studio-generated code was a monolith UI and API logic intertwined. Using Gemini CLI with community prompts, we refactored it into a pure React SPA.
```
$ gemini refactor — split-concerns — target frontend-only
```
This gave us a clean Vite project that spoke HTTP instead of direct SDK calls. The CLI isn’t magic, but it accelerated boilerplate generation for components, routes, and API clients.
### Phase 4: ADK Agent Design
Here’s where it got sophisticated. We used the Google Agent Development Kit’s local Web UI to visually design our dual-agent workflow.
Get Diondcm’s stories in your inbox
Join Medium for free to get updates from this writer.
The ADK let us:
- Test different models side-by-side
- A/B test prompts with real student essays
- Export agent configurations as JSON for version control
We iterated on the linguistic agent’s rubric for two days, feeding it 50+ sample essays until the scoring aligned with our teacher’s expert assessments.
### Phase 5: FastAPI Orchestration
Finally, we stitched it together. The FastAPI layer became a stateless orchestrator that:
1. Validates uploads (max 10MB, image MIME types)
2. Calls Gemini for text extraction
3. Spawns parallel agent tasks
4. Streams status updates via Server-Sent Events (optional)
5. Returns aggregated JSON
The beauty? No database. No state. No infrastructure to manage. Perfect for Cloud Run’s serverless model.
— -
## Architecture Highlights: Stateless by Design
```mermaid
graph TD
User →|uploads image| Frontend[Cloud Run: React SPA]
Frontend →|POST /upload| Backend[Cloud Run: FastAPI]
Backend →|extract_text()| GeminiExtract[Gemini 2.5 Flash]
Backend →|orchestrate()| Agent1[ADK: Linguistic Agent]
Backend →|orchestrate()| Agent2[ADK: Argumentative Agent]
Agent1 →|calls| GeminiLing[Gemini 2.5 Flash]
Agent2 →|calls| GeminiArg[Gemini 2.5 Pro]
GeminiLing →|results| Backend
GeminiArg →|results| Backend
Backend →|JSON response| Frontend
Frontend →|generates| PDF[PDF Report]
PDF →|download| User
```
Key Architectural Decisions:
1. Separate Cloud Run Services: Frontend and backend deploy independently. We can scale, update, or roll back each without affecting the other.
2. Zero Data Retention: Images live in memory only during analysis. Nothing hits disk or Cloud Storage. This isn’t a feature — it’s the default.
3. Parallel Agent Execution: `asyncio.TaskGroup` for concurrency. Cloud Run’s 2 vCPU allocation handles this perfectly.
4. Environment-Based Config: One-click switching between `gemini-2.5-flash` (cheap, fast) and `gemini-2.5-pro` (premium analysis) via environment variables.
— -
## The Hard Parts: What Actually Challenged Us
### Challenge 1: OCR That Doesn’t Suck
Handwriting is chaotic. Slanted letters, crossed-out words, poor lighting. Early tests with basic vision models failed ~30% of the time.
Solution: We engineered the extraction prompt to be resilient. Instead of “extract text,” we asked Gemini to:
```
“Transcribe this as a careful human would. If uncertain, indicate [unclear]
and move on. Preserve line breaks and paragraphs.”
```
This reduced error rates to <5% by setting the right expectation: accuracy over completeness.
### Challenge 2: Fair Scoring Across Languages
A grammar mistake in Portuguese differs from one in English. Our initial rubric was too Brazil-centric.
Solution: We added a language detection step and made the agents self-adaptive:
```python
# Agent prompt snippet
“First, detect the text’s language. Then apply the appropriate
linguistic framework for that language. Score 0–10, but explain the
criteria relative to the detected language.”
```
Now, the same agent handles English, Portuguese, and Spanish essays, providing culturally aware feedback.
### Challenge 3: Teen Privacy Without Sacrificing UX
Requiring registration would kill adoption. Storing essays would violate privacy.
Solution:
- Hardcoded credentials for demo access (`devpost` / `#CloudRunHackathon`)
- Ephemeral processing: Image → Text → Analysis → Garbage collection
- Client-side PDF generation: The report exists only in the user’s browser
Trade-off: Users can’t retrieve past analyses. But for a formative tool, that’s acceptable — and maybe even desirable for privacy.
— -
## Deployment: Cloud Run in the Real World
Our `cloudbuild.yaml` is embarrassingly simple:
```yaml
steps:
— name: ‘gcr.io/cloud-builders/docker’
args: [‘build’, ‘-t’, ‘gcr.io/$PROJECT_ID/evalume-backend’, ‘./backend’]
— name: ‘gcr.io/cloud-builders/docker’
args: [‘push’, ‘gcr.io/$PROJECT_ID/evalume-backend’]
— name: ‘gcr.io/google.com/cloudsdktool/cloud-sdk’
args: [‘gcloud’, ‘run’, ‘deploy’, ‘evalume-backend’, …]
```
Cost Optimization:
- Min instances: 0 (we pay only for usage)
- Max instances: 10 (prevents runaway costs)
- Memory: 1Gi backend, 512Mi frontend (right-sized)
- Gemini: Flash for extraction, Pro only for analysis (token-conscious)
Monitoring:
Cloud Run’s built-in metrics show an average response time of 3.2 seconds, which is acceptable for a complex AI pipeline.
— -
## What We’d Do Next
If this graduates from hackathon to product:
1. Multi-modal Input: Support typed text and .docx uploads
2. Progress Tracking: Optional, encrypted local storage (client-side only)
3. Teacher Dashboard: Aggregate class-level insights (anonymized)
4. Fine-tuned Model: Train a Gemini adapter on expert-scored essays for even better alignment
5. Accessibility: WCAG 2.1 AA compliance, screen reader support
But for now, it does exactly what it says on the tin: helps students improve their writing, privately and instantly.
— -
## The Takeaway
Building EvaluMe reinforced a belief: the best AI tools aren’t the most complex — they’re the most focused.
We didn’t invent new models. We didn’t build a data lake. We leveraged Google’s incredible infrastructure (Cloud Run, Gemini, ADK) and orchestrated it to address a specific human problem.
The result is a system where:
- A student in São Paulo can photograph her essay
- Get feedback in Portuguese
- Download a PDF
- And leave zero trace on our servers
That’s the magic of serverless + AI + intentional design.
— -
Try it yourself: Clone the repo, deploy to Cloud Run, and analyze your own handwriting. The whole stack deploys in under 15 minutes.
Repo: github.com/diondcm/EvaluMe
What would you build if you stopped worrying about infrastructure and started focusing on impact?




