, authorized contracts are foundational paperwork that outline the relationships, obligations, and duties between events. Whether or not it’s a partnership settlement, an NDA, or a provider contract, these paperwork typically include crucial data that drives decision-making, danger administration, and compliance. Nevertheless, navigating and extracting insights from these contracts could be a advanced and time-consuming course of.
On this put up, we’ll discover how we will streamline the method of understanding and dealing with authorized contracts by implementing an end-to-end resolution utilizing Agentic Graphrag. I see GraphRAG as an umbrella time period for any methodology that retrieves or causes over data saved in a data graph, enabling extra structured and context-aware responses.
By structuring authorized contracts right into a data graph in Neo4j, we will create a robust repository of knowledge that’s straightforward to question and analyze. From there, we’ll construct a LangGraph agent that permits customers to ask particular questions concerning the contracts, making it potential to quickly uncover new insights.
The code is on the market on this GitHub repository.
Why structuring knowledge issues
Some domains work properly with naive RAG, however authorized contracts current distinctive challenges.

As proven within the picture, relying solely on a vector index to retrieve related chunks can introduce dangers, similar to pulling data from irrelevant contracts. It’s because authorized language is very structured, and related wording throughout totally different agreements can result in incorrect or deceptive retrieval. These limitations spotlight the necessity for a extra structured method, similar to GraphRAG, to make sure exact and context-aware retrieval.
To implement GraphRAG, we first must assemble a data graph.

To construct a data graph for authorized contracts, we want a option to extract structured data from paperwork and retailer it alongside the uncooked textual content. An LLM will help by studying by way of contracts and figuring out key particulars similar to events, dates, contract varieties, and vital clauses. As an alternative of treating the contract as only a block of textual content, we break it down into structured parts that replicate its underlying authorized which means. For instance, an LLM can acknowledge that “ACME Inc. agrees to pay $10,000 per thirty days beginning January 1, 2024” accommodates each a fee obligation and a begin date, which we will then retailer in a structured format.
As soon as we now have this structured knowledge, we retailer it in a data graph, the place entities like firms, agreements, and clauses are represented as represented together with their relationships. The unstructured textual content stays obtainable, however now we will use the structured layer to refine our searches and make retrieval way more exact. As an alternative of simply fetching probably the most related textual content chunks, we will filter contracts based mostly on their attributes. This implies we will reply questions that naive RAG would wrestle with, similar to what number of contracts have been signed final month or whether or not we now have any energetic agreements with a selected firm. These questions require aggregation and filtering, which isn’t potential with commonplace vector-based retrieval alone.
By combining structured and unstructured knowledge, we additionally make retrieval extra context-aware. If a consumer asks a couple of contract’s fee phrases, we be certain that the search is constrained to the proper settlement fairly than counting on textual content similarity, which could pull in phrases from unrelated contracts. This hybrid method overcomes the restrictions of naive RAG and permits for a a lot deeper and extra dependable evaluation of authorized paperwork.
Graph development
We’ll leverage an LLM to extract structured data from authorized paperwork, utilizing the CUAD (Contract Understanding Atticus Dataset), a broadly used benchmark dataset for contract evaluation licensed beneath CC BY 4.0. CUAD dataset accommodates over 500 contracts, making it a great dataset for evaluating our structured extraction pipeline.
The token rely distribution for the contracts is visualized under.

