microsoft / microsoft/Agents

Activity Protocol Schema v5 Translation pseudocode (v3 → v5)

Open
#481 0 comments 0 reactions 1 assignee View on GitHub

@gurubhg is already working on this.

Since Mar 17, 2026.

Discussion Specs
Dominant language
TypeSpec
Stars
1.1k
Forks
340
Avg merge
3d 13h
Merged PRs (30d)
12

Description

Activity Protocol v5 — Translation Pseudocode (§17.4)

Parent issue: #472 — Activity Protocol Schema v5 Proposal
Section: §17.4 Translation pseudocode (v3 → v5)

This sub-issue contains the complete translation pseudocode for converting v3 activities to v5 format.


17.4 Translation pseudocode (v3 → v5)
def v3_to_v5(v3_activity):
    v5 = {
        "v": "5",
        "type": map_type(v3_activity["type"]),
        "id": v3_activity.get("id", generate_id()),
        "timestamp": v3_activity.get("timestamp", now_utc()),
        "channelId": v3_activity.get("channelId"),
        "conversation": { "id": v3_activity["conversation"]["id"] },
        "from": { "id": v3_activity["from"]["id"],
                  "name": v3_activity["from"].get("name"),
                  "role": v3_activity["from"].get("role") },
    }

    # Map recipient → to[]
    if "recipient" in v3_activity:
        v5["to"] = [{ "id": v3_activity["recipient"]["id"],
                      "name": v3_activity["recipient"].get("name") }]

    # Map replyToId → relatesTo.inReplyTo
    if "replyToId" in v3_activity:
        v5.setdefault("relatesTo", {})["inReplyTo"] = {
            "activityId": v3_activity["replyToId"]
        }

    # Map message content → payload
    if v3_activity["type"] == "message":
        v5["payload"] = build_payload(v3_activity)

    # Map messageUpdate → message with replaces
    if v3_activity["type"] == "messageUpdate":
        v5.setdefault("relatesTo", {})["replaces"] = {
            "activityId": v3_activity["id"]
        }
        v5["payload"] = build_payload(v3_activity)

    # Map messageDelete → event with tombstone
    if v3_activity["type"] == "messageDelete":
        v5["name"] = "message.delete"
        v5.setdefault("relatesTo", {})["tombstones"] = {
            "activityId": v3_activity["id"]
        }

    # Map conversationUpdate → event with payload
    if v3_activity["type"] == "conversationUpdate":
        v5["name"] = "conversation.update"
        v5["payload"] = {
            "contentType": "application/vnd.microsoft.activity.conversation.update+json",
            "content": {
                "membersAdded": v3_activity.get("membersAdded", []),
                "membersRemoved": v3_activity.get("membersRemoved", []),
                "topicName": v3_activity.get("topicName")
            }
        }

    # Map typing → event
    if v3_activity["type"] == "typing":
        v5["name"] = "typing"
        # No payload needed for typing

    # Map messageReaction → event with reactsTo
    if v3_activity["type"] == "messageReaction":
        v5["name"] = "message.reaction"
        if "replyToId" in v3_activity:
            v5.setdefault("relatesTo", {})["reactsTo"] = {
                "activityId": v3_activity["replyToId"]
            }
        if "reactionsAdded" in v3_activity or "reactionsRemoved" in v3_activity:
            v5["payload"] = {
                "contentType": "application/vnd.microsoft.activity.reaction+json",
                "content": {
                    "reactionsAdded": v3_activity.get("reactionsAdded", []),
                    "reactionsRemoved": v3_activity.get("reactionsRemoved", [])
                }
            }

    # Map invoke → command
    if v3_activity["type"] == "invoke":
        v5["name"] = v3_activity.get("name", "")
        if "value" in v3_activity:
            v5["payload"] = {
                "contentType": v3_activity.get("valueType", "application/json"),
                "content": v3_activity["value"]
            }

    # Map endOfConversation → event
    if v3_activity["type"] == "endOfConversation":
        v5["name"] = "conversation.end"
        if "code" in v3_activity:
            v5["payload"] = {
                "contentType": "application/json",
                "content": { "code": v3_activity["code"] }
            }

    # Map event → event (direct mapping)
    if v3_activity["type"] == "event":
        v5["name"] = v3_activity["name"]
        if "value" in v3_activity:
            v5["payload"] = {
                "contentType": v3_activity.get("valueType", "application/json"),
                "content": v3_activity["value"]
            }

    # Map contactRelationUpdate → event
    if v3_activity["type"] == "contactRelationUpdate":
        v5["name"] = "contact.update"
        v5["payload"] = {
            "contentType": "application/vnd.microsoft.activity.contact.update+json",
            "content": { "action": v3_activity.get("action") }
        }

    # Map installationUpdate → event
    if v3_activity["type"] == "installationUpdate":
        v5["name"] = "installation.update"
        v5["payload"] = {
            "contentType": "application/vnd.microsoft.activity.installation.update+json",
            "content": { "action": v3_activity.get("action") }
        }

    # Map handoff → event
    if v3_activity["type"] == "handoff":
        v5["name"] = "handoff"
        if "value" in v3_activity:
            v5["payload"] = {
                "contentType": "application/json",
                "content": v3_activity["value"]
            }

    # Map locale → locale entity
    if "locale" in v3_activity:
        v5.setdefault("entities", []).append({
            "type": "locale",
            "language": v3_activity["locale"]
        })

    # Map suggestedActions → payload item
    if "suggestedActions" in v3_activity and v3_activity["suggestedActions"]:
        payload = v5.get("payload", {})
        items = payload.get("items", [])
        items.append({
            "kind": "json",
            "contentType": "application/vnd.microsoft.activity.suggestedActions+json",
            "content": v3_activity["suggestedActions"]
        })
        payload["items"] = items
        if "contentType" not in payload:
            payload["contentType"] = "multipart/mixed"
        v5["payload"] = payload

    # Map callerId → security entity
    if "callerId" in v3_activity:
        v5.setdefault("entities", []).append({
            "type": "security",
            "caller": { "id": v3_activity["callerId"] }
        })

    return v5


