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

> Source: <https://dev.to/fazil_hasanov_8150a43b0ff/building-a-conversational-ai-agent-with-python-and-rasa-a-step-by-step-guide-1fig>
> Published: 2026-06-05 17:04:59+00:00

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.

| 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
```

Verify the installation:

```
rasa --version
# Expected output: Rasa Open Source 3.x.x
```

Create a fresh directory and initialise a Rasa project:

```
mkdir travel-bot && cd travel-bot
rasa init --no-prompt
```

The command scaffolds the following structure:

```
travel-bot/
├─ actions/
│  └─ actions.py
├─ data/
│  ├─ nlu.yml
│  └─ stories.yml
├─ config.yml
├─ domain.yml
└─ credentials.yml
```

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

`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
```

**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.

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
```

**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/...
```

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
```

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.

The `action_flight_status`

action contacts a mock API to retrieve real‑time flight data. Replace `actions/actions.py`

with:

``` python
# 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)]
```

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

``` python
# 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)
```

Run it in the background and change `API_ENDPOINT`

to `http://localhost:5001/status`

.

```
rasa run actions
# Output: Action endpoint listening on 5055
```

Leave this terminal open; Rasa Core will call the endpoint whenever `action_flight_status`

is triggered.

Launch the chatbot in the shell:

```
rasa shell
```

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! 👋
```

**
