KGraphLang: Knowledge Graph Query Language for Reasoning LLMs

KGraphLang is a knowledge graph query language designed for Ensemble Reasoning and Reasoning LLMs.

Reasoning LLMs are trained to split a request into steps and then consider and follow each step to complete the request, revising the steps as needed.  Ensemble Reasoning is a method of taking advantage of this step-wise reasoning process to execute steps directly during the reasoning process.  This dramatically improves the performance of A.I. Agents by eliminating the latency of switching between LLMs and tool calls.

More information about Ensemble Reasoning is available in the articles:

Why a new query language?

KGraphLang is designed to be simpler than query languages like SQL and graph query languages like SPARQL and OpenCypher.

By stripping the query language to just the essentials, we empower LLMs and fine-tuned SLMs to generate syntactically valid queries without ambiguity.

The loss of the more general and complex syntax of other query languages is made up for by defining domain specific predicates.

The KGraphLang specification defines predicates as relations between parameters.  An application of KGraphLang defines domain specific predicates such as friend(?X,?Y) which defines a friend relation between parameters ?X and ?Y.

By defining good predicates we can choose exactly what the LLM can access and hide any complexity inside the predicate implementations, keeping the LLM interface clean and minimalist.  This also allows direct control over what information the LLM can access as predicates act as the gateway to all data.

As an example, let’s consider a supply chain application.

Using KGraphLang, we can define predicates for supply chain cases such as:

  • Product Information: product(?Product, ?Supplier, ?ProductInfo)
  • Supplier Information: supplier(?Supplier, ?Product, ?SupplierInfo)
  • Shipping Routes: route(?Source, ?Destination, ?Route)
  • Weather Data and Predictions: weather(?Location, ?DateTime, ?Report)
  • Delivery Cost Prediction: cost(?Route, ?Product, ?Cost)
  • Performance Prediction: performance(?Supplier, ?Product, ?Route,
    ?Weather, ?Performance)

These predicates are implemented using queries to the underlying knowledge graph or via code, such as a weather service API.

The A.I. Agent, Porter, receives a report that the delivery of 500 controller motors that are critically needed in the manufacturing process of widgets for WidgetCo is delayed, and is given the task of finding an alternate source.

Porter can generate kgraphlang queries to lookup information using the predicates and find an alternate source of the controller motors that fit the needs of WidgetCo.  These queries are processed as they are generated, so the reasoning trajectory can change as information is retrieved in real time.

Thus, Porter can generate and execute a kgraphlang query such as:

?MinCost = min { ?C |
	?Supplier in ?Suppliers,
	route(?Supplier, WidgetCo, ?Route),
	cost(?Route, ControllerMotor, ?C)
}

during reasoning to determine the minimum delivery cost given a list of suppliers, and have this cost estimate affect the next steps of Porter’s reasoning.  The next step could be using the performance() predicate to estimate the likelihood of on-time delivery.  Porter can decide to switch to a different supplier if the cost and delivery estimate is not acceptable.  Much in the same way a Reasoning LLM can continue to reason on a math problem until a solution is found, a Reasoning LLM can continue to use kgraphlang queries until an acceptable solution is found, and the alternate source of controller motors is secured.

KGraphLang Predicates

Predicates define a relation between parameters. For knowledge graphs, these often are traversals on the knowledge graph.  For instance,

friend(?Person1, ?Person2)

could define a traversal on the graph from ?Person1 to ?Person2 along a relation or “edge” representing friendship.

These can be chained together, such as:

friend(?Person1, ?Person2), friend(?Person2, ?Person3)

which would follow a two-hop path from ?Person1 to ?Person3 along “friendship” relations.

The kgraphlang predicates can directly be implemented in code, or use an underlying data source.  For Ensemble Reasoning, the implementation uses KGraphService to implement predicates over a knowledge graph.

Besides traversing the knowledge graph, there are two other major types of predicates: Vector Similarity Predicates and String Hash Predicates.

