透過 Microsoft BotFramework-WebChat 的 botchat.js 連接 Botframework 做的 Chatbot ,輕鬆 Web Application

我們透過 Microsoft BotFramework 來製作 Chatbot 程式後,除了可以接各透的 IM Channel 外,最快速的就是將它整合到現有的 Web Application 之中。Microsoft BotFramework-WebChat 已有提供範例讓我們去整合。
但是一般的網站並不需要一下子就顯示 WebChat ,而是在下方需要一個機器人的小圖示,按下去之後再顯示出 WebChat 。

解法

依上面的需求,我們需要 2 個 div, 一個放網頁下方的小圖示(WebChatButton), 另一個是放 WebChat (WebChatDialog)。
所以預設的 html 如下,

<div ID=“WebChatButtonD” onclick=“開啟WebChat的function”></div><div id=“WebChatDialog”></div>

透過 botchat.js 來建立 WebChat 需要一些設定值,所以就先定訂這些設定值的 interface ,如下,

interface IBotChatUIConfig {
botId: string,
botName: string,
userId: string,
userName: string,
botChatIconUrl: string, //下方機器人的圖示
chatTitle: string, // WebChat Header 的文字
directLineOptions: any, //DirectLine 的設定
locale: string, //語系
showWebChatButton: boolean // 是否顯示下方的機器人圖示
}

再來可以透過 BotChatUI 來封裝要一開始顯示出網頁下方的機器人圖示,按下後,要開啟 WebChat ,而按下 WebChat 的 Header 後,要將 WebChat Hide 起來,並顯示出機器人的圖示。
開始的畫面
另外,我們也可以透過調整 css 來讓 WebChat 長的不一樣,例如我們可以在 WebChat 中也顯示機器人的圖示。
開啟 WebChat 畫面

所以在即有的 Web Application 之中只要加入 WebChatCustomization 的 Botchat 目錄中的檔案,然後在要加入 WebChat 的網頁中加入以下的 Script 及設定您要的值,您就會有美美的 WebChat 了哦!

<!– chatbot –>
<link href=“Botchat/CSS/botchat.css” rel=“stylesheet” />
<link href=“Botchat/CSS/botchat-fullwindow.css” rel=“stylesheet” />
<link href=“Botchat/CSS/botchatCustom.css” rel=“stylesheet” />
<script src=“Botchat/Scripts/botchat-es5.js”></script>
<script src=“Botchat/Scripts/BotChatUI.js”></script>
<script>
var botChatUI = new BotChatUI({
botId: ‘RainmakerBot’,
botName: ‘小亂機器人’,
userId: ‘Rainmaker’,
userName: ‘亂馬客’,
chatTitle: ‘小亂機器人, 按我可縮小’,
locale: ‘zh-tw’,
directLineOptions: {
secret: ,
token: ,
pollingInterval: 1000,
webSocket: false
},
showWebChatButton: true
});
</script>
<!– chatbot –>

因為我想讓 IE 也可以 run ,所以我加入的是 botchat-es5.js。如果您不考慮 IE 的話,可以使用 botchat.js。
在 BotChatUI.ts 中,有特別處理,開啟 WebChat 時才建立 DirectLine 及 WebChat,關閉 WebChat 時,會先儲存 DirectLine 的 ConversationId,並 close 連線。重新開啟 WebChat 時,才知道原本的 ConversationId 是什麼,重新將原本的內容 Load 回來。
為什麼要這樣子呢? 因為我們是使用 offline Direct Line,目前還不 Support WebSocket ,所以 WebChat 會一直 Polling。所以當關掉 WebChat 時,DirectLine close 後,它就不會再 polling。
那要怎麼讓它重新再連線呢? 目前我們的做法是將原本的 WebChat 移掉再重新建立。詳細可以參考 BotChatUI.ts startConversation / endConversation functions。
而重新連的方式,如果有更好的方式會更新上去,如果大家知道的話,也請跟大家分享。

參考資料

WebChatCustomization

透過 WinDbg 來找出 ASP.NET CPU 100% ASP.NET 程式的問題

