From 94b864b75641ba16f0a4227a469d0b3ffb340047 Mon Sep 17 00:00:00 2001 From: emre-openai Date: Thu, 4 Sep 2025 12:48:19 -0700 Subject: [PATCH 01/11] initial commit --- examples/agents_sdk/session_memory.ipynb | 1016 ++++++++++++++++++++++ 1 file changed, 1016 insertions(+) create mode 100644 examples/agents_sdk/session_memory.ipynb diff --git a/examples/agents_sdk/session_memory.ipynb b/examples/agents_sdk/session_memory.ipynb new file mode 100644 index 0000000000..d0a926f6fc --- /dev/null +++ b/examples/agents_sdk/session_memory.ipynb @@ -0,0 +1,1016 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "3bcfc573", + "metadata": {}, + "source": [ + "# Context Engineering: Short-Term Memory Management with Sessions from OpenAI Agents SDK " + ] + }, + { + "cell_type": "markdown", + "id": "ae724bd5", + "metadata": {}, + "source": [] + }, + { + "cell_type": "markdown", + "id": "eeab798a", + "metadata": {}, + "source": [ + "In this cookbook, we’ll explore how to **manage context effectively using the `Session` object from the [OpenAI Agents SDK](https://github.com/openai/openai-agents-python)**.\n", + "\n", + "AI agents often operate in **long-running, multi-turn interactions**, where keeping the right balance of context is critical. If too much is carried forward, the model risks distraction, inefficiency, or outright failure. If too little is preserved, the agent loses coherence. This guide focuses on two proven context management techniques—**trimming** and **compression**—used by production teams to keep agents fast, reliable, and cost-efficient.\n", + "\n", + "#### Why Context Management Matters\n", + "\n", + "* **Sustained coherence across long threads** – Keep the agent anchored to the latest user goal without dragging along stale details. Session-level trimming and summaries prevent “yesterday’s plan” from overriding today’s ask.\n", + "* **Higher tool-call accuracy** – Focused context improves function selection and argument filling, reducing retries, timeouts, and cascading failures during multi-tool runs.\n", + "* **Lower latency & cost** – Smaller, sharper prompts cut tokens per turn and attention load.\n", + "* **Error & hallucination containment** – Summaries act as “clean rooms” that correct or omit prior mistakes; trimming avoids amplifying bad facts (“context poisoning”) turn after turn.\n", + "* **Easier debugging & observability** – Stable summaries and bounded histories make logs comparable: you can diff summaries, attribute regressions, and reproduce failures reliably.\n", + "* **Multi-issue and handoff resilience** – In multi-problem chats, per-issue mini-summaries let the agent pause/resume, escalate to humans, or hand off to another agent while staying consistent.\n", + "\n", + "#### Real-World Scenario\n", + "\n", + "We’ll ground the techniques in practical examples, such as:\n", + "\n", + "* **Multi-turn Customer Service Conversations**\n", + " When a user raises multiple issues over a long chat history, the agent must stay consistent without carrying every detail forward.\n", + "\n", + "#### Techniques Covered\n", + "\n", + "To address these challenges, we introduce two concrete approaches using OpenAI Agents SDK:\n", + "\n", + "1. **Trimming Messages** – selectively dropping older or less relevant turns.\n", + "2. **Summarizing Messages** – compressing prior exchanges into structured, shorter representations.\n", + "\n", + "\n", + "
" + ] + }, + { + "cell_type": "markdown", + "id": "fc613968", + "metadata": {}, + "source": [ + "## Prerequisites\n", + "\n", + "Before running this cookbook, you must set up the following accounts and complete a few setup actions. These prerequisites are essential to interact with the APIs used in this project.\n", + "\n", + "#### Step0: OpenAI Account\n", + "\n", + "- **Purpose:** \n", + " You need an OpenAI account to access language models and use the Agents SDK featured in this cookbook.\n", + "\n", + "- **Action:** \n", + " [Sign up for an OpenAI account](https://openai.com) if you don’t already have one. Once you have an account, create an API key by visiting the [OpenAI API Keys page](https://platform.openai.com/api-keys)." + ] + }, + { + "cell_type": "markdown", + "id": "3cd9a109", + "metadata": {}, + "source": [ + "#### Step1: Install the Required Libraries\n", + "\n", + "Below we install the `openai-agents` library (the [OpenAI Agents SDK](https://github.com/openai/openai-agents-python)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "87818100", + "metadata": {}, + "outputs": [], + "source": [ + "%pip install openai-agents nest_asyncio" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "f6348e69", + "metadata": {}, + "outputs": [], + "source": [ + "from openai import OpenAI\n", + "\n", + "client = OpenAI()" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "34b39f4c", + "metadata": {}, + "outputs": [], + "source": [ + "from agents import set_tracing_disabled\n", + "set_tracing_disabled(True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d89e8e8d", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Evaluating AI agents is crucial to ensure accuracy, fairness, safety, security, and alignment with human values and objectives.\n" + ] + } + ], + "source": [ + "import asyncio\n", + "from agents import Agent, Runner\n", + "\n", + "async def main():\n", + " agent = Agent(\n", + " name=\"Assistant\",\n", + " instructions=\"Reply very concisely.\",\n", + " )\n", + "\n", + " result = await Runner.run(agent, \"Tell me why it is important to evaluate AI agents.\")\n", + " print(result.final_output)\n", + "\n", + "loop = asyncio.get_running_loop()\n", + "await loop.create_task(main())\n" + ] + }, + { + "cell_type": "markdown", + "id": "c39dab9d", + "metadata": {}, + "source": [ + "### Define Agents\n", + "\n", + "We can start by defining the necessary components from Agents SDK Library." + ] + }, + { + "cell_type": "markdown", + "id": "a2725d9d", + "metadata": {}, + "source": [ + "#### Customer Service Agent" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4451ca22", + "metadata": {}, + "outputs": [], + "source": [ + "support_agent = Agent(\n", + " name=\"Customer Support Assistant\",\n", + " model=\"gpt-5\",\n", + " instructions=(\n", + " \"You are a patient, step-by-step IT support assistant. \"\n", + " \"Your role is to help customers troubleshoot and resolve issues with devices and software. \"\n", + " \"Guidelines:\\n\"\n", + " \"- Be concise and use numbered steps where possible.\\n\"\n", + " \"- Ask only one focused, clarifying question at a time before suggesting next actions.\\n\"\n", + " \"- Track and remember multiple issues across the conversation; update your understanding as new problems emerge.\\n\"\n", + " \"- When a problem is resolved, briefly confirm closure before moving to the next.\\n\"\n", + " \"- If the session grows long, proactively summarize solved and open issues to maintain clarity.\\n\"\n", + " \"- Prefer verified knowledge base information; avoid speculation.\\n\"\n", + " )\n", + ")\n" + ] + }, + { + "cell_type": "markdown", + "id": "b8074e05", + "metadata": {}, + "source": [ + "## 1. Context Summarization " + ] + }, + { + "cell_type": "markdown", + "id": "12c8cf80", + "metadata": {}, + "source": [ + "#### Implement Custom Session Object" + ] + }, + { + "cell_type": "markdown", + "id": "5993098b", + "metadata": {}, + "source": [ + "We are using [Session](https://openai.github.io/openai-agents-python/sessions/) object from [OpenAI Agents Python SDK](https://openai.github.io/openai-agents-python/). Here’s a `MyCustomSession` implementation that **keeps only the last N turns** (a “turn” = one user message and everything until the next user message—including the assistant reply and any tool calls/results). It’s in-memory and trims automatically on every write and read.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 94, + "id": "1b468c78", + "metadata": {}, + "outputs": [], + "source": [ + "from __future__ import annotations\n", + "\n", + "import asyncio\n", + "from collections import deque\n", + "from typing import Any, Deque, Dict, Iterable, List, Optional, Tuple, Union, cast\n", + "\n", + "from agents.memory.session import SessionABC\n", + "from agents.items import TResponseInputItem # typically a dict-like item\n", + "\n", + "\n", + "def _is_user_msg(item: TResponseInputItem) -> bool:\n", + " \"\"\"\n", + " Heuristic: treat items with role=='user' as user messages.\n", + " Falls back to type=='message' and author checks if your SDK uses a different shape.\n", + " Adjust this if your item schema differs.\n", + " \"\"\"\n", + " if isinstance(item, dict):\n", + " role = cast(Dict[str, Any], item).get(\"role\")\n", + " if role == \"user\":\n", + " return True\n", + " # Some SDKs encode messages as {\"type\": \"message\", \"role\": \"...\"}\n", + " if item.get(\"type\") == \"message\" and item.get(\"role\") == \"user\":\n", + " return True\n", + " # Extend here if you carry custom classes with .role attribute:\n", + " role_attr = getattr(item, \"role\", None)\n", + " return role_attr == \"user\"\n", + "\n", + "\n", + "class MyCustomSession(SessionABC):\n", + " \"\"\"\n", + " Custom session that keeps only the last N user-turns.\n", + " A 'turn' is defined as a user message and all subsequent items\n", + " (assistant/tool calls/results) up to—but not including—the next user message.\n", + "\n", + " Works entirely in memory. If you need persistence, replace the in-memory\n", + " deque with your storage of choice (SQLite/Redis/etc.), preserving the\n", + " trimming logic in `_trim_to_last_turns`.\n", + " \"\"\"\n", + "\n", + " def __init__(self, session_id: str, max_turns: int = 8):\n", + " self.session_id = session_id\n", + " self.max_turns = max(1, max_turns)\n", + " self._items: Deque[TResponseInputItem] = deque() # full chronological log\n", + " self._lock = asyncio.Lock()\n", + "\n", + " # ---- SessionABC API ----\n", + "\n", + " async def get_items(self, limit: int | None = None) -> List[TResponseInputItem]:\n", + " \"\"\"\n", + " Return the history trimmed to the last N turns.\n", + " If `limit` is provided, return at most that many most-recent items\n", + " from within the trimmed history.\n", + " \"\"\"\n", + " async with self._lock:\n", + " trimmed = self._trim_to_last_turns(list(self._items))\n", + " if limit is not None and limit >= 0:\n", + " return trimmed[-limit:]\n", + " return trimmed\n", + "\n", + " async def add_items(self, items: List[TResponseInputItem]) -> None:\n", + " \"\"\"\n", + " Append new items, then trim to last N turns.\n", + " \"\"\"\n", + " if not items:\n", + " return\n", + " async with self._lock:\n", + " self._items.extend(items)\n", + " # Trim in place by rebuilding from trimmed list\n", + " trimmed = self._trim_to_last_turns(list(self._items))\n", + " self._items.clear()\n", + " self._items.extend(trimmed)\n", + "\n", + " async def pop_item(self) -> TResponseInputItem | None:\n", + " \"\"\"\n", + " Remove and return the most recent item (post-trim).\n", + " \"\"\"\n", + " async with self._lock:\n", + " if not self._items:\n", + " return None\n", + " return self._items.pop()\n", + "\n", + " async def clear_session(self) -> None:\n", + " \"\"\"\n", + " Remove all items for this session.\n", + " \"\"\"\n", + " async with self._lock:\n", + " self._items.clear()\n", + "\n", + " # ---- Helpers ----\n", + "\n", + " def _trim_to_last_turns(self, items: List[TResponseInputItem]) -> List[TResponseInputItem]:\n", + " \"\"\"\n", + " Keep only the suffix of `items` that contains the last `max_turns` user messages\n", + " and everything after the earliest of those user messages.\n", + "\n", + " Algorithm:\n", + " 1) Scan from the end to find indices of the last `max_turns` user messages.\n", + " 2) Cut history to start from the earliest of those (inclusive).\n", + " Edge cases:\n", + " - If there are fewer than `max_turns` user messages, keep entire history.\n", + " - If there are no user messages yet, treat all existing items as a single turn and keep them.\n", + " \"\"\"\n", + " if not items:\n", + " return items\n", + "\n", + " # Find indices of user messages scanning from the end\n", + " user_indices: List[int] = []\n", + " for idx in range(len(items) - 1, -1, -1):\n", + " if _is_user_msg(items[idx]):\n", + " user_indices.append(idx)\n", + " if len(user_indices) >= self.max_turns:\n", + " break\n", + "\n", + " if not user_indices:\n", + " # No user messages yet; keep everything\n", + " return items\n", + "\n", + " # The earliest index among the last N user messages\n", + " cut_from = min(user_indices) # since we collected from the end\n", + " return items[cut_from:]\n", + "\n", + " # ---- Optional convenience API (not part of SessionABC) ----\n", + "\n", + " async def set_max_turns(self, max_turns: int) -> None:\n", + " async with self._lock:\n", + " self.max_turns = max(1, int(max_turns))\n", + " trimmed = self._trim_to_last_turns(list(self._items))\n", + " self._items.clear()\n", + " self._items.extend(trimmed)\n", + "\n", + " async def raw_items(self) -> List[TResponseInputItem]:\n", + " \"\"\"Return the untrimmed in-memory log (for debugging).\"\"\"\n", + " async with self._lock:\n", + " return list(self._items)\n" + ] + }, + { + "cell_type": "markdown", + "id": "21bd5b28", + "metadata": {}, + "source": [ + "Let's define the custom session object we implemented." + ] + }, + { + "cell_type": "code", + "execution_count": 95, + "id": "951ad6da", + "metadata": {}, + "outputs": [], + "source": [ + "# Keep only the last 8 turns (user + assistant/tool interactions)\n", + "session = MyCustomSession(\"my_session\", max_turns=8)" + ] + }, + { + "cell_type": "markdown", + "id": "07ab99cd", + "metadata": {}, + "source": [ + "**How to choose the right `max_turns`?**\n", + "\n", + "Determining this parameter usually requires experimentation with your conversation history. One approach is to extract the total number of turns across conversations and analyze their distribution. Another option is to use an LLM to evaluate conversations—identifying how many tasks or issues each one contains and calculating the average number of turns needed per issue.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 96, + "id": "c59d40b9", + "metadata": {}, + "outputs": [], + "source": [ + "message = \"There is a red light on the dashboard.\"" + ] + }, + { + "cell_type": "code", + "execution_count": 97, + "id": "03b15552", + "metadata": {}, + "outputs": [], + "source": [ + "result = await Runner.run(\n", + " support_agent,\n", + " message,\n", + " session=session\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 63, + "id": "c94beb6f", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'A red light usually indicates an issue with your internet connection. Is the red light on your modem or router?'" + ] + }, + "execution_count": 63, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "result.raw_responses[0].output[0].content[0].text" + ] + }, + { + "cell_type": "code", + "execution_count": 64, + "id": "50382117", + "metadata": {}, + "outputs": [], + "source": [ + "res = await session.raw_items()" + ] + }, + { + "cell_type": "code", + "execution_count": 65, + "id": "395c1bd1", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "16" + ] + }, + "execution_count": 65, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "len(res)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "51f60675", + "metadata": {}, + "outputs": [], + "source": [ + "# Example flow\n", + "await session.add_items([{\"role\": \"user\", \"content\": \"Hi, my router won't connect.\"}])\n", + "await session.add_items([{\"role\": \"assistant\", \"content\": \"Let's check your firmware version.\"}])\n", + "await session.add_items([{\"role\": \"user\", \"content\": \"Firmware v1.0.3; still failing.\"}])\n", + "await session.add_items([{\"role\": \"assistant\", \"content\": \"Try a factory reset.\"}])\n", + "await session.add_items([{\"role\": \"user\", \"content\": \"Reset done; error 42 now.\"}])\n", + "# At this point, with max_turns=3, everything *before* the earliest of the last 3 user\n", + "# messages is summarized into a synthetic pair, and the last 3 turns remain verbatim.\n", + "\n", + "history = await session.get_items()\n", + "# Pass `history` into your agent runner / responses call as the conversation context.\n" + ] + }, + { + "cell_type": "markdown", + "id": "ad8dd9c5", + "metadata": {}, + "source": [ + "**What counts as a “turn”**\n", + "\n", + "* A **turn** = one **user** message **plus everything that follows it** (assistant replies, tool calls, tool results) **until the next user message**.\n", + "\n", + "** When trimming happens\n", + "\n", + "* On **write**: `add_items(...)` appends the new items, then immediately trims the stored history.\n", + "* On **read**: `get_items(...)` returns a **trimmed** view (so even if you bypassed a write, reads won’t leak old turns).\n", + "\n", + "**How it decides what to keep**\n", + "\n", + "1. Treat any item with `role == \"user\"` as a **user message** (via `_is_user_msg`).\n", + "2. Scan the history **backwards** and collect the indices of the last **N** user messages (`max_turns`).\n", + "3. Find the **earliest** index among those N user messages.\n", + "4. **Keep everything from that index to the end**; drop everything before it.\n", + "\n", + "That preserves each complete turn boundary: if the earliest kept user message is at index `k`, you also keep all assistant/tool items that came after `k`.\n", + "\n", + "**Edge cases**\n", + "\n", + "* **Fewer than N user messages**: keep **everything** (no trimming yet).\n", + "* **No user messages**: keep **everything** (treat as a single in-progress turn).\n", + "* **`limit` in `get_items(limit=…)`**: applied **after** trimming; returns only the last `limit` items of the already-trimmed slice.\n", + "\n", + "**Tiny example**\n", + "\n", + "History (old → new):\n", + "\n", + "```\n", + "0: user(\"Hi\")\n", + "1: assistant(\"Hello!\")\n", + "2: tool_call(\"lookup\")\n", + "3: tool_result(\"…\")\n", + "4: user(\"It didn't work\")\n", + "5: assistant(\"Try rebooting\")\n", + "6: user(\"Rebooted, now error 42\")\n", + "7: assistant(\"On it\")\n", + "```\n", + "\n", + "With `max_turns = 2`, the last two user messages are at indices **4** and **6**.\n", + "Earliest of those is **4** → keep items **4..7**, drop **0..3**.\n", + "\n", + "**Why this works well**\n", + "\n", + "* You always keep **complete** turns, so the assistant retains the immediate context it needs (both the user’s last asks and the assistant/tool steps in between).\n", + "* It prevents context bloat by discarding older turns wholesale, not just messages.\n", + "\n", + "**Customization knobs**\n", + "\n", + "* Change `max_turns` at init or via `set_max_turns(...)`.\n", + "* Adjust `_is_user_msg(...)` if your item schema differs.\n", + "* If you’d rather cap by **message count** or **tokens**, replace `_trim_to_last_turns(...)` or add a second pass that measures tokens.\n", + "\n", + "**Complexity**\n", + "\n", + "* Each trim scans the list once ⇒ **O(n)** per trim (n = items in memory). In practice this is cheap because n is bounded by your recent turns.\n", + "\n", + "That’s the whole flow: **append → find last N user boundaries → keep from the earliest boundary onward**.\n" + ] + }, + { + "cell_type": "markdown", + "id": "d6fa349f", + "metadata": {}, + "source": [ + "## 2. Context Summarization " + ] + }, + { + "cell_type": "markdown", + "id": "5380492d", + "metadata": {}, + "source": [ + "A well-crafted summarization prompt is essential for preserving the context of a conversation, and it should always be tailored to the specific use case. Think of it like being a customer support agent handing off a case to the next agent—what concise yet critical details would they need to continue smoothly? The prompt should strike the right balance: not overloaded with unnecessary information, but not so sparse that key context is lost. Achieving this balance requires careful design and ongoing experimentation to fine-tune the level of detail." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7be8b2e5", + "metadata": {}, + "outputs": [], + "source": [ + "SUMMARY_PROMPT = \"\"\"\n", + "You are a senior customer-support assistant for tech devices, setup, and software issues.\n", + "Compress the earlier conversation into a precise, reusable snapshot for future turns.\n", + "\n", + "Before you write (do this silently):\n", + "- Contradiction check: compare user claims with system instructions and tool definitions/logs; note any conflicts or reversals.\n", + "- Temporal ordering: sort key events by time; the most recent update wins. If timestamps exist, keep them.\n", + "- Hallucination control: if any fact is uncertain/not stated, mark it as UNVERIFIED rather than guessing.\n", + "\n", + "Write a structured, factual summary ≤ 200 words using the sections below (use the exact headings):\n", + "\n", + "• Product & Environment:\n", + " - Device/model, OS/app versions, network/context if mentioned.\n", + "\n", + "• Reported Issue:\n", + " - Single-sentence problem statement (latest state).\n", + "\n", + "• Steps Tried & Results:\n", + " - Chronological bullets (include tool calls + outcomes, errors, codes).\n", + "\n", + "• Identifiers:\n", + " - Ticket #, device serial/model, account/email (only if provided).\n", + "\n", + "• Timeline Milestones:\n", + " - Key events with timestamps or relative order (e.g., 10:32 install → 10:41 error).\n", + "\n", + "• Tool Performance Insights:\n", + " - What tool calls worked/failed and why (if evident).\n", + "\n", + "• Current Status & Blockers:\n", + " - What’s resolved vs pending; explicit blockers preventing progress.\n", + "\n", + "• Next Recommended Step:\n", + " - One concrete action (or two alternatives) aligned with policies/tools.\n", + "\n", + "• Escalation:\n", + " - “Suggested” or “Not needed”; include reason and target team if applicable.\n", + "\n", + "Rules:\n", + "- Be concise, no fluff; use short bullets, verbs first.\n", + "- Do not invent new facts; quote error strings/codes exactly when available.\n", + "- If previous info was superseded, note “Superseded:” and omit details unless critical.\n", + "\"\"\"\n" + ] + }, + { + "cell_type": "markdown", + "id": "4a9f7b15", + "metadata": {}, + "source": [ + "**Key Principles for Designing Summarization Prompts**\n", + "\n", + "* **Milestones:** Highlight important events in the conversation—for example, when an issue is resolved, valuable information is uncovered, or all necessary details have been collected.\n", + "\n", + "* **Contradiction Check:** Ensure the summary does not conflict with system instructions or tool definitions. This is especially critical for reasoning models, which are more prone to conflicts in the context.\n", + "\n", + "* **Timestamps & Temporal Flow:** Incorporate timing of events in the summary. This helps the model reason about updates in sequence and reduces confusion when forgetting or remembering the latest memory over a timeline.\n", + "\n", + "* **Chunking:** Organize details into categories or sections rather than long paragraphs. Structured grouping improves an LLM’s ability to understand relationships between pieces of information.\n", + "\n", + "* **Tool Performance Insights:** Capture lessons learned from multi-turn, tool-enabled interactions—for example, noting which tools worked effectively for specific queries and why. These insights are valuable for guiding future steps.\n", + "\n", + "* **Guidance & Examples:** Steer the summary with clear guidance. Where possible, extract concrete examples from the conversation history to make future turns more grounded and context-rich.\n", + "\n", + "* **Hallucination Control:** Be precise in what you include. Even minor hallucinations in a summary can propagate forward, contaminating future context with inaccuracies.\n", + "\n", + "* **Use Case Specificity:** Tailor the compression prompt to the specific use case. Think about how a human would track and recall information in working memory while solving the same task.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 151, + "id": "8f3f811e", + "metadata": {}, + "outputs": [], + "source": [ + "class LLMSummarizer:\n", + " def __init__(self, client, model=\"gpt-4o\", max_tokens=400, tool_trim_limit=600):\n", + " self.client = client\n", + " self.model = model\n", + " self.max_tokens = max_tokens\n", + " self.tool_trim_limit = tool_trim_limit\n", + "\n", + " async def summarize(self, messages: List[Item]) -> Tuple[str, str]:\n", + " user_shadow = \"Summarize the conversation we had so far.\"\n", + " # Map history into a compact prompt\n", + " history_snippets = []\n", + " for m in messages:\n", + " role = m.get(\"role\", \"assistant\")\n", + " content = (m.get(\"content\") or \"\").strip()\n", + " if not content:\n", + " continue\n", + " # trim very long tool blobs\n", + " if role in (\"tool\", \"tool_result\") and len(content) > self.tool_trim_limit:\n", + " content = content[:self.tool_trim_limit] + \" …\"\n", + " history_snippets.append(f\"{role.upper()}: {content}\")\n", + " #print(history_snippets)\n", + " # Example using Responses; adapt if you use SDK Agents runs instead\n", + " prompt_messages = [\n", + " {\"role\": \"system\", \"content\": SUMMARY_PROMPT},\n", + " {\"role\": \"user\", \"content\": \"\\n\".join(history_snippets)}\n", + " ]\n", + " print(len(prompt_messages))\n", + " resp = await asyncio.to_thread(\n", + " self.client.responses.create,\n", + " model=self.model,\n", + " input=prompt_messages,\n", + " max_output_tokens=self.max_tokens\n", + " ) \n", + " \n", + " summary = resp.output_text\n", + "\n", + " await asyncio.sleep(0) # yield control\n", + " return user_shadow, summary" + ] + }, + { + "cell_type": "code", + "execution_count": 152, + "id": "a3e7cff8", + "metadata": {}, + "outputs": [], + "source": [ + "import asyncio\n", + "from collections import deque\n", + "from typing import Optional\n", + "import itertools\n", + "\n", + "class SummarizingSession:\n", + " \"\"\"\n", + " Keeps the last N user turns verbatim.\n", + " Summarizes everything before that into a synthetic user→assistant pair.\n", + " \"\"\"\n", + " def __init__(\n", + " self,\n", + " max_turns: int = 3,\n", + " summarizer: Optional[Summarizer] = None,\n", + " session_id: Optional[str] = None,\n", + " ):\n", + " assert max_turns >= 1\n", + " self.max_turns = max_turns\n", + " self._items: deque[Item] = deque()\n", + " self._lock = asyncio.Lock()\n", + " self.session_id = session_id or \"default\"\n", + " self.summarizer = summarizer or HeuristicSummarizer()\n", + "\n", + " # ----- public API that mirrors common Session interfaces -----\n", + "\n", + " async def get_items(self, limit: Optional[int] = None) -> list[Item]:\n", + " async with self._lock:\n", + " data = list(self._items)\n", + " return data[-limit:] if limit else data\n", + "\n", + " async def add_items(self, items: list[Item]) -> None:\n", + " # Append first\n", + " async with self._lock:\n", + " self._items.extend(items)\n", + " need_summary, boundary_idx = self._should_summarize_locked()\n", + "\n", + " # If we need a summary, **do it without the lock** to avoid blocking others\n", + " if need_summary:\n", + " # Take a snapshot of the prefix to summarize\n", + " async with self._lock:\n", + " prefix = list(itertools.islice(self._items, 0, boundary_idx))\n", + " # Produce the summary outside the lock\n", + " user_shadow, assistant_summary = await self.summarizer.summarize(prefix)\n", + "\n", + " # Re-acquire and re-check (in case of concurrent updates)\n", + " async with self._lock:\n", + " need_summary_now, boundary_idx_now = self._should_summarize_locked()\n", + " if need_summary_now:\n", + " suffix = list(itertools.islice(self._items, boundary_idx_now, None)) \n", + " self._items.clear()\n", + " self._items.extend([\n", + " {\n", + " \"role\": \"user\",\n", + " \"content\": user_shadow,\n", + " \"metadata\": {\n", + " \"synthetic\": True,\n", + " \"kind\": \"history_summary_prompt\",\n", + " \"summary_for_turns\": f\"< all before idx {boundary_idx_now} >\",\n", + " },\n", + " },\n", + " {\n", + " \"role\": \"assistant\",\n", + " \"content\": assistant_summary,\n", + " \"metadata\": {\n", + " \"synthetic\": True,\n", + " \"kind\": \"history_summary\",\n", + " \"summary_for_turns\": f\"< all before idx {boundary_idx_now} >\",\n", + " },\n", + " },\n", + " ])\n", + " self._items.extend(suffix)\n", + " # else: another concurrent writer already summarized; do nothing.\n", + "\n", + " async def pop_item(self) -> Optional[Item]:\n", + " async with self._lock:\n", + " return self._items.pop() if self._items else None\n", + "\n", + " async def clear_session(self) -> None:\n", + " async with self._lock:\n", + " self._items.clear()\n", + "\n", + " def set_max_turns(self, n: int) -> None:\n", + " assert n >= 1\n", + " self.max_turns = n\n", + "\n", + " # ----- helpers -----\n", + "\n", + " def _is_user(self, it: Item) -> bool:\n", + " return it.get(\"role\") == \"user\"\n", + "\n", + " def _should_summarize_locked(self) -> tuple[bool, int]:\n", + " \"\"\"\n", + " Returns (need_summary, boundary_idx).\n", + " boundary_idx = earliest index to keep (start of last N user turns).\n", + " If False, boundary_idx is undefined.\n", + " \"\"\"\n", + " idxs = []\n", + " for i in range(len(self._items) - 1, -1, -1):\n", + " if self._is_user(self._items[i]):\n", + " idxs.append(i)\n", + " if len(idxs) == self.max_turns:\n", + " break\n", + " if len(idxs) < self.max_turns:\n", + " return False, -1 # not enough user turns yet\n", + "\n", + " boundary = min(idxs) # earliest of the last N user turns\n", + " if boundary <= 0:\n", + " return False, -1 # nothing to summarize before boundary\n", + " return True, boundary\n" + ] + }, + { + "cell_type": "code", + "execution_count": 165, + "id": "a8d22531", + "metadata": {}, + "outputs": [], + "source": [ + "session = SummarizingSession(\n", + " max_turns=4,\n", + " summarizer=LLMSummarizer(client), # or LLMSummarizer(client)\n", + ")\n", + "\n", + "# Example flow\n", + "await session.add_items([{\"role\": \"user\", \"content\": \"Hi, my router won't connect. by the way, I am using Windows 10. I tried troubleshooting via your FAQs but I didn't get anywhere. This is my third tiem calling you.\"}])\n", + "await session.add_items([{\"role\": \"assistant\", \"content\": \"Let's check your firmware version.\"}])\n", + "await session.add_items([{\"role\": \"user\", \"content\": \"Firmware v1.0.3; still failing.\"}])\n", + "await session.add_items([{\"role\": \"assistant\", \"content\": \"Try a factory reset.\"}])\n", + "await session.add_items([{\"role\": \"user\", \"content\": \"Reset done; error 42 now.\"}])\n", + "await session.add_items([{\"role\": \"assistant\", \"content\": \"Try to install a new firmware.\"}])\n", + "await session.add_items([{\"role\": \"user\", \"content\": \"I tried but I got another error now.\"}])\n", + "# At this point, with max_turns=3, everything *before* the earliest of the last 3 user\n", + "# messages is summarized into a synthetic pair, and the last 3 turns remain verbatim.\n", + "\n", + "history = await session.get_items()\n", + "# Pass `history` into your agent runner / responses call as the conversation context.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 166, + "id": "9f229de7", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[{'role': 'user',\n", + " 'content': \"Hi, my router won't connect. by the way, I am using Windows 10. I tried troubleshooting via your FAQs but I didn't get anywhere. This is my third tiem calling you.\"},\n", + " {'role': 'assistant', 'content': \"Let's check your firmware version.\"},\n", + " {'role': 'user', 'content': 'Firmware v1.0.3; still failing.'},\n", + " {'role': 'assistant', 'content': 'Try a factory reset.'},\n", + " {'role': 'user', 'content': 'Reset done; error 42 now.'},\n", + " {'role': 'assistant', 'content': 'Try to install a new firmware.'},\n", + " {'role': 'user', 'content': 'I tried but I got another error now.'}]" + ] + }, + "execution_count": 166, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "history" + ] + }, + { + "cell_type": "code", + "execution_count": 167, + "id": "1e26d8af", + "metadata": {}, + "outputs": [], + "source": [ + "message = \"I still have a problem with my router.\"" + ] + }, + { + "cell_type": "code", + "execution_count": 168, + "id": "950d663b", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2\n" + ] + } + ], + "source": [ + "result = await Runner.run(\n", + " support_agent,\n", + " message,\n", + " session=session\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 169, + "id": "6af52337", + "metadata": {}, + "outputs": [], + "source": [ + "history = await session.get_items()" + ] + }, + { + "cell_type": "code", + "execution_count": 170, + "id": "5c071451", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'synthetic': True,\n", + " 'kind': 'history_summary',\n", + " 'summary_for_turns': '< all before idx 2 >'}" + ] + }, + "execution_count": 170, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "history[1]['metadata']" + ] + }, + { + "cell_type": "code", + "execution_count": 171, + "id": "64d555ee", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "- **User Goal:** Connect router to Windows 10.\n", + "- **Constraints:** Previous attempts using FAQs were unsuccessful. Third contact for support.\n", + "- **Current Environment:** Windows 10.\n", + "- **Tried:** FAQ troubleshooting.\n", + "- **Current Status:** Unable to connect router.\n", + "- **Next Step:** Check router firmware version.\n" + ] + } + ], + "source": [ + "print(history[1]['content'])" + ] + }, + { + "cell_type": "markdown", + "id": "1a935659", + "metadata": {}, + "source": [ + "Once the history exceeds `max_turns`. It keeps the most recent N user turns intact, **summarizes everything older into two synthetic messages**:\n", + "\n", + "* `user`: *\"Summarize the conversation we had so far.\"*\n", + "* `assistant`: *generated summary*\n" + ] + }, + { + "cell_type": "markdown", + "id": "9744c6d8", + "metadata": {}, + "source": [ + "### Notes & design choices\n", + "\n", + "* **Turn boundary preserved at the “fresh” side**: the **last `max_turns` user turns** remain verbatim; everything older is compressed.\n", + "* **Two-message summary block**: easy for downstream tooling to detect or display (`metadata.synthetic == True`).\n", + "* **Async + lock discipline**: we **release the lock** while the (potentially slow) summarization runs; then re-check the condition before applying the summary to avoid racey merges.\n", + "* **Idempotent behavior**: if more messages arrive during summarization, the post-await recheck prevents stale rewrites.\n" + ] + }, + { + "cell_type": "markdown", + "id": "730afac9", + "metadata": {}, + "source": [ + "## Evals\n", + "\n", + "At the end of the day, **evals is all you need**—even for context engineering. The key question to ask is: *how do we know the model isn’t “losing context” or \"confusing context\"?*\n", + "\n", + "While a full cookbook aorund memory could stand on its own in the future, here are some lightweight evaluation harness ideas to start with:\n", + "\n", + "* **Baseline & Deltas:** Continue running your core eval sets and compare before/after experiments to measure memory improvements.\n", + "* **LLM-as-Judge:** Use a model with a carefully designed grader prompt to evaluate summarization quality. Focus on whether it captures the most important details in the correct format.\n", + "* **Transcript Replay:** Re-run long conversations and measure next-turn accuracy with and without context trimming. Metrics could include exact match on entities/IDs and rubric-based scoring on reasoning quality.\n", + "* **Error Regression Tracking:** Watch for common failure modes—unanswered questions, dropped constraints, or unnecessary/repeated tool calls.\n", + "* **Token Pressure Checks:** Flag cases where token limits force dropping protected context. Log before/after token counts to detect when critical details are being pruned." + ] + }, + { + "cell_type": "markdown", + "id": "e2843701", + "metadata": {}, + "source": [ + "---" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "openai", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.9" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} From a74768cdfbbb2c3e87bc1c8ca2f3b3e9ef940c2b Mon Sep 17 00:00:00 2001 From: emre-openai Date: Thu, 4 Sep 2025 12:56:23 -0700 Subject: [PATCH 02/11] tweak --- examples/agents_sdk/session_memory.ipynb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/agents_sdk/session_memory.ipynb b/examples/agents_sdk/session_memory.ipynb index d0a926f6fc..d60869ff5f 100644 --- a/examples/agents_sdk/session_memory.ipynb +++ b/examples/agents_sdk/session_memory.ipynb @@ -21,7 +21,7 @@ "source": [ "In this cookbook, we’ll explore how to **manage context effectively using the `Session` object from the [OpenAI Agents SDK](https://github.com/openai/openai-agents-python)**.\n", "\n", - "AI agents often operate in **long-running, multi-turn interactions**, where keeping the right balance of context is critical. If too much is carried forward, the model risks distraction, inefficiency, or outright failure. If too little is preserved, the agent loses coherence. This guide focuses on two proven context management techniques—**trimming** and **compression**—used by production teams to keep agents fast, reliable, and cost-efficient.\n", + "AI agents often operate in **long-running, multi-turn interactions**, where keeping the right balance of context is critical. If too much is carried forward, the model risks distraction, inefficiency, or outright failure. If too little is preserved, the agent loses coherence. This guide focuses on two proven context management techniques—**trimming** and **compression**—used by engineering teams to keep agents fast, reliable, and cost-efficient.\n", "\n", "#### Why Context Management Matters\n", "\n", @@ -34,10 +34,10 @@ "\n", "#### Real-World Scenario\n", "\n", - "We’ll ground the techniques in practical examples, such as:\n", + "We’ll ground the techniques in a practical example for one of the common long-running tasks, such as:\n", "\n", "* **Multi-turn Customer Service Conversations**\n", - " When a user raises multiple issues over a long chat history, the agent must stay consistent without carrying every detail forward.\n", + "In extended conversations about tech products—spanning both hardware and software—customers often surface multiple issues over time. The agent must stay consistent and goal-focused while retaining only the essentials rather than hauling along every past detail.\n", "\n", "#### Techniques Covered\n", "\n", From 5fef6f415c97c65ef8e883c72b35731014b905f2 Mon Sep 17 00:00:00 2001 From: emre-openai Date: Thu, 4 Sep 2025 14:35:04 -0700 Subject: [PATCH 03/11] tweak --- examples/agents_sdk/session_memory.ipynb | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/examples/agents_sdk/session_memory.ipynb b/examples/agents_sdk/session_memory.ipynb index d60869ff5f..d17fba1fe6 100644 --- a/examples/agents_sdk/session_memory.ipynb +++ b/examples/agents_sdk/session_memory.ipynb @@ -8,12 +8,6 @@ "# Context Engineering: Short-Term Memory Management with Sessions from OpenAI Agents SDK " ] }, - { - "cell_type": "markdown", - "id": "ae724bd5", - "metadata": {}, - "source": [] - }, { "cell_type": "markdown", "id": "eeab798a", @@ -43,7 +37,7 @@ "\n", "To address these challenges, we introduce two concrete approaches using OpenAI Agents SDK:\n", "\n", - "1. **Trimming Messages** – selectively dropping older or less relevant turns.\n", + "1. **Trimming Messages** – dropping older turns while keeping the last N turns.\n", "2. **Summarizing Messages** – compressing prior exchanges into structured, shorter representations.\n", "\n", "\n", From 67f98f817cd46d2f112cd929a6c3fdbfb86af51b Mon Sep 17 00:00:00 2001 From: emre-openai Date: Fri, 5 Sep 2025 12:08:47 -0700 Subject: [PATCH 04/11] tweak --- examples/agents_sdk/session_memory.ipynb | 637 ++++++++++++++++++++--- 1 file changed, 551 insertions(+), 86 deletions(-) diff --git a/examples/agents_sdk/session_memory.ipynb b/examples/agents_sdk/session_memory.ipynb index d17fba1fe6..20deef608e 100644 --- a/examples/agents_sdk/session_memory.ipynb +++ b/examples/agents_sdk/session_memory.ipynb @@ -107,15 +107,15 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "d89e8e8d", + "execution_count": 3, + "id": "fe54469a", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "Evaluating AI agents is crucial to ensure accuracy, fairness, safety, security, and alignment with human values and objectives.\n" + "Evaluating AI agents ensures reliability, safety, ethical alignment, performance accuracy, and helps avoid biases, improving overall trust and effectiveness.\n" ] } ], @@ -123,17 +123,14 @@ "import asyncio\n", "from agents import Agent, Runner\n", "\n", - "async def main():\n", - " agent = Agent(\n", - " name=\"Assistant\",\n", - " instructions=\"Reply very concisely.\",\n", - " )\n", "\n", - " result = await Runner.run(agent, \"Tell me why it is important to evaluate AI agents.\")\n", - " print(result.final_output)\n", + "agent = Agent(\n", + " name=\"Assistant\",\n", + " instructions=\"Reply very concisely.\",\n", + ")\n", "\n", - "loop = asyncio.get_running_loop()\n", - "await loop.create_task(main())\n" + "result = await Runner.run(agent, \"Tell me why it is important to evaluate AI agents.\")\n", + "print(result.final_output)\n" ] }, { @@ -156,7 +153,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 214, "id": "4451ca22", "metadata": {}, "outputs": [], @@ -172,8 +169,6 @@ " \"- Ask only one focused, clarifying question at a time before suggesting next actions.\\n\"\n", " \"- Track and remember multiple issues across the conversation; update your understanding as new problems emerge.\\n\"\n", " \"- When a problem is resolved, briefly confirm closure before moving to the next.\\n\"\n", - " \"- If the session grows long, proactively summarize solved and open issues to maintain clarity.\\n\"\n", - " \"- Prefer verified knowledge base information; avoid speculation.\\n\"\n", " )\n", ")\n" ] @@ -204,7 +199,7 @@ }, { "cell_type": "code", - "execution_count": 94, + "execution_count": 6, "id": "1b468c78", "metadata": {}, "outputs": [], @@ -213,7 +208,7 @@ "\n", "import asyncio\n", "from collections import deque\n", - "from typing import Any, Deque, Dict, Iterable, List, Optional, Tuple, Union, cast\n", + "from typing import Any, Deque, Dict, List, Tuple, cast\n", "\n", "from agents.memory.session import SessionABC\n", "from agents.items import TResponseInputItem # typically a dict-like item\n", @@ -237,7 +232,7 @@ " return role_attr == \"user\"\n", "\n", "\n", - "class MyCustomSession(SessionABC):\n", + "class TrimmingSession(SessionABC):\n", " \"\"\"\n", " Custom session that keeps only the last N user-turns.\n", " A 'turn' is defined as a user message and all subsequent items\n", @@ -355,13 +350,13 @@ }, { "cell_type": "code", - "execution_count": 95, + "execution_count": 26, "id": "951ad6da", "metadata": {}, "outputs": [], "source": [ "# Keep only the last 8 turns (user + assistant/tool interactions)\n", - "session = MyCustomSession(\"my_session\", max_turns=8)" + "session = TrimmingSession(\"my_session\", max_turns=2)" ] }, { @@ -376,7 +371,7 @@ }, { "cell_type": "code", - "execution_count": 96, + "execution_count": 27, "id": "c59d40b9", "metadata": {}, "outputs": [], @@ -386,7 +381,7 @@ }, { "cell_type": "code", - "execution_count": 97, + "execution_count": 28, "id": "03b15552", "metadata": {}, "outputs": [], @@ -400,59 +395,92 @@ }, { "cell_type": "code", - "execution_count": 63, + "execution_count": 29, "id": "c94beb6f", "metadata": {}, + "outputs": [], + "source": [ + "conversation = await session.get_items()\n" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "id": "626a6e57", + "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "'A red light usually indicates an issue with your internet connection. Is the red light on your modem or router?'" + "[{'content': 'There is a red light on the dashboard.', 'role': 'user'},\n", + " {'id': 'rs_68ba0a2abf3c8196adc68a947b457ae0001b849827e34d0e',\n", + " 'summary': [],\n", + " 'type': 'reasoning',\n", + " 'content': []},\n", + " {'id': 'msg_68ba0a3480748196b7059d0e23d87350001b849827e34d0e',\n", + " 'content': [{'annotations': [],\n", + " 'text': 'Which device or system is the dashboard on (e.g., car, printer, router, software)?',\n", + " 'type': 'output_text',\n", + " 'logprobs': []}],\n", + " 'role': 'assistant',\n", + " 'status': 'completed',\n", + " 'type': 'message'}]" ] }, - "execution_count": 63, + "execution_count": 30, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "result.raw_responses[0].output[0].content[0].text" + "conversation" ] }, { "cell_type": "code", - "execution_count": 64, - "id": "50382117", + "execution_count": 32, + "id": "395c1bd1", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "3" + ] + }, + "execution_count": 32, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ - "res = await session.raw_items()" + "len(conversation)" ] }, { "cell_type": "code", - "execution_count": 65, - "id": "395c1bd1", + "execution_count": 31, + "id": "7ca19df0", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "16" + "('user', 'There is a red light on the dashboard.')" ] }, - "execution_count": 65, + "execution_count": 31, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "len(res)" + "conversation[0]['role'], conversation[0]['content']" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 50, "id": "51f60675", "metadata": {}, "outputs": [], @@ -463,6 +491,7 @@ "await session.add_items([{\"role\": \"user\", \"content\": \"Firmware v1.0.3; still failing.\"}])\n", "await session.add_items([{\"role\": \"assistant\", \"content\": \"Try a factory reset.\"}])\n", "await session.add_items([{\"role\": \"user\", \"content\": \"Reset done; error 42 now.\"}])\n", + "await session.add_items([{\"role\": \"assistant\", \"content\": \"test1\"}])\n", "# At this point, with max_turns=3, everything *before* the earliest of the last 3 user\n", "# messages is summarized into a synthetic pair, and the last 3 turns remain verbatim.\n", "\n", @@ -470,6 +499,51 @@ "# Pass `history` into your agent runner / responses call as the conversation context.\n" ] }, + { + "cell_type": "code", + "execution_count": 51, + "id": "6dc29a28", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "4" + ] + }, + "execution_count": 51, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "len(history)" + ] + }, + { + "cell_type": "code", + "execution_count": 52, + "id": "07430b49", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[{'role': 'user', 'content': 'Firmware v1.0.3; still failing.'},\n", + " {'role': 'assistant', 'content': 'Try a factory reset.'},\n", + " {'role': 'user', 'content': 'Reset done; error 42 now.'},\n", + " {'role': 'assistant', 'content': 'test1'}]" + ] + }, + "execution_count": 52, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "history" + ] + }, { "cell_type": "markdown", "id": "ad8dd9c5", @@ -477,9 +551,9 @@ "source": [ "**What counts as a “turn”**\n", "\n", - "* A **turn** = one **user** message **plus everything that follows it** (assistant replies, tool calls, tool results) **until the next user message**.\n", + "* A **turn** = one **user** message **plus everything that follows it** (assistant replies, reasoning, tool calls, tool results) **until the next user message**.\n", "\n", - "** When trimming happens\n", + "**When trimming happens**\n", "\n", "* On **write**: `add_items(...)` appends the new items, then immediately trims the stored history.\n", "* On **read**: `get_items(...)` returns a **trimmed** view (so even if you bypassed a write, reads won’t leak old turns).\n", @@ -543,17 +617,39 @@ "## 2. Context Summarization " ] }, + { + "cell_type": "markdown", + "id": "8a6c453e", + "metadata": {}, + "source": [ + "Once the history exceeds `max_turns`. It keeps the most recent N user turns intact, **summarizes everything older into two synthetic messages**:\n", + "\n", + "* `user`: *\"Summarize the conversation we had so far.\"*\n", + "* `assistant`: *{generated summary}*\n", + "\n", + "The shadow prompt from the user to requst the summarization added to keep natural flow of the conversation without confusing the chat flow between user and assistant. Final version of the generated summary injected to assistant message." + ] + }, + { + "cell_type": "markdown", + "id": "cf4b8d8c", + "metadata": {}, + "source": [ + "**Summarization Prompt**\n", + "\n" + ] + }, { "cell_type": "markdown", "id": "5380492d", "metadata": {}, "source": [ - "A well-crafted summarization prompt is essential for preserving the context of a conversation, and it should always be tailored to the specific use case. Think of it like being a customer support agent handing off a case to the next agent—what concise yet critical details would they need to continue smoothly? The prompt should strike the right balance: not overloaded with unnecessary information, but not so sparse that key context is lost. Achieving this balance requires careful design and ongoing experimentation to fine-tune the level of detail." + "A well-crafted summarization prompt is essential for preserving the context of a conversation, and it should always be tailored to the specific use case. Think of it like **being a customer support agent handing off a case to the next agent**. What concise yet critical details would they need to continue smoothly? The prompt should strike the right balance: not overloaded with unnecessary information, but not so sparse that key context is lost. Achieving this balance requires careful design and ongoing experimentation to fine-tune the level of detail." ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 66, "id": "7be8b2e5", "metadata": {}, "outputs": [], @@ -593,9 +689,6 @@ "• Next Recommended Step:\n", " - One concrete action (or two alternatives) aligned with policies/tools.\n", "\n", - "• Escalation:\n", - " - “Suggested” or “Not needed”; include reason and target team if applicable.\n", - "\n", "Rules:\n", "- Be concise, no fluff; use short bullets, verbs first.\n", "- Do not invent new facts; quote error strings/codes exactly when available.\n", @@ -624,12 +717,14 @@ "\n", "* **Hallucination Control:** Be precise in what you include. Even minor hallucinations in a summary can propagate forward, contaminating future context with inaccuracies.\n", "\n", - "* **Use Case Specificity:** Tailor the compression prompt to the specific use case. Think about how a human would track and recall information in working memory while solving the same task.\n" + "* **Use Case Specificity:** Tailor the compression prompt to the specific use case. Think about how a human would track and recall information in working memory while solving the same task.\n", + "\n", + "* **Model Choice:** Select a summarizer model based on use case requirements, summary length, and tradeoffs between latency and cost. In some cases, using the same model as the agent itself can be advantageous.\n" ] }, { "cell_type": "code", - "execution_count": 151, + "execution_count": 134, "id": "8f3f811e", "metadata": {}, "outputs": [], @@ -676,7 +771,206 @@ }, { "cell_type": "code", - "execution_count": 152, + "execution_count": 195, + "id": "4bb5c4e9", + "metadata": {}, + "outputs": [], + "source": [ + "import asyncio\n", + "import itertools\n", + "from collections import deque\n", + "from typing import Optional, List, Tuple, Dict, Any\n", + "\n", + "class SummarizingSession:\n", + " \"\"\"\n", + " Keeps the last N *user* turns verbatim.\n", + " Summarizes everything before that into a synthetic user→assistant pair.\n", + " Internally stores (message, metadata) records. Exposes:\n", + " - get_items(): model-safe messages only (no metadata)\n", + " - get_full_history(): [{ \"message\": msg, \"metadata\": meta }, ...]\n", + " \"\"\"\n", + "\n", + " # Only these keys are sent to the model. Everything else goes to metadata.\n", + " _ALLOWED_MSG_KEYS = {\"role\", \"content\", \"name\"}\n", + "\n", + " def __init__(\n", + " self,\n", + " max_turns: int = 3,\n", + " summarizer: Optional[\"Summarizer\"] = None,\n", + " session_id: Optional[str] = None,\n", + " ):\n", + " assert max_turns >= 1\n", + " self.max_turns = max_turns\n", + " # Each record: {\"msg\": {...}, \"meta\": {...}}\n", + " self._records: deque[Dict[str, Dict[str, Any]]] = deque()\n", + " self._lock = asyncio.Lock()\n", + " self.session_id = session_id or \"default\"\n", + " self.summarizer = summarizer\n", + "\n", + " # --------- public API used by your runner ---------\n", + "\n", + " async def get_items(self, limit: Optional[int] = None) -> List[Dict[str, Any]]:\n", + " \"\"\"\n", + " Returns messages in a model-safe shape (no metadata).\n", + " Runner.run(..., session=self) should call this.\n", + " \"\"\"\n", + " async with self._lock:\n", + " data = list(self._records)\n", + " msgs = [self._sanitize_for_model(rec[\"msg\"]) for rec in data]\n", + " return msgs[-limit:] if limit else msgs\n", + "\n", + " async def add_items(self, items: List[Dict[str, Any]]) -> None:\n", + " async with self._lock:\n", + " for it in items:\n", + " msg, meta = self._split_msg_and_meta(it)\n", + " self._records.append({\"msg\": msg, \"meta\": meta})\n", + " need_summary, boundary_idx = self._should_summarize_locked()\n", + "\n", + " if need_summary:\n", + " async with self._lock:\n", + " prefix_records = list(itertools.islice(self._records, 0, boundary_idx))\n", + " prefix_msgs = [r[\"msg\"] for r in prefix_records]\n", + "\n", + " user_shadow, assistant_summary = await self._summarize(prefix_msgs)\n", + "\n", + " async with self._lock:\n", + " need_summary_now, boundary_idx_now = self._should_summarize_locked()\n", + " if not need_summary_now:\n", + " # normalize anyway if summarization got skipped\n", + " self._normalize_synthetic_flags_locked()\n", + " return\n", + "\n", + " suffix_records = list(itertools.islice(self._records, boundary_idx_now, None))\n", + " self._records.clear()\n", + "\n", + " # Synthetic summary pair keeps synthetic=True\n", + " self._records.extend([\n", + " {\n", + " \"msg\": {\"role\": \"user\", \"content\": user_shadow},\n", + " \"meta\": {\n", + " \"synthetic\": True,\n", + " \"kind\": \"history_summary_prompt\",\n", + " \"summary_for_turns\": f\"< all before idx {boundary_idx_now} >\",\n", + " },\n", + " },\n", + " {\n", + " \"msg\": {\"role\": \"assistant\", \"content\": assistant_summary},\n", + " \"meta\": {\n", + " \"synthetic\": True,\n", + " \"kind\": \"history_summary\",\n", + " \"summary_for_turns\": f\"< all before idx {boundary_idx_now} >\",\n", + " },\n", + " },\n", + " ])\n", + " self._records.extend(suffix_records)\n", + "\n", + " # ✅ Ensure all real messages explicitly have synthetic=False\n", + " self._normalize_synthetic_flags_locked()\n", + " else:\n", + " # ✅ Even when we don't summarize, enforce the invariant\n", + " async with self._lock:\n", + " self._normalize_synthetic_flags_locked()\n", + "\n", + " async def pop_item(self) -> Optional[Dict[str, Any]]:\n", + " async with self._lock:\n", + " if not self._records:\n", + " return None\n", + " rec = self._records.pop()\n", + " return dict(rec[\"msg\"]) # model-safe\n", + "\n", + " async def clear_session(self) -> None:\n", + " async with self._lock:\n", + " self._records.clear()\n", + "\n", + " def set_max_turns(self, n: int) -> None:\n", + " assert n >= 1\n", + " self.max_turns = n\n", + "\n", + " # --------- full-history (for debugging/analytics/observability) ---------\n", + "\n", + " # ✅ Backfill safeguard for older records that might lack the flag\n", + " def _normalize_synthetic_flags_locked(self) -> None:\n", + " for rec in self._records:\n", + " role = rec[\"msg\"].get(\"role\")\n", + " if role in (\"user\", \"assistant\") and \"synthetic\" not in rec[\"meta\"]:\n", + " rec[\"meta\"][\"synthetic\"] = False\n", + "\n", + " \n", + " async def get_full_history(self, limit: Optional[int] = None) -> List[Dict[str, Any]]:\n", + " \"\"\"\n", + " Returns combined history where each entry is:\n", + " { \"message\": {role, content[, name]}, \"metadata\": {...} }\n", + " This is NOT sent to the model; it's for your logs/UI/debugging.\n", + " \"\"\"\n", + " async with self._lock:\n", + " data = list(self._records)\n", + " out = [{\"message\": dict(rec[\"msg\"]), \"metadata\": dict(rec[\"meta\"])} for rec in data]\n", + " return out[-limit:] if limit else out\n", + "\n", + " # Backwards-compatible alias if you were using this name before\n", + " async def get_items_with_metadata(self, limit: Optional[int] = None) -> List[Dict[str, Any]]:\n", + " return await self.get_full_history(limit)\n", + "\n", + " # --------- helpers ---------\n", + "\n", + " def _split_msg_and_meta(self, it: Dict[str, Any]) -> Tuple[Dict[str, Any], Dict[str, Any]]:\n", + " msg = {k: v for k, v in it.items() if k in self._ALLOWED_MSG_KEYS}\n", + " extra = {k: v for k, v in it.items() if k not in self._ALLOWED_MSG_KEYS}\n", + " meta = dict(extra.pop(\"metadata\", {}))\n", + " meta.update(extra)\n", + "\n", + " if \"role\" not in msg or \"content\" not in msg:\n", + " msg.setdefault(\"role\", \"user\")\n", + " msg.setdefault(\"content\", str(it))\n", + "\n", + " # ✅ Default synthetic flag for real (non-summarized) messages\n", + " role = msg.get(\"role\")\n", + " if role in (\"user\", \"assistant\") and \"synthetic\" not in meta:\n", + " meta[\"synthetic\"] = False\n", + " return msg, meta\n", + "\n", + " def _sanitize_for_model(self, msg: Dict[str, Any]) -> Dict[str, Any]:\n", + " \"\"\"\n", + " Strictly keep only allowed keys for model input.\n", + " \"\"\"\n", + " return {k: v for k, v in msg.items() if k in self._ALLOWED_MSG_KEYS}\n", + "\n", + " def _is_user(self, rec: Dict[str, Dict[str, Any]]) -> bool:\n", + " return rec[\"msg\"].get(\"role\") == \"user\"\n", + "\n", + " def _should_summarize_locked(self) -> Tuple[bool, int]:\n", + " \"\"\"\n", + " Find the earliest index among the last `max_turns` user messages.\n", + " Everything before that index becomes the summarization prefix.\n", + " \"\"\"\n", + " idxs = []\n", + " for i in range(len(self._records) - 1, -1, -1):\n", + " if self._is_user(self._records[i]):\n", + " idxs.append(i)\n", + " if len(idxs) == self.max_turns:\n", + " break\n", + " if len(idxs) < self.max_turns:\n", + " return False, -1\n", + " boundary = min(idxs)\n", + " if boundary <= 0:\n", + " return False, -1\n", + " return True, boundary\n", + "\n", + " async def _summarize(self, prefix_msgs: List[Dict[str, Any]]) -> Tuple[str, str]:\n", + " \"\"\"\n", + " Adapter to your summarizer. Provide *model-safe* messages only.\n", + " \"\"\"\n", + " if not self.summarizer:\n", + " # Fallback summary if no summarizer is configured\n", + " return (\"Summarize the conversation we had so far.\", \"Summary unavailable.\")\n", + " # Only send role/content/name to the summarizer as well\n", + " clean_prefix = [self._sanitize_for_model(m) for m in prefix_msgs]\n", + " return await self.summarizer.summarize(clean_prefix)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 177, "id": "a3e7cff8", "metadata": {}, "outputs": [], @@ -702,7 +996,7 @@ " self._items: deque[Item] = deque()\n", " self._lock = asyncio.Lock()\n", " self.session_id = session_id or \"default\"\n", - " self.summarizer = summarizer or HeuristicSummarizer()\n", + " self.summarizer = summarizer\n", "\n", " # ----- public API that mirrors common Session interfaces -----\n", "\n", @@ -794,18 +1088,26 @@ }, { "cell_type": "code", - "execution_count": 165, + "execution_count": 211, "id": "a8d22531", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2\n" + ] + } + ], "source": [ "session = SummarizingSession(\n", - " max_turns=4,\n", - " summarizer=LLMSummarizer(client), # or LLMSummarizer(client)\n", + " max_turns=3,\n", + " summarizer=LLMSummarizer(client)\n", ")\n", "\n", "# Example flow\n", - "await session.add_items([{\"role\": \"user\", \"content\": \"Hi, my router won't connect. by the way, I am using Windows 10. I tried troubleshooting via your FAQs but I didn't get anywhere. This is my third tiem calling you.\"}])\n", + "await session.add_items([{\"role\": \"user\", \"content\": \"Hi, my router won't connect. by the way, I am using Windows 10. I tried troubleshooting via your FAQs but I didn't get anywhere. This is my third tiem calling you. I am based in the US and one of Premium customers.\"}])\n", "await session.add_items([{\"role\": \"assistant\", \"content\": \"Let's check your firmware version.\"}])\n", "await session.add_items([{\"role\": \"user\", \"content\": \"Firmware v1.0.3; still failing.\"}])\n", "await session.add_items([{\"role\": \"assistant\", \"content\": \"Try a factory reset.\"}])\n", @@ -821,16 +1123,16 @@ }, { "cell_type": "code", - "execution_count": 166, + "execution_count": 212, "id": "9f229de7", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "[{'role': 'user',\n", - " 'content': \"Hi, my router won't connect. by the way, I am using Windows 10. I tried troubleshooting via your FAQs but I didn't get anywhere. This is my third tiem calling you.\"},\n", - " {'role': 'assistant', 'content': \"Let's check your firmware version.\"},\n", + "[{'role': 'user', 'content': 'Summarize the conversation we had so far.'},\n", + " {'role': 'assistant',\n", + " 'content': \"• Product & Environment:\\n - Windows 10, router (specific model UNVERIFIED).\\n\\n• Reported Issue:\\n - Router won't connect.\\n\\n• Steps Tried & Results:\\n - Used FAQs for troubleshooting; no resolution.\\n\\n• Identifiers:\\n - Premium customer; based in the US (no specific identifiers provided).\\n\\n• Timeline Milestones:\\n - Third interaction reported by user.\\n\\n• Tool Performance Insights:\\n - User FAQs insufficient for resolution.\\n\\n• Current Status & Blockers:\\n - Connection issue unresolved; firmware version not yet checked.\\n\\n• Next Recommended Step:\\n - Verify and update the router firmware version.\"},\n", " {'role': 'user', 'content': 'Firmware v1.0.3; still failing.'},\n", " {'role': 'assistant', 'content': 'Try a factory reset.'},\n", " {'role': 'user', 'content': 'Reset done; error 42 now.'},\n", @@ -838,7 +1140,7 @@ " {'role': 'user', 'content': 'I tried but I got another error now.'}]" ] }, - "execution_count": 166, + "execution_count": 212, "metadata": {}, "output_type": "execute_result" } @@ -849,7 +1151,47 @@ }, { "cell_type": "code", - "execution_count": 167, + "execution_count": 213, + "id": "afc57803", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "• Product & Environment:\n", + " - Windows 10, router (specific model UNVERIFIED).\n", + "\n", + "• Reported Issue:\n", + " - Router won't connect.\n", + "\n", + "• Steps Tried & Results:\n", + " - Used FAQs for troubleshooting; no resolution.\n", + "\n", + "• Identifiers:\n", + " - Premium customer; based in the US (no specific identifiers provided).\n", + "\n", + "• Timeline Milestones:\n", + " - Third interaction reported by user.\n", + "\n", + "• Tool Performance Insights:\n", + " - User FAQs insufficient for resolution.\n", + "\n", + "• Current Status & Blockers:\n", + " - Connection issue unresolved; firmware version not yet checked.\n", + "\n", + "• Next Recommended Step:\n", + " - Verify and update the router firmware version.\n" + ] + } + ], + "source": [ + "print(history[1]['content'])" + ] + }, + { + "cell_type": "code", + "execution_count": 201, "id": "1e26d8af", "metadata": {}, "outputs": [], @@ -859,7 +1201,7 @@ }, { "cell_type": "code", - "execution_count": 168, + "execution_count": 202, "id": "950d663b", "metadata": {}, "outputs": [ @@ -881,7 +1223,7 @@ }, { "cell_type": "code", - "execution_count": 169, + "execution_count": 203, "id": "6af52337", "metadata": {}, "outputs": [], @@ -891,30 +1233,141 @@ }, { "cell_type": "code", - "execution_count": 170, + "execution_count": 206, "id": "5c071451", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "{'synthetic': True,\n", - " 'kind': 'history_summary',\n", - " 'summary_for_turns': '< all before idx 2 >'}" + "{'role': 'assistant',\n", + " 'content': '**Product & Environment:**\\n- Device: Router\\n- OS: Windows 10\\n- Firmware: v1.0.3\\n\\n**Reported Issue:**\\n- Router fails to connect to the internet, now showing error 42.\\n\\n**Steps Tried & Results:**\\n- Checked FAQs: No resolution.\\n- Firmware version checked: v1.0.3.\\n- Factory reset performed: Resulted in error 42.\\n\\n**Identifiers:**\\n- UNVERIFIED\\n\\n**Timeline Milestones:**\\n- User attempted FAQ troubleshooting.\\n- Firmware checked after initial advice.\\n- Factory reset led to error 42.\\n\\n**Tool Performance Insights:**\\n- FAQs and basic reset process did not resolve the issue.\\n\\n**Current Status & Blockers:**\\n- Error 42 unresolved; firmware update needed.\\n\\n**Next Recommended Step:**\\n- Install the latest firmware update and check for resolution.'}" ] }, - "execution_count": 170, + "execution_count": 206, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "history[1]['metadata']" + "history[1]" ] }, { "cell_type": "code", - "execution_count": 171, + "execution_count": 205, + "id": "10bb10d8", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'role': 'user', 'content': 'Summarize the conversation we had so far.'}" + ] + }, + "execution_count": 205, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "history[0]" + ] + }, + { + "cell_type": "code", + "execution_count": 207, + "id": "a4bc29f1", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[{'role': 'user', 'content': 'Summarize the conversation we had so far.'},\n", + " {'role': 'assistant',\n", + " 'content': '**Product & Environment:**\\n- Device: Router\\n- OS: Windows 10\\n- Firmware: v1.0.3\\n\\n**Reported Issue:**\\n- Router fails to connect to the internet, now showing error 42.\\n\\n**Steps Tried & Results:**\\n- Checked FAQs: No resolution.\\n- Firmware version checked: v1.0.3.\\n- Factory reset performed: Resulted in error 42.\\n\\n**Identifiers:**\\n- UNVERIFIED\\n\\n**Timeline Milestones:**\\n- User attempted FAQ troubleshooting.\\n- Firmware checked after initial advice.\\n- Factory reset led to error 42.\\n\\n**Tool Performance Insights:**\\n- FAQs and basic reset process did not resolve the issue.\\n\\n**Current Status & Blockers:**\\n- Error 42 unresolved; firmware update needed.\\n\\n**Next Recommended Step:**\\n- Install the latest firmware update and check for resolution.'},\n", + " {'role': 'user', 'content': 'I tried but I got another error now.'},\n", + " {'content': 'I still have a problem with my router.', 'role': 'user'},\n", + " {'content': [], 'role': 'user'},\n", + " {'content': [{'annotations': [],\n", + " 'text': 'Sorry you’re still stuck. What is the exact error code/message you see now during the firmware update, and does it appear in the router’s web UI or elsewhere?\\n\\nWhile you check that, try these quick, safe steps:\\n1) Verify the firmware file exactly matches your router’s model and hardware revision (check the label on the router) and region.\\n2) Re‑download the firmware from the vendor site and verify its checksum (MD5/SHA256) if provided.\\n3) Use a wired Ethernet connection to a LAN port, disable Wi‑Fi on the PC, and try a different browser with extensions disabled.\\n4) Ensure you’re uploading the correct file type (e.g., .bin/.img), not a ZIP; don’t rename the file.\\n5) Reboot the router and your PC, then retry the upload; after starting the update, wait at least 10 minutes and don’t power off.\\n\\nNote: “Error 42” meanings vary by brand; once you share the exact current error text and where it appears, I’ll give brand‑specific steps (including recovery options if needed).',\n", + " 'type': 'output_text',\n", + " 'logprobs': []}],\n", + " 'role': 'assistant'}]" + ] + }, + "execution_count": 207, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "history" + ] + }, + { + "cell_type": "code", + "execution_count": 208, + "id": "5448ce93", + "metadata": {}, + "outputs": [], + "source": [ + "full_history = await session.get_items_with_metadata()\n" + ] + }, + { + "cell_type": "code", + "execution_count": 209, + "id": "ddec0fb2", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[{'message': {'role': 'user',\n", + " 'content': 'Summarize the conversation we had so far.'},\n", + " 'metadata': {'synthetic': True,\n", + " 'kind': 'history_summary_prompt',\n", + " 'summary_for_turns': '< all before idx 6 >'}},\n", + " {'message': {'role': 'assistant',\n", + " 'content': '**Product & Environment:**\\n- Device: Router\\n- OS: Windows 10\\n- Firmware: v1.0.3\\n\\n**Reported Issue:**\\n- Router fails to connect to the internet, now showing error 42.\\n\\n**Steps Tried & Results:**\\n- Checked FAQs: No resolution.\\n- Firmware version checked: v1.0.3.\\n- Factory reset performed: Resulted in error 42.\\n\\n**Identifiers:**\\n- UNVERIFIED\\n\\n**Timeline Milestones:**\\n- User attempted FAQ troubleshooting.\\n- Firmware checked after initial advice.\\n- Factory reset led to error 42.\\n\\n**Tool Performance Insights:**\\n- FAQs and basic reset process did not resolve the issue.\\n\\n**Current Status & Blockers:**\\n- Error 42 unresolved; firmware update needed.\\n\\n**Next Recommended Step:**\\n- Install the latest firmware update and check for resolution.'},\n", + " 'metadata': {'synthetic': True,\n", + " 'kind': 'history_summary',\n", + " 'summary_for_turns': '< all before idx 6 >'}},\n", + " {'message': {'role': 'user',\n", + " 'content': 'I tried but I got another error now.'},\n", + " 'metadata': {'synthetic': False}},\n", + " {'message': {'content': 'I still have a problem with my router.',\n", + " 'role': 'user'},\n", + " 'metadata': {'synthetic': False}},\n", + " {'message': {'content': [], 'role': 'user'},\n", + " 'metadata': {'id': 'rs_68ba192de700819dbed28ad768a9c48205277fe33200f1e3',\n", + " 'summary': [],\n", + " 'type': 'reasoning',\n", + " 'synthetic': False}},\n", + " {'message': {'content': [{'annotations': [],\n", + " 'text': 'Sorry you’re still stuck. What is the exact error code/message you see now during the firmware update, and does it appear in the router’s web UI or elsewhere?\\n\\nWhile you check that, try these quick, safe steps:\\n1) Verify the firmware file exactly matches your router’s model and hardware revision (check the label on the router) and region.\\n2) Re‑download the firmware from the vendor site and verify its checksum (MD5/SHA256) if provided.\\n3) Use a wired Ethernet connection to a LAN port, disable Wi‑Fi on the PC, and try a different browser with extensions disabled.\\n4) Ensure you’re uploading the correct file type (e.g., .bin/.img), not a ZIP; don’t rename the file.\\n5) Reboot the router and your PC, then retry the upload; after starting the update, wait at least 10 minutes and don’t power off.\\n\\nNote: “Error 42” meanings vary by brand; once you share the exact current error text and where it appears, I’ll give brand‑specific steps (including recovery options if needed).',\n", + " 'type': 'output_text',\n", + " 'logprobs': []}],\n", + " 'role': 'assistant'},\n", + " 'metadata': {'id': 'msg_68ba19400060819db38bcb891e9aec7605277fe33200f1e3',\n", + " 'status': 'completed',\n", + " 'type': 'message',\n", + " 'synthetic': False}}]" + ] + }, + "execution_count": 209, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "full_history" + ] + }, + { + "cell_type": "code", + "execution_count": 210, "id": "64d555ee", "metadata": {}, "outputs": [ @@ -922,12 +1375,35 @@ "name": "stdout", "output_type": "stream", "text": [ - "- **User Goal:** Connect router to Windows 10.\n", - "- **Constraints:** Previous attempts using FAQs were unsuccessful. Third contact for support.\n", - "- **Current Environment:** Windows 10.\n", - "- **Tried:** FAQ troubleshooting.\n", - "- **Current Status:** Unable to connect router.\n", - "- **Next Step:** Check router firmware version.\n" + "**Product & Environment:**\n", + "- Device: Router\n", + "- OS: Windows 10\n", + "- Firmware: v1.0.3\n", + "\n", + "**Reported Issue:**\n", + "- Router fails to connect to the internet, now showing error 42.\n", + "\n", + "**Steps Tried & Results:**\n", + "- Checked FAQs: No resolution.\n", + "- Firmware version checked: v1.0.3.\n", + "- Factory reset performed: Resulted in error 42.\n", + "\n", + "**Identifiers:**\n", + "- UNVERIFIED\n", + "\n", + "**Timeline Milestones:**\n", + "- User attempted FAQ troubleshooting.\n", + "- Firmware checked after initial advice.\n", + "- Factory reset led to error 42.\n", + "\n", + "**Tool Performance Insights:**\n", + "- FAQs and basic reset process did not resolve the issue.\n", + "\n", + "**Current Status & Blockers:**\n", + "- Error 42 unresolved; firmware update needed.\n", + "\n", + "**Next Recommended Step:**\n", + "- Install the latest firmware update and check for resolution.\n" ] } ], @@ -935,17 +1411,6 @@ "print(history[1]['content'])" ] }, - { - "cell_type": "markdown", - "id": "1a935659", - "metadata": {}, - "source": [ - "Once the history exceeds `max_turns`. It keeps the most recent N user turns intact, **summarizes everything older into two synthetic messages**:\n", - "\n", - "* `user`: *\"Summarize the conversation we had so far.\"*\n", - "* `assistant`: *generated summary*\n" - ] - }, { "cell_type": "markdown", "id": "9744c6d8", @@ -966,9 +1431,9 @@ "source": [ "## Evals\n", "\n", - "At the end of the day, **evals is all you need**—even for context engineering. The key question to ask is: *how do we know the model isn’t “losing context” or \"confusing context\"?*\n", + "At the end of the day, **evals is all you need** for context engineering. The key question to ask is: *how do we know the model isn’t “losing context” or \"confusing context\"?*\n", "\n", - "While a full cookbook aorund memory could stand on its own in the future, here are some lightweight evaluation harness ideas to start with:\n", + "While a full cookbook around memory could stand on its own in the future, here are some lightweight evaluation harness ideas to start with:\n", "\n", "* **Baseline & Deltas:** Continue running your core eval sets and compare before/after experiments to measure memory improvements.\n", "* **LLM-as-Judge:** Use a model with a carefully designed grader prompt to evaluate summarization quality. Focus on whether it captures the most important details in the correct format.\n", From 9e1ff774ad974a5a476db986667469c222fcae76 Mon Sep 17 00:00:00 2001 From: emre-openai Date: Sun, 7 Sep 2025 21:31:27 -0700 Subject: [PATCH 05/11] images added --- examples/agents_sdk/session_memory.ipynb | 509 ++++++++++++++++------- images/memory_comparison.jpg | Bin 0 -> 72986 bytes images/summarizingSession.jpg | Bin 0 -> 46764 bytes images/trimingSession.jpg | Bin 0 -> 37425 bytes 4 files changed, 362 insertions(+), 147 deletions(-) create mode 100644 images/memory_comparison.jpg create mode 100644 images/summarizingSession.jpg create mode 100644 images/trimingSession.jpg diff --git a/examples/agents_sdk/session_memory.ipynb b/examples/agents_sdk/session_memory.ipynb index 20deef608e..66948c4484 100644 --- a/examples/agents_sdk/session_memory.ipynb +++ b/examples/agents_sdk/session_memory.ipynb @@ -15,7 +15,8 @@ "source": [ "In this cookbook, we’ll explore how to **manage context effectively using the `Session` object from the [OpenAI Agents SDK](https://github.com/openai/openai-agents-python)**.\n", "\n", - "AI agents often operate in **long-running, multi-turn interactions**, where keeping the right balance of context is critical. If too much is carried forward, the model risks distraction, inefficiency, or outright failure. If too little is preserved, the agent loses coherence. This guide focuses on two proven context management techniques—**trimming** and **compression**—used by engineering teams to keep agents fast, reliable, and cost-efficient.\n", + "AI agents often operate in **long-running, multi-turn interactions**, where keeping the right balance of context is critical. If too much is carried forward, the model risks distraction, inefficiency, or outright failure. If too little is preserved, the agent loses coherence. This guide focuses on two proven context management techniques—**trimming** and **compression**—to keep agents fast, reliable, and cost-efficient.\n", + "\n", "\n", "#### Why Context Management Matters\n", "\n", @@ -24,7 +25,22 @@ "* **Lower latency & cost** – Smaller, sharper prompts cut tokens per turn and attention load.\n", "* **Error & hallucination containment** – Summaries act as “clean rooms” that correct or omit prior mistakes; trimming avoids amplifying bad facts (“context poisoning”) turn after turn.\n", "* **Easier debugging & observability** – Stable summaries and bounded histories make logs comparable: you can diff summaries, attribute regressions, and reproduce failures reliably.\n", - "* **Multi-issue and handoff resilience** – In multi-problem chats, per-issue mini-summaries let the agent pause/resume, escalate to humans, or hand off to another agent while staying consistent.\n", + "* **Multi-issue and handoff resilience** – In multi-problem chats, per-issue mini-summaries let the agent pause/resume, escalate to humans, or hand off to another agent while staying consistent.\n" + ] + }, + { + "cell_type": "markdown", + "id": "1e0e1913", + "metadata": {}, + "source": [ + "![Memory Comparison in AI Agents](../../images/memory_comparison.jpg)" + ] + }, + { + "cell_type": "markdown", + "id": "7068564c", + "metadata": {}, + "source": [ "\n", "#### Real-World Scenario\n", "\n", @@ -105,6 +121,14 @@ "set_tracing_disabled(True)" ] }, + { + "cell_type": "markdown", + "id": "778b7952", + "metadata": {}, + "source": [ + "Let's test the installed libraries by defining and running an agent." + ] + }, { "cell_type": "code", "execution_count": 3, @@ -178,7 +202,7 @@ "id": "b8074e05", "metadata": {}, "source": [ - "## 1. Context Summarization " + "## 1. Context Trimming " ] }, { @@ -350,13 +374,13 @@ }, { "cell_type": "code", - "execution_count": 26, + "execution_count": null, "id": "951ad6da", "metadata": {}, "outputs": [], "source": [ "# Keep only the last 8 turns (user + assistant/tool interactions)\n", - "session = TrimmingSession(\"my_session\", max_turns=2)" + "session = TrimmingSession(\"my_session\", max_turns=3)" ] }, { @@ -544,6 +568,22 @@ "history" ] }, + { + "cell_type": "markdown", + "id": "d8e8de9a", + "metadata": {}, + "source": [ + "Below, you can see how the trimming session works for max_turns=3." + ] + }, + { + "cell_type": "markdown", + "id": "42471c87", + "metadata": {}, + "source": [ + "![Context Trimming in Session](../../images/trimingSession.jpg)" + ] + }, { "cell_type": "markdown", "id": "ad8dd9c5", @@ -600,13 +640,7 @@ "\n", "* Change `max_turns` at init or via `set_max_turns(...)`.\n", "* Adjust `_is_user_msg(...)` if your item schema differs.\n", - "* If you’d rather cap by **message count** or **tokens**, replace `_trim_to_last_turns(...)` or add a second pass that measures tokens.\n", - "\n", - "**Complexity**\n", - "\n", - "* Each trim scans the list once ⇒ **O(n)** per trim (n = items in memory). In practice this is cheap because n is bounded by your recent turns.\n", - "\n", - "That’s the whole flow: **append → find last N user boundaries → keep from the earliest boundary onward**.\n" + "* If you’d rather cap by **message count** or **tokens**, replace `_trim_to_last_turns(...)` or add a second pass that measures tokens.\n" ] }, { @@ -627,7 +661,7 @@ "* `user`: *\"Summarize the conversation we had so far.\"*\n", "* `assistant`: *{generated summary}*\n", "\n", - "The shadow prompt from the user to requst the summarization added to keep natural flow of the conversation without confusing the chat flow between user and assistant. Final version of the generated summary injected to assistant message." + "The shadow prompt from the user to request the summarization added to keep natural flow of the conversation without confusing the chat flow between user and assistant. Final version of the generated summary injected to assistant message." ] }, { @@ -701,11 +735,11 @@ "id": "4a9f7b15", "metadata": {}, "source": [ - "**Key Principles for Designing Summarization Prompts**\n", + "**Key Principles for Designing Memory Summarization Prompts**\n", "\n", "* **Milestones:** Highlight important events in the conversation—for example, when an issue is resolved, valuable information is uncovered, or all necessary details have been collected.\n", "\n", - "* **Contradiction Check:** Ensure the summary does not conflict with system instructions or tool definitions. This is especially critical for reasoning models, which are more prone to conflicts in the context.\n", + "* **Contradiction Check:** Ensure the summary does not conflict with itself, system instructions or tool definitions. This is especially critical for reasoning models, which are more prone to conflicts in the context.\n", "\n", "* **Timestamps & Temporal Flow:** Incorporate timing of events in the summary. This helps the model reason about updates in sequence and reduces confusion when forgetting or remembering the latest memory over a timeline.\n", "\n", @@ -724,7 +758,7 @@ }, { "cell_type": "code", - "execution_count": 134, + "execution_count": 219, "id": "8f3f811e", "metadata": {}, "outputs": [], @@ -771,7 +805,7 @@ }, { "cell_type": "code", - "execution_count": 195, + "execution_count": null, "id": "4bb5c4e9", "metadata": {}, "outputs": [], @@ -1088,7 +1122,288 @@ }, { "cell_type": "code", - "execution_count": 211, + "execution_count": 237, + "id": "0d8bd4c5", + "metadata": {}, + "outputs": [], + "source": [ + "import asyncio\n", + "import itertools\n", + "from collections import deque\n", + "from typing import Optional, List, Tuple, Dict, Any\n", + "\n", + "class SummarizingSession:\n", + " \"\"\"\n", + " Keeps the last N *user turns* verbatim (keep_last_n_turns).\n", + " A turn = one real user message + everything that follows it (assistant replies,\n", + " reasoning, tool calls, tool results) until the next real user message.\n", + " Summarizes everything before that into a synthetic user→assistant pair.\n", + "\n", + " Summarization is triggered once the number of *real* user turns\n", + " (non-synthetic 'user' messages) exceeds `context_limit`.\n", + "\n", + " Internally stores (message, metadata) records. Exposes:\n", + " - get_items(): model-safe messages only (no metadata)\n", + " - get_full_history(): [{ \"message\": msg, \"metadata\": meta }, ...]\n", + " \"\"\"\n", + "\n", + " # Only these keys are sent to the model. Everything else goes to metadata.\n", + " _ALLOWED_MSG_KEYS = {\"role\", \"content\", \"name\"}\n", + "\n", + " def __init__(\n", + " self,\n", + " keep_last_n_turns: int = 3,\n", + " context_limit: int = 3,\n", + " summarizer: Optional[\"Summarizer\"] = None,\n", + " session_id: Optional[str] = None,\n", + " ):\n", + " assert context_limit >= 1\n", + " assert keep_last_n_turns >= 0\n", + " assert keep_last_n_turns <= context_limit, \"keep_last_n_turns should not be greater than context_limit\"\n", + " self.keep_last_n_turns = keep_last_n_turns\n", + " self.context_limit = context_limit\n", + " # Each record: {\"msg\": {...}, \"meta\": {...}}\n", + " self._records: deque[Dict[str, Dict[str, Any]]] = deque()\n", + " self._lock = asyncio.Lock()\n", + " self.session_id = session_id or \"default\"\n", + " self.summarizer = summarizer\n", + "\n", + " # --------- public API used by your runner ---------\n", + "\n", + " async def get_items(self, limit: Optional[int] = None) -> List[Dict[str, Any]]:\n", + " \"\"\"\n", + " Returns messages in a model-safe shape (no metadata).\n", + " Runner.run(..., session=self) should call this.\n", + " \"\"\"\n", + " async with self._lock:\n", + " data = list(self._records)\n", + " msgs = [self._sanitize_for_model(rec[\"msg\"]) for rec in data]\n", + " return msgs[-limit:] if limit else msgs\n", + "\n", + " async def add_items(self, items: List[Dict[str, Any]]) -> None:\n", + " async with self._lock:\n", + " for it in items:\n", + " msg, meta = self._split_msg_and_meta(it)\n", + " self._records.append({\"msg\": msg, \"meta\": meta})\n", + " need_summary, boundary_idx = self._should_summarize_locked()\n", + "\n", + " if need_summary:\n", + " async with self._lock:\n", + " prefix_records = list(itertools.islice(self._records, 0, boundary_idx))\n", + " prefix_msgs = [r[\"msg\"] for r in prefix_records]\n", + "\n", + " user_shadow, assistant_summary = await self._summarize(prefix_msgs)\n", + "\n", + " async with self._lock:\n", + " need_summary_now, boundary_idx_now = self._should_summarize_locked()\n", + " if not need_summary_now:\n", + " # normalize anyway if summarization got skipped\n", + " self._normalize_synthetic_flags_locked()\n", + " return\n", + "\n", + " suffix_records = list(itertools.islice(self._records, boundary_idx_now, None))\n", + " self._records.clear()\n", + "\n", + " # Synthetic summary pair keeps synthetic=True\n", + " self._records.extend([\n", + " {\n", + " \"msg\": {\"role\": \"user\", \"content\": user_shadow},\n", + " \"meta\": {\n", + " \"synthetic\": True,\n", + " \"kind\": \"history_summary_prompt\",\n", + " \"summary_for_turns\": f\"< all before idx {boundary_idx_now} >\",\n", + " },\n", + " },\n", + " {\n", + " \"msg\": {\"role\": \"assistant\", \"content\": assistant_summary},\n", + " \"meta\": {\n", + " \"synthetic\": True,\n", + " \"kind\": \"history_summary\",\n", + " \"summary_for_turns\": f\"< all before idx {boundary_idx_now} >\",\n", + " },\n", + " },\n", + " ])\n", + " self._records.extend(suffix_records)\n", + "\n", + " # ✅ Ensure all real messages explicitly have synthetic=False\n", + " self._normalize_synthetic_flags_locked()\n", + " else:\n", + " # ✅ Even when we don't summarize, enforce the invariant\n", + " async with self._lock:\n", + " self._normalize_synthetic_flags_locked()\n", + "\n", + " async def pop_item(self) -> Optional[Dict[str, Any]]:\n", + " async with self._lock:\n", + " if not self._records:\n", + " return None\n", + " rec = self._records.pop()\n", + " return dict(rec[\"msg\"]) # model-safe\n", + "\n", + " async def clear_session(self) -> None:\n", + " async with self._lock:\n", + " self._records.clear()\n", + "\n", + " def set_max_turns(self, n: int) -> None:\n", + " \"\"\"\n", + " Back-compat: interpret as updating context_limit.\n", + " Ensures keep_last_n_turns <= context_limit.\n", + " \"\"\"\n", + " assert n >= 1\n", + " self.context_limit = n\n", + " if self.keep_last_n_turns > self.context_limit:\n", + " self.keep_last_n_turns = self.context_limit\n", + "\n", + " # --------- full-history (for debugging/analytics/observability) ---------\n", + "\n", + " # ✅ Backfill safeguard for older records that might lack the flag\n", + " def _normalize_synthetic_flags_locked(self) -> None:\n", + " for rec in self._records:\n", + " role = rec[\"msg\"].get(\"role\")\n", + " if role in (\"user\", \"assistant\") and \"synthetic\" not in rec[\"meta\"]:\n", + " rec[\"meta\"][\"synthetic\"] = False\n", + "\n", + " \n", + " async def get_full_history(self, limit: Optional[int] = None) -> List[Dict[str, Any]]:\n", + " \"\"\"\n", + " Returns combined history where each entry is:\n", + " { \"message\": {role, content[, name]}, \"metadata\": {...} }\n", + " This is NOT sent to the model; it's for your logs/UI/debugging.\n", + " \"\"\"\n", + " async with self._lock:\n", + " data = list(self._records)\n", + " out = [{\"message\": dict(rec[\"msg\"]), \"metadata\": dict(rec[\"meta\"])} for rec in data]\n", + " return out[-limit:] if limit else out\n", + "\n", + " # Backwards-compatible alias if you were using this name before\n", + " async def get_items_with_metadata(self, limit: Optional[int] = None) -> List[Dict[str, Any]]:\n", + " return await self.get_full_history(limit)\n", + "\n", + " # --------- helpers ---------\n", + "\n", + " def _split_msg_and_meta(self, it: Dict[str, Any]) -> Tuple[Dict[str, Any], Dict[str, Any]]:\n", + " msg = {k: v for k, v in it.items() if k in self._ALLOWED_MSG_KEYS}\n", + " extra = {k: v for k, v in it.items() if k not in self._ALLOWED_MSG_KEYS}\n", + " meta = dict(extra.pop(\"metadata\", {}))\n", + " meta.update(extra)\n", + "\n", + " if \"role\" not in msg or \"content\" not in msg:\n", + " msg.setdefault(\"role\", \"user\")\n", + " msg.setdefault(\"content\", str(it))\n", + "\n", + " # ✅ Default synthetic flag for real (non-summarized) messages\n", + " role = msg.get(\"role\")\n", + " if role in (\"user\", \"assistant\") and \"synthetic\" not in meta:\n", + " meta[\"synthetic\"] = False\n", + " return msg, meta\n", + "\n", + " def _sanitize_for_model(self, msg: Dict[str, Any]) -> Dict[str, Any]:\n", + " \"\"\"\n", + " Strictly keep only allowed keys for model input.\n", + " \"\"\"\n", + " return {k: v for k, v in msg.items() if k in self._ALLOWED_MSG_KEYS}\n", + "\n", + " def _is_user(self, rec: Dict[str, Dict[str, Any]]) -> bool:\n", + " return rec[\"msg\"].get(\"role\") == \"user\"\n", + "\n", + " def _should_summarize_locked(self) -> Tuple[bool, int]:\n", + " \"\"\"\n", + " Trigger summarization if the number of *real* user turns exceeds `context_limit`.\n", + "\n", + " Keep the last `keep_last_n_turns` *turns* verbatim:\n", + " find the earliest index among the last `keep_last_n_turns` real user messages;\n", + " everything before that index becomes the summarization prefix.\n", + "\n", + " Returns: (need_summary: bool, boundary_idx: int)\n", + " \"\"\"\n", + " # Collect indices of real user messages (turn starts)\n", + " user_idxs: List[int] = []\n", + " for i, rec in enumerate(self._records):\n", + " if self._is_user(rec) and not rec[\"meta\"].get(\"synthetic\", False):\n", + " user_idxs.append(i)\n", + "\n", + " real_turns = len(user_idxs)\n", + " if real_turns <= self.context_limit:\n", + " return False, -1\n", + "\n", + " # Determine boundary according to \"turns\"\n", + " if self.keep_last_n_turns == 0:\n", + " # summarize everything; keep no turns verbatim\n", + " boundary = len(self._records)\n", + " else:\n", + " if len(user_idxs) < self.keep_last_n_turns:\n", + " return False, -1 # defensive; should not happen due to the check above\n", + " # earliest index among the last N real user-turn starts\n", + " boundary = user_idxs[-self.keep_last_n_turns]\n", + "\n", + " # If boundary is 0 and we intend to keep >=1 turn, there's nothing before to summarize.\n", + " if boundary <= 0 and self.keep_last_n_turns > 0:\n", + " return False, -1\n", + "\n", + " return True, boundary\n", + "\n", + " async def _summarize(self, prefix_msgs: List[Dict[str, Any]]) -> Tuple[str, str]:\n", + " \"\"\"\n", + " Adapter to your summarizer. Provide *model-safe* messages only.\n", + " \"\"\"\n", + " if not self.summarizer:\n", + " # Fallback summary if no summarizer is configured\n", + " return (\"Summarize the conversation we had so far.\", \"Summary unavailable.\")\n", + " # Only send role/content/name to the summarizer as well\n", + " clean_prefix = [self._sanitize_for_model(m) for m in prefix_msgs]\n", + " return await self.summarizer.summarize(clean_prefix)\n" + ] + }, + { + "cell_type": "markdown", + "id": "214228c8", + "metadata": {}, + "source": [ + "![Contxt Trimming in Session](../../images/SummarizingSession.jpg)" + ] + }, + { + "cell_type": "markdown", + "id": "97b12761", + "metadata": {}, + "source": [ + "**High‑level idea**\n", + "\n", + "* **A turn** = one **real user** message **plus everything that follows it** (assistant replies, tool calls/results, etc.) **until the next real user message**.\n", + "* You configure two knobs:\n", + "\n", + " * **`context_limit`**: the maximum number of **real user turns** allowed in the raw history before we summarize.\n", + " * **`keep_last_n_turns`**: how many of the most recent **turns** to keep verbatim when we do summarize.\n", + "\n", + " * Invariant: `keep_last_n_turns <= context_limit`.\n", + "* When the number of **real** user turns exceeds `context_limit`, the session:\n", + "\n", + " 1. **Summarizes** everything **before** the earliest of the last `keep_last_n_turns` turn starts,\n", + " 2. Injects a **synthetic user→assistant pair** at the top of the kept region:\n", + "\n", + " * `user`: `\"Summarize the conversation we had so far.\"` (shadow prompt)\n", + " * `assistant`: `{generated summary}`\n", + " 3. **Keeps** the last `keep_last_n_turns` turns **verbatim**.\n", + "\n", + "This guarantees the last `keep_last_n_turns` turns are preserved exactly as they occurred, while all earlier content is compressed into the two synthetic messages.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 244, + "id": "6d867c0b", + "metadata": {}, + "outputs": [], + "source": [ + "session = SummarizingSession(\n", + " keep_last_n_turns=2,\n", + " context_limit=4,\n", + " summarizer=LLMSummarizer(client)\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, "id": "a8d22531", "metadata": {}, "outputs": [ @@ -1101,10 +1416,6 @@ } ], "source": [ - "session = SummarizingSession(\n", - " max_turns=3,\n", - " summarizer=LLMSummarizer(client)\n", - ")\n", "\n", "# Example flow\n", "await session.add_items([{\"role\": \"user\", \"content\": \"Hi, my router won't connect. by the way, I am using Windows 10. I tried troubleshooting via your FAQs but I didn't get anywhere. This is my third tiem calling you. I am based in the US and one of Premium customers.\"}])\n", @@ -1114,16 +1425,27 @@ "await session.add_items([{\"role\": \"user\", \"content\": \"Reset done; error 42 now.\"}])\n", "await session.add_items([{\"role\": \"assistant\", \"content\": \"Try to install a new firmware.\"}])\n", "await session.add_items([{\"role\": \"user\", \"content\": \"I tried but I got another error now.\"}])\n", - "# At this point, with max_turns=3, everything *before* the earliest of the last 3 user\n", - "# messages is summarized into a synthetic pair, and the last 3 turns remain verbatim.\n", - "\n", + "await session.add_items([{\"role\": \"assistant\", \"content\": \"Can you please provide me with the error code?\"}])\n", + "await session.add_items([{\"role\": \"user\", \"content\": \"It says 404 not found when I try to access the page.\"}])\n", + "await session.add_items([{\"role\": \"assistant\", \"content\": \"Are you connected to the internet?\"}])\n", + "# At this point, with context_limit=4, everything *before* the earliest of the last 4 turns\n", + "# is summarized into a synthetic pair, and the last 2 turns remain verbatim.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e2e1b482", + "metadata": {}, + "outputs": [], + "source": [ "history = await session.get_items()\n", - "# Pass `history` into your agent runner / responses call as the conversation context.\n" + "# Pass `history` into your agent runner / responses call as the conversation context." ] }, { "cell_type": "code", - "execution_count": 212, + "execution_count": 246, "id": "9f229de7", "metadata": {}, "outputs": [ @@ -1132,15 +1454,16 @@ "text/plain": [ "[{'role': 'user', 'content': 'Summarize the conversation we had so far.'},\n", " {'role': 'assistant',\n", - " 'content': \"• Product & Environment:\\n - Windows 10, router (specific model UNVERIFIED).\\n\\n• Reported Issue:\\n - Router won't connect.\\n\\n• Steps Tried & Results:\\n - Used FAQs for troubleshooting; no resolution.\\n\\n• Identifiers:\\n - Premium customer; based in the US (no specific identifiers provided).\\n\\n• Timeline Milestones:\\n - Third interaction reported by user.\\n\\n• Tool Performance Insights:\\n - User FAQs insufficient for resolution.\\n\\n• Current Status & Blockers:\\n - Connection issue unresolved; firmware version not yet checked.\\n\\n• Next Recommended Step:\\n - Verify and update the router firmware version.\"},\n", - " {'role': 'user', 'content': 'Firmware v1.0.3; still failing.'},\n", - " {'role': 'assistant', 'content': 'Try a factory reset.'},\n", - " {'role': 'user', 'content': 'Reset done; error 42 now.'},\n", - " {'role': 'assistant', 'content': 'Try to install a new firmware.'},\n", - " {'role': 'user', 'content': 'I tried but I got another error now.'}]" + " 'content': \"• Product & Environment:\\n - Router with firmware v1.0.3, Windows 10, US-based, Premium customer.\\n\\n• Reported Issue:\\n - Router won't connect to the internet.\\n\\n• Steps Tried & Results:\\n - Followed FAQs for troubleshooting; no resolution.\\n - Checked firmware version: v1.0.3; issue persists.\\n - Performed factory reset; encountered error 42.\\n\\n• Identifiers:\\n - None provided.\\n\\n• Timeline Milestones:\\n - Initial troubleshooting via FAQs → Firmware check → Factory reset → Error 42.\\n\\n• Tool Performance Insights:\\n - Factory reset unsuccessful in resolving connection issue; led to error 42.\\n\\n• Current Status & Blockers:\\n - Connection issue unresolved; error 42 after reset is blocking progress.\\n\\n• Next Recommended Step:\\n - Install new firmware version compatible with device.\"},\n", + " {'role': 'user', 'content': 'I tried but I got another error now.'},\n", + " {'role': 'assistant',\n", + " 'content': 'Can you please provide me with the error code?'},\n", + " {'role': 'user',\n", + " 'content': 'It says 404 not found when I try to access the page.'},\n", + " {'role': 'assistant', 'content': 'Are you connected to the internet?'}]" ] }, - "execution_count": 212, + "execution_count": 246, "metadata": {}, "output_type": "execute_result" } @@ -1190,124 +1513,16 @@ ] }, { - "cell_type": "code", - "execution_count": 201, - "id": "1e26d8af", - "metadata": {}, - "outputs": [], - "source": [ - "message = \"I still have a problem with my router.\"" - ] - }, - { - "cell_type": "code", - "execution_count": 202, - "id": "950d663b", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "2\n" - ] - } - ], - "source": [ - "result = await Runner.run(\n", - " support_agent,\n", - " message,\n", - " session=session\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": 203, - "id": "6af52337", - "metadata": {}, - "outputs": [], - "source": [ - "history = await session.get_items()" - ] - }, - { - "cell_type": "code", - "execution_count": 206, - "id": "5c071451", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'role': 'assistant',\n", - " 'content': '**Product & Environment:**\\n- Device: Router\\n- OS: Windows 10\\n- Firmware: v1.0.3\\n\\n**Reported Issue:**\\n- Router fails to connect to the internet, now showing error 42.\\n\\n**Steps Tried & Results:**\\n- Checked FAQs: No resolution.\\n- Firmware version checked: v1.0.3.\\n- Factory reset performed: Resulted in error 42.\\n\\n**Identifiers:**\\n- UNVERIFIED\\n\\n**Timeline Milestones:**\\n- User attempted FAQ troubleshooting.\\n- Firmware checked after initial advice.\\n- Factory reset led to error 42.\\n\\n**Tool Performance Insights:**\\n- FAQs and basic reset process did not resolve the issue.\\n\\n**Current Status & Blockers:**\\n- Error 42 unresolved; firmware update needed.\\n\\n**Next Recommended Step:**\\n- Install the latest firmware update and check for resolution.'}" - ] - }, - "execution_count": 206, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "history[1]" - ] - }, - { - "cell_type": "code", - "execution_count": 205, - "id": "10bb10d8", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'role': 'user', 'content': 'Summarize the conversation we had so far.'}" - ] - }, - "execution_count": 205, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "history[0]" - ] - }, - { - "cell_type": "code", - "execution_count": 207, - "id": "a4bc29f1", + "cell_type": "markdown", + "id": "df6004db", "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[{'role': 'user', 'content': 'Summarize the conversation we had so far.'},\n", - " {'role': 'assistant',\n", - " 'content': '**Product & Environment:**\\n- Device: Router\\n- OS: Windows 10\\n- Firmware: v1.0.3\\n\\n**Reported Issue:**\\n- Router fails to connect to the internet, now showing error 42.\\n\\n**Steps Tried & Results:**\\n- Checked FAQs: No resolution.\\n- Firmware version checked: v1.0.3.\\n- Factory reset performed: Resulted in error 42.\\n\\n**Identifiers:**\\n- UNVERIFIED\\n\\n**Timeline Milestones:**\\n- User attempted FAQ troubleshooting.\\n- Firmware checked after initial advice.\\n- Factory reset led to error 42.\\n\\n**Tool Performance Insights:**\\n- FAQs and basic reset process did not resolve the issue.\\n\\n**Current Status & Blockers:**\\n- Error 42 unresolved; firmware update needed.\\n\\n**Next Recommended Step:**\\n- Install the latest firmware update and check for resolution.'},\n", - " {'role': 'user', 'content': 'I tried but I got another error now.'},\n", - " {'content': 'I still have a problem with my router.', 'role': 'user'},\n", - " {'content': [], 'role': 'user'},\n", - " {'content': [{'annotations': [],\n", - " 'text': 'Sorry you’re still stuck. What is the exact error code/message you see now during the firmware update, and does it appear in the router’s web UI or elsewhere?\\n\\nWhile you check that, try these quick, safe steps:\\n1) Verify the firmware file exactly matches your router’s model and hardware revision (check the label on the router) and region.\\n2) Re‑download the firmware from the vendor site and verify its checksum (MD5/SHA256) if provided.\\n3) Use a wired Ethernet connection to a LAN port, disable Wi‑Fi on the PC, and try a different browser with extensions disabled.\\n4) Ensure you’re uploading the correct file type (e.g., .bin/.img), not a ZIP; don’t rename the file.\\n5) Reboot the router and your PC, then retry the upload; after starting the update, wait at least 10 minutes and don’t power off.\\n\\nNote: “Error 42” meanings vary by brand; once you share the exact current error text and where it appears, I’ll give brand‑specific steps (including recovery options if needed).',\n", - " 'type': 'output_text',\n", - " 'logprobs': []}],\n", - " 'role': 'assistant'}]" - ] - }, - "execution_count": 207, - "metadata": {}, - "output_type": "execute_result" - } - ], "source": [ - "history" + "You can use the get_items_with_metadata method to get the full history of the session including the metadata for debugging and analysis purposes." ] }, { "cell_type": "code", - "execution_count": 208, + "execution_count": 248, "id": "5448ce93", "metadata": {}, "outputs": [], @@ -1418,7 +1633,7 @@ "source": [ "### Notes & design choices\n", "\n", - "* **Turn boundary preserved at the “fresh” side**: the **last `max_turns` user turns** remain verbatim; everything older is compressed.\n", + "* **Turn boundary preserved at the “fresh” side**: the **`keep_last_n_turns` user turns** remain verbatim; everything older is compressed.\n", "* **Two-message summary block**: easy for downstream tooling to detect or display (`metadata.synthetic == True`).\n", "* **Async + lock discipline**: we **release the lock** while the (potentially slow) summarization runs; then re-check the condition before applying the summary to avoid racey merges.\n", "* **Idempotent behavior**: if more messages arrive during summarization, the post-await recheck prevents stale rewrites.\n" diff --git a/images/memory_comparison.jpg b/images/memory_comparison.jpg new file mode 100644 index 0000000000000000000000000000000000000000..245575eb4936c7697df48dbf841f97506c23d436 GIT binary patch literal 72986 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

-_OPLm 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

4^6q)AoAOJcQ2`Rq(8iJ!vN^A_(ErM} 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 literal 0 HcmV?d00001 diff --git a/images/summarizingSession.jpg b/images/summarizingSession.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ddd68580fbbbd5ce46e0fd5612be10771e8caf7b GIT binary patch literal 46764 zcmeFYcU+Utwl5qkAc!bMkS--4B_O?nbP}3Wg@8yWKtQ?(f=cg%-jQBICv=qFd+1HN zbm>TaR$6ZOD%sjJZtu>P=Yu5A4^~CizzypXJSPp=N1pr`S zUV!UGtO>BRw83+ARXMPd?B5GI0T{aV6aawRINGbrOFe$6qx%@=+uv*aDl;^8c=PM~ zUl2y_@z^iv0Kf?7U-109+qfpC4#pUUAD9oLJw`dkSQ1Q{#Qg8+C%@80e@~14O1n6| zam4UE|CP4aRF}r2O)%*v=6|G({zx0YvH!&%jNuW5TRZ*I^^1O4jBjeArGfdrgZWSa z902M7d4SZf_G9uGvP}m71djlKo8iCj8N~wtRXzZ~qp9Eb7(W95_q_oC)WGk1zx(8k zp}pbX#NEPNZzbZ0y_ExOedIaPQ#a;^E)N$Gb~# z7Z>*);XQ);4;~Ocz{4jZCVD`ONk8}{1nXDLo44*@Dn7W2dlz%}|0P|21rXl3;f8f^ z6N?^jgAnT`A=Y&(fEqK_{*(Try*Svn@7%g^6AKqpZ2SNKz{bM5fsKC$4;$we?#YIp=q&m|~n=oC}=h=H45P2;8A(5Sj|WNdi_ zE)TDiru|1oBSa~XnNM2QF|DkbNyfoAqp4*9ql5x84{ll`{7lUvLMIPb!yjOZ&N(ZnBP2M64fPiqhAnuPsCL7f|Gf%GpWYyK3VO&e%E=!ri4Q;mKQ_gVhrm38NDyAlM#Qp9osJ_4Hf<6P?Wk`+WD^R`?JGK75!y0azl;4u;3OeeRGQSGXtk=} zHNt-LQQ%Dc(_~%K7Y%gI>~#H4yKY5j$XNo3UX+StBj;X=`RjIMLOMobaTp2^pNZG5& zse_fPO_n;Ph|0Kmk!@)>6=81Xg1)pkl;dxuNuOOMj}>5BsRc2@$~9lLE7`XU0aBk# zPb+lWgGB@csv=aZXX#%i>Hm;N>t51)ZYycv^1&NLiuZrtiVtEXW~xE)7>eO$6$djG z#wZJ?i)BaIZ1JcJGu)iws$^DKHR)+4F6dQI*&`&KjbH6Z$%cT>n#|AN?)C3#AHRM zRCLokDA;-qy)bB12DAdT>u#43aCO?Cq#blE)e58Q%lKzrS0l3<67P)kx-n3w;*B#k zH1Fw)3~xQuA&^L`swnR9aA#vp3YAJOfa5$)7tVq3^eC?CM<~~3ILo0uKyt6$m~V|U zabI|s7WDD<{^%mIqOK|SI1_!TI2S^HnKqTUn{Dx8+G*+vB$CNV#}2=aMD zXXzQ{CfxY9C=3_Os_86A?opscjcmvb9HPKO7Y?KynfXel@w%-tM-4P0MTJqr6iyS~ zv7{D2B1*Ar3$b~}5C0?U7%qSD=6S6cOdroVc&!Fs1U8tzFsYslZg+Of5kwhpMGD1& zg1);_k1Cf^Zr;u3X$?>)~@k`Al;frd`!GKdXN zWt{p4|NKA;vkHCEW?~AJ(|;wkm0N1EShmfEH;0!RN&QKqFI_!>9;_I;9-*fmLQqu; z(H-=0G}|-_9xHNi7OwGW(}$XEEjM@);yk>yOJeC5GVnnUM!{cTXzNZJ7AMW4a=%8l zNWMVF)aLHJ>7*R>r&f?@0)B22a28UFXgTF91|mhN2ku@(!_-44)uU}0Y}<0`$!Lo^ z9lO_9hNn#Hz!By#VeEnJdeKsLW)L37vF@n}*?J4muo~k?$%jLeOZ5@fypjSC zAK4>6?#_74C;Ol!)N;hzC^#idy>q+~td`?qv&VQ0pD_NWQnrrHwq&>1PYB8nf~hZ3 zH@vT4Hk8q{=2-B0 z(+b3QfA|nr{e`eF)e?TV0wj({dk^SFTx-dD0CSS}GRuCs>e_OufMQ z_+zPvawb>*frYJA2J)mTZ<-Y2!%EV)<(=(i6Ybp0?2o~aAkgGUEO{3TjDrRJbbgTn zn%FPsy|cA;3T8NnDYl6A#UWyn*YaD67DT?{^Lke4L$4|Le4D1IL9wUE4x&cKs!kG> zW@8P5s6WrzNkF~XW4G7N(B6Re*i&44Le7c;7TfUA8R z{2PPFx;w`Rgv%BmrwXeUBuM{R`vMyyFH>BmZpq{%g?|W;($%yB&HzeGbjHkdw)eNk z*S-zSw-=yIjH=shjv7eWSa(u3nTm>?GfKN!>&$ANCi6m|*}h4xZTutoyzDVDvID72 z1uyL`EJOK0*+Rr8WiV4Y`b5*PDQ%*Z>R7?gdUAHzl%_G~bR57b^tL3jJwlObitpn# z^u6|1*33C{Rg8veLx>(4 zsRE6~Si5>*(&JxCPl4Od_Qf*xbL7DSyoWx7LBQaV_BRx3MXJJyGNIu)VL z#QUU`!0OVIn(FbW2p$GT+uSPo+mEgRcP^^ym`7$42vzJP=)hmY4&w4db)5sl>9hZ*2LbiEE;xN1mI)AxQtiAZ9%TJ`7 zCzYkD5eD5!n~Wcbnx5xHo;?3mjjqI{_?5goqCxhu(CI~ioxyU`pC*w5Hx1M+`07s(D&bYi-QD`1y2g^oMI#9&IdBtanlR*LQA7)>ZVwM#`*G z*MQ80?5Gg)e)qMCpB+Dc8}M7W&MRO?_=L!KF+;{5>RPI@CfZEx~)bxZjinR zuv_LQINI#|zAupbr7|@C$cF7APKXk1Fx8>;uIk~HlF)2?(of}v1IWA+L2ti%_T0}b z+ghLKMLpMhUJdW|g+)U)IJb@S8=!&L049PzI*`D{%v@Z<)_u5YR{D4MnZAvsk$%1V zsy_7NI#a$Z2Y;Q9xEtN#DZ)xIOtk45&>MHu%ieP$D19mtV}(j0qfIf2t~nz1+=M?p z4Q($AOcd8K393TQstNR_K$(UO?7N7N;ktMeRZ^+%(`HP`lin7Ku)c}lvv9D%GA%NL(#I#p^ zDQBUu6II_#>SGJKM`1mb*%42j7rxAYem{+achd?;*ZKq%5nk zR*=S;o~4RjGEVFEs+Q~)|JDIz97m#UD`qEKJZbEFe3HOF=_wDm~*K6YgAJIHR8x7F#{W6)->1P`v3dnJn9|1*l1=e zG5?W+{>haJP8)lb-$v?3;2x%%YEW5{M zHb4rd;E#&GkdX-c_md#(k5$I&k5!~4`TwkGE`9`ysYJ)phQ5ieDsvTGw_76sX5c?jVMJet3L8`J_H6D84F zNur5Vle%O~p!j~=9A&Vmf~`a<_TjAfI5#B;hYlq;Y7w>J5>feNv?Q{m2bRi3j-YLq z?(G?&mg;ugB}TV>tLk5=F+LFjMdZ;S?WRtw2fO6`=z%GlL=4VsY7=kzz)slBN+_b( z(%A3XB`>AkM0!&aQHt^}8X6ZAGG!Jua!#Ht z)lT2TRl2e+WE@<8y0TWOF+-X~hjB(H&rRD{zG+joOpLc=q-xcOD{n|AYg=uO#qTB0 z_7_^}(KL76;2G`VaOD1+%7}KGJBLRe_>@{XB>;fM{R|V#oc?k}d~_)=$JN#;ZF3Dc zRUJQ^f7?Mumk2)Llo(PLsG;?Xx0J@X7n#B%0lc8QU=~(zObY|UX=l$WQUASe=VI)# zk(?Q5;K=kjzP^`Qe7q#gWb0PSeDYHR|450XVe^BnhlZ>}MR4hH&DhrDX8|Q&$7@eGM!bG;^vw*{4)1P(UI&$6d4 zEN)!Vu5wDWfYMG6E~o|pl=m>|X~lfJ@#0h`bydO$s7xq}JHt1i@JzDyEEYML2+m>S zc-bUFR|tJg@XBG#_oSAH^!(di>bS>H9lDMO7HYe6Z0hQkpiH@RhBa<-gS&aY)G>9>~-IuTZ#}BDxVRJ-ZiI{dAN|?hz#xnY;*sOA|7vzX;~vN zITzcQiRVM~O>u;9W^HJTC0iMg576f$-m`9|9@1#TL4VIJ+j0^YoGptA(`V3dob4*H zf}*Kl8th9{6I|VsUiO;y3~KemF2~4QGUoNkStY1#s$frj#~{WwHiwt*{GPSR*2mH{ zBe>gQaM$nXWGOShm#uVDv{`r9;ucFHVj+(Bh|Xe3sH&u$kWj7N}fX_S`VRv)#qSCb4F7m?QPRkBC**;gG;J;f~KbUkY5}XY@vbGXjHm~TanoiRyv%s_5=2gY{5=u)$%&@ z^9&Vr!`66s;XJfG5y>3;@E|8q&*xSZ(ykeBVxpVyE|pkU{CeiKm4btkl8$KzW@^^hjUCpjjl4v5&d0*F??$Oc=5Y6xLs!Cf#^@ z_1Rt{Yfib;CMRs;b~@#yYe=DzYU_U||Nq-^BD>e}!o#5n934y&4*Ed$%;SPUBMn>O za?W^0-_e0R_=Ev@cy*{26La8_+`>}asFqqoNs{36X=Bfm9YXjuAa^5>CGQ~+{Pv-` zs=>^y0=e_W%_@Zlmk-k(s<93=p4M&;-;$8EkOi7*{?nBaa}$g8Tk`(Pp~kSi-MTnT ztv_-AEtwvz&V}WP(ca{cW+mvIyRP!3cU)K6p)j=z*?J{G*~2Z);7$$sy;uUEw>h0^ zU~k+mvr~JDB(@^ayKQ9j3|_-ByvsCErO|P8 z^Y=giJ(%C@g&P^k|8TxVw!$TJYAwXX!XD=eMuCm$ES_*tk+Dan+DUE$EHsW776F+kUT zn!g8XK6lOS!HV>QOZXSH{$n~=T6?)PTt2|e3Sjc{d8-7}7VXgBZ~qr(#o&@o8Y-i; zobLm-d#({JvT8$Uu2twSPF;+G!D+QcorJIC;+kCd$EJ^{c8#e1;so&uj#*7r|egqzP13y!`05F=i#Vnoqnv;+M+%QOz8Vn_UoXORt>CTVp8Nw$q6pF*da6NUN=d6v|Mg7Yha@G;X9J0Ci*H|?2c-sul>W?dLs=olD6T-V4tC;z6 zgO<2et&Za#oJ-)sh4&n^JZs$xe!kg&orh+pR{z6#N$ZVDep{3OWma74qFR|Z)td(A zrs$Zf>Y$}M!Z=AbqKhY6hoJ{@mom$u3r&xe7ro1y!6*pU`8-$#YD z#4UibIaEI0P+~u;*ve@u45Ob`*{As45D8p<+Dyiz#58F=wi%MfRGAr~2qqIk09arD z(;0t5t4UBIr~C1qtCfu# zRN96s+}D6UtM};lEp++gIviqt(fw>R(EapjG^;wjlx9_E^_Fqefiw#ws{D!H<6}Ce zM%xe2X`FmCQL&au4b3}y8>D$6ujp?%9d@@%%>uCZ2;bLgMEnToF3MwA;XnRisnVr7 z5bh{9oe!bUYHQKy{798(F(=ln3spx^461@)^GbZV^H*d)Wsc0g93Ggbo&MHsr{fpZ za}D4_o~VVCwivHY9P=O)>bhp*U%cwg-CPW;)$F*ZmI{=hZXaqB&XNaHT1p8Bb`Zf- zTc3xpm-Zc=TL*4XC73EXX+)1AjGRl&#K+FRyWVf|4Al%Ut;BQIRL(5p=Aoy5w)sZ& zn+}Jh72lrdsr?y>golC_}+|o9>yN7IA`M5t&vovak)^!*#MTcR0CRaIiIZd z41PIJEbMY1P%d+8O>LQq)jqIO=XUg%Vwf0+FyaXMsNP-?XQo1@|4H@f=9}Pcl0)M; z9{Q;z8JPv3!fM4+&VAoPj0x*I|I`GJam{Llo^JD|+^nw8mE4u>I>03>Cx2oh|3GPf z2466@_N4reEMfVQVGC242=SV^4J5ytvR|c3YMXg2F6YZhW9@;A2rFUo2F_6<3~nnXE75SXxJdxZSxs zK2<%k0*u>uM|{T)g;t`#uVeJ@J#Jhlw{BWgKUFY#dzq~o!pYi@XwFi-g?=@XK(sO4 z{K%DPMm=7-Q*E-%hUD?_VCjrY^UNTo{!W0X!?MF_}KI-<_NvedU=5XFUf zicwW!$h{Z&WqSTqj2mOFa=}J%q_=utvoPe&`Isn_mmfs{Z zOT;-B(BVxoE`#)0idRX^))L(hiy8O-h^lHyecs??X(a?q9kJ&mO8p zjouVrG7+dac@>Z4clS)W$oPpaW;@UAyz-(bjU2t-cl**<=gRe?3S!^<*z;4ZZ7gRw z4*t-tCDl;TmrISZQV5ZZYQC);-hldFW|HS`$C{P+It0st_rW$>43Nl1 z1Q2`FB#Kfi#Sd#a`V9ijmalj!6r%40jB-Fmjk1!L7nJ!%>&XTecO(SVY*D-dAIq3# zl}{UjG^}rXo-yAE<)N>YUFoGu1m@NqC%PX=d#T5pPXW;ZAuvIFzN7 z+w(RCtVXcGQ^)`AD^3i@sgflDnXRm{;NE#ftEkPxjmIN8qU&`goA;0_-EtEp0uU)} z-xT$+UH%p@i->1^gX=iXKzZcNdw%jx(JO+=P=8DH)l#MgQb-8bd^-_|54C+b%HRJu zo;?QYLWDL}OZklTX^_o5m2L?C{^Q=QxRTn6j@K%&EHNQvt|}!y5g|W7>tw+FGwdZwso+E!?5i?@MF^J%Yd1I}8pL<`Tl0n1MhwVzn4?R(1RO3vqY z%DE0i&dSA%HUxStD!Lx5Fyd&R$tQx=z|m%PM0<&)ltKu$g+s{I1Kwh9&C_+~P5#cb z*@3uO_nOk;;6+)VnFvbFm?feUfxx|6$VKZUo;fk^;nAw0ahFGx57)9kxY~@6ET}qO ziM(gkuhgsq6TDO|{wT)DrwCM^W}0&Bb^uj3)AN5)2NONFq1<5VQv|0H`ojAINLI}Lck4P>^9Rhr?itF~kt0E616RL%<^ z#AljrcRYi|TeB%6$(MzDa)GEt5(s42v?4gTCOEj!ZrR#FoM#xA12sqc6=$0&DLPeW0P1l}PTk19gEPGTGl<3I`T$GLQ63-%AlNp(qT zQ0GZ2%ZGL;OrTqpPc7Z2>A9s-gVnXYBQ17Ba*M?@Z-@yz9Ay;~?X%)?B_p=pC>nwV z@NjRjVh4Q#AkTm}$8( zAd+RW-o$_$Vap7=m{vRt`~LVWBwIK>O3QG6svPx7Hq0<)Tg(t@rnMvX7Daa~^ki_+ zCA=!Z&Med!^*O*z-R(X#T)H{>Vq>+L&+J*BXuE-#qFPbH!~wYEQ)ZO3-fX+R{auZ4 zA3FaTXmMstm9t&fQsOHtSqWlvAvlbMRS$NrHVUMiJ6NU$goy=$w@!mG)M!jQ=(>m2%>a_NHf|+|HS25sChOZxWJ4a`eeOj)c=ugCI+$2yy3hlDa@u~W7~q7Apnrc_Tec(> zD;~=O>KX0*ml~mMi%_1mQ)ua!nS8&DjQL)8ZkUA1&j6cM>mhNRlYirpa#28XF@Qd; zjQ4v^)0uZX0tW+gqb=s$^_|ym(56>B#>?&6@LTGqQaGK&ypkH<}aNZ9vxt>MVqzc~8s8N>uVJ0@Vd!FmUk}Hni zQ9FALa3VNQQ4L2&_ol4XGUo`#o6MpN)%az+oAr%nL##L`fX%v9(G-wJPPpM?C*n)q zncF<8Yhv})qLha*jdP8wVq$qUdT&U5sT)(4U(9)X(=!I(vn*Lk_W`rJIBKIg_bf1| z9dI=CpX#PAdLbG=tE(6f%>UMtp>)T`zEZqmy31B(f4zQOiX6BsHWgvl-b)= zas$#KAah$9lU!7U;)bzTV??=?xnHKIVHj5dwzUMI1}h9h3@ z_}zk}sxY|pKh=H@rnhgDuNY66-1I`W@HxCUyFr(9hPOJR8$=YV@9#~|8;ww)*Yh2o zHi;drY-VI$a3`89R6(pS8h!Pd*-#GbnVe+TWL4~D)KS_h>Uw)zykL9mSo{H*%fS;5 ztW8b~{p)8)o5}us%0sj1a*1Y(9$?~wahSQ_w|swtuUm!RHWA(?D}9@Sn6vktdb~;S z36z;e9dl@**-${6L`A4W@AwB*i2qYr4vlM(%MR);JZo5ofBvMu_0BtV?Ndb5B-KfD zBhOZgFE;5LnU7xbOR~1J2S;sYwE3RfN3;qNi033rRUzuL_(RYU0jv`;W@ZsPeIxi&znOW6Aots=Yf=Sn82slsiEw`a*Z-v}z2pXbHqle$y6u74w7h zTWnB_1odzbgT_{YmKNU40PZls$~H2JaFgMHB|i+d1_JPr(zL$DFPjNlUK`}^Mz2F$%0(Y*$&a?^vp z@DI#!!)i5#o*XR}z%v`S1~Nyi4Ut+N*qbOSa#2dJBMq8f5yPg)rfLZ-uB~<+L?PP| zkLWiHGSr^W$0OT3iS`G%oBU6M9HB@271i|C=YR3G{1#v=MA7_3gQx%gDGtNS7aL}- z7^neYn|8$`Y5D(F`lBQCxNs*-YSV3ToO3rPwRa!tMFCN0Pp&jd0o4-(!MFW6qLw)9 z>Svi5F(-zqv!<&t$hz;k_{X&C6X^^LXA&Os>h~x}V?i9;j)8g18Uhv-(-y*`$}ayV zY)YvN#6(-M{*1qB{xR(z+}8AUn5;)i>#e z)e9(nZrf*1ny)`8Zmt^bE^#J+@jA8|KG+xgT?6E<0b8@NB~)MUKE%Elr~5xtki|Mr zufgNQsi~i3lrb>Aw198}|D^!V;KA5IFN_aRSM_=jmtV$g%98Q(iN4u!L6D9+Y(AmK zYVv$=aoAHTSX)kv^%E~h&@Fj20Z&Jr8TsvBo|5IHCI8`E^iYjwYmUvksJy<(hE;2R%wIR*_bs`} z_!u@^Tv|DYdCLAjU6{~&YX1Ey+DbZsxo{-8X$#n4<^*bs=|}6zIlu^`B345ykOWyK zdvv#}p`^$~Ix_SrZ^F?PA_+&8N1kDN;ib)k`u6NfMP|AQRi_b%2{~Dl6N@To=7s5L z+0PIePdU}AppZxC2HWI3*A*=wKky;SP<`Pg+{%eiyT`;VCr7;bd6k}>)tlZ#MN27g z#a;m;z9+%9NWr&V!_QM*O+jSHHV4a@_(vg(FWIYT@T-T1D_KROw&>Nh%e6=2cggD) zgHOWVept~YQcK2w#FTOA=~*p@={ev&Q$1K?a?w9VN3ZS++G)^m`7ZKBS7!I8qIE?G zY~=M~h#S?dr&Kz-9c#L>N6Hsc&N@4NUtI$jhg1lKw6gCPDlLmX5_|@2vQHV76tx~V zsK}%=@gvDlB-YhcH3GYl^@F7YC*+z*G*(a!w4y=N0$)bhiqK8zEEerPGvRxY+)_B=;EL_Z$z}Azx#?8n%wpnIDG~;f5}M&z)7jrvWF~(ABx%hAT?a z^wpo|P4{Dvb9}ACLZG1M8^eKUJ+B$@%-y>R_R8Z;gJOwohW!6{6t?XY; z0#;8=EAHFW8DfPUjTl#zpQ$i@IpEqCiCnVwEb6qP#DrL(m^f>EFs}{19>qdoQb5zD z(uBGPP$1wLIQHt^{f34cgE3!5EsC(ie%e)ISkJyvpE%4s3_F_4y6buHvdrLTu6vPX zb{w}GHQHUD_CQab6Z3#`uQEM>w+m6883#2p5VkOiy;2U#qEO6&A5>+(P4{%-HM++9 zbIg;OFH%7VRBTy9^XZbcKG5LGNVUmJ*U>)Jivuw>z0psOt*1ovF@SJ+UKZA_r|ia} zP8!V~!peZ5H3m8}D;3A1up>bx;rWY7YqBdaaSGm}M()PDXA;1-+eRraH#%nQTKp&T zEQMF`dk4%$c*Ng-#x2G*IQ;OMiY9CECkP(Onfu76Fl%h4Jk+4=-6C>(jw|n8nF@sg z3@n|;TM)?j1X0;C&lqFm=S>>%#(y-Sz6HEL23n-F3pgh6;bCPx(UkMk(IFZs>FjYc z%!1+%XORgOzX|&G(fdteY|yCr2~oOX^>Xw?gS7gPoK{qe$|C9ECmwG29gP6SG+cuv z5_d!R#8VC4`CG&Am+4IvOQavehMw6-5aLm~b?b<4sSYxvvMO z4RA5rIjaeCEc!%k`&sCKm7t?eib_pN@+eD%0(=Y?Uv_DWVK^JYZ<&*Out+F@I83Y{H&CKPu zeN1BS&109~x;5MZT+@nsiI@`~3nzIsSjHt_OfcupfM*EEr;?5F+v*zz+2}$=k)U3R z(RHUvt#SLU5`Ly0@g^91RwsdiYjdCtx9?yDl9MqiQdc4Pn6$H+XOc~{laEF&N6~(s z)2O}ZlKR8@5c;gK>S{F|i#cvXI~VnImjz1SrSmS1r&TmxkMG^SDqaph70J=Dd+-P0?1Kd5<^skAEA*}3z z`NgF+ZXBXL5qkEJkX0zd)W#h|eG~#dce`NG&hj*&KEvCWA;cu2RpbNoI~dB@6%ymXv&zNL>VqYzO(xN%Yv zejMj&hiJ4?Lv3yY!D@^HZ$l{8xp~e@Z0^SsNTk{$qBq#B>dbdn=ew0GHGI3H>5F)8 zn3f65@Y3CW6cx#%7EjK2jET{AMm7m%8olfX%^vSMR9hFyhiz-mNrcAVP&KjHo5wMK zZLN`jZf>e#&X}FG#gErKh6l&Gk@eJ&YS3O9bQHn(;j#GK)2iyZ=8FcOnsyIJmFudSAgN9} z3SmBbEUc>FoF=?()qm=HBK>CVnW?pSFCQWoU77EA z$Hs<`9EkgB)EyLf_&7NK2Lo$8Zo<{`D1&PN1MOu#O)AZ6gKnarYrwMcHK0;@WHEKI z;c)))%aGZ}ozxlJ7q@mJ+CvUK!`{z)%@6o9chtA?-?-q$M+OI?ja514-A^a*JqoVO zW5*7Ep&H4)e-Wp3`pKH}Q~g|`sTY8?mGT;MVzfID$$KxPhlRT<6mir$h{k2sQ65E@#3vRW1?g@#Fz{wzEhj4aUlp)#28?&s(b7xZ@e zdxwW?_9~LbbgM>B&kRCG2&&Iwnz%O}psF*#sLX9}tl(MJ8`EJHC$3GZ!g?A1gSm3$ zIvrq;b5F#90&Jz|odFBmJPb9huDOsoJayNsmD!YoH#TNdYKbi?VWB`cHCHo=wK?Bz z%5mZ7C(X0bNPC1)Ydn!PgR{uJLPRN3HCTrQ4U`GVgz@Ou82G**9NxNnGbW%qRJhL= zsUh`Qo@CQE>Z^ZMObaw$E$3U2;s)Q=yjtN&6w;?$ll>FSO@?vg{a3&({|^9wJ-5NJ z<$Q5o<6bD6lw!m+pj`X%z4A%0o$e!E%(>hhb3*AO#vpKxJ1bruZ-x8 zzc{rX776^WkK(}`+NE9#=iV1kxp?*6lsl~3I>Dr;qDFpEeVZQX+CTv$TkDXV)IW+o9lV%cs=#~(ynnwf7m zmjl1gpNXDwEbjxA+sf#>ZEPuCSi3#*MLa(J$-&r#y0Pc~pu$m^^+*peoe`7N63W?RM8JRi;|lVjkz6d~QW7 zt{)`864)DEsj3*0ei|}9W>$%C@8X#S<&*@$0u}xiaKS`z{|)dUOn7sJuU{Zldqeuc zL)MxnFoImv7QB@_W+V6uW={n9ojG5HddlAK9`t*FiRo%alePh6D}ujBhWBJ!6ey;n zi*oc~yRyQvcsagECz@9)a|K^9sDnxc`(2H1cSC_mW_+Pfwsp`!m+Lr+XSyKVaNt%kY)CoWC1-80!#s@}Z!9l=iDgQb+x5{ngwnEW;6r8c&3AcE{HP zTaF`ftk}zZ>DYIvfS#*ShGG5xYzJV+f#%0UmxPSd+kQ${D_63iiL^KS>i?q|1o|89 zH#yHW!@w>=VQ=y3`qVCN{x507Qnnqul39DnOs(JrX&F+cN>6Q@epWI5+Eop#>m-5&&vSAfT<7G%%fNcEnH}oxq zz|fW0iNlTjKXW$?KmRx2rqEZa&)QYxnW9g|I;g9cY#*cYa-h0;@Meb7J~k<>^0f)! z4OA2p z@Dyt@s;}NJhcM||pJ-3VuxEiK22Cud1fwrH#B=z01*ksvjc%r1t&NnAn*0yQXfA)VD`rPKh)n&iRcPY(52*o?7fSY4$13qs>X0RqAx~ zJYb31jbWxeBT8R3$bIv=%hFKZi=~S-eA5zGRbN>#NynlCgyJ%p!8q(e zbr#ae8i=Lw_64>17cUd_HlIX*_Eo}@Msu|iaDzET}IZF$J`u_X6^$%Ck$@F^K3QDnd&i%qQnG&kNjaP1o z0`%R}ls?=VT~@#{SSE5Ds*YBGSI{w<_eKGMCI0^Jlpxi-C$&}UH$@Kf2LXQuegbl^ zRq&H2ZBO~vWw&>6`uIxAN}IlC^~Q4&e>m7Tu)oP_H%_G?mxYAzsO9$lqIVx4)y(cO zytWw^Zab@h`%NOBI<`i#42-QB{9DZH$*Or$g!jG&z-EqbM4=Me2b7A zEj-I;6eM@HQuE19*8U!mUSAcWNEsl>X5!b|g^kRH68E~PCS7?EP4&GkPe6FwjNH20 z8c2XNA0<#N;Usz@T$!r*dS9Gq!W6$H*Dus$JZ8C9xC&9CfdC2T^&}`^q)UGX1ByCiYc`mbGSg>d{^mh;a?v&jRJHsxRc2@_0Ifg|_lwUj?aj$tFA8i_AToJ=i;hQLF z4S5g&Q8)PP-$-{3coZgxnQS*Cn{m9U-s6T4nIu>SST_)9j=wNRlxj695vlqI#}Z|M zBg)jPpb&*dtX+_ZqOA}WbDGL%{x)x3I|HZ&Io{BrTx&*kq7&DwFXLa51g6TSoSSu0 zaB#(gbcQ2`fTS4s5L7D`Kx69FJhU}J_NAiWGpnOkSFzL!ynV*RNBQ=B{skezlt_xS zdXt6t&w~pT0#$mD&fqWE(#`G1P>Ogh--T#!d2<0Ll||Mve$*K4$9kEq+5<`95rT-f zW7?5#l2q2&g60@FHWVxs4omI}&%7yh!$Y;@@>uJ}uqXPi>&8W`5T5g?P6$rRRKKYb zoMcDksM`p-Yd6o$0(asryB8$mgy;^XhB@d_`p2>XXx%UG=DJ8L{Sz{G%akd?Te7f* zf!lmQ6BsXrGe}vEkMSK9Hk-_TeVFhtJ`ZnCnrwirn>;}~)J~tTvVIfeh%>=JiLL3{ zI|H9AXQz;f9hHQZ=^%{H`-YY=i%O9qNMSxGYBZCHXo0MGaV~sX zhOu$iwO-!%^+$vMO1LkuQ%*8V9VY^PY!`LqCTA%;X6Zpp*053Py+#t1j zZIpC==@|)O0GgtJV-mf>_v2Mk;22OQl1A7iWigdstDR1QqL0;(5ytU8kOu>)@Y41J zQkcwL16rO8Yd9){<|7xeI(K3OKJ0szYm0vs$qM-|&a_(l= zRo}~Vz;8BIqY9^X+7}haBWiXN4@xgW0!0#_{x71-O(cbanPkib0yxT4_^qTOEWrp6 z6LebPIDGI#k@Bmn#AE^OYwF?lA2h1RmK_rWf~b6p4JYBwhXc2I(>EWzi&=YaXYH#T za*5myFffEgc3a+sEV;_hU683)A-@~&yPXdlxjb$im(?$?r`+xu zrH&SWyBSX7`)-^EFA5_22TD8)9+ta8T?8-xANJk@DvE7e8*NO83P_eL&`pvC$r)5) zlW8&yA~`k)NRs51oS|uQQgW8qBmpI7kW7;#OBPTv_}cT|V>n0m+2_80+;{IAx5j{` zx)!UdR@GcpbI$MkX76TJeO;B2^9I;r(d8HCf|Fa3FYDuUkn)%+eAU<$pXr3xc;n)? z(ZfO~biEt<^;lnvFQTSkKdSUrL3_tPW1)BV8yiQPvUJT9#a+1}bnw2NC6pz=gk^L<4j4{g96P z8U2Q(K`yFtI$$u$o3;{9@wUC2c`RxGl5hH{YP@jOX4U1hc(IQpbr?j+2nOb$@R|F*B`^2w ztzc>WsCOMvvgd-25_1@U*<&L0pUbjDxqyb(KV{9T!Bw43dFUV6@?2GhNt=6~5;|!J=c@$v41J81E8QnGm?vpWetYoj`{C8@ z)+`=T?{B$rF^*lP(m2BF>?ogVIvQ>=D!V>38)(<;J3PKe*iRL|>L9Z~&lRobCsdE| z{R4-yXeWey*twlt#QH(KTm|t623U*F1>+rL;%H%Hg`5OOG>A@i+TH|B?POsS8W{3E z$NBBJj(4Euxg++>#geiwfSX09%lNPHHbEHBg|Hsx2nY<{(geTdj(l2>T%wth=hYk} zG$`n34ec`QdfZ2zx+_c_%@^1i!$PT8pY84pdQojTHlisgG;O19>RjV4X~$pL8GBhE z4Q{3wH!oY5HcdaCK5b_eQwkfl{WjXvesPC>b}M&1-#b3k5=mo->062BK6deIA4;Z( zzUXIfXi4~-$Z5x9++K!GbrL*^D7USXEK! z=okS%E+lzee@U`te~#g4Gka3z1T!_O#)uYa+y=@&!kU+8YpWlBr!1)-{n@8ZM7*ub zPTYEwmKmR@@>5bq^s-Hv>h|YFPyLCxb3Qeo0kBao{S$Bk3v8+ zn8=XK$aE_0f+>GmE_8iO*-6 zEFVzNyg!2}T>DqxI*dpngK@#F%TL;^hoX8LKLDXZ-yQ(yd;=XldrL_q|nR~h-P4}Yq11oc886dnZXkXKa+f}9Jz@u&i{jg zSemGIhyX1Qf6@2=ilC=IdxK}dCg6Q~#c-c%VOk=VO8ord-4k;At&C~_9_@eG7Hvbv zVs=fmms4FJ>?P=AIJZUW3qW?+^2aCcgGob0s87l1_AITZhcDnZ1@ z$GdEN(LnpWGD-(f@P2RMLVcg7*AS!k?DZX8+@%7v?o4a4}3&9*Gw~NQp z)1TH9u2u2(&IULHZZ?Op#Od3l)4|C*B2gK&2ZUy56=|e>FgLh1_?+Z=WkZCaeolmN z4#FY(E~+dXeLGb@sh9|d%dd5F!vT?5VoT{;v@qG4C_609{!z3oMgspG zT%2Or#-)_cY)-=CJ6W_wr$iEVwVgu!#H*-s5c9}Kf4^|t>P4HWGBa+fC}K#xl1j27 zYoG=M1++yXPGw98?7a9xn5rB2=e9N}GC$DPczJrh=3teJ-1z9)&2?j4ij%62HEjYl z8nbv>Lj|Q+AZtjUtbL9V9|@VKr;{X~7I~ZhWawJ%n-7`tUJbE=(&3AhS;{QJ3j2kF z$jttiToEKw$SiG?VjQ{U^&!-_yqC5{Lo!-mK@X9fgL)YENU&L zpCZR@NSBP0x+P zrL>Dm$dlOt0rl3nj{*aGR5J(Otr9DRN7p!Pzv@KzF}#ee{mA{=b3151ZBlZ7u6^b( z&GM{5*;f2ljL98$Zprh5WbwtiiiJ-r!>9HK4NPcnIMHMPt^2%Z&OQbPefBG+=0qaX z!Rt9pmhFH4?eFB(pNHf)cb!eILb#4O06!$Wx<>{&Sh=B#$jUQ|=7~G5ARr(=mFe%} zkw}6>shQZYONsOeBx)|?s#(F?M)n8?%0Nu#nG%_zVT8{kWYSgBoeH))@}Kmjte*pZ ze@Ed0zlI!t>s4dG?k(2%%N_>njKT*koK?5N3{U$*&P=}A17PbH7RB}S2ZNcn&udKj zJUeRRvK?p?4#4?=+mE^DzvJtl{S*W>`pK36UuTGaTrg>!ZwB)m)BL+F0e()&{=9Y} zpXoR-i-Wm+Tr~j|?~IS|vgKqAMQCjUtIN*1IPVdiJviNXaa`Mds%^{{xBGf*iP%yc zeB`C{2yi3&CgvdUMPVO9JMTC((BRYXs-HfUmK)_dFdESo*w7*)o?6f^9allj3BhuR ztCFXQdvzDbSSH*G{^sU8<=qQx>itKY5*tiLc6H5atrbWW*x8 z46%u>Y>7|J0Oj^WpY4u#Uu8ot@$$ud%-^;C;B;ijf33)wM6o*tPlR9F@Q^Dsj(Ul5 z74mZa@w*D7pN(VMqF-@hT|!s0(b!`36pmDyWL{OcIw|jN9l_cW$G%9s8mUrAI-gdl zNfxMCjyRfL4gr^TQB5tuTJ(F|)T?dHl$=;gDPUt*!>1~DDp$a01VWct40QH*M2-ZL zDn;duPv${l`#k4Wj=56|<_HMa`0&|nsg)mV9_oiUSkHI5#b}|&8%a8B5mOA%$VFq2 zK+2$fB~1&e$X`*K&*rl$;Of^QJho@x+dEvnpFs3!x3`Jopo%uErs}{xWC9Z?3|2`r zO8IE$sVIKxY81=!p<&hY3qW=6yv#?uroHQIKJPT5Lir*rup{|wi`84U?<&k4zn7rLK!1Kj_G`*3UeQr1A zty4PNKo=~KY-P86=JGj-`egoA#AI#TRz?90{oh_V7!g-8*e#*9K{~f$~YwYyzg-@nmth2Qd99Dk8r9x zU!y({Nu$fVlfahpb_i1F;Mkq1rV|qYf9HkPZAcw9vw@mm_^zF<2@)X_wUwaD4bi$9 z6|w2!2&T)^qyZO9bve7SjkL`ru<{hF?gVI$@G}aC~fnbmd zLTC)=8U!Q@iOpbnmm=7+)LGJ);n|5;&}`Am-RCkE@n2u8VI)VPlI0R%b|Ukg7F?>^ zHr!nF^s~R_iWJ>RRnUO^_NoYe<&p?uF3aAtV`1bl?G@u`j95}_!1pWo^}6Ch^x9Zv zuaFH7eF3!lvlxQ{=lMut@eB+^Z!ITblf)Y3$y@p(GxNN0C8Kqb7Sco05F3c2hZ%3i z!!WX2RiJINuB+AUum1!eGw|>$k@HWS3~2^!Z|?ao03A+@ZG~0;@Z`CR{TTC>zo=m6 zvk6a525&@Pt#Lu|PRQ5bKv;lrGc1b9#1ID2v`ev-Tz)na3+AEcI9w86G5IjOrw=VX3P*`)h z)sWQHsLJuIz7|uylj`O^ozNndLmgLT*R*hpb(dSrLCu!nbb%kDG84b1Rzsxg9mRQt z&(V45`P$Br1MaA46QsqB#k7*LH?h-_m%%kMi{Orn#U~VhsPUnvgt|bh!qX2o@~q`= zFd5nvNmmx0JF&>A<_^0nd`;1{D_||uf_Xc3MXVHOAbuuc+HZ!0@kUz^Ext*bw%sK> z86oVZ9gI^DV9l`P^5AJY=ey1)DXv-YUQJ;c@$aQZ&Vr;CVl)sR?gxjfooR@Iv zPRPUrZkC?a#0Gt_9d8a@0iGR=Y_;Fd+SQ3R$%-PQ2y0%TJk7LYat(0} z_*!)VVDg!N&#UgZ_v^&0+OnYL&A;AQIKDl5vz6-4ZkqoWo+-B2iC>@pptT|`cJ+jo ztz%tHu)mX&1ydas1g^vaP38vjmNIkm*iojDy;-5g!xh~sI=lF!w$b;VwjcGwKI^8KA5o&PsTh3%(WX5>h#)MPY^o9!Cc)AYyaGS^U4Vc z={Z%XbKchHW#^-B8=Akjwg<4O!wwxt|7W$Zy#vnQN~dp1>|#4Ch65YP7i-at4hS=8 zu~NR)z!z4+_H$7@BUvIfgae52IBLTWop3XQ!D-JX?!z7wSLo43wVg&q5dJFzXA-J$lL zQREhGWx&k+L{e$fiU^DV{;H%q80r)UO&pT7d$}Qh4bd8_mpz4sZUjbtV$F)}4ZaVO zMecY#5A|V?FSv~z%X9r1gRWDPva*1(;RqM!F~q?RDA$%2yAj8l-r;+qLR$5Zx3-E( z!oLbp>48lJj7lIAFFgDn-T8MSwvs18r)TYAxu`Dy3c!n?A7BBm?jNmhd;#3(&+^Dw zI+9rgVCTEN$ISmR{pF|<$%QWGN;P3`?`uKA#as5Qkyi9ijv`#6>yAVwy&s`c{Ng;#2B7L`2O%(k`93fz0563E(+vh-GTtgO;M#n z^C+4mLvurOUc0x5I);+@J~>*`5BZ_#ygV_tJJZ$7tbpVgC9#uN?_UM0Xt!!Iq?l=c z=F-VCYm04gWQj0GFY6j{r8+56`I474gKJuaCIn(TkOnm9)PYi)UPgpf)=X}1SF-m- zXG9uJeaVY}N1vb52=l+}@#5~I%ghWzbxc5_a`zLF6&vHvp}Eb%K94>Nd20xzCrVZ& zq&b>9&u4*IRmFvj%Vr=HP{b4yjVdrSv=b<4mVq`;CvB^(>zdX$005{lX>+Voi@N2J zGYQLcBS2aZCv(aWc-2;ES<4;}*S}^=jz@IsW83cvm2|M0} z@tEg8+msP+NYV|%=F`~S8=fO+ALQWkEHW4@Bg5Zb55C*s|M?3mdU&??czd1itLG|1 z6sB;f?gS?j8jChQ$vSp{r{`k$9&+m5pNJp-S!V7>Aqq_N>bpk85Ae zkcItL>eY<`>vR_OCfh4 zqgcHY_inS)>nds;5Dv$nnMccmCr^s~b=Tkc0g2}q=*`=fX%x0k*Q#cG!g@p!W{*bf z>Tv^pg*|JmT==w%kyRwtJ}j0N|JE3rGTx25aW8-NDJ7ojlauyB_rmedKHqU@o33Yq z4aSC25y6Ybhf~d+7-Cn7=E;D4O+d8p7l2UF!ON2GP_-ZMabpofNz4B~-vhF=+xtPdwlCWu?BL4kD%^4&f`qaOLMVp+k}=s|QpK?Rs;n3loKUH@pcvj@ zQcpcx?Q_-zNx;uklUG<>Dn6vhoud#Kx%ne<@@p^@qiOngRApJySMbH3giZx&QUyZm zh%4KWN9`KETIBFuaCUdLLCE6W z*pp@QBjQz%ERZ>C?C zdy>P9_C6eo^8QS&;{no&lyxZ1z#36_$a2T28%BMnWAU}+(Fu*Pie@zTVUSWDFnVQ5 z#}UA?+js`cbwa8-7n8Ei!E`yy-)_Y4Wwu6Ct!OpF+NAnY+E9CJsCO+e$}uowtJOO- zwNR^0K6$m`+N~HfE&-v61b|Km3ySUZLZ>foAqS&C&pz00spq0huL$3{1t+vs*$ zpL}wrz!<;a-KiMT<|%IUnx^|j#mB1?dgFG=ZYgcIHQS+bQpgo`*$9G^*KG^Mc~_NY zjCkRqzS71inXbeetp4g#u|{lMJqX>+UGhc5sZCZCq0asDEPuo#F4-Cyk|vnjgInmj zGuLhf#3)<)-;70DRv=V-32cCwA@PBMD1w{k&oHL*12XB~2?d2-$z)!f85n8&IG-0( zPW&!*IB#^gNyW@StjWy`<7piEpDKU8zE|e6w$8C*7>YLN@^(6sqEPPkGKh``Tx7x2 zp#xlCa%?g}1?axFqz>cNB{H^}7RJk=3oqby>S)_F5L(QfQh+Ze=~>0f)c48qY^d}? zKX!(|gO_Ulcn{wvZhm{_LFM#B=S;|ms1}KkV z@V3ZSIFZBRy_|0S7fv6}@;Ys(U$qs@>$Oy{E*sbS$wc~h+HMnNBMIyoR)*Jq@O(Yehy zT6zttd`Mhwh*0bGcDiT-jd7|}|Hv`n-(G-flvRvtX?M4c6Lv)K=a*)?(q#sBLWN@h?G_a#% zKPtxfqtg4*6P{JAarCwkx7vWvmAZ9>cT#eBTK^!}`P}7l#6*r^Gc+-fV9^DPL&Yrdt`8W6&8kemvK0 z%Dz}xBp~aSJy?O5)Ey`a=!?l?stE1CTD|=rQ{&eUEY4Az=f_VAcmgh^o{B`=ot4;E ztx|mAB#U8@q?&jW?vrz2q|is*uuBI0o(aZ}o!5L8?b-R1*CTu(j=qlaL6v4}N-0xF zjF}I(4*Hg|ltO%!D$6nIMXRr)eW7gxe*1c^()T4G>PvmXIs++PO{L;@S_2|PSj z_QE5-9>RZi^7}asF0w*ZR^fbkMA*Vh-{GOf0*gtwV2%r}WDG4QGji|-YJ*|iv;wtB zY{}`6VQKsRogxNehlVvWKEhu4jt|5B2AR86;DP~c;T0CUaeunF_bV*f->`PPrjDIa ztI6aRCK!V9yUL4T-=;F>y-ON*tM!AExW<(PtkjL&TELVY7zKJc94j6|g-FZun}_Vf z?a_=~(s?V0A|h?|&wSPiBt%x}jPhJiq*x#(GOQ$Rg6iV0ij0;+A*N<4z5&0!W9cry zAbPtVL0ZD83SlE3Va)|Y^lB4k(9ab^RnDdvTntT~6Qpd?hGT=O3Gf?5SN+>b;UJ*2 zbe3I0Lc|o_jbYWa&7RW(rnIR1{mUm+){FRO^@XX;pTtXb^1D01WD=qkFMYFKXq?TX~T^_rP+@lq9Gac3%cU&S(54%#s| z*g2Az%*zwD1Qzbp9O4~dGv=+0imLba)UjC973hKZE>IffXIl@^*ZS5D4qEe@u*J%Q zKoDRQFv2p0<5$-QH?!DHOvXB;pV8~tFClUi)0hJc|0~xoP4mZ~IDx;h3$ETBU|qoY zHCxuXw&0wb3%)T$*K|iMxFO_8vh?)ahY@LMk0!&O^5}-Z-DeqWBgk&K{7DRoLcZ%? zBw*Xu8kgNX?YQ($bTZ!TEz*E3_@nqn85pYG*gYJb&dOAfSRVi+4Jd4KmxL#nz?V+IZSZ~GG7dh4XJx!j4e zN)i?#Kfno+-Fi9I;sCZ;Hp~J$ZwW(Uk0+hU3p?BPS@0=vWm;d#$S0o?*cOWDd|;;7 z%tX~lk&>w;Nu=2$M!B6dC>U%-pf7}!5`iEK$crQ~x+aXwECVoVgZD+uYXVW+fe<)r zCy^cRlXj|-#8B6y=bv&EYeE%EV}OHsj8(YNf<;?-ws}@OcU+@OP?MJ<9jwuGz-UYk z94Lu_tVJPy;D-|G#hoQqt*_a=HVz(mn{$$}a?EFucD#G`$M58M2v#V+B0atX)_w@3Er5i z#L5n$+6^>eos6D`Mb3nSmD%t+Br??O?Ksk%sJYNp>1R z+{Ye2H+gK+Q6N;Bn%+;PUqfs`AD3~Ix8x^3{b#TJ`QX2J!{io< z&=DFVh-|fSl8vHK5bQLiMXt&qht?i=Z&SdyI5ImdpDBgghFe*`9hJ6M3klg_$Vr>K zDmu1{j_)JmN{j~IsP#F!_MitDeS9OZ8A)}${V86V`6%ga8o!2HG@dt{A(&UN@Liuj zT(|F!DjdJ1Upatz_#H>V zk(gVRzSbBa^D8C+MncNG)bYENp3?I}l`PzhRT%A!gj^J9Z+aVmYn4KJIzI+N!EE6S z8D0M^o`8YV{x1>BiO!7s*g2odYHyLX3MQHkDrHbhgxMFB9KDotFz!H0+b^JEgjQCk z<*v^OAVhmsOV;UR%Sw^M7Nr=aum()t%CaIVF*b{q69Ya*mDodj*2q zrdJ;^E|3hlL8&}CK%QmEo7w-qo~4D@Z;t>WOkGJj8j4l>=I&E{!Il?BJj`0g&7h*O zxW0`%_97*5I!uz;tz91$&UWs^*7nuW{JLOp!$UJ`vza%x2Mg|F&hIZL+CbY5wDaSwh5~rla9iyVM zEL17fP0N$*$F%H6G&~mz#OEw2uR}9<1K;xZGgP!EM%j5y>pxBQ^^Y~lv$m9luRx~5 zc+%-rQS}AjBpD92xn@5jxAYy$N~h2&9dzuLDnBTIqBFo8H58mJc_P z`WQrInAu^a`qrwLnyBKlP-&{U-C%f6}gS@<`6+<&$#q_uE60zbsxqc#f02Q zlW3l_t;XjKEccB`w8NbejE6hTvXUrtk9x92)2tIL1H0TtcpPq;b>Bd(5U-oe!{}8# z+^yUnw3hQELh|FDX7IDxpW>qBJf7}1J*V~=z3RF#6To@cFW`?r@J%w9GD>Hlt)N5Y zMmTaAH@0wnhF8zj*McnxXUsf%QznCv6?u}UtKr>lK#c78<778U1Q$0jiXbtD@W|IHTw-#}+hXR~A$ zQeRQpz#n!mIxE=aRcGs#7KNA#^CO{*!_hq)dn@rlD^>D@Ge*OyVvhM~gM_M6Y!&Rb z4d#-Bx)9v?=4(dACaVU^Z0 zrnCyyFkoEOP>1f-ojwDH>c$W!FSFT?cuV%)q-&?Wjqe$5x@tpk>KjyzV8y0d>|av`$oHnR@QforG8=w&4s0wCyQ%kn-D!iqNCh+gf0XaQ2+b z2w)!YWJu8STQ9=j&wLHA{B=C2$6{7Ez6}!gw=<`{hwDd5f}q%ay~uir&GXQixRl&C zOCgMU`0jH!9%?<$rW3+$JkeMYVK#_);@J9gZJ(twjXGf@G0~Kq+g|`U8+lD)TU*=p z9-$O=UIktse+2FRR6Ij;0p~Bws8=M^_x3+65IC>q;%TE5B704F9Zk$?M!858+s%_w zSa@l6$9Cu{s>_!l&p2z5QUw!rLgXZ{!!s*}$rV< zuJL09Z83JEThmN$$a3Nnv?i3K!p<_|z`K;d%t5tHnsK{YWI{q#**KCftPv}F^1dU` zzvt_8`M2eNvpWBq)%jb#>c3r9N9WaflkyjU_{GyniCRH7ut$=Ri?IYS>4SX&E=Z;X znVll<8!et>nPE1YX*%?hRgpB7K%?Z)n6BpXI2cpUx69Kjhhhi&vODA{7!sh>E zi`3Sk;F4dguK6wJ^pP=`i;+$YUEfPRq!flCS)4=)VU!ct*SBPWDpElwb>Qd*BX(gb zQtm=~<>^PYRa*Lp#YndIrlXFWS+Y`p^dz44iE%@aKA|W*oD9q=Ut9SmyjRcYQ<8N4r*QoV)$ycCl)`Xpz>V>#4Zho2 z(} z3R<`1phH2A5t4HRxjlnYJup7<)OQs$2OmqFl$>0I@^@b)1SnY7{8n+6VCz=!zjkRE z#xJ-%8s@J|G*Wb(5y;~?l(&+LZs)M96dM@yXm4Y=(tPbqj!K87lk)oNsP&y2O?{2` zc7p7r6Ob}`g~8U>VYZsai^bs2FI&zf1BN{$n5rz{ZcURd3Djb!+RLI(bc3tITKz8j zN{2Q>rX>{VfJi#f9sy?Y1mI`>eGbKZ{zy^!cR(!C7oo%sb{1B%hxY0%6TucogE=rY zocjqLCpb$R$Ii4p{PW(q1}R8dEgkQW8)k#&YA3I{mkVL7uI_@NR!$~KpSehSkGzMavH?{+!7u3r{w;b;082MYp`76L|w7@<1 z7wt+)bF+ZsVNbjFj0?)ts1`0~lSxTf&D2npiSJOX2vG8)_vBdTdLq{Jz0 z+ELIsbV8ko(`o}CUyT{f0ZY%esBh~ol_fX#Xa~&WVC^T zmovw7WY1o13DVA4&=V6M>0mKrO6io8w6lyFRCp;eClLKWQOS`IwZSz_+Yg=t=YPy< zZ>L-3VTGMf85L|;j^$$DlzcDALQ&=tk+vQ8Y$MOA3T)7HX%J6lTO6|e~lI^^J>8EUxwP{^8sgaY?nP)&H^0+}s z2ElxRn#K!Uv6-0sN;(#)h&-82;!OMcti9fv{o$%pEA1+DSUQV#(7eJsrNhYU&ahkb z!`x1*%%<`Wrj_#eG=904v}$fBw?2*FvL3-(qut~ z9hj(@d2OPDCBR7$V)IB@%h*W0b^CT={{h4bG#0@T>eBL*HM)qRByU;Ij4WI8niVTz zK8R0;o)9&|%QO#D8LV$f6Lfwsbr?exiZ%1Dka3SXzqH#ivvRuF6vP+TGSs%tNh}NG zyzEDNzAP*m+iljQdpO2NP;KOoG4Ain&vFxceX*vs2{}lY`0i5*CyFId!cr{3#%+Zc zRwB7W94lRhEsXj}98r|YFU~0;$9%EPcSUbt+@GUrc8e49BZc=!w!+5rihHK495ZqZ z9LLyfi6JLURP@9wNO15NHo@z1EwhKVy4SY zFTym95hXOE_5_i~4s;C!+Sr>7LM2VCG>;PE&)&T6l_Ec%>hE`OJEC~?$e_--;-2Nf z!@!|Xlpo))Nr?qtn%i|T4o}#!`=Uw8B>uOQ(|45y%CEVd@UOX@XWVc4acxry?R%wq zQ*weN>_eKK+uoD6XWdAcpEbP_pP9uL24+jq$!k`Voq-O$j)8OQUuffo<&jg49gM_= zOJ#(%#4B;4So6*5x$bjST`SSz_ryiNsT>G@>q~RXO-w=)Q$ghO4|E^STW0)^N1Dpyt397(&lgi! zzz<-2 zX*Nt=bX9yFs}{(ru|C+%u6|1!${sI7Bl*jswl!V8_al;KH%!Pkc`62Ru5uiMfIM+& zu7A#%`>8<8?-YsR0a$^1&j5hCx>zm;}qvJ0&MW494m z!G&Aopl$479$ z-t4#5IPbBQcWD}wn{ngSD~6IhEkyQ)TGq!I`O3~`%8RwlAC3;ZBcoUgjc`Ti<0KO; z!IsIzAl3$UEd)s{_RKTjJ=vAcIsKshCusC3 zEtjXFwm$ShV(s#zpU`M27x5>)6tzsVEXF%s4?=^k0TW$Vuw#L20sqP5X4-L|=N8j0QR7Fq4g5!FOB(w-&`-^z!{ozuLyeKcZKZll z$QVlLb(3OaNCE}E?XE(@`QUST`k z8;xc~WbGZl<$8wsmthhOf5MesS>*pzorN2&95unIuc1aVQJ&RMG&*irkvKXZu}lvk zPul_*evad+D-P4haRvHEB`g;ggB*sr|R+*Sg>e5v_C}-iu71ECdfeYYwp3FGI*aa%1>1? zTQywFpAia{9?l5571;e=XzorKEyrkGwL2u!CKqG4|FD42uR=^ov>rR9DdeA+Mnk0p zNYxkDV4G+6Uxn(a=fMaQa9>%Mj%EN6$OS8&tR)uSn*j5>)c0B|xv8v~S_<(bHHs9( zhKlkMZZ0w;bnKGr5Kg#Z_hgSsri&-dQ*es*e0t~uYJ6xPp$9XZc%CT1+K}OdTL-Ej zdtO zhn*~kf!C^Ww4QjdoQADx9mE-*1`21)DKFZ|HNLK+UA%WK_kY&LrNtvLQNIDjE{D4K zN)6s#%E7Fz$B~n=hc!Ltj+lz3>|cLzbJ2;Yr;?CeZXschF@jA^BQ;TiI2o&q?i5!J zwid2>YM%$Z-TcJXuf~-ma==jhB=g?WUS_k)#&vvl&ClNvf!RXloji3o%*P{%?-a5M^5Gv2rGUefP#w#~t$K5C`HHPX$02UTE8lL$GF8GE`>Q2?yJdcB z8_!rX{F2n<8`?6*-b@Js7YKFa`i(SE4<#~v#FJbnC#sgw!Y>#E@>*LDlacDtAv-|q zM<-?$-n1Q*_99*P)#MxvzW{pthojFH&F)X~y5iHVkvDW`y)iA9C&SBzvG0EWQrPSj z)}p8c|HQS!%ZaYB`ciS;7yf&0RW>j%Md;iMqsVUKaUC*Nh@v-PG6u1j4%b2;LdZNh zXe45pfu4bz8~nTkz@DByp*dMpw#EvQe)e0)adpK{>Ghw(6c_RtCak)3oWMogfou9L zB?+7xWlN~#;qEeeiGh60v>;hv9$_lSfNG>Wbgje6$wdY9NPCjGeXi6rX0{8Xi>JMw z6qN92UOjzAJT%d=LSQeSBPd&s3+;aOe3R>qktK?jh}^18Dzi)tQCI|wA`W5c%N;$< zadxx=o0*r?AX0{NjG&VDte*YD{PI^;e;bX#)c-MaqUm$eWS~2qxE4V#8UGlH(b7nd zV#y}PXf)0MtIZ4`H-~-_CAa95OT?LJ1U>1^7vy#1%z0*bU36uKH35%SC?%QzU#R?y zRkvd}<5;1m`I|H*^+%eoGLoIcVeP-t%XE3e7hNAFJM~yl?9FT07i*)QDvKu?c<_%AaH55&p1TRyThohj&fB00C*fBrZKD$4qCuj`a}_BYR?-V8-&8OIzA1fj z){tGD7HM76twAZKGt`Gmb|y_@A_p?jbh#V+NA5TyLVK3uW)N_lqNJpmm<|=g2je&S zauo+cno{=cdfMY}HF+nwne!gZx~FWlt1-(LrcuNpefU+73AqP* zUPo4{J4E>%8;b_2G9B+Y^M@3)AE)ZYb}SZIF_@ZcXrSPUE>%bbJr^jEH6uR}dLX-y zAwTn0wn_V|j(jKb=RRk}iRe(ok}qsFan9$tAD{6_ts?#0}ek zH5@0rc4Nb{uI(f*m%jk$W(%0Mx6ddm#1;pCpJ49eBT;3V<6Bp-Ec#o`C_AyYnw3S8 z5IK340!(?c!@xkzOV)msBxb4^Va8et_Mi7BvPrJMU~MV;`km`HD>m63&@trj+t>$# z>E7yPc~AFD8=mx)<@eBV?Nbox)K_*(?!iGlT#%ekGL{j6Q##i^p4%_92TngZwg& zO|XAAZG2_6XeR&5+DzIepn%VV(1w&?&b?nrI9@3b*11~vzCJg|2SiR3< zNeOF>5X|jx;IUc(QA_|?8Ejul?riAJ90KF`XyY{;bZOIf?^|gbSSvcXlG{DSMIrT~w#Vqklp@E@2JtzX|M{a2Mhdmzd7%Bg0 z0->beTSGs_8h+q@{k8hU|9mh0!#BT;Z`~vL-`L`R-`L_mT*be%`ezSjlg|=+_r0}8 zy;C1qUr|;U=OOzXW`4AMSxBZcZQ}geYJ*-w4<_N;;`RmrxCMrevWBsk60+^s#Li_t zh&Jc!3bb&#X;@m(M1Fg$>u_bh% zaTUGzu7{z@r(A9~*)*smz;8wVqH+Q~OR@>S^z( zF<8+xz!PJ;J6*fOoPb5a;BLY&{l&pB{gp@6DhmgPnNxAMvKo9YU-R8`s*kxZdZ9~r zXd*a^3oa%t2NII1UPaBj1i1%8$SE1-6y@?Ps6dyJZSveC3* zO#Emb@HK9Jm9WJu=~?m~nYg0&?nYB}Zm~K`cQ_`>$pao{w_U5(lrwANndvTl0JMjK z(v0mrtSu};!knaKUPIV@`*3!P_HLvBD|_-L6i=_`FF^|1iLUj*NO8dx>2E)Yw)QZu z>e!pvyfm-&fuf?eUB-nsC}JKXUKX>8efa2ssw|Kk;pK9V)ZJj6(7gVDp=3STb|iOn z{%S9>8fiy*l-Pz&GhKQk9QAUeXCJrvo;E`jqz z2*l()H4L%3vBtkT+hJv_T};#^7nb2vrp1}r6NX*o#%NwKXo-f1=7BA`RyAo&b879v z9yyPxZqBw_-^ynxlu%c17`n=cR;{oVu+$<&+Ies@s51YpO1z5C!2wC5_hSIo8xLpi z7})1tgQHQYf2s<vB+DM*g0Aj(=(F|f^G|APjX8fp5L1cT83UrR7=7? zH{r`n+h*Xxn<#kWhOg4Q|DfR_I1z(X+S%Y$r;Vew##5azU9D@!_upxPdfX4wCAeg4<=`S)C(|I(K|)rb-Ap55b_v~A|*M;PKB zaLR`)%0O3E5MEDVd{ws!x)3!B|Ma4N8ANH4(#SWRzDXm;GZ1~ii1E!kyTT!QJ@4@# zix8Uw;6jZsdrkuUK%94a=!|l+BJ<8cmb3weuJv7Yon~5LcU|bc;CCtb?{D>3YJ$2# z3-3EvGECI@v`<@$I5+9)aYt^S6VSC#5SxD5P>o3YCaeB~C_S$ZxIAv9Sdci71Rh?rK!ck=)F3UihyajYR+a zJbFCcK9tiNhwo#C_f1tE+v=;Y_FHNGA4`TQH52U~eg1e`4c!(-(F9zD=4jR9GAp=5 zVstonp;>uyaF#=$38rGZX-VMv#l9i*zVX8BG2e%v;qf0C)R@`vyP!6WTgjh>uKwy| z`48=Wi;BdkFW%UCp`%#P3ha30sUWug&ImMou4eMdAI;0OO8>a00YOLJtJ>OoNPw)Q zUp&RVd9`$2yS8U^U;N=o@QPYQyL>V87l2%BIxq8iv}nfZHvoaO{8tEq1o(drh~eQk z_=WH{JVYhbQ0EhAp5ekkB_Iq_X_%y}xTJl)97}0jGVsKHKoNDKEas1tmvbgauup1n z=Zng0h%zBl84}VR(%{jTlCM;*eev7D=V1SLbus_pvi@@Q=jgL)wj67U^{5C&?BMz5 zm@q!gLFEN?Hxe7IEU8cSG5BQL3;>O`?>@tMv$XfcNlV=uJ#PRvWRfuu z{^uiLqujw*Rr-F?PFo5|7CuL+T>k*oE0mP4jr+mvHxV4Zu$$3p!Q(~4G^^q6dbc@o zY6kCG>FW8dlW?ZV|pnB=8_3gO0`2^lJ#|V+v z)|GgSPxK5*i{6bZqGDPjc)9q}DVHbmY+lEOR*ymD#?z-tnMY<6F+a}N{qV%SgB3gN zgJB=;s^lt-!6wv+2G}y{MdS0;7>5>3($z9y;Yqeku5s0wQBh@L#KR>Y_Fy8gOI`48 z;sA$ib7JWs11?i5WDw`!prdETgMkcgr>iq^>hPW97PqG}*eGy!Rf1_MwCpU2Gu~;< z=25|jNw$r#;zk`Yy$lJx&OSd`8_GYhdQ9EKy#?_>;e8tG?A^{?dA+U_YZBsYQ_IBume30~xU80Pb^0f`+^6b&E_eardR!wXF&nAgny>50# z^V-(V3CkC5c{NwrRMRjnNF>E0Q04L-Z&ihjmG;a%(>1qR|CV(+suDFRQ$FoboXkq0 zgp+F@@$w!nEH*pV@#^uUJDKvlZja~p+*sS{WXG?&ZqcNLcec89^*sA-9CVpIaZi|Q zn!Ch)iRxwdbAUUEi{BU8PXM0P(tJLln(ea3=gz3<%OtifIJdZN?w_`0rdyZfhXW7nctM@(J4-vXD4 zFO_zC+%~l^)9w3VBU7QCPL-5NS2^tK3!}dSPco8WKaPA0Leu$AtLt{I1|AY~Vtx0Q z{J!{9;4KIH>>N-#4ZUoEkvFsEC7eIhJ0XkZao?sN(*hI^uPiwIq4bDIjfy0HJTvI6@fofVgpO^syQ=J)*)dhNGmQmL@E)GzXQ(=5%{SHT)F)OR#y_|B?wwtiyC{`k@SKJH$}5?3 zb$WLD$CUbg*Sh^8?@&X}_o=t8TBWo<&b^f?nB}*%QVb7r|r_G1yNFuPI`&{)H0jnsnPK-`R0b>mg_`o48{$fO{*G~m2Nwf{@Qf32X-q*(0pX^%d1T(o{I_2c7Kp{+YEK07v3$$81+Os~jN zy-7h{o|=e>v1g*H1@kX7>8`w@v0C|Wj3@VhhPOFK#jd@Y*QK@mUbOSNo~B#Nm+or| znDi@Tv67^Lv(+bj>q5@l8JEmZVJCiEVOM%<}f0sg?@- z%)Mg8RN1^;w}I1cAsMH$`gwc}794qE+LWT6J?)m@p@l7_#pW^7_9c~8NB(CBIpuS& zINk1XmXdPdlKyv>YvV8SS{&5)kk!+yIOnR3iF?T6qq$*kw%3`b=$9G(F_$sjvHtgI z!?l;sYR&hbc>2m+x82!_=W-U8DW1sOoG0pHY!rB?;Zo08-h4r+JAt$NH8q3IbZX>^ zG!|wW*a=C+?#gm6)0-%E|7gPYKKZ|#2In&lXKh>k_|{@q*Y?DSdkefbSDPJCo1XIW z?-z|1{lW^3TF&b{Ci3!Zn__TywQDW6wy{I_!BRoS=dyB<|A*)spak}YmdJ*Rb!rwTsa(l#qo erA3ibWvM1`a>pg+h)cY|-v&|w7elT6|C<2iNe?#w literal 0 HcmV?d00001 diff --git a/images/trimingSession.jpg b/images/trimingSession.jpg new file mode 100644 index 0000000000000000000000000000000000000000..471df538d04909e2f856277db446e5cedec46969 GIT binary patch literal 37425 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 F{~zl(3(x=n literal 0 HcmV?d00001 From 7c01dfd0f89e7130f2e8ff7b45a72ee0717ae308 Mon Sep 17 00:00:00 2001 From: emre-openai Date: Sun, 7 Sep 2025 21:34:42 -0700 Subject: [PATCH 06/11] tweak --- examples/agents_sdk/session_memory.ipynb | 319 +---------------------- 1 file changed, 1 insertion(+), 318 deletions(-) diff --git a/examples/agents_sdk/session_memory.ipynb b/examples/agents_sdk/session_memory.ipynb index 66948c4484..cde96fc48d 100644 --- a/examples/agents_sdk/session_memory.ipynb +++ b/examples/agents_sdk/session_memory.ipynb @@ -805,324 +805,7 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "4bb5c4e9", - "metadata": {}, - "outputs": [], - "source": [ - "import asyncio\n", - "import itertools\n", - "from collections import deque\n", - "from typing import Optional, List, Tuple, Dict, Any\n", - "\n", - "class SummarizingSession:\n", - " \"\"\"\n", - " Keeps the last N *user* turns verbatim.\n", - " Summarizes everything before that into a synthetic user→assistant pair.\n", - " Internally stores (message, metadata) records. Exposes:\n", - " - get_items(): model-safe messages only (no metadata)\n", - " - get_full_history(): [{ \"message\": msg, \"metadata\": meta }, ...]\n", - " \"\"\"\n", - "\n", - " # Only these keys are sent to the model. Everything else goes to metadata.\n", - " _ALLOWED_MSG_KEYS = {\"role\", \"content\", \"name\"}\n", - "\n", - " def __init__(\n", - " self,\n", - " max_turns: int = 3,\n", - " summarizer: Optional[\"Summarizer\"] = None,\n", - " session_id: Optional[str] = None,\n", - " ):\n", - " assert max_turns >= 1\n", - " self.max_turns = max_turns\n", - " # Each record: {\"msg\": {...}, \"meta\": {...}}\n", - " self._records: deque[Dict[str, Dict[str, Any]]] = deque()\n", - " self._lock = asyncio.Lock()\n", - " self.session_id = session_id or \"default\"\n", - " self.summarizer = summarizer\n", - "\n", - " # --------- public API used by your runner ---------\n", - "\n", - " async def get_items(self, limit: Optional[int] = None) -> List[Dict[str, Any]]:\n", - " \"\"\"\n", - " Returns messages in a model-safe shape (no metadata).\n", - " Runner.run(..., session=self) should call this.\n", - " \"\"\"\n", - " async with self._lock:\n", - " data = list(self._records)\n", - " msgs = [self._sanitize_for_model(rec[\"msg\"]) for rec in data]\n", - " return msgs[-limit:] if limit else msgs\n", - "\n", - " async def add_items(self, items: List[Dict[str, Any]]) -> None:\n", - " async with self._lock:\n", - " for it in items:\n", - " msg, meta = self._split_msg_and_meta(it)\n", - " self._records.append({\"msg\": msg, \"meta\": meta})\n", - " need_summary, boundary_idx = self._should_summarize_locked()\n", - "\n", - " if need_summary:\n", - " async with self._lock:\n", - " prefix_records = list(itertools.islice(self._records, 0, boundary_idx))\n", - " prefix_msgs = [r[\"msg\"] for r in prefix_records]\n", - "\n", - " user_shadow, assistant_summary = await self._summarize(prefix_msgs)\n", - "\n", - " async with self._lock:\n", - " need_summary_now, boundary_idx_now = self._should_summarize_locked()\n", - " if not need_summary_now:\n", - " # normalize anyway if summarization got skipped\n", - " self._normalize_synthetic_flags_locked()\n", - " return\n", - "\n", - " suffix_records = list(itertools.islice(self._records, boundary_idx_now, None))\n", - " self._records.clear()\n", - "\n", - " # Synthetic summary pair keeps synthetic=True\n", - " self._records.extend([\n", - " {\n", - " \"msg\": {\"role\": \"user\", \"content\": user_shadow},\n", - " \"meta\": {\n", - " \"synthetic\": True,\n", - " \"kind\": \"history_summary_prompt\",\n", - " \"summary_for_turns\": f\"< all before idx {boundary_idx_now} >\",\n", - " },\n", - " },\n", - " {\n", - " \"msg\": {\"role\": \"assistant\", \"content\": assistant_summary},\n", - " \"meta\": {\n", - " \"synthetic\": True,\n", - " \"kind\": \"history_summary\",\n", - " \"summary_for_turns\": f\"< all before idx {boundary_idx_now} >\",\n", - " },\n", - " },\n", - " ])\n", - " self._records.extend(suffix_records)\n", - "\n", - " # ✅ Ensure all real messages explicitly have synthetic=False\n", - " self._normalize_synthetic_flags_locked()\n", - " else:\n", - " # ✅ Even when we don't summarize, enforce the invariant\n", - " async with self._lock:\n", - " self._normalize_synthetic_flags_locked()\n", - "\n", - " async def pop_item(self) -> Optional[Dict[str, Any]]:\n", - " async with self._lock:\n", - " if not self._records:\n", - " return None\n", - " rec = self._records.pop()\n", - " return dict(rec[\"msg\"]) # model-safe\n", - "\n", - " async def clear_session(self) -> None:\n", - " async with self._lock:\n", - " self._records.clear()\n", - "\n", - " def set_max_turns(self, n: int) -> None:\n", - " assert n >= 1\n", - " self.max_turns = n\n", - "\n", - " # --------- full-history (for debugging/analytics/observability) ---------\n", - "\n", - " # ✅ Backfill safeguard for older records that might lack the flag\n", - " def _normalize_synthetic_flags_locked(self) -> None:\n", - " for rec in self._records:\n", - " role = rec[\"msg\"].get(\"role\")\n", - " if role in (\"user\", \"assistant\") and \"synthetic\" not in rec[\"meta\"]:\n", - " rec[\"meta\"][\"synthetic\"] = False\n", - "\n", - " \n", - " async def get_full_history(self, limit: Optional[int] = None) -> List[Dict[str, Any]]:\n", - " \"\"\"\n", - " Returns combined history where each entry is:\n", - " { \"message\": {role, content[, name]}, \"metadata\": {...} }\n", - " This is NOT sent to the model; it's for your logs/UI/debugging.\n", - " \"\"\"\n", - " async with self._lock:\n", - " data = list(self._records)\n", - " out = [{\"message\": dict(rec[\"msg\"]), \"metadata\": dict(rec[\"meta\"])} for rec in data]\n", - " return out[-limit:] if limit else out\n", - "\n", - " # Backwards-compatible alias if you were using this name before\n", - " async def get_items_with_metadata(self, limit: Optional[int] = None) -> List[Dict[str, Any]]:\n", - " return await self.get_full_history(limit)\n", - "\n", - " # --------- helpers ---------\n", - "\n", - " def _split_msg_and_meta(self, it: Dict[str, Any]) -> Tuple[Dict[str, Any], Dict[str, Any]]:\n", - " msg = {k: v for k, v in it.items() if k in self._ALLOWED_MSG_KEYS}\n", - " extra = {k: v for k, v in it.items() if k not in self._ALLOWED_MSG_KEYS}\n", - " meta = dict(extra.pop(\"metadata\", {}))\n", - " meta.update(extra)\n", - "\n", - " if \"role\" not in msg or \"content\" not in msg:\n", - " msg.setdefault(\"role\", \"user\")\n", - " msg.setdefault(\"content\", str(it))\n", - "\n", - " # ✅ Default synthetic flag for real (non-summarized) messages\n", - " role = msg.get(\"role\")\n", - " if role in (\"user\", \"assistant\") and \"synthetic\" not in meta:\n", - " meta[\"synthetic\"] = False\n", - " return msg, meta\n", - "\n", - " def _sanitize_for_model(self, msg: Dict[str, Any]) -> Dict[str, Any]:\n", - " \"\"\"\n", - " Strictly keep only allowed keys for model input.\n", - " \"\"\"\n", - " return {k: v for k, v in msg.items() if k in self._ALLOWED_MSG_KEYS}\n", - "\n", - " def _is_user(self, rec: Dict[str, Dict[str, Any]]) -> bool:\n", - " return rec[\"msg\"].get(\"role\") == \"user\"\n", - "\n", - " def _should_summarize_locked(self) -> Tuple[bool, int]:\n", - " \"\"\"\n", - " Find the earliest index among the last `max_turns` user messages.\n", - " Everything before that index becomes the summarization prefix.\n", - " \"\"\"\n", - " idxs = []\n", - " for i in range(len(self._records) - 1, -1, -1):\n", - " if self._is_user(self._records[i]):\n", - " idxs.append(i)\n", - " if len(idxs) == self.max_turns:\n", - " break\n", - " if len(idxs) < self.max_turns:\n", - " return False, -1\n", - " boundary = min(idxs)\n", - " if boundary <= 0:\n", - " return False, -1\n", - " return True, boundary\n", - "\n", - " async def _summarize(self, prefix_msgs: List[Dict[str, Any]]) -> Tuple[str, str]:\n", - " \"\"\"\n", - " Adapter to your summarizer. Provide *model-safe* messages only.\n", - " \"\"\"\n", - " if not self.summarizer:\n", - " # Fallback summary if no summarizer is configured\n", - " return (\"Summarize the conversation we had so far.\", \"Summary unavailable.\")\n", - " # Only send role/content/name to the summarizer as well\n", - " clean_prefix = [self._sanitize_for_model(m) for m in prefix_msgs]\n", - " return await self.summarizer.summarize(clean_prefix)\n" - ] - }, - { - "cell_type": "code", - "execution_count": 177, - "id": "a3e7cff8", - "metadata": {}, - "outputs": [], - "source": [ - "import asyncio\n", - "from collections import deque\n", - "from typing import Optional\n", - "import itertools\n", - "\n", - "class SummarizingSession:\n", - " \"\"\"\n", - " Keeps the last N user turns verbatim.\n", - " Summarizes everything before that into a synthetic user→assistant pair.\n", - " \"\"\"\n", - " def __init__(\n", - " self,\n", - " max_turns: int = 3,\n", - " summarizer: Optional[Summarizer] = None,\n", - " session_id: Optional[str] = None,\n", - " ):\n", - " assert max_turns >= 1\n", - " self.max_turns = max_turns\n", - " self._items: deque[Item] = deque()\n", - " self._lock = asyncio.Lock()\n", - " self.session_id = session_id or \"default\"\n", - " self.summarizer = summarizer\n", - "\n", - " # ----- public API that mirrors common Session interfaces -----\n", - "\n", - " async def get_items(self, limit: Optional[int] = None) -> list[Item]:\n", - " async with self._lock:\n", - " data = list(self._items)\n", - " return data[-limit:] if limit else data\n", - "\n", - " async def add_items(self, items: list[Item]) -> None:\n", - " # Append first\n", - " async with self._lock:\n", - " self._items.extend(items)\n", - " need_summary, boundary_idx = self._should_summarize_locked()\n", - "\n", - " # If we need a summary, **do it without the lock** to avoid blocking others\n", - " if need_summary:\n", - " # Take a snapshot of the prefix to summarize\n", - " async with self._lock:\n", - " prefix = list(itertools.islice(self._items, 0, boundary_idx))\n", - " # Produce the summary outside the lock\n", - " user_shadow, assistant_summary = await self.summarizer.summarize(prefix)\n", - "\n", - " # Re-acquire and re-check (in case of concurrent updates)\n", - " async with self._lock:\n", - " need_summary_now, boundary_idx_now = self._should_summarize_locked()\n", - " if need_summary_now:\n", - " suffix = list(itertools.islice(self._items, boundary_idx_now, None)) \n", - " self._items.clear()\n", - " self._items.extend([\n", - " {\n", - " \"role\": \"user\",\n", - " \"content\": user_shadow,\n", - " \"metadata\": {\n", - " \"synthetic\": True,\n", - " \"kind\": \"history_summary_prompt\",\n", - " \"summary_for_turns\": f\"< all before idx {boundary_idx_now} >\",\n", - " },\n", - " },\n", - " {\n", - " \"role\": \"assistant\",\n", - " \"content\": assistant_summary,\n", - " \"metadata\": {\n", - " \"synthetic\": True,\n", - " \"kind\": \"history_summary\",\n", - " \"summary_for_turns\": f\"< all before idx {boundary_idx_now} >\",\n", - " },\n", - " },\n", - " ])\n", - " self._items.extend(suffix)\n", - " # else: another concurrent writer already summarized; do nothing.\n", - "\n", - " async def pop_item(self) -> Optional[Item]:\n", - " async with self._lock:\n", - " return self._items.pop() if self._items else None\n", - "\n", - " async def clear_session(self) -> None:\n", - " async with self._lock:\n", - " self._items.clear()\n", - "\n", - " def set_max_turns(self, n: int) -> None:\n", - " assert n >= 1\n", - " self.max_turns = n\n", - "\n", - " # ----- helpers -----\n", - "\n", - " def _is_user(self, it: Item) -> bool:\n", - " return it.get(\"role\") == \"user\"\n", - "\n", - " def _should_summarize_locked(self) -> tuple[bool, int]:\n", - " \"\"\"\n", - " Returns (need_summary, boundary_idx).\n", - " boundary_idx = earliest index to keep (start of last N user turns).\n", - " If False, boundary_idx is undefined.\n", - " \"\"\"\n", - " idxs = []\n", - " for i in range(len(self._items) - 1, -1, -1):\n", - " if self._is_user(self._items[i]):\n", - " idxs.append(i)\n", - " if len(idxs) == self.max_turns:\n", - " break\n", - " if len(idxs) < self.max_turns:\n", - " return False, -1 # not enough user turns yet\n", - "\n", - " boundary = min(idxs) # earliest of the last N user turns\n", - " if boundary <= 0:\n", - " return False, -1 # nothing to summarize before boundary\n", - " return True, boundary\n" - ] - }, - { - "cell_type": "code", - "execution_count": 237, + "execution_count": 250, "id": "0d8bd4c5", "metadata": {}, "outputs": [], From 412fd42918c3907ff6e8415bebf4ce53a4c081d4 Mon Sep 17 00:00:00 2001 From: emre-openai Date: Sun, 7 Sep 2025 22:42:16 -0700 Subject: [PATCH 07/11] tweak --- examples/agents_sdk/session_memory.ipynb | 581 ++++++++++------------- 1 file changed, 263 insertions(+), 318 deletions(-) diff --git a/examples/agents_sdk/session_memory.ipynb b/examples/agents_sdk/session_memory.ipynb index cde96fc48d..995a87c1ac 100644 --- a/examples/agents_sdk/session_memory.ipynb +++ b/examples/agents_sdk/session_memory.ipynb @@ -13,10 +13,9 @@ "id": "eeab798a", "metadata": {}, "source": [ - "In this cookbook, we’ll explore how to **manage context effectively using the `Session` object from the [OpenAI Agents SDK](https://github.com/openai/openai-agents-python)**.\n", - "\n", "AI agents often operate in **long-running, multi-turn interactions**, where keeping the right balance of context is critical. If too much is carried forward, the model risks distraction, inefficiency, or outright failure. If too little is preserved, the agent loses coherence. This guide focuses on two proven context management techniques—**trimming** and **compression**—to keep agents fast, reliable, and cost-efficient.\n", "\n", + "In this cookbook, we’ll explore how to **manage context effectively using the `Session` object from the [OpenAI Agents SDK](https://github.com/openai/openai-agents-python)**.\n", "\n", "#### Why Context Management Matters\n", "\n", @@ -164,7 +163,7 @@ "source": [ "### Define Agents\n", "\n", - "We can start by defining the necessary components from Agents SDK Library." + "We can start by defining the necessary components from Agents SDK Library. Instructions added based on the use case during agent creation." ] }, { @@ -218,13 +217,13 @@ "id": "5993098b", "metadata": {}, "source": [ - "We are using [Session](https://openai.github.io/openai-agents-python/sessions/) object from [OpenAI Agents Python SDK](https://openai.github.io/openai-agents-python/). Here’s a `MyCustomSession` implementation that **keeps only the last N turns** (a “turn” = one user message and everything until the next user message—including the assistant reply and any tool calls/results). It’s in-memory and trims automatically on every write and read.\n" + "We are using [Session](https://openai.github.io/openai-agents-python/sessions/) object from [OpenAI Agents Python SDK](https://openai.github.io/openai-agents-python/). Here’s a `TrimmingSession` implementation that **keeps only the last N turns** (a “turn” = one user message and everything until the next user message—including the assistant reply and any tool calls/results). It’s in-memory and trims automatically on every write and read.\n" ] }, { "cell_type": "code", - "execution_count": 6, - "id": "1b468c78", + "execution_count": 252, + "id": "95ed36a3", "metadata": {}, "outputs": [], "source": [ @@ -232,87 +231,67 @@ "\n", "import asyncio\n", "from collections import deque\n", - "from typing import Any, Deque, Dict, List, Tuple, cast\n", + "from typing import Any, Deque, Dict, List, cast\n", "\n", "from agents.memory.session import SessionABC\n", - "from agents.items import TResponseInputItem # typically a dict-like item\n", + "from agents.items import TResponseInputItem # dict-like item\n", + "\n", + "ROLE_USER = \"user\"\n", "\n", "\n", "def _is_user_msg(item: TResponseInputItem) -> bool:\n", - " \"\"\"\n", - " Heuristic: treat items with role=='user' as user messages.\n", - " Falls back to type=='message' and author checks if your SDK uses a different shape.\n", - " Adjust this if your item schema differs.\n", - " \"\"\"\n", + " \"\"\"Return True if the item represents a user message.\"\"\"\n", + " # Common dict-shaped messages\n", " if isinstance(item, dict):\n", - " role = cast(Dict[str, Any], item).get(\"role\")\n", - " if role == \"user\":\n", - " return True\n", - " # Some SDKs encode messages as {\"type\": \"message\", \"role\": \"...\"}\n", - " if item.get(\"type\") == \"message\" and item.get(\"role\") == \"user\":\n", - " return True\n", - " # Extend here if you carry custom classes with .role attribute:\n", - " role_attr = getattr(item, \"role\", None)\n", - " return role_attr == \"user\"\n", + " role = item.get(\"role\")\n", + " if role is not None:\n", + " return role == ROLE_USER\n", + " # Some SDKs: {\"type\": \"message\", \"role\": \"...\"}\n", + " if item.get(\"type\") == \"message\":\n", + " return item.get(\"role\") == ROLE_USER\n", + " # Fallback: objects with a .role attr\n", + " return getattr(item, \"role\", None) == ROLE_USER\n", "\n", "\n", "class TrimmingSession(SessionABC):\n", " \"\"\"\n", - " Custom session that keeps only the last N user-turns.\n", - " A 'turn' is defined as a user message and all subsequent items\n", - " (assistant/tool calls/results) up to—but not including—the next user message.\n", + " Keep only the last N *user turns* in memory.\n", "\n", - " Works entirely in memory. If you need persistence, replace the in-memory\n", - " deque with your storage of choice (SQLite/Redis/etc.), preserving the\n", - " trimming logic in `_trim_to_last_turns`.\n", + " A turn = a user message and all subsequent items (assistant/tool calls/results)\n", + " up to (but not including) the next user message.\n", " \"\"\"\n", "\n", " def __init__(self, session_id: str, max_turns: int = 8):\n", " self.session_id = session_id\n", - " self.max_turns = max(1, max_turns)\n", - " self._items: Deque[TResponseInputItem] = deque() # full chronological log\n", + " self.max_turns = max(1, int(max_turns))\n", + " self._items: Deque[TResponseInputItem] = deque() # chronological log\n", " self._lock = asyncio.Lock()\n", "\n", " # ---- SessionABC API ----\n", "\n", " async def get_items(self, limit: int | None = None) -> List[TResponseInputItem]:\n", - " \"\"\"\n", - " Return the history trimmed to the last N turns.\n", - " If `limit` is provided, return at most that many most-recent items\n", - " from within the trimmed history.\n", - " \"\"\"\n", + " \"\"\"Return history trimmed to the last N user turns (optionally limited to most-recent `limit` items).\"\"\"\n", " async with self._lock:\n", " trimmed = self._trim_to_last_turns(list(self._items))\n", - " if limit is not None and limit >= 0:\n", - " return trimmed[-limit:]\n", - " return trimmed\n", + " return trimmed[-limit:] if (limit is not None and limit >= 0) else trimmed\n", "\n", " async def add_items(self, items: List[TResponseInputItem]) -> None:\n", - " \"\"\"\n", - " Append new items, then trim to last N turns.\n", - " \"\"\"\n", + " \"\"\"Append new items, then trim to last N user turns.\"\"\"\n", " if not items:\n", " return\n", " async with self._lock:\n", " self._items.extend(items)\n", - " # Trim in place by rebuilding from trimmed list\n", " trimmed = self._trim_to_last_turns(list(self._items))\n", " self._items.clear()\n", " self._items.extend(trimmed)\n", "\n", " async def pop_item(self) -> TResponseInputItem | None:\n", - " \"\"\"\n", - " Remove and return the most recent item (post-trim).\n", - " \"\"\"\n", + " \"\"\"Remove and return the most recent item (post-trim).\"\"\"\n", " async with self._lock:\n", - " if not self._items:\n", - " return None\n", - " return self._items.pop()\n", + " return self._items.pop() if self._items else None\n", "\n", " async def clear_session(self) -> None:\n", - " \"\"\"\n", - " Remove all items for this session.\n", - " \"\"\"\n", + " \"\"\"Remove all items for this session.\"\"\"\n", " async with self._lock:\n", " self._items.clear()\n", "\n", @@ -320,36 +299,28 @@ "\n", " def _trim_to_last_turns(self, items: List[TResponseInputItem]) -> List[TResponseInputItem]:\n", " \"\"\"\n", - " Keep only the suffix of `items` that contains the last `max_turns` user messages\n", - " and everything after the earliest of those user messages.\n", - "\n", - " Algorithm:\n", - " 1) Scan from the end to find indices of the last `max_turns` user messages.\n", - " 2) Cut history to start from the earliest of those (inclusive).\n", - " Edge cases:\n", - " - If there are fewer than `max_turns` user messages, keep entire history.\n", - " - If there are no user messages yet, treat all existing items as a single turn and keep them.\n", + " Keep only the suffix containing the last `max_turns` user messages and everything after\n", + " the earliest of those user messages.\n", + "\n", + " If there are fewer than `max_turns` user messages (or none), keep all items.\n", " \"\"\"\n", " if not items:\n", " return items\n", "\n", - " # Find indices of user messages scanning from the end\n", - " user_indices: List[int] = []\n", - " for idx in range(len(items) - 1, -1, -1):\n", - " if _is_user_msg(items[idx]):\n", - " user_indices.append(idx)\n", - " if len(user_indices) >= self.max_turns:\n", - " break\n", + " count = 0\n", + " start_idx = 0 # default: keep all if we never reach max_turns\n", "\n", - " if not user_indices:\n", - " # No user messages yet; keep everything\n", - " return items\n", + " # Walk backward; when we hit the Nth user message, mark its index.\n", + " for i in range(len(items) - 1, -1, -1):\n", + " if _is_user_msg(items[i]):\n", + " count += 1\n", + " if count == self.max_turns:\n", + " start_idx = i\n", + " break\n", "\n", - " # The earliest index among the last N user messages\n", - " cut_from = min(user_indices) # since we collected from the end\n", - " return items[cut_from:]\n", + " return items[start_idx:]\n", "\n", - " # ---- Optional convenience API (not part of SessionABC) ----\n", + " # ---- Optional convenience API ----\n", "\n", " async def set_max_turns(self, max_turns: int) -> None:\n", " async with self._lock:\n", @@ -369,12 +340,12 @@ "id": "21bd5b28", "metadata": {}, "source": [ - "Let's define the custom session object we implemented." + "Let's define the custom session object we implemented with max_turns=3." ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 280, "id": "951ad6da", "metadata": {}, "outputs": [], @@ -395,17 +366,17 @@ }, { "cell_type": "code", - "execution_count": 27, + "execution_count": 281, "id": "c59d40b9", "metadata": {}, "outputs": [], "source": [ - "message = \"There is a red light on the dashboard.\"" + "message = \"There is a red light blinking on my laptop.\"" ] }, { "cell_type": "code", - "execution_count": 28, + "execution_count": 282, "id": "03b15552", "metadata": {}, "outputs": [], @@ -419,31 +390,31 @@ }, { "cell_type": "code", - "execution_count": 29, + "execution_count": 283, "id": "c94beb6f", "metadata": {}, "outputs": [], "source": [ - "conversation = await session.get_items()\n" + "history = await session.get_items()\n" ] }, { "cell_type": "code", - "execution_count": 30, + "execution_count": 284, "id": "626a6e57", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "[{'content': 'There is a red light on the dashboard.', 'role': 'user'},\n", - " {'id': 'rs_68ba0a2abf3c8196adc68a947b457ae0001b849827e34d0e',\n", + "[{'content': 'There is a red light blinking on my laptop.', 'role': 'user'},\n", + " {'id': 'rs_68be66229c008190aa4b3c5501f397080fdfa41323fb39cb',\n", " 'summary': [],\n", " 'type': 'reasoning',\n", " 'content': []},\n", - " {'id': 'msg_68ba0a3480748196b7059d0e23d87350001b849827e34d0e',\n", + " {'id': 'msg_68be662f704c8190969bdf539701a3e90fdfa41323fb39cb',\n", " 'content': [{'annotations': [],\n", - " 'text': 'Which device or system is the dashboard on (e.g., car, printer, router, software)?',\n", + " 'text': 'A blinking red light usually indicates a power/battery or hardware fault, but the meaning varies by brand.\\n\\nWhat is the exact make and model of your laptop?\\n\\nWhile you check that, please try these quick checks:\\n1) Note exactly where the red LED is (charging port, power button, keyboard edge) and the blink pattern (e.g., constant blink, 2 short/1 long).\\n2) Plug the charger directly into a known‑good wall outlet (no power strip), ensure the charger tip is fully seated, and look for damage to the cable/port. See if the LED behavior changes.\\n3) Leave it on charge for 30 minutes in case the battery is critically low.\\n4) Power reset: unplug the charger; if the battery is removable, remove it. Hold the power button for 20–30 seconds. Reconnect power (and battery) and try turning it on.\\n5) Tell me the LED location, blink pattern, and what changed after these steps.',\n", " 'type': 'output_text',\n", " 'logprobs': []}],\n", " 'role': 'assistant',\n", @@ -451,71 +422,31 @@ " 'type': 'message'}]" ] }, - "execution_count": 30, + "execution_count": 284, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "conversation" - ] - }, - { - "cell_type": "code", - "execution_count": 32, - "id": "395c1bd1", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "3" - ] - }, - "execution_count": 32, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "len(conversation)" - ] - }, - { - "cell_type": "code", - "execution_count": 31, - "id": "7ca19df0", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "('user', 'There is a red light on the dashboard.')" - ] - }, - "execution_count": 31, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "conversation[0]['role'], conversation[0]['content']" + "history" ] }, { "cell_type": "code", - "execution_count": 50, + "execution_count": 285, "id": "51f60675", "metadata": {}, "outputs": [], "source": [ "# Example flow\n", - "await session.add_items([{\"role\": \"user\", \"content\": \"Hi, my router won't connect.\"}])\n", - "await session.add_items([{\"role\": \"assistant\", \"content\": \"Let's check your firmware version.\"}])\n", + "await session.add_items([{\"role\": \"user\", \"content\": \"I am using a macbook pro and it has some overheating issues too.\"}])\n", + "await session.add_items([{\"role\": \"assistant\", \"content\": \"I see. Let's check your firmware version.\"}])\n", "await session.add_items([{\"role\": \"user\", \"content\": \"Firmware v1.0.3; still failing.\"}])\n", - "await session.add_items([{\"role\": \"assistant\", \"content\": \"Try a factory reset.\"}])\n", + "await session.add_items([{\"role\": \"assistant\", \"content\": \"Could you please try a factory reset?\"}])\n", "await session.add_items([{\"role\": \"user\", \"content\": \"Reset done; error 42 now.\"}])\n", - "await session.add_items([{\"role\": \"assistant\", \"content\": \"test1\"}])\n", + "await session.add_items([{\"role\": \"assistant\", \"content\": \"Leave it on charge for 30 minutes in case the battery is critically low. Is there any other error message?\"}])\n", + "await session.add_items([{\"role\": \"user\", \"content\": \"Yes, I see error 404 now.\"}])\n", + "await session.add_items([{\"role\": \"assistant\", \"content\": \"Do you see it on the browser while accessing a website?\"}])\n", "# At this point, with max_turns=3, everything *before* the earliest of the last 3 user\n", "# messages is summarized into a synthetic pair, and the last 3 turns remain verbatim.\n", "\n", @@ -525,17 +456,17 @@ }, { "cell_type": "code", - "execution_count": 51, + "execution_count": 286, "id": "6dc29a28", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "4" + "6" ] }, - "execution_count": 51, + "execution_count": 286, "metadata": {}, "output_type": "execute_result" } @@ -546,7 +477,7 @@ }, { "cell_type": "code", - "execution_count": 52, + "execution_count": 287, "id": "07430b49", "metadata": {}, "outputs": [ @@ -554,12 +485,16 @@ "data": { "text/plain": [ "[{'role': 'user', 'content': 'Firmware v1.0.3; still failing.'},\n", - " {'role': 'assistant', 'content': 'Try a factory reset.'},\n", + " {'role': 'assistant', 'content': 'Could you please try a factory reset?'},\n", " {'role': 'user', 'content': 'Reset done; error 42 now.'},\n", - " {'role': 'assistant', 'content': 'test1'}]" + " {'role': 'assistant',\n", + " 'content': 'Leave it on charge for 30 minutes in case the battery is critically low. Is there any other error message?'},\n", + " {'role': 'user', 'content': 'Yes, I see error 404 now.'},\n", + " {'role': 'assistant',\n", + " 'content': 'Do you see it on the browser while accessing a website?'}]" ] }, - "execution_count": 52, + "execution_count": 287, "metadata": {}, "output_type": "execute_result" } @@ -607,12 +542,6 @@ "\n", "That preserves each complete turn boundary: if the earliest kept user message is at index `k`, you also keep all assistant/tool items that came after `k`.\n", "\n", - "**Edge cases**\n", - "\n", - "* **Fewer than N user messages**: keep **everything** (no trimming yet).\n", - "* **No user messages**: keep **everything** (treat as a single in-progress turn).\n", - "* **`limit` in `get_items(limit=…)`**: applied **after** trimming; returns only the last `limit` items of the already-trimmed slice.\n", - "\n", "**Tiny example**\n", "\n", "History (old → new):\n", @@ -638,7 +567,7 @@ "\n", "**Customization knobs**\n", "\n", - "* Change `max_turns` at init or via `set_max_turns(...)`.\n", + "* Change `max_turns` at init.\n", "* Adjust `_is_user_msg(...)` if your item schema differs.\n", "* If you’d rather cap by **message count** or **tokens**, replace `_trim_to_last_turns(...)` or add a second pass that measures tokens.\n" ] @@ -739,6 +668,8 @@ "\n", "* **Milestones:** Highlight important events in the conversation—for example, when an issue is resolved, valuable information is uncovered, or all necessary details have been collected.\n", "\n", + "* **Use Case Specificity:** Tailor the compression prompt to the specific use case. Think about how a human would track and recall information in working memory while solving the same task.\n", + "\n", "* **Contradiction Check:** Ensure the summary does not conflict with itself, system instructions or tool definitions. This is especially critical for reasoning models, which are more prone to conflicts in the context.\n", "\n", "* **Timestamps & Temporal Flow:** Incorporate timing of events in the summary. This helps the model reason about updates in sequence and reduces confusion when forgetting or remembering the latest memory over a timeline.\n", @@ -751,15 +682,13 @@ "\n", "* **Hallucination Control:** Be precise in what you include. Even minor hallucinations in a summary can propagate forward, contaminating future context with inaccuracies.\n", "\n", - "* **Use Case Specificity:** Tailor the compression prompt to the specific use case. Think about how a human would track and recall information in working memory while solving the same task.\n", - "\n", - "* **Model Choice:** Select a summarizer model based on use case requirements, summary length, and tradeoffs between latency and cost. In some cases, using the same model as the agent itself can be advantageous.\n" + "* **Model Choice:** Select a summarizer model based on use case requirements, summary length, and tradeoffs between latency and cost. In some cases, using the same model as the AI agent itself can be advantageous.\n" ] }, { "cell_type": "code", - "execution_count": 219, - "id": "8f3f811e", + "execution_count": 289, + "id": "6c2265ee", "metadata": {}, "outputs": [], "source": [ @@ -771,66 +700,72 @@ " self.tool_trim_limit = tool_trim_limit\n", "\n", " async def summarize(self, messages: List[Item]) -> Tuple[str, str]:\n", + " \"\"\"\n", + " Create a compact summary from `messages`.\n", + "\n", + " Returns:\n", + " Tuple[str, str]: The shadow user line to keep dialog natural,\n", + " and the model-generated summary text.\n", + " \"\"\"\n", " user_shadow = \"Summarize the conversation we had so far.\"\n", - " # Map history into a compact prompt\n", - " history_snippets = []\n", - " for m in messages:\n", - " role = m.get(\"role\", \"assistant\")\n", + " TOOL_ROLES = {\"tool\", \"tool_result\"}\n", + "\n", + " def to_snippet(m: Item) -> str | None:\n", + " role = (m.get(\"role\") or \"assistant\").lower()\n", " content = (m.get(\"content\") or \"\").strip()\n", " if not content:\n", - " continue\n", - " # trim very long tool blobs\n", - " if role in (\"tool\", \"tool_result\") and len(content) > self.tool_trim_limit:\n", - " content = content[:self.tool_trim_limit] + \" …\"\n", - " history_snippets.append(f\"{role.upper()}: {content}\")\n", - " #print(history_snippets)\n", - " # Example using Responses; adapt if you use SDK Agents runs instead\n", + " return None\n", + " # Trim verbose tool outputs to keep prompt compact \n", + " if role in TOOL_ROLES and len(content) > self.tool_trim_limit:\n", + " content = content[: self.tool_trim_limit] + \" …\"\n", + " return f\"{role.upper()}: {content}\"\n", + "\n", + " # Build compact, trimmed history\n", + " history_snippets = [s for m in messages if (s := to_snippet(m))]\n", + "\n", " prompt_messages = [\n", " {\"role\": \"system\", \"content\": SUMMARY_PROMPT},\n", - " {\"role\": \"user\", \"content\": \"\\n\".join(history_snippets)}\n", + " {\"role\": \"user\", \"content\": \"\\n\".join(history_snippets)},\n", " ]\n", - " print(len(prompt_messages))\n", + "\n", " resp = await asyncio.to_thread(\n", - " self.client.responses.create,\n", - " model=self.model,\n", - " input=prompt_messages,\n", - " max_output_tokens=self.max_tokens\n", - " ) \n", - " \n", - " summary = resp.output_text\n", + " self.client.responses.create,\n", + " model=self.model,\n", + " input=prompt_messages,\n", + " max_output_tokens=self.max_tokens,\n", + " )\n", "\n", + " summary = resp.output_text\n", " await asyncio.sleep(0) # yield control\n", - " return user_shadow, summary" + " return user_shadow, summary\n" ] }, { "cell_type": "code", - "execution_count": 250, - "id": "0d8bd4c5", + "execution_count": 296, + "id": "30ca3d6e", "metadata": {}, "outputs": [], "source": [ "import asyncio\n", - "import itertools\n", "from collections import deque\n", "from typing import Optional, List, Tuple, Dict, Any\n", "\n", + "Record = Dict[str, Dict[str, Any]] # {\"msg\": {...}, \"meta\": {...}}\n", + "\n", "class SummarizingSession:\n", " \"\"\"\n", - " Keeps the last N *user turns* verbatim (keep_last_n_turns).\n", - " A turn = one real user message + everything that follows it (assistant replies,\n", - " reasoning, tool calls, tool results) until the next real user message.\n", - " Summarizes everything before that into a synthetic user→assistant pair.\n", - "\n", - " Summarization is triggered once the number of *real* user turns\n", - " (non-synthetic 'user' messages) exceeds `context_limit`.\n", - "\n", - " Internally stores (message, metadata) records. Exposes:\n", - " - get_items(): model-safe messages only (no metadata)\n", - " - get_full_history(): [{ \"message\": msg, \"metadata\": meta }, ...]\n", + " Session that keeps only the last N *user turns* verbatim and summarizes the rest.\n", + "\n", + " - A *turn* starts at a real user message and includes everything until the next real user message.\n", + " - When the number of real user turns exceeds `context_limit`, everything before the earliest\n", + " of the last `keep_last_n_turns` user-turn starts is summarized into a synthetic user→assistant pair.\n", + " - Stores full records (message + metadata). Exposes:\n", + " • get_items(): model-safe messages only (no metadata)\n", + " • get_full_history(): [{\"message\": msg, \"metadata\": meta}, ...]\n", " \"\"\"\n", "\n", - " # Only these keys are sent to the model. Everything else goes to metadata.\n", + " # Only these keys are ever sent to the model; the rest live in metadata.\n", " _ALLOWED_MSG_KEYS = {\"role\", \"content\", \"name\"}\n", "\n", " def __init__(\n", @@ -843,195 +778,208 @@ " assert context_limit >= 1\n", " assert keep_last_n_turns >= 0\n", " assert keep_last_n_turns <= context_limit, \"keep_last_n_turns should not be greater than context_limit\"\n", + "\n", " self.keep_last_n_turns = keep_last_n_turns\n", " self.context_limit = context_limit\n", - " # Each record: {\"msg\": {...}, \"meta\": {...}}\n", - " self._records: deque[Dict[str, Dict[str, Any]]] = deque()\n", - " self._lock = asyncio.Lock()\n", - " self.session_id = session_id or \"default\"\n", " self.summarizer = summarizer\n", + " self.session_id = session_id or \"default\"\n", "\n", - " # --------- public API used by your runner ---------\n", + " self._records: deque[Record] = deque()\n", + " self._lock = asyncio.Lock()\n", "\n", + " # --------- public API used by your runner ---------\n", " async def get_items(self, limit: Optional[int] = None) -> List[Dict[str, Any]]:\n", - " \"\"\"\n", - " Returns messages in a model-safe shape (no metadata).\n", - " Runner.run(..., session=self) should call this.\n", - " \"\"\"\n", + " \"\"\"Return model-safe messages only (no metadata).\"\"\"\n", " async with self._lock:\n", " data = list(self._records)\n", " msgs = [self._sanitize_for_model(rec[\"msg\"]) for rec in data]\n", " return msgs[-limit:] if limit else msgs\n", "\n", " async def add_items(self, items: List[Dict[str, Any]]) -> None:\n", + " \"\"\"Append new items and, if needed, summarize older turns.\"\"\"\n", + " # 1) Ingest items\n", " async with self._lock:\n", " for it in items:\n", " msg, meta = self._split_msg_and_meta(it)\n", " self._records.append({\"msg\": msg, \"meta\": meta})\n", - " need_summary, boundary_idx = self._should_summarize_locked()\n", "\n", - " if need_summary:\n", + " need_summary, boundary = self._summarize_decision_locked()\n", + "\n", + " # 2) No summarization needed → just normalize flags and exit\n", + " if not need_summary:\n", " async with self._lock:\n", - " prefix_records = list(itertools.islice(self._records, 0, boundary_idx))\n", - " prefix_msgs = [r[\"msg\"] for r in prefix_records]\n", + " self._normalize_synthetic_flags_locked()\n", + " return\n", "\n", - " user_shadow, assistant_summary = await self._summarize(prefix_msgs)\n", + " # 3) Prepare summary prefix (model-safe copy) outside the lock\n", + " async with self._lock:\n", + " snapshot = list(self._records)\n", + " prefix_msgs = [r[\"msg\"] for r in snapshot[:boundary]]\n", "\n", - " async with self._lock:\n", - " need_summary_now, boundary_idx_now = self._should_summarize_locked()\n", - " if not need_summary_now:\n", - " # normalize anyway if summarization got skipped\n", - " self._normalize_synthetic_flags_locked()\n", - " return\n", - "\n", - " suffix_records = list(itertools.islice(self._records, boundary_idx_now, None))\n", - " self._records.clear()\n", - "\n", - " # Synthetic summary pair keeps synthetic=True\n", - " self._records.extend([\n", - " {\n", - " \"msg\": {\"role\": \"user\", \"content\": user_shadow},\n", - " \"meta\": {\n", - " \"synthetic\": True,\n", - " \"kind\": \"history_summary_prompt\",\n", - " \"summary_for_turns\": f\"< all before idx {boundary_idx_now} >\",\n", - " },\n", + " user_shadow, assistant_summary = await self._summarize(prefix_msgs)\n", + "\n", + " # 4) Re-check and apply summary atomically\n", + " async with self._lock:\n", + " still_need, new_boundary = self._summarize_decision_locked()\n", + " if not still_need:\n", + " self._normalize_synthetic_flags_locked()\n", + " return\n", + "\n", + " snapshot = list(self._records)\n", + " suffix = snapshot[new_boundary:] # keep-last-N turns live here\n", + "\n", + " # Replace with: synthetic pair + suffix\n", + " self._records.clear()\n", + " self._records.extend([\n", + " {\n", + " \"msg\": {\"role\": \"user\", \"content\": user_shadow},\n", + " \"meta\": {\n", + " \"synthetic\": True,\n", + " \"kind\": \"history_summary_prompt\",\n", + " \"summary_for_turns\": f\"< all before idx {new_boundary} >\",\n", " },\n", - " {\n", - " \"msg\": {\"role\": \"assistant\", \"content\": assistant_summary},\n", - " \"meta\": {\n", - " \"synthetic\": True,\n", - " \"kind\": \"history_summary\",\n", - " \"summary_for_turns\": f\"< all before idx {boundary_idx_now} >\",\n", - " },\n", + " },\n", + " {\n", + " \"msg\": {\"role\": \"assistant\", \"content\": assistant_summary},\n", + " \"meta\": {\n", + " \"synthetic\": True,\n", + " \"kind\": \"history_summary\",\n", + " \"summary_for_turns\": f\"< all before idx {new_boundary} >\",\n", " },\n", - " ])\n", - " self._records.extend(suffix_records)\n", + " },\n", + " ])\n", + " self._records.extend(suffix)\n", "\n", - " # ✅ Ensure all real messages explicitly have synthetic=False\n", - " self._normalize_synthetic_flags_locked()\n", - " else:\n", - " # ✅ Even when we don't summarize, enforce the invariant\n", - " async with self._lock:\n", - " self._normalize_synthetic_flags_locked()\n", + " # Ensure all real user/assistant messages explicitly have synthetic=False\n", + " self._normalize_synthetic_flags_locked()\n", "\n", " async def pop_item(self) -> Optional[Dict[str, Any]]:\n", + " \"\"\"Pop the latest message (model-safe), if any.\"\"\"\n", " async with self._lock:\n", " if not self._records:\n", " return None\n", " rec = self._records.pop()\n", - " return dict(rec[\"msg\"]) # model-safe\n", + " return dict(rec[\"msg\"])\n", "\n", " async def clear_session(self) -> None:\n", + " \"\"\"Remove all records.\"\"\"\n", " async with self._lock:\n", " self._records.clear()\n", "\n", " def set_max_turns(self, n: int) -> None:\n", " \"\"\"\n", - " Back-compat: interpret as updating context_limit.\n", - " Ensures keep_last_n_turns <= context_limit.\n", + " Back-compat shim for old callers: update `context_limit`\n", + " and clamp `keep_last_n_turns` if needed.\n", " \"\"\"\n", " assert n >= 1\n", " self.context_limit = n\n", " if self.keep_last_n_turns > self.context_limit:\n", " self.keep_last_n_turns = self.context_limit\n", "\n", - " # --------- full-history (for debugging/analytics/observability) ---------\n", - "\n", - " # ✅ Backfill safeguard for older records that might lack the flag\n", - " def _normalize_synthetic_flags_locked(self) -> None:\n", - " for rec in self._records:\n", - " role = rec[\"msg\"].get(\"role\")\n", - " if role in (\"user\", \"assistant\") and \"synthetic\" not in rec[\"meta\"]:\n", - " rec[\"meta\"][\"synthetic\"] = False\n", + " # Full history (debugging/analytics/observability)\n", "\n", - " \n", " async def get_full_history(self, limit: Optional[int] = None) -> List[Dict[str, Any]]:\n", " \"\"\"\n", - " Returns combined history where each entry is:\n", - " { \"message\": {role, content[, name]}, \"metadata\": {...} }\n", - " This is NOT sent to the model; it's for your logs/UI/debugging.\n", + " Return combined history entries in the shape:\n", + " {\"message\": {role, content[, name]}, \"metadata\": {...}}\n", + " This is NOT sent to the model; for logs/UI/debugging only.\n", " \"\"\"\n", " async with self._lock:\n", " data = list(self._records)\n", " out = [{\"message\": dict(rec[\"msg\"]), \"metadata\": dict(rec[\"meta\"])} for rec in data]\n", " return out[-limit:] if limit else out\n", "\n", - " # Backwards-compatible alias if you were using this name before\n", + " # Back-compat alias\n", " async def get_items_with_metadata(self, limit: Optional[int] = None) -> List[Dict[str, Any]]:\n", " return await self.get_full_history(limit)\n", "\n", - " # --------- helpers ---------\n", + " # Internals\n", "\n", " def _split_msg_and_meta(self, it: Dict[str, Any]) -> Tuple[Dict[str, Any], Dict[str, Any]]:\n", + " \"\"\"\n", + " Split input into (msg, meta):\n", + " - msg keeps only _ALLOWED_MSG_KEYS; if role/content missing, default them.\n", + " - everything else goes under meta (including nested \"metadata\" if provided).\n", + " - default synthetic=False for real user/assistant unless explicitly set.\n", + " \"\"\"\n", " msg = {k: v for k, v in it.items() if k in self._ALLOWED_MSG_KEYS}\n", " extra = {k: v for k, v in it.items() if k not in self._ALLOWED_MSG_KEYS}\n", " meta = dict(extra.pop(\"metadata\", {}))\n", " meta.update(extra)\n", "\n", - " if \"role\" not in msg or \"content\" not in msg:\n", - " msg.setdefault(\"role\", \"user\")\n", - " msg.setdefault(\"content\", str(it))\n", + " msg.setdefault(\"role\", \"user\")\n", + " msg.setdefault(\"content\", str(it))\n", "\n", - " # ✅ Default synthetic flag for real (non-summarized) messages\n", " role = msg.get(\"role\")\n", " if role in (\"user\", \"assistant\") and \"synthetic\" not in meta:\n", " meta[\"synthetic\"] = False\n", " return msg, meta\n", "\n", - " def _sanitize_for_model(self, msg: Dict[str, Any]) -> Dict[str, Any]:\n", - " \"\"\"\n", - " Strictly keep only allowed keys for model input.\n", - " \"\"\"\n", - " return {k: v for k, v in msg.items() if k in self._ALLOWED_MSG_KEYS}\n", + " @staticmethod\n", + " def _sanitize_for_model(msg: Dict[str, Any]) -> Dict[str, Any]:\n", + " \"\"\"Drop anything not allowed in model calls.\"\"\"\n", + " return {k: v for k, v in msg.items() if k in SummarizingSession._ALLOWED_MSG_KEYS}\n", "\n", - " def _is_user(self, rec: Dict[str, Dict[str, Any]]) -> bool:\n", - " return rec[\"msg\"].get(\"role\") == \"user\"\n", + " @staticmethod\n", + " def _is_real_user_turn_start(rec: Record) -> bool:\n", + " \"\"\"True if record starts a *real* user turn (role=='user' and not synthetic).\"\"\"\n", + " return (\n", + " rec[\"msg\"].get(\"role\") == \"user\"\n", + " and not rec[\"meta\"].get(\"synthetic\", False)\n", + " )\n", "\n", - " def _should_summarize_locked(self) -> Tuple[bool, int]:\n", + " def _summarize_decision_locked(self) -> Tuple[bool, int]:\n", " \"\"\"\n", - " Trigger summarization if the number of *real* user turns exceeds `context_limit`.\n", + " Decide whether to summarize and compute the boundary index.\n", "\n", - " Keep the last `keep_last_n_turns` *turns* verbatim:\n", - " find the earliest index among the last `keep_last_n_turns` real user messages;\n", - " everything before that index becomes the summarization prefix.\n", + " Returns:\n", + " (need_summary, boundary_idx)\n", "\n", - " Returns: (need_summary: bool, boundary_idx: int)\n", + " If need_summary:\n", + " • boundary_idx is the earliest index among the last `keep_last_n_turns`\n", + " *real* user-turn starts.\n", + " • Everything before boundary_idx becomes the summary prefix.\n", " \"\"\"\n", - " # Collect indices of real user messages (turn starts)\n", - " user_idxs: List[int] = []\n", - " for i, rec in enumerate(self._records):\n", - " if self._is_user(rec) and not rec[\"meta\"].get(\"synthetic\", False):\n", - " user_idxs.append(i)\n", + " user_starts: List[int] = [\n", + " i for i, rec in enumerate(self._records) if self._is_real_user_turn_start(rec)\n", + " ]\n", + " real_turns = len(user_starts)\n", "\n", - " real_turns = len(user_idxs)\n", + " # Not over the limit → nothing to do\n", " if real_turns <= self.context_limit:\n", " return False, -1\n", "\n", - " # Determine boundary according to \"turns\"\n", + " # Keep zero turns verbatim → summarize everything\n", " if self.keep_last_n_turns == 0:\n", - " # summarize everything; keep no turns verbatim\n", - " boundary = len(self._records)\n", - " else:\n", - " if len(user_idxs) < self.keep_last_n_turns:\n", - " return False, -1 # defensive; should not happen due to the check above\n", - " # earliest index among the last N real user-turn starts\n", - " boundary = user_idxs[-self.keep_last_n_turns]\n", - "\n", - " # If boundary is 0 and we intend to keep >=1 turn, there's nothing before to summarize.\n", - " if boundary <= 0 and self.keep_last_n_turns > 0:\n", + " return True, len(self._records)\n", + "\n", + " # Otherwise, keep the last N turns; summarize everything before the earliest of those\n", + " if len(user_starts) < self.keep_last_n_turns:\n", + " return False, -1 # defensive (shouldn't happen given the earlier check)\n", + "\n", + " boundary = user_starts[-self.keep_last_n_turns]\n", + "\n", + " # If there is nothing before boundary, there is nothing to summarize\n", + " if boundary <= 0:\n", " return False, -1\n", "\n", " return True, boundary\n", "\n", + " def _normalize_synthetic_flags_locked(self) -> None:\n", + " \"\"\"Ensure all real user/assistant records explicitly carry synthetic=False.\"\"\"\n", + " for rec in self._records:\n", + " role = rec[\"msg\"].get(\"role\")\n", + " if role in (\"user\", \"assistant\") and \"synthetic\" not in rec[\"meta\"]:\n", + " rec[\"meta\"][\"synthetic\"] = False\n", + "\n", " async def _summarize(self, prefix_msgs: List[Dict[str, Any]]) -> Tuple[str, str]:\n", " \"\"\"\n", - " Adapter to your summarizer. Provide *model-safe* messages only.\n", + " Ask the configured summarizer to compress the given prefix.\n", + " Uses model-safe messages only. If no summarizer is configured,\n", + " returns a graceful fallback.\n", " \"\"\"\n", " if not self.summarizer:\n", - " # Fallback summary if no summarizer is configured\n", " return (\"Summarize the conversation we had so far.\", \"Summary unavailable.\")\n", - " # Only send role/content/name to the summarizer as well\n", " clean_prefix = [self._sanitize_for_model(m) for m in prefix_msgs]\n", " return await self.summarizer.summarize(clean_prefix)\n" ] @@ -1072,7 +1020,7 @@ }, { "cell_type": "code", - "execution_count": 244, + "execution_count": 291, "id": "6d867c0b", "metadata": {}, "outputs": [], @@ -1086,18 +1034,10 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 292, "id": "a8d22531", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "2\n" - ] - } - ], + "outputs": [], "source": [ "\n", "# Example flow\n", @@ -1117,7 +1057,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 293, "id": "e2e1b482", "metadata": {}, "outputs": [], @@ -1128,7 +1068,7 @@ }, { "cell_type": "code", - "execution_count": 246, + "execution_count": 294, "id": "9f229de7", "metadata": {}, "outputs": [ @@ -1137,7 +1077,7 @@ "text/plain": [ "[{'role': 'user', 'content': 'Summarize the conversation we had so far.'},\n", " {'role': 'assistant',\n", - " 'content': \"• Product & Environment:\\n - Router with firmware v1.0.3, Windows 10, US-based, Premium customer.\\n\\n• Reported Issue:\\n - Router won't connect to the internet.\\n\\n• Steps Tried & Results:\\n - Followed FAQs for troubleshooting; no resolution.\\n - Checked firmware version: v1.0.3; issue persists.\\n - Performed factory reset; encountered error 42.\\n\\n• Identifiers:\\n - None provided.\\n\\n• Timeline Milestones:\\n - Initial troubleshooting via FAQs → Firmware check → Factory reset → Error 42.\\n\\n• Tool Performance Insights:\\n - Factory reset unsuccessful in resolving connection issue; led to error 42.\\n\\n• Current Status & Blockers:\\n - Connection issue unresolved; error 42 after reset is blocking progress.\\n\\n• Next Recommended Step:\\n - Install new firmware version compatible with device.\"},\n", + " 'content': '• Product & Environment:\\n - Router with Firmware v1.0.3, Windows 10, based in the US.\\n\\n• Reported Issue:\\n - Router fails to connect.\\n\\n• Steps Tried & Results:\\n - Checked FAQs: No resolution.\\n - Checked firmware version: v1.0.3, problem persists.\\n - Factory reset: Resulted in error 42.\\n\\n• Identifiers:\\n - Premium customer (no specific identifier provided).\\n\\n• Timeline Milestones:\\n - Initial troubleshooting via FAQs.\\n - Firmware check (before factory reset).\\n - Factory reset → Error 42.\\n\\n• Tool Performance Insights:\\n - Firmware version check successful.\\n - Factory reset resulted in new error (42).\\n\\n• Current Status & Blockers:\\n - Connection issue unresolved; error 42 is the immediate blocker.\\n\\n• Next Recommended Step:\\n - Install a new firmware update.'},\n", " {'role': 'user', 'content': 'I tried but I got another error now.'},\n", " {'role': 'assistant',\n", " 'content': 'Can you please provide me with the error code?'},\n", @@ -1146,7 +1086,7 @@ " {'role': 'assistant', 'content': 'Are you connected to the internet?'}]" ] }, - "execution_count": 246, + "execution_count": 294, "metadata": {}, "output_type": "execute_result" } @@ -1157,8 +1097,8 @@ }, { "cell_type": "code", - "execution_count": 213, - "id": "afc57803", + "execution_count": 295, + "id": "7bb3457c", "metadata": {}, "outputs": [ { @@ -1166,28 +1106,33 @@ "output_type": "stream", "text": [ "• Product & Environment:\n", - " - Windows 10, router (specific model UNVERIFIED).\n", + " - Router with Firmware v1.0.3, Windows 10, based in the US.\n", "\n", "• Reported Issue:\n", - " - Router won't connect.\n", + " - Router fails to connect.\n", "\n", "• Steps Tried & Results:\n", - " - Used FAQs for troubleshooting; no resolution.\n", + " - Checked FAQs: No resolution.\n", + " - Checked firmware version: v1.0.3, problem persists.\n", + " - Factory reset: Resulted in error 42.\n", "\n", "• Identifiers:\n", - " - Premium customer; based in the US (no specific identifiers provided).\n", + " - Premium customer (no specific identifier provided).\n", "\n", "• Timeline Milestones:\n", - " - Third interaction reported by user.\n", + " - Initial troubleshooting via FAQs.\n", + " - Firmware check (before factory reset).\n", + " - Factory reset → Error 42.\n", "\n", "• Tool Performance Insights:\n", - " - User FAQs insufficient for resolution.\n", + " - Firmware version check successful.\n", + " - Factory reset resulted in new error (42).\n", "\n", "• Current Status & Blockers:\n", - " - Connection issue unresolved; firmware version not yet checked.\n", + " - Connection issue unresolved; error 42 is the immediate blocker.\n", "\n", "• Next Recommended Step:\n", - " - Verify and update the router firmware version.\n" + " - Install a new firmware update.\n" ] } ], @@ -1329,7 +1274,7 @@ "source": [ "## Evals\n", "\n", - "At the end of the day, **evals is all you need** for context engineering. The key question to ask is: *how do we know the model isn’t “losing context” or \"confusing context\"?*\n", + "Ultimately, **evals is all you need** for context engineering too. The key question to ask is: *how do we know the model isn’t “losing context” or \"confusing context\"?*\n", "\n", "While a full cookbook around memory could stand on its own in the future, here are some lightweight evaluation harness ideas to start with:\n", "\n", From 8922b5c14cd032d49a33e0b90e9043053e46a7cc Mon Sep 17 00:00:00 2001 From: emre-openai Date: Sun, 7 Sep 2025 23:02:11 -0700 Subject: [PATCH 08/11] registry added --- authors.yaml | 5 +++++ examples/agents_sdk/session_memory.ipynb | 2 +- registry.yaml | 8 ++++++++ 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/authors.yaml b/authors.yaml index d84aa18f87..090c4642e3 100644 --- a/authors.yaml +++ b/authors.yaml @@ -7,6 +7,11 @@ rajpathak-openai: website: "https://www.linkedin.com/in/rajpathakopenai/" avatar: "https://avatars.githubusercontent.com/u/208723614?s=400&u=c852eed3be082f7fbd402b5a45e9b89a0bfed1b8&v=4" +emreokcular: + name: "Emre Okcular" + website: "https://www.linkedin.com/in/emreokcular/" + avatar: "https://avatars.githubusercontent.com/u/26163154?v=4" + chelseahu-openai: name: "Chelsea Hu" website: "https://www.linkedin.com/in/chelsea-tsaiszuhu/" diff --git a/examples/agents_sdk/session_memory.ipynb b/examples/agents_sdk/session_memory.ipynb index 995a87c1ac..36b57febdc 100644 --- a/examples/agents_sdk/session_memory.ipynb +++ b/examples/agents_sdk/session_memory.ipynb @@ -5,7 +5,7 @@ "id": "3bcfc573", "metadata": {}, "source": [ - "# Context Engineering: Short-Term Memory Management with Sessions from OpenAI Agents SDK " + "# Context Engineering: Short-Term Memory Management with Sessions from OpenAI Agents SDK" ] }, { diff --git a/registry.yaml b/registry.yaml index 862aa2a9de..254d7ae0ab 100644 --- a/registry.yaml +++ b/registry.yaml @@ -4,6 +4,14 @@ # should build pages for, and indicates metadata such as tags, creation date and # authors for each page. +- title: Context Engineering - Short-Term Memory Management with Sessions from OpenAI Agents SDK + path: examples/agents_sdk/session_memory.ipynb + date: 2025-09-09 + authors: + - emreokcular + tags: + - agents-sdk + - title: Using Evals API on Image Inputs path: examples/evaluation/use-cases/EvalsAPI_Image_Inputs.ipynb date: 2025-07-15 From 33f74cda8d6e1e6e89b00c4c89a0a59de79def73 Mon Sep 17 00:00:00 2001 From: emre-openai Date: Sun, 7 Sep 2025 23:03:17 -0700 Subject: [PATCH 09/11] tweak --- examples/agents_sdk/session_memory.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/agents_sdk/session_memory.ipynb b/examples/agents_sdk/session_memory.ipynb index 36b57febdc..65a81439a3 100644 --- a/examples/agents_sdk/session_memory.ipynb +++ b/examples/agents_sdk/session_memory.ipynb @@ -5,7 +5,7 @@ "id": "3bcfc573", "metadata": {}, "source": [ - "# Context Engineering: Short-Term Memory Management with Sessions from OpenAI Agents SDK" + "# Context Engineering - Short-Term Memory Management with Sessions from OpenAI Agents SDK" ] }, { From f750cfa09d33b539ee9833bbbe2a6af15e6ba539 Mon Sep 17 00:00:00 2001 From: emre-openai Date: Wed, 10 Sep 2025 01:09:47 -0700 Subject: [PATCH 10/11] fixing images --- examples/agents_sdk/session_memory.ipynb | 2 +- images/summarizingSession.jpg | Bin 46764 -> 47238 bytes 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/agents_sdk/session_memory.ipynb b/examples/agents_sdk/session_memory.ipynb index 65a81439a3..28b0db42e8 100644 --- a/examples/agents_sdk/session_memory.ipynb +++ b/examples/agents_sdk/session_memory.ipynb @@ -989,7 +989,7 @@ "id": "214228c8", "metadata": {}, "source": [ - "![Contxt Trimming in Session](../../images/SummarizingSession.jpg)" + "![Context Trimming in Session](../../images/summarizingSession.jpg)" ] }, { diff --git a/images/summarizingSession.jpg b/images/summarizingSession.jpg index ddd68580fbbbd5ce46e0fd5612be10771e8caf7b..a238a25cd07fc4361565a3d35f0d6ccece272899 100644 GIT binary patch literal 47238 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 From f33da144225f4ca6031ba301a709fa541f8fc162 Mon Sep 17 00:00:00 2001 From: emre-openai Date: Wed, 10 Sep 2025 12:46:37 -0700 Subject: [PATCH 11/11] updated based on the PR comment --- examples/agents_sdk/session_memory.ipynb | 119 ++++++++++++++++++++--- 1 file changed, 107 insertions(+), 12 deletions(-) diff --git a/examples/agents_sdk/session_memory.ipynb b/examples/agents_sdk/session_memory.ipynb index 28b0db42e8..85eccf6a90 100644 --- a/examples/agents_sdk/session_memory.ipynb +++ b/examples/agents_sdk/session_memory.ipynb @@ -13,9 +13,11 @@ "id": "eeab798a", "metadata": {}, "source": [ - "AI agents often operate in **long-running, multi-turn interactions**, where keeping the right balance of context is critical. If too much is carried forward, the model risks distraction, inefficiency, or outright failure. If too little is preserved, the agent loses coherence. This guide focuses on two proven context management techniques—**trimming** and **compression**—to keep agents fast, reliable, and cost-efficient.\n", + "AI agents often operate in **long-running, multi-turn interactions**, where keeping the right balance of **context** is critical. If too much is carried forward, the model risks distraction, inefficiency, or outright failure. If too little is preserved, the agent loses coherence. \n", "\n", - "In this cookbook, we’ll explore how to **manage context effectively using the `Session` object from the [OpenAI Agents SDK](https://github.com/openai/openai-agents-python)**.\n", + "Here, context refers to the total window of tokens (input + output) that the model can attend to at once. For [GPT-5](https://platform.openai.com/docs/models/gpt-5), this capacity is up to 272k input tokens and 128k output tokens but even such a large window can be overwhelmed by uncurated histories, redundant tool results, or noisy retrievals. This makes context management not just an optimization, but a necessity.\n", + "\n", + "In this cookbook, we’ll explore how to **manage context effectively using the `Session` object from the [OpenAI Agents SDK](https://github.com/openai/openai-agents-python)**, focusing on two proven context management techniques—**trimming** and **compression**—to keep agents fast, reliable, and cost-efficient.\n", "\n", "#### Why Context Management Matters\n", "\n", @@ -35,6 +37,18 @@ "![Memory Comparison in AI Agents](../../images/memory_comparison.jpg)" ] }, + { + "cell_type": "markdown", + "id": "4ae8fdc3", + "metadata": {}, + "source": [ + "The [OpenAI Responses API](https://platform.openai.com/docs/api-reference/responses/create#responses-create-previous_response_id) includes **basic memory support** through built-in state and message chaining with `previous_response_id`.\n", + "\n", + "You can continue a conversation by passing the prior response’s `id` as `previous_response_id`, or you can manage context manually by collecting outputs into a list and resubmitting them as the `input` for the next response.\n", + "\n", + "What you don’t get is **automatic memory management**. That’s where the **Agents SDK** comes in. It provides [session memory](https://openai.github.io/openai-agents-python/sessions/) on top of Responses, so you no longer need to manually append `response.output` or track IDs yourself. The session becomes the **memory object**: you simply call `session.run(\"...\")` repeatedly, and the SDK handles context length, history, and continuity—making it far easier to build coherent, multi-turn agents." + ] + }, { "cell_type": "markdown", "id": "7068564c", @@ -50,15 +64,76 @@ "\n", "#### Techniques Covered\n", "\n", - "To address these challenges, we introduce two concrete approaches using OpenAI Agents SDK:\n", + "To address these challenges, we introduce two separate concrete approaches using OpenAI Agents SDK:\n", + "\n", + "- **Context Trimming** – dropping older turns while keeping the last N turns.\n", + " - **Pros**\n", + "\n", + " * **Deterministic & simple:** No summarizer variability; easy to reason about state and to reproduce runs.\n", + " * **Zero added latency:** No extra model calls to compress history.\n", + " * **Fidelity for recent work:** Latest tool results, parameters, and edge cases stay verbatim—great for debugging.\n", + " * **Lower risk of “summary drift”:** You never reinterpret or compress facts.\n", + "\n", + " **Cons**\n", "\n", - "1. **Trimming Messages** – dropping older turns while keeping the last N turns.\n", - "2. **Summarizing Messages** – compressing prior exchanges into structured, shorter representations.\n", + " * **Forgets long-range context abruptly:** Important earlier constraints, IDs, or decisions can vanish once they scroll past N.\n", + " * **User experience “amnesia”:** Agent can appear to “forget” promises or prior preferences midway through long sessions.\n", + " * **Wasted signal:** Older turns may contain reusable knowledge (requirements, constraints) that gets dropped.\n", + " * **Token spikes still possible:** If a recent turn includes huge tool payloads, your last-N can still blow up the context.\n", "\n", + " - **Best when**\n", "\n", + " - Your tasks in the conversation is indepentent from each other with non-overlapping context that does not reuqire carrying previous details further.\n", + " - You need predictability, easy evals, and low latency (ops automations, CRM/API actions).\n", + " - The conversation’s useful context is local (recent steps matter far more than distant history).\n", + "\n", + "- **Context Summarization** – compressing prior messages(assistant, user, tools, etc.) into structured, shorter summaries injected into the conversation history.\n", + "\n", + " - **Pros**\n", + "\n", + " * **Retains long-range memory compactly:** Past requirements, decisions, and rationales persist beyond N.\n", + " * **Smoother UX:** Agent “remembers” commitments and constraints across long sessions.\n", + " * **Cost-controlled scale:** One concise summary can replace hundreds of turns.\n", + " * **Searchable anchor:** A single synthetic assistant message becomes a stable “state of the world so far.”\n", + "\n", + " **Cons**\n", + "\n", + " * **Summarization loss & bias:** Details can be dropped or misweighted; subtle constraints may vanish.\n", + " * **Latency & cost spikes:** Each refresh adds model work (and potentially tool-trim logic).\n", + " * **Compounding errors:** If a bad fact enters the summary, it can **poison** future behavior (“context poisoning”).\n", + " * **Observability complexity:** You must log summary prompts/outputs for auditability and evals.\n", + "\n", + " - **Best when**\n", + "\n", + " - You have use cases where your tasks needs context collected accross the flow such as planning/coaching, RAG-heavy analysis, policy Q&A.\n", + " - You need continuity over long horizons and carry the important details further to solve related tasks.\n", + " - Sessions exceed N turns but must preserve decisions, IDs, and constraints reliably.\n", "
" ] }, + { + "cell_type": "markdown", + "id": "3765f2b8", + "metadata": {}, + "source": [ + "**Quick comparison**" + ] + }, + { + "cell_type": "markdown", + "id": "940e5bf7", + "metadata": {}, + "source": [ + "| Dimension | **Trimming (last-N turns)** | **Summarizing (older → generated summary)** |\n", + "| ----------------- | ------------------------------- | ------------------------------------ |\n", + "| Latency / Cost | Lowest (no extra calls) | Higher at summary refresh points |\n", + "| Long-range recall | Weak (hard cut-off) | Strong (compact carry-forward) |\n", + "| Risk type | Context loss | Context distortion/poisoning |\n", + "| Observability | Simple logs | Must log summary prompts/outputs |\n", + "| Eval stability | High | Needs robust summary evals |\n", + "| Best for | Tool-heavy ops, short workflows | Analyst/concierge, long threads |\n" + ] + }, { "cell_type": "markdown", "id": "fc613968", @@ -68,7 +143,7 @@ "\n", "Before running this cookbook, you must set up the following accounts and complete a few setup actions. These prerequisites are essential to interact with the APIs used in this project.\n", "\n", - "#### Step0: OpenAI Account\n", + "#### Step0: OpenAI Account and `OPENAI_API_KEY`\n", "\n", "- **Purpose:** \n", " You need an OpenAI account to access language models and use the Agents SDK featured in this cookbook.\n", @@ -77,6 +152,26 @@ " [Sign up for an OpenAI account](https://openai.com) if you don’t already have one. Once you have an account, create an API key by visiting the [OpenAI API Keys page](https://platform.openai.com/api-keys)." ] }, + { + "cell_type": "markdown", + "id": "094205e7", + "metadata": {}, + "source": [ + "**Before running the workflow, set your environment variables:**\n", + "\n", + "```\n", + "# Your openai key\n", + "os.environ[\"OPENAI_API_KEY\"] = \"sk-proj-...\"\n", + "```\n", + "\n", + "Alternatively, you can set your OpenAI API key for use by the agents via the `set_default_openai_key` function by importing agents library .\n", + "\n", + "```\n", + "from agents import set_default_openai_key\n", + "set_default_openai_key(\"YOUR_API_KEY\")\n", + "```" + ] + }, { "cell_type": "markdown", "id": "3cd9a109", @@ -84,7 +179,7 @@ "source": [ "#### Step1: Install the Required Libraries\n", "\n", - "Below we install the `openai-agents` library (the [OpenAI Agents SDK](https://github.com/openai/openai-agents-python)" + "Below we install the `openai-agents` library ([OpenAI Agents SDK](https://github.com/openai/openai-agents-python))" ] }, { @@ -130,7 +225,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "id": "fe54469a", "metadata": {}, "outputs": [ @@ -201,7 +296,7 @@ "id": "b8074e05", "metadata": {}, "source": [ - "## 1. Context Trimming " + "## Context Trimming" ] }, { @@ -577,7 +672,7 @@ "id": "d6fa349f", "metadata": {}, "source": [ - "## 2. Context Summarization " + "## Context Summarization" ] }, { @@ -1150,12 +1245,12 @@ }, { "cell_type": "code", - "execution_count": 248, + "execution_count": null, "id": "5448ce93", "metadata": {}, "outputs": [], "source": [ - "full_history = await session.get_items_with_metadata()\n" + "full_history = await session.get_items_with_metadata()" ] }, {