Blueprint 003 preview showing an agentic search engine grounded in an approved store catalog
Ting Blueprint · 003

Build an Agentic Search Engine for Any Store

An implementation-grade reference for multilingual store search that interprets natural language, calls grounded catalog tools, applies deterministic business rules, asks useful follow-ups and returns auditable ranked products.

Difficulty
Advanced
Estimated build time
5–10 days
Architecture type
Tool-using grounded search
Updated
August 2026
Language
English
On this page
Reference boundary

A reusable search engine, not a store-specific build

This Blueprint covers the critical path for any catalog-backed store: understand an English or Arabic request, preserve safe session context, call approved tools, apply deterministic catalog and policy rules, ask for missing details, and return traceable ranked products. The records are fictional and the architecture is a reference implementation.

Included

  • Natural-language product, SKU and attribute search
  • English and Arabic request handling
  • Grounded catalog, inventory and compatibility tools
  • Deterministic filtering, ranking and clarification

Not included

  • Checkout, payment or order creation
  • A live VIN, identity or customer-profile service
  • A mechanical, medical or safety diagnosis
  • Ting prompts, private integrations or production scoring
Control flow

Interpret, retrieve, validate and rank

The language model converts a shopper request into a validated contract and chooses from a small allowlist of tools. Catalog results are then validated, filtered and ranked by application code. Every customer-visible product ID must originate in a tool result.

Agentic store-search workflow from shopper request through interpretation, catalog tools and business rules to grounded ranked products.
Swipe horizontally to explore the full workflow.A store-neutral reference map; the architecture and state diagrams below define the executable behavior.
MermaidInterpret, retrieve, validate and rank
01Shopper Request
02Interpret and Clarify
03Approved Catalog Tools
04Rules and Ranking
05Grounded Results
View Mermaid source
flowchart LR
    Q["Shopper request"] --> I["Language, intent & entities"]
    I --> C{"Enough context?"}
    C -- "No" --> F["Focused follow-up"]
    F --> I
    C -- "Yes" --> T["Approved catalog tools"]
    T --> G["Eligibility & business rules"]
    G --> R["Deterministic ranking"]
    R --> O["Grounded products & next action"]
MermaidSearch state transitions
01Interpreting
02Retrieving
03Validating
04Ranking
05Responding
View Mermaid source
stateDiagram-v2
    [*] --> IDLE
    IDLE --> INTERPRETING
    INTERPRETING --> NEEDS_CLARIFICATION: missing detail
    NEEDS_CLARIFICATION --> INTERPRETING: shopper correction
    INTERPRETING --> RETRIEVING: valid contract
    RETRIEVING --> VALIDATING
    VALIDATING --> RANKING: grounded candidates
    VALIDATING --> NEEDS_CLARIFICATION: uncertain fit
    RANKING --> RESPONDING
    RESPONDING --> COMPLETE
    INTERPRETING --> FALLBACK: provider or schema failure
    RETRIEVING --> FALLBACK: timeout
    FALLBACK --> COMPLETE: deterministic result
    FALLBACK --> FAILED: no safe result
Educational choices

Named reference stack

The reference uses AI SDK for the tool loop, OpenRouter as a server-side model gateway, Zod for every boundary, Postgres for catalog/search state, and a Web Hoster / Cloud Infrastructure for the application runtime. These are educational choices, not claims about Ting’s production stack.

OpenRouter

A server-side model gateway with an approved model and explicit timeout, cost and fallback policy.

Official documentation

Postgres

Catalog mirror, search sessions, policy versions, evaluation cases and audit events.

Official documentation

Web Hoster / Cloud Infrastructure

Application runtime, same-origin API, secret injection, health checks and rollback.

Official documentation
Startable project

Repository, install and environment

Keep provider access and business adapters under server-only modules. The browser sends a bounded request to one same-origin endpoint; it never receives provider credentials, private traces or unrestricted catalog access.

Project treeTEXT
agentic-store-search/
├─ app/
│  ├─ api/search/route.ts
│  └─ search/page.tsx
├─ src/search/
│  ├─ agent.ts
│  ├─ contracts.ts
│  ├─ state.ts
│  ├─ ranking.ts
│  ├─ safety.ts
│  ├─ observability.ts
│  ├─ providers/openrouter.ts
│  └─ tools/
│     ├─ catalog.ts
│     ├─ inventory.ts
│     └─ compatibility.ts
├─ data/
│  ├─ catalog.fixture.json
│  └─ evaluation-cases.jsonl
├─ tests/
│  ├─ grounding.test.ts
│  ├─ ranking.test.ts
│  └─ locale-parity.test.ts
└─ .env.example
Install packagesSHELL
npm install ai @ai-sdk/openai-compatible zod pg
npm install -D typescript tsx vitest @types/node
.env.exampleENV
# Placeholder values only. Keep every secret server-side.
OPENROUTER_API_KEY=replace_with_server_only_key
OPENROUTER_MODEL=replace_with_approved_model_id
DATABASE_URL=postgres://user:password@host:5432/store_search
CATALOG_SOURCE_URL=https://catalog.example.invalid/api
SEARCH_REQUEST_TIMEOUT_MS=28000
SEARCH_MAX_TOOL_STEPS=7
SEARCH_DAILY_TOKEN_BUDGET=replace_with_integer
Typed truth

