Utilizza il Time To Live per messaggi e memorie

I messaggi e i ricordi possono rimanere ricercabili al termine della conversazione che li ha creati. Il Time To Live consente alle applicazioni di scadere automaticamente i record dopo un periodo definito, ad esempio per soddisfare i requisiti di conformità, rimuovere informazioni probabilmente obsolete o risparmiare spazio di storage.

Questa guida mostra come configurare la scadenza dei record per i messaggi e le memorie supportati da Oracle DB. Vengono inoltre descritti i job di rimozione e le sostituzioni Time To Live per record.

La memoria agente Oracle applica il time-to-live in due livelli:

Quando si seleziona un ancoraggio per record, utilizzare TimeToLiveAnchor.CREATED_AT per contare da quando Oracle memorizza la riga oppure TimeToLiveAnchor.TIMESTAMP per contare dall'indicatore orario dell'evento del record.

Nota: utilizzare il Time To Live quando i record devono rimanere disponibili per un periodo definito e quindi scadere automaticamente. Ad esempio, un'applicazione potrebbe conservare i dettagli di supporto per 30 giorni o le informazioni temporanee sui task per una settimana.

Nota: è opportuno tenere presenti alcune regole di conservazione:

Suggerimento: per l'impostazione dei package, vedere Introduzione alla memoria dell'agente. Se hai bisogno di un Oracle AI Database locale per questo esempio, segui Esegui Oracle AI Database localmente.

Configurare i valori predefiniti di conservazione a livello di schema

Creare un client OracleAgentMemory con schema_policy=SchemaPolicy.CREATE_IF_NECESSARY quando si desidera che l'SDK crei o esegua l'upgrade dello schema Oracle gestito e renda persistente un criterio di conservazione accanto a esso.

from datetime import datetime, timezone

import oracledb

from oracleagentmemory.apis import TimeToLiveAnchor
from oracleagentmemory.core import (
    MemoryRetentionConfig,
    OracleAgentMemory,
    SchemaPolicy,
)
from oracleagentmemory.core.embedders.embedder import Embedder
from oracleagentmemory.core.llms.llm import Llm

embedder = Embedder(model="YOUR_EMBEDDING_MODEL")
llm = Llm(
    model="YOUR_MEMORY_LLM_MODEL",
    api_base="YOUR_MEMORY_LLM_BASE_URL",
    api_key="YOUR_MEMORY_LLM_API_KEY",
)
db_pool = oracledb.SessionPool(
    user="YOUR DB USER",
    password="YOUR DB PASSWORD",
    dsn="localhost:1521/...",
)
memory_store_id = "T_TIME_TO_LIVE"

memory = OracleAgentMemory(
    connection=db_pool,
    embedder=embedder,
    llm=llm,
    schema_policy=SchemaPolicy.CREATE_IF_NECESSARY,
    memory_store_id=memory_store_id,
    memory_retention_config=MemoryRetentionConfig(
        default_ttl_days=30,
        max_ttl_days=90,
    ),
)

In questa configurazione:

Riferimento API: OracleAgentMemory

Aggiungi messaggi e memorie con Time To Live

Utilizzare le scritture di thread per fare affidamento sul TTL predefinito dello schema per alcuni record durante l'impostazione di un TTL diverso per altri.

ttl_thread = memory.create_thread(
    thread_id="ttl_demo_thread",
    user_id="user_123",
)

message_ids = ttl_thread.add_messages(
    [
        {
            "id": "msg-ttl-1",
            "role": "user",
            "content": (
                "I opened ticket 1042 yesterday because the laptop battery failed."
            ),
            "timestamp": "2026-04-01T09:00:00Z",
        },
        {
            "id": "msg-ttl-2",
            "role": "assistant",
            "content": (
                "I will keep ticket 1042 active and send a replacement checklist."
            ),
            "timestamp": "2026-04-01T09:01:00Z",
        },
    ]
)

timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
#^ for instance, 2026-04-01T09:00:00Z

