RichMessageBase and Welcome message.

This commit is contained in:
Deep Blue 2023-11-27 15:00:26 -06:00
parent b7d9469cf1
commit cf08261afb
35 changed files with 210 additions and 60 deletions

View file

@ -81,4 +81,9 @@ public abstract class ConversationHookBase : IConversationHook
{
return Task.CompletedTask;
}
public virtual Task OnUserAgentConnectedInitially(Conversation conversation)
{
return Task.CompletedTask;
}
}

View file

@ -10,6 +10,14 @@ public interface IConversationHook
Conversation Conversation { get; }
IConversationHook SetConversation(Conversation conversation);
/// <summary>
/// Triggered when user connects with agent first time.
/// This hook is the good timing to show welcome infomation.
/// </summary>
/// <param name="conversation"></param>
/// <returns></returns>
Task OnUserAgentConnectedInitially(Conversation conversation);
/// <summary>
/// Triggered once for every new conversation.
/// </summary>

View file

@ -5,6 +5,7 @@ public interface IConversationService
IConversationStateService States { get; }
string ConversationId { get; }
Task<Conversation> NewConversation(Conversation conversation);
Task MarkConnectionReady(string conversationId);
void SetConversationId(string conversationId, List<string> states);
Task<Conversation> GetConversation(string id);
Task<List<Conversation>> GetConversations();

View file

@ -2,5 +2,6 @@ namespace BotSharp.Abstraction.Messaging;
public interface IRichMessage
{
string Type { get; }
string Text { get; set; }
}

View file

