Skip to main content

Build a durable summarization app in Python with Resonate, Flask, and Ollama

Welcome to the Resonate Python SDK quickstart tutorial!

In this tutorial, you will build a summarization application using the Resonate Python SDK, Flask, and Ollama. By doing so, you will gain experience with the Distributed Async Await programming model and be exposed to the core features of the SDK.

This tutorial follows the philosophy of "progressive disclosure", starting with a simple example and building on it step by step. Each part introduces a new concept or feature of Resonate. You can choose to stop at the end of any part of the tutorial and still have a working application.

Each part of the tutorial corresponds to a working example in the Resonate Python SDK examples repository. Each code example has a link above it that will take you to the source code in the repository.

Final example application

Want to jump straight to working with the final example application? You can find the source code in the Resonate Python SDK examples repository.

In part 1, you will start with a single Application Node and observe the SDK's ability to automatically retry application-level failures (failed function executions). Then in part 2 you will connect your application to the Resonate Server to enable recovery from platform-level failures and see how a function execution can recover from a process crash (Durable Execution). In part 3 you will build on the previous example by separating your HTTP Server from your application code and run multiple Application Nodes to see how Resonate facilitates fan-out/fan-in use cases. After, in part 4 you will add a third step to your workflow that blocks the execution on input from a human-in-the-loop and unblock it from another process. Finally, in part 5 you will integrate a webscraper, such as Beautiful Soup, and an LLM, such as Ollama, to bring your application to life.

By the end of this tutorial you'll have a good understanding of the Resonate Python SDK and how to build Distributed Async Await applications with it.

Prerequisites

This tutorial assumes that you have Python 3 and a package manager installed. This tutorial recommends using Rye as the package manager.

Part 5 of this tutorial assumes you have Ollama installed and model "llama3.1" running locally on your machine.

Part 1 Incremental adoption

In this part of the tutorial you'll see how Resonate promotes incremental adoption. You can get started with the Resonate programming model without connecting to an external orchestrator or promise store, such as the Resonate Server. Using the SDK without the Resonate Server still provides your application with automatic function execution retries.

Start by setting up a new project.

Create a project folder:

mkdir resonate-quickstart && cd resonate-quickstart

Initialize a new Rye project:

rye init summarize --script

Sync the project:

cd summary
rye sync

Install the dependencies:

rye add resonate-sdk flask

Rye automatically generates a init.py file and main.py file in the src/summarize directory. Leave the directory, but delete both of these files.

You will start by creating a two step workflow application. The first step is to download content from a URL and the second step is to summarize the content.

Create an app.py file in the src/summarize directory and copy and paste the workflow code snippet below:

quickstart/part-1/src/summarize/app.py

from resonate.context import Context
from resonate.stores.local import LocalStore, MemoryStorage
from resonate.resonate import Resonate
# ...
import random
import time

# ...

# Create a Resonate instance with a local store
resonate = Resonate(store=LocalStore(MemoryStorage()))


# Define the downloadAndSummarize workflow
# Register it with Resonate as a top-level orchestrating generator
@resonate.register
def downloadAndSummarize(ctx: Context, url: str):
print("Downloading and summarizing content from", url)
# Download the content from the provided URL
content = yield ctx.lfc(download, url)
# Summarize the downloaded content
summary = yield ctx.lfc(summarize, url, content)
# Return the summary
return summary


def download(ctx: Context, url: str):
print(f"Downloading data from {url}")
time.sleep(2.5)
# Simulate a failure to download data 50% of the time
if random.randint(0, 100) > 50:
print("Download failed")
raise Exception("Failed to download data")
print("Download successful")
return f"This is the content of {url}"


def summarize(ctx: Context, url: str, content: str):
print("Summarizing content...")
time.sleep(2.5)
# Simulate a failure to summarize content 50% of the time
if random.randint(0, 100) > 50:
print("Summarization failed")
raise Exception("Failed to summarize content")
print("Summarization successful")
return f"This is the summary of {url}."

When executed, the previous code example demonstrates how Resonate provides automatic retries for application-level failures.

The previous code example simulates downloading a page from the internet and summarizing the content of that page. It represents the "business logic" of the service. The main function, downloadAndSummarize(), orchestrates the steps of the application, download() then summarize(). It is decorated with @resonate.register which registers the function with the Resonate instance, and enables it to be invoked with Resonate's .run() method.