backfilled_memory_id = ttl_thread.add_memory(
    "Ticket 1042 battery-failure report was filed on 2026-04-01.",
    memory_id="mem-ttl-backfill",
    timestamp=timestamp,
    ttl_days=90,
    ttl_anchor=TimeToLiveAnchor.TIMESTAMP,
)

short_lived_memory_id = ttl_thread.add_memory(
    "Replacement checklist should be revisited within one week.",
    memory_id="mem-ttl-short",
    ttl_days=7,
)

print(message_ids)
print(backfilled_memory_id)
print(short_lived_memory_id)
#['msg-ttl-1', 'msg-ttl-2']
#mem-ttl-backfill
#mem-ttl-short

In questo esempio:

Riferimento API: OracleThread

Aggiorna Time To Live su record esistenti

Utilizzare update_message() o update_memory() per aggiornare una scadenza esistente senza sostituire il contenuto memorizzato.

ttl_thread.update_message("msg-ttl-1", ttl_days=14)

ttl_thread.update_memory(
    "mem-ttl-backfill",
    ttl_days=60,
    ttl_anchor=TimeToLiveAnchor.TIMESTAMP,
)

Queste chiamate mantengono in vigore il contenuto e i metadati memorizzati durante il ricalcolo della finestra di scadenza. Tenere presente che gli aggiornamenti conservano la scadenza corrente a meno che non si passi ttl_days o ttl_anchor.

Riferimento API: OracleThread

Assicurarsi che il job di rimozione gestita esista

Quando la memoria dell'agente Oracle crea o aggiorna lo schema Oracle gestito, crea anche due job DBMS_SCHEDULER giornalieri. Si eliminano le righe di messaggi e memoria scadute e i relativi chunk di recupero. L'altro elimina i chunk di recupero orfani la cui riga di origine non esiste più o non corrisponde più al relativo tipo di origine. Entrambi i job rimuovono le righe in batch invece di emettere un'eliminazione di grandi dimensioni, eseguono il commit tra i batch e acquisiscono un indicatore orario di riferimento in anticipo in modo che ogni esecuzione utilizzi un limite di scadenza coerente.

Ogni definizione dello scheduler imposta schedule_limit su un giorno in modo che un'esecuzione che inizia troppo tardi possa essere saltata invece di essere eseguita arbitrariamente in ritardo. Ogni corpo del job interrompe separatamente l'utilizzo di nuovi batch dopo un giorno dall'ora di inizio dell'esecuzione. Il limite di runtime specificato viene controllato tra i batch, pertanto è consentito terminare e eseguire il commit di un batch già in corso. Alla fine di un'esecuzione, ogni job scrive un breve riepilogo DBMS_OUTPUT che include se è stato raggiunto il limite di runtime e il numero di righe eliminate.

Quando SchemaPolicy.CREATE_IF_NECESSARY deve creare uno dei due job, ma l'utente dello schema non dispone di CREATE JOB, l'impostazione viene completata con un'avvertenza. I record scaduti rimangono nascosti dalle letture e dalla ricerca e i blocchi isolati rimangono finché un utente con privilegi non crea il job mancante.

Se si imposta memory_store_id in modo esplicito, anteporre ai nomi dei job e delle tabelle l'ID e il carattere di sottolineatura. Se si utilizza ancora il prefisso table_name_prefix obsoleto, applicarlo. I nomi dei job gestiti predefiniti sono PURGE_EXPIRED_RECORDS_J e PURGE_ORPHANED_CHUNKS_J.

Suggerimento: utilizzare le seguenti opzioni DBA quando il proprietario dello schema non dispone di CREATE JOB.

Concedi privilegi job scheduler

GRANT CREATE JOB TO app_schema;
-- Run OracleAgentMemory schema setup as app_schema.
REVOKE CREATE JOB FROM app_schema;

Crea manualmente il job di rimozione

Sostituire APP_SCHEMA con il proprietario dello schema gestito e adeguare i nomi degli oggetti se è configurato un prefisso nome tabella. Il blocco PL/SQL seguente è intenzionalmente compatto, pertanto rimane entro il limite di lunghezza job_action di Oracle Scheduler anche quando i nomi oggetto con prefisso sono lunghi:

