Real-time Augmented AI with Structured Data
Most AI retrieval systems are built for unstructured content — PDFs, notes, chat history. But the most valuable data in any organisation is structured: CRM records, ERP transactions, contracts, tickets, financial accounts, HR systems.
Purple8 Hyper Graph bridges that gap. It lets you model structured data as a connected graph and query it with hybrid search — so your AI gets relational, up-to-date context with a single round-trip, not a pipeline of JOINs and vector calls.
The core idea
Traditional RAG:
User question → embed → vector search → top-k chunks → LLMGraph-augmented AI with structured data:
User question → embed → Cypher (vector + relationships + filters) → structured context row → LLMThe difference: the LLM sees not just what a record says, but who owns it, what it's connected to, and what state it's in right now.
Step 1 — Model your structured data as a graph
Any structured record becomes a node. Foreign keys become edges.
from purple8_graph import GraphEngine
engine = GraphEngine("./data")
# CRM records — add_node(node_id, labels, properties)
engine.add_node("acct-001", ["Account"], {"name": "Acme Corp", "arr": 240000, "tier": "enterprise", "industry": "manufacturing"})
engine.add_node("ctt-001", ["Contact"], {"name": "Jane Smith", "role": "CFO", "email": "jane@acme.com"})
engine.add_node("opp-001", ["Opportunity"], {"name": "Renewal Q3", "value": 120000, "stage": "negotiation", "close_date": "2026-09-30"})
engine.add_node("tkt-001", ["SupportTicket"], {"subject": "API timeout errors", "priority": "high", "status": "open"})
engine.add_node("ctr-001", ["Contract"], {"start": "2025-10-01", "renewal_date": "2026-09-30", "auto_renew": False})
# Relationships encode the structure that SQL can't traverse in one query
# add_edge(edge_id, edge_type, source_id, target_id)
engine.add_edge("opp-001_belongs_to_acct-001", "BELONGS_TO", "opp-001", "acct-001")
engine.add_edge("acct-001_contact_ctt-001", "PRIMARY_CONTACT", "acct-001", "ctt-001")
engine.add_edge("acct-001_ticket_tkt-001", "HAS_TICKET", "acct-001", "tkt-001")
engine.add_edge("acct-001_contract_ctr-001", "HAS_CONTRACT", "acct-001", "ctr-001")No schema required
You don't need to define a schema before ingesting. Purple8 infers it from the data. Add new record types — a new ERP object, a new CRM field — without any DDL or migration.
Step 2 — Retrieve structured context with one Cypher query
At query time, hybrid Cypher combines vector similarity with relationship traversal and property filters — in a single round-trip.
import openai
user_question = "What's the renewal risk on Acme Corp?"
query_vec = openai.embeddings.create(input=user_question, model="text-embedding-3-small").data[0].embedding
results = engine.query("""
CALL db.vector.search('Opportunity', $vec, 5) YIELD node, score
WHERE score > 0.70
MATCH (node)-[:BELONGS_TO]->(acct:Account)
MATCH (acct)<-[:PRIMARY_CONTACT]-(contact:Contact)
OPTIONAL MATCH (acct)-[:HAS_TICKET]->(t:SupportTicket {status: 'open'})
OPTIONAL MATCH (acct)-[:HAS_CONTRACT]->(c:Contract)
RETURN
node.name AS opportunity,
node.stage AS stage,
node.value AS value,
node.close_date AS close_date,
acct.name AS account,
acct.arr AS arr,
acct.tier AS tier,
contact.name AS owner,
contact.email AS owner_email,
count(t) AS open_tickets,
c.renewal_date AS renewal_date,
c.auto_renew AS auto_renew,
score
ORDER BY score DESC
LIMIT 1
""", vec=query_vec)One query. Zero application-side joins.
Step 3 — Inject structured context into the LLM
row = results[0]
system_prompt = "You are a revenue intelligence assistant. Answer based only on the structured context provided."
user_prompt = f"""
Structured context:
- Opportunity: {row['opportunity']} (stage: {row['stage']}, value: ${row['value']:,}, closes: {row['close_date']})
- Account: {row['account']} ({row['tier']} tier, ARR: ${row['arr']:,})
- Primary contact: {row['owner']} <{row['owner_email']}>
- Open support tickets: {row['open_tickets']}
- Contract renewal: {row['renewal_date']} (auto-renew: {'yes' if row['auto_renew'] else 'no'})
Question: {user_question}
"""
response = openai.chat.completions.create(
model="gpt-4o",
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt},
]
)
print(response.choices[0].message.content)Step 4 — Keep context fresh with real-time graph updates
The graph is your source of truth. Update records as they change in your source system — the next AI query automatically gets the latest state.
# A deal just moved stage — update the graph (properties is the 3rd positional arg)
engine.update_node("opp-001", properties={"stage": "closed-won", "close_date": "2026-07-15"})
# A new ticket opened
engine.add_node("tkt-002", ["SupportTicket"], {"subject": "Data export failure", "priority": "critical", "status": "open"})
engine.add_edge("acct-001_ticket_tkt-002", "HAS_TICKET", "acct-001", "tkt-002")No cache invalidation. No pipeline re-run. The next vector search returns the updated context.
Step 5 — Trigger AI workflows automatically on SLA breach or stage entry
Use the Journey Engine to fire AI analysis whenever a journey instance enters a stage or breaches its SLA — without polling. Journeys track discrete instances (e.g. one Opportunity per renewal cycle) through a defined stage list; the AI advisor and HITL gates are configured per stage, not as arbitrary property-change conditions on graph nodes.
from purple8_graph import GraphEngine
from purple8_graph.journey import (
JourneyEngine, JourneyAIAdvisor, StageSpec, SLAPolicy, AIStepConfig, AIStepTrigger,
)
from purple8_graph.genai import LiteLLMProvider
je = JourneyEngine(engine)
je.define_journey("renewal-risk-check", stages=[
StageSpec(
name="negotiation",
sla=SLAPolicy(warn_after_seconds=3600 * 24 * 25, breach_after_seconds=3600 * 24 * 30),
ai_step=AIStepConfig(trigger=AIStepTrigger.ON_SLA_WARN),
),
StageSpec(
name="human-review",
hitl={"required": True, "assigned_role": "csm", "title": "Renewal risk review"},
),
])
provider = LiteLLMProvider(model="gpt-4o")
je.register_ai_advisor(JourneyAIAdvisor(je, provider))
# Start an instance tied to the Opportunity node
instance = je.start("renewal-risk-check", entity_id="opp-001")When the negotiation stage's SLA warning fires (25 days in, per SLAPolicy.warn_after_seconds), SLAMonitor automatically invokes the registered JourneyAIAdvisor, which writes an AI_ADVISED edge with its recommendation. In v0.72.0, every AI-triggered transition or SLA event uses the durable Journey AI outbox (ai_dispatched flag on the audit edge + je.replay_pending_ai_dispatches() on crash recovery), ensuring advisor triggers survive abrupt process crashes (SIGKILL). Advancing the instance into human-review automatically creates a HITLTask for a CSM — all with a full audit trail.
Why this beats SQL + vector search
| SQL JOIN + vector | Purple8 Hyper Graph | |
|---|---|---|
| Round-trips per query | 2–4 | 1 |
| Arbitrary relationship depth | ❌ | ✅ |
| Schema migrations for new objects | Required | Not required |
| Real-time updates in context | Cache invalidation | Direct node update |
| AI workflow triggers on data change | External job/event | Built-in Journey Engine |
| Hybrid keyword + semantic + graph | ❌ | ✅ |
More use cases
See the Use Cases page for working Cypher examples across:
- CRM deal intelligence — renewal risk, churn signals
- Financial risk traversal — counterparty exposure chains
- HR org-chart AI — reporting structure, headcount queries
- Supply chain disruption alerts — real-time supplier risk
- Regulatory compliance Q&A — policy-to-data-asset mapping
Further reading
- Hybrid Search guide — BM25 + vector + graph in one query
- Journey Engine guide — trigger AI workflows on graph changes
- Schema & Data Model — ingest structured data without upfront DDL
- LLM / RAG Grounding use case — unstructured variant