The same question: what evidence made the system say this? “Try more Reels” is easy to display. It is harder to explain whether that came from an owner’s explicit preference, a pattern in past posts, or an outcome from a recommendation they already accepted.
That question shaped how I built SocialPulse’s memory layer. I use Hindsight [https://github.com/vectorize-io/hindsight]
to retain brand-specific preferences, performance observations, and recommendation outcomes, then recall them when generating new options. The point is not to make the system remember everything. It is to make the recommendation traceable to the right kind of history—and to keep observations from masquerading as instruction. The first time I looked at a content recommendation, I kept asking.
## A small system with an explicit memory boundary
SocialPulse is split into a React frontend and a FastAPI backend. The backend owns the domain flow: SQLAlchemy models hold brands, posts, metrics, and recommendation history; services.py calculates engagement analytics and builds recommendations; main.py exposes routes for learning from posts, generating recommendations, recording feedback, and browsing memory. The frontend has separate pages for analytics, recommendations, and memory exploration, so the evidence is visible alongside the suggested action.
The central path is straightforward. I calculate engagement observations from a brand’s posts, retain th

ose observations in Hindsight, and later recall memories for a recommendation query. The recommendation function receives brand details, current analytics, retrieved memories, and prior recommendations. That separation matters: Hindsight provides persistent memory; SocialPulse applies the product’s interpretation rules.
The integration uses one Hindsight bank per brand. A memory for one brand should not leak into another brand’s recommendations. The service constructs bank IDs from the brand ID and passes a context label when retaining each memory. That gives memory a useful boundary and source category instead of treating it as one application-wide text pile.
## I wanted memory to preserve the reasoning
The learning route does more than save a label like “Reels are best.” It calculates a sample size and date range, then writes an observation that names the winning format, its engagement rate, the overall average, and the historical sample. It also explicitly says the correlation does not prove causation.
obs_text = (
f"Historical observation:\n"
f"Across {sample_size} analyzed posts from {date_range}, {obs}\n"
f"This is an observed historical correlation and does not prove causation."
)
memory.retain(
brand_id,
obs_text,
"Performance learning",
brand_name=b.name,
)
That is the most important decision in the integration. Without the sample and time range, the stored sentence can travel farther than its evidence deserves. If a later run recalls “Reels had the highest observed average,” it should also have a way to recover how many posts were analyzed and which dates they covered.
I keep owner intent in a different category. When someone provides feedback, the route retains it as an owner preference, rather than silently blending it with engagement statistics:
memory.retain(
brand_id,
f"Owner preference:\n{act.feedback}",
"Owner feedback",
brand_name=brand_name,
)
That distinction is not cosmetic. If an owner says “avoid promotional posts,” that is a constraint about what they want. If promotional posts happened to have lower average engagement in a dataset, that is an observation. The code parses those categories separately so the recommendation logic can give them different treatment. People should be able to override a historical correlation; the system should not pretend those two inputs mean the same thing.
Recall is only useful when the next step is inspectable
When a recommendation request arrives, the route recalls history using the brand and the user’s query. It also fetches current analytics and recent recommendation history. Those inputs go together into generate_recommendations:
memories_list = memory.recall(
brand_id,
query=req.query,
brand_name=b.name,
)
if not memories_list:
memories_list = memory.list(brand_id)
The recall query is expanded with the kinds of context SocialPulse cares about: brand preferences, previous decisions, owner feedback, performance observations, recommendation outcomes, and strategy learnings. On the service side, recalled text is deduplicated and categorized before recommendation options are assembled. The result includes a memory_used explanation for each option, which makes the memory influence visible in the UI.
This is where I find Hindsight most useful. It gives SocialPulse persistent, queryable context across sessions, but the application still owns the policy. A recalled preference can shape the output; an observed format-performance relationship can inform it; a prior recommendation outcome can help avoid repeating a strategy. None of those should become an invisible instruction that cannot be inspected. The Hindsight documentation [https://hindsight.vectorize.io/]
describes the retain and recall operations behind this pattern,
while *Vectorize guide to agent memory *[https://vectorize.io/what-is-agent-memory]
provides broader context for why persistent memory matters in an agent-like workflow.
A recommendation has more than one kind of evidence
Consider a brand whose historical posts show a higher observed engagement rate for educational Reels. The owner also says they want to avoid promotional messaging. Later, they ask what to publish tomorrow. SocialPulse can retrieve both signals, look at the current analytics, and generate a small set of options that respects the stated preference while using the historical pattern as evidence.
If the owner accepts one option and provides feedback, SocialPulse stores that feedback and recommendation outcome. On the next request, those memories join the history. The interaction is not “the system learned that Reels are good.” It is closer to: the system observed a pattern in a dated sample, the owner expressed a constraint, and an earlier recommendation produced an outcome. That is a much more useful basis for a conversation about what to try next.
The interface reinforces this by showing analytics, recommendation history, and memory details rather than hiding them behind one generated paragraph. A developer or user can inspect which memory backend is active and what evidence was applied. That visibility matters because the service supports Hindsight Cloud and a local JSON fallback. Cloud credentials are configured through environment settings; failures during initialization or operations fall back locally. That is practical for continuity, but it means an operator must pay attention to backend status and persistence semantics. A local fallback is useful for development and resilience; it is not automatically equivalent to a shared cloud bank in a multi-instance deployment.
The tradeoffs I had to make explicit
The analytics are descriptive. Engagement rate is computed from likes, comments, shares, and saves divided by reach. Formats and categories are grouped, and their average rates are compared. The code includes an explicit caveat that these historical observations do not prove the format caused the result. I want that caveat to remain part of the product, because a neat ranking can otherwise imply more certainty than the data provides.
Memory introduces a second quality problem: retrieval can find something relevant-sounding that is stale or weakly supported. Keeping the sample size and date range in the retained text helps, and separating preferences from observations helps even more. Still, teams need retention, deletion, and versioning policies once memory is persistent and user-specific. Those are operational parts of the design, not details to postpone until after recommendations ship.
The local fallback has a related boundary. It is keyed by brand and writes to a JSON file. That is simple for one process, but production deployment needs deliberate choices about concurrent writes, durable storage, backup, and consistent behavior across replicas. The Hindsight bank abstraction gives the cloud path a cleaner per-brand boundary; the fallback should be treated as a distinct storage mode and surfaced as such.
What I learned
- Persist evidence with the conclusion. Sample size and date range make a recalled observation more interpretable.
- Keep user intent separate from measured behavior. A preference is a constraint; a metric pattern is evidence.
-
Make memory’s influence visible. An explanation such as
memory_usedgives people a way to inspect the connection between recall and recommendation. - Treat fallback storage as a real architecture choice. Local JSON and shared cloud memory have different durability and concurrency properties.
- Keep claims proportional to the analysis. Historical averages can guide what to test next; they do not establish causation.
Hindsight did not make SocialPulse’s recommendations trustworthy by itself. It made it possible to carry relevant history forward and gave me a place to distinguish what the owner asked for from what the data happened to show. That distinction is the foundation of a recommendation I can explain—and a system I can improve when the next result contradicts the last one.
[http://Code.in]






Top comments (0)