BEGIN
  DBMS_SCHEDULER.CREATE_JOB(
    job_name => 'APP_SCHEMA.PURGE_EXPIRED_RECORDS_J',
    job_type => 'PLSQL_BLOCK',
    job_action => q'[
      DECLARE
        bs CONSTANT PLS_INTEGER := 1000;
        n PLS_INTEGER;
        mc PLS_INTEGER := 0;
        xc PLS_INTEGER := 0;
        mr PLS_INTEGER := 0;
        xr PLS_INTEGER := 0;
        tl PLS_INTEGER := 0;
        rt TIMESTAMP(6) WITH TIME ZONE := SYSTIMESTAMP;
        dl TIMESTAMP(6) WITH TIME ZONE := rt + INTERVAL '1 00:00:00' DAY TO SECOND;
      BEGIN
        LOOP
          IF SYSTIMESTAMP >= dl THEN
            tl := 1;
            EXIT;
          END IF;
          DELETE FROM APP_SCHEMA.RECORD_CHUNKS c
          WHERE c.rowid IN (
            SELECT rid
            FROM (
              SELECT c.rowid AS rid
              FROM APP_SCHEMA.RECORD_CHUNKS c
              WHERE c.source_record_type = 'message'
                AND EXISTS (
                  SELECT 1
                  FROM APP_SCHEMA.MESSAGE m
                  WHERE m.record_id = c.source_id
                    AND m.expires_at IS NOT NULL
                    AND m.expires_at <= rt
                )
              FETCH FIRST bs ROWS ONLY
            )
          );
          n := SQL%ROWCOUNT;
          mc := mc + n;
          EXIT WHEN n = 0;
          COMMIT;
        END LOOP;

        LOOP
          IF SYSTIMESTAMP >= dl THEN
            tl := 1;
            EXIT;
          END IF;
          DELETE FROM APP_SCHEMA.RECORD_CHUNKS c
          WHERE c.rowid IN (
            SELECT rid
            FROM (
              SELECT c.rowid AS rid
              FROM APP_SCHEMA.RECORD_CHUNKS c
              WHERE c.source_record_type IN ('fact', 'guideline', 'memory', 'preference')
                AND EXISTS (
                  SELECT 1
                  FROM APP_SCHEMA.MEMORY m
                  WHERE m.record_id = c.source_id
                    AND m.memory_type = c.source_record_type
                    AND m.expires_at IS NOT NULL
                    AND m.expires_at <= rt
                )
              FETCH FIRST bs ROWS ONLY
            )
          );
          n := SQL%ROWCOUNT;
          xc := xc + n;
          EXIT WHEN n = 0;
          COMMIT;
        END LOOP;

        LOOP
          IF SYSTIMESTAMP >= dl THEN
            tl := 1;
            EXIT;
          END IF;
          DELETE FROM APP_SCHEMA.MESSAGE
          WHERE rowid IN (
            SELECT rid
            FROM (
              SELECT rowid AS rid
              FROM APP_SCHEMA.MESSAGE
              WHERE expires_at IS NOT NULL
                AND expires_at <= rt
              FETCH FIRST bs ROWS ONLY
            )
          );
          n := SQL%ROWCOUNT;
          mr := mr + n;
          EXIT WHEN n = 0;
          COMMIT;
        END LOOP;

        LOOP
          IF SYSTIMESTAMP >= dl THEN
            tl := 1;
            EXIT;
          END IF;
          DELETE FROM APP_SCHEMA.MEMORY
          WHERE rowid IN (
            SELECT rid
            FROM (
              SELECT rowid AS rid
              FROM APP_SCHEMA.MEMORY
              WHERE expires_at IS NOT NULL
                AND expires_at <= rt
              FETCH FIRST bs ROWS ONLY
            )
          );
          n := SQL%ROWCOUNT;
          xr := xr + n;
          EXIT WHEN n = 0;
          COMMIT;
        END LOOP;

        DBMS_OUTPUT.PUT_LINE(
          'OracleAgentMemory purge reference_time='
          || TO_CHAR(rt, 'YYYY-MM-DD"T"HH24:MI:SS.FF3TZH:TZM')
        );
        DBMS_OUTPUT.PUT_LINE(
          'OracleAgentMemory purge run_deadline='
          || TO_CHAR(dl, 'YYYY-MM-DD"T"HH24:MI:SS.FF3TZH:TZM')
        );
        DBMS_OUTPUT.PUT_LINE(
          'OracleAgentMemory purge runtime_limit_reached='
          || CASE WHEN tl = 1 THEN 'true' ELSE 'false' END
        );
        DBMS_OUTPUT.PUT_LINE(
          'OracleAgentMemory purge deleted message chunk rows='
          || TO_CHAR(mc)
        );
        DBMS_OUTPUT.PUT_LINE(
          'OracleAgentMemory purge deleted fact/guideline/memory/preference chunk rows='
          || TO_CHAR(xc)
        );
        DBMS_OUTPUT.PUT_LINE(
          'OracleAgentMemory purge deleted message rows='
          || TO_CHAR(mr)
        );
        DBMS_OUTPUT.PUT_LINE(
          'OracleAgentMemory purge deleted fact/guideline/memory/preference rows='
          || TO_CHAR(xr)
        );
      END;
    ]',
    start_date => SYSTIMESTAMP,
    repeat_interval => 'FREQ=DAILY;INTERVAL=1',
    enabled => FALSE,
    auto_drop => FALSE,
    comments => 'OracleAgentMemory expired-record purge'
  );
  DBMS_SCHEDULER.SET_ATTRIBUTE(
    name => 'APP_SCHEMA.PURGE_EXPIRED_RECORDS_J',
    attribute => 'logging_level',
    value => DBMS_SCHEDULER.LOGGING_RUNS
  );
  DBMS_SCHEDULER.SET_ATTRIBUTE(
    name => 'APP_SCHEMA.PURGE_EXPIRED_RECORDS_J',
    attribute => 'store_output',
    value => TRUE
  );
  DBMS_SCHEDULER.SET_ATTRIBUTE(
    name => 'APP_SCHEMA.PURGE_EXPIRED_RECORDS_J',
    attribute => 'schedule_limit',
    value => INTERVAL '1 00:00:00' DAY TO SECOND
  );
  DBMS_SCHEDULER.ENABLE('APP_SCHEMA.PURGE_EXPIRED_RECORDS_J');
END;
/

Il cleanup dei pezzi orfani viene eseguito come job giornaliero separato:

BEGIN
  DBMS_SCHEDULER.CREATE_JOB(
    job_name => 'APP_SCHEMA.PURGE_ORPHANED_CHUNKS_J',
    job_type => 'PLSQL_BLOCK',
    job_action => q'[
      DECLARE
        bs CONSTANT PLS_INTEGER := 1000;
        n PLS_INTEGER;
        oc PLS_INTEGER := 0;
        tl PLS_INTEGER := 0;
        rt TIMESTAMP(6) WITH TIME ZONE := SYSTIMESTAMP;
        dl TIMESTAMP(6) WITH TIME ZONE := rt + INTERVAL '1 00:00:00' DAY TO SECOND;
      BEGIN
        LOOP
          IF SYSTIMESTAMP >= dl THEN
            tl := 1;
            EXIT;
          END IF;
          DELETE FROM APP_SCHEMA.RECORD_CHUNKS c
          WHERE c.rowid IN (
            SELECT rid
            FROM (
              SELECT c.rowid AS rid
              FROM APP_SCHEMA.RECORD_CHUNKS c
              WHERE NOT (
                (
                  c.source_record_type = 'message'
                  AND EXISTS (
                    SELECT 1
                    FROM APP_SCHEMA.MESSAGE m
                    WHERE m.record_id = c.source_id
                  )
                )
                OR (
                  c.source_record_type IN ('fact', 'guideline', 'memory', 'preference')
                  AND EXISTS (
                    SELECT 1
                    FROM APP_SCHEMA.MEMORY m
                    WHERE m.record_id = c.source_id
                      AND m.memory_type = c.source_record_type
                  )
                )
                OR (
                  c.source_record_type = 'user_profile'
                  AND EXISTS (
                    SELECT 1
                    FROM APP_SCHEMA.ACTOR_PROFILE a
                    WHERE a.actor_id = c.source_id
                      AND a.actor_type = 'user'
                  )
                )
                OR (
                  c.source_record_type = 'agent_profile'
                  AND EXISTS (
                    SELECT 1
                    FROM APP_SCHEMA.ACTOR_PROFILE a
                    WHERE a.actor_id = c.source_id
                      AND a.actor_type = 'agent'
                  )
                )
              )
              FETCH FIRST bs ROWS ONLY
            )
          );
          n := SQL%ROWCOUNT;
          oc := oc + n;
          EXIT WHEN n = 0;
          COMMIT;
        END LOOP;
        DBMS_OUTPUT.PUT_LINE(
          'OracleAgentMemory orphan cleanup reference_time='
          || TO_CHAR(rt, 'YYYY-MM-DD"T"HH24:MI:SS.FF3TZH:TZM')
        );
        DBMS_OUTPUT.PUT_LINE(
          'OracleAgentMemory orphan cleanup run_deadline='
          || TO_CHAR(dl, 'YYYY-MM-DD"T"HH24:MI:SS.FF3TZH:TZM')
        );
        DBMS_OUTPUT.PUT_LINE(
          'OracleAgentMemory orphan cleanup runtime_limit_reached='
          || CASE WHEN tl = 1 THEN 'true' ELSE 'false' END
        );
        DBMS_OUTPUT.PUT_LINE(
          'OracleAgentMemory orphan cleanup deleted chunk rows='
          || TO_CHAR(oc)
        );
      END;
    ]',
    start_date => SYSTIMESTAMP,
    repeat_interval => 'FREQ=DAILY;INTERVAL=1',
    enabled => FALSE,
    auto_drop => FALSE,
    comments => 'OracleAgentMemory orphaned-chunk cleanup'
  );
  DBMS_SCHEDULER.SET_ATTRIBUTE(
    name => 'APP_SCHEMA.PURGE_ORPHANED_CHUNKS_J',
    attribute => 'logging_level',
    value => DBMS_SCHEDULER.LOGGING_RUNS
  );
  DBMS_SCHEDULER.SET_ATTRIBUTE(
    name => 'APP_SCHEMA.PURGE_ORPHANED_CHUNKS_J',
    attribute => 'store_output',
    value => TRUE
  );
  DBMS_SCHEDULER.SET_ATTRIBUTE(
    name => 'APP_SCHEMA.PURGE_ORPHANED_CHUNKS_J',
    attribute => 'schedule_limit',
    value => INTERVAL '1 00:00:00' DAY TO SECOND
  );
  DBMS_SCHEDULER.ENABLE('APP_SCHEMA.PURGE_ORPHANED_CHUNKS_J');
