IBM Developer

Article

Extract and analyze medical data with Docling, CrewAI, and AI Agents

Build an agent-based workflow that parses scanned blood reports, extracts key health parameters, and predicts CKD using a trained AI model

By Anjala N S, Deepak C Shetty

Early detection of chronic kidney disease (CKD) is key to slowing or even stopping its progression. It helps manage common risk factors such as diabetes, high blood pressure, and heart disease. Timely action can protect kidney function and lower the chances of expensive treatments such as hospitalization, dialysis, or transplants.

This article shows how an agentic workflow can support early CKD detection. By combining structured patient data and AI, especially in nephrology, we can identify CKD in its early stages using routine blood and urine tests. This example focuses on a key marker of kidney health: the blood’s filtration capacity, measured by estimated Glomerular Filtration Rate (eGFR). For more information, see PMC Article on CKD.

eGFR is an important diagnostic marker used to check how well the kidneys are filtering the blood. It gives an estimate of the glomerular filtration rate (GFR), which is the rate at which the kidneys remove waste and extra fluids from the body. eGFR is calculated using formulas like the CKD-EPI or MDRD equation, based on:

  • Serum creatinine level

  • Age

  • Sex

  • Race (in some cases)

The following image shows the eGFR ranges and the related stages of kidney function.

stages of kidney disease

Image reference

Prerequisites

Set up a watsonx.ai project

To create a watsonx.ai project, complete the following steps:

  1. Log in to your IBM Cloud account.

  2. Go to Manage > Access (IAM) > API Keys and create a new API key. Save it as your WATSONX_APIKEY.

  3. Open watsonx.ai and sign in using your IBM Cloud account.

  4. Create a new project by following the steps in IBM Cloud documentation.

  5. To get your project ID, open the project in watsonx.ai, go to the Manage tab, and copy the Project ID under the General section. Save it as your WATSONX_PROJECT_ID.

This setup will allow CrewAI agents to access foundation models in watsonx.ai and connect with other tools as needed.

Set up Python environment and variables

  1. Make sure Python version 3.10 or 3.11 is installed. Check with:

    python --version

  2. Create a virtual environment:

    python -m venv crewai_watsonx_venv

  3. Activate the virtual environment:

    • On Windows:

      crewai_watsonx_venv\Scripts\activate

    • On Linux or macOS:

      source crewai_watsonx_venv/bin/activate

  4. Install the required packages:

    pip install crewai==0.75.0 langchain==0.3.4 langchain-community==0.3.3 litellm==1.50.2 crewai.tool

  5. Create a .env file in your project folder and add these lines:

     WATSONX_APIKEY=<Your Watsonx API Key>
     WATSONX_PROJECT_ID=<Your Watsonx Project ID>
     WATSONX_URL=https://us-south.ml.cloud.ibm.com
    

Define custom tools for Docling

You can create custom tools to interact with the Docling environment.

Start by importing the required libraries:

import subprocess
import os
import json
from crewai.tools import BaseTool
from typing import Type
from pydantic import BaseModel, Field

Define input schema for Docling tool

Create the input schema to pass the PDF file path:

class DoclingToolInput(BaseModel):
    pdf_file_name: str = Field(..., description="Path of the PDF file")

Create tool to convert PDF to JSON

Define the tool that parses a PDF file and returns a JSON output:

class DoclingTool(BaseTool):
    name: str = "Docling"
    description: str = "Tool used to parse input PDF file and convert it into a JSON file"
    args_schema: Type[BaseModel] = DoclingToolInput

Convert PDF to JSON using command line

This function uses the Docling command-line tool to convert a PDF file into a JSON file.

def _run(self, pdf_file_name: str) -> str:
    """Uses Docling to process a PDF file and convert it to a JSON file."""
    try:
        cmd = f"docling --to json --force-ocr {pdf_file_name}"
        result = subprocess.run(cmd, shell=True, capture_output=True, text=True)

        if result.returncode == 0:
            json_file = os.path.splitext(pdf_file_name)[0] + ".json"
            print("Document processed successfully! Output saved to file:", json_file)
            return json_file
        else:
            return f"Error processing document: {result.stderr}"
    except Exception as e:
        return f"Exception occurred: {str(e)}"

