Skip to content

FastAPI#

The FastAPIAdapter allows you to expose your FastAgency workflows as a REST API using the FastAPI framework.

Use Cases#

When to Use the FastAPIAdapter:

  • Custom Client Applications: If you want to build your own client applications in a different language, (e.g., HTML/JavaScript, Go, Java, etc.), that interacts with your FastAgency workflows using REST API and WebSockets.

  • Moderate User Demand: This adapter is a good fit for scenarios where workflows need to be executed by multiple workers to efficiently handle higher machine load.

  • Simplified Production Setup: This adapter is a good choice when you need a simple and easy-to-manage setup for deploying FastAgency workflows as an ASGI server in production.

Architecture Overview#

This section provides high-level architecture diagrams for the two available setups of using FastAPI with:

Mesop FastAPI App

The system consists of two main components:

Mesop Client App#

The client App serves as the frontend interface for the system. It includes:

  • MesopUI: A friendly web interface for users to interact with the workflows. It facilitates the communication with the user and the FastAPIProvider.
  • FastAPIProvider: A component that facilitates communication between the Mesop client and the FastAPIAdapter.

This app handles all client interactions and presents the results back to the user.

FastAPI App#

The FastAPI App forms the backend of our system and consists of:

  • AutoGen Workflows: These define the core logic and behavior of our application, utilizing agents to perform various tasks and achieve specific goals.
  • The FastAPIAdapter: This component communicates with AutoGen, and implements routes and websocket for FastAPI.
  • FastAPI: Provides the infrastructure for building and exposing AutoGen workflows via REST API.

Custom FastAPI App

The system consists of two main components:

Custom Client App#

This application serves as the frontend interface for the system. It includes:

  • Custom Client: A client application built in a different language, (e.g., HTML/JavaScript, Go, Java, etc.) facilitates communication between the user and the FastAPIAdapter.

This application handles all interactions with the FastAPIAdapter and presents the results back to the user.

FastAPI App#

The FastAPI App forms the backend of our system and consists of:

  • AutoGen Workflows: These define the core logic and behavior of our application, utilizing agents to perform various tasks and achieve specific goals.
  • The FastAPIAdapter: This component communicates with AutoGen, and implements routes and websocket for FastAPI.
  • FastAPI: Provides the infrastructure for building and exposing AutoGen workflows via REST API.

Building Custom Client Applications#

To write a custom application that interacts with FastAPIAdapter, it's essential to first understand the available server routes it provides and their purposes. This knowledge forms the foundation of the client-server interaction model.

Available API Endpoints#

FastAPIAdapter provides three primary endpoints for building client applications:

Route Method Purpose
/fastagency/discovery GET Lists the available workflows that can be initiated
/fastagency/initiate_workflow POST Starts a new workflow instance for the chosen workflow
/fastagency/ws WebSocket Handles real-time workflow communication

Now that we understand the available routes, let's visualize how these components interact in a typical client-server communication flow.

System Interaction Flow#

The following sequence diagram illustrates the step-by-step process of how a custom client application interacts with the FastAPIAdapter:

sequenceDiagram
participant Client as Custom Client Application
participant FastAPI as FastAPIAdapter

Note over Client,FastAPI: 1. Setup & Discovery Phase
Client->>FastAPI: GET /fastagency/discovery
FastAPI-->>Client: Available Workflows

Client->>Client: Display workflow options to user

Note over Client,FastAPI: 2. Workflow Initiation
Client->>FastAPI: POST /fastagency/initiate_workflow
FastAPI-->>Client: Workflow Configuration

Note over Client,FastAPI: 3. WebSocket Connection
Client->>FastAPI: Initiate a WebSocket Connection (/fastagency/ws)
FastAPI-->>Client: WebSocket Connection Established

Note over Client,FastAPI: 4. Real-time Communication
Client->>FastAPI: Send Initial WebSocket Message
FastAPI-->>Client: Acknowledge Connection