Both the download() and summarize() functions include a 2.5-second timeout to simulate the time required for downloading and summarizing content from a URL. And both functions are designed to “fail” 50% of the time. When they return an error, Resonate automatically retries them until they succeed.

Local Function Invocations

Notice how these functions are invoked using ctx.lfc. LFC stands for Local Function Call and is syntactic sugar for Resonate's standard Local Function Invocation. When the application is connected to the Resonate Server, the promise created by ctx.lfc() is stored there. When the application is not connected to the Resonate Server, the promise is stored in local memory. Local memory enables the application to provide automtic retries for application-level failures without connecting to a supervisor service.

Next you will turn your app into an HTTP service with a single route handler. The handler will use Resonate's .run() method to invoke the summarization workflow enabling Resonate to supervise the execution of the functions and ensure they complete. In your app.py add the import to the top of the file, and then add the HTTP route handler below the downloadAndSummarize() workflow code.

quickstart/part-1/src/summarize/app.py

# ...
from flask import Flask, request, jsonify
# ...

app = Flask(__name__)

# ...


# Define a route handler for the /summarize endpoint
@app.route("/summarize", methods=["POST"])
def summarize_route_handler():
try:
# Extract JSON data from the request
data = request.get_json()
if "url" not in data:
return jsonify({"error": "URL not provided"}), 400

# Extract the URL from the request
url = data["url"]

# Run the summarize function asynchronously
promise = downloadAndSummarize.run(id=f"downloadAndSummarize-{url}", url=url)

# Return the result as JSON
return jsonify({"summary": promise.result()})
except Exception as e:
return jsonify({"error": str(e)}), 500


# Define a main function to start the Flask app
def main():
app.run(host="127.0.0.1", port=5000)
print("Serving HTTP on port 5000...")


# Run the main function when the script is invoked
if __name__ == "__main__":
main()

When executed, the previous code example initializes an HTTP Server listening to the /summarize route on port 5000. When a request comes in, the HTTP Server extracts the URL from the request, runs the downloadAndSummarize() function with Resonate's .run() method, and returns the result as JSON.

downloadAndSummarize Call Graph diagram

When calling Resonate's .run() method you must provide a unique identifier and the function’s arguments. The identifier serves as the Durable Promise ID that correlates to the function invocation. Note that the Durable Promise ID does not correlate to a function execution, but instead the invocation of the function. The function may be executed as many times as needed to complete and resolve the promise.

If you use the same identifier for multiple calls to the .run() method, you will receive the result from the initial execution without re-running the function. To get a new result on each call, you need to supply a different promise ID.

Lastly, make sure to update your pyproject.toml file to have the following scripts:

[project.scripts]
"app" = "summarize.app:main"

Rerun the sync command after updating the pyproject.toml file:

rye sync

To start the summarization service using rye, run the following command:

rye run app

Then, to summarize the content of a URL, from another terminal send a POST request to the /summarize endpoint.

curl -X POST http://localhost:5000/summarize -H "Content-Type: application/json" -d '{"url": "http://example.com"}'

Watch the log output of your service. There is a good chance that you will see either "download failed" or "summarization failed" or both one or more times.

However, even if you see those failures logged, eventually you should get the text response, "This is a summary of the text", back to where you made POST request.

After the request for the URL succeeds, try running it again with the same URL. Notice that on subsequent requests with the same URL, Resonate does not start the executions again but returns the result immediately.

This is because the URL is the unique part of the Promise ID. The Resonate Python SDK stores that promise locally and ensures that if the same ID is used in subsequent calls, the result of the execution associated with that promise is returned.

However, if you restart the service or provide a different URL, the service will execute the functions again.

In the next part of the tutorial, you will connect to a Resonate Server which stores the promise remotely, so that even if your HTTP server crashes, the function executions will be able to eventually complete.

Part 2 Crash recovery

In this part of the tutorial you will connect your service to a Resonate Server to enable recovery from platform-level failures, effectively providing "Durable Execution". The Resonate Server acts as a supervisor and orchestrator for your application, storing promises and providing tasks to Application Nodes.

Follow these steps to install and run the Resonate Server:

brew install resonatehq/tap/resonate
resonate serve

By default, the Resonate Server runs on localhost port 8001 using an SQLite database.

After the server is running, you can update your application code.

There are two places where you need to update your code for this part of the tutorial.

