#Calling Dagger Modules in FastAPI

1 messages · Page 1 of 1 (latest)

weary plume
#

I have a FastAPI app and created a folder called dagger_modules. I used dagger init --sdk=python --name=hello-world to create a basic Dagger module that simply prints “Hello World”. I want to call this function from the /hello endpoint and have it run in the same way as when I execute dagger call hello-world from the CLI. The implementation below works, but I’m not sure if it’s correct—it feels more like a workaround. The cold start time is also significant, and preloading the python:3.11-slim image feels like another workaround.

`// DaggerFastAPIDemo/main.py
@app.get("/hello")
async def hello_world_module_endpoint():
"""Endpoint that executes the hello-world Dagger module"""
logger.info("/hello endpoint called. Using Dagger global client.")
try:
module_source_path = "./dagger_modules/src/hello_world"
logger.info(f"Loading Dagger module from: {os.path.abspath(module_source_path)}")
exec_command = get_module_execution_command("main", "HelloWorld", "hello_world ")
container = dag.container().from_("python:3.11-slim")
.with_exec(["pip", "install", "--upgrade", "pip"])
.with_exec(["pip", "install", "dagger-io==0.18.5"])
.with_directory("/app", dag.host().directory("dagger_modules/src/hello_world"))
.with_workdir("/app")
.with_exec(["python", "-c", exec_command])
message = await container.stdout()
logger.info(f"Received message from hello-world function: {message}")
return {"message": message.strip()}

except Exception as e:
    logger.exception(f"Error executing hello-world module: {str(e)}")
    raise HTTPException(status_code=500, detail=str(e))

// DaggerFastAPIDemo/dagger_modules/src/hello_world/main.py
from dagger import function, object_type

@object_type
class HelloWorld:
@function
def hello_world(self) -> str:
"""Return a Hello World greeting."""
return "Hello, World!"`

#

def get_module_execution_command(module_name, class_name, method_name): """Generate a standardized Python command to execute a module""" return f"from {module_name} import {class_name}; obj = {class_name}(); print(obj.{method_name}())"

faint frost
opaque remnant
#

Hi @weary plume, we just had a livestream where we walked through a fast api example that you might find useful. Here is the final code repo: https://github.com/jasonmccallister/fastapi-sample-app for reference.

The video has a lost of good information in there that you might find useful as we walk through the entire setup of Daggerizing the FastAPI application.

lean saddle
faint frost
opaque remnant
faint frost
#

@weary plume for these cases it's better to use the python SDK directly instead of using modules for now

#

that will allow you to call dagger pipelines within your python app

weary plume
#

I think I need to flip the script.

Dagger module

hello_module.py

import dagger
from dagger import function

@function
def say_hello(name: str) -> dagger.Container:
return (
dagger.container()
.from_("python:3.11-slim")
.with_exec(["python", "-c", f"print('Hello, {name}!')"])
)

@function
def reverse_output(input_container: dagger.Container) -> dagger.Container:
return (
input_container
.with_exec(["bash", "-c", "rev"])
)

FastAPI

main.py

import dagger
import hello_module
import asyncio
from fastapi import FastAPI

app = FastAPI()

@app.get("/reverse-hello")
async def reverse_hello(name: str = "World"):
async with dagger.Connection() as client:
hello_container = hello_module.say_hello(name)
reversed_container = hello_module.reverse_output(hello_container)
output = await reversed_container.stdout()
return {"result": output.strip()}

http://localhost:8000/reverse-hello?name=World

faint frost
weary plume
#

@faint frost Good catch. They should be plain functions that return Container steps.

faint frost
weary plume
#

@faint frost This is what I mean: https://github.com/dancomanlive/dagger-fastapi-llm-demo. I created a pattern where each tool is implemented as a pair of functions: one that constructs a container with the necessary configuration and mounted scripts, and another that executes the container and returns the result. This approach keeps the code clean, maintainable, and extensible while sidestepping the current limitations of calling module functions programmatically. By storing the tool scripts in a dedicated directory and mounting them at runtime, we maintain separation of concerns while still leveraging Dagger’s powerful container orchestration capabilities.

faint frost
#

I thought the whole idea of this thread is to use modules along with the FastAPI code 🤔

weary plume
#

@faint frost That was the plan until I learned about the limitations of the Python SDK.

As I understand it, I can’t use Dagger modules unless I go through GraphQL, which feels like more of a hassle. I’d be happy to be proven wrong, though.

Do you have any idea how long it might take for the Python SDK to support calling module functions outside of dagger call?

#

I would love for my organization to adopt Dagger as soon as possible, and while this limitation is not a complete blocker, it is something I would like to help move forward. I am happy to contribute in any way I can.

faint frost
weary plume
#

@faint frost Yes, once Python client generators arrive, I’ll refactor back to Dagger modules. For now each tool is a Python script plus a container image.

faint frost
# weary plume <@336241811179962368> That was the plan until I learned about the limitations of...

As I understand it, I can’t use Dagger modules unless I go through GraphQL, which feels like more of a hassle. I’d be happy to be proven wrong, though.

yeah.. going through GraphQL I wouldn't recommend it unless it's something you need with urgency. Regarding when python client generators might be available, I'll defer that to @normal moat and @lean saddle . I know work is being done on that area but it's hard to give an accurate estimate rn

lean saddle
#

I think your use case is very cool! So if as a PoC you can try to use TypeScript or Go instead of Python (until Python supports client generation), that would be amazing 😄

opaque remnant
lean saddle
#

Not yet, it's in "beta" mode so we didn't update the documentation yet, there are few things that can be improved before making this public

weary plume
#

@lean saddle Glad you like it! I’m now trying to integrate Superlinked and Qdrant to make it more “real,” so to speak. The entire AI team is working in Python, so I need to stick to that stack. What’s needed to complete the Python client generation? I’d be happy to contribute to speed things up and help get Python Dagger modules rolling!

weary plume
#

from dagger import dag
import json

async def run_rag_pipeline(query: str, collection: str = "default"):
# Input data for the pipeline
input_data = {
"query": query,
"collection": collection,
"qdrant_host": "qdrant:6333"
}
input_file = dag.host().file().from_string(json.dumps(input_data))

Step 1: Retrieve

retrieve_output = (
    dag.container()
    .from_("mycompany/retrieve:v1.0.0")
    .with_mounted_file("/input.json", input_file)
    .with_exec(["--input", "/input.json", "--output", "/output.json"])
    .file("/output.json")
)

Step 2: Augment (uses retrieve’s output)

augment_input = dag.host().file().from_string(await retrieve_output.contents())
augment_output = (
    dag.container()
    .from_("mycompany/augment:v1.0.0")
    .with_mounted_file("/input.json", augment_input)
    .with_exec(["--input", "/input.json", "--output", "/output.json"])
    .file("/output.json")
)

Step 3: Generate

generate_input = dag.host().file().from_string(await augment_output.contents())
generate_output = (
    dag.container()
    .from_("mycompany/generate:v1.0.0")
    .with_mounted_file("/input.json", generate_input)
    .with_exec(["--input", "/input.json", "--output", "/output.json"])
    .file("/output.json")
)

return await generate_output.contents()

FastAPI integration

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

class RagRequest(BaseModel):
query: str
collection: str = "default"

@app.post("/rag")
async def run_rag(request: RagRequest):
async with dag.Connection(dag.Config()) as client:
result = await run_rag_pipeline(request.query, request.collection)
return json.loads(result)

When the Python SDK supports calling Dagger modules I can just swap the containers for each RAG step with Dagger modules.

Hypothetical example: