<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Memoria on lo0 — Blog Técnico</title><link>https://blog.lo0.es/tags/memoria/</link><description>Recent content in Memoria on lo0 — Blog Técnico</description><generator>Hugo -- gohugo.io</generator><language>es</language><lastBuildDate>Thu, 17 Sep 2026 05:00:00 +0200</lastBuildDate><atom:link href="https://blog.lo0.es/tags/memoria/index.xml" rel="self" type="application/rss+xml"/><item><title>Memoria entre hilos para los agentes de retail: PostgresStore, el cliente que vuelve y lo que no se debe recordar</title><link>https://blog.lo0.es/posts/memoria-agentes-langgraph-postgresstore-cliente-que-vuelve/</link><pubDate>Thu, 17 Sep 2026 05:00:00 +0200</pubDate><guid>https://blog.lo0.es/posts/memoria-agentes-langgraph-postgresstore-cliente-que-vuelve/</guid><description>&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>El checkpointer es memoria de un hilo; el almacén es memoria entre hilos. Un cliente que vuelve mañana abre un hilo nuevo y el agente no sabe nada de ayer salvo lo que alguien haya puesto en el almacén. &lt;code>PostgresStore&lt;/code> es ese almacén: una tabla &lt;code>store&lt;/code> con espacio de nombres, clave y valor JSONB, y opcionalmente una tabla &lt;code>store_vectors&lt;/code> con embeddings para buscar por significado.&lt;/p>
&lt;p>El almacén llega al agente por &lt;code>store=&lt;/code> en &lt;code>create_agent&lt;/code> y se lee desde las herramientas y desde el prompt dinámico por &lt;code>runtime.store&lt;/code>. Verificado: un dato escrito en un hilo por el cliente &lt;code>C9&lt;/code> aparece en otro hilo del mismo cliente y no aparece en un hilo de &lt;code>C10&lt;/code>. El aislamiento es el espacio de nombres, &lt;code>(&amp;quot;clientes&amp;quot;, cliente_id)&lt;/code>, y el identificador lo pone el contexto de ejecución, no el modelo.&lt;/p>
&lt;p>Lo que se guarda lo decide la aplicación. Las preferencias que el cliente declara sí, por una herramienta con esquema cerrado. Las que el modelo deduce, no. Los hechos de negocio (última devolución, pedidos abiertos) no se duplican en el almacén: ya están en sus tablas y la herramienta los consulta ahí.&lt;/p>
&lt;p>El TTL existe y tiene tres partes: &lt;code>default_ttl&lt;/code> en minutos, &lt;code>refresh_on_read&lt;/code> que renueva la caducidad al leer, y un barredor que borra lo caducado. El barredor es un hilo dentro del proceso; con tres réplicas son tres barredores. Se desactiva y &lt;code>sweep_ttl()&lt;/code> se llama desde el CronJob de retención del artículo 5.&lt;/p>
&lt;p>No hay borrado por espacio de nombres. Para cumplir una solicitud de supresión hay que listar las claves del cliente con &lt;code>search&lt;/code> y borrarlas una a una con &lt;code>delete&lt;/code>. Es un bucle de tres líneas y una consulta SQL de comprobación, y tiene que existir antes de guardar el primer dato.&lt;/p>
&lt;p>&lt;code>InMemoryStore&lt;/code> no admite TTL: &lt;code>put(..., ttl=...)&lt;/code> lanza &lt;code>NotImplementedError&lt;/code>. Los tests del almacén con TTL van en el grupo de Postgres, no en el de memoria.&lt;/p>
&lt;h2 id="estás-aquí-lo-que-el-hilo-no-recuerda">Estás aquí: lo que el hilo no recuerda&lt;/h2>
&lt;p>Los cuatro agentes de la &lt;a href="https://blog.lo0.es/posts/agentes-langgraph-retail-marco-serie/">serie de retail&lt;/a> tienen persistencia por hilo. Un hilo es un turno de trabajo del dependiente, una conversación con un cliente sobre un pedido, una pasada de compras o una ficha. Cuando el hilo termina, lo que se aprendió en él se va con la retención.&lt;/p>
&lt;p>Hay cosas que deberían quedarse. Un cliente que ha dicho tres veces que prefiere recoger en la tienda de al lado no debería decirlo una cuarta. Un dependiente de la sección de menaje no debería tener que decir &amp;ldquo;menaje&amp;rdquo; en cada pregunta. Un proveedor que avisó de un retraso el lunes no debería sorprender al planificador del jueves.&lt;/p>
&lt;p>Eso es el almacén, y este artículo lo añade a los agentes 1, 2 y 3. Es el primero de una tanda de dos; el segundo pone un supervisor delante de los cuatro.&lt;/p>
&lt;h2 id="la-analogía-la-ficha-de-cliente-detrás-del-mostrador">La analogía: la ficha de cliente detrás del mostrador&lt;/h2>
&lt;p>Un buen dependiente lleva en la cabeza a los clientes habituales: la señora que recoge los jueves, el que siempre pide factura. Cuando el dependiente cambia de turno, eso se pierde, y por eso las tiendas que lo hacen bien tienen una ficha de cliente detrás del mostrador con dos o tres líneas escritas a mano. Lo que no tiene la ficha es lo que el sistema ya sabe: los pedidos, las devoluciones, la dirección. Eso se consulta.&lt;/p>
&lt;p>El almacén es la ficha. Las tablas de negocio son el sistema. La regla de qué va en cada sitio es la misma que en la tienda: en la ficha, lo que el cliente ha dicho y no está en ningún otro sitio; en el sistema, todo lo demás.&lt;/p>
&lt;h2 id="checkpointer-y-almacén">Checkpointer y almacén&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;th>Checkpointer&lt;/th>
&lt;th>Almacén&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Unidad&lt;/td>
&lt;td>Hilo (&lt;code>thread_id&lt;/code>)&lt;/td>
&lt;td>Espacio de nombres (tupla de cadenas) y clave&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Contenido&lt;/td>
&lt;td>Estado completo del grafo por superstep&lt;/td>
&lt;td>Documentos JSONB pequeños&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Quién escribe&lt;/td>
&lt;td>El motor, en cada superstep&lt;/td>
&lt;td>Herramientas y aplicación, cuando quieren&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Búsqueda&lt;/td>
&lt;td>Por &lt;code>thread_id&lt;/code>&lt;/td>
&lt;td>Por prefijo de espacio de nombres, filtro sobre el valor y, con índice, por significado&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Caducidad&lt;/td>
&lt;td>Retención por hilo entero, externa&lt;/td>
&lt;td>TTL por documento, con barredor&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Tabla&lt;/td>
&lt;td>&lt;code>checkpoints&lt;/code> y compañía&lt;/td>
&lt;td>&lt;code>store&lt;/code> y &lt;code>store_vectors&lt;/code>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Los dos viven en la base &lt;code>agentes&lt;/code>, en tablas distintas, y los dos tienen &lt;code>setup()&lt;/code> propio que corre en el mismo Job de migraciones del &lt;a href="https://blog.lo0.es/posts/servir-agentes-langgraph-sin-langgraph-api/">artículo 5&lt;/a>.&lt;/p>
&lt;h2 id="qué-se-guarda-y-qué-no">Qué se guarda y qué no&lt;/h2>
&lt;p>La decisión más importante del artículo se toma antes de escribir código. Tres categorías:&lt;/p>
&lt;p>&lt;strong>Se guarda: lo que el cliente o el empleado declara y no está en ninguna tabla.&lt;/strong> Preferencia de recogida, canal de contacto preferido, idioma, &amp;ldquo;no me llaméis por la mañana&amp;rdquo;, la sección en la que trabaja el dependiente. Son datos que la persona ha dicho y que quiere que se recuerden.&lt;/p>
&lt;p>&lt;strong>No se guarda: lo que el modelo deduce.&lt;/strong> &amp;ldquo;Parece un cliente exigente&amp;rdquo;, &amp;ldquo;suele devolver&amp;rdquo;. Lo primero es una opinión del modelo sobre una persona y no tiene sitio en ningún almacén de una empresa que responda ante el RGPD. Lo segundo es un hecho que ya está en &lt;code>devoluciones&lt;/code> y que la herramienta puede contar cuando haga falta, con la fecha y el importe, en vez de como etiqueta.&lt;/p>
&lt;p>&lt;strong>No se duplica: lo que está en las tablas de negocio.&lt;/strong> Pedidos, devoluciones, stock, incidencias. El almacén no es una caché de la base de datos; es la ficha escrita a mano.&lt;/p>
&lt;p>Para el caso 3 hay una cuarta categoría: avisos de proveedores que no encajan en &lt;code>incidencias&lt;/code> porque no son incidencias todavía (&amp;ldquo;nos han dicho que la fábrica cierra en agosto&amp;rdquo;). Van al almacén con espacio de nombres &lt;code>(&amp;quot;proveedores&amp;quot;, proveedor_id)&lt;/code> y los escribe compras desde su aplicación, no el modelo.&lt;/p>
&lt;h2 id="el-almacén">El almacén&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">psycopg_pool&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">ConnectionPool&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">langgraph.store.postgres&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">PostgresStore&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">langgraph.store.base&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">TTLConfig&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">pool_agentes&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">ConnectionPool&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">os&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">environ&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;PG_AGENTES&amp;#34;&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="n">min_size&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">2&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">max_size&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">8&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">kwargs&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="s2">&amp;#34;autocommit&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">True&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;prepare_threshold&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">})&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">almacen&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">PostgresStore&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">pool_agentes&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">index&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="s2">&amp;#34;dims&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">1024&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;embed&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">emb&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;fields&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;texto&amp;#34;&lt;/span>&lt;span class="p">]},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">ttl&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">TTLConfig&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">default_ttl&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">60&lt;/span> &lt;span class="o">*&lt;/span> &lt;span class="mi">24&lt;/span> &lt;span class="o">*&lt;/span> &lt;span class="mi">365&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">refresh_on_read&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">True&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">omit_expired&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">True&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">sweep_interval_minutes&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">None&lt;/span>&lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>emb&lt;/code> es el mismo &lt;code>OpenAIEmbeddings&lt;/code> del &lt;a href="https://blog.lo0.es/posts/agente-empleado-tienda-langgraph-rag-tools/">caso 1&lt;/a>, contra el modelo de embeddings servido en vLLM detrás de LiteLLM, con &lt;code>check_embedding_ctx_length=False&lt;/code>. &lt;code>dims&lt;/code> tiene que coincidir con la dimensión del modelo; el índice se crea con esa dimensión en &lt;code>setup()&lt;/code> y no se cambia después sin recrear la tabla.&lt;/p>
&lt;p>&lt;code>fields&lt;/code> dice qué campos del valor se indexan. Solo &lt;code>texto&lt;/code>. El resto del documento (origen, fecha, quién lo escribió) es metadato y se filtra, no se busca.&lt;/p>
&lt;p>El TTL de un año con renovación al leer es la política para preferencias: un cliente que vuelve las mantiene; uno que no vuelve en un año las pierde. &lt;code>sweep_interval_minutes=None&lt;/code> desactiva el barredor en proceso; se explica más abajo.&lt;/p>
&lt;p>&lt;code>setup()&lt;/code> con &lt;code>index&lt;/code> configurado aplica las migraciones de la tabla &lt;code>store&lt;/code> y además las de &lt;code>store_vectors&lt;/code>, empezando por &lt;code>CREATE EXTENSION IF NOT EXISTS vector&lt;/code> dentro de un bloque &lt;code>DO&lt;/code>. Eso exige que el rol del Job pueda crear extensiones, o que la extensión ya exista en la base. En un cluster de CloudNativePG lo segundo es lo habitual: la extensión se declara en el &lt;code>Cluster&lt;/code> y el rol del Job no necesita ser superusuario.&lt;/p>
&lt;h2 id="las-herramientas">Las herramientas&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="nd">@tool&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">recordar_preferencia&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">tipo&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">Literal&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;recogida&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;contacto&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;idioma&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;horario&amp;#34;&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="n">valor&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">str&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">runtime&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">ToolRuntime&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">ContextoCliente&lt;/span>&lt;span class="p">])&lt;/span> &lt;span class="o">-&amp;gt;&lt;/span> &lt;span class="nb">dict&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;&amp;#34;&amp;#34;Guarda una preferencia que el cliente ha declarado explícitamente en esta conversación.&amp;#34;&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">ns&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;clientes&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">runtime&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">context&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">cliente_id&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">runtime&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">store&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">put&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">ns&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="sa">f&lt;/span>&lt;span class="s2">&amp;#34;pref-&lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="n">tipo&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;texto&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="sa">f&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="n">tipo&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">: &lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="n">valor&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;tipo&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">tipo&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;valor&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">valor&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;origen&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;cliente&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;hilo&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">runtime&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">config&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;configurable&amp;#34;&lt;/span>&lt;span class="p">][&lt;/span>&lt;span class="s2">&amp;#34;thread_id&amp;#34;&lt;/span>&lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">})&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="p">{&lt;/span>&lt;span class="s2">&amp;#34;ok&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">True&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;tipo&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">tipo&lt;/span>&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nd">@tool&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">preferencias&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">runtime&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">ToolRuntime&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">ContextoCliente&lt;/span>&lt;span class="p">])&lt;/span> &lt;span class="o">-&amp;gt;&lt;/span> &lt;span class="nb">list&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="nb">dict&lt;/span>&lt;span class="p">]:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;&amp;#34;&amp;#34;Preferencias declaradas por el cliente en conversaciones anteriores.&amp;#34;&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">ns&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;clientes&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">runtime&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">context&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">cliente_id&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="p">[{&lt;/span>&lt;span class="s2">&amp;#34;tipo&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">it&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">value&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;tipo&amp;#34;&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="s2">&amp;#34;valor&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">it&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">value&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;valor&amp;#34;&lt;/span>&lt;span class="p">]}&lt;/span> &lt;span class="k">for&lt;/span> &lt;span class="n">it&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">runtime&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">store&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">search&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">ns&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">limit&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">10&lt;/span>&lt;span class="p">)]&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Tres decisiones.&lt;/p>
&lt;p>&lt;strong>La clave es &lt;code>pref-{tipo}&lt;/code>, no un identificador nuevo por escritura.&lt;/strong> Una preferencia nueva del mismo tipo sobreescribe la anterior. Sin eso, &amp;ldquo;recogida: T002&amp;rdquo; y &amp;ldquo;recogida: T001&amp;rdquo; conviven y el modelo elige.&lt;/p>
&lt;p>&lt;strong>El tipo es un &lt;code>Literal&lt;/code> cerrado.&lt;/strong> El modelo no puede inventar categorías. Si el cliente dice algo que no encaja, no se guarda. Es la parte del esquema que hace que el almacén no acumule opiniones.&lt;/p>
&lt;p>&lt;strong>El espacio de nombres viene del contexto.&lt;/strong> &lt;code>runtime.context.cliente_id&lt;/code> lo puso la aplicación a partir de la sesión. El modelo no ve ese parámetro y no puede escribir en la ficha de otro cliente. Verificado: un dato escrito con contexto &lt;code>C9&lt;/code> no aparece en una búsqueda con contexto &lt;code>C10&lt;/code>, en otro hilo, con otro agente construido sobre el mismo almacén.&lt;/p>
&lt;p>La búsqueda de &lt;code>preferencias&lt;/code> no lleva &lt;code>query&lt;/code>: devuelve todo el espacio de nombres, que son como mucho cuatro documentos. La búsqueda semántica se usa en el caso 3, donde los avisos de un proveedor pueden ser decenas:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="nd">@tool&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">avisos_proveedor&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">consulta&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">str&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">runtime&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">ToolRuntime&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">ContextoCompras&lt;/span>&lt;span class="p">])&lt;/span> &lt;span class="o">-&amp;gt;&lt;/span> &lt;span class="nb">list&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="nb">dict&lt;/span>&lt;span class="p">]:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;&amp;#34;&amp;#34;Avisos recientes de compras sobre el proveedor relacionados con la consulta.&amp;#34;&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">ns&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;proveedores&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">runtime&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">context&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">proveedor_id&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>&lt;span class="s2">&amp;#34;texto&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">it&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">value&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;texto&amp;#34;&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="s2">&amp;#34;fecha&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">it&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">value&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;fecha&amp;#34;&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="s2">&amp;#34;similitud&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">round&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">it&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">score&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">3&lt;/span>&lt;span class="p">)}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">for&lt;/span> &lt;span class="n">it&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">runtime&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">store&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">search&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">ns&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">query&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">consulta&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">limit&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">3&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="n">it&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">score&lt;/span> &lt;span class="ow">is&lt;/span> &lt;span class="ow">not&lt;/span> &lt;span class="kc">None&lt;/span> &lt;span class="ow">and&lt;/span> &lt;span class="n">it&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">score&lt;/span> &lt;span class="o">&amp;gt;&lt;/span> &lt;span class="mf">0.4&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Con el índice configurado, &lt;code>search&lt;/code> con &lt;code>query&lt;/code> calcula el embedding de la consulta con &lt;code>emb&lt;/code>, lo compara por coseno contra &lt;code>store_vectors&lt;/code> dentro del prefijo, y devuelve &lt;code>SearchItem&lt;/code> con &lt;code>score&lt;/code>. Sin &lt;code>query&lt;/code>, es un listado por prefijo con filtro opcional sobre el valor.&lt;/p>
&lt;h2 id="el-prompt-que-lee-el-almacén">El prompt que lee el almacén&lt;/h2>
&lt;p>Para el caso 2 las preferencias no van por herramienta. Van en el prompt, porque el modelo tiene que saberlas antes de proponer nada y no debería tener que acordarse de llamar a &lt;code>preferencias&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="nd">@dynamic_prompt&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">prompt_cliente&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">request&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">ModelRequest&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">-&amp;gt;&lt;/span> &lt;span class="nb">str&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">ctx&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">request&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">runtime&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">context&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">prefs&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">request&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">runtime&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">store&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">search&lt;/span>&lt;span class="p">((&lt;/span>&lt;span class="s2">&amp;#34;clientes&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">ctx&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">cliente_id&lt;/span>&lt;span class="p">),&lt;/span> &lt;span class="n">limit&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">10&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">lineas&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="se">\n&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">join&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="sa">f&lt;/span>&lt;span class="s2">&amp;#34;- &lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="n">p&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">value&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;texto&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="k">for&lt;/span> &lt;span class="n">p&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">prefs&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="ow">or&lt;/span> &lt;span class="s2">&amp;#34;- (ninguna)&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="sa">f&lt;/span>&lt;span class="s2">&amp;#34;Atiendes al cliente &lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="n">ctx&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">cliente_id&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2"> de la tienda &lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="n">ctx&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">tienda&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">.&lt;/span>&lt;span class="se">\n&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="sa">f&lt;/span>&lt;span class="s2">&amp;#34;Preferencias que el cliente ha declarado en otras conversaciones:&lt;/span>&lt;span class="se">\n&lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="n">lineas&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="se">\n&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;Respétalas al proponer recogida, contacto o plazo. No las cites como si las hubieras deducido.&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">...&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Verificado que &lt;code>request.runtime.store&lt;/code> está disponible en el prompt dinámico y devuelve lo mismo que desde una herramienta. Es una consulta a Postgres por cada llamada al modelo; con &lt;code>refresh_on_read=True&lt;/code>, además renueva la caducidad de esas preferencias en cada llamada, que es lo que se quiere para un cliente activo y lo que hay que tener en cuenta al estimar escrituras: cada lectura es también un &lt;code>UPDATE&lt;/code> de &lt;code>expires_at&lt;/code>.&lt;/p>
&lt;p>La última frase del prompt existe porque un modelo que recibe &amp;ldquo;prefiere recogida en T002&amp;rdquo; tiende a decir &amp;ldquo;veo que sueles recoger en T002&amp;rdquo;, y eso a un cliente le suena a vigilancia. La preferencia se aplica, no se comenta.&lt;/p>
&lt;h2 id="el-ttl-y-el-barredor">El TTL y el barredor&lt;/h2>
&lt;p>&lt;code>TTLConfig&lt;/code> tiene cuatro campos: &lt;code>default_ttl&lt;/code> en minutos para lo que se escriba sin &lt;code>ttl&lt;/code> explícito, &lt;code>refresh_on_read&lt;/code> para renovar al leer, &lt;code>omit_expired&lt;/code> para que las lecturas ignoren lo caducado aunque el barredor no haya pasado, y &lt;code>sweep_interval_minutes&lt;/code> para el barredor.&lt;/p>
&lt;p>El barredor es &lt;code>start_ttl_sweeper()&lt;/code>: un hilo dentro del proceso que cada N minutos ejecuta &lt;code>sweep_ttl()&lt;/code>, que es un &lt;code>DELETE FROM store WHERE expires_at IS NOT NULL AND expires_at &amp;lt; NOW()&lt;/code>. Con tres réplicas del servicio del caso 2, son tres hilos haciendo el mismo &lt;code>DELETE&lt;/code> sobre la misma tabla. No rompe nada; es trabajo repetido y una fuente de bloqueos breves que no hace falta tener.&lt;/p>
&lt;p>Se deja &lt;code>sweep_interval_minutes=None&lt;/code> y el barrido va en el CronJob de retención del artículo 5, una línea más:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="n">borrados&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">almacen&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">sweep_ttl&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">log&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">info&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;store: &lt;/span>&lt;span class="si">%d&lt;/span>&lt;span class="s2"> documentos caducados borrados&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">borrados&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>omit_expired=True&lt;/code> es lo que hace que entre barrido y barrido lo caducado no se lea. Con &lt;code>False&lt;/code>, una preferencia caducada hace once meses seguiría apareciendo hasta la noche.&lt;/p>
&lt;p>Los plazos por espacio de nombres: un año con renovación para &lt;code>clientes&lt;/code>, 90 días sin renovación para &lt;code>proveedores&lt;/code> (un aviso de hace tres meses ya es una incidencia o no fue nada), y 180 días con renovación para &lt;code>empleados&lt;/code>. Se pasan como &lt;code>ttl=&lt;/code> en cada &lt;code>put&lt;/code>, en minutos, y &lt;code>default_ttl&lt;/code> es solo la red.&lt;/p>
&lt;h2 id="el-borrado-que-la-ley-pide">El borrado que la ley pide&lt;/h2>
&lt;p>Un cliente puede pedir que se borre lo que la empresa sabe de él, y la respuesta tiene que ser completa. El almacén no tiene &amp;ldquo;borra todo lo de este espacio de nombres&amp;rdquo;. &lt;code>BaseStore&lt;/code> tiene &lt;code>delete(namespace, key)&lt;/code> y &lt;code>list_namespaces&lt;/code>, y nada más.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">suprimir_cliente&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">cliente_id&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">str&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">-&amp;gt;&lt;/span> &lt;span class="nb">int&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">ns&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;clientes&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">cliente_id&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">items&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">almacen&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">search&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">ns&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">limit&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">1000&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">for&lt;/span> &lt;span class="n">it&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">items&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">almacen&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">delete&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">ns&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">it&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">key&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="nb">len&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">items&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Y la comprobación, que es la que se guarda como evidencia de la solicitud:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="cl">&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">count&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">*&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">store&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">WHERE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">prefix&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;clientes.&amp;#39;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">||&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">%&lt;/span>&lt;span class="n">s&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">count&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">*&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">store_vectors&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">WHERE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">prefix&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;clientes.&amp;#39;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">||&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">%&lt;/span>&lt;span class="n">s&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>El prefijo en la tabla es la tupla unida por puntos. Las dos consultas tienen que dar cero, y las dos van en el registro de la solicitud junto con la fecha.&lt;/p>
&lt;p>Esta función existe y está probada antes de que el agente guarde la primera preferencia. Y va acompañada de la del checkpointer: los hilos del cliente en &lt;code>hilos&lt;/code> se borran con &lt;code>delete_thread&lt;/code>, y las trazas de Langfuse con su API de borrado por &lt;code>user_id&lt;/code>, que es otro artículo. Lo que este artículo fija es que el almacén no puede ser el sitio donde un dato sobre una persona se queda sin que nadie sepa borrarlo.&lt;/p>
&lt;h2 id="lo-que-se-prueba-y-lo-que-no">Lo que se prueba y lo que no&lt;/h2>
&lt;p>Con &lt;code>InMemoryStore&lt;/code> se prueba todo el aislamiento y la lectura: el almacén admite &lt;code>index&lt;/code> con &lt;code>FakeEmbeddings&lt;/code> de &lt;code>langchain-core&lt;/code>, y los tests del &lt;a href="https://blog.lo0.es/posts/probar-agentes-langgraph-modelo-falso-pytest/">artículo de pruebas&lt;/a> se extienden con tres:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">test_preferencia_sobrevive_al_hilo&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">store&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">agente_a&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">build_cliente&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">ModeloGuion&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">responses&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">llamada&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;recordar_preferencia&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">tipo&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;recogida&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">valor&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;T002&amp;#34;&lt;/span>&lt;span class="p">),&lt;/span> &lt;span class="n">AIMessage&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">content&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;ok&amp;#34;&lt;/span>&lt;span class="p">)]),&lt;/span> &lt;span class="n">InMemorySaver&lt;/span>&lt;span class="p">(),&lt;/span> &lt;span class="n">store&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">agente_a&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">invoke&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">entrada&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;me lo llevo a T002&amp;#34;&lt;/span>&lt;span class="p">),&lt;/span> &lt;span class="n">context&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">Ctx&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;C9&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;T001&amp;#34;&lt;/span>&lt;span class="p">),&lt;/span> &lt;span class="n">config&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">hilo&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;h1&amp;#34;&lt;/span>&lt;span class="p">))&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">agente_b&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">build_cliente&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">ModeloGuion&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">responses&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">llamada&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;preferencias&amp;#34;&lt;/span>&lt;span class="p">),&lt;/span> &lt;span class="n">AIMessage&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">content&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;ok&amp;#34;&lt;/span>&lt;span class="p">)]),&lt;/span> &lt;span class="n">InMemorySaver&lt;/span>&lt;span class="p">(),&lt;/span> &lt;span class="n">store&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">r&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">agente_b&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">invoke&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">entrada&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;hola&amp;#34;&lt;/span>&lt;span class="p">),&lt;/span> &lt;span class="n">context&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">Ctx&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;C9&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;T003&amp;#34;&lt;/span>&lt;span class="p">),&lt;/span> &lt;span class="n">config&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">hilo&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;h2&amp;#34;&lt;/span>&lt;span class="p">))&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">assert&lt;/span> &lt;span class="n">json&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">loads&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">tool_msg&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">r&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">content&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="p">[{&lt;/span>&lt;span class="s2">&amp;#34;tipo&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;recogida&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;valor&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;T002&amp;#34;&lt;/span>&lt;span class="p">}]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">test_otro_cliente_no_ve_nada&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">store&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">...&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">assert&lt;/span> &lt;span class="n">json&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">loads&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">tool_msg&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">r&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">content&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="p">[]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">test_sin_almacen_las_herramientas_no_rompen&lt;/span>&lt;span class="p">():&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">agente&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">build_cliente&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">ModeloGuion&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">responses&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">llamada&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;preferencias&amp;#34;&lt;/span>&lt;span class="p">),&lt;/span> &lt;span class="n">AIMessage&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">content&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;ok&amp;#34;&lt;/span>&lt;span class="p">)]),&lt;/span> &lt;span class="n">InMemorySaver&lt;/span>&lt;span class="p">(),&lt;/span> &lt;span class="n">store&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">None&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">r&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">agente&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">invoke&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">entrada&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;hola&amp;#34;&lt;/span>&lt;span class="p">),&lt;/span> &lt;span class="n">context&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">Ctx&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;C9&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;T001&amp;#34;&lt;/span>&lt;span class="p">),&lt;/span> &lt;span class="n">config&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">hilo&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;h1&amp;#34;&lt;/span>&lt;span class="p">))&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">assert&lt;/span> &lt;span class="n">json&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">loads&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">tool_msg&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">r&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">content&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="p">[]&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>El tercero fija un detalle verificado: sin &lt;code>store=&lt;/code> en &lt;code>create_agent&lt;/code>, &lt;code>runtime.store&lt;/code> es &lt;code>None&lt;/code> dentro de la herramienta, y la herramienta tiene que comprobarlo y devolver vacío en vez de lanzar. Es el comportamiento que se quiere si el almacén se despliega después del agente.&lt;/p>
&lt;p>Lo que no se prueba en memoria: el TTL. &lt;code>InMemoryStore.put(..., ttl=...)&lt;/code> lanza &lt;code>NotImplementedError: TTL is not supported by InMemoryStore&lt;/code>. Los tests de caducidad, &lt;code>refresh_on_read&lt;/code>, &lt;code>omit_expired&lt;/code> y &lt;code>sweep_ttl&lt;/code> van en el grupo de Postgres, con un TTL de un minuto y un &lt;code>sleep&lt;/code>.&lt;/p>
&lt;h2 id="lo-que-se-mide">Lo que se mide&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Métrica&lt;/th>
&lt;th>Dónde&lt;/th>
&lt;th>Qué dice&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Documentos por cliente&lt;/td>
&lt;td>&lt;code>SELECT prefix, count(*) FROM store GROUP BY prefix&lt;/code>&lt;/td>
&lt;td>Con el esquema cerrado, cuatro como máximo. Más, el &lt;code>Literal&lt;/code> se ha abierto&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Preferencias escritas por conversación&lt;/td>
&lt;td>Spans de &lt;code>recordar_preferencia&lt;/code> en Langfuse&lt;/td>
&lt;td>Por debajo de 0,1. Un modelo que guarda algo en cada conversación está guardando lo que no debe&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Conversaciones del caso 2 en las que se aplicó una preferencia&lt;/td>
&lt;td>Comparar la propuesta con &lt;code>store&lt;/code>&lt;/td>
&lt;td>La utilidad. Si no cambia ninguna propuesta, la memoria no sirve&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Caducados borrados por barrido&lt;/td>
&lt;td>Log del CronJob&lt;/td>
&lt;td>Debe crecer a partir del mes doce y no antes&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Solicitudes de supresión y tiempo hasta cero&lt;/td>
&lt;td>Registro de solicitudes&lt;/td>
&lt;td>La obligación. Tiene que ser el mismo día&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h2 id="checklist">Checklist&lt;/h2>
&lt;ul>
&lt;li>Hay una lista escrita de qué se guarda, qué no y qué no se duplica, y el &lt;code>Literal&lt;/code> de tipos la refleja.&lt;/li>
&lt;li>El espacio de nombres viene de &lt;code>runtime.context&lt;/code>, nunca de un argumento del modelo.&lt;/li>
&lt;li>Las claves son por tipo de preferencia y sobreescriben.&lt;/li>
&lt;li>&lt;code>index&lt;/code> usa el modelo de embeddings de la serie y &lt;code>dims&lt;/code> coincide.&lt;/li>
&lt;li>&lt;code>setup()&lt;/code> del almacén corre en el Job de migraciones y la extensión &lt;code>vector&lt;/code> existe antes.&lt;/li>
&lt;li>&lt;code>sweep_interval_minutes=None&lt;/code> y &lt;code>sweep_ttl()&lt;/code> en el CronJob de retención.&lt;/li>
&lt;li>&lt;code>omit_expired=True&lt;/code>.&lt;/li>
&lt;li>TTL por espacio de nombres pasado en cada &lt;code>put&lt;/code>.&lt;/li>
&lt;li>&lt;code>suprimir_cliente&lt;/code> existe, está probada y deja evidencia con las dos consultas a cero.&lt;/li>
&lt;li>Las herramientas comprueban &lt;code>runtime.store is None&lt;/code>.&lt;/li>
&lt;li>Los tests de TTL están en el grupo de Postgres.&lt;/li>
&lt;/ul>
&lt;h2 id="trampas-y-cosas-que-no-son-lo-que-parecen">Trampas y cosas que no son lo que parecen&lt;/h2>
&lt;p>&lt;strong>El almacén como caché de la base de negocio.&lt;/strong> Se desincroniza y se convierte en la fuente que el modelo prefiere. Solo lo que no está en ninguna tabla.&lt;/p>
&lt;p>&lt;strong>El modelo escribiendo lo que deduce.&lt;/strong> Un &lt;code>put&lt;/code> con texto libre desde una herramienta sin esquema acaba con opiniones sobre personas en Postgres. &lt;code>Literal&lt;/code> cerrado y origen &lt;code>cliente&lt;/code>.&lt;/p>
&lt;p>&lt;strong>Tres réplicas, tres barredores.&lt;/strong> El barredor es un hilo en proceso. Desactivar y barrer desde el CronJob.&lt;/p>
&lt;p>&lt;strong>&lt;code>refresh_on_read&lt;/code> escribe.&lt;/strong> Cada lectura desde el prompt renueva &lt;code>expires_at&lt;/code>. Es un &lt;code>UPDATE&lt;/code> por llamada al modelo; se cuenta.&lt;/p>
&lt;p>&lt;strong>No hay borrado por espacio de nombres.&lt;/strong> Listar y borrar, y comprobar en SQL. Antes de la primera escritura.&lt;/p>
&lt;p>&lt;strong>&lt;code>InMemoryStore&lt;/code> no tiene TTL.&lt;/strong> &lt;code>NotImplementedError&lt;/code>. Los tests de caducidad van contra Postgres.&lt;/p>
&lt;p>&lt;strong>&lt;code>setup()&lt;/code> crea la extensión.&lt;/strong> Necesita permiso o la extensión ya creada. En CNPG, se declara en el &lt;code>Cluster&lt;/code>.&lt;/p>
&lt;p>&lt;strong>&amp;ldquo;Veo que sueles…&amp;rdquo;&lt;/strong> El modelo comenta la preferencia y suena a vigilancia. El prompt dice que se aplique y no se cite.&lt;/p>
&lt;h2 id="cierre">Cierre&lt;/h2>
&lt;p>La memoria entre hilos es una tabla con espacio de nombres, un esquema cerrado de lo que se puede escribir, un TTL por tipo de dato y una función de borrado que existe antes que el primer &lt;code>put&lt;/code>. LangGraph pone la tabla, la búsqueda y la caducidad; la aplicación pone lo demás, y lo demás es lo que decide si la ficha de cliente es una ayuda o un problema.&lt;/p>
&lt;p>El siguiente artículo cierra la tanda con un supervisor: un punto de entrada que decide si una conversación es del asistente de sala, del agente de cliente o de compras, y la traspasa con &lt;code>Command(goto=...)&lt;/code> sin perder el hilo ni el almacén.&lt;/p>
&lt;h2 id="ver-también">Ver también&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://blog.lo0.es/posts/agentes-langgraph-retail-marco-serie/">Agentes con LangGraph para retail: el marco de la serie&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/agente-cliente-devoluciones-langgraph-interrupt-hitl/">Agente de atención al cliente con LangGraph: el límite de aprobación humana&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/agente-cadena-suministro-langgraph-send-map-reduce/">Agente de cadena de suministro con LangGraph: incidencias y reposición con map-reduce&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/servir-agentes-langgraph-sin-langgraph-api/">Servir agentes de LangGraph sin langgraph-api&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/probar-agentes-langgraph-modelo-falso-pytest/">Probar agentes de LangGraph sin modelo: tests de grafo con pytest&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/aislar-agentes-ia-cliente-cluster/">Aislar agentes de IA por cliente en el cluster&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="fuentes">Fuentes&lt;/h2>
&lt;ul>
&lt;li>Memoria y almacén en LangGraph: &lt;a href="https://docs.langchain.com/oss/python/langgraph/memory">https://docs.langchain.com/oss/python/langgraph/memory&lt;/a>&lt;/li>
&lt;li>Persistencia en LangGraph (&lt;code>BaseStore&lt;/code>, &lt;code>PostgresStore&lt;/code>): &lt;a href="https://docs.langchain.com/oss/python/langgraph/persistence">https://docs.langchain.com/oss/python/langgraph/persistence&lt;/a>&lt;/li>
&lt;li>Código de &lt;code>langgraph.store.postgres&lt;/code> (langgraph-checkpoint-postgres 3.1.2): &lt;code>PostgresStore.__init__&lt;/code>, &lt;code>setup&lt;/code>, &lt;code>sweep_ttl&lt;/code>, &lt;code>start_ttl_sweeper&lt;/code>, &lt;code>MIGRATIONS&lt;/code>, &lt;code>VECTOR_MIGRATIONS&lt;/code>&lt;/li>
&lt;li>Código de &lt;code>langgraph.store.base&lt;/code> (langgraph-checkpoint 4.2.0): &lt;code>TTLConfig&lt;/code>, &lt;code>IndexConfig&lt;/code>, &lt;code>BaseStore.put&lt;/code>, &lt;code>search&lt;/code>, &lt;code>delete&lt;/code>, &lt;code>list_namespaces&lt;/code>&lt;/li>
&lt;li>Código de &lt;code>langgraph.store.memory&lt;/code> (langgraph-checkpoint 4.2.0): &lt;code>InMemoryStore.put&lt;/code> y el error de TTL&lt;/li>
&lt;li>pgvector: &lt;a href="https://github.com/pgvector/pgvector">https://github.com/pgvector/pgvector&lt;/a>&lt;/li>
&lt;/ul></description></item></channel></rss>