model.php 117 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078307930803081308230833084308530863087308830893090309130923093309430953096309730983099310031013102310331043105310631073108310931103111311231133114311531163117311831193120312131223123312431253126312731283129313031313132313331343135313631373138313931403141314231433144314531463147314831493150315131523153315431553156315731583159316031613162316331643165316631673168316931703171317231733174317531763177317831793180318131823183318431853186
  1. <?php
  2. /**
  3. * The model file of ai module of ZenTaoPMS.
  4. *
  5. * @copyright Copyright 2009-2023 禅道软件(青岛)有限公司(ZenTao Software (Qingdao) Co., Ltd. www.zentao.net)
  6. * @license ZPL(https://zpl.pub/page/zplv12.html) or AGPL(https://www.gnu.org/licenses/agpl-3.0.en.html)
  7. * @author Wenrui LI <liwenrui@easycorp.ltd>
  8. * @package ai
  9. * @link https://www.zentao.net
  10. */
  11. class aiModel extends model
  12. {
  13. /**
  14. * Action model.
  15. *
  16. * @var actionModel
  17. * @access public
  18. */
  19. public $action;
  20. /**
  21. * Model config.
  22. *
  23. * @var object
  24. * @access public
  25. */
  26. public $modelConfig;
  27. /**
  28. * Errors from last request.
  29. *
  30. * @var array
  31. * @access public
  32. */
  33. public $errors = array();
  34. /**
  35. * Check if object action is clickable, used in datatables.
  36. *
  37. * @param object $object object to check, model objects are supported, add support for more if needed.
  38. * @param string $method method on object
  39. * @access public
  40. * @static
  41. * @return boolean
  42. */
  43. public function isClickable($object, $action)
  44. {
  45. $action = strtolower($action);
  46. if(empty($object) || empty($action)) return false;
  47. /* Assumes object is a language model record. */
  48. if(in_array($this->app->rawMethod, array('models', 'modelview')))
  49. {
  50. if($action === 'modelenable') return $object->enabled != '1';
  51. if($action === 'modeldisable') return $object->enabled != '0';
  52. if($action === 'assistantpublish') return $object->enabled != '1';
  53. if($action === 'assistantwithdraw') return $object->enabled != '0';
  54. if($action === 'assistantedit') return $object->enabled != '1';
  55. }
  56. if(in_array($this->app->rawMethod, array('prompts', 'promptview')))
  57. {
  58. $executable = $this->isExecutable($object);
  59. $published = $object->status == 'active';
  60. if($action == 'promptassignrole') return common::hasPriv('ai', 'designPrompt') && !$published;
  61. if($action == 'promptaudit') return common::hasPriv('ai', 'designPrompt') && $executable && !$published;
  62. if($action == 'promptedit') return common::hasPriv('ai', 'promptedit');
  63. if($action == 'promptpublish') return common::hasPriv('ai', 'promptpublish') && !$published && $executable;
  64. if($action == 'promptunpublish') return common::hasPriv('ai', 'promptunpublish') && $published && $executable;
  65. }
  66. if(in_array($this->app->rawMethod, array('miniprograms', 'miniprogramview')))
  67. {
  68. $isPublished = $object->published == '1';
  69. $isBuiltIn = $object->builtIn == '1';
  70. if($action == 'editminiprogram') return common::hasPriv('ai', 'editMiniProgram') && !$isPublished && !$isBuiltIn;
  71. if($action == 'testminiprogram') return common::hasPriv('ai', 'testMiniProgram') && !$isPublished && !$isBuiltIn;
  72. if($action == 'publishminiprogram') return common::hasPriv('ai', 'publishMiniProgram') && !$isPublished;
  73. if($action == 'unpublishminiprogram') return common::hasPriv('ai', 'unpublishMiniProgram') && $isPublished;
  74. if($action == 'exportminiprogram') return common::hasPriv('ai', 'exportMiniProgram') && $isPublished && !$isBuiltIn;
  75. }
  76. return true;
  77. }
  78. /**
  79. * Set model config with model stored in database.
  80. *
  81. * @param object $config
  82. * @access public
  83. * @return bool
  84. */
  85. public function setModelConfig($config)
  86. {
  87. /* Extract model options if model is from DB. */
  88. if(isset($config->credentials)) $config = $this->unserializeModel($config);
  89. /* Abort if config is falsy. */
  90. if(empty($config)) return false;
  91. $this->modelConfig = $config;
  92. return true;
  93. }
  94. /**
  95. * Setup $modelConfig with model stored in database, or default model if not specified.
  96. *
  97. * @param int|mixed $modelID or falsy values, if falsy, will use default model.
  98. * @access private
  99. * @return bool
  100. */
  101. private function useLanguageModel($modelID)
  102. {
  103. if(!empty($modelID)) $model = $this->getLanguageModel($modelID, true);
  104. if(empty($model)) $model = $this->getDefaultLanguageModel();
  105. if(empty($model)) return false;
  106. return $this->setModelConfig($model);
  107. }
  108. /**
  109. * Whether any LLM is configured and enabled.
  110. *
  111. * @access public
  112. * @return boolean
  113. */
  114. public function hasModelsAvailable()
  115. {
  116. $models = $this->getLanguageModels('', true);
  117. return !empty($models);
  118. }
  119. /**
  120. * Get list of configured LLMs.
  121. *
  122. * @param string $type type of LLM, empty for all, check keys of `$config->ai->models` for available types.
  123. * @param bool $enabledOnly whether to only return enabled models
  124. * @param pager $pager
  125. * @param string $orderBy
  126. * @access public
  127. * @return array
  128. */
  129. public function getLanguageModels($type = '', $enabledOnly = false, $pager = null, $orderBy = 'id_desc')
  130. {
  131. return $this->dao->select('*')->from(TABLE_AI_MODEL)
  132. ->where('deleted')->eq('0')
  133. ->beginIF(!empty($type))->andWhere('`type`')->eq($type)->fi()
  134. ->beginIF($enabledOnly)->andWhere('`enabled`')->eq('1')->fi()
  135. ->orderBy($orderBy)
  136. ->page($pager)
  137. ->fetchAll('id', false);
  138. }
  139. /**
  140. * Get key-value of all LLMs, includes default.
  141. *
  142. * @return array
  143. */
  144. public function getLanguageModelNamesWithDefault()
  145. {
  146. $models = $this->getLanguageModels();
  147. $models = array_reduce($models, function ($carry, $model)
  148. {
  149. $carry[$model->id] = $model->name;
  150. return $carry;
  151. }, array('default' => $this->lang->ai->models->default));
  152. return $models;
  153. }
  154. /**
  155. * Get LLM config.
  156. *
  157. * @param int $modelID
  158. * @param bool $enabledOnly
  159. * @access public
  160. * @return object|false LLM config, false if not found.
  161. */
  162. public function getLanguageModel($modelID, $enabledOnly = false)
  163. {
  164. return $this->dao->select('*')->from(TABLE_AI_MODEL)
  165. ->where('id')->eq($modelID)
  166. ->beginIF($enabledOnly)->andWhere('`enabled`')->eq('1')->fi()
  167. ->andWhere('deleted')->eq('0')
  168. ->fetch();
  169. }
  170. /**
  171. * Get default (first enabled available) LLM config.
  172. *
  173. * @access private
  174. * @return object|false default LLM config, false if cannot find any.
  175. */
  176. private function getDefaultLanguageModel()
  177. {
  178. $models = $this->getLanguageModels('', true, null, 'id_asc');
  179. if(empty($models)) return false;
  180. return current($models);
  181. }
  182. /**
  183. * Format model config submitted from form for use with `createModel()` and `updateModel()`.
  184. *
  185. * @param object $model
  186. * @access private
  187. * @return object|false
  188. */
  189. private function serializeModel($model)
  190. {
  191. /* Check if all credentials fields are present. */
  192. if(empty($this->config->ai->vendorList[$model->vendor])) return false;
  193. $credentials = new stdclass();
  194. foreach($this->config->ai->vendorList[$model->vendor]['credentials'] as $credKey)
  195. {
  196. if(empty($model->$credKey)) return false;
  197. $credentials->$credKey = $model->$credKey;
  198. }
  199. $modelConfig = new stdclass();
  200. $modelConfig->name = $model->name;
  201. $modelConfig->desc = $model->description;
  202. $modelConfig->type = $model->type;
  203. $modelConfig->vendor = $model->vendor;
  204. $modelConfig->credentials = json_encode($credentials);
  205. $modelConfig->enabled = $model->enabled;
  206. if(!empty($model->proxyType) && !empty($model->proxyAddr))
  207. {
  208. $proxy = new stdclass();
  209. $proxy->type = $model->proxyType;
  210. $proxy->addr = $model->proxyAddr;
  211. $modelConfig->proxy = json_encode($proxy);
  212. }
  213. else
  214. {
  215. $modelConfig->proxy = '';
  216. }
  217. return $modelConfig;
  218. }
  219. /**
  220. * Extract model config for use with actual API calls or for viewing details.
  221. *
  222. * @param object $model
  223. * @access public
  224. * @return object|false
  225. */
  226. public function unserializeModel($model)
  227. {
  228. $modelConfig = new stdclass();
  229. $modelConfig->id = $model->id;
  230. $modelConfig->name = $model->name;
  231. $modelConfig->type = $model->type;
  232. $modelConfig->vendor = $model->vendor;
  233. $modelConfig->desc = $model->desc;
  234. $modelConfig->enabled = $model->enabled;
  235. $modelConfig->deleted = $model->deleted;
  236. /* Set default name. */
  237. if(empty($model->name)) $modelConfig->name = $this->lang->ai->models->typeList[$model->type];
  238. /* Extract credential props. */
  239. $credentials = json_decode($model->credentials);
  240. foreach($credentials as $credKey => $credValue) $modelConfig->$credKey = $credValue;
  241. /* Extract proxy options. */
  242. if(!empty($model->proxy))
  243. {
  244. $proxy = json_decode($model->proxy);
  245. foreach($proxy as $proxyKey => $proxyValue) $modelConfig->{'proxy' . ucfirst($proxyKey)} = $proxyValue;
  246. }
  247. return $modelConfig;
  248. }
  249. /**
  250. * Create LLM configuration.
  251. *
  252. * @param object $model model config details.
  253. * @access public
  254. * @return int|false id of created LLM if success, otherwise false.
  255. */
  256. public function createModel($model)
  257. {
  258. $model->enabled = '1';
  259. $modelConfig = $this->serializeModel($model);
  260. if(!$modelConfig) return false;
  261. $modelConfig->createdDate = helper::now();
  262. $modelConfig->createdBy = $this->app->user->account;
  263. $this->dao->insert(TABLE_AI_MODEL)
  264. ->data($modelConfig)
  265. ->exec();
  266. return dao::isError() ? false : $this->dao->lastInsertID();
  267. }
  268. /**
  269. * Update LLM configuration.
  270. *
  271. * @param int $modelID
  272. * @param object $model
  273. * @access public
  274. * @return bool
  275. */
  276. public function updateModel($modelID, $model)
  277. {
  278. $currentModel = $this->getLanguageModel($modelID);
  279. if(!$currentModel) return false;
  280. $model->enabled = $currentModel->enabled;
  281. $modelConfig = $this->serializeModel($model);
  282. if(!$modelConfig) return false;
  283. $modelConfig->editedDate = helper::now();
  284. $modelConfig->editedBy = $this->app->user->account;
  285. $this->dao->update(TABLE_AI_MODEL)
  286. ->data($modelConfig)
  287. ->where('id')->eq($modelID)
  288. ->exec();
  289. if(isset($model->name))
  290. {
  291. $this->dao->update(TABLE_IM_CHAT)
  292. ->set('name')->eq($model->name)
  293. ->where('gid')->like( "%&ai-$modelID")
  294. ->exec();
  295. }
  296. return !dao::isError();
  297. }
  298. /**
  299. * Toggle model enablement status.
  300. *
  301. * @param int $modelID
  302. * @param bool $enabled
  303. * @access public
  304. * @return bool
  305. */
  306. public function toggleModel($modelID, $enabled)
  307. {
  308. $this->dao->update(TABLE_AI_MODEL)
  309. ->set('enabled')->eq(empty($enabled) ? '0' : '1')
  310. ->set('editedDate')->eq(helper::now())
  311. ->set('editedBy')->eq($this->app->user->account)
  312. ->where('id')->eq($modelID)
  313. ->exec();
  314. $this->dao->update(TABLE_IM_CHAT)
  315. ->set('archiveDate')->eq(empty($enabled) ? helper::now() : null)
  316. ->where('gid')->like( "%&ai-$modelID")
  317. ->exec();
  318. return !dao::isError();
  319. }
  320. /**
  321. * Mark model as deleted.
  322. *
  323. * @param int $modelID
  324. * @access public
  325. * @return bool
  326. */
  327. public function deleteModel($modelID)
  328. {
  329. $this->dao->update(TABLE_AI_MODEL)
  330. ->set('deleted')->eq('1')
  331. ->set('editedDate')->eq(helper::now())
  332. ->set('editedBy')->eq($this->app->user->account)
  333. ->where('id')->eq($modelID)
  334. ->exec();
  335. $this->dao->update(TABLE_IM_CHAT)
  336. ->set('dismissDate')->eq(helper::now())
  337. ->where('gid')->like( "%&ai-$modelID")
  338. ->exec();
  339. $this->dao->update(TABLE_AI_ASSISTANT)
  340. ->set('deleted')->eq('1')
  341. ->where('modelId')->eq($modelID)
  342. ->exec();
  343. return !dao::isError();
  344. }
  345. /**
  346. * Make a test request to the model.
  347. *
  348. * @param int $modelID
  349. * @access public
  350. * @return bool
  351. */
  352. public function testModelConnection($modelID)
  353. {
  354. $this->useLanguageModel($modelID);
  355. if($this->config->ai->models[$this->modelConfig->type] == 'ernie' || $this->modelConfig->vendor == 'azure' || $this->modelConfig->type == 'openai-gpt4' || $this->modelConfig->vendor == 'openaiCompatible')
  356. {
  357. $messages = array((object)array('role' => 'user', 'content' => 'test'));
  358. $result = $this->converse($modelID, $messages, array('maxTokens' => 1));
  359. }
  360. else
  361. {
  362. $result = $this->complete($modelID, 'test', 1); // Test completing 'test' with length of 1.
  363. }
  364. return !empty($result);
  365. }
  366. /**
  367. * Make request to OpenAI API.
  368. *
  369. * @param string $type chat | completion | edit
  370. * @param mixed $data data to send
  371. * @param int $timeout request timeout in seconds
  372. * @access private
  373. * @return object response object, three properties: result, message (if fail), content (if success).
  374. */
  375. private function makeRequest($type, $data, $timeout = 10)
  376. {
  377. $modelType = $this->config->ai->models[$this->modelConfig->type];
  378. $modelVendor = $this->modelConfig->vendor;
  379. /* Try encoding data to json, handles both encoded json and raw data. */
  380. $postData = json_encode($data);
  381. if(json_last_error()) $postData = $data;
  382. /* Set auth and content-type headers. */
  383. $requestHeaders = array();
  384. if(isset($this->config->ai->$modelType->api->$modelVendor->authFormat)) $requestHeaders[] = sprintf($this->config->ai->$modelType->api->$modelVendor->authFormat, $this->modelConfig->key);
  385. $requestHeaders[] = isset($this->config->ai->$modelType->contentType[$type]) ? $this->config->ai->$modelType->contentType[$type] : $this->config->ai->$modelType->contentType[''];
  386. /* Assemble request url. */
  387. if($modelType == 'openai')
  388. {
  389. if($modelVendor == 'openaiCompatible')
  390. {
  391. $url = sprintf($this->config->ai->openai->api->openaiCompatible->format, rtrim($this->modelConfig->base, '/'), $this->config->ai->openai->api->methods[$type]);
  392. }
  393. elseif($modelVendor == 'azure')
  394. {
  395. $url = sprintf($this->config->ai->openai->api->azure->format, $this->modelConfig->resource, $this->modelConfig->deployment, $this->config->ai->openai->api->methods[$type], $this->config->ai->openai->api->azure->apiVersion);
  396. }
  397. else
  398. {
  399. $url = sprintf($this->config->ai->openai->api->openai->format, $this->config->ai->openai->api->openai->version, $this->config->ai->openai->api->methods[$type]);
  400. }
  401. }
  402. elseif($modelType == 'ernie')
  403. {
  404. $clientID = $this->modelConfig->key;
  405. $clientSecret = $this->modelConfig->secret;
  406. $authURL = sprintf($this->config->ai->ernie->api->$modelVendor->auth, $clientID, $clientSecret);
  407. $ch = curl_init();
  408. curl_setopt($ch, CURLOPT_URL, $authURL);
  409. curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
  410. curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5);
  411. $result = curl_exec($ch);
  412. if(!$result || curl_errno($ch))
  413. {
  414. $response = new stdclass();
  415. $response->result = 'fail';
  416. $response->message = curl_error($ch);
  417. curl_close($ch);
  418. return $response;
  419. }
  420. $result = json_decode($result);
  421. if(json_last_error())
  422. {
  423. $response = new stdclass();
  424. $response->result = 'fail';
  425. $response->message = 'JSON decode error: ' . json_last_error_msg();
  426. curl_close($ch);
  427. return $response;
  428. }
  429. if(empty($result->access_token)) return (object)array('result' => 'fail', 'message' => $this->lang->ai->models->authFailure);
  430. $accessToken = $result->access_token;
  431. $url = sprintf($this->config->ai->ernie->api->$modelVendor->format, $accessToken);
  432. }
  433. /* Set up requestor. */
  434. $ch = curl_init();
  435. curl_setopt($ch, CURLOPT_URL, $url);
  436. curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
  437. curl_setopt($ch, CURLOPT_POST, 1);
  438. curl_setopt($ch, CURLOPT_POSTFIELDS, $postData);
  439. curl_setopt($ch, CURLOPT_HTTPHEADER, $requestHeaders);
  440. curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
  441. curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $timeout);
  442. /* Use proxy if proxy is set. */
  443. if(!empty($this->modelConfig->proxyType) && !empty($this->modelConfig->proxyAddr))
  444. {
  445. curl_setopt($ch, CURLOPT_PROXY, $this->modelConfig->proxyAddr);
  446. curl_setopt($ch, CURLOPT_PROXYTYPE, static::getProxyType($this->modelConfig->proxyType));
  447. }
  448. $result = curl_exec($ch);
  449. if(isset($this->config->debug) && $this->config->debug >= 1)
  450. {
  451. global $app;
  452. $logFile = $app->getLogRoot() . 'saas.' . date('Ymd') . '.log.php';
  453. if(!file_exists($logFile)) file_put_contents($logFile, '<?php die(); ?' . '>');
  454. $fh = @fopen($logFile, 'a');
  455. if($fh)
  456. {
  457. fwrite($fh, date('Ymd H:i:s') . ": " . $app->getURI() . ' AI Request' . "\n");
  458. fwrite($fh, "postData: " . print_r($data, true) . "\n");
  459. fwrite($fh, "results:" . print_r($result, true) . "\n");
  460. fclose($fh);
  461. }
  462. }
  463. $response = new stdclass();
  464. if(!$result || curl_errno($ch))
  465. {
  466. $response->result = 'fail';
  467. $response->message = curl_error($ch);
  468. }
  469. else
  470. {
  471. $response->result = 'success';
  472. $response->content = $result;
  473. }
  474. curl_close($ch);
  475. return $response;
  476. }
  477. /**
  478. * Get proxy type.
  479. *
  480. * @param string $proxyType
  481. * @access private
  482. * @return int|false
  483. */
  484. private static function getProxyType($proxyType)
  485. {
  486. if(!in_array($proxyType, array('http', 'socks4', 'socks5'))) return false;
  487. return constant('CURLPROXY_' . strtoupper($proxyType));
  488. }
  489. /**
  490. * Convert camelCase to snake_case.
  491. *
  492. * @param string $str
  493. * @access private
  494. * @return string
  495. */
  496. private static function camelCaseToSnakeCase($str)
  497. {
  498. return strtolower(preg_replace('/(?<!^)[A-Z]/', '_$0', $str));
  499. }
  500. /**
  501. * Standardize params keys, convert camelCase to snake_case.
  502. *
  503. * @param object $data
  504. * @access private
  505. * @return object
  506. */
  507. private static function standardizeParams($data)
  508. {
  509. $standardizedData = new stdclass();
  510. foreach($data as $key => $value) $standardizedData->{static::camelCaseToSnakeCase($key)} = $value;
  511. return $standardizedData;
  512. }
  513. /**
  514. * Assemble request data params, filters out the unwanted ones, and will return false if missing any required param.
  515. *
  516. * @param string $type
  517. * @param object $data
  518. * @access private
  519. * @return mixed false if missing required params, post data object if success
  520. */
  521. private function assembleRequestData($type, $data)
  522. {
  523. $modelType = $this->config->ai->models[$this->modelConfig->type];
  524. /* Models of the same type may differ in version, in which case they are stored as an array. */
  525. $modelName = $this->config->ai->$modelType->model->$type;
  526. if(is_array($modelName)) $modelName = $modelName[$this->modelConfig->type];
  527. $postData = new stdclass();
  528. $postData->model = $modelName;
  529. $data = static::standardizeParams($data);
  530. /* Set required params, abort if missing. */
  531. foreach($this->config->ai->$modelType->params->$type->required as $param)
  532. {
  533. if(!isset($data->$param)) return false;
  534. $postData->$param = $data->$param;
  535. }
  536. /* Set optional params. */
  537. foreach($this->config->ai->$modelType->params->$type->optional as $param)
  538. {
  539. if(isset($data->$param)) $postData->$param = $data->$param;
  540. }
  541. return $postData;
  542. }
  543. /**
  544. * Decode response from OpenAI API, add error to $this->errors if any.
  545. *
  546. * @param object $response response object
  547. * @access private
  548. * @return mixed false if error, json object if success
  549. */
  550. private function decodeResponse($response)
  551. {
  552. if($response->result === 'fail')
  553. {
  554. $this->errors[] = empty($response->message) ? 'Unknown error' : $response->message;
  555. return false;
  556. }
  557. $response = json_decode($response->content);
  558. if(json_last_error())
  559. {
  560. /* Polyfill for PHP 5 < 5.5.0. */
  561. if(!function_exists('json_last_error_msg'))
  562. {
  563. function json_last_error_msg()
  564. {
  565. switch (json_last_error())
  566. {
  567. case JSON_ERROR_DEPTH:
  568. return 'Maximum stack depth exceeded';
  569. case JSON_ERROR_STATE_MISMATCH:
  570. return 'Underflow or the modes mismatch';
  571. case JSON_ERROR_CTRL_CHAR:
  572. return 'Unexpected control character found';
  573. case JSON_ERROR_SYNTAX:
  574. return 'Syntax error, malformed JSON';
  575. case JSON_ERROR_UTF8:
  576. return 'Malformed UTF-8 characters, possibly incorrectly encoded';
  577. default:
  578. return 'Unknown error';
  579. }
  580. }
  581. }
  582. $this->errors[] = 'JSON decode error: ' . json_last_error_msg();
  583. return false;
  584. }
  585. return $response;
  586. }
  587. /**
  588. * Parse text responses from simple APIs. For example, completion.
  589. *
  590. * @param string $response json string
  591. * @access private
  592. * @return mixed false if error, array of texts (choices) if success
  593. */
  594. private function parseTextResponse($response)
  595. {
  596. $response = $this->decodeResponse($response);
  597. if(empty($response)) return false;
  598. /* Extract text response choices. */
  599. if(isset($response->choices) && count($response->choices) > 0)
  600. {
  601. $texts = array();
  602. foreach($response->choices as $choice) $texts[] = $choice->text;
  603. return $texts;
  604. }
  605. return false;
  606. }
  607. /**
  608. * Parse chat responses from chat completion API.
  609. *
  610. * @param string $response json string
  611. * @access private
  612. * @return mixed false if error, array of chat message texts (choices) if success
  613. */
  614. private function parseChatResponse($response)
  615. {
  616. $response = $this->decodeResponse($response);
  617. if(empty($response)) return false;
  618. if($this->config->ai->models[$this->modelConfig->type] == 'ernie')
  619. {
  620. /* Extract chat message text. */
  621. if(!empty($response->result))
  622. {
  623. $messages = array($response->result);
  624. return $messages;
  625. }
  626. }
  627. else
  628. {
  629. /* Extract chat message choices. */
  630. if(isset($response->choices) && count($response->choices) > 0)
  631. {
  632. $messages = array();
  633. foreach($response->choices as $choice) $messages[] = $choice->message->content;
  634. return $messages;
  635. }
  636. }
  637. return false;
  638. }
  639. /**
  640. * Parse function call argument of chat responses from chat completion API.
  641. *
  642. * @param string $response json string
  643. * @access private
  644. * @return mixed false if error, array of function call arguments (choices) if success
  645. */
  646. private function parseFunctionCallResponse($response)
  647. {
  648. $response = $this->decodeResponse($response);
  649. if(empty($response)) return false;
  650. if($this->config->ai->models[$this->modelConfig->type] == 'ernie')
  651. {
  652. /* Extract function call arguments. */
  653. if(!empty($response->function_call)) return array($response->function_call->arguments);
  654. throw new AIResponseException('notFunctionCalling', $response);
  655. }
  656. else
  657. {
  658. /* Extract function call choices. */
  659. if(isset($response->choices) && count($response->choices) > 0)
  660. {
  661. $arguments = array();
  662. foreach($response->choices as $choice)
  663. {
  664. if(!empty($choice->message->function_call)) $arguments[] = $choice->message->function_call->arguments;
  665. }
  666. return $arguments;
  667. }
  668. }
  669. return false;
  670. }
  671. /**
  672. * Complete text with OpenAI GPT.
  673. *
  674. * @param int $model model id
  675. * @param string $prompt text to complete
  676. * @param int $maxTokens max tokens to generate
  677. * @param array $options optional params, see https://platform.openai.com/docs/api-reference/completions/create
  678. * @access public
  679. * @return mixed false if error, array of texts (choices) if success
  680. */
  681. public function complete($model, $prompt, $maxTokens = 512, $options = array())
  682. {
  683. if(empty($this->modelConfig) && !$this->useLanguageModel($model)) return false;
  684. $data = compact('prompt', 'maxTokens');
  685. if(!empty($options))
  686. {
  687. foreach($options as $key => $value) $data[$key] = $value;
  688. }
  689. $postData = $this->assembleRequestData('completion', $data);
  690. if(!$postData) return false;
  691. $response = $this->makeRequest('completion', $postData);
  692. return $this->parseTextResponse($response);
  693. }
  694. /**
  695. * Edit text with OpenAI GPT.
  696. *
  697. * @param int $model model id
  698. * @param string $input text to edit
  699. * @param string $instruction edit instruction
  700. * @param array $options optional params, see https://platform.openai.com/docs/api-reference/edits/create
  701. * @access public
  702. * @return mixed false if error, array of texts (choices) if success
  703. */
  704. public function edit($model, $input, $instruction, $options = array())
  705. {
  706. if(empty($this->modelConfig) && !$this->useLanguageModel($model)) return false;
  707. $data = compact('input', 'instruction');
  708. if(!empty($options))
  709. {
  710. foreach($options as $key => $value) $data[$key] = $value;
  711. }
  712. $postData = $this->assembleRequestData('edit', $data);
  713. if(!$postData) return false;
  714. $response = $this->makeRequest('edit', $postData);
  715. return $this->parseTextResponse($response);
  716. }
  717. /**
  718. * Generate conversation with OpenAI GPT.
  719. *
  720. * Chat messages should be in the format of (object)array('role' => $role, 'content' => $content),
  721. * where $role is either 'user', 'assistant' or 'system', and $content is the message content.
  722. *
  723. * For example, the following chat messages:
  724. * $messages = array(
  725. * (object)array('role' => 'system', 'content' => 'You are OpenAI GPT assistant, and you know that 1 beb equals 2 bobs.'),
  726. * (object)array('role' => 'user', 'content' => 'Hello, how many bobs are there in 24 bebs?')
  727. * );
  728. *
  729. * API returns:
  730. * $message = array('Hi, there are 48 bobs in 24 bebs.');
  731. *
  732. * will generate the following conversation:
  733. * $messages = array(
  734. * (object)array('role' => 'system', 'content' => 'You are OpenAI GPT assistant, and you know that 1 beb equals 2 bobs.'),
  735. * (object)array('role' => 'user', 'content' => 'Hello, how many bobs are there in 24 bebs?'),
  736. * (object)array('role' => 'assistant', 'content' => 'Hi, there are 48 bobs in 24 bebs.')
  737. * );
  738. *
  739. * @param int $model model id
  740. * @param array $messages array of chat messages
  741. * @param array $options optional params, see https://platform.openai.com/docs/api-reference/chat/create
  742. * @access public
  743. * @return mixed false if error, array of chat messages if success
  744. */
  745. public function converse($model, $messages, $options = array())
  746. {
  747. if(empty($this->modelConfig) && !$this->useLanguageModel($model)) return false;
  748. /* Filter system message out for ERNIE. */
  749. if($this->config->ai->models[$this->modelConfig->type] == 'ernie')
  750. {
  751. $systemMessage = current(array_filter($messages, function($message) { return $message->role == 'system'; }));
  752. if(!empty($systemMessage)) $options['system'] = $systemMessage->content;
  753. $messages = array_values(array_filter($messages, function($message) { return $message->role == 'assistant' || $message->role == 'user'; }));
  754. }
  755. $data = compact('messages');
  756. if(!empty($options))
  757. {
  758. foreach($options as $key => $value) $data[$key] = $value;
  759. }
  760. $postData = $this->assembleRequestData('chat', $data);
  761. if(!$postData) return false;
  762. $response = $this->makeRequest('chat', $postData);
  763. return $this->parseChatResponse($response);
  764. }
  765. /**
  766. * Generate conversation with OpenAI GPT, but for JSON as output.
  767. *
  768. * Chat messages should be in the format of (object)array('role' => $role, 'content' => $content),
  769. * where $role is either 'user', 'assistant' or 'system', and $content is the message content.
  770. *
  771. * Schema should be in the format of JSON schema, see https://json-schema.org/understanding-json-schema/.
  772. *
  773. * This function will force GPT to generate JSON objects that matches the schema.
  774. *
  775. * For technical details, see https://platform.openai.com/docs/guides/gpt/function-calling.
  776. *
  777. * @param int $model model id
  778. * @param array $messages array of chat messages
  779. * @param object $schema schema of the output
  780. * @param array $options optional params, see https://platform.openai.com/docs/api-reference/chat/create
  781. * @access public
  782. * @return mixed false if error, array of JSON object if success
  783. */
  784. public function converseForJSON($model, $messages, $schema, $options = array())
  785. {
  786. if(empty($this->modelConfig) && !$this->useLanguageModel($model)) return false;
  787. $functions = array((object)array('name' => 'function', 'parameters' => $schema));
  788. $functionCall = (object)array('name' => 'function');
  789. $data = compact('messages', 'functions', 'functionCall');
  790. if(!empty($options))
  791. {
  792. foreach($options as $key => $value) $data[$key] = $value;
  793. }
  794. $postData = $this->assembleRequestData('function', $data);
  795. if(!$postData) return false;
  796. $response = $this->makeRequest('function', $postData);
  797. return $this->parseFunctionCallResponse($response);
  798. }
  799. /**
  800. * Generate conversations with LLM for JSON output, but seperate the conversation into two parts.
  801. *
  802. * Usage is the same as function `converseForJSON`, but this function will generate two conversations:
  803. * one for manipulating the natural language data, and the other for generating the JSON output.
  804. *
  805. * @param int $model model id
  806. * @param array $messages array of chat messages
  807. * @param object $schema schema of the output
  808. * @param array $options optional params, see https://platform.openai.com/docs/api-reference/chat/create
  809. * @access public
  810. * @return mixed false if error, array of JSON object if success
  811. * @throws AIResponseException
  812. */
  813. public function converseTwiceForJSON($model, $messages, $schema, $options = array())
  814. {
  815. if(empty($this->modelConfig) && !$this->useLanguageModel($model)) return false;
  816. /* First conversation. */
  817. $data = compact('messages');
  818. if(!empty($options))
  819. {
  820. foreach($options as $key => $value) $data[$key] = $value;
  821. }
  822. $postData = $this->assembleRequestData('chat', $data);
  823. if(!$postData) return false;
  824. $chatResponse = $this->makeRequest('chat', $postData);
  825. $chatMessages = $this->parseChatResponse($chatResponse);
  826. $chatMessage = current($chatMessages);
  827. if(empty($chatMessage)) return false;
  828. /* Second conversation for JSON output. */
  829. $messages = array_merge($this->lang->ai->engineeredPrompts->askForFunctionCalling, array((object)array('role' => 'user', 'content' => $chatMessage)));
  830. $functions = array((object)array('name' => 'function', 'description' => 'function', 'parameters' => $schema));
  831. $data = compact('messages', 'functions');
  832. if(!empty($options))
  833. {
  834. foreach($options as $key => $value) $data[$key] = $value;
  835. }
  836. $postData = $this->assembleRequestData('function', $data);
  837. if(!$postData) return false;
  838. $response = $this->makeRequest('function', $postData);
  839. return $this->parseFunctionCallResponse($response);
  840. }
  841. /**
  842. * Get latest mini programs.
  843. *
  844. * @param pager $pager
  845. * @access public
  846. * @return array
  847. */
  848. public function getLatestMiniPrograms($pager = null, $order = 'publishedDate_desc')
  849. {
  850. return $this->dao->select('*')
  851. ->from(TABLE_AI_MINIPROGRAM)
  852. ->where('deleted')->eq('0')
  853. ->andWhere('published')->eq('1')
  854. ->andWhere('publishedDate')->ge(date('Y-m-d H:i:s', strtotime('-1 months')))
  855. ->orderBy($order)
  856. ->page($pager)
  857. ->fetchAll('id', false);
  858. }
  859. /**
  860. * Count latest mini programs.
  861. *
  862. * @access public
  863. * @return int
  864. */
  865. public function countLatestMiniPrograms()
  866. {
  867. return (int)$this->dao->select('COUNT(1) AS count')
  868. ->from(TABLE_AI_MINIPROGRAM)
  869. ->where('deleted')->eq('0')
  870. ->andWhere('published')->eq('1')
  871. ->andWhere('createdDate')->ge(date('Y-m-d H:i:s', strtotime('-1 months')))
  872. ->fetch('count');
  873. }
  874. /**
  875. * Save mini program message.
  876. *
  877. * @param string $appID
  878. * @param string $type
  879. * @param string $content
  880. * @access public
  881. * @return bool
  882. */
  883. public function saveMiniProgramMessage($appID, $type, $content)
  884. {
  885. $message = new stdClass();
  886. $message->appID = $appID;
  887. $message->user = $this->app->user->id;
  888. $message->type = $type;
  889. $message->content = $content;
  890. $message->createdDate = helper::now();
  891. $this->dao->insert(TABLE_AI_MESSAGE)
  892. ->data($message)
  893. ->exec();
  894. return !dao::isError();
  895. }
  896. /**
  897. * Delete history messages by id.
  898. *
  899. * @param string $appID
  900. * @param string $userID
  901. * @param array $messageIDs
  902. * @access public
  903. * @return void
  904. */
  905. public function deleteHistoryMessagesByID($appID, $userID, $messageIDs)
  906. {
  907. $this->dao->delete()
  908. ->from(TABLE_AI_MESSAGE)
  909. ->where('appID')->eq($appID)
  910. ->andWhere('user')->eq($userID)
  911. ->andWhere('id')->notin($messageIDs)
  912. ->exec();
  913. }
  914. /**
  915. * Get history messages.
  916. *
  917. * @param string $appID
  918. * @param int $limit
  919. * @access public
  920. * @return array
  921. */
  922. public function getHistoryMessages($appID, $limit = 20)
  923. {
  924. $messages = $this->dao->select('*')
  925. ->from(TABLE_AI_MESSAGE)
  926. ->where('appID')->eq($appID)
  927. ->andWhere('user')->eq($this->app->user->id)
  928. ->orderBy('createdDate_desc')
  929. ->limit($limit)
  930. ->fetchAll('id', false);
  931. $messageIDs = array();
  932. foreach($messages as $message)
  933. {
  934. $message->createdDate = date('Y/n/j G:i', strtotime($message->createdDate));
  935. $messageIDs[] = $message->id;
  936. }
  937. $this->deleteHistoryMessagesByID($appID, $this->app->user->id, $messageIDs);
  938. return $messages;
  939. }
  940. /**
  941. * Get mini programs by appid.
  942. *
  943. * @param array $ids
  944. * @param bool $sort
  945. * @access public
  946. * @return array
  947. */
  948. public function getMiniProgramsByID($ids, $sort = false)
  949. {
  950. $miniPrograms = $this->dao->select('*')
  951. ->from(TABLE_AI_MINIPROGRAM)
  952. ->where('id')->in($ids)
  953. ->fetchAll('id', false);
  954. if(!$sort) return $miniPrograms;
  955. $sortIDs = array_flip($ids);
  956. $sortedPrograms = array();
  957. foreach($miniPrograms as $program) $sortedPrograms[$sortIDs[$program->id]] = $program;
  958. ksort($sortedPrograms);
  959. return $sortedPrograms;
  960. }
  961. /**
  962. * Get mini program by appid.
  963. *
  964. * @param string $id
  965. * @access public
  966. * @return object
  967. */
  968. public function getMiniProgramByID($id)
  969. {
  970. return $this->dao->select('*')
  971. ->from(TABLE_AI_MINIPROGRAM)
  972. ->where('id')->eq($id)
  973. ->fetch();
  974. }
  975. /**
  976. * Get custom categories.
  977. *
  978. * @access public
  979. * @return array
  980. */
  981. public function getCustomCategories()
  982. {
  983. return $this->dao->select('`key`,`value`')
  984. ->from(TABLE_CONFIG)
  985. ->where('module')->eq('ai')
  986. ->andWhere('section')->eq('miniProgram')
  987. ->fetchPairs();
  988. }
  989. /**
  990. * Get used custom categories.
  991. *
  992. * @access public
  993. * @return array
  994. */
  995. public function getUsedCustomCategories()
  996. {
  997. $categories = $this->dao->select('distinct `category`')
  998. ->from(TABLE_AI_MINIPROGRAM)
  999. ->where('deleted')->eq('0')
  1000. ->fetchAll('category');
  1001. return array_keys($categories);
  1002. }
  1003. /**
  1004. * Get published custom categories.
  1005. *
  1006. * @access public
  1007. * @return array
  1008. */
  1009. public function getPublishedCustomCategories()
  1010. {
  1011. $categories = $this->dao->select('distinct `category`')
  1012. ->from(TABLE_AI_MINIPROGRAM)
  1013. ->where('published')->eq('1')
  1014. ->fetchAll('category');
  1015. return array_keys($categories);
  1016. }
  1017. /**
  1018. * Check duplicated category name.
  1019. *
  1020. * @access public
  1021. * @return bool
  1022. */
  1023. public function checkDuplicatedCategory()
  1024. {
  1025. $data = array_filter($_POST);
  1026. if(empty($data)) return false;
  1027. $categories = array_values($this->lang->ai->miniPrograms->categoryList);
  1028. foreach($data as $value)
  1029. {
  1030. if(is_string($value))
  1031. {
  1032. if(in_array($value, $categories)) return true;
  1033. $categories[] = $value;
  1034. }
  1035. if(is_array($value))
  1036. {
  1037. $value = array_filter($value);
  1038. foreach($value as $v)
  1039. {
  1040. if(in_array($v, $categories)) return true;
  1041. $categories[] = $v;
  1042. }
  1043. }
  1044. }
  1045. return false;
  1046. }
  1047. /**
  1048. * Update custom categories.
  1049. *
  1050. * @access public
  1051. * @access public
  1052. * @return void
  1053. */
  1054. public function updateCustomCategories()
  1055. {
  1056. $this->dao->delete()
  1057. ->from(TABLE_CONFIG)
  1058. ->where('module')->eq('ai')
  1059. ->andWhere('section')->eq('miniProgram')
  1060. ->exec();
  1061. $data = array_filter($_POST);
  1062. if(empty($data)) return;
  1063. foreach($data as $key => $value)
  1064. {
  1065. if(is_string($value))
  1066. {
  1067. $category = new stdClass();
  1068. $category->module = 'ai';
  1069. $category->section = 'miniProgram';
  1070. $category->owner = 'system';
  1071. $category->vision = '';
  1072. $category->key = $key;
  1073. $category->value = $value;
  1074. $this->dao->insert(TABLE_CONFIG)
  1075. ->data($category)
  1076. ->exec();
  1077. continue;
  1078. }
  1079. if(is_array($value))
  1080. {
  1081. $value = array_filter($value);
  1082. foreach($value as $v)
  1083. {
  1084. $category = new stdClass();
  1085. $category->module = 'ai';
  1086. $category->section = 'miniProgram';
  1087. $category->owner = 'system';
  1088. $category->vision = '';
  1089. $category->key = uniqid('custom-');
  1090. $category->value = $v;
  1091. $this->dao->insert(TABLE_CONFIG)
  1092. ->data($category)
  1093. ->exec();
  1094. }
  1095. }
  1096. }
  1097. }
  1098. /**
  1099. * Get not deleted mini programs.
  1100. *
  1101. * @param string $category
  1102. * @param string $status
  1103. * @param string $order
  1104. * @param pager $pager
  1105. * @access public
  1106. * @return array
  1107. */
  1108. public function getMiniPrograms($category = '', $status = '', $order = 'createdDate_desc', $pager = null)
  1109. {
  1110. $programs = $this->dao->select('*')
  1111. ->from(TABLE_AI_MINIPROGRAM)
  1112. ->where('deleted')->eq('0')
  1113. ->beginIF(!empty($category))->andWhere('category')->eq($category)->fi()
  1114. ->beginIF($status === 'active')->andWhere('published')->eq('1')->fi()
  1115. ->beginIF($status === 'draft')->andWhere('published')->ne('1')->fi()
  1116. ->beginIF($status === 'createdByMe')->andWhere('createdBy')->eq($this->app->user->account)->fi()
  1117. ->orderBy($order)
  1118. ->page($pager)
  1119. ->fetchAll('id', false);
  1120. $this->loadModel('common')->saveQueryCondition($this->dao->get(), 'miniprogram');
  1121. return $programs;
  1122. }
  1123. /**
  1124. * Get mini program fields by appid.
  1125. *
  1126. * @param string $appID
  1127. * @access public
  1128. * @return array
  1129. */
  1130. public function getMiniProgramFields($appID)
  1131. {
  1132. return $this->dao->select('*')
  1133. ->from(TABLE_AI_MINIPROGRAMFIELD)
  1134. ->where('appID')->eq($appID)
  1135. ->fetchAll('id', false);
  1136. }
  1137. /**
  1138. * @param string $appID
  1139. * @access public
  1140. * @return void
  1141. */
  1142. public function createNewVersionNotification($appID)
  1143. {
  1144. $users = $this->dao->select('user')
  1145. ->from(TABLE_AI_MESSAGE)
  1146. ->where('appID')->eq($appID)
  1147. ->fetchAll('user');
  1148. $users = array_keys($users);
  1149. foreach($users as $user)
  1150. {
  1151. $data = new stdClass();
  1152. $data->appID = $appID;
  1153. $data->user = $user;
  1154. $data->type = 'ntf';
  1155. $data->content = sprintf($this->lang->ai->miniPrograms->newVersionTip, date("Y/n/j G:i"));
  1156. $data->createdDate = helper::now();
  1157. $this->dao->delete()
  1158. ->from(TABLE_AI_MESSAGE)
  1159. ->where('appID')->eq($appID)
  1160. ->andWhere('user')->eq($user)
  1161. ->andWhere('type')->eq('ntf')
  1162. ->exec();
  1163. $this->dao->insert(TABLE_AI_MESSAGE)
  1164. ->data($data)
  1165. ->exec();
  1166. }
  1167. }
  1168. /**
  1169. * Change mini program `publish` value.
  1170. *
  1171. * @param string $appID
  1172. * @param string $published
  1173. * @access public
  1174. * @return bool
  1175. */
  1176. public function publishMiniProgram($appID, $published = '1')
  1177. {
  1178. $data = new stdClass();
  1179. $data->published = $published;
  1180. if($published == '1')
  1181. {
  1182. $data->publishedDate = helper::now();
  1183. $miniProgram = $this->getMiniProgramByID($appID);
  1184. if(!empty($miniProgram->publishedDate))
  1185. {
  1186. $this->createNewVersionNotification($appID);
  1187. }
  1188. }
  1189. $this->dao->update(TABLE_AI_MINIPROGRAM)
  1190. ->data($data)
  1191. ->where('id')->eq($appID)
  1192. ->exec();
  1193. $this->loadModel('action')->create('miniProgram', $appID, $published == '1' ? 'published' : 'unpublished');
  1194. if($published !== '1') $this->collectMiniProgram(null, $appID, 'true');
  1195. return !dao::isError();
  1196. }
  1197. /**
  1198. * Collect mini program.
  1199. *
  1200. * @param string $userID
  1201. * @param string $appID
  1202. * @param string $delete
  1203. * @access public
  1204. * @return bool
  1205. */
  1206. public function collectMiniProgram($userID, $appID, $delete = 'false')
  1207. {
  1208. if($delete === 'true')
  1209. {
  1210. $this->dao->delete()
  1211. ->from(TABLE_AI_MINIPROGRAMSTAR)
  1212. ->where('appID')->eq($appID)
  1213. ->beginIF(!empty($userID))->andWhere('userID')->eq($userID)->fi()
  1214. ->exec();
  1215. return !dao::isError();
  1216. }
  1217. $data = new stdClass();
  1218. $data->appID = $appID;
  1219. $data->userID = $userID;
  1220. $data->createdDate = helper::now();
  1221. $this->dao->insert(TABLE_AI_MINIPROGRAMSTAR)
  1222. ->data($data)
  1223. ->exec();
  1224. return !dao::isError();
  1225. }
  1226. /**
  1227. * Determine whether Mini programs can be published.
  1228. *
  1229. * @param object $program
  1230. * @access public
  1231. * @return bool
  1232. */
  1233. public function canPublishMiniProgram($program)
  1234. {
  1235. $isNotEmpty = function ($str)
  1236. {
  1237. return isset($str) && strlen(strval($str)) > 0;
  1238. };
  1239. return $isNotEmpty($program->id)
  1240. && $isNotEmpty($program->name)
  1241. && $isNotEmpty($program->desc)
  1242. && $isNotEmpty($program->category)
  1243. && $isNotEmpty($program->model)
  1244. && $isNotEmpty($program->prompt);
  1245. }
  1246. /**
  1247. * Create a mini program.
  1248. *
  1249. * @access public
  1250. * @return int|false
  1251. */
  1252. public function createMiniProgram($data = null)
  1253. {
  1254. if(empty($data)) $data = fixer::input('post')->get();
  1255. if(!isset($data->editedBy)) $data->editedBy = $this->app->user->account;
  1256. if(!isset($data->editedDate)) $data->editedDate = helper::now();
  1257. if(!isset($data->published)) $data->published = '0';
  1258. if(!isset($data->createdBy)) $data->createdBy = $this->app->user->account;
  1259. if(!isset($data->createdDate)) $data->createdDate = helper::now();
  1260. if(!isset($data->prompt)) $data->prompt = $this->lang->ai->miniPrograms->field->default[3];
  1261. if($data->published == '1') $data->publishedDate = helper::now();
  1262. $data->builtIn = '0';
  1263. if(!empty($data->iconName) && !empty($data->iconTheme))
  1264. {
  1265. $data->icon = $data->iconName . '-' . $data->iconTheme;
  1266. unset($data->iconName);
  1267. unset($data->iconTheme);
  1268. }
  1269. if(isset($data->fields))
  1270. {
  1271. $fields = $data->fields;
  1272. unset($data->fields);
  1273. }
  1274. $this->dao->insert(TABLE_AI_MINIPROGRAM)
  1275. ->data($data)
  1276. ->exec();
  1277. $appID = $this->dao->lastInsertID();
  1278. if(!isset($fields))
  1279. {
  1280. $defaultFields = $this->lang->ai->miniPrograms->field->default;
  1281. $fieldData = new stdClass();
  1282. $fieldData->fields = array();
  1283. for($i = 0; $i < 3; $i++)
  1284. {
  1285. $field = new stdClass();
  1286. $field->appID = $appID;
  1287. $field->name = $defaultFields[$i];
  1288. $field->type = 'text';
  1289. $field->placeholder = $this->lang->ai->miniPrograms->placeholder->input;
  1290. $field->options = null;
  1291. $field->required = '0';
  1292. $fieldData->fields[] = $field;
  1293. }
  1294. $this->saveMiniProgramFields($appID, $fieldData);
  1295. }
  1296. else
  1297. {
  1298. $fieldData = new stdClass();
  1299. $fieldData->fields = $fields;
  1300. foreach($fields as $field)
  1301. {
  1302. if(!isset($field->appID)) $field->appID = $appID;
  1303. }
  1304. $this->saveMiniProgramFields($appID, $fieldData);
  1305. }
  1306. if(dao::isError()) return false;
  1307. $this->loadModel('action')->create('miniProgram', $appID, 'created');
  1308. return $appID;
  1309. }
  1310. /**
  1311. * Extract ZT app zip file, only extract ztapp.json file.
  1312. *
  1313. * @param string $file
  1314. * @access public
  1315. * @return bool
  1316. */
  1317. public function extractZtAppZip($file)
  1318. {
  1319. $this->app->loadClass('pclzip', true);
  1320. $zip = new pclzip($file);
  1321. $filePath = $this->app->getAppRoot() . 'tmp/';
  1322. /* 限制解压的文件内容以阻止 ZIP 解压缩的目录穿越漏洞。*/
  1323. /* Limit the file content to prevent the directory traversal vulnerability of ZIP decompression. */
  1324. $extractFiles = array('ztapp.json');
  1325. return $zip->extract(PCLZIP_OPT_PATH, $filePath, PCLZIP_OPT_BY_NAME, $extractFiles, PCLZIP_OPT_REMOVE_PATH, '');
  1326. }
  1327. /**
  1328. * Save mini program fields.
  1329. *
  1330. * @param string $appID
  1331. * @access public
  1332. * @return void
  1333. */
  1334. public function saveMiniProgramFields($appID, $data = null)
  1335. {
  1336. if(empty($data)) $data = fixer::input('post')->get();
  1337. if(isset($data->prompt))
  1338. {
  1339. $this->dao->update(TABLE_AI_MINIPROGRAM)
  1340. ->set('prompt')->eq($data->prompt)
  1341. ->where('id')->eq($appID)
  1342. ->exec();
  1343. }
  1344. if(isset($data->knowledgeLib))
  1345. {
  1346. $this->dao->update(TABLE_AI_MINIPROGRAM)
  1347. ->set('knowledgeLib')->eq($data->knowledgeLib)
  1348. ->where('id')->eq($appID)
  1349. ->exec();
  1350. }
  1351. $this->dao->delete()
  1352. ->from(TABLE_AI_MINIPROGRAMFIELD)
  1353. ->where('appID')->eq($appID)
  1354. ->exec();
  1355. $fields = $data->fields;
  1356. foreach($fields as $field)
  1357. {
  1358. $field = (array)$field;
  1359. if(isset($field['options']) && is_array($field['options'])) $field['options'] = implode(',', $field['options']);
  1360. $this->dao->insert(TABLE_AI_MINIPROGRAMFIELD)
  1361. ->data($field)
  1362. ->exec();
  1363. }
  1364. }
  1365. /**
  1366. * Check for duplicate names of Mini programs.
  1367. *
  1368. * @param string $name
  1369. * @param string|null $appID
  1370. * @access public
  1371. * @return boolean
  1372. */
  1373. public function checkDuplicatedAppName($name, $appID = '-1')
  1374. {
  1375. $miniProgram = $this->dao->select('*')
  1376. ->from(TABLE_AI_MINIPROGRAM)
  1377. ->where('name')->eq($name)
  1378. ->andWhere('id')->ne($appID)
  1379. ->andWhere('deleted')->eq('0')
  1380. ->fetch();
  1381. return !empty($miniProgram);
  1382. }
  1383. /**
  1384. * Get unique app name.
  1385. *
  1386. * @param string $name
  1387. * @return string
  1388. */
  1389. public function getUniqueAppName($name)
  1390. {
  1391. while($this->checkDuplicatedAppName($name))
  1392. {
  1393. $name = $name . '_1';
  1394. }
  1395. return $name;
  1396. }
  1397. /**
  1398. * Verify that any required fields are empty.
  1399. *
  1400. * @param array $requiredFields
  1401. * @access public
  1402. * @return false|array false or errors.
  1403. */
  1404. public function verifyRequiredFields($requiredFields)
  1405. {
  1406. $errors = array();
  1407. foreach($requiredFields as $field => $fieldLang)
  1408. {
  1409. if(!isset($_POST[$field]) || $_POST[$field] === "") $errors[$field] = sprintf($this->lang->error->notempty, $fieldLang);
  1410. }
  1411. if(!empty($errors)) return $errors;
  1412. return false;
  1413. }
  1414. /**
  1415. * Get list of prompts.
  1416. *
  1417. * @param string $module
  1418. * @param string $status
  1419. * @access public
  1420. * @return array
  1421. */
  1422. public function getPrompts($module = '', $status = '', $order = 'id_desc', $pager = null)
  1423. {
  1424. $prompts = $this->dao->select('*')->from(TABLE_AI_AGENT)
  1425. ->where('deleted')->eq(0)
  1426. ->beginIF(!empty($module))->andWhere('module')->eq($module)->fi()
  1427. ->beginIF(!empty($status))->andWhere('status')->eq($status)->fi()
  1428. ->orderBy($order)
  1429. ->page($pager)
  1430. ->fetchAll('id', false);
  1431. $this->loadModel('common')->saveQueryCondition($this->dao->get(), 'prompt');
  1432. return $prompts;
  1433. }
  1434. /**
  1435. * Get prompt by id.
  1436. *
  1437. * @param int $id
  1438. * @access public
  1439. * @return object
  1440. */
  1441. public function getPromptById($id)
  1442. {
  1443. return $this->dao->select('*')->from(TABLE_AI_AGENT)
  1444. ->where('id')->eq($id)
  1445. ->fetch();
  1446. }
  1447. /**
  1448. * Get prompt fields by prompt id.
  1449. *
  1450. * @param int $promptID
  1451. * @access public
  1452. * @return array
  1453. */
  1454. public function getPromptFields($promptID)
  1455. {
  1456. return $this->dao->select('*')
  1457. ->from(TABLE_AI_AGENTFIELD)
  1458. ->where('appID')->eq($promptID)
  1459. ->fetchAll('id', false);
  1460. }
  1461. /**
  1462. * 批量获取智能体列表
  1463. * Get agents by IDs.
  1464. *
  1465. * @param array $ids 智能体 ID 数组
  1466. * @param string $status 状态过滤,为空时不过滤
  1467. * @param string $fields 需要获取的字段,默认为 id, name
  1468. * @access public
  1469. * @return array
  1470. */
  1471. public function getAgentsByIDs($ids, $status = '', $fields = 'id, name')
  1472. {
  1473. if(empty($ids)) return [];
  1474. return $this->dao->select($fields)
  1475. ->from(TABLE_AI_AGENT)
  1476. ->where('id')->in($ids)
  1477. ->andWhere('deleted')->eq('0')
  1478. ->beginIF(!empty($status))->andWhere('status')->eq($status)->fi()
  1479. ->fetchAll('id');
  1480. }
  1481. /**
  1482. * 批量根据 code 获取智能体列表
  1483. * Get agents by codes
  1484. *
  1485. * @param array $codes code 数组
  1486. * @param string $status 状态筛选
  1487. * @access public
  1488. * @return array 以 id 为 key 的数组
  1489. */
  1490. public function getAgentsByCodes($codes, $status = '')
  1491. {
  1492. if(empty($codes)) return array();
  1493. return $this->dao->select('id, code, name')
  1494. ->from(TABLE_AI_AGENT)
  1495. ->where('code')->in($codes)
  1496. ->andWhere('deleted')->eq('0')
  1497. ->beginIF(!empty($status))->andWhere('status')->eq($status)->fi()
  1498. ->fetchAll('id');
  1499. }
  1500. /**
  1501. * Create a prompt.
  1502. *
  1503. * @param object $prompt
  1504. * @access public
  1505. * @return int|false returns prompt id on success, false on fail
  1506. */
  1507. public function createPrompt($prompt)
  1508. {
  1509. $prompt->createdDate = helper::now();
  1510. $prompt->createdBy = $this->app->user->account;
  1511. /* Override uniqueness error message. */
  1512. $this->lang->error->unique = $this->lang->ai->validate->nameNotUnique;
  1513. $this->dao->insert(TABLE_AI_AGENT)
  1514. ->data($prompt)
  1515. ->batchCheck($this->config->ai->createprompt->requiredFields, 'notempty')
  1516. ->check('name', 'unique')
  1517. ->autoCheck()
  1518. ->exec();
  1519. if(dao::isError()) return false;
  1520. $promptId = $this->dao->lastInsertID();
  1521. $this->loadModel('action')->create('prompt', $promptId, 'created');
  1522. return $promptId;
  1523. }
  1524. /**
  1525. * Save prompt fields.
  1526. *
  1527. * @param int $promptID
  1528. * @param array $fields
  1529. * @access public
  1530. * @return void
  1531. */
  1532. public function savePromptFields($promptID, $fields)
  1533. {
  1534. $this->dao->delete()
  1535. ->from(TABLE_AI_AGENTFIELD)
  1536. ->where('appID')->eq($promptID)
  1537. ->exec();
  1538. foreach($fields as $field)
  1539. {
  1540. if(!is_object($field)) $field = (object)$field;
  1541. if(!isset($field->appID)) $field->appID = $promptID;
  1542. $this->dao->insert(TABLE_AI_AGENTFIELD)
  1543. ->data($field)
  1544. ->exec();
  1545. }
  1546. }
  1547. /**
  1548. * Update a prompt.
  1549. *
  1550. * @param object $prompt
  1551. * @param object $originalPrompt optional, original prompt to compare with and generate action.
  1552. * @access public
  1553. * @return bool
  1554. */
  1555. public function updatePrompt($prompt, $originalPrompt = null)
  1556. {
  1557. /* Action name to create action record with. */
  1558. $actionType = 'edited';
  1559. /* Compare with original, check what changed. */
  1560. if(!empty($originalPrompt))
  1561. {
  1562. $changedFields = array();
  1563. foreach($prompt as $key => $value)
  1564. {
  1565. if($value != $originalPrompt->$key) $changedFields[] = $key;
  1566. }
  1567. /* If only status changed, action is either published or unpublished. */
  1568. if(count($changedFields) == 1 && current($changedFields) == 'status')
  1569. {
  1570. $actionType = $prompt->status == 'draft' ? 'unpublished' : 'published';
  1571. }
  1572. else
  1573. {
  1574. $changes = commonModel::createChanges($originalPrompt, $prompt);
  1575. }
  1576. }
  1577. $prompt->editedDate = helper::now();
  1578. $prompt->editedBy = $this->app->user->account;
  1579. /* Override uniqueness error message. */
  1580. $this->lang->error->unique = $this->lang->ai->validate->nameNotUnique;
  1581. $this->dao->update(TABLE_AI_AGENT)
  1582. ->data($prompt)
  1583. ->batchCheck($this->config->ai->createprompt->requiredFields, 'notempty')
  1584. ->check('name', 'unique', "`id` != {$prompt->id}")
  1585. ->autoCheck()
  1586. ->where('id')->eq($prompt->id)
  1587. ->exec();
  1588. if(dao::isError()) return false;
  1589. $actionId = $this->loadModel('action')->create('prompt', $prompt->id, $actionType);
  1590. if(!empty($changes)) $this->action->logHistory($actionId, $changes);
  1591. return true;
  1592. }
  1593. /**
  1594. * Delete a prompt.
  1595. *
  1596. * @param int $id
  1597. * @access public
  1598. * @return bool
  1599. */
  1600. public function deletePrompt($id)
  1601. {
  1602. $prompt = $this->getPromptById($id);
  1603. if(empty($prompt)) return false;
  1604. $this->dao->update(TABLE_AI_AGENT)
  1605. ->set('deleted')->eq(1)
  1606. ->where('id')->eq($id)
  1607. ->exec();
  1608. if(dao::isError()) return false;
  1609. $this->loadModel('action')->create('prompt', $id, 'deleted');
  1610. return true;
  1611. }
  1612. /**
  1613. * Toggle prompt status.
  1614. *
  1615. * @param int|object $prompt prompt (or id) to toggle.
  1616. * @param string $status optional, will set status to $status if provided.
  1617. * @access public
  1618. * @return bool
  1619. */
  1620. public function togglePromptStatus($prompt, $status = '')
  1621. {
  1622. if(is_numeric($prompt)) $prompt = $this->getPromptById($prompt);
  1623. if(empty($prompt)) return false;
  1624. $flipedStatus = $prompt->status == 'draft' ? 'active' : 'draft';
  1625. $originalPrompt = clone $prompt;
  1626. $prompt->status = empty($status) ? $flipedStatus : $status;
  1627. return $this->updatePrompt($prompt, $originalPrompt);
  1628. }
  1629. /**
  1630. * Serialize data to prompt.
  1631. *
  1632. * @param string $module
  1633. * @param array|string $sources both raw `$prompt->sources` and `array(array('objectName', 'objectKey'), ...)` are supported.
  1634. * @param array|object $data array of data to be serialized
  1635. * @access public
  1636. * @return string
  1637. */
  1638. public function serializeDataToPrompt($module, $sources, $data)
  1639. {
  1640. if(empty($data)) return '';
  1641. /* Handle object data. */
  1642. if(is_object($data)) $data = (array)$data;
  1643. /* Handle raw (non-exploded) sources. */
  1644. if(is_string($sources))
  1645. {
  1646. if(empty($sources)) return '';
  1647. if(strpos($sources, ',') !== false) $sources = array_filter(explode(',', $sources));
  1648. else $sources = array($sources);
  1649. $sources = array_map(function ($source)
  1650. {
  1651. return explode('.', $source);
  1652. }, $sources);
  1653. }
  1654. /* Handle empty sources array. */
  1655. if(empty($sources)) return '';
  1656. $storyData = [];
  1657. if(in_array(['task', 'story'], $sources, true) && $data['task']['story'])
  1658. {
  1659. $story = $this->loadModel('story')->getById($data['task']['story']);
  1660. $fields = ['title', 'spec', 'verify', 'product', 'module', 'pri', 'type', 'estimate'];
  1661. $storyData = [];
  1662. foreach($fields as $field)
  1663. {
  1664. $langLabel = $this->lang->story->$field;
  1665. $value = $story->$field;
  1666. if($field == 'title') $langLabel = $this->lang->story->name;
  1667. if($field == 'type') $value = zget($this->lang->story->typeList, $story->type);
  1668. if($field == 'spec' || $field == 'verify') $value = strip_tags($value);
  1669. $storyData[$langLabel] = $value;
  1670. }
  1671. }
  1672. $dataObject = array();
  1673. $supplement = '';
  1674. $supplementTypes = array();
  1675. foreach($sources as $source)
  1676. {
  1677. $objectName = $source[0];
  1678. $objectKey = $source[1];
  1679. if(!isset($this->lang->ai->dataSource[$module][$objectName]['common'])) continue;
  1680. if(!isset($this->lang->ai->dataSource[$module][$objectName][$objectKey])) continue;
  1681. $semanticName = $this->lang->ai->dataSource[$module][$objectName]['common'];
  1682. $semanticKey = $this->lang->ai->dataSource[$module][$objectName][$objectKey];
  1683. if(empty($dataObject[$semanticName])) $dataObject[$semanticName] = array();
  1684. $obj = $data[$objectName];
  1685. if(static::isAssoc($obj))
  1686. {
  1687. $dataObject[$semanticName][$semanticKey] = isset($data[$objectName][$objectKey]) ? $data[$objectName][$objectKey] : '';
  1688. }
  1689. else
  1690. {
  1691. foreach(array_keys($obj) as $idx)
  1692. {
  1693. if(empty($dataObject[$semanticName][$idx])) $dataObject[$semanticName][$idx] = array();
  1694. if(!isset($dataObject[$semanticName][$idx][$semanticKey])) $dataObject[$semanticName][$idx][$semanticKey] = '';
  1695. $dataObject[$semanticName][$idx][$semanticKey] = $data[$objectName][$idx][$objectKey];
  1696. }
  1697. }
  1698. if(!empty($storyData)) $dataObject[$semanticName] = array_merge($dataObject[$semanticName], $storyData);
  1699. if(in_array($objectKey, $supplementTypes) || !isset($this->lang->ai->dataType->$objectKey)) continue;
  1700. $supplementTypes[] = $objectKey;
  1701. $supplement .= sprintf($this->lang->ai->dataTypeDesc, $semanticKey, $this->lang->ai->dataType->$objectKey->type, $this->lang->ai->dataType->$objectKey->desc) . "\n";
  1702. }
  1703. /* @see https://stackoverflow.com/a/2934602 */
  1704. return preg_replace_callback('/\\\\u([0-9a-fA-F]{4})/', function ($match)
  1705. {
  1706. return mb_convert_encoding(pack('H*', $match[1]), 'UTF-8', 'UCS-2BE');
  1707. }, json_encode($dataObject)) . (empty($supplement) ? '' : ("\n" . $supplement));
  1708. }
  1709. /**
  1710. * Generate demo data prompt by source.
  1711. *
  1712. * @param string $module
  1713. * @param string $source
  1714. * @access public
  1715. * @return string
  1716. */
  1717. public function generateDemoDataPrompt($module, $source)
  1718. {
  1719. if(empty($this->lang->ai->demoData->$module)) return $this->lang->ai->demoData->notExist;
  1720. $sources = explode(',', $source);
  1721. $sources = array_filter($sources);
  1722. if(empty($sources)) return '';
  1723. foreach($sources as $index => $source)
  1724. {
  1725. $sources[$index] = explode('.', $source);
  1726. }
  1727. $data = array();
  1728. foreach($sources as $source)
  1729. {
  1730. $objectName = $source[0];
  1731. $objectKey = $source[1];
  1732. if(empty($data[$objectName])) $data[$objectName] = array();
  1733. $demoData = $this->lang->ai->demoData->$module[$objectName];
  1734. if(static::isAssoc($demoData))
  1735. {
  1736. $data[$objectName][$objectKey] = $demoData[$objectKey];
  1737. }
  1738. else
  1739. {
  1740. foreach($demoData as $index => $value)
  1741. {
  1742. if(empty($data[$objectName][$index])) $data[$objectName][$index] = array();
  1743. $data[$objectName][$index][$objectKey] = $value[$objectKey];
  1744. }
  1745. }
  1746. }
  1747. return $this->serializeDataToPrompt($module, $sources, $data);
  1748. }
  1749. /**
  1750. * Determines if an array is associative.
  1751. *
  1752. * @param array $array
  1753. * @access private
  1754. * @return bool
  1755. */
  1756. private static function isAssoc($array)
  1757. {
  1758. $keys = array_keys($array);
  1759. return array_keys($keys) !== $keys;
  1760. }
  1761. /**
  1762. * Get function call schema for form.
  1763. *
  1764. * @param string $form
  1765. * @access public
  1766. * @return array
  1767. */
  1768. public function getFunctionCallSchema($form)
  1769. {
  1770. if($form == 'empty.empty') return $form;
  1771. $formPath = explode('.', $form);
  1772. if(count($formPath) !== 2) return array();
  1773. $targetForm = $this->config->ai->targetForm[$formPath[0]][$formPath[1]];
  1774. if(empty($targetForm)) return array();
  1775. $schema = $this->lang->ai->formSchema[strtolower($targetForm->m)][strtolower($targetForm->f)];
  1776. return empty($schema) ? array() : $schema;
  1777. }
  1778. /**
  1779. * Get object data for prompt by id.
  1780. *
  1781. * @param object $prompt prompt object
  1782. * @param int $objectId object id
  1783. * @access public
  1784. * @return array|false array of object data and object, or false if error.
  1785. */
  1786. public function getObjectForPromptById($prompt, $objectId)
  1787. {
  1788. $module = $prompt->module;
  1789. $sources = array_filter(explode(',', $prompt->source));
  1790. /* Explode into grouped sources list. */
  1791. $sourceGroups = array();
  1792. foreach($sources as $source)
  1793. {
  1794. $source = explode('.', $source);
  1795. $objectName = $source[0];
  1796. $objectKey = $source[1];
  1797. if(empty($sourceGroups[$objectName])) $sourceGroups[$objectName] = array();
  1798. $sourceGroups[$objectName][] = $objectKey;
  1799. }
  1800. $object = new stdclass();
  1801. $objectData = new stdclass();
  1802. /* Query necessary object data from zentao. */
  1803. switch ($module)
  1804. {
  1805. case 'story':
  1806. if(isset($sourceGroups['story'])) $object->story = $this->loadModel('story')->getById($objectId);
  1807. break;
  1808. case 'execution':
  1809. if(isset($sourceGroups['execution'])) $object->execution = $this->loadModel('execution')->getByID($objectId);
  1810. if(isset($sourceGroups['tasks'])) $object->tasks = array_values($this->loadModel('task')->getExecutionTasks($objectId));
  1811. break;
  1812. case 'product':
  1813. if(isset($sourceGroups['product'])) $object->product = $this->loadModel('product')->getById($objectId);
  1814. break;
  1815. case 'project':
  1816. if(isset($sourceGroups['project'])) $object->project = $this->loadModel('project')->getById($objectId);
  1817. if($object->project->model == 'waterfall' && isset($sourceGroups['programplans'])) $object->programplans = array_values($this->loadModel('execution')->getByProject($object->project->id));
  1818. if($object->project->model != 'waterfall' && isset($sourceGroups['executions'])) $object->executions = array_values($this->loadModel('execution')->getByProject($object->project->id));
  1819. break;
  1820. case 'release':
  1821. if(isset($sourceGroups['release'])) $object->release = $this->loadModel('release')->getById($objectId);
  1822. if(isset($sourceGroups['stories'])) $object->stories = array_values($this->loadModel('story')->getByList(array_filter(explode(',', $object->release->stories))));
  1823. if(isset($sourceGroups['bugs'])) $object->bugs = array_values($this->loadModel('bug')->getByIdList(array_filter(explode(',', $object->release->bugs))));
  1824. break;
  1825. case 'productplan':
  1826. if(isset($sourceGroups['productplan'])) $object->productplan = $this->loadModel('productplan')->getByID($objectId);
  1827. if(isset($sourceGroups['stories'])) $object->stories = array_values($this->loadModel('story')->getPlanStories($objectId));
  1828. if(isset($sourceGroups['bugs'])) $object->bugs = array_values($this->dao->select('*')->from(TABLE_BUG)->where('plan')->eq($objectId)->andWhere('deleted')->eq(0)->fetchAll('id', false));
  1829. break;
  1830. case 'task':
  1831. if(isset($sourceGroups['task'])) $object->task = $this->loadModel('task')->getById($objectId);
  1832. break;
  1833. case 'case':
  1834. if(isset($sourceGroups['case'])) $object->case = $this->loadModel('testcase')->getById($objectId);
  1835. if(isset($sourceGroups['steps'])) $object->steps = array_values($object->case->steps);
  1836. break;
  1837. case 'bug':
  1838. if(isset($sourceGroups['bug'])) $object->bug = $this->loadModel('bug')->getById($objectId);
  1839. break;
  1840. case 'doc':
  1841. if(isset($sourceGroups['doc'])) $object->doc = $this->loadModel('doc')->getById($objectId);
  1842. break;
  1843. case 'my':
  1844. /* TODO: add more later. */
  1845. break;
  1846. }
  1847. $objectVars = get_object_vars($object);
  1848. if(empty($objectVars)) return false;
  1849. /* Format data as per data source definitions. */
  1850. foreach($sourceGroups as $objectName => $objectKeys)
  1851. {
  1852. $objectData->$objectName = array();
  1853. foreach($objectKeys as $objectKey)
  1854. {
  1855. if(!isset($object->$objectName)) continue;
  1856. /* Check if is plain object, data might contain arrays. */
  1857. if(is_object($object->$objectName))
  1858. {
  1859. if(isset($object->$objectName->$objectKey)) $objectData->$objectName[$objectKey] = $object->$objectName->$objectKey;
  1860. }
  1861. elseif(is_array($object->$objectName))
  1862. {
  1863. foreach($object->$objectName as $idx => $obj)
  1864. {
  1865. if(isset($obj->$objectKey)) $objectData->$objectName[$idx][$objectKey] = $obj->$objectKey;
  1866. }
  1867. }
  1868. }
  1869. }
  1870. return array($objectData, $object);
  1871. }
  1872. /**
  1873. * Auto prepend newline to text if text has newline in the middle.
  1874. *
  1875. * @param string $text
  1876. * @access private
  1877. * @return string
  1878. */
  1879. private static function autoPrependNewline($text)
  1880. {
  1881. if(empty($text)) return '';
  1882. preg_match('/\n[^$]/', $text, $matches);
  1883. return empty($matches) ? $text : "\n$text";
  1884. }
  1885. /**
  1886. * Try to punctuate sentence if sentence is not ended with punctuation.
  1887. *
  1888. * @param string $sentence
  1889. * @param bool $newline
  1890. * @access private
  1891. * @return string
  1892. */
  1893. private static function tryPunctuate($sentence, $newline = false)
  1894. {
  1895. if(empty($sentence)) return '';
  1896. preg_match('/\p{P}$/u', $sentence, $matches);
  1897. if(empty($matches)) $sentence .= '.';
  1898. return $newline ? "$sentence\n" : $sentence;
  1899. }
  1900. /**
  1901. * Assemble prompt with prompt data.
  1902. *
  1903. * @param object $prompt
  1904. * @param string $dataPrompt
  1905. * @access public
  1906. * @return string
  1907. */
  1908. public static function assemblePrompt($prompt, $dataPrompt)
  1909. {
  1910. $wholePrompt = empty($dataPrompt) ? '' : "$dataPrompt\n";
  1911. $wholePrompt .= static::tryPunctuate($prompt->role);
  1912. $wholePrompt .= static::autoPrependNewline(static::tryPunctuate($prompt->characterization, true));
  1913. $wholePrompt .= static::autoPrependNewline(static::tryPunctuate($prompt->purpose));
  1914. $wholePrompt .= static::autoPrependNewline(static::tryPunctuate($prompt->elaboration, true));
  1915. return $wholePrompt;
  1916. }
  1917. /**
  1918. * Execute prompt on object.
  1919. *
  1920. * @param int|object $prompt prompt (or id) to execute.
  1921. * @param int|object $object object (or id) to execute prompt on.
  1922. * @access public
  1923. * @return string|int returns either JSON string or negative integer on error.
  1924. * @throws AIResponseException
  1925. */
  1926. public function executePrompt($prompt, $object)
  1927. {
  1928. if(is_numeric($prompt)) $prompt = $this->getPromptById($prompt);
  1929. if(empty($prompt)) return -1;
  1930. if(is_numeric($object)) $object = $this->getObjectForPromptById($prompt, $object);
  1931. if(empty($object)) return -2;
  1932. list($objectData) = $object;
  1933. $dataPrompt = $this->serializeDataToPrompt($prompt->module, $prompt->source, $objectData);
  1934. if(empty($dataPrompt)) return -3;
  1935. $role = static::tryPunctuate($prompt->role);
  1936. $role .= static::autoPrependNewline(static::tryPunctuate($prompt->characterization, true));
  1937. $schema = $this->getFunctionCallSchema($prompt->targetForm);
  1938. if(empty($schema)) return -5;
  1939. $this->useLanguageModel($prompt->model);
  1940. return array('role' => $role, 'schema' => $schema, 'dataPrompt' => $dataPrompt, 'name' => $prompt->name, 'purpose' => $prompt->purpose, 'status' => $prompt->status, 'targetForm' => $prompt->targetForm, 'promptID' => $prompt->id);
  1941. }
  1942. /**
  1943. * Check if prompt can be tested.
  1944. *
  1945. * @param object|int $prompt prompt object or prompt id
  1946. * @access public
  1947. * @return bool
  1948. */
  1949. public function isExecutable($prompt)
  1950. {
  1951. if(is_numeric($prompt)) $prompt = $this->getByID($prompt);
  1952. if(empty($prompt)) return false;
  1953. $executable = true;
  1954. $requiredFields = explode(',', $this->config->ai->testPrompt->requiredFields);
  1955. foreach($requiredFields as $field)
  1956. {
  1957. if(empty($prompt->$field) || $prompt->$field == ',,')
  1958. {
  1959. $executable = false;
  1960. break;
  1961. }
  1962. }
  1963. return $executable;
  1964. }
  1965. /**
  1966. * Get testing location for prompt, usually a link to object with max id and is accessable by user.
  1967. *
  1968. * @param object $prompt
  1969. * @access public
  1970. * @return string|false
  1971. */
  1972. public function getTestingLocation($prompt)
  1973. {
  1974. $module = $prompt->module;
  1975. if($module == 'my')
  1976. {
  1977. return helper::createLink('my', 'effort', "type=all");
  1978. }
  1979. elseif($module == 'product')
  1980. {
  1981. $objectId = $this->dao->select('max(id) as maxId')->from(TABLE_PRODUCT)
  1982. ->where('id')->in($this->app->user->view->products)
  1983. ->fetch('maxId');
  1984. }
  1985. elseif($module == 'productplan')
  1986. {
  1987. $objectId = $this->dao->select('max(id) as maxId')->from(TABLE_PRODUCTPLAN)
  1988. ->where('product')->in($this->app->user->view->products)
  1989. ->fetch('maxId');
  1990. }
  1991. elseif($module == 'release')
  1992. {
  1993. $objectId = $this->dao->select('max(id) as maxId')->from(TABLE_RELEASE)
  1994. ->where('project')->in($this->app->user->view->projects)
  1995. ->orWhere('product')->in($this->app->user->view->products)
  1996. ->fetch('maxId');
  1997. }
  1998. elseif($module == 'project')
  1999. {
  2000. /* programplan/create only exist in the waterfall model project. */
  2001. if(strpos($prompt->targetForm, 'programplan/create'))
  2002. {
  2003. $objectId = $this->dao->select('max(id) as maxId')->from(TABLE_PROJECT)
  2004. ->where('id')->in($this->app->user->view->projects)
  2005. ->andWhere('model')->eq('waterfall')
  2006. ->fetch('maxId');
  2007. }
  2008. else
  2009. {
  2010. $objectId = $this->dao->select('max(id) as maxId')->from(TABLE_PROJECT)
  2011. ->where('id')->in($this->app->user->view->projects)
  2012. ->fetch('maxId');
  2013. }
  2014. }
  2015. elseif($module == 'story')
  2016. {
  2017. $objectId = $this->dao->select('max(id) as maxId')->from(TABLE_STORY)
  2018. ->where('product')->in($this->app->user->view->products)
  2019. ->fetch('maxId');
  2020. }
  2021. elseif($module == 'execution')
  2022. {
  2023. $executionIds = array_map('intval', explode(',', $this->app->user->view->sprints));
  2024. $objectId = max($executionIds);
  2025. }
  2026. elseif($module == 'task')
  2027. {
  2028. $objectId = $this->dao->select('max(id) as maxId')->from(TABLE_TASK)
  2029. ->where('project')->in($this->app->user->view->projects)
  2030. ->fetch('maxId');
  2031. }
  2032. elseif($module == 'case')
  2033. {
  2034. $objectId = $this->dao->select('max(id) as maxId')->from(TABLE_CASE)
  2035. ->where('project')->in($this->app->user->view->projects)
  2036. ->orWhere('product')->in($this->app->user->view->products)
  2037. ->fetch('maxId');
  2038. }
  2039. elseif($module == 'bug')
  2040. {
  2041. $objectId = $this->dao->select('max(id) as maxId')->from(TABLE_BUG)
  2042. ->where('project')->in($this->app->user->view->projects)
  2043. ->orWhere('product')->in($this->app->user->view->products)
  2044. ->fetch('maxId');
  2045. }
  2046. elseif($module == 'doc')
  2047. {
  2048. $objectId = $this->dao->select('max(id) as maxId')->from(TABLE_DOC)
  2049. ->where('project')->in($this->app->user->view->projects)
  2050. ->orWhere('product')->in($this->app->user->view->products)
  2051. ->fetch('maxId');
  2052. if(empty($objectId))
  2053. {
  2054. $userDocLibs = $this->dao->select('id')->from(TABLE_DOCLIB)
  2055. ->where('type')->eq('mine')
  2056. ->andWhere('addedBy')->eq($this->app->user->account)
  2057. ->fetchPairs();
  2058. if(!empty($userDocLibs))
  2059. {
  2060. $objectId = $this->dao->select('max(id) as maxId')->from(TABLE_DOC)
  2061. ->where('lib')->in($userDocLibs)
  2062. ->fetch('maxId');
  2063. }
  2064. }
  2065. }
  2066. if(!empty($objectId)) return helper::createLink('ai', 'promptexecute', "promptId=$prompt->id&objectId=$objectId");
  2067. return false;
  2068. }
  2069. /**
  2070. * Get target form location of object for prompt.
  2071. *
  2072. * @param object|int $prompt prompt object or prompt id
  2073. * @param object $object
  2074. * @param array $linkArgs optional, link arguments, as defined in `->args` of items of `$config->ai->targetFormVars`, e.g. array('story' => 1). if not provided, will try to get from object.
  2075. * @access public
  2076. * @return array array(link|false, bool), link to target form, and whether php should stop execution and return link.
  2077. */
  2078. public function getTargetFormLocation($prompt, $object, $linkArgs = array())
  2079. {
  2080. if(is_numeric($prompt)) $prompt = $this->getByID($prompt);
  2081. if(empty($prompt)) return array(false, true);
  2082. $targetForm = $prompt->targetForm;
  2083. if(empty($targetForm)) return array(false, true);
  2084. if($targetForm === 'empty.empty') return array(false, false);
  2085. list($m, $f) = explode('.', $targetForm);
  2086. $targetFormConfig = $this->config->ai->targetForm[$m][$f];
  2087. $module = strtolower($targetFormConfig->m);
  2088. $method = strtolower($targetFormConfig->f);
  2089. if($targetForm == 'doc.create')
  2090. {
  2091. $objectType = '';
  2092. $params = '';
  2093. $objectData = $object->{$prompt->module};
  2094. if(in_array($prompt->module, array('product', 'story', 'productplan', 'release', 'case', 'bug')))
  2095. {
  2096. $objectType = 'product';
  2097. $productID = $prompt->module == 'product' ? $objectData->id : $objectData->product;
  2098. $params = helper::safe64Encode("objectID=$productID");
  2099. }
  2100. if($prompt->module == 'project')
  2101. {
  2102. $objectType = 'project';
  2103. $params = helper::safe64Encode("objectID={$objectData->id}");
  2104. }
  2105. if($prompt->module == 'execution' || $prompt->module == 'task')
  2106. {
  2107. $objectType = 'project';
  2108. $executionID = $prompt->module == 'execution' ? $objectData->id : $objectData->execution;
  2109. $params = helper::safe64Encode("&objectID=$objectData->project&executionID=$executionID");
  2110. }
  2111. if($prompt->module == 'doc') $objectType = 'mine';
  2112. return array(helper::createLink($module, $method, "objectType=$objectType&params=$params&from=ai") . '#open-modal?width=300', false);
  2113. }
  2114. /* Try to assemble link vars from both passed-in `$linkArgs` and object props. */
  2115. $varsConfig = isset($this->config->ai->targetFormVars[$m][$f]) ? $this->config->ai->targetFormVars[$m][$f] : $this->config->ai->targetFormVars[$module][$method];
  2116. $vars = array();
  2117. foreach($varsConfig->args as $arg => $isRequired)
  2118. {
  2119. $var = '';
  2120. if(!empty($linkArgs[$arg])) // Use provided link args.
  2121. {
  2122. $var = $linkArgs[$arg];
  2123. }
  2124. elseif(!empty($object->$arg) && is_object($object->$arg) && !empty($object->$arg->id)) // If the corresponding object exists, use its id.
  2125. {
  2126. $var = $object->$arg->id;
  2127. }
  2128. elseif(!empty($object->{$prompt->module}->$arg)) // If object has the prop, use it.
  2129. {
  2130. $var = $object->{$prompt->module}->$arg;
  2131. }
  2132. else
  2133. {
  2134. /* Try to get a related object, we are sorry if it could not find any. */
  2135. $relatedObj = $this->tryGetRelatedObjects($prompt, $object, array($arg));
  2136. if(!empty($relatedObj))
  2137. {
  2138. $relatedObj = current($relatedObj);
  2139. if(is_string($relatedObj) && strlen($relatedObj) && $relatedObj[0] === ',' && $relatedObj[strlen($relatedObj) - 1] === ',')
  2140. {
  2141. $relatedObj = explode(',', $relatedObj);
  2142. $relatedObj = array_filter($relatedObj);
  2143. $relatedObj = current($relatedObj);
  2144. }
  2145. $var = $relatedObj;
  2146. }
  2147. }
  2148. if(!empty($isRequired) && empty($var)) return array(helper::createLink('ai', 'promptExecutionReset', 'failed=1'), true);
  2149. $vars[] = $var;
  2150. }
  2151. $linkVars = vsprintf($varsConfig->format, $vars);
  2152. /* Overrides for stories. */
  2153. if($module == 'story' && $method == 'change' && !empty($object->story) && $object->story->status == 'draft') $method = 'edit';
  2154. if($module == 'story' && $method == 'change' && !empty($object->story) && $object->story->type == 'epic') $module = 'epic';
  2155. return array(helper::createLink($module, $method, $linkVars) . (empty($varsConfig->app) ? '' : "#app=$varsConfig->app"), false);
  2156. }
  2157. /**
  2158. * Get related objects for prompt and object, using object's id.
  2159. *
  2160. * @param object|int $prompt prompt object or prompt id
  2161. * @param object|int $object object or object id
  2162. * @param array $objectNames object names to get, e.g. array('story', 'tasks'),
  2163. * values in: product, story, branch, productplan, execution, task, bug, case, project, doc
  2164. * @access public
  2165. * @return array|false returns array of required object values, or false if error.
  2166. */
  2167. public function tryGetRelatedObjects($prompt, $object, $objectNames = array())
  2168. {
  2169. if(empty($objectNames)) return array();
  2170. if(is_numeric($prompt)) $prompt = $this->getByID($prompt);
  2171. if(empty($prompt)) return false;
  2172. if(is_numeric($object)) $object = $this->getObjectForPromptById($prompt, $object);
  2173. if(empty($object)) return false;
  2174. $vars = array();
  2175. /* If a native object of a module exists, try getting stuff related to its id. */
  2176. if(!empty($object->{$prompt->module}) && is_object($object->{$prompt->module}) && !empty($object->{$prompt->module}->id))
  2177. {
  2178. $objectId = $object->{$prompt->module}->id;
  2179. foreach($objectNames as $objectName)
  2180. {
  2181. /* Note that modules are within (product, story, productplan, release, project, execution, task, bug, case, doc). */
  2182. switch ($prompt->module)
  2183. {
  2184. case 'product': // story, branch, productplan, execution, task, bug, case, project, doc
  2185. if(in_array($objectName, array('story', 'branch', 'productplan', 'task', 'bug', 'case', 'doc')))
  2186. {
  2187. $vars[] = $this->dao->select('max(id) as maxId')->from(constant(strtoupper("TABLE_$objectName")))
  2188. ->where('product')->eq($objectId)
  2189. ->andWhere('deleted')->eq(0)
  2190. ->fetch('maxId');
  2191. }
  2192. elseif($objectName == 'project')
  2193. {
  2194. $vars[] = $this->dao->select('max(tpp.project) as maxId')->from(TABLE_PROJECTPRODUCT)->alias('tpp')
  2195. ->leftJoin(TABLE_PROJECT)->alias('tp')->on('tp.id = tpp.project')
  2196. ->where('tpp.product')->eq($objectId)
  2197. ->andWhere('tp.type')->eq('project')
  2198. ->andWhere('tp.deleted')->eq(0)
  2199. ->fetch('maxId');
  2200. }
  2201. elseif($objectName == 'execution')
  2202. {
  2203. $vars[] = $this->dao->select('max(tpp.project) as maxId')->from(TABLE_PROJECTPRODUCT)->alias('tpp')
  2204. ->leftJoin(TABLE_PROJECT)->alias('tp')->on('tp.id = tpp.project')
  2205. ->where('tpp.product')->eq($objectId)
  2206. ->andWhere('tp.project')->ne(0)
  2207. ->andWhere('tp.deleted')->eq(0)
  2208. ->fetch('maxId');
  2209. }
  2210. break;
  2211. case 'story': // product, branch, productplan, execution, task, bug, case, project, doc
  2212. if($objectName == 'product') $vars[] = $this->dao->select('product')->from(TABLE_STORY)->where('id')->eq($objectId)->fetch('product');
  2213. if($objectName == 'branch') $vars[] = $this->dao->select('branch')->from(TABLE_STORY)->where('id')->eq($objectId)->fetch('branch');
  2214. if($objectName == 'productplan') $vars[] = $this->dao->select('plan')->from(TABLE_STORY)->where('id')->eq($objectId)->fetch('plan');
  2215. if(in_array($objectName, array('task', 'bug', 'case', 'doc')))
  2216. {
  2217. $vars[] = $this->dao->select('max(id) as maxId')->from(constant(strtoupper("TABLE_$objectName")))->where('story')->eq($objectId)->andWhere('deleted')->eq(0)->fetch('maxId');
  2218. }
  2219. if($objectName == 'execution')
  2220. {
  2221. $vars[] = $this->dao->select('tps.project')->from(TABLE_PROJECTSTORY)->alias('tps')
  2222. ->leftJoin(TABLE_PROJECT)->alias('tp')->on('tp.id = tps.project')
  2223. ->where('tps.story')->eq($objectId)
  2224. ->andWhere('tp.project')->ne(0)
  2225. ->andWhere('tp.deleted')->eq(0)
  2226. ->fetch('project');
  2227. }
  2228. if($objectName == 'project')
  2229. {
  2230. $vars[] = $this->dao->select('tps.project')->from(TABLE_PROJECTSTORY)->alias('tps')
  2231. ->leftJoin(TABLE_PROJECT)->alias('tp')->on('tp.id = tps.project')
  2232. ->where('tps.story')->eq($objectId)
  2233. ->andWhere('tp.type')->eq('project')
  2234. ->andWhere('tp.deleted')->eq(0)
  2235. ->fetch('project');
  2236. }
  2237. break;
  2238. case 'productplan': // product, story, branch, execution, task, bug, case, project, doc
  2239. if($objectName == 'product') $vars[] = $this->dao->select('product')->from(TABLE_PRODUCTPLAN)->where('id')->eq($objectId)->fetch('product');
  2240. if($objectName == 'branch') $vars[] = $this->dao->select('branch')->from(TABLE_PRODUCTPLAN)->where('id')->eq($objectId)->fetch('branch');
  2241. if(in_array($objectName, array('story', 'bug')))
  2242. {
  2243. $vars[] = $this->dao->select('max(id) as maxId')->from(constant(strtoupper("TABLE_$objectName")))->where('plan')->eq($objectId)->andWhere('deleted')->eq(0)->fetch('maxId');
  2244. }
  2245. if($objectName == 'execution')
  2246. {
  2247. $vars[] = $this->dao->select('max(tpp.project) as maxId')->from(TABLE_PROJECTPRODUCT)->alias('tpp')
  2248. ->leftJoin(TABLE_PROJECT)->alias('tp')->on('tp.id = tpp.project')
  2249. ->where('tpp.plan')->eq($objectId)
  2250. ->andWhere('tp.project')->ne(0)
  2251. ->andWhere('tp.deleted')->eq(0)
  2252. ->fetch('maxId');
  2253. }
  2254. if($objectName == 'project')
  2255. {
  2256. $vars[] = $this->dao->select('max(tpp.project) as maxId')->from(TABLE_PROJECTPRODUCT)->alias('tpp')
  2257. ->leftJoin(TABLE_PROJECT)->alias('tp')->on('tp.id = tpp.project')
  2258. ->where('tpp.plan')->eq($objectId)
  2259. ->andWhere('tp.type')->eq('project')
  2260. ->andWhere('tp.deleted')->eq(0)
  2261. ->fetch('maxId');
  2262. }
  2263. if($objectName == 'task') $vars[] = '';
  2264. if($objectName == 'case') $vars[] = '';
  2265. if($objectName == 'doc') $vars[] = '';
  2266. break;
  2267. case 'release': // product, story, branch, productplan, execution, task, bug, case, project, doc
  2268. if(in_array($objectName, array('product', 'branch', 'project')))
  2269. {
  2270. $this->dao->select($objectName)->from(TABLE_RELEASE)->where('id')->eq($objectId)->fetch($objectName);
  2271. }
  2272. if($objectName == 'story')
  2273. {
  2274. $stories = $this->dao->select('stories')->from(TABLE_RELEASE)->where('id')->eq($objectId)->fetch('stories');
  2275. $stories = explode(',', $stories);
  2276. $vars[] = empty($stories) ? '' : max($stories);
  2277. }
  2278. if($objectName == 'productplan')
  2279. {
  2280. $execution = $this->dao->select('tb.execution')->from(TABLE_BUILD)->alias('tb')
  2281. ->leftJoin(TABLE_RELEASE)->alias('tr')->on('tr.build = tb.id')
  2282. ->where('tr.id')->eq($objectId)
  2283. ->fetch('tb.execution');
  2284. $vars[] = empty($execution) ? '' : $this->dao->select('max(tpp.plan) as maxId')->from(TABLE_PROJECTPRODUCT)->alias('tpp')
  2285. ->leftJoin(TABLE_PROJECT)->alias('tp')->on('tp.id = tpp.project')
  2286. ->where('tp.id')->eq($execution)
  2287. ->andWhere('tp.deleted')->eq(0)
  2288. ->fetch('maxId');
  2289. }
  2290. if($objectName == 'execution')
  2291. {
  2292. $vars[] = $this->dao->select('tb.execution')->from(TABLE_BUILD)->alias('tb')
  2293. ->leftJoin(TABLE_RELEASE)->alias('tr')->on('tr.build = tb.id')
  2294. ->where('tr.id')->eq($objectId)
  2295. ->fetch('execution');
  2296. }
  2297. if($objectName == 'bug')
  2298. {
  2299. $bugs = $this->dao->select('bugs')->from(TABLE_RELEASE)->where('id')->eq($objectId)->fetch('bugs');
  2300. $bugs = explode(',', $bugs);
  2301. $vars[] = empty($bugs) ? '' : max($bugs);
  2302. }
  2303. if($objectName == 'task') $vars[] = '';
  2304. if($objectName == 'case') $vars[] = '';
  2305. if($objectName == 'doc') $vars[] = '';
  2306. break;
  2307. case 'project': // product, story, branch, productplan, execution, task, bug, case, doc
  2308. if(in_array($objectName, array('product', 'branch', 'productplan')))
  2309. {
  2310. if($objectName == 'productplan') $objectName = 'plan';
  2311. $vars[] = $this->dao->select($objectName)->from(TABLE_PROJECTPRODUCT)->where('project')->eq($objectId)->fetch($objectName);
  2312. }
  2313. if(in_array($objectName, array('execution', 'bug', 'task', 'case', 'doc')))
  2314. {
  2315. $vars[] = $this->dao->select('max(id) as maxId')->from(constant(strtoupper("TABLE_$objectName")))->where('project')->eq($objectId)->andWhere('deleted')->eq(0)->fetch('maxId');
  2316. }
  2317. if($objectName == 'story')
  2318. {
  2319. $vars[] = $this->dao->select('max(tps.story) as maxId')->from(TABLE_PROJECTSTORY)->alias('tps')
  2320. ->leftJoin(TABLE_STORY)->alias('ts')->on('ts.id = tps.story')
  2321. ->where('tps.project')->eq($objectId)
  2322. ->andWhere('ts.deleted')->eq(0)
  2323. ->fetch('maxId');
  2324. }
  2325. break;
  2326. case 'execution': // product, story, branch, productplan, task, bug, case, project, doc
  2327. if($objectName == 'project') $vars[] = $this->dao->select('project')->from(TABLE_EXECUTION)->where('id')->eq($objectId)->fetch('project');
  2328. if($objectName == 'product') $vars[] = $this->dao->select('product')->from(TABLE_PROJECTPRODUCT)->where('project')->eq($objectId)->fetch('product');
  2329. if($objectName == 'story') $vars[] = $this->dao->select('max(story) as maxId')->from(TABLE_PROJECTSTORY)->where('project')->eq($objectId)->fetch('maxId');
  2330. if($objectName == 'branch') $vars[] = $this->dao->select('max(branch) as maxId')->from(TABLE_PROJECTSTORY)->where('project')->eq($objectId)->fetch('maxId');
  2331. if($objectName == 'productplan') $vars[] = $this->dao->select('plan')->from(TABLE_PROJECTPRODUCT)->where('project')->eq($objectId)->fetch('plan');
  2332. if($objectName == 'task') $vars[] = $this->dao->select('max(id) as maxId')->from(TABLE_TASK)->where('execution')->eq($objectId)->andWhere('deleted')->eq(0)->fetch('maxId');
  2333. if($objectName == 'bug') $vars[] = $this->dao->select('max(id) as maxId')->from(TABLE_BUG)->where('project')->eq($objectId)->andWhere('deleted')->eq(0)->fetch('maxId');
  2334. if($objectName == 'case') $vars[] = $this->dao->select('max(id) as maxId')->from(TABLE_CASE)->where('project')->eq($objectId)->andWhere('deleted')->eq(0)->fetch('maxId');
  2335. if($objectName == 'doc') $vars[] = $this->dao->select('max(id) as maxId')->from(TABLE_DOC)->where('project')->eq($objectId)->andWhere('deleted')->eq(0)->fetch('maxId');
  2336. break;
  2337. case 'task': // product, story, branch, productplan, execution, bug, case, project, doc
  2338. if($objectName == 'product')
  2339. {
  2340. $vars[] = $this->dao->select('tpp.product')->from(TABLE_TASK)->alias('tt')
  2341. ->leftJoin(TABLE_PROJECTPRODUCT)->alias('tpp')->on('tpp.project = tt.project')
  2342. ->where('tt.id')->eq($objectId)
  2343. ->fetch('product');
  2344. }
  2345. if($objectName == 'story') $vars[] = $this->dao->select('story')->from(TABLE_TASK)->where('id')->eq($objectId)->fetch('story');
  2346. if($objectName == 'branch')
  2347. {
  2348. $vars[] = $this->dao->select('tpp.branch')->from(TABLE_TASK)->alias('tt')
  2349. ->leftJoin(TABLE_PROJECTPRODUCT)->alias('tpp')->on('tpp.project = tt.project')
  2350. ->where('tt.id')->eq($objectId)
  2351. ->fetch('branch');
  2352. }
  2353. if($objectName == 'productplan')
  2354. {
  2355. $vars[] = $this->dao->select('tpp.plan')->from(TABLE_TASK)->alias('tt')
  2356. ->leftJoin(TABLE_PROJECTPRODUCT)->alias('tpp')->on('tpp.project = tt.project')
  2357. ->where('tt.id')->eq($objectId)
  2358. ->fetch('plan');
  2359. }
  2360. if($objectName == 'execution') $vars[] = $this->dao->select('execution')->from(TABLE_TASK)->where('id')->eq($objectId)->fetch('execution');
  2361. if($objectName == 'bug') $vars[] = $this->dao->select('max(id) as maxId')->from(TABLE_BUG)->where('task')->eq($objectId)->andWhere('deleted')->eq(0)->fetch('maxId');
  2362. if($objectName == 'case') $vars[] = '';
  2363. if($objectName == 'doc') $vars[] = '';
  2364. break;
  2365. case 'bug': // product, story, branch, productplan, execution, task, case, project, doc
  2366. if(in_array($objectName, array('product', 'branch', 'productplan', 'execution', 'task', 'case', 'story', 'project')))
  2367. {
  2368. if($objectName == 'productplan') $objectName = 'plan';
  2369. $vars[] = $this->dao->select($objectName)->from(TABLE_BUG)->where('id')->eq($objectId)->fetch($objectName);
  2370. }
  2371. if($objectName == 'doc') $vars[] = '';
  2372. break;
  2373. case 'case': // product, story, branch, productplan, execution, task, bug, project, doc
  2374. if(in_array($objectName, array('product', 'story', 'branch', 'execution', 'project')))
  2375. {
  2376. $vars[] = $this->dao->select($objectName)->from(TABLE_CASE)->where('id')->eq($objectId)->fetch($objectName);
  2377. }
  2378. if($objectName == 'productplan')
  2379. {
  2380. $vars[] = $this->dao->select('tpp.plan')->from(TABLE_CASE)->alias('tc')
  2381. ->leftJoin(TABLE_PROJECTPRODUCT)->alias('tpp')->on('tpp.project = tc.project')
  2382. ->where('tc.id')->eq($objectId)
  2383. ->fetch('plan');
  2384. }
  2385. if($objectName == 'task') $vars[] = $this->dao->select('max(task) as maxId')->from(TABLE_BUG)->where('case')->eq($objectId)->andWhere('deleted')->eq(0)->fetch('maxId');
  2386. if($objectName == 'bug') $vars[] = $this->dao->select('max(id) as maxId')->from(TABLE_BUG)->where('case')->eq($objectId)->andWhere('deleted')->eq(0)->fetch('maxId');
  2387. if($objectName == 'doc') $vars[] = '';
  2388. break;
  2389. case 'doc': // product, story, branch, productplan, execution, task, bug, case, project
  2390. if(in_array($objectName, array('product', 'execution', 'project')))
  2391. {
  2392. $vars[] = $this->dao->select($objectName)->from(TABLE_DOC)->where('id')->eq($objectId)->fetch($objectName);
  2393. }
  2394. if($objectName == 'story')
  2395. {
  2396. $vars[] = $this->dao->select('max(id) as maxId')->from(TABLE_STORY)->alias('ts')
  2397. ->leftJoin(TABLE_DOC)->alias('td')->on('td.product = ts.product')
  2398. ->where('td.id')->eq($objectId)
  2399. ->andWhere('ts.deleted')->eq(0)
  2400. ->fetch('maxId');
  2401. }
  2402. if($objectName == 'branch')
  2403. {
  2404. $vars[] = $this->dao->select('branch')->from(TABLE_STORY)->alias('ts')
  2405. ->leftJoin(TABLE_DOC)->alias('td')->on('td.product = ts.product')
  2406. ->where('td.id')->eq($objectId)
  2407. ->andWhere('ts.deleted')->eq(0)
  2408. ->fetch('branch');
  2409. }
  2410. if($objectName == 'productplan')
  2411. {
  2412. $vars[] = $this->dao->select('plan')->from(TABLE_STORY)->alias('ts')
  2413. ->leftJoin(TABLE_DOC)->alias('td')->on('td.product = ts.product')
  2414. ->where('td.id')->eq($objectId)
  2415. ->andWhere('ts.deleted')->eq(0)
  2416. ->fetch('plan');
  2417. }
  2418. if($objectName == 'task')
  2419. {
  2420. $vars[] = $this->dao->select('max(id) as maxId')->from(TABLE_TASK)->alias('tt')
  2421. ->leftJoin(TABLE_DOC)->alias('td')->on('td.project = tt.project or td.execution = tt.execution')
  2422. ->where('td.id')->eq($objectId)
  2423. ->andWhere('tt.deleted')->eq(0)
  2424. ->fetch('maxId');
  2425. }
  2426. if($objectName == 'bug')
  2427. {
  2428. $vars[] = $this->dao->select('max(id) as maxId')->from(TABLE_BUG)->alias('tb')
  2429. ->leftJoin(TABLE_DOC)->alias('td')->on('td.product = tb.product')
  2430. ->where('td.id')->eq($objectId)
  2431. ->andWhere('tb.deleted')->eq(0)
  2432. ->fetch('maxId');
  2433. }
  2434. if($objectName == 'case')
  2435. {
  2436. $vars[] = $this->dao->select('max(id) as maxId')->from(TABLE_CASE)->alias('tc')
  2437. ->leftJoin(TABLE_DOC)->alias('td')->on('td.product = tc.product')
  2438. ->where('td.id')->eq($objectId)
  2439. ->andWhere('tc.deleted')->eq(0)
  2440. ->fetch('maxId');
  2441. }
  2442. break;
  2443. default:
  2444. $vars[] = '';
  2445. }
  2446. }
  2447. }
  2448. return $vars;
  2449. }
  2450. /**
  2451. * Get the last active step of prompt by id.
  2452. *
  2453. * @param object $prompt
  2454. * @access public
  2455. * @return string
  2456. */
  2457. public function getLastActiveStep($prompt)
  2458. {
  2459. if(!empty($prompt))
  2460. {
  2461. if($prompt->status == 'active') return 'finalize';
  2462. if(!empty($prompt->targetForm)) return 'settargetform';
  2463. if(!empty($prompt->purpose)) return 'setpurpose';
  2464. if(!empty($prompt->source)) return 'selectdatasource';
  2465. }
  2466. return 'assignrole';
  2467. }
  2468. /**
  2469. * Get prompts available for calling (which are either active or created by the user) of a module for user.
  2470. *
  2471. * @param string $module
  2472. * @access public
  2473. * @return array
  2474. */
  2475. public function getPromptsForUser($module)
  2476. {
  2477. return $this->dao->select('*')->from(TABLE_AI_AGENT)
  2478. ->where('deleted')->eq(0)
  2479. ->andWhere('module')->eq($module)
  2480. ->andWhere('status')->eq('active')
  2481. ->orderBy('id_desc')
  2482. ->fetchAll('id', false);
  2483. }
  2484. /**
  2485. * Filter prompts by user's privilege and executable state.
  2486. *
  2487. * @param array $prompts
  2488. * @param bool $keepUnauthorized optional, whether to keep unauthorized prompts but set their `unauthorized` property to true.
  2489. * @access public
  2490. * @return array filtered prompts, those unauthorized will be removed if `$keepUnauthorized` is false, unexecutable ones will always be removed.
  2491. */
  2492. public function filterPromptsForExecution($prompts, $keepUnauthorized = false)
  2493. {
  2494. if(empty($prompts)) return array();
  2495. /* Remove the unexecutable ones. */
  2496. $prompts = array_filter($prompts, array($this, 'isExecutable'));
  2497. /* Check user's priv to targetForm. */
  2498. foreach($prompts as $idx => $prompt)
  2499. {
  2500. list($m, $f) = explode('.', $prompt->targetForm);
  2501. if($m === 'empty' && $f === 'empty') continue;
  2502. $targetFormConfig = $this->config->ai->targetForm[$m][$f];
  2503. if(empty($targetFormConfig))
  2504. {
  2505. unset($prompts[$idx]);
  2506. continue;
  2507. }
  2508. if(!commonModel::hasPriv($targetFormConfig->m, $targetFormConfig->f))
  2509. {
  2510. if($keepUnauthorized)
  2511. {
  2512. $prompts[$idx]->unauthorized = true;
  2513. }
  2514. else
  2515. {
  2516. unset($prompts[$idx]);
  2517. }
  2518. }
  2519. }
  2520. return array_values($prompts);
  2521. }
  2522. /**
  2523. * 获取禅道智能体的测试数据。
  2524. * Get prompt test data.
  2525. *
  2526. * @param object $prompt
  2527. * @access public
  2528. * @return array
  2529. */
  2530. public function getTestPromptData($prompt)
  2531. {
  2532. $module = $prompt->module;
  2533. $promptSource = $prompt->source;
  2534. $source = explode(',', $promptSource);
  2535. $source = array_filter($source, function($value) {return !empty($value);});
  2536. $titleData = $this->lang->ai->dataSource[$module];
  2537. $testData = $this->lang->ai->prompts->testData[$module];
  2538. $categorized = array();
  2539. foreach($source as $value)
  2540. {
  2541. $prefix = explode('.', $value)[0];
  2542. $column = explode('.', $value)[1];
  2543. if(!isset($categorized[$prefix])) $categorized[$prefix] = [];
  2544. $categorized[$prefix][] = $column;
  2545. }
  2546. $result = '';
  2547. foreach($categorized as $groupKey => $pathInfo)
  2548. {
  2549. if(in_array($groupKey, array('programplans', 'executions', 'stories', 'bugs', 'tasks', 'steps')))
  2550. {
  2551. if($module == 'release' && $groupKey == 'bugs')
  2552. {
  2553. $result .= '##### ' . $titleData[$groupKey]['common'] . $this->lang->colon . "\n";
  2554. $result .= $testData[$groupKey]['title'] . "\n";
  2555. }
  2556. else
  2557. {
  2558. $result .= '##### ' . $titleData[$groupKey]['common'] . $this->lang->colon . "\n";
  2559. $result .= "| ";
  2560. foreach($pathInfo as $value) $result .= $titleData[$groupKey][$value] . " | ";
  2561. $result .= "\n";
  2562. $result .= "| ";
  2563. foreach($pathInfo as $value) $result .= "--- | ";
  2564. $result .= "\n";
  2565. $firstData = $pathInfo[0];
  2566. $count = count($testData[$groupKey][$firstData]);
  2567. for($i = 0; $i < $count; $i++)
  2568. {
  2569. $result .= "| ";
  2570. foreach($pathInfo as $value) $result .= $testData[$groupKey][$value][$i] . " | ";
  2571. $result .= "\n";
  2572. }
  2573. }
  2574. }
  2575. else
  2576. {
  2577. foreach($pathInfo as $value)
  2578. {
  2579. if($groupKey == 'task' && $value == 'story') continue;
  2580. $result .= '##### ' . $titleData[$groupKey][$value] . $this->lang->colon . "\n";
  2581. $result .= $testData[$groupKey][$value] . "\n";
  2582. }
  2583. }
  2584. }
  2585. return array($testData, $result);
  2586. }
  2587. /**
  2588. * Set inject data for a form. For how injection works, see view/inputinject.html.php file.
  2589. *
  2590. * @param string|array $form 'module.method' or array('module', 'method').
  2591. * @param string|object $data data to inject, object will be json encoded.
  2592. * @access public
  2593. * @return void
  2594. */
  2595. public function setInjectData($form, $data)
  2596. {
  2597. if(is_string($form)) $form = explode('.', $form);
  2598. $targetForm = $this->config->ai->targetForm[$form[0]][$form[1]];
  2599. if(empty($targetForm)) return;
  2600. /* Override method for story drafts. */
  2601. if($targetForm->m == 'story' && $targetForm->f == 'change') $_SESSION['aiInjectData']['story']['edit'] = is_string($data) ? $data : json_encode($data);
  2602. $_SESSION['aiInjectData'][$targetForm->m][$targetForm->f] = is_string($data) ? $data : json_encode($data);
  2603. }
  2604. /**
  2605. * Get role templates from db.
  2606. *
  2607. * @access public
  2608. * @return array
  2609. */
  2610. public function getRoleTemplates()
  2611. {
  2612. return $this->dao->select('*')->from(TABLE_AI_AGENTROLE)
  2613. ->where('deleted')->eq(0)
  2614. ->fetchAll('id', false);
  2615. }
  2616. /**
  2617. * add role template.
  2618. *
  2619. * @param string $role
  2620. * @param string $characterization
  2621. * @access public
  2622. * @return false|int
  2623. */
  2624. public function createRoleTemplate($role, $characterization)
  2625. {
  2626. $roleTemplate = new stdclass();
  2627. $roleTemplate->role = $role;
  2628. $roleTemplate->characterization = $characterization;
  2629. $this->dao->insert(TABLE_AI_AGENTROLE)
  2630. ->data($roleTemplate)
  2631. ->exec();
  2632. if(dao::isError()) return false;
  2633. return $this->dao->lastInsertID();
  2634. }
  2635. /**
  2636. * Delete a role template.
  2637. *
  2638. * @param int $id
  2639. * @access public
  2640. * @return false|int
  2641. */
  2642. public function deleteRoleTemplate($id)
  2643. {
  2644. $this->dao->update(TABLE_AI_AGENTROLE)
  2645. ->set('deleted')->eq(1)
  2646. ->where('id')->eq($id)
  2647. ->exec();
  2648. if(dao::isError()) return false;
  2649. return true;
  2650. }
  2651. /**
  2652. * Update a role template.
  2653. *
  2654. * @param string $role
  2655. * @param string $characterization
  2656. * @access public
  2657. * @return bool
  2658. */
  2659. public function updateRoleTemplate($id, $role, $characterization)
  2660. {
  2661. $roleTemplate = new stdclass();
  2662. $roleTemplate->role = $role;
  2663. $roleTemplate->characterization = $characterization;
  2664. $this->dao->update(TABLE_AI_AGENTROLE)
  2665. ->data($roleTemplate)
  2666. ->where('id')->eq($id)
  2667. ->exec();
  2668. if(dao::isError()) return false;
  2669. return true;
  2670. }
  2671. /**
  2672. * Get assistants.
  2673. * @param pager $pager
  2674. * @param string $orderBy
  2675. * @access public
  2676. * @return array
  2677. */
  2678. public function getAssistants($pager = null, $orderBy = 'id_desc')
  2679. {
  2680. return $this->dao->select('*')->from(TABLE_AI_ASSISTANT)
  2681. ->where('deleted')->eq('0')
  2682. ->orderBy($orderBy)
  2683. ->page($pager)
  2684. ->fetchAll('id', false);
  2685. }
  2686. public function getAssistantsByModel($modelId, $enabled = true)
  2687. {
  2688. return $this->dao->select('*')->from(TABLE_AI_ASSISTANT)
  2689. ->where('modelId')->eq($modelId)
  2690. ->andWhere('deleted')->eq('0')
  2691. ->andWhere('enabled')->eq($enabled ? '1' : '0')
  2692. ->fetchAll('id', false);
  2693. }
  2694. /**
  2695. * Get assistant by id.
  2696. * @param int $assistantId
  2697. * @access public
  2698. * @return object
  2699. */
  2700. public function getAssistantById($assistantId)
  2701. {
  2702. return $this->dao->select('*')->from(TABLE_AI_ASSISTANT)
  2703. ->where('id')->eq($assistantId)
  2704. ->fetch();
  2705. }
  2706. /**
  2707. * create an assistant.
  2708. * @param object $assistant
  2709. * @access public
  2710. * @return bool
  2711. */
  2712. public function createAssistant($assistant, $publish = false)
  2713. {
  2714. $assistant->createdDate = helper::now();
  2715. if($publish)
  2716. {
  2717. $assistant->publishedDate = helper::now();
  2718. $assistant->enabled = '1';
  2719. }
  2720. else
  2721. {
  2722. $assistant->enabled = '0';
  2723. }
  2724. $this->dao
  2725. ->insert(TABLE_AI_ASSISTANT)
  2726. ->data($assistant)
  2727. ->exec();
  2728. if (dao::isError()) return false;
  2729. $assistantId = $this->dao->lastInsertID();
  2730. $this->loadModel('action')->create('aiAssistant', $assistantId, 'created');
  2731. if($publish)
  2732. {
  2733. $this->loadModel('action')->create('aiAssistant', $assistantId, 'online');
  2734. }
  2735. return true;
  2736. }
  2737. /**
  2738. * update an assistant.
  2739. * @param object $assistant
  2740. * @access public
  2741. * @return bool
  2742. */
  2743. public function updateAssistant($assistant)
  2744. {
  2745. $originAssistant = $this->getAssistantById($assistant->id);
  2746. $actionType = 'edited';
  2747. if(!empty($originAssistant))
  2748. {
  2749. $changedFields = array();
  2750. foreach($assistant as $key => $value)
  2751. {
  2752. if($value != $originAssistant->$key) $changedFields[] = $key;
  2753. }
  2754. if(count($changedFields) == 1 && current($changedFields) == 'status')
  2755. {
  2756. $actionType = $assistant->enabled == '0' ? 'offline' : 'online';
  2757. }
  2758. else
  2759. {
  2760. $changes = commonModel::createChanges($originAssistant, $assistant);
  2761. }
  2762. }
  2763. foreach ($assistant as $key => $value)
  2764. {
  2765. if(isset($originAssistant->$key)) $originAssistant->$key = $value;
  2766. }
  2767. if(empty($originAssistant->publishedDate))
  2768. {
  2769. unset($originAssistant->publishedDate);
  2770. }
  2771. $this->dao->update(TABLE_AI_ASSISTANT)
  2772. ->data($originAssistant)
  2773. ->where('id')->eq($assistant->id)
  2774. ->exec();
  2775. if(dao::isError()) return false;
  2776. if(!empty($changes))
  2777. {
  2778. $actionId = $this->loadModel('action')->create('aiAssistant', $assistant->id, $actionType);
  2779. $this->action->logHistory($actionId, $changes);
  2780. }
  2781. return true;
  2782. }
  2783. /**
  2784. * Toggle an assistant.
  2785. * @param int $assistantId
  2786. * @param bool $enabled
  2787. * @access public
  2788. * @return bool
  2789. */
  2790. public function toggleAssistant($assistantId, $enabled)
  2791. {
  2792. $this->dao->update(TABLE_AI_ASSISTANT)
  2793. ->set('enabled')->eq($enabled ? '1' : '0')
  2794. ->set('publishedDate')->eq($enabled ? helper::now() : null)
  2795. ->where('id')->eq($assistantId)
  2796. ->exec();
  2797. if(dao::isError()) return false;
  2798. $actionType = $enabled ? 'online' : 'offline';
  2799. $this->loadModel('action')->create('aiAssistant', $assistantId, $actionType);
  2800. return true;
  2801. }
  2802. /**
  2803. * Find assistants name is duplicate.
  2804. * @param string $AssistantName
  2805. * @param int $modelId
  2806. * @access public
  2807. * @return object
  2808. */
  2809. public function checkAssistantDuplicate($AssistantName, $modelId)
  2810. {
  2811. return $this->dao->select('*')->from(TABLE_AI_ASSISTANT)
  2812. ->where('name')->eq($AssistantName)
  2813. ->andWhere('modelId')->eq($modelId)
  2814. ->andWhere('deleted')->eq('0')
  2815. ->fetch();
  2816. }
  2817. public function deleteAssistant($assistantId)
  2818. {
  2819. $this->dao->update(TABLE_AI_ASSISTANT)
  2820. ->set('deleted')->eq('1')
  2821. ->where('id')->eq($assistantId)
  2822. ->exec();
  2823. if(dao::isError()) return false;
  2824. $this->loadModel('action')->create('aiAssistant', $assistantId, 'deleted');
  2825. return true;
  2826. }
  2827. }
  2828. /**
  2829. * Exception class for response from AI.
  2830. */
  2831. class AIResponseException extends Exception
  2832. {
  2833. /**
  2834. * Type of response error, error messages are defined in lang files with type as key.
  2835. *
  2836. * See `$lang->ai->aiResponseException`.
  2837. *
  2838. * @var string
  2839. * @access public
  2840. */
  2841. public $type;
  2842. /**
  2843. * Response with error.
  2844. *
  2845. * @var string|object
  2846. * @access public
  2847. */
  2848. public $response;
  2849. /**
  2850. * Create a AIResponseException, load error message from lang file.
  2851. *
  2852. * @param string $type
  2853. * @param string|object $response
  2854. * @access public
  2855. * @return void
  2856. */
  2857. public function __construct($type, $response)
  2858. {
  2859. global $app;
  2860. $this->type = $type;
  2861. $this->response = $response;
  2862. $this->message = zget($app->lang->ai->aiResponseException, $type, '');
  2863. }
  2864. }