First, in your app.py file switch from importing LocalStore and MemoryStorage to importing RemoteStore and pass an instance of it to the Resonate constructor, while providing the URL of the Resonate Server. In this case, the URL of the server is http://localhost:8001.

quickstart/part-2/src/summarize/app.py

from resonate.stores.remote import RemoteStore
# ...

# Create an instance of Resonate with a remote promise store
resonate = Resonate(store=RemoteStore(url="http://localhost:8001"))

Next, add a 10 second sleep to the downloadAndSummarize() function between download() and summarize().

quickstart/part-2/src/summarize/app.py

# ...
# Define the downloadAndSummarize workflow
# Register it with Resonate as a top-level orchestrating generator
@resonate.register
def downloadAndSummarize(ctx: Context, url: str):
print("Downloading and summarizing content from", url)
# Download the content from the provided URL
content = yield ctx.lfc(download, url)
print(content)
# Add a delay so you have time to simulate a failure
time.sleep(10)
# Summarize the downloaded content
summary = yield ctx.lfc(summarize, url, content)
print(summary)
# Return the summary
return summary

You are adding a 10 second sleep so that you have time, between the download() step and the summarize() step, to kill the service before the summarize() step starts.

Now that you have your code updated, it is time to simulate a service outage.

If you haven't already, restart your HTTP service with the updated code.

rye run app

After your service is running with the updated code, send the POST request to your service.

curl -X POST http://localhost:3000/summarize -H "Content-Type: application/json" -d '{"url": "http://example.com"}'

Let it run it until you see the log "download successful", then kill (Ctrl-c) the service.

Durable Execution in action

In the first part of the tutorial, killing the application would have resulted in the loss of the state of the executions. However, because the service is now connected to the Resonate Server, when you bring the service back up, the execution sequence continues from where it left off.

Start the service again. Assuming that you stopped the service after the download() function ran, you should now see the summarize() function logs and the application complete its execution.

Lets, look at the sequence diagram to understand what happened.

Remote promise storage diagram with retries

In the diagram above, you can map function 1 to downloadAndSummarize(), function 2 to download(), and function 3 to summarize(). Notice how function 1 gets the result of function 2 from the Durable Promise after the process restarts. That is because the result of function 2 was stored in promise 2 before the process crashed. This effectively resumes the execution of function 1 from where it left off.

You can also simulate a crash on the client side by killing the cURL request before the downloadAndSummarize() function completes. Then send the same cURL request again before the function completes, and with the same URL, and notice how Resonate doesn't start a new execution. Instead the cURL request waits on the existing execution to complete. This shows how Durable Promise IDs act as Idempotency Keys.

Next, inspect the promise in the Resonate Server to see that it resolved successfully.

You can inspect the promises stored in the Resonate Server via the Resonate CLI.

resonate promise get summarize-http://example.com

You should see output similar to the following:

Id:       summarize-http://example.com
State: RESOLVED
Timeout: 9008909898871320

Idempotency Key (create): summarize-http://example.com
Idempotency Key (complete): summarize-http://example.com

Param:
Headers:
Data:
{"func":"downloadAndSummarize","args":["http://example.com"]}

Value:
Headers:
Data:
"This is a summary of the text"

Tags:
resonate:invocation: true
Promise statuses

Try out your app again with another url (to create a new promise with a different ID). Then, kill the HTTP service halfway through execution. Query for the promise in the Resonate Server and check out the status.

You should see that the promise is marked PENDING.

Bring your HTTP service back up and and then query for the promise again.

It should be marked RESOLVED.

Next, you will separate the HTTP Server from the application code and run multiple Application Nodes to see how Resonate facilitates fan-out/fan-in use cases.

Part 3 Fan-out/Fan-in

In this part of the tutorial, you will separate the HTTP service code from the Application Node code so that the HTTP service and the workflow code can run in different processes. Once separated you will run multiple Application Nodes and send multiple requests to the HTTP service and see which Application Node picks up the work.

downloadAndSummmarize Fan-out/Fan-in diagram

Start by creating a new file gateway.py in src/summarize.

Move the HTTP service code from app.py to gateway.py. You will also need to update the instantiation of the Resonate specify a task source and the method by which the tasks are acquired. And lastly you will need to introduce a dispatch() function which can be invoked with Resonate's .run() method.

It should look like the following:

quickstart/part-3/src/summarize/gateway.py

