此处使用的框架是 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