問題

我們有一個 ASP.NET 的系統部署到 IIS 上(Windows 2012, .NET 4.x),有時候會導致 w3wp.exe 吃掉所有的 CPU 資源,一直要等到應用程式回收後,程式再重新啟動後就正常了。這種狀況不定期會發生。

解法

我們可以使用 WinDbg 來找出導致 IIS CPU 100% 的原因,方法如下,

  • 透過「工作管理員」來「建立傾印檔案」
    當發生 IIS 導致 CPU 100% 時,開啟「工作管理員」,按右鍵選取「建立傾印檔案」。要依 Web 應用程式的平台(x86/x64)來開啟「工作管理員」(x86的工作管理員在 C:\Windows\SysWOW64\taskmgr.exe )。
  • 安裝 WinDbg
    請參考 Debugging Tools for Windows (WinDbg, KD, CDB, NTSD) 來安裝對應的版本。
  • 設定 Symbol Path
    先建立一個 C:\RTX64_SYMBOLS 目錄(依您自行定義),然後開啟 Command 設定 _NT_SYMBOL_PATH 的環境變數,如下,

    set _NT_SYMBOL_PATH=srv*C:\RTX64_SYMBOLS*https://msdl.microsoft.com/download/symbols

    set _NT_SYMBOL_PATH所以在 command 中,輸入 set 後,就可以看到 _NT_SYMBOL_PATH 的設定值哦,

    _NT_SYMBOL_PATH當然,如果常常會用到的話,就直接設定到 環境變數之前,下次 debug 時,就不用再設定一次了哦! 詳細請參考Set up your system to use Microsoft’s public Symbol Server

  • 在 WinDbg 中找問題
    • 載入 sos.dll
      WinDbg 有 x86/x64 的版本,我是使用 x64 的版本,所以 sos.dll 的路徑是 C:\Windows\Microsoft.NET\Framework64\v4.0.30319\sos.dll,您輸入如果發生錯誤的話,請使用 C:\Windows\Microsoft.NET\Framework\v4.0.30319\sos.dll 。
    • 設定 Symbol 檔目錄
      !sym noisy
      .cordll -ve -u -l
    • 透過 !runaway 顯示Threads所佔的時間
      輸入 !runaway 後,會顯示各 Thread 所花費的時間,花最多的會在最上面,如下圖,

      !runaway

      !runaway

    • 透過 ~[thread]s 切換到該 thread 的位置
      由上圖所示,Thread 40 佔最多時間,所以我們切到它的位罝去,

      ~40s

      ~40s

    • 透過 !clrstack 來查看呼叫堆疉
      !clrstack

      !clrstack從上圖可以發現,應該是有關 Dictionary 操作的問題,而它是由我們系統中 TemplateCfg.initial() 這個 Method 所引起的。

  • 檢視並調整程式碼
    開啟 TemplateCfg 程式碼,在 initial 這個 Method 之中會建立 Dictionary 物件,Clear 它,並 Insert 資料,但這些 Dictionary 的變數卻又設定成 static 。TemplateCfg.initial()即然這些 Dictionary 是 static 的,而且它們值又都是相同的,就沒有必要每次 request 時,就重新建立並 insert 這些資料。
    所以將 initial 裡面的 Code 搬到 static constructor 。
    調整完程式碼後,從去年觀察到目前,已經沒有再發生 CPU 100% 的狀況。

參考資料

Debugging in Production Part 1 – Analyzing 100% CPU Usage Using Windbg
WinDBG 應用實例:找出 ASP.NET CPU 100% 原因
Debugging Tools for Windows (WinDbg, KD, CDB, NTSD)
Set up your system to use Microsoft’s public Symbol Server
High CPU Hangs – 05
中小型研发团队架构实践:生产环境诊断利器WinDbg帮你快速分析异常情况Dump文件
Intro to WinDBG for .NET Developers
.NET Debugging Demos Lab 4: High CPU Hang – Review

