Skip to content

Commit 67b8438

Browse files
authored
Merge pull request #129 from restackio/community-lmnt
2 parents 73d44ae + bf3a675 commit 67b8438

23 files changed

+558
-0
lines changed

Diff for: community/lmnt/.env.Example

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
LMNT_API_KEY=<your-lmnt-api-key>
2+
3+
# Restack Cloud (Optional)
4+
5+
# RESTACK_ENGINE_ID=<your-engine-id>
6+
# RESTACK_ENGINE_API_KEY=<your-engine-api-key>
7+
# RESTACK_ENGINE_ADDRESS=<your-engine-address>
8+
# RESTACK_ENGINE_API_ADDRESS=<your-engine-api-address>

Diff for: community/lmnt/.gitignore

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
__pycache__
2+
.pytest_cache
3+
venv
4+
.env
5+
.vscode
6+
poetry.lock
7+
8+
src/media/*
9+
!src/media/.gitkeep
10+

Diff for: community/lmnt/Dockerfile

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
FROM python:3.12-slim
2+
3+
WORKDIR /app
4+
5+
RUN apt-get update && apt-get install -y
6+
7+
RUN pip install poetry
8+
9+
COPY pyproject.toml ./
10+
11+
COPY . .
12+
13+
# Configure poetry to not create virtual environment
14+
RUN poetry config virtualenvs.create false
15+
16+
# Install dependencies
17+
RUN poetry install --no-interaction --no-ansi
18+
19+
# Expose port 80
20+
EXPOSE 80
21+
22+
CMD poetry run python -m src.services

Diff for: community/lmnt/README.md

+153
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
# Restack AI - LMNT Example
2+
3+
This repository contains a simple example project to help you scale with Restack AI.
4+
It demonstrates how to scale reliably to millions of workflows on a local machine with LMNT voice generation.
5+
6+
## Motivation
7+
8+
When scaling AI workflows, you want to make sure that you can handle failures and retries gracefully.
9+
This example demonstrates how to do this with Restack AI and LMNT's voice generation API.
10+
11+
### Workflow Steps
12+
13+
The table below shows the execution of 50 workflows in parallel, each with three steps.
14+
Steps 2 is LMNT voice generation functions that must adhere to a rate limit of 1 concurrent call per second.
15+
16+
| Step | Workflow 1 | Workflow 2 | ... | Workflow 50 |
17+
| ---- | ---------- | ---------- | --- | ----------- |
18+
| 1 | Basic | Basic | ... | Basic |
19+
| 2 | LMNT | LMNT | ... | LMNT |
20+
21+
### Traditional Rate Limit Management
22+
23+
When running multiple workflows in parallel, managing the rate limit for LMNT functions is crucial. Here are common strategies:
24+
25+
1. **Task Queue**: Use a task queue (e.g., Celery, RabbitMQ) to schedule LMNT calls, ensuring only one is processed at a time.
26+
2. **Rate Limiting Middleware**: Implement middleware to queue requests and process them at the allowed rate.
27+
3. **Semaphore or Locking**: Use a semaphore or lock to control access, ensuring only one LMNT function runs per second.
28+
29+
### With Restack
30+
31+
Restack automates rate limit management, eliminating the need for manual strategies. Define the rate limit in the service options, and Restack handles queuing and execution:
32+
33+
```python
34+
client.start_service(
35+
task_queue="lmnt",
36+
functions=[lmnt_list_voices, lmnt_synthesize],
37+
options=ServiceOptions(
38+
rate_limit=1,
39+
max_concurrent_function_runs=1
40+
)
41+
)
42+
```
43+
44+
Focus on building your logics while Restack ensures efficient and resilient workflow execution.
45+
46+
### On Restack UI
47+
48+
You can see from the parent workflow how long each child workflow stayed in queue and how long was the execution time.
49+
50+
![Parent Workflow](./ui-parent.png)
51+
52+
And for each child workflow, for each step you can see how long the function stayed in queue, how long the function took to execute and how many retries happened.
53+
54+
![Child Workflow](./ui-child.png)
55+
56+
## Prerequisites
57+
58+
- Python 3.10 or higher
59+
- Poetry (for dependency management)
60+
- Docker (for running the Restack services)
61+
- LMNT API key (sign up at https://www.lmnt.com)
62+
63+
## Prerequisites
64+
65+
- Docker (for running Restack)
66+
- Python 3.10 or higher
67+
68+
## Start Restack
69+
70+
To start the Restack, use the following Docker command:
71+
72+
```bash
73+
docker run -d --pull always --name restack -p 5233:5233 -p 6233:6233 -p 7233:7233 ghcr.io/restackio/restack:main
74+
```
75+
76+
## Start python shell
77+
78+
```bash
79+
poetry env use 3.10 && poetry shell
80+
```
81+
82+
## Install dependencies
83+
84+
```bash
85+
poetry install
86+
```
87+
88+
```bash
89+
poetry env info # Optional: copy the interpreter path to use in your IDE (e.g. Cursor, VSCode, etc.)
90+
```
91+
92+
```bash
93+
poetry run dev
94+
```
95+
96+
## Run workflows
97+
98+
### from UI
99+
100+
You can run workflows from the UI by clicking the "Run" button.
101+
102+
![Run workflows from UI](./ui-endpoints.png)
103+
104+
### from API
105+
106+
You can run one workflow from the API by using the generated endpoint:
107+
108+
`POST http://localhost:6233/api/workflows/ChildWorkflow`
109+
110+
or multiple workflows by using the generated endpoint:
111+
112+
`POST http://localhost:6233/api/workflows/ExampleWorkflow`
113+
114+
### from any client
115+
116+
You can run workflows with any client connected to Restack, for example:
117+
118+
```bash
119+
poetry run schedule
120+
```
121+
122+
executes `schedule_workflow.py` which will connect to Restack and execute the `ChildWorkflow` workflow.
123+
124+
```bash
125+
poetry run scale
126+
```
127+
128+
executes `schedule_scale.py` which will connect to Restack and execute the `ExampleWorkflow` workflow.
129+
130+
```bash
131+
poetry run interval
132+
```
133+
134+
executes `schedule_interval.py` which will connect to Restack and execute the `ChildWorkflow` workflow every second.
135+
136+
## Deploy on Restack Cloud
137+
138+
To deploy the application on Restack, you can create an account at [https://console.restack.io](https://console.restack.io)
139+
140+
## Project Structure
141+
142+
- `src/`: Main source code directory
143+
- `client.py`: Initializes the Restack client
144+
- `functions/`: Contains function definitions
145+
- `workflows/`: Contains workflow definitions
146+
- `services.py`: Sets up and runs the Restack services
147+
- `schedule_workflow.py`: Example script to schedule and run a workflow
148+
- `schedule_interval.py`: Example script to schedule and a workflow every second
149+
- `schedule_scale.py`: Example script to schedule and run 50 workflows at once
150+
151+
# Deployment
152+
153+
Create an account on [Restack Cloud](https://console.restack.io) and follow instructions on site to create a stack and deploy your application on Restack Cloud.

Diff for: community/lmnt/pyproject.toml

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Project metadata
2+
[tool.poetry]
3+
name = "community_lmnt"
4+
version = "0.0.1"
5+
description = "A simple example to get started with the restack-ai SDK"
6+
authors = [
7+
"Restack Team <[email protected]>",
8+
]
9+
readme = "README.md"
10+
packages = [{include = "src"}]
11+
12+
[tool.poetry.dependencies]
13+
python = ">=3.10,<4.0"
14+
restack-ai = "^0.0.52"
15+
watchfiles = "^1.0.0"
16+
pydantic = "^2.10.5"
17+
lmnt = "1.1.4"
18+
19+
[tool.poetry.dev-dependencies]
20+
pytest = "6.2" # Optional: Add if you want to include tests in your example
21+
22+
# Build system configuration
23+
[build-system]
24+
requires = ["poetry-core"]
25+
build-backend = "poetry.core.masonry.api"
26+
27+
# CLI command configuration
28+
[tool.poetry.scripts]
29+
dev = "src.services:watch_services"
30+
services = "src.services:run_services"
31+
workflow = "schedule_workflow:run_schedule_workflow"
32+
interval = "schedule_interval:run_schedule_interval"
33+
scale = "schedule_scale:run_schedule_scale"

Diff for: community/lmnt/schedule_interval.py

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import asyncio
2+
import time
3+
from datetime import timedelta
4+
from restack_ai import Restack
5+
from restack_ai.restack import ScheduleSpec, ScheduleIntervalSpec
6+
7+
from src.client import client
8+
9+
async def main():
10+
11+
workflow_id = f"{int(time.time() * 1000)}-ChildWorkflow"
12+
await client.schedule_workflow(
13+
workflow_name="ChildWorkflow",
14+
workflow_id=workflow_id,
15+
schedule=ScheduleSpec(
16+
intervals=[ScheduleIntervalSpec(
17+
every=timedelta(seconds=1)
18+
)]
19+
)
20+
)
21+
22+
exit(0)
23+
24+
def run_schedule_interval():
25+
asyncio.run(main())
26+
27+
if __name__ == "__main__":
28+
run_schedule_interval()

Diff for: community/lmnt/schedule_scale.py

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import asyncio
2+
import time
3+
from restack_ai import Restack
4+
5+
from src.workflows.workflow import ExampleWorkflowInput
6+
7+
async def main():
8+
9+
client = Restack()
10+
11+
workflow_id = f"{int(time.time() * 1000)}-ExampleWorkflow"
12+
await client.schedule_workflow(
13+
workflow_name="ExampleWorkflow",
14+
workflow_id=workflow_id,
15+
input=ExampleWorkflowInput(max_amount=50)
16+
)
17+
18+
exit(0)
19+
20+
def run_schedule_scale():
21+
asyncio.run(main())
22+
23+
if __name__ == "__main__":
24+
run_schedule_scale()

Diff for: community/lmnt/schedule_workflow.py

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import asyncio
2+
import time
3+
from restack_ai import Restack
4+
5+
from src.client import client
6+
from src.workflows.child import ChildWorkflowInput
7+
8+
async def main():
9+
10+
workflow_id = f"{int(time.time() * 1000)}-ChildWorkflow"
11+
run_id = await client.schedule_workflow(
12+
workflow_name="ChildWorkflow",
13+
workflow_id=workflow_id,
14+
input=ChildWorkflowInput(name="Hi, my name is John Doe", voice="morgan")
15+
)
16+
17+
await client.get_workflow_result(
18+
workflow_id=workflow_id,
19+
run_id=run_id
20+
)
21+
22+
exit(0)
23+
24+
def run_schedule_workflow():
25+
asyncio.run(main())
26+
27+
if __name__ == "__main__":
28+
run_schedule_workflow()

Diff for: community/lmnt/src/__init__.py

Whitespace-only changes.

Diff for: community/lmnt/src/client.py

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import os
2+
from restack_ai import Restack
3+
from restack_ai.restack import CloudConnectionOptions
4+
from dotenv import load_dotenv
5+
6+
# Load environment variables from a .env file
7+
load_dotenv()
8+
9+
10+
engine_id = os.getenv("RESTACK_ENGINE_ID")
11+
address = os.getenv("RESTACK_ENGINE_ADDRESS")
12+
api_key = os.getenv("RESTACK_ENGINE_API_KEY")
13+
api_address = os.getenv("RESTACK_ENGINE_API_ADDRESS")
14+
15+
connection_options = CloudConnectionOptions(
16+
engine_id=engine_id,
17+
address=address,
18+
api_key=api_key,
19+
api_address=api_address
20+
)
21+
client = Restack(connection_options)

Diff for: community/lmnt/src/functions/__init__.py

Whitespace-only changes.

Diff for: community/lmnt/src/functions/function.py

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
from restack_ai.function import function, log, FunctionFailure
2+
3+
tries = 0
4+
5+
from pydantic import BaseModel
6+
7+
class ExampleFunctionInput(BaseModel):
8+
name: str
9+
10+
@function.defn()
11+
async def example_function(input: ExampleFunctionInput) -> str:
12+
try:
13+
global tries
14+
15+
if tries == 0:
16+
tries += 1
17+
raise FunctionFailure(message="Simulated failure", non_retryable=False)
18+
19+
log.info("example function started", input=input)
20+
return f"Hello, {input.name}!"
21+
except Exception as e:
22+
log.error("example function failed", error=e)
23+
raise e

Diff for: community/lmnt/src/functions/list_voices.py

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
"""Module for LMNT speech synthesis functionality."""
2+
import os
3+
from typing import Dict, Any
4+
from restack_ai.function import function, FunctionFailure
5+
from .utils.client import lmnt_client
6+
7+
@function.defn()
8+
async def lmnt_list_voices() -> Dict[str, Any]:
9+
client = None
10+
try:
11+
client = await lmnt_client()
12+
voices = await client.list_voices()
13+
return {"voices": voices}
14+
except Exception as e:
15+
raise FunctionFailure(f"Failed to list voices: {str(e)}", non_retryable=True) from e
16+
finally:
17+
if client and hasattr(client, 'close'):
18+
await client.close()

0 commit comments

Comments
 (0)