Skip to main content

Part 4: Conversational Intelligence with Copilot Studio (PVA)

🔹 Introduction: Giving Bots a Brain

To go beyond basic replies, your bot needs memory, logic, and intent recognition. Enter Copilot Studio via Direct Line API.

🧠 What is Copilot Studio (formerly PVA)?

Copilot Studio allows you to build conversational bots powered by Microsoft’s AI and natural language understanding. It can handle logic, flows, and memory.

🌐 Why Use Direct Line API?

Direct Line API allows external apps (e.g., Azure Functions or custom backends) to securely send and receive messages with your Copilot Studio bot.

🔁 Token Expiry & Caching Tip:


Direct Line tokens typically expire in 24 hours. Caching these tokens per user session reduces overhead and improves responsiveness.

Token Management and Caching

def _get_cached_token(from_user: str) -> dict | None:
    current_time = datetime.now()
    expired_users = [
        user for user, data in token_cache.items()
        if current_time - data['created_at'] >= timedelta(hours=24)
    ]
    for expired_user in expired_users:
        del token_cache[expired_user]
    if from_user in token_cache:
        cached_data = token_cache[from_user]
        if current_time - cached_data['created_at'] < timedelta(hours=24):
            return cached_data
    return None

Creating a Direct Line Token and Conversation

def get_directline_token(self, from_user: str) -> dict:
    user_id = f'dl_{uuid.uuid4()}'
    direct_line_endpoint = f"{self.PVA_TOKEN_ENDPOINT}conversations/"
    headers = {
        'Authorization': f'Bearer {self.DIRECT_LINE_SECRET}',
        'Content-Type': 'application/json'
    }
    payload = {'user': {'id': from_user}}
    response = requests.post(direct_line_endpoint, headers=headers, json=payload)
    response.raise_for_status()
    return response.json()

Posting Context and Messages to Copilot Studio

def set_mobile_number(self, token: str, conv_id: str, user_id: str, text: str):
    url = f"{self.PVA_TOKEN_ENDPOINT}conversations/{conv_id}/activities"
    headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
    payload = {
        "type": "event",
        "name": "UserContext",
        "from": { "id": user_id },
        "value": { "userMobileNumber": text }
    }
    requests.post(url, headers=headers, json=payload)

def post_message(
    self,
    token: str,
    conv_id: str,
    user_id: str,
    text: str,
    product_name: str = None,
    used_info_template: bool = False,
location: str = None
):
    url = f"{self.PVA_TOKEN_ENDPOINT}conversations/{conv_id}/activities"
    headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
    data = {
        "type": "message",
        "from": {"id": user_id},
        "text": text,
        "value": {
            "userMobileNumber": user_id,
            "productName": product_name if product_name else "Unknown Product",
            "usedInfoTemplate": used_info_template,
            "location": location if location else "Not Selected"
        }
    }
    requests.post(url, headers=headers, json=data)

Fetching Bot Replies

def get_reply(self, token, conv_id, watermark=None, processed_watermarks=None, timeout_sec=20):
    url = f"{self.PVA_TOKEN_ENDPOINT}conversations/{conv_id}/activities"
    headers = {"Authorization": f"Bearer {token}"}
    params = {"watermark": watermark} if watermark else {}
    start = time.time()
    bot_activities = []
    last_watermark = watermark or "0"
    processed_watermarks = processed_watermarks or set()
    while time.time() - start < timeout_sec:
        r = requests.get(url, headers=headers, params=params, timeout=10)
        r.raise_for_status()
        data = r.json()
        activities = data.get("activities", [])
        if not activities:
            time.sleep(0.5)
            continue
        current_watermark = data.get("watermark")
        new_activities = [
            {
                "message": act["text"],
                "suggested_actions": [
                    {"title": action["title"], "value": action["value"]}
                    for action in act.get("suggestedActions", {}).get("actions", [])
                ] if "suggestedActions" in act else [],
                "attachments": act.get("attachments", []),
                "watermark": extract_watermark_number(act.get("id", "0"))
            }
            for act in activities
            if (
                act.get("from", {}).get("role") == "bot"
                and act.get("text")
                and extract_watermark_number(act.get("id", "0")) not in processed_watermarks
            )
        ]
        if new_activities:
            processed_watermarks.update(act["watermark"] for act in new_activities)
            bot_activities.extend(new_activities)
            last_watermark = current_watermark
            return bot_activities, last_watermark
        time.sleep(0.5)
    return bot_activities, last_watermark

Tips:

  • Use watermarks to avoid duplicate processing.
  • Pass user context (e.g., mobile number, product selection) for personalized responses.
  • Implement token and session caching for efficiency.

🔹 Engagement Tip:

Your bot can now recognize “reschedule my order” and respond intelligently. Think of it as your AI-powered front desk.

Section Summary
In this part, we added intelligence to your bot using Copilot Studio (formerly Power Virtual Agents) and the Direct Line API. You learned how to manage authentication tokens, initiate conversations, and post contextual messages to the bot. These capabilities give your chatbot the ability to understand intent, respond logically, and handle complex dialogs.