activate FastAPI
activate Client
Note right of FastAPI: Message Processing Loop
FastAPI->>Client: Send Workflow Message
Client->>FastAPI: Send Response If Required
FastAPI->>Client: Send Next Workflow Message
deactivate Client
deactivate FastAPI

To better understand this diagram, let's break down the key steps involved in the client-server interaction:

Understanding the Flow#

The interaction between client and server follows these key steps:

  • Discovery: Client fetches available workflows from the server.
  • Selection: User selects a workflow to execute.
  • Initiation: Client requests to start the chosen workflow.
  • Connection: WebSocket connection established for real-time communication. These includes:
    • Server sending workflow message to the client.
    • Client sends optional response to server if previous server message requires user input.
    • Server processes and sends the next workflow message.
Message Types#

Before diving into the implementation, we need to learn a bit about the message types that FastAPIAdapter provides. Understanding these will help us handle messages in our custom client and display them properly to the users.

FastAgency tags each message sent from the server to the client over WebSockets with a type attribute. This helps the client differentiate between different types of messages and handle them accordingly. Let’s break them down into two categories:

Messages for Display:

  • text_message: A basic text message from the server, intended for display to the user. It doesn’t require any action from the user, serving purely as information or status updates.
  • workflow_started: This message indicates the start of a new workflow. The message includes the workflow’s name and a description along with other details.
  • workflow_completed: This message signals that the current workflow has been successfully completed. The client can use this to notify the user or transition to the next step in the application.
  • suggested_function_call: Indicates that the LLM has suggested a function call.
  • function_call_execution: Indicates that the LLM has executed the suggested function call.
  • error: Indicates that an error occurred during the workflow. The client can handle this by displaying an error message or prompting the user to retry the action.

Messages That Require User Response:

  • text_input: This message prompts the client to gather input from the user. It could be a question or request for data. The client should provide a way for the user to respond (e.g., a text input or text area) and then send the response back to the server.
  • multiple_choice: This message requires the user to choose from a predefined set of options provided by the LLM. The client should present these options (e.g., checkboxes or radio buttons) and submit the user’s selection back to the server.

A full list of message types and their detailed usage will be available soon in the FastAgency Adapter’s OpenAPI specification—stay tuned!

Implementation Guide#

In the following sections, we'll walk through the process of creating a custom client application that implements the flow we've just described. We'll build a simple web-based client that demonstrates how to interact with FastAPIAdapter effectively.

Our implementation will cover these key aspects:

  • Fetching and displaying available workflows.
  • Handling workflow initiation.
  • Managing WebSockets connection.
  • Processing real-time messages.

Note

Before we examine the code:

  • The below example uses a simple HTML with JavaScript, all in a single string and served directly from the FastAPI App for simplicity.
  • This approach is not suitable for production but ideal for demonstrating core concepts.
  • In a real-world scenario, you'd use a separate frontend, built with frameworks like React or Vue.js, or other languages such as Java, Go, or Ruby, based on your project needs.

Let's begin by looking at the code structure and then break down each component.

Installation#

We strongly recommend using Cookiecutter for setting up the project. Cookiecutter creates the project folder structure, default workflow, automatically installs all the necessary requirements, and creates a devcontainer that can be used with Visual Studio Code.

You can setup the project using Cookiecutter by following the project setup guide.

Alternatively, you can use pip + venv.

Before getting started, ensure that FastAgency is installed with support for the AutoGen runtime, along with the mesop, fastapi, and server submodules by running the following command:

pip install "fastagency[autogen,mesop,fastapi,server]"

This command installs FastAgency with support for both the mesop and console interfaces for AutoGen workflows, but with FastAPI serving input requests and running workflows.

Before getting started, ensure that FastAgency is installed with support for the AutoGen runtime, along with the fastapi, and server submodules by running the following command:

pip install "fastagency[autogen,fastapi,server]"

This command installs FastAgency, but with FastAPI serving input requests and running workflows.

