diff --git a/tutorials/signals-ai-agent-context/build-ai-integration.md b/tutorials/signals-ai-agent-context/build-ai-integration.md index 6ffc02fa7..ad64ba5fe 100644 --- a/tutorials/signals-ai-agent-context/build-ai-integration.md +++ b/tutorials/signals-ai-agent-context/build-ai-integration.md @@ -2,16 +2,21 @@ title: "Build the Signals AI integration using the Vercel AI SDK" position: 5 sidebar_label: "Connect Signals and AI agent" -description: "Connect Snowplow Signals to a Vercel AI SDK agent by fetching user attributes and injecting them into the system prompt." -keywords: ["vercel ai sdk", "system prompt", "signals context", "ai agent", "streaming", "next.js api route"] -date: "2026-04-10" +description: "Connect Snowplow Signals to a Vercel AI SDK agent by fetching profile attributes and the agentic context narrative, and injecting both into the system prompt." +keywords: ["vercel ai sdk", "system prompt", "signals context", "agentic context", "ai agent", "streaming", "next.js api route"] +date: "2026-07-31" --- The next step is to connect Signals to your AI agent, via the Vercel AI SDK. ## Fetch Signals context -Create the module that fetches and formats user attributes from Signals: +Your agent will fetch two kinds of context from Signals on every chat request, using the same session ID for both: + +* Profile attributes, from the service: computed aggregates that you format into a prompt section yourself +* Recent session activity, from the [agentic context](/docs/signals/applications/agentic-contexts/): fetched with `format: "narrative"`, which returns a ready-made text block, so there's no formatting code to write + +Create the module that fetches both: ```tsx // lib/signals-context.ts @@ -35,70 +40,134 @@ function getSignalsClient(): Signals | null { return signalsInstance; } -const SERVICE_NAME = "web-agent-context"; +const SERVICE_NAME = "web_agent_context"; +const AGENTIC_CONTEXT_NAME = "web_agent_activity"; + +// Profile attributes: computed aggregates, served by the Signals service +async function getProfileSection( + signals: Signals, + domainSessionId: string, +): Promise { + const attributes = await signals.getServiceAttributes({ + name: SERVICE_NAME, + attribute_key: "domain_sessionid", + identifier: domainSessionId, + }); + + // The service returns a key for every attribute it serves, with a null value + // until the session has produced data. Drop those, so a brand new session + // yields no profile section rather than a list of nulls. + const populated = Object.entries(attributes ?? {}).filter( + ([, value]) => value !== null && value !== undefined, + ); + + if (populated.length === 0) { + return ""; + } -function formatAttributes(attributes: Record): string { - const lines = Object.entries(attributes).map( + const lines = populated.map( ([key, value]) => `- ${key}: ${JSON.stringify(value)}`, ); return [ - "## Real-Time User Context (Snowplow Signals)", - "The following attributes describe the current user's session behavior on this application:", + "## User profile (Snowplow Signals attributes)", + "Computed attributes describing the current user's session so far:", ...lines, ].join("\n"); } +// Session activity: LLM-ready narrative, served by the agentic context +async function getActivitySection( + signals: Signals, + domainSessionId: string, +): Promise { + const narrative = await signals.getAgenticContext({ + name: AGENTIC_CONTEXT_NAME, + identifier: domainSessionId, + format: "narrative", + }); + + if (!narrative) return ""; + + return [ + "## Recent session activity (Snowplow Signals agentic context)", + narrative, + ].join("\n"); +} + export async function getSignalsContext( domainSessionId: string, ): Promise { const signals = getSignalsClient(); if (!signals) return ""; - try { - const attributes = await signals.getServiceAttributes({ - name: SERVICE_NAME, - attribute_key: "domain_sessionid", - identifier: domainSessionId, - }); - - if (!attributes || Object.keys(attributes).length === 0) { - return ""; + // Fetch both in parallel; if one fails, the other is still used + const [profile, activity] = await Promise.allSettled([ + getProfileSection(signals, domainSessionId), + getActivitySection(signals, domainSessionId), + ]); + + const sections: string[] = []; + for (const result of [profile, activity]) { + if (result.status === "fulfilled" && result.value) { + sections.push(result.value); + } else if (result.status === "rejected") { + console.error("[signals-context] Signals fetch failed:", result.reason); } - - return formatAttributes(attributes); - } catch (error) { - console.error( - "[signals-context] Failed to fetch signals attributes:", - error, - ); - return ""; } + + return sections.join("\n\n"); } ``` -The raw response format from the service, pulled using `signals.getServiceAttributes()`, looks like this: +The profile fetch returns raw attribute values from the service, which `getProfileSection()` formats into a Markdown list: ```json { - "page_views_count": 12, - "unique_pages_viewed": 5, - "first_event_timestamp": "2026-04-09T14:23:01.000Z", - "last_event_timestamp": "2026-04-09T14:41:03.000Z" + "page_views_count": 5, + "unique_pages_viewed": [ + "https://signal-shop.example.com/", + "https://signal-shop.example.com/products", + "https://signal-shop.example.com/products/3", + "https://signal-shop.example.com/products/7" + ], + "first_event_timestamp": "2026-07-29T14:14:13.013Z", + "last_event_timestamp": "2026-07-29T14:16:13.976Z" } ``` -The `formatAttributes()` function converts that into a markdown section that can be appended to the agent's system prompt, for example: +Note the null filter in `getProfileSection()`. A service returns a key for every attribute it serves, valued `null` until the session has produced data: -```markdown -## Real-Time User Context (Snowplow Signals) -The following attributes describe the current user's session behavior on this application: -- page_views_count: 12 -- unique_pages_viewed: 5 -- first_event_timestamp: "2026-04-09T14:23:01.000Z" -- last_event_timestamp: "2026-04-09T14:41:03.000Z" +```json +{ + "page_views_count": null, + "unique_pages_viewed": null, + "first_event_timestamp": null, + "last_event_timestamp": null +} +``` + +Filter those nulls out before formatting, so a session with no data yet contributes no profile section at all, rather than a list of nulls the model could read as facts about the user. + +The activity fetch needs no formatting. With `format: "narrative"`, `getAgenticContext()` returns the prompt you configured, followed by a block delimited by `[START CONTEXT]` and `[END CONTEXT]`. For a five-page browsing session, that looks like: + +```text +You are a helpful assistant for the Signal Shop web store. Use this recent activity to understand what the user is exploring right now, and tailor your answers to it. +[START CONTEXT] +10 seconds on the current page. Session started 132 seconds ago. Based on last 50 recorded events for the last 1800 seconds. +## Real-time user behaviour +Events are ordered from oldest to most recent. +seconds_since_start_of_session, event, url, event_context +0, page_view, /, {page_title: 'Signal Shop'} +25, page_view, /products, {page_title: 'All products | Signal Shop'} +56, page_view, /products/3, {page_title: 'Aurora Wireless Headphones | Signal Shop'} +91, page_view, /products/7, {page_title: 'Linen Overshirt | Signal Shop'} +122, page_view, /products/3, {page_title: 'Aurora Wireless Headphones | Signal Shop'} +[END CONTEXT] ``` -If Signals isn't configured or a fetch fails, the `getSignalsContext()` function returns an empty string. The agent still works without the Signals context. +The opening summary and the event table are generated by Signals from the events you selected when defining the agentic context. + +If Signals isn't configured or both fetches fail, the `getSignalsContext()` function returns an empty string. The agent still works without the Signals context. ## Build the agent @@ -109,8 +178,10 @@ Create the function that constructs the system prompt with the Signals context a const BASE_INSTRUCTIONS = `You are a helpful assistant for this application. Help users understand features, answer questions, and guide them through their journey. -When you have real-time user context available (provided below), use it to personalize -your responses. Reference what the user has been looking at to give more relevant answers.`; +When real-time user context is available below, use it to personalize your responses. +The user profile section describes the session in aggregate. The recent session +activity section lists what the user has just been doing, oldest event first. +Reference what the user has been looking at to give more relevant answers.`; export function createAgent(signalsContext?: string) { const systemPrompt = @@ -120,7 +191,7 @@ export function createAgent(signalsContext?: string) { } ``` -The model treats the Signals block as factual context about the current user. No special prompting is needed beyond including it: LLMs naturally incorporate provided context when formulating responses. +The agentic context's own `prompt` instructions arrive at the top of the narrative string, ahead of `[START CONTEXT]`, so you can steer the agent from your Signals configuration as well as from `BASE_INSTRUCTIONS`. ## Build the chat API route @@ -144,7 +215,8 @@ export async function POST(request: Request) { // Extract the Snowplow session ID passed from the frontend const snowplowDomainSessionId = pageContext?.snowplowDomainSessionId || ""; - // Fetch real-time user attributes from Signals + // Fetch real-time user context from Signals: + // profile attributes + the session activity narrative let signalsContext = ""; if (snowplowDomainSessionId) { signalsContext = await getSignalsContext(snowplowDomainSessionId); @@ -164,13 +236,7 @@ export async function POST(request: Request) { } ``` -:::note[Model providers] -This example uses [Vercel AI Gateway](https://vercel.com/docs/ai-gateway), which routes requests to any supported model provider with a single API key. - -To use a different model, change the model string e.g. `gateway("anthropic/claude-sonnet-4.5")` or `gateway("google/gemini-2.5-pro")`. - -See the [full list of supported models](https://vercel.com/ai-gateway/models). The Signals integration works identically regardless of which model you choose. -::: +This example uses [Vercel AI Gateway](https://vercel.com/docs/ai-gateway), which routes requests to any supported model provider with a single API key. To use a different model, change the model string, for example `gateway("anthropic/claude-sonnet-4.5")` or `gateway("google/gemini-2.5-pro")`. See the [full list of supported models](https://vercel.com/ai-gateway/models). ## Build the chat frontend diff --git a/tutorials/signals-ai-agent-context/conclusion.md b/tutorials/signals-ai-agent-context/conclusion.md index d67aabc60..2a7b20987 100644 --- a/tutorials/signals-ai-agent-context/conclusion.md +++ b/tutorials/signals-ai-agent-context/conclusion.md @@ -3,8 +3,8 @@ title: "Conclusion and next steps" position: 7 sidebar_label: "Conclusion" description: "Summary of the Signals AI agent tutorial and ideas for extending it with richer attributes, interventions, and multi-dimensional context." -keywords: ["signals next steps", "ai agent extensions", "interventions", "attribute groups"] -date: "2026-04-10" +keywords: ["signals next steps", "ai agent extensions", "interventions", "attribute groups", "agentic context"] +date: "2026-07-31" --- In this tutorial, you've built a Next.js AI agent that uses Snowplow Signals to deliver personalized, context-aware responses based on live user behavior. @@ -14,8 +14,9 @@ Here's what you set up: * Snowplow Browser tracker capturing page views, page pings, and link clicks * A Signals attribute group computing real-time session-level attributes * A Signals service exposing those attributes via API +* A Signals agentic context capturing the session's recent activity as an LLM-ready narrative * A floating chat widget that passes the Snowplow session ID with every request -* A Vercel AI SDK agent that fetches and injects those attributes into its system prompt +* A Vercel AI SDK agent that fetches both and injects them into its system prompt Here are some next steps ideas for extending what you've built. @@ -40,7 +41,7 @@ Try exploring how you could use interventions within this application. You can combine Signals real-time stream attributes with automatic ingestion of batch attributes, using data sources within your warehouse, to give your agent a complete picture of the user. This could include attributes such as user profile data, CRM attributes, or product usage history. -Try setting up a batch attribute group to ingest as part of your `web-agent-context` service. +Try setting up a batch attribute group to ingest as part of your `web_agent_context` service. The Vercel AI SDK's system prompt is just a string: you can compose it from as many sources as you need. diff --git a/tutorials/signals-ai-agent-context/configure-signals.md b/tutorials/signals-ai-agent-context/configure-signals.md index c50e56166..9091d6769 100644 --- a/tutorials/signals-ai-agent-context/configure-signals.md +++ b/tutorials/signals-ai-agent-context/configure-signals.md @@ -1,32 +1,58 @@ --- -title: "Configure Snowplow Signals" +title: "Configure Signals attributes and an agentic context" position: 4 sidebar_label: "Configure Signals" -description: "Create an attribute group, publish it, and set up a Signals service to serve real-time user attributes." -keywords: ["snowplow signals", "attribute group", "signals service", "real-time attributes", "profiles store"] -date: "2026-04-10" +description: "Create an attribute group, a service, and an agentic context to serve real-time profile attributes and session activity to your AI agent." +keywords: ["snowplow signals", "attribute group", "signals service", "agentic context", "real-time attributes", "profiles store"] +date: "2026-07-31" --- -The next step is to define the user attributes you want to compute. You'll do this within [Snowplow Console](https://console.snowplowanalytics.com). +```mdx-code-block +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; +``` + +The next step is to define the real-time context you want Signals to serve. You'll set up two complementary resources: + +* An [attribute group](/docs/signals/concepts/#attribute-groups) and [service](/docs/signals/concepts/#services) that compute and serve profile attributes: aggregate metrics that describe the session, such as how many pages the user has viewed +* An [agentic context](/docs/signals/agentic-contexts/) that captures recent session activity: the user's latest events, readable as an LLM-ready narrative + +The two work together. Attributes describe the session in aggregate, while the agentic context records what the user has just been doing, event by event. Both are scoped to the same `domain_sessionid` attribute key, so your agent can fetch both using the session ID it already has. + +## Ask the Snowplow Assistant + +You can create all three resources by asking an AI assistant: the [Snowplow Assistant](/docs/llms-support/console-agent/) in Console, or your own assistant connected to the [Snowplow MCP server](/docs/llms-support/snowplow-mcp/), which you can install with `npx plugins add snowplow/skills`. Paste this prompt: + +```text +In Signals, create and publish an attribute group from the Basic Web template with +domain_sessionid as the attribute key, and a service called web_agent_context that serves +it. Then create and publish an agentic context called web_agent_activity, also keyed on +domain_sessionid, buffering the last 50 page_view events from the last 30 minutes and +keeping the event_name, page_urlpath, and page_title properties. Set its prompt to: "You +are a helpful assistant for this web store. Use this recent activity to understand what the +user is exploring right now, and tailor your answers to it." +``` + +To set the same resources up by hand instead, work through the sections below. ## Create a Basic Web attribute group Use one of Signals' built-in [attribute group](/docs/signals/concepts/#attribute-groups) templates to define attributes. Use the `domain_sessionid` as attribute key to compute session-level attributes. -1. In [Console](https://console.snowplowanalytics.com), navigate to **Signals** > **Attribute Groups** -2. Click **Create attribute group** and choose **Basic Web** -3. Set the **Attribute Key** to `domain_sessionid` +1. In [Console](https://console.snowplowanalytics.com), navigate to **Signals** > **Attribute groups** +2. Click **Create attribute group**, then click **Use** on the **Basic Web** template +3. Set the **Attribute key** to `domain_sessionid` -The Basic Web template includes these attributes: +The template fills in the form with four attributes, which Console lists with the aggregation and property behind each one: -| Attribute | Description | -| ----------------------- | ------------------------------------------ | -| `page_views_count` | Total number of page views in the session | -| `unique_pages_viewed` | List of unique URLs visited in the session | -| `first_event_timestamp` | When the session started | -| `last_event_timestamp` | When the most recent event was recorded | +| Attribute | Aggregation and property | Description | +| ----------------------- | ------------------------ | -------------------------------------------------- | +| `page_views_count` | Counter over `page_view` | Total number of page views in the session | +| `unique_pages_viewed` | Unique list of `page_url`| The full URLs the user has visited in the session | +| `first_event_timestamp` | First `derived_tstamp` | When the session started | +| `last_event_timestamp` | Last `derived_tstamp` | When the most recent event was recorded | -Test your attribute group by clicking **Run Preview** before saving, to verify it's computing correctly based on recent events in your pipeline. This runs a query against your event data in your data warehouse and shows the computed attributes for recent sessions. +Because `unique_pages_viewed` aggregates `page_url` rather than `page_urlpath`, it holds complete URLs including the scheme and host. The agentic context you define later uses `page_urlpath`, so the two sections of the prompt describe the same pages at different levels of detail. Click **Create attribute group** when you're happy with the attribute group. @@ -48,8 +74,113 @@ Services allow you to combine multiple attribute groups if needed, but for this 1. Navigate to **Signals** > **Services** 2. Click **Create service** 3. Configure: - - **Name**: `web-agent-context` + - **Name**: `web_agent_context` - **Attribute groups**: Select the attribute group you just published 4. Click **Create service** +Signals names take letters, numbers, and underscores only, so use the underscores above rather than hyphens. The **Attribute groups** picker lists published groups only, which is why you published yours first. + The page will show you retrieval instructions for Node.js. You'll need these to set up your API client in the next step. + +## Create an agentic context + +The service you just created serves computed aggregates. To also give your agent a chronological record of what the user is doing, [define an agentic context](/docs/signals/agentic-contexts/): a rolling record of the user's recent events that Signals can return as a plain-language narrative, ready to drop into a prompt. + +For this app, capture page view events, keeping three properties from each one: + +| Property | Purpose | +| -------------- | ---------------------------------------------------------------- | +| `event_name` | Populates the event column of the narrative table | +| `page_urlpath` | Populates the URL column of the narrative table | +| `page_title` | Extra detail, included in the narrative's `event_context` column | + +The `event_name` and `page_urlpath` atomic properties feed the narrative's dedicated columns. Any other property you select appears in its `event_context` column. + +Your app also tracks page pings and link clicks. Leave the page pings out, because the buffer holds a limited number of events and heartbeat pings would crowd out the meaningful activity. + + + + +Go to **Signals** > **Agentic contexts** in Console and create a new agentic context. The **Create context** form is a single page you scroll through, so work down it section by section. + +Start with **Details** and **Prompt**: + +| Field | Value | +| ------------- | --------------------------------------------------------- | +| Name | `web_agent_activity` | +| Primary owner | Your email address, which Console fills in for you | +| Description | `Recent session activity for the web store support agent` | +| Prompt | The prompt text below | + +Use this as the prompt: + +```text +You are a helpful assistant for this web store. Use this recent activity to +understand what the user is exploring right now, and tailor your answers to it. +``` + +![The Create context form in Snowplow Console. The Details section holds the name web_agent_activity, a greyed-out Primary owner field pre-filled with the signed-in user's email address, and the description. The Prompt section below holds the web store assistant instructions, under helper text reading that these instructions will be added on top of the retrieved agentic context.](./images/agentic-context-create-form.png) + +Under **Lookback Window**, set **Max events** to `50` and **Max age** to `30` minutes. Console restates the window underneath the fields, so you can check it reads as the last 50 events within 30 minutes. The details page shows the same setting as **Max Age (seconds)**, where 30 minutes reads as `1800`. + +Under **Events and Properties**, click **Add event** and choose `page_view` at version `1-0-0`. Then use **Add property** to attach `event_name`, `page_urlpath`, and `page_title` to it. + +![The Lookback Window section of the Create context form, with Max events set to 50 and Max age set to 30 minutes, restated underneath as the last 50 events within 30 minutes. The Events and Properties section below shows page_view version 1-0-0 with a Data structure badge, three properties attached, and chips for event_name, page_urlpath, and page_title.](./images/agentic-context-lookback-and-events.png) + +Click **Create**. Console saves the agentic context with a **Draft** status, so open its details page, click **Publish**, and confirm, to send the configuration to your Signals infrastructure. To change it later, use **Edit** on the same page, which starts a new draft and leaves the published version live until you publish again. + + + + +You can also define agentic contexts programmatically with the [Signals Python SDK](https://pypi.org/project/snowplow-signals/), where the `EventLog` class is the building block. Start by [connecting to Signals](/docs/signals/connection/) to create a `Signals` object called `sp_signals`, then define the agentic context: + +```python +from snowplow_signals import ( + EventLog, + EventSelection, + EventLogEvent, + EventLogAtomicProperty, + domain_sessionid, +) + +web_agent_activity = EventLog( + name="web_agent_activity", + description="Recent session activity for the web store support agent", + owner="user@company.com", + prompt=( + "You are a helpful assistant for this web store. " + "Use this recent activity to understand what the user is exploring " + "right now, and tailor your answers to it." + ), + attribute_key=domain_sessionid, + max_events=50, + max_age_seconds=1800, + events=[ + EventSelection( + event=EventLogEvent( + name="page_view", + vendor="com.snowplowanalytics.snowplow", + version="1-0-0", + ), + properties=[ + EventLogAtomicProperty(name="event_name"), + EventLogAtomicProperty(name="page_urlpath"), + EventLogAtomicProperty(name="page_title"), + ], + ), + ], +) +``` + +Publish it to send the configuration to your Signals infrastructure: + +```python +sp_signals.publish([web_agent_activity]) +``` + + + + +The `prompt` text travels with the agentic context: Signals hands it to your agent alongside the captured activity, so you can refine the instructions later without touching your application code. + +With the attribute group, service, and agentic context all published, you're ready to wire them into your app. diff --git a/tutorials/signals-ai-agent-context/images/agentic-context-create-form.png b/tutorials/signals-ai-agent-context/images/agentic-context-create-form.png new file mode 100644 index 000000000..3165adf53 Binary files /dev/null and b/tutorials/signals-ai-agent-context/images/agentic-context-create-form.png differ diff --git a/tutorials/signals-ai-agent-context/images/agentic-context-lookback-and-events.png b/tutorials/signals-ai-agent-context/images/agentic-context-lookback-and-events.png new file mode 100644 index 000000000..ca9e834ad Binary files /dev/null and b/tutorials/signals-ai-agent-context/images/agentic-context-lookback-and-events.png differ diff --git a/tutorials/signals-ai-agent-context/introduction.md b/tutorials/signals-ai-agent-context/introduction.md index 2ed06ba30..e3e34aad8 100644 --- a/tutorials/signals-ai-agent-context/introduction.md +++ b/tutorials/signals-ai-agent-context/introduction.md @@ -3,18 +3,24 @@ title: "Learn how to build an AI agent with real-time user context using Signals position: 1 sidebar_label: "Introduction" description: "Build a Next.js AI agent that uses Snowplow Signals to understand what your users are doing in real time." -keywords: ["snowplow signals", "ai agent", "vercel ai sdk", "real-time context", "next.js"] -date: "2026-04-10" +keywords: ["snowplow signals", "ai agent", "vercel ai sdk", "real-time context", "agentic context", "next.js"] +date: "2026-07-31" --- In this tutorial, you'll build a Next.js AI agent that uses [Snowplow Signals](/docs/signals/introduction/) to understand what your users are doing in real time. Instead of responding generically to every user, the agent will have live awareness of the current user's session behavior: which pages they've visited, what they've been exploring, and how long they've been on the site. +The agent will draw on two complementary kinds of Signals context: + +* Profile attributes: computed aggregates about the session, such as page view counts, served by a Signals [service](/docs/signals/concepts/#services). Use attributes when you want defined metrics that your agent, or any other consumer, can rely on. +* An [agentic context](/docs/signals/agentic-contexts/): the user's recent activity, returned as an LLM-ready narrative. Use it when you want to ground the agent in the user's immediate journey, without writing aggregation or formatting logic. + The app will: 1. Track user behavior automatically using the [Snowplow Browser tracker](/docs/sources/web-trackers/) 2. Compute live user attributes with Snowplow Signals -3. Inject those attributes into the AI agent's system prompt using the Vercel AI SDK -4. Deliver contextually aware responses that respond to what the user is actually doing +3. Capture recent session activity with a Signals agentic context +4. Inject both into the AI agent's system prompt using the Vercel AI SDK +5. Deliver contextually aware responses that respond to what the user is actually doing Adding real-time context from Signals can improve responses. In this example, the user has spent 20 minutes exploring the enterprise pricing page: @@ -36,18 +42,49 @@ The agent can tailor its response based on the user's actual behavior, making fo The flow works like this: - The Snowplow Browser tracker streams behavioral [events](/docs/fundamentals/events/) to your Collector -- Signals computes live session attributes from that stream +- Signals computes live session attributes from that stream, and buffers the session's recent events for the agentic context - On the front-end, the `ChatWidget` reads the Snowplow session ID from the tracker's cookie and sends it alongside every chat request as `pageContext.snowplowDomainSessionId` -- The Next.js `/api/chat` route uses that session ID to fetch fresh attributes from Signals and appends them to the system prompt +- The Next.js `/api/chat` route uses that session ID to fetch both the profile attributes and the activity narrative from Signals, and appends both to the system prompt - The model's response is streamed back through the Vercel AI Gateway to the browser -Architecture diagram showing the full data and request flow. In the browser, the Snowplow tracker fires page views, page pings, and link clicks to the Snowplow Collector. The Collector produces enriched events, which flow into Snowplow Signals. Signals computes session attributes and exposes them via a GET attributes by session endpoint. On the Next.js server, the API Chat route receives messages and a Session ID from the browser chat widget, calls the Signals endpoint to fetch session attributes, and passes those attributes as a system prompt alongside the messages to Vercel AI Gateway. The gateway returns a streamed response, which the browser renders in the chat widget. +```mermaid +flowchart TD + subgraph browser ["Browser"] + tracker["Snowplow Browser tracker
page views, page pings, link clicks"] + widget["ChatWidget
reads the session ID
from the tracker cookie"] + end + + collector["Snowplow Collector
and enrichment"] + + subgraph signals ["Snowplow Signals"] + service["Service
computed profile attributes"] + agentic["Agentic context
buffer of recent session events"] + end + + subgraph server ["Next.js server"] + route["/api/chat route"] + prompt["System prompt
base instructions
+ profile attributes
+ activity narrative"] + end + + gateway["Vercel AI Gateway"] + + tracker --> collector + collector --> service + collector --> agentic + widget -- "messages and session ID" --> route + route -- "getServiceAttributes" --> service + route -- "getAgenticContext, narrative format" --> agentic + service -- "attribute values" --> prompt + agentic -- "narrative text block" --> prompt + prompt --> gateway + gateway -- "streamed response" --> widget +``` ## Prerequisites This tutorial requires: -* A Snowplow account with [Signals deployed](/docs/signals/connection/) +* A Snowplow account and pipeline with [Signals enabled](/docs/signals/setup/) * Node.js 18+ and npm/pnpm * A [Vercel AI Gateway API key](https://vercel.com/docs/ai-gateway/getting-started) * This tutorial uses `openai/gpt-4o-mini` via AI Gateway, but any supported model works diff --git a/tutorials/signals-ai-agent-context/setup.md b/tutorials/signals-ai-agent-context/setup.md index d75c2b604..ad9185a8d 100644 --- a/tutorials/signals-ai-agent-context/setup.md +++ b/tutorials/signals-ai-agent-context/setup.md @@ -4,7 +4,7 @@ position: 2 sidebar_label: "Set up the project" description: "Scaffold a Next.js project and install the required dependencies for Snowplow tracking, Signals, and the Vercel AI SDK." keywords: ["next.js setup", "vercel ai sdk", "snowplow browser tracker", "project scaffold"] -date: "2026-04-10" +date: "2026-07-31" --- First, you'll need to create a Next.js project. You could also use an existing project. @@ -23,9 +23,7 @@ This will create a new Next.js app using TypeScript and Tailwind CSS. As well as installing the Vercel AI SDK, it'll add the Snowplow browser tracker and link click plugin for client-side tracking, and the Signals Node client to fetch user attributes server-side. -:::note[Existing projects] -The code in this tutorial uses the `@/*` path alias (e.g. `@/lib/snowplow`, `@/components/chat-widget`). This is configured by default by `create-next-app`. If you're adding to an existing project, make sure your `tsconfig.json` has `"@/*": ["./*"]` in `compilerOptions.paths`. -::: +The code in this tutorial uses the `@/*` path alias (for example `@/lib/snowplow`, `@/components/chat-widget`), which `create-next-app` configures by default. If you're adding to an existing project, make sure your `tsconfig.json` has `"@/*": ["./*"]` in `compilerOptions.paths`. ## Install AI Elements @@ -68,7 +66,7 @@ AI_GATEWAY_API_KEY=your-vercel-ai-gateway-api-key NEXT_PUBLIC_SNOWPLOW_COLLECTOR_URL=https://your-collector-url.com # Snowplow Signals -SNOWPLOW_SIGNALS_BASE_URL=https://signals.snowplowanalytics.com +SNOWPLOW_SIGNALS_BASE_URL=https://YOUR_ID.signals.snowplowanalytics.com SNOWPLOW_SIGNALS_API_KEY=your-signals-api-key SNOWPLOW_SIGNALS_API_KEY_ID=your-signals-api-key-id SNOWPLOW_SIGNALS_ORG_ID=your-org-id diff --git a/tutorials/signals-ai-agent-context/test.md b/tutorials/signals-ai-agent-context/test.md index acc2bfc3f..ec7044fe3 100644 --- a/tutorials/signals-ai-agent-context/test.md +++ b/tutorials/signals-ai-agent-context/test.md @@ -3,8 +3,8 @@ title: "Try out the Signals and Vercel AI integration" position: 6 sidebar_label: "Test the app" description: "Run your Next.js app, build up behavioral context by browsing, and see how the AI agent uses real-time Signals data." -keywords: ["testing", "signals context", "ai agent demo", "real-time personalization"] -date: "2026-04-10" +keywords: ["testing", "signals context", "agentic context", "ai agent demo", "real-time personalization"] +date: "2026-07-31" --- Your application is now ready to try out. @@ -19,13 +19,15 @@ Make sure you've replaced the placeholder values in `.env.local` with real crede ## Build up behavioral context -Open your app in a browser and browse around for a few minutes. Visit different pages, click some links, and spend time on different sections. The Browser tracker will record these interactions, and Signals will compute your attributes in real time. +Open your app in a browser and browse around for a few minutes. Visit different pages, click some links, and spend time on different sections. Revisit one product page after looking at another, so the activity narrative has a pattern worth noticing. -Open the chat and ask a general question. If your Signals service is returning attributes for your session, the agent's response will reference what you've been doing. +The Browser tracker records these interactions, Signals computes your attributes in real time, and the agentic context buffers the events themselves. + +Open the chat and ask a general question. If Signals is returning context for your session, the agent's response will reference what you've been doing. ## Verify Signals context -You can verify that the app is receiving the Signals context by adding a log to the API route: +You can verify that the app is receiving both kinds of context by adding a log to the API route: ```tsx console.log( @@ -34,9 +36,12 @@ console.log( ); ``` -If the context is empty, check: -* Is your attribute group published? -* Did you create a service with the right name? -* Have you been browsing for long enough for events to flow through the pipeline? +A healthy log contains both sections: the profile attributes as a Markdown list, and the activity narrative wrapped in `[START CONTEXT]` and `[END CONTEXT]`. + +For the profile section, confirm in Console that your attribute group is published and that your service uses the name from the previous step, then browse for long enough that events flow through the pipeline. You can also ask the Snowplow Assistant to confirm both are published. A brand new session shows no profile section at all: the service returns every attribute as `null` until events arrive, and `getProfileSection()` filters those out. + +For the activity section, confirm your agentic context is published, that you selected the `page_view` event when you defined it, and that your events are newer than the `max_age_seconds` you configured. + +Because each fetch is handled independently, one section can appear without the other, so a failure to reach either won't take the agent down. -To rerun the attribute group test query in Console, click **Edit** on your attribute group page > **Run Preview**. +To confirm the tracker and Signals agree on your session, use the [Snowplow Inspector browser extension](/docs/testing/snowplow-inspector/signals-integration/). It shows the events leaving the page alongside the live attribute values Signals holds for your session, which separates a tracking problem from a Signals configuration problem. diff --git a/tutorials/signals-ai-agent-context/vercel-architecture.png b/tutorials/signals-ai-agent-context/vercel-architecture.png deleted file mode 100644 index fec25a401..000000000 Binary files a/tutorials/signals-ai-agent-context/vercel-architecture.png and /dev/null differ