Search JSON for next text node

This tool searches a JSON file for a specific key-value pair and returns the next node with a "text" key.

class Find_Next_Text_NodeInput(BaseModel):
    """Input schema for the JSON search tool."""
    file_name_json: str = Field(..., description="Name of the JSON file to parse")
    value: str = Field(..., description="The value to search for in the JSON file")
class Find_Next_Text_Node(BaseTool):
    name: str = "find_next_text_node"
    description: str = "Searches a JSON file for a key-value pair and returns the next node with key 'text'"
    args_schema: Type[BaseModel] = Find_Next_Text_NodeInput

    def _run(self, file_name_json: str, value: str) -> str:
        print("File name JSON:", file_name_json)

        with open(file_name_json, "r") as file:
            data = json.load(file)

        next_text_node = self.find_next_text_node_func(data, 'text', value)
        return f"The value of {value} is {next_text_node['text']}"
    def find_next_text_node_func(self, json_data, target_key, target_value):
        """
        Searches for a matching key-value pair and returns the next node with the key 'text'.
        """
        found_target = False

        def recursive_search(node):
            nonlocal found_target

            if isinstance(node, dict):
                for key, value in node.items():
                    if key == target_key and value == target_value:
                        found_target = True
                    elif key == "text" and found_target:
                        return node
                    elif isinstance(value, (dict, list)):
                        result = recursive_search(value)
                        if result:
                            return result

            elif isinstance(node, list):
                for item in node:
                    result = recursive_search(item)
                    if result:
                        return result

            return None

        return recursive_search(json_data)

YAML configuration for Agent setup

This YAML setup makes it easier to manage and maintain agent roles and goals.

docling_agent:
  role: >
    Document Processing Specialist
  goal: >
    Extract, process, and structure documents using IBM Docling.
  backstory: >
    An AI-powered assistant that helps process documents for AI applications.

JSON_data_extractor:
  role: >
    Data Extraction Specialist
  goal: >
    Extract the value of the input field from the JSON file.
  backstory: >
    You are a data extraction specialist trained to accurately parse and retrieve
    key-value pairs from structured JSON data. You ensure precise and relevant
    outputs for data analysis, reporting, or automation tasks.

Reference YAML files

For information about recommended YAML configurations, see:

Chronic Kidney Disease AI Agent workflow

This workflow shows how AI agents can help analyze biomarker tests to manage CKD. Key steps include:

  • Comprehensive biomarker analysis

  • Early detection and risk assessment

  • Disease stage classification

  • Personalized treatment suggestions

  • Scalable and adaptable framework for various use cases

Chronic Kidney Disease AI Agent workflow

Agentic workflow for blood report analysis

This workflow uses Docling to read scanned blood test reports, CrewAI to manage agents, and IBM Watsonx as the language model to guide agent interactions.

The input reader agent (Docling + CrewAI) converts the scanned blood report into JSON format. Key parameters from the report are then extracted and passed to a trained prediction model to classify the case as CKD or non-CKD.

The following steps outline how this agent-based system supports early detection of chronic kidney disease using blood analysis.

blood report analysis agentic workflow

Key steps in the blood report Agent workflow

  1. Blood report ingestion agent: Collects and digitizes routine blood test reports from labs or hospital systems.

  2. Key parameter extraction Agent: Automatically extracts important biomarkers such as:

    • Serum Creatinine

    • Blood Urea Nitrogen (BUN)

    • Albumin

    • eGFR (calculated)

  3. Contextual data Agent: Adds patient details like age, sex, and medical history to support accurate interpretation.

  4. Prediction Agent: Uses a trained AI model to classify the case as CKD or non-CKD based on the extracted data.

Initialize CrewAI tools and language model

Use the following code to import necessary components and set up tools for agent interaction:

