DEV Community

Fazil Hasanov
Fazil Hasanov

Posted on

Building a Conversational AI Agent with Python and Rasa: A Step‑by‑Step Guide

Introduction

Conversational AI is no longer a niche hobby; enterprises use it for customer support, lead qualification, and internal tooling. Rasa Open Source gives you a production‑ready stack that runs on‑premise, lets you keep data private, and offers full Python extensibility. In this article we’ll walk through a complete Rasa project from scratch, covering environment setup, NLU training, story‑driven dialogue management, custom actions, testing, and deployment. By the end you’ll have a working chatbot that can greet users, answer FAQs, and fetch dynamic data from an external API.

Actionable Insight – Start every Rasa project in its own virtual environment. It isolates dependencies and makes CI/CD pipelines deterministic.

Prerequisites

Requirement Version
Python 3.9‑3.11
Rasa 3.6+
pip latest
git any

You’ll also need a basic familiarity with YAML and Python. If you haven’t installed Rasa yet, run:

python -m venv rasa-env
source rasa-env/bin/activate   # Windows: .\rasa-env\Scripts\activate
pip install --upgrade pip
pip install rasa
Enter fullscreen mode Exit fullscreen mode

Verify the installation:

rasa --version
# Expected output: Rasa Open Source 3.x.x
Enter fullscreen mode Exit fullscreen mode

Project Skeleton

Create a fresh directory and initialise a Rasa project:

mkdir travel-bot && cd travel-bot
rasa init --no-prompt
Enter fullscreen mode Exit fullscreen mode

The command scaffolds the following structure:

travel-bot/
├─ actions/
│  └─ actions.py
├─ data/
│  ├─ nlu.yml
│  └─ stories.yml
├─ config.yml
├─ domain.yml
└─ credentials.yml
Enter fullscreen mode Exit fullscreen mode

We’ll replace the auto‑generated files with our own definitions.

Defining the Domain

domain.yml is the single source of truth for intents, entities, slots, actions, and responses. For a travel‑assistant bot we need:

version: "3.0"

intents:
  - greet
  - goodbye
  - ask_flight_status
  - inform

entities:
  - flight_number

slots:
  flight_number:
    type: text
    influence_conversation: false

responses:
  utter_greet:
    - text: "Hey there! I’m your travel assistant. How can I help you today?"
  utter_goodbye:
    - text: "Safe travels! 👋"
  utter_ask_flight:
    - text: "Sure, could you share the flight number?"
  utter_flight_status:
    - text: "Fetching status for flight {flight_number}..."
  utter_flight_not_found:
    - text: "I couldn’t locate that flight. Please double‑check the number."

actions:
  - action_flight_status
Enter fullscreen mode Exit fullscreen mode

Why this matters – Slots are lightweight containers for user‑provided data (e.g., a flight number). By declaring influence_conversation: false we tell the dialogue policy not to treat the slot as a decision factor, which keeps the conversation flow deterministic.

Training the NLU Model

Rasa’s NLU component learns from annotated examples. Replace data/nlu.yml with:

version: "3.0"
nlu:
  - intent: greet
    examples: |
      - hi
      - hello
      - hey there
      - good morning

  - intent: goodbye
    examples: |
      - bye
      - see you later
      - goodbye
      - catch you later

  - intent: ask_flight_status
    examples: |
      - what's the status of flight AA123?
      - can you check flight BA456?
      - flight status for DL789
      - I need the status of flight LH321
    entities:
      - flight_number: AA123
      - flight_number: BA456
      - flight_number: DL789
      - flight_number: LH321

  - intent: inform
    examples: |
      - it's AA123
      - flight number is BA456
      - the flight is DL789
Enter fullscreen mode Exit fullscreen mode

Actionable Insight – Use the entities: block inside each example to seed the entity extractor. This speeds up convergence and reduces the need for a large dataset.

Train the model:

rasa train
# Output: Model trained successfully. Model path: models/...
Enter fullscreen mode Exit fullscreen mode

Story‑Based Dialogue Management

Stories encode the expected path of a conversation. Create data/stories.yml:

version: "3.0"
stories:
  - story: greet and ask flight status
    steps:
      - intent: greet
      - action: utter_greet
      - intent: ask_flight_status
      - action: utter_ask_flight
      - intent: inform
        entities:
          flight_number: "AA123"
      - slot_was_set:
          - flight_number: "AA123"
      - action: action_flight_status
      - action: utter_flight_status

  - story: goodbye
    steps:
      - intent: goodbye
      - action: utter_goodbye
Enter fullscreen mode Exit fullscreen mode

Rasa’s default policy (TED Policy) will learn a finite‑state machine from these stories. For more complex flows you can add rules.yml or use form actions, but the above suffices for a minimal demo.

Implementing a Custom Action

The action_flight_status action contacts a mock API to retrieve real‑time flight data. Replace actions/actions.py with:

# actions/actions.py
import requests
from typing import Any, Text, Dict, List

from rasa_sdk import Action, Tracker
from rasa_sdk.executor import CollectingDispatcher
from rasa_sdk.events import SlotSet

API_ENDPOINT = "https://api.mockflight.com/status"


class ActionFlightStatus(Action):
    """Calls an external flight‑status API and returns a friendly message."""

    def name(self) -> Text:
        return "action_flight_status"

    def run(
        self,
        dispatcher: CollectingDispatcher,
        tracker: Tracker,
        domain: Dict[Text, Any],
    ) -> List[Dict[Text, Any]]:
        flight_number = tracker.get_slot("flight_number")
        if not flight_number:
            dispatcher.utter_message(text="I need a flight number first.")
            return []

        try:
            response = requests.get(f"{API_ENDPOINT}/{flight_number}", timeout=5)
            response.raise_for_status()
            data = response.json()
            status = data.get("status", "unknown")
            message = f"✈️ Flight {flight_number} is currently *{status}*."
        except requests.RequestException:
            message = "❗️ I couldn’t reach the flight service. Please try again later."
        except KeyError:
            message = "❓ I didn’t understand the response from the service."

        dispatcher.utter_message(text=message)
        # Clear the slot so the bot can handle a new query cleanly
        return [SlotSet("flight_number", None)]
Enter fullscreen mode Exit fullscreen mode

Tip – If you don’t have a real API, spin up a simple Flask mock:

# mock_flight_api.py
from flask import Flask, jsonify
app = Flask(__name__)

@app.route("/status/<flight>")
def status(flight):
    return jsonify({"status": "on time" if flight.endswith("1") else "delayed"})

if __name__ == "__main__":
    app.run(port=5001)
Enter fullscreen mode Exit fullscreen mode

Run it in the background and change API_ENDPOINT to http://localhost:5001/status.

Running the Action Server

rasa run actions
# Output: Action endpoint listening on 5055
Enter fullscreen mode Exit fullscreen mode

Leave this terminal open; Rasa Core will call the endpoint whenever action_flight_status is triggered.

Interactive Testing

Launch the chatbot in the shell:

rasa shell
Enter fullscreen mode Exit fullscreen mode

Sample interaction:

User: hi
Bot: Hey there! I’m your travel assistant. How can I help you today?
User: what's the status of flight AA123?
Bot: Sure, could you share the flight number?
User: it's AA123
Bot: Fetching status for flight AA123...
✈️ Flight AA123 is currently *on time*.
User: bye
Bot: Safe travels! 👋
Enter fullscreen mode Exit fullscreen mode

**

Top comments (0)