phpbb_ailabs/privet/ailabs/controller/chatgpt.php

311 lines
12 KiB
PHP

<?php
/**
*
* AI Labs extension
*
* @copyright (c) 2023, privet.fun, https://privet.fun
* @license GNU General Public License, version 2 (GPL-2.0)
*
*/
namespace privet\ailabs\controller;
use privet\ailabs\includes\GenericCurl;
use Symfony\Component\HttpFoundation\JsonResponse;
use privet\ailabs\includes\AIController;
use privet\ailabs\includes\resultParse;
/*
config (example)
{
"api_key": "<api-key>",
"url_chat": "https://api.openai.com/v1/chat/completions",
"model": "gpt-3.5-turbo",
"temperature": 0.9,
"max_tokens": 4096,
"message_tokens": 1024,
"top_p": 1,
"frequency_penalty": 0,
"presence_penalty": 0.6,
"prefix": "This is optional field you can remove it or populate with something like this -> Pretend your are Bender from Futurma",
"prefix_tokens": 16
"max_quote_length": 10
}
template
{info}[quote={poster_name} post_id={post_id} user_id={poster_id}]{request}[/quote]{response}
*/
class chatgpt extends AIController
{
// https://platform.openai.com/docs/api-reference/chat/create#chat/create-max_tokens
// By default, the number of tokens the model can return will be (4096 - prompt tokens).
protected $max_tokens = 4096;
protected $message_tokens = 2048;
protected function process()
{
$this->job['status'] = 'exec';
$set = [
'status' => $this->job['status'],
'log' => json_encode($this->log)
];
$this->job_update($set);
$this->post_update($this->job);
if (!empty($this->cfg->message_tokens)) {
$this->message_tokens = $this->cfg->message_tokens;
}
if (!empty($this->cfg->max_tokens)) {
$this->max_tokens = (int)$this->cfg->max_tokens;
}
$prefix_tokens = empty($this->cfg->prefix_tokens) ? 0 : $this->cfg->prefix_tokens;
$api_key = $this->cfg->api_key;
$this->cfg->api_key = null;
$api = new GenericCurl($api_key);
$this->job['status'] = 'fail';
$response = $this->language->lang('AILABS_ERROR_CHECK_LOGS');
$api_response = null;
$request_tokens = null;
$response_tokens = null;
$total_replaced = 0;
$original_request = $this->job['request'];
if ($total_replaced > 0) {
$this->job['request'] = trim(str_replace(' ', ' ', $this->job['request']));
$this->log['request.original'] = $original_request;
$this->log['request.adjusted'] = $this->job['request'];
}
$messages = [];
$info = null;
$posts = [];
$post_first_taken = null;
$post_first_discarded = null;
$mode = $this->job['post_mode'];
$history = ['post_text' => $this->job['post_text']];
$pattern = '/<QUOTE\sauthor="' . $this->job['ailabs_username'] . '"\spost_id="(.*)"\stime="(.*)"\suser_id="' . $this->job['ailabs_user_id'] . '">/';
$this->log['history.pattern'] = $pattern;
$this->log_flush();
// Attempt to unwind history using quoted posts
$history_tokens = 0;
$round = -1;
do {
$round++;
$matches = null;
preg_match_all(
$pattern,
$history['post_text'],
$matches
);
$history = null;
if ($matches != null && !empty($matches) && !empty($matches[1][0])) {
$postid = (int) $matches[1][0];
$sql = 'SELECT j.job_id, j.post_id, j.response_post_id, j.request, j.response, p.post_text, p.post_time, j.request_tokens, j.response_tokens ' .
'FROM ' . $this->jobs_table . ' j ' .
'JOIN ' . POSTS_TABLE . ' p ON p.post_id = j.post_id ' .
'WHERE ' . $this->db->sql_build_array('SELECT', ['response_post_id' => $postid]);
$result = $this->db->sql_query($sql);
$history = $this->db->sql_fetchrow($result);
$this->db->sql_freeresult($result);
if (!empty($history)) {
$count_tokens = $history['request_tokens'] + $history['response_tokens'];
$discard = $this->max_tokens < ($this->message_tokens + $history_tokens + $count_tokens);
$posts[] = [
'postid' => $postid,
'request_tokens' => $history['request_tokens'],
'response_tokens' => $history['response_tokens'],
'runnig_total_tokens' => $history_tokens + $count_tokens,
'discard' => $discard
];
if ($discard) {
$post_first_discarded = $postid;
break;
}
$post_first_taken = $postid;
$history_tokens += $count_tokens;
$history_decoded_request = utf8_decode_ncr($history['request']);
$history_decoded_response = utf8_decode_ncr($history['response']);
array_unshift(
$messages,
['role' => 'user', 'content' => trim($history_decoded_request)],
['role' => 'assistant', 'content' => trim($history_decoded_response)]
);
if ($round == 0) {
// Remove quoted content from the quoted post
$post_text = sprintf(
'<r><QUOTE author="%1$s" post_id="%2$s" time="%3$s" user_id="%4$s"><s>[quote=%1$s post_id=%2$s time=%3$s user_id=%4$s]</s>%6$s<e>[/quote]</e></QUOTE>%5$s</r>',
$this->job['ailabs_username'],
(string) $postid,
(string) $this->job['post_time'],
(string) $this->job['ailabs_user_id'],
$this->job['request'],
property_exists($this->cfg, 'max_quote_length') ?
$this->trim_words($history_decoded_response, (int) $this->cfg->max_quote_length) : $history_decoded_response
);
$sql = 'UPDATE ' . POSTS_TABLE .
' SET ' . $this->db->sql_build_array('UPDATE', ['post_text' => utf8_encode_ucr($post_text)]) .
' WHERE post_id = ' . (int) $this->job['post_id'];
$result = $this->db->sql_query($sql);
$this->db->sql_freeresult($result);
}
}
}
} while (!empty($history));
if (!empty($posts)) {
$this->log['history.posts'] = $posts;
$this->log_flush();
}
if (!empty($this->cfg->prefix)) {
array_unshift(
$messages,
['role' => 'system', 'content' => $this->cfg->prefix]
);
}
$messages[] = ['role' => 'user', 'content' => trim($this->job['request'])];
$this->log['request.messages'] = $messages;
$this->log_flush();
try {
// https://api.openai.com/v1/chat/completions
$api_result = $api->sendRequest($this->cfg->url_chat, 'POST', [
'model' => $this->cfg->model,
'messages' => $messages,
'temperature' => (float) $this->cfg->temperature,
// https://platform.openai.com/docs/api-reference/chat/create#chat/create-max_tokens
// By default, the number of tokens the model can return will be (4096 - prompt tokens).
// 'max_tokens' => (int) $this->cfg->max_tokens,
'frequency_penalty' => (float) $this->cfg->frequency_penalty,
'presence_penalty' => (float)$this->cfg->presence_penalty,
]);
/*
Response example:
{
'id': 'chatcmpl-1p2RTPYSDSRi0xRviKjjilqrWU5Vr',
'object': 'chat.completion',
'created': 1677649420,
'model': 'gpt-3.5-turbo',
'usage': {'prompt_tokens': 56, 'completion_tokens': 31, 'total_tokens': 87},
'choices': [
{
'message': {
'role': 'assistant',
'content': 'The 2020 World Series was played in Arlington, Texas at the Globe Life Field, which was the new home stadium for the Texas Rangers.'
},
'finish_reason': 'stop',
'index': 0
}
]
}
*/
$json = json_decode($api_result);
$this->log['response'] = $json;
$this->log['response.codes'] = $api->responseCodes;
$this->log_flush();
if (
empty($json->object) ||
empty($json->choices) ||
$json->object != 'chat.completion' ||
!in_array(200, $api->responseCodes)
) {
} else {
$this->job['status'] = 'ok';
$api_response = $json->choices[0]->message->content;
$response = $api_response;
$request_tokens = $json->usage->prompt_tokens;
$response_tokens = $json->usage->completion_tokens;
if ($history_tokens > 0 || $prefix_tokens > 0) {
$this->log['request.tokens.raw'] = $request_tokens;
$this->log['request.tokens.adjusted'] = $request_tokens - $history_tokens - $prefix_tokens;
}
}
} catch (\Exception $e) {
$this->log['exception'] = $e->getMessage();
$this->log_flush();
}
$this->log['finish'] = date('Y-m-d H:i:s');
if (!empty($posts)) {
$viewtopic = "{$this->root_path}viewtopic.{$this->php_ext}";
$discarded = '';
if ($post_first_discarded != null) {
$discarded = $this->language->lang('AILABS_POSTS_DISCARDED', $viewtopic, $post_first_discarded);
}
$total_posts_count = count($posts) * 2 + 2;
$total_tokens_used_count = $request_tokens + $response_tokens;
$info = $this->language->lang(
'AILABS_DISCARDED_INFO',
$viewtopic,
$post_first_taken,
$total_posts_count,
$discarded,
$total_tokens_used_count,
$this->max_tokens
);
}
$resultParse = new resultParse();
$resultParse->message = $response;
$resultParse->info = $info;
$response = $this->replace_vars($this->job, $resultParse);
$data = $this->post_response($this->job, $response);
$this->job['response_time'] = time();
$this->job['response_post_id'] = $data['post_id'];
$set = [
'status' => $this->job['status'],
'attempts' => $this->job['attempts'] + 1,
'response_time' => $this->job['response_time'],
'response' => utf8_encode_ucr($api_response),
'request_tokens' => $request_tokens - $history_tokens - $prefix_tokens,
'response_post_id' => $this->job['response_post_id'],
'response_tokens' => $response_tokens,
'log' => json_encode($this->log)
];
$this->job_update($set);
$this->post_update($this->job);
return new JsonResponse($this->log);
}
}