Most contracts on this dataset are comparatively quick, with token counts under 10,000. Nevertheless, there are some for much longer contracts, with just a few reaching as much as 80,000 tokens. These lengthy contracts are uncommon, whereas shorter ones make up the bulk. The distribution exhibits a steep drop-off, which means lengthy contracts are the exception fairly than the rule.
We’re utilizing Gemini-2.0-Flash for extraction, which has a 1 million token enter restrict, so dealing with these contracts isn’t an issue. Even the longest contracts in our dataset (round 80,000 tokens) match properly throughout the mannequin’s capability. Since most contracts are a lot shorter, we don’t have to fret about truncation or breaking paperwork into smaller chunks for processing.
Structured knowledge extraction
Most business LLMs have the choice to make use of Pydantic objects to outline the schema of the output. An instance for location:
class Location(BaseModel):
"""
Represents a bodily location together with handle, metropolis, state, and nation.
"""
handle: Non-obligatory[str] = Area(
..., description="The road handle of the situation.Use None if not offered"
)
metropolis: Non-obligatory[str] = Area(
..., description="The town of the situation.Use None if not offered"
)
state: Non-obligatory[str] = Area(
..., description="The state or area of the situation.Use None if not offered"
)
nation: str = Area(
...,
description="The nation of the situation. Use the two-letter ISO commonplace.",
)
When utilizing LLMs for structured output, Pydantic helps outline a transparent schema by specifying the forms of attributes and offering descriptions that information the mannequin’s responses. Every subject has a kind, similar to str
or Non-obligatory[str]
, and an outline that tells the LLM precisely easy methods to format the output.
For instance, in a Location
mannequin, we outline key attributes like handle
, metropolis
, state
, and nation
, specifying what knowledge is predicted and the way it ought to be structured. The nation
subject, as an illustration, follows two-letter nation code commonplace like "US"
, "FR"
, or "JP"
, as a substitute of inconsistent variations like “United States” or “USA.” This precept applies to different structured knowledge as properly, ISO 8601 retains dates in a normal format (YYYY-MM-DD
), and so forth.
By defining structured output with Pydantic, we make LLM responses extra dependable, machine-readable, and simpler to combine into databases or APIs. Clear subject descriptions additional assist the mannequin generate appropriately formatted knowledge, decreasing the necessity for post-processing.
The Pydantic schema fashions will be extra subtle just like the Contract mannequin under, which captures key particulars of a authorized settlement, guaranteeing the extracted knowledge follows a standardized construction.
class Contract(BaseModel):
"""
Represents the important thing particulars of the contract.
"""
abstract: str = Area(
...,
description=("Excessive degree abstract of the contract with related details and particulars. Embrace all related data to supply full image."
"Do no use any pronouns"),
)
contract_type: str = Area(
...,
description="The kind of contract being entered into.",
enum=CONTRACT_TYPES,
)
events: Listing[Organization] = Area(
...,
description="Listing of events concerned within the contract, with particulars of every occasion's position.",
)
effective_date: str = Area(
...,
description=(
"Enter the date when the contract turns into efficient in yyyy-MM-dd format."
"If solely the yr (e.g., 2015) is understood, use 2015-01-01 because the default date."
"All the time fill in full date"
),
)
contract_scope: str = Area(
...,
description="Description of the scope of the contract, together with rights, duties, and any limitations.",
)
period: Non-obligatory[str] = Area(
None,
description=(
"The period of the settlement, together with provisions for renewal or termination."
"Use ISO 8601 durations commonplace"
),
)
end_date: Non-obligatory[str] = Area(
None,
description=(
"The date when the contract expires. Use yyyy-MM-dd format."
"If solely the yr (e.g., 2015) is understood, use 2015-01-01 because the default date."
"All the time fill in full date"
),
)
total_amount: Non-obligatory[float] = Area(
None, description="Complete worth of the contract."
)
governing_law: Non-obligatory[Location] = Area(
None, description="The jurisdiction's legal guidelines governing the contract."
)
clauses: Non-obligatory[List[Clause]] = Area(
None, description=f"""Related summaries of clause varieties. Allowed clause varieties are {CLAUSE_TYPES}"""
)
This contract schema organizes key particulars of authorized agreements in a structured means, making it simpler to research with LLMs. It contains several types of clauses, similar to confidentiality or termination, every with a brief abstract. The events concerned are listed with their names, areas, and roles, whereas contract particulars cowl issues like begin and finish dates, complete worth, and governing legislation. Some attributes, similar to governing legislation, will be outlined utilizing nested fashions, enabling extra detailed and complicated outputs.
The nested object method works properly with some AI fashions that deal with advanced knowledge relationships, whereas others could wrestle with deeply nested particulars.
We are able to take a look at our method utilizing the next instance. We’re utilizing the LangChain framework to orchestrate LLMs.
llm = ChatGoogleGenerativeAI(mannequin="gemini-2.0-flash")
llm.with_structured_output(Contract).invoke(
"Tomaz works with Neo4j since 2017 and can make a billion greenback till 2030."
"The contract was signed in Las Vegas"
)
which outputs
Contract(
abstract="Tomaz works with Neo4j since 2017 and can make a billion greenback till 2030.",
contract_type="Service",
events=[
Organization(
name="Tomaz",
location=Location(
address=None,
city="Las Vegas",
state=None,
country="US"
),
role="employee"
),
Organization(
name="Neo4j",
location=Location(
address=None,
city=None,
state=None,
country="US"
),
role="employer"
)
],
effective_date="2017-01-01",
contract_scope="Tomaz will work with Neo4j",
period=None,
end_date="2030-01-01",
total_amount=1_000_000_000.0,
governing_law=None,
clauses=None
)
Now that our contract knowledge is in a structured format, we will outline the Cypher question wanted to import it into Neo4j, mapping entities, relationships, and key clauses right into a graph construction. This step transforms uncooked extracted knowledge right into a queryable data graph, enabling environment friendly traversal and retrieval of contract insights.
UNWIND $knowledge AS row
MERGE (c:Contract {file_id: row.file_id})
SET c.abstract = row.abstract,
c.contract_type = row.contract_type,
c.effective_date = date(row.effective_date),
c.contract_scope = row.contract_scope,
c.period = row.period,
c.end_date = CASE WHEN row.end_date IS NOT NULL THEN date(row.end_date) ELSE NULL END,
c.total_amount = row.total_amount
WITH c, row
CALL (c, row) {
WITH c, row
WHERE row.governing_law IS NOT NULL
MERGE (c)-[:HAS_GOVERNING_LAW]->(l:Location)
SET l += row.governing_law
}
FOREACH (occasion IN row.events |
MERGE (p:Occasion {identify: occasion.identify})
MERGE (p)-[:HAS_LOCATION]->(pl:Location)
SET pl += occasion.location
MERGE (p)-[pr:PARTY_TO]->(c)
SET pr.position = occasion.position
)
FOREACH (clause IN row.clauses |
MERGE (c)-[:HAS_CLAUSE]->(cl:Clause {sort: clause.clause_type})
SET cl.abstract = clause.abstract
)
This Cypher question imports structured contract knowledge into Neo4j by creating Contract
nodes with attributes similar to abstract
, contract_type
, effective_date
, period
, and total_amount
. If a governing legislation is specified, it hyperlinks the contract to a Location
node. Events concerned within the contract are saved as Occasion
nodes, with every occasion related to a Location
and assigned a task in relation to the contract. The question additionally processes clauses, creating Clause
nodes and linking them to the contract whereas storing their sort and abstract.
After processing and importing the contracts, the ensuing graph follows the next graph schema.