@ -1,8 +1,8 @@
namespace BotSharp.Abstraction.Messaging.Models.RichContent
{
public class QuickReplyMessage : IRichMessage
public class QuickReplyMessage : RichMessageBase, IRichMessage
{
public string Text { get; set; } = string.Empty;
public override string Type => "quick reply";
[JsonPropertyName("quick_replies")]
public List<QuickReplyElement> QuickReplies { get; set; } = new List<QuickReplyElement>();

View file

@ -3,12 +3,12 @@ namespace BotSharp.Abstraction.Messaging.Models.RichContent.Template
/// <summary>
/// https://developers.facebook.com/docs/messenger-platform/send-messages/buttons
/// </summary>
public class ButtonTemplateMessage : IRichMessage
public class ButtonTemplateMessage : RichMessageBase, IRichMessage, ITemplateMessage
{
public string Text { get; set; } = string.Empty;
public override string Type => "template";
[JsonPropertyName("template_type")]
public string TemplateType => "button";
public override string TemplateType => "button";
public List<ButtonElement> Buttons { get; set; } = new List<ButtonElement>();
}

View file

@ -4,15 +4,17 @@ namespace BotSharp.Abstraction.Messaging.Models.RichContent.Template;
/// Coupon Template
/// https://developers.facebook.com/docs/messenger-platform/send-messages/template/coupon
/// </summary>
public class CouponTemplateMessage : IRichMessage, ITemplateMessage
public class CouponTemplateMessage : RichMessageBase, IRichMessage, ITemplateMessage
{
[JsonIgnore]
public string Text { get; set; }
public string Title { get; set; }
public string Subtitle { get; set; }
public override string Type => "template";
[JsonPropertyName("template_type")]
public string TemplateType => "coupon";
public override string TemplateType => "coupon";
[JsonIgnore]
public override string Text { get; set; }
public string Title { get; set; }
public string Subtitle { get; set; }
[JsonPropertyName("coupon_code")]
public string CouponCode { get; set; }

View file

@ -1,8 +1,8 @@
namespace BotSharp.Abstraction.Messaging.Models.RichContent.Template
{
public class MultiSelectTemplateMessage : IRichMessage
public class MultiSelectTemplateMessage : RichMessageBase, IRichMessage
{
public string Text { get; set; } = string.Empty;
public override string Type => "template";
[JsonPropertyName("template_type")]
public string TemplateType => "multi-select";

View file

@ -2,6 +2,7 @@ namespace BotSharp.Abstraction.Messaging.Models.RichContent.Template;
public class ProductTemplateMessage : TemplateMessageBase<ProductElement>, IRichMessage
{
[JsonPropertyName("template_type")]
public override string TemplateType => "product";
}

View file

@ -1,12 +1,15 @@
namespace BotSharp.Abstraction.Messaging.Models.RichContent.Template
{
public class TemplateMessageBase<T>
public class TemplateMessageBase<T> : RichMessageBase
{
[JsonIgnore]
public string Text { get; set; }
public override string Type => "template";
[JsonPropertyName("template_type")]
public virtual string TemplateType => string.Empty;
public T[] Elements { get; set; }
}
}

View file

@ -1,8 +1,8 @@
namespace BotSharp.Abstraction.Messaging.Models.RichContent;
public class TextMessage : IRichMessage
public class TextMessage : RichMessageBase, IRichMessage
{
public string Text { get; set; } = string.Empty;
public string Type => "text";
public TextMessage(string text)
{

View file

@ -0,0 +1,11 @@
namespace BotSharp.Abstraction.Messaging;
public class RichMessageBase
{
public virtual string Type { get; }
public virtual string Text { get; set; }
[JsonPropertyName("template_type")]
public virtual string TemplateType { get; }
}

View file

@ -84,6 +84,9 @@ public partial class ConversationService : IConversationService
var hooks = _services.GetServices<IConversationHook>().ToList();
foreach (var hook in hooks)
{
// If user connect agent first time
await hook.OnUserAgentConnectedInitially(sess);
await hook.OnConversationInitialized(record);
}
@ -115,4 +118,15 @@ public partial class ConversationService : IConversationService
_state.Load(_conversationId);
states.ForEach(x => _state.SetState(x.Split('=')[0], x.Split('=')[1]));
}
public async Task MarkConnectionReady(string conversationId)
{
var hooks = _services.GetServices<IConversationHook>();
var conv = await GetConversation(conversationId);
foreach (var hook in hooks)
{
// Need to check if user connected with agent is the first time.
await hook.OnUserAgentConnectedInitially(conv);
}
}
}

View file

@ -41,6 +41,13 @@ public class ConversationController : ControllerBase, IApiAdapter
return ConversationViewModel.FromSession(conv);
}
[HttpPatch("/conversation/{conversationId}/ready")]
public async Task ReadyToInteractive([FromRoute] string conversationId)
{
var service = _services.GetRequiredService<IConversationService>();
await service.MarkConnectionReady(conversationId);
}
[HttpGet("/conversations/{agentId}")]
public async Task<IEnumerable<ConversationViewModel>> GetConversations()
{

View file

@ -1,6 +1,7 @@
using BotSharp.Abstraction.Agents.Models;
using BotSharp.Abstraction.Functions.Models;
using BotSharp.Abstraction.Routing.Models;
using System.Text.Json.Serialization;
namespace BotSharp.OpenAPI.ViewModels.Agents;
@ -15,12 +16,20 @@ public class AgentViewModel
public List<AgentResponse> Responses { get; set; }
public List<string> Samples { get; set; }
public bool IsPublic { get; set; }
[JsonPropertyName("allow_routing")]
public bool AllowRouting { get; set; }
public bool Disabled { get; set; }
public List<string> Profiles { get; set; }
[JsonPropertyName("routing_rules")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public List<RoutingRule> RoutingRules { get; set; }
[JsonPropertyName("created_datetime")]
public DateTime CreatedDateTime { get; set; }
[JsonPropertyName("updated_datetime")]
public DateTime UpdatedDateTime { get; set; }
public static AgentViewModel FromAgent(Agent agent)

View file

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
@ -6,7 +6,7 @@
<LangVersion>$(LangVersion)</LangVersion>
<VersionPrefix>$(BotSharpVersion)</VersionPrefix>
<GeneratePackageOnBuild>$(GeneratePackageOnBuild)</GeneratePackageOnBuild>
<GenerateDocumentationFile>True</GenerateDocumentationFile>
<GenerateDocumentationFile>$(GenerateDocumentationFile)</GenerateDocumentationFile>
</PropertyGroup>
<ItemGroup>

View file

@ -7,7 +7,7 @@
<LangVersion>$(LangVersion)</LangVersion>
<VersionPrefix>$(BotSharpVersion)</VersionPrefix>
<GeneratePackageOnBuild>$(GeneratePackageOnBuild)</GeneratePackageOnBuild>
<GenerateDocumentationFile>True</GenerateDocumentationFile>
<GenerateDocumentationFile>$(GenerateDocumentationFile)</GenerateDocumentationFile>
</PropertyGroup>
<ItemGroup>

View file

@ -1,3 +1,4 @@
using BotSharp.Abstraction.Messaging;
using Microsoft.AspNetCore.SignalR;
namespace BotSharp.Plugin.ChatHub.Hooks;
@ -14,6 +15,30 @@ public class ChatHubConversationHook : ConversationHookBase
_chatHub = chatHub;
}
public override async Task OnUserAgentConnectedInitially(Conversation conversation)
{
var agentService = _services.GetService<IAgentService>();
var agent = await agentService.LoadAgent(conversation.AgentId);
// Check if the Welcome template exists.
var welcomeTemplate = agent.Templates.FirstOrDefault(x => x.Name == "welcome");
if (welcomeTemplate != null)
{
var messages = JsonSerializer.Deserialize<RichMessageBase[]>(welcomeTemplate.Content, new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
});
foreach (var message in messages)
{
await Task.Delay(300);
await OnResponseGenerated(new RoleDialogModel(AgentRole.Assistant, message.Text));
}
}
await base.OnUserAgentConnectedInitially(conversation);
}
public override async Task OnConversationInitialized(Conversation conversation)
{
var userService = _services.GetRequiredService<IUserService>();

View file

@ -17,28 +17,6 @@ public class SignalRHub : Hub
_user = user;
}
public async Task OnMessageReceivedFromClient(string message)
{
// await Clients.User(_user.Id).SendAsync("ReceiveMessage", message);
}
/// <summary>
/// Received message from client
/// </summary>
/// <param name="user"></param>
/// <param name="message"></param>
/// <returns></returns>
public async Task SendMessage(string user, string message)
{
// await Clients.User(_user.Id).SendAsync("ReceiveMessage", message);
}
public async Task SendMessageToCaller(string user, string message)
=> await Clients.Caller.SendAsync("ReceiveMessage", user, message);
public async Task SendMessageToGroup(string user, string message)
=> await Clients.Group("SignalR Users").SendAsync("ReceiveMessage", user, message);
public override Task OnConnectedAsync()
{
Console.WriteLine($"SignalR Hub: {Context.UserIdentifier} connected in {Context.ConnectionId} [{DateTime.Now}]");

View file

@ -6,7 +6,7 @@
<LangVersion>$(LangVersion)</LangVersion>
<VersionPrefix>$(BotSharpVersion)</VersionPrefix>
<GeneratePackageOnBuild>$(GeneratePackageOnBuild)</GeneratePackageOnBuild>
<GenerateDocumentationFile>True</GenerateDocumentationFile>
<GenerateDocumentationFile>$(GenerateDocumentationFile)</GenerateDocumentationFile>
</PropertyGroup>
<ItemGroup>

View file

@ -6,7 +6,7 @@
<LangVersion>$(LangVersion)</LangVersion>
<VersionPrefix>$(BotSharpVersion)</VersionPrefix>
<GeneratePackageOnBuild>$(GeneratePackageOnBuild)</GeneratePackageOnBuild>
<GenerateDocumentationFile>True</GenerateDocumentationFile>
<GenerateDocumentationFile>$(GenerateDocumentationFile)</GenerateDocumentationFile>
</PropertyGroup>
<ItemGroup>

View file

@ -6,7 +6,7 @@
<LangVersion>$(LangVersion)</LangVersion>
<VersionPrefix>$(BotSharpVersion)</VersionPrefix>
<GeneratePackageOnBuild>$(GeneratePackageOnBuild)</GeneratePackageOnBuild>
<GenerateDocumentationFile>True</GenerateDocumentationFile>
<GenerateDocumentationFile>$(GenerateDocumentationFile)</GenerateDocumentationFile>
</PropertyGroup>
<ItemGroup>

View file

@ -5,7 +5,7 @@
<LangVersion>$(LangVersion)</LangVersion>
<VersionPrefix>$(BotSharpVersion)</VersionPrefix>
<GeneratePackageOnBuild>$(GeneratePackageOnBuild)</GeneratePackageOnBuild>
<GenerateDocumentationFile>GenerateDocumentationFile</GenerateDocumentationFile>
<GenerateDocumentationFile>$(GenerateDocumentationFile)</GenerateDocumentationFile>
</PropertyGroup>
<ItemGroup>

View file

@ -10,4 +10,6 @@ public class AttachmentMessage : IRichMessage
[JsonPropertyName("attachment")]
public AttachmentBody Attachment { get; set; }
public string Type => "template";
}

View file

@ -6,7 +6,7 @@
<LangVersion>$(LangVersion)</LangVersion>
<VersionPrefix>$(BotSharpVersion)</VersionPrefix>
<GeneratePackageOnBuild>$(GeneratePackageOnBuild)</GeneratePackageOnBuild>
<GenerateDocumentationFile>True</GenerateDocumentationFile>
<GenerateDocumentationFile>$(GenerateDocumentationFile)</GenerateDocumentationFile>
</PropertyGroup>
<ItemGroup>

View file

@ -5,7 +5,7 @@
<LangVersion>$(LangVersion)</LangVersion>
<VersionPrefix>$(BotSharpVersion)</VersionPrefix>
<GeneratePackageOnBuild>$(GeneratePackageOnBuild)</GeneratePackageOnBuild>
<GenerateDocumentationFile>True</GenerateDocumentationFile>
<GenerateDocumentationFile>$(GenerateDocumentationFile)</GenerateDocumentationFile>
</PropertyGroup>
<PropertyGroup>

View file

@ -0,0 +1,8 @@
[
{
"text": "Hello, I'm an AI assistant that help you do variety of tasks."
},
{
"text": "How can I help you today?"
}
]

View file

@ -1,11 +1,11 @@
{
"name": "embedded-chat",
"name": "web-live-chat",
"version": "0.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "embedded-chat",
"name": "web-live-chat",
"version": "0.1.0",
"dependencies": {
"@microsoft/signalr": "^8.0.0",

View file

@ -16,6 +16,19 @@
* @property {string} assembly - The plugin assembly.
*/
/**
* @typedef {Object} AgentWelcomeInfo
* @property {string[]} messages - The welcome messages in Rich content format.
*/
/**
* @typedef {Object} AgentModel
* @property {string} id - Agent Id.
* @property {string} name - Agent name.
* @property {string} description - Agent description.
* @property {AgentWelcomeInfo} welcome_info - Welcome information.
*/
/**
* @typedef {Object} ConversationModel
* @property {string} id - The conversation id.

View file

@ -0,0 +1,20 @@
import { agentListUrl, agentDetailUrl } from '$lib/services/api-endpoints.js';
import axios from 'axios';
/**
* @returns {Promise<import('$typedefs').AgentModel[]>}
*/
export async function getAgents() {
const response = await axios.get(agentListUrl);
return response.data;
}
/**
* @param {string} id
* @returns {Promise<import('$typedefs').AgentModel>}
*/
export async function getAgent(id) {
let url = agentDetailUrl.replace("{id}", id);
const response = await axios.get(url);
return response.data;
}

View file

@ -14,6 +14,7 @@ export const agentDetailUrl = `${host}/agent/{id}`;
// conversation
export const conversationInitUrl = `${host}/conversation/{agentId}`
export const conversationReadyUrl = `${host}/conversation/{conversationId}/ready`
export const conversationMessageUrl = `${host}/conversation/{agentId}/{conversationId}`
export const conversationsUrl = `${host}/conversations/{agentId}`
export const dialogsUrl = `${host}/conversation/{conversationId}/dialogs`

View file

@ -2,6 +2,7 @@ import {
conversationInitUrl,
conversationMessageUrl,
dialogsUrl,
conversationReadyUrl,
} from './api-endpoints.js';
import axios from 'axios';
@ -17,6 +18,16 @@ export async function newConversation(agentId) {
return response.data;
}
/**
* Coversation is ready
* @param {string} conversationId
*/
export async function conversationReady(conversationId) {
let url = conversationReadyUrl.replace("{conversationId}", conversationId);
const response = await axios.patch(url, {});
return response.data;
}
/**
* Get dialog history
* @param {string} conversationId

View file

@ -13,7 +13,8 @@
/** @type {import('$typedefs').ConversationModel} */
let conversation;
let conversationId = "undefined";
let agentId = "undefined";
let agentId = params.agentId;
onMount(async () => {
let userToken = "";
@ -25,9 +26,11 @@
userToken = $page.url.searchParams.get('token') ?? "unauthorized";
}
setAuthorization(userToken);
conversation = await newConversation(params.agentId);
// new conversation
conversation = await newConversation(agentId);
conversationId = conversation.id;
agentId = params.agentId;
window.location.href = `/chat/${agentId}/${conversationId}?token=${userToken}`;
});
</script>

View file

@ -1,5 +1,29 @@
<script>
import Chat from './chat-box.svelte'
import Chat from './chat-box.svelte';
import { page } from '$app/stores';
import { onMount } from 'svelte';
import { setAuthorization } from '$lib/helpers/http';
import { myInfo } from '$lib/services/auth-service.js';
import { getAgent } from '$lib/services/agent-service.js';
const params = $page.params;
/** @type {import('$typedefs').AgentModel} */
let agent;
/** @type {import('$typedefs').UserModel} */
let currentUser;
onMount(async () => {
const token = $page.url.searchParams.get('token') ?? "unauthorized";
setAuthorization(token);
currentUser = await myInfo(token);
// get agent profile
let agentId = params.agentId;
agent = await getAgent(agentId);
});
</script>
<Chat />
<Chat currentUser={currentUser} agent={agent} />

View file

@ -13,10 +13,9 @@
import { page } from '$app/stores';
import { onMount } from 'svelte';
import Link from 'svelte-link';
import { myInfo } from '$lib/services/auth-service.js';
import { signalr } from '$lib/services/signalr-service.js';
import { sendMessageToHub, GetDialogs } from '$lib/services/conversation-service.js';
import { setAuthorization } from '$lib/helpers/http';
import { signalr } from '$lib/services/signalr-service.js';
import { sendMessageToHub, GetDialogs, conversationReady } from '$lib/services/conversation-service.js';
const options = {
scrollbars: {
@ -33,8 +32,11 @@
let text = "Hi";
/** @type {import('$typedefs').AgentModel} */
export let agent;
/** @type {import('$typedefs').UserModel} */
let currentUser;
export let currentUser;
// @ts-ignore
let scrollbar;
@ -46,14 +48,15 @@
const token = $page.url.searchParams.get('token') ?? "unauthorized";
setAuthorization(token);
currentUser = await myInfo(token);
dialogs = await GetDialogs(params.conversationId);
signalr.onMessageReceivedFromClient = onMessageReceivedFromClient;
signalr.onMessageReceivedFromCsr = onMessageReceivedFromCsr;
signalr.onMessageReceivedFromAssistant = onMessageReceivedFromAssistant;
await signalr.start();
await conversationReady(params.conversationId);
const scrollElements = document.querySelectorAll('.scrollbar');
scrollElements.forEach((item) => {
scrollbar = OverlayScrollbars(item, options);