ChatTrait.php 13.7 KB
<?php
/**
 * +-----------------------------------------------------------------------------------------------------------------------
 * trait :聊天 trait
 * +-----------------------------------------------------------------------------------------------------------------------
 *
 * PHP version 7
 *
 * @category  App\Models\Traits
 * @package   App\Models\Traits
 * @author    Richer <yangzi1028@163.com>
 * @date      2023年4月24日14:02:44
 * @copyright 2020-2021 Richer (http://www.Richer.com/)
 * @license   http://www.Richer.com/ License
 * @link      http://www.Richer.com/
 */
namespace App\Models\Traits;

use App\Models\Chat\ChatRecordItem;
use App\Models\System\SystemSetting;
use App\Models\User\TimesRecord;
use App\Models\User\User;
use App\Util\OpenAI\src\OpenAi;
use Exception;
use GuzzleHttp\Client;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
use Tectalic\OpenAi\Authentication;
use Tectalic\OpenAi\ClientException;
use Tectalic\OpenAi\Manager;

/**
 * Trait ChatTrait
 * @package App\Models\Traits
 */
trait ChatTrait
{
    use SyncTrait;

    /**
     * @var string
     */
    protected $ai_model = 'gpt-3.5-turbo'; // 'gpt-4'; // AI 模型
    protected $log_channel = 'openai'; // 日志 频道
    protected $log_desc =  '通过 openai 接口发送消息'; // 日志描述
    protected $context_timeout=  60 * 30; // 上下文超时时间
    protected $consumption_times =  1; // 每次请求消耗次数

    /**
     * 执行发送消息:基于上下文发送或者单独发送,通过流的方式进行发送
     *
     * @param $user
     * @param $model
     * @param string $content 内容
     * @param int $context 是否支持上下文
     * @param boolean $stream 是否基于 stream流 方式进行请求
     * @return mixed
     * @throws Exception
     */
    public function send($user, $model, $content = '', $context = 0, $stream = true)
    {
        record_log($this->log_channel, $this->log_desc, 'begin');

        $prepare = $this->prepareRequest($user, $model, $content, $context, $stream);

        $open_api_key = $prepare['open_api_key'];
        $send_data = $prepare['send_data'];
        $context_timeout = $prepare['context_timeout'];
        $consume_times = $prepare['consume_times'];
        $cache_key = $prepare['cache_key'];

        if ($stream !== true) {
            $open_ai = Manager::build(
                new  Client([
//                'proxy' => 'http://127.0.0.1:10809',
                    'verify' => false
                ]),
                new Authentication($open_api_key)
            );
            $answer = $this->sendRequest($open_ai, $send_data, 'general');
        } else {
            $open_ai = new OpenAi($open_api_key);
            $answer = $this->sendRequest($open_ai, $send_data);
        }

        if ($context == 1) {
            // 响应消息
            $messages[] = ['role' => 'assistant', 'content' => $answer];
            // 将上下文放入缓存中
            Cache::put($cache_key, $messages, $context_timeout);
        }

        record_log($this->log_channel, '原始 answer:' . $answer);

        // update by Richer 于 2023年5月4日10:35:50 需要将 gpt 等进行替换
        $answer = $this->replaceStr($answer);

        record_log($this->log_channel, '替换后 answer:' . $answer);

        // 写入回答创建AI的聊天记录
        $result = $this->recordChat($user, $model, $answer, $consume_times);

        record_log($this->log_channel, $this->log_desc, 'end');
        return $result;
    }

