/* ----------------------------------------------------------------------------- This source file is part of OpenSpace3D For the latest info, see http://www.openspace3d.com Copyright (c) 2024 I-maginer This program is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA, or go to http://www.gnu.org/copyleft/lesser.txt ----------------------------------------------------------------------------- */ struct PChatGPT = [ OBGPT_inst : PInstance, OBGPT_sUrl : S, OBGPT_sApiKey : S, OBGPT_sModel : S, OBGPT_sRole : S, OBGPT_fTemp : F, OBGPT_iMaxTokens : I, OBGPT_lHistory : [JsonValue r1], OBGPT_lInitialContent : [JsonValue r1] ] mkPChatGPT;; fun deleteOb(inst)= clearHttpRequest; clearHttpCookies; 0;; fun addHistoryEntry(obstr, jval)= set obstr.OBGPT_lHistory = lcat obstr.OBGPT_lHistory jval::nil; 0;; fun addInitialHistory(obstr, s, type)= if ((type == 0) || (s == nil)) then nil else ( let if (s == nil) then "" else s -> s in let makeJsonObject ["role" makeJsonString if (type == 1) then "user" else "assistant"]::["content" makeJsonString _DMSapplyByGlobalVar s]::nil -> jscont in set obstr.OBGPT_lInitialContent = lcat obstr.OBGPT_lInitialContent jscont::nil; ); 0;; fun getQAFromContent(s, pos)= let strfind "[Q]" s pos -> qpos in let strfind "[A]" s pos -> apos in if ((qpos == nil) && (apos == nil)) then nil else if (((qpos == nil) && (apos != nil)) || ((qpos != nil) && (apos != nil) && (qpos > apos))) then let strfind "[Q]" s (apos + 1) -> qnpos in let strfind "[A]" s (apos + 1) -> anpos in let if (qnpos != nil) && (qnpos < anpos) then qnpos else if (anpos == nil) then (strlen s) else anpos -> npos in let substr s (apos + 3) (npos - (apos + 3)) -> acont in [2 npos acont] else let strfind "[Q]" s (qpos + 1) -> qnpos in let strfind "[A]" s (qpos + 1) -> anpos in let if (qnpos != nil) && (qnpos < anpos) then qnpos else if (anpos == nil) then (strlen s) else anpos -> npos in let substr s (qpos + 3) (npos - (qpos + 3)) -> qcont in [1 npos qcont] ;; fun applyRoleContent(obstr, role)= let strfind "[Q]" role 0 -> qpos in if (qpos == nil) then ( set obstr.OBGPT_sRole = role; set obstr.OBGPT_lInitialContent = nil; set obstr.OBGPT_lHistory = nil; ) else ( set obstr.OBGPT_sRole = substr role 0 qpos; //addLogMessage strcat "AI : role > " obstr.OBGPT_sRole; let getQAFromContent role 0 -> ret in while (ret != nil) do ( let ret -> [type npos cont] in ( //addLogMessage strcatn "AI : "::(if (type == 1) then "[Q] > " else "[A] > ")::cont::nil; addInitialHistory obstr cont type; set ret = getQAFromContent role npos; ); ); set obstr.OBGPT_lHistory = obstr.OBGPT_lInitialContent; ); 0;; fun readResponse(obstr, url, data)= if ((!strcmp data "") || (data == nil)) then "" else ( let parseJson data -> jsonvalue in let getJsonObjectValue jsonvalue "choices" -> jschoices in let hd (getJsonArray jschoices) -> jschoice in let getJsonObjectValue jschoice "message" -> jsmessage in let getJsonObjectValue jschoice "delta" -> jsdelta in let if (jsmessage == nil) then getJsonObjectValue jsdelta "content" else getJsonObjectValue jsmessage "content" -> jscontent in let getJsonString jscontent -> content in ( if (content != nil) then ( //remove at the end of stream if ((strfind "<|eot_id|>" content (strlen content) - 10) != nil) then set content = strcat (substr content 0 (strlen content) - 10) "\n" else if (((strfind "<|end_of_text|>" content (strlen content) - 15) != nil) || ((strfind "<|end_of_turn|>" content (strlen content) - 15) != nil)) then set content = strcat (substr content 0 (strlen content) - 15) "\n" else if ((strfind "" content (strlen content) - 4) != nil) then set content = strcat (substr content 0 (strlen content) - 4) "\n" else nil; content; ) else ( let getJsonObjectValue jsonvalue "error" -> jserror in let getJsonObjectValue jserror "message" -> jsmessage in let getJsonString jsmessage -> errormesg in if (errormesg != nil) then ( errormesg; ) else if (jsdelta == nil) then ( addLogMessage strcatn "ChatGPT PlugIT : "::url::" failed! > "::data::nil; nil; ) else ""; ); ); );; fun cbGetContent(url, data, p) = let p -> [obstr prompt] in let "" -> rep in if (data != nil) then ( if (!strcmpi "data:" substr strtrim data 0 5) then //chunks stream ( let strToListSep data "data: " -> l in while (l != nil) do ( let strtrim hd l -> nd in if (!strcmp (substr nd 0 6) "[DONE]") then set rep = strcat rep "\n" else // end of stream set rep = strcat rep readResponse obstr url nd; set l = tl l; ); 0; ) else ( set rep = readResponse obstr url data; 0; ); if (!strcmp rep "") then nil else ( if (prompt) then nil else let makeJsonObject ["role" makeJsonString "assistant"]::["content" makeJsonString rep]::nil -> jsresponse in addHistoryEntry obstr jsresponse; SendPluginEvent obstr.OBGPT_inst "Response" rep nil; ); 0; ) else ( addLogMessage strcatn "ChatGPT PlugIT : "::url::" failed or empty response!"::nil; SendPluginEvent obstr.OBGPT_inst "Response error" nil nil; 0; ); 0;; fun cbGetProgress(url, data, p) = let p -> [obstr prompt pdata] in let "" -> rep in let strcat pdata data -> data in let 0 -> endstream in if (data == nil) then nil else ( if (!strcmpi "data:" substr strtrim data 0 5) then //chunks stream ( let strToListSep data "data: " -> l in while (l != nil) do ( let hd l -> nd in let strtrim nd -> snd in if (!strcmp (substr snd 0 6) "[DONE]") then // end of stream ( set rep = strcat rep "\n"; set endstream = 1; 0; ) else if (!strcmp (substr snd ((strlen snd) - 1) 1) "}") then ( set rep = strcat rep (readResponse obstr url snd); 0; ) else ( mutate p <- [_ _ strcatn pdata::"data: "::nd::nil]; 0; ); set l = tl l; ); 0; ) else ( set rep = readResponse obstr url data; 0; ); if (!strcmp rep "") then nil else SendPluginEvent obstr.OBGPT_inst "Response stream" rep nil; if (!endstream) then nil else SendPluginEvent obstr.OBGPT_inst "End stream" rep nil; 0; ); 0;; fun makeChatGptRequest(obstr, message)= let "Content-Type: application/json"::(strcat "Authorization: Bearer " obstr.OBGPT_sApiKey)::nil -> header in let strcat obstr.OBGPT_sUrl "/v1/chat/completions" -> url in let makeJsonObject ["role" makeJsonString "system"]::["content" makeJsonString _DMSapplyByGlobalVar obstr.OBGPT_sRole]::nil -> jssystem in let makeJsonObject ["role" makeJsonString "user"]::["content" makeJsonString _DMSapplyByGlobalVar message]::nil -> jsuser in let makeJsonObject ["type" makeJsonString "text"]::nil -> jsformat in let makeJsonObject ["model" makeJsonString obstr.OBGPT_sModel]:: ["messages" makeJsonArray (lcat jssystem::obstr.OBGPT_lHistory jsuser::nil)]:: ["max_tokens" makeJsonNum obstr.OBGPT_iMaxTokens]:: ["stream" makeJsonBool 1]:: ["response_format" jsformat]:: ["temperature" makeJsonNumF obstr.OBGPT_fTemp]::nil -> cmdjson in ( SendPluginEvent obstr.OBGPT_inst "Requested input" _DMSapplyByGlobalVar message nil; addHistoryEntry obstr jsuser; let exportJson cmdjson -> jsonparam in //let addLogMessage jsonparam -> _ in downloadFilePostExt url jsonparam header mkfun3 @cbGetProgress [obstr 0 ""] mkfun3 @cbGetContent [obstr 0]; ); 0;; //Skip history fun makeChatGptPrompt(obstr, message)= let "Content-Type: application/json"::(strcat "Authorization: Bearer " obstr.OBGPT_sApiKey)::nil -> header in let strcat obstr.OBGPT_sUrl "/v1/chat/completions" -> url in let makeJsonObject ["role" makeJsonString "system"]::["content" makeJsonString _DMSapplyByGlobalVar obstr.OBGPT_sRole]::nil -> jssystem in let makeJsonObject ["role" makeJsonString "user"]::["content" makeJsonString _DMSapplyByGlobalVar message]::nil -> jsuser in let makeJsonObject ["type" makeJsonString "text"]::nil -> jsformat in let makeJsonObject ["model" makeJsonString obstr.OBGPT_sModel]:: ["messages" makeJsonArray jssystem::jsuser::nil]:: ["max_tokens" makeJsonNum obstr.OBGPT_iMaxTokens]:: ["stream" makeJsonBool 0]:: ["response_format" jsformat]:: ["temperature" makeJsonNumF obstr.OBGPT_fTemp]::nil -> cmdjson in ( let exportJson cmdjson -> jsonparam in downloadFilePost url jsonparam header mkfun3 @cbGetContent [obstr 1]; ); 0;; fun makeChatGptPicture(obstr, message)= let "Content-Type: application/json"::(strcat "Authorization: Bearer " obstr.OBGPT_sApiKey)::nil -> header in let strcat obstr.OBGPT_sUrl "/v1/chat/completions" -> url in if (message == nil) then nil else let strfind "\n" message 0 -> stpos in let substr message 0 stpos -> request in let substr message stpos+1 strlen message -> param in let if (!strcmp "http" (substr param 0 4)) then makeJsonString param else if ((_checkpack param) != nil) then let G2DloadBmp _channel param -> lbmp in let _GETbitmapSize lbmp -> [bw bh] in let [320 ftoi (((itof bw) /. (itof bh)) *. 320.0)] -> [bw bh] in let G2DstrechBitmap _channel lbmp bw bh 0 -> bmp in let _BTCompBitmap bmp 60 -> sbmp in let base64_encode sbmp -> sb64img in ( _DSbitmap bmp; _DSbitmap lbmp; makeJsonString strcat "data:image/jpeg;base64," sb64img; ) else //data from capture plugIT let !strcmpi (substr param 0 1) "Z" -> iszip in let (strfindi "_" param 2) -> fp in let atoi (substr param 2 fp - 2) -> width in let (strfind "_" param fp + 1) -> fp2 in let atoi (substr param (fp + 1) (fp2 - fp - 1)) -> height in let if !iszip then substr param (fp2 + 1) (strlen param) else unzip substr param (fp2 + 1) (strlen param) -> data in let base64_encode data -> sb64img in makeJsonString strcat "data:image/jpeg;base64," sb64img -> picturedata in let makeJsonObject ["role" makeJsonString "system"]::["content" makeJsonString _DMSapplyByGlobalVar obstr.OBGPT_sRole]::nil -> jssystem in let makeJsonObject ["type" makeJsonString "text"]::["text" makeJsonString _DMSapplyByGlobalVar request]::nil -> jmessage in let makeJsonObject ["type" makeJsonString "image_url"]::["image_url" makeJsonObject ["url" picturedata]::nil]::nil -> jpicture in let makeJsonObject ["role" makeJsonString "user"]::["content" makeJsonArray jmessage::jpicture::nil]::nil -> jsuser in let makeJsonObject ["model" makeJsonString obstr.OBGPT_sModel]:: ["messages" makeJsonArray jssystem::jsuser::nil]:: ["max_tokens" makeJsonNum obstr.OBGPT_iMaxTokens]:: ["stream" makeJsonBool 0]:: ["temperature" makeJsonNumF obstr.OBGPT_fTemp]::nil -> cmdjson in ( let exportJson cmdjson -> jsonparam in downloadFilePost url jsonparam header mkfun3 @cbGetContent [obstr 1]; ); 0;; fun cbIsConnected(testurl, p, rc) = let p -> [obstr message prompt] in if (rc == 1) then // Active Internet Connection detected ( if (!prompt) then makeChatGptRequest obstr message else if (prompt == 1) then makeChatGptPrompt obstr message else if (prompt == 2) then makeChatGptPicture obstr message else nil; 0; ) // No Active Internet Connection detected else ( addLogMessage "ChatGPT PlugIT : no internet connection found!"; 0; );; fun cbRequest(inst, from, action, param, reply, obstr)= if (param == nil) then nil else checkInternetConnection nil @cbIsConnected [obstr param 0]; 0;; fun cbSingleRequest(inst, from, action, param, reply, obstr)= if (param == nil) then nil else checkInternetConnection nil @cbIsConnected [obstr param 1]; 0;; fun cbDescribPictureRequest(inst, from, action, param, reply, obstr)= if (param == nil) then nil else checkInternetConnection nil @cbIsConnected [obstr param 2]; 0;; fun cbResetHistory(inst, from, action, param, reply, obstr)= set obstr.OBGPT_lHistory = obstr.OBGPT_lInitialContent; //stop current requests; clearHttpRequest; clearHttpCookies; 0;; fun cbSetRole(inst, from, action, param, reply, obstr)= applyRoleContent obstr param; 0;; fun cbSetCreativity(inst, from, action, param, reply, obstr)= if ((atof param) == nil) then nil else set obstr.OBGPT_fTemp = atof param; if (obstr.OBGPT_fTemp <. 0.1) then set obstr.OBGPT_fTemp = 0.1 else if (obstr.OBGPT_fTemp >. 2.0) then set obstr.OBGPT_fTemp = 2.0 else nil; 0;; fun cbSetMaxTokens(inst, from, action, param, reply, obstr)= if ((atoi param) == nil) then nil else set obstr.OBGPT_iMaxTokens = atoi param; 0;; fun cbSetApiUrl(inst, from, action, param, reply, obstr)= if ((param == nil) || (!strcmp obstr.OBGPT_sApiKey param)) then nil else ( set obstr.OBGPT_sUrl = param; SendPluginEvent obstr.OBGPT_inst "Api Url changed" obstr.OBGPT_sUrl nil; ); 0;; fun cbSetApiKey(inst, from, action, param, reply, obstr)= if (!strcmp obstr.OBGPT_sApiKey param) then nil else ( set obstr.OBGPT_sApiKey = param; SendPluginEvent obstr.OBGPT_inst "Api Key changed" nil nil; ); 0;; fun cbSetModel(inst, from, action, param, reply, obstr)= if (!strcmp obstr.OBGPT_sModel param) then nil else ( set obstr.OBGPT_sModel = param; SendPluginEvent obstr.OBGPT_inst "AI model changed" obstr.OBGPT_sModel nil; ); 0;; fun newOb(inst)= let getPluginInstanceParam inst "url" -> url in let if (url == nil) then "https://api.openai.com" else url -> url in let getPluginInstanceParam inst "key" -> key in let getPluginInstanceParam inst "model" -> model in let getPluginInstanceParam inst "role" -> role in let atof (getPluginInstanceParam inst "temp") -> temp in let if temp == nil then 0.7 else temp -> temp in let atoi (getPluginInstanceParam inst "maxtoken") -> maxtoken in let if maxtoken == nil then 500 else maxtoken -> maxtoken in let mkPChatGPT [inst url key model nil temp maxtoken nil nil] -> obstr in ( applyRoleContent obstr role; PluginRegisterAction inst "Request" mkfun6 @cbRequest obstr; PluginRegisterAction inst "Reset history" mkfun6 @cbResetHistory obstr; PluginRegisterAction inst "Single request" mkfun6 @cbSingleRequest obstr; PluginRegisterAction inst "Describ picture" mkfun6 @cbDescribPictureRequest obstr; PluginRegisterAction inst "Set role" mkfun6 @cbSetRole obstr; PluginRegisterAction inst "Set creativity" mkfun6 @cbSetCreativity obstr; PluginRegisterAction inst "Set max tokens" mkfun6 @cbSetMaxTokens obstr; PluginRegisterAction inst "Set api url" mkfun6 @cbSetApiUrl obstr; PluginRegisterAction inst "Set api key" mkfun6 @cbSetApiKey obstr; PluginRegisterAction inst "Set model" mkfun6 @cbSetModel obstr; setPluginInstanceCbDel inst @deleteOb; ); 0;; fun IniPlug(file)= PlugRegister @newOb nil; setPluginEditor @dynamicedit; 0;;