openAI 通过php方式 发送请求,流数据形式传输,php 实现chatGPT功能

书接上文,如何在PHP中对接openAI接口

PHP调用OpenAI API的方法

此处使用的框架是 symfony ,可自行根据自己框架开发,大同小异,框架无所谓,主要是功能!
先上代码

<?php
namespace LdWxappPlugin\Api\Resource\Chatapi;
use ApiBundle\Api\ApiRequest;
use ApiBundle\Api\Resource\AbstractResource;
use ApiBundle\Api\Annotation\ApiConf;
use AppBundle\Common\ArrayToolkit;
use Symfony\Component\HttpFoundation\StreamedResponse;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
class ChatapiConversation extends AbstractResource
{
   
    protected $host = 'http://*.*.*.*'; // 服务器地址
    protected $uri = '/v1/chat/completions'; // 调用的接口地址
    protected $authKey = 'Basic c2VjcmV********'; // api鉴权的key
    protected $message = '';
    protected $sources = [];
    protected $conversationId = ''; 
    protected $currentMessage='';
    /**
     * @var array|mixed
     */
    private $document;
    /**
     * @param ApiRequest $request
     * @param $conversationId 会话id
     */
     // 前端输入完信息,发送时调用的请求
    public function add(ApiRequest $request,$conversationId)
    {
   
        $this->verifySend();
        $this->conversationId = $conversationId;
        //刷新缓冲区
        ob_implicit_flush(true);
        ob_end_flush();
        $response = new StreamedResponse();

        // 设置响应头,指定Content-Type为text/event-stream
        $response->headers->set('Content-Type', 'text/event-stream');
        $response->headers->set('Cache-Control', 'no-cache');
        $response->headers->set('Charset', 'UTF-8');
        $response->headers->set('X-Accel-Buffering', 'no');
        $params = $request->request->all();
        $aiParams = $this->filterAiParams($params);
        //插入用户消息
        $this->getChatApiConversationService()->addUserMessage($this->conversationId,$aiParams);
        // 设置响应内容生成器
        $response->setCallback(function () use ($request,$aiParams) {
   
            $this->forwardAiRequest($aiParams);
        });

        // 发送响应
        $response->send();
    }

    public function update(ApiRequest $request,$conversationId,$flag)
    {
   
        $params = $request->request->all();
        $message = $params['messages']['0']['content'] ?? '';
        $result = $this->getChatApiConversationService()->addAiMessage($conversationId,['message'=>$message],'');
        if ($result){
   
            return ['status'=>'success','message'=>'补充消息成功','code'=> 1,"data"=>['messageId'=>$result['id']] ];
        } else {
   
            return ['status'=>'fail','message'=>'补充消息成功','code'=> 0];
        }
    }

    /**
     * @param $chunk
     * @return string
     */
    public function formatAiResponse($chunk)
    {
   
        if($chunk == "Internal Server Error"){
   
            throw new BadRequestHttpException("AI好像开小差了~请联系客服");
        }


        //替换掉data:
        $chunkJsonStr = trim(str_replace('data:','',$chunk));
        $isComplete = json_decode($this->currentMessage,true);
        if($isComplete == null){
   
            $this->currentMessage .= $chunkJsonStr;
        } else {
   
            $this->currentMessage = $chunkJsonStr;
        }
        if($chunkJsonStr !='[DONE]'){
   
            //这里会出现单条消息超限多条消息拼接的情况,
            $chunkArr = json_decode($this->currentMessage,true);
            if (!$chunkArr){
   
                return null;
            }
            $this->currentMessage = '';
            $originMessage = $chunkArr['choices'][0]['delta']['content'] ?? '';
            //拼接当前条消息数据
            $this->message .= $originMessage;
            //赋值引用文档
            if($chunkArr['choices'][0]['sources']){
   
                $this->document = $chunkArr['choices'][0]['sources'];
            }

            //重新拼装前端结构
            $customChunkArr = ["status"=>"going","content"=>$originMessage];

            return "data: ".json_encode($customChunkArr,JSON_UNESCAPED_UNICODE)."\n\n";
        } else {
   
            //传输结束,这里处理数据入库$this->message
            
            $sources = $this->getTaskByPageLabel($this->document);
            $insertData = [
                'message' => $this->message,
                "sourceFrom" => json_encode($sources,JSON_UNESCAPED_UNICODE)
            ];
            //这里判断是否当前课程学员
            //先查出当前会话对应goods

            $conversation = $this->getChatApiService()->findByConversationId($this->conversationId);
            $isMember = false;
            $id = $conversation[0]['goodsId'] ?? 0;
            if($id !== 0){
   
                //查询当goods_
                $goodsApiRequest = new ApiRequest("/api/goods/{
     $id}", 'GET', []);
                try{
   
                    $goods = $this->container->get('api_resource_kernel')->handleApiRequest($goodsApiRequest);
                    $isMember = $goods['isMember'];
                }catch(\Exception $e){
   
                    $catchFlag = 1;
                }
            }
            //根据taskId查询task


            $originData = json_encode(['message'=>$this->message,'sources'=>$this->document],JSON_UNESCAPED_UNICODE);
            $result = $this->getChatApiConversationService()->addAiMessage($this->conversationId,$insertData,$originData);
            return "data: ".json_encode(["status"=>"done","finishData"=>['id'=>$result['id'],"sourceFrom"=>$sources,"isMember"=>$isMember]],JSON_UNESCAPED_UNICODE)."\n\n";
        }

    }