Let’s additionally check out a single contract.

This graph represents a contract construction the place a contract (orange node) connects to numerous clauses (purple nodes), events (blue nodes), and areas (violet nodes). The contract has three clauses: Renewal & Termination, Legal responsibility & Indemnification, and Confidentiality & Non-Disclosure. Two events, Modus Media Worldwide and Dragon Techniques, Inc., are concerned, every linked to their respective areas, Netherlands (NL) and United States (US). The contract is ruled by U.S. legislation. The contract node additionally accommodates further metadata, together with dates and different related particulars.
A public read-only occasion containing CUAD authorized contracts is on the market with the next credentials.
URI: neo4j+s://demo.neo4jlabs.com
username: legalcontracts
password: legalcontracts
database: legalcontracts
Entity decision
Entity decision in authorized contracts is difficult attributable to variations in how firms, people, and areas are referenced. An organization may seem as “Acme Inc.” in a single contract and “Acme Company” in one other, requiring a course of to find out whether or not they discuss with the identical entity.
One method is to generate candidate matches utilizing textual content embeddings or string distance metrics like Levenshtein distance. Embeddings seize semantic similarity, whereas string distance measures character-level variations. As soon as candidates are recognized, further analysis is required, evaluating metadata similar to addresses or tax IDs, analyzing shared relationships within the graph, or incorporating human evaluation for crucial circumstances.
For resolving entities at scale, each open-source options like Dedupe and business instruments like Senzing supply automated strategies. Choosing the proper method will depend on knowledge high quality, accuracy necessities, and whether or not handbook oversight is possible.
With the authorized graph constructed, we will transfer onto the agentic GraphRAG implementation.
Agentic GraphRAG
Agentic architectures fluctuate broadly in complexity, modularity, and reasoning capabilities. At their core, these architectures contain an LLM appearing as a central reasoning engine, typically supplemented with instruments, reminiscence, and orchestration mechanisms. The important thing differentiator is how a lot autonomy the LLM has in making selections and the way interactions with exterior programs are structured.
One of many easiest and handiest designs, significantly for chatbot-like implementations, is a direct LLM-with-tools method. On this setup, the LLM serves because the decision-maker, dynamically deciding on which instruments to invoke (if any), retrying operations when essential, and executing a number of instruments in sequence to satisfy advanced requests.

