Back to Blog
8 min read

Building FAQ Bots with QnA Maker

QnA Maker is a cloud-based NLP service that allows you to create a natural conversational layer over your data. It’s perfect for building FAQ bots, help desk assistants, and knowledge bases that can answer questions in natural language.

Creating a QnA Maker Knowledge Base

# Create QnA Maker resource
az cognitiveservices account create \
    --name myqnamaker \
    --resource-group myResourceGroup \
    --kind QnAMaker \
    --sku S0 \
    --location westus

# Create App Service for runtime
az appservice plan create \
    --name myqnaplan \
    --resource-group myResourceGroup \
    --sku S1

az webapp create \
    --name myqnaruntime \
    --resource-group myResourceGroup \
    --plan myqnaplan

Building Knowledge Base Programmatically

import requests
import json
import time

class QnAMakerClient:
    def __init__(self, subscription_key, endpoint, runtime_endpoint):
        self.subscription_key = subscription_key
        self.endpoint = endpoint
        self.runtime_endpoint = runtime_endpoint

    def create_knowledge_base(self, name, qna_pairs, urls=None, files=None):
        """Create a new knowledge base."""
        create_url = f"{self.endpoint}/qnamaker/v4.0/knowledgebases/create"

        body = {
            "name": name,
            "qnaList": qna_pairs,
            "urls": urls or [],
            "files": files or []
        }

        headers = {
            "Ocp-Apim-Subscription-Key": self.subscription_key,
            "Content-Type": "application/json"
        }

        response = requests.post(create_url, headers=headers, json=body)
        operation_id = response.headers.get("Location").split("/")[-1]

        # Wait for creation to complete
        return self._wait_for_operation(operation_id)

    def _wait_for_operation(self, operation_id):
        """Poll for operation completion."""
        status_url = f"{self.endpoint}/qnamaker/v4.0/operations/{operation_id}"
        headers = {"Ocp-Apim-Subscription-Key": self.subscription_key}

        while True:
            response = requests.get(status_url, headers=headers)
            result = response.json()

            if result["operationState"] == "Succeeded":
                return result["resourceLocation"].split("/")[-1]
            elif result["operationState"] == "Failed":
                raise Exception(f"Operation failed: {result}")

            time.sleep(5)

    def add_qna_pairs(self, kb_id, qna_pairs):
        """Add Q&A pairs to existing knowledge base."""
        update_url = f"{self.endpoint}/qnamaker/v4.0/knowledgebases/{kb_id}"

        body = {
            "add": {
                "qnaList": qna_pairs
            }
        }

        headers = {
            "Ocp-Apim-Subscription-Key": self.subscription_key,
            "Content-Type": "application/json"
        }

        response = requests.patch(update_url, headers=headers, json=body)
        return response.status_code == 204

    def publish(self, kb_id):
        """Publish the knowledge base."""
        publish_url = f"{self.endpoint}/qnamaker/v4.0/knowledgebases/{kb_id}"
        headers = {"Ocp-Apim-Subscription-Key": self.subscription_key}

        response = requests.post(publish_url, headers=headers)
        return response.status_code == 204

    def get_answer(self, kb_id, question, top=3):
        """Query the knowledge base for answers."""
        query_url = f"{self.runtime_endpoint}/qnamaker/knowledgebases/{kb_id}/generateAnswer"

        body = {
            "question": question,
            "top": top,
            "scoreThreshold": 30,
            "strictFilters": [],
            "context": None
        }

        headers = {
            "Authorization": f"EndpointKey {self.endpoint_key}",
            "Content-Type": "application/json"
        }

        response = requests.post(query_url, headers=headers, json=body)
        return response.json()


# Example usage
client = QnAMakerClient(
    subscription_key="your-subscription-key",
    endpoint="https://your-resource.cognitiveservices.azure.com",
    runtime_endpoint="https://your-runtime.azurewebsites.net"
)

# Define Q&A pairs
qna_pairs = [
    {
        "id": 1,
        "answer": "Our store hours are Monday to Friday, 9 AM to 6 PM, and Saturday 10 AM to 4 PM.",
        "source": "manual",
        "questions": [
            "What are your store hours?",
            "When are you open?",
            "What time do you open?",
            "What time do you close?"
        ],
        "metadata": [
            {"name": "category", "value": "general"}
        ]
    },
    {
        "id": 2,
        "answer": "You can return items within 30 days of purchase with a receipt. Items must be unused and in original packaging.",
        "source": "manual",
        "questions": [
            "What is your return policy?",
            "Can I return an item?",
            "How do I return something?",
            "What is the refund policy?"
        ],
        "metadata": [
            {"name": "category", "value": "returns"}
        ]
    },
    {
        "id": 3,
        "answer": "Standard shipping takes 5-7 business days. Express shipping takes 2-3 business days. Free shipping on orders over $50.",
        "source": "manual",
        "questions": [
            "How long does shipping take?",
            "What are the shipping options?",
            "Do you offer free shipping?",
            "How much is shipping?"
        ],
        "metadata": [
            {"name": "category", "value": "shipping"}
        ]
    }
]

