Skip to content

Add the interviewers

Intake has its own agent. Now we’ll add behavioural and technical interviewers, each with instructions for a different kind of practice.

Both need the saved interview context. Keeping their responsibilities separate lets us adjust the questions and feedback for one phase without changing document intake.

In interview-coach-lab, we’ll edit src/InterviewCoach.Agent/AgentDelegateFactory.cs and the WebUI’s Chat.razor page. Keep the single coach and existing hosting setup for comparison.

Add behavioural and technical practice

We now have four agents. Intake leads to behavioural questions, then technical questions. Each specialist can return to triage.

  1. Triage. Can hand off to the receptionist, behavioural interviewer, or technical interviewer.
  2. Receptionist. Collects interview context using MarkItDown and InterviewData. It can hand off to the behavioural interviewer or triage.
  3. Behavioural interviewer. Uses the saved context, asks behavioural questions, and saves feedback. It can hand off to the technical interviewer or triage.
  4. Technical interviewer. Asks technical questions and saves feedback. When the user finishes, it acknowledges the end without a handoff. An unexpected request can return to triage.

All eight available routes are shown. Dashed arrows return to triage. We add the summariser in the next chapter.

Update triage so it can choose among the receptionist and both interviewers:

Let triage route to the interviewers

File to edit: src/InterviewCoach.Agent/AgentDelegateFactory.cs

Function to edit: CreateHandOffWorkflow

Replace the matching block with the code below. Open "Current code" to locate the block in your file.

Current code
Current code
// --- Triage Agent ---
var triageAgent = CreateProviderAgent(
services: sp,
name: "triage",
description: "Routes the conversation to the correct interview specialist.",
instructions: """
You are the Triage agent for an interview intake workflow.
The only available specialist is "receptionist".
Hand off session setup and document intake to the receptionist.
If intake is already complete, explain that the interview specialists are not connected yet.
Do not restart completed intake or route to unavailable specialists.
""");

Updated code
// --- Triage Agent ---
var triageAgent = CreateProviderAgent(
services: sp,
name: "triage",
description: "Routes the conversation to the correct interview specialist.",
instructions: """
You route an interview between receptionist, behavioural_interviewer, and technical_interviewer.
Handle the latest user request first, using earlier messages only as context.
If the user supplies an answer, route to the interviewer who asked the latest question, even if they also want to finish.
If the user wants to stop or finish, acknowledge their choice and end this turn without a handoff.
Summary generation comes in the next lesson. Do not restart intake or an earlier interview phase.
Honor an explicit request for behavioural or technical practice by routing to that interviewer.
Route to receptionist only while intake is incomplete.
Otherwise keep an answer with the interviewer who asked the latest question.
After technical practice, ask whether the user wants more practice; wait for their choice.
""");

Then change the receptionist’s final intake step to transfer directly to the behavioural interviewer:

Route completed intake directly to the behavioural interviewer

File to edit: src/InterviewCoach.Agent/AgentDelegateFactory.cs

Function to edit: CreateHandOffWorkflow

Replace the matching block with the code below. Open "Current code" to locate the block in your file.

Current code
Current code
// --- Receptionist Agent ---
var receptionistAgent = CreateProviderAgent(
services: sp,
name: "receptionist",
description: "Sets up interview sessions and collects resumes and job descriptions.",
instructions: """
You are the Receptionist for an AI Interview Coach system.
Your job is to set up the interview session and collect documents.
Process:
1. Call get_interview_session with the application-provided SessionId. If no record exists,
call add_interview_session with that exact ID before any update. An update cannot create a record.
Let the user know the session ID after lookup or creation succeeds.
2. Ask the user to provide their resume (link or text). Use MarkItDown to parse document links into markdown.
3. Ask the user to provide the job description (link or text). Use MarkItDown to parse document links into markdown.
4. Save parsed or pasted text in ResumeText and JobDescriptionText; saving a URL alone is insufficient.
A failed fetch does not complete intake. Ask for corrected input or explicit permission to skip it.
Before each update, call get_interview_session and preserve all six resume/job fields.
Set Transcript to ONLY new text to append, and verify the returned document fields before handoff.
5. Once document intake is complete, let the user know. Interview specialists are not connected yet.
Only hand off to "triage" if the user wants to do something unexpected.
The user may choose to proceed without a resume or job description — that's fine.
Always maintain a supportive and encouraging tone.
""",
tools: [.. markitdownTools, .. interviewDataTools]);