from crewai import Agent, Crew, Process, Task, LLM
from crewai.project import CrewBase, agent, crew, task
from ckd_v3.tools.custom_tool import DoclingTool
from ckd_v3.tools.custom_tool import Find_Next_Text_Node

Refer to the Crew Class with Decorators documentation for setup examples.

Next, configure the IBM Watsonx language model. This use case uses the watsonx/meta-llama/llama-3-1-70b-instruct model:

llm = LLM(
    model="watsonx/meta-llama/llama-3-1-70b-instruct",
    base_url="<your-watsonx-base-url>",
    api_key="<your-watsonx-api-key>"
)

Define the CKD Crew and Docling Agent

This example sets up the CkdV3 crew and a document processing agent using configurations from YAML files.

@CrewBase
class CkdV3():
    """CKD crew setup"""
    agents_config = 'config/agents.yaml'
    tasks_config = 'config/tasks.yaml'

    def docling_agent(self) -> Agent:
        return Agent(
            config=self.agents_config['docling_agent'],
            verbose=True,
            tools=[DoclingTool()],
            llm=llm,
        )

Define tasks for PDF and JSON parsing

These tasks handle the parsing of a PDF file and the resulting JSON data.

@task
def parse_pdf(self) -> Task:
    return Task(
        config=self.tasks_config['parse_pdf'],
    )

def parse_json(self) -> Task:
    return Task(
        config=self.tasks_config['parse_json'],
        context=[self.parse_pdf()]
    )

Create the CKD Crew

This function sets up the CKD crew with defined agents and tasks.

@crew
def crew(self) -> Crew:
    return Crew(
        agents=self.agents,
        tasks=self.tasks,
        process=Process.sequential,
        verbose=True,
    )

Build a machine learning model for CKD prediction

To predict Chronic Kidney Disease (CKD), we the following dataset from Kaggle: Chronic Kidney Disease

We trained a predictive model using this dataset and hosted it on watsonx.ai in IBM Cloud.

To download the dataset:

  • Go to the dataset page.

  • Click the Data tab.

  • Download the required files manually.

Reference for building and deploying the model: IBM Cloud tutorial for ML model lifecycle.

Prediction input and output details

Model input:

  • Fields: Example fields include bp (blood pressure), sg (specific gravity), pc (pus cell), and others

  • Values: Corresponding values for each input field

Model output:

  • Prediction: 0 indicates CKD is present, 1 means CKD is not present

  • Probability: Shows how confident the model is about the prediction

Steps to get a prediction:

  1. Authentication: Use your API key to get an authentication token

  2. Scoring: Send the input fields and values along with the token to get the prediction

The following examples show how to use cURL to send requests to the model. The token is stored in the IAM_TOKEN environment variable.

Example 1: Two inputs with CKD detected

Request: The following curl command sends two patient records for prediction. Both are expected to be classified as having CKD.

curl -X POST \
  --header "Content-Type: application/json" \
  --header "Accept: application/json" \
  --header "Authorization: Bearer $IAM_TOKEN" \
  -d '{
    "input_data": [
      {
        "fields": [
          "age", "blood_pressure", "specific_gravity", "albumin", "sugar",
          "red_blood_cells", "pus_cell", "pus_cell_clumps", "bacteria",
          "blood_glucose_random", "blood_urea", "serum_creatinine", "sodium",
          "potassium", "haemoglobin", "packed_cell_volume",
          "white_blood_cell_count", "red_blood_cell_count", "hypertension",
          "diabetes_mellitus", "coronary_artery_disease", "appetite",
          "peda_edema", "aanemia"
        ],
        "values": [
          [
            "48.0", "70.0", "1.005", "4.0", "0.0", 1, 0, 1, 0,
            "117.0", "56.0", "3.8", "111.0", "2.5", "11.2", "32",
            "6700", "3.9", 1, 0, 0, 1, 1, 1
          ],
          [
            "63.0", "70.0", "1.01", "3.0", "0.0", 0, 0, 1, 0,
            "380.0", "60.0", "2.7", "131.0", "4.2", "10.8", "32",
            "4500", "3.8", 1, 1, 0, 1, 1, 0
          ]
        ]
      }
    ]
  }' <API endpoint>

