BxDolAiJudgeTypeSafe.php 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132
  1. <?php defined('BX_DOL') or die('hack attempt');
  2. /**
  3. * Copyright (c) UNA, Inc - https://una.io
  4. * MIT License - https://opensource.org/licenses/MIT
  5. *
  6. * @defgroup UnaCore UNA Core
  7. * @{
  8. */
  9. /**
  10. * TypeSafe.ai judge (Jev / "System One" models) - https://docs.typesafe.ai
  11. *
  12. * Model row: type = typesafe, capabilities = judge, model = jev-latest | jev-preview | jev-1.13.0,
  13. * key = API key, params JSON:
  14. * baseUri - API root, default https://api.typesafe.ai/v1
  15. * timeout - request timeout in seconds, default 15
  16. * retries - how many times to retry on 429 / 529 / network error, default 2
  17. *
  18. * Limits (as of Jev 1.13): 64k tokens per request, 32k for state + longest question,
  19. * choice up to 255 options, score 2..10 levels, text only.
  20. */
  21. class BxDolAiJudgeTypeSafe extends BxDolAiJudge
  22. {
  23. const BASE_URI = 'https://api.typesafe.ai/v1';
  24. const ENDPOINT = '/systemone';
  25. protected $_sBaseUri;
  26. protected $_iTimeout;
  27. protected $_iRetries;
  28. public function __construct(array $aModel, array $aParams)
  29. {
  30. parent::__construct($aModel, $aParams);
  31. $this->_sBaseUri = rtrim(!empty($aParams['baseUri']) ? $aParams['baseUri'] : self::BASE_URI, '/');
  32. $this->_iTimeout = (int)($aParams['timeout'] ?? 15);
  33. $this->_iRetries = (int)($aParams['retries'] ?? 2);
  34. }
  35. public function ask($mixedState, array $aQuestions): array
  36. {
  37. if (!$aQuestions)
  38. throw new Exception('No questions');
  39. foreach ($aQuestions as $sId => $aQuestion)
  40. $this->_validateQuestion($sId, $aQuestion);
  41. $aRequest = [
  42. 'state' => $mixedState,
  43. 'model' => $this->_aModel['model'],
  44. 'questions' => $aQuestions,
  45. ];
  46. $this->_aLastRequest = $aRequest;
  47. $this->_aLastAnswers = [];
  48. $aHeaders = [
  49. 'Authorization: Bearer ' . trim($this->_aModel['key']),
  50. 'Accept: application/json',
  51. ];
  52. $sUrl = $this->_sBaseUri . self::ENDPOINT;
  53. $iAttempt = 0;
  54. while (true) {
  55. $sHttpCode = 0;
  56. $sResponse = bx_file_get_contents($sUrl, $aRequest, 'post-json', $aHeaders, $sHttpCode, [], $this->_iTimeout);
  57. $iHttpCode = (int)$sHttpCode;
  58. if ($iHttpCode == 200 && $sResponse !== '' && $sResponse !== false)
  59. break;
  60. $bRetry = $iAttempt < $this->_iRetries && ($iHttpCode == 0 || $iHttpCode == 429 || $iHttpCode >= 500);
  61. if (!$bRetry)
  62. throw new Exception("TypeSafe API error: HTTP {$iHttpCode} " . $this->_errorFromResponse($sResponse));
  63. $iAttempt++;
  64. usleep(500000 * $iAttempt); // 0.5s, 1s, ...
  65. }
  66. $aResponse = json_decode($sResponse, true);
  67. if (!is_array($aResponse) || !isset($aResponse['answers']) || !is_array($aResponse['answers']))
  68. throw new Exception('TypeSafe API error: unexpected response ' . mb_substr((string)$sResponse, 0, 300));
  69. $this->_aLastUsage = isset($aResponse['usage']) && is_array($aResponse['usage']) ? $aResponse['usage'] : [];
  70. $this->_sLastModel = (string)($aResponse['model'] ?? '');
  71. $this->_aLastAnswers = $aResponse['answers'];
  72. return $aResponse['answers'];
  73. }
  74. protected function _validateQuestion($sId, $aQuestion)
  75. {
  76. if (!is_array($aQuestion) || empty($aQuestion['type']) || !isset($aQuestion['instructions']))
  77. throw new Exception("Question '{$sId}' is malformed, use BxDolAiJudge::noul/choice/score");
  78. switch ($aQuestion['type']) {
  79. case self::Q_NOUL:
  80. break;
  81. case self::Q_CHOICE:
  82. $iOptions = isset($aQuestion['criteria']) && is_array($aQuestion['criteria']) ? count($aQuestion['criteria']) : 0;
  83. if ($iOptions < 2 || $iOptions > 255)
  84. throw new Exception("Question '{$sId}': choice needs 2..255 options");
  85. break;
  86. case self::Q_SCORE:
  87. $iLevels = isset($aQuestion['criteria']) && is_array($aQuestion['criteria']) ? count($aQuestion['criteria']) : 0;
  88. if ($iLevels < 2 || $iLevels > 10)
  89. throw new Exception("Question '{$sId}': score needs 2..10 levels");
  90. break;
  91. default:
  92. throw new Exception("Question '{$sId}': unknown type {$aQuestion['type']}");
  93. }
  94. }
  95. protected function _errorFromResponse($sResponse): string
  96. {
  97. if (!$sResponse)
  98. return '(empty response)';
  99. $a = json_decode($sResponse, true);
  100. if (is_array($a)) {
  101. if (!empty($a['error']))
  102. return is_array($a['error']) ? json_encode($a['error']) : (string)$a['error'];
  103. if (!empty($a['detail']))
  104. return is_array($a['detail']) ? json_encode($a['detail']) : (string)$a['detail'];
  105. if (!empty($a['message']))
  106. return (string)$a['message'];
  107. }
  108. return mb_substr((string)$sResponse, 0, 300);
  109. }
  110. }
  111. /** @} */