n4nAI

How to deploy an AI agent for patient intake forms

Step-by-step tutorial to build and deploy a conversational AI agent for patient intake forms using Python, Pydantic, and an OpenAI-compatible LLM endpoint.

n4n Team3 min read619 words

Audio narration

Coming soon — every post will get a voice note here.

Building an AI agent patient intake forms system means replacing static web forms with a conversational layer that extracts structured data from free-text chat. This tutorial walks through a production-minded implementation using Python, Pydantic, and an OpenAI-compatible LLM endpoint, culminating in a deployable FastAPI service.

Prerequisites

  • Python 3.11 or newer
  • openai Python package (v1.40+)
  • pydantic v2
  • fastapi and uvicorn for the API layer
  • An API key for any OpenAI-compatible chat completion endpoint

You should be comfortable with async Python and basic HTTP services. No prior healthcare interoperability knowledge is required, though we will touch on PHI handling.

Define the intake data model

Start with a strict schema. The LLM will emit JSON that must validate against this. Using Pydantic gives you a free JSON schema for tool definitions and runtime validation.

from pydantic import BaseModel, EmailStr, Field
from typing import Optional

class PatientIntake(BaseModel):
    full_name: str = Field(..., description="Patient legal name")
    date_of_birth: str = Field(..., description="ISO 8601 date, e.g. 1990-05-12")
    phone: str = Field(..., description="Contact phone number with country code")
    email: Optional[EmailStr] = None
    insurance_provider: Optional[str] = None
    insurance_member_id: Optional[str] = None
    chief_complaint: str = Field(..., description="Primary reason for the visit")
    symptom_duration_days: Optional[int] = None

Keep the field set minimal. Every extra field is another thing the model can get wrong or that the patient must provide. You can always extend the model later without rewriting the agent loop.

Configure the LLM client

We use the standard OpenAI client but point it at an OpenAI-compatible gateway. For example, n4n.ai exposes a single endpoint that fronts 240+ models and automatically fails over when a provider is rate-limited, which matters when intake traffic spikes.

import os
from openai import OpenAI

client = OpenAI(
    base_url="https://api.n4n.ai/v1",
    api_key=os.environ["LLM_API_KEY"]
)
MODEL = "gpt-4o-mini"  # swap for any model id your gateway supports

If you run your own proxy or use OpenAI directly, change base_url accordingly. The rest of the code is identical.

Build the conversation agent

The agent loops: send history to the model, check for tool calls, and either ingest a completed intake or push the model’s question back to the user. We define one tool, submit_intake, that accepts the Pydantic schema.

SYSTEM_PROMPT = """You are a clinic intake assistant. Collect patient details through natural conversation.
Required before calling submit_intake: full_name, date_of_birth, phone, chief_complaint.
Ask one question at a time. Do not invent values. If the patient refuses, note 'unknown'."""

tools = [{
    "type": "function",
    "function": {
        "name": "submit_intake",
        "description": "Record finalized patient intake",
        "parameters": PatientIntake.model_json_schema()
    }
}]

def step(messages: list) -> dict:
    resp = client.chat.completions.create(
        model=MODEL,
        messages=messages,
        tools=tools,
        tool_choice="auto"
    )
    return resp.choices[0].message.model_dump()

I prefer tool_choice="auto" over forced because it lets the model ask clarifying questions when a field is ambiguous. If you force the tool early, you get nulls or guesses. The system prompt explicitly forbids invention; that instruction is non-negotiable for intake.

Scripted end-to-end run

The following function simulates a user by feeding canned replies until the agent calls the tool. In a live system you would await real input instead of looping over a list.

def run_scripted(user_turns: list[str]) -> PatientIntake:
    messages = [{"role": "system", "content": SYSTEM_PROMPT}]
    for turn in user_turns:
        messages.append({"role": "user", "content": turn})
        msg = step(messages)
        messages.append(msg)
        if msg.get("tool_calls"):
            args = msg["tool_calls"][0]["function"]["arguments"]
            return PatientIntake.model_validate_json(args)
        # no tool call: assistant asked a question, scripted turn continues
    raise RuntimeError("Agent did not complete intake")

