getByConversation( $conversationUuid, $limit ); if ( empty( $firstMessages ) ) { return null; } // Use the first user message as the basis for the title. $first_user_message = ''; foreach ( $firstMessages as $msg ) { if ( 'user' === $msg->role && '' !== trim( (string) $msg->content ) ) { $first_user_message = (string) $msg->content; break; } } if ( '' === $first_user_message ) { return null; } $title = self::title_from_text( $first_user_message ); return '' !== $title ? $title : null; } catch ( \Throwable $e ) { Log::error( 'Title generation failed', [ 'conversation_uuid' => $conversationUuid, 'error' => $e->getMessage(), 'error_class' => get_class( $e ), ] ); return null; } } /** * Build a short, clean conversation title from raw message text. * * Strips markup, collapses whitespace, and truncates to a sensible length * on a word boundary. No AI is involved. * * @param string $text Raw user message content. * @return string Title, or an empty string if the text has no usable content. */ private static function title_from_text( string $text ): string { $text = trim( wp_strip_all_tags( $text ) ); if ( '' === $text ) { return ''; } // Keep titles short: at most 8 words, hard-capped at 70 characters. $title = wp_trim_words( $text, 8, '…' ); if ( mb_strlen( $title ) > 70 ) { $title = rtrim( mb_substr( $title, 0, 70 ) ) . '…'; } return $title; } /** * Process pending title generations * This should be called by a cron job or hook * * Note: Only processes conversations WITHOUT titles (title IS NULL) * to avoid regenerating existing titles * * @return void */ public static function processPending(): void { global $wpdb; $conversations_table = $wpdb->prefix . 'aisentic_conversations'; $messages_table = $wpdb->prefix . 'aisentic_messages'; // Find conversations without titles that have at least 2 messages // Only processes NULL titles, never regenerates existing ones // Table names are safe - $wpdb->prefix is controlled by WordPress core $conversations_table = $wpdb->prefix . 'aisentic_conversations'; // phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter -- $wpdb->prefix is trusted $messages_table = $wpdb->prefix . 'aisentic_messages'; // phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter -- $wpdb->prefix is trusted $sql = "SELECT c.uuid, c.id FROM `{$conversations_table}` c WHERE c.title IS NULL AND (SELECT COUNT(*) FROM `{$messages_table}` m WHERE m.conversation_uuid = c.uuid) >= %d AND (SELECT MAX(m.created_at) FROM `{$messages_table}` m WHERE m.conversation_uuid = c.uuid) <= (NOW() - INTERVAL 30 MINUTE) LIMIT %d"; $query = $wpdb->prepare( $sql, 2, 5 ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter $conversations = $wpdb->get_results( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- $query is built via $wpdb->prepare() above. if ( empty( $conversations ) ) { return; } $conversationRepo = new Conversation_Repository(); foreach ( $conversations as $conv ) { $title = self::generateTitle( $conv->uuid ); if ( $title ) { $conversationRepo->update( $conv->uuid, [ 'title' => $title ] ); } } } }