The AI layer today is two disconnected islands: a reactive chat agent (stateless between conversations) and batch meeting notes extraction (results sit passively in JSONB). The agent can't recall that "in last Tuesday's meeting, the client mentioned wanting to retire at 58" when the adviser later asks about retirement planning in chat. Every conversation starts from zero.
Agent Memory bridges this gap — a persistent, structured knowledge store that auto-captures facts, decisions, and action items from meetings and conversations, then makes them queryable by the chat agent. It turns the AI from a stateless chatbot into a practice partner that accumulates intelligence over time.
Why this is the single highest-leverage addition:
- Force multiplier: Meeting notes become 10x more valuable when the agent can reference them in future conversations. Every future feature (briefings, reminders, compliance) becomes a simple query against memory.
- Unstoppable lock-in: After 6 months, the AI knows things the adviser forgot. You can't switch platforms without losing that accumulated intelligence.
- Architecturally natural: Fits cleanly into existing patterns (Core context, Oban workers, BAML tools, PubSub).
- No competitor does this: Financial planning platforms have chatbots. None have an AI that genuinely remembers across interactions.
public.agent_knowledge_entries
├── id binary_id (PK)
├── client_id binary_id (FK → clients.id, nullable)
├── user_id binary_id (FK → users.id)
├── source_type enum (:meeting, :conversation, :manual)
├── source_id binary_id (polymorphic FK, nullable)
├── entry_type enum (:fact, :action_item, :decision, :preference, :life_goal)
├── category string ("income", "expense", "asset", "liability", "demographic", etc.)
├── content string (human-readable text)
├── data map/JSONB (structured typed data — mirrors FactData from financial_facts.baml)
├── metadata map/JSONB (sources, priority, deadline, kind)
├── status enum (:active, :superseded, :dismissed)
├── superseded_by_id binary_id (self-ref FK, nullable)
├── search_text tsvector (GENERATED from content + category)
├── inserted_at / updated_at
Indexes:
- GIN on
search_text - Composite on
(client_id, status, entry_type) - On
(source_type, source_id)for backfill idempotency
File: apps/core/lib/core/schemas/agent_knowledge_entry.ex
Standard use Core.Schema with belongs_to :client and belongs_to :user. Ecto.Enum for source_type, entry_type, status.
File: apps/core/lib/core/contexts/agent_memory/knowledge_entry_spec.ex
Follow pattern from AIChatConversationSpec — output_spec/0 with primitive types, export/1.
File: apps/core/lib/core/contexts/agent_memory_context.ex
Core functions:
create_entry(attrs)/create_entries(list)— single + bulk insertget_entries(opts)— filtered via TokenOperator (by_client, by_user, by_entry_type, by_status)search_entries(query_text, opts)— full-text search:WHERE client_id = $1 AND status = 'active' AND search_text @@ plainto_tsquery('english', $2)get_client_knowledge_summary(client_id, opts)— recent active entries grouped by entry_type, capped count (what the agent calls)supersede_entry(old_id, new_attrs)— mark old as:superseded, create newdismiss_entry(id)— mark:dismissed- PubSub:
subscribe/1,broadcast_knowledge_updated/1
File: apps/core/lib/core/workers/meetings/extract_knowledge_worker.ex
Hook point: AnalyzeTranscriptWorker.batch_exhausted/1 — after all meeting analysis branches complete, enqueue ExtractKnowledgeWorker.new(%{meeting_id: meeting_id}).
The worker:
- Loads meeting with completed
notesJSONB - Maps
notes["financial_facts"]→ entries withentry_type: :fact(direct 1:1 mapping —StructuredFinancialFactfields align with schema columns) - Maps
notes["action_items"]→ entries withentry_type: :action_item - Calls
AgentMemoryContext.create_entries/1 - Broadcasts via PubSub
No additional BAML call needed — the extraction is already done by meeting notes analysis.
BAML definitions — add to chat_agent.baml:
class SearchClientKnowledge {
tool_name "SearchClientKnowledge" @alias("tool_name")
client_id string
query string
entry_type string?
}
class GetClientKnowledgeSummary {
tool_name "GetClientKnowledgeSummary" @alias("tool_name")
client_id string
}
Add both to the ToolRequest union type.
Tool module: apps/core/lib/core/ai/chat_agent/tools/memory_tools.ex
- Follow exact pattern from
FactFindTools defimpl Tool, for: Baml.SearchClientKnowledge→ callsAgentMemoryContext.search_entries/2defimpl Tool, for: Baml.GetClientKnowledgeSummary→ callsAgentMemoryContext.get_client_knowledge_summary/2
System prompt addition: instruct the agent to always check client knowledge summary after identifying a client, before answering questions.
Design decision: Explicit tool calls (not auto-injection). Rationale: avoids wasting tokens on irrelevant context, follows existing SearchClient → FetchFactFind sequential pattern, simpler to implement and test.
File: apps/core/lib/mix/tasks/agent_memory/backfill_meetings.ex
Queries completed meetings with non-null notes, maps financial_facts + action_items to knowledge entries, bulk inserts. Idempotent via content hash + source_id dedup.
MVP: PostgreSQL tsvector (no new dependencies)
- Generated column:
to_tsvector('english', coalesce(content, '') || ' ' || coalesce(category, '')) - GIN index for fast lookup
- Combined with filter predicates (client_id, status, entry_type) for precise results
Future: pgvector for semantic search if keyword matching proves insufficient for natural language queries.
- Knowledge Timeline UI: LiveView component in Practice showing entries grouped by type, with dismiss/status controls
- Chat Knowledge Sidebar: "What I know about [Client]" panel in ChatLive
- Conversation Extraction: Async worker + BAML prompt to extract knowledge from chat conversations
- Entry Superseding: Deduplication across meetings when the same fact is mentioned again with updated values
- Pre-Meeting Briefing: Query knowledge for upcoming meeting's client, generate summary
- Action Item Reminders: Oban cron scanning entries with
entry_type: :action_itemandmetadata.deadline - Practice-Level Insights: Aggregations across all clients ("5 clients exposed to same risk factor")
Based on Jaya Gupta's "AI's trillion-dollar opportunity: Context graphs" and Animesh Koratana's "How to build a context graph".
The next trillion-dollar platforms won't be built by adding AI to existing systems of record — they'll be built by capturing the reasoning that connects data to action. Today, a CRM stores "20% discount." It doesn't store who approved the deviation, why, what precedent was considered, or what policy exception was invoked. The reasoning lived in a Zoom call or a Slack DM and was never treated as data.
A context graph is the accumulated structure formed by decision traces — not the LLM's chain-of-thought, but a living record of how context turned into action, stitched across entities and time so that precedent becomes searchable.
Financial planning is exactly the kind of domain where context graphs are most valuable. Consider:
- Exception-heavy decisions: Financial advice is never "one size fits all." Every client has unique circumstances, risk tolerances, and life goals. The adviser's judgment — why they recommended a particular pension structure over another — is the most valuable knowledge in the practice, and it's never captured.
- "Glue function" par excellence: Financial advisers sit at the intersection of compliance, product providers, client relationships, tax planning, and estate planning. They are the human context carriers that Gupta's article identifies as the prime opportunity for AI systems of record.
- Precedent matters enormously: "How did we handle the last client who wanted to retire early with a DB pension and a buy-to-let portfolio?" This question is asked in every practice, answered from memory, and never recorded.
The MVP (Phase 1) stores atomic facts — the "what." The context graph stores decision traces — the "why." Here's the evolution:
Phase 4 — Decision Traces (extending the knowledge entry model)
Add entry_type: :decision_trace to AgentKnowledgeEntry with richer data JSONB:
%{
"trigger": "Client asked about early retirement at 58",
"context_gathered": ["current_pension_value", "projected_growth", "state_pension_age", "lifestyle_expectations"],
"alternatives_considered": ["defer to 60", "partial retirement at 58", "full retirement at 58 with drawdown"],
"recommendation": "Partial retirement at 58 with phased drawdown from SIPP",
"reasoning": "Preserves tax-free lump sum optionality while covering living expenses",
"outcome": "Client agreed, action items created for pension transfer",
"participants": ["adviser_id", "client_id"],
"related_entries": ["fact_id_1", "fact_id_2", "action_item_id_1"]
}
Capture points:
- The chat agent already has the full conversation context when a recommendation is made. A post-conversation BAML extraction (
ExtractDecisionTrace) can identify moments where the adviser made a judgment call and extract the structured trace. - Meeting notes already extract action items and financial facts. A second pass can identify decision moments — points where alternatives were discussed and a direction was chosen.
Phase 5 — Precedent Search ("Agents as Informed Walkers")
Koratana's key insight: the ontology of a context graph shouldn't be predefined — it should emerge from agent walks. Each time the chat agent searches for client knowledge, follows a chain of related entries, or retrieves a decision trace, that traversal pattern reveals which entities and relationships actually matter.
Implementation:
- Log every
SearchClientKnowledgeandGetClientKnowledgeSummarytool call with the query, results returned, and which results the agent actually used in its response - Over time, these "walks" reveal: which categories of knowledge are most frequently accessed together, which decision traces are referenced as precedent, which client attributes are most predictive of similar situations
- Build a
FindSimilarPrecedenttool: given a current client situation, search for decision traces from similar clients/situations using the learned traversal patterns
Phase 6 — World Model for Financial Planning
Koratana's deepest insight: a context graph with enough accumulated structure becomes a world model — not just "what happened" but a model that enables simulation and counterfactual reasoning.
For financial planning, this means:
- Simulation: "If we recommend a drawdown strategy for this client, based on 50 similar decision traces, what concerns typically arise? What's the probability the client asks to revisit within 12 months?"
- Counterfactual reasoning: "What would have happened if we'd recommended the annuity instead of the drawdown for clients like X? Based on similar profiles, how did those decisions play out?"
- Practice-level learning: The agent doesn't need to be retrained. As Koratana writes: "Keep the model fixed, improve the world model it reasons over." Each new meeting, each new conversation, each new decision trace expands the evidence base the agent reasons over at inference time.
This is the path from "AI chatbot for advisers" to "AI system of record for financial planning decisions."
Traditional systems of record store what happened (transactions, holdings, client data). A context graph stores why it happened (the reasoning, the precedent, the judgment). In a regulated industry like financial planning, the "why" is arguably more valuable than the "what" — it's what regulators ask for, what compliance needs, and what makes one practice better than another.
The MVP (Phases 1-3) delivers immediate, tangible value. The context graph vision (Phases 4-6) is what makes the platform irreplaceable.
Neo4j's demo uses a two-layer model:
Entities (What exists):
- Person (customers, employees)
- Account (checking, savings, trading, margin)
- Transaction (deposits, withdrawals, transfers)
- Organization (banks, vendors, counterparties)
- Policy (business rules and thresholds)
Decision Traces (What happened and why):
- Decision — the core event with full reasoning
- DecisionContext — state snapshot at decision time
- Exception — policy overrides with justification
- Escalation — when decisions need higher authority
- Community — clusters of related decisions (auto-detected via Louvain)
Relationships (where graphs shine):
// Causal chain
(:Decision)-[:CAUSED]->(:Decision)
(:Decision)-[:INFLUENCED]->(:Decision)
(:Decision)-[:PRECEDENT_FOR]->(:Decision)
// Context
(:Decision)-[:ABOUT]->(:Person|:Account|:Transaction)
(:Decision)-[:APPLIED_POLICY]->(:Policy)
(:Decision)-[:GRANTED_EXCEPTION]->(:Exception)
(:Decision)-[:TRIGGERED]->(:Escalation)
Key capability: hybrid search — semantic embeddings (text similarity of reasoning) + structural embeddings (FastRP graph topology similarity) for finding precedents that are both about similar things and in similar contexts.
Agent tools in the Neo4j demo:
| Tool | Purpose |
|---|---|
search_customer |
Find customers and related entities |
get_customer_decisions |
All decisions about a specific customer |
find_precedents |
Hybrid semantic + structural precedent search |
find_similar_decisions |
FastRP-based structural similarity |
get_causal_chain |
Trace what caused a decision and what it led to |
record_decision |
Create new decisions with full context |
find_decision_community |
Louvain clustering of related decisions |
get_policy |
Retrieve applicable business rules |
Timeline Entities (already exist in the system):
| Entity | Schema | Purpose |
|---|---|---|
| Client | Core.Schemas.Client |
The person being advised |
| Adviser (User) | Core.Schemas.User |
The financial adviser |
| Firm | Core.Schemas.Firm |
The advisory practice |
| FactFind | Core.Schemas.FactFind |
17-section structured client profile |
| Meeting | Core.Schemas.Meeting |
Recorded client interactions |
| PlatformPortfolio | Core.Schemas.PlatformPortfolio |
Investment portfolio with drift/rebalance rules |
| PlatformAccount | Core.Schemas.PlatformAccount |
Client investment account |
| RiskQuestionnaire | Core.Schemas.RiskProfiler.QuestionnaireResponse |
Risk tolerance assessment |
| Conversation | Core.Schemas.Conversation |
Threaded async communication |
| ApplicationEvent | Core.Schemas.ApplicationEvent |
Audit log (28 event kinds) |
Timeline Decision Traces (to be captured): → See Appendix B below
Timeline Policies (implicit, should be made explicit):
- Risk tolerance bands per client profile
- Rebalance rules (tolerance_limit, annual, drift percentages)
- KYC/AML compliance requirements
- Suitability assessment criteria (FCA regulations)
- Capacity for loss thresholds
- Fee structures and discount policies
These are the highest-value decisions — the "why did the adviser recommend X?" that regulators ask for and competitors can't replicate.
| Decision Type | Description | Capture Point | Currently Captured? |
|---|---|---|---|
| Product Recommendation | Why a specific pension/ISA/GIA was recommended over alternatives | Meeting transcript (AI extraction) + Chat agent conversation | Partially — financial facts extracted from meetings, but not the reasoning behind recommendations |
| Risk Profile Override | Adviser overrides the questionnaire score with justification | QuestionnaireResponse.score_override field + override_reason + overridden_at |
Yes — score, reason, and timestamp are captured |
| Asset Allocation Decision | Why this specific allocation split was chosen for this client | PlatformAccountAssetAllocationIntent.allocations |
Only the "what" — the allocation percentages. Not why this split, what alternatives were considered |
| Drawdown vs Annuity | The critical retirement income decision | Meeting transcript + suitability report | No — lives in PDF reports and meeting recordings, not as structured data |
| Fund/Provider Selection | Why provider X was chosen over provider Y | Meeting/conversation | No — not captured at all |
| Contribution Level | Why the adviser recommended contributing X per month | Chat agent calculation + meeting discussion | No — only the final figure in the fact-find |
| Protection Recommendation | Life insurance, income protection, critical illness decisions | Meeting transcript | Partially — facts extracted, but not the needs analysis reasoning |
| Decision Type | Description | Capture Point | Currently Captured? |
|---|---|---|---|
| Rebalance Decision | Why a rebalance was triggered and executed | PlatformPortfolio drift status + PlatformDriftNotification |
Only the trigger — drift exceeded threshold. Not: was the adviser consulted? Did they agree? Any exceptions? |
| Rebalance Rule Change | Switching from tolerance-based to annual rebalancing | RebalanceRule update |
Only the new state — not why the rule was changed |
| Drift Tolerance Override | Accepting drift above threshold temporarily | Not captured | No — adviser may decide to leave drift in place (e.g., market timing view) but this isn't recorded |
| Model Portfolio Assignment | Why this model was chosen for this client | Platform setup | No — the assignment exists, the reasoning doesn't |
| Switch/Transfer | Moving assets between providers or funds | ApplicationEvent (create_platform_transaction) | Only the action — not the catalyst (meeting discussion, market event, life change) |
| Decision Type | Description | Capture Point | Currently Captured? |
|---|---|---|---|
| Onboarding Acceptance | Taking on a new client — why, suitability assessment | Initial meeting + KYC | Only KYC status — not the adviser's judgment on fit |
| Annual Review Outcome | What changed, what was recommended, follow-ups | Meeting transcript + action items | Partially — facts and actions extracted, but no structured "review outcome" |
| Client Offboarding | Why a client relationship was ended | Conversation/meeting | No — not captured as a decision |
| Fee Negotiation | Why a specific fee was agreed | Meeting/conversation | No — only the final fee is stored |
| Referral Decision | Referring to a specialist (tax, legal, mortgage) | Meeting action items | Partially — may appear as an action item, but not linked to the reasoning |
| Decision Type | Description | Capture Point | Currently Captured? |
|---|---|---|---|
| KYC Determination | Approve/decline/needs-review with reasoning | KycTransaction status change + ApplicationEvent |
Yes — status, timestamp, and encrypted payload captured |
| Suitability Assessment | Is this product suitable for this client? | Modular report sections | Partially — report fields exist (capacity_for_loss, risk_profile_appropriate_investment_strategy) but not as queryable decision records |
| Capacity for Loss | Can the client afford to lose this money? | FactFind + modular report | Flag only — boolean in report, reasoning in meeting |
| Vulnerable Client Flag | Identifying and documenting vulnerability | FactFind health/needs fields | Data exists — but no decision record of when/why vulnerability was identified |
| Complaint Resolution | How a complaint was handled and resolved | Conversation threads | Thread exists — but no structured decision trace |
| Decision Type | Description | Capture Point | Currently Captured? |
|---|---|---|---|
| Tool Call Chain | What tools the agent used and in what order to answer a question | Chat agent execution trace | No — conversation messages saved, but not the tool call sequence |
| Fact-Find Interpretation | How the agent interpreted ambiguous client data | Chat agent response | No — the response is saved, not the reasoning path |
| Meeting Note Extraction Confidence | Which facts the AI was confident vs uncertain about | BAML extraction pipeline | No — the 3-phase validation exists but confidence scores aren't persisted |
┌─────────────────────────────────────────────────────────┐
│ DECISION SOURCES │
├─────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌──────────────┐ ┌─────────────┐ │
│ │ MEETINGS │ │ CHAT AGENT │ │ PLATFORM │ │
│ │ │ │ │ │ ACTIONS │ │
│ │ Transcript │ │ Conversation │ │ │ │
│ │ → AI Extract│ │ → Tool Calls │ │ Rebalance │ │
│ │ → Facts │ │ → Responses │ │ KYC Status │ │
│ │ → Actions │ │ → Choices │ │ Allocation │ │
│ │ → Decisions │ │ │ │ Transfers │ │
│ │ (NEW) │ │ │ │ │ │
│ └──────┬──────┘ └──────┬───────┘ └──────┬──────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ KNOWLEDGE EXTRACTION LAYER │ │
│ │ │ │
│ │ ExtractKnowledgeWorker (meetings) │ │
│ │ ExtractChatKnowledgeWorker (conversations) │ │
│ │ ExtractDecisionTraceWorker (NEW - Phase 4) │ │
│ │ ApplicationEvent listener (platform actions) │ │
│ └──────────────────────┬───────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ AGENT KNOWLEDGE ENTRIES │ │
│ │ │ │
│ │ Facts | Action Items | Decisions | Preferences │ │
│ │ ↓ (Phase 4+) │ │
│ │ Decision Traces with full context │ │
│ └──────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────┘
1. Meeting Transcripts (highest value, requires new BAML extraction)
Meetings are where the real decisions happen. The existing extraction pipeline captures facts and action items but misses decision moments. A new BAML function ExtractDecisionTraces would identify:
- Moments where alternatives were discussed ("we could do A or B...")
- The adviser's recommendation and reasoning ("I'd suggest A because...")
- The client's response and concerns ("but what about...")
- The agreed outcome ("let's go with A and review in 6 months")
2. ApplicationEvent audit log (structured, already captured, just needs enrichment)
The system already logs 28 event kinds with encrypted payloads. These are pure "state clock" events — what changed. The gap is the "event clock" — why it changed. For each ApplicationEvent, the decision trace would capture:
- What triggered this action (meeting ID, conversation ID, market event)
- Who approved it and through what process
- What the state was before the change (DecisionContext snapshot)
3. Risk Profile Overrides (already partially captured)
QuestionnaireResponse already has score_override, override_reason, overridden_at. This is the closest thing to a decision trace that exists today. The pattern could be generalized.
4. Chat Agent Tool Chains (meta-decisions, zero-cost capture)
Every chat conversation already has the full message history. Adding structured logging of tool calls (SearchClient → FetchFactFind → CalculatorMultiply → response) creates a decision trace for free. This shows how the agent reached its answer.
The Neo4j demo is compelling, but introducing a graph database is a significant infrastructure change. The pragmatic path:
Phase 1-3 (PostgreSQL, current plan): Flat knowledge entries with related_entries in JSONB metadata. Sufficient for basic recall and search. Decision traces stored as rich JSONB with trigger, alternatives, reasoning, outcome fields.
Phase 4-5 (PostgreSQL with recursive queries): Add an agent_knowledge_relations join table:
agent_knowledge_relations
├── source_entry_id FK → agent_knowledge_entries
├── target_entry_id FK → agent_knowledge_entries
├── relation_type enum (:caused, :influenced, :precedent_for, :superseded, :about)
├── metadata JSONB
This enables causal chain traversal with WITH RECURSIVE CTEs. Not as elegant as Cypher, but functional for chains of depth 3-5 (typical in financial planning).
Phase 6+ (evaluate graph DB): If the relation table grows large and query patterns become deeply recursive (chains > 5 hops, community detection, structural similarity), then evaluate Neo4j or Apache AGE (PostgreSQL graph extension) as a secondary store. The knowledge entries remain in PostgreSQL as the source of truth; the graph DB becomes a projection optimized for traversal queries.
Key Neo4j capabilities to replicate pragmatically in PostgreSQL:
| Neo4j Feature | PostgreSQL Equivalent | Sufficient for Phase 1-5? |
|---|---|---|
| Causal chain traversal | WITH RECURSIVE CTE on relations table |
Yes (depth ≤ 5) |
| Precedent search (semantic) | pgvector on content embeddings | Yes |
| Precedent search (structural) | Not feasible without graph DB | Skip until Phase 6 |
| Community detection (Louvain) | Not feasible without graph DB | Skip until Phase 6 |
| Node embeddings (FastRP) | Not feasible without graph DB | Skip until Phase 6 |
| Policy application tracking | APPLIED_POLICY relation + Policy table |
Yes |
What it does: Given any decision or event, trace backwards to find what caused it and forwards to find what it triggered. In Neo4j this is a simple path query; in PostgreSQL it's a WITH RECURSIVE CTE over the relations table.
Concrete scenarios in financial planning:
Scenario A — Regulatory audit trail: An FCA auditor asks: "Why was this client moved from a cautious to a balanced portfolio?"
The causal chain surfaces:
Annual Review Meeting (2025-09-15)
→ Client disclosed inheritance of £200k (financial fact)
→ Adviser reassessed capacity for loss (decision: increased)
→ Risk questionnaire re-taken (score: 6 → 7)
→ Portfolio reallocated from Cautious to Balanced (action)
→ Rebalance executed (platform transaction)
Without causal chains, the auditor sees: "portfolio changed on Oct 3rd." With them, they see the full justified reasoning path from life event → assessment → action. This is the difference between a compliance checkbox and a defensible audit trail.
Scenario B — Understanding client outcomes: An adviser reviews a client whose portfolio underperformed. The causal chain reveals:
Market volatility event (2025-03)
→ Drift notification: equity allocation dropped 12% below target
→ Adviser decision: delay rebalance (reasoning: "client nervous, prefer to wait")
→ Second drift notification (2025-06)
→ Adviser decision: rebalance now
→ But missed the recovery window → underperformance
This turns hindsight into learnable insight. The adviser can see: "when I delayed rebalancing due to client anxiety, it cost X% over 3 months." This is how practices get systematically better.
Scenario C — Complaint resolution: Client complains: "You put me in the wrong fund." The causal chain instantly surfaces:
Initial meeting → risk assessment → suitability determination → fund selection → client agreement
Every link is timestamped with reasoning. The complaint is resolved in minutes instead of days of file review.
Why it's powerful for us: Financial planning is a regulated industry where "why" matters as much as "what." Every decision must be justifiable. Causal chains transform our platform from a record of outcomes into a record of reasoning — which is exactly what regulators, compliance officers, and advisers themselves need.
What it does: Given a current situation described in natural language, find past decisions with similar reasoning, context, or outcomes. Uses pgvector embeddings on decision content to find semantically similar entries.
Concrete scenarios:
Scenario A — New adviser onboarding: A junior adviser joins the practice and encounters their first client wanting to consolidate 4 old workplace pensions. They ask the chat agent: "How have we handled pension consolidation before?"
Precedent search returns the 5 most similar past decisions:
- "Client had 3 DB pensions — we recommended keeping DB, consolidating only DC pensions due to safeguarded benefits"
- "Client had 4 DC pensions with high charges — consolidated into SIPP, saved 0.8% in annual fees"
- "Client wanted to consolidate but one pension had a valuable guaranteed annuity rate — advised keeping it separate"
The junior adviser now has the practice's accumulated wisdom at their fingertips, not just their own limited experience. They're making decisions informed by every similar case the practice has ever handled.
Scenario B — Pre-meeting preparation: Before a review meeting, the agent automatically searches for precedents matching this client's current situation:
- "Client approaching retirement with mixed DB/DC pensions" → surfaces how similar cases were handled
- "Client recently divorced with pension sharing order" → surfaces relevant past decisions and pitfalls
The adviser walks into the meeting with a briefing based on collective practice experience, not just this client's file.
Scenario C — Quality assurance: Practice manager asks: "Show me all decisions where we recommended drawdown for clients over 70 with capacity for loss rated low." Semantic search finds these even if the exact terminology varied across different advisers' notes. This surfaces potential suitability concerns across the book.
Why it's powerful for us: Financial advice quality is bottlenecked by individual adviser experience. A 30-year veteran has seen everything; a 3-year adviser hasn't. Precedent search democratizes the practice's collective intelligence. It's the difference between "I think we should..." and "Based on 12 similar cases we've handled, the approach that worked best was..."
What it does: Unlike semantic search (which matches on text similarity), structural search uses graph embeddings (FastRP) to find decisions that occupy similar positions in the graph — same types of entities, same relationship patterns — even if the text descriptions are completely different.
Concrete scenario: An adviser is handling a complex case: a business owner client with a SSAS pension, cross-border tax implications, and a recent health diagnosis affecting their retirement timeline.
Semantic search might find: "client with health issue" or "business owner pension" separately. Structural search finds: "here's a past decision that involved the same combination — a business owner entity connected to a self-administered pension, connected to a tax complication, connected to a health-related timeline change." The topology matches, even if the specific details are different.
Why it's powerful (future): This is where the context graph goes beyond "smart search" into genuine pattern recognition. It answers: "I've never seen this exact situation, but I've seen situations with the same shape." This is what experienced advisers do intuitively — and what makes their judgment so valuable. Graph embeddings formalize it.
Why Phase 6: Requires a graph database (FastRP is a GDS algorithm). The value is real but the infrastructure cost is high. Semantic search covers 80% of the need; structural search is the remaining 20% for truly complex cases.
What it does: Automatically groups decisions that are densely interconnected through causal and influence relationships. No manual tagging needed — the algorithm discovers the clusters.
Concrete scenarios:
Scenario A — Discovering systemic issues: Louvain detects a community of 23 decisions across 15 clients, all connected by:
- A rebalance delay decision → linked to the same market event
- A risk profile reassessment → all during the same quarter
- A common fund switch → all moving away from the same provider
The practice manager sees: "We have a cluster of decisions all triggered by Provider X's fund underperformance. 15 clients were affected. 8 have been reviewed. 7 still need attention."
Without community detection, these are 23 isolated records. With it, they're a systemic pattern that demands a practice-wide response.
Scenario B — Identifying advice themes: Over a year, the algorithm discovers clusters like:
- "Pre-retirement planning" cluster: 45 decisions involving drawdown, annuity, tax-free lump sum, state pension timing
- "Protection gap" cluster: 30 decisions involving life insurance, income protection, and critical illness after fact-find reviews
- "Inheritance tax planning" cluster: 20 decisions involving trusts, gifts, pension beneficiary nominations
These clusters become the practice's actual service lines, discovered from data rather than assumed from marketing materials. They reveal what the practice actually does vs. what it says it does.
Scenario C — Training & CPD: Clusters of decisions involving FCA regulatory changes (e.g., Consumer Duty implementation) surface: how did the practice adapt? Which advisers changed their approach first? What worked? This becomes structured CPD material derived from real practice decisions.
Why Phase 6: Louvain requires a graph database. The value is at the practice/firm level (strategic, not transactional). It's incredibly powerful for practice management but not needed for individual adviser productivity, which is the Phase 1-3 priority.
What it does: Computes a vector representation of each decision based on its position in the graph — what entities it touches, what it caused, what caused it, what policies it applied. Two decisions with similar FastRP embeddings have similar structural context, regardless of their text.
Concrete scenario: The system can answer: "This is a 'high-complexity retirement transition' type decision" — not because anyone labelled it, but because its graph fingerprint (connected to: multiple pension entities, a property entity, a tax planning consideration, a spouse's pension, a health factor) matches the structural pattern of past decisions that were labelled that way.
Why it's powerful (future): This enables automatic complexity scoring, automatic routing (complex cases → senior adviser), and anomaly detection (this decision's structure doesn't match any known pattern → flag for review).
Why Phase 6: Same infrastructure requirements as structural precedent search. The immediate value can be approximated with simpler heuristics (count of related entities, number of decision types involved) until the graph infrastructure justifies the investment.
What it does: Links each decision to the specific policies/rules that were applied (or overridden). In PostgreSQL, this is a relation between knowledge entries and a new policies table.
Concrete scenarios:
Scenario A — Consumer Duty compliance: FCA's Consumer Duty requires firms to demonstrate they're acting in clients' best interests. Policy tracking creates an automatic evidence trail:
- Decision: "Recommended fund switch from active to passive"
- Applied policies: "Consumer Duty: cost-effectiveness requirement", "Client's stated preference for low-cost options"
- Outcome: "Annual fee reduction of 0.6%, projected 15-year saving of £45,000"
Every decision is linked to the regulatory principle it satisfies. Compliance reporting becomes a query, not a manual exercise.
Scenario B — Detecting policy drift: The system can answer: "In the last quarter, how often was the 'suitability assessment required' policy applied vs. skipped?" If advisers are consistently bypassing a policy (even with justification), that's a signal — either the policy needs updating or the behaviour needs addressing.
Scenario C — Policy change impact analysis: When a regulation changes (e.g., pension lifetime allowance abolished), the system can find all past decisions that referenced the old policy: "Show me every decision where 'lifetime allowance check' was applied." This surfaces exactly which clients need reviewing and what was decided based on the now-obsolete rule.
Why it's powerful for us: Financial planning is one of the most regulated professional services. The gap between "we have policies" and "we can prove every decision followed policies" is enormous. Policy tracking closes that gap automatically. It transforms compliance from retrospective paperwork into real-time assurance.
Why it's feasible now (Phase 4-5): Unlike community detection or structural search, policy tracking is just a relation + a small table. No graph database needed. A policies table with name, description, category, effective_from, effective_to and a APPLIED_POLICY relation type in the relations table covers it.
| Phase | Capability | User Value | Business Value |
|---|---|---|---|
| 1-3 | Knowledge recall | "The agent remembers what we discussed" | Stickiness, time savings |
| 4 | Decision traces | "The agent knows why we decided that" | Audit trail, compliance |
| 4-5 | Causal chains | "Trace any outcome back to its root cause" | Regulatory defence, learning |
| 4-5 | Semantic precedent search | "How did we handle this before?" | Advice quality, adviser onboarding |
| 4-5 | Policy tracking | "Prove every decision followed policy" | Consumer Duty compliance |
| 6+ | Structural search | "Find decisions with similar shape" | Complex case routing |
| 6+ | Community detection | "Discover systemic patterns across the book" | Practice management, risk |
| 6+ | Node embeddings | "Automatic complexity scoring" | Anomaly detection, QA |
The first five rows are achievable with PostgreSQL and deliver transformative value. The last three require a graph database and are the "moat" — once you have them, no competitor can replicate your accumulated decision intelligence.
| File | Change |
|---|---|
apps/core/priv/baml/chat_agent/baml_src/chat_agent.baml |
Add MemoryTools to ToolRequest union + system prompt |
apps/core/lib/core/workers/meetings/analyze_transcript_worker.ex |
Enqueue ExtractKnowledgeWorker in batch_exhausted |
apps/core/lib/core/ai/chat_agent/tools/fact_find_tools.ex |
Pattern reference for MemoryTools |
apps/core/lib/core/contexts/meetings_context.ex |
Pattern reference for AgentMemoryContext |
apps/core/priv/baml/meeting_notes/baml_src/financial_facts.baml |
Reference for StructuredFinancialFact → KnowledgeEntry mapping |
| File | Purpose |
|---|---|
apps/core/priv/repo/migrations/*_create_agent_knowledge_entries.exs |
Table migration |
apps/core/lib/core/schemas/agent_knowledge_entry.ex |
Ecto schema |
apps/core/lib/core/contexts/agent_memory_context.ex |
Context module |
apps/core/lib/core/contexts/agent_memory/knowledge_entry_spec.ex |
Spec module |
apps/core/lib/core/workers/meetings/extract_knowledge_worker.ex |
Post-meeting extraction |
apps/core/lib/core/ai/chat_agent/tools/memory_tools.ex |
Agent tool implementations |
apps/core/lib/mix/tasks/agent_memory/backfill_meetings.ex |
Backfill task |
apps/core/test/core/contexts/agent_memory_context_test.exs |
Context tests |
apps/core/test/core/ai/chat_agent/tools/memory_tools_test.exs |
Tool tests |
apps/core/test/core/workers/meetings/extract_knowledge_worker_test.exs |
Worker tests |
- Unit tests: Context CRUD, search, spec export, tool dispatch
- Integration test: Create meeting → run analysis → verify knowledge entries created → chat agent calls SearchClientKnowledge → returns relevant entries
- Manual test: Send a message in Practice chat like "What do you know about [client name]?" → agent should call GetClientKnowledgeSummary and return knowledge from past meetings
- Backfill test: Run mix task on dev DB, verify entries created from existing meeting notes