diff --git a/content/develop/ai/context-engine/_index.md b/content/develop/ai/context-engine/_index.md index 635742a8a4..83103d1552 100644 --- a/content/develop/ai/context-engine/_index.md +++ b/content/develop/ai/context-engine/_index.md @@ -21,6 +21,7 @@ Redis Iris eliminates the infrastructure burden of building context-aware AI age {{< tile-card icon="images/dev/icons/icon-redis-agent-memory-64-duotone.png" title="Agent Memory" description="Persistent short-term and long-term memory across agent interactions" url="/develop/ai/context-engine/agent-memory" >}} {{< tile-card icon="images/dev/icons/icon-redis-context-retriever-64-duotone.png" title="Context Retriever" description="Governed, schema-first data access tools for agents" url="/develop/ai/context-engine/context-retriever" >}} {{< tile-card icon="images/dev/icons/icon-redis-iris-64-duotone.png" title="Agent Playbooks" description="Publish reusable answers and procedures for agents" url="/develop/ai/context-engine/agent-playbook" >}} + {{< tile-card icon="images/dev/icons/icon-redis-iris-64-duotone.png" title="Lens" description="Debug agent answers by tracing the context behind each turn" url="/develop/ai/context-engine/lens" >}} ## What is Redis Iris? diff --git a/content/develop/ai/context-engine/lens/AGENTS.md b/content/develop/ai/context-engine/lens/AGENTS.md new file mode 100644 index 0000000000..3ad21785d6 --- /dev/null +++ b/content/develop/ai/context-engine/lens/AGENTS.md @@ -0,0 +1,54 @@ +# Lens docs — conventions for AI-assisted editing + +These pages document **Lens**, the Redis Iris observability tool for AI agents. Read the +repository-root `AGENTS.md` first for style and site mechanics. This file adds terminology, +disclosure, and framing rules for this directory, and wins where the two conflict. + +## Product names + +| Use | Not | Note | +| --- | --- | --- | +| Lens | Iris Lens, Redis Lens, Agent Lens | Every mention, including titles. | +| Context Gateway | Context Retriever | In prose. API literals such as `GetContextRetrieverResult` stay as they are. | +| Agent Memory, Agent Playbooks | RAM, Playbook service | The Iris services Lens traces. | +| Iris services | Iris products | Agent Memory, Agent Playbooks, and Context Gateway as a group, matching the other Iris pages. | +| preview | Beta | Lens availability. | + +`redis-iris` (`redis_iris.lens`) is the instrumentation package. `redis-agent-lens` +(`redis_agent_lens`) is the read client. They read different key variables: +`IRIS_LENS_API_KEY` for export, `LENS_API_KEY` for reads. Never swap them. + +## The source is private + +Lens source is private, so a file path, repository URL, or "see the implementation" +pointer is a dead end for readers. Do not link to source code. + +Leave out implementation internals: storage design, internal service names, internal span +and attribute names (`iris.lens.*`, `iris.component.*`), cursor encoding, and design +history. Document the public SDK calls, settings, and API fields. + +## Present what Lens does + +Say where a capability is present, not where it's missing. Write "Playbook discovery +results include their similarity score", not "Agent Memory recalls have no score". Name +the cases that have a capability, so readers don't assume it applies everywhere. + +Leave unshipped and deferred features out entirely. The availability banner is the only +preview notice; don't add lists of gaps or "coming soon" notes. + +## Deployments + +Lens runs on Redis Cloud and self-managed. Pages are deployment-neutral: the same steps +work on both. Name a deployment only where behavior differs. + +Readers get endpoints and keys from their administrator. Until the operator pages exist, +say so without linking to them, and mark each handoff with a TODO comment. + +## Terms + +- A **turn** is one agent exchange and one trace. Its trace ID is the turn ID. +- A **session** groups turns, and a **thread** is a conversation within a session. Readers + choose session and thread IDs. +- A **step** is one recorded operation in a turn, such as a memory recall or a model call. + Use "step" in prose; "span" only where the reader works with OpenTelemetry directly. +- **Fetched** is what a source returned. **Used** is what entered the prompt. diff --git a/content/develop/ai/context-engine/lens/CLAUDE.md b/content/develop/ai/context-engine/lens/CLAUDE.md new file mode 100644 index 0000000000..43c994c2d3 --- /dev/null +++ b/content/develop/ai/context-engine/lens/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md diff --git a/content/develop/ai/context-engine/lens/_index.md b/content/develop/ai/context-engine/lens/_index.md new file mode 100644 index 0000000000..99776e73b1 --- /dev/null +++ b/content/develop/ai/context-engine/lens/_index.md @@ -0,0 +1,61 @@ +--- +Title: Lens +alwaysopen: false +categories: +- docs +- develop +- ai +description: Debug AI agent answers by tracing the context behind each turn, from retrieval to prompt. +linkTitle: Lens +hideListLinks: true +weight: 27 +bannerText: Lens is currently available in preview. Features and behavior are subject to change. +bannerChildren: true +--- + +## What is Lens? + +Lens is a Redis Iris observability tool that traces your AI agent's turns. A turn is one agent exchange, such as answering a single message. For each turn, Lens records what each source returned, such as a memory recall or a retrieval, and what entered the prompt. It also records which model and tool calls ran, how long each step took, and how the turn ended. + +When your agent gives a wrong answer, Lens shows where it went wrong. For example, you might see that the right memory was recalled but dropped from the prompt, or that a retrieval came back empty. + +
+ {{< tile-card icon="images/icon_logo/icon-data-structures-64-midnight.png" title="Concepts" description="Learn how sessions, turns, steps, and fetched and used context work" url="/develop/ai/context-engine/lens/concepts" >}} + {{< tile-card icon="images/dev/icons/icon-redis-code-64-duotone.png" title="Quickstart" description="Trace your first turn and read it back" url="/develop/ai/context-engine/lens/quickstart" >}} + {{< tile-card icon="images/dev/icons/icon-redis-code-64-duotone.png" title="Agent integration" description="Trace agents built with LangGraph, the OpenAI Agents SDK, and more" url="/develop/ai/context-engine/lens/agent-integration" >}} +
+ +## Why use Lens? + +- **See why an agent answered the way it did:** Compare what each source returned with what entered the prompt, turn by turn. +- **Iris services trace themselves:** Agent Memory, Agent Playbooks, and Context Gateway calls appear in the turn with no extra code. +- **Model calls traced automatically:** Calls through OpenAI, Anthropic, Amazon Bedrock, and LangChain clients appear with their prompts, completions, and token counts. +- **Built on OpenTelemetry:** Lens joins the tracer your application already uses, and Iris service steps reach your existing observability tools as well. +- **You control content capture:** Capture prompts and retrieved text for debugging, or keep only IDs, hashes, and counts. +- **Background export:** Lens exports in the background, so your agent never waits for it. If an export fails, the error stays out of your agent's code. + +## How it works + +1. Your agent opens a turn for each message with the Redis Iris software development kit (SDK). +1. The SDK records each step in the turn: retrievals, memory recalls, prompt assembly, model calls, tool calls, and Iris service calls. +1. Lens assembles the turn and groups it into sessions by agent. +1. You inspect the turn in the Lens user interface (UI) or query it with the read application programming interface (API). + +![Your agent and the Iris services it calls record steps through the Redis Iris SDK. The SDK exports each turn to Lens, where you inspect it in the Lens UI or query it with the read API.](/images/ai/context-engine/lens-how-it-works.svg) +{width="700"} + +## Get started + +1. Get your Lens endpoints and keys from your administrator. + +1. [Trace your first turn](/content/develop/ai/context-engine/lens/quickstart.md). + +## Use Lens + +- [Instrument an agent](/content/develop/ai/context-engine/lens/instrument.md): record turns, retrievals, prompt assembly, and custom steps. +- [Integrate with an agent framework](/content/develop/ai/context-engine/lens/agent-integration.md): trace LangGraph, OpenAI Agents SDK, Google Agent Development Kit (ADK), and your own loop. +- [Inspect sessions](/content/develop/ai/context-engine/lens/inspect-sessions.md): find a session and read its turns in the Lens UI. +- [Configure Lens](/content/develop/ai/context-engine/lens/configure.md): settings, keys, content capture, and OpenTelemetry. +- [Query sessions and turns](/content/develop/ai/context-engine/lens/read-api.md): read turns from code. +- [Limits and errors](/content/develop/ai/context-engine/lens/limits.md): request limits, export behavior, and troubleshooting. +- [API reference](/content/develop/ai/context-engine/lens/api-reference.md): the read API operations and data models. diff --git a/content/develop/ai/context-engine/lens/agent-integration.md b/content/develop/ai/context-engine/lens/agent-integration.md new file mode 100644 index 0000000000..ded03c35d8 --- /dev/null +++ b/content/develop/ai/context-engine/lens/agent-integration.md @@ -0,0 +1,101 @@ +--- +alwaysopen: false +categories: +- docs +- develop +- ai +description: Trace LangGraph, OpenAI Agents SDK, Google ADK, and your own agent loop with Lens, across Agent Memory, Agent Playbooks, and Context Gateway. +hideListLinks: true +linktitle: Agent integration +title: Integrate Lens with agent frameworks +weight: 20 +--- + +To trace an agent run in Lens, open a turn around it. Each step the agent takes inside the run, such as a memory recall or a model call, is recorded in that turn. For more about turns and steps, see [Lens concepts](/content/develop/ai/context-engine/lens/concepts.md). + +Before you start, get the Lens connection values from the [quickstart](/content/develop/ai/context-engine/lens/quickstart.md#configure-the-sdk). The framework examples also need a model provider key, such as `OPENAI_API_KEY`. + +## Where the turn opens + +| Framework | Open the turn around | +| --------- | -------------------- | +| LangGraph | `app.invoke()` or `app.ainvoke()`. With an Iris service attached, `attach()` opens the turn for you. | +| OpenAI Agents SDK | `Runner.run()` | +| Google Agent Development Kit (ADK) | `runner.run_async()` | +| Your own loop | The handler for one message | + +To trace model calls inside the turn, install `redis-iris[llm-tracing]` and call `lens.init()` at startup. See [Trace model calls](/content/develop/ai/context-engine/lens/instrument.md#trace-model-calls). + +## Trace one turn across Iris services + +When your agent uses Agent Memory, Agent Playbooks, and Context Gateway, a single Lens turn shows how each Iris service contributed to the agent's response. This example needs the connection values for each Iris service. + +`workflows.turn()` runs each service's context step before the model and its write-back after. Every service call joins the turn: + +{{< clients-example set="iris_lens_iris_products" step="iris_products_turn" description="" lang_filter="Python (Iris)" footer="hide" />}} + +The turn shows the memory recall and the Playbook discovery, then any Context Gateway tool calls your agent makes, and the write-back. In asynchronous code, `workflows.aturn()` runs the services' context steps at the same time. The outer `lens.turn()` sets the outcome. When a turn is already open, `workflows.turn()` and `attach()` add their steps to it rather than starting another. + +## LangGraph + +Install LangGraph and the LangChain OpenAI integration: + +```bash +python -m pip install 'redis-iris[llm-tracing]' langgraph langchain-openai +``` + +Wrap the graph run in `lens.turn()`. LangGraph's thread ID identifies the conversation, so pass it as the Lens session ID: + +{{< clients-example set="iris_lens_langgraph" step="langgraph_turn" description="" lang_filter="Python (Iris)" footer="hide" />}} + +Each model call in the graph joins the turn with its token counts. + +### Attach an Iris service + +When the graph uses Agent Memory or Agent Playbooks, `attach()` adds the service's context before your first node and its write-back after your last. With Lens on, every run of an attached graph is one turn. The session comes from `thread_id` and the user from `user_id` in the run's configuration: + +{{< clients-example set="iris_lens_langgraph_attach" step="langgraph_attach" description="" lang_filter="Python (Iris)" footer="hide" />}} + +`attach()` needs the `langgraph` extra: `python -m pip install 'redis-iris[langgraph]'`. + +## OpenAI Agents SDK + +Install the OpenAI Agents SDK: + +```bash +python -m pip install 'redis-iris[llm-tracing]' openai-agents +``` + +Wrap `Runner.run()` in `lens.turn()`: + +{{< clients-example set="iris_lens_openai_agents" step="openai_agents_turn" description="" lang_filter="Python (Iris)" footer="hide" />}} + +The OpenAI Agents SDK calls models through the OpenAI client, so each model call joins the turn with its token counts. + +## Google ADK + +Install the Google ADK: + +```bash +python -m pip install redis-iris google-adk +``` + +Wrap the `runner.run_async()` loop in `lens.turn()`: + +{{< clients-example set="iris_lens_google_adk" step="google_adk_turn" description="" lang_filter="Python (Iris)" footer="hide" />}} + +The turn records the agent run, its outcome, and every Iris service call the agent makes. The example uses a Gemini model and needs `GOOGLE_API_KEY`. + +## Your own agent loop + +Wrap the handler for one message in `lens.turn()`, and set the outcome on each path: + +{{< clients-example set="iris_lens_own_loop" step="own_loop_turn" description="" lang_filter="Python (Iris)" footer="hide" />}} + +Each call to `handle()` records one turn under the session and user IDs you pass. The turn is marked `completed` when the model answers, or `failed` when the call raises. Record your retrievals and prompt assembly inside the same turn to see which context reached the model. + +## Next steps + +- [Instrument an agent](/content/develop/ai/context-engine/lens/instrument.md): record retrievals, memory recalls, prompt assembly, and custom steps. +- [Inspect sessions](/content/develop/ai/context-engine/lens/inspect-sessions.md): read the turns you traced in the Lens user interface (UI). +- [Query sessions and turns](/content/develop/ai/context-engine/lens/read-api.md): read turns from code. diff --git a/content/develop/ai/context-engine/lens/api-reference.md b/content/develop/ai/context-engine/lens/api-reference.md new file mode 100644 index 0000000000..16e62246e0 --- /dev/null +++ b/content/develop/ai/context-engine/lens/api-reference.md @@ -0,0 +1,18 @@ +--- +Title: Lens read API +linkTitle: API reference +layout: apireference +type: page +weight: 80 +params: + sourcefile: ./openapi.json + sortOperationsAlphabetically: false + schemaDefinitionsTagName: Data models + showPageContent: true +--- + +Use the Lens read application programming interface (API) to list agents and sessions, and to read turns, their captured content, and Context Gateway tool results. Every operation is a `GET` request scoped to your instance. + +[Download the OpenAPI contract]({{< relref "/develop/ai/context-engine/lens/api-reference/" >}}openapi.json) to generate clients or inspect schemas. + +See [Query sessions and turns](/content/develop/ai/context-engine/lens/read-api.md) for examples, and [Limits and errors](/content/develop/ai/context-engine/lens/limits.md) for request limits and error responses. diff --git a/content/develop/ai/context-engine/lens/api-reference/openapi.json b/content/develop/ai/context-engine/lens/api-reference/openapi.json new file mode 100644 index 0000000000..88c4b871ae --- /dev/null +++ b/content/develop/ai/context-engine/lens/api-reference/openapi.json @@ -0,0 +1,2426 @@ +{ + "openapi": "3.0.2", + "info": { + "title": "LensDataPlaneService", + "version": "1.0.0", + "description": "Read-only session and turn API for an Iris Lens instance." + }, + "paths": { + "/v1/instances/{instanceId}/agents": { + "get": { + "description": "Lists every agent with at least one retained session, sorted by agent ID. The list spans all retained sessions regardless of range, so a selector can offer any agent the instance can show sessions for. An instance with no sessions returns an empty list.", + "operationId": "ListAgents", + "parameters": [ + { + "name": "instanceId", + "in": "path", + "schema": { + "type": "string", + "maxLength": 256, + "minLength": 1 + }, + "required": true + } + ], + "responses": { + "200": { + "description": "ListAgents 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ListAgentsResponseContent" + } + } + } + }, + "400": { + "description": "BadRequestError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestErrorResponseContent" + } + } + } + }, + "401": { + "description": "AuthenticationError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthenticationErrorResponseContent" + } + } + } + }, + "403": { + "description": "ForbiddenError 403 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "500": { + "description": "UnexpectedError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnexpectedErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + } + } + }, + "/v1/instances/{instanceId}/sessions": { + "get": { + "description": "Lists sessions for one agent in newest-activity-first order. Pagination fixes the first page's last-seen upper bound but does not materialize an immutable snapshot: concurrent activity can move sessions out of the remaining result set or change their relative order. Start a new traversal to obtain the current view. A cursor is valid only with the same effective filters used to create it; a mismatch returns BadRequestError.", + "operationId": "ListSessions", + "parameters": [ + { + "name": "instanceId", + "in": "path", + "schema": { + "type": "string", + "maxLength": 256, + "minLength": 1 + }, + "required": true + }, + { + "name": "agentId", + "in": "query", + "description": "Agent scope. Lens never exposes an unscoped session list.", + "schema": { + "type": "string", + "maxLength": 256, + "minLength": 1, + "description": "Agent scope. Lens never exposes an unscoped session list." + }, + "required": true + }, + { + "name": "range", + "in": "query", + "description": "Requested lookback range. The service clamps it to instance retention and returns effectiveRange.", + "schema": { + "type": "string", + "pattern": "^[1-9][0-9]*[mhdw]$", + "description": "Requested lookback range. The service clamps it to instance retention and returns effectiveRange." + }, + "required": true + }, + { + "name": "sessionIdPrefix", + "in": "query", + "description": "Case-insensitive session ID prefix; infix search is not supported.", + "schema": { + "type": "string", + "maxLength": 256, + "minLength": 2, + "description": "Case-insensitive session ID prefix; infix search is not supported." + } + }, + { + "name": "userId", + "in": "query", + "description": "Case-insensitive exact user ID match.", + "schema": { + "type": "string", + "maxLength": 256, + "minLength": 1, + "description": "Case-insensitive exact user ID match." + } + }, + { + "name": "outcome", + "in": "query", + "description": "Session facets matched with OR semantics.", + "style": "form", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SessionOutcomeFacet" + }, + "maxItems": 6, + "description": "Session facets matched with OR semantics." + }, + "explode": true + }, + { + "name": "limit", + "in": "query", + "description": "Maximum rows returned. The server always supplies this value to RediSearch LIMIT.", + "schema": { + "type": "integer", + "default": 50, + "maximum": 200, + "minimum": 1, + "description": "Maximum rows returned. The server always supplies this value to RediSearch LIMIT.", + "format": "int32" + } + }, + { + "name": "cursor", + "in": "query", + "description": "Opaque continuation returned by ListSessions. Its encoded payload contains snapshotUpperBound, lastSeen, sessionId, and a deterministic validation hash of the effective filters. Reusing it with different effective filters returns BadRequestError.", + "schema": { + "type": "string", + "maxLength": 2048, + "minLength": 1, + "pattern": "^[A-Za-z0-9_-]+$", + "description": "Opaque continuation returned by ListSessions. Its encoded payload contains snapshotUpperBound, lastSeen, sessionId, and a deterministic validation hash of the effective filters. Reusing it with different effective filters returns BadRequestError." + } + } + ], + "responses": { + "200": { + "description": "ListSessions 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ListSessionsResponseContent" + } + } + } + }, + "400": { + "description": "BadRequestError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestErrorResponseContent" + } + } + } + }, + "401": { + "description": "AuthenticationError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthenticationErrorResponseContent" + } + } + } + }, + "403": { + "description": "ForbiddenError 403 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "500": { + "description": "UnexpectedError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnexpectedErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + } + } + }, + "/v1/instances/{instanceId}/sessions/{sessionId}": { + "get": { + "description": "Returns the retained turns and outcome facets for a session.", + "operationId": "GetSession", + "parameters": [ + { + "name": "instanceId", + "in": "path", + "schema": { + "type": "string", + "maxLength": 256, + "minLength": 1 + }, + "required": true + }, + { + "name": "sessionId", + "in": "path", + "schema": { + "type": "string", + "maxLength": 256, + "minLength": 1 + }, + "required": true + } + ], + "responses": { + "200": { + "description": "GetSession 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetSessionResponseContent" + } + } + } + }, + "400": { + "description": "BadRequestError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestErrorResponseContent" + } + } + } + }, + "401": { + "description": "AuthenticationError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthenticationErrorResponseContent" + } + } + } + }, + "403": { + "description": "ForbiddenError 403 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "500": { + "description": "UnexpectedError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnexpectedErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + } + } + }, + "/v1/instances/{instanceId}/turns/{turnId}": { + "get": { + "description": "Returns one turn. turnId is the turn's OTel trace ID; Lens defines no separate turn identifier.", + "operationId": "GetTurn", + "parameters": [ + { + "name": "instanceId", + "in": "path", + "schema": { + "type": "string", + "maxLength": 256, + "minLength": 1 + }, + "required": true + }, + { + "name": "turnId", + "in": "path", + "schema": { + "type": "string", + "pattern": "^[0-9a-f]{32}$" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "GetTurn 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetTurnResponseContent" + } + } + } + }, + "400": { + "description": "BadRequestError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestErrorResponseContent" + } + } + } + }, + "401": { + "description": "AuthenticationError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthenticationErrorResponseContent" + } + } + } + }, + "403": { + "description": "ForbiddenError 403 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "500": { + "description": "UnexpectedError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnexpectedErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + } + } + }, + "/v1/instances/{instanceId}/turns/{turnId}/content/{hash}": { + "get": { + "description": "Returns captured content for one turn hash. Before reading the content tier, the service MUST load the addressed turn, verify that the turn references the requested hash, and enforce the turn's logical content expiry. A missing reference or expired logical reference returns a successful expired response with no body, so callers cannot distinguish expiration from a hash that was never stored.", + "operationId": "GetContent", + "parameters": [ + { + "name": "instanceId", + "in": "path", + "schema": { + "type": "string", + "maxLength": 256, + "minLength": 1 + }, + "required": true + }, + { + "name": "turnId", + "in": "path", + "schema": { + "type": "string", + "pattern": "^[0-9a-f]{32}$" + }, + "required": true + }, + { + "name": "hash", + "in": "path", + "schema": { + "type": "string", + "pattern": "^[0-9a-f]{64}$" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "GetContent 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetContentResponseContent" + } + } + } + }, + "400": { + "description": "BadRequestError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestErrorResponseContent" + } + } + } + }, + "401": { + "description": "AuthenticationError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthenticationErrorResponseContent" + } + } + } + }, + "403": { + "description": "ForbiddenError 403 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "500": { + "description": "UnexpectedError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnexpectedErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + } + } + }, + "/v1/instances/{instanceId}/turns/{turnId}/tools/{spanId}/result": { + "get": { + "description": "Classifies only the captured result owned by the addressed Context Retriever span. Authorization, span ownership, and content expiry are enforced before loading the body.", + "operationId": "GetContextRetrieverResult", + "parameters": [ + { + "name": "instanceId", + "in": "path", + "schema": { + "type": "string", + "maxLength": 256, + "minLength": 1 + }, + "required": true + }, + { + "name": "turnId", + "in": "path", + "schema": { + "type": "string", + "pattern": "^[0-9a-f]{32}$" + }, + "required": true + }, + { + "name": "spanId", + "in": "path", + "schema": { + "type": "string", + "pattern": "^[0-9a-f]{16}$" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "GetContextRetrieverResult 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetContextRetrieverResultResponseContent" + } + } + } + }, + "400": { + "description": "BadRequestError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestErrorResponseContent" + } + } + } + }, + "401": { + "description": "AuthenticationError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthenticationErrorResponseContent" + } + } + } + }, + "403": { + "description": "ForbiddenError 403 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "500": { + "description": "UnexpectedError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnexpectedErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "Agent": { + "type": "object", + "description": "One agent an instance has seen, identified by its raw iris.agent.id. A display label is not part of this contract yet; see the TDD's agents-seen record for the planned telemetry-sourced label.", + "properties": { + "agentId": { + "type": "string", + "maxLength": 256, + "minLength": 1 + } + }, + "required": [ + "agentId" + ] + }, + "AgentMemoryCandidate": { + "type": "object", + "properties": { + "sessionId": { + "type": "string" + }, + "capture": { + "type": "string", + "description": "Why the text was not captured: the row's capture reason, else the span's capture state." + }, + "snapshot": { + "$ref": "#/components/schemas/ContentReference" + } + } + }, + "AgentMemoryOperation": { + "type": "object", + "description": "Id and hash lists are capped and keep empty-string entries for index alignment; counts are the true totals.", + "properties": { + "storeId": { + "type": "string" + }, + "requestMemoryIds": { + "type": "array", + "items": { + "type": "string" + } + }, + "requestCount": { + "type": "integer", + "minimum": 0, + "format": "int32" + }, + "sessionId": { + "type": "string", + "description": "The session the server acknowledged, else the one requested." + }, + "eventId": { + "type": "string", + "description": "The event the server acknowledged, else the one requested." + }, + "resultIds": { + "type": "array", + "items": { + "type": "string" + } + }, + "resultCount": { + "type": "integer", + "minimum": 0, + "format": "int32" + }, + "errorIds": { + "type": "array", + "items": { + "type": "string" + } + }, + "errorCount": { + "type": "integer", + "minimum": 0, + "format": "int32" + }, + "hasNextPage": { + "type": "boolean" + }, + "writtenHashes": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "AgentMemoryRetrieval": { + "type": "object", + "properties": { + "storeId": { + "type": "string", + "description": "The memory store the call read, from iris.component.resource.id." + }, + "limit": { + "type": "integer", + "minimum": 0, + "format": "int32" + }, + "filterOp": { + "type": "string" + }, + "filterFields": { + "type": "array", + "items": { + "type": "string" + } + }, + "hasNextPage": { + "type": "boolean" + }, + "summary": { + "$ref": "#/components/schemas/AgentMemorySummary" + } + } + }, + "AgentMemorySummary": { + "type": "object", + "properties": { + "content": { + "$ref": "#/components/schemas/ContentReference" + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "used": { + "type": "boolean" + } + }, + "required": [ + "content" + ] + }, + "AuthenticationErrorResponseContent": { + "type": "object", + "description": "Authentication credentials are missing, malformed, or invalid.", + "properties": { + "title": { + "type": "string", + "description": "A short, human-readable summary of the problem\n type. It SHOULD NOT change from occurrence to occurrence of the\n problem, except for purposes of localization (e.g., using\n proactive content negotiation; see [RFC7231], Section 3.4)." + }, + "status": { + "type": "integer", + "default": 401, + "description": "The HTTP status code ([RFC7231], Section 6) generated by the origin server for this occurrence of the problem.", + "format": "int32" + }, + "detail": { + "type": "string", + "description": "A human-readable explanation specific to this occurrence of the problem." + }, + "instance": { + "type": "string", + "description": "A URI reference that identifies the specific occurrence of the problem. It may or may not yield further information if dereferenced." + }, + "type": { + "$ref": "#/components/schemas/AuthenticationErrorType" + } + }, + "required": [ + "status", + "title", + "type" + ] + }, + "AuthenticationErrorType": { + "type": "string", + "description": "Problem type URI for authentication errors.", + "enum": [ + "/errors/authentication-failed" + ] + }, + "BadRequestErrorResponseContent": { + "type": "object", + "description": "Request validation or input decoding failed.", + "properties": { + "title": { + "type": "string", + "description": "A short, human-readable summary of the problem\n type. It SHOULD NOT change from occurrence to occurrence of the\n problem, except for purposes of localization (e.g., using\n proactive content negotiation; see [RFC7231], Section 3.4)." + }, + "status": { + "type": "integer", + "default": 400, + "description": "The HTTP status code ([RFC7231], Section 6) generated by the origin server for this occurrence of the problem.", + "format": "int32" + }, + "detail": { + "type": "string", + "description": "A human-readable explanation specific to this occurrence of the problem." + }, + "instance": { + "type": "string", + "description": "A URI reference that identifies the specific occurrence of the problem. It may or may not yield further information if dereferenced." + }, + "type": { + "$ref": "#/components/schemas/BadRequestErrorType" + }, + "fields": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FieldError" + }, + "description": "Optional field-level validation errors." + } + }, + "required": [ + "status", + "title", + "type" + ] + }, + "BadRequestErrorType": { + "type": "string", + "description": "Problem type URI for bad request errors.", + "enum": [ + "/errors/invalid-data" + ] + }, + "ComponentCallOutcome": { + "type": "string", + "enum": [ + "ok", + "failed", + "cancelled", + "unknown" + ] + }, + "ComponentOperation": { + "type": "object", + "properties": { + "spanId": { + "type": "string", + "pattern": "^[0-9a-f]{16}$" + }, + "component": { + "type": "string" + }, + "operation": { + "type": "string", + "description": "The source's own operation name, verbatim." + }, + "outcome": { + "$ref": "#/components/schemas/ComponentCallOutcome" + }, + "startedAt": { + "type": "string", + "format": "date-time" + }, + "endedAt": { + "type": "string", + "format": "date-time" + }, + "durationMs": { + "type": "integer", + "minimum": 0, + "format": "int64" + }, + "captureState": { + "type": "string" + }, + "errorType": { + "type": "string" + }, + "httpStatus": { + "type": "integer", + "format": "int32" + }, + "input": { + "$ref": "#/components/schemas/ContentReference" + }, + "agentMemory": { + "$ref": "#/components/schemas/AgentMemoryOperation" + } + }, + "required": [ + "component", + "operation", + "outcome", + "spanId", + "startedAt" + ] + }, + "ContentAvailability": { + "type": "string", + "enum": [ + "available", + "expired", + "masked", + "not-captured", + "fetch-failed" + ] + }, + "ContentKind": { + "type": "string", + "enum": [ + "prompt", + "completion", + "tool-arguments", + "tool-result", + "retrieval-document", + "recalled-memory", + "playbook-entry", + "playbook-snapshot", + "memory-snapshot", + "component-input" + ] + }, + "ContentReference": { + "type": "object", + "properties": { + "kind": { + "$ref": "#/components/schemas/ContentKind" + }, + "availability": { + "$ref": "#/components/schemas/ContentAvailability" + }, + "hash": { + "type": "string", + "pattern": "^[0-9a-f]{64}$", + "description": "Present only when content was captured and an address was produced." + } + }, + "required": [ + "availability", + "kind" + ] + }, + "ContextCandidate": { + "type": "object", + "properties": { + "id": { + "type": "string", + "maxLength": 256, + "minLength": 1 + }, + "score": { + "type": "number", + "description": "Absent when the source returned no score. scoreBand says why.", + "format": "double" + }, + "scoreBand": { + "$ref": "#/components/schemas/ScoreBand" + }, + "type": { + "type": "string", + "maxLength": 256, + "minLength": 1, + "description": "What the source says the candidate is, such as a memory's type." + }, + "createdAt": { + "type": "string", + "description": "When the source created the candidate, as the source reported it.", + "format": "date-time" + }, + "playbook": { + "$ref": "#/components/schemas/PlaybookCandidate" + }, + "agentMemory": { + "$ref": "#/components/schemas/AgentMemoryCandidate" + }, + "used": { + "type": "boolean" + }, + "content": { + "$ref": "#/components/schemas/ContentReference" + } + }, + "required": [ + "content", + "id", + "scoreBand" + ] + }, + "ContextManifest": { + "type": "object", + "properties": { + "entries": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ContextManifestEntry" + } + } + }, + "required": [ + "entries" + ] + }, + "ContextManifestEntry": { + "type": "object", + "properties": { + "source": { + "type": "string", + "maxLength": 256, + "minLength": 1 + }, + "state": { + "$ref": "#/components/schemas/ContextUsageState" + }, + "fetchedCount": { + "type": "integer", + "minimum": 0, + "description": "How many candidates the source returned. The true total: it exceeds the candidates listed when the list was capped.", + "format": "int32" + }, + "usedCount": { + "type": "integer", + "minimum": 0, + "format": "int32" + }, + "threshold": { + "type": "number", + "description": "The similarity floor the request set. Absent when the request set none.", + "format": "double" + }, + "componentSpanId": { + "type": "string", + "pattern": "^[0-9a-f]{16}$", + "description": "The retrieval span this entry came from, so a reader can place it in the span tree." + }, + "component": { + "type": "string", + "description": "The component that served the retrieval, from iris.component.name." + }, + "operation": { + "type": "string", + "description": "The source's own operation name, verbatim: the SDK method or the MCP tool name." + }, + "stage": { + "type": "string" + }, + "captureState": { + "type": "string" + }, + "errorType": { + "type": "string" + }, + "httpStatus": { + "type": "integer", + "format": "int32" + }, + "input": { + "$ref": "#/components/schemas/ContentReference" + }, + "playbook": { + "$ref": "#/components/schemas/PlaybookRetrieval" + }, + "agentMemory": { + "$ref": "#/components/schemas/AgentMemoryRetrieval" + }, + "candidates": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ContextCandidate" + }, + "description": "In the order the source returned them, which is its rank." + } + }, + "required": [ + "candidates", + "source", + "state" + ] + }, + "ContextRetrieverBlock": { + "type": "object", + "properties": { + "position": { + "type": "integer", + "minimum": 0, + "format": "int32" + }, + "classification": { + "$ref": "#/components/schemas/ContextRetrieverClassification" + }, + "queryType": { + "type": "string" + }, + "records": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ContextRetrieverRecord" + } + }, + "provenance": { + "$ref": "#/components/schemas/ContextRetrieverProvenance" + } + }, + "required": [ + "classification", + "position", + "records" + ] + }, + "ContextRetrieverCall": { + "type": "object", + "properties": { + "spanId": { + "type": "string", + "pattern": "^[0-9a-f]{16}$" + }, + "toolName": { + "type": "string" + }, + "outcome": { + "$ref": "#/components/schemas/ComponentCallOutcome" + }, + "captureState": { + "type": "string" + }, + "errorType": { + "type": "string", + "description": "Safe exception class captured only for a failed call; never an exception message." + }, + "startedAt": { + "type": "string", + "format": "date-time" + }, + "endedAt": { + "type": "string", + "format": "date-time" + }, + "content": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ContentReference" + } + } + }, + "required": [ + "captureState", + "content", + "outcome", + "spanId", + "startedAt", + "toolName" + ] + }, + "ContextRetrieverClassification": { + "type": "string", + "enum": [ + "unavailable", + "unclassified", + "records", + "aggregate", + "tool-error" + ] + }, + "ContextRetrieverProvenance": { + "type": "object", + "properties": { + "source": { + "type": "string" + }, + "sourceVersion": { + "type": "integer", + "minimum": 0, + "format": "int64" + }, + "retrievedAt": { + "type": "string", + "format": "date-time" + }, + "usage": {}, + "hybridScore": { + "type": "number", + "format": "double" + } + } + }, + "ContextRetrieverRecord": { + "type": "object", + "properties": { + "blockPosition": { + "type": "integer", + "minimum": 0, + "format": "int32" + }, + "resultPosition": { + "type": "integer", + "minimum": 0, + "format": "int32" + }, + "snapshot": {}, + "provenance": { + "$ref": "#/components/schemas/ContextRetrieverProvenance" + } + }, + "required": [ + "blockPosition", + "resultPosition", + "snapshot" + ] + }, + "ContextRetrieverToolStep": { + "type": "object", + "properties": { + "spanId": { + "type": "string", + "pattern": "^[0-9a-f]{16}$" + }, + "toolName": { + "type": "string" + }, + "toolCallId": { + "type": "string" + }, + "description": { + "type": "string" + }, + "framework": { + "type": "string" + }, + "operations": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ContextRetrieverCall" + } + } + }, + "required": [ + "operations", + "spanId" + ] + }, + "ContextUsageState": { + "type": "string", + "enum": [ + "entered-prompt", + "fetched-not-used", + "invoked-empty", + "not-instrumented" + ] + }, + "FieldError": { + "type": "object", + "description": "Validation error details for a single request field.", + "properties": { + "field": { + "type": "string", + "description": "Name of the invalid request field." + }, + "rule": { + "type": "string", + "description": "Validation rule that was violated." + }, + "message": { + "type": "string", + "description": "Human-readable validation error message." + } + }, + "required": [ + "field", + "message", + "rule" + ] + }, + "ForbiddenErrorResponseContent": { + "type": "object", + "description": "The caller is authenticated but not allowed to access the requested resource.", + "properties": { + "title": { + "type": "string", + "description": "A short, human-readable summary of the problem\n type. It SHOULD NOT change from occurrence to occurrence of the\n problem, except for purposes of localization (e.g., using\n proactive content negotiation; see [RFC7231], Section 3.4)." + }, + "status": { + "type": "integer", + "default": 403, + "description": "The HTTP status code ([RFC7231], Section 6) generated by the origin server for this occurrence of the problem.", + "format": "int32" + }, + "detail": { + "type": "string", + "description": "A human-readable explanation specific to this occurrence of the problem." + }, + "instance": { + "type": "string", + "description": "A URI reference that identifies the specific occurrence of the problem. It may or may not yield further information if dereferenced." + }, + "type": { + "$ref": "#/components/schemas/ForbiddenErrorType" + } + }, + "required": [ + "status", + "title", + "type" + ] + }, + "ForbiddenErrorType": { + "type": "string", + "description": "Problem type URI for authorization errors.", + "enum": [ + "/errors/insufficient-permissions" + ] + }, + "GatewayToolSpan": { + "type": "object", + "description": "Context Gateway metadata recorded by the Lens SDK. Upstream server labels and component are self-reported, not verified identity. Captured bodies and raw handle IDs are never projected.", + "properties": { + "upstreamId": { + "type": "string", + "maxLength": 512, + "minLength": 1, + "description": "Nonempty UTF-8 telemetry text, bounded by assembly max_text_bytes and omitted by the read API when it exceeds 512 bytes. Never contains a raw Gateway handle ID." + }, + "originalToolName": { + "type": "string", + "maxLength": 512, + "minLength": 1, + "description": "Nonempty UTF-8 telemetry text, bounded by assembly max_text_bytes and omitted by the read API when it exceeds 512 bytes. Never contains a raw Gateway handle ID." + }, + "catalogRevision": { + "type": "string", + "maxLength": 512, + "minLength": 1, + "description": "Nonempty UTF-8 telemetry text, bounded by assembly max_text_bytes and omitted by the read API when it exceeds 512 bytes. Never contains a raw Gateway handle ID." + }, + "upstreamServerName": { + "type": "string", + "maxLength": 512, + "minLength": 1, + "description": "Nonempty UTF-8 telemetry text, bounded by assembly max_text_bytes and omitted by the read API when it exceeds 512 bytes. Never contains a raw Gateway handle ID." + }, + "upstreamServerVersion": { + "type": "string", + "maxLength": 512, + "minLength": 1, + "description": "Nonempty UTF-8 telemetry text, bounded by assembly max_text_bytes and omitted by the read API when it exceeds 512 bytes. Never contains a raw Gateway handle ID." + }, + "upstreamComponent": { + "type": "string", + "maxLength": 512, + "minLength": 1, + "description": "Known Iris component reported by the Upstream: agent_memory, playbook, context_retriever, or context_gateway." + }, + "upstreamDurationMs": { + "type": "integer", + "maximum": 9007199254740991, + "minimum": 0, + "description": "Nonnegative recorded integer in the exact JavaScript integer range.", + "format": "int64" + }, + "resultCacheStatus": { + "type": "string", + "maxLength": 512, + "minLength": 1, + "description": "hit when the Gateway reports a result cache hit. Absence does not imply a miss." + }, + "resultCacheAgeMs": { + "type": "integer", + "maximum": 9007199254740991, + "minimum": 0, + "description": "Age of the cached result, present only with resultCacheStatus hit.", + "format": "int64" + }, + "failureReason": { + "type": "string", + "maxLength": 512, + "minLength": 1, + "description": "The Gateway failure reason, an open vocabulary. This does not replace the SDK-recorded tool outcome." + }, + "failureStage": { + "type": "string", + "maxLength": 512, + "minLength": 1, + "description": "Observed failure stage: connect, redirect, call, input_required, or result." + }, + "failureCompletion": { + "type": "string", + "maxLength": 512, + "minLength": 1, + "description": "Observed completion certainty: not_dispatched or unknown." + }, + "failureUpstreamCode": { + "type": "integer", + "maximum": 9007199254740991, + "minimum": -9007199254740991, + "description": "Recorded signed integer in the exact JavaScript integer range.", + "format": "int64" + }, + "resultHandleRef": { + "type": "string", + "maxLength": 16, + "minLength": 16, + "pattern": "^[0-9a-f]{16}$", + "description": "Non-capability correlation reference, never a raw handle ID." + }, + "resultHandleShape": { + "type": "string", + "maxLength": 512, + "minLength": 1, + "description": "Recorded result shape: collection, document, or text." + }, + "resultHandleCompleteness": { + "type": "string", + "maxLength": 512, + "minLength": 1, + "description": "Recorded result completeness: complete, partial, or unknown." + }, + "resultHandleBytes": { + "type": "integer", + "maximum": 9007199254740991, + "minimum": 0, + "description": "Nonnegative recorded integer in the exact JavaScript integer range.", + "format": "int64" + }, + "resultHandleItems": { + "type": "integer", + "maximum": 9007199254740991, + "minimum": 0, + "description": "Nonnegative recorded integer in the exact JavaScript integer range.", + "format": "int64" + }, + "searchResultCount": { + "type": "integer", + "maximum": 9007199254740991, + "minimum": 0, + "description": "Number of tools returned by search_tools, including an observed zero.", + "format": "int64" + } + } + }, + "GetContentResponseContent": { + "type": "object", + "properties": { + "hash": { + "type": "string", + "pattern": "^[0-9a-f]{64}$" + }, + "availability": { + "$ref": "#/components/schemas/ContentAvailability" + }, + "body": { + "type": "string", + "description": "Present only when availability is available.", + "format": "byte" + } + }, + "required": [ + "availability", + "hash" + ] + }, + "GetContextRetrieverResultResponseContent": { + "type": "object", + "properties": { + "spanId": { + "type": "string", + "pattern": "^[0-9a-f]{16}$" + }, + "outcome": { + "$ref": "#/components/schemas/ComponentCallOutcome" + }, + "captureState": { + "type": "string" + }, + "availability": { + "$ref": "#/components/schemas/ContentAvailability" + }, + "classification": { + "$ref": "#/components/schemas/ContextRetrieverClassification" + }, + "blocks": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ContextRetrieverBlock" + } + }, + "rawResult": { + "$ref": "#/components/schemas/ContentReference" + } + }, + "required": [ + "availability", + "blocks", + "captureState", + "classification", + "outcome", + "spanId" + ] + }, + "GetSessionResponseContent": { + "type": "object", + "properties": { + "session": { + "$ref": "#/components/schemas/SessionListItem" + }, + "threads": { + "type": "array", + "items": { + "type": "string", + "maxLength": 256, + "minLength": 1 + } + }, + "turns": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TurnSummary" + } + }, + "outcomeFacets": { + "type": "array", + "items": { + "$ref": "#/components/schemas/OutcomeFacetCount" + } + } + }, + "required": [ + "outcomeFacets", + "session", + "threads", + "turns" + ] + }, + "GetTurnResponseContent": { + "type": "object", + "properties": { + "traceId": { + "type": "string", + "pattern": "^[0-9a-f]{32}$" + }, + "sessionId": { + "type": "string", + "maxLength": 256, + "minLength": 1 + }, + "threadId": { + "type": "string", + "maxLength": 256, + "minLength": 1 + }, + "agentId": { + "type": "string", + "maxLength": 256, + "minLength": 1 + }, + "userId": { + "type": "string", + "maxLength": 256, + "minLength": 1 + }, + "startedAt": { + "type": "string", + "format": "date-time" + }, + "endedAt": { + "type": "string", + "format": "date-time" + }, + "durationMs": { + "type": "integer", + "minimum": 0, + "format": "int64" + }, + "outcome": { + "$ref": "#/components/schemas/TurnOutcome" + }, + "feedback": { + "type": "string" + }, + "tokens": { + "$ref": "#/components/schemas/TokenUsage" + }, + "spans": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SpanNode" + }, + "description": "Root nodes for the turn trace." + }, + "contextRetrieverToolSteps": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ContextRetrieverToolStep" + }, + "description": "Read-time grouping of Context Retriever calls under the nearest foreign execute_tool span. The original spans remain in spans." + }, + "resourceReads": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ResourceRead" + }, + "description": "Component resource reads in turn order; resource bytes are not captured." + }, + "playbookOperations": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PlaybookOperation" + }, + "description": "Observed Playbook proposal and publication operations in turn order, including proposal reads; content bodies are not captured." + }, + "componentOperations": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ComponentOperation" + }, + "description": "Observed component calls that are not retrievals, such as Agent Memory writes and text-less searches, in turn order. operation is verbatim; content bodies are not captured." + }, + "linkedTraceIds": { + "type": "array", + "items": { + "type": "string", + "pattern": "^[0-9a-f]{32}$" + } + }, + "manifest": { + "$ref": "#/components/schemas/ContextManifest" + } + }, + "required": [ + "agentId", + "contextRetrieverToolSteps", + "linkedTraceIds", + "manifest", + "sessionId", + "spans", + "startedAt", + "threadId", + "tokens", + "traceId" + ] + }, + "ListAgentsResponseContent": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Agent" + } + } + }, + "required": [ + "items" + ] + }, + "ListSessionsResponseContent": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SessionListItem" + }, + "maxItems": 200 + }, + "hasUserIds": { + "type": "boolean", + "description": "Whether any retained session for this instance has observed a user ID. This instance-level retained-index capability is independent of agent, range, filters, page, and cursor." + }, + "nextCursor": { + "type": "string", + "maxLength": 2048, + "minLength": 1, + "pattern": "^[A-Za-z0-9_-]+$", + "description": "Opaque cursor for the next page; omitted when this is the final page." + }, + "effectiveRange": { + "type": "string", + "pattern": "^[1-9][0-9]*[mhdw]$", + "description": "The requested range after retention clamping." + }, + "availableRanges": { + "type": "array", + "items": { + "type": "string", + "pattern": "^[1-9][0-9]*[mhdw]$", + "description": "Positive lookback duration such as 24h, 7d, or 30d." + }, + "description": "Ranges available for this instance, intersected across index and metrics retention." + } + }, + "required": [ + "availableRanges", + "effectiveRange", + "hasUserIds", + "items" + ] + }, + "NotFoundErrorResponseContent": { + "type": "object", + "description": "The requested resource does not exist.", + "properties": { + "title": { + "type": "string", + "description": "A short, human-readable summary of the problem\n type. It SHOULD NOT change from occurrence to occurrence of the\n problem, except for purposes of localization (e.g., using\n proactive content negotiation; see [RFC7231], Section 3.4)." + }, + "status": { + "type": "integer", + "default": 404, + "description": "The HTTP status code ([RFC7231], Section 6) generated by the origin server for this occurrence of the problem.", + "format": "int32" + }, + "detail": { + "type": "string", + "description": "A human-readable explanation specific to this occurrence of the problem." + }, + "instance": { + "type": "string", + "description": "A URI reference that identifies the specific occurrence of the problem. It may or may not yield further information if dereferenced." + }, + "type": { + "$ref": "#/components/schemas/NotFoundErrorType" + } + }, + "required": [ + "status", + "title", + "type" + ] + }, + "NotFoundErrorType": { + "type": "string", + "description": "Problem type URI for not-found errors.", + "enum": [ + "/errors/resource-not-found" + ] + }, + "OutcomeFacetCount": { + "type": "object", + "properties": { + "outcome": { + "$ref": "#/components/schemas/SessionOutcomeFacet" + }, + "count": { + "type": "integer", + "minimum": 0, + "format": "int32" + } + }, + "required": [ + "count", + "outcome" + ] + }, + "PlaybookCandidate": { + "type": "object", + "properties": { + "version": { + "type": "string" + }, + "selection": { + "type": "string" + }, + "contentDigest": { + "type": "string" + }, + "capture": { + "type": "string" + }, + "snapshot": { + "$ref": "#/components/schemas/ContentReference" + }, + "entryKey": { + "$ref": "#/components/schemas/ContentReference" + } + } + }, + "PlaybookOperation": { + "type": "object", + "properties": { + "spanId": { + "type": "string", + "pattern": "^[0-9a-f]{16}$" + }, + "action": { + "type": "string" + }, + "requestedProposalId": { + "type": "string" + }, + "requestedEvidenceId": { + "type": "string" + }, + "expectedRevision": { + "type": "string" + }, + "requestedEntryId": { + "type": "string" + }, + "requestedVersion": { + "type": "string" + }, + "proposalId": { + "type": "string" + }, + "proposalStatus": { + "type": "string" + }, + "proposalRevision": { + "type": "string" + }, + "evidenceId": { + "type": "string" + }, + "evidenceDisposition": { + "type": "string" + }, + "evidenceCount": { + "type": "string" + }, + "resultEntryId": { + "type": "string" + }, + "resultVersion": { + "type": "string" + }, + "publishedVersion": { + "type": "string" + }, + "outcome": { + "$ref": "#/components/schemas/ComponentCallOutcome" + }, + "errorType": { + "type": "string" + }, + "startedAt": { + "type": "string", + "format": "date-time" + }, + "endedAt": { + "type": "string", + "format": "date-time" + }, + "durationMs": { + "type": "integer", + "minimum": 0, + "format": "int64" + } + }, + "required": [ + "action", + "outcome", + "spanId", + "startedAt" + ] + }, + "PlaybookResourceRequest": { + "type": "object", + "properties": { + "requestedEntryId": { + "type": "string" + }, + "requestedVersion": { + "type": "string" + } + } + }, + "PlaybookRetrieval": { + "type": "object", + "properties": { + "playbookId": { + "type": "string", + "description": "The Playbook the call read, from iris.component.resource.id." + }, + "truncated": { + "type": "boolean" + }, + "budgetTruncated": { + "type": "boolean" + }, + "requestedEntryId": { + "type": "string" + }, + "requestedVersion": { + "type": "string" + } + } + }, + "Preview": { + "type": "object", + "properties": { + "availability": { + "$ref": "#/components/schemas/ContentAvailability" + }, + "text": { + "type": "string", + "maxLength": 512, + "description": "Bounded index-tier preview; omitted unless availability is available." + } + }, + "required": [ + "availability" + ] + }, + "ResourceRead": { + "type": "object", + "description": "One resource read by any component. operation is the source's own name, verbatim: get_skill_resource (Playbook SDK), fetch_resource (MCP tool), or read_resource (MCP resources/read).", + "properties": { + "spanId": { + "type": "string", + "pattern": "^[0-9a-f]{16}$" + }, + "component": { + "type": "string" + }, + "operation": { + "type": "string" + }, + "resourceUri": { + "type": "string", + "description": "The URI the call read: mcp.resource.uri for read_resource, iris.playbook.resource.uri for fetch_resource." + }, + "resourceId": { + "type": "string", + "description": "The resource ID an SDK call requested. MCP reads report resourceUri instead." + }, + "contentType": { + "type": "string" + }, + "contentDigest": { + "type": "string" + }, + "contentLength": { + "type": "string" + }, + "outcome": { + "$ref": "#/components/schemas/ComponentCallOutcome" + }, + "errorType": { + "type": "string" + }, + "startedAt": { + "type": "string", + "format": "date-time" + }, + "endedAt": { + "type": "string", + "format": "date-time" + }, + "durationMs": { + "type": "integer", + "minimum": 0, + "format": "int64" + }, + "playbook": { + "$ref": "#/components/schemas/PlaybookResourceRequest" + } + }, + "required": [ + "component", + "operation", + "outcome", + "spanId", + "startedAt" + ] + }, + "ScoreBand": { + "type": "string", + "description": "NOT_AVAILABLE: the source was asked and returned no score, as every Agent Memory recall does today. UNSCORED: the caller never supplied one. Neither is a low score.", + "enum": [ + "high", + "medium", + "low", + "unscored", + "not-available" + ] + }, + "ServiceUnavailableErrorResponseContent": { + "type": "object", + "properties": { + "title": { + "type": "string", + "description": "A short, human-readable summary of the problem\n type. It SHOULD NOT change from occurrence to occurrence of the\n problem, except for purposes of localization (e.g., using\n proactive content negotiation; see [RFC7231], Section 3.4)." + }, + "status": { + "type": "integer", + "default": 503, + "description": "The HTTP status code ([RFC7231], Section 6) generated by the origin server for this occurrence of the problem.", + "format": "int32" + }, + "detail": { + "type": "string", + "description": "A human-readable explanation specific to this occurrence of the problem." + }, + "instance": { + "type": "string", + "description": "A URI reference that identifies the specific occurrence of the problem. It may or may not yield further information if dereferenced." + }, + "type": { + "$ref": "#/components/schemas/ServiceUnavailableErrorType" + } + }, + "required": [ + "status", + "title", + "type" + ] + }, + "ServiceUnavailableErrorType": { + "type": "string", + "enum": [ + "/errors/service-unavailable" + ] + }, + "SessionListItem": { + "type": "object", + "properties": { + "agentIds": { + "type": "array", + "items": { + "type": "string", + "maxLength": 256, + "minLength": 1 + } + }, + "sessionId": { + "type": "string", + "maxLength": 256, + "minLength": 1 + }, + "userIds": { + "type": "array", + "items": { + "type": "string", + "maxLength": 256, + "minLength": 1 + } + }, + "firstSeen": { + "type": "string", + "format": "date-time" + }, + "lastSeen": { + "type": "string", + "format": "date-time" + }, + "turnCount": { + "type": "integer", + "minimum": 0, + "format": "int32" + }, + "outcomes": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SessionOutcomeFacet" + }, + "maxItems": 6 + }, + "firstPromptPreview": { + "$ref": "#/components/schemas/Preview" + }, + "lastResponsePreview": { + "$ref": "#/components/schemas/Preview" + } + }, + "required": [ + "agentIds", + "firstPromptPreview", + "firstSeen", + "lastResponsePreview", + "lastSeen", + "outcomes", + "sessionId", + "turnCount", + "userIds" + ] + }, + "SessionOutcomeFacet": { + "type": "string", + "description": "Session-level OR facet. UNSET represents turns that did not record iris.turn.outcome.", + "enum": [ + "completed", + "failed", + "abandoned", + "refused", + "timeout", + "unset" + ] + }, + "SpanKind": { + "type": "string", + "enum": [ + "internal", + "server", + "client", + "producer", + "consumer" + ] + }, + "SpanNode": { + "type": "object", + "properties": { + "spanId": { + "type": "string", + "pattern": "^[0-9a-f]{16}$" + }, + "parentSpanId": { + "type": "string", + "pattern": "^[0-9a-f]{16}$" + }, + "name": { + "type": "string", + "maxLength": 512, + "minLength": 1 + }, + "kind": { + "$ref": "#/components/schemas/SpanKind" + }, + "status": { + "$ref": "#/components/schemas/SpanStatus" + }, + "startedAt": { + "type": "string", + "format": "date-time" + }, + "endedAt": { + "type": "string", + "format": "date-time" + }, + "durationMs": { + "type": "integer", + "minimum": 0, + "format": "int64" + }, + "component": { + "type": "string", + "maxLength": 256, + "minLength": 1 + }, + "tool": { + "$ref": "#/components/schemas/ToolSpan" + }, + "content": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ContentReference" + }, + "description": "Hash references only; raw captured text is never embedded in turn detail." + }, + "children": { + "type": "array", + "default": [], + "items": { + "$ref": "#/components/schemas/SpanNode" + } + } + }, + "required": [ + "content", + "kind", + "name", + "spanId", + "startedAt", + "status" + ] + }, + "SpanStatus": { + "type": "string", + "enum": [ + "unset", + "ok", + "error" + ] + }, + "TokenUsage": { + "type": "object", + "properties": { + "input": { + "type": "integer", + "minimum": 0, + "format": "int64" + }, + "output": { + "type": "integer", + "minimum": 0, + "format": "int64" + }, + "total": { + "type": "integer", + "minimum": 0, + "format": "int64" + } + }, + "required": [ + "input", + "output", + "total" + ] + }, + "ToolComponent": { + "type": "string", + "enum": [ + "agent_memory", + "playbook", + "context_retriever", + "context_gateway" + ] + }, + "ToolOutcome": { + "type": "string", + "description": "ok means the call returned and does not assert tool success, including an MCP isError result without a Gateway failure entry. A Gateway failure entry can make it failed. Lens keeps the producer's outcome, including invalid_arguments recorded as ok.", + "enum": [ + "ok", + "failed", + "cancelled" + ] + }, + "ToolSpan": { + "type": "object", + "description": "Validated SDK iris.lens.tool identity only; captured bodies remain content references.", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "description": "Nonempty UTF-8 protocol tool name, preserved verbatim by Lens." + }, + "title": { + "type": "string", + "description": "The MCP tool title from tools/list when the SDK recorded it; Lens does not synthesize a title." + }, + "component": { + "$ref": "#/components/schemas/ToolComponent" + }, + "outcome": { + "$ref": "#/components/schemas/ToolOutcome" + }, + "gateway": { + "$ref": "#/components/schemas/GatewayToolSpan" + } + }, + "required": [ + "component", + "name" + ] + }, + "TurnOutcome": { + "type": "string", + "enum": [ + "completed", + "failed", + "abandoned", + "refused", + "timeout" + ] + }, + "TurnSummary": { + "type": "object", + "properties": { + "traceId": { + "type": "string", + "pattern": "^[0-9a-f]{32}$" + }, + "agentId": { + "type": "string", + "maxLength": 256, + "minLength": 1 + }, + "threadId": { + "type": "string", + "maxLength": 256, + "minLength": 1 + }, + "startedAt": { + "type": "string", + "format": "date-time" + }, + "endedAt": { + "type": "string", + "format": "date-time" + }, + "durationMs": { + "type": "integer", + "minimum": 0, + "format": "int64" + }, + "outcome": { + "$ref": "#/components/schemas/TurnOutcome" + }, + "feedback": { + "type": "string" + }, + "spanCount": { + "type": "integer", + "minimum": 0, + "format": "int32" + }, + "tokens": { + "$ref": "#/components/schemas/TokenUsage" + } + }, + "required": [ + "agentId", + "spanCount", + "startedAt", + "threadId", + "tokens", + "traceId" + ] + }, + "UnexpectedErrorResponseContent": { + "type": "object", + "description": "The service failed with an unexpected internal error.", + "properties": { + "title": { + "type": "string", + "description": "A short, human-readable summary of the problem\n type. It SHOULD NOT change from occurrence to occurrence of the\n problem, except for purposes of localization (e.g., using\n proactive content negotiation; see [RFC7231], Section 3.4)." + }, + "status": { + "type": "integer", + "default": 500, + "description": "The HTTP status code ([RFC7231], Section 6) generated by the origin server for this occurrence of the problem.", + "format": "int32" + }, + "detail": { + "type": "string", + "description": "A human-readable explanation specific to this occurrence of the problem." + }, + "instance": { + "type": "string", + "description": "A URI reference that identifies the specific occurrence of the problem. It may or may not yield further information if dereferenced." + }, + "type": { + "$ref": "#/components/schemas/UnexpectedErrorType" + } + }, + "required": [ + "status", + "title", + "type" + ] + }, + "UnexpectedErrorType": { + "type": "string", + "description": "Problem type URI for unexpected internal errors.", + "enum": [ + "/errors/unexpected-error" + ] + } + } + } +} diff --git a/content/develop/ai/context-engine/lens/concepts.md b/content/develop/ai/context-engine/lens/concepts.md new file mode 100644 index 0000000000..0e7f803ab7 --- /dev/null +++ b/content/develop/ai/context-engine/lens/concepts.md @@ -0,0 +1,81 @@ +--- +alwaysopen: false +categories: +- docs +- develop +- ai +description: Understand sessions, turns, steps, fetched and used context, outcomes, agents, content capture, and retention in Lens. +hideListLinks: true +linktitle: Concepts +title: Lens concepts +weight: 5 +--- + +These concepts explain what Lens records for each turn and how it organizes turns into sessions. + +## Sessions, threads, and turns + +A **turn** is one agent exchange: a user message in, an answer out. Lens records each turn as one OpenTelemetry trace, the standard record of one request and every operation it ran. The trace ID is the turn ID. + +A **session** groups the turns of one conversation. A **thread** is a sequence of turns within a session. You choose the session and thread IDs and pass them when you open a turn. When you omit the thread ID, Lens uses the session ID. + +Use opaque values, such as internal IDs or hashes, for session, thread, user, and agent IDs. + +![One turn shown as steps on a timeline. A memory recall and a Playbook discovery run at the same time. Prompt assembly uses 3 of the 4 recalled memories and drops 1. A model call, a Context Gateway tool call, and a second model call follow, and the turn ends with the outcome completed.](/images/ai/context-engine/lens-turn.svg) +{width="700"} + +## What a turn records + +Each operation in a turn is a **step**. A turn can include these steps: + +| Step | What Lens records | +| ---- | ----------------- | +| Retrieval | The query, the data source, and each result's ID, score, and text. | +| Memory recall | A retrieval from long-term memory, with the same details. | +| Prompt assembly | Which retrieved items entered the prompt. | +| Model call | The prompt, the completion, and token counts. | +| Tool call | The tool, its input, its result, and how long it took. | +| Iris service call | Agent Memory, Agent Playbooks, and Context Gateway calls. These services record their own steps. | +| Custom step | Any operation you wrap yourself. | + +Every step records its timing and whether it succeeded. See [Instrument an agent](/content/develop/ai/context-engine/lens/instrument.md) to record each kind of step. + +## Fetched and used context + +Lens compares what a source returned with what reached the model: + +- **Fetched** items are everything a retrieval or memory recall returned. +- **Used** items are the fetched items that entered the prompt. + +When you record prompt assembly, Lens marks each result as entered the prompt or not used, and marks retrievals that returned nothing. This shows when a relevant result was retrieved but dropped, for example at a token limit. Lens keeps this comparison for each turn as its **context manifest**. + +Lens also records each result's score where the source provides one. Agent Playbooks discovery results include their similarity score, and Context Gateway results include their hybrid search score. Your own retrievals include the scores you record. + +## Turn outcomes + +Set an outcome on each turn to record how it ended. You can then [filter sessions by outcome](/content/develop/ai/context-engine/lens/read-api.md#list-sessions) with the read application programming interface (API). + +| Outcome | Use it when | +| ------- | ----------- | +| `completed` | The agent finished its answer. | +| `failed` | An error stopped the turn. | +| `abandoned` | The user left before the turn finished. | +| `refused` | The agent declined the request. | +| `timeout` | The turn ran out of time. | + +A turn without an outcome still appears in Lens. Set the outcome on every path, including errors, so each turn is filterable. + +## Agents + +Every turn carries an **agent ID**. Set it once per process with `IRIS_LENS_AGENT_ID` or `lens.init()`, or per turn with `lens.turn(agent_id=...)`. One Lens instance holds the sessions of all your agents, and you choose an agent to view its sessions. + +## Content capture + +Lens captures content by default: prompts, completions, queries, retrieved text, memory text, frequently asked question (FAQ) answers, skill text, and tool inputs and results. + +You can turn content capture off. Lens then keeps each step's IDs, content hashes, counts, sizes, timing, and outcome. Fetched and used comparisons still work, because Lens compares hashes. See [Control content capture](/content/develop/ai/context-engine/lens/configure.md#control-content-capture). + +## Retention + +Lens keeps turn structure, timing, scores, and outcomes for the full retention window. Captured text has a shorter window. Your administrator sets both windows. + diff --git a/content/develop/ai/context-engine/lens/configure.md b/content/develop/ai/context-engine/lens/configure.md new file mode 100644 index 0000000000..148ea2078a --- /dev/null +++ b/content/develop/ai/context-engine/lens/configure.md @@ -0,0 +1,88 @@ +--- +alwaysopen: false +categories: +- docs +- develop +- ai +description: Configure the Lens SDK settings, API keys, content capture, OpenTelemetry integration, and TLS. +hideListLinks: true +linktitle: Configure +title: Configure Lens +weight: 30 +--- + +Configure the Lens module of the Redis Iris software development kit (SDK) with environment variables, `lens.init()`, or `LensSettings`. Lens reads its environment variables once, at the first Lens or Iris service call. To apply a variable you set later, call `lens.init()`. + +## Settings + +| Variable | `LensSettings` field | Description | +| -------- | -------------------- | ----------- | +| `IRIS_LENS_ENDPOINT` | `endpoint` | The Lens ingest endpoint. Setting it turns Lens on. | +| `IRIS_LENS_API_KEY` | `api_key` (read from the environment only) | The ingest key, sent with every export. | +| `IRIS_LENS_AGENT_ID` | `agent_id` | The default agent ID for this process. `lens.turn(agent_id=...)` overrides it. | +| `IRIS_LENS_SERVICE_NAME` | `service_name` | The service name recorded on every step. | +| `IRIS_LENS_ENABLED` | `enabled` | `false` keeps Lens off, even when an endpoint is set. | +| `IRIS_LENS_CAPTURE_CONTENT` | `capture_content` | Whether Lens captures prompts, completions, and retrieved text. Default `true`. | + +To configure Lens in code, pass `LensSettings` to `Iris`. The key still comes from `IRIS_LENS_API_KEY`: + +```python +from redis_iris import Iris, IrisSettings +from redis_iris.lens import LensSettings + +settings = LensSettings(endpoint="", agent_id="support-bot") +iris = Iris(IrisSettings(lens=settings)) +``` + +A blank variable counts as unset, so its default applies. `IRIS_LENS_CAPTURE_CONTENT` is the exception: a blank value turns capture off. + +## Advanced settings + +| Variable | Description | +| -------- | ----------- | +| `IRIS_LENS_HOST_SPANS` | `false` keeps Iris service steps out of your own tracer while Lens is off. Default `true`. | +| `IRIS_LENS_HOST_CAPTURE_CONTENT` | Whether Iris service steps sent to your own tracer include text. Default `false`. | +| `IRIS_LENS_AUTO_INSTRUMENT` | `false` turns off Iris service steps and keeps `lens.init()` from tracing model calls. | +| `IRIS_LENS_INSTRUMENT_AGENT_MEMORY` | `false` turns off Agent Memory steps. | +| `IRIS_LENS_INSTRUMENT_PLAYBOOK` | `false` turns off Agent Playbooks steps. | +| `IRIS_LENS_INSTRUMENT_THREADS` | `false` keeps `lens.init()` from carrying turns across Python threads. | +| `IRIS_LENS_OWN_PROVIDER` | `false` keeps Lens from installing a global OpenTelemetry tracer provider. | +| `IRIS_LENS_DROP_SCOPES` | A comma-separated list of OpenTelemetry instrumentation scope names that Lens never exports. It replaces the default list, so include any defaults you want to keep. | +| `IRIS_LENS_DROP_REDIS_COMMANDS` | A comma-separated list of Redis commands Lens never exports. | +| `IRIS_LENS_STRICT` | `true` makes `lens.turn()` raise an error when it has no session ID. | +| `IRIS_LENS_AUTH_HEADER` | The header that carries the ingest key. Default `Authorization`, as `Bearer `. | +| `IRIS_LENS_ALLOW_INSECURE` | `true` permits an `http://` endpoint that isn't a loopback address. | + +Lens uses its own exporter. It reads no `OTEL_EXPORTER_OTLP_*` variables. Proxies and certificate bundles come from `HTTPS_PROXY`, `NO_PROXY`, and `SSL_CERT_FILE`. + +## API keys and scopes + +Lens keys have one of three scopes: + +| Scope | Use it for | +| ----- | ---------- | +| `ingest` | Agents that send turns to Lens. | +| `read` | People and tools that read turns, in the Lens user interface (UI) or with the read application programming interface (API). | +| `full` | Both. | + +Agents send their key as `IRIS_LENS_API_KEY`. The read client reads its key from `LENS_API_KEY`. Give each agent an ingest key and each reader a read key, so an agent's key can't read other agents' turns. Your administrator creates keys. + + +## Control content capture + +Set `IRIS_LENS_CAPTURE_CONTENT=false` to send no text to Lens. Lens also removes prompt and completion text that other instrumentation added, before export. See [content capture](/content/develop/ai/context-engine/lens/concepts.md#content-capture) for what Lens keeps. + +## Use Lens with your existing OpenTelemetry setup + +Lens is built on OpenTelemetry and works with the tracing your application already has: + +- If your application has a tracer provider, Lens adds its processor to it. Steps your application records inside a turn are exported to Lens as well. +- If your application has no tracer provider, Lens installs one. To prevent that, set `IRIS_LENS_OWN_PROVIDER=false`. +- Lens steps also reach your provider's other processors, so they appear in your existing observability tools. +- While Lens is off, Iris service steps still go to your own tracer, without text. Your existing tools see Iris latency and errors with no Lens endpoint configured. + +## Connect over TLS + +Lens sends turns over Transport Layer Security (TLS), so use an `https://` endpoint. Lens verifies the server certificate and never follows redirects, so the key and captured content go only to the endpoint you set. + +For local testing, `http://` works with loopback addresses such as `localhost`. To permit `http://` elsewhere, set `IRIS_LENS_ALLOW_INSECURE=true`. diff --git a/content/develop/ai/context-engine/lens/inspect-sessions.md b/content/develop/ai/context-engine/lens/inspect-sessions.md new file mode 100644 index 0000000000..a1bb55eae6 --- /dev/null +++ b/content/develop/ai/context-engine/lens/inspect-sessions.md @@ -0,0 +1,49 @@ +--- +alwaysopen: false +categories: +- docs +- develop +- ai +description: Find a session in the Lens UI and read each turn's steps, timing, and captured content. +hideListLinks: true +linktitle: Inspect sessions +title: Inspect sessions +weight: 25 +--- + +Use the Lens user interface (UI) to find a session, open a turn, and see what happened at each step. + +## Connect to Lens + +Open Lens. If Lens asks for a credential, paste your read key and select **Connect**. + + +## Find a session + +1. Select an agent. Lens lists that agent's sessions, newest activity first. +1. Choose a time range. Lens offers the ranges your instance retains. +1. To narrow the loaded sessions, search by any part of a session ID or user ID. + +Each session shows its number of turns, its user IDs, and the outcomes of its turns. To search a whole time range by session ID prefix, user ID, or outcome, use the [read application programming interface (API)](/content/develop/ai/context-engine/lens/read-api.md#list-sessions). + +## Read a turn's steps + +Select a session to see its turns, then expand a turn. Lens shows the turn's steps as a timeline, in the order they ran: + +- Each step shows its name and how long it took. Steps that ran at the same time appear together. +- Recall steps list each result with its score, where the source provides one, and mark it **Entered prompt** or **Not used**. +- Iris service steps show the service and the operation, such as an Agent Playbooks discovery or a Context Gateway tool call. + +Select a recall, tool, or Iris service step to open its details, including the request and result Lens captured for it. + +## View captured content + +Step details show the text Lens captured, such as the recall query, each result, and tool input and output. When text isn't available, the step says why: + +| Label | Meaning | +| ----- | ------- | +| Content expired | The text is older than the content retention window. | +| Content not captured | Content capture was off when the turn ran. | +| Content could not be fetched | Lens couldn't load the text. Try again later. | + +See [content capture](/content/develop/ai/context-engine/lens/concepts.md#content-capture) and [retention](/content/develop/ai/context-engine/lens/concepts.md#retention). diff --git a/content/develop/ai/context-engine/lens/instrument.md b/content/develop/ai/context-engine/lens/instrument.md new file mode 100644 index 0000000000..2859e674a7 --- /dev/null +++ b/content/develop/ai/context-engine/lens/instrument.md @@ -0,0 +1,114 @@ +--- +alwaysopen: false +categories: +- docs +- develop +- ai +description: Record turns, retrievals, memory recalls, prompt assembly, model calls, and custom steps with the Redis Iris SDK. +hideListLinks: true +linktitle: Instrument an agent +title: Instrument an agent +weight: 15 +--- + +Record each agent turn and the steps inside it with `redis_iris.lens`, the Lens module of the Redis Iris software development kit (SDK). These examples need the Lens connection values from the [quickstart](/content/develop/ai/context-engine/lens/quickstart.md#configure-the-sdk). + +## Turn Lens on + +Call `lens.init()` once at startup. It turns Lens on, traces model calls, and sets the agent ID and service name: + +{{< clients-example set="iris_lens_instrument" step="init" description="" lang_filter="Python (Iris)" footer="hide" />}} + +Without `lens.init()`, Lens starts at the first Lens or Iris service call when `IRIS_LENS_ENDPOINT` is set, and records everything except model calls. You can also pass Lens settings to `Iris(IrisSettings(lens=...))`. See [Configure Lens](/content/develop/ai/context-engine/lens/configure.md) for every setting. + +## Open a turn + +Wrap each agent exchange in `lens.turn()`. Pass the session ID, and optionally a thread ID, a user ID, and an agent ID that overrides the process default: + +{{< clients-example set="iris_lens_instrument" step="open_turn" description="" lang_filter="Python (Iris)" footer="hide" />}} + +Use `set_attribute()` to add your own fields to the turn, and `set_outcome()` to record how it ended. See [turn outcomes](/content/develop/ai/context-engine/lens/concepts.md#turn-outcomes) for the values. + +`lens.turn()` works the same in asynchronous code: open it with `with` around the `await` calls of one exchange. + +## What Iris services record + +Agent Memory, Agent Playbooks, and Context Gateway calls record their own steps inside the open turn, in both their synchronous and asynchronous forms: + +| Service | Steps recorded | +| ------- | -------------- | +| Agent Memory | Memory recalls, session events, and memory writes and deletes. | +| Agent Playbooks | Discovery, skill loads, skill resource reads, and proposals. | +| Context Gateway | Tool listings and each tool call, with its upstream tool name. | + +These steps come from the Redis Iris service objects, such as `AgentMemory` and `AgentPlaybooks`. To record a call on a service's generated client (`.client`), wrap it in `lens.memory_recall()`, `lens.retrieval()`, or `lens.span()`. + +## Trace model calls + +Install the `llm-tracing` extra, then call `lens.init()` at startup: + +```bash +python -m pip install 'redis-iris[llm-tracing]' +``` + +Lens then records calls made through the OpenAI, Anthropic, Amazon Bedrock, and LangChain client libraries, including LangGraph. Each model call records its prompt, completion, and token counts, and the turn records its token totals. + +## Record retrievals + +Wrap your own retrieval in `lens.retrieval()` and record its results as `(id, score, text)` tuples: + +{{< clients-example set="iris_lens_instrument" step="retrieval" description="" lang_filter="Python (Iris)" footer="hide" />}} + +Pass `None` as the score for a source that doesn't return one. Lens records it as no score rather than a score of zero. + +## Record memory recalls + +Wrap a recall from a long-term memory store in `lens.memory_recall()`. The store ID becomes the recall's source: + +{{< clients-example set="iris_lens_instrument" step="memory_recall" description="" lang_filter="Python (Iris)" footer="hide" />}} + +## Record prompt assembly + +Record which retrieved items entered the prompt. Pass everything you fetched to `fetched()` and the items you kept to `used()`: + +{{< clients-example set="iris_lens_instrument" step="prompt_assembly" description="" lang_filter="Python (Iris)" footer="hide" />}} + +Lens matches items by their text, so pass the same items you recorded as results. The turn then shows which results entered the prompt and which weren't used. See [fetched and used context](/content/develop/ai/context-engine/lens/concepts.md#fetched-and-used-context). + +## Add custom steps + +Wrap any operation in `lens.span()` to record it in the turn. Pass attributes as keyword arguments, or set them on the step: + +{{< clients-example set="iris_lens_instrument" step="custom_span" description="" lang_filter="Python (Iris)" footer="hide" />}} + +Custom steps are part of the turn's trace, and the read API returns them in the turn's `spans`. + +## Include other instrumentation + +Lens exports the spans other OpenTelemetry instrumentation creates inside a turn, apart from a default set of low-level scopes such as database drivers. To also export spans created outside a turn, or from an excluded scope, wrap the call in `lens.include()`. It works as a decorator or a context manager: + +{{< clients-example set="iris_lens_instrument" step="include" description="" lang_filter="Python (Iris)" footer="hide" />}} + +## Get the turn ID + +Read the turn ID to link a turn to your own logs, or to [fetch the turn](/content/develop/ai/context-engine/lens/read-api.md#get-a-turn) with the read application programming interface (API). Inside a turn, call `lens.current_trace_id()`. After the turn, read `trace_id` from the turn handle: + +{{< clients-example set="iris_lens_instrument" step="turn_id" description="" lang_filter="Python (Iris)" footer="hide" />}} + +## Carry a turn across processes + +When a turn continues in another process, such as a queue worker, pass its context with the work. Call `lens.inject()` to write the turn context into a dictionary, and `lens.continue_from()` on the other side to resume the turn: + +{{< clients-example set="iris_lens_instrument" step="cross_process" description="" lang_filter="Python (Iris)" footer="hide" />}} + +Steps recorded in the worker join the same turn. The dictionary carries the session, user, and agent IDs, so send it only to services you trust, and resume only from dictionaries your own services created. + +Within one process, `lens.init()` with the `llm-tracing` extra carries the turn into threads your code starts. + +## Shut down + +Before a short-lived process exits, call `lens.shutdown()`: + +{{< clients-example set="iris_lens_instrument" step="shutdown" description="" lang_filter="Python (Iris)" footer="hide" />}} + +`shutdown()` waits for the export queue to flush and returns `True` when the flush completes. A failed export logs a warning on the `iris.lens` logger. `shutdown()` also turns Lens off for the rest of the process, so call it once, when your program is done tracing. Long-running services export continuously and call it only when they stop. diff --git a/content/develop/ai/context-engine/lens/limits.md b/content/develop/ai/context-engine/lens/limits.md new file mode 100644 index 0000000000..70a8484426 --- /dev/null +++ b/content/develop/ai/context-engine/lens/limits.md @@ -0,0 +1,82 @@ +--- +alwaysopen: false +categories: +- docs +- develop +- ai +description: Lens request limits, how the SDK exports turns, error responses, and troubleshooting. +hideListLinks: true +linktitle: Limits and errors +title: Lens limits and errors +weight: 40 +--- + +Check these limits when you design IDs and read large sessions, and use the error tables to handle failed requests. + +## Request limits + +| Limit | Value | +| ----- | ----- | +| Session, thread, user, and agent IDs | Up to 256 bytes each. The software development kit (SDK) shortens longer IDs and logs a warning. | +| Results per retrieval step | Up to 100. The fetched count keeps the true total. | +| Ingest request size | Up to 8 mebibytes (MiB) by default. | +| Sessions per page | Up to 200. Default 50. | +| Sessions per listed range | Up to 10,000. Narrow the range or add filters to list more. | +| Turns per session read | Up to 10,000. | + +Your administrator can change the ingest request size and sets the retention windows. + + +## How Lens exports + +The SDK exports turns in the background, so your agent doesn't wait on Lens: + +- Each export has a 10-second deadline. +- The SDK retries rate limits, timeouts, server errors, and connection errors within that deadline. A rate-limited export waits for the server's `Retry-After` value, up to 30 seconds. +- When an export fails, the SDK logs a warning on the `iris.lens` logger and keeps running. Export errors never raise into your agent. + +To see the SDK's warnings, configure Python `logging` to show `WARNING` messages, for example with `logging.basicConfig(level=logging.WARNING)`. + +## Ingest errors + +The ingest endpoint returns these statuses. The SDK names `401`, `403`, `413`, and `429` in its warnings: + +| Status | Meaning | Action | +| ------ | ------- | ------ | +| `400` | The export body isn't a valid OpenTelemetry request. | Export with the Redis Iris SDK. | +| `401` | The ingest key is missing or invalid. | Check `IRIS_LENS_API_KEY`. | +| `403` | The key can't ingest for this instance. | Use a key with the `ingest` or `full` scope. | +| `413` | The export exceeds the ingest request size. | Set `OTEL_BSP_MAX_EXPORT_BATCH_SIZE` below 64 to send smaller batches, or turn content capture off. | +| `429` | The instance is at capacity. | The SDK retries. Sustained `429` responses mean the instance needs more capacity. | +| `503` | Ingest is unavailable or not enabled for this instance. | The SDK retries. If it persists, ask your administrator to check that ingest is enabled. | + +## Read errors + +Read application programming interface (API) errors return a problem details body with a `type` and a `title`: + +| Status | `type` | Action | +| ------ | ------ | ------ | +| `400` | `/errors/invalid-data` | Correct the request parameters. The body lists each invalid field. | +| `401` | `/errors/authentication-failed` | Check `LENS_API_KEY`. | +| `403` | `/errors/insufficient-permissions` | Use a key with the `read` or `full` scope. | +| `404` | `/errors/resource-not-found` | Check the ID. A new turn becomes readable a few seconds after export. | +| `500` | `/errors/unexpected-error` | Retry the request. | +| `503` | `/errors/service-unavailable` | Retry after a short wait. | + +## Troubleshoot a missing turn + +If a turn doesn't appear in Lens, check these in order: + +1. **Lens is configured.** `IRIS_LENS_ENDPOINT` is set in the process, and `IRIS_LENS_ENABLED` isn't `false`. `lens.init()` returns `True` when both are true. With no endpoint, every Lens call does nothing. +1. **The turn has an agent ID.** Set `IRIS_LENS_AGENT_ID`, pass `agent_id` to `lens.init()`, or pass it to `lens.turn()`. Lens groups every turn by agent. +1. **The export succeeded.** A short-lived process calls `lens.shutdown()` before it exits. Check the `iris.lens` warnings for a failed export, such as a rejected key. +1. **The endpoint uses Transport Layer Security (TLS).** Lens refuses an `http://` endpoint that isn't a loopback address. See [Connect over TLS](/content/develop/ai/context-engine/lens/configure.md#connect-over-tls). +1. **You're looking in the right place.** Select the agent ID the turn used, and a time range that includes it. A turn opened without a session ID appears under the session `unassigned`. + +## Troubleshoot missing steps + +| Symptom | Check | +| ------- | ----- | +| Model calls don't appear in the turn. | Install `redis-iris[llm-tracing]` and call `lens.init()` at startup. Lens traces the OpenAI, Anthropic, Amazon Bedrock, and LangChain clients. | +| A source shows `not-instrumented`. | Record prompt assembly, and pass `fetched()` and `used()` the same items, with their text, that you recorded as results. | +| Iris service calls don't appear. | Make the calls inside `lens.turn()`, through the service objects such as `AgentMemory`, not their generated `.client`. | diff --git a/content/develop/ai/context-engine/lens/quickstart.md b/content/develop/ai/context-engine/lens/quickstart.md new file mode 100644 index 0000000000..9aae5c3db7 --- /dev/null +++ b/content/develop/ai/context-engine/lens/quickstart.md @@ -0,0 +1,103 @@ +--- +alwaysopen: false +categories: +- docs +- develop +- ai +description: Trace one agent turn with the Redis Iris SDK, then find it in Lens and read it back with the read API. +hideListLinks: true +linktitle: Quickstart +title: Lens quickstart +weight: 10 +--- + +Trace one agent turn with the Redis Iris software development kit (SDK), then find it in Lens and read it back with the Lens read application programming interface (API). + +## Before you begin + +Get these values from your administrator: + +- The Lens ingest endpoint and an ingest key, for sending turns. +- The Lens read endpoint, your instance ID, and a read key, for reading them back. + + +You also need Python 3.11 or later. The cURL example uses Bash and jq. This walkthrough needs no agent framework or model provider key. + +## Install the SDK + +Install the Redis Iris SDK, which includes Lens instrumentation, and the Lens read client: + +```bash +python -m venv .venv +source .venv/bin/activate +python -m pip install redis-iris redis-agent-lens +``` + +## Configure the SDK + +Export these values in the shell where you run the example: + +```bash +export IRIS_LENS_ENDPOINT='' +export IRIS_LENS_API_KEY='' +export IRIS_LENS_AGENT_ID='lens-quickstart' + +export LENS_URL='' +export LENS_INSTANCE_ID='' +export LENS_API_KEY='' +``` + +Setting `IRIS_LENS_ENDPOINT` turns Lens on. Use the endpoint's base address, including `https://`. The SDK sends `IRIS_LENS_API_KEY` with every export, and the read client sends `LENS_API_KEY` with every read. + +Keep keys out of source control and model prompts. + +## Trace one turn + +Expand the example to copy the complete program. Save it as `quickstart.py`, then run `python quickstart.py`. + +This turn records a memory recall that returns three memories, then a prompt assembly that uses two of them. + +{{< clients-example set="iris_lens_quickstart" step="trace_turn" description="" lang_filter="Python (Iris)" footer="hide" />}} + +The example prints the session ID and the turn ID. Keep both to find the turn. + +Lens exports turns in the background. Before a short-lived process exits, flush the export: + +{{< clients-example set="iris_lens_quickstart" step="flush" description="" lang_filter="Python (Iris)" footer="hide" />}} + +`shutdown()` returns `True` when the flush completes. If an export fails, the SDK logs a warning on the `iris.lens` logger. `shutdown()` also turns Lens off for the rest of the process, so call it only when your program is done tracing. + +## Find the turn + +A turn is readable a few seconds after its export. + +To view it in Lens: + +1. Open Lens. If Lens asks for a credential, paste your read key and select **Connect**. + +1. Select the `lens-quickstart` agent. +1. Select the session that matches the printed session ID. +1. Expand the turn. + +The turn shows an **Agent Memory** step with three memories: two marked **Entered prompt** and one marked **Not used**. + +To read the same turn with the read API, set `TURN_ID` for the cURL example first: + +```bash +export TURN_ID='' +``` + +{{< clients-example set="iris_lens_quickstart" step="find_turn" description="" lang_filter="Python (Iris),cURL (Iris)" footer="hide" />}} + +The Python example retries until the turn is readable, then prints its outcome and one line per source. The memory recall's source is its store ID: + +``` +Outcome: completed +quickstart-store: fetched 3, used 2 +``` + +## Next steps + +- [Learn the concepts](/content/develop/ai/context-engine/lens/concepts.md) behind sessions, turns, and fetched and used context. +- [Instrument an agent](/content/develop/ai/context-engine/lens/instrument.md) to record retrievals, model calls, and custom steps. +- [Integrate with an agent framework](/content/develop/ai/context-engine/lens/agent-integration.md) such as LangGraph, the OpenAI Agents SDK, or the Google Agent Development Kit (ADK). diff --git a/content/develop/ai/context-engine/lens/read-api.md b/content/develop/ai/context-engine/lens/read-api.md new file mode 100644 index 0000000000..ce6b6171c0 --- /dev/null +++ b/content/develop/ai/context-engine/lens/read-api.md @@ -0,0 +1,108 @@ +--- +alwaysopen: false +categories: +- docs +- develop +- ai +description: List agents and sessions, and read turns and their captured content with the Lens read API. +hideListLinks: true +linktitle: Read API +title: Query sessions and turns +weight: 35 +--- + +Use the Lens read application programming interface (API) to list sessions and read turns from code, for example to export failed turns or build your own reports. The Python examples use the Lens read client: + +```bash +python -m pip install redis-agent-lens +``` + +The cURL examples use Bash and jq. Every operation is a `GET` request. + +## Authenticate + +Export the read endpoint, your instance ID, and a read key: + +```bash +export LENS_URL='' +export LENS_INSTANCE_ID='' +export LENS_API_KEY='' +``` + +The read client reads its key from `LENS_API_KEY` and sends it as a bearer token. Every operation is scoped to your instance: + +{{< clients-example set="iris_lens_read_api" step="connect" description="" lang_filter="Python (Iris),cURL (Iris)" footer="hide" />}} + +## List agents + +List every agent with at least one retained session: + +{{< clients-example set="iris_lens_read_api" step="list_agents" description="" lang_filter="Python (Iris),cURL (Iris)" footer="hide" />}} + +## List sessions + +List one agent's sessions over a time range, newest activity first. This example lists the sessions with a failed turn in the last 24 hours, then follows the cursor to the next page: + +{{< clients-example set="iris_lens_read_api" step="list_sessions" description="" lang_filter="Python (Iris),cURL (Iris)" footer="hide" />}} + +| Parameter | Description | +| --------- | ----------- | +| `agentId` | Required. The agent whose sessions to list. | +| `range` | Required. A lookback duration such as `24h`, `7d`, or `30d`. | +| `sessionIdPrefix` | Sessions whose ID starts with this value, ignoring case. At least 2 characters. | +| `userId` | Sessions with this user ID, ignoring case. | +| `outcome` | Sessions with a turn that has any of these outcomes. Use `unset` for turns without an outcome. | +| `limit` | Sessions per page, up to 200. Default 50. | +| `cursor` | The `nextCursor` value from the previous page. | + +The Python client uses the same parameters in snake_case, such as `agent_id` and `session_id_prefix`. + +Lens fits the range to your instance's retention. The response returns the range it used as `effectiveRange`, and the ranges your instance supports as `availableRanges`. + +To read the next page, pass `nextCursor` with the same filters. When `nextCursor` is absent, you have the last page. A cursor expires after 5 minutes without use; start again from the first page. + +## Get a session + +Read a session's threads, turn summaries, and outcome counts: + +{{< clients-example set="iris_lens_read_api" step="get_session" description="" lang_filter="Python (Iris),cURL (Iris)" footer="hide" />}} + +Each turn summary includes the turn ID, outcome, duration, number of steps, and token counts. + +## Get a turn + +Read one turn by its ID. The response includes the turn's steps, token counts, outcome, and its **context manifest**: one entry per retrieval step, with what it fetched and what the prompt used. + +{{< clients-example set="iris_lens_read_api" step="get_turn" description="" lang_filter="Python (Iris),cURL (Iris)" footer="hide" />}} + +Each manifest entry has one of these states: + +| State | Meaning | +| ----- | ------- | +| `entered-prompt` | At least one result entered the prompt. | +| `fetched-not-used` | The source returned results, and none entered the prompt. | +| `invoked-empty` | The source returned no results. | +| `not-instrumented` | The turn recorded no prompt assembly, or the results had no text to match. | + +Each candidate in an entry includes its ID, score where the source provides one, and whether it entered the prompt. + +## Get captured content + +Captured text, such as a retrieved document or a model prompt, is stored by content hash. Read it with the turn ID and the hash from the turn: + +{{< clients-example set="iris_lens_read_api" step="get_content" description="" lang_filter="Python (Iris),cURL (Iris)" footer="hide" />}} + +The response's `availability` is `available` with the text in `body`, or says why the text isn't available, such as `expired` or `not-captured`. See [retention](/content/develop/ai/context-engine/lens/concepts.md#retention). + +## Get a Context Gateway tool result + +Read the result of a Context Gateway tool call in a turn, by the step's ID. The turn lists these steps in `contextRetrieverToolSteps`: + +{{< clients-example set="iris_lens_read_api" step="gateway_result" description="" lang_filter="Python (Iris),cURL (Iris)" footer="hide" />}} + +The result classifies what the tool returned, such as `records` or `aggregate`, and includes the returned data. + +## Next steps + +- See the [API reference](/content/develop/ai/context-engine/lens/api-reference.md) for every field. +- See [Limits and errors](/content/develop/ai/context-engine/lens/limits.md) for request limits and error responses. diff --git a/for-ais-only/lens-docs-design.md b/for-ais-only/lens-docs-design.md new file mode 100644 index 0000000000..e44054a303 --- /dev/null +++ b/for-ais-only/lens-docs-design.md @@ -0,0 +1,228 @@ +# Lens developer documentation design + +Design for the customer-facing Lens developer docs, tracked in +[MOD-20119](https://redislabs.atlassian.net/browse/MOD-20119). + +## Decisions + +- **Name:** "Lens" in prose, never "Iris Lens". Section folder: + `content/develop/ai/context-engine/lens/`. +- **Context Gateway, not Context Retriever.** Prose always says Context Gateway. API + literals such as `GetContextRetrieverResult` stay as they are in the generated reference. +- **Packages:** instrumentation ships in `redis-iris` (`redis_iris.lens`); the read client is + `redis-agent-lens`. Install both unpinned. Every release so far is a prerelease, which pip + and uv install when no stable release exists. Model-call tracing needs the + `redis-iris[llm-tracing]` extra. +- **Languages:** Python and cURL. Instrumentation uses the Python SDK; the read API works + from any language over HTTP. +- **Deployments:** Lens runs on Redis Cloud and self-managed. The developer pages are + deployment-neutral: the same steps work on both. Name a deployment only where behavior + differs, such as signing in to the UI. +- **Availability banner:** "Lens is currently available in preview. Features and behavior + are subject to change." Prose says "preview", matching the other Iris products; never + "Beta". +- **Audience:** developers with a provisioned Lens instance and API keys. Install, upgrade, + and runbooks are operator docs, tracked in + [MOD-18980](https://redislabs.atlassian.net/browse/MOD-18980). +- **Present what Lens does.** Lens is in preview. Say where a capability is present, not where + it's missing: "Playbook discovery results include their similarity score", not "Agent + Memory recalls have no score". Name the cases that have a capability so readers don't + assume it applies everywhere. Leave out unshipped features entirely. The availability + banner is the only preview notice; no lists of gaps. +- **Customer-facing only.** Leave out storage tiers, internal service names, internal span + and attribute names (`iris.lens.*`, `iris.component.*`), cursor internals, and design + history. +- **Diagrams:** two SVGs in `static/images/ai/context-engine/`, each with a full-sentence alt + text. `lens-how-it-works.svg` on the landing page shows your agent and the Iris products + sending traces through the `redis-iris` SDK to Lens, read in the UI or through the read API. + `lens-turn.svg` in concepts shows one turn as its steps: memory recall, Playbook discovery, + prompt assembly (fetched vs. used), model call, Context Gateway tool call, and outcome. +- **Operator handoff:** until the MOD-18980 pages exist (presumably + `content/operate/iris/lens/`), state the prerequisite ("ask your administrator for your + Lens endpoint, an ingest key, and a read key"), add a TODO comment at each handoff point, + and don't link to unpublished paths. Handoff points: quickstart "Before you begin", + configure "API keys and scopes", and limits. +- **Opaque IDs:** one sentence in concepts, phrased as guidance: use opaque values, such as + internal IDs or hashes, for session, thread, user, and agent IDs. +- **Out of scope for now:** Pydantic AI, turn sampling, the turn `feedback` field, the + "masked" content state (generated API reference only), and `stats()`. + +## Sources + +Check against the latest `origin/main` of each repo, not a local checkout. + +- `redis-iris` `docs/core/lens.md` and `src/redis_iris/lens/`: SDK calls, settings, export + behavior. +- `iris` `lens/dataplane/api/redis.ai.lens.dataplane.smithy`: read API, outcomes, content + states, score bands, limits. +- `iris` `iris/ui/src/screens/lens/`: UI views and labels. +- `iris-sdks` `lens-python-sdk`: the `redis-agent-lens` read client (`LENS_API_KEY`). +- `redis-applied-ai/iris-onprem-preview` `examples/`: LangGraph, Google ADK, and OpenAI + Agents SDK agents that use Agent Memory, Agent Playbooks, and Context Gateway in one turn. + +## Structure + +Section: `content/develop/ai/context-engine/lens/`, linked from the Redis Iris context +engine index. Each topic has one home; other pages link to it. Frontmatter and the +availability banner (`bannerText`) copy the shape of the sibling Iris product pages; the +landing page links its pages with `tile-card`. + +``` +lens/ + +_index.md Lens + - What Lens shows you One trace per agent turn: what was fetched, what entered + the prompt, tokens, timing, outcome + - Why use Lens See why the agent answered the way it did; Iris products + trace themselves; built on OpenTelemetry; works alongside + your existing APM + - How it works Diagram (lens-how-it-works.svg): your agent > redis-iris + SDK > Lens > UI and read API + - Where to start Quickstart, concepts, agent frameworks + +concepts.md Lens concepts + - Sessions, threads, turns You choose session and thread IDs; a turn is one trace, + and its trace ID is the turn ID. Use opaque values, such as + internal IDs or hashes, for all IDs + (Diagram: lens-turn.svg) + - What a turn records Retrievals, memory recalls, prompt assembly, model calls, + tool calls, and Memory, Playbooks, and Context Gateway calls + - Fetched vs. used context What a source returned, what entered the prompt, and + retrievals that came back empty. Playbook discovery results + include their similarity score; Context Gateway results + include their hybrid search score + - Turn outcomes completed, failed, abandoned, refused, timeout + - Agents One agent ID per agent; all agents share an instance + - Content capture Prompts, completions, retrieved text; on by default. With + capture off, Lens keeps IDs, hashes, counts, and sizes + - Retention Lens keeps turn structure, timing, and scores for the full + retention window; captured text has a shorter window + +quickstart.md Lens quickstart + - Before you begin From your administrator: the Lens ingest endpoint and an + ingest key, plus the read API URL, instance ID, and a read + key. Python 3.11 or later (TODO: operator link) + - Install the SDK redis-iris, plus the llm-tracing extra for model calls + - Configure the SDK IRIS_LENS_ENDPOINT, IRIS_LENS_API_KEY, IRIS_LENS_AGENT_ID + - Trace one turn Turn + memory recall + prompt assembly; no model key needed + - Find the turn Get its ID, open it in the UI, fetch it with the read API + - Next steps Instrument, agent frameworks + +instrument.md Instrument an agent + - Turn Lens on Set the endpoint, pass Lens settings to Iris, or call + lens.init() to also trace model calls + - Open a turn lens.turn(): session, thread, user, agent, outcome + - What Iris products record Memory, Playbooks, and Context Gateway calls record + their own steps, sync or async + - Trace model calls OpenAI, Anthropic, Bedrock, LangChain with llm-tracing + - Record retrievals lens.retrieval(): query, data source, results with scores + - Record memory recalls lens.memory_recall() + - Record prompt assembly fetched() and used() + - Add custom spans lens.span() + - Include other spans lens.include() exports spans from other libraries + - Get the turn ID current_trace_id(), to link a turn to your own logs + - Carry a turn across processes inject() and continue_from() + - Shut down shutdown() flushes and turns Lens off for the process + +agent-integration.md Integrate with agent frameworks + - Where the turn opens Table per framework + - LangGraph attach() opens a turn for every graph run + - OpenAI Agents SDK Wrap Runner.run in lens.turn() + - Google ADK Wrap runner.run_async in lens.turn() + - Your own agent loop workflows.turn() or workflows.aturn() + - Example: one turn across Iris products Memory, Playbooks, and Context Gateway + +inspect-sessions.md Inspect sessions + - Connect to Lens Sign in, or paste a read key + - Find a session Filter by agent, time range, user, outcome, session ID + - Read a turn's steps Order, timing, call outcome, result counts, scores + - View captured content What each step returned, while its text is retained + +configure.md Configure Lens + - Settings Endpoint, API key, agent ID, service name, enabled, + content capture + - Advanced settings Own tracer provider, insecure endpoint, drop scopes, + drop Redis commands, thread instrumentation + - API keys and scopes ingest for agents (IRIS_LENS_API_KEY), read for people and + tools (LENS_API_KEY for the read client), full for both; + your administrator creates keys (TODO: operator link) + - Control content capture For Lens, and separately for your own tracer + - Use Lens with your existing OpenTelemetry setup Lens joins your tracer provider; + Iris product spans reach your APM whether Lens is on or off + - Connect over TLS Use an https endpoint; loopback works for local testing + +read-api.md Query sessions and turns + - Authenticate Read API URL, instance ID, and read key (a bearer token); + the read client reads LENS_API_KEY, never IRIS_LENS_API_KEY + - List agents + - List sessions Agent ID and time range, optional filters, cursor + pagination; the response returns the effective range + - Get a session Threads, turns, and outcome counts + - Get a turn Steps and context manifest + - Get captured content By content hash + - Get a Context Gateway tool result + +limits.md Limits and errors + - Request limits Up to 200 sessions per page; sessions up to 10,000 turns; + ingest requests up to 8 MiB by default; session, thread, + user, and agent IDs up to 256 bytes (the SDK truncates + longer IDs and logs a warning); your administrator sets + retention windows and can change the ingest cap + (TODO: operator link) + - How Lens exports Export runs in the background and retries transient + errors; your agent never waits on or fails because of Lens + - Errors Ingest and read error codes + - Troubleshoot a missing turn Endpoint not set, process exited before shutdown(), + missing session ID + +api-reference.md Lens API reference (generated from the public read API OpenAPI document) +``` + +## Examples + +Runnable examples use the repo's tabbed code example system with the Iris tabs. Sources live +in `local_examples/iris/lens//` with `STEP_START`/`STEP_END` markers and example IDs +prefixed `iris_lens_`: `.py` files render as **Python (Iris)** and `.sh` files as +**cURL (Iris)**. Pages render them with `clients-example`, filtered with +`lang_filter="Python (Iris),cURL (Iris)"`. Every example reads its endpoint and keys from +environment variables and uses placeholders, never real values. + +| Set | Page | Steps | Needs | +|---|---|---|---| +| `iris_lens_quickstart` | quickstart | trace a turn, flush, find the turn (Python and cURL) | Lens only | +| `iris_lens_instrument` | instrument | init, open a turn, retrieval, memory recall, prompt assembly, custom span, include, turn ID, cross-process, shutdown | Lens only | +| `iris_lens_langgraph` | agent-integration | wrap a graph run in a turn | Model key | +| `iris_lens_langgraph_attach` | agent-integration | attach Agent Memory to a graph | Model key, Agent Memory | +| `iris_lens_openai_agents` | agent-integration | wrap `Runner.run` in a turn | Model key | +| `iris_lens_google_adk` | agent-integration | wrap `run_async` in a turn | Gemini key | +| `iris_lens_own_loop` | agent-integration | wrap one message handler | Lens only | +| `iris_lens_iris_products` | agent-integration | one turn across Agent Memory, Agent Playbooks, and Context Gateway | All three products | +| `iris_lens_read_api` | read-api | connect, list agents, list sessions, get a session, get a turn, get content, get a gateway result (Python and cURL) | Read key | + +Settings and environment variables on the configure page are plain code blocks, not +example sets. + +## Validation + +- **Offline checks on every PR:** a CI workflow scoped to the Lens pages and example sources. + It checks that each example source parses, its steps match the `clients-example` steps the + pages reference, and its imports resolve against the published packages. No live service. +- **Live run before publication:** a script runs each displayed example against a disposable + local stack (iris-onprem-preview) with Agent Memory, Agent Playbooks, and Context Gateway. + Each case gets fresh keys and a fresh session. A case passes when the read API returns the + turn the example created, with the expected steps. Framework examples call a real model + only when `OPENAI_API_KEY` is set. +- **Facts:** check every value in the limits page, the outcome list, and the settings + against the source files in [Sources](#sources) at the release being documented. +- **Build:** the Hugo build passes with no unresolved link warnings, and every page is + linked from the section index. + +## Remaining work + +- [x] Write the ten pages and two diagrams. +- [x] Write the example sets, and check each against a local ingest stand-in or a fake read API. +- [x] Add the API reference from the published read API OpenAPI document. +- [x] Link the section from the context engine index. +- [ ] Add the offline CI check and the live-run script, and run the live run. +- [ ] When the MOD-18980 operator pages land, replace the administrator wording and resolve + the TODO comments. diff --git a/local_examples/iris/lens/agents/google_adk.py b/local_examples/iris/lens/agents/google_adk.py new file mode 100644 index 0000000000..efc7b78281 --- /dev/null +++ b/local_examples/iris/lens/agents/google_adk.py @@ -0,0 +1,39 @@ +# EXAMPLE: iris_lens_google_adk +# STEP_START google_adk_turn +import asyncio + +from google.adk.agents import Agent +from google.adk.runners import InMemoryRunner +from google.genai import types + +from redis_iris import lens + +lens.init(agent_id="support-bot") +agent = Agent( + name="support", + model="gemini-2.0-flash", + instruction="Answer refund questions in two sentences.", +) +runner = InMemoryRunner(agent, app_name="support") + + +async def handle(session_id, user_id, message): + await runner.session_service.create_session( + app_name="support", user_id=user_id, session_id=session_id + ) + content = types.Content(role="user", parts=[types.Part(text=message)]) + answer = "" + with lens.turn(session_id, user_id=user_id) as turn: + async for event in runner.run_async( + user_id=user_id, session_id=session_id, new_message=content + ): + if event.is_final_response() and event.content: + answer = event.content.parts[0].text + turn.set_outcome("completed") + return answer + + +print(asyncio.run(handle("session-1", "user-1042", "Where can I see my refund status?"))) +# STEP_END + +lens.shutdown() diff --git a/local_examples/iris/lens/agents/iris_products.py b/local_examples/iris/lens/agents/iris_products.py new file mode 100644 index 0000000000..eaec7bb7cd --- /dev/null +++ b/local_examples/iris/lens/agents/iris_products.py @@ -0,0 +1,34 @@ +# EXAMPLE: iris_lens_iris_products +# STEP_START iris_products_turn +from redis_iris import Iris, lens, workflows +from redis_iris.hooks import stack + +lens.init(agent_id="support-bot") + +iris = Iris.from_env() +memory = iris.agent_memory(assistant_id="support-bot") +playbook = iris.agent_playbooks() +gateway = iris.context_gateway() + +hooks = stack(memory.hooks(), playbook.disclosure(audience="support")) + + +def run_agent(context, message, tools): + # Your model and tool loop goes here. It receives the Iris context and tools. + return "Your refund was issued on May 2." + + +def handle(owner, message, thread_id): + with lens.turn(thread_id, user_id=owner) as outer: + with workflows.turn(hooks, message, owner=owner, session=thread_id) as turn: + tools = [*gateway.model_tools(), *hooks.tools()] + answer = run_agent(turn.context, message, tools) + turn.record(answer) + outer.set_outcome("completed") + return answer + + +print(handle("user-1042", "Where can I see my refund status?", "thread-1")) +# STEP_END + +lens.shutdown() diff --git a/local_examples/iris/lens/agents/langgraph_agent.py b/local_examples/iris/lens/agents/langgraph_agent.py new file mode 100644 index 0000000000..511f245aa3 --- /dev/null +++ b/local_examples/iris/lens/agents/langgraph_agent.py @@ -0,0 +1,33 @@ +# EXAMPLE: iris_lens_langgraph +# STEP_START langgraph_turn +from langchain_openai import ChatOpenAI +from langgraph.graph import END, START, MessagesState, StateGraph + +from redis_iris import lens + +lens.init(agent_id="support-bot") +model = ChatOpenAI(model="gpt-4o-mini") + + +def assistant(state: MessagesState): + return {"messages": [model.invoke(state["messages"])]} + + +graph = StateGraph(MessagesState) +graph.add_node("assistant", assistant) +graph.add_edge(START, "assistant") +graph.add_edge("assistant", END) +app = graph.compile() + +thread_id = "thread-1" +with lens.turn(thread_id, user_id="user-1042") as turn: + result = app.invoke( + {"messages": [("user", "Where can I see my refund status?")]}, + {"configurable": {"thread_id": thread_id}}, + ) + turn.set_outcome("completed") + +print(result["messages"][-1].content) +# STEP_END + +lens.shutdown() diff --git a/local_examples/iris/lens/agents/langgraph_attach.py b/local_examples/iris/lens/agents/langgraph_attach.py new file mode 100644 index 0000000000..e0769566fe --- /dev/null +++ b/local_examples/iris/lens/agents/langgraph_attach.py @@ -0,0 +1,34 @@ +# EXAMPLE: iris_lens_langgraph_attach +from langchain_openai import ChatOpenAI +from langgraph.graph import END, START, MessagesState, StateGraph + +from redis_iris import lens + +lens.init(agent_id="support-bot") +model = ChatOpenAI(model="gpt-4o-mini") + + +def assistant(state: MessagesState): + return {"messages": [model.invoke(state["messages"])]} + + +graph = StateGraph(MessagesState) +graph.add_node("assistant", assistant) +graph.add_edge(START, "assistant") +graph.add_edge("assistant", END) + +# STEP_START langgraph_attach +from redis_iris import AgentMemory +from redis_iris.integrations.langgraph import attach + +memory = AgentMemory.from_env() +app = attach(memory, graph).compile() + +result = app.invoke( + {"messages": [("user", "Where can I see my refund status?")]}, + {"configurable": {"user_id": "user-1042", "thread_id": "thread-1"}}, +) +# STEP_END + +print(result["messages"][-1].content) +lens.shutdown() diff --git a/local_examples/iris/lens/agents/openai_agents.py b/local_examples/iris/lens/agents/openai_agents.py new file mode 100644 index 0000000000..f75f4e302b --- /dev/null +++ b/local_examples/iris/lens/agents/openai_agents.py @@ -0,0 +1,23 @@ +# EXAMPLE: iris_lens_openai_agents +# STEP_START openai_agents_turn +import asyncio + +from agents import Agent, Runner + +from redis_iris import lens + +lens.init(agent_id="support-bot") +agent = Agent(name="Support", instructions="Answer refund questions in two sentences.") + + +async def handle(session_id, user_id, message): + with lens.turn(session_id, user_id=user_id) as turn: + result = await Runner.run(agent, message) + turn.set_outcome("completed") + return result.final_output + + +print(asyncio.run(handle("session-1", "user-1042", "Where can I see my refund status?"))) +# STEP_END + +lens.shutdown() diff --git a/local_examples/iris/lens/agents/own_loop.py b/local_examples/iris/lens/agents/own_loop.py new file mode 100644 index 0000000000..8f2bd546aa --- /dev/null +++ b/local_examples/iris/lens/agents/own_loop.py @@ -0,0 +1,25 @@ +# EXAMPLE: iris_lens_own_loop +# STEP_START own_loop_turn +from redis_iris import lens + + +def call_model(message, context): + # Your model call goes here. + return "Your refund was issued on May 2." + + +def handle(session_id, user_id, message): + with lens.turn(session_id, user_id=user_id) as turn: + try: + answer = call_model(message, context=[]) + except Exception: + turn.set_outcome("failed") + raise + turn.set_outcome("completed") + return answer + + +print(handle("session-1", "user-1042", "Where can I see my refund status?")) +# STEP_END + +lens.shutdown() diff --git a/local_examples/iris/lens/instrument/instrument.py b/local_examples/iris/lens/instrument/instrument.py new file mode 100644 index 0000000000..23d1d58342 --- /dev/null +++ b/local_examples/iris/lens/instrument/instrument.py @@ -0,0 +1,100 @@ +# EXAMPLE: iris_lens_instrument +# STEP_START init +from redis_iris import lens + +lens.init(agent_id="support-bot", service_name="support-api") +# STEP_END + +# STEP_START open_turn +import uuid + +session_id = f"session-{uuid.uuid4().hex[:8]}" + +with lens.turn(session_id, thread_id="thread-1", user_id="user-1042") as turn: + turn.set_attribute("app.channel", "web") + answer = "Your refund was issued on May 2." + turn.set_outcome("completed") +# STEP_END + +# STEP_START retrieval +question = "How long do refunds take?" +documents = [ + ("doc-17", 0.88, "Refunds post within 5 to 7 business days."), + ("doc-04", 0.61, "Store credit is available immediately."), +] + +with lens.turn(session_id) as turn: + with lens.retrieval(query=question, data_source="help-center") as search: + search.results(documents) + turn.set_outcome("completed") +# STEP_END + +# STEP_START memory_recall +memories = [ + ("mem-1", 0.91, "The customer prefers email updates."), + ("mem-2", 0.42, "The customer asked about gift wrapping in March."), +] + +with lens.turn(session_id) as turn: + with lens.memory_recall(query=question, store_id="support-memories") as recall: + recall.results(memories) + turn.set_outcome("completed") +# STEP_END + +# STEP_START prompt_assembly +with lens.turn(session_id) as turn: + with lens.retrieval(query=question, data_source="help-center") as search: + search.results(documents) + with lens.memory_recall(query=question, store_id="support-memories") as recall: + recall.results(memories) + + with lens.prompt_assembly() as prompt: + prompt.fetched(documents + memories) + prompt.used([documents[0], memories[0]]) + turn.set_outcome("completed") +# STEP_END + +# STEP_START custom_span +with lens.turn(session_id) as turn: + with lens.span("rank_documents", candidates=len(documents)) as step: + ranked = sorted(documents, key=lambda d: d[1], reverse=True) + step.set_attribute("app.top_document", ranked[0][0]) + turn.set_outcome("completed") +# STEP_END + +# STEP_START include +@lens.include() +def search_vector_store(query): + # Spans that another OpenTelemetry instrumentation creates in here are exported to Lens. + return documents +# STEP_END + +# STEP_START turn_id +with lens.turn(session_id) as turn: + print(f"Turn ID: {lens.current_trace_id()}") + turn.set_outcome("completed") +print(f"Turn ID after the turn: {turn.trace_id}") +# STEP_END + +# STEP_START cross_process +def enqueue(job): + print(f"Queued job with {len(job['lens'])} Lens headers") + return job + + +def worker(job): + with lens.continue_from(job["lens"]): + with lens.span("summarize_case"): + pass + + +with lens.turn(session_id) as turn: + job = enqueue({"case": "case-881", "lens": lens.inject()}) + worker(job) + turn.set_outcome("completed") +# STEP_END + +# STEP_START shutdown +if not lens.shutdown(timeout_millis=10_000): + print("Lens did not finish exporting before the timeout") +# STEP_END diff --git a/local_examples/iris/lens/quickstart/quickstart.py b/local_examples/iris/lens/quickstart/quickstart.py new file mode 100644 index 0000000000..d79203c14d --- /dev/null +++ b/local_examples/iris/lens/quickstart/quickstart.py @@ -0,0 +1,59 @@ +# EXAMPLE: iris_lens_quickstart +# STEP_START trace_turn +import os +import uuid + +from redis_iris import lens + +question = "Where can I see my refund status?" +memories = [ + ("mem-1", 0.91, "The customer prefers email updates."), + ("mem-2", 0.84, "The customer's refund was issued on May 2."), + ("mem-3", 0.42, "The customer asked about gift wrapping in March."), +] + +session_id = f"quickstart-{uuid.uuid4().hex[:8]}" + +with lens.turn(session_id, user_id="user-1042") as turn: + with lens.memory_recall(query=question, store_id="quickstart-store") as recall: + recall.results(memories) + + with lens.prompt_assembly() as prompt: + prompt.fetched(memories) + prompt.used(memories[:2]) + + answer = "Your refund was issued on May 2. You'll get an email when it posts." + turn.set_outcome("completed") + +turn_id = turn.trace_id +print(f"Session: {session_id}") +print(f"Turn: {turn_id}") +# STEP_END + +# STEP_START flush +if not lens.shutdown(timeout_millis=10_000): + raise RuntimeError("Lens did not finish flushing the turn") +# STEP_END + +# STEP_START find_turn +import time + +from redis_agent_lens import Lens, errors + +with Lens( + server_url=os.environ["LENS_URL"], + instance_id=os.environ["LENS_INSTANCE_ID"], +) as client: + for _ in range(30): + try: + detail = client.get_turn(turn_id=turn_id) + break + except errors.NotFoundErrorResponseContent: + time.sleep(2) + else: + raise RuntimeError("The turn isn't readable. Check the iris.lens warnings for an export error.") + +print(f"Outcome: {detail.outcome.value}") +for entry in detail.manifest.entries: + print(f"{entry.source}: fetched {entry.fetched_count}, used {entry.used_count}") +# STEP_END diff --git a/local_examples/iris/lens/quickstart/quickstart.sh b/local_examples/iris/lens/quickstart/quickstart.sh new file mode 100644 index 0000000000..a0764edade --- /dev/null +++ b/local_examples/iris/lens/quickstart/quickstart.sh @@ -0,0 +1,9 @@ +# EXAMPLE: iris_lens_quickstart +set -euo pipefail + +# STEP_START find_turn +curl --fail-with-body --silent --show-error \ + "${LENS_URL%/}/v1/instances/$LENS_INSTANCE_ID/turns/$TURN_ID" \ + -H "Authorization: Bearer $LENS_API_KEY" \ + | jq '{outcome, sources: [.manifest.entries[] | {source, fetchedCount, usedCount}]}' +# STEP_END diff --git a/local_examples/iris/lens/read_api/read_api.py b/local_examples/iris/lens/read_api/read_api.py new file mode 100644 index 0000000000..2f90be9be9 --- /dev/null +++ b/local_examples/iris/lens/read_api/read_api.py @@ -0,0 +1,64 @@ +# EXAMPLE: iris_lens_read_api +# STEP_START connect +import os + +from redis_agent_lens import Lens + +client = Lens( + server_url=os.environ["LENS_URL"], + instance_id=os.environ["LENS_INSTANCE_ID"], +) +# STEP_END + +# STEP_START list_agents +agents = client.list_agents() +for agent in agents.items: + print(agent.agent_id) +# STEP_END + +# STEP_START list_sessions +page = client.list_sessions(agent_id="support-bot", range="24h", outcome=["failed"]) +print(f"Showing {page.effective_range} of sessions") + +sessions = list(page.items) +while page.next_cursor: + page = client.list_sessions( + agent_id="support-bot", range="24h", outcome=["failed"], cursor=page.next_cursor + ) + sessions.extend(page.items) + +for session in sessions: + print(session.session_id, session.turn_count, session.last_seen) +# STEP_END + +# STEP_START get_session +if not sessions: + raise SystemExit("No matching sessions in the last 24 hours") + +session = client.get_session(session_id=sessions[0].session_id) +for summary in session.turns: + outcome = summary.outcome.value if summary.outcome else "no outcome" + print(summary.trace_id, outcome, summary.duration_ms, summary.tokens.total) +# STEP_END + +# STEP_START get_turn +turn = client.get_turn(turn_id=session.turns[0].trace_id) +for entry in turn.manifest.entries: + print(f"{entry.source}: {entry.state.value}, fetched {entry.fetched_count}, used {entry.used_count}") + for candidate in entry.candidates: + print(f" {candidate.id} score={candidate.score} used={candidate.used}") +# STEP_END + +# STEP_START get_content +for entry in turn.manifest.entries: + for candidate in entry.candidates: + if candidate.content and candidate.content.hash: + content = client.get_content(turn_id=turn.trace_id, hash=candidate.content.hash) + print(candidate.id, content.availability.value, content.body) +# STEP_END + +# STEP_START gateway_result +for step in turn.context_retriever_tool_steps: + result = client.get_context_retriever_result(turn_id=turn.trace_id, span_id=step.span_id) + print(step.tool_name, result.outcome.value, result.classification.value) +# STEP_END diff --git a/local_examples/iris/lens/read_api/read_api.sh b/local_examples/iris/lens/read_api/read_api.sh new file mode 100644 index 0000000000..98d6c4ec0e --- /dev/null +++ b/local_examples/iris/lens/read_api/read_api.sh @@ -0,0 +1,48 @@ +# EXAMPLE: iris_lens_read_api +set -euo pipefail + +# STEP_START connect +LENS_API="${LENS_URL%/}/v1/instances/$LENS_INSTANCE_ID" +AUTH="Authorization: Bearer $LENS_API_KEY" +# STEP_END + +# STEP_START list_agents +curl --fail-with-body --silent --show-error "$LENS_API/agents" -H "$AUTH" \ + | jq -r '.items[].agentId' +# STEP_END + +# STEP_START list_sessions +curl --fail-with-body --silent --show-error -G "$LENS_API/sessions" -H "$AUTH" \ + --data-urlencode "agentId=support-bot" \ + --data-urlencode "range=24h" \ + --data-urlencode "outcome=failed" > sessions.json +jq '{effectiveRange, nextCursor, sessions: [.items[] | {sessionId, turnCount, lastSeen}]}' sessions.json +SESSION_ID=$(jq -er '.items[0].sessionId' sessions.json) +# STEP_END + +# STEP_START get_session +curl --fail-with-body --silent --show-error "$LENS_API/sessions/$SESSION_ID" -H "$AUTH" > session.json +jq '[.turns[] | {traceId, outcome, durationMs, tokens: .tokens.total}]' session.json +TURN_ID=$(jq -er '.turns[0].traceId' session.json) +# STEP_END + +# STEP_START get_turn +curl --fail-with-body --silent --show-error "$LENS_API/turns/$TURN_ID" -H "$AUTH" > turn.json +jq '[.manifest.entries[] | {source, state, fetchedCount, usedCount}]' turn.json +# STEP_END + +# STEP_START get_content +HASH=$(jq -r '.manifest.entries[0].candidates[0].content.hash // empty' turn.json) +if [ -n "$HASH" ]; then + curl --fail-with-body --silent --show-error "$LENS_API/turns/$TURN_ID/content/$HASH" -H "$AUTH" \ + | jq '{availability, body}' +fi +# STEP_END + +# STEP_START gateway_result +for SPAN_ID in $(jq -r '.contextRetrieverToolSteps[].spanId' turn.json); do + curl --fail-with-body --silent --show-error \ + "$LENS_API/turns/$TURN_ID/tools/$SPAN_ID/result" -H "$AUTH" \ + | jq '{spanId, outcome, classification}' +done +# STEP_END diff --git a/static/images/ai/context-engine/lens-how-it-works.svg b/static/images/ai/context-engine/lens-how-it-works.svg new file mode 100644 index 0000000000..787f457ecf --- /dev/null +++ b/static/images/ai/context-engine/lens-how-it-works.svg @@ -0,0 +1,41 @@ + + How Lens works: your agent and Iris services, the Redis Iris SDK, Lens, and the Lens UI and read API + Left to right: your agent opens a turn, and the Iris services it calls, Agent Memory, Agent Playbooks, and Context Gateway, record their own steps. The Redis Iris SDK exports the turn to Lens, which assembles turns and groups them into sessions. You inspect sessions in the Lens UI or query turns with the read API. + + + + + + + + TRACE A TURN + + + Your agent + lens.turn() + + Agent Memory + Agent Playbooks + Context Gateway + + + SDK + + + Lens + assembles turns + groups sessions + by agent + + + + + + Lens UI + inspect sessions + + + Read API + query turns + + diff --git a/static/images/ai/context-engine/lens-turn.svg b/static/images/ai/context-engine/lens-turn.svg new file mode 100644 index 0000000000..7d9d7d80a4 --- /dev/null +++ b/static/images/ai/context-engine/lens-turn.svg @@ -0,0 +1,39 @@ + + One Lens turn as a timeline of steps, from memory recall to the outcome + One turn in a session, shown as steps on a timeline. A memory recall fetches 4 memories and Playbook discovery runs at the same time. Prompt assembly then uses 3 of the 4 memories and drops 1. A model call follows, then a Context Gateway tool call, then a second model call. The turn ends with the outcome completed. + + + + ONE TURN · ONE TRACE + + + + + Memory recall + fetched 4 + + + Playbook discovery + runs in parallel + + + Prompt assembly + used 3 · dropped 1 + + + Model call + prompt · completion · tokens + + + Context Gateway tool call + input · result + + + Model call + + + + Outcome: completed + time → + +