- Created XmlFiles control class for handling XML file views and regeneration. - Implemented method to retrieve clients with XML feeds in the factory class. - Added database migration to include google_merchant_account_id in clients table. - Created migrations for products_keyword_planner_terms and products_merchant_sync_log tables. - Added campaign_keywords table migration for managing campaign keyword data. - Developed main view template for displaying XML files and their statuses. - Introduced a debug script for analyzing product URLs and their statuses.
458 lines
18 KiB
PHP
458 lines
18 KiB
PHP
<?php
|
|
namespace services;
|
|
class OpenAiApi
|
|
{
|
|
static private $api_url = 'https://api.openai.com/v1/chat/completions';
|
|
|
|
static private $system_prompt = 'Jesteś ekspertem od optymalizacji feedów produktowych Google Merchant Center i reklam Google Ads (Performance Max, Shopping).
|
|
|
|
OFICJALNE ZASADY GOOGLE MERCHANT CENTER — BEZWZGLĘDNIE PRZESTRZEGAJ:
|
|
|
|
TYTUŁY (title) — max 150 znaków:
|
|
Zalecana struktura: [Marka] [Typ produktu] [Kluczowe atrybuty: kolor, rozmiar, materiał — TYLKO jeśli wynikają z nazwy]
|
|
- Najważniejsze informacje umieść w pierwszych 70 znakach (reszta może być obcięta na mobile)
|
|
- Każdy wariant produktu musi mieć unikalny tytuł (różne kolory/rozmiary = różne tytuły)
|
|
- Tytuł MUSI być spójny z nazwą produktu na stronie docelowej
|
|
- Używaj naturalnego języka — pisz dla ludzi, nie dla algorytmów
|
|
ZAKAZANE w tytułach:
|
|
- Tekst promocyjny: bestseller, hit, okazja, wyprzedaż, rabat, najlepszy, TOP, #1, idealny, polecamy, nowość
|
|
- Wykrzykniki (!) i nadmierna interpunkcja
|
|
- WIELKIE LITERY (wyjątek: ustalone skróty jak LED, USB, TV)
|
|
- Emotikony, emoji, symbole dekoracyjne (★, ♥, ✓)
|
|
- Informacje o cenie, dostawie, promocji
|
|
- Wezwania do działania (kup teraz, sprawdź, zamów)
|
|
- Upychanie słów kluczowych (keyword stuffing) — np. powtarzanie tych samych fraz
|
|
- Kody wewnętrzne, SKU, numery katalogowe
|
|
- Cechy produktu których NIE MA w oryginalnej nazwie (nie wymyślaj koloru, materiału, rozmiaru)
|
|
|
|
OPISY (description) — max 5000 znaków, ale najważniejsze info w pierwszych 160-500 znakach:
|
|
- Opisz cechy, specyfikacje techniczne, zastosowanie, grupę docelową
|
|
- Wymień atrybuty niewidoczne na zdjęciu (materiał, wzór, przeznaczenie wiekowe)
|
|
- Ton neutralny, informacyjny — jak w katalogu produktowym
|
|
ZAKAZANE w opisach:
|
|
- Tekst promocyjny i reklamowy (te same słowa co w tytułach)
|
|
- Nazwa firmy/sklepu (chyba że to marka produktu)
|
|
- Informacje o wysyłce, cenach, promocjach
|
|
- Wezwania do działania
|
|
- Opisy innych produktów, akcesoriów nie wchodzących w skład oferty
|
|
- Tekst zastępczy (lorem ipsum, placeholder)
|
|
- Powielanie tego samego opisu dla wielu produktów
|
|
- HTML, tagi formatowania
|
|
|
|
OGÓLNE:
|
|
- Pisz poprawną polszczyzną (ortografia, gramatyka, interpunkcja)
|
|
- Używaj pisowni zdaniowej lub tytułowej — NIE CAPS
|
|
- Dane w feedzie MUSZĄ zgadzać się z danymi na stronie produktowej
|
|
- Opisuj TYLKO cechy wynikające z nazwy produktu — nie wymyślaj
|
|
|
|
Twoje odpowiedzi muszą być:
|
|
- Ściśle zgodne z polityką Google Merchant Center
|
|
- Zgodne z prawdą (opisuj tylko cechy widoczne w nazwie produktu)
|
|
- Zoptymalizowane pod kątem trafności wyszukiwań w Google Ads
|
|
- W języku polskim';
|
|
|
|
static public function is_configured()
|
|
{
|
|
return (bool) GoogleAdsApi::get_setting( 'openai_api_key' );
|
|
}
|
|
|
|
static public function fetch_page_content( $url )
|
|
{
|
|
$ch = curl_init( $url );
|
|
curl_setopt_array( $ch, [
|
|
CURLOPT_RETURNTRANSFER => true,
|
|
CURLOPT_FOLLOWLOCATION => true,
|
|
CURLOPT_TIMEOUT => 10,
|
|
CURLOPT_USERAGENT => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
|
|
CURLOPT_HTTPHEADER => [
|
|
'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
|
'Accept-Language: pl-PL,pl;q=0.9,en;q=0.8',
|
|
],
|
|
CURLOPT_SSL_VERIFYPEER => false
|
|
] );
|
|
|
|
$html = curl_exec( $ch );
|
|
$http_code = curl_getinfo( $ch, CURLINFO_HTTP_CODE );
|
|
curl_close( $ch );
|
|
|
|
if ( $http_code !== 200 || !$html ) return '';
|
|
|
|
// Usuń skrypty, style, komentarze
|
|
$html = preg_replace( '/<script[^>]*>.*?<\/script>/si', '', $html );
|
|
$html = preg_replace( '/<style[^>]*>.*?<\/style>/si', '', $html );
|
|
$html = preg_replace( '/<!--.*?-->/s', '', $html );
|
|
$html = preg_replace( '/<nav[^>]*>.*?<\/nav>/si', '', $html );
|
|
$html = preg_replace( '/<footer[^>]*>.*?<\/footer>/si', '', $html );
|
|
$html = preg_replace( '/<header[^>]*>.*?<\/header>/si', '', $html );
|
|
|
|
// Zamień tagi na spacje i wyciągnij tekst
|
|
$text = strip_tags( $html );
|
|
$text = html_entity_decode( $text, ENT_QUOTES, 'UTF-8' );
|
|
$text = preg_replace( '/\s+/', ' ', $text );
|
|
$text = trim( $text );
|
|
|
|
// Ogranicz do ~3000 znaków
|
|
if ( mb_strlen( $text ) > 3000 )
|
|
$text = mb_substr( $text, 0, 3000 ) . '...';
|
|
|
|
return $text;
|
|
}
|
|
|
|
static private function call_api( $system_prompt, $user_prompt, $max_tokens = 500, $temperature = 0.7, $extra_payload = [] )
|
|
{
|
|
$api_key = GoogleAdsApi::get_setting( 'openai_api_key' );
|
|
$model = GoogleAdsApi::get_setting( 'openai_model' ) ?: 'gpt-5-mini';
|
|
$is_gpt5_model = ( strpos( $model, 'gpt-5' ) === 0 );
|
|
|
|
// GPT-5.x wymaga max_completion_tokens, starsze modele używają max_tokens
|
|
$tokens_key = $is_gpt5_model ? 'max_completion_tokens' : 'max_tokens';
|
|
|
|
$payload = [
|
|
'model' => $model,
|
|
'messages' => [
|
|
[ 'role' => 'system', 'content' => $system_prompt ],
|
|
[ 'role' => 'user', 'content' => $user_prompt ]
|
|
],
|
|
$tokens_key => $max_tokens
|
|
];
|
|
|
|
// Modele GPT-5 (w tym gpt-5-mini) nie wspierają niestandardowej temperatury.
|
|
if ( !$is_gpt5_model )
|
|
{
|
|
$payload['temperature'] = $temperature;
|
|
}
|
|
|
|
if ( is_array( $extra_payload ) && !empty( $extra_payload ) )
|
|
{
|
|
$payload = array_merge( $payload, $extra_payload );
|
|
}
|
|
|
|
$ch = curl_init( self::$api_url );
|
|
curl_setopt_array( $ch, [
|
|
CURLOPT_RETURNTRANSFER => true,
|
|
CURLOPT_POST => true,
|
|
CURLOPT_HTTPHEADER => [
|
|
'Content-Type: application/json',
|
|
'Authorization: Bearer ' . $api_key
|
|
],
|
|
CURLOPT_POSTFIELDS => json_encode( $payload ),
|
|
CURLOPT_TIMEOUT => 30
|
|
] );
|
|
|
|
$response = curl_exec( $ch );
|
|
$http_code = curl_getinfo( $ch, CURLINFO_HTTP_CODE );
|
|
curl_close( $ch );
|
|
|
|
if ( $http_code !== 200 )
|
|
{
|
|
$error = json_decode( $response, true );
|
|
return [ 'status' => 'error', 'message' => $error['error']['message'] ?? 'Błąd API OpenAI (HTTP ' . $http_code . ')' ];
|
|
}
|
|
|
|
$data = json_decode( $response, true );
|
|
$content = trim( $data['choices'][0]['message']['content'] ?? '' );
|
|
|
|
return [ 'status' => 'ok', 'suggestion' => $content ];
|
|
}
|
|
|
|
static private function build_context_text( $context )
|
|
{
|
|
$lines = [];
|
|
$lines[] = 'Nazwa produktu: ' . ( $context['original_name'] ?? '—' );
|
|
|
|
if ( !empty( $context['current_title'] ) )
|
|
$lines[] = 'Obecny tytuł (custom): ' . $context['current_title'];
|
|
|
|
if ( !empty( $context['current_description'] ) )
|
|
$lines[] = 'Obecny opis: ' . $context['current_description'];
|
|
|
|
if ( !empty( $context['current_category'] ) )
|
|
$lines[] = 'Obecna kategoria Google: ' . $context['current_category'];
|
|
|
|
if ( !empty( $context['offer_id'] ) )
|
|
$lines[] = 'ID oferty: ' . $context['offer_id'];
|
|
|
|
if ( !empty( $context['custom_label_4'] ) )
|
|
$lines[] = 'Status produktu: ' . $context['custom_label_4'];
|
|
|
|
$lines[] = '';
|
|
$lines[] = 'Metryki reklamowe (ostatnie 30 dni):';
|
|
$lines[] = '- Wyświetlenia: ' . ( $context['impressions_30'] ?? 0 );
|
|
$lines[] = '- Kliknięcia: ' . ( $context['clicks_30'] ?? 0 );
|
|
$lines[] = '- CTR: ' . ( $context['ctr'] ?? 0 ) . '%';
|
|
$lines[] = '- Koszt: ' . ( $context['cost'] ?? 0 ) . ' PLN';
|
|
$lines[] = '- Konwersje: ' . ( $context['conversions'] ?? 0 );
|
|
$lines[] = '- Wartość konwersji: ' . ( $context['conversions_value'] ?? 0 ) . ' PLN';
|
|
$lines[] = '- ROAS: ' . ( $context['roas'] ?? 0 ) . '%';
|
|
|
|
if ( !empty( $context['page_content'] ) )
|
|
{
|
|
$lines[] = '';
|
|
$lines[] = 'Treść ze strony produktu (użyj tych informacji do stworzenia dokładniejszego opisu):';
|
|
$lines[] = $context['page_content'];
|
|
}
|
|
|
|
return implode( "\n", $lines );
|
|
}
|
|
|
|
static public function suggest_title( $context )
|
|
{
|
|
$context_text = self::build_context_text( $context );
|
|
$keyword_planner_text = '';
|
|
|
|
if ( !empty( $context['keyword_planner_terms'] ) && is_array( $context['keyword_planner_terms'] ) )
|
|
{
|
|
$keyword_lines = [];
|
|
$keyword_lines[] = 'Najpopularniejsze frazy z Google Ads Keyword Planner (na bazie URL produktu, posortowane malejąco po średniej liczbie wyszukiwań):';
|
|
|
|
foreach ( array_slice( $context['keyword_planner_terms'], 0, 15 ) as $term )
|
|
{
|
|
$text = trim( (string) ( $term['keyword_text'] ?? '' ) );
|
|
if ( $text === '' )
|
|
{
|
|
continue;
|
|
}
|
|
|
|
$avg_monthly = (int) ( $term['avg_monthly_searches'] ?? 0 );
|
|
$keyword_lines[] = '- ' . $text . ' (avg miesięcznie: ' . $avg_monthly . ')';
|
|
}
|
|
|
|
if ( count( $keyword_lines ) > 1 )
|
|
{
|
|
$keyword_lines[] = 'Użyj tych fraz WYBIÓRCZO i naturalnie (bez upychania słów kluczowych), tylko jeśli pasują do produktu.';
|
|
$keyword_planner_text = "\n\n" . implode( "\n", $keyword_lines );
|
|
}
|
|
}
|
|
|
|
$prompt = 'Zaproponuj zoptymalizowany tytuł produktu dla Google Merchant Center.
|
|
|
|
WYMAGANIA:
|
|
- Max 150 znaków, najważniejsze info w pierwszych 70 znakach
|
|
- Struktura: [Marka jeśli jest w nazwie] [Typ produktu] [Kluczowe cechy z nazwy] [Dla kogo/okazja jeśli wynika z nazwy]
|
|
- Pisownia zdaniowa lub tytułowa, naturalny język
|
|
- Tytuł musi brzmieć jak wpis w katalogu produktowym
|
|
|
|
BEZWZGLĘDNY ZAKAZ (odrzucenie przez Google):
|
|
- Słowa promocyjne: bestseller, hit, idealny, najlepszy, polecamy, okazja, nowość, TOP
|
|
- Wykrzykniki (!), emotikony, symbole dekoracyjne
|
|
- WIELKIE LITERY (wyjątek: LED, USB, TV)
|
|
- Wezwania do działania, informacje o cenie/dostawie
|
|
- Cechy wymyślone — opisuj TYLKO to co wynika z oryginalnej nazwy lub treści strony produktu
|
|
- Jeśli podano treść ze strony produktu, wykorzystaj ją do wzbogacenia tytułu o rzeczywiste cechy (marka, materiał, kolor, rozmiar itp.)
|
|
|
|
' . $context_text . $keyword_planner_text . '
|
|
|
|
Zwróć TYLKO tytuł, bez cudzysłowów, bez wyjaśnień.';
|
|
|
|
return self::call_api( self::$system_prompt, $prompt );
|
|
}
|
|
|
|
static public function suggest_description( $context )
|
|
{
|
|
$context_text = self::build_context_text( $context );
|
|
$has_page = !empty( $context['page_content'] );
|
|
$keyword_planner_text = '';
|
|
|
|
if ( !empty( $context['keyword_planner_terms'] ) && is_array( $context['keyword_planner_terms'] ) )
|
|
{
|
|
$keyword_lines = [];
|
|
$keyword_lines[] = 'Najpopularniejsze frazy z Google Ads Keyword Planner (na bazie URL produktu, posortowane malejąco po średniej liczbie wyszukiwań):';
|
|
|
|
foreach ( array_slice( $context['keyword_planner_terms'], 0, 15 ) as $term )
|
|
{
|
|
$text = trim( (string) ( $term['keyword_text'] ?? '' ) );
|
|
if ( $text === '' )
|
|
{
|
|
continue;
|
|
}
|
|
|
|
$avg_monthly = (int) ( $term['avg_monthly_searches'] ?? 0 );
|
|
$keyword_lines[] = '- ' . $text . ' (avg miesięcznie: ' . $avg_monthly . ')';
|
|
}
|
|
|
|
if ( count( $keyword_lines ) > 1 )
|
|
{
|
|
$keyword_lines[] = 'W opisie wykorzystuj te frazy naturalnie i wyłącznie gdy realnie pasują do produktu (bez keyword stuffing).';
|
|
$keyword_planner_text = "\n\n" . implode( "\n", $keyword_lines );
|
|
}
|
|
}
|
|
|
|
$length_guide = $has_page
|
|
? '- Napisz rozbudowany, szczegółowy opis: ok. 1000 znaków (800-1200)
|
|
- Wykorzystaj szczegóły ze strony produktu: skład zestawu, materiały, wymiary, kolory, przeznaczenie
|
|
- Każdy akapit/punkt powinien wnosić NOWĄ informację — NIE powtarzaj tych samych elementów'
|
|
: '- Napisz opis o długości ok. 1000 znaków (800-1200) — jeśli brak szczegółów, opisz ogólnie zastosowanie i grupę docelową
|
|
- Opisuj TYLKO to co wynika z oryginalnej nazwy — nie wymyślaj konkretnych parametrów';
|
|
|
|
$prompt = 'Napisz zoptymalizowany opis produktu dla Google Merchant Center.
|
|
|
|
WYMAGANIA:
|
|
' . $length_guide . '
|
|
- Najważniejsze info na początku (pierwsze 160 znaków widoczne w wynikach)
|
|
- Rzeczowo opisz: co to jest, z czego się składa, do czego służy, dla kogo
|
|
- Ton neutralny, informacyjny — jak w specyfikacji produktowej
|
|
- Każdy akapit musi zawierać INNĄ informację niż poprzedni — NIGDY nie powtarzaj tych samych elementów
|
|
|
|
FORMATOWANIE — Google Merchant Center obsługuje podstawowe HTML:
|
|
- Używaj <br> do oddzielania akapitów/sekcji
|
|
- Używaj <b>pogrubienia</b> dla kluczowych cech (np. nazwy elementów zestawu, materiał)
|
|
- Używaj <ul><li>...</li></ul> gdy wymieniasz elementy zestawu lub listę cech
|
|
- NIE używaj innych tagów HTML (h1, p, div, span, img, a, table itp.)
|
|
- NIE używaj Markdown — tylko dozwolone tagi HTML
|
|
|
|
BEZWZGLĘDNY ZAKAZ (odrzucenie przez Google):
|
|
- Słowa promocyjne: bestseller, hit, okazja, najlepszy, polecamy, nowość, idealny
|
|
- Wykrzykniki (!), emotikony, WIELKIE LITERY
|
|
- Wezwania do działania: kup teraz, zamów, sprawdź, dodaj do koszyka
|
|
- Informacje o cenie, dostawie, promocjach, nazwie sklepu
|
|
- Opisy akcesoriów/produktów nie wchodzących w skład oferty
|
|
- Cechy wymyślone — opisuj TYLKO to co wynika z nazwy lub treści strony produktu
|
|
|
|
' . $context_text . $keyword_planner_text . '
|
|
|
|
Zwróć TYLKO opis w formacie HTML (używając dozwolonych tagów), bez cudzysłowów, bez wyjaśnień.';
|
|
|
|
$tokens = $has_page ? 1500 : 1000;
|
|
return self::call_api( self::$system_prompt, $prompt, $tokens );
|
|
}
|
|
|
|
static public function suggest_category( $context, $categories = [] )
|
|
{
|
|
$context_text = self::build_context_text( $context );
|
|
|
|
$cats_text = '';
|
|
if ( !empty( $categories ) )
|
|
{
|
|
$cats_list = array_slice( $categories, 0, 50 );
|
|
$cats_text = "\n\nDostępne kategorie Google Product Taxonomy (format: id - tekst):\n";
|
|
foreach ( $cats_list as $cat )
|
|
{
|
|
$cats_text .= $cat['id'] . ' - ' . $cat['text'] . "\n";
|
|
}
|
|
}
|
|
|
|
$prompt = 'Dopasuj produkt do najlepszej kategorii Google Product Taxonomy.
|
|
Wybierz najbardziej szczegółową (najgłębszą) kategorię pasującą do produktu.
|
|
|
|
' . $context_text . $cats_text . '
|
|
|
|
Zwróć TYLKO ID kategorii (liczbę), bez wyjaśnień.';
|
|
|
|
return self::call_api( self::$system_prompt, $prompt );
|
|
}
|
|
|
|
static public function suggest_negative_keywords_to_exclude( $search_terms_rows, $analysis_context = [] )
|
|
{
|
|
$rows = is_array( $search_terms_rows ) ? $search_terms_rows : [];
|
|
$rows = array_slice( $rows, 0, 150 );
|
|
|
|
$campaign_name = trim( (string) ( $analysis_context['campaign_name'] ?? '' ) );
|
|
$campaign_type = trim( (string) ( $analysis_context['campaign_type'] ?? '' ) );
|
|
$ad_group_name = trim( (string) ( $analysis_context['ad_group_name'] ?? '' ) );
|
|
$ad_group_id = (int) ( $analysis_context['ad_group_id'] ?? 0 );
|
|
|
|
$context_lines = [];
|
|
$context_lines[] = 'KONTEKST KAMPANII:';
|
|
$context_lines[] = '- Nazwa kampanii: ' . ( $campaign_name !== '' ? $campaign_name : '-' );
|
|
$context_lines[] = '- Rodzaj kampanii: ' . ( $campaign_type !== '' ? $campaign_type : '-' );
|
|
if ( $ad_group_id > 0 || $ad_group_name !== '' )
|
|
{
|
|
$context_lines[] = '- Wybrana grupa reklam: ' . ( $ad_group_name !== '' ? $ad_group_name : ( '#' . $ad_group_id ) );
|
|
}
|
|
else
|
|
{
|
|
$context_lines[] = '- Wybrana grupa reklam: wszystkie';
|
|
}
|
|
|
|
$lines = [];
|
|
foreach ( $rows as $row )
|
|
{
|
|
$id = (int) ( $row['id'] ?? 0 );
|
|
if ( $id <= 0 )
|
|
{
|
|
continue;
|
|
}
|
|
|
|
$lines[] = json_encode( [
|
|
'id' => $id,
|
|
'phrase' => trim( (string) ( $row['search_term'] ?? '' ) ),
|
|
'ad_group' => trim( (string) ( $row['ad_group_name'] ?? '' ) ),
|
|
'clicks_all' => (float) ( $row['clicks_all_time'] ?? 0 ),
|
|
'cost_all' => (float) ( $row['cost_all_time'] ?? 0 ),
|
|
'value_all' => (float) ( $row['conversion_value_all_time'] ?? 0 ),
|
|
'roas_all' => (float) ( $row['roas_all_time'] ?? 0 ),
|
|
'clicks_30' => (float) ( $row['clicks_30'] ?? 0 ),
|
|
'cost_30' => (float) ( $row['cost_30'] ?? 0 ),
|
|
'value_30' => (float) ( $row['conversion_value_30'] ?? 0 ),
|
|
'roas_30' => (float) ( $row['roas_30'] ?? 0 )
|
|
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES );
|
|
}
|
|
|
|
$prompt = 'Przeanalizuj frazy wyszukiwane Google Ads i wskaż, które warto wykluczyć jako negatywne słowa kluczowe.
|
|
|
|
' . implode( "\n", $context_lines ) . '
|
|
|
|
KRYTERIA OCENY:
|
|
- Priorytet: frazy z kosztami i kliknięciami bez wartości konwersji, niski/zerowy ROAS, nietrafna intencja.
|
|
- Nie zaznaczaj wszystkich na siłę. Jeśli fraza ma potencjał, ustaw akcję "keep".
|
|
- Oceniaj zarówno dane all-time, jak i 30d.
|
|
- Powód musi być krótki, konkretny i oparty na danych.
|
|
|
|
FORMAT ODPOWIEDZI (BEZWZGLĘDNIE):
|
|
Zwróć WYŁĄCZNIE poprawny JSON (bez markdown i bez komentarzy), zgodny ze schematem:
|
|
{
|
|
"items": [
|
|
{
|
|
"id": 123,
|
|
"phrase": "fraza",
|
|
"action": "exclude" lub "keep",
|
|
"reason": "krótki powód"
|
|
}
|
|
]
|
|
}
|
|
|
|
Zasady formatu:
|
|
- Pole id musi być identyczne z wejściowym id.
|
|
- action może mieć tylko wartości: "exclude" albo "keep".
|
|
- reason max 120 znaków.
|
|
- Nie dodawaj żadnych dodatkowych pól.
|
|
|
|
DANE WEJŚCIOWE (JSONL, 1 rekord na linię):
|
|
' . implode( "\n", $lines );
|
|
|
|
$schema = [
|
|
'type' => 'json_schema',
|
|
'json_schema' => [
|
|
'name' => 'negative_keyword_recommendations',
|
|
'schema' => [
|
|
'type' => 'object',
|
|
'additionalProperties' => false,
|
|
'properties' => [
|
|
'items' => [
|
|
'type' => 'array',
|
|
'items' => [
|
|
'type' => 'object',
|
|
'additionalProperties' => false,
|
|
'properties' => [
|
|
'id' => [ 'type' => 'integer' ],
|
|
'phrase' => [ 'type' => 'string' ],
|
|
'action' => [ 'type' => 'string', 'enum' => [ 'exclude', 'keep' ] ],
|
|
'reason' => [ 'type' => 'string' ]
|
|
],
|
|
'required' => [ 'id', 'phrase', 'action', 'reason' ]
|
|
]
|
|
]
|
|
],
|
|
'required' => [ 'items' ]
|
|
],
|
|
'strict' => true
|
|
]
|
|
];
|
|
|
|
$rows_count = count( $rows );
|
|
$max_tokens = min( 6000, max( 2200, $rows_count * 30 ) );
|
|
|
|
return self::call_api( self::$system_prompt, $prompt, $max_tokens, 0.2, [ 'response_format' => $schema ] );
|
|
}
|
|
}
|