Skip to content

Add ToolsRetriever class and convert_retriever_to_tool() function #332

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from

Conversation

oskarhane
Copy link
Member

@oskarhane oskarhane commented May 8, 2025

Description

In addition to the major parts of this PR, I've refactored and moved the tool.py file into its own directory to group relevant tool files together. This move touches a lot of files because of updated imports.

ToolsRetriever

Now that we have tool support in the LLM interface, let's create a retriever that can use one or more tools to retrieve data.

This retriever follows the Retriever interface so it can be used within the GraphRAG class to get the full e2e experience (see example file).

The ToolsRetriever uses an LLM to decide on what tools to use to find the relevant data.

calendar_tool = CalendarTool()
weather_tool = WeatherTool()

llm = OpenAILLM(...)
driver = GraphDatabase.driver(...)

tools_retriever = ToolsRetriever(
    driver=driver,
    llm=llm,
    tools=[calendar_tool, weather_tool],
)

graphrag = GraphRAG(
    llm=llm,
    retriever=tools_retriever,
)

result = graphrag.search(query_text="Tell me about tomorrow", return_context=False)

convert_retriever_to_tool ()

This function is a way to convert a Retriever to a Tool so it can be used within the ToolsRetriever. This is useful when you might want to have both a VectorRetriever and a Text2CypherRetreiver as a fallback.
See new example files for usage.

Type of Change

  • New feature
  • Bug fix
  • Breaking change
  • Documentation update
  • Project configuration change

How Has This Been Tested?

  • Unit tests
  • E2E tests
  • Manual tests

Checklist

The following requirements should have been met (depending on the changes in the branch):

  • Documentation has been updated
  • Unit tests have been updated
  • E2E tests have been updated
  • Examples have been updated
  • New files have copyright header
  • CLA (https://neo4j.com/developer/cla/) has been signed
  • CHANGELOG.md updated if appropriate

@oskarhane oskarhane requested a review from a team as a code owner May 8, 2025 10:47

# Define parameters for the static retriever tool
static_parameters = ObjectParameter(
description="Parameters for the Neo4j information retriever",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this description really required? It doesn't seem to have a real added-value.

)

# Convert the retriever to a tool with specific parameters
static_tool = convert_retriever_to_tool(
Copy link
Contributor

@stellasia stellasia May 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd say this should be a method from the Retriever class, and parameters should be encapsulated in this class as well, these parameters are bound to the search method and won't change from one instance to another.

So something like:

class Retriever:
   def get_parameters(self) -> ObjectParameter:
       raise NotImplementedError()  # need to be implemented in subclasses

   def convert_to_tool(self, name: str, description: Optional[str] = None) -> Tool:
       # rest of the function goes here

Note: as a future improvement, I think we could infer the parameters from the search method signature without having to redeclare it.


def __init__(
self,
driver: neo4j.Driver,
Copy link
Contributor

@stellasia stellasia May 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we consider updating the Retriever interface if this one does not use the driver?

# Extract arguments from the tool call
tool_args = tool_call.arguments or {}

# Always include the query_text in the arguments for tools that might need it
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't the LLM include the query if needed by the tool? It should be in the tool parameters in this case?

# Execute the tool with the provided arguments
tool_result = selected_tool.execute(**tool_args)
# If the tool result is a RawSearchResult, extract its records
if hasattr(tool_result, "records"):
Copy link
Contributor

@stellasia stellasia May 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can maybe enforce the fact that Tool.execute must return a list of something (or list of strings?), or do we really want to create fake records?

@@ -211,23 +211,28 @@ def validate_properties(self) -> "ObjectParameter":
class Tool(ABC):
"""Abstract base class defining the interface for all tools in the neo4j-graphrag library."""

_name: str
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you remember why we need to have these declarations here? Mypy stuff?

"""
# Use provided name or infer it from the retriever
if name is None:
name = getattr(retriever, "name", None) or getattr(
Copy link
Contributor

@stellasia stellasia May 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we expected retrievers to have a name attribute, maybe we should add this to the interface?

# arguments like query_text, top_k, etc., passed as keyword arguments.
# The Tool's 'parameters' definition (e.g., ObjectParameter) ensures
# that these arguments are provided in kwargs when Tool.execute is called.
return retriever.get_search_results(**kwargs)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not using the search method?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants