Inspiration

A thesis defense is the most consequential hour in a student's research life, and almost nothing exists to prepare them for it.

What is available is generic question banks. They have never read the student's manuscript, they cannot react to the quality of an answer, and they ask about the topic rather than about the argument. So students walk into their defense having never been tested on the real weak point of their own work, which is precisely where an experienced examiner aims first.

The gap is not a shortage of questions. It is that nothing has read the document the questions should come from.

CITRA Viva closes that by doing the one thing a question bank cannot. It reads the manuscript first, and everything it asks comes out of what it found there.

What it does

Five agents run a defense.

  1. The Draft Analyzer reads the full manuscript and builds a Weakness Map: the points where a skeptical examiner would press hardest, each one anchored to a verbatim quote from the text.
  2. The Question Strategist turns that map into an ordered examination, weighted by how hard a committee is likely to press rather than by where the weaknesses appear in the draft.
  3. The Examiner asks each question, judges the answer, and decides in the same breath what to do next: press harder, offer a chance to clarify, move on, or record the point as undefended.
  4. The Session Reflection agent writes the closing report from the transcript, and produces the habits behind the gaps so the next session is sharper than this one.
  5. A Claim-Support Checker runs beside the defense on a different question: does this cited source actually carry the sentence it was cited for?

The defense can be spoken. The examiner's question is synthesised while the turn is being judged, so the voice arrives with the text rather than after it. A spoken answer goes up to a live model in 100-millisecond frames while the student is still talking, so the transcript lands under a second after they stop instead of several seconds later, and the end of the turn is declared by the student rather than guessed at by the API.

Every session ends with an indicator on the 4.00 scale, and the distinction that makes it defensible is where the number comes from. It is computed from what the examiner recorded turn by turn, never asked of a model. The same transcript always produces the same score, every point traces to a specific judged answer, and the workings are shown beside the total.

The report leaves as a formatted PDF, with the indicator, its workings, and the full transcript, and as Markdown beside it, because the two are for different readers: one is handed to a supervisor, the other is pasted back into the next session.

And it can all be taken back. One request deletes every session a student has run, every manuscript they uploaded, and every Weakness Map made from them. The maps are the part that is easy to miss: they live in a different collection from the sessions and every finding in one quotes the manuscript word for word, so a deletion that took the sessions alone would leave a student's own sentences behind after they asked to be forgotten. The sign-in goes last, never first, because a failure partway through the other order would strand the manuscripts with nobody able to sign in and reach them.

The constraint that shapes everything

The agent never writes the student's argument for them. It asks, it challenges, and it records. It never supplies the defense.

That rule is enforced in the schema rather than left to good intentions. There is no "suggested fix" field and no "replacement sentence" field anywhere in the Weakness Map.

It also decided a feature we refused. Answer recommendations would have been easy to build and would have destroyed the product: a student who can read a suggested answer before replying is not defending anything. What shipped instead is the marking scheme. The student may reveal what a good answer would need to contain, never what to say, and the reveal is recorded in the transcript so it appears in the report. Help that hides itself from the record is not help.

How we built it

Model. gemini-3.7-flash on the Gemini Enterprise Agent Platform, formerly Vertex AI, is the reasoning behind all five agents. Three further Gemini models carry the voice: gemini-2.5-flash-tts speaks the examiner's question, gemini-live-2.5-flash-native-audio transcribes the student's answer while it is being spoken, and gemini-3.5-flash transcribes a whole uploaded recording on the browsers that cannot open an audio context at 16 kHz. That last one is not a leftover. It is the path a real fraction of devices take, and it is described under Challenges below.

Agent framework. All five agents are declared as Google ADK LlmAgent instances with a bound output_schema and with disallow_transfer_to_parent and disallow_transfer_to_peers set. That last pair is the architecture, not a preference: ADK itself refuses a handoff between agents, so the separation does not depend on a prompt asking for it or on anyone remembering. The Google GenAI SDK serves the requests, which keeps the running container small.

Infrastructure. Cloud Run serves both the API and the web app. Firestore holds every piece of state: the session, the manuscript, and the weakness profile carried between sessions. Firebase Authentication decides who may open a session. Cloud Trace records one span per agent call, nested under the request, so the chain of decisions behind a single turn can be read after the fact. Cloud Build and Artifact Registry build and store the images.

What is enforced in code rather than requested in a prompt:

  • Every finding must quote the manuscript word for word, and every quote is matched back against the text. A finding whose quote cannot be found is discarded before the student sees it, because accusing somebody over words they did not write is a far more expensive failure than missing one weakness.
  • A gap cannot be recorded until the student has been offered at least one chance to clarify. When the model reaches for it too early, the decision is downgraded and the override is shown in the transcript rather than applied quietly.
  • Two concurrent turns cannot overwrite each other. Every session carries a revision, checked inside a Firestore transaction.

No RAG. The manuscript goes into the context window whole. That is deliberate: quote verification is what makes every finding checkable, and a model that only ever saw retrieved fragments makes that guarantee far weaker.

Challenges we ran into

Almost every hard bug in this project shared one shape: nothing failed.

The transcript that said "yes and no". A student spoke a long answer and the box filled with three words that had nothing to do with what they said. Not truncated, not garbled, and no error anywhere. MediaRecorder produces WebM, Gemini reads WAV, MP3, AIFF, AAC, OGG and FLAC, and given bytes it cannot decode the model does not refuse. It answers anyway, from nothing.