END;
/

È utile confermare la definizione dello scheduler e ispezionare la cronologia delle esecuzioni recenti. Sostituire APP_SCHEMA se necessario e applicare il prefisso nome tabella configurato al nome del job quando la distribuzione ne utilizza uno.

Verificare che il job di rimozione gestito esista e abbia mantenuto le impostazioni di ispezione previste:

SELECT owner,
       job_name,
       enabled,
       state,
       repeat_interval,
       schedule_limit,
       logging_level,
       store_output,
       start_date,
       last_start_date,
       next_run_date
FROM   all_scheduler_jobs
WHERE  owner = 'APP_SCHEMA'
AND    job_name IN (
         'PURGE_EXPIRED_RECORDS_J',
         'PURGE_ORPHANED_CHUNKS_J'
       );

Controllare le esecuzioni recenti quando è necessario confermare che le esecuzioni di rimozione sono in corso e sono state completate correttamente:

SELECT log_date,
       status,
       run_duration,
       output,
       additional_info
FROM   all_scheduler_job_run_details
WHERE  owner = 'APP_SCHEMA'
AND    job_name IN (
         'PURGE_EXPIRED_RECORDS_J',
         'PURGE_ORPHANED_CHUNKS_J'
       )
ORDER BY log_date DESC;

Eseguire uno dei due job una volta manualmente se si desidera convalidare immediatamente l'installazione:

BEGIN
  DBMS_SCHEDULER.RUN_JOB(
    job_name => 'APP_SCHEMA.PURGE_EXPIRED_RECORDS_J',
    use_current_session => TRUE
  );
END;
/

Conclusione

In questa guida è stato illustrato come i valori predefiniti di conservazione a livello di schema, i valori ttl_days per record e TimeToLiveAnchor funzionano insieme, come i job gestiti rimuovono le righe scadute e orfane e come aggiornare le scadenze esistenti da Python.

→ Dopo aver appreso come controllare la durata dei record, ora è possibile passare a Usa filtro metadati e metadati.

Codice completo

Copiare il codice completo che segue.

#Copyright © 2026 Oracle and/or its affiliates.
#This software is under the Apache License 2.0
#(LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0) or Universal Permissive License
#(UPL) 1.0 (LICENSE-UPL or https://oss.oracle.com/licenses/upl), at your option.

#Oracle Agent Memory Code Example - Use Time-to-Live for Messages and Memories
#-----------------------------------------------------------------------------

##Create a retention aware client

from datetime import datetime, timezone

import oracledb

from oracleagentmemory.apis import TimeToLiveAnchor
from oracleagentmemory.core import (
    MemoryRetentionConfig,
    OracleAgentMemory,
    SchemaPolicy,
)
from oracleagentmemory.core.embedders.embedder import Embedder
from oracleagentmemory.core.llms.llm import Llm

embedder = Embedder(model="YOUR_EMBEDDING_MODEL")
llm = Llm(
    model="YOUR_MEMORY_LLM_MODEL",
    api_base="YOUR_MEMORY_LLM_BASE_URL",
    api_key="YOUR_MEMORY_LLM_API_KEY",
)
db_pool = oracledb.SessionPool(
    user="YOUR DB USER",
    password="YOUR DB PASSWORD",
    dsn="localhost:1521/...",
)
memory_store_id = "T_TIME_TO_LIVE"

memory = OracleAgentMemory(
    connection=db_pool,
    embedder=embedder,
    llm=llm,
    schema_policy=SchemaPolicy.CREATE_IF_NECESSARY,
    memory_store_id=memory_store_id,
    memory_retention_config=MemoryRetentionConfig(
        default_ttl_days=30,
        max_ttl_days=90,
    ),
)

##Add messages and memories with time to live

ttl_thread = memory.create_thread(
    thread_id="ttl_demo_thread",
    user_id="user_123",
)

message_ids = ttl_thread.add_messages(
    [
        {
            "id": "msg-ttl-1",
            "role": "user",
            "content": (
                "I opened ticket 1042 yesterday because the laptop battery failed."
            ),
            "timestamp": "2026-04-01T09:00:00Z",
        },
        {
            "id": "msg-ttl-2",
            "role": "assistant",
            "content": (
                "I will keep ticket 1042 active and send a replacement checklist."
            ),
            "timestamp": "2026-04-01T09:01:00Z",
        },
    ]
)

timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
#^ for instance, 2026-04-01T09:00:00Z

backfilled_memory_id = ttl_thread.add_memory(
    "Ticket 1042 battery-failure report was filed on 2026-04-01.",
    memory_id="mem-ttl-backfill",
    timestamp=timestamp,
    ttl_days=90,
    ttl_anchor=TimeToLiveAnchor.TIMESTAMP,
)

short_lived_memory_id = ttl_thread.add_memory(
    "Replacement checklist should be revisited within one week.",
    memory_id="mem-ttl-short",
    ttl_days=7,
)

print(message_ids)
print(backfilled_memory_id)
print(short_lived_memory_id)
#['msg-ttl-1', 'msg-ttl-2']
#mem-ttl-backfill
#mem-ttl-short

##Refresh time to live on existing records

ttl_thread.update_message("msg-ttl-1", ttl_days=14)

ttl_thread.update_memory(
    "mem-ttl-backfill",
    ttl_days=60,
    ttl_anchor=TimeToLiveAnchor.TIMESTAMP,
)