Example: Student and Teacher Learning Chat#

In this example, we'll create a simple learning chatbot where a student agent asks questions and a teacher agent responds, simulating a learning environment. We'll use MesopUI for the web interface and the FastAPIAdapter to expose the workflow as a REST API.

In this example, we'll create a simple learning chatbot where a student agent asks questions and a teacher agent responds, simulating a learning environment. We'll create a custom client using HTML and JavaScript for the web interface and the FastAPIAdapter to expose the workflow as a REST API.

Step-by-Step Breakdown#

1. Define Workflow#

To get started, define the workflow that your application will use. This is where you specify how the agents interact and what they do. Here's a simple example of a workflow definition:

import os
from typing import Any

from autogen.agentchat import ConversableAgent
from fastagency import UI
from fastagency.runtimes.autogen import AutoGenWorkflows

llm_config = {
    "config_list": [
        {
            "model": "gpt-4o-mini",
            "api_key": os.getenv("OPENAI_API_KEY"),
        }
    ],
    "temperature": 0.8,
}

wf = AutoGenWorkflows()


@wf.register(name="simple_learning", description="Student and teacher learning chat")  # type: ignore[misc]
def simple_workflow(ui: UI, params: dict[str, Any]) -> str:
    initial_message = ui.text_input(
        sender="Workflow",
        recipient="User",
        prompt="I can help you learn about mathematics. What subject you would like to explore?",
    )

    student_agent = ConversableAgent(
        name="Student_Agent",
        system_message="You are a student willing to learn.",
        llm_config=llm_config,
    )
    teacher_agent = ConversableAgent(
        name="Teacher_Agent",
        system_message="You are a math teacher.",
        llm_config=llm_config,
    )

    chat_result = student_agent.initiate_chat(
        teacher_agent,
        message=initial_message,
        summary_method="reflection_with_llm",
        max_turns=3,
    )

    return str(chat_result.summary)

2. Import Required Modules#

Next, import the required modules from the FastAgency. Import the FastAPIAdapter class to expose the workflows as a REST API.

from typing import Any

from fastagency.adapters.fastapi import FastAPIAdapter
from fastapi import FastAPI

Next, import the required modules from the FastAgency and AutoGen. These imports provide the essential building blocks for creating agents, workflows, and integrating with the custom client. Additionally, import the FastAPIAdapter and HTMLResponse class to expose the workflows as a REST API.

import os
from typing import Any

from autogen.agentchat import ConversableAgent
from fastapi import FastAPI
from fastapi.responses import HTMLResponse

from fastagency import UI
from fastagency.adapters.fastapi import FastAPIAdapter
from fastagency.runtimes.autogen import AutoGenWorkflows

3. Define FastAgency Application#

Create an instance of the FastAPIAdapter and pass your workflow to it. Then, include a router to the FastAPI application. The adapter will have all REST API and WebSocket routes for communicating with the client.

adapter = FastAPIAdapter(provider=wf)

app = FastAPI()
app.include_router(adapter.router)

4. Adapter Chaining#

Finally, create an additional specification file for an application using MesopUI to connect to the FastAPIAdapter.

main_2_mesop.py

from fastagency.adapters.fastapi import FastAPIAdapter
from fastagency.app import FastAgency
from fastagency.ui.mesop import MesopUI

fastapi_url = "http://localhost:8008"

provider = FastAPIAdapter.create_provider(
    fastapi_url=fastapi_url,
)
ui = MesopUI()


