diff --git a/specs/activity/schema/activity-protocol.schema.json b/specs/activity/schema/activity-protocol.schema.json new file mode 100644 index 00000000..a542cd7d --- /dev/null +++ b/specs/activity/schema/activity-protocol.schema.json @@ -0,0 +1,826 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://raw.githubusercontent.com/microsoft/botframework-sdk/main/specs/activity/protocol-activity.schema.json", + "title": "Activity", + "description": "An Activity is the basic communication type for the Bot Framework 3.0 protocol.", + "type": "object", + "discriminator": { + "propertyName": "type" + }, + "oneOf": [ + { + "$ref": "#/definitions/message" + }, + { + "$ref": "#/definitions/contactRelationUpdate" + }, + { + "$ref": "#/definitions/conversationUpdate" + }, + { + "$ref": "#/definitions/typing" + }, + { + "$ref": "#/definitions/endOfConversation" + }, + { + "$ref": "#/definitions/event" + }, + { + "$ref": "#/definitions/invoke" + }, + { + "$ref": "#/definitions/messageUpdate" + }, + { + "$ref": "#/definitions/messageDelete" + }, + { + "$ref": "#/definitions/installationUpdate" + }, + { + "$ref": "#/definitions/messageReaction" + }, + { + "$ref": "#/definitions/suggestion" + }, + { + "$ref": "#/definitions/trace" + }, + { + "$ref": "#/definitions/handoff" + } + ], + "definitions": { + "activity": { + "type": "object", + "properties": { + "type": { + "type": "string", + "description": "The type of the activity." + }, + "id": { + "type": "string", + "description": "The unique identifier for the activity." + }, + "timestamp": { + "type": "string", + "format": "date-time", + "description": "The UTC timestamp of the activity." + }, + "localTimestamp": { + "type": "string", + "format": "date-time", + "description": "The local timestamp of the activity." + }, + "localTimezone": { + "type": "string", + "description": "The timezone of the local timestamp." + }, + "callerId": { + "type": "string", + "description": "The caller ID." + }, + "serviceUrl": { + "type": "string", + "description": "The URL of the service." + }, + "channelId": { + "type": "string", + "description": "The ID of the channel." + }, + "from": { + "$ref": "#/definitions/channelAccount", + "description": "The sender of the activity." + }, + "conversation": { + "$ref": "#/definitions/conversationAccount", + "description": "The conversation the activity is part of." + }, + "recipient": { + "$ref": "#/definitions/channelAccount", + "description": "The recipient of the activity." + }, + "entities": { + "type": "array", + "items": { + "$ref": "#/definitions/entity" + }, + "description": "The entities of the activity." + }, + "channelData": { + "type": "object", + "description": "The channel-specific data." + }, + "replyToId": { + "type": "string", + "description": "The ID of the activity to reply to." + } + }, + "required": [ + "type", + "channelId", + "from", + "conversation", + "recipient", + "serviceUrl" + ] + }, + "messageBase": { + "type": "object", + "allOf": [ + { + "$ref": "#/definitions/activity" + } + ], + "properties": { + "locale": { + "type": "string", + "description": "The locale of the activity." + }, + "text": { + "type": "string", + "description": "The text of the activity." + }, + "textFormat": { + "type": "string", + "description": "The format of the text.", + "enum": [ + "plain", + "markdown", + "xml" + ] + }, + "speak": { + "type": "string", + "description": "The text to be spoken." + }, + "inputHint": { + "type": "string", + "description": "A hint for the input.", + "enum": [ + "accepting", + "expecting", + "ignoring" + ] + }, + "summary": { + "type": "string", + "description": "A summary of the activity." + }, + "suggestedActions": { + "$ref": "#/definitions/suggestedActions", + "description": "The suggested actions for the activity." + }, + "attachments": { + "type": "array", + "items": { + "$ref": "#/definitions/attachment" + }, + "description": "The attachments of the activity." + }, + "attachmentLayout": { + "type": "string", + "description": "The layout of the attachments.", + "enum": [ + "list", + "carousel" + ] + }, + "value": { + "type": "object", + "description": "The value of the activity." + }, + "expiration": { + "type": "string", + "format": "date-time", + "description": "The expiration time of the activity." + }, + "importance": { + "type": "string", + "description": "The importance of the activity.", + "enum": [ + "low", + "normal", + "high" + ] + }, + "deliveryMode": { + "type": "string", + "description": "The delivery mode of the activity.", + "enum": [ + "normal", + "notification", + "expectReplies" + ] + }, + "listenFor": { + "type": "array", + "items": { + "type": "string" + }, + "description": "A list of phrases to listen for." + }, + "semanticAction": { + "$ref": "#/definitions/semanticAction", + "description": "The semantic action of the activity." + } + } + }, + "message": { + "type": "object", + "allOf": [ + { + "$ref": "#/definitions/messageBase" + } + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "message" + ] + } + }, + "required": [ "type" ] + }, + "contactRelationUpdate": { + "type": "object", + "allOf": [ + { + "$ref": "#/definitions/activity" + } + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "contactRelationUpdate" + ] + }, + "action": { + "type": "string", + "enum": [ + "add", + "remove" + ] + } + }, + "required": [ "type" ] + }, + "conversationUpdate": { + "type": "object", + "allOf": [ + { + "$ref": "#/definitions/activity" + } + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "conversationUpdate" + ] + }, + "membersAdded": { + "type": "array", + "items": { + "$ref": "#/definitions/channelAccount" + }, + "description": "The members added to the conversation." + }, + "membersRemoved": { + "type": "array", + "items": { + "$ref": "#/definitions/channelAccount" + }, + "description": "The members removed from the conversation." + }, + "topicName": { + "type": "string", + "description": "The name of the topic." + }, + "historyDisclosed": { + "type": "boolean", + "description": "Indicates if the history is disclosed." + } + }, + "required": [ "type" ] + }, + "typing": { + "type": "object", + "allOf": [ + { + "$ref": "#/definitions/activity" + } + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "typing" + ] + } + }, + "required": [ "type" ] + }, + "endOfConversation": { + "type": "object", + "allOf": [ + { + "$ref": "#/definitions/activity" + } + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "endOfConversation" + ] + }, + "code": { + "type": "string", + "description": "The code of the activity." + }, + "text": { + "type": "string", + "description": "The text of the activity." + } + }, + "required": [ "type" ] + }, + "event": { + "type": "object", + "allOf": [ + { + "$ref": "#/definitions/activity" + } + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "event" + ] + }, + "name": { + "type": "string", + "description": "The name of the activity." + }, + "value": { + "type": "object", + "description": "The value of the activity." + }, + "relatesTo": { + "$ref": "#/definitions/conversationReference", + "description": "A reference to another conversation." + } + }, + "required": [ + "type", + "name" + ] + }, + "invoke": { + "type": "object", + "allOf": [ + { + "$ref": "#/definitions/activity" + } + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "invoke" + ] + }, + "name": { + "type": "string", + "description": "The name of the activity." + }, + "value": { + "type": "object", + "description": "The value of the activity." + }, + "relatesTo": { + "$ref": "#/definitions/conversationReference", + "description": "A reference to another conversation." + } + }, + "required": [ + "type", + "name" + ] + }, + "messageUpdate": { + "type": "object", + "allOf": [ + { + "$ref": "#/definitions/messageBase" + } + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "messageUpdate" + ] + } + }, + "required": [ "type" ] + }, + "messageDelete": { + "type": "object", + "allOf": [ + { + "$ref": "#/definitions/activity" + } + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "messageDelete" + ] + } + }, + "required": [ "type" ] + }, + "installationUpdate": { + "type": "object", + "allOf": [ + { + "$ref": "#/definitions/activity" + } + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "installationUpdate" + ] + }, + "action": { + "type": "string", + "enum": [ + "add", + "remove" + ] + } + }, + "required": [ "type" ] + }, + "messageReaction": { + "type": "object", + "allOf": [ + { + "$ref": "#/definitions/activity" + } + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "messageReaction" + ] + }, + "reactionsAdded": { + "type": "array", + "items": { + "$ref": "#/definitions/messageReactionObject" + }, + "description": "The reactions added to the activity." + }, + "reactionsRemoved": { + "type": "array", + "items": { + "$ref": "#/definitions/messageReactionObject" + }, + "description": "The reactions removed from the activity." + } + }, + "required": [ "type" ] + }, + "suggestion": { + "type": "object", + "allOf": [ + { + "$ref": "#/definitions/messageBase" + } + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "suggestion" + ] + }, + "textHighlights": { + "type": "array", + "items": { + "$ref": "#/definitions/textHighlight" + }, + "description": "The text highlights of the activity." + } + }, + "required": [ "type" ] + }, + "trace": { + "type": "object", + "allOf": [ + { + "$ref": "#/definitions/activity" + } + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "trace" + ] + }, + "name": { + "type": "string", + "description": "The name of the activity." + }, + "label": { + "type": "string", + "description": "The label of the activity." + }, + "valueType": { + "type": "string", + "description": "The type of the value." + }, + "value": { + "type": "object", + "description": "The value of the activity." + }, + "relatesTo": { + "$ref": "#/definitions/conversationReference", + "description": "A reference to another conversation." + } + }, + "required": [ "type" ] + }, + "handoff": { + "type": "object", + "allOf": [ + { + "$ref": "#/definitions/activity" + } + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "handoff" + ] + } + }, + "required": [ "type" ] + }, + "channelAccount": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "The ID of the channel account." + }, + "name": { + "type": "string", + "description": "The name of the channel account." + }, + "aadObjectId": { + "type": "string", + "description": "The Azure Active Directory object ID of the channel account." + }, + "role": { + "type": "string", + "description": "The role of the channel account.", + "enum": [ + "user", + "bot" + ] + } + }, + "required": [ + "id" + ] + }, + "conversationAccount": { + "type": "object", + "properties": { + "isGroup": { + "type": "boolean", + "description": "Indicates if the conversation is a group conversation." + }, + "conversationType": { + "type": "string", + "description": "The type of the conversation." + }, + "id": { + "type": "string", + "description": "The ID of the conversation." + }, + "name": { + "type": "string", + "description": "The name of the conversation." + }, + "aadObjectId": { + "type": "string", + "description": "The Azure Active Directory object ID of the conversation." + }, + "role": { + "type": "string", + "description": "The role of the conversation account.", + "enum": [ + "user", + "bot" + ] + }, + "tenantId": { + "type": "string", + "description": "The tenant ID of the conversation." + } + }, + "required": [ + "id" + ] + }, + "messageReactionObject": { + "type": "object", + "properties": { + "type": { + "type": "string", + "description": "The type of the message reaction." + } + } + }, + "suggestedActions": { + "type": "object", + "properties": { + "to": { + "type": "array", + "items": { + "type": "string" + }, + "description": "The recipients of the suggested actions." + }, + "actions": { + "type": "array", + "items": { + "$ref": "#/definitions/cardAction" + }, + "description": "The suggested actions." + } + } + }, + "attachment": { + "type": "object", + "properties": { + "contentType": { + "type": "string", + "description": "The content type of the attachment." + }, + "contentUrl": { + "type": "string", + "description": "The URL of the attachment content." + }, + "content": { + "type": "object", + "description": "The content of the attachment." + }, + "name": { + "type": "string", + "description": "The name of the attachment." + }, + "thumbnailUrl": { + "type": "string", + "description": "The URL of the attachment thumbnail." + } + } + }, + "entity": { + "type": "object", + "properties": { + "type": { + "type": "string", + "description": "The type of the entity." + } + } + }, + "conversationReference": { + "type": "object", + "properties": { + "activityId": { + "type": "string", + "description": "The ID of the activity." + }, + "user": { + "$ref": "#/definitions/channelAccount", + "description": "The user." + }, + "bot": { + "$ref": "#/definitions/channelAccount", + "description": "The bot." + }, + "conversation": { + "$ref": "#/definitions/conversationAccount", + "description": "The conversation." + }, + "channelId": { + "type": "string", + "description": "The ID of the channel." + }, + "serviceUrl": { + "type": "string", + "description": "The URL of the service." + }, + "locale": { + "type": "string", + "description": "The locale." + } + } + }, + "textHighlight": { + "type": "object", + "properties": { + "text": { + "type": "string", + "description": "The text to highlight." + }, + "occurrence": { + "type": "integer", + "description": "The occurrence of the text to highlight." + } + } + }, + "cardAction": { + "type": "object", + "properties": { + "type": { + "type": "string", + "description": "The type of the card action.", + "enum": [ + "openUrl", + "imBack", + "postBack", + "playAudio", + "playVideo", + "showImage", + "downloadFile", + "signin", + "call", + "messageBack" + ] + }, + "title": { + "type": "string", + "description": "The title of the card action." + }, + "image": { + "type": "string", + "description": "The image of the card action." + }, + "text": { + "type": "string", + "description": "The text of the card action." + }, + "displayText": { + "type": "string", + "description": "The display text of the card action." + }, + "value": { + "type": "string", + "description": "The value of the card action." + }, + "channelData": { + "type": "object", + "description": "The channel-specific data." + } + } + }, + "semanticAction": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "The ID of the semantic action." + }, + "entities": { + "type": "object", + "description": "The entities of the semantic action." + }, + "state": { + "type": "string", + "enum": [ + "start", + "continue", + "done" + ] + } + } + } + } +} \ No newline at end of file diff --git a/specs/activity/schema/validator/csharp/Program.cs b/specs/activity/schema/validator/csharp/Program.cs new file mode 100644 index 00000000..ec0e1188 --- /dev/null +++ b/specs/activity/schema/validator/csharp/Program.cs @@ -0,0 +1,130 @@ +using NJsonSchema; +using NJsonSchema.Validation; +using Newtonsoft.Json.Linq; + +static async Task LoadSchemaAsync(string schemaPath) => await JsonSchema.FromFileAsync(schemaPath); + +string schemaFile = Path.Combine(AppContext.BaseDirectory, "activity.schema.json"); +if (!File.Exists(schemaFile)) +{ + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine($"Schema file not found: {schemaFile}"); + Console.ResetColor(); + return; +} + +// Discover example files relative to project root (navigate up to validator folder structure) +// Expect examples under ../examples/json +string? baseDir = AppContext.BaseDirectory; +// Try to locate examples directory by walking up a few levels +string? examplesDir = null; +var current = new DirectoryInfo(baseDir); +for (int i = 0; i < 6 && current != null; i++) +{ + var probe = Path.Combine(current.FullName, "examples", "json"); + if (Directory.Exists(probe)) { examplesDir = probe; break; } + current = current.Parent; +} + +if (examplesDir == null) +{ + Console.ForegroundColor = ConsoleColor.Yellow; + Console.WriteLine("Examples directory not found (expected ../examples/json). Will fall back to input.json if present."); + Console.ResetColor(); +} + +var schema = await LoadSchemaAsync(schemaFile); + +List<(string name, JToken instance)> instances = new(); + +if (examplesDir != null) +{ + foreach (var file in Directory.GetFiles(examplesDir, "*.json", SearchOption.TopDirectoryOnly).OrderBy(f => f)) + { + try + { + var text = File.ReadAllText(file); + instances.Add((Path.GetFileName(file), JToken.Parse(text))); + } + catch (Exception ex) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine($"Failed to parse {file}: {ex.Message}"); + Console.ResetColor(); + } + } +} + +// Backward compatibility: single input.json +string singleInput = Path.Combine(AppContext.BaseDirectory, "input.json"); +if (File.Exists(singleInput)) +{ + try + { + instances.Add(("input.json", JToken.Parse(File.ReadAllText(singleInput)))); + } + catch (Exception ex) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine("input.json parse error: " + ex.Message); + Console.ResetColor(); + } +} + +if (instances.Count == 0) +{ + Console.ForegroundColor = ConsoleColor.Yellow; + Console.WriteLine("No JSON instances found to validate."); + Console.ResetColor(); + return; +} + +int total = 0; +int failures = 0; + +foreach (var (name, token) in instances) +{ + total++; + var errors = schema.Validate(token); + if (errors.Count == 0) + { + Console.ForegroundColor = ConsoleColor.Green; + Console.WriteLine($"[OK] {name} ({token["type"] ?? "unknown-type"})"); + Console.ResetColor(); + } + else + { + failures++; + Console.ForegroundColor = ConsoleColor.Yellow; + Console.WriteLine($"[FAIL] {name} ({token["type"] ?? "unknown-type"}) - {errors.Count} error(s)"); + Console.ResetColor(); + int i = 1; + foreach (var e in errors) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine($" [{i}] {e.Kind}"); + Console.ResetColor(); + Console.WriteLine($" Path : {(string.IsNullOrEmpty(e.Path) ? "$" : "$" + e.Path)}"); + Console.WriteLine($" Detail : {e}"); + if (!string.IsNullOrEmpty(e.Property)) + Console.WriteLine($" Property: {e.Property}"); + i++; + } + } +} + +Console.WriteLine(); +Console.WriteLine($"Summary: {total - failures} valid / {failures} invalid / {total} total"); +if (failures == 0) +{ + Console.ForegroundColor = ConsoleColor.Green; + Console.WriteLine("All examples are valid."); + Console.ResetColor(); +} +else +{ + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine("Some examples failed validation."); + Console.ResetColor(); + Environment.ExitCode = 1; +} \ No newline at end of file diff --git a/specs/activity/schema/validator/csharp/SchemaValidator.csproj b/specs/activity/schema/validator/csharp/SchemaValidator.csproj new file mode 100644 index 00000000..0f7062ce --- /dev/null +++ b/specs/activity/schema/validator/csharp/SchemaValidator.csproj @@ -0,0 +1,14 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + diff --git a/specs/activity/schema/validator/csharp/SchemaValidator.sln b/specs/activity/schema/validator/csharp/SchemaValidator.sln new file mode 100644 index 00000000..101d0469 --- /dev/null +++ b/specs/activity/schema/validator/csharp/SchemaValidator.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.14.36414.22 d17.14 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SchemaValidator", "SchemaValidator.csproj", "{2F44ACFB-7A40-4433-8D58-476B682C7AF7}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {2F44ACFB-7A40-4433-8D58-476B682C7AF7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2F44ACFB-7A40-4433-8D58-476B682C7AF7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2F44ACFB-7A40-4433-8D58-476B682C7AF7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2F44ACFB-7A40-4433-8D58-476B682C7AF7}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {833ED1E0-B359-4969-936C-1C5ED1B6803F} + EndGlobalSection +EndGlobal diff --git a/specs/activity/schema/validator/csharp/dotnetSoln_dharsingh/MyProject.sln b/specs/activity/schema/validator/csharp/dotnetSoln_dharsingh/MyProject.sln new file mode 100644 index 00000000..1210e868 --- /dev/null +++ b/specs/activity/schema/validator/csharp/dotnetSoln_dharsingh/MyProject.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.14.36119.2 d17.14 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MyProject", "MyProject\MyProject.csproj", "{DC6AFA74-6604-4C88-BA06-9207526EC36D}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {DC6AFA74-6604-4C88-BA06-9207526EC36D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DC6AFA74-6604-4C88-BA06-9207526EC36D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DC6AFA74-6604-4C88-BA06-9207526EC36D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DC6AFA74-6604-4C88-BA06-9207526EC36D}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {55922005-71F0-4A91-A296-63361F368FE1} + EndGlobalSection +EndGlobal diff --git a/specs/activity/schema/validator/csharp/dotnetSoln_dharsingh/MyProject/Models/ActivityValidationResult.cs b/specs/activity/schema/validator/csharp/dotnetSoln_dharsingh/MyProject/Models/ActivityValidationResult.cs new file mode 100644 index 00000000..11a6d049 --- /dev/null +++ b/specs/activity/schema/validator/csharp/dotnetSoln_dharsingh/MyProject/Models/ActivityValidationResult.cs @@ -0,0 +1,10 @@ +namespace MyProject.Models +{ + public class ActivityValidationResult + { + public bool IsValid { get; set; } + public List Errors { get; set; } = new List(); + public string? ActivityType { get; set; } + public string? ValidationMessage { get; set; } + } +} diff --git a/specs/activity/schema/validator/csharp/dotnetSoln_dharsingh/MyProject/MyProject.csproj b/specs/activity/schema/validator/csharp/dotnetSoln_dharsingh/MyProject/MyProject.csproj new file mode 100644 index 00000000..c3bd073b --- /dev/null +++ b/specs/activity/schema/validator/csharp/dotnetSoln_dharsingh/MyProject/MyProject.csproj @@ -0,0 +1,25 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + + + + Always + + + + diff --git a/specs/activity/schema/validator/csharp/dotnetSoln_dharsingh/MyProject/MyProject.http b/specs/activity/schema/validator/csharp/dotnetSoln_dharsingh/MyProject/MyProject.http new file mode 100644 index 00000000..e7eff835 --- /dev/null +++ b/specs/activity/schema/validator/csharp/dotnetSoln_dharsingh/MyProject/MyProject.http @@ -0,0 +1,6 @@ +@MyProject_HostAddress = http://localhost:5285 + +GET {{MyProject_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/specs/activity/schema/validator/csharp/dotnetSoln_dharsingh/MyProject/Program.cs b/specs/activity/schema/validator/csharp/dotnetSoln_dharsingh/MyProject/Program.cs new file mode 100644 index 00000000..48863a6d --- /dev/null +++ b/specs/activity/schema/validator/csharp/dotnetSoln_dharsingh/MyProject/Program.cs @@ -0,0 +1,25 @@ +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container. + +builder.Services.AddControllers(); +// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.UseHttpsRedirection(); + +app.UseAuthorization(); + +app.MapControllers(); + +app.Run(); diff --git a/specs/activity/schema/validator/csharp/dotnetSoln_dharsingh/MyProject/Properties/launchSettings.json b/specs/activity/schema/validator/csharp/dotnetSoln_dharsingh/MyProject/Properties/launchSettings.json new file mode 100644 index 00000000..a11308b6 --- /dev/null +++ b/specs/activity/schema/validator/csharp/dotnetSoln_dharsingh/MyProject/Properties/launchSettings.json @@ -0,0 +1,41 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:46155", + "sslPort": 44389 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5285", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7010;http://localhost:5285", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/specs/activity/schema/validator/csharp/dotnetSoln_dharsingh/MyProject/Services/ActivityProtocolValidator.cs b/specs/activity/schema/validator/csharp/dotnetSoln_dharsingh/MyProject/Services/ActivityProtocolValidator.cs new file mode 100644 index 00000000..050260cd --- /dev/null +++ b/specs/activity/schema/validator/csharp/dotnetSoln_dharsingh/MyProject/Services/ActivityProtocolValidator.cs @@ -0,0 +1,119 @@ +using MyProject.Models; +using System.Text.Json; +using Newtonsoft.Json.Schema; +using NewtonsoftJson = Newtonsoft.Json; +using NewtonsoftJObject = Newtonsoft.Json.Linq.JObject; + +namespace MyProject.Services +{ + public class ActivityProtocolValidator + { + private readonly JSchema _activitySchema; + + public ActivityProtocolValidator() + { + _activitySchema = LoadSchema(); + } + + public ActivityValidationResult ValidateActivity(string jsonInput) + { + var result = new ActivityValidationResult(); + + try + { + if (string.IsNullOrWhiteSpace(jsonInput)) + { + result.Errors.Add("JSON input cannot be null or empty"); + result.ValidationMessage = "Input validation failed: null or empty JSON"; + return result; + } + + // First, validate JSON format using System.Text.Json + JsonDocument jsonDoc; + try + { + jsonDoc = JsonDocument.Parse(jsonInput); + } + catch (JsonException ex) + { + result.Errors.Add($"Invalid JSON format: {ex.Message}"); + result.ValidationMessage = "JSON parsing error"; + return result; + } + + // Extract activity type if present + if (jsonDoc.RootElement.TryGetProperty("type", out JsonElement typeElement)) + { + result.ActivityType = typeElement.GetString(); + } + + // Convert to Newtonsoft.Json JObject for schema validation + var newtonsoftJsonObject = NewtonsoftJObject.Parse(jsonInput); + + // Validate against schema + IList errorMessages; + bool isValid = newtonsoftJsonObject.IsValid(_activitySchema, out errorMessages); + + result.IsValid = isValid; + result.Errors = errorMessages.ToList(); + + if (isValid) + { + result.ValidationMessage = $"Activity validation successful for type: {result.ActivityType ?? "unknown"}"; + } + else + { + result.ValidationMessage = $"Activity validation failed for type: {result.ActivityType ?? "unknown"}"; + } + + jsonDoc.Dispose(); + } + catch (NewtonsoftJson.JsonException ex) + { + result.Errors.Add($"Invalid JSON format: {ex.Message}"); + result.ValidationMessage = "JSON parsing error"; + } + catch (Exception ex) + { + result.Errors.Add($"Validation error: {ex.Message}"); + result.ValidationMessage = "Unexpected validation error"; + } + + return result; + } + + public async Task ValidateActivityAsync(string jsonInput) + { + return await Task.Run(() => ValidateActivity(jsonInput)); + } + + private JSchema LoadSchema(string? customPath = null) + { + try + { + string schemaPath; + + if (!string.IsNullOrEmpty(customPath)) + { + schemaPath = customPath; + } + else + { + schemaPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "activity_protocol.schema"); + } + + if (!File.Exists(schemaPath)) + { + throw new FileNotFoundException($"Activity protocol schema file not found at: {schemaPath}"); + } + + var schemaJson = File.ReadAllText(schemaPath); + return JSchema.Parse(schemaJson); + } + catch (Exception ex) + { + throw new InvalidOperationException($"Failed to load activity protocol schema: {ex.Message}", ex); + } + } + } +} \ No newline at end of file diff --git a/specs/activity/schema/validator/csharp/dotnetSoln_dharsingh/MyProject/Tests/01-message.json b/specs/activity/schema/validator/csharp/dotnetSoln_dharsingh/MyProject/Tests/01-message.json new file mode 100644 index 00000000..fab119d7 --- /dev/null +++ b/specs/activity/schema/validator/csharp/dotnetSoln_dharsingh/MyProject/Tests/01-message.json @@ -0,0 +1,38 @@ +{ + "type": "message", + "id": "message-activity-1", + "timestamp": "2025-09-15T10:00:01.000Z", + "localTimestamp": "2025-09-15T03:00:01.000-07:00", + "localTimezone": "America/Los_Angeles", + "serviceUrl": "https://smba.trafficmanager.net/amer/", + "channelId": "msteams", + "from": { "id": "user-id-1", "name": "John Doe" }, + "conversation": { "isGroup": true, "id": "conv-1", "name": "Project Discussion" }, + "recipient": { "id": "bot-id-1", "name": "HelpBot" }, + "text": "Hello, I need help with my order.", + "textFormat": "plain", + "locale": "en-US", + "attachments": [ + { + "contentType": "application/vnd.microsoft.card.hero", + "content": { + "title": "Order #12345", + "text": "Status: Shipped" + } + } + ], + "suggestedActions": { + "actions": [ + { + "type": "imBack", + "title": "Track Package", + "value": "track order 12345" + }, + { + "type": "imBack", + "title": "Cancel Order", + "value": "cancel order 12345" + } + ] + } +} diff --git a/specs/activity/schema/validator/csharp/dotnetSoln_dharsingh/MyProject/Tests/02-contactRelationUpdate.json b/specs/activity/schema/validator/csharp/dotnetSoln_dharsingh/MyProject/Tests/02-contactRelationUpdate.json new file mode 100644 index 00000000..4385c25a --- /dev/null +++ b/specs/activity/schema/validator/csharp/dotnetSoln_dharsingh/MyProject/Tests/02-contactRelationUpdate.json @@ -0,0 +1,11 @@ +{ + "type": "contactRelationUpdate", + "id": "contact-relation-update-1", + "timestamp": "2025-09-15T10:00:02.000Z", + "serviceUrl": "https://smba.trafficmanager.net/amer/", + "channelId": "skype", + "from": { "id": "user-id-1", "name": "John Doe" }, + "conversation": { "id": "conv-2" }, + "recipient": { "id": "bot-id-1", "name": "WelcomeBot" }, + "action": "add" +} diff --git a/specs/activity/schema/validator/csharp/dotnetSoln_dharsingh/MyProject/Tests/03-conversationUpdate.json b/specs/activity/schema/validator/csharp/dotnetSoln_dharsingh/MyProject/Tests/03-conversationUpdate.json new file mode 100644 index 00000000..8f31bb22 --- /dev/null +++ b/specs/activity/schema/validator/csharp/dotnetSoln_dharsingh/MyProject/Tests/03-conversationUpdate.json @@ -0,0 +1,13 @@ +{ + "type": "conversationUpdate", + "id": "conversation-update-1", + "timestamp": "2025-09-15T10:00:03.000Z", + "serviceUrl": "https://smba.trafficmanager.net/amer/", + "channelId": "msteams", + "from": { "id": "user-id-2", "name": "Jane Smith" }, + "conversation": { "isGroup": true, "id": "conv-1", "name": "Project Discussion" }, + "recipient": { "id": "bot-id-1", "name": "HelpBot" }, + "membersAdded": [ { "id": "user-id-3", "name": "Sam Brown" } ], + "membersRemoved": [], + "topicName": "Project Discussion Kickoff" +} diff --git a/specs/activity/schema/validator/csharp/dotnetSoln_dharsingh/MyProject/Tests/04-typing.json b/specs/activity/schema/validator/csharp/dotnetSoln_dharsingh/MyProject/Tests/04-typing.json new file mode 100644 index 00000000..a085fed1 --- /dev/null +++ b/specs/activity/schema/validator/csharp/dotnetSoln_dharsingh/MyProject/Tests/04-typing.json @@ -0,0 +1,10 @@ +{ + "type": "typing", + "id": "typing-1", + "timestamp": "2025-09-15T10:00:04.000Z", + "serviceUrl": "https://smba.trafficmanager.net/amer/", + "channelId": "webchat", + "from": { "id": "user-id-1", "name": "John Doe" }, + "conversation": { "id": "conv-3" }, + "recipient": { "id": "bot-id-1", "name": "EchoBot" } +} diff --git a/specs/activity/schema/validator/csharp/dotnetSoln_dharsingh/MyProject/Tests/05-endOfConversation.json b/specs/activity/schema/validator/csharp/dotnetSoln_dharsingh/MyProject/Tests/05-endOfConversation.json new file mode 100644 index 00000000..d612a606 --- /dev/null +++ b/specs/activity/schema/validator/csharp/dotnetSoln_dharsingh/MyProject/Tests/05-endOfConversation.json @@ -0,0 +1,12 @@ +{ + "type": "endOfConversation", + "id": "end-of-conv-1", + "timestamp": "2025-09-15T10:00:05.000Z", + "serviceUrl": "https://smba.trafficmanager.net/amer/", + "channelId": "msteams", + "from": { "id": "bot-id-1", "name": "SurveyBot" }, + "conversation": { "id": "conv-4" }, + "recipient": { "id": "user-id-4", "name": "Peter Jones" }, + "code": "completedSuccessfully", + "text": "Thanks for completing the survey!" +} diff --git a/specs/activity/schema/validator/csharp/dotnetSoln_dharsingh/MyProject/Tests/06-event.json b/specs/activity/schema/validator/csharp/dotnetSoln_dharsingh/MyProject/Tests/06-event.json new file mode 100644 index 00000000..5e6a504d --- /dev/null +++ b/specs/activity/schema/validator/csharp/dotnetSoln_dharsingh/MyProject/Tests/06-event.json @@ -0,0 +1,12 @@ +{ + "type": "event", + "id": "event-1", + "timestamp": "2025-09-15T10:00:06.000Z", + "serviceUrl": "https://smba.trafficmanager.net/amer/", + "channelId": "webchat", + "from": { "id": "user-id-1", "name": "John Doe" }, + "conversation": { "id": "conv-5" }, + "recipient": { "id": "bot-id-1", "name": "LocationBot" }, + "name": "locationReceived", + "value": { "latitude": 47.6062, "longitude": -122.3321 } +} diff --git a/specs/activity/schema/validator/csharp/dotnetSoln_dharsingh/MyProject/Tests/07-invoke.json b/specs/activity/schema/validator/csharp/dotnetSoln_dharsingh/MyProject/Tests/07-invoke.json new file mode 100644 index 00000000..ca29e277 --- /dev/null +++ b/specs/activity/schema/validator/csharp/dotnetSoln_dharsingh/MyProject/Tests/07-invoke.json @@ -0,0 +1,12 @@ +{ + "type": "invoke", + "id": "invoke-1", + "timestamp": "2025-09-15T10:00:07.000Z", + "serviceUrl": "https://smba.trafficmanager.net/amer/", + "channelId": "msteams", + "from": { "id": "user-id-1", "name": "John Doe" }, + "conversation": { "id": "conv-6" }, + "recipient": { "id": "bot-id-1", "name": "TaskBot" }, + "name": "task/submit", + "value": { "taskId": "task-987", "data": { "comments": "All done." } } +} diff --git a/specs/activity/schema/validator/csharp/dotnetSoln_dharsingh/MyProject/Tests/08-messageUpdate.json b/specs/activity/schema/validator/csharp/dotnetSoln_dharsingh/MyProject/Tests/08-messageUpdate.json new file mode 100644 index 00000000..b4cf0870 --- /dev/null +++ b/specs/activity/schema/validator/csharp/dotnetSoln_dharsingh/MyProject/Tests/08-messageUpdate.json @@ -0,0 +1,12 @@ +{ + "type": "messageUpdate", + "id": "message-activity-1", + "timestamp": "2025-09-15T10:00:08.000Z", + "serviceUrl": "https://smba.trafficmanager.net/amer/", + "channelId": "msteams", + "from": { "id": "user-id-1", "name": "John Doe" }, + "conversation": { "id": "conv-1" }, + "recipient": { "id": "bot-id-1", "name": "HelpBot" }, + "text": "Hello, I need help with my order #12345.", + "locale": "en-US" +} diff --git a/specs/activity/schema/validator/csharp/dotnetSoln_dharsingh/MyProject/Tests/09-messageDelete.json b/specs/activity/schema/validator/csharp/dotnetSoln_dharsingh/MyProject/Tests/09-messageDelete.json new file mode 100644 index 00000000..44b86ce9 --- /dev/null +++ b/specs/activity/schema/validator/csharp/dotnetSoln_dharsingh/MyProject/Tests/09-messageDelete.json @@ -0,0 +1,10 @@ +{ + "type": "messageDelete", + "id": "message-activity-to-delete", + "timestamp": "2025-09-15T10:00:09.000Z", + "serviceUrl": "https://smba.trafficmanager.net/amer/", + "channelId": "msteams", + "from": { "id": "user-id-1", "name": "John Doe" }, + "conversation": { "id": "conv-1" }, + "recipient": { "id": "bot-id-1", "name": "HelpBot" } +} diff --git a/specs/activity/schema/validator/csharp/dotnetSoln_dharsingh/MyProject/Tests/10-installationUpdate.json b/specs/activity/schema/validator/csharp/dotnetSoln_dharsingh/MyProject/Tests/10-installationUpdate.json new file mode 100644 index 00000000..09f494d6 --- /dev/null +++ b/specs/activity/schema/validator/csharp/dotnetSoln_dharsingh/MyProject/Tests/10-installationUpdate.json @@ -0,0 +1,11 @@ +{ + "type": "installationUpdate", + "id": "install-update-1", + "timestamp": "2025-09-15T10:00:10.000Z", + "serviceUrl": "https://smba.trafficmanager.net/amer/", + "channelId": "msteams", + "from": { "id": "user-id-admin", "name": "Admin User" }, + "conversation": { "id": "conv-tenant-level" }, + "recipient": { "id": "bot-id-1", "name": "AdminBot" }, + "action": "add" +} diff --git a/specs/activity/schema/validator/csharp/dotnetSoln_dharsingh/MyProject/Tests/11-messageReaction.json b/specs/activity/schema/validator/csharp/dotnetSoln_dharsingh/MyProject/Tests/11-messageReaction.json new file mode 100644 index 00000000..1cb415fe --- /dev/null +++ b/specs/activity/schema/validator/csharp/dotnetSoln_dharsingh/MyProject/Tests/11-messageReaction.json @@ -0,0 +1,13 @@ +{ + "type": "messageReaction", + "id": "reaction-1", + "timestamp": "2025-09-15T10:00:11.000Z", + "serviceUrl": "https://smba.trafficmanager.net/amer/", + "channelId": "msteams", + "from": { "id": "user-id-2", "name": "Jane Smith" }, + "conversation": { "id": "conv-1" }, + "recipient": { "id": "bot-id-1", "name": "HelpBot" }, + "replyToId": "message-activity-1", + "reactionsAdded": [ { "type": "like" } ], + "reactionsRemoved": [] +} diff --git a/specs/activity/schema/validator/csharp/dotnetSoln_dharsingh/MyProject/Tests/12-suggestion.json b/specs/activity/schema/validator/csharp/dotnetSoln_dharsingh/MyProject/Tests/12-suggestion.json new file mode 100644 index 00000000..6b68ad16 --- /dev/null +++ b/specs/activity/schema/validator/csharp/dotnetSoln_dharsingh/MyProject/Tests/12-suggestion.json @@ -0,0 +1,13 @@ +{ + "type": "suggestion", + "id": "suggestion-1", + "timestamp": "2025-09-15T10:00:12.000Z", + "serviceUrl": "https://smba.trafficmanager.net/amer/", + "channelId": "cortana", + "from": { "id": "bot-id-1", "name": "CalendarBot" }, + "conversation": { "id": "conv-7" }, + "recipient": { "id": "user-id-1", "name": "John Doe" }, + "replyToId": "original-message-id", + "text": "I can create that meeting for you.", + "textHighlights": [ { "text": "meet on Monday", "occurrence": 1 } ] +} diff --git a/specs/activity/schema/validator/csharp/dotnetSoln_dharsingh/MyProject/Tests/13-trace.json b/specs/activity/schema/validator/csharp/dotnetSoln_dharsingh/MyProject/Tests/13-trace.json new file mode 100644 index 00000000..94d6e9bb --- /dev/null +++ b/specs/activity/schema/validator/csharp/dotnetSoln_dharsingh/MyProject/Tests/13-trace.json @@ -0,0 +1,18 @@ +{ + "type": "trace", + "id": "trace-1", + "timestamp": "2025-09-15T10:00:13.000Z", + "serviceUrl": "https://smba.trafficmanager.net/amer/", + "channelId": "emulator", + "from": { "id": "bot-id-1", "name": "DebuggingBot" }, + "conversation": { "id": "conv-debug" }, + "recipient": { "id": "user-id-dev", "name": "Developer" }, + "name": "LUIS_TRACE", + "label": "LUIS Trace", + "valueType": "https://www.luis.ai/schemas/trace", + "value": { + "query": "book a flight to paris", + "topScoringIntent": { "intent": "BookFlight", "score": 0.98 }, + "entities": [ { "type": "Location", "entity": "paris" } ] + } +} diff --git a/specs/activity/schema/validator/csharp/dotnetSoln_dharsingh/MyProject/Tests/14-handoff.json b/specs/activity/schema/validator/csharp/dotnetSoln_dharsingh/MyProject/Tests/14-handoff.json new file mode 100644 index 00000000..35e86e3d --- /dev/null +++ b/specs/activity/schema/validator/csharp/dotnetSoln_dharsingh/MyProject/Tests/14-handoff.json @@ -0,0 +1,11 @@ +{ + "type": "handoff", + "id": "handoff-1", + "timestamp": "2025-09-15T10:00:14.000Z", + "serviceUrl": "https://smba.trafficmanager.net/amer/", + "channelId": "custom-channel", + "from": { "id": "bot-id-1", "name": "TriageBot" }, + "conversation": { "id": "conv-8" }, + "recipient": { "id": "human-agent-system", "name": "Live Agent Hub" }, + "value": { "reason": "User requested human agent", "transcript": [ "User: I need to speak to a person.", "Bot: OK, I'll connect you to a live agent." ] } +} diff --git a/specs/activity/schema/validator/csharp/dotnetSoln_dharsingh/MyProject/Tests/15-simple-message.json b/specs/activity/schema/validator/csharp/dotnetSoln_dharsingh/MyProject/Tests/15-simple-message.json new file mode 100644 index 00000000..6b631190 --- /dev/null +++ b/specs/activity/schema/validator/csharp/dotnetSoln_dharsingh/MyProject/Tests/15-simple-message.json @@ -0,0 +1,9 @@ +{ + "type": "message", + "channelId": "emulator", + "from": { "id": "user1", "name": "User One" }, + "conversation": { "id": "conv1", "name": "Conversation 1" }, + "recipient": { "id": "bot", "name": "Bot" }, + "serviceUrl": "https://example.org", + "text": "Hello, world!" +} diff --git a/specs/activity/schema/validator/csharp/dotnetSoln_dharsingh/MyProject/Tests/ActivityProtocolValidatorTests.cs b/specs/activity/schema/validator/csharp/dotnetSoln_dharsingh/MyProject/Tests/ActivityProtocolValidatorTests.cs new file mode 100644 index 00000000..eaee66e5 --- /dev/null +++ b/specs/activity/schema/validator/csharp/dotnetSoln_dharsingh/MyProject/Tests/ActivityProtocolValidatorTests.cs @@ -0,0 +1,98 @@ +using MyProject.Services; +using Xunit; + +namespace MyProject.Tests +{ + public class ActivityProtocolValidatorTests + { + private readonly ActivityProtocolValidator _validator; + + public ActivityProtocolValidatorTests() + { + _validator = new ActivityProtocolValidator(); + } + + [Fact] + public void ValidateActivity_WithValidMessageActivity_ShouldReturnValid() + { + // Arrange + var validMessageJson = """ + { + "type": "message", + "id": "msg-001", + "channelId": "webchat", + "from": { + "id": "user123", + "name": "John Doe" + }, + "conversation": { + "id": "conv-456" + }, + "recipient": { + "id": "bot789", + "name": "My Bot" + }, + "serviceUrl": "https://example.com/api", + "text": "Hello, world!", + "timestamp": "2025-09-17T10:00:00Z" + } + """; + + // Act + var result = _validator.ValidateActivity(validMessageJson); + + // Assert + Assert.True(result.IsValid); + Assert.Equal("message", result.ActivityType); + Assert.Empty(result.Errors); + } + + [Theory] + [InlineData("01-message.json")] + [InlineData("02-contactRelationUpdate.json")] + [InlineData("03-conversationUpdate.json")] + [InlineData("04-typing.json")] + [InlineData("05-endOfConversation.json")] + [InlineData("06-event.json")] + public void ValidateActivity_FromJsonFile(string filename) + { + // Arrange + var messageJsonPath = Path.Combine("Tests", filename); + + // Verify file exists + Assert.True(File.Exists(messageJsonPath), $"Test file not found at: {messageJsonPath}"); + + var messageJsonContent = File.ReadAllText(messageJsonPath); + + // Act + var result = _validator.ValidateActivity(messageJsonContent); + + // Assert + Assert.True(result.IsValid, $"Validation failed. Errors: {string.Join(", ", result.Errors)}"); + Assert.Equal("message", result.ActivityType); + Assert.Empty(result.Errors); + Assert.Contains("Activity validation successful", result.ValidationMessage); + } + + [Fact] + public async Task ValidateActivityAsync_FromMessageJsonFile_ShouldReturnValid() + { + // Arrange + var messageJsonPath = Path.Combine("Tests", "message.json"); + + // Verify file exists + Assert.True(File.Exists(messageJsonPath), $"Test file not found at: {messageJsonPath}"); + + var messageJsonContent = await File.ReadAllTextAsync(messageJsonPath); + + // Act + var result = await _validator.ValidateActivityAsync(messageJsonContent); + + // Assert + Assert.True(result.IsValid, $"Async validation failed. Errors: {string.Join(", ", result.Errors)}"); + Assert.Equal("message", result.ActivityType); + Assert.Empty(result.Errors); + Assert.Contains("Activity validation successful", result.ValidationMessage); + } + } +} \ No newline at end of file diff --git a/specs/activity/schema/validator/csharp/dotnetSoln_dharsingh/MyProject/Tests/message.json b/specs/activity/schema/validator/csharp/dotnetSoln_dharsingh/MyProject/Tests/message.json new file mode 100644 index 00000000..89337365 --- /dev/null +++ b/specs/activity/schema/validator/csharp/dotnetSoln_dharsingh/MyProject/Tests/message.json @@ -0,0 +1,48 @@ +{ + "type": "message", + "id": "message-activity-1", + "timestamp": "2025-09-15T10:00:01.000Z", + "localTimestamp": "2025-09-15T03:00:01.000-07:00", + "localTimezone": "America/Los_Angeles", + "serviceUrl": "https://smba.trafficmanager.net/amer/", + "channelId": "msteams", + "from": { + "id": "user-id-1", + "name": "John Doe" + }, + "conversation": { + "isGroup": true, + "id": "conv-1", + "name": "Project Discussion" + }, + "recipient": { + "id": "bot-id-1", + "name": "HelpBot" + }, + "text": "Hello, I need help with my order.", + "textFormat": "plain", + "locale": "en-US", + "attachments": [ + { + "contentType": "application/vnd.microsoft.card.hero", + "content": { + "title": "Order #12345", + "text": "Status: Shipped" + } + } + ], + "suggestedActions": { + "actions": [ + { + "type": "imBack", + "title": "Track Package", + "value": "track order 12345" + }, + { + "type": "imBack", + "title": "Cancel Order", + "value": "cancel order 12345" + } + ] + } +} \ No newline at end of file diff --git a/specs/activity/schema/validator/csharp/dotnetSoln_dharsingh/MyProject/activity_protocol.schema b/specs/activity/schema/validator/csharp/dotnetSoln_dharsingh/MyProject/activity_protocol.schema new file mode 100644 index 00000000..601cf216 --- /dev/null +++ b/specs/activity/schema/validator/csharp/dotnetSoln_dharsingh/MyProject/activity_protocol.schema @@ -0,0 +1,827 @@ + +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://raw.githubusercontent.com/microsoft/botframework-sdk/main/specs/activity/protocol-activity.schema.json", + "title": "Activity", + "description": "An Activity is the basic communication type for the Bot Framework 3.0 protocol.", + "type": "object", + "discriminator": { + "propertyName": "type" + }, + "oneOf": [ + { + "$ref": "#/definitions/message" + }, + { + "$ref": "#/definitions/contactRelationUpdate" + }, + { + "$ref": "#/definitions/conversationUpdate" + }, + { + "$ref": "#/definitions/typing" + }, + { + "$ref": "#/definitions/endOfConversation" + }, + { + "$ref": "#/definitions/event" + }, + { + "$ref": "#/definitions/invoke" + }, + { + "$ref": "#/definitions/messageUpdate" + }, + { + "$ref": "#/definitions/messageDelete" + }, + { + "$ref": "#/definitions/installationUpdate" + }, + { + "$ref": "#/definitions/messageReaction" + }, + { + "$ref": "#/definitions/suggestion" + }, + { + "$ref": "#/definitions/trace" + }, + { + "$ref": "#/definitions/handoff" + } + ], + "definitions": { + "activity": { + "type": "object", + "properties": { + "type": { + "type": "string", + "description": "The type of the activity." + }, + "id": { + "type": "string", + "description": "The unique identifier for the activity." + }, + "timestamp": { + "type": "string", + "format": "date-time", + "description": "The UTC timestamp of the activity." + }, + "localTimestamp": { + "type": "string", + "format": "date-time", + "description": "The local timestamp of the activity." + }, + "localTimezone": { + "type": "string", + "description": "The timezone of the local timestamp." + }, + "callerId": { + "type": "string", + "description": "The caller ID." + }, + "serviceUrl": { + "type": "string", + "description": "The URL of the service." + }, + "channelId": { + "type": "string", + "description": "The ID of the channel." + }, + "from": { + "$ref": "#/definitions/channelAccount", + "description": "The sender of the activity." + }, + "conversation": { + "$ref": "#/definitions/conversationAccount", + "description": "The conversation the activity is part of." + }, + "recipient": { + "$ref": "#/definitions/channelAccount", + "description": "The recipient of the activity." + }, + "entities": { + "type": "array", + "items": { + "$ref": "#/definitions/entity" + }, + "description": "The entities of the activity." + }, + "channelData": { + "type": "object", + "description": "The channel-specific data." + }, + "replyToId": { + "type": "string", + "description": "The ID of the activity to reply to." + } + }, + "required": [ + "type", + "channelId", + "from", + "conversation", + "recipient", + "serviceUrl" + ] + }, + "messageBase": { + "type": "object", + "allOf": [ + { + "$ref": "#/definitions/activity" + } + ], + "properties": { + "locale": { + "type": "string", + "description": "The locale of the activity." + }, + "text": { + "type": "string", + "description": "The text of the activity." + }, + "textFormat": { + "type": "string", + "description": "The format of the text.", + "enum": [ + "plain", + "markdown", + "xml" + ] + }, + "speak": { + "type": "string", + "description": "The text to be spoken." + }, + "inputHint": { + "type": "string", + "description": "A hint for the input.", + "enum": [ + "accepting", + "expecting", + "ignoring" + ] + }, + "summary": { + "type": "string", + "description": "A summary of the activity." + }, + "suggestedActions": { + "$ref": "#/definitions/suggestedActions", + "description": "The suggested actions for the activity." + }, + "attachments": { + "type": "array", + "items": { + "$ref": "#/definitions/attachment" + }, + "description": "The attachments of the activity." + }, + "attachmentLayout": { + "type": "string", + "description": "The layout of the attachments.", + "enum": [ + "list", + "carousel" + ] + }, + "value": { + "type": "object", + "description": "The value of the activity." + }, + "expiration": { + "type": "string", + "format": "date-time", + "description": "The expiration time of the activity." + }, + "importance": { + "type": "string", + "description": "The importance of the activity.", + "enum": [ + "low", + "normal", + "high" + ] + }, + "deliveryMode": { + "type": "string", + "description": "The delivery mode of the activity.", + "enum": [ + "normal", + "notification", + "expectReplies" + ] + }, + "listenFor": { + "type": "array", + "items": { + "type": "string" + }, + "description": "A list of phrases to listen for." + }, + "semanticAction": { + "$ref": "#/definitions/semanticAction", + "description": "The semantic action of the activity." + } + } + }, + "message": { + "type": "object", + "allOf": [ + { + "$ref": "#/definitions/messageBase" + } + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "message" + ] + } + }, + "required": [ "type" ] + }, + "contactRelationUpdate": { + "type": "object", + "allOf": [ + { + "$ref": "#/definitions/activity" + } + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "contactRelationUpdate" + ] + }, + "action": { + "type": "string", + "enum": [ + "add", + "remove" + ] + } + }, + "required": [ "type" ] + }, + "conversationUpdate": { + "type": "object", + "allOf": [ + { + "$ref": "#/definitions/activity" + } + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "conversationUpdate" + ] + }, + "membersAdded": { + "type": "array", + "items": { + "$ref": "#/definitions/channelAccount" + }, + "description": "The members added to the conversation." + }, + "membersRemoved": { + "type": "array", + "items": { + "$ref": "#/definitions/channelAccount" + }, + "description": "The members removed from the conversation." + }, + "topicName": { + "type": "string", + "description": "The name of the topic." + }, + "historyDisclosed": { + "type": "boolean", + "description": "Indicates if the history is disclosed." + } + }, + "required": [ "type" ] + }, + "typing": { + "type": "object", + "allOf": [ + { + "$ref": "#/definitions/activity" + } + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "typing" + ] + } + }, + "required": [ "type" ] + }, + "endOfConversation": { + "type": "object", + "allOf": [ + { + "$ref": "#/definitions/activity" + } + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "endOfConversation" + ] + }, + "code": { + "type": "string", + "description": "The code of the activity." + }, + "text": { + "type": "string", + "description": "The text of the activity." + } + }, + "required": [ "type" ] + }, + "event": { + "type": "object", + "allOf": [ + { + "$ref": "#/definitions/activity" + } + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "event" + ] + }, + "name": { + "type": "string", + "description": "The name of the activity." + }, + "value": { + "type": "object", + "description": "The value of the activity." + }, + "relatesTo": { + "$ref": "#/definitions/conversationReference", + "description": "A reference to another conversation." + } + }, + "required": [ + "type", + "name" + ] + }, + "invoke": { + "type": "object", + "allOf": [ + { + "$ref": "#/definitions/activity" + } + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "invoke" + ] + }, + "name": { + "type": "string", + "description": "The name of the activity." + }, + "value": { + "type": "object", + "description": "The value of the activity." + }, + "relatesTo": { + "$ref": "#/definitions/conversationReference", + "description": "A reference to another conversation." + } + }, + "required": [ + "type", + "name" + ] + }, + "messageUpdate": { + "type": "object", + "allOf": [ + { + "$ref": "#/definitions/messageBase" + } + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "messageUpdate" + ] + } + }, + "required": [ "type" ] + }, + "messageDelete": { + "type": "object", + "allOf": [ + { + "$ref": "#/definitions/activity" + } + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "messageDelete" + ] + } + }, + "required": [ "type" ] + }, + "installationUpdate": { + "type": "object", + "allOf": [ + { + "$ref": "#/definitions/activity" + } + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "installationUpdate" + ] + }, + "action": { + "type": "string", + "enum": [ + "add", + "remove" + ] + } + }, + "required": [ "type" ] + }, + "messageReaction": { + "type": "object", + "allOf": [ + { + "$ref": "#/definitions/activity" + } + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "messageReaction" + ] + }, + "reactionsAdded": { + "type": "array", + "items": { + "$ref": "#/definitions/messageReactionObject" + }, + "description": "The reactions added to the activity." + }, + "reactionsRemoved": { + "type": "array", + "items": { + "$ref": "#/definitions/messageReactionObject" + }, + "description": "The reactions removed from the activity." + } + }, + "required": [ "type" ] + }, + "suggestion": { + "type": "object", + "allOf": [ + { + "$ref": "#/definitions/messageBase" + } + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "suggestion" + ] + }, + "textHighlights": { + "type": "array", + "items": { + "$ref": "#/definitions/textHighlight" + }, + "description": "The text highlights of the activity." + } + }, + "required": [ "type" ] + }, + "trace": { + "type": "object", + "allOf": [ + { + "$ref": "#/definitions/activity" + } + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "trace" + ] + }, + "name": { + "type": "string", + "description": "The name of the activity." + }, + "label": { + "type": "string", + "description": "The label of the activity." + }, + "valueType": { + "type": "string", + "description": "The type of the value." + }, + "value": { + "type": "object", + "description": "The value of the activity." + }, + "relatesTo": { + "$ref": "#/definitions/conversationReference", + "description": "A reference to another conversation." + } + }, + "required": [ "type" ] + }, + "handoff": { + "type": "object", + "allOf": [ + { + "$ref": "#/definitions/activity" + } + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "handoff" + ] + } + }, + "required": [ "type" ] + }, + "channelAccount": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "The ID of the channel account." + }, + "name": { + "type": "string", + "description": "The name of the channel account." + }, + "aadObjectId": { + "type": "string", + "description": "The Azure Active Directory object ID of the channel account." + }, + "role": { + "type": "string", + "description": "The role of the channel account.", + "enum": [ + "user", + "bot" + ] + } + }, + "required": [ + "id" + ] + }, + "conversationAccount": { + "type": "object", + "properties": { + "isGroup": { + "type": "boolean", + "description": "Indicates if the conversation is a group conversation." + }, + "conversationType": { + "type": "string", + "description": "The type of the conversation." + }, + "id": { + "type": "string", + "description": "The ID of the conversation." + }, + "name": { + "type": "string", + "description": "The name of the conversation." + }, + "aadObjectId": { + "type": "string", + "description": "The Azure Active Directory object ID of the conversation." + }, + "role": { + "type": "string", + "description": "The role of the conversation account.", + "enum": [ + "user", + "bot" + ] + }, + "tenantId": { + "type": "string", + "description": "The tenant ID of the conversation." + } + }, + "required": [ + "id" + ] + }, + "messageReactionObject": { + "type": "object", + "properties": { + "type": { + "type": "string", + "description": "The type of the message reaction." + } + } + }, + "suggestedActions": { + "type": "object", + "properties": { + "to": { + "type": "array", + "items": { + "type": "string" + }, + "description": "The recipients of the suggested actions." + }, + "actions": { + "type": "array", + "items": { + "$ref": "#/definitions/cardAction" + }, + "description": "The suggested actions." + } + } + }, + "attachment": { + "type": "object", + "properties": { + "contentType": { + "type": "string", + "description": "The content type of the attachment." + }, + "contentUrl": { + "type": "string", + "description": "The URL of the attachment content." + }, + "content": { + "type": "object", + "description": "The content of the attachment." + }, + "name": { + "type": "string", + "description": "The name of the attachment." + }, + "thumbnailUrl": { + "type": "string", + "description": "The URL of the attachment thumbnail." + } + } + }, + "entity": { + "type": "object", + "properties": { + "type": { + "type": "string", + "description": "The type of the entity." + } + } + }, + "conversationReference": { + "type": "object", + "properties": { + "activityId": { + "type": "string", + "description": "The ID of the activity." + }, + "user": { + "$ref": "#/definitions/channelAccount", + "description": "The user." + }, + "bot": { + "$ref": "#/definitions/channelAccount", + "description": "The bot." + }, + "conversation": { + "$ref": "#/definitions/conversationAccount", + "description": "The conversation." + }, + "channelId": { + "type": "string", + "description": "The ID of the channel." + }, + "serviceUrl": { + "type": "string", + "description": "The URL of the service." + }, + "locale": { + "type": "string", + "description": "The locale." + } + } + }, + "textHighlight": { + "type": "object", + "properties": { + "text": { + "type": "string", + "description": "The text to highlight." + }, + "occurrence": { + "type": "integer", + "description": "The occurrence of the text to highlight." + } + } + }, + "cardAction": { + "type": "object", + "properties": { + "type": { + "type": "string", + "description": "The type of the card action.", + "enum": [ + "openUrl", + "imBack", + "postBack", + "playAudio", + "playVideo", + "showImage", + "downloadFile", + "signin", + "call", + "messageBack" + ] + }, + "title": { + "type": "string", + "description": "The title of the card action." + }, + "image": { + "type": "string", + "description": "The image of the card action." + }, + "text": { + "type": "string", + "description": "The text of the card action." + }, + "displayText": { + "type": "string", + "description": "The display text of the card action." + }, + "value": { + "type": "string", + "description": "The value of the card action." + }, + "channelData": { + "type": "object", + "description": "The channel-specific data." + } + } + }, + "semanticAction": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "The ID of the semantic action." + }, + "entities": { + "type": "object", + "description": "The entities of the semantic action." + }, + "state": { + "type": "string", + "enum": [ + "start", + "continue", + "done" + ] + } + } + } + } +} diff --git a/specs/activity/schema/validator/csharp/dotnetSoln_dharsingh/MyProject/appsettings.Development.json b/specs/activity/schema/validator/csharp/dotnetSoln_dharsingh/MyProject/appsettings.Development.json new file mode 100644 index 00000000..0c208ae9 --- /dev/null +++ b/specs/activity/schema/validator/csharp/dotnetSoln_dharsingh/MyProject/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/specs/activity/schema/validator/csharp/dotnetSoln_dharsingh/MyProject/appsettings.json b/specs/activity/schema/validator/csharp/dotnetSoln_dharsingh/MyProject/appsettings.json new file mode 100644 index 00000000..10f68b8c --- /dev/null +++ b/specs/activity/schema/validator/csharp/dotnetSoln_dharsingh/MyProject/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/specs/activity/schema/validator/examples/activity-examples.md b/specs/activity/schema/validator/examples/activity-examples.md new file mode 100644 index 00000000..fbd5a484 --- /dev/null +++ b/specs/activity/schema/validator/examples/activity-examples.md @@ -0,0 +1,348 @@ +# Activity Protocol Examples + +This file contains example JSON payloads for each of the activity types defined in the `activity-protocol.schema.json`. + +## 1. Message + +A `message` activity represents content intended to be shown within a conversational interface. + +```json +{ + "type": "message", + "id": "message-activity-1", + "timestamp": "2025-09-15T10:00:01.000Z", + "localTimestamp": "2025-09-15T03:00:01.000-07:00", + "localTimezone": "America/Los_Angeles", + "serviceUrl": "https://smba.trafficmanager.net/amer/", + "channelId": "msteams", + "from": { "id": "user-id-1", "name": "John Doe" }, + "conversation": { "isGroup": true, "id": "conv-1", "name": "Project Discussion" }, + "recipient": { "id": "bot-id-1", "name": "HelpBot" }, + "text": "Hello, I need help with my order.", + "textFormat": "plain", + "locale": "en-US", + "attachments": [ + { + "contentType": "application/vnd.microsoft.card.hero", + "content": { + "title": "Order #12345", + "text": "Status: Shipped" + } + } + ], + "suggestedActions": { + "actions": [ + { + "type": "imBack", + "title": "Track Package", + "value": "track order 12345" + }, + { + "type": "imBack", + "title": "Cancel Order", + "value": "cancel order 12345" + } + ] + } +} +``` + +## 2. Contact Relation Update + +A `contactRelationUpdate` activity signals a change in the relationship between the bot and a user. + +```json +{ + "type": "contactRelationUpdate", + "id": "contact-relation-update-1", + "timestamp": "2025-09-15T10:00:02.000Z", + "serviceUrl": "https://smba.trafficmanager.net/amer/", + "channelId": "skype", + "from": { "id": "user-id-1", "name": "John Doe" }, + "conversation": { "id": "conv-2" }, + "recipient": { "id": "bot-id-1", "name": "WelcomeBot" }, + "action": "add" +} +``` + +## 3. Conversation Update + +A `conversationUpdate` activity describes a change in a conversation's members, description, or other properties. + +```json +{ + "type": "conversationUpdate", + "id": "conversation-update-1", + "timestamp": "2025-09-15T10:00:03.000Z", + "serviceUrl": "https://smba.trafficmanager.net/amer/", + "channelId": "msteams", + "from": { "id": "user-id-2", "name": "Jane Smith" }, + "conversation": { "isGroup": true, "id": "conv-1", "name": "Project Discussion" }, + "recipient": { "id": "bot-id-1", "name": "HelpBot" }, + "membersAdded": [ + { "id": "user-id-3", "name": "Sam Brown" } + ], + "membersRemoved": [], + "topicName": "Project Discussion Kickoff" +} +``` + +## 4. Typing + +A `typing` activity indicates that a user or bot is typing. + +```json +{ + "type": "typing", + "id": "typing-1", + "timestamp": "2025-09-15T10:00:04.000Z", + "serviceUrl": "https://smba.trafficmanager.net/amer/", + "channelId": "webchat", + "from": { "id": "user-id-1", "name": "John Doe" }, + "conversation": { "id": "conv-3" }, + "recipient": { "id": "bot-id-1", "name": "EchoBot" } +} +``` + +## 5. End of Conversation + +An `endOfConversation` activity signals the end of a conversation. + +```json +{ + "type": "endOfConversation", + "id": "end-of-conv-1", + "timestamp": "2025-09-15T10:00:05.000Z", + "serviceUrl": "https://smba.trafficmanager.net/amer/", + "channelId": "msteams", + "from": { "id": "bot-id-1", "name": "SurveyBot" }, + "conversation": { "id": "conv-4" }, + "recipient": { "id": "user-id-4", "name": "Peter Jones" }, + "code": "completedSuccessfully", + "text": "Thanks for completing the survey!" +} +``` + +## 6. Event + +An `event` activity communicates programmatic information from a client or channel to a bot. + +```json +{ + "type": "event", + "id": "event-1", + "timestamp": "2025-09-15T10:00:06.000Z", + "serviceUrl": "https://smba.trafficmanager.net/amer/", + "channelId": "webchat", + "from": { "id": "user-id-1", "name": "John Doe" }, + "conversation": { "id": "conv-5" }, + "recipient": { "id": "bot-id-1", "name": "LocationBot" }, + "name": "locationReceived", + "value": { + "latitude": 47.6062, + "longitude": -122.3321 + } +} +``` + +## 7. Invoke + +An `invoke` activity is a request/response style activity that communicates programmatic information between a client/channel and a bot. + +```json +{ + "type": "invoke", + "id": "invoke-1", + "timestamp": "2025-09-15T10:00:07.000Z", + "serviceUrl": "https://smba.trafficmanager.net/amer/", + "channelId": "msteams", + "from": { "id": "user-id-1", "name": "John Doe" }, + "conversation": { "id": "conv-6" }, + "recipient": { "id": "bot-id-1", "name": "TaskBot" }, + "name": "task/submit", + "value": { + "taskId": "task-987", + "data": { + "comments": "All done." + } + } +} +``` + +## 8. Message Update + +A `messageUpdate` activity represents an update of an existing message activity. + +```json +{ + "type": "messageUpdate", + "id": "message-activity-1", + "timestamp": "2025-09-15T10:00:08.000Z", + "serviceUrl": "https://smba.trafficmanager.net/amer/", + "channelId": "msteams", + "from": { "id": "user-id-1", "name": "John Doe" }, + "conversation": { "id": "conv-1" }, + "recipient": { "id": "bot-id-1", "name": "HelpBot" }, + "text": "Hello, I need help with my order #12345.", + "locale": "en-US" +} +``` + +## 9. Message Delete + +A `messageDelete` activity represents a deletion of an existing message activity. + +```json +{ + "type": "messageDelete", + "id": "message-activity-to-delete", + "timestamp": "2025-09-15T10:00:09.000Z", + "serviceUrl": "https://smba.trafficmanager.net/amer/", + "channelId": "msteams", + "from": { "id": "user-id-1", "name": "John Doe" }, + "conversation": { "id": "conv-1" }, + "recipient": { "id": "bot-id-1", "name": "HelpBot" } +} +``` + +## 10. Installation Update + +An `installationUpdate` activity represents the installation or uninstallation of a bot. + +```json +{ + "type": "installationUpdate", + "id": "install-update-1", + "timestamp": "2025-09-15T10:00:10.000Z", + "serviceUrl": "https://smba.trafficmanager.net/amer/", + "channelId": "msteams", + "from": { "id": "user-id-admin", "name": "Admin User" }, + "conversation": { "id": "conv-tenant-level" }, + "recipient": { "id": "bot-id-1", "name": "AdminBot" }, + "action": "add" +} +``` + +## 11. Message Reaction + +A `messageReaction` activity represents a social interaction on an existing message. + +```json +{ + "type": "messageReaction", + "id": "reaction-1", + "timestamp": "2025-09-15T10:00:11.000Z", + "serviceUrl": "https://smba.trafficmanager.net/amer/", + "channelId": "msteams", + "from": { "id": "user-id-2", "name": "Jane Smith" }, + "conversation": { "id": "conv-1" }, + "recipient": { "id": "bot-id-1", "name": "HelpBot" }, + "replyToId": "message-activity-1", + "reactionsAdded": [ + { "type": "like" } + ], + "reactionsRemoved": [] +} +``` + +## 12. Suggestion + +A `suggestion` activity allows a bot to suggest content to a single user that augments a previous activity. + +```json +{ + "type": "suggestion", + "id": "suggestion-1", + "timestamp": "2025-09-15T10:00:12.000Z", + "serviceUrl": "https://smba.trafficmanager.net/amer/", + "channelId": "cortana", + "from": { "id": "bot-id-1", "name": "CalendarBot" }, + "conversation": { "id": "conv-7" }, + "recipient": { "id": "user-id-1", "name": "John Doe" }, + "replyToId": "original-message-id", + "text": "I can create that meeting for you.", + "textHighlights": [ + { + "text": "meet on Monday", + "occurrence": 1 + } + ] +} +``` + +## 13. Trace + +A `trace` activity is used for logging and debugging purposes. It is typically not shown to the user. + +```json +{ + "type": "trace", + "id": "trace-1", + "timestamp": "2025-09-15T10:00:13.000Z", + "serviceUrl": "https://smba.trafficmanager.net/amer/", + "channelId": "emulator", + "from": { "id": "bot-id-1", "name": "DebuggingBot" }, + "conversation": { "id": "conv-debug" }, + "recipient": { "id": "user-id-dev", "name": "Developer" }, + "name": "LUIS_TRACE", + "label": "LUIS Trace", + "valueType": "https://www.luis.ai/schemas/trace", + "value": { + "query": "book a flight to paris", + "topScoringIntent": { + "intent": "BookFlight", + "score": 0.98 + }, + "entities": [ + { "type": "Location", "entity": "paris" } + ] + } +} +``` + +## 14. Handoff + +A `handoff` activity is used to request or signal a change in focus between elements inside a bot, such as handing off to a human agent. + +```json +{ + "type": "handoff", + "id": "handoff-1", + "timestamp": "2025-09-15T10:00:14.000Z", + "serviceUrl": "https://smba.trafficmanager.net/amer/", + "channelId": "custom-channel", + "from": { "id": "bot-id-1", "name": "TriageBot" }, + "conversation": { "id": "conv-8" }, + "recipient": { "id": "human-agent-system", "name": "Live Agent Hub" }, + "value": { + "reason": "User requested human agent", + "transcript": [ + "User: I need to speak to a person.", + "Bot: OK, I'll connect you to a live agent." + ] + } +} +``` + +## 15. Simple + +```json +{ + "type": "message", + "channelId": "emulator", + "from": { + "id": "user1", + "name": "User One" + }, + "conversation": { + "id": "conv1", + "name": "Conversation 1" + }, + "recipient": { + "id": "bot", + "name": "Bot" + }, + "serviceUrl": "https://example.org", + "text": "Hello, world!" +} +``` diff --git a/specs/activity/schema/validator/examples/json/01-message.json b/specs/activity/schema/validator/examples/json/01-message.json new file mode 100644 index 00000000..fab119d7 --- /dev/null +++ b/specs/activity/schema/validator/examples/json/01-message.json @@ -0,0 +1,38 @@ +{ + "type": "message", + "id": "message-activity-1", + "timestamp": "2025-09-15T10:00:01.000Z", + "localTimestamp": "2025-09-15T03:00:01.000-07:00", + "localTimezone": "America/Los_Angeles", + "serviceUrl": "https://smba.trafficmanager.net/amer/", + "channelId": "msteams", + "from": { "id": "user-id-1", "name": "John Doe" }, + "conversation": { "isGroup": true, "id": "conv-1", "name": "Project Discussion" }, + "recipient": { "id": "bot-id-1", "name": "HelpBot" }, + "text": "Hello, I need help with my order.", + "textFormat": "plain", + "locale": "en-US", + "attachments": [ + { + "contentType": "application/vnd.microsoft.card.hero", + "content": { + "title": "Order #12345", + "text": "Status: Shipped" + } + } + ], + "suggestedActions": { + "actions": [ + { + "type": "imBack", + "title": "Track Package", + "value": "track order 12345" + }, + { + "type": "imBack", + "title": "Cancel Order", + "value": "cancel order 12345" + } + ] + } +} diff --git a/specs/activity/schema/validator/examples/json/02-contactRelationUpdate.json b/specs/activity/schema/validator/examples/json/02-contactRelationUpdate.json new file mode 100644 index 00000000..4385c25a --- /dev/null +++ b/specs/activity/schema/validator/examples/json/02-contactRelationUpdate.json @@ -0,0 +1,11 @@ +{ + "type": "contactRelationUpdate", + "id": "contact-relation-update-1", + "timestamp": "2025-09-15T10:00:02.000Z", + "serviceUrl": "https://smba.trafficmanager.net/amer/", + "channelId": "skype", + "from": { "id": "user-id-1", "name": "John Doe" }, + "conversation": { "id": "conv-2" }, + "recipient": { "id": "bot-id-1", "name": "WelcomeBot" }, + "action": "add" +} diff --git a/specs/activity/schema/validator/examples/json/03-conversationUpdate.json b/specs/activity/schema/validator/examples/json/03-conversationUpdate.json new file mode 100644 index 00000000..8f31bb22 --- /dev/null +++ b/specs/activity/schema/validator/examples/json/03-conversationUpdate.json @@ -0,0 +1,13 @@ +{ + "type": "conversationUpdate", + "id": "conversation-update-1", + "timestamp": "2025-09-15T10:00:03.000Z", + "serviceUrl": "https://smba.trafficmanager.net/amer/", + "channelId": "msteams", + "from": { "id": "user-id-2", "name": "Jane Smith" }, + "conversation": { "isGroup": true, "id": "conv-1", "name": "Project Discussion" }, + "recipient": { "id": "bot-id-1", "name": "HelpBot" }, + "membersAdded": [ { "id": "user-id-3", "name": "Sam Brown" } ], + "membersRemoved": [], + "topicName": "Project Discussion Kickoff" +} diff --git a/specs/activity/schema/validator/examples/json/04-typing.json b/specs/activity/schema/validator/examples/json/04-typing.json new file mode 100644 index 00000000..a085fed1 --- /dev/null +++ b/specs/activity/schema/validator/examples/json/04-typing.json @@ -0,0 +1,10 @@ +{ + "type": "typing", + "id": "typing-1", + "timestamp": "2025-09-15T10:00:04.000Z", + "serviceUrl": "https://smba.trafficmanager.net/amer/", + "channelId": "webchat", + "from": { "id": "user-id-1", "name": "John Doe" }, + "conversation": { "id": "conv-3" }, + "recipient": { "id": "bot-id-1", "name": "EchoBot" } +} diff --git a/specs/activity/schema/validator/examples/json/05-endOfConversation.json b/specs/activity/schema/validator/examples/json/05-endOfConversation.json new file mode 100644 index 00000000..d612a606 --- /dev/null +++ b/specs/activity/schema/validator/examples/json/05-endOfConversation.json @@ -0,0 +1,12 @@ +{ + "type": "endOfConversation", + "id": "end-of-conv-1", + "timestamp": "2025-09-15T10:00:05.000Z", + "serviceUrl": "https://smba.trafficmanager.net/amer/", + "channelId": "msteams", + "from": { "id": "bot-id-1", "name": "SurveyBot" }, + "conversation": { "id": "conv-4" }, + "recipient": { "id": "user-id-4", "name": "Peter Jones" }, + "code": "completedSuccessfully", + "text": "Thanks for completing the survey!" +} diff --git a/specs/activity/schema/validator/examples/json/06-event.json b/specs/activity/schema/validator/examples/json/06-event.json new file mode 100644 index 00000000..5e6a504d --- /dev/null +++ b/specs/activity/schema/validator/examples/json/06-event.json @@ -0,0 +1,12 @@ +{ + "type": "event", + "id": "event-1", + "timestamp": "2025-09-15T10:00:06.000Z", + "serviceUrl": "https://smba.trafficmanager.net/amer/", + "channelId": "webchat", + "from": { "id": "user-id-1", "name": "John Doe" }, + "conversation": { "id": "conv-5" }, + "recipient": { "id": "bot-id-1", "name": "LocationBot" }, + "name": "locationReceived", + "value": { "latitude": 47.6062, "longitude": -122.3321 } +} diff --git a/specs/activity/schema/validator/examples/json/07-invoke.json b/specs/activity/schema/validator/examples/json/07-invoke.json new file mode 100644 index 00000000..ca29e277 --- /dev/null +++ b/specs/activity/schema/validator/examples/json/07-invoke.json @@ -0,0 +1,12 @@ +{ + "type": "invoke", + "id": "invoke-1", + "timestamp": "2025-09-15T10:00:07.000Z", + "serviceUrl": "https://smba.trafficmanager.net/amer/", + "channelId": "msteams", + "from": { "id": "user-id-1", "name": "John Doe" }, + "conversation": { "id": "conv-6" }, + "recipient": { "id": "bot-id-1", "name": "TaskBot" }, + "name": "task/submit", + "value": { "taskId": "task-987", "data": { "comments": "All done." } } +} diff --git a/specs/activity/schema/validator/examples/json/08-messageUpdate.json b/specs/activity/schema/validator/examples/json/08-messageUpdate.json new file mode 100644 index 00000000..b4cf0870 --- /dev/null +++ b/specs/activity/schema/validator/examples/json/08-messageUpdate.json @@ -0,0 +1,12 @@ +{ + "type": "messageUpdate", + "id": "message-activity-1", + "timestamp": "2025-09-15T10:00:08.000Z", + "serviceUrl": "https://smba.trafficmanager.net/amer/", + "channelId": "msteams", + "from": { "id": "user-id-1", "name": "John Doe" }, + "conversation": { "id": "conv-1" }, + "recipient": { "id": "bot-id-1", "name": "HelpBot" }, + "text": "Hello, I need help with my order #12345.", + "locale": "en-US" +} diff --git a/specs/activity/schema/validator/examples/json/09-messageDelete.json b/specs/activity/schema/validator/examples/json/09-messageDelete.json new file mode 100644 index 00000000..44b86ce9 --- /dev/null +++ b/specs/activity/schema/validator/examples/json/09-messageDelete.json @@ -0,0 +1,10 @@ +{ + "type": "messageDelete", + "id": "message-activity-to-delete", + "timestamp": "2025-09-15T10:00:09.000Z", + "serviceUrl": "https://smba.trafficmanager.net/amer/", + "channelId": "msteams", + "from": { "id": "user-id-1", "name": "John Doe" }, + "conversation": { "id": "conv-1" }, + "recipient": { "id": "bot-id-1", "name": "HelpBot" } +} diff --git a/specs/activity/schema/validator/examples/json/10-installationUpdate.json b/specs/activity/schema/validator/examples/json/10-installationUpdate.json new file mode 100644 index 00000000..09f494d6 --- /dev/null +++ b/specs/activity/schema/validator/examples/json/10-installationUpdate.json @@ -0,0 +1,11 @@ +{ + "type": "installationUpdate", + "id": "install-update-1", + "timestamp": "2025-09-15T10:00:10.000Z", + "serviceUrl": "https://smba.trafficmanager.net/amer/", + "channelId": "msteams", + "from": { "id": "user-id-admin", "name": "Admin User" }, + "conversation": { "id": "conv-tenant-level" }, + "recipient": { "id": "bot-id-1", "name": "AdminBot" }, + "action": "add" +} diff --git a/specs/activity/schema/validator/examples/json/11-messageReaction.json b/specs/activity/schema/validator/examples/json/11-messageReaction.json new file mode 100644 index 00000000..1cb415fe --- /dev/null +++ b/specs/activity/schema/validator/examples/json/11-messageReaction.json @@ -0,0 +1,13 @@ +{ + "type": "messageReaction", + "id": "reaction-1", + "timestamp": "2025-09-15T10:00:11.000Z", + "serviceUrl": "https://smba.trafficmanager.net/amer/", + "channelId": "msteams", + "from": { "id": "user-id-2", "name": "Jane Smith" }, + "conversation": { "id": "conv-1" }, + "recipient": { "id": "bot-id-1", "name": "HelpBot" }, + "replyToId": "message-activity-1", + "reactionsAdded": [ { "type": "like" } ], + "reactionsRemoved": [] +} diff --git a/specs/activity/schema/validator/examples/json/12-suggestion.json b/specs/activity/schema/validator/examples/json/12-suggestion.json new file mode 100644 index 00000000..6b68ad16 --- /dev/null +++ b/specs/activity/schema/validator/examples/json/12-suggestion.json @@ -0,0 +1,13 @@ +{ + "type": "suggestion", + "id": "suggestion-1", + "timestamp": "2025-09-15T10:00:12.000Z", + "serviceUrl": "https://smba.trafficmanager.net/amer/", + "channelId": "cortana", + "from": { "id": "bot-id-1", "name": "CalendarBot" }, + "conversation": { "id": "conv-7" }, + "recipient": { "id": "user-id-1", "name": "John Doe" }, + "replyToId": "original-message-id", + "text": "I can create that meeting for you.", + "textHighlights": [ { "text": "meet on Monday", "occurrence": 1 } ] +} diff --git a/specs/activity/schema/validator/examples/json/13-trace.json b/specs/activity/schema/validator/examples/json/13-trace.json new file mode 100644 index 00000000..94d6e9bb --- /dev/null +++ b/specs/activity/schema/validator/examples/json/13-trace.json @@ -0,0 +1,18 @@ +{ + "type": "trace", + "id": "trace-1", + "timestamp": "2025-09-15T10:00:13.000Z", + "serviceUrl": "https://smba.trafficmanager.net/amer/", + "channelId": "emulator", + "from": { "id": "bot-id-1", "name": "DebuggingBot" }, + "conversation": { "id": "conv-debug" }, + "recipient": { "id": "user-id-dev", "name": "Developer" }, + "name": "LUIS_TRACE", + "label": "LUIS Trace", + "valueType": "https://www.luis.ai/schemas/trace", + "value": { + "query": "book a flight to paris", + "topScoringIntent": { "intent": "BookFlight", "score": 0.98 }, + "entities": [ { "type": "Location", "entity": "paris" } ] + } +} diff --git a/specs/activity/schema/validator/examples/json/14-handoff.json b/specs/activity/schema/validator/examples/json/14-handoff.json new file mode 100644 index 00000000..35e86e3d --- /dev/null +++ b/specs/activity/schema/validator/examples/json/14-handoff.json @@ -0,0 +1,11 @@ +{ + "type": "handoff", + "id": "handoff-1", + "timestamp": "2025-09-15T10:00:14.000Z", + "serviceUrl": "https://smba.trafficmanager.net/amer/", + "channelId": "custom-channel", + "from": { "id": "bot-id-1", "name": "TriageBot" }, + "conversation": { "id": "conv-8" }, + "recipient": { "id": "human-agent-system", "name": "Live Agent Hub" }, + "value": { "reason": "User requested human agent", "transcript": [ "User: I need to speak to a person.", "Bot: OK, I'll connect you to a live agent." ] } +} diff --git a/specs/activity/schema/validator/examples/json/15-simple-message.json b/specs/activity/schema/validator/examples/json/15-simple-message.json new file mode 100644 index 00000000..6b631190 --- /dev/null +++ b/specs/activity/schema/validator/examples/json/15-simple-message.json @@ -0,0 +1,9 @@ +{ + "type": "message", + "channelId": "emulator", + "from": { "id": "user1", "name": "User One" }, + "conversation": { "id": "conv1", "name": "Conversation 1" }, + "recipient": { "id": "bot", "name": "Bot" }, + "serviceUrl": "https://example.org", + "text": "Hello, world!" +} diff --git a/specs/activity/schema/validator/js/README.md b/specs/activity/schema/validator/js/README.md new file mode 100644 index 00000000..60eea49b --- /dev/null +++ b/specs/activity/schema/validator/js/README.md @@ -0,0 +1,106 @@ +# Activity Schema Validator (JavaScript) + +This directory contains a lightweight validation script for the Activity Protocol schema. It compiles the JSON Schema with Ajv and validates all embedded JSON examples found in `activity-examples.md`. + +## Contents + +- `validate-activity.js` – Loads `activity-protocol.schema.json`, extracts JSON code blocks from `activity-examples.md`, and validates each example. +- `README.md` – This documentation. + +## Prerequisites + +- Node.js 18+ (supports ES Modules & top-level features used here) +- Dependencies: `ajv`, `ajv-formats` + +Install (from the repo root or this folder): + +```bash +npm install ajv ajv-formats --save-dev +``` + +> If a top-level `package.json` already exists (it does in this repo), you can just install once at the root and run the script from this subfolder. + +## Running the Validator + +From this directory: + +```bash +node validate-activity.js +``` + +You can also run from the repo root: + +```bash +node specs/activity/schema/validator/js/validate-activity.js +``` + +Exit codes: +- `0` – All examples are valid. +- `1` – At least one example failed validation (errors printed to stderr). + +## How It Works + +1. Reads the schema at `../../activity-protocol.schema.json`. +2. Compiles it with Ajv (draft-07, `discriminator` support enabled, all errors collected). +3. Reads `../../activity-examples.md` and extracts every fenced block marked as: + ``` + ```json + { ... } + ``` + ``` +4. Parses each block to an object (ignoring blocks that don't parse). +5. Validates each example and prints a per-example result plus a final summary. + + +## Modifying the Schema + +When adding a new Activity type: + +1. Create a new definition under `definitions` (e.g., `"myCustomActivity"`). +2. Give it a `properties.type.const` with its unique discriminator value. +3. Add a `$ref` to it in the top-level `oneOf` array. +4. Re-run the validator to ensure examples still pass. + +For additional message-like variants: + +- Reuse the shared `messageBase` (introduced to avoid conflicting `const` inheritance) via: + ```json + "allOf": [ { "$ref": "#/definitions/messageBase" } ] + ``` + then add a `type.const` specific to your new variant. + +## Extending Example Extraction + +If you want to also validate examples stored as separate `.json` files, you could enhance `validate-activity.js`: + +```js +// Pseudocode addition +const examplesDir = path.join(__dirname, '../../examples'); +if (fs.existsSync(examplesDir)) { + for (const f of fs.readdirSync(examplesDir)) { + if (f.endsWith('.json')) { + examples.push(JSON.parse(fs.readFileSync(path.join(examplesDir,f), 'utf8'))); + } + } +} +``` + +## Troubleshooting + +| Problem | Likely Cause | Fix | +|---------|--------------|-----| +| `MODULE_NOT_FOUND: ajv` | Dependencies not installed | Run `npm install ajv ajv-formats` | +| All examples suddenly failing | Schema syntax error | Validate schema JSON (e.g. with an online validator) | +| Discriminator not selecting subtype | Missing `type.const` or absent subtype in `oneOf` | Add const + add `$ref` in `oneOf` | +| ES Module import error | Older Node.js or `type` not set | Run with Node 18+; package root may need `{ "type": "module" }` | + +## License + +See the repository's top-level `LICENSE` (MIT). + +## Contributing + +Open a PR with schema changes plus updated examples. Ensure `node validate-activity.js` returns success. + +--- +Generated documentation. diff --git a/specs/activity/schema/validator/js/validate-activity.js b/specs/activity/schema/validator/js/validate-activity.js new file mode 100644 index 00000000..4570c90c --- /dev/null +++ b/specs/activity/schema/validator/js/validate-activity.js @@ -0,0 +1,76 @@ +import fs from 'fs' +import path from 'path' +import Ajv from 'ajv' +import addFormats from 'ajv-formats' + +const __dirname = path.dirname(new URL(import.meta.url).pathname.substring(1)) + +// Initialize AJV +const ajv = new Ajv({ allErrors: true, discriminator: true }) +addFormats(ajv) + +// Load the schema +const schemaPath = path.join(__dirname, '../../activity-protocol.schema.json') +const schema = JSON.parse(fs.readFileSync(schemaPath, 'utf8')) + +// Compile the schema +const validate = ajv.compile(schema) + +// Collect examples from markdown +const examples = [] +const mdExamplesPath = path.join(__dirname, '../../activity-examples.md') +if (fs.existsSync(mdExamplesPath)) { + const examplesContent = fs.readFileSync(mdExamplesPath, 'utf8') + const codeBlockRegex = /```json\n([\s\S]*?)\n```/g + let match + while ((match = codeBlockRegex.exec(examplesContent)) !== null) { + try { + examples.push({ source: 'markdown', data: JSON.parse(match[1]) }) + } catch (e) { + console.error('Failed to parse JSON example from markdown:', e) + } + } +} + +// Collect standalone JSON example files +const jsonExamplesDir = path.join(__dirname, '../examples/json') +if (fs.existsSync(jsonExamplesDir)) { + for (const file of fs.readdirSync(jsonExamplesDir)) { + if (file.toLowerCase().endsWith('.json')) { + try { + const data = JSON.parse(fs.readFileSync(path.join(jsonExamplesDir, file), 'utf8')) + examples.push({ source: file, data }) + } catch (e) { + console.error(`Failed to parse JSON example file ${file}:`, e) + } + } + } +} + +if (examples.length === 0) { + console.warn('No examples found to validate.') + process.exit(0) +} + +// Validate each example +let allValid = true +examples.forEach((example, index) => { + const valid = validate(example.data) + const label = example.data.type || 'unknown' + const origin = example.source + if (valid) { + console.log(`Example ${index + 1} [${origin}] (${label}) is valid.`) + } else { + allValid = false + console.error(`Example ${index + 1} [${origin}] (${label}) is invalid:`) + console.error(validate.errors) + } + console.log('---') +}) + +if (allValid) { + console.log('All examples are valid!') +} else { + console.error('Some examples are invalid.') + process.exit(1) +}