    //前置验证
    protected function verifySend()
    {
   
        $userId = $this->getCurrentUser()->getId();
        if (!$userId){
   
            //登录验证
            throw new BadRequestHttpException("请先登录");
        }
        $factory = $this->biz->offsetGet('ratelimiter.factory');
        $rateLimiter = $factory('chat_send_message', 5, 60);
        $remained = $rateLimiter->check($userId);
        if (!$remained) {
   
            throw new BadRequestHttpException("发送过于频繁请于1分钟后重试");
        }
    }

    public function filterAiParams($params)
    {
   
        if(!$params['messages'] || $params['conversationId']){
   
            throw new BadRequestHttpException("缺失必要参数");
        }
        $params['stream'] = true;
        $params['use_context'] = true;
        $params['include_sources'] = true;
        return $params;
    }


    public function forwardAiRequest($params)
    {
   
        $curl = curl_init();
        curl_setopt_array($curl, array(
            CURLOPT_URL => $this->host.$this->uri,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_ENCODING => '',
            CURLOPT_MAXREDIRS => 10,
            CURLOPT_TIMEOUT => 0,
            CURLOPT_FOLLOWLOCATION => true,
            CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
            CURLOPT_CUSTOMREQUEST => 'POST',
            CURLOPT_POSTFIELDS => json_encode($params),
            CURLOPT_HTTPHEADER => array(
                'Accept: application/json',
                'Authorization: '.$this->authKey,
                'Content-Type: application/json'
            ),
            // 启用流式传输模式
        ));
        curl_setopt($curl, CURLOPT_BUFFERSIZE, 16384);
        curl_setopt($curl, CURLOPT_WRITEFUNCTION, function ($ch, $chunk) {
   
            // 处理接收到的流式数据
            $echoStream = $this->formatAiResponse($chunk);
            if ($echoStream != null){
   
                echo $echoStream;
            }
            flush();
            return strlen($chunk);
        });
        $response = curl_exec($curl);
        curl_close($curl);
        // echo $response;
    }


    //根据map,page_label获取task
    protected function getTaskByPageLabel($sources)
    {
   
        $tasks = [];
        if($sources){
   
            foreach ($sources as $source){
   
                $pageLabel = $source['document']['doc_metadata']['page_label'] ?? 0;
                $score = $source['score'] ?? 0;
                if ($pageLabel && $score>0.5){
   
                    $task = self::getPageTaskMap()[$pageLabel];
                    $taskInfo = $this->getTaskService()->getTask($task['taskId']);
                    $task['courseId'] = $taskInfo['courseId'];
                    $tasks[] = $task;
                }
            }
        }
        return array_unique($tasks);
    }