sample = [
    "Hi, I'm Jane Doe, born May 12 1990.",
    "My number is 555-123-4567.",
    "I have a cough that won't go away, about a week now.",
    "Jane@example.com, insurance Aetna, member W123456."
]

intake = run_scripted(sample)
print(intake.model_dump_json(indent=2))

Expected output:

{
  "full_name": "Jane Doe",
  "date_of_birth": "1990-05-12",
  "phone": "555-123-4567",
  "email": "jane@example.com",
  "insurance_provider": "Aetna",
  "insurance_member_id": "W123456",
  "chief_complaint": "Cough that won't go away",
  "symptom_duration_days": 7
}

The model maps “about a week” to 7 and normalizes the date. That is the core value of an AI agent patient intake forms flow: free text in, clean records out.

Persist the intake

Write to a local SQLite database. In production you would swap this for your EHR or a HIPAA-eligible datastore.

import sqlite3

def persist(intake: PatientIntake):
    conn = sqlite3.connect("intakes.db")
    cur = conn.cursor()
    cur.execute("""
        CREATE TABLE IF NOT EXISTS intakes (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            payload TEXT,
            created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
        )""")
    cur.execute("INSERT INTO intakes (payload) VALUES (?)", (intake.model_dump_json(),))
    conn.commit()
    conn.close()

Call persist(intake) immediately after validation succeeds.

Expose as an HTTP service

FastAPI wraps the session state. We keep an in-memory dict for demo; use Redis in production.

from fastapi import FastAPI
from typing import Dict

app = FastAPI()
sessions: Dict[str, list] = {}

@app.post("/intake/{sid}")
async def intake_message(sid: str, body: dict):
    if sid not in sessions:
        sessions[sid] = [{"role": "system", "content": SYSTEM_PROMPT}]
    sessions[sid].append({"role": "user", "content": body["text"]})
    msg = step(sessions[sid])
    sessions[sid].append(msg)
    if msg.get("tool_calls"):
        intake = PatientIntake.model_validate_json(msg["tool_calls"][0]["function"]["arguments"])
        persist(intake)
        del sessions[sid]
        return {"status": "completed", "intake": intake.model_dump()}
    return {"status": "waiting", "assistant": msg.get("content")}

Run it:

uvicorn intake_api:app --port 8000

Then POST a message:

curl -X POST localhost:8000/intake/abc123 -H 'content-type: application/json' -d '{"text":"I am John, DOB 1982-03-01, phone +15551234"}'

Response while waiting:

{"status":"waiting","assistant":"Thanks. What's the main reason for your visit today?"}

Hardening for healthcare

An AI agent patient intake forms deployment handles PHI. Treat every message as sensitive. Disable verbose logging, encrypt the database, and set TTLs on session caches. If you use a shared LLM gateway, confirm it forwards provider cache-control hints and does not retain prompts.

Function calling reduces hallucinated fields because the schema constrains output. Still, never trust the model for clinical triage—route chief complaints to a human or a rules engine.

Automatic fallback across providers is the difference between a 2 AM page and a silent retry. If your gateway supports it, lean on it; an intake form that errors out mid-conversation loses the patient. The gateway’s per-token metering also helps track cost per completed intake, which is more useful than raw API spend.

Where to go next

Add an SMS front-end by swapping the FastAPI layer for a Twilio webhook. Extend the Pydantic model with appointment slot preferences and call a scheduling tool. The pattern stays the same: tight schema, single submit tool, validate before persist.

That is a deployable baseline for an AI agent patient intake forms pipeline without bespoke form builders.

Tagshealthcare-ai-agentspatient-intakeautomationhealth-tech

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All ai agents in healthcare operations posts →