The diagram represents a easy LangGraph agent workflow. It begins at __start__
, shifting to the assistant
node, the place the LLM processes consumer enter. From there, the assistant can both name instruments
to fetch related data or transition on to __end__
to finish the interplay. If a software is used, the assistant processes the response earlier than deciding whether or not to name one other software or finish the session. This construction permits the agent to autonomously decide when exterior data is required earlier than responding.
This method is especially well-suited to stronger business fashions like Gemini or GPT-4o, which excel at reasoning and self-correction.
Instruments
LLMs are highly effective reasoning engines, however their effectiveness typically will depend on how properly they’re geared up with exterior instruments. These instruments , whether or not database queries, APIs, or search features, lengthen an LLM’s skill to retrieve details, carry out calculations, or work together with structured knowledge.

Designing instruments which might be each normal sufficient to deal with numerous queries and exact sufficient to return significant outcomes is extra artwork than science. What we’re actually constructing is a semantic layer between the LLM and the underlying knowledge. Slightly than requiring the LLM to know the precise construction of a Neo4j data graph or a database schema, we outline instruments that summary away these complexities.
With this method, the LLM doesn’t must know whether or not contract data is saved as graph nodes and relationships or as uncooked textual content in a doc retailer. It solely must invoke the proper software to fetch related knowledge based mostly on a consumer’s query.
In our case, the contract retrieval software serves as this semantic interface. When a consumer asks about contract phrases, obligations, or events, the LLM calls a structured question software that interprets the request right into a database question, retrieves related data, and presents it in a format the LLM can interpret and summarize. This allows a versatile, model-agnostic system the place totally different LLMs can work together with contract knowledge with no need direct data of its storage or construction.
There’s no one-size-fits-all commonplace for designing an optimum toolset. What works properly for one mannequin could fail for an additional. Some fashions deal with ambiguous software directions gracefully, whereas others wrestle with advanced parameters or require specific prompting. The trade-off between generality and task-specific effectivity means software design requires iteration, testing, and fine-tuning for the LLM in use.
For contract evaluation, an efficient software ought to retrieve contracts and summarize key phrases with out requiring customers to phrase queries rigidly. Attaining this flexibility will depend on considerate immediate engineering, strong schema design, and adaptation to totally different LLM capabilities. As fashions evolve, so do methods for making instruments extra intuitive and efficient.
On this part, we’ll discover totally different approaches to software implementation, evaluating their flexibility, effectiveness, and compatibility with numerous LLMs.
My most well-liked method is to dynamically and deterministically assemble a Cypher question and execute it towards the database. This methodology ensures constant and predictable question era whereas sustaining implementation flexibility. By structuring queries this fashion, we reinforce the semantic layer, permitting consumer inputs to be seamlessly translated into database retrievals. This retains the LLM centered on retrieving related data fairly than understanding the underlying knowledge mannequin.
Our software is meant to determine related contracts, so we have to present the LLM with choices to look contracts based mostly on numerous attributes. The enter description is once more offered as a Pydantic object.
class ContractInput(BaseModel):
min_effective_date: Non-obligatory[str] = Area(
None, description="Earliest contract efficient date (YYYY-MM-DD)"
)
max_effective_date: Non-obligatory[str] = Area(
None, description="Newest contract efficient date (YYYY-MM-DD)"
)
min_end_date: Non-obligatory[str] = Area(
None, description="Earliest contract finish date (YYYY-MM-DD)"
)
max_end_date: Non-obligatory[str] = Area(
None, description="Newest contract finish date (YYYY-MM-DD)"
)
contract_type: Non-obligatory[str] = Area(
None, description=f"Contract sort; legitimate varieties: {CONTRACT_TYPES}"
)
events: Non-obligatory[List[str]] = Area(
None, description="Listing of events concerned within the contract"
)
summary_search: Non-obligatory[str] = Area(
None, description="Examine abstract of the contract"
)
nation: Non-obligatory[str] = Area(
None, description="Nation the place the contract applies. Use the two-letter ISO commonplace."
)
energetic: Non-obligatory[bool] = Area(None, description="Whether or not the contract is energetic")
monetary_value: Non-obligatory[MonetaryValue] = Area(
None, description="The whole quantity or worth of a contract"
)
With LLM instruments, attributes can take numerous types relying on their goal. Some fields are easy strings, similar to contract_type
and nation
, which retailer single values. Others, like events
, are lists of strings, permitting a number of entries (e.g., a number of entities concerned in a contract).
Past fundamental knowledge varieties, attributes can even signify advanced objects. For instance, monetary_value
makes use of a MonetaryValue
object, which incorporates structured knowledge similar to foreign money sort and the operator. Whereas attributes with nested objects supply a transparent and structured illustration of knowledge, fashions are inclined to wrestle to deal with them successfully, so we must always maintain them easy.
As a part of this mission, we’re experimenting with a further cypher_aggregation
attribute, offering the LLM with larger flexibility for situations that require particular filtering or aggregation.
cypher_aggregation: Non-obligatory[str] = Area(
None,
description="""Customized Cypher assertion for superior aggregations and analytics.
This might be appended to the bottom question:
```
MATCH (c:Contract)
WITH c, abstract, contract_type, contract_scope, effective_date, end_date, events, energetic, monetary_value, contract_id, international locations
```
Examples:
1. Depend contracts by sort:
```
RETURN contract_type, rely(*) AS rely ORDER BY rely DESC
```
2. Calculate common contract period by sort:
```
WITH contract_type, effective_date, end_date
WHERE effective_date IS NOT NULL AND end_date IS NOT NULL
WITH contract_type, period.between(effective_date, end_date).days AS period
RETURN contract_type, avg(period) AS avg_duration ORDER BY avg_duration DESC
```
3. Calculate contracts per efficient date yr:
```
RETURN effective_date.yr AS yr, rely(*) AS rely ORDER BY yr
```
4. Counts the occasion with the best variety of energetic contracts:
```
UNWIND events AS occasion
WITH occasion.identify AS party_name, energetic, rely(*) AS contract_count
WHERE energetic = true
RETURN party_name, contract_count
ORDER BY contract_count DESC
LIMIT 1
```
"""
The cypher_aggregation
attribute permits LLMs to outline customized Cypher statements for superior aggregations and analytics. It extends the bottom question by appending question-specified aggregation logic, enabling versatile filtering and computation.
This function helps use circumstances similar to counting contracts by sort, calculating common contract period, analyzing contract distributions over time, and figuring out key events based mostly on contract exercise. By leveraging this attribute, the LLM can dynamically generate insights tailor-made to particular analytical wants with out requiring predefined question buildings.
Whereas this flexibility is efficacious, it ought to be rigorously evaluated, as elevated adaptability comes at the price of lowered consistency and robustness as a result of added complexity of the operation.
We should clearly outline the perform’s identify and outline when presenting it to the LLM. A well-structured description helps information the mannequin in utilizing the perform appropriately, guaranteeing it understands its goal, anticipated inputs, and outputs. This reduces ambiguity and improves the LLM’s skill to generate significant and dependable queries.
class ContractSearchTool(BaseTool):
identify: str = "ContractSearch"
description: str = (
"helpful for when you should reply questions associated to any contracts"
)
args_schema: Kind[BaseModel] = ContractInput
Lastly, we have to implement a perform that processes the given inputs, constructs the corresponding Cypher assertion, and executes it effectively.
The core logic of the perform facilities on developing the Cypher assertion. We start by matching the contract as the muse of the question.
cypher_statement = "MATCH (c:Contract) "
Subsequent, we have to implement the perform that processes the enter parameters. On this instance, we primarily use attributes to filter contracts based mostly on the given standards.
Easy property filtering
For instance, the contract_type
attribute is used to carry out easy node property filtering.
if contract_type:
filters.append("c.contract_type = $contract_type")
params["contract_type"] = contract_type
This code provides a Cypher filter for contract_type
whereas utilizing question parameters for values to stop question injection safety situation.
Because the potential contract sort values are introduced within the attribute description
contract_type: Non-obligatory[str] = Area(
None, description=f"Contract sort; legitimate varieties: {CONTRACT_TYPES}"
)
we don’t have to fret about mapping values from enter to legitimate contract varieties because the LLM will deal with that.
Inferred property filtering
We’re constructing instruments for an LLM to work together with a data graph, the place the instruments function an abstraction layer over structured queries. A key function is the power to make use of inferred properties at runtime, much like an ontology however dynamically computed.
if energetic is just not None:
operator = ">=" if energetic else "<"
filters.append(f"c.end_date {operator} date()")
Right here, energetic
acts as a runtime classification, figuring out whether or not a contract is ongoing (>= date()
) or expired (< date()
). This logic extends structured KG queries by computing properties solely when wanted, enabling extra versatile LLM reasoning. By dealing with logic like this inside instruments, we make sure the LLM interacts with simplified, intuitive operations, conserving it centered on reasoning fairly than question formulation.
Neighbor filtering
Typically filtering will depend on neighboring nodes, similar to limiting outcomes to contracts involving particular events. The events
attribute is an non-compulsory checklist, and when offered, it ensures solely contracts linked to these entities are thought-about:
if events:
parties_filter = []
for i, occasion in enumerate(events):
party_param_name = f"party_{i}"
parties_filter.append(
f"""EXISTS {{
MATCH (c)<-[:PARTY_TO]-(occasion)
WHERE toLower(occasion.identify) CONTAINS ${party_param_name}
}}"""
)
params[party_param_name] = occasion.decrease()
This code filters contracts based mostly on their related events, treating the logic as AND, which means all specified situations have to be met for a contract to be included. It iterates by way of the offered events
checklist and constructs a question the place every occasion situation should maintain.
For every occasion, a novel parameter identify is generated to keep away from conflicts. The EXISTS
clause ensures that the contract has a PARTY_TO
relationship to a celebration whose identify accommodates the desired worth. The identify is transformed to lowercase to permit case-insensitive matching. Every occasion situation is added individually, implementing an implicit AND between them.
If extra advanced logic have been wanted, similar to supporting OR situations or permitting totally different matching standards, the enter would want to alter. As an alternative of a easy checklist of occasion names, a structured enter format specifying operators could be required.
Moreover, we may implement a party-matching methodology that tolerates minor typos, enhancing the consumer expertise by dealing with variations in spelling and formatting.
Customized operator filtering
So as to add extra flexibility, we will introduce an operator object as a nested attribute, permitting extra management over filtering logic. As an alternative of hardcoding comparisons, we outline an enumeration for operators and use it dynamically.
For instance, with financial values, a contract may have to be filtered based mostly on whether or not its complete quantity is bigger than, lower than, or precisely equal to a specified worth. As an alternative of assuming a set comparability logic, we outline an enum that represents the potential operators:
class NumberOperator(str, Enum):
EQUALS = "="
GREATER_THAN = ">"
LESS_THAN = "<"
class MonetaryValue(BaseModel):
"""The whole quantity or worth of a contract"""
worth: float
operator: NumberOperator
if monetary_value:
filters.append(f"c.total_amount {monetary_value.operator.worth} $total_value")
params["total_value"] = monetary_value.worth
This method makes the system extra expressive. As an alternative of inflexible filtering guidelines, the software interface permits the LLM to specify not only a worth however the way it ought to be in contrast, making it simpler to deal with a broader vary of queries whereas conserving the LLM’s interplay easy and declarative.
Some LLMs wrestle with nested objects as inputs, making it more durable to deal with structured operator-based filtering. Including a between operator introduces further complexity because it requires two separate values, which might result in ambiguity in parsing and enter validation.
Min and Max attributes
To maintain issues less complicated, I are inclined to gravitate towards utilizing min
and max
attributes for dates, as this naturally helps vary filtering and makes the between logic easy.
if min_effective_date:
filters.append("c.effective_date >= date($min_effective_date)")
params["min_effective_date"] = min_effective_date
if max_effective_date:
filters.append("c.effective_date <= date($max_effective_date)")
params["max_effective_date"] = max_effective_date
This perform filters contracts based mostly on an efficient date vary by including an non-compulsory decrease and higher certain situation when min_effective_date
and max_effective_date
are offered, guaranteeing that solely contracts throughout the specified date vary are included.
Semantic search
An attribute will also be used for semantic search, the place as a substitute of counting on a vector index upfront, we use a post-filtering method to metadata filtering. First, structured filters, like date ranges, financial values, or events, are utilized to slim down the candidate set. Then, vector search is carried out over this filtered subset to rank outcomes based mostly on semantic similarity.
if summary_search:
cypher_statement += (
"WITH c, vector.similarity.cosine(c.embedding, $embedding) "
"AS rating ORDER BY rating DESC WITH c, rating WHERE rating > 0.9 "
) # Outline a threshold restrict
params["embedding"] = embeddings.embed_query(summary_search)
else: # Else we type by newest
cypher_statement += "WITH c ORDER BY c.effective_date DESC "
This code applies semantic search when summary_search
is offered by computing cosine similarity between the contract’s embedding and the question embedding, ordering outcomes by relevance, and filtering out low-scoring matches with a threshold of 0.9. In any other case, it defaults to sorting contracts by the newest effective_date
.
Dynamic queries
The cypher aggregation attribute is an experiment I needed to check that provides the LLM a level of partial text2cypher functionality, permitting it to dynamically generate aggregations after the preliminary structured filtering. As an alternative of predefining each potential aggregation, this method lets the LLM specify calculations like counts, averages, or grouped summaries on demand, making queries extra versatile and expressive. Nevertheless, since this shifts extra question logic to the LLM, guaranteeing all generated queries work appropriately turns into difficult, as malformed or incompatible Cypher statements can break execution. This trade-off between flexibility and reliability is a key consideration in designing the system.
if cypher_aggregation:
cypher_statement += """WITH c, c.abstract AS abstract, c.contract_type AS contract_type,
c.contract_scope AS contract_scope, c.effective_date AS effective_date, c.end_date AS end_date,
[(c)<-[r:PARTY_TO]-(occasion) | {occasion: occasion.identify, position: r.position}] AS events, c.end_date >= date() AS energetic, c.total_amount as monetary_value, c.file_id AS contract_id,
apoc.coll.toSet([(c)<-[:PARTY_TO]-(occasion)-[:LOCATED_IN]->(nation) | nation.identify]) AS international locations """
cypher_statement += cypher_aggregation
If no cypher aggregation is offered, we return the overall rely of recognized contracts together with solely 5 instance contracts to keep away from overwhelming the immediate. Dealing with extreme rows is essential, as an LLM scuffling with a large end result set isn’t helpful. Moreover, LLM producing solutions with 100 contract titles isn’t a great consumer expertise both.
cypher_statement += """WITH gather(c) AS nodes
RETURN {
total_count_of_contracts: measurement(nodes),
example_values: [
el in nodes[..5] |
{abstract:el.abstract, contract_type:el.contract_type,
contract_scope: el.contract_scope, file_id: el.file_id,
effective_date: el.effective_date, end_date: el.end_date,
monetary_value: el.total_amount, contract_id: el.file_id,
events: [(el)<-[r:PARTY_TO]-(occasion) | {identify: occasion.identify, position: r.position}],
international locations: apoc.coll.toSet([(el)<-[:PARTY_TO]-()-[:LOCATED_IN]->(nation) | nation.identify])}
]
} AS output"""
This cypher assertion collects all matching contracts into a listing, returning the overall rely and as much as 5 instance contracts with key attributes, together with abstract, sort, scope, dates, financial worth, related events with roles, and distinctive nation areas.
Now that our contract search software is constructed, we hand it off to the LLM and identical to that, we now have agentic GraphRAG applied.
Agent Benchmark
In the event you’re severe about implementing agentic GraphRAG, you want an analysis dataset, not simply as a benchmark however as a basis for the whole mission. A well-constructed dataset helps outline the scope of what the system ought to deal with, guaranteeing that preliminary improvement aligns with real-world use circumstances. Past that, it turns into a useful software for evaluating efficiency, permitting you to measure how properly the LLM interacts with the graph, retrieves data, and applies reasoning. It’s additionally important for immediate engineering optimizations, letting you iteratively refine queries, software use, and response formatting with clear suggestions fairly than guesswork. With no structured dataset, you’re flying blind, making enhancements more durable to quantify and inconsistencies harder to catch.
The code for the benchmark is obtainable on GitHub.
I’ve compiled a listing of twenty-two questions which we’ll use to judge the system. Moreover, we’re going to introduce a brand new metric known as answer_satisfaction
the place we might be present a customized immediate.
answer_satisfaction = AspectCritic(
identify="answer_satisfaction",
definition="""You'll consider an ANSWER to a authorized QUESTION based mostly on a offered SOLUTION.
Price the reply on a scale from 0 to 1, the place:
- 0 = incorrect, considerably incomplete, or deceptive
- 1 = appropriate and sufficiently full
Take into account these analysis standards:
1. Factual correctness is paramount - the reply should not contradict the answer
2. The reply should handle the core parts of the answer
3. Further related data past the answer is suitable and should improve the reply
4. Technical authorized terminology ought to be used appropriately if current within the resolution
5. For quantitative authorized analyses, correct figures have to be offered
+ fewshots
"""
Many questions can return a considerable amount of data. For instance, asking for contracts signed earlier than 2020 may yield tons of of outcomes. Because the LLM receives each the overall rely and some instance entries, our analysis ought to deal with the overall rely, fairly than which particular examples the LLM chooses to point out.

The offered outcomes point out that every one evaluated fashions (Gemini 1.5 Professional, Gemini 2.0 Flash, and GPT-4o) carry out equally properly for many software calls, with GPT-4o barely outperforming the Gemini fashions (0.82 vs. 0.77). The noticeable distinction emerges primarily when partial text2cypher
is used, significantly for numerous aggregation operations.
Observe that that is solely 22 pretty easy questions, so we didn’t actually discover reasoning capabilities of LLMs.
Moreover, I’ve seen initiatives the place accuracy will be improved considerably by leveraging Python for aggregations, as LLMs usually deal with Python code era and execution higher than producing advanced Cypher queries immediately.
Internet Software
We’ve additionally constructed a easy React internet software, powered by LangGraph hosted on FastAPI, which streams responses on to the frontend. Particular because of Anej Gorkic for creating the net app.
You possibly can launch the whole stack with the next command:
docker compose up
And navigate to localhost:5173

Abstract
As LLMs acquire stronger reasoning capabilities, they, when paired with the proper instruments, can grow to be highly effective brokers for navigating advanced domains like authorized contracts. On this put up, we’ve solely scratched the floor, specializing in core contract attributes whereas barely touching the wealthy number of clauses present in real-world agreements. There’s vital room for progress, from increasing clause protection to refining software design and interplay methods.
The code is on the market on GitHub.
Photographs
All photos on this put up have been created by the creator.