無法載入檔案或組件 ‘log4net, Version=1.2.10.0, Culture=neutral, PublicKeyToken=692fbea5521e1304’ 或其相依性的其中之一

最近同事詢問他們使用 Crytal Report Viewer 時,會發生 log4net 版本衝突的問題。如下圖所示,
CrystalReportViewer Runtime Error

如果是不同版本衝突的話,可以參考 如何讓不同 PublicKeyToken 的 DLL assemblyBinding 到可以用版本? 透過 assemblyBinding 的方式就可以解決。 但剛好我們的 AP 自已用的 log4net 版本也是 1.2.10.0,所以是 相同版本,不同的 PublickToken 的衝突。 詳細訊息如下,

沒有辦法解決 “log4net, Version=1.2.10.0, Culture=neutral, PublicKeyToken=1b44e1d426115821” 和 “log4net, Version=1.2.10.0, Culture=neutral, PublicKeyToken=692fbea5521e1304” 之間的衝突。
任意選擇 “log4net, Version=1.2.10.0, Culture=neutral, PublicKeyToken=1b44e1d426115821”。

有查到 Error: Could not load log4net assembly 似乎是因為不同的平台(x86/x64) 所用的 log4net 會不同。
x86(32位元)用的 PublicToken 是 692fbea5521e1304,x64(64位元)或是 AnyCPU 用的 PublicToken 則是 1b44e1d426115821。
而我們 ap 平台是 x64 的,但 Crystal Report Viewer 卻使用了 x86(32位元) 的 log4net 。

嗯… 我想應該是那裡設定出了錯吧 …

跟同事討論後,同事又說只有在某台機器上才會出現那個錯誤,如果是透過 Browser 直接使用系統就正常,透過 VS.NET 啟動程式去 Debug 時,就會出現那個錯誤。
我想是因為如果透過 Browser 去使用系統的話,是連到 IIS ,而 IIS 設定的是 x64(64位元);如果透過 VS.NET 則是連到 IIS Express,預設它是使用 x86(32位元)。
於是筆者將 IIS 上的應用程式集區設定成 啟用 32 位元,再執行程式,則一樣會出現 log4net 的衝突狀況(  重現問題了 …^_^)。
所以就請同事在 VS.NET 中設定讓 IIS Express 使用 64 位元就沒問題了哦!
工具 -> 選項 -> (search textbox 中輸入 iis) , 並勾選 將 64 位元版本的 IIS Express 用於網站和專案(U) 選項勾起來就可以了哦! 如下,
將 64 位元版本的 IIS Express 用於網站和專案(U)

如果您是系統出了錯,請 Check 應用程式集區是否設定成了 「啟用32位元應用程式」哦!

分享 Call Language Understanding (LUIS) predictions api 的 3 種方式

在前一篇 「線上客服 + Chatbot = 智慧客服」我們透過 LUIS + Vital ESP(FAQ系統)  來打造智慧客服 Chatbot 。 在上線之前,需要先透過一堆問題去 Call LUIS  的 api ,來驗證我們在 LUIS  中設定的意圖及 Entities 是否正確。

即然是 Call LUIS  api 雲端服務,就會有費用問題,所以我們先來看一下 LUIS api 的價格說明,

luis.ai api 價格說明

  1. LUIS API – 免費: 每月最多可以 Call 10,000 次
  2. LUIS API – 基本: 每月最多可以 Call 10,000 次 (免費),超過後,每 Call 1,000 次約台幣 45.09 元
    詳細請參考 認知服務定價 – Language Understanding (LUIS)

目前我們可以在 LUIS 上手動測試意圖,但如果想要更有彈性的話,就要透過程式去呼叫它的 api ,所以以下我們就介紹 Call api 的 3 種方式,

Call luis.ai predictions api 的 3 種方式

