Skip to content

Getting Started with Genie

Prerequisites

You should start off with the following prerequisites:

  1. Python 3.11 or above
  2. Docker
  3. pip
  4. Any IDE of your choice
  5. 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:

docker-compose.yml
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:

flow.py
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:

meta.yaml
renderer:
That is it. This tells Genie that any template in this directory (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:

intro.jinja2
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:

meta.yaml
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:

ai_creates_response.jinja2
- 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:

config.yaml
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:

  1. 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
  2. Details for the Celery instance
  3. What broker to use (here we use Redis)
  4. What backend to use (also Redis)
  5. Some configurations of the Genie environment
  6. What is the top-most directory of our templates (here set to the local directory)
  7. How many instances of invokers each worker should instantiate (one is enough)
  8. The Cross-Origin Resource Sharing (CORS) configuration that our API should accept
  9. For development purposes, we are quite lenient and allow everything
  10. 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:

main.py
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.

  1. We create the Genie engine
  2. We register our templates
  3. 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)