Getting Started with Genie
Prerequisites
You should start off with the following prerequisites:
- Python 3.11 or above
- Docker
- pip
- Any IDE of your choice
- Access to the API of an LLM
We will use a locally running Ollama model, but there are invokers for many more LLMs.
Installation
Installing your environment
Locally running LLM
Ensure that you have Ollama running. See the Ollama documentation on how to download and Install Ollama.
Start the Ollama service. On Linux, by running sudo systemctl start ollama
, but depending
on your environment and the way you installed Ollama.
Make sure you have pulled down the gemma3:1b
model by running ollama pull gemma3:1b
.
Create your virtual environment
Create and move into the directory that will hold your Genie.
mkdir my-first-genie
cd my-first-genie
Create and activate your virtual Python environment
python -m venv .venv
source .venv/bin/activate
Install dependencies
We rely on a number of packages. Create a file called requirements.txt
and paste the following
content:
genie-flow
genie-flow-invoker-ollama
And then run the following command to install the required packages:
pip install -r requirements.txt
This will download the Genie Flow and Genie Flow Invoker Ollama packages from PyPi. With it will come the dependencies. Most notable are Celery (to implement the worker paradigm) and FastAPI (to serve the Genie API). Other important ones include Python State Machine (the core state machine package), Pydantic (data validation) and Jinja2 (the templating engine). There are, of course, other dependencies, but pip will ensure all of these are installed.
Start the necessary services
The Genie framework maintains state on the server. That means, the only thing a client will have
to share with the API, is a session_id
and any new content that is required. The state of the
machine, the chat history and any properties added to the data model, all of these are retained
on the server side.
Genie relies on Redis and MongoDB for this persistence. Redis for active sessions and MongoDB for sessions that have lapsed from Redis but should still be retrievable.
The quickest way to spin up these services is by using the following docker-compose.yml
file. Create that file in the root of your project, and paste the following content:
services:
redis:
image: redis:latest
ports:
- "6379:6379"
mongodb:
image: genie-dev-mongodb:latest
ports:
- "27017:27017"
environment:
MONGO_INITDB_ROOT_USERNAME: dev_user
MONGO_INITDB_ROOT_PASSWORD: dev_password
MONGO_INITDB_DATABASE: genie_db
Then start these two services, using docker compose up -d
which will pull the redis image as well as
the specially prepared MongoDB empty database.
You can check if these services are up by running docker compose ps -a
.
Create your agent's directories
Next, create a directory where the code for your agent will live. Later on, we will create some of the necessary plumbing files (for configuration) right next to this agent directory.
$ mkdir my_first_agent
$ cd my_first_agent
$ mkdir -p templates/{response,llm}
Define your Agent
The Agent Flow
We now need to create our agent flow. Let's do that by creating a file called flow.py
inside our agent directory my_first_agent
. Copy the following content there:
from genie_flow.genie import GenieModel, GenieStateMachine
from statemachine import State
class MyFirstAgent(GenieAgent):
@classmethod
def get_state_machine_class(cls) -> type[GenieStateMachine]:
return MyFirstMachine
class MyFirstMachine(GenieStateMachine):
# STATES
intro = State(initial=True, value=100)
ai_creates_response = State(value=200)
user_enters_query = State(value=300)
# EVENTS AND TRANSITIONS
user_input = (
intro.to(ai_creates_response)
| user_enters_query.to(ai_creates_response)
)
processing_done = (
ai_creates_response.to(user_enters_query)
)
# TEMPLATES
templates = dict(
intro="response/intro.jinja2",
ai_creates_response="llm/ai_creates_response.jinja2",
user_enters_query="response/user_enters_query.jinja2",
)
This defines your entire agent (except for the prompts).
Create the Renderer Templates
In the directory my_first_agent/templates/response
create a file called meta.yaml
, with
the following content:
renderer:
my_first_agent/templates/response
)
will be used to render output back to the human actor.
Next, add a file called intro.jinja2
to the same directory, with the following content:
Welcome to this simple Question and Answer dialogue Genie Flow example!
How can I help? Please go ahead and ask me anything.
This is the introductory text that the agent uses to start the dialogue. Since this template
lives in a directory for which the meta.yaml
file specifies renderer:
, the Genie engine knows
that this template is solely going to be used to render output back to the user.
Finally, we want to be able to send the output from the AI back to the user too. This is done in
a template called user_enters_query.jinja2
. Create that file and put the following content in it:
{ actor_input }
This template will render the input given by "the previous actor". In our case, the previous actor will have been the LLM, so this template renders the output back to the user.
Create the LLM Templates
The template that is used to render the prompt that we want to send to the LLM lives in the directory
my_first_agent/templates/llm
. Let's create the meta.yaml
file there, with the following content:
invoker:
type: genie_flow_invoker.invoker.ollama.OllamaChatInvoker
ollama_url: localhost:11434
model: gemma3:1b
This tells the Genie engine that templates in this directory will have to be used to call the
OllamaChatInvoker
. So, the engine will render any template here and send it off to the Ollama
Chat completion invocation. That invoker expects a YAML
content of the chat history that it
will need to complete, or respond to. Let's create a file here, called ai_creates_response.jinja2
and fill this file with the following content:
- role: system
content: |
You are a friendly chatbot, aiming to have a dialogue with a human user.
Your aim is to respond logically, taking the dialogue you had into account.
Be succinct, to the point, but friendly.
Stick to the language that the user start their conversation in.
{{ chat_history }}
- role: user
content: |
{{ actor_input|indent(width=4, first=True) }}
This template is YAML speak for a list of objects. Each object has two properties: role
and content
.
It represents the chat history that Ollama will need to complete.
The line that says {{ chat_history }}
is the template placeholder for a property called
chat_history
. This property contains the string representation of the chat history in YAML
syntax. At some point, it might look like:
- role: user
content: |
hey, good morning. how are you?
- role: assistant
content: |
Good morning to you too! I’m doing well, thank you for asking. Just here, ready to assist. 😊
How about you? How’s your day going so far?
This chat history will be placed at the position in the template where it now says {{ chat_history }}
after which, the template continues to add a final user
message. If the user input was "I want to fly
to the moon", that will be the content of the property actor_input
. So the final part of the template
will render to:
- role: user
content: |
I want to fly to the moon
The cryptic |indent(width=4, first=True)
will make the Jinja engine indent the content of
actor_input
by four spaces, starting with the first line. This is required to make sure that the
entire template renders as correct YAML.
Concluding
We should now have a directory structure that looks as follows:
my_first_agent/
my_first_agent/templates/
my_first_agent/templates/response
my_first_agent/templates/response/meta.yaml
my_first_agent/templates/response/intro.jinja2
my_first_agent/templates/response/user_enters_query.jina2
my_first_agent/templates/llm/
my_first_agent/templates/llm/meta.yaml
my_first_agent/templates/llm/ai_creates_response.jinja2
my_first_agent/flow.py
This is everything we need to define our agent. The agent flow is defined in flow.py
, the necessary
templates in their respective directories, each with a meta.yaml
file that tells the Genie engine
how to deal with these templates.
Connecting the dots
The configuration
We need to configure the Genie engine that we are about to start. That configuration is done through
a file called config.yaml
. Create that file right next to our my_first_agent
directory. Not
inside that directory. Let's paste the following content in there:
persistence:
application_prefix: my-first
object_store:
host: localhost
port: 6379
db: 1
object_compression: false
expiration_seconds: 86400
lock_store:
host: localhost
port: 6379
db: 2
expiration_seconds: 240
progress_store:
host: localhost
port: 6379
db: 3
expiration_seconds: 240
mongo_store:
host: localhost
port: 27017
username: dev_user
password: dev_password
db: genie_db
celery:
broker: redis://localhost:6379/0
backend: redis://localhost:6379/0
redis_socket_timeout: 4.0
redis_socket_connect_timeout: 4.0
api:
debug: true
root_path: /api/v1
genie_environment:
template_root_path: .
pool_size: 1
invokers:
cors:
allow_origins:
- "*"
allow_credentials: true
allow_methods:
- "*"
allow_headers:
- "*"
This configuration tells the Genie engine:
- How to store session details
- In what Redis database it should retain all components of sessions
- Which instance of MongoDB should be used to persist session data
- Details for the Celery instance
- What broker to use (here we use Redis)
- What backend to use (also Redis)
- Some configurations of the Genie environment
- What is the top-most directory of our templates (here set to the local directory)
- How many instances of invokers each worker should instantiate (one is enough)
- The Cross-Origin Resource Sharing (CORS) configuration that our API should accept
- For development purposes, we are quite lenient and allow everything
- In production situations, you may want to limit the sharing of resources, cross-origin
The main.py
plumbing
The final thing we need is a Python file that connects all the dots. In the root of our project, create
a file called main.py
. So, that file sites right next to the directory my_first_agent
, not
inside it! The content of that file is quite boilerplate:
from genie_flow import GenieFlow
from my_first_agent.flow import MyFirstAgent
# create an instance of the genie flow engine
genie_flow = GenieFlow.from_yaml("config.yaml")
# tell the engine where it can find the relevant templates
genie_flow.genie_environment.register_template_directory(
"response",
"my_first_agent/templates/response,
)
genie_flow.genie_environment.register_template_directory(
"llm",
"my_first_agent/templates/llm",
)
# register our agent
genie_flow.genie_environment.register_model("my_first", MyFirstAgent)
# short-cut to connect to the worker
celery_app = genie_flow.celery_app
This is all the plumbing that we need.
- We create the Genie engine
- We register our templates
- We register our Agent model
And we are done! All that is left is to start our agent.
Testing your Agent
During development, we typically use a simple command line interface to test our agents. Have a look at the cli front-end.
You can also spin up the web chat interface. That is a React front-end that can talk to the Genie API. It has a number of additional features, such as rendering Markdown, upload files, and more. Have a look at this page for this graphical front-end.
Starting your Agent
To start your agent, you need to make sure your Redis and MongoDB services are up. This is done by
docker compose ps
and checking to see if the two services defined in our docker-compose.yml
are running.
Now we can start our Agent. This is done by starting the agent Worker and then the agent API service. We typically run these two in separate terminals. But some people prefer to use their IDE's starting and stopping features, which will also make debugging easier.
Starting the Worker
The Worker is a process that listens to the Celery queue that lives in Redis. When new work (in Genie's case new works mean new invocations) is put on the queue, this Worker will pick it up and execute.
On the Celery website there is more information on how Celery workers are executed.
To start your worker, in a separate terminal, execute the following commands:
source .venv/bin/activate
celery --app main.celery_app worker --concurrency 2
This will start a worker with a concurrency of 2
, meaning that two tasks can be executed in parallel.
You will see something like this:
...
-------------- celery@datadude v5.5.2 (immunity)
--- ***** -----
-- ******* ---- Linux-6.15.8-arch1-1-x86_64-with-glibc2.41 2025-07-30 17:51:02
- *** --- * ---
- ** ---------- [config]
- ** ---------- .> app: genie_flow:0x7f3df1f76650
- ** ---------- .> transport: redis://localhost:6379/0
- ** ---------- .> results: redis://localhost:6379/0
- *** --- * --- .> concurrency: 2 (prefork)
-- ******* ---- .> task events: OFF (enable -E to monitor tasks in this worker)
--- ***** -----
-------------- [queues]
.> celery exchange=celery(direct) key=celery
Starting the API
In a new terminal, we can start the API. The Genie framework uses FastAPI to service the API. There are different
options to service a FastAPI API. We have good experience with uvicorn
.
Install uvicorn
as follows:
source .venv/bin/activate
pip install uvicorn
To start the API, execute the following:
source .venv/bin/activate
uvicorn main:genie_flow.fastapi_app --host 0.0.0.0
This will start running the API on all of the network interfaces of your machine.
Your output should be something like this:
INFO: Will watch for changes in these directories: ['/home/willem/development/chatgpgenie']
INFO: Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)
INFO: Started reloader process [29277] using WatchFiles
...
INFO: Started server process [29290]
INFO: Waiting for application startup.
INFO: Application startup complete.
Give it a twirl
Now we have a worker, and the API is running!
Let's start one more terminal and do the following:
source .venv/bin/activate
python test_dialogue.py
With the test script we created before, we should now be welcomed by our first chatbot!
Welcome to this simple Question and Answer dialogue Genie Flow example!
How can I help? Please go ahead and ask me anything.
>> Hey, good morning. How are things with you?
Good morning to you too! I’m doing well, thank you for asking. Just here, ready to assist.
How about you? How’s your day going so far?
>> _
You can now directly interact with your LLM.
Where, from here?
(TODO - DOES ANYTHING NEED TO BE ADDED HERE)