false, 'eponymous_id' => 0, 'wp_user_id' => 0, 'subject_id' => 0, 'email' => '', 'name' => '', 'matches' => array() ); if ( '' === $query ) { return $out; } $ci = $wpdb->prefix . 'aieo_customer_intelligence'; if ( is_email( $query ) ) { $row = $wpdb->get_row( $wpdb->prepare( "SELECT eponymous_id, wp_user_id, email FROM `{$ci}` WHERE email = %s LIMIT 1", sanitize_email( $query ) ), ARRAY_A ); if ( $row ) { return self::finalize( $out, (int) $row['eponymous_id'], (int) $row['wp_user_id'], (string) $row['email'] ); } $u = get_user_by( 'email', $query ); if ( $u ) { $r2 = $wpdb->get_row( $wpdb->prepare( "SELECT eponymous_id, wp_user_id, email FROM `{$ci}` WHERE wp_user_id = %d LIMIT 1", $u->ID ), ARRAY_A ); return self::finalize( $out, $r2 ? (int) $r2['eponymous_id'] : 0, (int) $u->ID, (string) $u->user_email, $u->display_name ); } return $out; } $users = get_users( array( 'search' => '*' . esc_attr( $query ) . '*', 'search_columns' => array( 'display_name', 'user_login', 'user_email', 'user_nicename' ), 'number' => 10, 'fields' => array( 'ID', 'display_name', 'user_email' ), ) ); if ( count( $users ) === 1 ) { $u = $users[0]; $r2 = $wpdb->get_row( $wpdb->prepare( "SELECT eponymous_id, wp_user_id, email FROM `{$ci}` WHERE wp_user_id = %d LIMIT 1", $u->ID ), ARRAY_A ); return self::finalize( $out, $r2 ? (int) $r2['eponymous_id'] : 0, (int) $u->ID, (string) $u->user_email, $u->display_name ); } if ( count( $users ) > 1 ) { foreach ( $users as $u ) { $out['matches'][] = array( 'wp_user_id' => (int) $u->ID, 'name' => (string) $u->display_name, 'email' => (string) $u->user_email ); } } return $out; } private static function finalize( $out, $eponymous_id, $wp_user_id, $email, $name = '' ) { $out['found'] = ( $eponymous_id > 0 || $wp_user_id > 0 ); $out['eponymous_id'] = (int) $eponymous_id; $out['wp_user_id'] = (int) $wp_user_id; $out['email'] = (string) $email; if ( '' === $name && $wp_user_id > 0 ) { $u = get_userdata( $wp_user_id ); $name = $u ? (string) $u->display_name : ''; } $out['name'] = $name; if ( $wp_user_id > 0 && class_exists( 'AIEO_Subject_Resolver' ) ) { $out['subject_id'] = (int) AIEO_Subject_Resolver::subject_for_user( $wp_user_id ); } return $out; } public static function assemble( array $ids ) { global $wpdb; $eid = (int) ( $ids['eponymous_id'] ?? 0 ); $uid = (int) ( $ids['wp_user_id'] ?? 0 ); $sid = (int) ( $ids['subject_id'] ?? 0 ); $bundle = array( 'identity' => array( 'eponymous_id' => $eid, 'wp_user_id' => $uid, 'subject_id' => $sid ), 'cdp' => array(), 'vitals' => array(), 'behaviour' => array(), 'recommendations' => array(), 'lookalikes' => array(), 'intent' => array(), ); $ci = $wpdb->prefix . 'aieo_customer_intelligence'; $row = null; if ( self::has_table( $ci ) ) { if ( $eid > 0 ) { $row = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM `{$ci}` WHERE eponymous_id = %d LIMIT 1", $eid ), ARRAY_A ); } if ( ! $row && $uid > 0 ) { $row = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM `{$ci}` WHERE wp_user_id = %d LIMIT 1", $uid ), ARRAY_A ); } } if ( $row ) { $pick = array( 'orders_count','ltv_total','aov','recency_days','rfm_cell','monetary_band','frequency_band','recency_band', 'price_band','avg_item_price','discount_share','top_brand_id','top_brand_share','top_category_id','top_category_share', 'brand_affinity_json','category_affinity_json','core_needs_json','replenishment_json','segments_json', 'dormancy_cohort','reactivation_score','inferred_top_priority','inferred_priorities_json','inferred_priorities_confidence', 'tnp_status','open_rate_lifetime','click_rate_lifetime','open_rate_30d','days_since_last_engagement', 'sale_purchase_cnt','new_product_purchase_cnt','gift_completed_count','monthly_offer_purchase_cnt', 'first_order_at','last_order_at' ); foreach ( $pick as $c ) { if ( array_key_exists( $c, $row ) ) { $bundle['cdp'][ $c ] = $row[ $c ]; } } $bundle['cdp']['top_category_name'] = self::term_name( (int) ( $row['top_category_id'] ?? 0 ), array( 'product_cat' ) ); $bundle['cdp']['top_brand_name'] = self::brand_name( (int) ( $row['top_brand_id'] ?? 0 ) ); $bundle['cdp']['segments'] = self::json_list( $row['segments_json'] ?? '' ); $bundle['cdp']['core_needs'] = self::term_names( self::json_list( $row['core_needs_json'] ?? '' ), array( 'pa_need' ) ); } $uv = $wpdb->prefix . 'aieo_user_vitals'; if ( $uid > 0 && self::has_table( $uv ) ) { $v = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM `{$uv}` WHERE user_id = %d LIMIT 1", $uid ), ARRAY_A ); if ( $v ) { foreach ( array( 'soft1_recorded_speciality','soft1_turnover','soft1_loyalty_points_total','eshop_loyalty_points','soft1_latest_location','soft1_first_transaction_date','soft1_latest_transaction_date' ) as $c ) { if ( array_key_exists( $c, $v ) && '' !== (string) $v[ $c ] && null !== $v[ $c ] ) { $bundle['vitals'][ $c ] = $v[ $c ]; } } } } if ( $uid > 0 && class_exists( 'AIEO_DMM_User_Behavior_Metrics' ) && method_exists( 'AIEO_DMM_User_Behavior_Metrics', 'risk_band' ) ) { $bundle['behaviour']['return_risk_band'] = (int) AIEO_DMM_User_Behavior_Metrics::risk_band( $uid ); } if ( $sid > 0 && class_exists( 'AIEO_Behavioural_Reco' ) ) { $bundle['behaviour']['has_behavioural_vector'] = class_exists( 'AIEO_Behavioural_Vector' ) && null !== AIEO_Behavioural_Vector::get_vector( $sid ); foreach ( AIEO_Behavioural_Reco::recommend( $sid, 8 ) as $r ) { $bundle['recommendations'][] = array( 'item_id' => (int) $r['item_id'], 'title' => self::product_title( (int) $r['item_id'] ), 'score' => round( (float) $r['score'], 4 ), ); } foreach ( AIEO_Behavioural_Reco::lookalikes( $sid, 5 ) as $l ) { $bundle['lookalikes'][] = array( 'subject_id' => (int) $l['subject_id'], 'similarity' => round( (float) $l['score'], 4 ) ); } $is = $wpdb->prefix . 'aieo_interaction_summary'; if ( self::has_table( $is ) ) { $rows = $wpdb->get_results( $wpdb->prepare( "SELECT item_id, views, carts, wishlists, weight, last_at FROM `{$is}` WHERE subject_id = %d AND item_type='product' AND conversions=0 AND (carts>0 OR wishlists>0 OR weight>=4) ORDER BY weight DESC LIMIT 6", $sid ), ARRAY_A ); foreach ( (array) $rows as $r ) { $bundle['intent'][] = array( 'item_id' => (int) $r['item_id'], 'title' => self::product_title( (int) $r['item_id'] ), 'views' => (int) $r['views'], 'carted' => (int) $r['carts'] > 0, 'wished' => (int) $r['wishlists'] > 0, 'last_at' => (string) $r['last_at'], ); } } } return $bundle; } public static function persona( array $bundle ) { $cdp = $bundle['cdp']; $vitals = $bundle['vitals']; $p = array( 'signals' => array(), 'not_available' => array() ); $price_band = isset( $cdp['price_band'] ) ? (int) $cdp['price_band'] : 0; $discount = isset( $cdp['discount_share'] ) ? (float) $cdp['discount_share'] : null; if ( $price_band > 0 || null !== $discount ) { if ( null !== $discount && $discount >= 0.5 ) { $label = 'deal-driven'; } elseif ( $price_band >= 4 || ( null !== $discount && $discount < 0.15 ) ) { $label = 'premium / full-price'; } else { $label = 'mid-market'; } $from = array(); if ( $price_band > 0 ) { $from[] = 'price_band'; } if ( null !== $discount ) { $from[] = 'discount_share'; } $p['signals']['affluence'] = array( 'label' => $label, 'confidence' => ( count( $from ) > 1 ) ? 'medium' : 'low', 'from' => implode( ' + ', $from ), ); } if ( ! empty( $vitals['soft1_recorded_speciality'] ) ) { $p['signals']['profession'] = array( 'label' => (string) $vitals['soft1_recorded_speciality'], 'confidence' => 'high', 'from' => 'ERP speciality (SOFT1)' ); } elseif ( ! empty( $cdp['top_category_name'] ) ) { $p['signals']['profession'] = array( 'label' => 'inferred from category affinity: ' . $cdp['top_category_name'], 'confidence' => 'low', 'from' => 'category_affinity' ); } if ( ! empty( $cdp['top_category_name'] ) || ! empty( $cdp['top_brand_name'] ) ) { $p['signals']['affinity'] = array( 'top_category' => (string) ( $cdp['top_category_name'] ?? '' ), 'top_brand' => (string) ( $cdp['top_brand_name'] ?? '' ), 'needs' => $cdp['core_needs'] ?? array(), 'confidence' => 'medium', ); } if ( ! empty( $cdp['inferred_top_priority'] ) ) { $p['signals']['priorities'] = array( 'top' => (string) $cdp['inferred_top_priority'], 'confidence' => isset( $cdp['inferred_priorities_confidence'] ) ? round( (float) $cdp['inferred_priorities_confidence'], 2 ) : null, 'from' => 'survey extrapolation', ); } $p['signals']['lifecycle'] = array( 'segments' => $cdp['segments'] ?? array(), 'dormancy' => (string) ( $cdp['dormancy_cohort'] ?? '' ), 'reactivation_score'=> isset( $cdp['reactivation_score'] ) ? (int) $cdp['reactivation_score'] : null, 'recency_days' => isset( $cdp['recency_days'] ) ? (int) $cdp['recency_days'] : null, ); $p['not_available'] = array( 'age' => 'no date-of-birth stored anywhere', 'gender' => 'only from a survey response (not this record)', 'time_of_day_profile' => 'not rolled up per customer (only per order in order_stats)', 'device' => 'not rolled up per customer', 'acquisition_channel' => 'tracked per session, not rolled up per customer', ); return $p; } public static function narrate( array $bundle, array $persona ) { if ( ! class_exists( 'AI_eShop_Chat_Anthropic_API' ) ) { return array( 'ok' => false, 'text' => '', 'error' => 'chat_api_unavailable' ); } $ctx = array( 'cdp' => $bundle['cdp'], 'vitals' => $bundle['vitals'], 'behaviour' => $bundle['behaviour'], 'recommendations' => $bundle['recommendations'], 'intent' => $bundle['intent'], 'persona_signals' => $persona['signals'], 'not_available' => array_keys( $persona['not_available'] ), ); unset( $ctx['cdp']['top_brand_id'], $ctx['cdp']['top_category_id'] ); $system = "You are a retail customer-intelligence analyst writing a SHORT operator dossier about one (anonymous) customer from the structured signals provided. " . "Write 4-7 sentences, plain and specific. Describe: what they are into (categories/brands/needs), how they behave and buy (value tier, discount sensitivity, cadence, reliability), their current intent (what they're considering but haven't bought), their lifecycle/dormancy, and the single best next action. " . "State inferences as inferences and respect the stated confidence. NEVER invent age, gender, profession, or any fact not in the signals — if a field is in 'not_available', do not guess it. Do not output a name or any identifier. No preamble, no bullet lists — one tight paragraph."; $messages = array( array( 'role' => 'user', 'content' => 'Signals:\n' . wp_json_encode( $ctx ) ) ); $model = (string) get_option( 'aieo_chat_model', 'claude-sonnet-4-5' ); $res = AI_eShop_Chat_Anthropic_API::get_instance()->chat_completion( $messages, array( 'system' => $system, 'model' => $model, 'max_tokens' => 600, 'temperature' => 0.4, 'source' => class_exists( 'AIEO_AI_Sources' ) ? AIEO_AI_Sources::CHAT_PROFILE : 'aieo.chat.profile', ) ); if ( is_wp_error( $res ) ) { return array( 'ok' => false, 'text' => '', 'error' => $res->get_error_message() ); } $text = ''; if ( is_array( $res ) && ! empty( $res['content'][0]['text'] ) ) { $text = (string) $res['content'][0]['text']; } return $text !== '' ? array( 'ok' => true, 'text' => $text, 'error' => '' ) : array( 'ok' => false, 'text' => '', 'error' => 'empty_response' ); } private static function json_list( $json ) { $d = json_decode( (string) $json, true ); if ( ! is_array( $d ) ) { return array(); } return array_values( array_slice( array_is_list( $d ) ? $d : array_keys( $d ), 0, 8 ) ); } private static function term_names( $ids, $taxonomies ) { $out = array(); foreach ( (array) $ids as $id ) { if ( is_numeric( $id ) ) { $n = self::term_name( (int) $id, $taxonomies ); $out[] = ( '' !== $n ) ? $n : (string) $id; } else { $out[] = (string) $id; } } return $out; } private static function term_name( $term_id, $taxonomies ) { $term_id = (int) $term_id; if ( $term_id <= 0 ) { return ''; } foreach ( (array) $taxonomies as $tax ) { $t = get_term( $term_id, $tax ); if ( $t && ! is_wp_error( $t ) ) { return (string) $t->name; } } $t = get_term( $term_id ); return ( $t && ! is_wp_error( $t ) ) ? (string) $t->name : ''; } private static function brand_name( $brand_id ) { return self::term_name( (int) $brand_id, array( 'product_brand', 'pa_brand', 'pwb-brand', 'yith_product_brand' ) ); } private static function product_title( $pid ) { $pid = (int) $pid; return $pid > 0 ? (string) get_the_title( $pid ) : ''; } private static function has_table( $t ) { global $wpdb; return (string) $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $t ) ) === $t; } }