Vector Similarity Predicates make use of vectors and vector search to find similar knowledge graph elements.  This is critical to support “Graph RAG” functionality ( https://microsoft.github.io/graphrag/).  A node representing a supplier in our earlier example could be similar to other suppliers if they supply similar products or are otherwise similar.

String Hash Predicates make use of string hashing and string hash searching to find text that is similar on a character by character basis.  This can be helpful to match names like “Jon Smith” would be very similar to “John Smyth”, or to find documents that contain overlapping language.

KGraphLang Syntax

KGraphLang supports a comprehensive syntax while maintaining simplicity to enable high quality LLM query generation.

The syntax includes:

  • Predicates implemented in code or via queries to an underlying data source
  • Predicate annotation with extra-logical values like @top_k(10) to control predicate output
  • Grouping and logical AND, OR, NOT
  • Comparisons: >, <, >=, <=, != 
  • Aggregation functions: collections, count, sum, average, max, min
  • Math functions
  • Data types: string, number, boolean, date, time, currency, geolocation, units, URIs
  • Complex data types for List, Map
  • Membership and Subset in Lists and Maps
  • Single and multi-line comments

Here’s an example:

?uri_prop = 'urn:uri_prop'^URI,
?name_prop = 'urn:name_prop'^URI,
?email_prop = 'urn:email_prop'^URI,

?prop_list = [ ?uri_prop, ?name_prop, ?email_prop ],

person_uri_list(?PersonList), 

?PersonEmailMapList = collection { 
    ?PersonMapRecord | 
    ?Pid in ?PersonList,
    get_person_map(?Pid, ?prop_list, ?PersonMapRecord)
}.

This retrieves a list of Person records, each of which having an id, name, and email address.

KGraphLang Implementation

The KGraphLang implementation evaluates kgraphlang queries based on a set of registered predicates.

The predicates are implemented either using an underlying data source or directly in code.  

Predicates implemented using KGraphService use an underlying knowledge graph implemented by a graph and vector database.  This means that, internally, the kgraphlang query is translated into a target query language such as OpenCypher, SPARQL, or GraphQL which is used with the implementing database.  So, predicates are a means of bundling complex queries into simple chunks the LLM can easily work with.

An example of a predicate directly implemented in code could be:

weather(?Location, ?DateTime, ?Report)

which could be implemented via an API call to a weather service to get an accurate weather report at the time the query is evaluated.

The KGraphLang implementation parses the query and then evaluates it, but there are cases when the parse can be used directly.  Parsing the kgraphlang query produces an AST (abstract syntax tree), which can be manipulated first and then later evaluated.  This is useful in cases of optimizing the query or replacing predicates with pre-cached values.

KGraphLang Fine-tuning

Reasoning LLMs such as R1-Distill-Llama are successful in producing valid KGraphLang queries using only prompting.  However, fine-tuning should improve generation and reduce the prompting needed for KGraphLang requests to only the predicate definitions for that request.  Also, fine-tuning SLMs should allow domain specific SLMs to produce valid KGraphLang queries with specific sets of predicates included in the training, making for a highly optimized query capability.

We’re in the process of collecting datasets to use for kgraphlang fine-tuning.

Source Code

All source code is open-source and available via GitHub.

KGraphLang is implemented in the repo:

https://github.com/vital-ai/kgraphlang

KGraphService is implemented in the repo:

https://github.com/vital-ai/kgraphservice

The Vital LLM Ensemble Reasoner, which uses KGraphLang is implemented in:

https://github.com/vital-ai/vital-llm-reasoner

The vLLM-based server running the Ensemble Reasoner is implemented in:

https://github.com/vital-ai/vital-llm-reasoner-server

Next steps

A follow-up article will present an implementation of KGraphLang predicates based on sample datasets to make it easy to run examples, and subsequent articles will explore deploying Ensemble Reasoning with kgraphlang.

If you are interested in utilizing kgraphlang as part of your Reasoning LLM and A.I. Agent implementations, please contact us at Vital.ai!

Reasoning, LLMs, Neuro-Symbolic AI, and Defeasible Logic (with Python Example)

I recently had the above interaction with ChatGPT (GPT-4).

I asked:

There is a standard 3 color traffic light that is displaying a color. The color is not green or blue. What color or colors could it be?

And got the response:

If a standard 3-color traffic light is displaying a color that is not green or blue, then it could only be displaying red. Standard traffic lights consist of red, yellow (amber), and green colors, and since blue is not a color in standard traffic lights, if it’s not displaying green, it must be red.

So, a wrong answer, and somewhat nonsensical as it had the correct information (yellow and red) a few words earlier in the same sentence, and yet then concluded the wrong answer, red.

This response was a bit of the luck of the draw. I asked the same question later in the day and got the correct response of “yellow and red”. And, there are techniques of prompting such as “Chain of Thought” that trigger LLMs to generate a response in a more stepwise fashion, leading to more accurate and reliable results. However, for such a simple case we should not have to carefully construct a prompt or hope to be lucky.

If you forgive the LLM by it being “tricked” by the prompt mentioning “blue” then you are removing one of the main use-cases of LLMs: “RAG” (retrieval augmented generation) applications. These applications perform a query of various data sources to collect potentially relevant information and then depend on the LLM to sort through it to pick out the relevant information from unstructured and messy query results to use as supporting information when generating a response. RAG Applications couldn’t work without the LLM being able to separate out and ignore irrelevant facts like a Traffic Light is not displaying blue, or pink, or purple.

By contrast, using classic symbolic artificial intelligence, with a reasoning logic language like prolog, we could define a “TrafficLight” and infer the possible colors in a couple lines of code, with no ambiguity.

Of course symbolic artificial intelligence has its own limitations, including brittleness and an inability to scale well, which is why we’ve moved on to machine learning and generative models like LLMs.

But, we should not have to give up what was good about the symbolist approach to use the new developments of artificial intelligence.

There are efforts underway to combine the symbolist approach with the newer forms of artificial intelligence. There are various names for this effort, but a popular one is Neuro-Symbolic AI.

Let’s say we are creating an application to recommend movies. A symbolist approach might define a relationship:

EnjoysGenre(Person, Genre)

and use that relationship to define facts like:

EnjoysGenre(john, scifi)

and then a further relationship could be defined by composing relationships:

LikeMovie(Person, Movie) :- EnjoysGenre(Person, Genre), HasGenre(Movie, Genre)

with this new relation then able to “predict” enjoying a movie if that movie happens to be in the genre that you like, such as:

LikeMovie(john, starwars) :- EnjoysGenre(john, scifi), HasGenre(starwars, scifi)

Neuro-Symbolic AI extends the symbolic model by learning to perform predictive tasks such as:

  • Predict instances of relations such as LikeMovie based on training with known examples. In the context of Knowledge Graphs this is known as Knowledge Graph Completion as it fills in a Knowledge Graph with predicted relationships based on existing relationships.
  • Assign weights to components of rules which would learn how much influence “Genre” should have in the relation LikeMovie compared to other components.
  • Generate new kinds of relations which could then factor into other relations, and so on, recursively. For instance, ReleaseYear or MovieCountryOfOrigin could be learned to be relations of interest and factor into relations such as LikeMovie. ForeignFilm could be learned to be the relation between MovieCountryOfOrigin and the logical NOT of PersonCountryOfOrigin and be included as a factor in LikeMovie (i.e. a foreign film to you is a film from any other country but your own country of origin). We could ask the model to come up with a relationship for DateNightMovies which it could learn to be a composition of the partners’ preferences and perhaps something more light-hearted, influenced by previous DateNights.

These tasks may use classic feature driven machine learning models and recommendation systems or may use newer techniques taking advantage of deep learning, embeddings, and transformer models. Some examples of the latter include Graph Neural Networks (see PyG), Probabilistic Soft Logic, and Logic Tensor Networks.

One aim of using Neuro-Symbolic AI vs machine learning is to make the reasoning explainable. The output can include a trace of its reasoning why it thinks you should watch the movie “Miller’s Crossing” based on the genre, director, being similar to a movie you watched and liked recently, and so forth whereas machine learning is more of a black box without much explanation possible.

Future LLMs may have Neuro-Symbolic AI modules as components, similar to how Mixture-of-Expert models combine multiple component models into one melded LLM.

Currently such Neuro-Symbolic models can be used in combination with an existing LLM, taking advantage of such techniques as “function calling”. In function calling, the LLM composes a request to an external resource (a “function”) and that function returns some information that can help the LLM complete its task. So, as example, if the LLM can generate a function call in the form of a query like:

LikeMovie(john, ?Movie)

Then the Neuro-Symbolic AI Model can take over and do the reasoning to generate ?Movie results for john, and then the LLM can use those results to complete its task. This is essentially just another “RAG” query to get contextual information to complete a task.

If we used our LLM to generate logical statements from the prompt, something like:

traffic_light(green, false)

And then used a function calling to “run” those logical statements within a logical reasoner (Neuro-Symbolic or just symbolic), we can use the LLM for what it is good at and use the reasoner for what it is good at to come to our answer.

One aspect of our simple Traffic Light question is that it rests on a finite enumerated list: green, yellow, and red. Our reasoning system must use a process of elimination. If we know that the traffic light is not green and not red, then reasoning can infer that it is yellow, even without that fact explicitly stated. This is easily accomplished in a symbolic system, but as with our example at the start, LLMs can struggle with this.

One important feature of symbolic systems that I have not seen replicated in a Neuro-Symbolic context as of yet is Defeasible Reasoning. Defeasible Reasoning allows certain knowledge to “defeat” other knowledge as part of a reasoning process. This allows new knowledge to override old knowledge, more specific knowledge to override more general knowledge, and knowledge of a higher rank to override less ranked knowledge.

Defeasible Reasoning solves the problem of an inference system coming into conflict by having rules that generate conflicting conclusions. Consider a rule such as:

All Birds Fly

which classifies all instances of the Bird class into a CanFly class. Now consider adding a rule such as:

Penguins Can Not Fly

which classifies all instances of the Penguin class into the CanNotFly class.

Now we have Penguins that are classified as both CanFly (as Birds) and CanNotFly (as Penguins) creating a logical contradiction, which, for a logical inference system, is very bad. Having A and not A both be true simultaneously grinds everything to a halt.

Defeasible Reasoning solves this by having the more specific rule for Penguins defeat the more general rule for all Birds.

Another example of this is the so-called “Nixon Diamond” problem because by one path of reasoning U.S. President Nixon was a pacifist as a Quaker (Society of Friends) and by another path of reasoning was a non-pacifist based on his Republican policies of the Vietnamese War. Defeasible Reasoning provides a tie-breaker between the pacifist and non-pacifist conclusion to avoid a logical contradiction when determining Nixon’s classification for Pacifism.

So in this case, based on ranking of rules or by supporting evidence, the path through the Republican policies “defeats” the Quaker pacifism causing Nixon to be classified as non-pacifist.

One inference engine that implements Defeasible Reasoning is the open-source Ergo Engine (https://github.com/ErgoAI/ErgoEngine). Ergo is based on frame logic making it a cross-over between a logic language and an object oriented language (via “frames” in place of objects). Besides defeasible reasoning it has other advanced features including a convenient way of expressing negative knowledge, as we’ll see in the example below.

Ergo has a python interface, and an example using python is in the repo:
https://github.com/vital-ai/vital-logic-python

:- use_argumentation_theory.
////////////////////////////////////////////////////
// defeasible reasoning example
Human::Thing.
Mortal::Thing.
Immortal::Thing.
Undead::Thing.
MagicUser::Human.
Mortality::AbstractThing.
Mortal:Mortality.
Immortal:Mortality.
Undead:Mortality.
@{default} \neg ?P:Immortal :- ?P:Human.
@{default} ?P:Mortal :- ?P:Human.
@{magical} ?X:Immortal :- ?X:MagicUser.
@{magical} \neg ?X:Mortal :- ?X:MagicUser.
\overrides({magical},default).
// Instance Data
Socrates:Human.
Merlin:Human.
Merlin:MagicUser.
// Rules
mortality(?Human, ?Mortal) :- ?Human:Human,
    ?Human:?Mortal, ?Mortal:Mortality.

Above is a screenshot from PyCharm for the vital-logic-python project and some example rules from the “test_rules.ergo” file in the project.

The classic example from Logic 101 is:

The inference from the premises “all men are mortal” and “Socrates is a man” to the conclusion “Socrates is mortal” is deductively valid.

https://en.wikipedia.org/wiki/Deductive_reasoning

The example Ergo rules above extend this Logic 101 classic to define Defeasible Rules for the class Human as being Mortal but the class Magic User as being Immortal with magical rules overriding (defeating) the default ones. We define two instances of Human, Socrates and Merlin, with Merlin being a Magic User. The rule mortality(?Human, ?Mortality) allows listing out the humans and how they classify as mortal or immortal, with the results being:

?Human = Socrates, ?Mortality = Mortal
?Human = Merlin, ?Mortality = Immortal

The rule:

\neg ?P:Immortal :- ?P:Human.

is an example of a negative rule, encoding negative information, where Humans are not classified as Immortal, unless some rule can “defeat” this.

The result of the query mortality(?Human, ?Mortality) changes for Merlin when the fact:

Merlin:MagicUser.

is added into the database. This is an example of non-monotonic reasoning as the conclusion Merlin is mortal is retracted and a new inference is added for Merlin is immortal when the fact is added. The inference engine must keep track of what conclusions to remove and which to add when facts and rules change. Being able to handle changing facts and conclusions as knowledge changes is a critical component of an AI application.

The repo contains sample python code like:

   for row in pyergo_query('?C::Thing@logic, Merlin:?C@logic.'):
        print("row", row[0])

which runs a query that uses the reasoning rules to generate results, and prints them out. So, integrating python and Ergo is pretty simple. The above prints out the classes assigned to Merlin that are also subclasses of Thing within the database called “logic”.

There is also some sample code for the Traffic Light case mentioned at the start represented as symbolic rules.

Given the python interface, it is straightforward to combine Ergo queries with python code for LLMs, using LLM libraries such as LangChain to access models like OpenAI’s GPT-4 and Anthropic’s Claude. With the function call approach mentioned above, Python can be used to integrate symbolic reasoning with LLMs. If you are a developer, hope you give it a try! We’ll have some examples of using Neuro Symbolic AI using PyG for Graph Neural Networks coming along too. These examples can be used with Agents in the Vital AI Agent Ecosystem and with Agents deployed on Chat.ai.

To learn more about the Agent Ecosystem, check out: https://www.vital.ai/agent-ecosystem.html

To learn more about deploying agents on Chat.ai check out: https://chat.ai/developers.html

If you are interested in Vital.ai helping your organization build and deploy agents, please contact us: https://www.vital.ai/about.html#contact