    /**
     * 准备请求参数
     *
     * @param $user
     * @param $model
     * @param $content
     * @param $context
     * @param $stream
     * @return array|false
     * @throws Exception
     */
    public function prepareRequest($user, $model, $content, $context, $stream = true)
    {
        record_log($this->log_channel, 'request:' . json_encode(request()->all()));

        record_log($this->log_channel, 'content:' . $content);

        // 判断用户积分是否足够
        $times = $user->times;
        $systemSetting = SystemSetting::getSetting();
        $consume_times = $model->consume_times;
        if ($times <= 0 || $times < $consume_times) {
            throw new Exception("提问失败,次数不足。");
        }
        // 上下文超时时间
        $context_timeout = optional($systemSetting)->context_timeout * 60 ? : $this->context_timeout;

//        $keys = [
//            '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',
//            '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',
//            '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',
//            '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',
//            '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',
//        ];
//        $open_api_key = Arr::get($keys, array_rand($keys, 1), $open_api_key);
//        dd($open_api_key);

        $open_api_key = config('openai.api_key') ;// '你的Open Ai key';
        // 获取随机的 key
        record_log($this->log_channel, 'open_api_key:' . $open_api_key);

        // 获取当前会话的缓存键
        $cache_key = 'chat_context_' . $model->id;
        // 判断当前的聊天是否是基于上下文的聊天,如果是基于上下的文的聊天,需要携带上下文
        if ($context == 1) {
            record_log($this->log_channel, '开始上下文聊天');

            // 从缓存中获取当前会话的上下文
            $messages = Cache::get($cache_key);

            // 本次用户发送的消息
            $messages[] = ['role' => 'user', 'content' => $content];
        } else {
            record_log($this->log_channel, '开始无上下文聊天');
            $messages[] = ['role' => 'user', 'content' => $content];
        }

        record_log($this->log_channel, 'messages:' . my_json_encode($messages));
        $send_data = [
            'model' => $this->ai_model,
            'messages' => $messages,
            'temperature' => 1.0,
//            "max_tokens" => 150,
            "frequency_penalty" => 0,
            "presence_penalty" => 0,
            'stream' => $stream
        ];
        record_log($this->log_channel, '请求内容:' . my_json_encode($send_data));

        return [
            'open_api_key'      => $open_api_key,
            'send_data'         => $send_data,
            'context_timeout'   => $context_timeout,
            'consume_times'   => $consume_times,
            'cache_key'     => $cache_key,
        ];
    }

    /**
     * 发送请求
     * @param $open_ai
     * @param $send_data
     * @param string $type
     * @return string
     */
    public static function sendRequest($open_ai, $send_data, $type = 'stream')
    {
        $answer = '';
        if ($type === 'chunked') {
            // 设置响应头信息
            header('Access-Control-Allow-Credentials: true');
            // 设置响应头信息
            header('Transfer-Encoding: chunked');
            header('Cache-Control: no-cache');
            header('Access-Control-Allow-Origin: *');
            header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
            header('Access-Control-Allow-Headers: Content-Type');
            header('Connection: keep-alive');
            header('X-Accel-Buffering: no');

//            header('Access-Control-Allow-Credentials: true');
//            // 设置响应头信息
//            header('Transfer-Encoding: chunked');
//            header('Content-Type: text/plain');
//            header('Cache-Control: no-cache');
//            header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
//            header('Access-Control-Allow-Headers: Content-Type');
//            header('Connection: keep-alive');
            $complete = $open_ai->chat($send_data, function ($curl_info, $response) use (&$answer) {
                //闭包函数处理流
                $data = [];
                $lines = explode("\n", $response);
                foreach ($lines as $line) {
                    if (!str_contains($line, ':')) {
                        continue;
                    }
                    [$name, $value] = explode(':', $line, 2);
                    if ($name == 'data') {
                        $data[] = trim($value);
                    }
                }
                foreach ($data as $message) {
                    if ('[DONE]' === $message) {
                        echo "0\r\n\r\n";
                    } else {
                        $message = json_decode($message, true);
                        $content = $message['choices'][0]['delta']['content'] ?? '';
                        $answer .= $content;
//                        record_log('openai', '内容: ' . $content );
                        echo urlencode($content) . "\r\n";
                    }
                }
                ob_flush();
                flush();
                return strlen($response);
            });
        } elseif ($type === 'stream') {
            header('Content-type: text/event-stream');
            header('Cache-Control: no-cache');
            ob_end_flush();
            $complete = $open_ai->chat($send_data, function ($curl_info, $data) use (&$answer) {
//                record_log($this->log_channel, '开始请求' );

                $deltas = explode("\n", $data);
                $content = '';
                foreach ($deltas as $delta) {
                    if (strpos($delta, 'data: ') !== 0) {
                        continue;
                    }
                    $json = json_decode(substr($delta, 6));
                    if (isset($json->choices[0]->delta)) {
                        $content = $json->choices[0]->delta->content ?? "";
                        $answer .= $content;
                    } elseif (isset($json->error->message)) {
                        $content = $json->error->message;
                    } elseif(trim($delta) == "data: [DONE]") {
                        $content = " ";
                    } else {
                        $content = "对不起,我不知道怎么去回答。";
                    }
                    echo str_replace("\n", "\\n", $content )."\n\n";
                    flush();
                }

//                record_log($this->log_channel, '内容: ' . $content );

                if (connection_aborted()) {
                    return 0;
                }

//            echo PHP_EOL;
//            ob_flush();
                return strlen($data);
            });

            echo "event: stop\n";
            echo "data: stopped\n\n";
        } elseif($type === 'general') {
            $response = $open_ai->chatCompletions()->create(
                new \Tectalic\OpenAi\Models\ChatCompletions\CreateRequest($send_data)
            )->toModel();

            $answer = $response->choices[0]->message->content;
        }

        return $answer;
    }