app = FastAgency(
    provider=provider,
    ui=ui,

4. Serving the Custom HTML Client#

Finally, use the HTML Response from FastAPI to serve the custom client code.

html = """
<!DOCTYPE html>
<html>
   <head>
      <title>FastAgency Chat App</title>
   </head>
   <body>
      <h1>FastAgency Chat App</h1>
      <div id="workflows"></div>
      <ul id="messages"></ul>
      <script>
         const API_URL = 'http://127.0.0.1:8008/fastagency';
         const WS_URL = 'ws://127.0.0.1:8008/fastagency/ws'; // nosemgrep
         let socket;

         async function fetchWorkflows() {
             const response = await fetch(`${API_URL}/discovery`);
             const workflows = await response.json();
             const container = document.getElementById('workflows');
             workflows.forEach(workflow => {
                 const button = document.createElement('button');
                 button.textContent = workflow.description;
                 button.onclick = () => startWorkflow(workflow.name);
                 container.appendChild(button);
             });
         }

         async function startWorkflow(name) {
             const payload = {
                 workflow_name: name,
                 workflow_uuid: generateUUID(),
                 user_id: null, // Set to null for single-user applications; otherwise, provide the appropriate user ID
                 params: {}
             };
             const response = await fetch(`${API_URL}/initiate_workflow`, {
                 method: 'POST',
                 headers: { 'Content-Type': 'application/json' },
                 body: JSON.stringify(payload)
             });
             const workflowJson = await response.json();
             connectWebSocket(workflowJson);
         }

         function connectWebSocket(workflowJson) {
             socket = new WebSocket(WS_URL);
             socket.onopen = () => {
                 const initMessage = {
                     name: workflowJson.name,
                     workflow_uuid: workflowJson.workflow_uuid,
                     user_id: workflowJson.user_id,
                     params: {}
                 };
                 socket.send(JSON.stringify(initMessage));
             };
             socket.onmessage = (event) => handleMessage(JSON.parse(event.data));
         }

         function handleMessage(message) {
             const messagesList = document.getElementById('messages');
             const li = document.createElement('li');
             if (message.type === 'text_input') {
                 const response = prompt(message.content.prompt);
                 socket.send(response);
                 li.textContent = `${message.sender} -> ${message.recipient}: ${message.content.prompt}`;
             } else {
                 li.textContent = `${message.sender} -> ${message.recipient}: ${message.content?.body || message?.type || JSON.stringify(message)}`;
             }
             messagesList.appendChild(li);
         }

         fetchWorkflows();

         // Helper function for generating UUID
         function generateUUID() {
            return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
                if (c === 'x') {
                return (Math.random() * 16 | 0).toString(16);
                } else {
                return (Math.random() * 16 | 0 & 0x3 | 0x8).toString(16);
                }
            });
         }
      </script>
   </body>
</html>
"""

@app.get("/")
async def get() -> HTMLResponse:
    return HTMLResponse(html)

Complete Application Code#

Please copy and paste the following code into the same folder, using the file names exactly as mentioned below.

workflow.py
import os
from typing import Any

from autogen.agentchat import ConversableAgent
from fastagency import UI
from fastagency.runtimes.autogen import AutoGenWorkflows

llm_config = {
    "config_list": [
        {
            "model": "gpt-4o-mini",
            "api_key": os.getenv("OPENAI_API_KEY"),
        }
    ],
    "temperature": 0.8,
}

wf = AutoGenWorkflows()


@wf.register(name="simple_learning", description="Student and teacher learning chat")  # type: ignore[misc]
def simple_workflow(ui: UI, params: dict[str, Any]) -> str:
    initial_message = ui.text_input(
        sender="Workflow",
        recipient="User",
        prompt="I can help you learn about mathematics. What subject you would like to explore?",
    )

    student_agent = ConversableAgent(
        name="Student_Agent",
        system_message="You are a student willing to learn.",
        llm_config=llm_config,
    )
    teacher_agent = ConversableAgent(
        name="Teacher_Agent",
        system_message="You are a math teacher.",
        llm_config=llm_config,
    )

    chat_result = student_agent.initiate_chat(
        teacher_agent,
        message=initial_message,
        summary_method="reflection_with_llm",
        max_turns=3,
    )

    return str(chat_result.summary)

main_1_fastapi.py
from typing import Any

from fastagency.adapters.fastapi import FastAPIAdapter
from fastapi import FastAPI

from ..workflow import wf

adapter = FastAPIAdapter(provider=wf)

app = FastAPI()
app.include_router(adapter.router)


# this is optional, but we would like to see the list of available workflows
@app.get("/")
def list_workflows() -> dict[str, Any]:
    return {"Workflows": {name: wf.get_description(name) for name in wf.names}}


# start the adapter with the following command
# uvicorn my_fastagency_app.deployment.main_1_fastapi:app --reload

main_2_mesop.py
from fastagency.adapters.fastapi import FastAPIAdapter
from fastagency.app import FastAgency
from fastagency.ui.mesop import MesopUI

fastapi_url = "http://localhost:8008"

provider = FastAPIAdapter.create_provider(
    fastapi_url=fastapi_url,
)
ui = MesopUI()


app = FastAgency(
    provider=provider,
    ui=ui,
    title="My FastAgency App",
)

# start the provider with the following command
# gunicorn my_fastagency_app.deployment.main_2_mesop:app -b 0.0.0.0:8888 --reload

main_fastapi_custom_client.py
import os
from typing import Any

from autogen.agentchat import ConversableAgent
from fastapi import FastAPI
from fastapi.responses import HTMLResponse

from fastagency import UI
from fastagency.adapters.fastapi import FastAPIAdapter
from fastagency.runtimes.autogen import AutoGenWorkflows

html = """
<!DOCTYPE html>
<html>
   <head>
      <title>FastAgency Chat App</title>
   </head>
   <body>
      <h1>FastAgency Chat App</h1>
      <div id="workflows"></div>
      <ul id="messages"></ul>
      <script>
         const API_URL = 'http://127.0.0.1:8008/fastagency';
         const WS_URL = 'ws://127.0.0.1:8008/fastagency/ws'; // nosemgrep
         let socket;

         async function fetchWorkflows() {
             const response = await fetch(`${API_URL}/discovery`);
             const workflows = await response.json();
             const container = document.getElementById('workflows');
             workflows.forEach(workflow => {
                 const button = document.createElement('button');
                 button.textContent = workflow.description;
                 button.onclick = () => startWorkflow(workflow.name);
                 container.appendChild(button);
             });
         }

         async function startWorkflow(name) {
             const payload = {
                 workflow_name: name,
                 workflow_uuid: generateUUID(),
                 user_id: null, // Set to null for single-user applications; otherwise, provide the appropriate user ID
                 params: {}
             };
             const response = await fetch(`${API_URL}/initiate_workflow`, {
                 method: 'POST',
                 headers: { 'Content-Type': 'application/json' },
                 body: JSON.stringify(payload)
             });
             const workflowJson = await response.json();
             connectWebSocket(workflowJson);
         }

         function connectWebSocket(workflowJson) {
             socket = new WebSocket(WS_URL);
             socket.onopen = () => {
                 const initMessage = {
                     name: workflowJson.name,
                     workflow_uuid: workflowJson.workflow_uuid,
                     user_id: workflowJson.user_id,
                     params: {}
                 };
                 socket.send(JSON.stringify(initMessage));
             };
             socket.onmessage = (event) => handleMessage(JSON.parse(event.data));
         }

         function handleMessage(message) {
             const messagesList = document.getElementById('messages');
             const li = document.createElement('li');
             if (message.type === 'text_input') {
                 const response = prompt(message.content.prompt);
                 socket.send(response);
                 li.textContent = `${message.sender} -> ${message.recipient}: ${message.content.prompt}`;
             } else {
                 li.textContent = `${message.sender} -> ${message.recipient}: ${message.content?.body || message?.type || JSON.stringify(message)}`;
             }
             messagesList.appendChild(li);
         }

         fetchWorkflows();

         // Helper function for generating UUID
         function generateUUID() {
            return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
                if (c === 'x') {
                return (Math.random() * 16 | 0).toString(16);
                } else {
                return (Math.random() * 16 | 0 & 0x3 | 0x8).toString(16);
                }
            });
         }
      </script>
   </body>
</html>
"""

llm_config = {
    "config_list": [
        {
            "model": "gpt-4o-mini",
            "api_key": os.getenv("OPENAI_API_KEY"),
        }
    ],
    "temperature": 0.8,
}

wf = AutoGenWorkflows()

@wf.register(name="simple_learning", description="Student and teacher learning chat")
def simple_workflow(ui: UI, params: dict[str, Any]) -> str:
    initial_message = ui.text_input(
        sender="Workflow",
        recipient="User",
        prompt="I can help you learn about mathematics. What subject you would like to explore?",
    )

    student_agent = ConversableAgent(
        name="Student_Agent",
        system_message="You are a student willing to learn.",
        llm_config=llm_config,
        # human_input_mode="ALWAYS",
    )
    teacher_agent = ConversableAgent(
        name="Teacher_Agent",
        system_message="You are a math teacher.",
        llm_config=llm_config,
        # human_input_mode="ALWAYS",
    )

    chat_result = student_agent.initiate_chat(
        teacher_agent,
        message=initial_message,
        summary_method="reflection_with_llm",
        max_turns=5,
    )

    return chat_result.summary  # type: ignore[no-any-return]

adapter = FastAPIAdapter(provider=wf)

app = FastAPI()
app.include_router(adapter.router)

@app.get("/")
async def get() -> HTMLResponse:
    return HTMLResponse(html)


# start the provider with the following command
# uvicorn main_fastapi_custom_client:app --port 8008 --reload

Run Application#

In this setup, we need to run two commands in separate terminal windows:

  • Start FastAPI application using uvicorn:

Terminal 1

uvicorn main_1_fastapi:app --host 0.0.0.0 --port 8008 --reload
  • Start Mesop web interface using gunicorn:

Terminal 2

gunicorn main_2_mesop:app -b 0.0.0.0:8888 --reload

First, install the package using package manager such as pip and then run it:

  • Start FastAPI application using uvicorn:

Terminal 1

pip install uvicorn
uvicorn main_1_fastapi:app --host 0.0.0.0 --port 8008 --reload
  • Start Mesop web interface using gunicorn:

Terminal 2

pip install gunicorn
gunicorn main_2_mesop:app -b 0.0.0.0:8888 --reload
  • Start FastAPI application using uvicorn:

Terminal 1

pip install uvicorn
uvicorn main_1_fastapi:app --host 0.0.0.0 --port 8008 --reload
  • Start Mesop web interface using waitress:

Terminal 2

pip install waitress
waitress-serve --listen=0.0.0.0:8888 main_2_mesop:app

Once everything is set up, you can run your FastAgency application using the following command.

  • Start FastAPI application using uvicorn:

Terminal 1

uvicorn main_fastapi_custom_client:app --port 8008 --reload

Output#

The outputs will vary based on the interface. Here is the output of the last terminal starting the UI:

[2024-10-10 13:19:18 +0530] [23635] [INFO] Starting gunicorn 23.0.0
[2024-10-10 13:19:18 +0530] [23635] [INFO] Listening at: http://127.0.0.1:8888 (23635)
[2024-10-10 13:19:18 +0530] [23635] [INFO] Using worker: sync
[2024-10-10 13:19:18 +0530] [23645] [INFO] Booting worker with pid: 23645

Output Screenshot

INFO:     Will watch for changes in these directories: ['/tmp/custom_fastapi_demo']
INFO:     Uvicorn running on http://0.0.0.0:8008 (Press CTRL+C to quit)
INFO:     Started reloader process [73937] using StatReload
INFO:     Started server process [73940]
INFO:     Waiting for application startup.
INFO:     Application startup complete.
Output Screenshot

The FastAPIAdapter provides a powerful solution for developers seeking a user-friendly and efficient way to expose their FastAgency workflows as REST API, contributing to building production-ready applications.