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
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.
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"]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 resultNamed 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.
AI SDK
Tool-loop orchestration, bounded steps and typed model outputs.
OpenRouter
A server-side model gateway with an approved model and explicit timeout, cost and fallback policy.
Zod
Validation for requests, interpretations, tool inputs and public responses.
Postgres
Catalog mirror, search sessions, policy versions, evaluation cases and audit events.
Web Hoster / Cloud Infrastructure
Application runtime, same-origin API, secret injection, health checks and rollback.
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.
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.examplenpm install ai @ai-sdk/openai-compatible zod pg
npm install -D typescript tsx vitest @types/node# 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_integerCatalog, 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.
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"
});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)
})
});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[]>;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.
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.
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.
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.Only from an approved catalog-tool result.
Compatibility, policy and inventory enforced deterministically.
Documented weights for words, attributes, availability and business priority.
Describe ranking evidence without invented facts or hidden reasoning.
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.
{
"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"
}Correction
Reinterpret and rerun the same gates.
Uncertainty
Ask one focused question instead of displaying false confidence.
Zero results
Explain the boundary and offer a useful refinement.
Memory
Store safe context only, never full sensitive identifiers.
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.
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" }
});
}
}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.
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);
});Relevant approved products divided by all displayed products.
No displayed ID exists outside a tool result.
Share of questions that produce a better grounded result.
Same constraints, meaning and products in Arabic and English.
p50 and p95 per stage and for the whole request.
Share of requests served by deterministic fallback.
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.
# 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.
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.
