Build an AI Interview Analyzer
Use Gemini to turn stakeholder notes into traceable requirements briefs.
Introduction
30 Second Summary
Important conversations can sound clear in the moment. Later, polished notes can hide the details that still need confirmation.
In this project, you will build a Google Colab notebook that uses the Gemini API to turn fictional stakeholder notes into a reviewable requirements brief. You will compare a polished prose summary with structured findings that preserve evidence for human review.
What You'll Build
When you run your finished notebook, fictional interview notes become a categorized brief with source excerpts and a queue of questions for the next stakeholder conversation.
By the end of this project, you'll have:
- A categorized requirements brief where you can scan business objectives, stakeholder needs, candidate requirements, assumptions, risks, and open questions.
- A set of traceable findings that point back to exact excerpts and show whether each conclusion is explicit or inferred.
- An audit report that measures evidence coverage, checks known topics, and collects uncertain findings for human review.
- Secret Mission: Test whether the analyzer surfaces a direct conflict between Operations and Finance.
Are there any prerequisites?
You need a Google account plus basic familiarity with Python. The project uses fictional notes because Gemini API free-tier content may be used to improve Google products.
Before We Start
Before the hands-on work begins, commit to building a first-pass stakeholder-interview analysis assistant. A human business analyst must validate its findings, requirements, acceptance criteria, assumptions, risks, ambiguities, and stakeholder conflicts before anyone acts on them.
Set Up a Secure Colab Workspace
Your stakeholder analyzer needs a dependable connection to the Gemini API before it can process interview notes. Google Colab gives the project a hosted notebook that stays with your Google account.
The Google Gen AI SDK gives your notebook an authenticated Gemini client. Colab Secrets keeps the credential outside shared notebook code.
In this step, get ready to:
- Create a Gemini API key on the free tier.
- Prepare a Colab notebook with Google Gen AI SDK 2.31.0.
- Connect the notebook securely to Gemini.
Create your Gemini API key
Google AI Studio issues the credential that authorizes your notebook to use Gemini. The credential belongs to a Google project linked to your account.
Protect Your Interview Data
Gemini 3.8 Flash input is free of charge on the free tier. Output is also free of charge.
Free-tier content is marked as used to improve Google products. Use only fictional or sanitized interview notes throughout this project.
Keep confidential information out of the notebook. Keep personal or proprietary information out too.
Copying an API key can feel risky. You will move it into Colab Secrets without placing it in a notebook cell.
- Open Google AI Studio in your browser.
- Sign in using your Google account.
- Go to the API Keys page.
- Keep the project on the free tier if Google AI Studio presents a billing choice.
- Click Create API key if the project does not have a key.
- Copy the API key from the page.
Treat this key like a password. Leave it out of notebook cells so it cannot appear in shared code.
Create your notebook and install the SDK
A Colab notebook stores its code cells in an .ipynb file. Pinning the SDK gives each fresh Colab runtime the package release used by this project.
- Open Google Colab in a new browser tab.
- Sign in using the same Google account.
- Create a new notebook from the Colab landing page.
- Select the notebook title at the top of the page.
- Enter stakeholder_interview_analyzer.ipynb as the notebook name.
The first installation can take a moment while Colab downloads the package. A busy cell indicator means the runtime is still working.
- Install the pinned SDK by running this command in the first code cell:
!pip install google-genai==2.31.0
What Does This Install?
The command installs the google-genai package at version 2.31.0. This package provides the Python client used to send requests to Gemini.
Keeping the installation in the notebook makes the dependency reproducible after Colab starts a fresh runtime.
- Wait for the installation cell to finish.
- Check the installation output for the installed package version.
✔️ I see version 2.31.0
Your notebook has Google Gen AI SDK 2.31.0. Continue to the secret setup below.
ⓧ I see an older version
- Return to the first code cell.
- Confirm that the command ends with ==2.31.0.
- Run the installation cell again.
The completed installation output should identify version 2.31.0.
ⓧ The package is not installed
- Confirm that the Colab runtime is connected.
- Return to the first code cell.
- Run the installation cell again.
Wait for the cell to finish before continuing.
Package Installation Failing?
Check that the package name contains the hyphen in google-genai. Confirm that the version pin is 2.31.0.
Help me troubleshoot the SDK installation in Colab.
Store the secret and verify the connection
The Secrets panel lets notebook code retrieve your credential while keeping its value out of the cells. Notebook access must be enabled before userdata.get() can retrieve the key.
- Select the Secrets tab in Colab's left panel.
- Create a secret named GEMINI_API_KEY.
- Paste the copied API key into the secret value.
- Enable notebook access beside GEMINI_API_KEY.
The secret row should now show the name GEMINI_API_KEY with notebook access enabled. Its credential value stays outside your code cells.
The first setup cell imports the modules needed for Gemini requests. It also prepares Python to work with JSON results later.
- Add a new code cell beneath the installation cell.
- Add the notebook imports by pasting this code into the new cell:
import json
from google import genai
from google.colab import userdata
What Do These Imports Provide?
- The json module prepares the notebook to parse structured results later.
- The genai import provides the Gemini client.
- The userdata import lets the notebook retrieve enabled Colab secrets.
- Run the imports cell using its play button.
The cell should finish without an exception. This confirms that the installed SDK can be imported by the notebook runtime.
Imports Not Completing?
Confirm that the installation cell completed before this cell. Check each import against the code above.
Help me fix the imports in my Colab notebook.
The next cell selects the model. It also retrieves the enabled secret before creating the client.
- Add a new code cell beneath the imports cell.
- Configure the authenticated Gemini client by pasting this code into the new cell:
MODEL_ID = "gemini-3.8-flash"
GEMINI_API_KEY = userdata.get("GEMINI_API_KEY")
client = genai.Client(api_key=GEMINI_API_KEY)
What Does This Configuration Do?
- The MODEL_ID value directs requests to gemini-3.8-flash.
- The GEMINI_API_KEY value comes from the enabled Colab secret.
- The client object uses that credential to authenticate requests.
- Run the client configuration cell using its play button.
The cell should finish without an exception. This confirms that Colab can retrieve the enabled secret.
Secret Access Failing?
Confirm that the secret name is exactly GEMINI_API_KEY. Check that notebook access is enabled beside the secret.
Help me troubleshoot Colab secret access without exposing my key.
A small request now checks the entire path from your notebook to Gemini. The Interactions API returns its final text through interaction.output_text.
- Add a new code cell beneath the client configuration cell.
- Add the connection check by pasting this code into the new cell:
interaction = client.interactions.create(
model=MODEL_ID,
input="Reply with exactly: Gemini connection ready",
store=False,
)
print(interaction.output_text)
What Does This Check Prove?
- The request uses the model stored in MODEL_ID.
- The fixed input makes the response easy to recognize.
- The store=False setting opts out of Interaction object storage.
- The final line prints the returned text beneath the cell.
Before you run the connection check, do you expect the request to reach Gemini successfully?
- Run the connection check using its play button.
You should see Gemini connection ready beneath the cell. That result confirms your secure Gemini workspace can send authenticated requests.
Connection Check Not Working?
- Confirm that the API key remains active in Google AI Studio.
- Confirm that notebook access remains enabled beside GEMINI_API_KEY.
- Rerun the installation cell if the Colab runtime restarted.
Help me troubleshoot my secure Gemini connection in Colab.
✔️ Awesome, I've got everything!
Your notebook contains the authenticated Gemini connection setup. Your API key remains outside the notebook code.
ⓧ I'd like to double check the full code
import json
from google import genai
from google.colab import userdata
MODEL_ID = "gemini-3.8-flash"
GEMINI_API_KEY = userdata.get("GEMINI_API_KEY")
client = genai.Client(api_key=GEMINI_API_KEY)
interaction = client.interactions.create(
model=MODEL_ID,
input="Reply with exactly: Gemini connection ready",
store=False,
)
print(interaction.output_text)
Your authenticated workspace is ready. Next, you will generate a polished summary before testing whether its claims can be traced to evidence.
Expose Free-Form Summary Limits
Your secure Google Colab workspace can now reach the Gemini API without exposing your key. The next question is whether fluent output gives you enough control for business analysis.
Stakeholder interviews mix objectives, needs, constraints, and ambiguous language. In this step, you will generate a concise prose summary before testing whether its findings can be checked reliably.
In this step, get ready to:
- Store a fictional purchase-request interview transcript in the notebook.
- Generate a concise free-form summary with Gemini.
- Test the summary for machine-checkable evidence and review fields.
Add fictional interview notes
A useful requirements exercise needs several stakeholder perspectives. This fictional transcript includes operational pain points, approval rules, reporting needs, and an undefined measure of success.
- Use the notebook's cell toolbar to add a code cell below the connection check.
- Store the fictional transcript in SAMPLE_NOTES by pasting this code into the new cell:
SAMPLE_NOTES = """
Project: Improve the internal purchase-request process at Northstar Services.
Priya, Operations Manager: "Requests arrive by email, and I spend about six hours each week chasing missing cost centre and supplier details."
Priya: "Requests under £5,000 should go to the requester's department manager. Requests of £5,000 or more should go to Finance after the department manager reviews them."
Daniel, Finance Lead: "A named manager must approve every purchase request. I also need a weekly report of pending requests older than five working days."
Mia, Requester: "I often do not know where my request is. I want an email update when it is approved or returned for changes."
Project Sponsor: "The new process should feel much faster and go live before 1 April, but we have not agreed what faster means."
""".strip()
What does this code do?
- The SAMPLE_NOTES variable stores one fictional stakeholder interview transcript.
- The triple-quoted string preserves the transcript across several lines.
- The .strip() call removes empty space from the start and end of the transcript.
- Run the cell by clicking its play button.
- Add another code cell below the transcript cell.
- Display the stored transcript by running:
SAMPLE_NOTES
What should you see?
You should see the Northstar Services transcript beneath the cell. The output should include statements from Operations, Finance, the requester, and the project sponsor.
Transcript missing or incomplete?
- Check that the definition cell ran before the cell containing SAMPLE_NOTES.
- Compare the opening and closing triple quotes with the code above.
Ask for help with the transcript cell.
Generate a free-form summary
A prose prompt gives the model broad freedom to decide what belongs in the response. You will use that freedom first before judging how well the result preserves the distinctions in the source notes.
- Add a code cell below the transcript output.
- Define create_naive_summary(notes) by pasting this code into the new cell:
def create_naive_summary(notes):
prompt = (
"Write a concise prose summary of these stakeholder interview notes. "
"Do not return JSON or a table.\n\n" + notes
)
interaction = client.interactions.create(
model=MODEL_ID,
input=prompt,
store=False,
)
return interaction.output_text
What does this code do?
- The prompt asks for a concise prose summary without enforcing fields or a table.
- The interaction sends the prompt and notes to the model identified by MODEL_ID.
- The store=False setting disables storage of the Interaction object.
- The function returns the generated text through interaction.output_text.
- Run the function cell by clicking its play button.
Before you generate the summary, predict whether one polished paragraph can keep every objective, need, constraint, and ambiguity distinct.
- Generate the summary in a new code cell by running:
naive_summary = create_naive_summary(SAMPLE_NOTES)
print(naive_summary)
What did the summary produce?
You should see a fluent prose summary of the purchase-request process. The exact wording can vary between runs.
The prose blends stakeholder needs with approval rules and project constraints. It may also smooth over the undefined meaning of faster.
- Find the part of the output that describes the goal of a faster process.
- Check whether that part quotes an exact supporting excerpt from the notes.
- Check whether the summary labels the statement as explicit or inferred.
- Check whether the summary marks the undefined speed target for human review.
The summary sounds convincing, but its claims have no enforced traceability. Fluency alone does not show which findings deserve stakeholder confirmation.
Summary cell not returning prose?
- Confirm that the earlier connection check still prints Gemini connection ready.
- Confirm that the enabled Colab secret is still named GEMINI_API_KEY.
- Rerun the secure connection cell before rerunning the summary cell.
Help me debug the summary request.
Test the summary's machine-checkability
A JSON response can expose named fields that software checks consistently. The current response is prose, so you will test what a parser can verify about it.
- Add a code cell below the generated summary.
- Define inspect_naive_output(summary) by pasting this code into the new cell:
def inspect_naive_output(summary):
try:
json.loads(summary)
parse_result = "The response happened to parse as JSON, but no schema enforced its fields."
except json.JSONDecodeError:
parse_result = "The prose response is not structured JSON."
return {
"machine_checkable": False,
"reason": "No schema enforced evidence, interpretation, requirement type, or review fields.",
"parse_result": parse_result,
}
What does this code do?
- The try block asks json.loads(summary) to parse the model output.
- The except json.JSONDecodeError branch records that ordinary prose is not structured JSON.
- The returned dictionary states that no schema enforced evidence, interpretation, requirement type, or review fields.
- The check remains False even if the response happens to parse because no required field structure was imposed.
- Run the inspection function cell by clicking its play button.
Before you run the check, ask yourself whether readable prose gives a JSON parser enough structure to verify its claims.
- Inspect the stored prose summary in a new code cell by running:
print(json.dumps(inspect_naive_output(naive_summary), indent=2))
What should you see?
You should see "machine_checkable": false in the printed dictionary. The reason states that no schema enforced evidence, interpretation, requirement type, or review fields.
The parse result should explain that the prose response is not structured JSON. This is the designed shortfall of the free-form approach.
Inspection cell failing?
- Rerun the cell that assigns the generated response to naive_summary.
- Rerun the cell that defines inspect_naive_output(summary).
- Check that the notebook's original import cell still includes import json.
Help me debug the machine-checkability test.
✔️ Awesome, I've got everything!
Great. Double-check that your notebook is saved with the transcript, summary function, and inspection function.
ⓧ I'd like to double check the full code
import json
from google import genai
from google.colab import userdata
MODEL_ID = "gemini-3.8-flash"
GEMINI_API_KEY = userdata.get("GEMINI_API_KEY")
client = genai.Client(api_key=GEMINI_API_KEY)
interaction = client.interactions.create(
model=MODEL_ID,
input="Reply with exactly: Gemini connection ready",
store=False,
)
print(interaction.output_text)
SAMPLE_NOTES = """
Project: Improve the internal purchase-request process at Northstar Services.
Priya, Operations Manager: "Requests arrive by email, and I spend about six hours each week chasing missing cost centre and supplier details."
Priya: "Requests under £5,000 should go to the requester's department manager. Requests of £5,000 or more should go to Finance after the department manager reviews them."
Daniel, Finance Lead: "A named manager must approve every purchase request. I also need a weekly report of pending requests older than five working days."
Mia, Requester: "I often do not know where my request is. I want an email update when it is approved or returned for changes."
Project Sponsor: "The new process should feel much faster and go live before 1 April, but we have not agreed what faster means."
""".strip()
def create_naive_summary(notes):
prompt = (
"Write a concise prose summary of these stakeholder interview notes. "
"Do not return JSON or a table.\n\n" + notes
)
interaction = client.interactions.create(
model=MODEL_ID,
input=prompt,
store=False,
)
return interaction.output_text
def inspect_naive_output(summary):
try:
json.loads(summary)
parse_result = "The response happened to parse as JSON, but no schema enforced its fields."
except json.JSONDecodeError:
parse_result = "The prose response is not structured JSON."
return {
"machine_checkable": False,
"reason": "No schema enforced evidence, interpretation, requirement type, or review fields.",
"parse_result": parse_result,
}
That is the critical gap made visible: your summary reads well, but software cannot audit its evidence or review status. Next, you will constrain the analysis to a traceable requirements schema.
Build a Traceable Requirements Analyzer
Your Google Colab notebook now exposes the weakness in the free-form summary. Its fluent prose gives you no reliable way to audit evidence or separate explicit findings from inferred ones.
You will now constrain Gemini with a predictable structure. This turns the interview notes into a draft requirements brief that a business analyst can trace and review.
In this step, get ready to:
- Define a schema for objectives, stakeholder needs, requirements, assumptions, risks, and open questions.
- Add evidence, interpretation, acceptance criteria, and human-review fields.
- Generate a structured requirements brief from the fictional interview notes.
Define the requirements schema
A JSON Schema describes the shape that the model must return. The schema preserves distinctions that disappeared inside the prose summary.
How does the schema support analysis?
The requirement categories separate business, stakeholder, solution, and transition concerns. Functional solution requirements describe behaviour. Non-functional solution requirements describe qualities or constraints.
Each objective, need, or requirement includes an evidence excerpt. Its interpretation records whether the finding is explicit or inferred.
The review flag keeps inferred findings and draft acceptance criteria visible to the human analyst. The schema creates an auditable first pass.
The schema is the longest cell in this notebook, so matching its nested braces can feel fiddly. The full reference keeps every object boundary visible while you add it.
- Switch back to the Colab notebook from the previous step.
- Define ANALYSIS_SCHEMA in a new code cell beneath inspect_naive_output(summary) by copying its section from the second tab below.
✔️ Awesome, I've got everything!
Great. Confirm that the schema cell ends with "additionalProperties": False for the top-level object.
ⓧ I'd like to double check the full code
import json
from google import genai
from google.colab import userdata
MODEL_ID = "gemini-3.8-flash"
GEMINI_API_KEY = userdata.get("GEMINI_API_KEY")
client = genai.Client(api_key=GEMINI_API_KEY)
interaction = client.interactions.create(
model=MODEL_ID,
input="Reply with exactly: Gemini connection ready",
store=False,
)
print(interaction.output_text)
SAMPLE_NOTES = """
Project: Improve the internal purchase-request process at Northstar Services.
Priya, Operations Manager: "Requests arrive by email, and I spend about six hours each week chasing missing cost centre and supplier details."
Priya: "Requests under £5,000 should go to the requester's department manager. Requests of £5,000 or more should go to Finance after the department manager reviews them."
Daniel, Finance Lead: "A named manager must approve every purchase request. I also need a weekly report of pending requests older than five working days."
Mia, Requester: "I often do not know where my request is. I want an email update when it is approved or returned for changes."
Project Sponsor: "The new process should feel much faster and go live before 1 April, but we have not agreed what faster means."
""".strip()
def create_naive_summary(notes):
prompt = (
"Write a concise prose summary of these stakeholder interview notes. "
"Do not return JSON or a table.\n\n" + notes
)
interaction = client.interactions.create(
model=MODEL_ID,
input=prompt,
store=False,
)
return interaction.output_text
def inspect_naive_output(summary):
try:
json.loads(summary)
parse_result = "The response happened to parse as JSON, but no schema enforced its fields."
except json.JSONDecodeError:
parse_result = "The prose response is not structured JSON."
return {
"machine_checkable": False,
"reason": "No schema enforced evidence, interpretation, requirement type, or review fields.",
"parse_result": parse_result,
}
ANALYSIS_SCHEMA = {
"type": "object",
"properties": {
"business_objective": {
"type": "object",
"properties": {
"statement": {
"type": "string",
"description": "A concise draft business objective grounded in the notes.",
},
"evidence": {
"type": "string",
"description": "An exact contiguous excerpt from the notes supporting the objective.",
},
"interpretation": {
"type": "string",
"enum": ["explicit", "inferred"],
},
"review_required": {"type": "boolean"},
},
"required": ["statement", "evidence", "interpretation", "review_required"],
"additionalProperties": False,
},
"stakeholder_needs": {
"type": "array",
"items": {
"type": "object",
"properties": {
"stakeholder": {"type": "string"},
"need": {"type": "string"},
"evidence": {
"type": "string",
"description": "An exact contiguous excerpt from the notes supporting the need.",
},
"interpretation": {
"type": "string",
"enum": ["explicit", "inferred"],
},
"review_required": {"type": "boolean"},
},
"required": [
"stakeholder",
"need",
"evidence",
"interpretation",
"review_required",
],
"additionalProperties": False,
},
},
"requirements": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {"type": "string"},
"category": {
"type": "string",
"enum": [
"business",
"stakeholder",
"solution_functional",
"solution_non_functional",
"transition",
],
},
"statement": {"type": "string"},
"evidence": {
"type": "string",
"description": "An exact contiguous excerpt from the notes supporting the requirement.",
},
"interpretation": {
"type": "string",
"enum": ["explicit", "inferred"],
},
"acceptance_criteria": {
"type": "array",
"items": {"type": "string"},
"description": "Draft testable criteria that a human analyst must validate.",
},
"review_required": {"type": "boolean"},
},
"required": [
"id",
"category",
"statement",
"evidence",
"interpretation",
"acceptance_criteria",
"review_required",
],
"additionalProperties": False,
},
},
"assumptions": {
"type": "array",
"items": {"type": "string"},
},
"risks": {
"type": "array",
"items": {"type": "string"},
},
"open_questions": {
"type": "array",
"items": {"type": "string"},
},
},
"required": [
"business_objective",
"stakeholder_needs",
"requirements",
"assumptions",
"risks",
"open_questions",
],
"additionalProperties": False,
}
What does this schema enforce?
- The top-level required list prevents any major analysis section from disappearing.
- The category enum limits each candidate requirement to one supported classification.
- The interpretation enum limits findings to explicit or inferred.
- The closed objects reject extra fields outside the planned structure.
- Run the ANALYSIS_SCHEMA cell.
Colab marks the cell as completed without an exception. Your schema is now available to the analyzer.
Does the schema cell fail?
Compare the closing braces and commas with the full reference. A missing comma between properties or an unmatched brace prevents Python from creating the dictionary.
Use the notebook's error location to find the first affected line. Help me find the syntax problem in my ANALYSIS_SCHEMA cell.
Connect the schema to Gemini
The schema controls the response shape. The analyzer prompt controls how the model interprets the notes inside that shape.
The function requires exact evidence excerpts and marks unsupported interpretations for review. It also treats every requirement and acceptance criterion as a draft for stakeholder validation.
- Define analyze_notes(notes) in a new code cell beneath the schema by adding this function:
def analyze_notes(notes):
prompt = (
"Act as a first-pass assistant to a business analyst. Analyze only the supplied "
"stakeholder notes. Do not invent facts. Use exact excerpts from the notes in every "
"evidence field. Mark anything not directly stated as inferred and requiring review. "
"Treat all requirements and acceptance criteria as drafts for stakeholder validation. "
"Surface ambiguous language, missing measures, and incompatible stakeholder rules as "
"open questions.\n\nStakeholder notes:\n" + notes
)
interaction = client.interactions.create(
model=MODEL_ID,
input=prompt,
response_format={
"type": "text",
"mime_type": "application/json",
"schema": ANALYSIS_SCHEMA,
},
store=False,
)
return json.loads(interaction.output_text)
What does this function do?
- The prompt limits the analysis to the supplied stakeholder notes.
- The response_format configuration requests application/json that follows ANALYSIS_SCHEMA.
- The store=False setting opts out of Interaction object storage.
- The json.loads call converts the returned JSON text into a Python dictionary.
- Run the analyze_notes(notes) cell.
Colab marks the function cell as completed without an exception. The analyzer is now ready to accept notes.
Does the function cell fail?
Confirm that the schema cell ran before the function cell. Check that ANALYSIS_SCHEMA uses the same spelling in both cells.
Compare the prompt parentheses and the response_format dictionary with the reference. Help me debug my analyze_notes function.
Use the tabs below to compare the complete notebook through this point.
✔️ Awesome, I've got everything!
Excellent. Double-check that the notebook has saved your schema and analyzer function.
ⓧ I'd like to double check the full code
import json
from google import genai
from google.colab import userdata
MODEL_ID = "gemini-3.8-flash"
GEMINI_API_KEY = userdata.get("GEMINI_API_KEY")
client = genai.Client(api_key=GEMINI_API_KEY)
interaction = client.interactions.create(
model=MODEL_ID,
input="Reply with exactly: Gemini connection ready",
store=False,
)
print(interaction.output_text)
SAMPLE_NOTES = """
Project: Improve the internal purchase-request process at Northstar Services.
Priya, Operations Manager: "Requests arrive by email, and I spend about six hours each week chasing missing cost centre and supplier details."
Priya: "Requests under £5,000 should go to the requester's department manager. Requests of £5,000 or more should go to Finance after the department manager reviews them."
Daniel, Finance Lead: "A named manager must approve every purchase request. I also need a weekly report of pending requests older than five working days."
Mia, Requester: "I often do not know where my request is. I want an email update when it is approved or returned for changes."
Project Sponsor: "The new process should feel much faster and go live before 1 April, but we have not agreed what faster means."
""".strip()
def create_naive_summary(notes):
prompt = (
"Write a concise prose summary of these stakeholder interview notes. "
"Do not return JSON or a table.\n\n" + notes
)
interaction = client.interactions.create(
model=MODEL_ID,
input=prompt,
store=False,
)
return interaction.output_text
def inspect_naive_output(summary):
try:
json.loads(summary)
parse_result = "The response happened to parse as JSON, but no schema enforced its fields."
except json.JSONDecodeError:
parse_result = "The prose response is not structured JSON."
return {
"machine_checkable": False,
"reason": "No schema enforced evidence, interpretation, requirement type, or review fields.",
"parse_result": parse_result,
}
ANALYSIS_SCHEMA = {
"type": "object",
"properties": {
"business_objective": {
"type": "object",
"properties": {
"statement": {
"type": "string",
"description": "A concise draft business objective grounded in the notes.",
},
"evidence": {
"type": "string",
"description": "An exact contiguous excerpt from the notes supporting the objective.",
},
"interpretation": {
"type": "string",
"enum": ["explicit", "inferred"],
},
"review_required": {"type": "boolean"},
},
"required": ["statement", "evidence", "interpretation", "review_required"],
"additionalProperties": False,
},
"stakeholder_needs": {
"type": "array",
"items": {
"type": "object",
"properties": {
"stakeholder": {"type": "string"},
"need": {"type": "string"},
"evidence": {
"type": "string",
"description": "An exact contiguous excerpt from the notes supporting the need.",
},
"interpretation": {
"type": "string",
"enum": ["explicit", "inferred"],
},
"review_required": {"type": "boolean"},
},
"required": [
"stakeholder",
"need",
"evidence",
"interpretation",
"review_required",
],
"additionalProperties": False,
},
},
"requirements": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {"type": "string"},
"category": {
"type": "string",
"enum": [
"business",
"stakeholder",
"solution_functional",
"solution_non_functional",
"transition",
],
},
"statement": {"type": "string"},
"evidence": {
"type": "string",
"description": "An exact contiguous excerpt from the notes supporting the requirement.",
},
"interpretation": {
"type": "string",
"enum": ["explicit", "inferred"],
},
"acceptance_criteria": {
"type": "array",
"items": {"type": "string"},
"description": "Draft testable criteria that a human analyst must validate.",
},
"review_required": {"type": "boolean"},
},
"required": [
"id",
"category",
"statement",
"evidence",
"interpretation",
"acceptance_criteria",
"review_required",
],
"additionalProperties": False,
},
},
"assumptions": {
"type": "array",
"items": {"type": "string"},
},
"risks": {
"type": "array",
"items": {"type": "string"},
},
"open_questions": {
"type": "array",
"items": {"type": "string"},
},
},
"required": [
"business_objective",
"stakeholder_needs",
"requirements",
"assumptions",
"risks",
"open_questions",
],
"additionalProperties": False,
}
def analyze_notes(notes):
prompt = (
"Act as a first-pass assistant to a business analyst. Analyze only the supplied "
"stakeholder notes. Do not invent facts. Use exact excerpts from the notes in every "
"evidence field. Mark anything not directly stated as inferred and requiring review. "
"Treat all requirements and acceptance criteria as drafts for stakeholder validation. "
"Surface ambiguous language, missing measures, and incompatible stakeholder rules as "
"open questions.\n\nStakeholder notes:\n" + notes
)
interaction = client.interactions.create(
model=MODEL_ID,
input=prompt,
response_format={
"type": "text",
"mime_type": "application/json",
"schema": ANALYSIS_SCHEMA,
},
store=False,
)
return json.loads(interaction.output_text)
What should the notebook contain?
The notebook retains the secure connection and free-form-summary cells. It now adds the complete schema followed by analyze_notes(notes).
Keep the cells in this order so the client, notes, schema, and analyzer exist before you run the structured request.
Run the traceable analysis
Schema conformance gives you predictable JSON. The values still remain draft analysis that a human must examine.
- Create the structured draft from SAMPLE_NOTES by running this line in a new code cell:
analysis = analyze_notes(SAMPLE_NOTES)
What happens during this request?
The function sends the fictional transcript with the schema-constrained prompt. It parses the returned text into the analysis dictionary.
Before you reveal the result, consider this question: which named sections do you expect to replace the blended prose summary.
- Print the structured brief by running this line in a new code cell:
print(json.dumps(analysis, indent=2, ensure_ascii=False))
What should you see?
You see a formatted JSON object with business_objective, stakeholder_needs, requirements, assumptions, risks, and open_questions.
The objective, needs, and requirements include evidence excerpts. Requirements also include IDs, categories, interpretation labels, draft acceptance criteria, and review flags.
That is the core analyzer working. Your notebook now produces a reviewable brief instead of one undifferentiated narrative.
Does the structured request fail?
Confirm that the connection, schema, and function cells completed before the request cell. Re-run those cells in notebook order if the runtime was restarted.
Check the schema reference and the nested response_format keys against the full code. Help me debug my structured requirements request.
Your requirements brief is structured and traceable. Next, you will test whether its evidence and topic coverage hold up before the draft reaches a stakeholder conversation.
Add Evidence and Coverage Checks
Your JSON Schema now keeps the draft brief predictable. Each finding has a defined place for evidence, interpretation, review status, and acceptance criteria.
However, schema-valid JSON can still contain semantically wrong values. This step adds deterministic checks before a business analyst reviews or shares the output.
In this step, get ready to:
- Verify each evidence excerpt against the original interview notes.
- Measure coverage for the scenario's expected topics.
- Collect uncertain findings in a human review queue.
Validate evidence against the source notes
Evidence validation supports traceability by checking whether each quoted excerpt appears in the source notes. Normalizing capitalization, quotation marks, and whitespace prevents harmless formatting differences from breaking the comparison.
- Add the text-normalization helper in a new Colab code cell by running this code:
def normalize_text(text):
return " ".join(
text.casefold()
.replace('"', "")
.replace("“", "")
.replace("”", "")
.split()
)
What does this helper do?
- The casefold() call makes capitalization irrelevant during comparison.
- The three replace() calls remove straight and curly quotation marks.
- The final split() call collapses inconsistent whitespace before the words are joined again.
- Add the evidence-audit function in the next Colab code cell by running this code:
def validate_evidence(analysis, notes):
evidence_items = [
("business_objective", analysis["business_objective"]["evidence"])
]
for index, need in enumerate(analysis["stakeholder_needs"], start=1):
evidence_items.append((f"stakeholder_need_{index}", need["evidence"]))
for requirement in analysis["requirements"]:
evidence_items.append((requirement["id"], requirement["evidence"]))
normalized_notes = normalize_text(notes)
checks = []
for label, evidence in evidence_items:
checks.append(
{
"item": label,
"evidence": evidence,
"evidence_found": normalize_text(evidence) in normalized_notes,
}
)
passed = sum(check["evidence_found"] for check in checks)
total = len(checks)
coverage = round((passed / total) * 100, 1) if total else 0.0
return {
"passed": passed,
"total": total,
"evidence_coverage_percent": coverage,
"checks": checks,
}
How does the evidence audit work?
- The function collects evidence from the business objective, stakeholder needs, and candidate requirements.
- Each normalized excerpt is checked for membership in the normalized source notes.
- The returned audit preserves every check alongside the passed count, total count, and coverage percentage.
Before you test the audit, do you expect every generated excerpt to match the original notes exactly?
- Create a temporary Colab code cell to test the evidence audit by running this code:
analysis = analyze_notes(SAMPLE_NOTES)
evidence_audit = validate_evidence(analysis, SAMPLE_NOTES)
print(json.dumps(evidence_audit, indent=2, ensure_ascii=False))
What should the audit reveal?
Each entry in checks shows the generated excerpt beside an evidence_found result. The percentage summarizes how much of the generated evidence can be traced directly to the interview notes.
You will see an evidence coverage percentage plus a check for every objective, need, and requirement excerpt. Any false result identifies evidence that needs human review.
Missing checks or seeing a cell error?
- Confirm that you ran the cell defining normalize_text() before the cell defining validate_evidence().
- Confirm that the structured analysis cell completed before you ran the temporary audit cell.
- Compare the dictionary keys in validate_evidence() with the schema fields from the previous step.
Help me debug my evidence audit.
Measure topic coverage and build the review queue
Exact evidence is only one quality signal. Topic coverage checks whether known scenario details appear somewhere in the analysis, while the review queue gathers findings that still need an analyst's judgment.
- Add the topic-coverage function in a new Colab code cell by running this code:
def evaluate_topic_coverage(analysis, expected_topics):
analysis_text = json.dumps(analysis, ensure_ascii=False).casefold()
findings = {
topic: topic.casefold() in analysis_text for topic in expected_topics
}
covered = sum(findings.values())
total = len(findings)
coverage = round((covered / total) * 100, 1) if total else 0.0
return {
"covered": covered,
"total": total,
"topic_coverage_percent": coverage,
"findings": findings,
}
What does topic coverage measure?
The function converts the complete analysis to searchable text. It reports whether each expected topic appears and calculates the overall coverage percentage.
This check measures presence only. A human analyst still decides whether each topic was interpreted correctly.
- Add the human-review queue function in the next Colab code cell by running this code:
def build_review_queue(analysis, evidence_audit):
queue = []
objective = analysis["business_objective"]
if objective["review_required"]:
queue.append("Review inferred business objective: " + objective["statement"])
for need in analysis["stakeholder_needs"]:
if need["review_required"]:
queue.append("Review stakeholder need: " + need["need"])
for requirement in analysis["requirements"]:
if requirement["review_required"]:
queue.append(
f"Review {requirement['id']}: {requirement['statement']}"
)
for check in evidence_audit["checks"]:
if not check["evidence_found"]:
queue.append("Verify unsupported evidence for " + check["item"])
for question in analysis["open_questions"]:
queue.append("Ask stakeholder: " + question)
return queue
What enters the review queue?
- Findings marked with review_required become explicit analyst-review tasks.
- Evidence excerpts that fail the source check become verification tasks.
- Every open question becomes a prompt for the next stakeholder conversation.
Before you run the combined check, which source do you expect to contribute the most items to the review queue?
- Create a second temporary Colab code cell to inspect topic coverage and the review queue by running this code:
expected_topics = [
"cost centre",
"supplier",
"£5,000",
"five working days",
"email",
]
topic_audit = evaluate_topic_coverage(analysis, expected_topics)
print(json.dumps(topic_audit, indent=2, ensure_ascii=False))
review_queue = build_review_queue(analysis, evidence_audit)
for item in review_queue:
print("-", item)
How should you read these results?
The topic audit shows whether each expected phrase appears in the structured brief. The review queue remains non-empty because ambiguous measures, inferred findings, or open questions still require human decisions.
Requirements and acceptance criteria remain drafts even when both audit percentages are high. Deterministic checks support stakeholder confirmation without replacing it.
You will see a topic coverage percentage followed by review tasks. The queue gives the analyst a concrete agenda for validating the draft.
Seeing an empty or incomplete review queue?
- Confirm that build_review_queue() appends every item from open_questions.
- Confirm that the function reads failed checks from evidence_audit.
- Rerun the structured analysis cell if the active runtime no longer contains analysis.
Help me inspect my review queue.
Run the complete audited workflow
The final workflow puts the designed obstacle and its solution side by side. It prints the free-form summary inspection before producing the structured brief, deterministic audits, and human review queue.
- Add the complete workflow function in a new Colab code cell by running this code:
def run_demo():
print("=== Free-form summary ===")
naive_summary = create_naive_summary(SAMPLE_NOTES)
print(naive_summary)
print(json.dumps(inspect_naive_output(naive_summary), indent=2))
print("\n=== Structured requirements brief ===")
analysis = analyze_notes(SAMPLE_NOTES)
print(json.dumps(analysis, indent=2, ensure_ascii=False))
print("\n=== Evidence audit ===")
evidence_audit = validate_evidence(analysis, SAMPLE_NOTES)
print(json.dumps(evidence_audit, indent=2, ensure_ascii=False))
print("\n=== Topic coverage ===")
expected_topics = [
"cost centre",
"supplier",
"£5,000",
"five working days",
"email",
]
topic_audit = evaluate_topic_coverage(analysis, expected_topics)
print(json.dumps(topic_audit, indent=2, ensure_ascii=False))
print("\n=== Human review queue ===")
review_queue = build_review_queue(analysis, evidence_audit)
for item in review_queue:
print("-", item)
return analysis, evidence_audit, topic_audit, review_queue
What does the workflow connect?
- The first section demonstrates that fluent prose has no enforced evidence structure.
- The structured section uses Gemini to produce the schema-constrained draft brief.
- The final sections run deterministic evidence checks, topic checks, and human-review routing.
This final cell sends two model requests. Wait until every output section has printed before judging the result.
Before you run the workflow, do you expect the deterministic checks to remove the need for stakeholder confirmation?
- Add the final workflow call in the next Colab code cell by running this code:
analysis, evidence_audit, topic_audit, review_queue = run_demo()
What should the final cell produce?
The cell stores the structured analysis and both audit results for later inspection. It also preserves the non-empty review queue as the handoff to human validation.
You will see the free-form inspection, structured requirements brief, evidence audit, topic coverage audit, and human review queue. The review queue confirms that automated structure still ends with analyst judgment.
- Remove the first temporary audit cell using that cell's menu.
- Remove the second temporary audit cell using that cell's menu.
Before the final check, what evidence would convince you that the notebook audits both traceability and scenario coverage?
- Run the final workflow cell again to verify the completed notebook.
You will see an evidence coverage percentage, a topic coverage percentage, and a non-empty human review queue. Any failed evidence check remains visible for an analyst to investigate.
Workflow stops before the review queue?
- Run every notebook cell from the secure connection cell through run_demo() in order.
- Check that each helper function name matches the final code exactly.
- Confirm that the expected topics preserve the spelling and symbols shown in SAMPLE_NOTES.
Help me debug the complete analyzer workflow.
✔️ Awesome, I've got everything!
Great work. Your notebook now turns a draft AI analysis into an auditable handoff for human review.
ⓧ I'd like to double check the full code
import json
from google import genai
from google.colab import userdata
MODEL_ID = "gemini-3.8-flash"
GEMINI_API_KEY = userdata.get("GEMINI_API_KEY")
client = genai.Client(api_key=GEMINI_API_KEY)
interaction = client.interactions.create(
model=MODEL_ID,
input="Reply with exactly: Gemini connection ready",
store=False,
)
print(interaction.output_text)
SAMPLE_NOTES = """
Project: Improve the internal purchase-request process at Northstar Services.
Priya, Operations Manager: "Requests arrive by email, and I spend about six hours each week chasing missing cost centre and supplier details."
Priya: "Requests under £5,000 should go to the requester's department manager. Requests of £5,000 or more should go to Finance after the department manager reviews them."
Daniel, Finance Lead: "A named manager must approve every purchase request. I also need a weekly report of pending requests older than five working days."
Mia, Requester: "I often do not know where my request is. I want an email update when it is approved or returned for changes."
Project Sponsor: "The new process should feel much faster and go live before 1 April, but we have not agreed what faster means."
""".strip()
def create_naive_summary(notes):
prompt = (
"Write a concise prose summary of these stakeholder interview notes. "
"Do not return JSON or a table.\n\n" + notes
)
interaction = client.interactions.create(
model=MODEL_ID,
input=prompt,
store=False,
)
return interaction.output_text
def inspect_naive_output(summary):
try:
json.loads(summary)
parse_result = "The response happened to parse as JSON, but no schema enforced its fields."
except json.JSONDecodeError:
parse_result = "The prose response is not structured JSON."
return {
"machine_checkable": False,
"reason": "No schema enforced evidence, interpretation, requirement type, or review fields.",
"parse_result": parse_result,
}
ANALYSIS_SCHEMA = {
"type": "object",
"properties": {
"business_objective": {
"type": "object",
"properties": {
"statement": {
"type": "string",
"description": "A concise draft business objective grounded in the notes.",
},
"evidence": {
"type": "string",
"description": "An exact contiguous excerpt from the notes supporting the objective.",
},
"interpretation": {
"type": "string",
"enum": ["explicit", "inferred"],
},
"review_required": {"type": "boolean"},
},
"required": ["statement", "evidence", "interpretation", "review_required"],
"additionalProperties": False,
},
"stakeholder_needs": {
"type": "array",
"items": {
"type": "object",
"properties": {
"stakeholder": {"type": "string"},
"need": {"type": "string"},
"evidence": {
"type": "string",
"description": "An exact contiguous excerpt from the notes supporting the need.",
},
"interpretation": {
"type": "string",
"enum": ["explicit", "inferred"],
},
"review_required": {"type": "boolean"},
},
"required": [
"stakeholder",
"need",
"evidence",
"interpretation",
"review_required",
],
"additionalProperties": False,
},
},
"requirements": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {"type": "string"},
"category": {
"type": "string",
"enum": [
"business",
"stakeholder",
"solution_functional",
"solution_non_functional",
"transition",
],
},
"statement": {"type": "string"},
"evidence": {
"type": "string",
"description": "An exact contiguous excerpt from the notes supporting the requirement.",
},
"interpretation": {
"type": "string",
"enum": ["explicit", "inferred"],
},
"acceptance_criteria": {
"type": "array",
"items": {"type": "string"},
"description": "Draft testable criteria that a human analyst must validate.",
},
"review_required": {"type": "boolean"},
},
"required": [
"id",
"category",
"statement",
"evidence",
"interpretation",
"acceptance_criteria",
"review_required",
],
"additionalProperties": False,
},
},
"assumptions": {
"type": "array",
"items": {"type": "string"},
},
"risks": {
"type": "array",
"items": {"type": "string"},
},
"open_questions": {
"type": "array",
"items": {"type": "string"},
},
},
"required": [
"business_objective",
"stakeholder_needs",
"requirements",
"assumptions",
"risks",
"open_questions",
],
"additionalProperties": False,
}
def analyze_notes(notes):
prompt = (
"Act as a first-pass assistant to a business analyst. Analyze only the supplied "
"stakeholder notes. Do not invent facts. Use exact excerpts from the notes in every "
"evidence field. Mark anything not directly stated as inferred and requiring review. "
"Treat all requirements and acceptance criteria as drafts for stakeholder validation. "
"Surface ambiguous language, missing measures, and incompatible stakeholder rules as "
"open questions.\n\nStakeholder notes:\n" + notes
)
interaction = client.interactions.create(
model=MODEL_ID,
input=prompt,
response_format={
"type": "text",
"mime_type": "application/json",
"schema": ANALYSIS_SCHEMA,
},
store=False,
)
return json.loads(interaction.output_text)
def normalize_text(text):
return " ".join(
text.casefold()
.replace('"', "")
.replace("“", "")
.replace("”", "")
.split()
)
def validate_evidence(analysis, notes):
evidence_items = [
("business_objective", analysis["business_objective"]["evidence"])
]
for index, need in enumerate(analysis["stakeholder_needs"], start=1):
evidence_items.append((f"stakeholder_need_{index}", need["evidence"]))
for requirement in analysis["requirements"]:
evidence_items.append((requirement["id"], requirement["evidence"]))
normalized_notes = normalize_text(notes)
checks = []
for label, evidence in evidence_items:
checks.append(
{
"item": label,
"evidence": evidence,
"evidence_found": normalize_text(evidence) in normalized_notes,
}
)
passed = sum(check["evidence_found"] for check in checks)
total = len(checks)
coverage = round((passed / total) * 100, 1) if total else 0.0
return {
"passed": passed,
"total": total,
"evidence_coverage_percent": coverage,
"checks": checks,
}
def evaluate_topic_coverage(analysis, expected_topics):
analysis_text = json.dumps(analysis, ensure_ascii=False).casefold()
findings = {
topic: topic.casefold() in analysis_text for topic in expected_topics
}
covered = sum(findings.values())
total = len(findings)
coverage = round((covered / total) * 100, 1) if total else 0.0
return {
"covered": covered,
"total": total,
"topic_coverage_percent": coverage,
"findings": findings,
}
def build_review_queue(analysis, evidence_audit):
queue = []
objective = analysis["business_objective"]
if objective["review_required"]:
queue.append("Review inferred business objective: " + objective["statement"])
for need in analysis["stakeholder_needs"]:
if need["review_required"]:
queue.append("Review stakeholder need: " + need["need"])
for requirement in analysis["requirements"]:
if requirement["review_required"]:
queue.append(
f"Review {requirement['id']}: {requirement['statement']}"
)
for check in evidence_audit["checks"]:
if not check["evidence_found"]:
queue.append("Verify unsupported evidence for " + check["item"])
for question in analysis["open_questions"]:
queue.append("Ask stakeholder: " + question)
return queue
def run_demo():
print("=== Free-form summary ===")
naive_summary = create_naive_summary(SAMPLE_NOTES)
print(naive_summary)
print(json.dumps(inspect_naive_output(naive_summary), indent=2))
print("\n=== Structured requirements brief ===")
analysis = analyze_notes(SAMPLE_NOTES)
print(json.dumps(analysis, indent=2, ensure_ascii=False))
print("\n=== Evidence audit ===")
evidence_audit = validate_evidence(analysis, SAMPLE_NOTES)
print(json.dumps(evidence_audit, indent=2, ensure_ascii=False))
print("\n=== Topic coverage ===")
expected_topics = [
"cost centre",
"supplier",
"£5,000",
"five working days",
"email",
]
topic_audit = evaluate_topic_coverage(analysis, expected_topics)
print(json.dumps(topic_audit, indent=2, ensure_ascii=False))
print("\n=== Human review queue ===")
review_queue = build_review_queue(analysis, evidence_audit)
for item in review_queue:
print("-", item)
return analysis, evidence_audit, topic_audit, review_queue
analysis, evidence_audit, topic_audit, review_queue = run_demo()
That is the audit layer working. Your notebook now exposes unsupported evidence, missing topics, and unresolved questions before anyone treats the draft as confirmed requirements.
Secret mission
Secret Mission: Challenge the Analyzer with a Stakeholder Conflict
Operations wants automatic approval for low-value requests. Finance demands manual approval for every request. Challenge your analyzer to expose both rules without choosing a side.
Clean Up Your Resources
Clean Up Your Resources
Decide whether to keep your resources available, pause your work for later, or delete them entirely. This project used Google Colab with the Gemini API free tier. The main project path has no ongoing charge.
Resources you used:
- One Google Colab notebook stored in Google Drive. It contains the completed stakeholder interview analyzer plus the conflict test.
- One Gemini API key associated with a Google Cloud project.
Keep everything running
No action is needed. Choose this option if you want to keep demonstrating the analyzer or refining its review checks.
- Keep stakeholder_interview_analyzer.ipynb in Google Drive.
- Keep GEMINI_API_KEY in Colab's Secrets panel.
- Retain the conflict test from secret_mission_cell.py for future demonstrations.
- Use fictional or sanitized notes for every future free-tier request because free-tier Gemini content is used to improve Google products.
Pause - I'll come back to this later
Close the hosted runtime while keeping your notebook available in Google Drive. The notebook remains ready for your next analysis session.
- Close the Google Colab browser tab.
- Leave stakeholder_interview_analyzer.ipynb in Google Drive.
- Keep the Gemini API key available through Colab's Secrets panel.
- Run the notebook from its first cell when you return so the fresh runtime installs the pinned package before reconnecting.
Delete - I don't want to use this again
Deletion permanently removes your project copy. Your working analyzer plus its conflict test disappear when you remove the notebook.
- Return to Google Drive.
- Locate stakeholder_interview_analyzer.ipynb.
- Delete stakeholder_interview_analyzer.ipynb from Google Drive.
- Confirm the notebook no longer appears among your retained files.
- Return to Google AI Studio's API Keys page.
- Find the Gemini API key associated with this notebook's Google Cloud project.
- Revoke the Gemini API key from the API Keys page.
- Confirm the API Keys page no longer shows the key as active.
Nice Work!
Nice Work!
That's a wrap! Your Google Colab notebook now supports the full path from stakeholder notes to a reviewable requirements brief.
What you learned:
- Used the Gemini API inside Google Colab to turn fictional stakeholder notes into a categorized requirements brief.
- Added exact source evidence throughout the brief. Separated explicit findings from inferred findings. Flagged draft acceptance criteria for human review.
- Ran deterministic evidence checks against the original notes. Measured topic coverage. Built a review queue for findings that need analyst judgment.
- Secret Mission: Challenged the analyzer with a conflict between Operations and Finance. Kept the final decision with a human stakeholder.
Ready to quiz yourself?