# Create knowledge base
kb_id = client.create_knowledge_base("Customer Support FAQ", qna_pairs)
print(f"Knowledge base created: {kb_id}")

# Publish
client.publish(kb_id)
print("Knowledge base published")

# Query
result = client.get_answer(kb_id, "When are you open?")
print(f"Answer: {result['answers'][0]['answer']}")
print(f"Confidence: {result['answers'][0]['score']}")

Multi-Turn Conversations

# Define multi-turn Q&A with follow-up prompts
multi_turn_qna = [
    {
        "id": 10,
        "answer": "We offer several payment methods. Which would you like to know about?",
        "source": "manual",
        "questions": [
            "What payment methods do you accept?",
            "How can I pay?"
        ],
        "metadata": [],
        "context": {
            "isContextOnly": False,
            "prompts": [
                {
                    "displayOrder": 1,
                    "displayText": "Credit Cards",
                    "qnaId": 11
                },
                {
                    "displayOrder": 2,
                    "displayText": "PayPal",
                    "qnaId": 12
                },
                {
                    "displayOrder": 3,
                    "displayText": "Bank Transfer",
                    "qnaId": 13
                }
            ]
        }
    },
    {
        "id": 11,
        "answer": "We accept Visa, MasterCard, American Express, and Discover. All cards are processed securely.",
        "source": "manual",
        "questions": ["Credit cards", "Tell me about credit cards"],
        "metadata": [],
        "context": {
            "isContextOnly": True,
            "prompts": []
        }
    },
    {
        "id": 12,
        "answer": "PayPal is available at checkout. You can use your PayPal balance or linked cards.",
        "source": "manual",
        "questions": ["PayPal", "Tell me about PayPal"],
        "metadata": [],
        "context": {
            "isContextOnly": True,
            "prompts": []
        }
    },
    {
        "id": 13,
        "answer": "Bank transfers are available for orders over $500. Contact support for wire transfer details.",
        "source": "manual",
        "questions": ["Bank transfer", "Wire transfer"],
        "metadata": [],
        "context": {
            "isContextOnly": True,
            "prompts": []
        }
    }
]

# Query with context for multi-turn
def get_answer_with_context(kb_id, question, previous_qna_id=None, previous_question=None):
    """Query with conversation context."""
    body = {
        "question": question,
        "top": 3,
        "scoreThreshold": 30
    }

    if previous_qna_id:
        body["context"] = {
            "previousQnAId": previous_qna_id,
            "previousUserQuery": previous_question
        }

    # Make API call...
    return result

Integrating with Bot Framework

// qnaBot.js
const { ActivityHandler } = require('botbuilder');
const { QnAMaker, QnAMakerEndpoint } = require('botbuilder-ai');

class QnABot extends ActivityHandler {
    constructor() {
        super();

        const endpoint = {
            knowledgeBaseId: process.env.QNA_KB_ID,
            endpointKey: process.env.QNA_ENDPOINT_KEY,
            host: process.env.QNA_HOST
        };

        this.qnaMaker = new QnAMaker(endpoint, {
            scoreThreshold: 30,
            top: 3
        });

        this.onMessage(async (context, next) => {
            // Get QnA answer
            const qnaResults = await this.qnaMaker.getAnswers(context);

            if (qnaResults.length > 0) {
                const topAnswer = qnaResults[0];

                // Send answer
                await context.sendActivity(topAnswer.answer);

                // Handle follow-up prompts
                if (topAnswer.context && topAnswer.context.prompts.length > 0) {
                    await this.sendPrompts(context, topAnswer.context.prompts);
                }
            } else {
                await context.sendActivity(
                    "I couldn't find an answer to your question. " +
                    "Please try rephrasing or contact support."
                );
            }

            await next();
        });
    }

    async sendPrompts(context, prompts) {
        const { CardFactory } = require('botbuilder');

        const card = {
            type: 'AdaptiveCard',
            $schema: 'http://adaptivecards.io/schemas/adaptive-card.json',
            version: '1.3',
            body: [
                {
                    type: 'TextBlock',
                    text: 'You might also want to know:',
                    wrap: true
                }
            ],
            actions: prompts.map(prompt => ({
                type: 'Action.Submit',
                title: prompt.displayText,
                data: { question: prompt.displayText }
            }))
        };

        await context.sendActivity({
            attachments: [CardFactory.adaptiveCard(card)]
        });
    }
}

module.exports.QnABot = QnABot;

Active Learning

def train_with_feedback(client, kb_id, question, answer_id):
    """Submit user feedback for active learning."""
    train_url = f"{client.endpoint}/qnamaker/v4.0/knowledgebases/{kb_id}/train"

    body = {
        "feedbackRecords": [
            {
                "userId": "user123",
                "userQuestion": question,
                "qnaId": answer_id
            }
        ]
    }

    headers = {
        "Ocp-Apim-Subscription-Key": client.subscription_key,
        "Content-Type": "application/json"
    }

    response = requests.post(train_url, headers=headers, json=body)
    return response.status_code == 204


# Get active learning suggestions
def get_suggestions(client, kb_id):
    """Get suggested alternative questions."""
    suggestions_url = f"{client.endpoint}/qnamaker/v4.0/knowledgebases/{kb_id}/alterations"

    headers = {"Ocp-Apim-Subscription-Key": client.subscription_key}
    response = requests.get(suggestions_url, headers=headers)

    return response.json()


# Add alternative questions based on suggestions
def accept_suggestion(client, kb_id, qna_id, new_question):
    """Add suggested question to QnA pair."""
    update_url = f"{client.endpoint}/qnamaker/v4.0/knowledgebases/{kb_id}"

    body = {
        "update": {
            "qnaList": [
                {
                    "id": qna_id,
                    "questions": {
                        "add": [new_question]
                    }
                }
            ]
        }
    }

    headers = {
        "Ocp-Apim-Subscription-Key": client.subscription_key,
        "Content-Type": "application/json"
    }

    response = requests.patch(update_url, headers=headers, json=body)
    return response.status_code == 204

Import from URLs and Files

# Create KB from website FAQs
kb_with_urls = {
    "name": "Product FAQ",
    "qnaList": [],
    "urls": [
        "https://www.example.com/faq",
        "https://www.example.com/support/help"
    ],
    "files": []
}

# Create KB from documents
kb_with_files = {
    "name": "Documentation KB",
    "qnaList": [],
    "urls": [],
    "files": [
        {
            "fileName": "product-manual.pdf",
            "fileUri": "https://storage.blob.core.windows.net/docs/product-manual.pdf"
        },
        {
            "fileName": "faq.docx",
            "fileUri": "https://storage.blob.core.windows.net/docs/faq.docx"
        }
    ]
}

# Supported file types:
# - PDF
# - DOCX
# - TXT
# - TSV (tab-separated Q&A pairs)
# - XLSX (Excel with Question/Answer columns)

Analytics and Monitoring

from azure.monitor.query import LogsQueryClient
from azure.identity import DefaultAzureCredential

def get_qna_analytics(workspace_id, days=7):
    """Get QnA Maker usage analytics from Application Insights."""
    credential = DefaultAzureCredential()
    client = LogsQueryClient(credential)

    query = f"""
    customEvents
    | where timestamp > ago({days}d)
    | where name == "QnAMessage"
    | extend question = tostring(customDimensions.Question)
    | extend answer = tostring(customDimensions.Answer)
    | extend score = todouble(customDimensions.Score)
    | extend kbId = tostring(customDimensions.KnowledgeBaseId)
    | summarize
        TotalQueries = count(),
        AvgScore = avg(score),
        LowScoreCount = countif(score < 50)
        by bin(timestamp, 1d)
    | order by timestamp desc
    """

    response = client.query_workspace(workspace_id, query, timespan=None)
    return response.tables[0]


def get_unanswered_questions(workspace_id, days=7, threshold=30):
    """Find questions with low confidence scores."""
    credential = DefaultAzureCredential()
    client = LogsQueryClient(credential)

    query = f"""
    customEvents
    | where timestamp > ago({days}d)
    | where name == "QnAMessage"
    | extend question = tostring(customDimensions.Question)
    | extend score = todouble(customDimensions.Score)
    | where score < {threshold}
    | summarize Count = count() by question
    | order by Count desc
    | take 50
    """

    response = client.query_workspace(workspace_id, query, timespan=None)
    return response.tables[0]

Conclusion

QnA Maker simplifies building knowledge-based chatbots:

  • Easy content import from URLs, documents, and manual entry
  • Natural language understanding for question matching
  • Multi-turn conversations with follow-up prompts
  • Active learning to improve accuracy over time
  • Bot Framework integration for multi-channel deployment

It’s the fastest path to creating FAQ bots and customer support assistants.

Michael John Pena

Michael John Pena

Senior Data Engineer based in Sydney. Writing about data, cloud, and technology.