303 lines
12 KiB
PHP
303 lines
12 KiB
PHP
<?php
|
||
/* =================================================================================
|
||
* License: GPL-2.0 license
|
||
* Author: 众产® https://ciy.cn/code
|
||
* Version: 0.1.0
|
||
====================================================================================*/
|
||
|
||
/*
|
||
$openai = new openai(''); //初始化
|
||
$openai->newsystem(''); //定义系统角色,新对话
|
||
$openai->completion(''); //发起对话
|
||
*/
|
||
|
||
namespace ciy;
|
||
|
||
class openai {
|
||
public $id; //扩展数据1
|
||
public $obj; //扩展数据2
|
||
private $aicfg = array();
|
||
public $messages = array();
|
||
private $tt = -999; //0 2
|
||
private $tp = -999; //0 2
|
||
private $fp = -999; //-2 2
|
||
private $pp = -999; //-2 2
|
||
private $debug = false;
|
||
|
||
public function __construct($ai) {
|
||
if (is_array($ai))
|
||
$this->aicfg = $ai;
|
||
}
|
||
public function debug($debug = true) {
|
||
$this->debug = $debug;
|
||
}
|
||
public function setparam($spstr) {
|
||
$sp = getstrparam($spstr, ',');
|
||
if (isset($sp['tt']))
|
||
$this->tt = (float)$sp['tt'];
|
||
if (isset($sp['tp']))
|
||
$this->tp = (float)$sp['tp'];
|
||
if (isset($sp['fp']))
|
||
$this->fp = (float)$sp['fp'];
|
||
if (isset($sp['pp']))
|
||
$this->pp = (float)$sp['pp'];
|
||
}
|
||
public function newsystem($msg = null) {
|
||
$this->messages = array();
|
||
if (!empty($msg))
|
||
$this->messages[] = array('role' => 'system', 'content' => $msg);
|
||
}
|
||
public function completion($prompt, $isjson = false, $funcdatarows = null, $toolcb = null) {
|
||
$this->messages[] = array('role' => 'user', 'content' => $prompt);
|
||
$tools = null;
|
||
if (is_array($funcdatarows)) {
|
||
$tools = array();
|
||
foreach ($funcdatarows as $funcdatarow) {
|
||
$fparam = array();
|
||
$paramjson = $funcdatarow['paramjson'];
|
||
if ($paramjson[0] == '{')
|
||
$fparam = json_decode($paramjson, true);
|
||
else {
|
||
$fparam = array();
|
||
$fparam['type'] = 'object';
|
||
$fparam['properties'] = array();
|
||
$fparam['required'] = array();
|
||
$paramjsons = getstrparam($paramjson, "\n");
|
||
foreach ($paramjsons as $key => $val) {
|
||
$require = false;
|
||
if ($key[0] == '*') {
|
||
$key = substr($key, 1);
|
||
$require = true;
|
||
}
|
||
$fparam['properties'][$key] = array();
|
||
$fparam['properties'][$key]['type'] = 'string';
|
||
$fparam['properties'][$key]['description'] = $val;
|
||
if ($require)
|
||
$fparam['required'][] = $key;
|
||
}
|
||
}
|
||
$tool = array();
|
||
$tool['type'] = 'function';
|
||
$tool['function'] = array();
|
||
$tool['function']['name'] = 'F' . $funcdatarow['id'];
|
||
$tool['function']['description'] = $funcdatarow['descs'];
|
||
$tool['function']['parameters'] = $fparam;
|
||
$tools[] = $tool;
|
||
}
|
||
}
|
||
$resultcontent = '';
|
||
while (true) {
|
||
$data = array();
|
||
$data['model'] = $this->aicfg['model'];
|
||
$data['messages'] = $this->messages;
|
||
if ($tools)
|
||
$data['tools'] = $tools;
|
||
if ($this->aicfg['maxtoken'] > 0)
|
||
$data['max_tokens'] = toint($this->aicfg['maxtoken']);
|
||
if ($this->fp > -999)
|
||
$data['frequency_penalty'] = $this->fp;
|
||
if ($this->pp > -999)
|
||
$data['presence_penalty'] = $this->pp;
|
||
if ($this->tt > -999)
|
||
$data['temperature'] = $this->tt;
|
||
if ($this->tp > -999)
|
||
$data['top_p'] = $this->tp;
|
||
|
||
if ($isjson)
|
||
$data['response_format'] = array('type' => 'json_object');
|
||
if ($this->debug) {
|
||
savelogfile('openai', $this->aicfg['baseurl']);
|
||
savelogfile('openai', json_encode($data, JSON_UNESCAPED_UNICODE));
|
||
}
|
||
$ch = curl_init();
|
||
curl_setopt($ch, CURLOPT_URL, $this->aicfg['baseurl'] . '/chat/completions');
|
||
curl_setopt($ch, CURLOPT_POST, true);
|
||
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
|
||
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||
'Content-Type: application/json',
|
||
'Authorization: Bearer ' . $this->aicfg['aikey']
|
||
]);
|
||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
|
||
$response = curl_exec($ch);
|
||
if ($this->debug) {
|
||
savelogfile('openai', '--------------------------------');
|
||
savelogfile('openai', $response);
|
||
savelogfile('openai', '');
|
||
}
|
||
curl_close($ch);
|
||
if ($response === false) {
|
||
curl_close($ch);
|
||
return 'ERR: ' . curl_error($ch) . ' (' . curl_errno($ch) . ')';
|
||
}
|
||
$json = json_decode($response, true);
|
||
if (isset($json['error_msg']))
|
||
return 'ERR: ' . $json['error_msg'];
|
||
if (isset($json['error']))
|
||
return 'ERR: ' . $json['error']['message'];
|
||
if (!isset($json['choices']))
|
||
return 'ERR: no choices.' . $response;
|
||
$finish = $json['choices'][0]['finish_reason'];
|
||
$message = $json['choices'][0]['message'];
|
||
$this->messages[] = $message;
|
||
if ($finish == 'tool_calls') {
|
||
foreach ($message['tool_calls'] as $tool_call) {
|
||
$result = $toolcb($tool_call['function']);
|
||
if (!is_string($result))
|
||
$result = json_encode($result, JSON_UNESCAPED_UNICODE);
|
||
else if (substr($result, 0, 3) == 'ERR')
|
||
return $result;
|
||
$this->messages[] = array(
|
||
"tool_call_id" => $tool_call['id'],
|
||
"role" => "tool",
|
||
"name" => $tool_call['function']['name'],
|
||
"content" => $result
|
||
);
|
||
}
|
||
continue;
|
||
}
|
||
if ($finish == 'length') {
|
||
if ($isjson) {
|
||
$message['content'] = self::fixjson($message['content']);
|
||
|
||
//以下暂时AI能力有限
|
||
// $context = mb_substr($content, -200, 200);
|
||
// $last_char = mb_substr($context, -1);
|
||
// $expected = '';
|
||
// switch ($last_char) {
|
||
// case '{': $expected = '对象未闭合'; break;
|
||
// case '[': $expected = '数组未闭合'; break;
|
||
// case ':': $expected = '值未完成'; break;
|
||
// case ',': $expected = '需要下一个元素'; break;
|
||
// }
|
||
|
||
// $tmp = <<<data
|
||
// 请严格作为JSON生成器继续输出,必须遵循以下规则:
|
||
// 1. 仅续写以下JSON片段,绝对不要重复已有内容
|
||
// 2. 当前状态:{$expected},最后字符是"{$last_char}"
|
||
// 3. 保持语法正确,自动闭合未完成的字符串/括号
|
||
// 4. 如果原JSON是对象,继续添加属性;如果是数组,继续添加元素
|
||
|
||
// 当前JSON片段(仅作上下文参考,不要重复):
|
||
// {$context}
|
||
|
||
// 请直接输出JSON续写内容(不要包含上下文片段):
|
||
// data;
|
||
// $this->messages[] = array('role' => 'user', 'content' => $tmp);
|
||
} else {
|
||
$resultcontent .= $message['content'];
|
||
$this->messages[] = array('role' => 'user', 'content' => '请继续完成之前的对话');
|
||
continue;
|
||
}
|
||
}
|
||
|
||
if (!$isjson) {
|
||
$resultcontent .= $message['content'];
|
||
$message['finish_reason'] = $finish;
|
||
$message['content'] = $resultcontent;
|
||
return $message;
|
||
}
|
||
$content = $message['content'];
|
||
$ind = strpos($content, '```json');
|
||
if ($ind !== false) {
|
||
$ind2 = strpos($content, '```', $ind + 7);
|
||
if ($ind2 !== false)
|
||
$content = substr($content, $ind + 7, $ind2 - $ind - 7);
|
||
}
|
||
$resultcontent .= $content;
|
||
$json = json_decode($resultcontent, true);
|
||
if ($json === null)
|
||
return 'ERR: json decode error.' . $resultcontent;
|
||
return $json;
|
||
}
|
||
}
|
||
public function chat($messages, $cb) {
|
||
$this->messages = $messages;
|
||
$data = [
|
||
'model' => $this->aicfg['model'],
|
||
'messages' => $this->messages,
|
||
'stream' => true,
|
||
];
|
||
if ($this->aicfg['maxtoken'] > 0)
|
||
$data['max_tokens'] = toint($this->aicfg['maxtoken']);
|
||
if ($this->fp > -999)
|
||
$data['frequency_penalty'] = $this->fp;
|
||
if ($this->pp > -999)
|
||
$data['presence_penalty'] = $this->pp;
|
||
if ($this->tt > -999)
|
||
$data['temperature'] = $this->tt;
|
||
if ($this->tp > -999)
|
||
$data['top_p'] = $this->tp;
|
||
if ($this->debug) {
|
||
savelogfile('openai', $this->aicfg['baseurl']);
|
||
savelogfile('openai', json_encode($data, JSON_UNESCAPED_UNICODE));
|
||
}
|
||
$ch = curl_init();
|
||
curl_setopt($ch, CURLOPT_URL, $this->aicfg['baseurl'] . '/chat/completions');
|
||
curl_setopt($ch, CURLOPT_POST, true);
|
||
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
|
||
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||
'Content-Type: application/json',
|
||
'Authorization: Bearer ' . $this->aicfg['aikey']
|
||
]);
|
||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
|
||
curl_setopt($ch, CURLOPT_WRITEFUNCTION, function ($ch, $data) use ($cb) {
|
||
if ($this->debug) {
|
||
savelogfile('openai', substr($data, 6, -2));
|
||
}
|
||
$cb($data);
|
||
return strlen($data);
|
||
});
|
||
curl_exec($ch);
|
||
if (curl_errno($ch))
|
||
return '错误: ' . curl_error($ch);
|
||
curl_close($ch);
|
||
return true;
|
||
}
|
||
function fixjson($jsonstr) {
|
||
$fixed = $jsonstr;
|
||
$inString = false;
|
||
$escaped = false;
|
||
for ($i = 0; $i < strlen($fixed); $i++) {
|
||
$char = $fixed[$i];
|
||
if (!$escaped && $char === '"') {
|
||
$inString = !$inString;
|
||
}
|
||
$escaped = ($char === '\\' && !$escaped);
|
||
}
|
||
if ($inString) {
|
||
$fixed .= '"';
|
||
}
|
||
$stack = [];
|
||
$inString = false;
|
||
$escaped = false;
|
||
for ($i = 0; $i < strlen($fixed); $i++) {
|
||
$char = $fixed[$i];
|
||
if (!$inString) {
|
||
if ($char === '{') {
|
||
array_push($stack, '}');
|
||
} elseif ($char === '[') {
|
||
array_push($stack, ']');
|
||
} elseif ($char === '}' || $char === ']') {
|
||
array_pop($stack);
|
||
}
|
||
}
|
||
if (!$escaped && $char === '"') {
|
||
$inString = !$inString;
|
||
}
|
||
$escaped = ($char === '\\' && !$escaped);
|
||
}
|
||
$fixed .= implode('', array_reverse($stack));
|
||
$fixed = preg_replace('/,\s*([\]}])/m', '$1', $fixed);
|
||
$fixed = preg_replace_callback(
|
||
'/([{,]\s*)([a-zA-Z_][a-zA-Z0-9_]*)(\s*:)/',
|
||
function ($matches) {
|
||
return $matches[1] . '"' . $matches[2] . '"' . $matches[3];
|
||
},
|
||
$fixed
|
||
);
|
||
return $fixed;
|
||
}
|
||
}
|