Catalog, interpretation and state contracts

The catalog schema defines what the engine may say. The interpretation schema defines what the model may request. A finite state model makes clarification, fallback and failure visible instead of hiding them in prose.

Product schema and fictional recordTYPESCRIPT
import { z } from "zod";

export const productSchema = z.object({
  id: z.string(),
  sku: z.string(),
  name: z.string(),
  locale: z.enum(["en", "ar"]),
  category: z.string(),
  attributes: z.record(z.string(), z.string()),
  price: z.number().nonnegative(),
  currency: z.string().length(3),
  availability: z.enum(["in_stock", "low_stock", "out_of_stock"]),
  compatibilityKeys: z.array(z.string()).default([]),
  sourceUpdatedAt: z.string().datetime()
});

// Fictional reference record — never a claim about a real store.
export const exampleProduct = productSchema.parse({
  id: "prd_demo_001", sku: "DEMO-FLT-001",
  name: "Premium Cabin Filter", locale: "en",
  category: "Service Parts", attributes: { size: "standard" },
  price: 89, currency: "SAR", availability: "in_stock",
  compatibilityKeys: ["sedan-2021-2.5"],
  sourceUpdatedAt: "2026-08-01T09:00:00.000Z"
});
Request interpretation contractTYPESCRIPT
export const interpretationSchema = z.object({
  language: z.enum(["en", "ar"]),
  intent: z.enum([
    "find_product", "exact_sku", "compare", "filter", "unknown"
  ]),
  querySummary: z.string().max(220),
  entities: z.object({
    productName: z.string().nullable(),
    sku: z.string().nullable(),
    category: z.string().nullable(),
    model: z.string().nullable(),
    year: z.number().int().nullable(),
    priceMax: z.number().positive().nullable(),
    availableOnly: z.boolean()
  }),
  missingInformation: z.array(z.string()).max(6),
  requestedTools: z.array(z.enum([
    "searchCatalog", "findExactSku", "checkInventory",
    "verifyCompatibility", "applyFilters"
  ])).min(1).max(5),
  nextQuestion: z.object({
    type: z.enum(["single_choice", "free_text", "none"]),
    text: z.string().max(220),
    options: z.array(z.string().max(80)).max(6)
  })
});
Finite state modelTYPESCRIPT
export type SearchState =
  | "idle" | "interpreting" | "needs_clarification"
  | "retrieving" | "validating" | "ranking"
  | "responding" | "complete" | "fallback" | "failed";

export const legalTransitions = {
  idle: ["interpreting"],
  interpreting: ["needs_clarification", "retrieving", "fallback"],
  needs_clarification: ["interpreting", "complete"],
  retrieving: ["validating", "fallback"],
  validating: ["ranking", "needs_clarification"],
  ranking: ["responding"],
  responding: ["complete"],
  fallback: ["retrieving", "complete", "failed"],
  complete: [], failed: []
} satisfies Record<SearchState, SearchState[]>;
Tools before claims

Grounded retrieval and memory rules

Each tool returns only approved fields. Session memory keeps the minimum useful context—locale, prior query, chosen attributes and masked identifiers—and expires on a documented schedule. Product copy is untrusted data and never becomes an instruction.

src/search/tools/catalog.tsTYPESCRIPT
export async function searchCatalog(input: {
  query: string; category?: string; limit?: number;
}) {
  const rows = await catalogRepository.search({
    query: input.query,
    category: input.category,
    limit: Math.min(input.limit ?? 18, 30)
  });

  // The model receives approved facts, never unrestricted database access.
  return rows.map((row) => ({
    id: row.id, sku: row.sku, name: row.name,
    category: row.category, price: row.price,
    currency: row.currency, availability: row.availability
  }));
}
  • Allow only named tools with validated inputs.
  • Persist the minimum useful session memory and expire it.
  • Treat HTML and product text as untrusted data.
  • Never show a product whose ID did not appear in a tool result.
Deterministic commerce

Ranking boundaries and corrections

The model can identify intent and missing information; application code owns eligibility, compatibility, price, stock, policy filters and the final score. A correction starts a new interpreted turn, preserves the prior request for audit, and reruns the same deterministic gates.

src/search/ranking.tsTYPESCRIPT
export function rankCandidates(candidates, request, rules) {
  return candidates
    .filter((item) => rules.allowedProductIds.has(item.id))
    .filter((item) => !rules.incompatibleProductIds.has(item.id))
    .map((item) => ({
      ...item,
      score:
        lexicalScore(item, request.query) * 0.45 +
        attributeScore(item, request.entities) * 0.30 +
        availabilityScore(item.availability) * 0.15 +
        businessPriority(item.id, rules) * 0.10
    }))
    .sort((a, b) => b.score - a.score || a.sku.localeCompare(b.sku));
}