Response:

{
  "predictions": [
    {
      "fields": ["prediction", "probability"],
      "values": [
        [0, [0.9997, 0.0003]],
        [0, [1.0, 0.0]]
      ]
    }
  ]
}

Note: A prediction value of 0 means CKD is present.

Example 2: One CKD positive and One CKD negative input

Request: This curl command submits two patient records. The first is predicted as CKD (positive), the second as non-CKD (negative).

curl -X POST \
  --header "Content-Type: application/json" \
  --header "Accept: application/json" \
  --header "Authorization: Bearer $IAM_TOKEN" \
  -d '{
    "input_data": [
      {
        "fields": [
          "age", "blood_pressure", "specific_gravity", "albumin", "sugar",
          "red_blood_cells", "pus_cell", "pus_cell_clumps", "bacteria",
          "blood_glucose_random", "blood_urea", "serum_creatinine", "sodium",
          "potassium", "haemoglobin", "packed_cell_volume",
          "white_blood_cell_count", "red_blood_cell_count", "hypertension",
          "diabetes_mellitus", "coronary_artery_disease", "appetite",
          "peda_edema", "aanemia"
        ],
        "values": [
          [
            "48.0", "70.0", "1.005", "4.0", "0.0", 1, 0, 1, 0,
            "117.0", "56.0", "3.8", "111.0", "2.5", "11.2", "32",
            "6700", "3.9", 1, 0, 0, 1, 1, 1
          ],
          [
            "40.0", "80.0", "1.025", "0.0", "0.0", 1, 1, 0, 0,
            "140.0", "10.0", "1.2", "135.0", "5.0", "15.0", "48",
            "10400", "4.5", 0, 0, 0, 0, 0, 0
          ]
        ]
      }
    ]
  }' <API endpoint>

Response:

{
  "predictions": [
    {
      "fields": ["prediction", "probability"],
      "values": [
        [0, [0.9997, 0.0003]],
        [1, [0.0755, 0.9245]]
      ]
    }
  ]
}

Note:

  • prediction: 0 indicates CKD is present

  • prediction: 1 indicates is not present

  • The probabilities indicate the model's confidence level for each prediction.

Summary

This article illustrated how to build an AI-powered system to detect Chronic Kidney Disease (CKD) early using scanned blood reports. It uses Docling to extract data from PDF files and converts them to structured JSON format. CrewAI agents process the extracted data, identify key biomarkers, and prepare it for prediction. A machine learning model hosted on IBM watsonx.ai classifies the inputs as CKD or non-CKD. The model uses patient parameters like creatinine levels, blood pressure, and albumin levels for prediction. Examples show how to send inputs and interpret the prediction results.

Acknowledgments

This article was created as part of the IBM Open Innovation Community Initiative:

Better Together: Better Together Data and AI – AI Assistants Healthcare

We extend our sincere thanks to our colleagues and leadership teams for their guidance, mentorship, and support throughout this journey—from early experimentation to successful validation of the concepts and technologies.

IBM Professionals:

Ahmad Azraq, Vishnu Kambampati, Mekki MacAulay

Red Hat Professionals:

Thalia Hooker, Kris Verlaenen

Source code

The source code is published in GitHub repo. Note that this is a proof-of-concept (PoC) implementation and not production-ready. It has been tested on Windows 11 but can be adapted to other platforms.

At a high-level, these are the steps to run the solution:

  1. Clone the repository:

    git clone https://github.com/IBM/i-oic-better-together-data-ai-healthcare

  2. In the tools/ckd_v3 folder, find the custom tool configuration, including Docling integration for PDF parsing.

  3. The yaml folder contains YAML files defining agents and task configurations.

  4. From the local folder, run main.py to execute the agentic workflow locally. Change the input data as needed.