在 LUIS predictions GET api 中有各語言的使用範例,以C#來看,使用非常方便,只要設定一些參數就可以取回 LUIS 解析的資料,本文將跟大家分享 Call 正式 api 的 GET/POST 方式,及 Call 測試 api 的方式,共 3 透方式。

  • call 正式 api 使用 GET,如下,
    static async Task<string> GetLuisResult(string reqString)
    {
    //最多是 500 個 ascii chars,中文我先取 230 個
    if (reqString.Length > 230)
    {
    reqString = reqString.Substring(0, 230);
    }
    var client = new HttpClient();
    var queryString = HttpUtility.ParseQueryString(string.Empty);
    const string appId = “你的appid”;
    const string subscriptionKey = “你的訂閱key”;
    // Request headers
    client.DefaultRequestHeaders.Add(“Ocp-Apim-Subscription-Key”, subscriptionKey);
    // Request parameters
    //如果回傳的內容中需要各 Intent 的話,請將 verbose 設定為 true
    queryString[“verbose”] = “true”;
    //queryString[“spellCheck”] = “{boolean}”;
    //queryString[“staging”] = “{boolean}”;
    //queryString[“bing-spell-check-subscription-key”] = “bing key”;
    //queryString[“log”] = “{boolean}”;
    var uri = $”https://eastasia.api.cognitive.microsoft.com/luis/v2.0/apps/{appId}?q=” + HttpUtility.UrlEncode(reqString)+ “&” + queryString;;
    var response = await client.GetAsync(uri);
    var result = await response.Content.ReadAsStringAsync();
    return result;
    }
  • call 正式 api 使用 POST (筆者試了老半天都試不出來,後來詢問 MS Herman 大大才知道,POST 的 body 前後要加 雙引號),如下,
    static async Task<string> GetLuisResultPost(string reqString)
    {
    //最多是 500 個 ascii chars,中文我先取 230 個
    if (reqString.Length > 230)
    {
    reqString = reqString.Substring(0, 230);
    }
    var client = new HttpClient();
    var queryString = HttpUtility.ParseQueryString(string.Empty);
    const string appId = “你的appid”;
    const string subscriptionKey = “你的訂閱key”;
    // Request headers
    client.DefaultRequestHeaders.Add(“Ocp-Apim-Subscription-Key”, subscriptionKey);
    // Request parameters
    //如果回傳的內容中需要各 Intent 的話,請將 verbose 設定為 true
    queryString[“verbose”] = “true”;
    //queryString[“spellCheck”] = “{boolean}”;
    //queryString[“staging”] = “{boolean}”;
    //queryString[“bing-spell-check-subscription-key”] = “bing key”;
    //queryString[“log”] = “{boolean}”;
    var uri = $”https://westus.api.cognitive.microsoft.com/luis/v2.0/apps/{appId}?” + queryString;
    HttpResponseMessage response;
    // Request body,前後要加上 雙引號 哦!
    byte[] byteData = Encoding.UTF8.GetBytes(“\”” + reqString + “\””);
    using (var content = new ByteArrayContent(byteData))
    {
    response = await client.PostAsync(uri, content);
    var result = await response.Content.ReadAsStringAsync();
    return result;
    }
    }
  • 註: 在 LUIS 設定好,按下 Train 後,請記得要到 PUBLISH , 按下 「Publish to production slot」,這樣才會部到各區域去哦!

知道如何 Call Production 的 api 之後,如果我們想要像 線上客服 + BOT 之路 一文中,先用個 Excel 檔去試看看的話(目前 luis.ai 上 support 上傳 json , 整批測試),多試個幾次,免費的 quota 應該一下子就沒了吧! 如下所示,
超過免費可呼叫的Quota

那怎麼辦呢?

  • 1.到 Azure 那買個服務,然後再將訂閱的 Key 加到 luis.ai 之中,如下,從Azure買付費的方案
  • 2.使用 luis.ai 測試的 api ,您可以在測試時,錄一下 network ,如下,luis.ai 測試的 api

所以 Call luis.ai 測試 api 的方式如下,