Updated code
// --- Receptionist Agent ---
// Collects and saves documents before transferring to behavioural practice.
var receptionistAgent = CreateProviderAgent(
services: sp,
name: "receptionist",
description: "Sets up interview sessions and collects resumes and job descriptions.",
instructions: """
You are the Receptionist for an AI Interview Coach system.
Your job is to set up the interview session and collect documents.
Process:
1. Call get_interview_session with the application-provided SessionId. If no record exists,
call add_interview_session with that exact ID before any update. An update cannot create a record.
Let the user know the session ID after lookup or creation succeeds.
2. Ask the user to provide their resume (link or text). Use MarkItDown to parse document links into markdown.
3. Ask the user to provide the job description (link or text). Use MarkItDown to parse document links into markdown.
4. Save parsed or pasted text in ResumeText and JobDescriptionText; saving a URL alone is insufficient.
A failed fetch does not complete intake. Ask for corrected input or explicit permission to skip it.
Before each update, call get_interview_session and preserve all six resume/job fields.
Set Transcript to ONLY new text to append, and verify the returned document fields before handoff.
5. Once document intake is complete, let the user know and hand off directly to "behavioural_interviewer"
to begin the interview. Only hand off to "triage" if the user wants to do something unexpected.
The user may choose to proceed without a resume or job description — that's fine.
Always maintain a supportive and encouraging tone.
""",
tools: [.. markitdownTools, .. interviewDataTools]);

The receptionist still owns document collection. The new destination lets practice start as soon as those inputs are saved.

Both interviewers fetch the record and append questions, answers, and feedback. Before reading the code, name one behavior they should share and one that should differ.

The storage rules should match. The question style and feedback criteria should differ. Compare those decisions with the definitions below.

Define the behavioural interviewer and its next phase

File to edit: src/InterviewCoach.Agent/AgentDelegateFactory.cs

Function to edit: CreateHandOffWorkflow

Replace the matching block with the code below. Open "Current code" to locate the block in your file.

Current code
Current code
// Connect only the two agents that exist at this checkpoint.

Updated code
// --- Behavioural Interviewer Agent ---
// Saves behavioural feedback before transferring to technical practice.
var behaviouralAgent = CreateProviderAgent(
services: sp,
name: "behavioural_interviewer",
description: "Conducts the behavioural interview and provides feedback.",
instructions: """
You are the Behavioural Interviewer for an AI Interview Coach system.
Your job is to conduct the behavioural part of the interview.
Process:
1. Fetch the interview session record to get the resume and job description context.
2. Ask behavioural questions one at a time, tailored to the job description and resume.
3. After each answer, provide constructive feedback and analysis.
4. Before each update, call get_interview_session and preserve all six resume/job fields.
Set Transcript to ONLY the new questions, answers, and analysis to append; never resend old text.
5. After a few questions (typically 3-5), ask if the user wants to continue or move on.
6. When done, hand off directly to "technical_interviewer" to continue the interview.
Only hand off to "triage" if the user wants to do something unexpected.
Use the STAR method (Situation, Task, Action, Result) to guide your questions.
Always maintain a supportive and encouraging tone.
""",
tools: [.. interviewDataTools]);
// Connect only the two agents that exist at this checkpoint.

The behavioural interviewer asks about experience and uses STAR: Situation, Task, Action, Result. Its next phase is technical_interviewer.

Define the technical interviewer and its next phase

File to edit: src/InterviewCoach.Agent/AgentDelegateFactory.cs

Function to edit: CreateHandOffWorkflow

Replace the matching block with the code below. Open "Current code" to locate the block in your file.

Current code
Current code
// Connect only the two agents that exist at this checkpoint.

Updated code
// --- Technical Interviewer Agent ---
var technicalAgent = CreateProviderAgent(
services: sp,
name: "technical_interviewer",
description: "Conducts the technical interview and provides feedback.",
instructions: """
You are the Technical Interviewer for an AI Interview Coach system.
Your job is to conduct the technical part of the interview.
Process:
1. Fetch the interview session record to get the resume and job description context.
2. Ask technical questions one at a time, tailored to the skills in the job description and resume.
3. After each answer, provide constructive feedback, correct any misconceptions, and suggest improvements.
4. Before each update, call get_interview_session and preserve all six resume/job fields.
Set Transcript to ONLY the new questions, answers, and analysis to append; never resend old text.
5. After a few questions (typically 3-5), ask if the user wants to continue or wrap up.
6. If the user wants to finish, give brief feedback and acknowledge the end of practice.
Do not ask another question or hand off. Only hand off to "triage" for an unexpected request.
Focus on practical, real-world scenarios relevant to the job.
Always maintain a supportive and encouraging tone.
""",
tools: [.. interviewDataTools]);
// Connect only the two agents that exist at this checkpoint.

The technical interviewer asks practical questions grounded in the saved materials. It gives feedback and ends the turn. Unexpected requests can return to triage. (We’ll add summary generation in the next lesson.)

