diff --git a/README.md b/README.md index 5664274..b7d99ce 100644 --- a/README.md +++ b/README.md @@ -10,3 +10,12 @@ This part of the knowledge bank is a Proxy server to analyze the traffic that go ## Docker setup You can find a simplified setup using Docker Compose by SebbeJohansson here: https://github.com/SebbeJohansson/emo-proxy-docker It uses nginx, dnsmasq, and mitmproxy to be able to pass through the api requests to the EMO Proxy. + +## Experimental +### ChatGPT Speak server +One of the most common responses from EMO is to respond using ChatGPT. +An experimental feature is to use a local Speak server to generate EMO's chatgpt speak responses instead of using the living.ai servers. +You can find a proof of concept server here: https://github.com/SebbeJohansson/EmoChatGptSpeakPOC + +Connect the server by adding the following line to your emoProxy.conf file: +`"chatGptSpeakServer": "http://localhost:8085"` \ No newline at end of file diff --git a/emoProxy.conf.example b/emoProxy.conf.example index 031636d..91de709 100644 --- a/emoProxy.conf.example +++ b/emoProxy.conf.example @@ -7,5 +7,8 @@ "postFS": "/tmp/", "logFileName": "/var/log/emoProxyss.log", "enableDatabaseAndAPI": false, # For now, default behavior is still without db and api. - "sqliteLocation": "/var/data/emo_logs.db" + "enableReplacements": false, # For now, default behavior is still without replacements. + + "sqliteLocation": "/var/data/emo_logs.db", + "chatGptSpeakServer": "" } \ No newline at end of file diff --git a/emoProxy.go b/emoProxy.go index 633627b..6392d09 100644 --- a/emoProxy.go +++ b/emoProxy.go @@ -33,7 +33,9 @@ type Configuration struct { PostFS string `json:"postFS"` LogFileName string `json:"logFileName"` EnableDatabaseAndAPI bool `json:"enableDatabaseAndAPI"` + EnableReplacements bool `json:"enableReplacements"` SqliteLocation string `json:"sqliteLocation"` + ChatGptSpeakServer string `json:"chatGptSpeakServer"` } var ( @@ -104,7 +106,9 @@ func loadConfig(filename string) error { PostFS: "/tmp/", LogFileName: "/var/log/emoProxy.log", EnableDatabaseAndAPI: false, + EnableReplacements: false, SqliteLocation: "/var/data/emo_logs.db", + ChatGptSpeakServer: "", } bytes, err := os.ReadFile(filename) @@ -317,6 +321,11 @@ func makeApiRequest(r *http.Request) string { logResponse(response) + if conf.EnableReplacements { + log.Println("Replacements enabled, checking for replacements...") + body = runReplacementsAndReturnModifiedBody(body, r) + } + if useDatabaseAndAPI { saveRequest(r.URL.RequestURI(), string(requestBody), string(body)) } @@ -346,10 +355,8 @@ func makeTtsRequest(r *http.Request) string { } defer response.Body.Close() - // read response body, _ := io.ReadAll(response.Body) - // write post request body to fs logBody(response.Header.Get("Content-Type"), body, "tts_") logResponse(response) @@ -382,10 +389,8 @@ func makeApiTtsRequest(r *http.Request) string { } defer response.Body.Close() - // read response body, _ := io.ReadAll(response.Body) - // write post request body to fs logBody(response.Header.Get("Content-Type"), body, "apitts_") logResponse(response) @@ -418,7 +423,6 @@ func makeResRequest(r *http.Request, w http.ResponseWriter) string { } defer response.Body.Close() - // read response body, _ := io.ReadAll(response.Body) logBody(response.Header.Get("Content-Type"), body, "res_") diff --git a/replacements.go b/replacements.go new file mode 100644 index 0000000..a88dab3 --- /dev/null +++ b/replacements.go @@ -0,0 +1,130 @@ +package main + +import ( + "bytes" + "encoding/json" + "io" + "log" + "net/http" + "net/url" +) + +func runReplacementsAndReturnModifiedBody(body []byte, r *http.Request) []byte { + typedBody := QueryResponse{} + decoder := json.NewDecoder(bytes.NewReader(body)) + decoder.DisallowUnknownFields() + + err := decoder.Decode(&typedBody) + if err != nil { + log.Println("Error when decoding JSON (", string(body), ") will return unhandled:", err) + return body + } else { + if typedBody.QueryResult == nil || typedBody.QueryId == "" { + log.Println("Unexpected query response, will return unhandled.") + return body + } + + if typedBody.QueryResult.Intent.Name == "chatgpt_speak" && conf.ChatGptSpeakServer != "" { + speakResponse := makeChatGptSpeakRequest(typedBody.QueryResult.QueryText, typedBody.LanguageCode, typedBody.QueryResult.BehaviorParas.Txt, r) + if speakResponse.Url != "" && speakResponse.Txt != "" { + log.Println("Successfully replaced chatgpt_speak response for request.") + typedBody.QueryResult.BehaviorParas.Url = speakResponse.Url + typedBody.QueryResult.BehaviorParas.Txt = speakResponse.Txt + } else { + log.Println("Failed to get valid response from ChatGptSpeakServer, keeping original response.") + } + } + modifiedBody, err := json.Marshal(typedBody) + if err != nil { + log.Println("Error when marshaling modified JSON, will return unhandled:", err) + return body + } + return modifiedBody + } +} + +func makeEmoSpeechRequest(text string, languageCode string, r *http.Request) EmoSpeechResponse { + request, _ := http.NewRequest("GET", "https://"+conf.Livingio_API_Server+"/emo/speech/tts?q="+url.QueryEscape(text)+"&l="+url.QueryEscape(languageCode), nil) + + val, exists := r.Header["Authorization"] + if exists { + request.Header.Add("Authorization", val[0]) + } + + val, exists = r.Header["Secret"] + if exists { + request.Header.Add("Secret", val[0]) + } + + request.Header.Del("User-Agent") + + httpclient := &http.Client{} + response, err := httpclient.Do(request) + + if err != nil { + log.Fatalf("An Error Occured %v", err) + } + defer response.Body.Close() + + body, _ := io.ReadAll(response.Body) + + var emoSpeechResponse EmoSpeechResponse + if err := json.Unmarshal([]byte(body), &emoSpeechResponse); err != nil { + log.Printf("Error unmarshaling ChatGptSpeakServer response: %v\n", err) + return EmoSpeechResponse{} + } + + return emoSpeechResponse +} + +func makeChatGptSpeakRequest(queryText string, languageCode string, fallbackResponse string, r *http.Request) BehaviorParas { + type ChatGptSpeakRequest struct { + QueryText string `json:"queryText"` + LanguageCode string `json:"languageCode"` + FallbackResponse string `json:"fallbackResponse,omitempty"` + } + type ChatGptSpeakResponse struct { + ResponseText string `json:"responseText"` + } + + chatGptRequestData := ChatGptSpeakRequest{ + QueryText: queryText, + LanguageCode: languageCode, + FallbackResponse: fallbackResponse, + } + chatGptRequestBody, _ := json.Marshal(chatGptRequestData) + chatGptRequest, _ := http.NewRequest("POST", conf.ChatGptSpeakServer+"/speak", bytes.NewBuffer(chatGptRequestBody)) + chatGptRequest.Header.Add("Content-Type", "application/json") + + chatGptClient := &http.Client{} + chatGptResponse, err := chatGptClient.Do(chatGptRequest) + if err != nil { + log.Fatalf("An Error Occured while calling ChatGptSpeakServer %v", err) + } + defer chatGptResponse.Body.Close() + + chatGptResponseBody, _ := io.ReadAll(chatGptResponse.Body) + + var chatGptTypedResponse ChatGptSpeakResponse + if err := json.Unmarshal([]byte(chatGptResponseBody), &chatGptTypedResponse); err != nil { + log.Printf("Error unmarshaling ChatGptSpeakServer response: %v\n", err) + return BehaviorParas{} + } + + if chatGptTypedResponse.ResponseText == "" { + log.Println("ChatGptSpeakServer returned empty response text") + return BehaviorParas{} + } + + emoSpeechResponse := makeEmoSpeechRequest(chatGptTypedResponse.ResponseText, languageCode, r) + if emoSpeechResponse.Code != 200 || emoSpeechResponse.Url == "" { + log.Printf("Error in EmoSpeechResponse: Code %d, Errmessage: %s\n", emoSpeechResponse.Code, emoSpeechResponse.Errmessage) + return BehaviorParas{} + } + behaviorParasResponse := BehaviorParas{ + Txt: chatGptTypedResponse.ResponseText, + Url: emoSpeechResponse.Url, + } + + return behaviorParasResponse +} diff --git a/types.go b/types.go new file mode 100644 index 0000000..8e20399 --- /dev/null +++ b/types.go @@ -0,0 +1,48 @@ +package main + +type Intent struct { + Name string `json:"name,omitempty"` + Confidence float64 `json:"confidence,omitempty"` +} + +type BehaviorParas struct { + UtilityType string `json:"utility_type,omitempty"` + Time []string `json:"time,omitempty"` + Txt string `json:"txt,omitempty"` + Url string `json:"url,omitempty"` + PreAnimation string `json:"pre_animation,omitempty"` + PostAnimation string `json:"post_animation,omitempty"` + PostBehavior string `json:"post_behavior,omitempty"` + RecBehavior string `json:"rec_behavior,omitempty"` + BehaviorParas *BehaviorParas `json:"behavior_paras,omitempty"` + Sentiment string `json:"sentiment,omitempty"` + Listen int `json:"listen,omitempty"` + AnimationName string `json:"animation_name,omitempty"` +} + +type QueryResult struct { + ResultCode string `json:"resultCode,omitempty"` + QueryText string `json:"queryText,omitempty"` + Intent *Intent `json:"intent,omitempty"` + RecBehavior string `json:"rec_behavior,omitempty"` + BehaviorParas *BehaviorParas `json:"behavior_paras,omitempty"` +} + +type QueryResponse struct { + QueryId string `json:"queryId,omitempty"` + QueryResult *QueryResult `json:"queryResult,omitempty"` + LanguageCode string `json:"languageCode,omitempty"` + Index int `json:"index,omitempty"` +} + +type TokenResponse struct { + AccessToken string `json:"access_token,omitempty"` + ExpireIn int `json:"expire_in,omitempty"` + Type string `json:"type,omitempty"` +} + +type EmoSpeechResponse struct { + Code int64 `json:"code"` + Errmessage string `json:"errmessage"` + Url string `json:"url"` +}