static async Task<string> GetLuisResultTest(string reqString)
{
//最多是 500 個 ascii chars,中文我先取 230 個
if (reqString.Length > 230)
{
reqString = reqString.Substring(0, 230);
}
var client = new HttpClient();
var queryString = HttpUtility.ParseQueryString(string.Empty);
const string appId = “你的appid”;
const string subscriptionKey = “你的免費訂閱key”;
// Request headers
client.DefaultRequestHeaders.Add(“Ocp-Apim-Subscription-Key”, subscriptionKey);
var uri = $”https://westus.api.cognitive.microsoft.com/luis/webapi/v2.0/apps/{appId}/versions/0.1/predict?example=” + HttpUtility.UrlEncode(reqString);
var response = await client.GetAsync(uri);
var result = await response.Content.ReadAsStringAsync();
return result;
}

這個測試的 api 跟正式機的差別除了內容不太相同,它取出的 Entity 中間會有空白,所以再使用上需要將空白清掉哦! 因為它是測試用的,所以在整批 Call 的時候,有時會 block 一段時間,我的做法是如果出錯就停個 15 秒,讓它再重試一次,後來就可以正常再 Call 了哦!

正式 api 的結果如下 ( verbose 參數為 true, 內容才會有 intents ),

{
“query”: “下載下來可以用了,可是什麼是網址識別”,
“topScoringIntent”: {
“intent”: “FAQ”,
“score”: 1.0
},
“intents”: [
{
“intent”: “FAQ”,
“score”: 1.0
},
{
“intent”: “None”,
“score”: 0.05330094
}
],
“entities”: [
{
“entity”: “網址”,
“type”: “Keyword”,
“startIndex”: 14,
“endIndex”: 15,
“score”: 0.9081365
},
{
“entity”: “識別”,
“type”: “Keyword”,
“startIndex”: 16,
“endIndex”: 17,
“score”: 0.9102927
},
{
“entity”: “下載”,
“type”: “Action”,
“startIndex”: 0,
“endIndex”: 1,
“score”: 0.9999258
}
]
}

測試 api 的結果如下( entityPredictions 中的 phrase 字之間會有空白哦!!! ),

{
    "id": null,
    "text": "下載下來可以用了,可是什麼是網址識別",
    "alteredText": "下載下來可以用了,可是什麼是網址識別",
    "tokenizedText": [
        "下"
    ],
    "intentPredictions": [
        {
            "id": "9e70ed0a-561c-45f4-b8a4-2439de42acb1",
            "name": "FAQ",
            "score": 1.0
        },
        {
            "id": "8d898fa2-667f-471d-b87f-309011627b5d",
            "name": "None",
            "score": 0.05
        }
    ],
    "entityPredictions": [
        {
            "id": "cb403a63-347c-448c-9b08-3e48d6ac0c61",
            "entityName": "Keyword",
            "startTokenIndex": 14,
            "endTokenIndex": 15,
            "phrase": "網 址",
            "entityType": 1
        },
        {
            "id": "cb403a63-347c-448c-9b08-3e48d6ac0c61",
            "entityName": "Keyword",
            "startTokenIndex": 16,
            "endTokenIndex": 17,
            "phrase": "識 別",
            "entityType": 1
        },
        {
            "id": "1771cbab-fb7b-4a83-b46d-4c085ba88341",
            "entityName": "Action",
            "startTokenIndex": 0,
            "endTokenIndex": 1,
            "phrase": "下 載",
            "entityType": 1
        }
    ],
    "tokenMetadata": null
}

預設 call 正式 api 時,會 log 查詢的資料,最近幾筆資料可以從 「 Review endpoint utterances 」 看到,如下
Review endpoint utterances

那如果我需要的是全部的 Log 呢? 可以到「 My apps 」,在那個 app 右邊的 … ,選取「 Export Endpoint Log 」,就可以了哦,如下,
Export Endpoint Logs

log4net 檔名使用「大寫字母」造成檔案被「覆寫」問題

事情是這樣子的…我們有個專案裡面有使用 log4net v 1.2.11.0 ,在案子裡面是使用 RollingFileAppender , rollingStyle 是 Composite ,依日期時間及檔案大小來切檔,設定檔如下,

繼續閱讀 “log4net 檔名使用「大寫字母」造成檔案被「覆寫」問題”