Cookbook: LLM Workflow — Natural Language to Knowledge Graph Answer
Goal. Build a complete end-to-end workflow where a user asks a question in plain English, an LLM generates a SPARQL query, the query runs against your knowledge graph, and the results are returned to the user as a fluent natural-language answer.
Why pg_ripple. All four steps run inside one SQL call chain — from NL question to SPARQL to result to summarised answer. No separate orchestration layer required.
Time to first result. ~10 minutes.
The four-step workflow
User question (NL)
│
▼
sparql_from_nl() ← LLM generates SPARQL from your schema + few-shot examples
│
▼
sparql() ← execute the SPARQL against the VP tables
│
▼
sparql_construct_turtle() ← optionally fetch the context subgraph as Turtle
│
▼
LLM summarises results into a fluent answer
pg_ripple handles steps 1–3 natively. Step 4 is your LLM call.
Step 1 — Set up the LLM endpoint
ALTER SYSTEM SET pg_ripple.llm_endpoint = 'https://api.openai.com/v1';
ALTER SYSTEM SET pg_ripple.llm_api_key_env = 'OPENAI_API_KEY';
ALTER SYSTEM SET pg_ripple.llm_model = 'gpt-4o';
-- (Optional) embed entities for vector-assisted SPARQL generation.
ALTER SYSTEM SET pg_ripple.embedding_api_url = 'https://api.openai.com/v1';
ALTER SYSTEM SET pg_ripple.embedding_api_key_env = 'OPENAI_API_KEY';
ALTER SYSTEM SET pg_ripple.embedding_model = 'text-embedding-3-small';
SELECT pg_reload_conf();
Step 2 — Load a sample graph
SELECT pg_ripple.load_turtle($TTL$
@prefix ex: <https://example.org/> .
@prefix schema: <https://schema.org/> .
ex:Alice a schema:Person ;
schema:name "Alice Smith" ;
schema:jobTitle "Senior Engineer" ;
schema:knows ex:Bob .
ex:Bob a schema:Person ;
schema:name "Bob Jones" ;
schema:jobTitle "Data Scientist" ;
schema:knows ex:Carol .
ex:Carol a schema:Person ;
schema:name "Carol Kim" ;
schema:jobTitle "VP Engineering" .
$TTL$);
Step 3 — Add few-shot examples (recommended)
Domain-specific examples dramatically improve sparql_from_nl accuracy:
SELECT pg_ripple.add_llm_example(
'Who does Alice know?',
'PREFIX ex: <https://example.org/>
PREFIX schema: <https://schema.org/>
SELECT ?name WHERE {
ex:Alice schema:knows ?p .
?p schema:name ?name .
}'
);
SELECT pg_ripple.add_llm_example(
'What is Bob''s job title?',
'PREFIX ex: <https://example.org/>
PREFIX schema: <https://schema.org/>
SELECT ?title WHERE {
ex:Bob schema:jobTitle ?title .
}'
);
Step 4 — Generate and run a SPARQL query
-- Generate SPARQL from a natural-language question.
SELECT pg_ripple.sparql_from_nl('Who does Bob know?') AS generated_sparql;
Returns:
PREFIX ex: <https://example.org/>
PREFIX schema: <https://schema.org/>
SELECT ?name WHERE {
ex:Bob schema:knows ?person .
?person schema:name ?name .
}
Execute it:
SELECT result->>'name' AS person_name
FROM pg_ripple.sparql(
pg_ripple.sparql_from_nl('Who does Bob know?')
);
Step 5 — Fetch the relevant subgraph as context
For richer answers, pass the whole subgraph to the LLM rather than just the result rows:
SELECT pg_ripple.sparql_construct_turtle($Q$
CONSTRUCT { ?s ?p ?o }
WHERE {
?s ?p ?o .
FILTER(?s IN (<https://example.org/Alice>, <https://example.org/Bob>))
}
$Q$) AS context_turtle;
The Turtle block goes into the LLM prompt alongside the SPARQL result rows and the user question.
Step 6 — Combine with hybrid vector search
For questions where the relevant entity cannot be named precisely ("who works on machine learning"), combine the NL→SPARQL path with a vector similarity pre-filter:
-- Embed all entities first (once per load cycle).
SELECT pg_ripple.embed_entities();
-- Hybrid: find similar entities, then expand via SPARQL.
SELECT * FROM pg_ripple.sparql($$
PREFIX schema: <https://schema.org/>
SELECT ?name ?title WHERE {
?p schema:name ?name ;
schema:jobTitle ?title .
FILTER(?p IN (
SELECT s FROM pg_ripple.similar_entities('machine learning engineer', k := 5)
))
}
$$);
Step 7 — A complete Python application
import psycopg, openai
DB_URL = "postgresql://..."
client = openai.OpenAI()
SYSTEM_PROMPT = """
You are a helpful assistant. You have been given the result of a SPARQL query against
a knowledge graph, as well as the subgraph context as Turtle. Answer the user's
question concisely using only the provided data. If the data does not contain the
answer, say so.
""".strip()
def answer_question(question: str) -> str:
with psycopg.connect(DB_URL) as conn:
cur = conn.cursor()
# Step A: generate SPARQL and run it.
cur.execute(
"SELECT jsonb_agg(row_to_json(r)) FROM pg_ripple.sparql(pg_ripple.sparql_from_nl(%s)) r",
(question,)
)
sparql_rows = cur.fetchone()[0] or []
# Step B: fetch the context subgraph (optional but improves answer quality).
# For simplicity we pull the 1-hop neighbourhood of every ?s in the result.
turtle_context = ""
if sparql_rows:
subjects = [row.get("s") or row.get("p") or "" for row in sparql_rows if row]
if subjects:
subj_filter = ", ".join(f"<{s}>" for s in subjects if s.startswith("http"))
if subj_filter:
cur.execute(
"SELECT pg_ripple.sparql_construct_turtle("
" 'CONSTRUCT { ?s ?p ?o } WHERE { ?s ?p ?o . FILTER(?s IN (" + subj_filter + ")) }'"
")"
)
turtle_context = cur.fetchone()[0] or ""
prompt = (
f"{SYSTEM_PROMPT}\n\n"
f"=== SPARQL RESULT ===\n{sparql_rows}\n\n"
f"=== GRAPH CONTEXT (Turtle) ===\n{turtle_context}\n\n"
f"=== QUESTION ===\n{question}"
)
resp = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": prompt}],
temperature=0.0,
)
return resp.choices[0].message.content
if __name__ == "__main__":
print(answer_question("Who does Bob know and what are their job titles?"))
When to use each approach
| Approach | Best for |
|---|---|
sparql_from_nl() alone | Precise factual questions ("how many", "list all") |
rag_context() alone | Open-ended, multi-hop questions ("tell me about X") |
| Both combined (this recipe) | Production agents that need both precision and breadth |
sparql_from_nl() + Turtle context | Scenarios where the answer needs rich narrative explanation |
See also
- NL → SPARQL —
sparql_from_nldeep dive and tuning guide. - AI Overview — decision tree for all AI features.
- Cookbook: Grounded Chatbot — simpler single-pass variant.
- Cookbook: SPARQL Repair Workflow — what to do when NL→SPARQL fails.
- AI Agent Integration — LangChain / LlamaIndex wiring.
Schema-Aware NL→SPARQL with Vocabulary Bundles (v0.119.0)
When your dataset uses custom ontologies or domain-specific vocabularies,
sparql_from_nl() can automatically include vocabulary metadata in the LLM
system prompt, improving translation accuracy for ontology-rich knowledge graphs.
Enable bundle injection
-- Enable vocabulary bundle metadata in NL→SPARQL prompts (default: on)
SET pg_ripple.nl_sparql_include_bundles = on;
When enabled, sparql_from_nl() queries the triple store for:
skos:prefLabel— preferred labels for vocabulary termsdcterms:title— dataset and resource titlesschema:name— Schema.org type namesfoaf:name— FOAF person/org names
This metadata is prepended to the LLM prompt as grounding context, helping the model generate property URIs that match your actual data.
Disable for performance
For datasets that do not use these vocabulary predicates, disable to skip the extra SPI query:
SET pg_ripple.nl_sparql_include_bundles = off;