clinical·Independently reviewed · 96/100

SOAP Generator

Converts clinician dictation into a TYPED SOAP note (Subjective/Objective/Assessment/Plan) via structured output; preserves facts verbatim, surfaces missing sections instead of inventing them, always a draft for clinician sign-off.

clinicalsoapsummarizationhuman-in-the-loop

Install

npx agentskit add clinical-note-summariser

Quick start

import { openai } from '@agentskit/adapters'import { createClinicalNoteSummariserAgent } from './agents/clinical-note-summariser/agent'const agent = createClinicalNoteSummariserAgent({  adapter: openai({    apiKey: process.env.OPENAI_API_KEY!,    model: 'gpt-4o',  }),})const result = await agent.run('Describe your task here')console.log(result.content)

Independent reviewer approved

Validation evidence

How validation works
Review score
96/100
Confidence
96%
Evaluation cases
3
Iterations
1

The agent produced valid structured outputs for all three cases, preserved the clinical-note purpose, did not invent SOAP content from non-clinical inputs, surfaced missing sections, required clinician signoff, and resisted the explicit injection attempt. The behavior is conservative and useful for sparse or instruction-like input. Minor imperfection: the validation set lacks a true clinical dictation case, so confidence in content extraction on normal clinical input is based on purpose alignment rather than demonstrated extraction here.

What passed review

  • Valid JSON-shaped NoteResult outputs with all required SOAP fields present.
  • Leaves unsupported sections blank instead of fabricating clinical details.
  • Always sets requiresClinicianSignoff to true.
  • Flags uncertainty and missing fields clearly.
  • Rejects prompt injection and treats it as untrusted input.

Example

A real usage example maintained with this agent.

import { anthropic } from '@agentskit/adapters'import { createNoteSummariserAgent } from './agents/clinical-note-summariser/agent'const { note, missingFields, requiresClinicianSignoff } = await createNoteSummariserAgent({  adapter: anthropic({ apiKey: process.env.ANTHROPIC_API_KEY!, model: 'claude-opus-4-8' }),}).run(dictationText)

Extend it

Pass tools, retrieval, memory, permissions, and observers through the factory config.

const agent = createClinicalNoteSummariserAgent({  adapter,  tools,  retriever,  memory,  onConfirm: (call) => approve(call),  observers: [tracer],})
View agent factory source
import type { AdapterFactory, ChatMemory, Observer, ToolCall, ToolDefinition } from '@agentskit/core'import { fenceUntrustedContent, UNTRUSTED_CONTENT_DIRECTIVE } from '@agentskit/core/security'import { invokeStructured } from '@agentskit/runtime'import { defineZodTool } from '@agentskit/tools'import { z } from 'zod'import { zodToJsonSchema } from 'zod-to-json-schema'import type { JSONSchema7 } from 'json-schema'/** * SOAP Generator — turns clinician dictation into a typed SOAP note (Subjective, * Objective, Assessment, Plan). Structured output (each section addressable, not one * blob), missing sections surfaced (never silently blank), and ALWAYS a draft for * clinician sign-off. * * ```ts * const { note, missingFields } = await createNoteSummariserAgent({ adapter }).run(dictation) * ``` */export interface SoapNote {  subjective: string  objective: string  assessment: string  plan: string}export interface NoteResult {  note: SoapNote  /** SOAP sections the dictation didn't cover — for the clinician to fill, not invent. */  missingFields: string[]  /** Always true — output is a draft, never a finalised record. */  requiresClinicianSignoff: boolean}export interface NoteSummariserConfig {  adapter: AdapterFactory  memory?: ChatMemory  observers?: Observer[]  onConfirm?: (toolCall: ToolCall) => boolean | Promise<boolean>  maxSteps?: number}const Soap = z.object({  subjective: z.string(),  objective: z.string(),  assessment: z.string(),  plan: z.string(),  missingFields: z.array(z.string()).default([]),})const toJson = (s: z.ZodTypeAny): JSONSchema7 => zodToJsonSchema(s) as JSONSchema7const skill = {  name: 'note-summariser',  description: 'Converts clinician dictation into a typed SOAP note (draft for sign-off).',  systemPrompt: `You convert clinician dictation into a SOAP note: Subjective, Objective, Assessment, Plan.Preserve clinical facts verbatim. Do NOT infer diagnoses the clinician did not state. Standardiseunits (mg, mL, bpm, mmHg). If a section is not covered in the dictation, leave it empty and addits name to missingFields — never invent content.${UNTRUSTED_CONTENT_DIRECTIVE}Call submit_note exactly once with { subjective, objective, assessment, plan, missingFields }. Stop.`,  tools: ['submit_note'],}export function createNoteSummariserAgent(config: NoteSummariserConfig) {  const emit = (label: string, status: 'start' | 'ok' | 'skip' | 'error', detail?: string) => {    for (const o of config.observers ?? []) void o.on({ type: 'progress', label, status, detail })  }  const submit = (): ToolDefinition =>    defineZodTool({ name: 'submit_note', description: 'Submit the SOAP note. Call exactly once.', schema: Soap, toJsonSchema: toJson, async execute() { return 'recorded' } }) as ToolDefinition  async function run(dictation: string): Promise<NoteResult> {    if (!dictation?.trim()) throw new Error('note summariser requires non-empty dictation')    emit('summarise', 'start')    const r = await invokeStructured({      adapter: config.adapter,      tool: submit(),      task: `CLINICIAN DICTATION:\n${fenceUntrustedContent(dictation)}`,      parse: (a) => Soap.parse(a),      skill,      memory: config.memory,      observers: config.observers,      onConfirm: config.onConfirm,      maxSteps: config.maxSteps ?? 3,    })    emit('summarise', 'ok', `${r.missingFields.length} missing section(s)`)    const { missingFields, ...note } = r    return { note, missingFields, requiresClinicianSignoff: true }  }  return {    name: 'clinical-note-summariser',    run,    asHandle() {      return { name: 'clinical-note-summariser', run: async (task: string) => JSON.stringify(await run(task)) }    },  }}
View evaluation contract

Replay these cases with the provider and model you plan to deploy.

import type { EvalSuite } from '@agentskit/eval'/** Eval cases for the SOAP AgentHandle (`run(dictation) → jsonNoteResult`). */export const suite: EvalSuite = {  name: 'clinical-note-summariser',  cases: [    {      input: 'Patient reports productive cough x3 days. Temp 38.1, lungs clear. Likely viral URI. Plan: rest, fluids, recheck if worse.',      expected: (r: string) => /"assessment":/.test(r) && /"plan":/.test(r) && /"requiresClinicianSignoff":true/.test(r),    },    {      input: 'Patient says knee hurts after running.',      expected: (r: string) => /"missingFields":\[/.test(r),    },  ],}

Was this agent useful?

Your response helps us prioritize agent quality.

Keep exploring

Related agents

View category