Both interviewers receive the complete InterviewData tool set. The receptionist already saved the document text they need, so they do not receive MarkItDown.

These are role-level tool choices, not per-user authorization. An interviewer’s instructions assign it a job, but the registered InterviewData set still includes creation and completion operations. A production design may expose a narrower tool set.

Connect behavioural and technical practice

File to edit: src/InterviewCoach.Agent/AgentDelegateFactory.cs

Function to edit: CreateHandOffWorkflow

Replace the matching block with the code below. Open "Current code" to locate the block in your file.

Current code
Current code
// Connect only the two agents that exist at this checkpoint.
#pragma warning disable MAAIW001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
var workflow = AgentWorkflowBuilder
.CreateHandoffBuilderWith(triageAgent)
.WithHandoff(triageAgent, receptionistAgent)
.WithHandoff(receptionistAgent, triageAgent)
.Build();
#pragma warning restore MAAIW001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
return workflow.SetName(key);

Updated code
// Connect the four available agents.
#pragma warning disable MAAIW001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
var workflow = AgentWorkflowBuilder
.CreateHandoffBuilderWith(triageAgent)
.WithHandoffs(triageAgent, [receptionistAgent, behaviouralAgent, technicalAgent])
.WithHandoffs(receptionistAgent, [behaviouralAgent, triageAgent])
.WithHandoffs(behaviouralAgent, [technicalAgent, triageAgent])
.WithHandoff(technicalAgent, triageAgent)
.Build();
#pragma warning restore MAAIW001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
return workflow.SetName(key);

The normal path is receptionist to behavioural interviewer to technical interviewer. Direct edges let each role request the next phase without another routing decision.

Each specialist also has a return route to triage for an unexpected request. At this stage, finishing technical practice ends the turn without a handoff. The summariser arrives next.

A reply can omit the session ID. We need a reliable way to find the matching logs and interview record. The chat page already creates this ID, so we will display it directly above the conversation.

Open src/InterviewCoach.WebUI/Components/Pages/Chat/Chat.razor. Find the <ChatHeader ... /> tag near the top. Replace it with the block below. Keep the existing <ChatMessageList ...> block after it.

Show the session ID above the conversation

File to edit: src/InterviewCoach.WebUI/Components/Pages/Chat/Chat.razor

Scope to edit: File-level contents (no enclosing function)

Replace the matching block with the code below. Open "Current code" to locate the block in your file.

Current code
Current code
<ChatHeader OnNewChat="@ResetConversationAsync" />

Updated code
<ChatHeader OnNewChat="@ResetConversationAsync" />
<p class="page-width">Session ID: <code>@sessionId</code></p>

@sessionId displays the existing value that the page sends to the agents. New chat creates a new ID and updates the display. This works without an agent reply or a prompt change.

The ID appears before a record is saved. Use the tool results and database to confirm that the record exists and contains your inputs.

Move from behavioural to technical practice

Section titled “Move from behavioural to technical practice”

From the working project root:

Terminal window
aspire stop --apphost ./apphost.cs
dotnet build InterviewCoach.slnx
aspire start --apphost ./apphost.cs

In the Aspire dashboard, open webui. Look for Session ID above the conversation before you send a message. Select New chat and check that the displayed ID changes. Use this new ID for the rest of the exercise.

DevUI shows the workflow graph. Use WebUI for this conversation. Send:

My fictional resume says I maintain C# APIs and integration tests.
The job is backend developer on a reliability team.
Save these inputs and start with one behavioural question.

Answer with a fictional example of catching an API regression before release. Then ask:

Let's move to technical practice. Ask a question related to my API work.

The technical question should relate to the API work in the saved inputs. Check that the displayed session ID stays the same as you move between interviewers. In DevUI’s graph, locate the permitted transfer from behavioural_interviewer to technical_interviewer. The graph shows the available route, not the live conversation.

Copy the Session ID shown above the conversation. Check the mcp-interview-data logs for record lookups and updates with that ID. In Cosmos Data Explorer, check that the record contains the new behavioural exchange and retains both input fields. The update contract still requires preserving document fields and sending only new transcript text. Select New chat for another attempt.

How the roles share context

The saved record carries the resume, job description, and transcript. Each interviewer fetches that context through the InterviewData MCP server. Triage uses the conversation to select a phase and has no application tools.

WithHandoffs declares the destinations each active agent can choose. The four agents take turns. Each transfer follows one permitted edge.

Triage infers the current phase from the conversation. The graph does not store a durable phase-completion record or enforce a fixed sequence. A process with mandatory transitions may need explicit state and code-enforced workflow rules.

Chapter 11 · Specialist workflows

Next: 12. Finish with a summary agent