<?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;
    }
}
