File: /var/www/html/laravel/app/Models/Notam.php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Queue\Jobs\Job;
use Illuminate\Support\Facades\DB;
use JsonSchema\Validator;
/**
* @property $id integer
* @property $input string
* @property $output string
* @property $chatgpt_response string
* @property $prompt_tokens string
* @property $completion_tokens integer
* @property $total_tokens integer
* @property $correct_response string
* @property $comments string
* @property $prompt_id integer
* @property $batch_id integer
* @property $user_id integer
* @property $request_hash string
* @property $match_percent int
* @property $fine_tuning int
*/
class Notam extends Model
{
protected $table = 'notams';
public const MAX_TOKENS = 4096;
public const IN_PROGRESS_STATUS = 'In progress';
protected $fillable = [
'input',
'output',
'chatgpt_response',
'prompt_tokens',
'completion_tokens',
'total_tokens',
'correct_response',
'comments',
'prompt_id',
'batch_id',
'user_id',
'request_hash',
'match_percent',
'fine_tuning',
'start_date',
'end_date',
'new_output',
'new_correct_response',
'local_fine_tuning',
'local_output',
'local_response',
'local_correct_response',
'local_match_percent',
'local_sent',
'gpt_rag',
'model',
'check_required_gpt',
'check_required_local',
'gpt_processing_time',
'local_processing_time',
'gpt41_output',
'gpt41_response',
'gpt41_correct_response',
'gpt41_match_percent',
'gpt41_processing_time',
'check_required_gpt41',
'gpt41_prompt_tokens',
'gpt41_completion_tokens',
'gpt41_total_tokens',
'geo_validation_error',
'gpt_validation_errors',
'gpt41_validation_errors'
];
public static function getValidResponse($hash)
{
$notam = self::query()->where('request_hash', $hash)->whereNotNull('correct_response')->first();
return $notam->correct_response ?? null;
}
public static function hash($input): string
{
$input = json_decode($input);
$text = is_object($input) && property_exists($input, 'Text') ? $input->Text : $input;
return hash('sha256', json_encode($text, JSON_UNESCAPED_UNICODE));
}
public static function sortJson(&$array): void
{
foreach ($array as &$value) {
if (is_array($value)) {
self::sortJson($value);
}
}
ksort($array);
}
public function prepareJsons(): void
{
$this->correct_response = self::prepareJson($this->correct_response);
$this->output = self::prepareJson($this->output);
$this->local_output = self::prepareJson($this->local_output);
}
public static function prepareJson($jsonString): string
{
// Removes } from strings like :"something}
$jsonString = preg_replace('/(Line":")([^{}]*)\},/', '$1$2,', $jsonString);
$jsonString = str_replace('”', '\"', $jsonString);
$jsonArray = json_decode($jsonString, true);
if (is_array($jsonArray)) {
self::formatJson($jsonArray);
self::sortJson($jsonArray);
return json_encode($jsonArray, JSON_UNESCAPED_UNICODE);
}
return $jsonString ?? '';
}
public static function formatJson(&$jsonArray): void
{
foreach ($jsonArray as $key => &$value) {
switch ($key) {
case 'SiteIndexA':
unset($jsonArray[$key]);
$jsonArray['siteIndexA'] = $value;
break;
case 'TYPE':
unset($jsonArray[$key]);
$jsonArray['type'] = $value;
break;
case 'TITLE':
unset($jsonArray[$key]);
$jsonArray['title'] = $value;
break;
case 'Traffic':
unset($jsonArray[$key]);
$jsonArray['traffic'] = $value;
break;
case 'Area':
unset($jsonArray[$key]);
$jsonArray['area'] = $value;
break;
case 'Text':
unset($jsonArray[$key]);
$jsonArray['text'] = $value;
break;
case 'CoordinatesLat':
unset($jsonArray[$key]);
$jsonArray['coordinatesLat'] = $value;
break;
case 'CoordinatesLon':
unset($jsonArray[$key]);
$jsonArray['coordinatesLon'] = $value;
break;
case 'Radius':
unset($jsonArray[$key]);
$jsonArray['radius'] = $value;
break;
case 'AltitudeMin':
unset($jsonArray[$key]);
$jsonArray['altitudeMin'] = $value;
break;
case 'AltitudeMax':
unset($jsonArray[$key]);
$jsonArray['altitudeMax'] = $value;
break;
case 'N_status':
unset($jsonArray[$key]);
$jsonArray['N_STATUS'] = $value;
break;
}
}
}
public function getText(): string
{
if (substr_count($this->input, '"Text"')) {
$input = json_decode($this->input);
$notamText = $input->Text ?? ($input->Text ?? '');
} else {
$notamText = $this->input;
}
return $notamText;
}
public function reachOutput($correctedResponse = false): string|false
{
if ($correctedResponse === false) {
$correctedResponse = $this->output;
}
$output = json_decode(self::prepareJson($correctedResponse));
$output = [
'CheckNeeded' => $output->CheckNeeded ?? true,
'A_Line' => $output->A_Line ?? null,
'B_Line' => $output->B_Line ?? null,
'C_Line' => $output->C_Line ?? null,
'D_Line' => $output->D_Line ?? null,
'E_Line' => $output->E_Line ?? null,
'F_Line' => $output->F_Line ?? null,
'G_Line' => $output->G_Line ?? null,
];
foreach ($output as $key => $item) {
if (empty($item) && !substr_count($key, 'Line') && !substr_count($key, 'CheckNeeded')) {
unset($output[$key]);
}
}
return json_encode($output, JSON_UNESCAPED_UNICODE);
}
public function validate(): Notam
{
$this->updateDates();
$this->prepareJsons();
$matchPercent = 100;
if (!$this->correct_response && $this->output) {
$this->correct_response = $this->output;
}
if ($this->correct_response) {
$validationErrors = $this->validateDataFormat($this->correct_response);
if (!count($validationErrors) || (count($validationErrors) == 1 && substr_count($validationErrors[0], 'CheckNeeded'))) {
$checkNeeded = false;
unset($validationErrors[0]);
} else {
$checkNeeded = true;
}
$json = json_decode($this->correct_response);
if (is_object($json)) {
if (!property_exists($json, 'CheckNeeded')
|| $json->CheckNeeded != $checkNeeded) {
$json->CheckNeeded = $checkNeeded;
$this->correct_response = json_encode($json);
}
}
}
if ($this->local_correct_response) {
$validationErrors = $this->validateDataFormat($this->local_correct_response);
$this->local_fine_tuning = 0;
if (empty($validationErrors)) {
$this->local_fine_tuning = 1;
}
if (!count($validationErrors) || (count($validationErrors) == 1 && substr_count($validationErrors[0], 'CheckNeeded'))) {
$checkNeeded = false;
unset($validationErrors[0]);
} else {
$checkNeeded = true;
}
$json = json_decode($this->local_correct_response);
if (is_object($json)) {
if (!property_exists($json, 'CheckNeeded')
|| $json->CheckNeeded != $checkNeeded) {
$json->CheckNeeded = $checkNeeded;
if ($checkNeeded) {
$this->local_fine_tuning = 0;
}
$this->local_correct_response = json_encode($json);
}
}
}
if ($this->gpt41_correct_response) {
// Fine tuning now will be based on GPT 4.1 response
$validationErrors = $this->validateDataFormat($this->gpt41_correct_response);
$this->fine_tuning = 0;
if (empty($validationErrors)) {
$this->fine_tuning = 1;
}
if (!count($validationErrors) || (count($validationErrors) == 1 && substr_count($validationErrors[0], 'CheckNeeded'))) {
$checkNeeded = false;
unset($validationErrors[0]);
} else {
$checkNeeded = true;
}
$json = json_decode($this->gpt41_correct_response);
if (is_object($json)) {
if (!property_exists($json, 'CheckNeeded')
|| $json->CheckNeeded != $checkNeeded) {
$json->CheckNeeded = $checkNeeded;
if ($checkNeeded) {
$this->fine_tuning = 0;
}
$this->gpt41_correct_response = json_encode($json);
}
}
}
$this->compareGptResponses($this->correct_response, $this->gpt41_correct_response);
Notam::query()->where('id', $this->id)
->update([
'match_percent' => floor($matchPercent),
'fine_tuning' => $this->fine_tuning ?? 0,
'local_fine_tuning' => $this->local_fine_tuning ?? 0,
'check_required_gpt' => substr_count($this->correct_response, '"CheckNeeded":true') > 0,
'check_required_local' => substr_count($this->local_correct_response, '"CheckNeeded":true') > 0,
'check_required_gpt41' => substr_count($this->gpt41_correct_response, '"CheckNeeded":true') > 0,
]);
return $this;
}
public function compareGptResponses($gptResponse, $additionalResponse)
{
$matchPercent = 0;
try {
if (!$gptResponse || !$additionalResponse) {
$this->gpt41_match_percent = $matchPercent;
$this->comments = 'One of the responses is empty, cannot compare GPT and Local model responses.';
$this->saveQuietly();
return $matchPercent;
}
$gptResponse = $this->cleanJson($this->prepareJson($this->reachOutput($gptResponse)));
$additionalModelResponse = $this->cleanJson($this->prepareJson($this->reachOutput($additionalResponse)));
if ($gptResponse !== $additionalModelResponse) {
similar_text(
$gptResponse,
$additionalModelResponse,
$matchPercent,
);
} else {
$matchPercent = 100;
}
$this->gpt41_match_percent = floor($matchPercent);
// if ($matchPercent < 90) {
// $localOutput = json_decode($this->local_correct_response);
// $localOutput->CheckNeeded = true;
// $message = json_encode($localOutput);
// $this->local_correct_response = $message;
//
// $gptOutput = json_decode($this->correct_response);
// if ($gptOutput) {
// $gptOutput->CheckNeeded = true;
// $gptOutput = json_encode($gptOutput);
// $this->correct_response = $gptOutput;
// }
// }
} catch (\Exception $e) {
$this->gpt41_match_percent = 0;
$this->comments = 'Error comparing GPT and Local model responses: ' . $e->getMessage();
}
$this->saveQuietly();
return floor($matchPercent);
}
public function updateDates($prefix = ''): void
{
$output = $this->output;
$correctResponse = $this->correct_response;
$json = null;
if ($correctResponse) {
$json = json_decode($this->correct_response);
} elseif ($output) {
$json = json_decode($this->output);
}
if ($json === null) {
return;
}
$startDate = null;
$endDate = null;
if (!empty($json->B_Line)) {
$startDate = date('Y-m-d H:i:s', strtotime($json->B_Line));
if ($startDate) {
$this->start_date = $startDate;
}
}
if (!empty($json->C_Line) && substr_count($json->C_Line, ':')) {
$endDate = date('Y-m-d H:i:s', strtotime($json->C_Line));
if ($endDate) {
$this->end_date = $endDate;
}
}
$this->saveQuietly();
}
public static function cleanJson($jsonString): string
{
if (is_string($jsonString)) {
$jsonString = str_replace(['```json', '```'], '', $jsonString);
$jsonString = trim($jsonString);
$data = json_decode($jsonString, true);
}
if (is_object($jsonString)) {
$data = json_decode(json_encode($jsonString), true);
}
if ($data) {
$data = self::cleanOptionalAttributes($data);
}
return json_encode($data, JSON_UNESCAPED_UNICODE);
}
public static function cleanOptionalAttributes($data): array
{
$optionalAttributes = ['Reference', 'CheckNeeded'];
if (is_array($data)) {
foreach ($data as $key => $value) {
if (is_array($value)) {
$data[$key] = self::cleanOptionalAttributes($value);
} elseif (in_array($key, $optionalAttributes)) {
unset($data[$key]);
}
}
}
return $data;
}
public function minifyJson($string)
{
if (substr_count($string, '”')) {
$string = str_replace('”', '\"', $string);
}
$data = json_decode($string, true);
$removeAllExcept = ['A_Line', 'B_Line', 'C_Line', 'D_Line', 'E_Line', 'F_Line', 'G_Line'];
foreach ($data as $key => $value) {
if (!in_array($key, $removeAllExcept)) {
unset($data[$key]);
}
}
return json_encode($data, JSON_UNESCAPED_UNICODE);
}
public function validateDataFormat($json)
{
$iniJson = $json;
if (is_string($json)) {
$json = json_decode($json);
}
$validator = new Validator();
$schema = str_replace('const', 'pattern', file_get_contents(base_path('public/schema.json')));
$schema = json_decode($schema);
$validator->validate($json, (object) $schema);
$errors = [];
if (!$validator->isValid()) {
foreach ($validator->getErrors() as $error) {
if (!empty($error['constraint']['params'])) {
foreach ($error['constraint']['params'] as $key => $value) {
if (is_array($value)) {
$value = implode(', ', $value);
}
$error['message'] = str_ireplace($value, ":$key", $error['message']);
$error['constraint']['params'][$key] = __($value, [], 'ru');
}
$errors[] = $error['property'] . ": " . __(
$error['message'],
$error['constraint']['params'], 'ru'
);
} else {
$errors[] = $error['property'] . ": " . __($error['message'], [], 'ru');
}
}
}
$errors = self::compareDates($json, $errors);
if ($this->id) {
if (!empty($errors)) {
if ($iniJson == $this->correct_response) {
$this->gpt_validation_errors = json_encode($errors);
}
if ($iniJson == $this->gpt41_correct_response) {
$this->gpt41_validation_errors = json_encode($errors);
}
} else {
if ($iniJson == $this->correct_response) {
$this->gpt_validation_errors = '';
}
if ($iniJson == $this->gpt41_correct_response) {
$this->gpt41_validation_errors = '';
}
}
$this->saveQuietly();
}
return $errors;
}
public static function compareDates($json, $errors): array
{
if (!is_array($json) && !is_object($json)) {
return $errors;
}
foreach ($json as $k => $value) {
if (is_object($value) || is_array($value)) {
$errors = self::compareDates($value, $errors);
} else {
if ($k === 'startDate') {
$startDate = $json->startDate;
if (property_exists($json, 'startTime') && gettype($json->startTime) !== "undefined") {
$startDate .= ' ' . $json->startTime[0] . $json->startTime[1] . ':' . ($json->startTime[2] ?? 0) . ($json->startTime[3] ?? 0);
}
$endDate = null;
if (property_exists($json, 'endDate') && gettype($json->endDate) !== "undefined" && $json->endDate !== 'PERM') {
$endDate = $json->endDate;
if (property_exists($json, 'endTime') && gettype($json->endTime) !== "undefined") {
$endDate .= ' ' . $json->endTime[0] . $json->endTime[1] . ':' . ($json->endTime[2] ?? 0) . ($json->endTime[3] ?? 0);
}
} else {
if ($json->endDate !== 'PERM') {
$errors[] = 'Отсутствует дата окончания';
continue;
}
}
if ($startDate && $endDate) {
if (strtotime($startDate) > strtotime($endDate)) {
$errors[] = 'Время начала действия больше времени окончания: ' . $startDate . ' > ' . $endDate;
}
}
} elseif ($k === 'B_Line') {
$startDate = self::textToDate($json->B_Line);
$endDate = null;
if (property_exists($json, 'C_Line') && gettype($json->C_Line) !== 'undefined' && $json->C_Line !== 'PERM') {
$endDate = self::textToDate($json->C_Line);
} elseif (!property_exists($json, 'C_Line') || $json->C_Line !== 'PERM') {
$errors[] = 'Отсутствует дата окончания (C_Line)';
continue;
}
if ($startDate && $endDate) {
if (strtotime($startDate) > strtotime($endDate)) {
$errors[] = 'Время начала действия NOTAM больше времени окончания: ' . $startDate . ' > ' . $endDate;
}
}
}
}
}
return $errors;
}
public static function textToDate($t)
{
$date = $t;
if (strlen($t) === 10) {
$date = '20' . $t[0] . $t[1] . '-' . $t[2] . $t[3] . '-' . $t[4] . $t[5] . ' ' . $t[6] . $t[7] . ":" . $t[8] . $t[9];
}
return $date;
}
public function processNotamFromQueue()
{
try {
if (!$this->prompt_id) {
return ['message' => 'Prompt ID is missing'];
}
$localModelMatchPercent = 0;
$prompt = Prompt::query()->find($this->prompt_id);
$promptText = $prompt->prompt . "\n" . $prompt->json;
$response = callGptProxi([
'object_id' => $this->id,
'input' => $this->getText(),
'key' => env('OPEN_AI_KEY'),
'prompt' => $promptText,
'model' => $prompt->model,
'temperature' => (float) $prompt->temperature,
'max_tokens' => self::MAX_TOKENS,
], 'notamNew');
$usage = [
'prompt_tokens' => 0,
'completion_tokens' => 0,
'total_tokens' => 0,
];
// Gpt Model result
if (property_exists($response, 'choices')) {
$usage = (array) $response->usage;
$message = $response->choices[0]->message->content ?? '';
$message = $this->prepareJson($message);
$messageData = json_decode($message);
if (is_object($messageData)) {
if (!property_exists($messageData, 'CheckNeeded') || !$messageData->CheckNeeded) {
$messageData->CheckNeeded = true; // Force human to check GPT response
$message = json_encode($messageData);
}
} else {
$response = callGptProxi([
'object_id' => $this->id,
'input' => $message,
'key' => env('OPEN_AI_KEY'),
'prompt' => 'Make this json to valid. Dont change data. If needed - you can cut small part of string at the end and close json, to fin 4096 tokens output',
'model' => $prompt->model,
'temperature' => (float) $prompt->temperature,
'max_tokens' => self::MAX_TOKENS,
], 'notamNew');
$message = $response->choices[0]->message->content ?? '';
if (substr_count($message, '”')) {
$message = str_replace('”', '\"', $message);
}
$message = $this->prepareJson($message);
$messageData = json_decode($message);
if (!is_object($messageData)) {
$this->comments = "Not valid response from OpenAI after second attempt. " . json_encode($response);
} elseif (!property_exists($messageData, 'CheckNeeded') || !$messageData->CheckNeeded) {
$messageData->CheckNeeded = true;
$message = json_encode($messageData);
}
}
} elseif (property_exists($response, 'output')) {
// Local model result
$output = $response->output;
$message = is_string($output) ? $output : json_encode($output);
$message = $this->prepareJson($message);
$messageData = json_decode($message);
$messageData->CheckNeeded = true;
$message = json_encode($messageData);
} else {
$this->comments = json_encode($response);
$this->saveQuietly();
}
// Gpt Model result
if (property_exists($response, 'choices')) {
if (substr_count($prompt->model, '4.1-mini')) {
$this->gpt41_output = $message;
$this->gpt41_correct_response = $message;
$this->gpt41_response = json_encode($response);
$this->gpt41_prompt_tokens = $usage['prompt_tokens'];
$this->gpt41_completion_tokens = $usage['completion_tokens'];
$this->gpt41_total_tokens = $usage['total_tokens'];
$this->fine_tuning = 0; // Reset fine tuning flag when new GPT response is received
$this->check_required_gpt41 = true; // Set CheckNeeded to true when new GPT response is received
// If no errors after GPT response - set flag CheckNeeded to false, but don't change Fine_tuning flag
$validationErrors = $this->validateDataFormat($this->gpt41_output);
if (!count($validationErrors)
|| (count($validationErrors) == 1 && substr_count($validationErrors[0], 'CheckNeeded'))) {
$messageData->CheckNeeded = false;
$message = json_encode($messageData);
$this->gpt41_output = $message;
$this->gpt41_correct_response = $message; // Fill correct response with GPT model response to make filters work
} else {
// Temporary disabled to save responses from GPT 4.1
// return $validationErrors;
}
} else {
$this->output = $message;
$this->correct_response = $message;
$this->chatgpt_response = json_encode($response);
$this->prompt_tokens = $usage['prompt_tokens'];
$this->completion_tokens = $usage['completion_tokens'];
$this->total_tokens = $usage['total_tokens'];
$this->fine_tuning = 0; // Reset fine tuning flag when new GPT response is received
$this->check_required_gpt = true; // Set CheckNeeded to true when new GPT response is received
// If no errors after GPT response - set flag CheckNeeded to false, but don't change Fine_tuning flag
$validationErrors = $this->validateDataFormat($this->output);
if (!count($validationErrors)
|| (count($validationErrors) == 1 && substr_count($validationErrors[0], 'CheckNeeded'))) {
$messageData->CheckNeeded = false;
$message = json_encode($messageData);
$this->output = $message;
$this->correct_response = $message; // Fill correct response with GPT model response to make filters work
} else {
return $validationErrors;
}
}
} else {
// Local model response
$this->local_output = $message;
$this->local_correct_response = $message; // Fill correct response with Local model response to make filters work
$this->local_response = json_encode($response); // Full Model response
if ($message && !substr_count($message, 'process in progress')) {
// If no errors after Local response - set flag CheckNeeded to false, but don't change Fine_tuning flag
$validationErrors = $this->validateDataFormat($this->local_output);
if (!count($validationErrors) || (count($validationErrors) == 1 && substr_count($validationErrors[0], 'CheckNeeded'))) {
$messageData->CheckNeeded = false;
$message = json_encode($messageData);
$this->local_output = $message;
$this->local_correct_response = $message; // Fill correct response with Local model response to make filters work
}
$this->compareGptResponses($this->correct_response, $this->local_correct_response);
} else {
$this->saveQuietly();
return $message;
}
}
$this->updated_at = date('Y-m-d H:i:s');
$this->saveQuietly(); // calling saveQuietly, because Validate() is called on Save and notam can be marked as Valid when we don't want it
$this->updateDates();
} catch (\Exception $exception) {
echo $exception->getMessage();
return false;
}
return true;
}
public static function getStats()
{
$prompt = Prompt::query()->where('type', 'notam')->where('is_active', 1)->latest()->first();
$fineTuningJobs = FineTuningJob::query()->where('prompt_id', $prompt->id)->orderBy('created_at', 'desc')->get();
$lastJobDate = date('Y-m-d H:i:s');
if ($fineTuningJobs->first()) {
$lastJobDate = $fineTuningJobs->first()->created_at;
}
$inProgressResponse = json_encode([
'status' => Notam::IN_PROGRESS_STATUS,
]);
$data = [
'gptProcessed' => Notam::query()->whereNotNull('chatgpt_response')->count(),
// 'localProcessed' => Notam::query()
// ->whereNotNull('local_correct_response')
// ->where('local_correct_response', '<>', $inProgressResponse)
// ->count(),
'gpt41Processed' => Notam::query()
->whereNotNull('gpt41_correct_response')
->where('gpt41_correct_response', '<>', $inProgressResponse)
->count(),
'gptEmpty' => Notam::query()
->whereNull('correct_response')
->orWhere('correct_response', '')->count(),
// 'localEmpty' => Notam::query()
// ->whereNull('local_response')
// ->where('created_at', '>', '2025-12-02')
// ->count(),
'gpt41Empty' => Notam::query()
->whereNull('gpt41_response')
->where('created_at', '>', '2026-04-01')
->count(),
'gptInProgress' => Notam::query()
->whereNotNull('correct_response')
->where('correct_response', $inProgressResponse)
->count(),
// 'localInProgress' => Notam::query()
// ->whereNotNull('local_correct_response')
// ->where('local_correct_response', $inProgressResponse)
// ->count(),
'gpt41InProgress' => Notam::query()
->whereNotNull('gpt41_correct_response')
->where('gpt41_correct_response', $inProgressResponse)
->count(),
'checkNeeded' => Notam::query()
->where('check_required_gpt', true)
->count(),
// 'localCheckNeeded' => Notam::query()
// ->where('check_required_local', true)
// ->count(),
'gpt41CheckNeeded' => Notam::query()
->where('check_required_gpt41', true)
->count(),
'actual' => Notam::query()
->whereRaw("start_date <= ?", [date('Y-m-d H:i')])
->where(function ($query) {
$query->whereRaw("end_date >= ?", [date('Y-m-d H:i')])
->orWhereRaw("end_date is NULL");
})->count(),
'archived' => Notam::query()
->where(function ($query) {
$query->whereRaw("start_date > ?", [date('Y-m-d H:i')])
->orWhereRaw("end_date < ?", [date('Y-m-d H:i')]);
})
->whereRaw("end_date IS NOT NULL")
->count(),
'geoJsonInvalid' => Notam::query()
->where('geo_validation_error', 1)
->count(),
'gpt41geoJsonInvalid' => Notam::query()
->where('geo_validation_error', 1)
->count(),
'validationErrors' => Notam::query()
->whereNotNull('gpt_validation_errors')
->where('gpt_validation_errors', '<>', '')
->count(),
'validationErrorsGpt41' => Notam::query()
->whereNotNull('gpt41_correct_response')
->whereNotNull('gpt41_validation_errors')
->where('gpt41_validation_errors', '<>', '')
->count(),
'noValidationErrors' => Notam::query()
->whereNull('gpt_validation_errors')
->orWhere('gpt_validation_errors', '=', '')
->count(),
'noValidationErrorsGpt41' => Notam::query()
->whereNotNull('gpt41_correct_response')
->where(function ($query) {
$query->whereNull('gpt41_validation_errors')
->orWhere('gpt41_validation_errors', '=', '');
})
->count(),
];
$data['noCheckNeeded'] = $data['gptProcessed'] - $data['checkNeeded'] - $data['gptInProgress'];
// $data['noLocalCheckNeeded'] = $data['localProcessed'] - $data['localCheckNeeded'] - $data['localInProgress'];
$data['noGpt41CheckNeeded'] = $data['gpt41Processed'] - $data['gpt41CheckNeeded'] - $data['gpt41InProgress'];
$data['averageGptProcessingTime'] = Notam::query()
->whereNotNull('gpt_processing_time')
->where('gpt_processing_time', '>', 0)
->avg('gpt_processing_time');
// Format seconds to human readable
$data['averageGptProcessingTime'] = gmdate("i:s", floor($data['averageGptProcessingTime']));
// $data['averageLocalProcessingTime'] = Notam::query()
// ->whereNotNull('local_processing_time')
// ->where('local_processing_time', '>', 0)
// ->avg('local_processing_time');
//
// // Format seconds to human readable
// $data['averageLocalProcessingTime'] = gmdate("i:s", floor($data['averageLocalProcessingTime']));
$data['averageGpt41ProcessingTime'] = Notam::query()
->whereNotNull('gpt41_processing_time')
->where('gpt41_processing_time', '>', 0)
->avg('gpt41_processing_time');
// Format seconds to human readable
$data['averageGpt41ProcessingTime'] = gmdate("i:s", floor($data['averageGpt41ProcessingTime']));
$data['jobsCount'] = DB::table('jobs')->where('payload', 'like', '%notam%')->count();
$data['failedJobsCount'] = DB::table('failed_jobs')->where('payload', 'like', '%notam%')->count();
return $data;
}
public function deleteBadDuplicates()
{
if ($this->id && $this->request_hash) {
Notam::query()
->where('id', '!=', $this->id)
->where('request_hash', $this->request_hash)
->where(function ($query) {
$query->where('fine_tuning', 0)
->orWhere('correct_response', 'like', "%\"CheckNeeded\":true%");
})->delete();
}
}
public function sendToRag($type = 'local')
{
$data['model'] = 'local'; // To make call to N8N local LLM proxy
$input = $this->input;
if (substr_count($input, '"Text"')) {
$input = json_decode($input, true);
$input = $input['Text'];
}
if ($type === 'gpt') {
$endpoint = 'dataToRagGpt';
if (!$this->correct_response) {
return true;
}
echo $input . ' ' . $this->correct_response . "\n\n";
$data['input'] = json_encode([
'input' => $input,
'output' => $this->correct_response,
]);
} else {
$endpoint = 'dataToRag';
if (!$this->local_correct_response) {
return true;
}
echo $input . ' ' . $this->local_correct_response . "\n\n";
$data['input'] = json_encode([
'input' => $input,
'output' => $this->local_correct_response,
]);
}
$result = callGptProxi($data, $endpoint, 'rag');
return $result;
}
public function calculateGptCost()
{
$allCosts = [
'gpt-5.2' => ['input' => 1.75, 'output' => 14.00],
'gpt-5.1' => ['input' => 1.25, 'output' => 10.00],
'gpt-5' => ['input' => 1.25, 'output' => 10.00],
'gpt-5-mini' => ['input' => 0.25, 'output' => 2.00],
'gpt-5-nano' => ['input' => 0.05, 'output' => 0.40],
'gpt-5.2-chat-latest' => ['input' => 1.75, 'output' => 14.00],
'gpt-5.1-chat-latest' => ['input' => 1.25, 'output' => 10.00],
'gpt-5-chat-latest' => ['input' => 1.25, 'output' => 10.00],
'gpt-5.1-codex-max' => ['input' => 1.25, 'output' => 10.00],
'gpt-5.1-codex' => ['input' => 1.25, 'output' => 10.00],
'gpt-5-codex' => ['input' => 1.25, 'output' => 10.00],
'gpt-5.2-pro' => ['input' => 21.00, 'output' => 168.00],
'gpt-5-pro' => ['input' => 15.00, 'output' => 120.00],
'gpt-4.1' => ['input' => 2.00, 'output' => 8.00],
'gpt-4.1-mini' => ['input' => 0.40, 'output' => 1.60],
'gpt-4.1-nano' => ['input' => 0.10, 'output' => 0.40],
'gpt-4o' => ['input' => 2.50, 'output' => 10.00],
'gpt-4o-2024-05-13' => ['input' => 5.00, 'output' => 15.00],
'gpt-4o-mini' => ['input' => 0.15, 'output' => 0.60],
];
$cost = 0;
if ($this->model && $this->prompt_tokens !== null && $this->completion_tokens !== null) {
$modelKey = strtolower($this->model);
if (!empty($allCosts[$modelKey])) {
if (array_key_exists($modelKey, $allCosts)) {
$costPerMillionInput = $allCosts[$modelKey]['input'];
$costPerMillionOutput = $allCosts[$modelKey]['output'];
$inputCost = round(($this->prompt_tokens / 1000000) * $costPerMillionInput, 6);
$outputCost = round(($this->completion_tokens / 1000000) * $costPerMillionOutput, 6);
$cost += $inputCost + $outputCost;
}
}
}
if ($this->gpt41_prompt_tokens !== null && $this->gpt41_completion_tokens !== null) {
$modelKey = 'gpt-4.1-mini';
if (!empty($allCosts[$modelKey])) {
if (array_key_exists($modelKey, $allCosts)) {
$costPerMillionInput = $allCosts[$modelKey]['input'];
$costPerMillionOutput = $allCosts[$modelKey]['output'];
$inputCost = round(($this->gpt41_prompt_tokens / 1000000) * $costPerMillionInput, 6);
$outputCost = round(($this->gpt41_completion_tokens / 1000000) * $costPerMillionOutput, 6);
$cost += $inputCost + $outputCost;
}
}
}
return round($cost, 6);
}
public function getMostActualResponse() {
if ($this->gpt41_correct_response) {
return $this->gpt41_correct_response;
}
if ($this->gpt41_output) {
return $this->gpt41_output;
}
return $this->correct_response ?? $this->output;
}
}