from resonate.task_sources.poller import Poller
from resonate.stores.remote import RemoteStore
from resonate.resonate import Resonate
from resonate.context import Context

from resonate.targets import poll
from flask import Flask, request, jsonify


app = Flask(__name__)

# Create an instance of Resonate
resonate = Resonate(
store=RemoteStore(url="http://localhost:8001"),
task_source=Poller(url="http://localhost:8002", group="gateway"),
)


# Define and register a top-level orchestrator coroutine
@resonate.register
def dispatch(ctx: Context, url: str):
yield ctx.rfi("downloadAndSummarize", url).options(
send_to=poll("summarization-nodes")
)
return




# Define a route handler for the /summarize endpoint
@app.route("/summarize", methods=["POST"])
def summarize_route_handler():
try:
# Extract JSON data from the request
data = request.get_json()
if "url" not in data:
return jsonify({"error": "URL not provided"}), 400

# Extract the URL from the request
url = data["url"]

# Use a Remote Function Invocation

dispatch.run(id=f"downloadAndSummarize-{url}", url=url)

# Return the result as JSON
return jsonify({"summary": "workflow started"}), 200
except Exception as e:
return jsonify({"error": str(e)}), 500


# Define a main function to start the Flask app
def main():
app.run(host="127.0.0.1", port=5000)
print("Serving HTTP on port 5000...")


# Run the main function when the script is executed
if __name__ == "__main__":
main()

In the previous code example, the highlighted lines indicate the changes you will need to make after moving the HTTP service code to gateway.py.

The dispatch() function uses ctx.rfi(), a Remote Function Invocation, to invoke the downloadAndSummarize() workflow. Note that in future iterations of the SDK, Remote Function Invocations should be possible directly from the Resonate instance.

The decision to not await on the result of the workflow in the gateway is purely so that you can start multiple workflows concurrently and see which Application Node picks up the work.

Next, update the app.py file with the following changes:

  • Update the instantiation of the Resonate instance to specify a task source and a method by which the tasks are acquired.
  • Import Event from threading and unpdate the main() function so that the Application Node stays alive waiting on thread events.

quickstart/part-3/src/summarize/app.py

from resonate.task_sources.poller import Poller
from resonate.stores.remote import RemoteStore
from resonate.resonate import Resonate
from resonate.context import Context

from threading import Event
import random
import time

# ...


# Define a main function to start the Application Node
def main():
print("App node running")
Event().wait()

The addition of Event().wait() ensures the Application Node runs indefinitely as a worker service.

Finally, update the [project.scripts] section of your pyproject.toml file to include the gateway script:

[project.scripts]
"gateway" = "summarize.gateway:main"
"app" = "summarize.app:main"

Now you can simulate a fan-out/fan-in use case by running multiple Application Nodes and sending multiple requests to the HTTP service.

In one terminal run the HTTP service:

rye run gateway

Then open two more terminals and in each one run an Application Node:

rye run app

Open one more terminal and use it to send multiple cURL requests to the HTTP service:

curl -X POST http://localhost:5000/summarize -H "Content-Type: application/json" -d '{"url": "<url>"}'

As you send more and more requests, you should start to see both of the Application Nodes picking up summarization tasks and processing them.

Then, kill one of the Application Nodes and notice how the other Application Node picks up the work from the failed one.

Next, you will add a step to the workflow that blocks the execution on input from a human-in-the-loop.

Part 4 - Human-in-the-loop

In this part of the tutorial, you will add a step to the workflow that blocks the execution on input from a human-in-the-loop. Resonate enables you to create and wait on a promise, blocking progress until the promise resolves. So far, you have created promises attached to function executions. But you can also create a promise that is resolved by an external process, such as a human-in-the-loop.

Let's update the application (app.py) to send the summarization of the text to a human for review. In this case we will pretend the summarization is sent in an email and that it contains two links: one link to accept the summarization as correct and another link to reject it, requiring the application to try summarizing the content again.

Therefore the summarization() function and a new send_email() function will be inside a loop that will continue until the summarization is accepted.

quickstart/part-4/src/summarize/app.py

# ...
# Define and register the downloadAndSummarize workflow
@resonate.register
def downloadAndSummarize(ctx: Context, url: str, email: str):
print("Downloading and summarizing content from", url)
# Download the content from the provided URL
content = yield ctx.lfc(download, url)
# Loop until the summary is confirmed
while True:
# Summarize the downloaded content
summary = yield ctx.lfc(summarize, url, content)

