← Back to list

Building a Doctor Appointment Assistant with OpenAI Function Calling (Python)

In this post, I’ll walk through a fully functional medical appointment assistant built using Python and OpenAI’s Responses API. This…

Saipriya Damarapati · 2026-04-03 17:41 · 1 claps · 3.9 min read
#chatbots #conversational-ai-chatbot #llm #openai-function-calling #responses-api
Open on Medium ↗
Wiki topics: LLM · Large Language Models CLI · Clinical Medicine

Building a Doctor Appointment Assistant with OpenAI Function Calling (Python)

In this post, I’ll walk through a fully functional medical appointment assistant built using Python and OpenAI’s Responses API. This implementation goes beyond a simple chatbot — it demonstrates structured tool calling, backend simulation, and controlled conversational flow.

If you’re exploring LLM-powered assistants with real-world workflows, this is a great reference architecture.

What This System Does

The assistant helps users:

  • Browse doctor specializations
  • View available doctors
  • Check appointment slots
  • Book and cancel appointments

All interactions are strictly structured in JSON, making the system predictable and production friendly.

Key Components of the Code

1. Mocked Backend Layer

The system simulates a backend using Python functions:

  • list_specializations() → Returns available specializations
  • list_doctors() → Returns doctor directory
  • get_current_date() → Provides system date context
  • get_slots_by_doctor() → Fetches available slots
  • book_slot() / cancel_slot() → Manages appointments

This abstraction allows the LLM to behave like it’s interacting with a real API.

2. Tool Definitions (Function Calling)

Each backend capability is exposed as a tool. These tools are passed to the model so that it can decide when a function needs to be called, extract the required structured arguments from the user’s input, and trigger the corresponding backend logic to complete the requested action.

tools = [
  tools_book_doctor_appointment,
  tools_list_doctors,
  tools_list_specializations,
  tools_get_current_date,
  tools_get_available_slots
]

3. System Prompt (The Brain)

The system prompt is carefully designed to enforce strict JSON responses and ensure deterministic workflows throughout the interaction. It also requires the assistant to explicitly ask for clarification whenever there is any ambiguity, and prevents it from making assumptions, especially when it comes to handling dates.

The references of the key components are already present in my previous post, towards the end of the post. The same samples used in playground environment were used in this python implementation also.

Core Execution Flow

The conversation loop is handled by three key functions:

1. call_responses_api

Handles communication with the OpenAI API

@retry(wait=wait_random_exponential(multiplier=1, max=40), stop=stop_after_attempt(3))
def call_responses_api(messages, tools=None, tool_choice=None, model=GPT_MODEL):
    try:
        response = client.responses.create(
            model=model,
            input=messages,
            tools=tools,
            tool_choice=tool_choice,
        )
        return response
    except Exception as e:
        print("Unable to generate response from responses API")
        print(f"Exception: {e}")
        return e

2. handle_chat_response

Processes the model output by first detecting whether any function calls have been made, then executing the corresponding backend functions based on those calls, and finally appending the results back into the conversation so that the model can continue the interaction with the updated context.