def map_type(v3_type):
    mapping = {
        "message": "message",
        "event": "event",
        "invoke": "command",
        "command": "command",
        "commandResult": "commandResult",
        "trace": "trace",
        "conversationUpdate": "event",      # name = "conversation.update"
        "contactRelationUpdate": "event",    # name = "contact.update"
        "installationUpdate": "event",       # name = "installation.update"
        "endOfConversation": "event",        # name = "conversation.end"
        "typing": "event",                   # name = "typing"
        "messageReaction": "event",          # name = "message.reaction"
        "messageDelete": "event",            # relatesTo.tombstones
        "messageUpdate": "message",          # relatesTo.replaces
        "suggestion": "message",
        "handoff": "event",                  # name = "handoff"
    }
    return mapping.get(v3_type, v3_type)


def build_payload(v3_activity):
    """Build a v5 payload from a v3 message-type activity."""
    payload = {}

    # Primary text content
    if "text" in v3_activity:
        text_format = v3_activity.get("textFormat", "plain")
        if text_format == "markdown":
            payload["contentType"] = "text/markdown"
        elif text_format == "xml":
            payload["contentType"] = "text/xml"
        else:
            payload["contentType"] = "text/plain"
        payload["content"] = v3_activity["text"]

    # Attachments → items
    if "attachments" in v3_activity and v3_activity["attachments"]:
        items = []
        for att in v3_activity["attachments"]:
            item = {
                "contentType": att.get("contentType", "application/octet-stream"),
            }
            # Determine kind from contentType
            ct = item["contentType"].lower()
            if ct.startswith("image/"):
                item["kind"] = "image"
            elif ct.startswith("audio/"):
                item["kind"] = "audio"
            elif ct.startswith("video/"):
                item["kind"] = "video"
            elif ct.startswith("application/vnd.microsoft.card."):
                item["kind"] = "json"
            elif ct.startswith("application/"):
                item["kind"] = "json"
            else:
                item["kind"] = "file"

            if "content" in att:
                item["content"] = att["content"]
            if "contentUrl" in att:
                item["contentUrl"] = att["contentUrl"]
            if "name" in att:
                item["name"] = att["name"]
            if "thumbnailUrl" in att:
                item["thumbnailUrl"] = att["thumbnailUrl"]

            items.append(item)

        payload["items"] = items
        if "contentType" not in payload:
            payload["contentType"] = "multipart/mixed"

    # value/valueType (for invoke/command types handled elsewhere,
    # but some message activities also use value)
    if "value" in v3_activity and "text" not in v3_activity:
        payload["contentType"] = v3_activity.get("valueType", "application/json")
        payload["content"] = v3_activity["value"]

    return payload if payload else {"contentType": "text/plain", "content": ""}

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.