User Preference Recall
Teach your agent to remember what each user cares about. Preferences compound over time into a personalization layer that makes every interaction feel tailor-made.
- Running Dakera server (Quickstart guide)
- An SDK installed: Python, TypeScript, Rust, or Go
- Basic familiarity with agent IDs and memory types
The Problem
Users expect AI assistants to remember them. The moment a user has to re-explain their preferred coding language, their dietary restrictions, or their time zone for the third time, trust in the AI erodes. Stateless AI is frustrating AI.
The naive solution — stuffing the entire chat history into the context window — hits two walls fast: token limits cap the lookback window at a few sessions, and raw chat is noisy signal for preference extraction. What you actually need is a lightweight, high-signal preference layer that accumulates facts about each user and surfaces the most relevant ones on demand.
SQL user tables store structured fields. Preferences are unstructured, fuzzy, and contextual — "prefers concise explanations when stressed" doesn't map to a column. Dakera's hybrid retrieval understands semantic intent alongside exact match, letting you store and retrieve freeform preferences that relational systems cannot.
Architecture: The Preference Accumulation Loop
This pattern gives each user a dedicated preferences agent, such as user-alice-prefs, separate from conversation history. Each agent's memories live in their own namespace, so preferences never compete with chat turns at recall time. Each preference is stored with an importance that reflects how sure you are of it (0.9–1.0 when stated, 0.7–0.89 when inferred). Importance is one term of the recall score and a filter: min_importance=0.8 keeps only the confident ones. Recall blends BM25 keyword matching with vector similarity, so both exact queries ("timezone") and semantic queries ("what hours do they work") return the right memories.
Step-by-Step Implementation
-
Give each user a preferences agentUse a consistent agent ID pattern like
user-{userId}-prefs. Its memories live in their own namespace, isolated from conversation history, which avoids retrieval noise. Never mix preferences with episodic memories in the same agent ID. -
Detect preferences during the conversationAdd a lightweight extraction step after each user message. Look for explicit statements ("I prefer X"), implicit signals ("can you use bullet points?"), and corrections ("actually, I switched to TypeScript"). Assign importance 0.9–1.0 for stated preferences, 0.7–0.89 for inferred ones and 0.5–0.69 for guesses (see the confidence tiers below).
-
Store with categorical tagsTag each preference with a category such as
language,format,timezone,dietary, orcommunication. Tags enable targeted recall — when generating code, only fetchlanguageandformatpreferences, not dietary ones. -
Recall at session start with a broad queryOn every new session, fire a recall with
top_k=8,min_importance=0.8and a generic query like "user preferences and settings". Inject the results into your system prompt before the first user message arrives. -
Handle preference updates and conflictsWhen a user changes a preference, use
update_memoryto overwrite the old value rather than storing a duplicate. To find the memory ID, recall with the category tag (for exampletags=["language"]). If you store a new memory instead, demote the old one below 0.8 withupdate_importanceorforgetit: importance is only one term of the recall score, so a higher importance alone does not make the new memory rank first.
Implementation
# 1. Store a detected preference (stated: 0.9 to 1.0)
curl -X POST http://localhost:3000/v1/memory/store \
-H "Authorization: Bearer dk-..." \
-H "Content-Type: application/json" \
-d '{
"agent_id": "user-alice-prefs",
"content": "User prefers Python over JavaScript for all backend code examples",
"importance": 0.95,
"memory_type": "semantic",
"tags": ["preference", "language", "coding"]
}'
# 2. Store formatting preference
curl -X POST http://localhost:3000/v1/memory/store \
-H "Authorization: Bearer dk-..." \
-H "Content-Type: application/json" \
-d '{
"agent_id": "user-alice-prefs",
"content": "User wants responses as concise bullet points, not paragraphs",
"importance": 0.9,
"memory_type": "semantic",
"tags": ["preference", "format", "communication"]
}'
# 3. Store timezone preference
curl -X POST http://localhost:3000/v1/memory/store \
-H "Authorization: Bearer dk-..." \
-H "Content-Type: application/json" \
-d '{
"agent_id": "user-alice-prefs",
"content": "User is in Pacific timezone (UTC-8), works 9am-6pm PST",
"importance": 0.92,
"memory_type": "semantic",
"tags": ["preference", "timezone", "availability"]
}'
# 4. Recall relevant preferences at session start
curl -X POST http://localhost:3000/v1/memory/recall \
-H "Authorization: Bearer dk-..." \
-H "Content-Type: application/json" \
-d '{"agent_id": "user-alice-prefs", "query": "coding language style preferences", "top_k": 8, "min_importance": 0.8}'
# 5. Update a changed preference in place
curl -X PUT http://localhost:3000/v1/memory/update/{memory_id} \
-H "Authorization: Bearer dk-..." \
-H "Content-Type: application/json" \
-d '{
"agent_id": "user-alice-prefs",
"content": "User switched from Python to TypeScript for new projects (May 2026)",
"importance": 0.95
}'from dakera import DakeraClient
from typing import Optional
client = DakeraClient(base_url="http://localhost:3000", api_key="dk-...")
PREF_CATEGORIES = {
"language": ["python", "javascript", "typescript", "rust", "go", "ruby"],
"format": ["bullet", "concise", "verbose", "markdown", "code block"],
"timezone": ["utc", "pst", "est", "gmt", "cet"],
"communication": ["formal", "casual", "brief", "detailed"],
}
def detect_preference_category(content: str) -> list[str]:
"""Simple heuristic: replace with an LLM classifier in production."""
tags = ["preference"]
content_lower = content.lower()
for cat, keywords in PREF_CATEGORIES.items():
if any(kw in content_lower for kw in keywords):
tags.append(cat)
return tags
def find_preference_id(user_id: str, category: str, query: str) -> Optional[str]:
"""Find the stored preference in one category, to update it in place."""
response = client.recall(agent_id=f"user-{user_id}-prefs", query=query,
top_k=1, tags=[category])
return response.memories[0].id if response.memories else None
def store_preference(
user_id: str,
preference: str,
importance: float = 0.9,
memory_id: Optional[str] = None
) -> str:
"""Store a new preference, or update an existing one. Returns the memory ID."""
agent_id = f"user-{user_id}-prefs"
if memory_id:
# Overwrite the old value, then set its importance
client.update_memory(agent_id=agent_id, memory_id=memory_id, content=preference)
client.update_importance(agent_id=agent_id, memory_ids=[memory_id], importance=importance)
return memory_id
stored = client.store_memory(
agent_id=agent_id,
content=preference,
memory_type="semantic",
importance=importance,
tags=detect_preference_category(preference)
)
return stored["id"]
# Seed Alice's preferences on first interaction
store_preference("alice", "User prefers Python over JavaScript for all backend code", 0.95)
store_preference("alice", "User wants concise bullet-point responses, not paragraphs", 0.9)
store_preference("alice", "User works in Pacific timezone (UTC-8), 9am-6pm PST", 0.92)
store_preference("alice", "User is senior engineer, skip beginner explanations", 0.88)
store_preference("alice", "User prefers async/await patterns over callbacks", 0.87)
# Later: Alice switches languages, so update the record instead of adding a second one
old_id = find_preference_id("alice", "language", "preferred backend language")
store_preference("alice", "User switched from Python to TypeScript for new projects", 0.95, old_id)
def build_system_prompt(user_id: str, task_context: str) -> str:
"""Build a personalized system prompt from stored preferences."""
response = client.recall(
agent_id=f"user-{user_id}-prefs",
query=f"preferences relevant to: {task_context}",
top_k=8,
min_importance=0.8
)
if not response.memories:
return "You are a helpful assistant."
pref_lines = "\n".join(f"- {m.content}" for m in response.memories)
return f"""You are a personalized assistant. Apply these known user preferences:
{pref_lines}
Always follow these preferences without being asked."""
# Use at the start of every session
system = build_system_prompt("alice", "writing Python API code")
print(system)
# Output:
# You are a personalized assistant. Apply these known user preferences:
# - User switched from Python to TypeScript for new projects
# - User wants concise bullet-point responses, not paragraphs
# - User is senior engineer, skip beginner explanations
# ...import { DakeraClient } from '@dakera-ai/dakera';
const BASE_URL = 'http://localhost:3000';
const API_KEY = process.env.DAKERA_API_KEY!;
const client = new DakeraClient({ baseUrl: BASE_URL, apiKey: API_KEY });
interface PreferenceOptions {
importance?: number;
tags?: string[];
memoryId?: string;
}
async function storePreference(
userId: string,
preference: string,
options: PreferenceOptions = {}
): Promise<void> {
const { importance = 0.9, tags = ['preference'], memoryId } = options;
const agentId = `user-${userId}-prefs`;
if (memoryId) {
// PUT /v1/memory/update/{id} changes content, importance and tags in one call
const res = await fetch(`${BASE_URL}/v1/memory/update/${memoryId}`, {
method: 'PUT',
headers: { Authorization: `Bearer ${API_KEY}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ agent_id: agentId, content: preference, importance, tags }),
});
if (!res.ok) throw new Error(`update failed: ${res.status}`);
return;
}
await client.storeMemory(agentId, {
content: preference,
memory_type: 'semantic',
importance,
tags
});
}
// Seed preferences
await storePreference('alice', 'User prefers Python for backend, TypeScript for frontend', {
importance: 0.95,
tags: ['preference', 'language']
});
await storePreference('alice', 'User wants concise bullet-point responses', {
importance: 0.90,
tags: ['preference', 'format']
});
await storePreference('alice', 'User is in Pacific timezone (UTC-8)', {
importance: 0.92,
tags: ['preference', 'timezone']
});
await storePreference('alice', 'User is senior engineer, skip beginner explanations', {
importance: 0.88,
tags: ['preference', 'communication']
});
// Build personalized system prompt at session start
async function buildSystemPrompt(userId: string, taskContext: string): Promise<string> {
const { memories } = await client.recall(`user-${userId}-prefs`, `preferences for: ${taskContext}`, {
top_k: 8,
min_importance: 0.8
});
if (!memories.length) return 'You are a helpful assistant.';
const prefLines = memories.map(m => `- ${m.content}`).join('\n');
return `You are a personalized assistant. Known user preferences:\n${prefLines}\n\nAlways apply these without being asked.`;
}
// Example: fetch only language preferences with a tag-filtered recall
async function getLanguagePrefs(userId: string): Promise<string[]> {
const { memories } = await client.recall(`user-${userId}-prefs`, 'programming language framework preference', {
top_k: 3,
tags: ['language']
});
return memories.map(m => m.content);
}
const system = await buildSystemPrompt('alice', 'writing REST API code');use dakera_client::{DakeraClient, RecallRequest, StoreMemoryRequest};
let client = DakeraClient::builder("http://localhost:3000").api_key("dk-...").build()?;
// Store preferences at the stated tier (0.9 to 1.0)
let prefs = [
("User prefers Python over JavaScript for all backend code", 0.95, "language"),
("User wants concise bullet-point responses, not paragraphs", 0.90, "format"),
("User is in Pacific timezone (UTC-8), works 9am-6pm PST", 0.92, "timezone"),
];
for (content, importance, category) in prefs {
client.store_memory(
StoreMemoryRequest::new("user-alice-prefs", content)
.with_importance(importance)
.with_tags(vec!["preference".into(), category.into()]),
).await?;
}
// Recall at session start. The Rust docs show no min_importance builder;
// send min_importance over REST if you need the 0.8 floor here.
let prefs = client.recall(
RecallRequest::new("user-alice-prefs", "coding language style format preferences").with_top_k(8),
).await?;
// Build system prompt from recalled preferences
let pref_text = prefs.memories
.iter()
.map(|m| format!("- {}", m.content))
.collect::<Vec<_>>()
.join("\n");
let system_prompt = format!(
"You are a personalized assistant. Known user preferences:\n{}\n\nApply these at all times.",
pref_text
);package main
import (
"context"
"fmt"
"log"
"os"
"strings"
"github.com/dakera-ai/dakera-go"
)
func main() {
client := dakera.NewClientWithOptions(dakera.ClientOptions{
BaseURL: "http://localhost:3000",
APIKey: os.Getenv("DAKERA_API_KEY"),
})
ctx := context.Background()
// Store preferences by confidence tier
prefs := []struct {
content string
importance float32
tags []string
}{
{"User prefers Python for backend, TypeScript for frontend", 0.95, []string{"preference", "language"}},
{"User wants concise bullet-point responses", 0.90, []string{"preference", "format"}},
{"User is in Pacific timezone (UTC-8)", 0.92, []string{"preference", "timezone"}},
{"User is senior engineer, skip beginner explanations", 0.88, []string{"preference", "communication"}},
}
for _, p := range prefs {
imp := p.importance
if _, err := client.StoreMemory(ctx, "user-alice-prefs", dakera.StoreMemoryRequest{
Content: p.content,
Importance: &imp,
Tags: p.tags,
}); err != nil {
log.Fatal(err)
}
}
// Recall at session start. The Go docs show no MinImportance field;
// send min_importance over REST if you need the 0.8 floor here.
recalled, err := client.Recall(ctx, "user-alice-prefs", dakera.RecallRequest{
Query: "coding language style preferences",
TopK: 8,
})
if err != nil {
log.Fatal(err)
}
lines := make([]string, 0, len(recalled.Memories))
for _, m := range recalled.Memories {
lines = append(lines, "- "+m.Content)
}
systemPrompt := fmt.Sprintf(
"You are a personalized assistant. Known preferences:\n%s\n\nApply these without being asked.",
strings.Join(lines, "\n"),
)
_ = systemPrompt
}Ready to make your AI remember every user?
Spin up Dakera and start storing preferences immediately.
Before & After: Memory State
Here is the concrete difference between a session with and without the User Preference Recall pattern. The left column shows raw agent state (no memory). The right column shows the preference store after 3 interactions.
{
// Agent has no persistent state.
// Every session starts blank.
// User must re-state preferences
// every single conversation.
agent_id: "user-alice",
memories: [],
// Result: frustrating repetition
// "As I mentioned before..."
// "I already told you I use Python"
}
{
"agent_id": "user-alice-prefs",
"memory_count": 5,
"memories": [
{
"content": "Prefers Python for backend",
"importance": 0.95,
"tags": ["preference","language"],
"memory_type": "semantic"
},
{
"content": "Wants bullet-point responses",
"importance": 0.90,
"tags": ["preference","format"]
},
{
"content": "Pacific timezone (UTC-8)",
"importance": 0.92,
"tags": ["preference","timezone"]
},
{
"content": "Senior engineer, skip basics",
"importance": 0.88,
"tags": ["preference","communication"]
},
{
"content": "Prefers async/await patterns",
"importance": 0.87,
"tags": ["preference","language"]
}
]
}
Real-World Example: E-Commerce Recommendation Agent
Scenario: ShopFlow AI, a fashion e-commerce platform, deploys an AI shopping assistant. Without preference recall, every product recommendation session asks the same sizing questions. With this pattern, the agent accumulates a preference profile over the customer's first 3 interactions.
Customer Journey: First 3 Sessions
Session 1 (Day 1): Customer says "I usually wear medium in European sizing and I'm looking for sustainable brands." The agent stores: EU size Medium, prefers sustainable fashion brands (importance: 0.92).
Session 2 (Day 3): "Can you only show me items under $150?" stored as: Budget ceiling $150 for clothing (importance: 0.9). Customer mentions they hate synthetic fabrics: Prefers natural fabrics: cotton, linen, wool (importance: 0.91).
Session 3 (Day 10): Customer asks about winter coats. At session start the agent recalls all three stored preferences and filters the catalog to EU Medium, sustainable brands, natural fabrics, under $150. No sizing or budget questions asked.
Use importance scores as confidence tiers. Explicitly stated preferences get 0.9–1.0. Inferred from behavior get 0.7–0.89. Guessed from context get 0.5–0.69. When recalling, set min_importance=0.8 to only include high-confidence facts in the system prompt. Lower confidence preferences can be surfaced as suggestions ("Based on past orders, you might prefer...").
Preference Signal Flow: From Behavior to Personalization
This diagram shows how raw user behavior is converted into structured preference memories that feed back into personalized agent responses.
Performance Characteristics
Preference recall is one request at session start. Run it alongside the rest of your session initialization rather than as a blocking dependency where you can. Under load, Dakera runs a limited number of recalls per core at once and queues the rest; a recall that waits more than 20 s gets a 503 with Retry-After, so keep the default system prompt ready as a fallback.
Edge Cases & Developer Gotchas
If a user stored "prefers JavaScript" two months ago and switched to TypeScript last week, the old memory may still outrank newer inferences if both have equal importance. Solution: When detecting a preference change, update the old entry in place with update_memory, or demote it below the 0.8 floor with update_importance, rather than just adding the new one. A higher importance on the new entry alone is not enough, because relevance carries most of the recall score.
Storing both preferences and conversation history under the same agent ID causes recall to mix them together. A query for "language preference" may return last week's Python debugging chat instead of the stored preference. Solution: Always use a separate agent ID suffix like -prefs for preferences. Keep episodic conversation memories under a different agent ID like user-alice-history.
Using top_k=3 for a general "what are this user's preferences?" query may return 3 language preferences and miss timezone and format preferences entirely, because they are all semantically similar. Solution: Use top_k=8 or higher for the session-start recall, issue targeted recalls by category tag, or list the whole profile by tag with batch_recall (see Advanced Configuration). The extra results only cost prompt tokens.
For brand-new users, recall will return an empty array. Your code must handle this gracefully — do not crash or inject an empty preferences block into the system prompt. Solution: Always have a default system prompt fallback. Only inject preferences when memories.length > 0. Consider adding a "getting to know you" flow for first-time users that proactively asks for key preferences.
A user who prefers "detailed explanations" for learning sessions but "bullet points" for production code questions may have conflicting memories. Both are valid — they just apply in different contexts. Solution: Store context metadata with each preference (e.g., tag with context:learning or context:production) and filter by context tag during recall.
Dakera lowers the importance of memories that are not accessed (half-life 168 hours by default; semantic memories decay at half that speed), and a recall only resets the timer for the memories it returns. A preference that rarely makes the top 8 can drift below min_importance=0.8 and drop out of the system prompt. Solution: Set semantic_decay to flat in the memory policy of the preferences agent's namespace (set_memory_policy in Python, or PUT on the namespace's memory_policy route) so preferences keep the importance you gave them, or re-assert importance with update_importance when the user confirms a preference.
SDK Reference
| Operation | Python | TypeScript | Purpose |
|---|---|---|---|
| Store preference | client.store_memory(agent_id, content, importance, memory_type, tags) |
client.storeMemory(agentId, {content, importance, memory_type, tags}) |
Persist a new preference |
| Recall preferences | client.recall(agent_id, query, top_k, min_importance, tags) |
client.recall(agentId, query, {top_k, min_importance, tags}) |
Retrieve top preferences by relevance |
| List by tag (no query) | client.batch_recall(BatchRecallRequest(agent_id, filter=BatchMemoryFilter(...), limit)) |
client.batchRecall({agent_id, filter}) |
Load every preference in a category |
| Filtered search | client.search_memories(agent_id, query, memory_type, top_k) |
POST /v1/memory/search |
Search one agent's memories with type and tag filters |
| Update preference | client.update_memory(agent_id, memory_id, content) |
PUT /v1/memory/update/{id} |
Overwrite a changed preference |
| Re-rank preference | client.update_importance(agent_id, memory_ids, importance) |
POST /v1/memory/importance |
Boost or demote a preference's rank |
| Delete preference | client.forget(agent_id, memory_id) |
client.forget(agentId, memoryId) |
Remove a preference (e.g., user revoked it) |
| Get single preference | client.get_memory(agent_id, memory_id) |
client.getMemory(agentId, memoryId) |
Fetch a specific preference by ID |
Advanced Configuration
TTL-Based Preference Expiry
For time-sensitive preferences — like a user's interest in a sale event — use ttl_seconds to auto-expire them. Permanent preferences like language choice should have no TTL.
# Expire sale interest after 7 days
client.store_memory(
agent_id="user-alice-prefs",
content="User is actively shopping for winter coats (sale season)",
importance=0.75,
memory_type="semantic",
tags=["preference", "seasonal", "shopping"],
ttl_seconds=604800 # 7 days
)
Load the Whole Profile With Batch Recall
Batch recall lists memories by filter, with no query and no embedding. When several agents need the same user's confident preferences (e.g., a recommendation agent and a support agent), each can load the full set by tag instead of hoping a semantic query ranks every category into the top results:
from dakera import BatchMemoryFilter, BatchRecallRequest
resp = client.batch_recall(BatchRecallRequest(
agent_id="user-alice-prefs",
filter=BatchMemoryFilter(tags=["preference"], min_importance=0.8),
limit=50,
))
print(resp.filtered, "confident preferences")
Preference Namespacing Strategy
For multi-tenant SaaS products, put the tenant in the agent ID to keep user preferences isolated:
# Pattern: {tenant}-{userId}-prefs
agent_id = "tenant-acme-user-alice-prefs"
# Each agent's memories live in _dakera_agent_{agent_id}. For a hard
# tenant boundary on the server, pin the tenant's API key to the
# prefix pattern _dakera_agent_tenant-acme-* (see Namespace Isolation)
When to Use This Pattern
- Personalized AI assistants serving returning users more than once
- E-commerce recommendation engines learning taste over time
- Customer support bots that remember ticket history and preferences
- Coding assistants that remember language, framework, and style choices
- Health or wellness apps remembering dietary restrictions and goals
- Any product where "the AI remembers you" is a differentiating feature
Run this pattern against a real Dakera.
The Playground executes live API calls in your browser. No account, no install. Then run it on your own server in five minutes.