Skip to content

5.1 Explore the API Codebase

In this step, you will review the backend API code and structure. You can create powerful and efficient AI copilots that streamline complex workflows by utilizing Python's versatile programming capabilities and Azure Database for PostgreSQL's vector search functionality. The copilot's backend API enriches its abilities to handle intricate data, provide real-time insights, and connect seamlessly with diverse services, making interactions more dynamic and informative.

API Implementation

The Woodgrove Bank API is built using the FastAPI Python library. FastAPI is a modern, high-performance web framework designed to enable you to build APIs with Python based on standard Python type hints. By decoupling the copilot UI from the backend using this approach, you ensure greater flexibility, maintainability, and scalability, allowing the copilot's capabilities to evolve independently from the UI.

The entry point of the FastAPI application is implemented in the src/api/app/main.py file. Open it now in Visual Studio Code and explore the code in sections. You can also expand the section below to see the code inline and review explanations for each line of code.

FASTAPI application code
src/api/app/main.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
from dotenv import load_dotenv
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from app.lifespan_manager import lifespan
from app.routers import (
    completions,
    deliverables,
    documents,
    embeddings,
    invoices,
    invoice_line_items,
    milestones,
    sows,
    status,
    statuses,
    validation,
    validation_results,
    vendors,
    webhooks
)

# Load environment variables from the .env file
load_dotenv()

# Instantiate the FastAPI app
app = FastAPI(
    lifespan=lifespan,
    title="Woodgrove Bank API",
    summary="Woodgrove Bank API for the Build Your Own Copilot with Azure Database for PostgreSQL Solution Accelerator",
    version="1.0.0",
    docs_url="/swagger",
    openapi_url="/swagger/v1/swagger.json"
)

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# Add routers to API endpoints
app.include_router(deliverables.router)
app.include_router(documents.router)
app.include_router(embeddings.router)
app.include_router(invoices.router)
app.include_router(invoice_line_items.router)
app.include_router(milestones.router)
app.include_router(sows.router)
app.include_router(status.router)
app.include_router(statuses.router)
app.include_router(validation.router)
app.include_router(validation_results.router)
app.include_router(vendors.router)
app.include_router(webhooks.router)

@app.get("/")
async def get():
    """API welcome message."""
    return {"message": "Welcome to the Woodgrove Bank API!"}

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="127.0.0.1", port=8000, log_level="info")
  1. Import libraries (lines 1-20): Required classes and functions are imported from various libraries.

  2. Load environment variables (line 23). The load_dotenv() function imports environment variable values stored in the .env file you created in the src\api\app folder.

  3. Instantiate the FastAPI app server (lines 26-33). The FastAPI application server is created, and some basic configuration settings are applied.

  4. Assign lifespan manager (line 27): The lifespan manager, defined in src\api\app\lifespan_manager.py manages the lifespan of objects used throughout the life of the app, such as database connection pools and the Azure OpenAI chat client. This approach ensures objects can be shared across sessions, creating them when the app starts and destroying them gracefully when it shuts down. Dependency injection is used to access lifespan-managed objects from the API endpoint code on routers.
  5. Assign Swagger metadata (lines 28-32): Various metadata fields add context to the API's exposed Swagger UI. The docs_url value changes the API's default documentation page from /docs to the more commonly used /swagger.

  6. Add endpoint routers (lines 44-56): In FastAPI, routers are components that help organize API code, grouping related endpoints and enabling modular route definitions for better maintainability and scalability of your application.

  7. Define a default route (lines 58-61): The "/" route maps to the application server's base URL.

    • It accepts GET requests without parameters (equivalent to a browser site visit).
    • It returns a JSON response with a "Welcome to the Woodgrove Bank API" message.
    • This serves as a "health check" for the app server, verifying it is alive (e.g., during setup).

Organize Code using Routers

When building more extensive applications, routers allow API endpoint code to be split across multiple files, providing a convenient tool for structuring your application. FastAPI's APIRouter class allows path operations to be maintained in a dedicated code file, isolated from other paths and logic.

For example, the Woodgrove Bank API contains a file dedicated to handling just vendors. This file, the submodule at api/app/routers/vendors.py, contains all the path operations related to vendors. The router separates vendor-specific logic from the rest of the application code. The router is connected to the FastAPI application using the APIRouter class.

Expand this block to view the Vendors router code
src/api/app/routers/vendors.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
from fastapi import APIRouter, Depends, HTTPException
from pydantic import parse_obj_as

# Initialize the router
router = APIRouter(
    prefix = "/vendors",
    tags = ["Vendors"],
    dependencies = [Depends(get_db_connection_pool)],
    responses = {404: {"description": "Not found"}}
)

@router.get('/', response_model = ListResponse[Vendor])
async def list_vendors(skip: int = 0, limit: int = 10, sortby: str = None, pool = Depends(get_db_connection_pool)):
    """Retrieves a list of vendors from the database."""
    async with pool.acquire() as conn:
        orderby = 'id'
        if (sortby):
            orderby = sortby

        if limit == -1:
            rows = await conn.fetch('SELECT * FROM vendors ORDER BY $1;', orderby)
        else:
            rows = await conn.fetch('SELECT * FROM vendors ORDER BY $1 LIMIT $2 OFFSET $3;', orderby, limit, skip)

        vendors = parse_obj_as(list[Vendor], [dict(row) for row in rows])

        total = await conn.fetchval('SELECT COUNT(*) FROM vendors;')

    if (limit == -1):
        limit = total

    return ListResponse[Vendor](data = vendors, total = len(vendors), skip = 0, limit = len(vendors))

@router.get('/{id:int}', response_model = Vendor)
async def get_by_id(id: int, pool = Depends(get_db_connection_pool)):
    """Retrieves a vendor by ID from the database."""
    async with pool.acquire() as conn:
        row = await conn.fetchrow('SELECT * FROM vendors WHERE id = $1;', id)
        if row is None:
            raise HTTPException(status_code=404, detail=f'A vendor with an id of {id} was not found.')
        vendor = parse_obj_as(Vendor, dict(row))
    return vendor

@router.get('/type/{type}', response_model = list[Vendor])
async def get_by_type(type: str, pool = Depends(get_db_connection_pool)):
    """Retrieves vendors of the specified type from the database."""
    async with pool.acquire() as conn:
        rows = await conn.fetch('SELECT * FROM vendors WHERE LOWER(type) = $1;', type.lower())
        if not rows or len(rows) == 0:
            raise HTTPException(status_code=404, detail=f'No vendors with a type of "{type}" were found.')
        vendors = parse_obj_as(list[Vendor], [dict(row) for row in rows])
    return vendors
  1. Define the router (lines 5-10): The APIRouter initialization.

    • The prefix allows you to specify the path prefix of all endpoints within the router. In this case, it is /vendors.
    • Setting tags allows the endpoints within the router to be grouped by a friendly name in the Swagger UI.
    • The dependencies array defines any dependencies injected into every endpoint request.
    • The responses object allows you to dictate the types of responses from the API endpoints defined within the router.
  2. Define the get vendors route (lines 12-32). The API's /vendors/ route maps to an endpoint for retrieving the list of vendors from the database.

    • It accepts GET requests from clients and extracts optional parameters.
    • It uses the injected database connection pool and invokes a SELECT query against the database to retrieve vendor records.
    • It returns a list of vendors.
  3. Define additional get routes (lines 34-52). The /vendors/{id:int} and vendors/type/{type} routes provide GET request endpoints for getting individual vendors by ID or a list of vendors by type.


Congratulations! You just reviewed the FastAPI application structure!