    //page与task映射map,先写死实现功能
    static protected function getPageTaskMap()
    {
   
        //page_label => taskInfo
        return  [
            1 => [
                'taskId'=>51147,
                'taskTitle'=>"课时1:课程概述、特点及说明",
            ],
            2 => [
                'taskId'=>51148,
                'taskTitle'=>"课时2:资产配置概述、均值方差模型配置法的 GPT 应用",
            ],
            3 => [
                'taskId'=>51148,
                'taskTitle'=>"课时2:资产配置概述、均值方差模型配置法的 GPT 应用",
            ],
            4 => [
                'taskId'=>51148,
                'taskTitle'=>"课时2:资产配置概述、均值方差模型配置法的 GPT 应用",
            ],
            5 => [
                'taskId'=>51149,
                'taskTitle'=>"课时3:Black-Litterman 策略、风险平价策略介绍及GPT 应用",
            ],
            6 => [
                'taskId'=>51149,
                'taskTitle'=>"课时3:Black-Litterman 策略、风险平价策略介绍及GPT 应用",
            ],
            7 => [
                'taskId'=>51149,
                'taskTitle'=>"课时3:Black-Litterman 策略、风险平价策略介绍及GPT 应用",
            ],
            8 => [
                'taskId'=>51150,
                'taskTitle'=>"课时4:Black-Litterman 策略、风险平价策略介绍及GPT 应用",
            ],
            9 => [
                'taskId'=>51150,
                'taskTitle'=>"课时4:Black-Litterman 策略、风险平价策略介绍及GPT 应用",
            ],
            10 => [
                'taskId'=>51151,
                'taskTitle'=>"课时5:基金分类、标签体系、收益风险指标分析",
            ],
            11 => [
                'taskId'=>51151,
                'taskTitle'=>"课时5:基金分类、标签体系、收益风险指标分析",
            ],
            12 => [
                'taskId'=>51152,
                'taskTitle'=>"课时6:基金投资体验分析和投资风格分析",
            ],
            13 => [
                'taskId'=>51152,
                'taskTitle'=>"课时6:基金投资体验分析和投资风格分析",
            ],
            14 => [
                'taskId'=>51152,
                'taskTitle'=>"课时6:基金投资体验分析和投资风格分析",
            ],
            15 => [
                'taskId'=>51153,
                'taskTitle'=>"课时7:基金插件工具介绍及营销文案编写",
            ],
            16 => [
                'taskId'=>51154,
                'taskTitle'=>"课时8:历史事件复盘",
            ],
            17 => [
                'taskId'=>51155,
                'taskTitle'=>"课时9:近期市场、经济复盘",
            ],
            18 => [
                'taskId'=>51156,
                'taskTitle'=>"课时10:未来经济展望",
            ],
            20 => [
                'taskId'=>51157,
                'taskTitle'=>"课时11:PPI 时间序列预测",
            ],
            21 => [
                'taskId'=>51158,
                'taskTitle'=>"课时12:周期划分到策略适配",
            ],
            22 => [
                'taskId'=>51174,
                'taskTitle'=>"课时13:金融数据分析简介",
            ],
            23 => [
                'taskId'=>51175,
                'taskTitle'=>"课时14:调研问卷分析案例",
            ],
            24 => [
                'taskId'=>51175,
                'taskTitle'=>"课时14:调研问卷分析案例",
            ],
            25 => [
                'taskId'=>51176,
                'taskTitle'=>"课时15:金融客户机器学习分群案例",
            ],
            26 => [
                'taskId'=>51176,
                'taskTitle'=>"课时15:金融客户机器学习分群案例",
            ],
            27 => [
                'taskId'=>51177,
                'taskTitle'=>"课时16:财经公众号内容提取案例",
            ],
            28 => [
                'taskId'=>51178,
                'taskTitle'=>"课时17:筛选符合减持新规的股票",
            ],
            29 => [
                'taskId'=>51159,
                'taskTitle'=>"课时18:数据提取:基金历史业绩(附 Noteable 插件介绍)",
            ],
            30 => [
                'taskId'=>51159,
                'taskTitle'=>"课时18:数据提取:基金历史业绩(附 Noteable 插件介绍)",
            ],
            31 => [
                'taskId'=>51160,
                'taskTitle'=>"课时19:数据提取:基金基础信息",
            ],
            32 => [
                'taskId'=>51161,
                'taskTitle'=>"课时20:数据提取:基金基础信息",
            ],
            33 => [
                'taskId'=>51162,
                'taskTitle'=>"课时21:数据分析:持仓数据图表化",
            ],
            34 => [
                'taskId'=>51163,
                'taskTitle'=>"课时22:数据分析:持仓数据分析",
            ],
            35 => [
                'taskId'=>51163,
                'taskTitle'=>"课时22:数据分析:持仓数据分析",
            ],
            36 => [
                'taskId'=>51164,
                'taskTitle'=>"课时23:数据分析:文字描述生成",
            ],
            37 => [
                'taskId'=>51165,
                'taskTitle'=>"课时24:数据分析:大类资产配比计算及增配建议",
            ],
            38 => [
                'taskId'=>51166,
                'taskTitle'=>"课时25:数据分析:基金持仓行业分布",
            ],
            39 => [
                'taskId'=>51167,
                'taskTitle'=>"课时26:内容生成:宏观经济与市场分析(及 KeyMate插件介绍)",
            ],
            40 => [
                'taskId'=>51167,
                'taskTitle'=>"课时26:内容生成:宏观经济与市场分析(及 KeyMate插件介绍)",
            ],
            41 => [
                'taskId'=>51168,
                'taskTitle'=>"课时27:内容生成:行业分析",
            ],
            42 => [
                'taskId'=>51169,
                'taskTitle'=>"课时28:内容生成:基金分析",
            ],
            43 => [
                'taskId'=>51170,
                'taskTitle'=>"课时29:风格适配:结构化 Prompt",
            ],
            44 => [
                'taskId'=>51171,
                'taskTitle'=>"课时30:风风格适配:Custom Instructions",
            ],
            45 => [
                'taskId'=>51172,
                'taskTitle'=>"课时31:风风格适配:Custom Instructions",
            ],
            46 => [
                'taskId'=>51173,
                'taskTitle'=>"课时 32: 资产配置报告展示",
            ],
            47 => [
                'taskId'=>51173,
                'taskTitle'=>"课时 32: 资产配置报告展示",
            ],

        ];
    }

    protected function getTaskService()
    {
   
        return $this->service('Task:TaskService');
    }

    private function getProductService()
    {
   
        return $this->service('Product:ProductService');
    }

    public function getGoodsService()
    {
   
        return $this