Level 2: Persona Services

Sentiments

Sentiments are the persistent preference records that let an external app turn raw interactions into an evolving taste model.

What Gets Stored

If you build a Level 2 layer on top of Prism and Mosaic, sentiments are the records you keep when a user expresses preference about a product, store, or other supported object. These records should also carry source metadata so your system knows where the signal came from.

  • Object identity such as product, store, or other supported object types
  • Sentiment type such as like, dislike, want, or have
  • Source context like import source, timestamp, and payload metadata
  • Current-state semantics so the platform can preserve history while exposing the latest signal

Why It Matters

Sentiment should do more than feed analytics. In a mature Level 2 setup, new sentiment activity can trigger debounced window generation or refresh flows, which means preference updates feed directly into downstream curation surfaces.

TypeScript Example

Prism and Mosaic do not maintain your sentiment ledger for you. A typical external setup is to store sentiments in your own backend, then use those records to decide when to regenerate windows.

type SentimentType = "like" | "dislike" | "want" | "have";
type SentimentObjectType = "product" | "store";

type CreateSentimentInput = {
  userId: string;
  objectId: string;
  objectType: SentimentObjectType;
  sentimentType: SentimentType;
  objectDomain?: string;
  productUrl?: string;
  source?: {
    type: "ui" | "import" | "sync";
    id?: string;
    ts?: number;
    payload?: Record<string, unknown>;
  };
};

type SentimentRecord = CreateSentimentInput & {
  id: string;
  createdAt: string;
  isCurrent: boolean;
};

async function recordSentiment(input: CreateSentimentInput): Promise<SentimentRecord> {
  const response = await fetch("/api/sentiments", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      user_id: input.userId,
      object_id: input.objectId,
      object_type: input.objectType,
      sentiment_type: input.sentimentType,
      object_domain: input.objectDomain,
      product_url: input.productUrl,
      source_type: input.source?.type,
      source_id: input.source?.id,
      source_ts: input.source?.ts,
      object_payload: input.source?.payload
    })
  });

  if (!response.ok) {
    throw new Error(`Failed to record sentiment: ${response.status}`);
  }

  return response.json();
}

Using Sentiment To Refresh Windows

The useful pattern is to keep the user interaction fast, then kick off asynchronous regeneration after the sentiment is safely stored.

async function handleProductLike(userId: string, product: {
  id: string;
  domain: string;
  url: string;
}) {
  await recordSentiment({
    userId,
    objectId: product.id,
    objectType: "product",
    sentimentType: "like",
    objectDomain: product.domain,
    productUrl: product.url,
    source: {
      type: "ui",
      ts: Date.now(),
      payload: { action: "heart_button" }
    }
  });

  void fetch("/api/windows/regenerate", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      userId,
      seedTypes: ["sentiment_cluster", "taste_cluster"]
    })
  });
}

Practical Guidance

  • Record sentiment as close to user intent as possible
  • Include source metadata when the signal came from another surface or ingestion pipeline
  • Treat deletion as a state transition, not a destructive erase, when auditability matters
  • Use sentiment streams to refresh windows asynchronously rather than blocking the interaction that created the signal

Key Point

The practical takeaway is that sentiments are one of the strongest bridges between interaction data and generated windows. Treat them as a core Level 2 primitive, not just a reaction badge layered on top of search.