def handle_chat_response(chat_response):
    """
    Process chat_response output items and append results to messages.
    """
    output_messages = []
    for item in chat_response.output:
        if item.type == "function_call":
            output_messages.append({
                "type": item.type,
                "name": item.name,
                "call_id": item.call_id,
                "arguments": item.arguments
            })
            if item.name == "get_current_date":
                output_data = get_current_date()
            elif item.name == "list_specializations":
                output_data = list_specializations()
            elif item.name == "list_doctors":
                output_data = list_doctors()
            elif item.name == "book_doctor_appointment":
                args = json.loads(item.arguments)
                appointment_id = args['appt_id']
                patient_name = args['patient_name']
                output_data = book_slot(appointment_id, patient_name)
            elif item.name == "get_available_slots":
                args = json.loads(item.arguments)
                doctorNum = args['doctorRegNo']
                date = args['date']
                output_data = get_slots_by_doctor(doctorNum, date)
            else:
                output_data = {"error": f"Unknown function {item.name}"}

            # Append as assistant message with function result
            output_messages.append({
                "type": "function_call_output",
                "call_id": item.call_id,
                "output": json.dumps(output_data, indent=2)
            })

        elif item.type == "message":
            output_messages.append({
                "role": "assistant",
                "content": [
                    {
                        "type": "output_text",
                        "text": item.content[0].text
                    }
                ]
            })
            pretty_print_conversation(output_messages)

            # Handle other types if needed
            user_input = input("user_input; type quit to end the conversation")
            user_messages = []
            user_messages.append({
                "role": "user",
                "content": [
                    {
                        "type": "input_text",
                        "text": user_input
                    }
                ]
            })
            pretty_print_conversation(user_messages)
            output_messages.extend(user_messages)

    return output_messages  # return the last appended message

3. run_conversation

Controls the iterative loop

def run_conversation(messages, tools, max_iterations=10):
    """
    Iteratively call call_responses_api and handle_chat_response
    until no new messages are added or max_iterations is reached.
    """
    pretty_print_conversation(messages)
    for _ in range(max_iterations):
        prev_len = len(messages)

        # Call the model
        chat_response = call_responses_api(messages, tools=tools)

        # Handle the response (append tool outputs, assistant replies, etc.)
        output_messages = handle_chat_response(chat_response)
        messages.extend(output_messages)

        if len(messages) == prev_len:
            # No new messages added → stop
            break

    return messages

Entry Point

messages = []

messages.append({
  "role": "system",
  "content": [
    {
      "type": "input_text",
      "text": system_message
    }
  ]
})

messages.append({
  "role": "user",
  "content": [
    {
      "type": "input_text",
      "text": "Hi"
    }
  ]
})

final_messages = run_conversation(messages, tools, 20)

Important Insight: The Cost of Stateless Conversations

Here’s the key takeaway from this implementation: every API call sends the entire conversation history to the model. In practice, this means that all previous messages, along with every tool call and each corresponding tool response, are repeatedly passed back to the LLM on every interaction.

This approach introduces a few important challenges. The token cost increases rapidly as the conversation grows, since more context needs to be transmitted each time. At the same time, latency also increases because the model has to process a larger input on every request. Over time, this makes the system harder to scale efficiently, especially for longer or more complex conversations.

Why It Happens

The OpenAI API in this setup is stateless, which means it does not retain any memory of previous interactions. To maintain context across the conversation, the entire message history must be explicitly resent with each request. This is done by extending the existing messages with new outputs using messages.extend(output_messages) and then passing the updated messages again in the next API call through call_responses_api(messages, tools=tools).

As a result, the model only knows what you include in the current request and has no inherent memory unless the full conversation context is provided every single time.

Final Thoughts

This implementation is a great example of how LLMs can be transformed into structured assistants, how function calling can be used effectively, and how predictable AI workflows can be designed. At the same time, it also highlights a real-world challenge that arises when building such systems.

Plain chatbot architectures become expensive and inefficient as conversations grow.

What’s Next?

To explore ways to maintain a memory of past interactions so that only the latest user message needs to be sent to the LLM, rather than repeatedly passing the entire conversation as tokens.


메타데이터
post_id
dcc51fcc7230
slug
building-a-doctor-appointment-assistant-with-openai-function-calling-python-dcc51fcc7230
url
https://medium.com/@saipriya.evolving/building-a-doctor-appointment-assistant-with-openai-function-calling-python-dcc51fcc7230
canonical_url
https://medium.com/@saipriya.evolving/building-a-doctor-appointment-assistant-with-openai-function-calling-python-dcc51fcc7230
author_url
https://medium.com/@saipriya.evolving
status
ok
fetched_at
2026-06-23 17:05:31