Skip to content

Commit 743fc10

Browse files
adileiMateusz Wasilewskiadilei
authored
Adilei readme editing (#428)
* Implement core chat functionality with API endpoints, message handling, and UI integration * Update project to .NET 9, implement live chat service, and add webhook handling * Refactor message handling in ChatController and WebhookService; improve error handling and UI feedback for failed messages * Update OutgoingWebhookUrl in appsettings.json to correct endpoint for live chat messages * Remove development appsettings.json file for live chat configuration * Implement conversation management and proactive messaging for live chat integration * Refactor IConversationManager to change UpdateServiceUrlMapping method from Task to void; remove unused conversation manager field in CopilotStudioAgent * working v1 * Refactor chat management by introducing ChatStorageService for conversation handling; update ChatController and LiveChatService for new conversation flow; enhance frontend to display conversation ID and manage chat state. * Refactor conversation management: update IConversationManager and ConversationManager to support new mapping methods; modify CopilotStudioAgent and MsTeamsProactiveMessage to use ConversationMapping for proactive messaging. * Update README.md to enhance project description and clarify webhook configuration; streamline API endpoints section and remove outdated features. * Add README.md to document project structure, configuration, and API flow for Handover To Live Agent Sample * Add images and zip files for Handover To Live Agent Sample documentation * Remove outdated HandoverAgentSample zip file to streamline project resources. * Update msAppId in skill manifest for Handoff Skill to reflect new application ID. * Update endpointUrl and msAppId placeholders in skill manifest for clarity * Enhance README and appsettings.json with detailed LiveChat configuration and setup instructions * readme editing * Move HandoverToLiveAgent to IntegrateWithEngagementHub/HandoverToLiveAgentUsingSkill - Reorganize sample under IntegrateWithEngagementHub directory - Rename folder to HandoverToLiveAgentUsingSkill for clarity - Update README with improved documentation: - Add comprehensive Prerequisites section - Clarify skill vs action terminology in mermaid diagram - Add detailed explanation of dual app registration requirement - Move architecture diagram to 'How It Works' section - Add security warnings for appsettings.json - Add 'Verify Your Setup' section with testing steps - Add comprehensive Troubleshooting section - Add Cleanup instructions - Replace 'Production Considerations' with 'Future Enhancements' - Fix terminology consistency (agent vs bot, skill vs action) - Use auto-incrementing numbered lists for easier maintenance --------- Co-authored-by: Mateusz Wasilewski <mawasile@microsoft.com> Co-authored-by: adilei <adileibowiz@microsoft.com>
1 parent a2730da commit 743fc10

23 files changed

Lines changed: 2146 additions & 0 deletions
158 KB
Loading
52.4 KB
Loading
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<Project Sdk="Microsoft.NET.Sdk.Web">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net9.0</TargetFramework>
5+
<Nullable>enable</Nullable>
6+
<ImplicitUsings>enable</ImplicitUsings>
7+
</PropertyGroup>
8+
9+
</Project>
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
using System.Runtime.ExceptionServices;
2+
using System.Security;
3+
using Microsoft.AspNetCore.Mvc;
4+
5+
[ApiController]
6+
[Route("api/[controller]")]
7+
public class ChatController : ControllerBase
8+
{
9+
private readonly ChatStorageService _chatStorage;
10+
private readonly WebhookService _webhookService;
11+
private readonly ILogger<ChatController> _logger;
12+
13+
public ChatController(ChatStorageService chatStorage, WebhookService webhookService, ILogger<ChatController> logger)
14+
{
15+
_chatStorage = chatStorage;
16+
_webhookService = webhookService;
17+
_logger = logger;
18+
}
19+
20+
// GET: api/chat/messages - Get all chat messages
21+
[HttpGet("messages")]
22+
public ActionResult<IEnumerable<ChatMessage>> GetMessages(string? conversationId = null)
23+
{
24+
var messages = _chatStorage.GetAllMessages(conversationId);
25+
return Ok(messages);
26+
}
27+
28+
// POST: api/chat/start - Start a new conversation and return conversation ID
29+
[HttpPost("start")]
30+
public ActionResult StartConversation()
31+
{
32+
try
33+
{
34+
var conversationId = Guid.NewGuid().ToString()[..5];
35+
_logger.LogInformation("Started new conversation with ID: {ConversationId}", conversationId);
36+
37+
_chatStorage.StartConversation(conversationId);
38+
39+
return Ok(new { conversationId });
40+
}
41+
catch (Exception ex)
42+
{
43+
_logger.LogError(ex, "Error starting conversation");
44+
return StatusCode(500, new { error = ex.Message });
45+
}
46+
}
47+
48+
// POST: api/chat/end - End a conversation
49+
[HttpPost("end")]
50+
public ActionResult EndConversation([FromBody] MessageRequest request)
51+
{
52+
try
53+
{
54+
_logger.LogInformation("Ending conversation with ID: {ConversationId}", request.ConversationId);
55+
_chatStorage.EndConversation(request.ConversationId);
56+
return Ok(new { message = "Conversation ended successfully" });
57+
}
58+
catch (Exception ex)
59+
{
60+
_logger.LogError(ex, "Error ending conversation");
61+
return StatusCode(500, new { error = ex.Message });
62+
}
63+
}
64+
65+
// POST: api/chat/send - Send a message (from Live Chat to Copilot Studio webhook)
66+
[HttpPost("send")]
67+
public async Task<ActionResult> SendMessage([FromBody] MessageRequest request)
68+
{
69+
if (string.IsNullOrWhiteSpace(request.Message))
70+
{
71+
return BadRequest(new { error = "Message text cannot be empty" });
72+
}
73+
74+
var message = new ChatMessage
75+
{
76+
ConversationId = request.ConversationId,
77+
Message = request.Message,
78+
Sender = "Contoso Support",
79+
Timestamp = DateTime.UtcNow
80+
};
81+
82+
// Send to webhook first
83+
var (statusCode, errorMessage) = await _webhookService.SendMessageAsync(message);
84+
85+
if (statusCode.HasValue && statusCode >= 200 && statusCode < 300)
86+
{
87+
// Only add to chat history if webhook send was successful
88+
_chatStorage.AddMessage(message.ConversationId, message);
89+
return Ok(new { message = "Message sent successfully", messageId = message.Id, timestamp = message.Timestamp, sender = message.Sender, conversationId = message.ConversationId });
90+
}
91+
else
92+
{
93+
return StatusCode(statusCode ?? 500, new { error = errorMessage });
94+
}
95+
}
96+
97+
// POST: api/chat/receive - Receive a message (from Copilot Studio )
98+
[HttpPost("receive")]
99+
public ActionResult ReceiveMessage([FromBody] MessageRequest request)
100+
{
101+
if (string.IsNullOrWhiteSpace(request.Message))
102+
{
103+
return BadRequest(new { error = "Message text cannot be empty" });
104+
}
105+
106+
var message = new ChatMessage
107+
{
108+
ConversationId = request.ConversationId,
109+
Message = request.Message,
110+
Sender = request.Sender ?? "Remote",
111+
Timestamp = DateTime.UtcNow
112+
};
113+
114+
_chatStorage.AddMessage(message.ConversationId, message);
115+
_logger.LogInformation("Received message from {Sender}: {Text}", message.Sender, message.Message);
116+
117+
return Ok(new { message = "Message received successfully", messageId = message.Id });
118+
}
119+
}
120+
121+
public class MessageRequest
122+
{
123+
public string ConversationId { get; set; } = string.Empty;
124+
public string Message { get; set; } = string.Empty;
125+
public string? Sender { get; set; }
126+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
public class ChatMessage
2+
{
3+
public string ConversationId { get; set; } = string.Empty;
4+
public string Id { get; set; } = Guid.NewGuid().ToString();
5+
public string Message { get; set; } = string.Empty;
6+
public string Sender { get; set; } = string.Empty;
7+
public DateTime Timestamp { get; set; } = DateTime.UtcNow;
8+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
var builder = WebApplication.CreateBuilder(args);
2+
3+
builder.WebHost.ConfigureKestrel(serverOptions =>
4+
{
5+
serverOptions.ListenAnyIP(5000);
6+
});
7+
8+
builder.Services.AddControllers();
9+
builder.Services.AddEndpointsApiExplorer();
10+
builder.Services.AddSingleton<ChatStorageService>();
11+
builder.Services.AddHttpClient<WebhookService>();
12+
13+
var app = builder.Build();
14+
15+
var webhookUrl = app.Configuration["WebhookSettings:OutgoingWebhookUrl"];
16+
app.Logger.LogInformation("Webhook URL configured: {WebhookUrl}", webhookUrl);
17+
18+
app.UseDefaultFiles();
19+
app.UseStaticFiles();
20+
app.UseRouting();
21+
app.MapControllers();
22+
23+
app.Run();
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
# Contoso Live Chat App
2+
3+
A simple live chat application designed as mock live agent handover scenarios with Copilot Studio. This application simulates a live chat support system that can receive conversations from Copilot Studio and send messages back.
4+
5+
## Project Structure
6+
7+
```
8+
ContosoLiveChatApp/
9+
├── Controllers/
10+
│ └── ChatController.cs # API endpoints for chat operations (used by Copilot Studio agent)
11+
├── Models/
12+
│ └── ChatMessage.cs # Chat message data model
13+
├── Services/
14+
│ ├── ChatStorageService.cs # Conversation-based message storage
15+
│ └── WebhookService.cs # Outgoing webhook sender (send messages to Copilot Studio agent)
16+
├── wwwroot/
17+
│ └── index.html # Chat UI with conversation management
18+
├── Program.cs # Application entry point
19+
├── appsettings.json # Configuration (webhook URL)
20+
```
21+
22+
## Configuration
23+
24+
Configure the webhook URL in `appsettings.json`:
25+
26+
```json
27+
{
28+
"WebhookSettings": {
29+
"OutgoingWebhookUrl": "http://localhost:5001/api/livechat/messages"
30+
}
31+
}
32+
```
33+
This endpoint points to the Copilot Studio agent skill URL. The default configuration assumes the HandoverToLiveAgentSample is running on port 5001.
34+
35+
## Running the Application
36+
37+
1. Navigate to the project directory:
38+
```powershell
39+
cd CopilotStudioSamples\HandoverToLiveAgent\ContosoLiveChatApp
40+
```
41+
42+
2. Restore dependencies and run:
43+
```powershell
44+
dotnet run
45+
```
46+
47+
3. Open your browser and navigate to:
48+
```
49+
http://localhost:5000
50+
```
51+
52+
## API Endpoints
53+
54+
| Method | Endpoint | Description |
55+
|--------|----------|-------------|
56+
| `POST` | `/api/chat/start` | Start a new conversation, returns `conversationId` |
57+
| `GET` | `/api/chat/messages?conversationId={id}` | Get messages for a conversation |
58+
| `POST` | `/api/chat/send` | Send message to Copilot Studio via webhook |
59+
| `POST` | `/api/chat/receive` | Receive message from Copilot Studio |
60+
| `POST` | `/api/chat/end` | End conversation and clear from memory |
61+
62+
## Architecture
63+
64+
### API Flow
65+
66+
```mermaid
67+
sequenceDiagram
68+
participant CS as Copilot Studio
69+
participant API as Chat API
70+
participant Storage as ChatStorageService
71+
participant UI as Live Chat UI
72+
73+
CS->>API: POST /api/chat/start
74+
API-->>CS: conversationId
75+
76+
Note over API,Storage: Conversation Active
77+
78+
CS->>API: POST /api/chat/receive (from MCS)
79+
API->>Storage: Store message
80+
Storage-->>UI: Display message
81+
82+
UI->>API: POST /api/chat/send (message)
83+
API->>Storage: Store message
84+
API->>CS: Forward via webhook (to MCS)
85+
CS-->>UI: Message delivered
86+
87+
Note over CS,UI: Messages exchanged...
88+
89+
CS->>API: POST /api/chat/end
90+
API->>Storage: Clear conversation
91+
API-->>CS: Conversation ended
92+
```
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
using System.Collections.Concurrent;
2+
3+
public class ChatStorageService
4+
{
5+
private readonly ConcurrentDictionary<string, IList<ChatMessage>> _activeConversations = new();
6+
private readonly ILogger<ChatStorageService> _logger;
7+
8+
public ChatStorageService(ILogger<ChatStorageService> logger)
9+
{
10+
_logger = logger;
11+
}
12+
13+
public void AddMessage(string conversationId, ChatMessage message)
14+
{
15+
_logger.LogInformation("Adding message to conversation ID: {ConversationId}", conversationId);
16+
if (_activeConversations.TryGetValue(conversationId, out var messages))
17+
{
18+
messages.Add(message);
19+
}
20+
else
21+
{
22+
_logger.LogWarning("No active conversation found with ID: {ConversationId}", conversationId);
23+
}
24+
}
25+
26+
public void StartConversation(string conversationId)
27+
{
28+
_activeConversations[conversationId] = new List<ChatMessage>();
29+
_logger.LogInformation("Started conversation with ID: {ConversationId}", conversationId);
30+
}
31+
32+
public void EndConversation(string conversationId)
33+
{
34+
_activeConversations.TryRemove(conversationId, out _);
35+
_logger.LogInformation("Ended conversation with ID: {ConversationId}", conversationId);
36+
}
37+
38+
public IEnumerable<ChatMessage> GetAllMessages(string? conversationId)
39+
{
40+
if (string.IsNullOrEmpty(conversationId))
41+
{
42+
return _activeConversations.Values
43+
.SelectMany(messages => messages)
44+
.OrderBy(m => m.Timestamp);
45+
}
46+
47+
_logger.LogInformation("Retrieving messages for conversation ID: {ConversationId}", conversationId);
48+
if (_activeConversations.TryGetValue(conversationId, out var messages))
49+
{
50+
return messages.OrderBy(m => m.Timestamp);
51+
}
52+
else
53+
{
54+
_logger.LogWarning("No active conversation found with ID: {ConversationId}", conversationId);
55+
return Enumerable.Empty<ChatMessage>();
56+
}
57+
}
58+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
using System.Text;
2+
using System.Text.Json;
3+
4+
public class WebhookService
5+
{
6+
private readonly HttpClient _httpClient;
7+
private readonly IConfiguration _configuration;
8+
private readonly ILogger<WebhookService> _logger;
9+
10+
public WebhookService(HttpClient httpClient, IConfiguration configuration, ILogger<WebhookService> logger)
11+
{
12+
_httpClient = httpClient;
13+
_configuration = configuration;
14+
_logger = logger;
15+
}
16+
17+
public async Task<Tuple<int?, string>> SendMessageAsync(ChatMessage message)
18+
{
19+
try
20+
{
21+
var webhookUrl = _configuration["WebhookSettings:OutgoingWebhookUrl"];
22+
23+
if (string.IsNullOrEmpty(webhookUrl))
24+
{
25+
_logger.LogWarning("Webhook URL is not configured");
26+
return new Tuple<int?, string>(null, "Webhook URL is not configured");
27+
}
28+
29+
var json = JsonSerializer.Serialize(message);
30+
var content = new StringContent(json, Encoding.UTF8, "application/json");
31+
32+
var response = await _httpClient.PostAsync(webhookUrl, content);
33+
34+
if (response.IsSuccessStatusCode)
35+
{
36+
_logger.LogInformation("Message sent successfully to webhook: {MessageId}", message.Id);
37+
return new Tuple<int?, string>((int)response.StatusCode, string.Empty);
38+
}
39+
else
40+
{
41+
_logger.LogWarning("Failed to send message to webhook. Status: {StatusCode}", response.StatusCode);
42+
var errorMessage = await response.Content.ReadAsStringAsync();
43+
return new Tuple<int?, string>((int)response.StatusCode, errorMessage);
44+
}
45+
}
46+
catch (Exception ex)
47+
{
48+
_logger.LogError(ex, "Error sending message to webhook");
49+
return new Tuple<int?, string>(null, ex.Message);
50+
}
51+
}
52+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"Logging": {
3+
"LogLevel": {
4+
"Default": "Information",
5+
"Microsoft.AspNetCore": "Warning"
6+
}
7+
},
8+
"AllowedHosts": "*",
9+
"WebhookSettings": {
10+
"OutgoingWebhookUrl": "http://localhost:5001/api/livechat/messages"
11+
}
12+
}

0 commit comments

Comments
 (0)