Building the Query Agent: Prolog's Flexibility in Action
In my previous posts, I outlined the theory: use LLMs as middleware for natural language understanding, while Prolog handles the precise logic. Today, I'm showing how it can answer database queries.
The first example shows the action logic of a car sales agent. Now, let's see how flexible Prolog can be as a query engine. This also means your agent can search beyond context. In practice, the Prolog database can be populated on the fly, with data from APIs, web scraping, or other sources. This also protects your sources from direct access.
(I added a post on how to get Prolog and Python working together here).
In this post I show how Prolog can translate user queries into precise logical statements, allowing for complex filtering, aggregation, and flexible querying patterns.
The heart of our system is a clean Prolog knowledge base:
car(bmw_1, bmw, series1, black, 2020, 30000).
car(audi_1, audi, a4, blue, 2019, 35000).
car(toyota_1, toyota, corolla, red, 2018, 20000).But here's where Prolog shines: we can create multiple views into this same data effortlessly:
make(CarId, Make) :- car(CarId, Make, _, _, _, _).
price(CarId, Price) :- car(CarId, _, _, _, _, Price).
year(CarId, Year) :- car(CarId, _, _, _, Year, _).And also add aliases to common queries:
luxury_car(CarId) :- price(CarId, Price), Price > 40000.
economy_car(CarId) :- price(CarId, Price), Price =< 20000.Need country of origin data? Just add the facts:
country_of_origin(bmw, germany).
country_of_origin(toyota, japan).And this will enable users to filter by country of origin without any changes to existing logic. The LLM will combine these facts as needed.
Want to add a "describe" helper for user-friendly output? Easy:
describe(CarId, Description) :-
car(CarId, Make, Model, Color, Year, Price),
format(string(Description), "~w ~w ~w (~w) - $~w",
[Year, Color, Make, Model, Price]).And Prolog will use it in queries, obtaining precisely formatted results
as part of the response.
Query Flexibility in Practice
Here's what makes this approach beautiful: the same knowledge base can handle vastly different query patterns without any code changes.
Simple lookup: "Show me all BMWs"
Translates to: `
make(CarId, bmw)`
Complex filtering: "German cars under $40,000 from 2020 or later"
make(CarId, Make), country_of_origin(Make, germany), price(CarId, Price), Price < 40000, year(CarId, Year), Year >= 2020
Aggregate queries: "How many cars do we have?"
Maps to:
count_cars(Count)
The LLM's job is simply to translate natural language into these Prolog queries. Once that's done, Prolog's inference engine takes over, guaranteeing consistent, logical results.
The Architecture in Action
The Python harness is surprisingly simple.
We form the prompt for the LLM to generate the Prolog query, and hinting at `describe/2` for formatting:
def generate_prolog_query(prolog_code: str) -> str:
lines = "You are a prolog query agent."
lines += "Consider the facts in the database, and translate the user query into a prolog query."
lines += "It is best to compose small predicates and separate attributes instead of row-queries."
lines += "Use compound queries, using small clauses, and variables as needed."
lines += "The describe/2 predicate is useful to use with queries to format the output."
lines += "The prolog code is as follows:\n"
lines += prolog_code
return linesThe LLM generates the Prolog query accordingly:
class Query(BaseModel):
clauses: list[str] = Field(..., description="List of clauses in the query")
query: str = Field(..., description="The full prolog query")Here, I've found that redundancy in `clauses` and `query` avoided some small syntax issues.
LLM translates the user's natural language question into a Prolog query:
user_input = input("Enter your query: ")
response = openai.responses.parse(
model="gpt-5-mini",
instructions=generate_prolog_query(prolog_code),
input=user_input,
text_format=Query
)We will find the query in `response.output_parsed.query`, and then execute it in Prolog:
prolog_query = response.output_parsed.query
results = list(prolog.query(prolog_query))Finally, we call the LLM again to format the results nicely for the user:
response = openai.responses.create(
input=f"{results}",
model="gpt-5-mini",
instructions="Layout the results in a user-friendly way. Use describe/2(CarId, Description) to gather more information about each
car. If no results, say no results found.",
)

