From 08492beb7c67e9d4b93d5709415fcabce64d3356 Mon Sep 17 00:00:00 2001 From: mich-elle-luna Date: Tue, 27 Oct 2026 13:06:12 -0700 Subject: [PATCH 1/3] Update Iris concepts, landing page, and tour for the GA services Rework the Redis Iris concepts pages around the GA lineup. Agent Playbooks replaces semantic caching as the fastest path, and Context Gateway's exact-match result cache is the only cache. Intent Router, Lens, and Agent Mesh join the mental model, and the request-flow diagram gains an Intent Router step. Banners come off the concepts pages. Rework the interactive tour to use Agent Playbooks and Context Gateway, with the current names drawn over the published drawing's labels, and add Intent Router as a new disc and step between Maya and the agent. Update the Iris landing page: three tiles per row, new service icons, and copy that matches the GA services. Add icons for Agent Playbooks, Lens, Intent Router, and Agent Mesh. Intent Router and Agent Mesh links are TODOs until their branches merge. Co-Authored-By: Claude Opus 5.5 (1M context) --- content/develop/ai/context-engine/_index.md | 20 +- .../ai/context-engine/concepts/_index.md | 65 ++-- .../concepts/interactive-demo.md | 12 +- .../context-engine/concepts/request-flow.md | 94 +++-- layouts/shortcodes/iris-demo.html | 9 +- static/css/iris-demo.css | 13 +- .../icon-redis-agent-mesh-64-duotone.png | Bin 0 -> 6678 bytes .../icon-redis-intent-router-64-duotone.png | Bin 0 -> 5467 bytes .../dev/icons/icon-redis-lens-64-duotone.png | Bin 0 -> 6411 bytes .../icons/icon-redis-playbooks-64-duotone.png | Bin 0 -> 4086 bytes static/js/iris-demo.js | 334 +++++++++++++----- 11 files changed, 374 insertions(+), 173 deletions(-) create mode 100644 static/images/dev/icons/icon-redis-agent-mesh-64-duotone.png create mode 100644 static/images/dev/icons/icon-redis-intent-router-64-duotone.png create mode 100644 static/images/dev/icons/icon-redis-lens-64-duotone.png create mode 100644 static/images/dev/icons/icon-redis-playbooks-64-duotone.png diff --git a/content/develop/ai/context-engine/_index.md b/content/develop/ai/context-engine/_index.md index ef97b98428..c90a890d40 100644 --- a/content/develop/ai/context-engine/_index.md +++ b/content/develop/ai/context-engine/_index.md @@ -4,34 +4,35 @@ 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 AI agents memory, 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 -bannerText: Agent Memory and Context Retriever are currently available in preview. Features and behavior are subject to change. --- Give your AI agents the context layer they need to reliably act on business data. -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: persistent memory, published answers and procedures, governed data access, live data sync, and tracing, fully managed on Redis Cloud or self-managed on your own infrastructure. -
+ +
{{< 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" >}} - {{< tile-card icon="images/dev/icons/icon-redis-iris-64-duotone.png" title="Agent Playbooks" description="Publish reusable answers and procedures for agents" url="/develop/ai/context-engine/agent-playbook" >}} - {{< tile-card icon="images/dev/icons/icon-redis-iris-64-duotone.png" title="Lens" description="Debug agent answers by tracing the context behind each turn" url="/develop/ai/context-engine/lens" >}} + {{< tile-card icon="images/dev/icons/icon-redis-playbooks-64-duotone.png" title="Agent Playbooks" description="Publish reusable answers and procedures for agents" url="/develop/ai/context-engine/agent-playbook" >}} + {{< tile-card icon="images/dev/icons/icon-redis-context-retriever-64-duotone.png" title="Context Gateway" description="One Model Context Protocol (MCP) endpoint for the tools of any MCP server and your Redis data" url="/develop/ai/context-engine/context-gateway" >}} + {{< tile-card icon="images/dev/icons/icon-redis-lens-64-duotone.png" title="Lens" description="Debug agent answers by tracing the context behind each turn" url="/develop/ai/context-engine/lens" >}}
## What is Redis Iris? 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? @@ -41,7 +42,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
@@ -52,6 +53,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 4f4374b67b..98a7cf67de 100644 --- a/content/develop/ai/context-engine/concepts/_index.md +++ b/content/develop/ai/context-engine/concepts/_index.md @@ -9,12 +9,11 @@ hideListLinks: true linktitle: Concepts title: How Redis Iris works weight: 5 -bannerText: Agent Memory and Context Retriever are currently available in preview. Features and behavior are subject to change. --- -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. +Redis Iris sits between your agent and your data. It's one system. Your agent asks 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. -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. +Use Redis Iris when you want to route each message to the agent, tool, or workflow that handles it, answer recurring questions with approved answers instead of new 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. ![An agent connects to Redis Iris, which stores memories, caches answers, and retrieves context, all backed by Redis or a connected database.](/images/ai/context-engine/iris-mental-model.svg) {width="650"} @@ -23,45 +22,60 @@ Use Redis Iris when you want agents to respond with cached answers instead of re | 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. + +## Alongside the request -This write path runs asynchronously. It doesn't slow down the response your application is waiting for. +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 @@ -69,22 +83,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 824fcb3609..48274a0ce2 100644 --- a/content/develop/ai/context-engine/concepts/interactive-demo.md +++ b/content/develop/ai/context-engine/concepts/interactive-demo.md @@ -4,19 +4,18 @@ categories: - docs - develop - ai -description: Step through one agent request across Data Integration, LangCache, Agent Memory, and Context Retriever, then take a service away to see what the agent loses. +description: Step through one agent request across Data Integration, Intent Router, Agent Playbooks, Agent Memory, and Context Gateway, then take a service away to see what the agent loses. hideListLinks: true linktitle: Interactive demo title: Redis Iris interactive demo weight: 10 -bannerText: Agent Memory and Context Retriever are currently available in preview. Features and behavior are subject to change. 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. +Redis Iris gives an AI agent routing, governed access to live business data, memory, and approved answers. This tour follows one customer request through all five services, one step at a time. It starts where business data enters Iris, with Data Integration, and then follows the request through the other four 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. -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. +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. {{< iris-demo >}} @@ -25,9 +24,10 @@ The tour runs in your browser and doesn't connect to any service. Each step link | Service | What it does in this request | Without it | |:--|:--|:--| | [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 Retriever | Looks up delivery times, alternatives, and past orders through governed tools | The agent can't check whether the food will arrive in time | +| Intent Router | Matches the message to two intents: the reorder and the delivery fee question | A model call has to work out what the message needs first, which takes longer and costs output tokens | +| [Context Gateway](/content/develop/ai/context-engine/context-gateway/_index.md) | 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 writes the answer, which takes longer, costs output tokens, and isn't tied to the approved answer | ## 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 2d85fda2cd..49f93afc34 100644 --- a/content/develop/ai/context-engine/concepts/request-flow.md +++ b/content/develop/ai/context-engine/concepts/request-flow.md @@ -9,14 +9,17 @@ hideListLinks: true linktitle: Request flow title: How a request flows through Redis Iris weight: 6 -bannerText: Agent Memory and Context Retriever are currently available in preview. Features and behavior are subject to change. --- [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 Model Context Protocol (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. + +[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 the diagram leaves them out. + + 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. @@ -32,22 +35,29 @@ nodes: description: | The calling application or AI agent that sends a prompt. docsUrl: "/develop/ai/agent-builder" - langcache: - label: "Cache hit?" - type: decision + intentRouter: + label: "Intent Router" + type: process col: 1 + row: 1 + description: | + Intent Router: 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, and your application dispatches. + playbooks: + label: "Playbook match?" + type: decision + col: 2 row: 0 description: | - Cache: checks whether a similar prompt is already cached before calling the model. + Agent Playbooks: matches the task against published FAQs and skills. A skill match returns a preview, and the agent loads the full procedure before the model call. links: - demo: - label: "Interactive demo" - url: "/develop/ai/context-engine/langcache/interactive-demo" - docsUrl: "/develop/ai/context-engine/langcache" + concepts: + label: "Concepts" + url: "/develop/ai/context-engine/agent-playbook/concepts" + docsUrl: "/develop/ai/context-engine/agent-playbook" agentMemory: label: "Agent Memory" type: process - col: 1 + col: 2 row: 1 description: | Agent Memory: session and long-term recall. Recalls session history and long-term facts about the user or task. @@ -56,33 +66,37 @@ nodes: label: "Interactive demo" url: "/develop/ai/context-engine/agent-memory/interactive-demo" docsUrl: "/develop/ai/context-engine/agent-memory" - contextRetriever: + contextGateway: label: "Context Gateway" type: process - col: 1 + col: 2 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. + Context Gateway: tool calls through one endpoint. Calls the tools of MCP servers and Context Surfaces to fetch the business data the agent needs. For tools you opt in, the result cache returns the stored result of an identical call. + links: + resultCache: + label: "Result cache" + url: "/develop/ai/context-engine/context-gateway/concepts#result-cache" docsUrl: "/develop/ai/context-engine/context-gateway" dataIntegration: label: "Data Integration" type: process - col: 0 + col: 1 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" + publishedAnswer: + label: "Published answer" type: terminal - col: 2 + col: 3 row: 0 description: | - Return cached response: on a cache hit, Cache returns the stored response directly, skipping the model call. + Return the published answer: on a FAQ match, the agent returns the answer your business approved, skipping the model call. modelCall: label: "Model call" type: process - col: 2 + col: 3 row: 1 description: | The model generates a response using the retrieved context. @@ -102,23 +116,28 @@ nodes: response: label: "Response" type: terminal - col: 3 + col: 4 row: 1 description: | The final response returned to the caller. edges: - e1: + e0: from: agent - to: langcache + to: intentRouter + kind: normal + path: "faqMatch,memory,context" + e1: + from: intentRouter + to: playbooks kind: normal - path: cacheHit + path: faqMatch e2: - from: langcache - to: cachedResponse + from: playbooks + to: publishedAnswer kind: branch - path: cacheHit + path: faqMatch e3: - from: agent + from: intentRouter to: agentMemory kind: normal path: memory @@ -128,18 +147,18 @@ edges: kind: normal path: memory e5: - from: agent - to: contextRetriever + from: intentRouter + to: contextGateway kind: normal label: "MCP" path: context e6: from: dataIntegration - to: contextRetriever + to: contextGateway kind: normal path: context e7: - from: contextRetriever + from: contextGateway to: modelCall kind: normal path: context @@ -156,10 +175,10 @@ edges: label: "App writes session event" path: "memory,context" paths: - cacheHit: - label: "Cache hit: fastest" + faqMatch: + label: "FAQ match: 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. + Agent Playbooks finds a published FAQ that matches the task, and the agent returns its answer directly. No model call generates the answer, so this path is the fastest and adds no generation cost. memory: label: "Needs memory: recalls session or long-term info" description: | @@ -172,12 +191,13 @@ paths: ## 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 ad83003241..ff965fddcf 100644 --- a/layouts/shortcodes/iris-demo.html +++ b/layouts/shortcodes/iris-demo.html @@ -3,8 +3,8 @@ --------- 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 Retriever. Each - step links to that service's own demo for depth, and the last step + request is matched by Intent Router and draws on Agent Playbooks, Agent Memory, and Context Gateway. Each + step links to that service's docs or 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. @@ -30,13 +30,14 @@ {{- /* 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. */}} + Data Integration, Context Gateway, and Agent Playbooks 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. */}}
    + data-link-cache="{{ relref .Page "/develop/ai/context-engine/agent-playbook" }}">
    diff --git a/static/css/iris-demo.css b/static/css/iris-demo.css index b47d22a70b..ad47fff5ea 100644 --- a/static/css/iris-demo.css +++ b/static/css/iris-demo.css @@ -10,6 +10,7 @@ .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); } /* ---- the stage: the Iris architecture diagram from redis.io/iris ---- */ .iris-stagewrap { position: relative; margin-bottom: 12px; } @@ -26,10 +27,17 @@ .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; } .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; } +/* Intent Router's disc, dimmed like the drawing dims a service, with its label only while it's lit. */ +.iris-router-disc, .iris-router-label { transition: opacity .45s; } +.iris-router:not(.is-lit) .iris-router-disc { opacity: .4; } +.iris-router:not(.is-lit) .iris-router-label { opacity: 0; } /* 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; } @@ -178,6 +186,7 @@ .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%; } @@ -226,7 +235,7 @@ .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 { 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; } 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 0000000000000000000000000000000000000000..0d7b3edfe14f587abd04339684e6397582970538 GIT binary patch literal 6678 zcmV+x8tLVUP)*&=)- znLGF1nLBf5?s~rGx%0m{bLM^Tym#hqGxNT`-~Wg4-uIq4&vVXsmjCm-r=4bA=4D>y zWnSiGUgl+9=4D>yWnSiGUXi+l5Y_yy++OZO2l_fUuIrnXx+VbU59|B7_V4cNb7Wp< z&Qj?Sb_HLSoAb7S_l5 z?6Zy;y3Vm;@jB;MpN@3Ax9}p*iQ{LOvl-zlt#jK7gsOzPgbN7O2+5Inw%3wY!E@|L zGg{%VX02M9Wg9!fW9fUZNb7o=Nb7Q|pzjF{zNc$DVd;IB*Y@l%mwoE^)tM`ocCt^O zQqh!L@dckuxPdU0u$6F_aFno{@DAY)Ld~KJ|Kv5!Z@!pqpECA%`k-zity}xT1zs5F z8J2#XGOS}qY_h#P?#`^WYpR%%ESm6Z5=Ik_g_v*$;cY_waGveUu-72LcXBM8W3deE zT704F>QuuJtNPiGrB>}cd6^G zPYleC$cisq5j^R``X5bytWQV!cX?B~g$utt;Q@k8nn*vvO4x_({{+FOD8URurBKh# z{`&Ir|^TwB8+22Hl?kt0AsMF3vBZ*hPbx_j)vg9lL`6UFW0_3^)ug| zy}{C}VfLL7~o&Cdy0b65`&bgaf#D7onLF+-cxUU9-W`zl%SC zgb#PIT^cDt8&l#1g*r(x@c7RmA@04AaGc;GjFPQvvi6glYuEI%j(WscKqvNKrCd)q z-;{cOp+2Oze`^V6YTQ?m@GN$T9fU^t&i&6;*T|ST^^fBE^0ZQgrRN}5*u$BA8`u5{1ogsy}?VJ|$d z8h;sYP9J6Id!MI>+p{3QU1UrdE3!UXAzXWQ37^#>e3==-pPeoIxjDk`cF~`?^zSU; zv)hE{;C|tFXTGow?Hlfn!;#IFWm4D;nzt@Ts70u*tn62@vV)VvJHk4`OR%6nl#IWl z>e=u3aCrmkn4!OgB?!9L(&uhrn=ne`tourMjvf|%k4J=c`F!-bBO>#ynIa5#laIN1 zWzH)gp;shyB&;P&AtXyma6aJ&#l`buwR@DGuT)pRW7&csmR@)Ij0&Ib6&bHj7OtJY z2){F12)|zx=kn(`Mb@$fBJF;|Zs$o=iA%HKlL^fUZxc=u{Dh-~7Rpg@4dGkKCDdCz z-IdRkv%UEA2BWJ#eQ-~av+>)&)*i{r=g1^mVUp3~z>9=exza57B*M9bX9(2`AYNY#8QK z8!{4YqaQUo7CWJJX%l=^!oLZh5}X=>w-J^St|C+@?)T;H*?C6htf?ErG5#$5ItlOb zW6FZ}2Q2=ab*aKWex%5pJB<{!QoIe%t2F5x0V<&rM=285>w`ypsm@H+_)5h8xx zS)Un}lKx0YnB+T-JO9;~le=0&}$W1&;9I=SCU(JZ=P-N^kX9T$M1#h`KOGG#X`6!mJ?P9nY9`K^X-y!gx`#? zkl@e|d=_Cf;pS+c^C5qqF1MaTu!b%CQG)f-a`Up+d`Loe<_`+_eSx44 z9}Ao&=G;O+_#*}h_uk(E{}%`g7e1c92r01;e06Z|<`6a$_JUFK0bvk_U^Nt=69PA? zTepeOjS%;esPtj|Q!E3zhNS>(#PL~PXaa}??o9oRjJo@Tec~9A{mCkkyLm$Z&d)y* z_Ls*AOaIP!6-E5qq5T4(V1BOmWr*jqB3#a% zKqA>FC?%U@Nce-gi<}?6(_Q&Y^5E^xNr%BtS3MYWdhA19GAb3<|SW269;2-B7P!tEi(Ul*Kv?FlELIu^Wf>KNb*-|k<@ zB^;n*GjtZtb*Y6af#2&PzkipoPa0$N+z*0Dt~H{m0B50qBKUSpCK8GtN?=( zKmlHt6oBIq?L&+|iO@%>F)XkPxY)pB;qTcQ$mWIGu2~7PmM;$M92^vqj}7)1+`sP; zjzx3xc98Jm|7nQHl#8YUR70*=rXO|Ix%XVSigHD3o06me_&qqn`FEi$$tXO4dzZ0&DRgmOLsgN?LefzQ9GuiSf z(6HrLfE(4i$&x$S6al5=VhWXAyDEe;nH>ZlUk?olZ%PV~LFlR6!_TO!q4+~W9R+c% z5boTzZ`1-m8VbNJ-f#pQR2Rex_?4f3EVo{vzn?+)0yX=iv161gg3zxej6!X|mxP}P zTS8zP;e_G^98m7zXAF@GOr+y)9+WvzgwKJhSuu~V;9MLXA`c+1d=~x%N-(LjRFaj8 z>YW}&5*Q?yvp^YJ2Vr_NNF>F@5oR9gcm+!fDKd$r@L3`2#4BFTetsvyH!ucWSp6RO z{D)C&Sx>{0Sg&DJ@n@D1Zc>{wK=4c%y9FyfCkjGWQ`4z@WN7@}=b*J*Ja>Xz1bphm z)20f#Ni`4#ei8|kv8cGMrzJ?7r=76sJsQUdr<0p1!mkJh@KFt}U@)D`WYJxOtq}qr z1&?Vlk+2iY(>MlW0VnLOl12K|n8f;o5l9|+q-|}C%xOpXb{HXDL+D%yG?Q>Q;RGXUMfKB4!-ZCE>Za@w1EgX_P$#J{~_J+?=o#Kf#q#gxutEixzP*e2yayB)kGK6+CE!>~!j9G$yTWX& z;sbDfeM&t)dr*7O!)P`>(+JNfId%S#Q~>&H9l}OQ_?)*Nr^ghAC(;lt;G==_vCWq+ zfm5iUyd6$u5P&(tGyKz*QJviir$);_jbZu|PR!A{_jf(LPmu4?QZfvP%@B)Gy5 z<|^qM!!fE$oB|+AcNb~^vlV~e!aP1@42Q~><4LlEfEit;gi{5(ncHIYbCJAWNQ3%IpWz#fgLo4gj|vg>EK>or72x6sC_p2WoU4}j>_Q@sTAfPTM?e8mki-i9+-fSosR>&m8Axn2 z-5&u3;NZ(AiZEFiJwlA78Tt{*pJDb>patRwX@p}SkJJh=4i4k?f)ZHp=RmFGlcE65VDAGR1z;oezltlkw6L=HvZMeP zDILH0tlqAqfn^0~0*i}aX zJctI$q3vvV21%v@oC*Vvmv#XytP8qiI^||Ddrb9|oY_3j1k=JWJ~wNOKO6Cm z%Z+9R%bWs8E9hFh_h-Lymi}{Yf+{svqc9^Ey$g;<7=LNBjb$6)d)WX5E#0EHdkYX< zJTuI>D0>Q^gmDKdc0sD#KM>V9@hZ{sgA~$AF(98)jH?t$F7IJFBURRL}#JVAJd@G4WFiupYDs|PDhrCsO+&>-H$A@uz<@#?yCMM)PG z_q_(lAWkN{fM{Z$SiLKuID;++idT6EFDU}gJRN=gJc?Ja9xsZj7>D1p0^<8ilrnWb zrhewt8^P}>k2x*7G<1Y#mnjyuA8!lgBjG-@eXSXdp2HA8HH3#-f%SVxp{@B4uxgR0 zWoyxnl9fZ0t}42TmXELyIHfvAIcw#rp}nA9ErstLNi0SP_ZM&kw0aY871K456h1hJ zm7_JMkraH8G0P?1%h9=F1T5yN5mbVj$Q~Zlp^wJ{8KAXY5GmoK2h-)Kc$$N*DLkhm z@h+n(i6VUF=Z^wO)2-t(v-@;uq_h`S_qAvP>Lq;7JdqvM&}p^p6rQqgGjK6u0YYn2k7+WL;%JW1vo(vNriRZCeo!^$aF3@ z5Z&~l0OqoI3BNMl#GR{AH)jV)LLD@xP%n;>fyRND`Y;6f9KJ3JXoi+CXGWtV3F;jd zY6#y0>UoZa5~ONzA!4BbH={4DoE2kc&OkytG=N&5v`q2BMo5^h9tmbkbx{&dZqy(2 z#hi9S2s%qqj%Q*$(25ErNosUrl%w<*eO>t|kez_O156A0Sa)SumHx`Yw zf^+&~sAk>K7%YcHcPQ_tyCZ~=__acxx-$X1mfGl0Rv$)Ja-7H12-(Hpoc^u%(nn-L ze^Ksj8Oy<*tGqAZ*HG`LL)~7&w2Mc+U>1~k2da!OiSwAQRF2fOdVPTqYniQd3%xs* zgAMnE5fhacMm!ql$*u;W2glEDRr5HHskziPm+`sYoI(Mn#Zv*QWYb!@8;_kN`u z0r!{+P+&}r(cVUGK5<0|_j{{y1m7IX!B#>bLiY37fH_+un)Rsi0%oRjpC`F+m16zc zxI|C@xUOd?2J=eA#ap5ttr+_R#r@umfq$yD zZ`mpvTMC8R0{%dgXt#ZhhXNo?o`S~H`;f=jCU+u-i>LK6P9M6V_%!BpgS?~0S_#fZ zlEP&$6x>?&X;FRir;^cQ2F%FfP^l740l;Ut8+10OQVRc&!f|9ObW|aY2o#BKMAwq)A)>9#@b0CzF{kgq0Q`3(Q|ysehRQh04EMN2j=vWgMQPRN z9|JG0NhA~@nhJn(elUn5ZjBW3esBou8RZVZ>#C8kLXY68Nddd|uu@7b_X^}Qvvx5R zpbB=U73jvz8MupzId(1y>xd5NZVe^2z~H^KOmq7To|_kgVUwnz1Wu4a+ZKm}fnA^) zyqVLBN>%}B^D4Z7+^S*E@HxK*?>VY!N;td~bql(DWNRowI;hu|#fcT11lsmk>{zPc zT{s5%7I()g>xxhdllZg7vEYM|^`em$a1k6+brh;@)7kF-#5v#4-0mY6L}`T8c1UNn zF&%qAVT~7Hq&7x$?QxWo^*}bRMj_*`G-t5x9ro!Vu@}hbYBX zeQV|b{0oJo`;=%~Frl&>@d~Z-uS;QKh zqL=E=nd@+u@?D*!u4O))^OF&qFa;k+B`6PLYZBfXnl8Us0ui;TijbX%w?BvIy5n1-nh819pg_0B zH-%qHl^{r7eF;sTcY&_H8Lxr6g%!n|sj{IARqz5zF3YZI&WL%LmwB0&d6}1anU{H) gmwB0&d6lv2|42+qlBIdjCIA2c07*qoM6N<$f)LiecmMzZ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..b6c950eb30cc5de9aa3a054c9c02906b220515d8 GIT binary patch literal 5467 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 PXbLbunBK3|agF&8_n|uw literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..b9087287fe872e3ada90a647e917c9a9a15c72cf GIT binary patch literal 6411 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 Z{{Xqi-&{jJF;4&h002ovPDHLkV1kW(47dOQ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..981464bf6088b5c43de37d258e71d8c319f81e44 GIT binary patch literal 4086 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 F{{hK^RqOx& literal 0 HcmV?d00001 diff --git a/static/js/iris-demo.js b/static/js/iris-demo.js index d588e8adbf..8184f55000 100644 --- a/static/js/iris-demo.js +++ b/static/js/iris-demo.js @@ -8,19 +8,26 @@ data enters through Data Integration (a change in PostgreSQL reaches Redis), 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 Retriever 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. 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, agent-memory-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. + + 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). Customers, restaurants, orders, and Maya's memories come from the same sample data as those demos. Keep them in sync. @@ -60,22 +67,33 @@ { 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; + 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 assistant's Router matches Maya's message to two of its intents, best first. Each target tells the app what to + dispatch: the food assistant for the reorder, and 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. */ var SERVICES = [ { key: "rdi", name: "Data Integration", role: "Brings business data in from PostgreSQL and keeps it in sync" }, - { key: "retriever", name: "Context Retriever", role: "Gives the agent governed tools to read that data" }, + { 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 Retriever", rdi: "Data Integration", model: "Model" }; + var NAMES = { router: "Intent Router", cache: "Agent Playbooks", memory: "Agent Memory", retriever: "Context Gateway", rdi: "Data Integration", 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], answer: 1620, reply: 1480 }; + var TOKENS = { plan: 38, answer: 61 }; /* --------------------------------------------------------------- helpers */ @@ -119,16 +137,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 +171,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 Retriever results carry a _retrieval stamp, with fields sorted (as the Context Retriever demo returns them). */ + /* Context Surface results carry a _retrieval stamp, with fields sorted (as the Context Retriever 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) }; @@ -176,7 +202,7 @@ 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 Retriever reads.", + code("restaurant:r209") + ", through a job file that uses the key template the Context Surface reads.", record: { opcode: "u", db: "fooddelivery", schema: "public", table: "restaurants", key: { id: "r209" }, after: lotusRow } } : { on: false, at: SYNC_AT, @@ -185,12 +211,23 @@ 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 assistant's intents. Two match: " + code("reorder") + + ", which goes to the food assistant, and " + code("delivery_fee") + ", which 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 has to answer the general question too." }); 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 }); @@ -217,7 +254,7 @@ 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 Retriever 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." + (on.rdi ? " Data Integration still keeps Redis up to date, but nothing reads it." : "") }); /* The reply, as segments credited to their source. */ var R = []; @@ -245,7 +282,7 @@ /* 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." }); + text: "A separate model call writes the delivery fee answer that the published FAQ would have supplied." }); 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." }); @@ -255,11 +292,14 @@ 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" }, @@ -271,7 +311,7 @@ text: on.memory ? "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" } + text: on.cache ? "From a published FAQ in " + MS.cache + " ms" : "From the model: " + seconds(MS.answer) + " and " + TOKENS.answer + " output tokens" } ]; return { on: on, background: background, steps: steps, reply: R, replyText: reply, checks: checks, ms: ms, tokens: tokens, alt: alt }; } @@ -282,6 +322,11 @@ supplied, with and without it (both are excerpts of trace() replies, which the tests check), and the scorecard check it moves. */ var WHAT_IF = { + 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." }, 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?", @@ -289,7 +334,7 @@ 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 Retriever, 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 in Redis is fresh, 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?", @@ -297,7 +342,7 @@ 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." } + why: "Without Agent Playbooks, a second model call writes the fee answer: " + seconds(MS.answer) + " and " + TOKENS.answer + " output tokens that the published FAQ would have saved. The wording matches here, but nothing ties it to 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. */ @@ -307,21 +352,24 @@ 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.", + { key: "router", at: "18:24", title: "Intent Router matches the message", lit: ["router", "agent"], + text: "Before the agent acts, Intent Router matches Maya's message against the intents the app defined. Two match, best first: reorder, which sends the request to the food assistant, and delivery_fee, which points the fee question at the support playbook. Matching takes " + MS.route + " ms, without a model call.", + adds: [{ src: "router", t: "reorder → food assistant" }, { 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 Retriever checks live data", lit: ["retriever", "redis", "agent"], - text: "Context Retriever 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.", + { key: "retriever", at: "18:24", title: "Context Gateway checks live data", lit: ["retriever", "redis", "agent"], + text: "Context Gateway gives the agent governed tools, through one Model Context Protocol (MCP) endpoint, 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.", 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: "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 Retriever", memory: "Agent Memory", cache: "LangCache" }; + var DEPTH = { rdi: "Data Integration", router: "Intent Router", retriever: "Context Gateway", memory: "Agent Memory", cache: "Agent Playbooks" }; /* ---- the PostgreSQL sample data, shown when the reader selects POSTGRES ---- The same rows as the RDI demo (rdi-demo.js), on the evening of this tour: @@ -380,7 +428,7 @@ /* ---- 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 Retriever reads ({table}:{id}), and the customers job leaves out email, as in the RDI demo. */ + the Context Surface reads ({table}:{id}), and the customers job leaves 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]; })), @@ -391,7 +439,7 @@ 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 Retriever's filter_order tool finds them by customer_id." + 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. */ @@ -426,48 +474,101 @@ 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, and router2 are added with the figures (FIGURES), 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"] }; + memory: [773, 307, 36], agent: [651, 385, 36], cache: [526, 469, 36] }; // maya and router: 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"]] }, + router: { layers: ["rdi", "retriever", "cache", "memory"], blend: "darken", groups: [[["maya"]], [["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"]], [["router2"]], [["maya2"]]] } // no tag: the step card shows the reply }; - 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 Retriever'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 + var STEP_OF = { rdi: 0, redis: 0, maya: 1, router: 2, cache: 3, memory: 4, retriever: 5, agent: 6 }; + /* ---- Intent Router and Maya, the user, drawn in the diagram's own style ---- + The drawing has neither, so the stage adds both on the empty diagonal down and to the right of the + agent: Intent Router first, because it matches Maya's message before the agent acts on it, then Maya. + Each is the agent's own disc, scaled about the agent's face centre (C0) and moved along that diagonal + (STEP, in multiples of it): Intent Router in the drawing's service colours with its icon, Maya with a + person where the agent has a robot. Each has a label box below it, as the agent does; Intent Router's + shows only while it's 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. */ + 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 + bottom: 37.6, // from C0 down to the disc's lowest point, at full size + reach: 44 // from a disc's centre to where its links start, along STEP, at full size + }; + var FIG = { + router: { at: 0.8, s: 0.7, label: "INTENT ROUTER", labelW: 81.4, labelDx: -25 }, // left, clear of its link to Maya + maya: { at: 1.6, s: 0.8, label: "MAYA", labelW: 38.3 } }; + Object.keys(FIG).forEach(function (k) { var f = FIG[k]; f.c = [C0[0] + STEP[0] * f.at, C0[1] + STEP[1] * f.at]; }); + /* A point `a` along the diagonal from C0, and `o` across it (down and to the left). */ + function diag(a, o) { + var ux = STEP[0] / STEP_LEN, uy = STEP[1] / STEP_LEN; + return [C0[0] + ux * a - uy * o, C0[1] + uy * a + ux * o]; + } + /* A link's two lines: `in` runs toward the agent, `out` away from it. */ + function link(near, far) { + var a = near.at * STEP_LEN + near.s * DISC.reach, b = far.at * STEP_LEN - far.s * DISC.reach; + return { in: [diag(b, 9), diag(a, 9)], out: [diag(a, 0), diag(b, 0)] }; + } + var LINKS = { router: link({ at: 0, s: 1 }, FIG.router), maya: link(FIG.router, FIG.maya) }; + 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); + HITS.router = [FIG.router.c[0], FIG.router.c[1], 26]; HITS.maya = [FIG.maya.c[0], FIG.maya.c[1], 30]; + TAG_SPOTS.router = [790, 528, "r"]; // below its label, clear of Maya + /* 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 = '' + + '' + + '' + + '' + + ''; - var MISSING_ROUTES = { rdi: ["pg", "rdi"], retriever: ["dbcr", "cragent", "cragent2"], memory: ["amdb", "amagent", "amagent2"], cache: ["dblc", "lcagent", "lcagent2"] }; + /* ---- 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", "rdi"] } + }; + 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; + } + + var MISSING_ROUTES = { router: [], 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: @@ -527,19 +628,50 @@ 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 buildMaya(NS) { - var g = document.createElementNS(NS, "g"), L = MAYA.label; + /* 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 mid(seg) { return ''; } + function linkG(k) { + var L = LINKS[k]; + return '' + + arrow(L.in) + arrow(L.out) + '' + mid(L.in) + mid(L.out) + ""; + } + function disc(f, colors, icon, iconStroke) { + var c = f.c, s = f.s, I = DISC.icon; + return '' + + '' + + '' + + '' + icon + ""; + } + function badge(f) { + var s = f.s, c = f.c; + return '' + + ''; + } + function label(f, cls) { + var x = f.c[0] + (f.labelDx || 0) - f.labelW / 2, y = f.c[1] + f.s * DISC.bottom + 5; + return '' + + '' + f.label + ""; + } + return { + links: linkG("router") + linkG("maya"), + router: '' + disc(FIG.router, ["#3A2227", "#FF4438", "#89302E"], '' + ROUTER_ICON + "", 0.04) + badge(FIG.router) + "" + label(FIG.router, "iris-router-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 = '' + - MAYA.arrows.map(function (d) { return ''; }).join("") + - MAYA.dots.map(function (c) { return ''; }).join("") + "" + - '' + - '' + - '' + - '' + - '' + - '' + - 'MAYA'; + g.innerHTML = m.links + '' + m.router + "" + m.maya; return g; } @@ -690,9 +822,11 @@ 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 relabels = buildRelabels(NS); + Object.keys(relabels).forEach(function (k) { over.insertBefore(relabels[k], 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 @@ -746,6 +880,11 @@ next.el.classList.add("is-current"); cur = next; if (gear) gear.setLit(keys.indexOf("rdi") >= 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; } @@ -804,7 +943,9 @@ 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"); + mayaLink.classList.toggle("is-lit", stepKey === "ask" || stepKey === "router" || stepKey === "reply" || stepKey === "whatif"); + routerLink.classList.toggle("is-lit", stepKey === "router" || stepKey === "reply" || stepKey === "whatif"); + router.classList.toggle("is-lit", stepKey === "router" || stepKey === "reply" || (stepKey === "whatif" && missing !== "router")); setTag(null); Array.prototype.forEach.call(packets.childNodes, function (n) { n.remove(); }); var flow = stepKey === "whatif" ? null : FLOWS[stepKey]; @@ -831,10 +972,10 @@ /* ------------------------------------------------------------------ view */ function init(root) { - var links = { rdi: root.getAttribute("data-link-rdi"), retriever: root.getAttribute("data-link-retriever"), + var links = { rdi: root.getAttribute("data-link-rdi"), router: root.getAttribute("data-link-router"), 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 full = trace({ rdi: true, router: true, retriever: true, memory: true, cache: true }); var reduced = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches; function go(i) { @@ -855,9 +996,14 @@ '
    Redisrestaurant:r209 avg_delivery_min: 45
    '; } else if (step.key === "ask") { f.appendChild(el("div", "rcr-msg rcr-user iris-askmsg", esc(MESSAGE))); + } else if (step.key === "router") { + f.innerHTML = INTENTS.map(function (it) { + return '
    ' + esc(it.name) + 'score ' + it.score.toFixed(2) + + '→ ' + esc(it.target.type + " " + it.target.ref) + "
    "; + }).join(""); } else if (step.key === "cache") { f.innerHTML = '
    Asked' + esc("“" + QUESTION + "”") + "
    " + - '
    Matched' + esc("“" + FEE_PROMPT + "”") + ' similarity ' + FEE_SIM.toFixed(2) + " · " + MS.cache + " ms
    " + + '
    FAQ' + esc("“" + FEE_PROMPT + "”") + ' score ' + FEE_SIM.toFixed(2) + " · version " + FEE_VERSION + " · " + MS.cache + " ms
    " + '
    Answer' + esc(FEE) + "
    "; } else if (step.key === "memory") { f.innerHTML = MEMORIES.slice(0, 3).map(function (m) { @@ -869,7 +1015,7 @@ ["filter_restaurant", "austin · thai · up to " + WITHIN + " minutes", "Bangkok Street Kitchen · 30 minutes"], ["filter_order", "u101 · r201", "pad thai with tofu · 2 orders"] ].map(function (r) { - return '
    ' + r[0] + '' + esc(r[1]) + '→ ' + esc(r[2]) + "
    "; + return '
    ' + GATEWAY_NS + "." + r[0] + '' + esc(r[1]) + '→ ' + esc(r[2]) + "
    "; }).join(""); } else if (step.key === "reply") { f.appendChild(replyBubble(full.reply)); @@ -878,7 +1024,7 @@ } return f; } - var SOURCES = { rdi: "Data Integration", retriever: "Context Retriever", memory: "Agent Memory", cache: "LangCache" }; + var SOURCES = { rdi: "Data Integration", 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) { @@ -934,7 +1080,7 @@ function whatIf() { var box = el("div", "iris-whatif"); var picks = el("div", "iris-picks"); - ["rdi", "retriever", "memory", "cache"].forEach(function (k) { + ["rdi", "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 +1089,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 = { rdi: true, 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. */ @@ -1113,5 +1259,5 @@ 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, ROUTES: ROUTES, HITS: HITS, trace: trace, STEPS: STEPS, WHAT_IF: WHAT_IF, SERVICES: SERVICES, MEMORIES: MEMORIES, MESSAGE: MESSAGE, FEE: FEE, RELABEL: RELABEL }; })(); From 67d6a3d542dc183bbfd5cef1353247efcfa8ee5f Mon Sep 17 00:00:00 2001 From: mich-elle-luna Date: Wed, 28 Oct 2026 17:58:29 -0700 Subject: [PATCH 2/3] Rework the Iris request flow, tour, and concepts diagram Replace the request-flow diagram with an interactive that shows the Iris stack beside questions from the tour's story: selecting a question sends a dot through Intent Router to the components that answer it, so the stack reads as options a request takes, not a fixed sequence. Remove Data Integration from the request flow and the tour. The tour's restaurant data is current in Redis, its stage is cropped to the part of the drawing it uses, and the Agent Playbooks disc shows the Playbooks icon. Add a preview mode to the tour and use it at the top of the Iris landing page as a link to the demo. Redraw the concepts diagram in the same side-by-side layout, with Intent Router, Lens, and Agent Mesh. Move each concepts page's diagram or interactive to the top, and update the landing and concepts page copy. Co-Authored-By: Claude Opus 5.5 (1M context) --- content/develop/ai/context-engine/_index.md | 6 +- .../ai/context-engine/concepts/_index.md | 10 +- .../concepts/interactive-demo.md | 9 +- .../context-engine/concepts/request-flow.md | 180 +------- layouts/shortcodes/iris-demo.html | 21 +- layouts/shortcodes/iris-request-flow.html | 28 ++ static/css/iris-demo.css | 41 +- static/css/iris-request-flow.css | 64 +++ .../ai/context-engine/iris-mental-model.svg | 78 +++- static/js/iris-demo.js | 422 ++++++------------ static/js/iris-request-flow.js | 240 ++++++++++ 11 files changed, 608 insertions(+), 491 deletions(-) create mode 100644 layouts/shortcodes/iris-request-flow.html create mode 100644 static/css/iris-request-flow.css create mode 100644 static/js/iris-request-flow.js diff --git a/content/develop/ai/context-engine/_index.md b/content/develop/ai/context-engine/_index.md index c90a890d40..c9ba0385ba 100644 --- a/content/develop/ai/context-engine/_index.md +++ b/content/develop/ai/context-engine/_index.md @@ -4,16 +4,16 @@ categories: - docs - develop - ai -description: Redis Iris is a suite of managed and self-managed services that give AI agents memory, approved answers, governed access to business data, and turn-by-turn tracing. +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, published answers and procedures, governed data access, live data sync, and tracing, fully managed on Redis Cloud or self-managed on your own infrastructure. +Redis Iris eliminates the infrastructure burden of building context-aware AI agents.
    diff --git a/content/develop/ai/context-engine/concepts/_index.md b/content/develop/ai/context-engine/concepts/_index.md index 98a7cf67de..5b608670af 100644 --- a/content/develop/ai/context-engine/concepts/_index.md +++ b/content/develop/ai/context-engine/concepts/_index.md @@ -4,19 +4,17 @@ 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 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. +![The Redis Iris stack beside what an agent needs. Every message goes to Intent Router first. Approved answers come from Agent Playbooks, earlier conversations from Agent Memory, and business data and other tools through Context Gateway. Lens and Agent Mesh work alongside every request.](/images/ai/context-engine/iris-mental-model.svg) +{width="800"} -Use Redis Iris when you want to route each message to the agent, tool, or workflow that handles it, answer recurring questions with approved answers instead of new 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. - -![An agent connects to Redis Iris, which stores memories, caches answers, and retrieves context, all backed by Redis or a connected database.](/images/ai/context-engine/iris-mental-model.svg) -{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 diff --git a/content/develop/ai/context-engine/concepts/interactive-demo.md b/content/develop/ai/context-engine/concepts/interactive-demo.md index 48274a0ce2..df976050d0 100644 --- a/content/develop/ai/context-engine/concepts/interactive-demo.md +++ b/content/develop/ai/context-engine/concepts/interactive-demo.md @@ -4,7 +4,7 @@ categories: - docs - develop - ai -description: Step through one agent request across Data Integration, Intent Router, Agent Playbooks, Agent Memory, and Context Gateway, then take a service away to see what the agent loses. +description: Step through one agent request across Intent Router, Agent Playbooks, Agent Memory, and Context Gateway, then take a service away to see what the agent loses. hideListLinks: true linktitle: Interactive demo title: Redis Iris interactive demo @@ -13,17 +13,16 @@ aliases: - /develop/ai/context-engine/interactive-demo/ --- -Redis Iris gives an AI agent routing, governed access to live business data, memory, and approved answers. This tour follows one customer request through all five services, one step at a time. It starts where business data enters Iris, with Data Integration, and then follows the request through the other four 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 docs or its own interactive demo, which covers it in depth. 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). 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 | Service | What it does in this request | Without it | |:--|:--|:--| -| [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 | | Intent Router | Matches the message to two intents: the reorder and the delivery fee question | A model call has to work out what the message needs first, which takes longer and costs output tokens | | [Context Gateway](/content/develop/ai/context-engine/context-gateway/_index.md) | 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 | diff --git a/content/develop/ai/context-engine/concepts/request-flow.md b/content/develop/ai/context-engine/concepts/request-flow.md index 49f93afc34..4659e64d3b 100644 --- a/content/develop/ai/context-engine/concepts/request-flow.md +++ b/content/develop/ai/context-engine/concepts/request-flow.md @@ -4,191 +4,29 @@ 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. 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: 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 Model Context Protocol (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. + +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. -[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 the diagram leaves them out. +[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. -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-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" - intentRouter: - label: "Intent Router" - type: process - col: 1 - row: 1 - description: | - Intent Router: 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, and your application dispatches. - playbooks: - label: "Playbook match?" - type: decision - col: 2 - row: 0 - description: | - Agent Playbooks: matches the task against published FAQs and skills. A skill match returns a preview, and the agent loads the full procedure before the model call. - links: - concepts: - label: "Concepts" - url: "/develop/ai/context-engine/agent-playbook/concepts" - docsUrl: "/develop/ai/context-engine/agent-playbook" - agentMemory: - label: "Agent Memory" - type: process - col: 2 - 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" - contextGateway: - label: "Context Gateway" - type: process - col: 2 - 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. For tools you opt in, the result cache returns the stored result of an identical call. - links: - resultCache: - label: "Result cache" - url: "/develop/ai/context-engine/context-gateway/concepts#result-cache" - docsUrl: "/develop/ai/context-engine/context-gateway" - dataIntegration: - label: "Data Integration" - type: process - col: 1 - 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" - publishedAnswer: - label: "Published answer" - type: terminal - col: 3 - row: 0 - description: | - Return the published answer: on a FAQ match, the agent returns the answer your business approved, skipping the model call. - modelCall: - label: "Model call" - type: process - col: 3 - 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: 4 - row: 1 - description: | - The final response returned to the caller. -edges: - e0: - from: agent - to: intentRouter - kind: normal - path: "faqMatch,memory,context" - e1: - from: intentRouter - to: playbooks - kind: normal - path: faqMatch - e2: - from: playbooks - to: publishedAnswer - kind: branch - path: faqMatch - e3: - from: intentRouter - to: agentMemory - kind: normal - path: memory - e4: - from: agentMemory - to: modelCall - kind: normal - path: memory - e5: - from: intentRouter - to: contextGateway - kind: normal - label: "MCP" - path: context - e6: - from: dataIntegration - to: contextGateway - kind: normal - path: context - e7: - from: contextGateway - 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: - faqMatch: - label: "FAQ match: fastest" - description: | - Agent Playbooks finds a published FAQ that matches the task, and the agent returns its answer directly. No model call generates the answer, so this path is the fastest and adds no generation 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. -``` - ## 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 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. diff --git a/layouts/shortcodes/iris-demo.html b/layouts/shortcodes/iris-demo.html index ff965fddcf..409f30337e 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 is matched by Intent Router and draws on Agent Playbooks, Agent Memory, and Context Gateway. Each + a time: a single agent request is matched by Intent Router and draws on Agent Playbooks, Agent Memory, and Context Gateway. Each step links to that service's docs or 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,14 +33,20 @@ {{- /* Links to each service's own demo, resolved here so they carry the site's base path. - Data Integration, Context Gateway, and Agent Playbooks have no tour-sized demo, so their links go to the overview. */}} + Context Gateway and Agent Playbooks 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. */}} -
    +{{- end }} +
    +{{- 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 ad47fff5ea..dace69b4e8 100644 --- a/static/css/iris-demo.css +++ b/static/css/iris-demo.css @@ -9,12 +9,14 @@ .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); } /* ---- 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; } @@ -31,6 +33,10 @@ /* 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; } @@ -38,16 +44,8 @@ .iris-router-disc, .iris-router-label { transition: opacity .45s; } .iris-router:not(.is-lit) .iris-router-disc { opacity: .4; } .iris-router:not(.is-lit) .iris-router-label { opacity: 0; } -/* 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; } +/* 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); @@ -63,10 +61,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); } } @@ -182,7 +177,6 @@ .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; } @@ -235,8 +229,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, .iris-relabel, .iris-router-disc, .iris-router-label { transition: none; } + .iris-artset, .iris-tag, .iris-maya-link, .iris-maya-link circle, .iris-relabel, .iris-router-disc, .iris-router-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 @@ -

    Redis Iris

    Store memories

    Cache answers

    Retrieve context

    Agent

    Reach Redis or any connected database

    \ No newline at end of file + + The Redis Iris stack on the left, and what an agent needs on the right. Every message is matched to an intent by Intent Router first. An answer the business already approved comes from Agent Playbooks. Something from an earlier conversation comes from Agent Memory. Live business data or another system's tools come through Context Gateway. Lens and Agent Mesh work alongside every request. + + + + + + + + THE REDIS IRIS STACK + WHAT THE AGENT NEEDS + + + + + Intent Router + Matches the message to an intent you defined + + + + Agent Playbooks + Published answers and procedures + + + + Agent Memory + Session and long-term recall + + + + Context Gateway + Governed tools over your data and MCP servers + + Alongside every request + + + + Lens + Traces each turn + + + + Agent Mesh + Keeps the reply stream resumable + + + + Every message is matched to an intent first + + + + + An answer your business already approved + + + + + Something from an earlier conversation + + + + + Live business data or another system's tools + + + diff --git a/static/js/iris-demo.js b/static/js/iris-demo.js index 8184f55000..7fddc3671e 100644 --- a/static/js/iris-demo.js +++ b/static/js/iris-demo.js @@ -5,8 +5,7 @@ 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 Intent Router matches it to intents, Agent Playbooks, Agent Memory, and Context Gateway each add one part of the answer, and the model writes @@ -17,7 +16,7 @@ trace() models every combination of services: the request's steps and API shapes (matching the Intent Router match API, the Agent Playbooks discovery API, the Context - Gateway MCP endpoint, agent-memory-demo.js, and rdi-demo.js), the reply + Gateway MCP endpoint, and agent-memory-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, @@ -27,7 +26,8 @@ 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). + 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. @@ -43,7 +43,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?"; @@ -51,7 +51,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" }; @@ -82,15 +82,14 @@ 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: "Agent Playbooks", role: "Answers recurring questions with answers your business approved" } ]; - var NAMES = { router: "Intent Router", cache: "Agent Playbooks", 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 = { route: 12, plan: 1100, cache: 41, memory: 190, tool: [74, 88, 81], answer: 1620, reply: 1480 }; var TOKENS = { plan: 38, answer: 61 }; @@ -187,30 +186,15 @@ /* ------------------------------------------------------------ the model */ - /* Everything that happens for one combination of services: the change - Data Integration syncs beforehand, the steps of the request, the reply + /* Everything that happens for one combination of services: 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; var lotusRow = clone(LOTUS); - lotusRow.avg_delivery_min = lotusMin; + lotusRow.avg_delivery_min = BUSY; function add(s) { steps.push(s); 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 the Context Surface 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 assistant's intents. Two match: " + code("reorder") + @@ -239,11 +223,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.", @@ -254,7 +237,7 @@ 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 = []; @@ -263,15 +246,12 @@ 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"); } 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 { @@ -301,8 +281,8 @@ 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" }, @@ -313,7 +293,7 @@ state: on.cache ? "ok" : "slow", text: on.cache ? "From a published FAQ in " + MS.cache + " ms" : "From the model: " + seconds(MS.answer) + " and " + TOKENS.answer + " output tokens" } ]; - 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, tokens: tokens, alt: alt }; } /* ------------------------------------------------------------- the tour */ @@ -327,14 +307,10 @@ 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." }, - 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." }, 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?", @@ -347,11 +323,8 @@ /* 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." }, + 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: "Before the agent acts, Intent Router matches Maya's message against the intents the app defined. Two match, best first: reorder, which sends the request to the food assistant, and delivery_fee, which points the fee question at the support playbook. Matching takes " + MS.route + " ms, without a model call.", adds: [{ src: "router", t: "reorder → food assistant" }, { src: "router", t: "delivery_fee → support playbook" }] }, @@ -362,19 +335,20 @@ 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, through one Model Context Protocol (MCP) endpoint, 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: "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", router: "Intent Router", retriever: "Context Gateway", memory: "Agent Memory", cache: "Agent Playbooks" }; + var DEPTH = { router: "Intent Router", retriever: "Context Gateway", memory: "Agent Memory", cache: "Agent Playbooks" }; - /* ---- 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: [ @@ -413,22 +387,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 - the Context Surface 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]; })), @@ -437,23 +398,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.", + 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 @@ -462,10 +420,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", @@ -479,23 +442,22 @@ }; /* 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], + var HITS = { redis: [413, 230, 44], retriever: [540, 318, 36], memory: [773, 307, 36], agent: [651, 385, 36], cache: [526, 469, 36] }; // maya and router: 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 - router: { layers: ["rdi", "retriever", "cache", "memory"], blend: "darken", groups: [[["maya"]], [["router"]]], tags: [[0, "router", "2 intents · 12 ms"]] }, + ask: { layers: ["retriever", "cache", "memory"], blend: "darken", groups: [[["maya"]]] }, // no tag: the step card shows her message + router: { layers: ["retriever", "cache", "memory"], blend: "darken", groups: [[["maya"]], [["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"]], [["router2"]], [["maya2"]]] } // no tag: the step card shows the reply }; - var STEP_OF = { rdi: 0, redis: 0, maya: 1, router: 2, cache: 3, memory: 4, retriever: 5, agent: 6 }; + var STEP_OF = { maya: 0, router: 1, cache: 2, memory: 3, retriever: 4, redis: 4, agent: 5 }; /* ---- Intent Router and Maya, the user, drawn in the diagram's own style ---- The drawing has neither, so the stage adds both on the empty diagonal down and to the right of the agent: Intent Router first, because it matches Maya's message before the agent acts on it, then Maya. @@ -551,7 +513,7 @@ 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", "rdi"] } + retriever: { box: [498.461, 235.889, 99.2506, 31.2039], lines: ["CONTEXT", "GATEWAY"], in: ["retriever"] } }; function buildRelabels(NS) { var out = {}; @@ -568,65 +530,36 @@ return out; } - var MISSING_ROUTES = { router: [], 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" + /* ---- 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: '' + + '' + + '' + + '' }; - 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 playbookDiscMarkup() { + var P = PLAYBOOK_DISC, I = DISC.icon, c = P.center; + return '' + + '' + + '' + P.icon + "" + + ''; + } + + /* 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]; + + 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() { @@ -675,126 +608,18 @@ return g; } - /* 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 = []; - - /* 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]); } - } - } - 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"; - } - - /* 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 run() { - var go = inView && !document.hidden; - if (go && !raf) raf = requestAnimationFrame(frame); - else if (!go && raf) { cancelAnimationFrame(raf); raf = 0; last = null; } - } - 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)); }); - }); - } - 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 */ }); - 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); } - }; - } - 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"; @@ -809,10 +634,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]; @@ -827,8 +653,11 @@ var mayaLink = figs.querySelector(".iris-link-maya"), routerLink = figs.querySelector(".iris-link-router"), router = figs.querySelector(".iris-router"); 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"); @@ -839,10 +668,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); }); @@ -861,7 +690,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"; @@ -879,7 +708,8 @@ 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; }); @@ -890,8 +720,8 @@ 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"); } @@ -942,7 +772,6 @@ 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 === "router" || stepKey === "reply" || stepKey === "whatif"); routerLink.classList.toggle("is-lit", stepKey === "router" || stepKey === "reply" || stepKey === "whatif"); router.classList.toggle("is-lit", stepKey === "router" || stepKey === "reply" || (stepKey === "whatif" && missing !== "router")); @@ -952,7 +781,7 @@ 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; }); @@ -972,10 +801,10 @@ /* ------------------------------------------------------------------ view */ function init(root) { - var links = { rdi: root.getAttribute("data-link-rdi"), router: root.getAttribute("data-link-router"), retriever: root.getAttribute("data-link-retriever"), + 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") }; var st = { step: 0, missing: null }; - var full = trace({ rdi: true, router: true, retriever: true, memory: true, cache: true }); + var full = trace({ router: true, retriever: true, memory: true, cache: true }); var reduced = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches; function go(i) { @@ -991,10 +820,7 @@ /* The one thing each step shows. */ function fact(step) { var f = el("div", "iris-fact"); - if (step.key === "rdi") { - f.innerHTML = '
    PostgreSQLUPDATE restaurants SET avg_delivery_min = 45 WHERE id = \'r209\';
    ' + - '
    Redisrestaurant:r209 avg_delivery_min: 45
    '; - } else if (step.key === "ask") { + if (step.key === "ask") { f.appendChild(el("div", "rcr-msg rcr-user iris-askmsg", esc(MESSAGE))); } else if (step.key === "router") { f.innerHTML = INTENTS.map(function (it) { @@ -1024,13 +850,13 @@ } return f; } - var SOURCES = { rdi: "Data Integration", router: "Intent Router", retriever: "Context Gateway", memory: "Agent Memory", cache: "Agent Playbooks" }; + 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"); @@ -1080,7 +906,7 @@ function whatIf() { var box = el("div", "iris-whatif"); var picks = el("div", "iris-picks"); - ["rdi", "router", "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"); @@ -1089,7 +915,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, router: 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. */ @@ -1103,7 +929,7 @@ return box; } - /* ---- the data windows: PostgreSQL and Redis ---- */ + /* ---- 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; @@ -1151,7 +977,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; @@ -1255,9 +1081,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 }, 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 = { figuresMarkup: figuresMarkup, ROUTES: ROUTES, HITS: HITS, trace: trace, STEPS: STEPS, WHAT_IF: WHAT_IF, SERVICES: SERVICES, MEMORIES: MEMORIES, MESSAGE: MESSAGE, FEE: FEE, RELABEL: RELABEL }; + 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.
    "; + q.to.forEach(function (t) { + rows += '
    ' + esc(byKey(STACK, t.key).name) + "" + esc(t.result) + "
    "; + }); + rows += '
    Model' + esc(q.model) + "
    " + + '
    AlongsideLens traces the turn, and Agent Mesh keeps the reply stream resumable if the connection drops.
    '; + var links = q.to.map(function (t) { + var c = byKey(STACK, t.key); + return c.doc ? '' + esc(c.name) + " docs →" : ""; + }).join(""); + detail.innerHTML = '
    “' + esc(q.text) + "”
    " + rows + '"; + } + + function pick(key) { + var q = byKey(QUESTIONS, key), tok = ++token; + current = key; + Object.keys(asks).forEach(function (k) { + asks[k].classList.toggle("is-on", k === key); + asks[k].setAttribute("aria-pressed", k === key ? "true" : "false"); + }); + clear(); + showDetail(q); + if (reduced || !sideBySide()) { + light("router", q.intent); + q.to.forEach(function (t) { light(t.key, t.tag); }); + ALONG.forEach(function (a) { light(a.key, a.tag); }); + return; + } + travel(toRouter(q), tok).then(function () { + if (tok !== token) return; + light("router", q.intent); + return Promise.all(q.to.map(function (t) { + return travel(down(t.key), tok).then(function () { if (tok === token) light(t.key, t.tag); }); + })); + }).then(function () { return wait(350); }).then(function () { + if (tok === token) ALONG.forEach(function (a) { light(a.key, a.tag); }); + }); + } + + /* Redraw for the new layout when the width changes, without replaying the animation. */ + var lastWidth = board.clientWidth; + window.addEventListener("resize", function () { + if (board.clientWidth === lastWidth || !current) return; + lastWidth = board.clientWidth; + token++; + var q = byKey(QUESTIONS, current); + clear(); + light("router", q.intent); + q.to.forEach(function (t) { light(t.key, t.tag); }); + ALONG.forEach(function (a) { light(a.key, a.tag); }); + }); + + pick(QUESTIONS[0].key); + } + + function boot() { Array.prototype.forEach.call(document.querySelectorAll(".irf[data-irf]"), init); } + if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", boot); else boot(); + + /* Exposed for tests. */ + window.IrisRequestFlow = { STACK: STACK, QUESTIONS: QUESTIONS }; +})(); From 9d06453973f68a584eaddc213de85e625c893bac Mon Sep 17 00:00:00 2001 From: paoloredis Date: Thu, 29 Oct 2026 11:40:06 +0100 Subject: [PATCH 3/3] Move Intent Router beside the agent and add a Lens step to the Iris tour The agent sends Maya's message to Intent Router and gets the matching intents back, as it does with the other services, so Intent Router now sits above the agent, between Context Gateway and Agent Memory. Maya talks to the agent directly, and the reply goes straight back to her. Step 2 says "agent" throughout, and Intent Router is dimmed on the reply step, because it writes no part of the reply. Add a Lens step before the what-if, marked as in preview, with Lens below the agent's label. The step shows the request as a timeline of what Lens records, and a request with a bug in which the allergy memory is found but never enters the prompt. Selecting a row plays only that step on the drawing. trace() gains a 1 ms prompt assembly step and an option to drop a memory from the prompt; the full reply, its 1.97 s, and its 113 tokens are unchanged. Without Agent Playbooks, the model now answers the fee question in the same call that writes the reply, and gives a vague answer, because it doesn't know the app's fees. There's no second model call. The what-if, its scorecard line, and the tour page's table say so. Co-Authored-By: Claude Opus 5.5 --- .../concepts/interactive-demo.md | 8 +- layouts/shortcodes/iris-demo.html | 8 +- static/css/iris-demo.css | 60 ++- static/js/iris-demo.js | 365 ++++++++++++++---- 4 files changed, 349 insertions(+), 92 deletions(-) diff --git a/content/develop/ai/context-engine/concepts/interactive-demo.md b/content/develop/ai/context-engine/concepts/interactive-demo.md index df976050d0..8e5c24f7b3 100644 --- a/content/develop/ai/context-engine/concepts/interactive-demo.md +++ b/content/develop/ai/context-engine/concepts/interactive-demo.md @@ -4,7 +4,7 @@ categories: - docs - develop - ai -description: Step through one agent request across Intent Router, Agent Playbooks, Agent Memory, and Context Gateway, then take a service away to see what the agent loses. +description: Step through one agent request across Intent Router, Agent Playbooks, Agent Memory, and Context Gateway, review it in Lens, then take a service away to see what the agent loses. hideListLinks: true linktitle: Interactive demo title: Redis Iris interactive demo @@ -15,7 +15,7 @@ aliases: {{< iris-demo >}} -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). The last step takes one service away to show what the agent loses without it. +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. 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. @@ -26,7 +26,9 @@ The tour runs in your browser and doesn't connect to any service. Each step link | Intent Router | Matches the message to two intents: the reorder and the delivery fee question | A model call has to work out what the message needs first, which takes longer and costs output tokens | | [Context Gateway](/content/develop/ai/context-engine/context-gateway/_index.md) | 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 | -| [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 writes the answer, which takes longer, costs output tokens, and isn't tied to the approved answer | +| [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/layouts/shortcodes/iris-demo.html b/layouts/shortcodes/iris-demo.html index 409f30337e..45c6f5166f 100644 --- a/layouts/shortcodes/iris-demo.html +++ b/layouts/shortcodes/iris-demo.html @@ -2,7 +2,8 @@ iris-demo --------- Scripted, dependency-free tour of Redis Iris as one system, one step at - a time: a single agent request is matched by Intent Router and draws on Agent Playbooks, Agent Memory, and Context Gateway. Each + a time: a single agent request is matched by Intent Router and draws on Agent Playbooks, Agent Memory, and Context Gateway, + and Lens shows it as it ran. Each step links to that service's docs or 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. @@ -33,7 +34,7 @@ {{- /* Links to each service's own demo, resolved here so they carry the site's base path. - Context Gateway and Agent Playbooks have no tour-sized demo, so their links go 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. */}} {{- $preview := eq (.Get "preview") "true" }} @@ -44,7 +45,8 @@ data-art="{{ "images/ai/context-engine/iris-diagram/" | relURL }}" data-link-retriever="{{ relref .Page "/develop/ai/context-engine/context-gateway" }}" data-link-memory="{{ relref .Page "/develop/ai/context-engine/agent-memory/interactive-demo" }}" - data-link-cache="{{ relref .Page "/develop/ai/context-engine/agent-playbook" }}"> + data-link-cache="{{ relref .Page "/develop/ai/context-engine/agent-playbook" }}" + data-link-lens="{{ relref .Page "/develop/ai/context-engine/lens" }}"> {{- if $preview }} diff --git a/static/css/iris-demo.css b/static/css/iris-demo.css index dace69b4e8..bac94fa077 100644 --- a/static/css/iris-demo.css +++ b/static/css/iris-demo.css @@ -10,6 +10,7 @@ .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-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; } @@ -40,10 +41,10 @@ .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; } -/* Intent Router's disc, dimmed like the drawing dims a service, with its label only while it's lit. */ -.iris-router-disc, .iris-router-label { transition: opacity .45s; } -.iris-router:not(.is-lit) .iris-router-disc { opacity: .4; } -.iris-router:not(.is-lit) .iris-router-label { opacity: 0; } +/* 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 { @@ -144,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; } @@ -199,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; } @@ -229,7 +279,7 @@ .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, .iris-relabel, .iris-router-disc, .iris-router-label, .iris-pbdisc-art { 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-chip.is-new { animation: none; } } diff --git a/static/js/iris-demo.js b/static/js/iris-demo.js index 7fddc3671e..cbf3edc171 100644 --- a/static/js/iris-demo.js +++ b/static/js/iris-demo.js @@ -9,7 +9,7 @@ week's curry within 30 minutes and asks about the delivery fee, then 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. The last step takes one + 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. @@ -18,7 +18,8 @@ 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, - and the last step compares it with one service missing. Modelled rather + 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. @@ -71,10 +72,13 @@ 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."; + /* 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 assistant's Router matches Maya's message to two of its intents, best first. Each target tells the app what to - dispatch: the food assistant for the reorder, and the support playbook's discovery tool for the fee question. */ + /* 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" } }, @@ -91,8 +95,8 @@ ]; var NAMES = { router: "Intent Router", cache: "Agent Playbooks", memory: "Agent Memory", retriever: "Context Gateway", model: "Model" }; /* Modelled timings (ms) and token counts. */ - var MS = { route: 12, plan: 1100, cache: 41, memory: 190, tool: [74, 88, 81], answer: 1620, reply: 1480 }; - var TOKENS = { plan: 38, 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 */ @@ -116,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)); } @@ -186,19 +191,28 @@ /* ------------------------------------------------------------ the model */ - /* Everything that happens for one combination of services: 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; + /* 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 = BUSY; - function add(s) { steps.push(s); if (s.ms && s.phase !== "record") ms += s.ms; if (s.tokens) tokens += s.tokens; return s; } + 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; + } /* 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 assistant's intents. Two match: " + code("reorder") + - ", which goes to the food assistant, and " + code("delivery_fee") + ", which points at the support playbook. No model call decides this.", + 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." }); @@ -211,7 +225,7 @@ 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 has to answer the general question too." }); + 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 }); @@ -242,6 +256,11 @@ /* 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"); @@ -250,25 +269,27 @@ 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.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 the published FAQ would have supplied." }); + 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." }); @@ -287,13 +308,13 @@ 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 a published FAQ 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, 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 */ @@ -316,9 +337,9 @@ 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 Agent Playbooks, a second model call writes the fee answer: " + seconds(MS.answer) + " and " + TOKENS.answer + " output tokens that the published FAQ would have saved. The wording matches here, but nothing ties it to the answer the support team approved." } + 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. */ @@ -326,8 +347,8 @@ { key: "ask", at: "18:24", title: "Maya asks the assistant", lit: ["agent"], 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: "Before the agent acts, Intent Router matches Maya's message against the intents the app defined. Two match, best first: reorder, which sends the request to the food assistant, and delivery_fee, which points the fee question at the support playbook. Matching takes " + MS.route + " ms, without a model call.", - adds: [{ src: "router", t: "reorder → food assistant" }, { src: "router", t: "delivery_fee → support playbook" }] }, + 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" }] }, @@ -339,10 +360,12 @@ 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 = { router: "Intent Router", retriever: "Context Gateway", memory: "Agent Memory", cache: "Agent Playbooks" }; + var DEPTH = { router: "Intent Router", retriever: "Context Gateway", memory: "Agent Memory", cache: "Agent Playbooks", lens: "Lens" }; /* ---- the sample data, as table rows ---- The same rows as the RDI demo (rdi-demo.js), on the evening of this tour: @@ -438,63 +461,90 @@ 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, maya2, router, and router2 are added with the figures (FIGURES), from their geometry. */ + /* 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 = { redis: [413, 230, 44], retriever: [540, 318, 36], - memory: [773, 307, 36], agent: [651, 385, 36], cache: [526, 469, 36] }; // maya and router: with the figures + 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 = { ask: { layers: ["retriever", "cache", "memory"], blend: "darken", groups: [[["maya"]]] }, // no tag: the step card shows her message - router: { layers: ["retriever", "cache", "memory"], blend: "darken", groups: [[["maya"]], [["router"]]], tags: [[0, "router", "2 intents · 12 ms"]] }, + /* 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"]], [["router2"]], [["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 + }; + /* 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 }; - /* ---- Intent Router and Maya, the user, drawn in the diagram's own style ---- - The drawing has neither, so the stage adds both on the empty diagonal down and to the right of the - agent: Intent Router first, because it matches Maya's message before the agent acts on it, then Maya. - Each is the agent's own disc, scaled about the agent's face centre (C0) and moved along that diagonal - (STEP, in multiples of it): Intent Router in the drawing's service colours with its icon, Maya with a - person where the agent has a robot. Each has a label box below it, as the agent does; Intent Router's - shows only while it's 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. */ + 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], // the face's plane, in units of its radius, at full size - bottom: 37.6, // from C0 down to the disc's lowest point, at full size - reach: 44 // from a disc's centre to where its links start, along STEP, at full size + 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: { at: 0.8, s: 0.7, label: "INTENT ROUTER", labelW: 81.4, labelDx: -25 }, // left, clear of its link to Maya - maya: { at: 1.6, s: 0.8, label: "MAYA", labelW: 38.3 } + 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] + STEP[0] * f.at, C0[1] + STEP[1] * f.at]; }); - /* A point `a` along the diagonal from C0, and `o` across it (down and to the left). */ - function diag(a, o) { - var ux = STEP[0] / STEP_LEN, uy = STEP[1] / STEP_LEN; - return [C0[0] + ux * a - uy * o, C0[1] + uy * a + ux * o]; + 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(near, far) { - var a = near.at * STEP_LEN + near.s * DISC.reach, b = far.at * STEP_LEN - far.s * DISC.reach; - return { in: [diag(b, 9), diag(a, 9)], out: [diag(a, 0), diag(b, 0)] }; + 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({ at: 0, s: 1 }, FIG.router), maya: link(FIG.router, FIG.maya) }; + 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); - HITS.router = [FIG.router.c[0], FIG.router.c[1], 26]; HITS.maya = [FIG.maya.c[0], FIG.maya.c[1], 30]; - TAG_SPOTS.router = [790, 528, "r"]; // below its label, clear of Maya + 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", @@ -505,6 +555,10 @@ '' + '' + ''; + /* 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 /* ---- 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 @@ -571,10 +625,10 @@ " L" + n(b[0] - ux * 3.2 + uy * 2.2) + " " + n(b[1] - uy * 3.2 - ux * 2.2) + '"/>'; } function mid(seg) { return ''; } - function linkG(k) { + function linkG(k, oneWay) { var L = LINKS[k]; return '' + - arrow(L.in) + arrow(L.out) + '' + mid(L.in) + mid(L.out) + ""; + (oneWay ? "" : arrow(L.in)) + arrow(L.out) + '' + (oneWay ? "" : mid(L.in)) + mid(L.out) + ""; } function disc(f, colors, icon, iconStroke) { var c = f.c, s = f.s, I = DISC.icon; @@ -589,14 +643,20 @@ return '' + ''; } + /* 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 x = f.c[0] + (f.labelDx || 0) - f.labelW / 2, y = f.c[1] + f.s * DISC.bottom + 5; - return '' + - '' + f.label + ""; + 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("") + ""; } + var SERVICE = ["#3A2227", "#FF4438", "#89302E"]; // the drawing's colours for a service disc: rim, outline, face return { - links: linkG("router") + linkG("maya"), - router: '' + disc(FIG.router, ["#3A2227", "#FF4438", "#89302E"], '' + ROUTER_ICON + "", 0.04) + badge(FIG.router) + "" + label(FIG.router, "iris-router-label"), + 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, "") }; @@ -604,7 +664,7 @@ function buildFigures(NS) { var m = figuresMarkup(), g = document.createElementNS(NS, "g"); g.setAttribute("class", "iris-maya"); - g.innerHTML = m.links + '' + m.router + "" + m.maya; + g.innerHTML = m.links + '' + m.router + '' + m.lens + "" + m.maya; return g; } @@ -651,6 +711,7 @@ 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"); @@ -770,14 +831,22 @@ el: wrap, art: box, dataBtns: dataBtns, - show: function (stepKey, missing) { - var tok = ++token; - mayaLink.classList.toggle("is-lit", stepKey === "ask" || stepKey === "router" || stepKey === "reply" || stepKey === "whatif"); - routerLink.classList.toggle("is-lit", stepKey === "router" || stepKey === "reply" || stepKey === "whatif"); - router.classList.toggle("is-lit", stepKey === "router" || stepKey === "reply" || (stepKey === "whatif" && missing !== "router")); + /* 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. */ @@ -802,9 +871,16 @@ function init(root) { 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") }; - var st = { step: 0, missing: null }; + 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) { @@ -819,6 +895,7 @@ /* The one thing each step shows. */ function fact(step) { + if (step.key === "lens") return lensView(); var f = el("div", "iris-fact"); if (step.key === "ask") { f.appendChild(el("div", "rcr-msg rcr-user iris-askmsg", esc(MESSAGE))); @@ -929,6 +1006,131 @@ return box; } + /* ---- 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.") + '

    ' + x.items.map(function (it) { + return "" + esc(it.name) + 'score ' + it.score.toFixed(2) + " → " + esc(it.target.type + " " + it.target.ref) + ""; + }).join("") + "
    "; + } else if (x.results) { + body = '
    Query: ' + esc(x.query) + "
    " + x.results.map(function (r) { + return '
    ' + (r.used ? "Entered prompt" : "Not used") + "" + + '' + esc(r.label) + " " + esc(r.text) + "
    "; + }).join(""); + } else if (x.tool) { + body = '
    Tool' + esc(x.tool) + 'Input' + esc(x.input) + + 'Result' + esc(x.result) + "
    "; + } else if (x.svc === "app") { + body = "

    " + esc("Maya's message, the FAQ's answer, " + (x.used === x.of ? "all " + x.of : x.used + " of the " + x.of) + " memories, and the tool results entered the prompt." + + x.dropped.map(function (m) { return " The app dropped the memory “" + m.text + "”"; }).join("")) + "

    "; + } else if (x.svc === "model") { + body = "

    " + esc(x.note + " Lens records the prompt, what the model wrote (the completion), and its " + x.tokens + " output tokens.") + "

    "; + } else body = "

    " + esc(x.note) + "

    "; + d.innerHTML = '
    ' + esc(x.name + (x.op ? " · " + x.op : "")) + ' ' + took(x.ms) + "
    " + body; + } + /* ---- the data window: Redis ---- */ /* It opens over the drawing only, so the step stays readable and usable below it. */ @@ -1038,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()); @@ -1054,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"; @@ -1072,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); @@ -1085,7 +1288,7 @@ 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 }, PREVIEW_DEFAULT_MS = 4200; + 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 = "";