# Create a Durable Promise to wait for confirmation
promise = yield ctx.rfi(DurablePromise(id=None))

# Send an email with the summary
yield ctx.lfc(send_email, summary, email, promise.id)

# Wait for the summary to be accepted or rejected
print("Waiting on confirmation")
confirmed = yield promise

if confirmed:
break

print("Workflow complete")
return

# ...

def send_email(ctx: Context, summary: str, email: str, promise_id: str):
print(f"Summary: {summary}")
print(
f"Click to confirm: http://localhost:5000/confirm?confirm=true&promise_id={promise_id}"
)
print(
f"Click to reject: http://localhost:5000/confirm?confirm=false&promise_id={promise_id}"
)
print("Email sent successfully")
return

In the previous code example, the downloadAndSummarize() function now includes a loop that continues until the summarization is accepted. The 10 second sleep was also removed to focus on the human-in-the-loop aspect of the workflow.

The entire workflow will wait until the promise is resolved by the human-in-the-loop. And the loop will break if the data we receive via the promise resolution is true, and continue if false.

Next, update the gateway.py file to include a route handler for the /confirm endpoint. This route handler resolves the promise created inside the loop that blocks progress after sending the email. Use a GET endpoint so that our links work in the browser. Additionally, make sure to separate the instance of the RemoteStore() into a global variable so that it can be accessed by the route handler.

quickstart/part-4/src/summarize/gateway.py

# ...
# Create an instance of Resonate
store = RemoteStore(url="http://localhost:8001")
resonate = Resonate(
store=store, task_source=Poller(url="http://localhost:8002", group="gateway")
)

# ...

# Define a route handler for the /confirm endpoint
@app.route("/confirm", methods=["GET"])
def confirm_email_route_handler():
global store
try:
# Extract parameters from the request
promise_id = request.args.get("promise_id")
confirm = request.args.get("confirm")

# Check if the required parameters are present
if not promise_id or confirm is None:
return jsonify({"error": "url and confirmation params are required"}), 400

# Convert to boolean
confirm = confirm.lower() == "true"

# Resolve the promise
store.resolve(
id=promise_id,
ikey=None,
strict=False,
headers=None,
data=json.dumps(confirm),
)
if confirm:
return jsonify({"message": "Summarization confirmed."}), 200
else:
return jsonify({"message": "Summarization rejected."}), 200
except Exception as e:
return jsonify({"error": str(e)}), 500

After updating gateway.py, restart your application and update your cURL request to include the email address.

curl -X POST http://localhost:5000/summarize -H "Content-Type: application/json" -d '{"email": "<email>", "url": "<url>"}'

You should eventually see the two links to confirm or reject the summarization. Click on the confirm link and notice how the workflow completes. Click on the reject link and notice how the summarization step runs again. The summarization step will repeat until you confirm the summarization.

Next, you will integrate a webscraper and an LLM to bring your application to life.

Part 5 Ollama integration

In this part of the tutorial, you will use Selenium and Beautiful Soup to scrape a webpage and Ollama to summarize the content. Besides integrating the libraries, the other key change will be to store the downloaded content on the Application Node instead of in a Durable Promise. Because we are downloading real content from the internet, we must plan for the possibility of downloading large amounts of data. While a Durable Promise can store many MBs of data, this example will show how you can enable and disable durability for local function invocations.

First, install the required libraries:

rye add selenium bs4 ollama

Next, make and propagate a change to gateway.py that creates a "clean" URL for use in promise IDs and file paths. A "clean" URL removes the http:// and https:// and replaces all "/" with "-".

quickstart/part-5/src/summarize/gateway.py

# ...
@resonate.register
def dispatch(ctx: Context, url: str, clean_url: str, email: str):
yield ctx.rfi("downloadAndSummarize", url, clean_url, email).options(
send_to=poll("summarization-nodes")
)
return


# Define a route handler for the /summarize endpoint
@app.route("/summarize", methods=["POST"])
def summarize_route_handler():
try:
# ...
email = data["email"]
clean_url = clean(url)

# Use a Remote Function Invocation
dispatch.run(f"downloadAndSummarize-{clean_url}", url, clean_url, email)

# Return the result as JSON
return jsonify({"summary": "workflow started"}), 200
except Exception as e:
return jsonify({"error": str(e)}), 500

# ...

