Skip to content

Study Assistant (AI-Powered)

AI-powered study configuration from protocol documents using Large Language Models.

Overview

The Study Assistant ingests study protocol PDFs and automatically generates:

  • Study metadata
  • Visit schedules
  • Battery configurations
  • Form definitions
  • Consent documents

ChangeSet Workflow

All AI-generated configurations use a ChangeSet approval workflow:

1. Ingest Protocol → 2. Generate ChangeSet → 3. Review → 4. Apply

Benefits: - Review before apply - Human oversight of AI decisions - Incremental changes - Apply changes step-by-step - Rollback support - Undo applied changes - Audit trail - Track all AI-generated configs

Protocol Ingestion

Upload Protocol

// Upload PDF protocol
const response = await api.uploadProtocol({
  study_id: study_id,
  file: protocolPdf,
});

// AI extracts key information
const extraction = response.extraction;

Extracted Information

AI extracts:

{
  "study_metadata": {
    "title": "Neurocognitive Assessment in Mild Cognitive Impairment",
    "phase": "Observational",
    "duration_weeks": 52,
    "target_enrollment": 100
  },
  "visits": [
    {
      "visit_name": "Baseline",
      "visit_number": 1,
      "window_start_days": 0,
      "window_end_days": 7,
      "procedures": ["Cognitive assessment", "Demographics", "Medical history"]
    },
    {
      "visit_name": "Month 3",
      "visit_number": 2,
      "window_start_days": 84,
      "window_end_days": 98,
      "procedures": ["Cognitive assessment", "Adverse events"]
    }
  ],
  "assessments": [
    {
      "name": "Cognitive Battery",
      "tasks": ["Simple RT", "CPT", "N-Back", "Digit Span"],
      "estimated_minutes": 30
    }
  ]
}

ChangeSet Generation

Create ChangeSet

// Generate ChangeSet from extraction
const changeSet = await api.createChangeSet({
  study_id: study_id,
  extraction_id: extraction.id,
  changes: [
    {
      entity_type: "visit",
      operation: "create",
      data: {
        visit_name: "Baseline",
        visit_number: 1,
        window_start_days: 0,
        window_end_days: 7,
      }
    },
    {
      entity_type: "battery",
      operation: "create",
      data: {
        name: "Cognitive Battery",
        tasks: ["simple_rt", "cpt", "nback", "digit_span"]
      }
    }
  ]
});

ChangeSet Structure

interface ChangeSet {
  id: string;
  study_id: string;
  status: "pending" | "approved" | "rejected" | "applied";
  changes: Change[];
  created_by: "ai" | "user";
  created_at: string;
}

interface Change {
  entity_type: "study" | "visit" | "battery" | "form" | "consent";
  operation: "create" | "update" | "delete";
  entity_id?: string; // For update/delete
  data: object;
  validation_errors?: string[];
}

Review Interface

Portal provides ChangeSet review UI:

// ChangeSetReview.tsx
function ChangeSetReview({ changeSetId }: { changeSetId: string }) {
  const { data: changeSet } = useChangeSet(changeSetId);

  return (
    <div>
      <h1>Review AI-Generated Configuration</h1>

      {changeSet.changes.map((change, index) => (
        <ChangeCard
          key={index}
          change={change}
          onAccept={() => acceptChange(change.id)}
          onReject={() => rejectChange(change.id)}
          onModify={(data) => modifyChange(change.id, data)}
        />
      ))}

      <button onClick={() => applyChangeSet(changeSetId)}>
        Apply All Changes
      </button>
    </div>
  );
}

Change Review Actions

  • Accept - Approve change as-is
  • Reject - Remove change from ChangeSet
  • Modify - Edit AI-generated data before applying

Apply ChangeSet

Once reviewed, apply changes to study:

// Apply all approved changes
await api.applyChangeSet(changeSetId);

// Apply selectively
await api.applyChangeSet(changeSetId, {
  change_ids: ["change_1", "change_3", "change_5"]
});

Changes are applied transactionally: - All changes succeed or none are applied - Rollback on error - Create audit log entry

Form Definition Extraction

AI extracts form structures from protocol:

{
  "form_name": "Demographics",
  "fields": [
    {
      "field_name": "age",
      "field_label": "Age",
      "field_type": "number",
      "validation": { "min": 18, "max": 120 }
    },
    {
      "field_name": "gender",
      "field_label": "Gender",
      "field_type": "radio",
      "choices": ["Male", "Female", "Other", "Prefer not to say"]
    },
    {
      "field_name": "race",
      "field_label": "Race/Ethnicity",
      "field_type": "checkbox",
      "choices": [
        "American Indian or Alaska Native",
        "Asian",
        "Black or African American",
        "Hispanic or Latino",
        "Native Hawaiian or Other Pacific Islander",
        "White"
      ]
    }
  ]
}

AI Model Configuration

# app/services/ai_assistant.py
from openai import AsyncOpenAI

class StudyAssistant:
    def __init__(self):
        self.client = AsyncOpenAI(api_key=settings.OPENAI_API_KEY)
        self.model = "gpt-4-turbo"

    async def extract_protocol(self, pdf_text: str) -> dict:
        response = await self.client.chat.completions.create(
            model=self.model,
            messages=[
                {
                    "role": "system",
                    "content": "Extract study information from protocol..."
                },
                {
                    "role": "user",
                    "content": pdf_text
                }
            ],
            response_format={"type": "json_object"}
        )

        return json.loads(response.choices[0].message.content)

Validation

AI-generated configs are validated before applying:

// Validate visit schedule
function validateVisitSchedule(visits: Visit[]): ValidationError[] {
  const errors = [];

  // Check for overlapping windows
  for (let i = 0; i < visits.length - 1; i++) {
    if (visits[i].window_end_days > visits[i + 1].window_start_days) {
      errors.push({
        field: `visits[${i}].window_end_days`,
        message: "Visit windows overlap"
      });
    }
  }

  // Check for missing visit numbers
  const visitNumbers = visits.map(v => v.visit_number).sort();
  for (let i = 1; i <= visitNumbers.length; i++) {
    if (!visitNumbers.includes(i)) {
      errors.push({
        field: "visits",
        message: `Missing visit number ${i}`
      });
    }
  }

  return errors;
}

Rollback

Revert applied ChangeSets:

// Rollback to previous state
await api.rollbackChangeSet(changeSetId);

Rollback creates inverse ChangeSet: - Create → Delete - Update → Revert to previous values - Delete → Recreate

Audit Trail

All AI-assisted operations are logged:

{
  "timestamp": "2026-01-24T12:34:56Z",
  "operation": "apply_changeset",
  "changeset_id": "cs_123",
  "user_id": 42,
  "changes_applied": 5,
  "changes_rejected": 2,
  "entities_created": {
    "visits": 3,
    "batteries": 1,
    "forms": 2
  }
}

Future Enhancements

  • Iterative refinement - Chat with AI to refine configurations
  • Template learning - AI learns from approved ChangeSets
  • Multi-protocol support - Compare protocols from multiple studies
  • Automated testing - AI generates test data for validation

Next Steps