The first fix made it worse. It recorded WebM and converted afterwards, falling back to the original when conversion failed, which meant a failed conversion looked exactly like a successful one. The real fix removed the container entirely and wrote raw samples straight into a WAV. That lesson became a rule the rest of the system follows: a fallback that hides the failure it is falling back from is worse than no fallback.

Four facts about the Live API, each established by testing rather than documentation. It is refused on the global endpoint and answers only in us-central1. It rejects a request for text output, so the session must ask for audio and discard the spoken reply. A system instruction telling it to stay silent made transcription intermittent. And most costly: with automatic activity detection on, it closed the turn at the first pause and treated everything afterwards as an interruption. Four sentences went in and one came out. A student pausing for breath has not finished, and only the student knows when they have.

Streaming that almost nobody received. The PCM streaming worked, was deployed, and was documented, and then chunks were only sent when the browser agreed to open the audio context at 16 kHz. Most devices run at 44.1 or 48. On those machines the socket opened, received nothing, and the answer quietly fell back to uploading the whole recording. The only symptom was the delay the feature existed to remove.

Telemetry that failed the way telemetry was added to catch. The obvious Cloud Trace exporter is deprecated and fails silently: it accepts spans, raises nothing, logs nothing, and delivers nothing. It was caught only because we read the trace back out of Cloud Trace instead of assuming the export had worked.

A flag that was never forwarded. The examiner's voice was supposed to arrive with the question. It arrived later, and the request still succeeded, the turn still returned, and the audio still played. Two route handlers rebuilt the request body for the backend and neither copied the speak flag. Presenting as slowness rather than as a bug is exactly why it survived so long.

Accomplishments that we're proud of

252 tests, including failure-path tests for a hallucinated quote, an exhausted quota, a refused credential, a concurrent write, an oversized draft, and a deletion that half succeeds. The unit tests run fully offline against a fake model, so they test our code rather than the provider's weather.

A score that can be argued with. The 4.00 indicator is arithmetic on the record, so it is reproducible, traceable to individual answers, and shown with its workings. A student who disagrees can point at a line rather than at a figure.

scripts/prove.py. One command that runs every claim the README makes and prints the real result: the model in use, all five agents verified as ADK agents with transfer refused both ways, the four defense agents running live on a real manuscript, the citation checker, the indicator computed from the transcript they just produced, the deployed revisions serving traffic, and the reasoning chain read back out of Cloud Trace. Nothing in it is mocked. If a claim in the documentation is untrue, that script is where it shows.

A deletion that means it. Erasing an account removes the sessions, the manuscripts, and the Weakness Maps in three different collections, and nothing in that path is tolerant of a failure: if any part cannot be removed, the request fails and says so. A deletion that reports success while the data survives is the one outcome that stops the student looking, which makes it worse than an honest error. The failure paths are tested rather than hoped for.

Memory that can be pointed at. A session remembers the one before it. What carries across is a structured weakness profile in Firestore: the gaps a student left unresolved, each one traceable back to a specific finding in a specific manuscript. The Question Strategy agent reads that profile before it plans, and marks which questions are returning to old ground, so the second mock defense targets what the first one left unfixed rather than covering the same order again. Keeping it structured is what lets a student ask why a question came back and get an answer that points at a line.

What we learned

A fallback that hides the failure it is falling back from is worse than no fallback. Learned from the audio conversion bug, and applied afterwards to every quiet path in the system.

Verify what can be verified, bound what cannot, and never let an unverifiable claim reach a place where being wrong would harm the student. This is the single principle the whole product rests on. It produced quote anchoring, the refusal to accept audio formats the model cannot read, and the decision to compute a score rather than ask for one.

Some configuration is worth not setting. ActivityHandling and TurnCoverage on the Live session were tried and changed nothing measurable, so they are absent. Configuration that has not been shown to matter is a future reader's false lead.

Prompts are requests, not guarantees. Every rule that actually matters here is enforced in code, and every override is recorded where the student can see it.

Read the result back. More than one bug in this project was invisible until we fetched what we had supposedly written and looked at it.

What's next for CITRA Viva: Agentic AI Thesis Defense Simulator

A shared store for synthesised speech. The examiner's voice is cached in the browser, which covers one student replaying their own question. A shared object store would carry it across devices and instances.

Richer observability. One span per agent call exists today. Per-question spans and a trace link inside the report would let a supervisor follow a student's reasoning as easily as we follow the system's.

Supervisor mode. Everything in a session is already recorded and traceable. The natural next step is letting a student share one session with their supervisor, read only, so the conversation about a weak point starts from the transcript rather than from memory.

More languages. The agents already answer in the language of the manuscript. The interface follows in English and Indonesian, and the shell around it is the only part that needs work to add more.

Built With

  • artifact-registry
  • cloud-build
  • cloud-run
  • cloud-trace
  • docker
  • fastapi
  • firebase-authentication
  • firestore
  • gemini-2.5-flash-tts
  • gemini-3.7-flash
  • gemini-live-api
  • google-adk
  • google-cloud
  • google-gemini
  • google-genai-sdk
  • next.js
  • opentelemetry
  • pydantic
  • python
  • react
  • tailwindcss
  • typescript
  • uvicorn
  • vertex-ai
  • websockets
Share this project:

Updates

Submission history