def clean(url):
tmp = re.sub(r"^https?://", "", url)
return tmp.replace("/", "-")

You are doing this because the webscraper requires a full-path URL to scrape the content, such as "https://example.com". In previous examples, you may have been providing "example.com" as the URL. This change ensures that we can provide a full-path URL to the webscraper and ensures that the promise ID and file path are clean, easy to read, and valid.

There are a few more places in app.py where you will make the distiction between using the clean URL vs the full-path URL.

You will also want to add .options(durable=False) to the invocation of the download() function to disable durability for the downloaded content.

quickstart/part-5/src/summarize/app.py

# ...
@resonate.register
def downloadAndSummarize(ctx: Context, url: str, clean_url: str, email: str):
print("Downloading and summarizing content from", url)
# Download the content from the provided URL
filename = yield ctx.lfc(download, url, clean_url).options(durable=False)
# ...


def download(ctx: Context, url: str, clean_url: str):
filename = f"{clean_url}.txt"

Up until this point we have been assuming a small amount of text is downloaded from the URL and the result of download() has been stored in a Durable Promise. But one of the benefits of running multiple Application Nodes, is to not just enable fan-out/fan-in use cases, but also to enable the ability to pick up work from a failed Application Node.

But what if the URL represents a large website that results in many MBs of content? If we store all that data into a Durable Promise, this could drastically affect the performance of our application.

Therefore, we want to store the downloaded content on the Application Node. If we do that, then we need to turn off durability for the invocation of the download() function, so that if the workflow resumes on another Application Node, the content will re-download on that node. That is what we did with adding .options(durable=False) to the invocation of the download() function.

Next, update the download() function to use Selenium and Beautiful Soup to scrape the content from the URL.

quickstart/part-5/src/summarize/app.py

# ...
def download(ctx: Context, url: str, clean_url: str):
filename = f"{clean_url}.txt"

# Check if the file already exists
if os.path.exists(filename):
print(f"File {filename} already exists. Skipping download.")
return filename

print(f"Downloading data from {url}")
try:
driver = webdriver.Chrome()
driver.get(url)
soup = BeautifulSoup(driver.page_source, "html.parser")
content = soup.get_text()
with open(filename, "w", encoding="utf-8") as f:
f.write(content)
driver.quit()
print(f"Content saved to {filename}")
return filename
except Exception as e:
print(f"Download failed: {e}")
raise Exception(f"Failed to download data: {e}")

The download() function will now scrape content from a url and save it to a file. Notice that the filename is now returned and stored in the Durable Promise instead of the content itself.

Next, update the summarize() function to read the file and use Ollama to summarize the content.

quickstart/part-5/src/summarize/app.py

# ...
def summarize(ctx: Context, url: str, filename: str):
print(f"Summarizing content from {url}")
try:
with open(filename, "r", encoding="utf-8") as f:
file_content = f.read()

options: ollama.Options | None = None

summary = ollama.chat(
model="llama3.1",
messages=[
{
"role": "system",
"content": "You review text scraped from a website and summarize it. Ignore text that does not support the narrative and purpose of the website.",
},
{"role": "user", "content": f"Content to summarize: {file_content}"},
],
options=options,
)
return summary
except Exception as e:
print(f"Summarization failed: {e}")
raise Exception(f"Failed to summarize content: {e}")

The summarize() function will now read the content from the file and provide a basic prompt to Ollama requesting a summarization.

You are now ready to run your application.

Restart your gateway and Application Nodes and send a cURL request to the /summarize endpoint with the URL of a website you would like to summarize.

Congratulations! You have successfully built a summarization application.

Conclusion

In this tutorial, you gained experience building Distributed Async Await applications using the Resonate Python SDK. And you were exposed to many of Resonates core value propositions, including:

  • A holistic programming model that enables you to build applications that are resilient to platform-level failures.
  • Incremental adoption that allows you to start building applications without connecting to an external orchestrator.
  • Faciliation of fan-out/fan-in use cases that enable you to run multiple Application Nodes and have them pick up work from failed Application Nodes.
  • Ability to block the execution of a workflow on input from a human-in-the-loop.
  • Flexibility to choose where to invoke functions and when to enable durability.

You now also have a fully functioning summarization application that is distributed, scalable, and durable built with Resonate, Flask, and Ollama.

If you have questions or any other feedback, feel free to reach out to the Resonate community on Discord!