    /**
     * 替换一些关键词
     *
     * @param $answer
     * @return string
     */
    public function replaceStr($answer): string
    {
        switch (true) {
            case stristr($answer, '我不是chatgpt-4'):
            case stristr($answer, '我不是chatgpt4'):
            case stristr($answer, '我不是chat-gpt4'):
            case stristr($answer, '我不是chat-gpt-4'):
            case stristr($answer, '我不是chatgpt-3.5'):
            case stristr($answer, '我不是chat-gpt-3.5'):
            case stristr($answer, '我不是chat-gpt3.5'):
            case stristr($answer, '我不是chatgpt3.5'):
            case stristr($answer, '我不是chatgpt-3'):
            case stristr($answer, '我不是chat-gpt-3'):
            case stristr($answer, '我不是chat-gpt3'):
            case stristr($answer, '我不是chatgpt3'):
            case stristr($answer, '我不是chatgpt'):
            case stristr($answer, '我不是chat-gpt'):
            case stristr($answer, '我不是gpt'):
            case stristr($answer, '我不是openai'):
                break;
            default:
                $replaces = [ 'chatgpt-4', 'chatgpt-3', 'chatgpt3.5','chatgpt3'];
                $answer = str_ireplace($replaces, '百度AI', $answer);

                $replaces = [ 'chatgpt', 'chat-gpt'];
                $answer = str_ireplace($replaces, '百度AI', $answer);

                $replaces = ['gpt-3'];
                $answer = str_ireplace($replaces, '百度AI', $answer);

                $replaces = [ 'gpt','-3AI'];
                $answer = str_ireplace($replaces, '百度AI', $answer);

                $replaces1 = ['(Generative Pre-trained Transformer)', '(Generative Pre-trained Transformer)'];
                $answer = str_ireplace($replaces1, '', $answer);

                $replaces2 = ['OpenAi'];
                $answer = str_ireplace($replaces2, 'Baidu', $answer);
        }
        return $answer;
    }

    /**
     * 写入回答创建AI的聊天记录
     *
     * @param $user
     * @param $model
     * @param $answer
     * @param $consume_times
     * @return mixed
     */
    public function recordChat($user, $model, $answer, $consume_times)
    {
        // 写入回答创建AI的聊天记录
        $item = [
            'user_id'   => $model->user_id,
            'user_type' => ChatRecordItem::AI,
            'body'      => $answer,
            'ai_model'  => $this->ai_model,
            'category_id'   => $model->category_id,
            'chat_type'     => $model->type,
        ];
        $result = $model->items()->create($item);

        // 记录中增加
        $user->timesRecords()->create([
            'recordable_id' => $result->id,
            'recordable_type' => ChatRecordItem::OBJ_NAME,
            'times' => $consume_times,
            'type'  => TimesRecord::DECREMENT,
            'event' => TimesRecord::QUESTION,
        ]);

        // 用户次数扣减
        User::where('id', $user->id)->decrement('times', $consume_times);

        return $result;
    }
}