// The language model may interpret intent. It may not invent candidates,
// compatibility, stock, price, eligibility, or the final numeric score.
Candidates

Only from an approved catalog-tool result.

Exclusion

Compatibility, policy and inventory enforced deterministically.

Score

Documented weights for words, attributes, availability and business priority.

Explanation

Describe ranking evidence without invented facts or hidden reasoning.

Public contract

Structured response and traceability

The API returns concise customer copy, grounded product IDs, applied rules, fallback state and a safe trace identifier. Hidden reasoning, provider keys, full personal identifiers and private model metadata never reach the client.

Public response contractJSON
{
  "requestId": "req_demo_7f3a",
  "state": "complete",
  "language": "en",
  "answer": {
    "headline": "I found three grounded options.",
    "body": "Results match the approved catalog and current filters.",
    "question": "",
    "requiresClarification": false
  },
  "productIds": ["prd_demo_001", "prd_demo_014"],
  "appliedRules": ["available_only", "compatibility_required"],
  "fallbackUsed": false,
  "traceId": "trace_demo_91c2"
}
01

Correction

Reinterpret and rerun the same gates.

02

Uncertainty

Ask one focused question instead of displaying false confidence.

03

Zero results

Explain the boundary and offer a useful refinement.

04

Memory

Store safe context only, never full sensitive identifiers.

Fail safely

Server handler and transparent fallback

Requests are schema-validated, time-bounded and never cached. Provider or schema failure routes to deterministic lexical/attribute search; the UI labels degraded behavior without blocking the shopper or pretending the agent completed a live tool run.

POST /api/searchTYPESCRIPT
export async function POST(request: Request) {
  const abort = AbortSignal.timeout(28_000);
  const input = searchRequestSchema.parse(await request.json());

  try {
    const result = await runAgenticSearch(input, { signal: abort });
    return Response.json(publicSearchResponseSchema.parse(result), {
      headers: { "Cache-Control": "no-store" }
    });
  } catch (error) {
    const fallback = await runDeterministicSearch(input);
    return Response.json({ ...fallback, fallbackUsed: true }, {
      status: 200,
      headers: { "Cache-Control": "no-store" }
    });
  }
}
Evidence before launch

Evaluation dataset, metrics and regression tests

Use bilingual fixtures for exact SKU, synonyms, typos, follow-ups, missing attributes, unsafe product text and provider failures. Track grounded-result precision, invalid-product rate, clarification usefulness, zero-result rate, latency, fallback rate and locale parity.

Critical regression casesTYPESCRIPT
const cases = [
  ["exact SKU", "DEMO-FLT-001", ["prd_demo_001"]],
  ["Arabic synonym", "فلتر المكيف", ["prd_demo_001"]],
  ["out-of-stock filter", "available only", []],
  ["explicit incompatibility", "sedan-2022", []],
  ["prompt injection in product text", "ignore rules", []],
  ["provider timeout", "fallback", ["deterministic"]]
];

test.each(cases)("%s remains grounded", async (_name, query, expected) => {
  const result = await runFixture(query);
  expect(result.productIds).toEqual(expected);
  expect(result.productIds.every(id => fixtureCatalog.has(id))).toBe(true);
});
Grounded precision

Relevant approved products divided by all displayed products.

Invalid-product rate

No displayed ID exists outside a tool result.

Clarification usefulness

Share of questions that produce a better grounded result.

Locale parity

Same constraints, meaning and products in Arabic and English.

Latency

p50 and p95 per stage and for the whole request.

Fallback rate

Share of requests served by deterministic fallback.

Production ownership

Deployment, persistence and observability

Persist request state only when continuity requires it; inject secrets at runtime; cap rate, token and tool budgets; expose health and readiness checks; redact logs; and assign owners for the catalog adapter, ranking policy, incident response and rollback.

Build, health checks and rollbackSHELL
# Web Hoster / Cloud Infrastructure
npm run build
npm run test

# Required runtime checks
curl -f https://store.example.invalid/api/health
curl -f https://store.example.invalid/api/search/readiness

# Rollback: redeploy the prior immutable release, then replay the
# evaluation set before restoring normal traffic.

Persistence

Postgres for continuity-required sessions, policy versions and audit events.

Limits

Request rate, input size, tokens, tool steps and timeout.

Observability

Stage latency, errors, fallback, zero results and ranking drift.

Secrets

Runtime injection with rotation and full redaction from logs and browser.

Ownership

Owners for catalog, ranking, incidents, rollback and Arabic review.

Production gate

Safety and readiness checklist

Launch only when grounded IDs, business-rule ownership, Arabic parity, privacy controls, accessibility, monitoring and rollback have named evidence. Safety-critical categories must require stronger confirmation and must never be presented as diagnosis.

Grounded IDs

Every displayed product came from a tool.

Rules outside the model

Price, stock, compatibility and policy are deterministic.

Privacy

Minimum data with masking and explicit expiry.

Untrusted input

Product text never becomes an instruction.

Tested rollback

Prior release and evaluation replay are ready.

Human ownership

Business owners approve rules and sensitive categories.