feat: MEMANTO integration — persistent memory for cross-session context (Phases 1-4)
Phase 1: Core Client
- New class-memanto-client.php: Singleton PHP client for MEMANTO API v2
- Health check with 5-min transient caching
- Agent CRUD (ensure, activate, deactivate sessions)
- Memory operations (remember, batch_remember, recall, recall_recent)
- Auto re-activation on expired session tokens (401 retry)
Phase 2: Write-Through Memory Hooks
- New class-memanto-context-enhancer.php: Orchestrates remember/recall
- Fires on: user message, plan generated/approved/rejected,
section written, block refined, config saved, session start/end
- All hooks via do_action() — zero coupling to MEMANTO when disabled
Phase 3: Context Enrichment
- Context builder injects recalled memories into AI prompts
via build_memanto_context() in build_working_context()
- 3-recall strategy: recent post memories, semantic search, user preferences
- Deduplication by content hash
Phase 4: Cross-Session Restore
- New REST endpoints: /memanto/restore, /memanto/preferences
- restore_session() recalls 15 recent memories + user preferences on editor load
- build_session_restore_message() creates AI-ready system message
- get_user_preferences_for_new_post() extracts tone/audience/length/language
- Frontend: 🧠 Restored badge in status bar with memory count tooltip
- Preference carry-over: auto-fills post config from stored user preferences
- deactivate_session() called on session end (triggers MEMANTO summary)
- Badge clears on new conversation start
Settings UI:
- MEMANTO Context Keeper section with enable toggle, URL, API key, test connection
- Settings registered via class-settings-v2.php + tab-memanto.php view
Graceful degradation: all MEMANTO calls guarded by is_active(),
frontend catches silently, plugin works identically when disabled.
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -202,7 +202,7 @@ class WP_Agentic_Writer_Conversation_Manager {
|
||||
|
||||
$session = $wpdb->get_row(
|
||||
$wpdb->prepare(
|
||||
"SELECT * FROM {$this->table_name} WHERE post_id = %d AND status = 'active' ORDER BY updated_at DESC LIMIT 1",
|
||||
"SELECT * FROM {$this->table_name} WHERE post_id = %d AND status != 'archived' ORDER BY updated_at DESC LIMIT 1",
|
||||
$post_id
|
||||
),
|
||||
ARRAY_A
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
636
includes/class-memanto-client.php
Normal file
636
includes/class-memanto-client.php
Normal file
@@ -0,0 +1,636 @@
|
||||
<?php
|
||||
/**
|
||||
* MEMANTO Client
|
||||
*
|
||||
* PHP client for MEMANTO API v2 — external semantic memory service.
|
||||
* Provides persistent, cross-session memory for the WP Agentic Writer plugin.
|
||||
*
|
||||
* @package WP_Agentic_Writer
|
||||
* @since 0.3.0
|
||||
*/
|
||||
|
||||
if (!defined("ABSPATH")) {
|
||||
exit();
|
||||
}
|
||||
|
||||
/**
|
||||
* Class WP_Agentic_Writer_Memanto_Client
|
||||
*
|
||||
* Communicates with a MEMANTO instance (powered by Moorcheh SDK).
|
||||
* All calls are wrapped in error handling — failures never disrupt the plugin.
|
||||
*/
|
||||
class WP_Agentic_Writer_Memanto_Client
|
||||
{
|
||||
/**
|
||||
* Singleton instance.
|
||||
*
|
||||
* @var WP_Agentic_Writer_Memanto_Client
|
||||
*/
|
||||
private static $instance = null;
|
||||
|
||||
/**
|
||||
* MEMANTO base URL (e.g. https://abc123.context.wpagentic.dev).
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $base_url = "";
|
||||
|
||||
/**
|
||||
* Cached health status.
|
||||
*
|
||||
* @var array|null { healthy: bool, checked_at: int } or null if not checked yet.
|
||||
*/
|
||||
private $health_cache = null;
|
||||
|
||||
/**
|
||||
* Get singleton instance.
|
||||
*
|
||||
* @return WP_Agentic_Writer_Memanto_Client
|
||||
*/
|
||||
public static function get_instance()
|
||||
{
|
||||
if (null === self::$instance) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
private function __construct()
|
||||
{
|
||||
$settings = get_option("wp_agentic_writer_settings", []);
|
||||
$this->base_url = untrailingslashit($settings["memanto_url"] ?? "");
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Configuration & Health
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Whether MEMANTO is configured (URL + Moorcheh key set).
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_configured()
|
||||
{
|
||||
return !empty($this->base_url) && !empty($this->get_moorcheh_key());
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether MEMANTO is enabled in settings.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_enabled()
|
||||
{
|
||||
$settings = get_option("wp_agentic_writer_settings", []);
|
||||
return !empty($settings["memanto_enabled"]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether MEMANTO is enabled, configured, and reachable.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_active()
|
||||
{
|
||||
return $this->is_enabled() &&
|
||||
$this->is_configured() &&
|
||||
$this->is_healthy();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check MEMANTO health endpoint. Result cached for 5 minutes.
|
||||
*
|
||||
* @return bool True if healthy.
|
||||
*/
|
||||
public function is_healthy()
|
||||
{
|
||||
if (!$this->is_configured()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Use cached result if fresh (5 minutes).
|
||||
if (null !== $this->health_cache) {
|
||||
if (time() - $this->health_cache["checked_at"] < 300) {
|
||||
return $this->health_cache["healthy"];
|
||||
}
|
||||
}
|
||||
|
||||
// Also check transient for cross-request caching.
|
||||
$cached = get_transient("wpaw_memanto_health");
|
||||
if (
|
||||
false !== $cached &&
|
||||
isset($cached["checked_at"]) &&
|
||||
time() - $cached["checked_at"] < 300
|
||||
) {
|
||||
$this->health_cache = $cached;
|
||||
return $cached["healthy"];
|
||||
}
|
||||
|
||||
$response = $this->get("/health");
|
||||
|
||||
if (is_wp_error($response)) {
|
||||
$this->health_cache = ["healthy" => false, "checked_at" => time()];
|
||||
set_transient("wpaw_memanto_health", $this->health_cache, 300);
|
||||
return false;
|
||||
}
|
||||
|
||||
$healthy =
|
||||
!empty($response["status"]) && "healthy" === $response["status"];
|
||||
$this->health_cache = ["healthy" => $healthy, "checked_at" => time()];
|
||||
set_transient("wpaw_memanto_health", $this->health_cache, 300);
|
||||
return $healthy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Force-refresh the health check (used by Test Connection button).
|
||||
*
|
||||
* @return array { healthy: bool, details: array|null }
|
||||
*/
|
||||
public function check_health_fresh()
|
||||
{
|
||||
if (!$this->is_configured()) {
|
||||
return ["healthy" => false, "details" => null];
|
||||
}
|
||||
|
||||
delete_transient("wpaw_memanto_health");
|
||||
$this->health_cache = null;
|
||||
|
||||
$response = $this->get("/health");
|
||||
|
||||
if (is_wp_error($response)) {
|
||||
return [
|
||||
"healthy" => false,
|
||||
"details" => ["error" => $response->get_error_message()],
|
||||
];
|
||||
}
|
||||
|
||||
$healthy =
|
||||
!empty($response["status"]) && "healthy" === $response["status"];
|
||||
$this->health_cache = ["healthy" => $healthy, "checked_at" => time()];
|
||||
set_transient("wpaw_memanto_health", $this->health_cache, 300);
|
||||
|
||||
return ["healthy" => $healthy, "details" => $response];
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Agent Management
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Ensure an agent exists. Creates if not found.
|
||||
*
|
||||
* @param string $agent_id Agent identifier (e.g. "wp-user-1" or "wp-post-42").
|
||||
* @return bool True on success.
|
||||
*/
|
||||
public function ensure_agent($agent_id)
|
||||
{
|
||||
if (!$this->is_configured()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if agent already exists.
|
||||
$agent = $this->get("/api/v2/agents/{$agent_id}");
|
||||
if (!is_wp_error($agent) && !empty($agent["agent_id"])) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Create agent.
|
||||
$result = $this->post("/api/v2/agents", [
|
||||
"agent_id" => $agent_id,
|
||||
"pattern" => "support",
|
||||
"description" => "WP Agentic Writer agent",
|
||||
]);
|
||||
|
||||
return !is_wp_error($result);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Session Management
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Activate a session for an agent. Returns cached token if still valid.
|
||||
*
|
||||
* @param string $agent_id Agent identifier.
|
||||
* @return string|false Session token or false on failure.
|
||||
*/
|
||||
public function activate_session($agent_id)
|
||||
{
|
||||
if (!$this->is_configured()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$transient_key = "wpaw_memanto_token_" . md5($agent_id);
|
||||
$cached_token = get_transient($transient_key);
|
||||
|
||||
if (!empty($cached_token)) {
|
||||
return $cached_token;
|
||||
}
|
||||
|
||||
$response = $this->post(
|
||||
"/api/v2/agents/{$agent_id}/activate",
|
||||
[],
|
||||
$agent_id,
|
||||
);
|
||||
|
||||
if (is_wp_error($response) || empty($response["session_token"])) {
|
||||
wpaw_debug_log("MEMANTO activate_session failed", [
|
||||
"agent_id" => $agent_id,
|
||||
]);
|
||||
return false;
|
||||
}
|
||||
|
||||
$token = $response["session_token"];
|
||||
$expires_at = strtotime($response["expires_at"] ?? "+6 hours");
|
||||
$ttl = max(60, $expires_at - time() - 300); // Expire 5 min before actual.
|
||||
|
||||
set_transient($transient_key, $token, $ttl);
|
||||
return $token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deactivate a session for an agent.
|
||||
*
|
||||
* Clears the cached token and sends the deactivate request
|
||||
* without injecting a session token (avoids re-activation loop).
|
||||
*
|
||||
* @param string $agent_id Agent identifier.
|
||||
* @return bool True on success.
|
||||
*/
|
||||
public function deactivate_session($agent_id)
|
||||
{
|
||||
$transient_key = "wpaw_memanto_token_" . md5($agent_id);
|
||||
delete_transient($transient_key);
|
||||
|
||||
if (!$this->is_configured()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$url = $this->base_url . "/api/v2/agents/{$agent_id}/deactivate";
|
||||
$headers = $this->get_headers();
|
||||
|
||||
$response = wp_remote_post($url, [
|
||||
"headers" => $headers,
|
||||
"body" => wp_json_encode([]),
|
||||
"timeout" => 10,
|
||||
]);
|
||||
|
||||
if (is_wp_error($response)) {
|
||||
wpaw_debug_log("MEMANTO deactivate_session failed", [
|
||||
"agent_id" => $agent_id,
|
||||
]);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Memory Operations
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Store a memory.
|
||||
*
|
||||
* @param string $agent_id Agent identifier.
|
||||
* @param string $content Memory content (max 10000 chars).
|
||||
* @param string $type Memory type (fact, preference, goal, decision, artifact, learning, event, instruction, relationship, context, observation, commitment, error).
|
||||
* @param array $tags Optional tags.
|
||||
* @param string $title Optional title (max 100 chars).
|
||||
* @return bool True on success.
|
||||
*/
|
||||
public function remember(
|
||||
$agent_id,
|
||||
$content,
|
||||
$type = "context",
|
||||
$tags = [],
|
||||
$title = "",
|
||||
) {
|
||||
if (!$this->is_active()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$body = [
|
||||
"content" => mb_substr($content, 0, 10000),
|
||||
"type" => $type,
|
||||
"tags" => $tags,
|
||||
"source" => "wp-agentic-writer",
|
||||
];
|
||||
|
||||
if (!empty($title)) {
|
||||
$body["title"] = mb_substr($title, 0, 100);
|
||||
}
|
||||
|
||||
$response = $this->post(
|
||||
"/api/v2/agents/{$agent_id}/remember",
|
||||
$body,
|
||||
$agent_id,
|
||||
);
|
||||
|
||||
if (is_wp_error($response)) {
|
||||
wpaw_debug_log("MEMANTO remember failed", [
|
||||
"agent_id" => $agent_id,
|
||||
"type" => $type,
|
||||
"error" => $response->get_error_message(),
|
||||
]);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Store multiple memories in batch (max 100).
|
||||
*
|
||||
* @param string $agent_id Agent identifier.
|
||||
* @param array $memories Array of memory items. Each item: { content, type, tags, title }.
|
||||
* @return bool True on success.
|
||||
*/
|
||||
public function batch_remember($agent_id, $memories)
|
||||
{
|
||||
if (!$this->is_active() || empty($memories)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$batch = [];
|
||||
foreach (array_slice($memories, 0, 100) as $item) {
|
||||
$entry = [
|
||||
"content" => mb_substr($item["content"] ?? "", 0, 10000),
|
||||
"type" => $item["type"] ?? "context",
|
||||
"source" => "wp-agentic-writer",
|
||||
];
|
||||
if (!empty($item["title"])) {
|
||||
$entry["title"] = mb_substr($item["title"], 0, 100);
|
||||
}
|
||||
if (!empty($item["tags"])) {
|
||||
$entry["tags"] = $item["tags"];
|
||||
}
|
||||
$batch[] = $entry;
|
||||
}
|
||||
|
||||
$response = $this->post(
|
||||
"/api/v2/agents/{$agent_id}/batch-remember",
|
||||
["memories" => $batch],
|
||||
$agent_id,
|
||||
);
|
||||
|
||||
if (is_wp_error($response)) {
|
||||
wpaw_debug_log("MEMANTO batch_remember failed", [
|
||||
"agent_id" => $agent_id,
|
||||
"count" => count($batch),
|
||||
"error" => $response->get_error_message(),
|
||||
]);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Semantic search / recall memories.
|
||||
*
|
||||
* @param string $agent_id Agent identifier.
|
||||
* @param string $query Search query.
|
||||
* @param array $type_filter Optional memory type filter.
|
||||
* @param int $limit Max results (default 10).
|
||||
* @param float $min_similarity Minimum similarity score 0-1.
|
||||
* @return array Recalled memories (empty on failure).
|
||||
*/
|
||||
public function recall(
|
||||
$agent_id,
|
||||
$query,
|
||||
$type_filter = [],
|
||||
$limit = 10,
|
||||
$min_similarity = 0.3,
|
||||
) {
|
||||
if (!$this->is_active()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$body = [
|
||||
"query" => $query,
|
||||
"limit" => $limit,
|
||||
"min_similarity" => $min_similarity,
|
||||
];
|
||||
|
||||
if (!empty($type_filter)) {
|
||||
$body["type"] = $type_filter;
|
||||
}
|
||||
|
||||
$response = $this->post(
|
||||
"/api/v2/agents/{$agent_id}/recall",
|
||||
$body,
|
||||
$agent_id,
|
||||
);
|
||||
|
||||
if (is_wp_error($response)) {
|
||||
wpaw_debug_log("MEMANTO recall failed", [
|
||||
"agent_id" => $agent_id,
|
||||
"query" => substr($query, 0, 100),
|
||||
"error" => $response->get_error_message(),
|
||||
]);
|
||||
return [];
|
||||
}
|
||||
|
||||
return is_array($response) ? $response : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Recall most recent memories.
|
||||
*
|
||||
* @param string $agent_id Agent identifier.
|
||||
* @param int $limit Max results.
|
||||
* @param array $type_filter Optional memory type filter.
|
||||
* @return array Recent memories (empty on failure).
|
||||
*/
|
||||
public function recall_recent($agent_id, $limit = 10, $type_filter = [])
|
||||
{
|
||||
if (!$this->is_active()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$body = ["limit" => $limit];
|
||||
if (!empty($type_filter)) {
|
||||
$body["type"] = $type_filter;
|
||||
}
|
||||
|
||||
$response = $this->post(
|
||||
"/api/v2/agents/{$agent_id}/recall/recent",
|
||||
$body,
|
||||
$agent_id,
|
||||
);
|
||||
|
||||
if (is_wp_error($response)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return is_array($response) ? $response : [];
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Agent ID Builders
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Build user-level agent ID.
|
||||
*
|
||||
* @param int $user_id WordPress user ID.
|
||||
* @return string Agent ID like "wp-user-1".
|
||||
*/
|
||||
public function get_user_agent_id($user_id)
|
||||
{
|
||||
return "wp-user-" . absint($user_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build post-level agent ID.
|
||||
*
|
||||
* @param int $post_id WordPress post ID.
|
||||
* @return string Agent ID like "wp-post-42".
|
||||
*/
|
||||
public function get_post_agent_id($post_id)
|
||||
{
|
||||
return "wp-post-" . absint($post_id);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// HTTP Helpers (private)
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Get the Moorcheh API key from settings.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function get_moorcheh_key()
|
||||
{
|
||||
$settings = get_option("wp_agentic_writer_settings", []);
|
||||
return $settings["memanto_moorcheh_key"] ?? "";
|
||||
}
|
||||
|
||||
/**
|
||||
* GET request to MEMANTO API.
|
||||
*
|
||||
* @param string $path API path (e.g. "/health", "/api/v2/agents/{id}").
|
||||
* @return array|WP_Error Decoded response or error.
|
||||
*/
|
||||
private function get($path)
|
||||
{
|
||||
$url = $this->base_url . $path;
|
||||
|
||||
$response = wp_remote_get($url, [
|
||||
"headers" => $this->get_headers(),
|
||||
"timeout" => 10,
|
||||
]);
|
||||
|
||||
return $this->parse_response($response);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST request to MEMANTO API.
|
||||
*
|
||||
* @param string $path API path.
|
||||
* @param array $body Request body.
|
||||
* @param string $agent_id Optional agent ID for session token injection.
|
||||
* @return array|WP_Error Decoded response or error.
|
||||
*/
|
||||
private function post($path, $body = [], $agent_id = "")
|
||||
{
|
||||
$url = $this->base_url . $path;
|
||||
$headers = $this->get_headers();
|
||||
|
||||
// Inject session token if we have an agent_id.
|
||||
if (!empty($agent_id)) {
|
||||
$token = $this->activate_session($agent_id);
|
||||
if ($token) {
|
||||
$headers["X-Session-Token"] = $token;
|
||||
}
|
||||
}
|
||||
|
||||
$response = wp_remote_post($url, [
|
||||
"headers" => $headers,
|
||||
"body" => wp_json_encode($body),
|
||||
"timeout" => 10,
|
||||
]);
|
||||
|
||||
// Handle expired token (401) — re-activate and retry once.
|
||||
if (!is_wp_error($response)) {
|
||||
$code = wp_remote_retrieve_response_code($response);
|
||||
if (401 === $code && !empty($agent_id)) {
|
||||
// Clear cached token and retry.
|
||||
delete_transient("wpaw_memanto_token_" . md5($agent_id));
|
||||
$token = $this->activate_session($agent_id);
|
||||
if ($token) {
|
||||
$headers["X-Session-Token"] = $token;
|
||||
$response = wp_remote_post($url, [
|
||||
"headers" => $headers,
|
||||
"body" => wp_json_encode($body),
|
||||
"timeout" => 10,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this->parse_response($response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build common headers for MEMANTO API requests.
|
||||
*
|
||||
* @return array Headers.
|
||||
*/
|
||||
private function get_headers()
|
||||
{
|
||||
return [
|
||||
"Content-Type" => "application/json",
|
||||
"Accept" => "application/json",
|
||||
"X-API-Key" => $this->get_moorcheh_key(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse HTTP response from MEMANTO API.
|
||||
*
|
||||
* @param array|WP_Error $response wp_remote response.
|
||||
* @return array|WP_Error Decoded body or error.
|
||||
*/
|
||||
private function parse_response($response)
|
||||
{
|
||||
if (is_wp_error($response)) {
|
||||
return new WP_Error(
|
||||
"memanto_connection_error",
|
||||
$response->get_error_message(),
|
||||
);
|
||||
}
|
||||
|
||||
$code = wp_remote_retrieve_response_code($response);
|
||||
$body = wp_remote_retrieve_body($response);
|
||||
|
||||
if (401 === $code) {
|
||||
// Let caller handle re-auth.
|
||||
return new WP_Error(
|
||||
"memanto_unauthorized",
|
||||
"Session token expired",
|
||||
);
|
||||
}
|
||||
|
||||
if ($code >= 400) {
|
||||
wpaw_debug_log("MEMANTO API error ({$code})", $body);
|
||||
return new WP_Error(
|
||||
"memanto_api_error",
|
||||
sprintf(
|
||||
"MEMANTO API error (%d): %s",
|
||||
$code,
|
||||
substr($body, 0, 200),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
$decoded = json_decode($body, true);
|
||||
return is_array($decoded) ? $decoded : [];
|
||||
}
|
||||
}
|
||||
754
includes/class-memanto-context-enhancer.php
Normal file
754
includes/class-memanto-context-enhancer.php
Normal file
@@ -0,0 +1,754 @@
|
||||
<?php
|
||||
/**
|
||||
* MEMANTO Context Enhancer
|
||||
*
|
||||
* Orchestrates when and what to remember/recall from MEMANTO.
|
||||
* Hooks into the existing Context Service to provide memory
|
||||
* enrichment without replacing any existing behavior.
|
||||
*
|
||||
* @package WP_Agentic_Writer
|
||||
* @since 0.3.0
|
||||
*/
|
||||
|
||||
if (!defined("ABSPATH")) {
|
||||
exit();
|
||||
}
|
||||
|
||||
/**
|
||||
* Class WP_Agentic_Writer_Memanto_Context_Enhancer
|
||||
*
|
||||
* Every public method checks is_active() first and returns gracefully
|
||||
* if MEMANTO is not available. The plugin never breaks.
|
||||
*/
|
||||
class WP_Agentic_Writer_Memanto_Context_Enhancer
|
||||
{
|
||||
/**
|
||||
* Singleton instance.
|
||||
*
|
||||
* @var WP_Agentic_Writer_Memanto_Context_Enhancer
|
||||
*/
|
||||
private static $instance = null;
|
||||
|
||||
/**
|
||||
* MEMANTO client reference.
|
||||
*
|
||||
* @var WP_Agentic_Writer_Memanto_Client
|
||||
*/
|
||||
private $client;
|
||||
|
||||
/**
|
||||
* Get singleton instance.
|
||||
*
|
||||
* @return WP_Agentic_Writer_Memanto_Context_Enhancer
|
||||
*/
|
||||
public static function get_instance()
|
||||
{
|
||||
if (null === self::$instance) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* Registers all WordPress action/filter hooks so that the sidebar
|
||||
* handlers only need a single do_action() call at each trigger point.
|
||||
*/
|
||||
private function __construct()
|
||||
{
|
||||
$this->client = WP_Agentic_Writer_Memanto_Client::get_instance();
|
||||
|
||||
// Session lifecycle.
|
||||
add_action(
|
||||
"wpaw_memanto_session_start",
|
||||
[$this, "on_session_start"],
|
||||
10,
|
||||
3,
|
||||
);
|
||||
add_action(
|
||||
"wpaw_memanto_session_end",
|
||||
[$this, "on_session_end"],
|
||||
10,
|
||||
2,
|
||||
);
|
||||
|
||||
// Write-through: remember on meaningful events.
|
||||
add_action(
|
||||
"wpaw_memanto_user_message",
|
||||
[$this, "on_user_message"],
|
||||
10,
|
||||
3,
|
||||
);
|
||||
add_action(
|
||||
"wpaw_memanto_plan_generated",
|
||||
[$this, "on_plan_generated"],
|
||||
10,
|
||||
2,
|
||||
);
|
||||
add_action(
|
||||
"wpaw_memanto_plan_approved",
|
||||
[$this, "on_plan_approved"],
|
||||
10,
|
||||
2,
|
||||
);
|
||||
add_action(
|
||||
"wpaw_memanto_plan_rejected",
|
||||
[$this, "on_plan_rejected"],
|
||||
10,
|
||||
2,
|
||||
);
|
||||
add_action(
|
||||
"wpaw_memanto_section_written",
|
||||
[$this, "on_section_written"],
|
||||
10,
|
||||
3,
|
||||
);
|
||||
add_action(
|
||||
"wpaw_memanto_block_refined",
|
||||
[$this, "on_block_refined"],
|
||||
10,
|
||||
3,
|
||||
);
|
||||
add_action(
|
||||
"wpaw_memanto_config_saved",
|
||||
[$this, "on_config_saved"],
|
||||
10,
|
||||
2,
|
||||
);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Session Lifecycle
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Called when a conversation session starts.
|
||||
* Ensures agents exist and recalls previous session state.
|
||||
*
|
||||
* @param string $session_id Session ID.
|
||||
* @param int $post_id Post ID.
|
||||
* @param int $user_id WordPress user ID.
|
||||
*/
|
||||
public function on_session_start($session_id, $post_id, $user_id)
|
||||
{
|
||||
if (!$this->client->is_active()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure user agent exists.
|
||||
if ($user_id > 0) {
|
||||
$this->client->ensure_agent(
|
||||
$this->client->get_user_agent_id($user_id),
|
||||
);
|
||||
}
|
||||
|
||||
// Ensure post agent exists.
|
||||
if ($post_id > 0) {
|
||||
$this->client->ensure_agent(
|
||||
$this->client->get_post_agent_id($post_id),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a conversation session ends.
|
||||
* Stores a session summary memory and deactivates the session.
|
||||
*
|
||||
* @param string $session_id Session ID.
|
||||
* @param int $post_id Post ID.
|
||||
*/
|
||||
public function on_session_end($session_id, $post_id)
|
||||
{
|
||||
if (!$this->client->is_active() || $post_id <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$post_agent = $this->client->get_post_agent_id($post_id);
|
||||
|
||||
// Store a session summary.
|
||||
$this->client->remember(
|
||||
$post_agent,
|
||||
"Session ended: " . $session_id,
|
||||
"context",
|
||||
["session:" . $session_id, "post:" . $post_id],
|
||||
"Session end",
|
||||
);
|
||||
|
||||
// Deactivate MEMANTO session to trigger summary generation.
|
||||
$this->client->deactivate_session($post_agent);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Write-Through: Remember on Meaningful Events
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Store a memory when user sends a chat message.
|
||||
*
|
||||
* @param string $session_id Session ID.
|
||||
* @param string $content User message content.
|
||||
* @param int $post_id Post ID.
|
||||
*/
|
||||
public function on_user_message($session_id, $content, $post_id)
|
||||
{
|
||||
if (!$this->client->is_active() || $post_id <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->client->remember(
|
||||
$this->client->get_post_agent_id($post_id),
|
||||
"User instruction: " . wp_strip_all_tags($content),
|
||||
"instruction",
|
||||
["post:" . $post_id, "session:" . $session_id],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a memory when a plan is generated.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
* @param array $plan Plan data.
|
||||
*/
|
||||
public function on_plan_generated($post_id, $plan)
|
||||
{
|
||||
if (!$this->client->is_active() || $post_id <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$title = $plan["title"] ?? "Untitled";
|
||||
$sections =
|
||||
isset($plan["sections"]) && is_array($plan["sections"])
|
||||
? count($plan["sections"])
|
||||
: 0;
|
||||
|
||||
$this->client->remember(
|
||||
$this->client->get_post_agent_id($post_id),
|
||||
sprintf('Plan generated: "%s" with %d sections', $title, $sections),
|
||||
"artifact",
|
||||
["post:" . $post_id, "type:plan"],
|
||||
"Plan: " . $title,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a memory when user approves a plan.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
* @param array $plan Plan data.
|
||||
*/
|
||||
public function on_plan_approved($post_id, $plan)
|
||||
{
|
||||
if (!$this->client->is_active() || $post_id <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$title = $plan["title"] ?? "Untitled";
|
||||
|
||||
$this->client->remember(
|
||||
$this->client->get_post_agent_id($post_id),
|
||||
sprintf('User approved plan: "%s"', $title),
|
||||
"decision",
|
||||
["post:" . $post_id, "type:plan"],
|
||||
"Plan approved",
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a memory when user rejects or requests plan changes.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
* @param string $reason Rejection/revision reason.
|
||||
*/
|
||||
public function on_plan_rejected($post_id, $reason)
|
||||
{
|
||||
if (!$this->client->is_active() || $post_id <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->client->remember(
|
||||
$this->client->get_post_agent_id($post_id),
|
||||
"User requested plan revision: " . wp_strip_all_tags($reason),
|
||||
"error",
|
||||
["post:" . $post_id, "type:plan"],
|
||||
"Plan revision",
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a memory when a section is written.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
* @param string $section_id Section identifier.
|
||||
* @param string $summary Brief section summary.
|
||||
*/
|
||||
public function on_section_written($post_id, $section_id, $summary)
|
||||
{
|
||||
if (!$this->client->is_active() || $post_id <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->client->remember(
|
||||
$this->client->get_post_agent_id($post_id),
|
||||
"Section written (" .
|
||||
$section_id .
|
||||
"): " .
|
||||
wp_strip_all_tags($summary),
|
||||
"artifact",
|
||||
["post:" . $post_id, "section:" . $section_id],
|
||||
"Section: " . $section_id,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a memory when a block is refined.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
* @param string $block_id Block identifier.
|
||||
* @param string $instruction Refinement instruction.
|
||||
*/
|
||||
public function on_block_refined($post_id, $block_id, $instruction)
|
||||
{
|
||||
if (!$this->client->is_active() || $post_id <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->client->remember(
|
||||
$this->client->get_post_agent_id($post_id),
|
||||
"Block refined (" .
|
||||
$block_id .
|
||||
"): " .
|
||||
wp_strip_all_tags($instruction),
|
||||
"instruction",
|
||||
["post:" . $post_id, "block:" . $block_id],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a memory when post config is saved.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
* @param array $config Post config data.
|
||||
*/
|
||||
public function on_config_saved($post_id, $config)
|
||||
{
|
||||
if (!$this->client->is_active()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$config_summary = sprintf(
|
||||
"tone=%s, audience=%s, length=%s, language=%s",
|
||||
$config["tone"] ?? "default",
|
||||
$config["audience"] ?? "general",
|
||||
$config["article_length"] ?? "medium",
|
||||
$config["language"] ?? "auto",
|
||||
);
|
||||
|
||||
// Store to post agent.
|
||||
if ($post_id > 0) {
|
||||
$this->client->remember(
|
||||
$this->client->get_post_agent_id($post_id),
|
||||
"Article config: " . $config_summary,
|
||||
"preference",
|
||||
["post:" . $post_id],
|
||||
"Post config",
|
||||
);
|
||||
}
|
||||
|
||||
// Store to user agent (cross-post preferences).
|
||||
$user_id = get_current_user_id();
|
||||
if ($user_id > 0) {
|
||||
$this->client->remember(
|
||||
$this->client->get_user_agent_id($user_id),
|
||||
"User preference: " . $config_summary,
|
||||
"preference",
|
||||
["user:" . $user_id],
|
||||
"Writing preferences",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Recall: Retrieve Memories for Context Enrichment
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Recall relevant memories to enrich AI prompt context.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
* @param int $user_id WordPress user ID.
|
||||
* @param string $current_message User's current message (for semantic search).
|
||||
* @return array Recalled memory items, each with 'type', 'content', 'title'.
|
||||
*/
|
||||
public function recall_for_context(
|
||||
$post_id,
|
||||
$user_id,
|
||||
$current_message = "",
|
||||
) {
|
||||
if (!$this->client->is_active()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$memories = [];
|
||||
$seen = [];
|
||||
|
||||
// 1. Recent post memories.
|
||||
if ($post_id > 0) {
|
||||
$post_agent = $this->client->get_post_agent_id($post_id);
|
||||
$recent = $this->client->recall_recent($post_agent, 10);
|
||||
|
||||
foreach ($this->normalize_memories($recent) as $item) {
|
||||
$hash = md5($item["content"]);
|
||||
if (!isset($seen[$hash])) {
|
||||
$seen[$hash] = true;
|
||||
$memories[] = $item;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Semantic recall based on current message.
|
||||
if (!empty($current_message)) {
|
||||
$semantic = $this->client->recall(
|
||||
$post_agent,
|
||||
$current_message,
|
||||
[],
|
||||
5,
|
||||
);
|
||||
foreach ($this->normalize_memories($semantic) as $item) {
|
||||
$hash = md5($item["content"]);
|
||||
if (!isset($seen[$hash])) {
|
||||
$seen[$hash] = true;
|
||||
$memories[] = $item;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. User preferences (cross-post).
|
||||
if ($user_id > 0) {
|
||||
$user_prefs = $this->client->recall(
|
||||
$this->client->get_user_agent_id($user_id),
|
||||
"writing preferences tone audience language",
|
||||
["preference"],
|
||||
5,
|
||||
);
|
||||
|
||||
foreach ($this->normalize_memories($user_prefs) as $item) {
|
||||
$hash = md5($item["content"]);
|
||||
if (!isset($seen[$hash])) {
|
||||
$seen[$hash] = true;
|
||||
$memories[] = $item;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $memories;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore session state from MEMANTO when reopening a post.
|
||||
*
|
||||
* Returns a structured restore payload that the frontend can use to:
|
||||
* - Display a "Restored from memory" badge
|
||||
* - Build a restored-session system message for the AI
|
||||
* - Pre-fill config from prior preferences
|
||||
*
|
||||
* @since 0.4.0
|
||||
* @param int $post_id Post ID being reopened.
|
||||
* @param int $user_id Current user ID.
|
||||
* @return array { restored: bool, memories: array, preferences: array, summary: string }
|
||||
*/
|
||||
public function restore_session($post_id, $user_id)
|
||||
{
|
||||
$empty = [
|
||||
"restored" => false,
|
||||
"memories" => [],
|
||||
"preferences" => [],
|
||||
"summary" => "",
|
||||
];
|
||||
|
||||
if (!$this->client->is_active() || $post_id <= 0) {
|
||||
return $empty;
|
||||
}
|
||||
|
||||
// Recall recent post memories (no current message — restore is about history).
|
||||
$post_agent = $this->client->get_post_agent_id($post_id);
|
||||
$recent = $this->client->recall_recent($post_agent, 15);
|
||||
$memories = $this->normalize_memories($recent);
|
||||
|
||||
// Recall user preferences (for cross-post config carry-over).
|
||||
$preferences = [];
|
||||
if ($user_id > 0) {
|
||||
$user_prefs = $this->client->recall(
|
||||
$this->client->get_user_agent_id($user_id),
|
||||
"writing preferences tone audience language length",
|
||||
["preference"],
|
||||
5,
|
||||
);
|
||||
$preferences = $this->normalize_memories($user_prefs);
|
||||
}
|
||||
|
||||
if (empty($memories) && empty($preferences)) {
|
||||
return $empty;
|
||||
}
|
||||
|
||||
// Build a human-readable summary for the restore badge tooltip.
|
||||
$summary = $this->build_restore_summary($memories, $preferences);
|
||||
|
||||
return [
|
||||
"restored" => true,
|
||||
"memories" => $memories,
|
||||
"preferences" => $preferences,
|
||||
"summary" => $summary,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a compact summary string of restored memories.
|
||||
*
|
||||
* @param array $memories Restored memories.
|
||||
* @param array $preferences Restored preferences.
|
||||
* @return string Summary text.
|
||||
*/
|
||||
private function build_restore_summary($memories, $preferences)
|
||||
{
|
||||
$parts = [];
|
||||
|
||||
if (!empty($memories)) {
|
||||
$parts[] = sprintf(
|
||||
/* translators: %d: number of memories */
|
||||
_n(
|
||||
"%d memory",
|
||||
"%d memories",
|
||||
count($memories),
|
||||
"wp-agentic-writer",
|
||||
),
|
||||
count($memories),
|
||||
);
|
||||
}
|
||||
|
||||
if (!empty($preferences)) {
|
||||
$parts[] = sprintf(
|
||||
/* translators: %d: number of preferences */
|
||||
_n(
|
||||
"%d preference",
|
||||
"%d preferences",
|
||||
count($preferences),
|
||||
"wp-agentic-writer",
|
||||
),
|
||||
count($preferences),
|
||||
);
|
||||
}
|
||||
|
||||
return implode(", ", $parts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user's writing preferences recalled from MEMANTO.
|
||||
*
|
||||
* Used when creating a new post to pre-fill the post config
|
||||
* with the user's habitual tone, audience, etc.
|
||||
*
|
||||
* @since 0.4.0
|
||||
* @param int $user_id WordPress user ID.
|
||||
* @return array { restored: bool, config: array }
|
||||
*/
|
||||
public function get_user_preferences_for_new_post($user_id)
|
||||
{
|
||||
$empty = ["restored" => false, "config" => []];
|
||||
|
||||
if (!$this->client->is_active() || $user_id <= 0) {
|
||||
return $empty;
|
||||
}
|
||||
|
||||
$user_prefs = $this->client->recall(
|
||||
$this->client->get_user_agent_id($user_id),
|
||||
"writing preferences tone audience language length",
|
||||
["preference"],
|
||||
3,
|
||||
);
|
||||
|
||||
$prefs = $this->normalize_memories($user_prefs);
|
||||
if (empty($prefs)) {
|
||||
return $empty;
|
||||
}
|
||||
|
||||
// Extract config fields from preference content.
|
||||
// Memory format: "User preference: tone=professional, audience=experts, length=long, language=en"
|
||||
$config = $this->extract_config_from_preferences($prefs);
|
||||
|
||||
if (empty($config)) {
|
||||
return $empty;
|
||||
}
|
||||
|
||||
return ["restored" => true, "config" => $config];
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse preference memory contents and extract config fields.
|
||||
*
|
||||
* @param array $prefs Normalized preference memories.
|
||||
* @return array Extracted config: { tone, audience, article_length, language }.
|
||||
*/
|
||||
private function extract_config_from_preferences($prefs)
|
||||
{
|
||||
$config = [];
|
||||
|
||||
// Combine all preference content into one blob for parsing.
|
||||
$blob = "";
|
||||
foreach ($prefs as $pref) {
|
||||
$blob .= " " . ($pref["content"] ?? "");
|
||||
}
|
||||
|
||||
// Match key=value patterns.
|
||||
if (preg_match('/tone\s*=\s*([^,\n]+)/i', $blob, $m)) {
|
||||
$val = trim($m[1]);
|
||||
if ("" !== $val && "default" !== strtolower($val)) {
|
||||
$config["tone"] = $val;
|
||||
}
|
||||
}
|
||||
if (preg_match('/audience\s*=\s*([^,\n]+)/i', $blob, $m)) {
|
||||
$val = trim($m[1]);
|
||||
if ("" !== $val && "general" !== strtolower($val)) {
|
||||
$config["audience"] = $val;
|
||||
}
|
||||
}
|
||||
if (preg_match('/(?:article_)?length\s*=\s*([^,\n]+)/i', $blob, $m)) {
|
||||
$val = trim($m[1]);
|
||||
if ("" !== $val && "medium" !== strtolower($val)) {
|
||||
$config["article_length"] = $val;
|
||||
}
|
||||
}
|
||||
if (preg_match('/language\s*=\s*([^,\n]+)/i', $blob, $m)) {
|
||||
$val = trim($m[1]);
|
||||
if ("" !== $val && "auto" !== strtolower($val)) {
|
||||
$config["language"] = $val;
|
||||
}
|
||||
}
|
||||
|
||||
return $config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a "restored session" system message for the AI.
|
||||
*
|
||||
* This message summarizes prior post work so the AI can resume
|
||||
* mid-article without re-asking the user for context.
|
||||
*
|
||||
* @since 0.4.0
|
||||
* @param array $restore_payload Result from restore_session().
|
||||
* @return string System message content (empty if nothing to restore).
|
||||
*/
|
||||
public function build_session_restore_message($restore_payload)
|
||||
{
|
||||
if (empty($restore_payload["restored"])) {
|
||||
return "";
|
||||
}
|
||||
|
||||
$memories = $restore_payload["memories"] ?? [];
|
||||
$preferences = $restore_payload["preferences"] ?? [];
|
||||
|
||||
if (empty($memories) && empty($preferences)) {
|
||||
return "";
|
||||
}
|
||||
|
||||
$lines = ["SESSION RESTORED FROM MEMORY"];
|
||||
$lines[] =
|
||||
"The user has returned to this post. Below is context from prior sessions.";
|
||||
$lines[] =
|
||||
"Use this to continue where the conversation left off without re-asking.";
|
||||
|
||||
// Summarize prior post work.
|
||||
if (!empty($memories)) {
|
||||
$lines[] = "";
|
||||
$lines[] = "## Prior session activity";
|
||||
|
||||
$grouped = [];
|
||||
foreach ($memories as $memory) {
|
||||
$type = $memory["type"] ?? "context";
|
||||
if (!isset($grouped[$type])) {
|
||||
$grouped[$type] = [];
|
||||
}
|
||||
$grouped[$type][] = $memory;
|
||||
}
|
||||
|
||||
// Render in priority order: plan > decision > instruction > artifact > error > context.
|
||||
$priority = [
|
||||
"artifact",
|
||||
"decision",
|
||||
"instruction",
|
||||
"error",
|
||||
"learning",
|
||||
"context",
|
||||
"preference",
|
||||
];
|
||||
foreach ($priority as $type) {
|
||||
if (empty($grouped[$type])) {
|
||||
continue;
|
||||
}
|
||||
$type_label = ucfirst($type) . "s";
|
||||
$lines[] = "### {$type_label}";
|
||||
foreach ($grouped[$type] as $m) {
|
||||
$content = trim($m["content"] ?? "");
|
||||
if ("" === $content) {
|
||||
continue;
|
||||
}
|
||||
$title = !empty($m["title"])
|
||||
? " [" . trim($m["title"]) . "]"
|
||||
: "";
|
||||
$lines[] = "- {$content}{$title}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Append user preferences (compact).
|
||||
if (!empty($preferences)) {
|
||||
$lines[] = "";
|
||||
$lines[] = "## User writing preferences";
|
||||
foreach ($preferences as $pref) {
|
||||
$content = trim($pref["content"] ?? "");
|
||||
if ("" !== $content) {
|
||||
$lines[] = "- {$content}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return implode("\n", $lines);
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize raw MEMANTO response into a uniform array.
|
||||
*
|
||||
* @param array $raw Raw response from recall/recall_recent.
|
||||
* @return array Normalized items: { type, content, title }.
|
||||
*/
|
||||
private function normalize_memories($raw)
|
||||
{
|
||||
if (!is_array($raw)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Handle different possible response shapes.
|
||||
$items = $raw;
|
||||
|
||||
// If response has a 'memories' or 'results' key, unwrap.
|
||||
if (isset($raw["memories"]) && is_array($raw["memories"])) {
|
||||
$items = $raw["memories"];
|
||||
} elseif (isset($raw["results"]) && is_array($raw["results"])) {
|
||||
$items = $raw["results"];
|
||||
}
|
||||
|
||||
$normalized = [];
|
||||
foreach ($items as $item) {
|
||||
if (!is_array($item)) {
|
||||
continue;
|
||||
}
|
||||
$normalized[] = [
|
||||
"type" => $item["type"] ?? "context",
|
||||
"content" => $item["content"] ?? ($item["text"] ?? ""),
|
||||
"title" => $item["title"] ?? "",
|
||||
];
|
||||
}
|
||||
|
||||
return $normalized;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user