diff --git a/content/develop/ai/context-engine/_index.md b/content/develop/ai/context-engine/_index.md
index 658e317709..a512ca4b26 100644
--- a/content/develop/ai/context-engine/_index.md
+++ b/content/develop/ai/context-engine/_index.md
@@ -4,18 +4,19 @@ categories:
- docs
- develop
- ai
-description: Redis Iris is a suite of managed and self-managed services for agent memory, semantic caching, and governed data access.
+description: Redis Iris is a suite of managed and self-managed services that give your customers accurate, consistent experiences with your AI agents by providing memories, approved answers, governed access to business data, and turn-by-turn tracing.
hideListLinks: true
linktitle: Redis Iris context engine
title: Redis Iris context engine
weight: 30
---
-Give your AI agents the context layer they need to reliably act on business data.
+{{< iris-demo preview="true" >}}
-Redis Iris eliminates the infrastructure burden of building context-aware AI agents: persistent memory, semantic caching, governed data access, and live data sync, fully managed on Redis Cloud or self-managed on your own infrastructure.
+Redis Iris eliminates the infrastructure burden of building context-aware AI agents.
-
+
+
{{< tile-card icon="images/dev/icons/icon-redis-iris-64-duotone.png" title="Concepts" description="What happens when an agent asks Redis Iris for context" url="/develop/ai/context-engine/concepts" >}}
{{< 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-iris-64-duotone.png" title="Context Gateway" description="One MCP endpoint for the tools of any MCP server and your Redis data" url="/develop/ai/context-engine/context-gateway" >}}
@@ -29,10 +30,11 @@ Redis Iris eliminates the infrastructure burden of building context-aware AI age
Redis Iris is a production-ready context engine for AI agents that:
-- **Reduces LLM costs**: Semantic caching returns cached responses for similar queries in milliseconds
+- **Answers recurring questions without a model call**: Agent Playbooks returns the answers and procedures your business published, matched to the agent's task by semantic similarity
- **Adds persistent memory**: Agents remember past interactions and user preferences across sessions
- **Connects tools and business data**: Context Gateway gives agents one MCP endpoint for the tools of any MCP server and your Redis data
- **Keeps data fresh**: Data Integration streams live changes from relational databases into Redis within seconds
+- **Shows why an agent answered the way it did**: Lens traces each turn, from what each source returned to what entered the prompt
- **Deploys your way**: Check each service’s operator guide for its managed and self-managed deployment options.
## Why use Redis Iris?
@@ -42,7 +44,7 @@ Redis Iris is a production-ready context engine for AI agents that:
For AI applications
Agents that remember context across sessions and users
-
Faster responses and lower costs through semantic caching
+
Consistent answers to recurring questions, from content your business approved
Reliable, structured access to live business data
No stale data: near real-time sync from your source databases
@@ -53,6 +55,7 @@ Redis Iris is a production-ready context engine for AI agents that:
Managed and self-managed deployment options documented for each service
Python and JavaScript SDKs and REST APIs for all services
Define your data model once, reuse it across all agents
+
Trace each agent turn to see which context reached the prompt
No database setup required on Redis Cloud
diff --git a/content/develop/ai/context-engine/concepts/_index.md b/content/develop/ai/context-engine/concepts/_index.md
index cb4708c039..5b608670af 100644
--- a/content/develop/ai/context-engine/concepts/_index.md
+++ b/content/develop/ai/context-engine/concepts/_index.md
@@ -4,63 +4,76 @@ categories:
- docs
- develop
- ai
-description: What happens when an agent asks Redis Iris for context, and why that's faster than assembling it yourself.
+description: What happens when an agent asks Redis Iris for context, and how it improves customer experiences.
hideListLinks: true
linktitle: Concepts
title: How Redis Iris works
weight: 5
---
-Redis Iris sits between your agent and your data. It's one system. Your agent asks for context. Iris returns it through the fastest available path: a cached answer, a remembered fact, or a live lookup against your business data. That data can live in Redis or in another database you connect. You don't wire together separate lookups yourself. Iris decides which path answers the request.
+
+{width="800"}
-Use Redis Iris when you want agents to respond with cached answers instead of repeat model calls, recall what they've learned across turns and sessions, and act on live business data. Iris builds and maintains that infrastructure for you.
-
-
-{width="650"}
+Your agent asks Redis Iris for context. Iris returns it through the fastest path that answers the request: an answer your business already approved, a remembered fact, or a live lookup against your business data. That data can live in Redis or in another database you connect. You don't wire together separate lookups yourself. Iris builds and maintains that infrastructure for you.
## The mental model
| Without a context layer | With Redis Iris |
|:---|:---|
+| Ask a model which agent or tool should handle each message | Match the message to an intent you defined, and dispatch to its target |
| Replay the full conversation history every turn | Recall only what's relevant, summarized or extracted once |
-| Call the model even when you already know the answer | Return a cached answer when a similar request already ran |
+| Have the model generate an answer your business already wrote | Return a published answer or procedure from a playbook |
| Give the agent a live database connection or let it generate its own queries | Give it a fixed set of governed tools that fetch exactly what's needed |
| Build separate integration code for every data source | Reach Redis or any connected database through the same interface |
+| Guess why an agent gave a wrong answer | Trace each turn to see what context was fetched and what entered the prompt |
+| Start a long agent response over when the client disconnects | Resume the response's event stream from the client's last cursor |
## What happens on a request
-Iris does up to three things before your agent's request reaches the model, in whatever combination the request needs:
+Iris does up to four things around your agent's model call, in whatever combination the request needs:
-1. **Recall.** Iris checks whether it already has a cached answer for a similar request, or relevant memory from earlier in the conversation or a past session. If so, it returns that instead of spending a model call.
-2. **Retrieve.** If the request depends on current business data, Iris fetches exactly what's needed through a governed tool call, not a raw database query the agent constructs itself.
-3. **Record.** After the model responds, Iris writes back what's worth remembering. The next request, in this conversation or a future one, can recall it.
+1. **Route.** Iris matches the message to an intent you defined and returns its target, such as the agent, tool, or workflow that handles it. No model call decides the route.
+2. **Recall.** Iris checks your playbooks for an answer or procedure your business already published, and recalls relevant memory from earlier in the conversation or a past session. A published frequently asked question (FAQ) can answer the request without a model call.
+3. **Retrieve.** If the request depends on current business data, Iris fetches exactly what's needed through a governed tool call, not a raw database query the agent constructs itself.
+4. **Record.** After the model responds, your application records the turn in Agent Memory. Iris extracts what's worth remembering, so the next request, in this conversation or a future one, can recall it.
-Not every request needs all three. A question the cache already answered skips straight to a response. A question about live inventory or account data skips the cache and goes straight to retrieval.
+Not every request needs all four. A recurring question that a published FAQ answers skips straight to a response. A question about live inventory or account data goes straight to retrieval.
## Two phases: serving and recording
Most requests touch Redis Iris at two points:
-1. **Before the model call**, check whether Iris already has what's needed: a cached answer, relevant memory, or business data a tool can fetch.
-2. **After the model responds**, let Iris record what's worth keeping. The next request doesn't start from nothing.
+1. **Before the model call**, route the message and check whether Iris already has what's needed: a published answer, relevant memory, or business data a tool can fetch.
+2. **After the model responds**, record the turn so Iris can keep what's worth keeping. The next request doesn't start from nothing.
### 1. Serving the request (reading)
Iris draws on whichever of its capabilities the request needs, before the model call:
-- **Cache check.** Iris compares the incoming prompt against previously cached responses. A close-enough match returns immediately, skipping the model call.
-- **Memory recall.** Iris searches session and long-term memory for what's relevant to this conversation or user. You don't need to replay the full transcript to the model.
-- **Governed retrieval.** If the request depends on current business data, Iris calls a fixed tool generated from your data model instead of running a raw query.
+- **Intent routing.** Intent Router compares the message with your intents' example phrasings and returns the closest intents, each with a score and a target. Your application decides whether and how to dispatch to the target.
+- **Playbook discovery.** [Agent Playbooks]({{< relref "/develop/ai/context-engine/agent-playbook" >}}) matches the agent's task against published entries by semantic similarity. A FAQ returns its full answer. A skill returns a preview, and the agent loads the full instructions only if it needs them.
+- **Memory recall.** [Agent Memory]({{< relref "/develop/ai/context-engine/agent-memory" >}}) searches session and long-term memory for what's relevant to this conversation or user. You don't need to replay the full transcript to the model.
+- **Governed retrieval.** [Context Gateway]({{< relref "/develop/ai/context-engine/context-gateway" >}}) calls a tool from a Model Context Protocol (MCP) server or from a Context Surface generated from your data model, instead of running a raw query. For tools you opt in, the Gateway's result cache returns the stored result of an identical call until its time to live (TTL) expires.
+
+
### 2. Recording the interaction (writing)
-Iris writes back what's worth keeping after the model responds. Your application doesn't do this explicitly:
+After the model responds, your application writes the turn to Agent Memory. Iris handles the rest:
-- **Session write.** Iris stores the conversation's events as they happen. The next turn has recent history available.
+- **Session write.** Your application stores the conversation's events as they happen. The next turn has recent history available.
- **Background extraction.** Iris pulls durable facts and preferences out of the conversation and stores them separately, with embeddings for later semantic search.
-- **Cache write.** Iris stores the new prompt-and-response pair as a candidate to serve the next similar request without a model call.
+- **Playbook proposals.** If you connect a memory store to a playbook, Iris suggests new FAQs from resolved conversations. A reviewer accepts and publishes each one before agents can retrieve it.
+
+Extraction and proposals run in the background. They don't slow down the response your application is waiting for.
-This write path runs asynchronously. It doesn't slow down the response your application is waiting for.
+## Alongside the request
+
+Two Iris services work alongside the request rather than supplying its context:
+
+- **[Lens]({{< relref "/develop/ai/context-engine/lens" >}})** traces each turn. It records what each source returned, what entered the prompt, and how long each step took. Agent Memory, Agent Playbooks, and Context Gateway calls appear in the trace with no extra code.
+- **Agent Mesh** keeps a long agent response deliverable when the client disconnects. It retains the response's events in Redis, so the client reconnects with its last cursor and receives the updates it missed.
+
## Where context lives
@@ -68,22 +81,31 @@ Redis Iris splits that context across stores, each built for a different lookup
| Store | Holds | Purpose |
|:---|:---|:---|
-| Cache index | Prompt embeddings and their cached responses | Sub-second similarity lookups before a model call |
+| Routers | Intents, their example phrasings with embeddings, and their targets | Routing each message to the agent, tool, or workflow that handles it |
+| Playbooks | Published FAQs and skills, with embeddings | Semantic discovery of answers and procedures your business approved |
| Memory store | Session events and extracted long-term facts, with embeddings | Recall across turns and sessions |
+| Gateway database | Cached tool results and result handles | Reuse of identical tool calls, and large results the agent reads in parts |
| Your business data | Whatever you already store in Redis or a connected database | Queried through governed tools, not a raw connection |
-Run all three fully managed on Redis Cloud, or self-managed on your own infrastructure. Either way, you configure what to keep and for how long, not how the underlying store works.
+Your data stays in your Redis databases. Each Iris service stores its data in Redis databases you own. On Redis Cloud, that's a database in your account. Self-managed, it's your own Redis deployment. Session events, extracted memories, playbook entries, cached tool results, and the embeddings Iris creates for search all use the database's memory like any other data, so you size and pay for that database as usual.
+
+Start small and stay in control of growth. On Redis Cloud, Agent Memory's Quick create uses your free 30 MB database, which is enough to try the quickstarts. As you grow, you configure what to keep and for how long, not how the underlying store works.
## Build against this flow
-- Let Iris decide what's cached and what's fresh. Don't build a separate caching layer on top of the model call.
+- Route with intents you define, not a model call that picks the next agent or tool. Use Intent Router to decide where a message goes, and Agent Playbooks to supply what the answer says.
+- Publish answers your business has approved as playbook FAQs, and repeatable procedures as skills. Don't build a separate caching layer on top of the model call.
+- Cache tool results sparingly. Opt in only the tools whose results stay valid for the TTL you set.
- Write session events as they happen. Let background extraction decide what's durable, rather than deciding yourself on every turn.
- Model your business data as entities and relationships once. Let a Context Gateway Context Surface generate the tools your agent calls, instead of writing a tool per query.
- Scope every write and lookup by user, session, or namespace. This keeps agents and users from colliding.
+- Trace turns with Lens while you build, so you can see which context reached the prompt.
## Next steps
- [How a request flows through Redis Iris]({{< relref "/develop/ai/context-engine/concepts/request-flow" >}})
+- [Agent Playbooks concepts]({{< relref "/develop/ai/context-engine/agent-playbook/concepts" >}})
- [Agent Memory overview]({{< relref "/develop/ai/context-engine/agent-memory/overview" >}})
- [Context Gateway concepts]({{< relref "/develop/ai/context-engine/context-gateway/concepts" >}})
+- [Lens concepts]({{< relref "/develop/ai/context-engine/lens/concepts" >}})
- An [interactive demo]({{< relref "/develop/ai/context-engine/agent-memory/interactive-demo" >}}) of Agent Memory that runs in your browser
diff --git a/content/develop/ai/context-engine/concepts/interactive-demo.md b/content/develop/ai/context-engine/concepts/interactive-demo.md
index 26cf8445c3..0a9bc86acb 100644
--- a/content/develop/ai/context-engine/concepts/interactive-demo.md
+++ b/content/develop/ai/context-engine/concepts/interactive-demo.md
@@ -13,11 +13,11 @@ aliases:
- /develop/ai/context-engine/interactive-demo/
---
-Redis Iris gives an AI agent governed access to live business data, memory, and cached answers. This tour follows one customer request through all four services, one step at a time. It starts where business data enters Iris, with Data Integration, and then follows the request through the other three services, in the order described in [How Redis Iris works](/content/develop/ai/context-engine/concepts/_index.md). The last step takes one service away to show what the agent loses without it.
+{{< iris-demo >}}
-The tour runs in your browser and doesn't connect to any service. Each step links to that service's own interactive demo, which covers it in depth with the same customer and data. The agent's reasoning, the reply, and the timings are illustrative.
+Redis Iris gives an AI agent routing, governed access to live business data, memory, and approved answers. This tour follows one customer request through four services, one step at a time, in the order described in [How Redis Iris works](/content/develop/ai/context-engine/concepts/_index.md). Lens then shows how the request ran. The last step takes one service away to show what the agent loses without it.
-{{< iris-demo >}}
+The tour runs in your browser and doesn't connect to any service. Each step links to that service's docs or its own interactive demo, which covers it in depth. The agent's reasoning, the reply, and the timings are illustrative.
## What each service adds
@@ -26,7 +26,9 @@ The tour runs in your browser and doesn't connect to any service. Each step link
| [Data Integration](/content/develop/ai/context-engine/data-integration/_index.md) | Syncs a change from PostgreSQL to Redis seconds after it happens | The agent reads last night's data and makes a promise it can't keep |
| Context Gateway | Looks up delivery times, alternatives, and past orders through governed tools | The agent can't check whether the food will arrive in time |
| [Agent Memory](/content/develop/ai/context-engine/agent-memory/_index.md) | Recalls last week's order and the customer's peanut allergy | The agent can find the order, but not the allergy |
-| [LangCache](/content/develop/ai/context-engine/langcache/_index.md) | Answers the general delivery fee question from the cache | The model answers it, which takes longer and costs output tokens |
+| [Agent Playbooks](/content/develop/ai/context-engine/agent-playbook/_index.md) | Answers the general delivery fee question with the frequently asked question (FAQ) the support team published | The model doesn't know the app's fees, so it gives a vague answer, or makes one up |
+
+[Lens](/content/develop/ai/context-engine/lens/_index.md) is in preview. It doesn't change the reply. It records each step of the request, including which memories entered the prompt, so you can see how the agent got there.
## Next steps
diff --git a/content/develop/ai/context-engine/concepts/request-flow.md b/content/develop/ai/context-engine/concepts/request-flow.md
index cc5f4d245e..4659e64d3b 100644
--- a/content/develop/ai/context-engine/concepts/request-flow.md
+++ b/content/develop/ai/context-engine/concepts/request-flow.md
@@ -4,179 +4,38 @@ categories:
- docs
- develop
- ai
-description: Trace how a single agent request draws on Redis Iris's caching, memory, and retrieval capabilities before a model call.
+description: See which parts of the Redis Iris stack answer which kind of question, from routing to approved answers, memory, and governed tools.
hideListLinks: true
linktitle: Request flow
title: How a request flows through Redis Iris
weight: 6
---
+{{< iris-request-flow >}}
+
+Select one of Maya's questions to see which parts of the stack answer it. Each question passes through Intent Router, then goes to the component, or components, it needs. The questions come from the [Redis Iris interactive demo](/content/develop/ai/context-engine/concepts/interactive-demo.md).
+
[How Redis Iris works]({{< relref "/develop/ai/context-engine/concepts" >}}) covers the mental model. This page traces the same idea through an actual request, capability by capability.
-Every capability in Iris manages the same limited resource: the model's context window. Semantic caching skips a model call when a similar one already ran. Agent Memory decides what's worth keeping from a conversation. Context Gateway returns the result of one tool call instead of a raw query result, and holds large results as handles. Treat "what goes into the next model call" as a budget. Manage it actively. It doesn't take care of itself once a request reaches Iris.
+Every capability in Iris manages the same limited resource: the model's context window. Agent Playbooks returns an approved answer, or a skill preview the agent expands only when it needs the full procedure. Agent Memory decides what's worth keeping from a conversation. Context Gateway returns the result of one tool call instead of a raw query result, and holds large results as handles. Treat "what goes into the next model call" as a budget. Manage it actively. It doesn't take care of itself once a request reaches Iris.
-These capabilities aren't a fixed sequence. A request can draw on any combination of them before a model call happens: semantic caching for responses to similar prompts, [Agent Memory]({{< relref "/develop/ai/context-engine/agent-memory" >}}) for session and long-term recall, and [Context Gateway]({{< relref "/develop/ai/context-engine/context-gateway" >}}) for tool calls to your MCP servers and your business data. [Data Integration]({{< relref "/develop/ai/context-engine/data-integration" >}}) runs in the background, keeping that business data fresh for Context Gateway to query.
+These capabilities aren't a fixed sequence. A request can draw on any combination of them before a model call happens: Intent Router to choose the agent, tool, or workflow that handles the message, [Agent Playbooks]({{< relref "/develop/ai/context-engine/agent-playbook" >}}) for published answers and procedures, [Agent Memory]({{< relref "/develop/ai/context-engine/agent-memory" >}}) for session and long-term recall, and [Context Gateway]({{< relref "/develop/ai/context-engine/context-gateway" >}}) for tool calls to your business data and other systems.
-Select a node in the diagram for a description of what that capability does, with a link to its docs. Select a scenario button to trace the path a request takes.
+Context Gateway gives the agent one Model Context Protocol (MCP) endpoint for every tool it's allowed to call. Behind that endpoint are the Gateway's Upstreams: MCP servers you register, and Context Surfaces that generate search tools from a data model over your Redis data. The agent never connects to those systems directly. It calls a tool through the Gateway, and the Gateway sends the call to the Upstream that serves it. In search mode, the agent finds tools with `search_tools` and runs them with `run_tool`, instead of loading every tool into its context.
-```context-map {id="iris-request-flow" scope="context-engine"}
-id: iris-request-flow
-scope: context-engine
-nodes:
- agent:
- label: "Agent"
- type: process
- col: 0
- row: 1
- description: |
- The calling application or AI agent that sends a prompt.
- docsUrl: "/develop/ai/agent-builder"
- langcache:
- label: "Cache hit?"
- type: decision
- col: 1
- row: 0
- description: |
- Cache: checks whether a similar prompt is already cached before calling the model.
- links:
- demo:
- label: "Interactive demo"
- url: "/develop/ai/context-engine/langcache/interactive-demo"
- docsUrl: "/develop/ai/context-engine/langcache"
- agentMemory:
- label: "Agent Memory"
- type: process
- col: 1
- row: 1
- description: |
- Agent Memory: session and long-term recall. Recalls session history and long-term facts about the user or task.
- links:
- demo:
- label: "Interactive demo"
- url: "/develop/ai/context-engine/agent-memory/interactive-demo"
- docsUrl: "/develop/ai/context-engine/agent-memory"
- contextRetriever:
- label: "Context Gateway"
- type: process
- col: 1
- row: 2
- description: |
- Context Gateway: tool calls through one endpoint. Calls the tools of MCP servers and Context Surfaces to fetch the business data the agent needs.
- docsUrl: "/develop/ai/context-engine/context-gateway"
- dataIntegration:
- label: "Data Integration"
- type: process
- col: 0
- row: 3
- description: |
- Data Integration: keeps business data fresh. Streams changes from source databases into the data layer Context Gateway queries.
- docsUrl: "/develop/ai/context-engine/data-integration"
- cachedResponse:
- label: "Cached response"
- type: terminal
- col: 2
- row: 0
- description: |
- Return cached response: on a cache hit, Cache returns the stored response directly, skipping the model call.
- modelCall:
- label: "Model call"
- type: process
- col: 2
- row: 1
- description: |
- The model generates a response using the retrieved context.
- links:
- googleAdk:
- label: "Google ADK"
- url: "/integrate/google-adk"
- bedrock:
- label: "Amazon Bedrock"
- url: "/integrate/amazon-bedrock"
- langchain:
- label: "LangChain"
- url: "/integrate/langchain-redis"
- ecosystem:
- label: "More integrations"
- url: "/develop/ai/ecosystem-integrations"
- response:
- label: "Response"
- type: terminal
- col: 3
- row: 1
- description: |
- The final response returned to the caller.
-edges:
- e1:
- from: agent
- to: langcache
- kind: normal
- path: cacheHit
- e2:
- from: langcache
- to: cachedResponse
- kind: branch
- path: cacheHit
- e3:
- from: agent
- to: agentMemory
- kind: normal
- path: memory
- e4:
- from: agentMemory
- to: modelCall
- kind: normal
- path: memory
- e5:
- from: agent
- to: contextRetriever
- kind: normal
- label: "MCP"
- path: context
- e6:
- from: dataIntegration
- to: contextRetriever
- kind: normal
- path: context
- e7:
- from: contextRetriever
- to: modelCall
- kind: normal
- path: context
- e8:
- from: modelCall
- to: response
- kind: normal
- path: "memory,context"
- e9:
- from: response
- to: agentMemory
- kind: loopback
- route: top
- label: "App writes session event"
- path: "memory,context"
-paths:
- cacheHit:
- label: "Cache hit: fastest"
- description: |
- Cache finds a similar prompt already cached and returns it directly. No model call, so this path is the fastest and adds no LLM cost.
- memory:
- label: "Needs memory: recalls session or long-term info"
- description: |
- For a question that depends on earlier turns or what's known about the user, Agent Memory supplies that recall before the model call.
- context:
- label: "Needs business data: retrieves via Context Gateway"
- description: |
- For a question that depends on live business data, Context Gateway calls tools to fetch it before the model call. Data Integration keeps that data fresh.
-```
+[Lens]({{< relref "/develop/ai/context-engine/lens" >}}) traces each of these steps, and Agent Mesh keeps the response stream resumable if the client disconnects. Neither supplies context to the model call, so they work alongside every request rather than answering it.
+
+
## Two things Iris changes
-**Isolation is about meaning, not just timing.** Multiple agents or users often share Iris. The conflict that matters usually isn't two writes touching the same record at the same instant. It's two agents writing similar-but-different memories about the same user, or two near-duplicate cache entries competing to answer the same class of question. Both writes can be individually valid and still disagree with each other. Scoping every write and lookup by user, session, and data type prevents this. Treat those scoping identifiers as carefully as you'd treat any other access boundary in your application.
+**Isolation is about meaning, not just timing.** Multiple agents or users often share Iris. The conflict that matters usually isn't two writes touching the same record at the same instant. It's two agents writing similar-but-different memories about the same user, or two near-duplicate playbook entries competing to answer the same class of question. Both writes can be individually valid and still disagree with each other. Scoping every write and lookup by user, session, and data type prevents this. Treat those scoping identifiers as carefully as you'd treat any other access boundary in your application.
**Trust follows the agent, not the data source.** Application code is trusted by default, and the database enforces permissions at the boundary between code and data. An agent complicates that. Its next action can be shaped by content it's processing, such as a retrieved document or a tool's own output. You don't fully control that content. Assume anything an agent reads can influence what it does next. Design the tools and memory it can reach with that in mind. Don't trust content just because a trusted code path read it.
## Next steps
+- [Agent Playbooks concepts]({{< relref "/develop/ai/context-engine/agent-playbook/concepts" >}})
- [Agent Memory overview]({{< relref "/develop/ai/context-engine/agent-memory/overview" >}})
- [Context Gateway concepts]({{< relref "/develop/ai/context-engine/context-gateway/concepts" >}})
- An [interactive demo]({{< relref "/develop/ai/context-engine/agent-memory/interactive-demo" >}}) of Agent Memory that runs in your browser
diff --git a/layouts/shortcodes/iris-demo.html b/layouts/shortcodes/iris-demo.html
index 43f95f49df..32da6236fd 100644
--- a/layouts/shortcodes/iris-demo.html
+++ b/layouts/shortcodes/iris-demo.html
@@ -2,14 +2,17 @@
iris-demo
---------
Scripted, dependency-free tour of Redis Iris as one system, one step at
- a time: data enters through Data Integration, then a single agent
- request draws on LangCache, Agent Memory, and Context Gateway. Each
+ a time. Each
step links to that service's own demo for depth, and the last step
takes one service away to show what the agent loses. Runs entirely in
the browser; no service is called.
- Usage (no arguments):
+ Usage:
{{< iris-demo >}}
+ {{< iris-demo preview="true" >}}
+
+ With preview="true", the stage plays the tour's steps by itself, with no
+ step cards or controls, as a link to the interactive demo page.
Base styles come from context-retriever-demo.css so the Iris demos share
one look; iris-demo.css adds this demo's pieces.
@@ -30,13 +33,21 @@
{{- /* Links to each service's own demo, resolved here so they carry the site's base path.
- The Data Integration demo isn't published yet, so its link goes to the overview. */}}
+ Context Gateway, Agent Playbooks, and Lens have no tour-sized demo, so their links go to the overview. */}}
+{{- /* TODO: add a data-link-router attribute, resolved with relref like the others, to /develop/ai/context-engine/intent-router when intent-router-docs-align merges into iris-ga-docs. Until then the Intent Router step has no "in depth" link. */}}
{{- /* The stage draws the Iris architecture diagram from redis.io/iris, as published there. */}}
-
+{{- if $preview }}
+
+{{- end }}
diff --git a/layouts/shortcodes/iris-request-flow.html b/layouts/shortcodes/iris-request-flow.html
new file mode 100644
index 0000000000..0fafefdf28
--- /dev/null
+++ b/layouts/shortcodes/iris-request-flow.html
@@ -0,0 +1,28 @@
+{{- /*
+ iris-request-flow
+ -----------------
+ Which part of the Redis Iris stack answers which question: the stack on
+ the left, questions from the Iris tour's story on the right. Selecting a
+ question sends a dot to Intent Router and on to the components that
+ answer it. Runs entirely in the browser; no service is called.
+
+ Usage (no arguments):
+ {{< iris-request-flow >}}
+
+ Base styles come from context-retriever-demo.css so the Iris demos share
+ one look; iris-request-flow.css adds this demo's pieces.
+*/ -}}
+
+{{- /* ?v= is a hash of the file, so a reload never runs a stale cached copy. */ -}}
+{{- $v := dict -}}
+{{- range slice "css/context-retriever-demo.css" "css/iris-request-flow.css" "js/iris-request-flow.js" -}}
+ {{- $v = merge $v (dict . (substr (md5 (os.ReadFile (print "static/" .))) 0 10)) -}}
+{{- end }}
+
+
+
+
+{{- /* data-base carries the site's base path, so the docs links work under a /docs/latest/ subpath. */}}
+
+
+
diff --git a/static/css/iris-demo.css b/static/css/iris-demo.css
index b47d22a70b..bac94fa077 100644
--- a/static/css/iris-demo.css
+++ b/static/css/iris-demo.css
@@ -9,11 +9,15 @@
.iris .iris-svc-cache { --svc: #b35900; --svc-bg: rgba(179, 89, 0, .09); }
.iris .iris-svc-memory { --svc: #1a7f4b; --svc-bg: rgba(26, 127, 75, .09); }
.iris .iris-svc-retriever { --svc: #6a4cff; --svc-bg: rgba(106, 76, 255, .09); }
-.iris .iris-svc-rdi { --svc: #0b7a99; --svc-bg: rgba(11, 122, 153, .09); }
+.iris .iris-svc-router { --svc: #b0306a; --svc-bg: rgba(176, 48, 106, .09); }
+.iris .iris-svc-lens { --svc: #ff4438; } /* Lens adds no part to the reply, so its colour only marks its step */
/* ---- the stage: the Iris architecture diagram from redis.io/iris ---- */
.iris-stagewrap { position: relative; margin-bottom: 12px; }
-.iris-stage { position: relative; aspect-ratio: 893 / 570; border-radius: 11px; overflow: hidden; background: #0D212C; }
+/* On the tour page, the drawing is a little narrower than the step cards below it, and centred; full width on phones. */
+.iris:not(.iris-preview) .iris-stagewrap { max-width: 75%; margin-left: auto; margin-right: auto; }
+@media (max-width: 640px) { .iris:not(.iris-preview) .iris-stagewrap { max-width: 100%; } }
+.iris-stage { position: relative; aspect-ratio: 613.5 / 423.5; /* VIEW in iris-demo.js */ border-radius: 11px; overflow: hidden; background: #0D212C; }
/* The four published versions of the drawing, stacked twice. In a stack, a step shows the versions it needs and the
script sets how they blend; isolation keeps that blend inside the stack. The new stack fades in over the old one. */
.iris-artset { position: absolute; inset: 0; isolation: isolate; opacity: 0; transition: opacity .45s; }
@@ -26,20 +30,23 @@
.iris-pk-core { fill: #fff; }
/* Maya, the user, in the drawing's own style. Her link to the agent is dimmed, as the drawing dims a link, except on the steps she's part of. */
.iris-maya { pointer-events: none; }
-.iris-maya-name { font-family: "Space Mono", var(--rcr-mono); font-size: 8.4px; letter-spacing: .3px; fill: #fff; }
+.iris-maya-name, .iris-relabel-name { font-family: "Space Mono", var(--rcr-mono); font-size: 8.4px; letter-spacing: .3px; fill: #fff; }
+/* The current service names over the drawing's published labels, shown with their lit version. */
+.iris-relabel { opacity: 0; transition: opacity .45s; pointer-events: none; }
+.iris-relabel.is-on { opacity: 1; }
+/* Agent Playbooks' disc, redrawn with its icon: dimmed as the drawing dims a service, at 40%, unless it's lit. */
+.iris-pbdisc { pointer-events: none; }
+.iris-pbdisc-art { opacity: .4; transition: opacity .45s; }
+.iris-pbdisc.is-lit .iris-pbdisc-art { opacity: 1; }
.iris-maya-link { opacity: .4; transition: opacity .45s; }
.iris-maya-link circle { opacity: 0; transition: opacity .45s; }
.iris-maya-link.is-lit, .iris-maya-link.is-lit circle { opacity: 1; }
-/* The Data Integration gear, turning as on redis.io/iris, in the drawing's own colors. Dimmed like the drawing dims it. */
-.iris-gear { pointer-events: none; }
-.iris-gear-patch { fill: #0D212C; }
-.iris-gear-back, .iris-gear-walls path { fill: #351D22; }
-.iris-gear-face { fill: #89302E; }
-.iris-gear-walls path, .iris-gear-face { stroke: #FF4438; stroke-width: .84px; stroke-linejoin: round; stroke-linecap: round; }
-.iris-gear-body, .iris-gear-ahead { transition: opacity .45s; }
-.iris-gear:not(.is-lit) .iris-gear-body { opacity: .4; }
-.iris-gear-ahead.is-dim, .iris-gear:not(.is-lit) .iris-gear-ahead.is-lit { opacity: 0; }
-.iris-gear:not(.is-lit) .iris-gear-ahead.is-dim { opacity: 1; }
+/* Intent Router's and Lens's discs, dimmed like the drawing dims a service, with their labels only while they're lit. */
+.iris-router-disc, .iris-router-label, .iris-lensfig-disc, .iris-lensfig-label { transition: opacity .45s; }
+.iris-router:not(.is-lit) .iris-router-disc, .iris-lensfig:not(.is-lit) .iris-lensfig-disc { opacity: .4; }
+.iris-router:not(.is-lit) .iris-router-label, .iris-lensfig:not(.is-lit) .iris-lensfig-label { opacity: 0; }
+/* The patch over the drawing's Data Integration corner, in the drawing's background colour. */
+.iris-cover { fill: #0D212C; pointer-events: none; }
.iris-tag {
position: absolute; pointer-events: none; white-space: nowrap; font-family: var(--rcr-mono); font-size: 10px; font-weight: 600;
color: #091a23; background: #fff; border-radius: 6px; padding: 3px 8px; box-shadow: 0 4px 14px rgba(0, 0, 0, .35);
@@ -55,10 +62,7 @@
fill: none; stroke: #FF4438; stroke-width: 2px; vector-effect: non-scaling-stroke; opacity: 0; pointer-events: none;
transform-box: fill-box; transform-origin: center;
}
-.iris-stage.is-step-rdi .iris-pulse { animation: iris-pulse 1.8s ease-out infinite; }
-.iris-stagewrap:has(.iris-pgbtn:hover) .iris-ring[data-src="pg"]:not(.iris-pulse),
.iris-stagewrap:has(.iris-redisbtn:hover) .iris-ring[data-src="redis"]:not(.iris-pulse) { opacity: 1; }
-.iris-stagewrap:has(.iris-pgbtn:focus-visible) .iris-ring[data-src="pg"]:not(.iris-pulse),
.iris-stagewrap:has(.iris-redisbtn:focus-visible) .iris-ring[data-src="redis"]:not(.iris-pulse) { opacity: 1; stroke: #fff; stroke-width: 2.5px; }
@keyframes iris-pulse { 0% { opacity: .95; transform: scale(1); } 100% { opacity: 0; transform: scale(1.3); } }
@@ -141,6 +145,7 @@
.iris-stephead { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 4px 10px; font-size: 11.5px; color: var(--rcr-faint); }
.iris-count { font-weight: 700; letter-spacing: .3px; text-transform: uppercase; color: var(--svc, var(--rcr-muted)); }
.iris .iris-clock { font-size: 11.5px; }
+.iris-badge { font-size: 11px; font-weight: 600; border: 1px solid currentColor; border-radius: 4px; padding: 0 6px; color: var(--svc, var(--rcr-muted)); }
.iris-title { font-size: 17px; font-weight: 700; line-height: 1.3; margin: 4px 0 6px; color: var(--rcr-ink); }
.iris-text { font-size: 14px; line-height: 1.55; color: var(--rcr-muted); max-width: 68ch; }
.iris-fact { display: grid; grid-template-columns: minmax(0, 1fr); gap: 6px; margin-top: 10px; padding: 9px 11px; border-radius: 8px; background: var(--rcr-surface-2); border: 1px solid var(--rcr-line); font-size: 13px; }
@@ -174,10 +179,10 @@
.iris .iris-replymsg { max-width: 100%; line-height: 1.65; background: #fff; }
/* A highlighter in four clearly different hues, so each part reads as its service at a glance. */
.iris-src { border-radius: 3px; padding: 1px 3px; background: var(--src-bg); box-shadow: inset 0 -2px 0 var(--src); -webkit-box-decoration-break: clone; box-decoration-break: clone; }
-.iris-src-rdi { --src: #0b7a99; --src-bg: #cdebf4; }
.iris-src-retriever { --src: #6a4cff; --src-bg: #e3ddff; }
.iris-src-memory { --src: #1a7f4b; --src-bg: #cfeedd; }
.iris-src-cache { --src: #b35900; --src-bg: #fde0c2; }
+.iris-src-router { --src: #b0306a; --src-bg: #f6d6e5; }
/* The key: one chip per service, styled like its parts of the reply. */
.iris-legend { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }
.iris-legend .rcr-lbl { flex-basis: 100%; }
@@ -196,6 +201,54 @@
.iris-totals { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 18px; font-size: 13px; color: var(--rcr-muted); }
.iris-totals b { color: var(--rcr-ink); }
+/* ---- the Lens step: the turn as a timeline of steps, and what Lens recorded for the selected one ---- */
+.iris-lens { gap: 8px; }
+.iris-lensmodes { display: flex; flex-wrap: wrap; gap: 6px; }
+.iris-lensnote:empty { position: absolute; } /* out of the grid's gaps, but still there for screen readers as a live region */
+.iris .iris-lensmode.is-on { color: #fff; background: var(--svc); border-color: var(--svc); }
+.iris .iris-lensbug { margin: 0; font-size: 13px; line-height: 1.5; color: var(--rcr-ink); border-left: 3px solid var(--rcr-bad); padding: 1px 0 1px 9px; }
+.iris-lenshead { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 2px 12px; font-size: 12px; color: var(--rcr-muted); }
+.iris-lenshead b { color: var(--rcr-ink); font-weight: 600; }
+.iris-nw { white-space: nowrap; }
+.iris-timeline { display: grid; gap: 1px; }
+.iris .iris-trow {
+ display: grid; grid-template-columns: minmax(0, 15em) minmax(0, 1fr) 4.2em; align-items: center; gap: 8px; width: 100%; margin: 0;
+ padding: 4px 6px; border: 1px solid transparent; border-radius: 6px; background: transparent; font: inherit; font-size: 12.5px; line-height: 1.35;
+ text-align: left; color: var(--rcr-ink); cursor: pointer;
+}
+.iris .iris-trow:hover { background: var(--rcr-surface); }
+.iris .iris-trow.is-on { background: var(--rcr-surface); border-color: var(--rcr-line-strong); }
+.iris-tname { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 600; }
+.iris-top { font-weight: 400; color: var(--rcr-muted); }
+.iris .iris-top.iris-bad { font-weight: 700; color: var(--rcr-bad); }
+.iris-ttrack { position: relative; height: 8px; border-radius: 4px; background: var(--rcr-line); }
+.iris-tbar { position: absolute; top: 0; bottom: 0; min-width: 3px; border-radius: 4px; background: var(--src, var(--rcr-faint)); }
+.iris-tms { text-align: right; color: var(--rcr-muted); }
+/* On a narrow screen, each step's bar goes under its name, which can wrap. */
+@container (max-width: 520px) {
+ .rcr .iris-trow { grid-template-columns: minmax(0, 1fr) auto; gap: 3px 8px; }
+ .rcr .iris-tname { white-space: normal; }
+ .rcr .iris-ttrack { grid-column: 1 / -1; grid-row: 2; height: 6px; }
+}
+.iris-tdetail { display: grid; gap: 5px; padding-top: 8px; border-top: 1px solid var(--rcr-line); font-size: 13px; }
+.iris-tdhead { font-weight: 700; font-size: 12.5px; }
+.iris .iris-tdetail p { margin: 0; font-size: inherit; line-height: 1.5; } /* not the page's paragraph size */
+.iris-tresult { display: grid; grid-template-columns: 8.2em minmax(0, 1fr); gap: 8px; align-items: baseline; }
+.iris-mark { justify-self: start; white-space: nowrap; font-size: 10.5px; font-weight: 700; border: 1px solid currentColor; border-radius: 4px; padding: 0 5px; color: var(--rcr-ok); }
+.iris-tresult.is-unused .iris-mark, .iris-tresult.is-unused > span:last-child { color: var(--rcr-bad); }
+.iris-tio { display: grid; grid-template-columns: 4.5em minmax(0, 1fr); gap: 3px 10px; align-items: baseline; }
+.iris .iris-tio code { justify-self: start; }
+.iris-tintents { display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: 3px 10px; align-items: baseline; } /* an intent's name never wraps */
+.iris .iris-tintents code { justify-self: start; }
+.iris-tintents .iris-kv { overflow-wrap: anywhere; }
+/* On a phone, each mark goes in front of its result instead of in a column of its own, and each intent's score and target go under its name. */
+@container (max-width: 400px) {
+ .rcr .iris-tresult { display: block; }
+ .rcr .iris-tresult .iris-mark { display: inline-block; margin-right: 6px; }
+ .rcr .iris-tintents { grid-template-columns: minmax(0, 1fr); gap: 1px; }
+ .rcr .iris-tintents .iris-kv { margin-bottom: 4px; }
+}
+
/* ---- what if a service were missing ---- */
.iris-whatif { display: grid; grid-template-columns: minmax(0, 1fr); gap: 8px; margin-top: 10px; }
.iris-picks { display: flex; flex-wrap: wrap; gap: 6px; }
@@ -226,8 +279,19 @@
.iris .iris-dot.is-on { background: var(--rcr-accent); transform: scale(1.25); }
@media (prefers-reduced-motion: reduce) {
- .iris-artset, .iris-tag, .iris-maya-link, .iris-maya-link circle { transition: none; }
+ .iris-artset, .iris-tag, .iris-maya-link, .iris-maya-link circle, .iris-relabel, .iris-router-disc, .iris-router-label, .iris-lensfig-disc, .iris-lensfig-label, .iris-pbdisc-art { transition: none; }
.iris-window, .iris-backdrop, .iris-flip.is-flipped .iris-new { animation: none; }
- .iris-stage.is-step-rdi .iris-pulse { animation: none; opacity: 1; }
.iris-chip.is-new { animation: none; }
}
+
+/* ---- preview: the stage alone, as a link to the tour ---- */
+.iris-preview-link { display: block; max-width: 70%; margin: 8px 0 24px; text-decoration: none; border-radius: 11px; }
+@media (max-width: 640px) { .iris-preview-link { max-width: 100%; } } /* a part of a phone screen is too small to read */
+.iris-preview-link:focus-visible { outline: 3px solid #FF4438; outline-offset: 3px; }
+.iris-preview .iris-stagewrap { margin-bottom: 0; }
+.iris-preview .iris-hit { pointer-events: none; }
+.iris-preview-cue {
+ display: inline-block; margin-top: 8px; font-family: var(--rcr-mono); font-size: 13px; font-weight: 600; color: #fff;
+ background: #091A23; border: 1px solid #5C707A; border-radius: 6px; padding: 5px 10px; transition: background .2s, border-color .2s;
+}
+.iris-preview-link:hover .iris-preview-cue, .iris-preview-link:focus-visible .iris-preview-cue { background: #FF4438; border-color: #FF4438; }
diff --git a/static/css/iris-request-flow.css b/static/css/iris-request-flow.css
new file mode 100644
index 0000000000..16b5fac67d
--- /dev/null
+++ b/static/css/iris-request-flow.css
@@ -0,0 +1,64 @@
+/* =========================================================================
+ Redis Iris request flow: which part of the stack answers which question
+ Additions to context-retriever-demo.css, which the shortcode loads first
+ so the Iris demos share one look. Scoped under `.irf`; the root element
+ also carries `.rcr` for the shared tokens and base styles.
+ ========================================================================= */
+
+/* One colour per component, matching the Iris tour's colours for the same services. */
+.irf .irf-c-router, .irf .irf-k-router { --c: #b0306a; --c-bg: #f6d6e5; }
+.irf .irf-c-playbooks, .irf .irf-k-playbooks { --c: #b35900; --c-bg: #fde0c2; }
+.irf .irf-c-memory, .irf .irf-k-memory { --c: #1a7f4b; --c-bg: #cfeedd; }
+.irf .irf-c-gateway, .irf .irf-k-gateway { --c: #6a4cff; --c-bg: #e3ddff; }
+.irf .irf-c-lens, .irf .irf-c-mesh, .irf .irf-k-along { --c: #5c707a; --c-bg: #e8ebec; }
+
+/* Stack on the left, questions on the right; stacked on a narrow screen. The overlay draws the dot's trail. */
+.irf-board { position: relative; display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 16px 56px; align-items: start; margin-bottom: 14px; }
+@container (max-width: 560px) { .irf-board { grid-template-columns: minmax(0, 1fr); } }
+.irf-over { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; overflow: visible; }
+.irf-trail { fill: none; stroke: #ff4438; stroke-width: 1.5; stroke-dasharray: 4 4; opacity: .55; transition: opacity .6s; }
+.irf-glow { fill: #ff4438; opacity: .35; }
+.irf-core { fill: #ff4438; }
+
+/* ---- the stack ---- */
+.irf-stack { display: grid; gap: 6px; }
+.irf-stack .rcr-lbl, .irf-asks .rcr-lbl { margin-bottom: 2px; }
+.irf-comp {
+ position: relative; display: grid; gap: 1px; border: 1px solid var(--rcr-line); border-left: 4px solid var(--c); border-radius: 8px;
+ background: var(--rcr-surface); padding: 8px 12px; transition: background .3s, border-color .3s, box-shadow .3s;
+}
+.irf-comp.is-along { border-style: dashed; border-left-style: solid; }
+.irf-comp.is-lit { background: var(--c); border-color: var(--c); box-shadow: 0 2px 8px rgba(9, 26, 35, .18); }
+.irf-comp.is-lit .irf-name, .irf-comp.is-lit .irf-role { color: #fff; }
+.irf-name { font-weight: 600; font-size: 14px; transition: color .3s; }
+.irf-role { font-size: 12.5px; color: var(--rcr-muted); transition: color .3s; }
+.irf-tag {
+ position: absolute; top: 8px; right: 10px; font-family: var(--rcr-mono); font-size: 11px; font-weight: 600; color: var(--c);
+ background: #fff; border-radius: 999px; padding: 1px 8px; opacity: 0; transition: opacity .3s;
+}
+.irf-comp.is-lit .irf-tag:not(:empty) { opacity: 1; }
+.irf-along { margin-top: 6px; font-size: 12px; font-style: italic; color: var(--rcr-faint); }
+.irf-base { margin-top: 4px; text-align: center; font-weight: 600; font-size: 13px; color: #fff; background: #091a23; border-radius: 8px; padding: 7px 12px; }
+
+/* ---- the questions ---- */
+.irf-asks { display: grid; gap: 10px; align-content: start; }
+.irf .irf-ask {
+ margin: 0; cursor: pointer; text-align: left; font: inherit; font-size: 14px; line-height: 1.45; color: #fff;
+ background: #163341; border: 1px solid #163341; border-radius: 14px 14px 4px 14px; padding: 10px 14px; transition: background .2s, border-color .2s, box-shadow .2s;
+}
+.irf .irf-ask:hover { border-color: var(--rcr-accent); }
+.irf .irf-ask.is-on { background: #091a23; border-color: var(--rcr-accent); box-shadow: 0 0 0 2px rgba(255, 68, 56, .35); }
+
+/* ---- what happened ---- */
+.irf-detail { display: grid; gap: 8px; }
+.irf-q { font-weight: 600; font-size: 15px; }
+.irf-row { display: grid; grid-template-columns: 9.5rem minmax(0, 1fr); gap: 10px; font-size: 13.5px; align-items: baseline; }
+@container (max-width: 560px) { .irf-row { grid-template-columns: minmax(0, 1fr); gap: 2px; } }
+.irf-rk { font-weight: 600; font-size: 12.5px; color: var(--c, var(--rcr-muted)); }
+.irf-links { display: flex; flex-wrap: wrap; gap: 6px 16px; font-size: 13px; }
+.irf .irf-doc { color: var(--rcr-accent-2); text-decoration: none; }
+.irf .irf-doc:hover { text-decoration: underline; }
+
+@media (prefers-reduced-motion: reduce) {
+ .irf-comp, .irf-tag, .irf-trail, .irf .irf-ask, .irf-name, .irf-role { transition: none; }
+}
diff --git a/static/images/ai/context-engine/iris-mental-model.svg b/static/images/ai/context-engine/iris-mental-model.svg
index fe82b1693a..3c7f986228 100644
--- a/static/images/ai/context-engine/iris-mental-model.svg
+++ b/static/images/ai/context-engine/iris-mental-model.svg
@@ -1 +1,77 @@
-
\ No newline at end of file
+
diff --git a/static/images/dev/icons/icon-redis-agent-mesh-64-duotone.png b/static/images/dev/icons/icon-redis-agent-mesh-64-duotone.png
new file mode 100644
index 0000000000..0d7b3edfe1
Binary files /dev/null and b/static/images/dev/icons/icon-redis-agent-mesh-64-duotone.png differ
diff --git a/static/images/dev/icons/icon-redis-intent-router-64-duotone.png b/static/images/dev/icons/icon-redis-intent-router-64-duotone.png
new file mode 100644
index 0000000000..b6c950eb30
Binary files /dev/null and b/static/images/dev/icons/icon-redis-intent-router-64-duotone.png differ
diff --git a/static/images/dev/icons/icon-redis-lens-64-duotone.png b/static/images/dev/icons/icon-redis-lens-64-duotone.png
new file mode 100644
index 0000000000..b9087287fe
Binary files /dev/null and b/static/images/dev/icons/icon-redis-lens-64-duotone.png differ
diff --git a/static/images/dev/icons/icon-redis-playbooks-64-duotone.png b/static/images/dev/icons/icon-redis-playbooks-64-duotone.png
new file mode 100644
index 0000000000..981464bf60
Binary files /dev/null and b/static/images/dev/icons/icon-redis-playbooks-64-duotone.png differ
diff --git a/static/js/iris-demo.js b/static/js/iris-demo.js
index c80afe5005..3466b044c2 100644
--- a/static/js/iris-demo.js
+++ b/static/js/iris-demo.js
@@ -5,22 +5,30 @@
One idea per step, on an animated isometric drawing after the Iris
architecture diagram, where packets show each step's data moving:
- data enters through Data Integration (a change in PostgreSQL reaches
- Redis), Maya asks the food delivery app's assistant to reorder last
+ Maya asks the food delivery app's assistant to reorder last
week's curry within 30 minutes and asks about the delivery fee, then
- LangCache, Agent Memory, and Context Gateway each add one part of the
- answer, and the model writes the reply. The last step takes one service
- away and shows only the part of the reply it changes. Each step links
- to that service's own demo, which covers it in depth; this page doesn't
- repeat their API detail.
+ Intent Router matches it to intents, Agent Playbooks, Agent Memory, and
+ Context Gateway each add one part of the answer, and the model writes
+ the reply. Lens then shows the request as it ran. The last step takes one
+ service away and shows only the part of the reply it changes. Each step
+ links to that service's docs or own demo, which cover it in depth; this
+ page doesn't repeat their API detail.
trace() models every combination of services: the request's steps and
- API shapes (matching agent-memory-demo.js, langcache-demo.js,
- context-retriever-demo.js, and rdi-demo.js), the reply credited by
- source, and the scorecard. The tour shows the full trace, and the last
- step compares it with one service missing. Modelled rather than
- observed: the similarity score, the agent's reasoning and reply, and
- the timings and token counts.
+ API shapes (matching the Intent Router match API, the Agent Playbooks discovery API, the Context
+ Gateway MCP endpoint, and agent-memory-demo.js), the reply
+ credited by source, and the scorecard. The tour shows the full trace,
+ the Lens step shows its steps as Lens records them, and the last step
+ compares it with one service missing. Modelled rather
+ than observed: the similarity score, the agent's reasoning and reply,
+ and the timings and token counts.
+
+ Internal keys predate the current services and stay as they are, because
+ the drawing, routes, and styles use them: "cache" is Agent Playbooks and
+ "retriever" is Context Gateway. The drawing still shows the published
+ LangCache and Context Retriever labels, so the stage draws the current
+ names over them (RELABEL). The drawing also shows Data Integration,
+ which the tour leaves out, so the stage covers that corner (COVER).
Customers, restaurants, orders, and Maya's memories come from the same
sample data as those demos. Keep them in sync.
@@ -36,7 +44,7 @@
var OWNER = "u101", ASSISTANT = "food-assistant", SESSION = "maya-1002-dinner", PAST_SESSION = "maya-0924-dinner";
var EXTRACTION_AT = "2026-09-24T18:03:07.314Z";
- var SYNC_AT = "2026-10-02T18:20:03Z", ASK_AT = "2026-10-02T18:24:10Z", REPLY_AT = "2026-10-02T18:24:13Z";
+ var ASK_AT = "2026-10-02T18:24:10Z", REPLY_AT = "2026-10-02T18:24:13Z";
var WITHIN = 30; // minutes Maya can wait
var MESSAGE = "Can you order the curry I had last week again? It needs to be here within 30 minutes. And how much is delivery these days?";
var QUESTION = "How much is delivery these days?";
@@ -44,7 +52,7 @@
function ts(y, m, d, h) { return Math.floor(Date.UTC(y, m - 1, d, h == null ? 12 : h) / 1000); }
var LOTUS = { id: "r209", name: "Lotus Thai", cuisine: "thai", city: "austin", rating: 4.1, price_level: 1, avg_delivery_min: 25 };
- var BUSY = 45; // Lotus Thai's delivery time tonight, after the kitchen got busy
+ var BUSY = 45; // Lotus Thai's delivery time tonight, after the kitchen got busy, as it stands in Redis
var BSK = { id: "r201", name: "Bangkok Street Kitchen", cuisine: "thai", city: "austin", rating: 4.7, price_level: 2, avg_delivery_min: 30 };
var CURRY = { id: "o3003", customer_id: "u101", restaurant_id: "r209", status: "delivered", total: 22.75, placed_ts: ts(2026, 9, 24, 18),
items: "Green curry without fish sauce, jasmine rice" };
@@ -60,22 +68,35 @@
{ key: "spicy", type: "semantic", text: "User loves spicy Thai food." },
{ key: "veg", type: "semantic", text: "User is vegetarian." }
];
- /* The LangCache demo's cached answer to the delivery fee question. */
+ /* The support playbook's published FAQ on the delivery fee. Discovery returns FAQs scored at or above the
+ playbook's threshold, 0.7 by default. */
var FEE_PROMPT = "How much is the delivery fee?";
var FEE = "Delivery costs $1.99 to $4.99, depending on distance, and you see the exact fee at checkout. Plus members get free delivery on orders over $15.";
- var FEE_SIM = 0.92;
+ /* Without the FAQ, the model answers the fee question on its own, in the same call that writes the reply. It doesn't
+ know the app's fees, so its answer is vague. */
+ var VAGUE = "Delivery fees depend on the restaurant and the distance, and you see the exact fee at checkout.";
+ var FEE_SIM = 0.92, FEE_VERSION = 3, FEE_KEY = "delivery-fee";
+ var FEE_INTENTS = ["What does delivery cost?", "Is delivery free for Plus members?"];
+ /* The agent's Router matches Maya's message to two of its intents, best first. Each target tells the agent what to
+ do: handle the reorder itself (food-assistant is the agent's ID), and call the support playbook's discovery tool for the fee question. */
+ var INTENTS = [
+ { name: "reorder", description: "The customer wants to order something they had before.", score: 0.86,
+ target: { type: "agent", ref: "food-assistant.reorder" } },
+ { name: "delivery_fee", description: "A general question about what delivery costs.", score: 0.78,
+ target: { type: "tool", ref: "playbooks.discover_entries", args: { playbook: "support" } } }
+ ];
- /* In the order data flows through Iris: in through Data Integration, then the services the agent calls. */
+ /* In the order the agent uses them. */
var SERVICES = [
- { key: "rdi", name: "Data Integration", role: "Brings business data in from PostgreSQL and keeps it in sync" },
+ { key: "router", name: "Intent Router", role: "Decides what each part of a message needs, without a model call" },
{ key: "retriever", name: "Context Gateway", role: "Gives the agent governed tools to read that data" },
{ key: "memory", name: "Agent Memory", role: "Remembers Maya across conversations" },
- { key: "cache", name: "LangCache", role: "Answers questions it has seen before" }
+ { key: "cache", name: "Agent Playbooks", role: "Answers recurring questions with answers your business approved" }
];
- var NAMES = { cache: "LangCache", memory: "Agent Memory", retriever: "Context Gateway", rdi: "Data Integration", model: "Model" };
+ var NAMES = { router: "Intent Router", cache: "Agent Playbooks", memory: "Agent Memory", retriever: "Context Gateway", model: "Model" };
/* Modelled timings (ms) and token counts. */
- var MS = { cache: 41, memory: 190, tool: [74, 88, 81], answer: 1620, reply: 1480 };
- var TOKENS = { answer: 61 };
+ var MS = { route: 12, plan: 1100, cache: 41, memory: 190, tool: [74, 88, 81], assemble: 1, reply: 1480, record: 25 };
+ var TOKENS = { plan: 38 };
/* --------------------------------------------------------------- helpers */
@@ -99,6 +120,7 @@
function code(s) { return "" + esc(s) + ""; }
function hhmm(iso) { return iso.slice(11, 16); }
function seconds(ms) { return (ms / 1000).toFixed(2) + " s"; }
+ function took(ms) { return ms >= 1000 ? seconds(ms) : ms + " ms"; }
function shellQuote(s) { return "'" + s.replace(/'/g, "'\\''") + "'"; }
function clone(o) { return JSON.parse(JSON.stringify(o)); }
@@ -119,16 +141,23 @@
return ["curl -s -X POST \"" + url + "\" \\", " -H \"Authorization: Bearer " + key + "\" \\", " -H \"Content-Type: application/json\" \\",
" -d " + shellQuote(JSON.stringify(body))].join("\n");
}
- var MCP = "https://.context-surfaces.redis.io/mcp";
+ /* The Gateway serves each Upstream's tools under its namespace: fooddelivery.filter_order. */
+ var GATEWAY_NS = "fooddelivery";
function curlMcp(body) {
- return ["curl -X POST " + MCP + " \\", " -H \"X-API-Key: \" \\", " -H \"Content-Type: application/json\" \\",
+ return ["curl -X POST \"$IRIS_CONTEXT_GATEWAY_URL\" \\", " -H \"Authorization: Bearer $IRIS_CONTEXT_GATEWAY_API_KEY\" \\", " -H \"Content-Type: application/json\" \\",
" -H \"Accept: application/json, text/event-stream\" \\", " -d " + shellQuote(JSON.stringify(body))].join("\n");
}
- function cacheSearch() {
- var request = { prompt: QUESTION };
- return { method: "POST", path: "/entries/search", status: "200", request: request,
- response: { data: [{ id: id32(FEE_PROMPT + "|{}"), prompt: FEE_PROMPT, response: FEE, attributes: {}, similarity: FEE_SIM, searchStrategy: "semantic" }] },
- curl: curlRest("https://$LANGCACHE_HOST/v1/caches/$CACHE_ID/entries/search", "$LANGCACHE_API_KEY", request) };
+ function playbookDiscover() {
+ var request = { query: QUESTION, limit: 3 };
+ return { method: "POST", path: "/discover", status: "200", request: request,
+ response: { pinnedEntries: [], discoveredEntries: [{ entryId: "ent_" + id32("faq:" + FEE_KEY), entryKey: FEE_KEY, entryType: "faq", version: FEE_VERSION,
+ intents: FEE_INTENTS, faq: { question: FEE_PROMPT, answer: FEE }, score: FEE_SIM }], truncated: false },
+ curl: curlRest("$PLAYBOOK_URL/v1/playbooks/$PLAYBOOK_ID/discover", "$PLAYBOOK_API_KEY", request) };
+ }
+ function routerMatch() {
+ var request = { query: MESSAGE, limit: 3 };
+ return { method: "POST", path: "/match", status: "200", request: request, response: { items: INTENTS },
+ curl: curlRest("$IRIS_INTENT_ROUTER_URL/iris/v2/intent/routers/$IRIS_INTENT_ROUTER_ID/match", "$IRIS_INTENT_ROUTER_API_KEY", request) };
}
function memoryRecord(m) {
return { createdAt: EXTRACTION_AT, id: id32("memory:" + m.key), memoryType: m.type, ownerId: OWNER, sessionId: PAST_SESSION,
@@ -146,13 +175,14 @@
return { method: "POST", path: "/session-memory/events", status: "201", request: request, response: { event: ev },
curl: curlRest("$AGENT_MEMORY_URL/v1/stores/$STORE_ID/session-memory/events", "$AGENT_MEMORY_API_KEY", request) };
}
- /* Context Gateway results carry a _retrieval stamp, with fields sorted (as the Context Gateway demo returns them). */
+ /* Context Surface results carry a _retrieval stamp, with fields sorted (as the Context Gateway demo returns them). */
function stamp(rec) {
var out = { _retrieval: { source: SURFACE, retrieved_at: ASK_AT.replace("Z", ".318Z"), source_version: 1 } };
Object.keys(rec).sort().forEach(function (k) { out[k] = rec[k]; });
return out;
}
function toolCall(n, name, args, result) {
+ name = GATEWAY_NS + "." + name;
var request = { jsonrpc: "2.0", id: n, method: "tools/call", params: { name: name, arguments: args } };
return { method: "tools/call", path: name, status: "ok", args: args, request: request,
response: { jsonrpc: "2.0", id: n, result: { content: [{ type: "text", text: JSON.stringify(result) }] } }, result: result, curl: curlMcp(request) };
@@ -161,36 +191,41 @@
/* ------------------------------------------------------------ the model */
- /* Everything that happens for one combination of services: the change
- Data Integration syncs beforehand, the steps of the request, the reply
- (as segments, each credited to the service it came from), and the
- scorecard. */
- function trace(on) {
- var steps = [], ms = 0, tokens = 0, n = 0;
- var lotusMin = on.rdi ? BUSY : LOTUS.avg_delivery_min;
+ /* Everything that happens for one combination of services: the steps of the request, each with its start
+ (at) in ms from the message, the reply (as segments, each credited to the service it came from), and the
+ scorecard. opts.drop names a memory that the app's code leaves out of the prompt, the bug the Lens step shows. */
+ function trace(on, opts) {
+ opts = opts || {};
+ var steps = [], ms = 0, end = 0, tokens = 0, n = 0;
var lotusRow = clone(LOTUS);
- lotusRow.avg_delivery_min = lotusMin;
- function add(s) { steps.push(s); if (s.ms && s.phase !== "record") ms += s.ms; if (s.tokens) tokens += s.tokens; return s; }
+ lotusRow.avg_delivery_min = BUSY;
+ var used = {}; // the memories that enter the prompt
+ if (on.memory) MEMORIES.forEach(function (m) { if (m.key !== opts.drop) used[m.key] = true; });
+ function add(s) {
+ steps.push(s);
+ if (s.ms) { s.at = end; end += s.ms; }
+ if (s.ms && s.phase !== "record") ms += s.ms;
+ if (s.tokens) tokens += s.tokens;
+ return s;
+ }
- var background = on.rdi ? {
- on: true, at: SYNC_AT,
- sql: "UPDATE restaurants SET avg_delivery_min = 45 WHERE id = 'r209';",
- text: "Lotus Thai's kitchen gets busy, and the app updates its delivery time in PostgreSQL. A second later, Data Integration has written the change to " +
- code("restaurant:r209") + ", through a job file that uses the key template Context Gateway reads.",
- record: { opcode: "u", db: "fooddelivery", schema: "public", table: "restaurants", key: { id: "r209" }, after: lotusRow }
- } : {
- on: false, at: SYNC_AT,
- sql: "UPDATE restaurants SET avg_delivery_min = 45 WHERE id = 'r209';",
- text: "Lotus Thai's kitchen gets busy, and the app updates its delivery time in PostgreSQL. Without Data Integration, Redis gets restaurant data from a nightly export at 02:00, so " +
- code("restaurant:r209") + " still says 25 minutes."
- };
+ /* Route */
+ if (on.router) add({ phase: "route", service: "router", ms: MS.route, title: "Matches the message to intents",
+ text: "Intent Router compares Maya's message with the example phrasings of the agent's intents, and finds two matches. " + code("reorder") +
+ " points back to the agent, and " + code("delivery_fee") + " points at the support playbook. No model call decides this.",
+ calls: [routerMatch()] });
+ else {
+ add({ phase: "route", service: "router", off: true, title: "No intents to match", text: "Intent Router is off, so nothing has sorted out what Maya's message needs." });
+ add({ phase: "model", service: "model", ms: MS.plan, tokens: TOKENS.plan, title: "Plans the request",
+ text: "A model call reads Maya's message first and works out what each part needs, which Intent Router would have matched without one." });
+ }
/* Recall */
- if (on.cache) add({ phase: "recall", service: "cache", ms: MS.cache, title: "Checks the cache for the general question",
- text: "The agent sends the general part of Maya's message to LangCache, " + code(QUESTION) + ". A similar question, " + code(FEE_PROMPT) +
- ", scores " + FEE_SIM.toFixed(2) + ", so the cached answer comes back without a model call. The personal part isn't cached, because its answer is Maya's alone.",
- calls: [cacheSearch()] });
- else add({ phase: "recall", service: "cache", off: true, title: "No cache to check", text: "LangCache is off, so the model has to answer the general question too." });
+ if (on.cache) add({ phase: "recall", service: "cache", ms: MS.cache, title: "Checks the support playbook for the general question",
+ text: "The agent sends the general part of Maya's message to Agent Playbooks, " + code(QUESTION) + ". The published FAQ " + code(FEE_PROMPT) +
+ " scores " + FEE_SIM.toFixed(2) + ", above the playbook's threshold, so its approved answer comes back without a model call. The personal part isn't sent, because no FAQ can answer it.",
+ calls: [playbookDiscover()] });
+ else add({ phase: "recall", service: "cache", off: true, title: "No playbook to check", text: "Agent Playbooks is off, so the model answers the fee question on its own, without the app's fees." });
if (on.memory) add({ phase: "recall", service: "memory", ms: MS.memory, title: "Recalls last week's order and Maya's preferences",
text: "A long-term memory search, scoped to Maya, returns what the assistant learned in last week's conversation: the order she means, her peanut allergy, and how she likes her food.",
calls: [memorySearch()], memories: MEMORIES });
@@ -202,11 +237,10 @@
if (!on.memory) add({ phase: "retrieve", service: "retriever", ms: MS.tool[0], title: "Looks for a curry in Maya's orders",
text: "Without memory, the agent searches her order history instead and finds order " + code("o3003") + " from Lotus Thai. The order record doesn't say how spicy she likes it, or that she's allergic to peanuts.",
calls: [toolCall(++n, "filter_order", { tag_conditions: [{ field: "customer_id", value: OWNER }], text_query: "curry" }, filterResult([CURRY]))] });
- add({ phase: "retrieve", service: "retriever", ms: MS.tool[1], fresh: on.rdi, title: "Checks Lotus Thai's delivery time",
- text: on.rdi ? "The restaurant record says " + BUSY + " minutes: Data Integration synced tonight's change four minutes ago. That's too slow for Maya." :
- "The restaurant record says 25 minutes, from last night's export. The agent takes it at face value.",
+ add({ phase: "retrieve", service: "retriever", ms: MS.tool[1], title: "Checks Lotus Thai's delivery time",
+ text: "The restaurant record says " + BUSY + " minutes tonight. That's too slow for Maya.",
calls: [toolCall(++n, "get_restaurant_by_id", { id: LOTUS.id }, stamp(lotusRow))] });
- if (lotusMin > WITHIN) {
+ if (BUSY > WITHIN) {
alt = true;
add({ phase: "retrieve", service: "retriever", ms: MS.tool[2], title: "Finds a Thai restaurant that can make it",
text: "Thai restaurants in Austin that deliver within " + WITHIN + " minutes: Bangkok Street Kitchen.",
@@ -217,63 +251,70 @@
calls: [toolCall(++n, "filter_order", { tag_conditions: [{ field: "customer_id", value: OWNER }, { field: "restaurant_id", value: BSK.id }] }, filterResult(PAD_THAI))] });
}
} else add({ phase: "retrieve", service: "retriever", off: true, title: "No tools to call",
- text: "Context Gateway is off, so the agent has no way to read restaurants or orders." + (on.rdi ? " Data Integration still keeps Redis up to date, but nothing reads it." : "") });
+ text: "Context Gateway is off, so the agent has no way to read restaurants or orders." });
/* The reply, as segments credited to their source. */
var R = [];
function say(t, src) { R.push({ t: t, src: src || null }); }
+ /* How Maya likes it, from the memories that reached the prompt. */
+ function prefs() {
+ var no = ["peanut", "fish"].filter(function (k) { return used[k]; }).map(function (k) { return k === "peanut" ? "peanuts" : "fish sauce"; });
+ return [used.spicy ? "extra spicy" : "", no.length ? "with no " + no.join(" or ") : ""].filter(Boolean).join(", ");
+ }
var memLine = "Last week you had the extra spicy green curry from Lotus Thai, with no peanuts or fish sauce. ";
if (on.memory) say(memLine, "memory");
else if (on.retriever) say("Your last curry order was the green curry without fish sauce from Lotus Thai. ", "retriever");
if (on.retriever && alt) {
- say("Lotus Thai is taking about " + BUSY + " minutes tonight", "rdi");
+ say("Lotus Thai is taking about " + BUSY + " minutes tonight", "retriever");
say(", so it wouldn't arrive in time. ");
say("Bangkok Street Kitchen can deliver in about 30 minutes, and you've ordered their pad thai with tofu twice. ", "retriever");
say("Should I order that");
- if (on.memory) { say(", "); say("extra spicy, with no peanuts or fish sauce", "memory"); }
+ if (on.memory) { say(", "); say(prefs(), "memory"); }
say("? ");
- } else if (on.retriever) {
- say("Lotus Thai can deliver in about 25 minutes. ", "stale");
- say("Should I order it again? ");
} else if (on.memory) {
say("I can't check delivery times right now, so I can't promise it'll arrive within 30 minutes. Should I order it anyway? ");
} else {
say("I don't have a record of what you had last week, and I can't look up your orders right now. What would you like to order? ");
}
- say(FEE, on.cache ? "cache" : null);
+ if (on.cache) say(FEE, "cache");
+ else say(VAGUE);
var reply = R.map(function (x) { return x.t; }).join("").trim();
/* Model call */
- if (!on.cache) add({ phase: "model", service: "model", ms: MS.answer, tokens: TOKENS.answer, title: "Answers the general question",
- text: "A separate model call writes the delivery fee answer that LangCache would have returned." });
+ add({ phase: "model", service: "app", ms: MS.assemble, title: "Assembles the prompt",
+ text: "The app puts Maya's message and everything the agent gathered into the prompt.",
+ used: Object.keys(used).length, of: on.memory ? MEMORIES.length : 0, dropped: MEMORIES.filter(function (m) { return on.memory && !used[m.key]; }) });
var replyTokens = Math.round(reply.split(/\s+/).length * 1.3); // about 1.3 tokens a word
add({ phase: "model", service: "model", ms: MS.reply, tokens: replyTokens, title: "Writes the reply",
text: "The model gets Maya's message plus the context gathered so far" + (on.memory || on.retriever ? "" : ", which is nothing") + ", and writes the reply." });
/* Record */
- if (on.memory) add({ phase: "record", service: "memory", ms: 25, title: "Stores the message and the reply",
+ if (on.memory) add({ phase: "record", service: "memory", ms: MS.record, title: "Stores the message and the reply",
text: "Both go into session memory as events. Background extraction later decides what's worth keeping long term, such as the restaurant Maya picks tonight.",
calls: [sessionEvent(0, "USER", MESSAGE, ASK_AT), sessionEvent(1, "ASSISTANT", reply, REPLY_AT)] });
else add({ phase: "record", service: "memory", off: true, title: "Nothing is remembered", text: "Agent Memory is off, so next time the assistant starts from nothing again." });
- if (on.cache) add({ phase: "record", service: "cache", note: true, title: "Nothing new to cache",
- text: "The general question was a cache hit, and the reply is personal, so the app doesn't store it." });
+ if (on.cache) add({ phase: "record", service: "cache", note: true, title: "Nothing to publish",
+ text: "The fee answer came from a published FAQ, and the rest of the reply is personal. Agents don't publish to a playbook: new FAQs arrive as proposals, and a reviewer publishes them." });
/* The scorecard. */
var checks = [
+ { key: "route", label: "Decides what to do without a model call",
+ state: on.router ? "ok" : "slow",
+ text: on.router ? "Intent Router matched 2 intents in " + MS.route + " ms" : "A planning model call: " + seconds(MS.plan) + " and " + TOKENS.plan + " output tokens" },
{ key: "time", label: "Arrives within 30 minutes",
- state: on.retriever && on.rdi ? "ok" : on.retriever ? "bad" : "warn",
- text: on.retriever && on.rdi ? "Lotus Thai is too slow tonight, so it offers Bangkok Street Kitchen" : on.retriever ? "Promises 25 minutes, but Lotus Thai is taking 45" : "Can't check delivery times" },
+ state: on.retriever ? "ok" : "warn",
+ text: on.retriever ? "Lotus Thai is too slow tonight, so it offers Bangkok Street Kitchen" : "Can't check delivery times" },
{ key: "order", label: "Knows what Maya had last week",
state: on.memory ? "ok" : on.retriever ? "warn" : "bad",
text: on.memory ? "Recalled from Agent Memory, with how she likes it" : on.retriever ? "Found in her order history, without how spicy she likes it" : "Doesn't know" },
{ key: "peanut", label: "Keeps her away from peanuts",
- state: on.memory ? "ok" : alt ? "bad" : "warn",
- text: on.memory ? "Knows about her allergy" : alt ? "Suggests pad thai without knowing about her allergy" : "Doesn't know about her allergy" },
+ state: used.peanut ? "ok" : alt ? "bad" : "warn",
+ text: used.peanut ? "Knows about her allergy" : alt ? "Suggests pad thai without knowing about her allergy" : "Doesn't know about her allergy" },
{ key: "fee", label: "Answers the delivery fee question",
- state: on.cache ? "ok" : "slow",
- text: on.cache ? "From LangCache in " + MS.cache + " ms" : "From the model: " + seconds(MS.answer) + " and " + TOKENS.answer + " output tokens" }
+ state: on.cache ? "ok" : "warn",
+ text: on.cache ? "With the answer the support team approved" : "Gives a vague answer, because the model doesn't know the app's fees" }
];
- return { on: on, background: background, steps: steps, reply: R, replyText: reply, checks: checks, ms: ms, tokens: tokens, alt: alt };
+ return { on: on, steps: steps, reply: R, replyText: reply, checks: checks, ms: ms, end: end, tokens: tokens, alt: alt, used: used };
}
/* ------------------------------------------------------------- the tour */
@@ -282,51 +323,55 @@
supplied, with and without it (both are excerpts of trace() replies,
which the tests check), and the scorecard check it moves. */
var WHAT_IF = {
- rdi: { check: "time",
- with: "Lotus Thai is taking about 45 minutes tonight, so it wouldn't arrive in time. Bangkok Street Kitchen can deliver in about 30 minutes",
- without: "Lotus Thai can deliver in about 25 minutes. Should I order it again?",
- why: "Without Data Integration, Redis only has last night's export, where Lotus Thai still says 25 minutes. The agent promises a delivery that will take 45." },
+ router: { check: "route",
+ with: "Last week you had the extra spicy green curry from Lotus Thai, with no peanuts or fish sauce.",
+ without: "Last week you had the extra spicy green curry from Lotus Thai, with no peanuts or fish sauce.",
+ why: "Without Intent Router, the reply says the same thing, but a model call has to work out what Maya wants first: " + seconds(MS.plan) + " and " + TOKENS.plan +
+ " output tokens. Intent Router matched the same two intents in " + MS.route + " ms." },
retriever: { check: "time",
with: "Lotus Thai is taking about 45 minutes tonight, so it wouldn't arrive in time. Bangkok Street Kitchen can deliver in about 30 minutes",
without: "I can't check delivery times right now, so I can't promise it'll arrive within 30 minutes. Should I order it anyway?",
- why: "Without Context Gateway, the agent has no tools to read restaurants or orders. The data in Redis is fresh, but nothing reads it." },
+ why: "Without Context Gateway, the agent has no tools to read restaurants or orders. The data is in Redis, but nothing reads it." },
memory: { check: "peanut",
with: "you've ordered their pad thai with tofu twice. Should I order that, extra spicy, with no peanuts or fish sauce?",
without: "you've ordered their pad thai with tofu twice. Should I order that?",
why: "Without Agent Memory, the agent finds the curry in Maya's order history, but nothing there says she's allergic to peanuts, so it suggests pad thai without a warning." },
cache: { check: "fee",
- with: "Delivery costs $1.99 to $4.99, depending on distance",
- without: "Delivery costs $1.99 to $4.99, depending on distance",
- why: "Without LangCache, the reply says the same thing, but a second model call writes the fee answer: " + seconds(MS.answer) + " and " + TOKENS.answer + " output tokens that the cache would have saved." }
+ with: "Delivery costs $1.99 to $4.99, depending on distance, and you see the exact fee at checkout.",
+ without: "Delivery fees depend on the restaurant and the distance, and you see the exact fee at checkout.",
+ why: "Without Agent Playbooks, the model answers the fee question on its own. It doesn't know the app's fees, so it gives a vague answer, or it could make one up. Agent Playbooks gives it the answer the support team approved." }
};
/* The tour, one idea per step. `lit` lists the map nodes the step is about;
`adds` is what the agent knows after it. */
var STEPS = [
- { key: "rdi", at: "18:20", title: "Data enters through Data Integration", lit: ["pg", "rdi", "redis"],
- text: "Lotus Thai's kitchen gets busy, so the app updates its delivery time in PostgreSQL. Data Integration captures the change and writes it to Redis about a second later. The services the agent calls read from that Redis database. Select POSTGRES or the Redis logo in the diagram to see the data in each.",
- adds: [{ src: "rdi", t: "Lotus Thai: 45 minutes tonight" }] },
{ key: "ask", at: "18:24", title: "Maya asks the assistant", lit: ["agent"],
- text: "Four minutes later, Maya asks for last week's curry. To answer, the agent needs what she had and what she can't eat, a restaurant that can deliver in time, and the delivery fee, which is the same for everyone." },
- { key: "cache", at: "18:24", title: "LangCache answers the general question", lit: ["cache", "redis", "agent"],
- text: "The delivery fee doesn't depend on who's asking, so the agent checks LangCache first. Another customer asked something similar earlier, and the cached answer comes back in " + MS.cache + " ms, without a model call. The rest of Maya's message is personal, so it isn't cached.",
+ text: "Maya asks for last week's curry. To answer, the agent needs what she had and what she can't eat, a restaurant that can deliver in time, and the delivery fee, which is the same for everyone." },
+ { key: "router", at: "18:24", title: "Intent Router matches the message", lit: ["router", "agent"],
+ text: "First, the agent sends Maya's message to Intent Router. Intent Router compares it with the app's intents, which are the kinds of requests the app expects. It finds two matches. The best match is reorder, which tells the agent to handle the reorder itself. The other is delivery_fee, which tells the agent to look up the delivery fee in the support playbook. Matching takes " + MS.route + " ms and doesn't need a model call. Intent Router only returns the matches. The agent decides what to do with them.",
+ adds: [{ src: "router", t: "reorder → agent" }, { src: "router", t: "delivery_fee → support playbook" }] },
+ { key: "cache", at: "18:24", title: "Agent Playbooks answers the general question", lit: ["cache", "redis", "agent"],
+ text: "Intent Router pointed the fee question at the support playbook, so the agent checks it first. The support team published an approved answer to this question, and discovery returns it in " + MS.cache + " ms, without a model call. The rest of Maya's message is personal, so no FAQ covers it.",
adds: [{ src: "cache", t: "Delivery fee: $1.99 to $4.99" }] },
{ key: "memory", at: "18:24", title: "Agent Memory recalls last week", lit: ["memory", "redis", "agent"],
text: "This is a new conversation, but Agent Memory kept what the assistant learned from Maya last week. A search scoped to her returns the order she means and the food she can't eat.",
adds: [{ src: "memory", t: "Last week: extra spicy green curry from Lotus Thai" }, { src: "memory", t: "Allergic to peanuts, avoids fish sauce" }] },
{ key: "retriever", at: "18:24", title: "Context Gateway checks live data", lit: ["retriever", "redis", "agent"],
- text: "Context Gateway gives the agent governed tools for the restaurant and order data that Data Integration keeps in sync. Lotus Thai is too slow tonight, so the agent finds a Thai restaurant that can make it, and checks what Maya has ordered there.",
+ text: "Context Gateway gives the agent governed tools, through one Model Context Protocol (MCP) endpoint, for the restaurant and order data in Redis. Lotus Thai is too slow tonight, so the agent finds a Thai restaurant that can make it, and checks what Maya has ordered there.",
adds: [{ src: "retriever", t: "Bangkok Street Kitchen: 30 minutes" }, { src: "retriever", t: "She's had their pad thai with tofu twice" }] },
{ key: "reply", at: "18:24", title: "The model writes one reply", lit: ["agent"],
text: "The model gets Maya's message and everything the agent gathered, and writes the reply. Each highlighted part came from a different service. Afterward, Agent Memory stores the conversation, so next time the assistant remembers tonight too." },
+ { key: "lens", title: "Lens shows how the request ran", lit: ["lens", "agent"], preview: true,
+ text: "Lens records how the agent handled Maya's message. For each step, it records what the step found and how long it took. For example, it shows which memories Agent Memory returned, and which of them made it into the prompt. If the agent gives a wrong answer, you can see which step caused it. Select a step to see what Lens recorded about it. To see Lens find a bug, select “A request with a bug”." },
{ key: "whatif", title: "What if a service were missing?", lit: [],
text: "Pick a service to take away. Only the part of the reply it supplied changes." }
];
- var DEPTH = { rdi: "Data Integration", retriever: "Context Gateway", memory: "Agent Memory", cache: "LangCache" };
+ var DEPTH = { router: "Intent Router", retriever: "Context Gateway", memory: "Agent Memory", cache: "Agent Playbooks", lens: "Lens" };
- /* ---- the PostgreSQL sample data, shown when the reader selects POSTGRES ----
+ /* ---- the sample data, as table rows ----
The same rows as the RDI demo (rdi-demo.js), on the evening of this tour:
- Maya's curry order has been delivered, and Lotus Thai's delivery time
- changes from 25 to 45 minutes at 18:20. Keep the rows in sync. */
+ Maya's curry order has been delivered, and Lotus Thai is taking 45
+ minutes tonight. Keep the rows in sync. The tour shows them as they
+ sit in Redis (RD). */
function pgEmail(name) { return name.toLowerCase().replace(/[^a-z]+/g, ".") + "@example.com"; }
var PG = [
{ name: "restaurants", columns: ["id", "name", "avg_delivery_min", "cuisine", "city", "rating", "price_level"], rows: [
@@ -365,22 +410,9 @@
["o3020", "u102", "r209", "delivered", 25.3, ts(2026, 9, 16, 13), "Drunken noodles, Thai iced tea"]
] }
];
- /* The rows this tour uses, and what each table's note says about them. */
- var PG_FOCUS = { restaurants: ["r209", "r201"], customers: ["u101"], orders: ["o3003", "o3001", "o3018"] };
- var PG_NOTES = {
- restaurants: "At 18:20, Lotus Thai's avg_delivery_min goes from 25 to 45. The agent offers Bangkok Street Kitchen instead.",
- customers: "Maya Chen is asking tonight. Her peanut allergy isn't in this table: Agent Memory has it from last week.",
- orders: "Maya's past orders: last week's green curry (o3003), and pad thai with tofu twice (o3001, o3018)."
- };
- /* The POSTGRES pill, copied from the published drawing: its outline, its bounding box, and that outline as a
- clip path for the button, in percent of the box. */
- var PG_PILL = "M132.663 127.145C137.158 127.145 141.635 128.149 145.084 130.141C146.062 130.705 147.288 131.625 148.282 132.806C149.215 133.914 149.927 135.23 150.068 136.685L150.089 136.978C150.089 137.819 150.139 138.739 150.163 139.593C150.188 140.465 150.187 141.278 150.093 141.952C149.783 144.171 148.18 146.209 145.084 147.996L96.3457 176.133C92.8977 178.123 88.3554 179.129 83.7939 179.129C79.2326 179.129 74.6911 178.123 71.2432 176.133C67.9454 174.229 65.9868 172.028 65.8174 169.705C65.7931 169.372 65.747 168.393 65.7305 167.413C65.7222 166.923 65.7212 166.438 65.7334 166.034C65.7461 165.615 65.7729 165.327 65.8057 165.205C66.3598 163.15 67.433 160.319 71.0928 158.286L71.0967 158.284L120.241 130.142L120.243 130.141C123.692 128.15 128.169 127.145 132.663 127.145Z";
- var PG_BOX = [65.7, 127.1, 84.5, 52];
- var PG_CLIP = "polygon(79.3% 0, 94% 5.8%, 100% 18.4%, 100% 28.5%, 94% 40.1%, 36.3% 94.2%, 21.4% 100%, 6.6% 94.2%, 0 81.8%, 0 74.6%, 6.3% 59.7%, 64.6% 5.8%)";
-
- /* ---- the same rows in Redis, shown when the reader selects the Redis logo ----
- Data Integration writes each row as a JSON document. Its job files give each table the key template
- Context Gateway reads ({table}:{id}), and the customers job leaves out email, as in the RDI demo. */
+ /* ---- the rows in Redis, shown when the reader selects the Redis logo ----
+ Each row is a JSON document under the key template the Context Surface reads ({table}:{id}). Customer
+ records leave out email, as in the RDI demo. */
function rdTable(t, prefix, drop) {
var keep = t.columns.map(function (c, i) { return i > 0 && drop.indexOf(c) < 0 ? i : -1; }).filter(function (i) { return i > 0; });
return { name: t.name, columns: ["key"].concat(keep.map(function (i) { return t.columns[i]; })),
@@ -389,23 +421,20 @@
var RD = [rdTable(PG[0], "restaurant:", []), rdTable(PG[1], "customer:", ["email"]), rdTable(PG[2], "order:", [])];
var RD_FOCUS = { restaurants: ["restaurant:r209", "restaurant:r201"], customers: ["customer:u101"], orders: ["order:o3003", "order:o3001", "order:o3018"] };
var RD_NOTES = {
- restaurants: "About a second after PostgreSQL changes, Data Integration writes Lotus Thai's new time to restaurant:r209.",
- customers: "The Data Integration job leaves out email, so addresses never reach Redis.",
- orders: "Maya's past orders. Context Gateway's filter_order tool finds them by customer_id."
+ restaurants: "Lotus Thai's avg_delivery_min is 45 tonight, so the agent offers Bangkok Street Kitchen instead.",
+ customers: "Maya Chen is asking tonight. Her peanut allergy isn't in this record: Agent Memory has it from last week.",
+ orders: "Maya's past orders. Context Gateway's " + GATEWAY_NS + ".filter_order tool finds them by customer_id."
};
- /* The two data windows, and where to select each in the drawing: a button over the shape, and a ring on
- the shape's own outline that pulses on the step where the data changes. `changed` is Lotus Thai's row. */
+ /* The data window, and where to select it in the drawing: a button over the Redis logo, and a ring on its outline. */
var DATA = {
- pg: { name: "Postgres", sub: "fooddelivery · public schema", label: "PostgreSQL", tables: PG, focus: PG_FOCUS, notes: PG_NOTES, changed: "r209", flipMs: 900,
- box: PG_BOX, clip: PG_CLIP, ring: '' },
- redis: { name: "Redis", sub: "one JSON document per row", label: "Redis", tables: RD, focus: RD_FOCUS, notes: RD_NOTES, changed: "restaurant:r209", flipMs: 1400,
+ redis: { name: "Redis", sub: "one JSON document per row", label: "Redis", tables: RD, focus: RD_FOCUS, notes: RD_NOTES,
box: [391.062, 172.175, 44.697, 45.2752], round: true, // the Redis logo
ring: '' } // just outside the logo, which is the ring's red
};
/* --------------------------------------------------------------- the stage */
- /* The Iris architecture diagram from redis.io/iris, as published: four
+ /* The Iris architecture diagram from redis.io/iris, as published:
versions of the same 893 x 570 drawing, each with one service
highlighted (static/images/ai/context-engine/iris-diagram/). A step
shows one version, or blends several: "lighten" keeps whatever is lit
@@ -414,10 +443,15 @@
database. Packets ride an overlay in the same coordinates, along
routes traced from the centres of the drawing's dashes. */
var ART_W = 893, ART_H = 570;
- var ART = { rdi: "data-integration.svg", retriever: "context-retriever.svg", cache: "langcache.svg", memory: "agent-memory.svg" };
+ /* The part of the drawing the stage shows: x, y, width, height. The tour leaves Data Integration out, so the
+ stage crops the drawing's empty left side and its frame, and positions everything in these units. */
+ var VIEW = [278, 145, 613.5, 423.5];
+ function vx(x) { return ((x - VIEW[0]) / VIEW[2] * 100) + "%"; }
+ function vy(y) { return ((y - VIEW[1]) / VIEW[3] * 100) + "%"; }
+ function vw(w) { return (w / VIEW[2] * 100) + "%"; }
+ function vh(h) { return (h / VIEW[3] * 100) + "%"; }
+ var ART = { retriever: "context-retriever.svg", cache: "langcache.svg", memory: "agent-memory.svg" };
var ROUTES = {
- pg: "M 126.9 162.7 L 127.9 163.4 L 128.7 163.9 L 131.6 165.4 L 133.5 166.6 L 134.9 167.5 L 137.8 169.0 L 139.7 170.2 L 141.1 171.0 L 144.0 172.6 L 145.9 173.7 L 147.3 174.6 L 150.2 176.2 L 151.2 176.8 L 152.9 177.6 L 154.1 178.0 L 158.3 178.0 L 159.7 177.6 L 161.4 176.7 L 162.2 176.3 L 164.7 174.8 L 166.6 173.7 L 168.0 173.0 L 170.5 171.4 L 172.3 170.4 L 173.7 169.6 L 176.2 168.1 L 178.1 167.1 L 179.5 166.3 L 182.0 164.8 L 183.8 163.8 L 185.3 163.0 L 187.7 161.5 L 189.6 160.4 L 191.0 159.7 L 193.5 158.1 L 195.3 157.1 L 196.8 156.3 L 199.2 154.8 L 201.1 153.8 L 202.5 153.0 L 205.0 151.5 L 206.9 150.5 L 208.3 149.7 L 210.8 148.2 L 212.6 147.1 L 214.1 146.4 L 216.5 144.8 L 217.6 144.3 L 219.2 143.5 L 220.4 143.2 L 224.7 143.1 L 226.0 143.5 L 227.0 143.9 L 227.6 144.3 L 228.4 144.8 L 230.9 146.1 L 232.5 147.1 L 233.9 148.0 L 236.3 149.3 L 238.0 150.3 L 239.4 151.1 L 241.8 152.4 L 243.5 153.4 L 244.8 154.2 L 247.1 155.5 L 247.3 155.5 L 248.3 156.2",
- rdi: "M 296.6 177.5 L 297.4 177.9 L 298.4 178.5 L 301.0 180.1 L 302.4 180.9 L 304.3 181.9 L 306.8 183.5 L 308.2 184.3 L 310.1 185.4 L 312.6 186.9 L 314.1 187.7 L 316.0 188.8 L 318.5 190.3 L 319.9 191.1 L 321.8 192.2 L 324.3 193.8 L 325.8 194.6 L 327.6 195.6 L 330.1 197.2 L 331.6 198.0 L 333.5 199.0 L 336.0 200.6 L 337.4 201.4 L 339.3 202.5 L 341.8 204.0 L 343.3 204.8 L 345.1 205.9 L 347.6 207.5 L 349.1 208.3 L 351.0 209.3 L 353.5 210.9 L 354.9 211.7 L 356.8 212.7 L 359.3 214.3 L 359.3 214.3 L 360.1 214.7",
dbcr: "M 461.0 271.6 L 461.7 272.1 L 462.7 272.7 L 465.3 274.1 L 466.6 274.9 L 468.4 275.9 L 470.9 277.3 L 472.3 278.1 L 474.0 279.2 L 476.6 280.6 L 477.9 281.4 L 479.7 282.4 L 482.3 283.8 L 483.6 284.6 L 485.4 285.7 L 487.9 287.1 L 489.3 287.9 L 491.0 288.9 L 493.6 290.3 L 495.0 291.1 L 496.7 292.2 L 499.3 293.6 L 500.6 294.4 L 502.4 295.4 L 504.8 296.8 L 505.0 296.8 L 505.7 297.3",
amdb: "M 748.4 276.7 L 744.0 274.2 L 738.1 270.8 L 732.2 267.4 L 726.3 264.0 L 720.5 260.6 L 714.6 257.2 L 708.7 253.8 L 702.8 250.4 L 696.9 247.0 L 691.1 243.6 L 685.2 240.2 L 679.3 236.9 L 673.4 233.5 L 667.6 230.1 L 661.7 226.7 L 655.8 223.3 L 649.9 219.9 L 644.0 216.5 L 638.2 213.1 L 632.3 209.7 L 626.4 206.3 L 620.5 202.9 L 614.7 199.5 L 608.8 196.1 L 602.9 192.7 L 597.0 189.3 L 591.1 186.0 L 585.3 182.6 L 579.4 179.2 L 573.5 175.8 L 570.4 173.9 L 563.9 171.3 L 556.7 169.6 L 548.9 168.9 L 541.1 169.2 L 533.6 170.4 L 526.7 172.7 L 521.6 175.3 L 516.2 178.4 L 510.2 181.9 L 504.2 185.3 L 498.2 188.8 L 492.2 192.2 L 486.2 195.7 L 480.2 199.2 L 474.2 202.6 L 468.2 206.1 L 464.1 208.5",
dblc: "M 362.6 275.7 L 359.3 277.7 L 353.4 281.1 L 347.5 284.5 L 341.7 287.9 L 335.8 291.2 L 330.0 294.6 L 324.1 298.0 L 318.2 301.4 L 312.4 304.8 L 309.2 306.6 L 304.7 310.3 L 301.7 314.5 L 300.5 319.0 L 300.9 323.5 L 303.2 327.8 L 307.0 331.8 L 311.6 334.7 L 316.8 337.8 L 322.7 341.2 L 328.6 344.6 L 334.5 348.0 L 340.4 351.4 L 346.3 354.8 L 352.2 358.2 L 358.0 361.6 L 363.9 365.0 L 369.8 368.4 L 375.7 371.8 L 381.6 375.2 L 387.5 378.6 L 393.4 382.0 L 399.3 385.4 L 405.2 388.8 L 411.1 392.2 L 416.9 395.6 L 422.8 399.0 L 428.7 402.4 L 434.6 405.8 L 440.5 409.2 L 446.4 412.6 L 452.3 416.0 L 458.2 419.4 L 464.1 422.8 L 470.0 426.2 L 475.8 429.6 L 481.7 433.0 L 486.8 435.9",
@@ -426,243 +460,226 @@
amagent: "M 728.8 334.6 L 711.2 344.7 L 689.7 357.1",
amagent2: "M 697.6 364.2 L 721.0 350.8 L 736.8 341.7",
lcagent: "M 572.3 436.8 L 595.2 423.6 L 610.7 414.7",
- lcagent2: "M 602.9 407.7 L 585.6 417.6 L 564.5 429.8",
- maya: "M 719.7 437.5 L 703.7 428.2 L 684.1 417.0",
- maya2: "M 691.5 410.9 L 712.9 423.2 L 727.1 431.4"
+ lcagent2: "M 602.9 407.7 L 585.6 417.6 L 564.5 429.8"
+ /* maya, maya2, router, router2, and lens are added with the figures (FIG), from their geometry. */
};
/* Where to select each part of the drawing, and where each value tag sits:
x, y (the tag's bottom edge), and which side it hangs from. */
- var HITS = { rdi: [292, 180, 52], redis: [413, 230, 44], retriever: [540, 318, 36],
- memory: [773, 307, 36], agent: [651, 385, 36], cache: [526, 469, 36], maya: [761, 455, 36] };
- var TAG_SPOTS = { cache: [484, 470, "r"], memory: [872, 262, "r"] };
+ var HITS = { redis: [413, 230, 44], retriever: [540, 318, 36],
+ memory: [773, 307, 36], agent: [651, 385, 36], cache: [526, 469, 36] }; // maya, router, and lens: with the figures
+ var TAG_SPOTS = { cache: [484, 470, "r"], memory: [872, 262, "r"] }; // router: with the figures
/* What each step shows: which versions of the drawing, how they blend,
and what moves. Groups run in order; the routes in a group run together.
A tag appears when the group it belongs to arrives. */
var FLOWS = {
- rdi: { layers: ["rdi"], groups: [[["pg"]], [["rdi"]]] }, // no tag: the step card shows the change that lands
- ask: { layers: ["rdi", "retriever", "cache", "memory"], blend: "darken", groups: [[["maya"]]] }, // no tag: the step card shows her message
- cache: { layers: ["cache"], groups: [[["lcagent2"]], [["dblc", 1]], [["dblc"]], [["lcagent"]]], tags: [[2, "cache", "cache hit · 0.92 · 41 ms"]] },
+ ask: { layers: ["retriever", "cache", "memory"], blend: "darken", groups: [[["maya"]]] }, // no tag: the step card shows her message
+ /* The agent sends the message to Intent Router, which sends back the intents that match. */
+ router: { layers: ["retriever", "cache", "memory"], blend: "darken", groups: [[["router2"]], [["router"]]], tags: [[0, "router", "2 intents · 12 ms"]] },
+ cache: { layers: ["cache"], groups: [[["lcagent2"]], [["dblc", 1]], [["dblc"]], [["lcagent"]]], tags: [[2, "cache", "FAQ · 0.92 · 41 ms"]] },
memory: { layers: ["memory"], groups: [[["amagent2"]], [["amdb"]], [["amdb", 1]], [["amagent"]]], tags: [[2, "memory", "5 memories"]] },
retriever: { layers: ["retriever"],
groups: [[["cragent2"]], [["dbcr"]], [["cragent"]], [["cragent2"]], [["dbcr"]], [["cragent"]], [["cragent2"]], [["dbcr"]], [["cragent"]]] }, // no tags: the step card lists the three calls
- reply: { layers: ["retriever", "memory", "cache"], groups: [[["cragent"], ["amagent"], ["lcagent"]], [["maya2"]]] } // no tag: the step card shows the reply
+ reply: { layers: ["retriever", "memory", "cache"], groups: [[["cragent"], ["amagent"], ["lcagent"]], [["maya2"]]] }, // no tag: the step card shows the reply
+ /* The request again, as Lens records it: each step's record goes to Lens as the step ends. The preview plays this;
+ the tour plays the step selected in Lens's timeline instead (LENS_FLOWS). */
+ lens: { layers: ["retriever", "memory", "cache"],
+ groups: [[["router"], ["lens"]], [["lcagent"], ["lens"]], [["amagent"], ["lens"]], [["cragent"], ["lens"]], [["maya2"], ["lens"]]] } // no tag: the step card shows the timeline
};
- var STEP_OF = { rdi: 0, redis: 0, maya: 1, cache: 2, memory: 3, retriever: 4, agent: 5 };
- /* ---- Maya, the user, drawn in the diagram's own style ----
- The drawing has no user, so the stage adds one: the agent's disc and
- label box, moved to the empty lower right, with a person where the
- agent has a robot. Her link to the agent is the Context Gateway's pair
- of arrows turned half a turn about the agent's disc; both discs look the
- same that way round, so the arrows meet them as the drawing's do. The
- link is lit on the steps she's part of. */
- var MAYA = {
- move: [110.06, 69.065], // from the agent's disc to Maya's
+ /* On the Lens step, the drawing plays only the step selected in the timeline: its trip to its service, and then its
+ record going to Lens. Prompt assembly and the model call happen inside the agent, so only their records move. */
+ var DIM = ["retriever", "cache", "memory"]; // blended with "darken", every service is dimmed
+ var LENS_FLOWS = {
+ router: { layers: DIM, blend: "darken", groups: [[["router2"]], [["router"]], [["lens"]]] },
+ cache: { layers: ["cache"], groups: [[["lcagent2"]], [["dblc", 1]], [["dblc"]], [["lcagent"]], [["lens"]]] },
+ memory: { layers: ["memory"], groups: [[["amagent2"]], [["amdb"]], [["amdb", 1]], [["amagent"]], [["lens"]]] },
+ retriever: { layers: ["retriever"], groups: [[["cragent2"]], [["dbcr"]], [["cragent"]], [["lens"]]] },
+ app: { layers: DIM, blend: "darken", groups: [[["lens"]]] },
+ model: { layers: DIM, blend: "darken", groups: [[["lens"]]] },
+ record: { layers: ["memory"], groups: [[["amagent2"]], [["amdb"]], [["lens"]]] } // Agent Memory stores the conversation
+ };
+ var STEP_OF = { maya: 0, router: 1, cache: 2, memory: 3, retriever: 4, redis: 4, agent: 5, lens: 6 };
+ /* ---- Intent Router, Lens, and Maya, the user, drawn in the diagram's own style ----
+ The drawing has none of them, so the stage adds each in an empty place next to the agent, on a line from
+ the agent's face centre (C0):
+ - Intent Router above the agent, between Context Gateway and Agent Memory: the agent calls it, as it
+ calls those services, and gets the matching intents back.
+ - Maya down and to the right, on the diagonal the drawing leaves free, so she talks to the agent directly.
+ - Lens below the agent's label: the agent sends it a record of each step.
+ Each is the agent's own disc, scaled about C0 and moved along its line (dir, by `at` units): Intent Router
+ and Lens in the drawing's service colours with their icons, Maya with a person where the agent has a robot.
+ Each has a label box, as the agent does; Intent Router's and Lens's show only while they're lit, as the
+ drawing shows only a lit service's label. Their links use the drawing's arrow style, one line each way,
+ with dots that show while the link is lit. Lens's link has one line, toward Lens. */
+ var C0 = [655.895, 388.655], STEP = [110.06, 69.065], STEP_LEN = Math.hypot(STEP[0], STEP[1]);
+ var DISC = {
rim: "M626.802 419.618C621.381 416.488 617.95 409.911 617.794 400.63C617.482 382.067 630.387 359.713 646.62 350.702C654.736 346.197 662.147 345.88 667.568 349.009L674.93 352.459C680.351 355.589 683.782 362.166 683.938 371.447C684.25 390.01 671.345 412.364 655.112 421.375C646.996 425.88 639.585 426.197 634.164 423.067L626.802 419.618Z",
face: "M655.897 421.803C671.75 412.649 684.602 390.389 684.602 372.083C684.603 353.777 671.752 346.358 655.899 355.511C640.046 364.664 627.193 386.924 627.193 405.23C627.192 423.536 640.044 430.956 655.897 421.803Z",
- icon: [28.7085, -16.5775, 0, 33.1498, 765.955, 457.72], // the disc face's plane, in units of its radius
- link: [-1, 0, 0, -1, 1301.74, 772.07], // half a turn about the agent's disc
- arrows: ["M619.301 356.381C619.585 356.381 619.815 356.247 619.814 356.083L619.804 353.41C619.804 353.246 619.573 353.114 619.289 353.114C619.005 353.114 618.775 353.248 618.776 353.412L618.784 355.788L614.669 355.793C614.385 355.793 614.155 355.926 614.155 356.09C614.156 356.254 614.387 356.387 614.671 356.387L619.301 356.381ZM582.472 334.495C582.271 334.379 581.945 334.38 581.745 334.496C581.544 334.612 581.545 334.8 581.746 334.916L582.109 334.706L582.472 334.495ZM582.109 334.706L581.746 334.916L618.937 356.294L619.3 356.084L619.663 355.874L582.472 334.495L582.109 334.706Z",
- "M609.695 361.335C609.896 361.45 610.222 361.45 610.422 361.334C610.623 361.217 610.622 361.029 610.421 360.914L610.058 361.124L609.695 361.335ZM572.866 339.449C572.582 339.449 572.352 339.582 572.353 339.746L572.363 342.419C572.363 342.583 572.594 342.716 572.878 342.716C573.162 342.715 573.392 342.582 573.392 342.418L573.383 340.042L577.498 340.037C577.782 340.037 578.012 339.903 578.012 339.739C578.011 339.575 577.78 339.443 577.496 339.443L572.866 339.449ZM572.867 339.746L572.504 339.956L609.695 361.335L610.058 361.124L610.421 360.914L573.23 339.535L572.867 339.746Z"],
- dots: [[589.669, 338.86], [600.591, 355.661]],
- label: [800, 442.7, 38.3, 24.751]
+ icon: [28.7085, -16.5775, 0, 33.1498] // the face's plane, in units of its radius, at full size
};
+ /* For each figure: its line from C0 (dir) and how far along it (at), its scale (s), where its link starts
+ (from C0) and stops (reach, short of the figure's centre, at full size), how far across the line its two
+ link lines sit, and its label box, placed from the figure's centre (dx, dy) and w wide. */
+ var FIG = {
+ router: { dir: [0, -1], at: 120, s: 0.7, from: 47, reach: 41.6, across: [4.5, -4.5],
+ label: { lines: ["INTENT", "ROUTER"], dx: 27.1, dy: -0.7, w: 46 } }, // to the right, under the dashes from Redis to Agent Memory
+ maya: { dir: [STEP[0] / STEP_LEN, STEP[1] / STEP_LEN], at: STEP_LEN, s: 0.85, from: 44, reach: 44, across: [9, 0],
+ label: { lines: ["MAYA"], dx: -19.15, dy: 37, w: 38.3 } },
+ lens: { dir: [0, 1], at: 146, s: 0.65, from: 77, reach: 46.8, across: [0, 0], // its link starts below the agent's label
+ label: { lines: ["LENS"], dx: 25.5, dy: -12.4, w: 43.1543 } }
+ };
+ Object.keys(FIG).forEach(function (k) { var f = FIG[k]; f.c = [C0[0] + f.dir[0] * f.at, C0[1] + f.dir[1] * f.at]; });
+ /* A point `a` along a figure's line from C0, and `o` across it (to the right of the line, looking along it). */
+ function along(f, a, o) {
+ var u = f.dir;
+ return [C0[0] + u[0] * a - u[1] * o, C0[1] + u[1] * a + u[0] * o];
+ }
+ /* A link's two lines: `in` runs toward the agent, `out` away from it. */
+ function link(f) {
+ var a = f.from, b = f.at - f.s * f.reach;
+ return { in: [along(f, b, f.across[0]), along(f, a, f.across[0])], out: [along(f, a, f.across[1]), along(f, b, f.across[1])] };
+ }
+ var LINKS = { router: link(FIG.router), maya: link(FIG.maya), lens: link(FIG.lens) };
+ function routeD(seg) { return "M " + seg[0][0].toFixed(1) + " " + seg[0][1].toFixed(1) + " L " + seg[1][0].toFixed(1) + " " + seg[1][1].toFixed(1); }
+ ROUTES.maya = routeD(LINKS.maya.in); ROUTES.maya2 = routeD(LINKS.maya.out);
+ ROUTES.router = routeD(LINKS.router.in); ROUTES.router2 = routeD(LINKS.router.out);
+ ROUTES.lens = routeD(LINKS.lens.out);
+ HITS.router = [FIG.router.c[0], FIG.router.c[1], 24]; HITS.maya = [FIG.maya.c[0], FIG.maya.c[1], 30]; HITS.lens = [FIG.lens.c[0], FIG.lens.c[1], 24];
+ TAG_SPOTS.router = [FIG.router.c[0], 236, "c"]; // above it, over the dashes, which are dimmed on its step
+ /* The red badge every service disc carries, copied from the Agent Memory disc in agent-memory.svg, with
+ that disc's face centre so it can be placed on another disc. */
+ var BADGE = { at: [778.549, 309.69], red: "M793.415 291.105C793.414 295.046 790.647 299.839 787.234 301.81C783.821 303.781 781.054 302.183 781.054 298.242C781.054 294.301 783.821 289.508 787.234 287.537C790.648 285.566 793.415 287.164 793.415 291.105Z",
+ white: "M789.972 293.216C789.517 294.11 789.027 295.115 788.046 295.682C787.169 296.188 786.843 295.523 786.82 294.843C787.012 295.18 787.387 295.327 787.973 294.972C789.099 294.281 789.872 292.712 789.872 291.689C789.872 290.465 789.045 290.06 787.61 290.888C786.583 291.481 785.311 292.647 784.476 293.811C784.466 294.519 784.821 295.227 784.948 295.053C785.672 294.06 786.247 293.358 786.804 292.85C785.979 294.342 784 297.843 783.758 298.398C783.785 298.764 784.212 299.54 784.421 299.42C784.484 299.383 784.539 299.312 784.603 299.205C785.199 298.12 785.686 297.175 786.118 296.286C786.179 297.188 786.597 298.093 787.764 297.419C788.809 296.816 789.844 295.386 790.317 293.237C790.371 292.975 790.117 292.941 789.972 293.216ZM788.782 292.388C788.782 292.98 788.255 293.575 787.773 293.853C787.516 294.002 787.318 294.041 787.162 294.035C787.449 293.388 787.734 292.73 788.04 292.025C788.58 291.815 788.782 292.029 788.782 292.388Z" };
+ /* Intent Router's icon, in the face's plane: a store with requests passing through it, one routed out. */
+ var ROUTER_ICON = '' +
+ '' +
+ '' +
+ '' +
+ '';
+ /* Lens's icon, in the face's plane: a magnifier over the agent's robot, as on the Lens landing page. */
+ var LENS_ICON = '' + // the lens and its handle
+ '' + // the robot's head and antenna
+ ''; // its eyes
- var MISSING_ROUTES = { rdi: ["pg", "rdi"], retriever: ["dbcr", "cragent", "cragent2"], memory: ["amdb", "amagent", "amagent2"], cache: ["dblc", "lcagent", "lcagent2"] };
-
- /* ---- the Data Integration gear turns, as it does on redis.io/iris ----
- The published drawing is still, so the stage draws its own gear over it:
- the drawing's own outline, turned in the gear's plane and extruded along
- its axis. The numbers are fitted to the drawing: c and A map the gear's
- plane to the page, with the hole as a unit circle; d is the extrusion;
- outer and hole are the outline in the plane, and corner lists the sharp
- corners of outer; front is the front face as path data in the plane. At
- angle 0 the gear matches the drawing. Over the area the gear sweeps
- (clip), the stage repaints the background and then the dashes and dots
- the drawing has in front of the gear, copied from the drawing itself:
- GEAR_AHEAD lists them by position in data-integration.svg, where the
- gear is lit, and in context-retriever.svg, where it's dimmed. */
- var GEAR = {
- c: [295.664, 181.039], A: [[11.1528, 11.3054], [-19.3183, 6.5268]], d: [-6.186000000000007, -3.5720000000000027],
- outer: [
- -0.422, 2.822, -0.361, 2.83, -0.299, 2.837, -0.237, 2.842, -0.175, 2.846, -0.114, 2.848, -0.051, 2.849, 0.011, 2.849, 0.072, 2.848, 0.134, 2.845, 0.196, 2.842, 0.258, 2.837,
- 0.32, 2.83, 0.381, 2.822, 0.611, 1.822, 0.624, 1.802, 0.681, 1.782, 0.736, 1.761, 0.791, 1.738, 0.846, 1.713, 0.857, 1.714, 1.745, 2.267, 1.798, 2.226, 1.85, 2.184,
- 1.901, 2.141, 1.952, 2.097, 2.001, 2.051, 2.05, 2.004, 2.104, 1.949, 2.156, 1.893, 2.207, 1.836, 2.256, 1.778, 2.303, 1.72, 1.755, 0.863, 1.753, 0.819, 1.786, 0.748,
- 1.815, 0.676, 1.842, 0.604, 1.856, 0.588, 2.876, 0.36, 2.888, 0.266, 2.896, 0.171, 2.901, 0.075, 2.902, -0.02, 2.901, -0.115, 2.896, -0.21, 2.888, -0.306, 2.876, -0.4,
- 1.84, -0.631, 1.829, -0.64, 1.827, -0.646, 1.801, -0.712, 1.773, -0.777, 1.742, -0.841, 1.744, -0.886, 2.303, -1.76, 2.256, -1.819, 2.207, -1.877, 2.157, -1.933, 2.104, -1.989,
- 2.05, -2.044, 2.001, -2.091, 1.952, -2.137, 1.901, -2.182, 1.85, -2.224, 1.798, -2.266, 1.745, -2.307, 0.82, -1.73, 0.812, -1.729, 0.809, -1.73, 0.751, -1.754, 0.693, -1.777,
- 0.634, -1.798, 0.621, -1.818, 0.381, -2.863, 0.32, -2.87, 0.258, -2.877, 0.196, -2.882, 0.134, -2.886, 0.072, -2.888, 0.011, -2.89, -0.051, -2.89, -0.113, -2.888, -0.175, -2.886,
- -0.237, -2.882, -0.299, -2.877, -0.361, -2.87, -0.422, -2.863, -0.665, -1.802, -0.679, -1.783, -0.73, -1.763, -0.782, -1.742, -0.832, -1.719, -0.841, -1.718, -0.844, -1.72, -1.786, -2.307,
- -1.839, -2.266, -1.891, -2.225, -1.942, -2.182, -1.992, -2.137, -2.042, -2.091, -2.091, -2.044, -2.145, -1.99, -2.197, -1.934, -2.248, -1.877, -2.297, -1.819, -2.344, -1.76, -1.761, -0.85,
- -1.759, -0.806, -1.793, -0.731, -1.823, -0.656, -1.833, -0.643, -1.836, -0.641, -2.917, -0.401, -2.929, -0.306, -2.937, -0.211, -2.941, -0.116, -2.943, -0.02, -2.941, 0.075, -2.937, 0.17,
- -2.929, 0.265, -2.917, 0.36, -1.852, 0.598, -1.839, 0.613, -1.806, 0.698, -1.77, 0.783, -1.772, 0.826, -2.344, 1.72, -2.297, 1.778, -2.248, 1.836, -2.197, 1.893, -2.145, 1.949,
- -2.091, 2.004, -2.042, 2.051, -1.992, 2.097, -1.942, 2.141, -1.891, 2.184, -1.839, 2.226, -1.786, 2.266, -0.88, 1.702, -0.869, 1.702, -0.819, 1.725, -0.77, 1.747, -0.719, 1.767,
- -0.669, 1.786, -0.655, 1.806
- ],
- corner: [0, 13, 14, 15, 21, 32, 33, 34, 38, 39, 47, 49, 53, 54, 55, 66, 72, 73, 74, 87, 88, 89, 95, 106, 107, 108, 111, 113, 121, 122, 125, 126, 127, 138, 144, 145],
- hole: [
- 0, 1, -0.063, 0.998, -0.127, 0.992, -0.189, 0.982, -0.251, 0.968, -0.313, 0.95, -0.373, 0.928, -0.432, 0.902, -0.49, 0.872, -0.546, 0.838, -0.601, 0.8, -0.653, 0.758,
- -0.703, 0.712, -0.764, 0.645, -0.819, 0.575, -0.866, 0.5, -0.907, 0.422, -0.94, 0.342, -0.966, 0.259, -0.984, 0.174, -0.996, 0.087, -0.999, 0, -0.996, -0.087, -0.984, -0.174,
- -0.966, -0.259, -0.94, -0.342, -0.907, -0.422, -0.866, -0.5, -0.819, -0.575, -0.764, -0.645, -0.703, -0.712, -0.653, -0.758, -0.601, -0.8, -0.546, -0.838, -0.49, -0.872, -0.432, -0.902,
- -0.373, -0.928, -0.313, -0.95, -0.251, -0.968, -0.189, -0.982, -0.127, -0.992, -0.063, -0.998, 0, -1, 0.063, -0.998, 0.127, -0.992, 0.189, -0.982, 0.252, -0.968, 0.313, -0.95,
- 0.373, -0.928, 0.432, -0.902, 0.49, -0.872, 0.546, -0.838, 0.601, -0.8, 0.653, -0.758, 0.703, -0.712, 0.764, -0.645, 0.819, -0.575, 0.866, -0.5, 0.907, -0.422, 0.94, -0.342,
- 0.966, -0.259, 0.984, -0.174, 0.996, -0.087, 0.999, 0, 0.996, 0.087, 0.984, 0.174, 0.966, 0.259, 0.94, 0.342, 0.907, 0.422, 0.866, 0.5, 0.819, 0.575, 0.764, 0.645,
- 0.703, 0.712, 0.653, 0.758, 0.601, 0.8, 0.546, 0.838, 0.49, 0.872, 0.432, 0.902, 0.373, 0.928, 0.313, 0.95, 0.252, 0.968, 0.189, 0.982, 0.127, 0.992, 0.063, 0.998
- ],
- front: "M -0.422 2.822 C -0.156 2.859 0.115 2.859 0.381 2.822 L 0.611 1.822 C 0.613 1.811 0.618 1.804 0.624 1.802 C 0.7 1.777 0.773 1.747 0.846 1.713 C 0.85 " +
- "1.711 0.854 1.711 0.857 1.714 L 1.745 2.267 C 1.851 2.187 1.953 2.1 2.05 2.004 C 2.142 1.914 2.226 1.818 2.303 1.72 L 1.755 0.863 C 1.747 0.852 1.746 " +
- "0.832 1.753 0.819 C 1.787 0.748 1.817 0.676 1.842 0.604 C 1.845 0.595 1.85 0.589 1.856 0.588 L 2.876 0.36 C 2.911 0.108 2.911 -0.148 2.876 -0.4 L " +
- "1.84 -0.631 C 1.836 -0.632 1.832 -0.636 1.829 -0.64 L 1.827 -0.646 C 1.802 -0.712 1.774 -0.777 1.742 -0.841 C 1.735 -0.854 1.736 -0.874 1.744 -0.886 " +
- "L 2.303 -1.76 C 2.226 -1.859 2.142 -1.954 2.05 -2.044 C 1.953 -2.14 1.851 -2.227 1.745 -2.307 L 0.82 -1.73 C 0.818 -1.729 0.815 -1.728 0.812 -1.729 L " +
- "0.809 -1.73 C 0.752 -1.755 0.693 -1.778 0.634 -1.798 C 0.628 -1.8 0.623 -1.808 0.621 -1.818 L 0.381 -2.863 C 0.115 -2.899 -0.156 -2.899 -0.422 -2.863 " +
- "L -0.665 -1.802 C -0.667 -1.792 -0.672 -1.785 -0.679 -1.783 C -0.731 -1.764 -0.782 -1.742 -0.832 -1.719 C -0.835 -1.718 -0.838 -1.718 -0.841 -1.718 L " +
- "-0.844 -1.72 L -1.786 -2.307 C -1.892 -2.227 -1.994 -2.14 -2.091 -2.044 C -2.183 -1.954 -2.267 -1.859 -2.344 -1.76 L -1.761 -0.85 C -1.754 -0.838 " +
- "-1.753 -0.819 -1.759 -0.806 C -1.783 -0.756 -1.804 -0.707 -1.823 -0.656 C -1.825 -0.65 -1.829 -0.645 -1.833 -0.643 L -1.836 -0.641 L -2.917 -0.401 C " +
- "-2.952 -0.148 -2.952 0.108 -2.917 0.36 L -1.852 0.598 C -1.847 0.599 -1.841 0.605 -1.839 0.613 C -1.819 0.67 -1.796 0.727 -1.77 0.783 C -1.764 0.796 " +
- "-1.765 0.815 -1.772 0.826 L -2.344 1.72 C -2.267 1.818 -2.183 1.913 -2.091 2.004 C -1.994 2.099 -1.892 2.187 -1.786 2.266 L -0.88 1.702 C -0.877 1.7 " +
- "-0.873 1.7 -0.869 1.702 C -0.803 1.734 -0.737 1.762 -0.669 1.786 C -0.662 1.789 -0.657 1.796 -0.655 1.806 L -0.422 2.822 Z M 0 1 C -0.253 1 -0.508 " +
- "0.904 -0.703 0.712 C -0.898 0.52 -0.999 0.263 -0.999 0 C -0.999 -0.263 -0.898 -0.52 -0.703 -0.712 C -0.508 -0.904 -0.253 -1 0 -1 C 0.253 -1 0.508 " +
- "-0.904 0.703 -0.712 C 0.898 -0.52 0.999 -0.263 0.999 0 C 0.999 0.263 0.898 0.52 0.703 0.712 C 0.508 0.904 0.253 1 0 1 Z",
- clip: "M240.8 205.2 L241.0 199.6 L241.7 193.7 L242.8 187.7 L244.4 181.6 L246.4 175.5 L248.9 169.4 L251.7 163.4 L254.9 157.5 L258.4 151.8 L262.2 146.3 L266.3 141.2 L270.6 136.4 L275.0 131.9 L279.7 128.0 L284.4 124.5 L289.1 121.5 L293.9 119.0 L298.6 117.2 L303.3 115.9 L307.8 115.2 L312.1 115.0 L316.2 115.5 L320.1 116.6 L323.6 118.3 L329.8 121.9 L333.1 124.1 L335.9 126.9 L338.4 130.2 L340.5 134.0 L342.1 138.3 L343.3 142.9 L344.0 148.0 L344.3 153.3 L344.1 159.0 L343.4 164.8 L342.3 170.8 L340.7 176.9 L338.7 183.0 L336.3 189.1 L333.5 195.1 L330.3 201.0 L326.8 206.7 L323.0 212.2 L318.9 217.3 L314.6 222.2 L310.1 226.6 L305.5 230.5 L300.8 234.0 L296.0 237.0 L291.2 239.5 L286.5 241.3 L281.9 242.6 L277.4 243.4 L273.0 243.5 L268.9 243.0 L265.1 241.9 L261.5 240.2 L255.3 236.6 L252.1 234.4 L249.2 231.6 L246.7 228.3 L244.7 224.5 L243.0 220.2 L241.8 215.6 L241.1 210.5Z"
+ /* ---- the current service names, drawn over the drawing's published labels ----
+ Each version of the drawing shows its lit service's label box, and data-integration.svg also shows the
+ Context Retriever box. The stage draws the same box, with the current name, at the same place, and
+ shows it whenever a version in `in` is lit (see compose()). Boxes are x, y, width, height, copied from
+ langcache.svg and context-retriever.svg. */
+ var RELABEL = {
+ cache: { box: [471.578, 518.156, 99.2506, 24.4832], lines: ["AGENT PLAYBOOKS"], in: ["cache"] },
+ retriever: { box: [498.461, 235.889, 99.2506, 31.2039], lines: ["CONTEXT", "GATEWAY"], in: ["retriever"] }
};
- var GEAR_AHEAD = { lit: [13, 14, 25, 46, 47, 48, 50], dim: [11, 12, 39, 40, 41] };
- var GEAR_SPEED = 45; // degrees a second, clockwise: one tooth a second, as on redis.io/iris
+ function buildRelabels(NS) {
+ var out = {};
+ Object.keys(RELABEL).forEach(function (k) {
+ var R = RELABEL[k], b = R.box, g = document.createElementNS(NS, "g");
+ var mid = b[1] + b[3] / 2, gap = 10, first = mid - (R.lines.length - 1) * gap / 2 + 2.95; // 2.95: half the glyphs' cap height
+ g.setAttribute("class", "iris-relabel");
+ g.innerHTML = '' +
+ R.lines.map(function (t, i) {
+ return '' + esc(t) + "";
+ }).join("");
+ out[k] = g;
+ });
+ return out;
+ }
- function buildMaya(NS) {
- var g = document.createElementNS(NS, "g"), L = MAYA.label;
- g.setAttribute("class", "iris-maya");
- g.innerHTML = '' +
- MAYA.arrows.map(function (d) { return ''; }).join("") +
- MAYA.dots.map(function (c) { return ''; }).join("") + "" +
- '' +
- '' +
- '' +
- '' +
- '' +
- '' +
- 'MAYA';
- return g;
+ /* ---- Agent Playbooks' icon on its disc ----
+ The drawing's Agent Playbooks disc still carries LangCache's sparkles, so the stage redraws the disc's face
+ with the Agent Playbooks icon (an open book with a star), and the disc's badge back on top. The drawing dims
+ an unlit service by drawing it at 40% opacity, so the stage does the same over a patch in the background
+ colour, which hides the old icon either way. The face and badge are copied from langcache.svg; the icon
+ sits in the face's plane, in units of its radius, as the agent's robot does. */
+ var PLAYBOOK_DISC = {
+ face: "M530.721 503.29C546.574 494.136 559.426 471.876 559.426 453.57C559.427 435.264 546.576 427.845 530.723 436.998C514.87 446.152 502.018 468.412 502.017 486.718C502.016 505.024 514.868 512.444 530.721 503.29Z",
+ center: [530.721, 470.142],
+ badge: ["M544.839 452.34C544.839 456.217 542.119 460.934 538.756 462.876C538.075 463.269 537.418 463.519 536.81 463.629L536.8 463.634C536.474 463.693 536.163 463.716 535.866 463.702C535.847 463.702 535.822 463.699 535.798 463.696C535.029 463.646 534.363 463.351 533.847 462.834C533.55 462.533 533.297 462.168 533.107 461.727C532.83 461.078 532.674 460.286 532.674 459.364C532.674 455.487 535.399 450.767 538.757 448.829C542.114 446.89 544.839 448.464 544.839 452.34Z",
+ "M542.017 454.35C541.511 455.328 540.967 456.434 539.873 457.065C538.9 457.627 538.537 456.902 538.51 456.163C538.724 456.526 539.142 456.683 539.791 456.29C541.044 455.523 541.902 453.8 541.902 452.684C541.902 451.351 540.984 450.915 539.389 451.835C538.246 452.495 536.833 453.785 535.904 455.063C535.893 455.836 536.289 456.604 536.432 456.41C537.235 455.323 537.872 454.55 538.493 453.985C537.575 455.625 535.376 459.468 535.107 460.079C535.135 460.48 535.613 461.32 535.844 461.187C535.915 461.145 535.976 461.067 536.047 460.951C536.707 459.76 537.251 458.723 537.729 457.748C537.801 458.729 538.262 459.715 539.56 458.966C540.72 458.297 541.874 456.72 542.396 454.368C542.462 454.081 542.176 454.046 542.017 454.35ZM540.692 453.457C540.692 454.106 540.104 454.763 539.571 455.071C539.285 455.236 539.065 455.282 538.889 455.278C539.214 454.567 539.527 453.844 539.868 453.073C540.467 452.833 540.692 453.065 540.692 453.457Z"],
+ icon: '' +
+ '' +
+ '' +
+ ''
+ };
+ function playbookDiscMarkup() {
+ var P = PLAYBOOK_DISC, I = DISC.icon, c = P.center;
+ return '' +
+ '' +
+ '' + P.icon + "" +
+ '';
}
- /* The turning gear, drawn into the stage's overlay once the copies of what's in front of it arrive.
- Without them it stays out, and the drawing's own still gear shows. */
- function buildGear(over, artBase, uid) {
- var NS = "http://www.w3.org/2000/svg", c = GEAR.c, A = GEAR.A, d = GEAR.d, front = GEAR.front.split(" ");
- var corner = {};
- GEAR.corner.forEach(function (i) { corner[i] = true; });
- var g = document.createElementNS(NS, "g");
- g.setAttribute("class", "iris-gear is-lit");
- g.setAttribute("clip-path", "url(#" + uid + "-gear)");
- g.innerHTML = '' +
- '' +
- '';
- var back = g.querySelector(".iris-gear-back"), face = g.querySelector(".iris-gear-face"), walls = g.querySelector(".iris-gear-walls"), pool = [];
+ /* The drawing's Data Integration corner (the source database pills, the gear, and their dashes into Redis),
+ which the tour leaves out: x, y, width, height of a patch in the drawing's background colour, inside its
+ rounded border. */
+ var COVER = [1.2, 1.2, 372, 262];
- /* A point in the gear's plane, turned, then placed on the page (and moved by ox, oy). */
- function pt(x, y, cs, sn, ox, oy) {
- var u = x * cs - y * sn, v = x * sn + y * cs;
- return [c[0] + A[0][0] * u + A[0][1] * v + ox, c[1] + A[1][0] * u + A[1][1] * v + oy];
- }
- function facePath(cs, sn, ox, oy) {
- var out = "", p;
- for (var i = 0; i < front.length; i++) {
- if (isNaN(front[i])) { out += front[i]; continue; }
- p = pt(+front[i], +front[i + 1], cs, sn, ox, oy);
- out += p[0].toFixed(2) + " " + p[1].toFixed(2) + " ";
- i++;
- }
- return out;
- }
- /* The side walls you can see: runs of outline edges whose outward normal points along d, split at
- sharp corners. Each run is drawn as the strip between its edges and their copy moved by d. */
- function addRuns(U, isHole, cs, sn, list) {
- var n = U.length / 2, P = [], vis = [], area = 0, i, k = 0, run = null;
- for (i = 0; i < n; i++) P.push(pt(U[2 * i], U[2 * i + 1], cs, sn, 0, 0));
- for (i = 0; i < n; i++) area += P[i][0] * P[(i + 1) % n][1] - P[(i + 1) % n][0] * P[i][1];
- var side = (area > 0 ? 1 : -1) * (isHole ? -1 : 1); // the solid's outward normal, for this winding
- for (i = 0; i < n; i++) {
- var ex = P[(i + 1) % n][0] - P[i][0], ey = P[(i + 1) % n][1] - P[i][1];
- vis.push(side * (ey * d[0] - ex * d[1]) > 0);
- }
- function starts(i) { return vis[i] !== vis[(i + n - 1) % n] || (!isHole && corner[i]); }
- for (i = 0; i < n; i++) if (starts(i)) { k = i; break; }
- for (var m = 0; m <= n; m++) {
- i = (k + m) % n;
- if (run && (m === n || starts(i))) { list.push(run); run = null; }
- if (m === n) break;
- if (vis[i]) { if (!run) run = [P[i]]; run.push(P[(i + 1) % n]); }
- }
+ var MISSING_ROUTES = { router: [], retriever: ["dbcr", "cragent", "cragent2"], memory: ["amdb", "amagent", "amagent2"], cache: ["dblc", "lcagent", "lcagent2"] };
+
+ /* The figures' markup: each link, then each disc with its icon and label. Kept as strings so the tests can draw it. */
+ function figuresMarkup() {
+ function n(v) { return (+v).toFixed(2); }
+ function arrow(seg) {
+ var a = seg[0], b = seg[1], l = Math.hypot(b[0] - a[0], b[1] - a[1]), ux = (b[0] - a[0]) / l, uy = (b[1] - a[1]) / l;
+ return '';
}
- function draw(deg) {
- var r = deg * Math.PI / 180, cs = Math.cos(r), sn = Math.sin(r), list = [], i, j;
- back.setAttribute("d", facePath(cs, sn, d[0], d[1]));
- face.setAttribute("d", facePath(cs, sn, 0, 0));
- addRuns(GEAR.outer, false, cs, sn, list);
- /* Far walls first. In the gear's plane, the side d points to is nearer the viewer. */
- list.forEach(function (w) {
- var sum = 0;
- w.forEach(function (p) { sum += (p[0] - c[0]) * d[0] + (p[1] - c[1]) * d[1]; });
- w.key = sum / w.length;
- });
- list.sort(function (a, b) { return a.key - b.key; });
- addRuns(GEAR.hole, true, cs, sn, list);
- for (i = 0; i < list.length; i++) {
- var w = list[i], s = "M";
- for (j = 0; j < w.length; j++) s += w[j][0].toFixed(2) + " " + w[j][1].toFixed(2) + " L";
- for (j = w.length - 1; j >= 0; j--) s += (w[j][0] + d[0]).toFixed(2) + " " + (w[j][1] + d[1]).toFixed(2) + (j ? " L" : "Z");
- if (!pool[i]) pool[i] = walls.appendChild(document.createElementNS(NS, "path"));
- pool[i].setAttribute("d", s);
- pool[i].style.display = "";
- }
- for (; i < pool.length; i++) pool[i].style.display = "none";
+ function mid(seg) { return ''; }
+ function linkG(k, oneWay) {
+ var L = LINKS[k];
+ return '' +
+ (oneWay ? "" : arrow(L.in)) + arrow(L.out) + '' + (oneWay ? "" : mid(L.in)) + mid(L.out) + "";
}
-
- /* Turn only while the stage is on screen and the tab is visible. */
- var angle = 0, last = null, raf = 0, inView = true;
- function frame(now) {
- if (last !== null) angle = (angle + Math.min(100, now - last) / 1000 * GEAR_SPEED) % 360;
- last = now;
- draw(angle);
- raf = requestAnimationFrame(frame);
+ function disc(f, colors, icon, iconStroke) {
+ var c = f.c, s = f.s, I = DISC.icon;
+ return '' +
+ '' +
+ '' +
+ '' + icon + "";
}
- function run() {
- var go = inView && !document.hidden;
- if (go && !raf) raf = requestAnimationFrame(frame);
- else if (!go && raf) { cancelAnimationFrame(raf); raf = 0; last = null; }
+ function badge(f) {
+ var s = f.s, c = f.c;
+ return '' +
+ '';
}
- function copy(file, idx, into) {
- return fetch(artBase + file).then(function (r) {
- if (!r.ok) throw new Error("HTTP " + r.status);
- return r.text();
- }).then(function (txt) {
- var kids = new DOMParser().parseFromString(txt, "image/svg+xml").documentElement.children;
- idx.forEach(function (i) { if (!kids[i]) throw new Error("drawing changed"); into.appendChild(document.importNode(kids[i], true)); });
- });
+ /* A one-line box is the agent label's size and style, and a two-line box Agent Memory's. */
+ function label(f, cls) {
+ var L = f.label, two = L.lines.length > 1, x = f.c[0] + L.dx, y = f.c[1] + L.dy, h = two ? 31.2039 : 24.751;
+ return '' +
+ L.lines.map(function (t, i) {
+ return '' + t + "";
+ }).join("") + "";
}
- Promise.all([copy(ART.rdi, GEAR_AHEAD.lit, g.querySelector(".iris-gear-ahead.is-lit")),
- copy(ART.retriever, GEAR_AHEAD.dim, g.querySelector(".iris-gear-ahead.is-dim"))]).then(function () {
- draw(0);
- over.insertBefore(g, over.querySelector(".iris-ring"));
- if (window.IntersectionObserver) new IntersectionObserver(function (es) { inView = es[0].isIntersecting; run(); }).observe(over);
- document.addEventListener("visibilitychange", run);
- run();
- }).catch(function () { /* the still drawing stays */ });
+ var SERVICE = ["#3A2227", "#FF4438", "#89302E"]; // the drawing's colours for a service disc: rim, outline, face
return {
- /* Lit when the step's drawing lights Data Integration; "darken" keeps only what every version lights. */
- setLit: function (on) { g.classList.toggle("is-lit", on); }
+ links: linkG("router") + linkG("maya") + linkG("lens", true),
+ router: '' + disc(FIG.router, SERVICE, '' + ROUTER_ICON + "", 0.04) + badge(FIG.router) + "" + label(FIG.router, "iris-router-label"),
+ lens: '' + disc(FIG.lens, SERVICE, '' + LENS_ICON + "", 0.04) + badge(FIG.lens) + "" + label(FIG.lens, "iris-lensfig-label"),
+ maya: disc(FIG.maya, ["#091A23", "#5C707A", "#163341"],
+ '', 0.042) + label(FIG.maya, "")
};
}
+ function buildFigures(NS) {
+ var m = figuresMarkup(), g = document.createElementNS(NS, "g");
+ g.setAttribute("class", "iris-maya");
+ g.innerHTML = m.links + '' + m.router + '' + m.lens + "" + m.maya;
+ return g;
+ }
function buildStage(artBase, onPick, onData) {
var uid = "iris" + Math.random().toString(36).slice(2, 8);
var reduced = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
var box = el("div", "iris-stage");
box.setAttribute("role", "img");
- /* Two stacks of the four versions. A step blends its versions in the stack that isn't showing, then
+ /* Two stacks of the versions. A step blends its versions in the stack that isn't showing, then
fades that stack in over the other, so a half-faded version never blends the wrong way. */
var sets = [0, 1].map(function () {
var set = el("div", "iris-artset"), layers = {};
- ["rdi", "retriever", "cache", "memory"].forEach(function (k) {
+ ["retriever", "cache", "memory"].forEach(function (k) {
var img = el("img", "iris-art");
+ img.style.left = vx(0); img.style.top = vy(0); img.style.width = vw(ART_W); img.style.height = vh(ART_H);
img.src = artBase + ART[k];
img.alt = "";
img.decoding = "async";
@@ -677,10 +694,11 @@
var NS = "http://www.w3.org/2000/svg";
var over = document.createElementNS(NS, "svg");
over.setAttribute("class", "iris-over");
- over.setAttribute("viewBox", "0 0 " + ART_W + " " + ART_H);
+ box.style.aspectRatio = VIEW[2] + " / " + VIEW[3];
+ over.setAttribute("viewBox", VIEW.join(" "));
over.setAttribute("aria-hidden", "true");
over.innerHTML = '' +
- '' +
+ '' +
Object.keys(ROUTES).map(function (k) { return ''; }).join("") +
Object.keys(HITS).map(function (k) {
var h = HITS[k];
@@ -690,11 +708,17 @@
return ["iris-ring", "iris-ring iris-pulse"].map(function (cls) { return DATA[k].ring.replace(/^<(\w+)/, '<$1 class="' + cls + '" data-src="' + k + '"'); }).join("");
}).join("") + '';
box.appendChild(over);
- var maya = buildMaya(NS);
- over.insertBefore(maya, over.querySelector(".iris-route"));
- var mayaLink = maya.querySelector(".iris-maya-link");
+ var figs = buildFigures(NS);
+ over.insertBefore(figs, over.querySelector(".iris-route"));
+ var mayaLink = figs.querySelector(".iris-link-maya"), routerLink = figs.querySelector(".iris-link-router"), router = figs.querySelector(".iris-router");
+ var lensLink = figs.querySelector(".iris-link-lens"), lens = figs.querySelector(".iris-lensfig");
+ var relabels = buildRelabels(NS);
+ Object.keys(relabels).forEach(function (k) { over.insertBefore(relabels[k], over.querySelector(".iris-route")); });
+ var pbDisc = document.createElementNS(NS, "g");
+ pbDisc.setAttribute("class", "iris-pbdisc");
+ pbDisc.innerHTML = playbookDiscMarkup();
+ over.insertBefore(pbDisc, over.querySelector(".iris-route"));
var packets = over.querySelector(".iris-packets");
- var gear = reduced ? null : buildGear(over, artBase, uid);
/* Real buttons over POSTGRES and the Redis logo, so the data is reachable by keyboard too. They sit outside
the drawing (role img) so assistive tech still finds them, and the data window opens over the drawing. */
var wrap = el("div", "iris-stagewrap");
@@ -705,10 +729,10 @@
b.type = "button";
b.setAttribute("aria-label", "Show the sample data in " + D.label);
b.title = "Show the sample data in " + D.label;
- b.style.left = (D.box[0] / ART_W * 100) + "%";
- b.style.top = (D.box[1] / ART_H * 100) + "%";
- b.style.width = (D.box[2] / ART_W * 100) + "%";
- b.style.height = (D.box[3] / ART_H * 100) + "%";
+ b.style.left = vx(D.box[0]);
+ b.style.top = vy(D.box[1]);
+ b.style.width = vw(D.box[2]);
+ b.style.height = vh(D.box[3]);
if (D.clip) b.style.clipPath = D.clip;
if (D.round) b.style.borderRadius = "50%";
b.addEventListener("click", function () { onData(b, k); });
@@ -727,7 +751,7 @@
/* Show some versions of the drawing: the first one as is, the rest blended onto it. */
function compose(keys, blend) {
var first = true, next = cur === sets[0] ? sets[1] : sets[0];
- ["rdi", "retriever", "cache", "memory"].forEach(function (k) {
+ ["retriever", "cache", "memory"].forEach(function (k) {
var on = keys.indexOf(k) >= 0, img = next.layers[k];
img.classList.toggle("is-on", on);
img.style.mixBlendMode = on && !first ? (blend || "lighten") : "normal";
@@ -745,14 +769,20 @@
next.el.style.opacity = "1";
next.el.classList.add("is-current");
cur = next;
- if (gear) gear.setLit(keys.indexOf("rdi") >= 0 && (keys.length === 1 || blend !== "darken"));
+ /* Agent Playbooks' disc is lit where its version is, as its label is. */
+ pbDisc.classList.toggle("is-lit", keys.indexOf("cache") >= 0 && (keys.length === 1 || blend !== "darken"));
+ /* A label shows where a version that has it is lit; "darken" keeps only what every version lights, which no label is. */
+ Object.keys(relabels).forEach(function (k) {
+ var shown = RELABEL[k].in.some(function (v) { return keys.indexOf(v) >= 0; });
+ relabels[k].classList.toggle("is-on", shown && (keys.length === 1 || blend !== "darken"));
+ });
}
function setTag(node, text) {
if (!node) { tag.classList.remove("is-on"); return; }
var p = TAG_SPOTS[node];
tag.textContent = text;
- tag.style.left = (p[0] / ART_W * 100) + "%";
- tag.style.top = (p[1] / ART_H * 100) + "%";
+ tag.style.left = vx(p[0]);
+ tag.style.top = vy(p[1]);
tag.style.transform = "translate(" + { c: "-50%", r: "-100%", l: "0" }[p[2]] + ", -100%)";
tag.classList.add("is-on");
}
@@ -801,17 +831,26 @@
el: wrap,
art: box,
dataBtns: dataBtns,
- show: function (stepKey, missing) {
- var tok = ++token;
- box.classList.toggle("is-step-rdi", stepKey === "rdi"); // POSTGRES and the Redis logo pulse on the step where the data changes
- mayaLink.classList.toggle("is-lit", stepKey === "ask" || stepKey === "reply" || stepKey === "whatif");
+ /* focus: on the Lens step, the part of the drawing that the step selected in its timeline is about (a LENS_FLOWS key).
+ Without one, as in the preview, the Lens step plays the whole request. */
+ show: function (stepKey, missing, focus) {
+ var tok = ++token, lensStep = stepKey === "lens";
+ /* Intent Router and its link are lit on its own step, on Lens's while its match is selected (or without a selection),
+ and on the what-if, where it's one of the services to take away. It writes no part of the reply, so the reply step
+ leaves it dimmed. */
+ var routerLit = stepKey === "router" || (lensStep && (!focus || focus === "router")) || (stepKey === "whatif" && missing !== "router");
+ mayaLink.classList.toggle("is-lit", stepKey === "ask" || stepKey === "reply" || (lensStep && !focus) || stepKey === "whatif");
+ routerLink.classList.toggle("is-lit", routerLit);
+ router.classList.toggle("is-lit", routerLit);
+ lensLink.classList.toggle("is-lit", lensStep);
+ lens.classList.toggle("is-lit", lensStep);
setTag(null);
Array.prototype.forEach.call(packets.childNodes, function (n) { n.remove(); });
- var flow = stepKey === "whatif" ? null : FLOWS[stepKey];
+ var flow = stepKey === "whatif" ? null : lensStep && focus ? LENS_FLOWS[focus] : FLOWS[stepKey];
var skip = {};
if (stepKey === "whatif") {
/* Every service, or every service but the missing one, with the reply flowing in. */
- var keys = ["rdi", "retriever", "cache", "memory"].filter(function (k) { return k !== missing; });
+ var keys = ["retriever", "cache", "memory"].filter(function (k) { return k !== missing; });
compose(keys, "lighten");
if (!missing) return;
(MISSING_ROUTES[missing] || []).forEach(function (k) { skip[k] = true; });
@@ -831,10 +870,17 @@
/* ------------------------------------------------------------------ view */
function init(root) {
- var links = { rdi: root.getAttribute("data-link-rdi"), retriever: root.getAttribute("data-link-retriever"),
- memory: root.getAttribute("data-link-memory"), cache: root.getAttribute("data-link-cache") };
- var st = { step: 0, missing: null };
- var full = trace({ rdi: true, retriever: true, memory: true, cache: true });
+ var links = { router: root.getAttribute("data-link-router"), retriever: root.getAttribute("data-link-retriever"),
+ memory: root.getAttribute("data-link-memory"), cache: root.getAttribute("data-link-cache"), lens: root.getAttribute("data-link-lens") };
+ var st = { step: 0, missing: null, lensBug: false, lensRow: 0 };
+ var full = trace({ router: true, retriever: true, memory: true, cache: true });
+ var bug = trace({ router: true, retriever: true, memory: true, cache: true }, { drop: "peanut" }); // the Lens step's turn with a bug
+ var lensTurns = { ran: lensRows(full), bug: lensRows(bug) };
+ /* The part of the drawing that the step selected in Lens's timeline is about (a LENS_FLOWS key). */
+ function lensFocus() {
+ var rows = st.lensBug ? lensTurns.bug : lensTurns.ran, x = rows[Math.min(st.lensRow, rows.length - 1)];
+ return x.focus;
+ }
var reduced = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
function go(i) {
@@ -849,15 +895,18 @@
/* The one thing each step shows. */
function fact(step) {
+ if (step.key === "lens") return lensView();
var f = el("div", "iris-fact");
- if (step.key === "rdi") {
- f.innerHTML = '
PostgreSQLUPDATE restaurants SET avg_delivery_min = 45 WHERE id = \'r209\';
";
}).join("");
} else if (step.key === "reply") {
f.appendChild(replyBubble(full.reply));
@@ -878,13 +927,13 @@
}
return f;
}
- var SOURCES = { rdi: "Data Integration", retriever: "Context Gateway", memory: "Agent Memory", cache: "LangCache" };
+ var SOURCES = { router: "Intent Router", retriever: "Context Gateway", memory: "Agent Memory", cache: "Agent Playbooks" };
/* The key comes first, styled like the parts of the reply, so the colors mean something before you read.
Pointing at a key or at a part of the reply shows only that service's parts; selecting a key keeps it so. */
function replyBubble(segments) {
var wrap = el("div", "iris-reply"), legend = el("div", "iris-legend"), keys = {}, pinned = null;
legend.appendChild(el("div", "rcr-lbl", "Where each part came from"));
- ["rdi", "retriever", "memory", "cache"].forEach(function (k) {
+ ["retriever", "memory", "cache"].forEach(function (k) {
var b = keys[k] = el("button", "iris-key iris-src-" + k, esc(SOURCES[k]));
b.type = "button";
b.setAttribute("aria-pressed", "false");
@@ -934,7 +983,7 @@
function whatIf() {
var box = el("div", "iris-whatif");
var picks = el("div", "iris-picks");
- ["rdi", "retriever", "memory", "cache"].forEach(function (k) {
+ ["router", "retriever", "memory", "cache"].forEach(function (k) {
var b = el("button", "rcr-chip iris-pick iris-svc-" + k + (st.missing === k ? " is-on" : ""), "Without " + esc(SOURCES[k]));
b.type = "button";
b.setAttribute("aria-pressed", st.missing === k ? "true" : "false");
@@ -943,7 +992,7 @@
});
box.appendChild(picks);
if (!st.missing) { box.appendChild(el("p", "iris-pickhint rcr-faint", "Pick a service.")); return box; }
- var w = WHAT_IF[st.missing], on = { rdi: true, retriever: true, memory: true, cache: true };
+ var w = WHAT_IF[st.missing], on = { router: true, retriever: true, memory: true, cache: true };
on[st.missing] = false;
var c = trace(on).checks.filter(function (x) { return x.key === w.check; })[0];
/* Excerpts start mid-reply, and get a trailing ellipsis only when they stop mid-sentence. */
@@ -957,7 +1006,132 @@
return box;
}
- /* ---- the data windows: PostgreSQL and Redis ---- */
+ /* ---- the Lens step: the turn as Lens records it ----
+ A timeline of the turn's steps, as Lens shows one, and what Lens recorded for the selected step. The
+ other view is the same turn with a bug in the app's code, which leaves the allergy memory out of the
+ prompt. Selecting a step redraws only the record, which is a live region, so a screen reader reads
+ the new record out. */
+
+ /* A trace's steps as Lens records them. Agent Memory, Agent Playbooks, and Context Gateway record their
+ own steps; the app records the Intent Router match (as a custom step) and the prompt assembly. Each row is
+ named by its service, with a word only where two rows would read the same (op), and `focus` is the part of
+ the drawing it's about (a LENS_FLOWS key). */
+ function lensRows(tr) {
+ return tr.steps.filter(function (s) { return s.ms && !s.off; }).map(function (s) {
+ var row = { svc: s.service, focus: s.service, at: s.at, ms: s.ms, op: "" }, c = s.calls && s.calls[0];
+ if (s.service === "router") {
+ row.name = "Intent Router"; row.items = c.response.items;
+ } else if (s.service === "cache") {
+ row.name = "Agent Playbooks"; row.query = c.request.query;
+ row.results = c.response.discoveredEntries.map(function (e) { return { label: "FAQ · similarity score " + e.score.toFixed(2), text: e.faq.question, used: true }; });
+ } else if (s.service === "memory" && s.phase === "recall") {
+ row.name = "Agent Memory"; row.op = "recall"; row.query = c.request.text;
+ row.results = MEMORIES.map(function (m) { return { label: m.type, text: m.text, used: !!tr.used[m.key] }; });
+ } else if (s.service === "retriever") {
+ row.name = "Context Gateway"; row.tool = c.path; row.input = spaced(c.args); row.result = brief(c.result);
+ } else if (s.service === "app") {
+ row.name = "Prompt assembly"; row.used = s.used; row.of = s.of; row.dropped = s.dropped;
+ } else if (s.service === "model") {
+ row.name = "Model call"; row.tokens = s.tokens; row.note = s.text;
+ } else {
+ row.name = "Agent Memory"; row.op = "save"; row.focus = "record";
+ row.note = "Stored Maya's message and the reply as events in session " + SESSION + ".";
+ }
+ return row;
+ });
+ }
+ /* JSON on one line, with a space after each comma and colon, so it wraps between values. */
+ function spaced(v) {
+ return JSON.stringify(v).replace(/("(?:[^"\\]|\\.)*")|([,:])/g, function (m, str, sep) { return str || sep + " "; });
+ }
+ /* A tool result in a few words: a restaurant record, or a filter's matches. */
+ function brief(r) {
+ if (!r.results) return r.name + ", " + r.avg_delivery_min + " minutes";
+ return r.count + (r.count === 1 ? " match: " : " matches: ") + r.results.map(function (x) {
+ return x.name ? x.name + ", " + x.avg_delivery_min + " minutes" : x.id + ", " + x.items;
+ }).join("; ");
+ }
+ function lensView() {
+ var box = el("div", "iris-fact iris-lens"), modes = el("div", "iris-lensmodes"), note = el("div", "iris-lensnote"), head = el("div", "iris-lenshead");
+ var list = el("div", "iris-timeline"), info = el("div", "iris-tdetail");
+ note.setAttribute("aria-live", "polite");
+ list.setAttribute("role", "group");
+ list.setAttribute("aria-label", "Steps in this request");
+ info.setAttribute("aria-live", "polite");
+ [modes, note, head, list, info].forEach(function (x) { box.appendChild(x); });
+ var turns = lensTurns;
+ function focusStage() { stage.show("lens", null, lensFocus()); } // the drawing plays the selected step
+ [[false, "The request as it ran"], [true, "A request with a bug"]].forEach(function (m) {
+ var b = el("button", "rcr-chip iris-lensmode", esc(m[1]));
+ b.type = "button";
+ b.dataset.mode = m[0] ? "bug" : "ran";
+ b.addEventListener("click", function () {
+ if (st.lensBug === m[0]) return;
+ st.lensBug = m[0];
+ st.lensRow = 0; // either request starts from its first step
+ draw();
+ focusStage();
+ });
+ modes.appendChild(b);
+ });
+ function select(i) {
+ st.lensRow = i;
+ Array.prototype.forEach.call(list.children, function (b, k) { b.classList.toggle("is-on", k === i); b.setAttribute("aria-pressed", k === i ? "true" : "false"); });
+ detail(info, (st.lensBug ? turns.bug : turns.ran)[i]);
+ }
+ function draw() {
+ var rows = st.lensBug ? turns.bug : turns.ran, total = (st.lensBug ? bug : full).end;
+ Array.prototype.forEach.call(modes.children, function (b) {
+ var on = (b.dataset.mode === "bug") === st.lensBug;
+ b.classList.toggle("is-on", on);
+ b.setAttribute("aria-pressed", on ? "true" : "false");
+ });
+ note.innerHTML = st.lensBug ? '
' + esc("Suppose a change to the app's code leaves the allergy memory out of the prompt. The reply still suggests the pad thai, " +
+ "but doesn't ask to leave out peanuts. Lens shows where it went wrong: Agent Memory returned the allergy, but the app never put it in the prompt. " +
+ "So the fix is in the app's code, not in Agent Memory or the model.") + "
" : "";
+ head.innerHTML = 'Session ' + esc(SESSION) + ' · agent ' + esc(ASSISTANT) + "" +
+ 'Outcome completed · ' + seconds(total) + "";
+ list.innerHTML = "";
+ rows.forEach(function (x, i) {
+ var b = el("button", "iris-trow iris-src-" + x.svc);
+ b.type = "button";
+ b.innerHTML = '' + esc(x.name) + (x.op ? ' ' + esc(x.op) + "" : "") +
+ (x.dropped && x.dropped.length ? ' ' + x.dropped.length + " dropped" : "") + "" +
+ '' +
+ '' + took(x.ms) + "";
+ b.addEventListener("click", function () { if (i !== st.lensRow) { select(i); focusStage(); } });
+ list.appendChild(b);
+ });
+ select(Math.min(st.lensRow, rows.length - 1));
+ }
+ draw();
+ return box;
+ }
+ /* What Lens recorded for one step. Results are marked as Lens marks them: Entered prompt, or Not used. */
+ function detail(d, x) {
+ var body;
+ if (x.items) {
+ body = "
" + esc("The app records this step itself, with the intents that matched, best first.") + '
" + body;
+ }
+
+ /* ---- the data window: Redis ---- */
/* It opens over the drawing only, so the step stays readable and usable below it. */
var modal = null, opener = null, dataSrc, dataTab, flipTimer = 0;
@@ -1005,7 +1179,7 @@
Array.prototype.forEach.call(tabs.children, function (b) { b.setAttribute("aria-selected", b.dataset.table === dataTab ? "true" : "false"); });
var t = dataSrc.tables.filter(function (x) { return x.name === dataTab; })[0], focus = dataSrc.focus[t.name];
/* On step 1 the change happens while you watch: Lotus Thai's time flips from 25 to 45. */
- var live = STEPS[st.step].key === "rdi" && !reduced;
+ var live = false; // no step changes the data while you watch
var html = '
' + t.columns.map(function (c, i) { return "
" + esc(c) + "
"; }).join("") + "
";
t.rows.forEach(function (r) {
var used = focus.indexOf(r[0]) >= 0, changed = r[0] === dataSrc.changed;
@@ -1066,12 +1240,13 @@
function render() {
var step = STEPS[st.step];
closeData(true); // the drawing is what each step is about
- stage.show(step.key, step.key === "whatif" ? st.missing : null);
+ stage.show(step.key, step.key === "whatif" ? st.missing : null, step.key === "lens" ? lensFocus() : null);
stage.art.setAttribute("aria-label", "Step " + (st.step + 1) + ": " + step.title);
below.innerHTML = "";
var card = el("div", "rcr-card iris-stepcard" + (DEPTH[step.key] ? " iris-svc-" + step.key : ""));
card.appendChild(el("div", "iris-stephead", 'Step ' + (st.step + 1) + " of " + STEPS.length + "" +
- (step.at ? 'Fri, Oct 2 · ' + step.at + " UTC" : "")));
+ (step.at ? 'Fri, Oct 2 · ' + step.at + " UTC" : "") +
+ (step.preview ? '' + esc(DEPTH[step.key]) + " is in preview" : "")));
card.appendChild(el("div", "iris-title", esc(step.title))); // a div, not a heading, so the site's heading styles and anchors stay out
card.appendChild(el("div", "iris-text", esc(step.text)));
if (step.key === "whatif") card.appendChild(whatIf());
@@ -1082,7 +1257,7 @@
card.appendChild(depth);
}
below.appendChild(card);
- if (step.key !== "whatif" && step.key !== "reply") below.appendChild(tray(st.step));
+ if (["reply", "lens", "whatif"].indexOf(step.key) < 0) below.appendChild(tray(st.step));
var nav = el("div", "iris-nav");
var back = el("button", "rcr-btn", "Back");
back.type = "button";
@@ -1100,7 +1275,7 @@
var last = st.step === STEPS.length - 1;
var next = el("button", "rcr-btn rcr-btn-primary", last ? "Start over" : "Next");
next.type = "button";
- next.addEventListener("click", function () { if (last) st.missing = null; go(last ? 0 : st.step + 1); });
+ next.addEventListener("click", function () { if (last) { st.missing = null; st.lensBug = false; st.lensRow = 0; } go(last ? 0 : st.step + 1); });
nav.appendChild(back);
nav.appendChild(dots);
nav.appendChild(next);
@@ -1109,9 +1284,43 @@
render();
}
- function boot() { Array.prototype.forEach.call(document.querySelectorAll(".iris[data-iris]"), init); }
+ /* ---- preview: the stage alone, playing the tour's steps by itself ----
+ For a page that links to the tour: no step cards, controls, or data windows, so the whole stage can sit
+ inside a link. Each step plays for long enough for its packets to arrive, and only while the stage is on
+ screen. With reduced motion, the stage shows the reply step, with every service lit, and stays still. */
+ var PREVIEW_MS = { retriever: 7600, reply: 5200, lens: 4600 }, PREVIEW_DEFAULT_MS = 4200;
+ function initPreview(root) {
+ var reduced = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
+ root.innerHTML = "";
+ root.classList.add("iris-preview");
+ var stage = buildStage(root.getAttribute("data-art") || "", function () {}, function () {});
+ Object.keys(stage.dataBtns).forEach(function (k) { stage.dataBtns[k].remove(); });
+ stage.art.setAttribute("aria-label", "An animated preview of the Redis Iris interactive tour");
+ root.appendChild(stage.el);
+ root.appendChild(el("span", "iris-preview-cue", "Take the interactive tour →")); // below the drawing, so it never covers a label
+ var keys = STEPS.map(function (x) { return x.key; }).filter(function (k) { return k !== "whatif"; });
+ if (reduced) { stage.show("reply"); return; }
+ var i = 0, timer = 0, inView = true;
+ function next() {
+ var key = keys[i];
+ stage.show(key);
+ i = (i + 1) % keys.length;
+ timer = setTimeout(function () { if (inView) next(); else timer = 0; }, PREVIEW_MS[key] || PREVIEW_DEFAULT_MS);
+ }
+ if (window.IntersectionObserver) new IntersectionObserver(function (es) {
+ inView = es[0].isIntersecting;
+ if (inView && !timer) next();
+ }).observe(root);
+ next();
+ }
+
+ function boot() {
+ Array.prototype.forEach.call(document.querySelectorAll(".iris[data-iris]"), function (root) {
+ if (root.hasAttribute("data-preview")) initPreview(root); else init(root);
+ });
+ }
if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", boot); else boot();
/* Exposed for tests. */
- window.IrisDemo = { trace: trace, STEPS: STEPS, WHAT_IF: WHAT_IF, SERVICES: SERVICES, MEMORIES: MEMORIES, MESSAGE: MESSAGE, FEE: FEE };
+ window.IrisDemo = { figuresMarkup: figuresMarkup, playbookDiscMarkup: playbookDiscMarkup, ROUTES: ROUTES, HITS: HITS, trace: trace, STEPS: STEPS, WHAT_IF: WHAT_IF, SERVICES: SERVICES, MEMORIES: MEMORIES, MESSAGE: MESSAGE, FEE: FEE, RELABEL: RELABEL };
})();
diff --git a/static/js/iris-request-flow.js b/static/js/iris-request-flow.js
new file mode 100644
index 0000000000..0ff9c4a2fd
--- /dev/null
+++ b/static/js/iris-request-flow.js
@@ -0,0 +1,240 @@
+/* =========================================================================
+ Redis Iris request flow: which part of the stack answers which question
+ Dependency-free. Everything runs in the browser; no Iris service is called.
+
+ The Iris stack sits on the left and the questions Maya asks the food
+ delivery app's assistant on the right, from the same story as the Iris
+ tour (iris-demo.js). Selecting a question sends a dot from it to Intent
+ Router, then on to the component, or components, that answer it, and
+ Lens and Agent Mesh light as they record the turn and stream the reply.
+ The point is that the stack isn't a fixed sequence: each question takes
+ the parts it needs. Scores, timings, and replies are illustrative.
+
+ Keep the questions, intents, and results in step with iris-demo.js.
+ ========================================================================= */
+(function () {
+ "use strict";
+ if (window.IrisRequestFlow) return; // loaded twice: the first copy already booted every widget
+
+ /* ------------------------------------------------------------- the stack */
+
+ /* Top to bottom. `along` marks the parts that work alongside the request rather than answering it. */
+ var STACK = [
+ { key: "router", name: "Intent Router", role: "Matches the message to an intent you defined" }, // TODO: add doc: "/develop/ai/context-engine/intent-router" when intent-router-docs-align merges
+ { key: "playbooks", name: "Agent Playbooks", role: "Published answers and procedures", doc: "/develop/ai/context-engine/agent-playbook" },
+ { key: "memory", name: "Agent Memory", role: "Session and long-term recall", doc: "/develop/ai/context-engine/agent-memory" },
+ { key: "gateway", name: "Context Gateway", role: "Governed tools over your data and MCP servers", doc: "/develop/ai/context-engine/context-gateway" },
+ { key: "lens", name: "Lens", role: "Traces each turn", doc: "/develop/ai/context-engine/lens", along: true },
+ { key: "mesh", name: "Agent Mesh", role: "Keeps the reply stream resumable", along: true }
+ ];
+
+ /* ---------------------------------------------------------- the questions */
+
+ /* Each question: the intent Intent Router matches, the components it goes to, what each returns, and whether
+ a model call writes the answer. `tag` is the short note on a component when the dot arrives. */
+ var QUESTIONS = [
+ { key: "fee", text: "How much is delivery these days?",
+ intent: "delivery_fee", score: 0.78, target: "tool playbooks.discover_entries",
+ to: [{ key: "playbooks", tag: "FAQ · 0.92",
+ result: "Returns the support team's published FAQ: delivery costs $1.99 to $4.99, and Plus members get free delivery over $15." }],
+ model: "No model call. The published answer is the reply." },
+ { key: "reorder", text: "Can you order the curry I had last week again?",
+ intent: "reorder", score: 0.86, target: "agent food-assistant.reorder",
+ to: [{ key: "memory", tag: "5 memories",
+ result: "Recalls last week's order, an extra spicy green curry from Lotus Thai with no peanuts or fish sauce, from a long-term memory search scoped to Maya." }],
+ model: "The model writes the reply from what Agent Memory recalled." },
+ { key: "time", text: "Can it be here within 30 minutes?",
+ intent: "delivery_time", score: 0.81, target: "agent food-assistant.delivery",
+ to: [{ key: "gateway", tag: "1 tool call",
+ result: "Calls fooddelivery.get_restaurant_by_id through one MCP endpoint. The Context Surface over the restaurant records says Lotus Thai is taking 45 minutes tonight." }],
+ model: "The model writes the reply: Lotus Thai can't make it in time." },
+ { key: "find", text: "Find me a Thai place that can deliver in time, no peanuts.",
+ intent: "find_restaurant", score: 0.83, target: "agent food-assistant.search",
+ to: [
+ { key: "gateway", tag: "2 tool calls",
+ result: "Calls fooddelivery.filter_restaurant for Thai restaurants in Austin within 30 minutes, and finds Bangkok Street Kitchen. Then checks Maya's past orders there." },
+ { key: "memory", tag: "peanut allergy",
+ result: "Recalls that Maya is allergic to peanuts and loves spicy Thai food, so the reply suggests her usual order without peanuts." }],
+ model: "The model writes one reply from both results, which arrived in parallel." }
+ ];
+ var ALONG = [{ key: "lens", tag: "turn traced" }, { key: "mesh", tag: "reply streamed" }];
+
+ /* --------------------------------------------------------------- helpers */
+
+ function esc(s) {
+ return String(s).replace(/[&<>"']/g, function (c) {
+ return { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c];
+ });
+ }
+ function el(tag, cls, html) {
+ var e = document.createElement(tag);
+ if (cls) e.className = cls;
+ if (html != null) e.innerHTML = html;
+ return e;
+ }
+ function byKey(list, key) { return list.filter(function (x) { return x.key === key; })[0]; }
+ function wait(ms) { return new Promise(function (r) { setTimeout(r, ms); }); }
+
+ /* ------------------------------------------------------------------ view */
+
+ function init(root) {
+ var reduced = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
+ var base = root.getAttribute("data-base") || "/";
+ var NS = "http://www.w3.org/2000/svg", token = 0, current = null;
+
+ root.innerHTML = "";
+ var board = el("div", "irf-board");
+ var stackCol = el("div", "irf-stack");
+ stackCol.appendChild(el("div", "rcr-lbl", "The Redis Iris stack"));
+ var cards = {};
+ STACK.forEach(function (c, i) {
+ if (c.along && !STACK[i - 1].along) stackCol.appendChild(el("div", "irf-along", "Alongside every request"));
+ var card = el("div", "irf-comp irf-c-" + c.key + (c.along ? " is-along" : ""),
+ '' + esc(c.name) + '' + esc(c.role) + "" +
+ '');
+ cards[c.key] = card;
+ stackCol.appendChild(card);
+ });
+ stackCol.appendChild(el("div", "irf-base", "Redis"));
+
+ var askCol = el("div", "irf-asks");
+ askCol.appendChild(el("div", "rcr-lbl", "Questions Maya asks"));
+ var asks = {};
+ QUESTIONS.forEach(function (q) {
+ var b = el("button", "irf-ask", esc(q.text));
+ b.type = "button";
+ b.setAttribute("aria-pressed", "false");
+ b.addEventListener("click", function () { pick(q.key); });
+ asks[q.key] = b;
+ askCol.appendChild(b);
+ });
+
+ var over = document.createElementNS(NS, "svg");
+ over.setAttribute("class", "irf-over");
+ over.setAttribute("aria-hidden", "true");
+ board.appendChild(stackCol);
+ board.appendChild(askCol);
+ board.appendChild(over);
+ root.appendChild(board);
+
+ var detail = el("div", "rcr-card irf-detail");
+ detail.setAttribute("aria-live", "polite");
+ root.appendChild(detail);
+
+ /* A point on an element's edge, in the board's coordinates. */
+ function at(node, side) {
+ var b = board.getBoundingClientRect(), r = node.getBoundingClientRect();
+ var x = side === "left" ? r.left : side === "right" ? r.right : r.left + r.width / 2;
+ return [x - b.left, r.top + r.height / 2 - b.top];
+ }
+ /* Side by side, the dot runs from the question's left edge to the router's right edge, then down the
+ stack's right side to each component. Stacked (a narrow screen), there's no clear path, so no dot. */
+ function sideBySide() { return askCol.getBoundingClientRect().left >= stackCol.getBoundingClientRect().right - 1; }
+ function travel(d, tok) {
+ return new Promise(function (resolve) {
+ if (tok !== token) return resolve();
+ over.setAttribute("viewBox", "0 0 " + board.clientWidth + " " + board.clientHeight);
+ var path = document.createElementNS(NS, "path");
+ path.setAttribute("d", d);
+ path.setAttribute("class", "irf-trail");
+ var dot = document.createElementNS(NS, "g");
+ dot.innerHTML = '';
+ over.appendChild(path);
+ over.appendChild(dot);
+ var len = path.getTotalLength(), dur = Math.max(450, len * 2.6), start = null;
+ function frame(now) {
+ if (tok !== token) { path.remove(); dot.remove(); return resolve(); }
+ if (start === null) start = now;
+ var t = Math.min(1, (now - start) / dur), e = t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2;
+ var p = path.getPointAtLength(len * e);
+ dot.setAttribute("transform", "translate(" + p.x.toFixed(1) + " " + p.y.toFixed(1) + ")");
+ if (t < 1) requestAnimationFrame(frame);
+ else { dot.remove(); setTimeout(function () { path.remove(); }, 600); resolve(); }
+ }
+ requestAnimationFrame(frame);
+ });
+ }
+ function toRouter(q) {
+ var a = at(asks[q.key], "left"), r = at(cards.router, "right"), mx = (a[0] + r[0]) / 2;
+ return "M " + a[0] + " " + a[1] + " C " + mx + " " + a[1] + " " + mx + " " + r[1] + " " + r[0] + " " + r[1];
+ }
+ function down(key) {
+ var r = at(cards.router, "right"), t = at(cards[key], "right"), x = r[0] + 18;
+ return "M " + r[0] + " " + r[1] + " L " + x + " " + r[1] + " L " + x + " " + t[1] + " L " + t[0] + " " + t[1];
+ }
+
+ function light(key, tag) {
+ cards[key].classList.add("is-lit");
+ cards[key].querySelector(".irf-tag").textContent = tag || "";
+ }
+ function clear() {
+ Object.keys(cards).forEach(function (k) {
+ cards[k].classList.remove("is-lit");
+ cards[k].querySelector(".irf-tag").textContent = "";
+ });
+ Array.prototype.forEach.call(over.querySelectorAll("path, g"), function (n) { n.remove(); });
+ }
+
+ function showDetail(q) {
+ var rows = '
Intent RouterMatches ' + esc(q.intent) +
+ " (score " + q.score.toFixed(2) + ") and returns its target, " + esc(q.target) + ". No model call decides this.