Recently, at Google Next '25, the Vertex AI team introduced the Agent Development Kit (ADK), a significant addition to the Vertex AI Agent Builder suite. ADK provides developers with the tools to construct advanced multi-agent systems. These systems can be integrated into enterprise platforms like Agent Space on Google Cloud, offering capabilities for robust management, deployment, and scaling.
The collaboration between Google Cloud and Neo4j over the years has involved testing and evaluating new GenAI developer frameworks during private preview stages, including Extensions, Reasoning Engine, Agent Framework, MCP Toolkit, Genkit and now the ADK. This experience allows us to share practical insights on utilizing these tools within the GCP environment.
The latest information on the ADK is available in the official GitHub repository, the comprehensive documentation and the example repository.
This article demonstrates the following procedures:
The outcome of this tutorial is presented in a Colab notebook, featuring an Investment Research Multi-Agent system that accesses a Neo4j database, utilizes custom Python functions, and integrates with the MCP Toolbox deployed as an MCP server on Google Cloud Run.
Begin by initializing your environment and installing the required libraries via uv:
uv init
uv add google-adk neo4j
Next, configure authentication. There are two primary methods, depending on whether you utilize the public Google AI Gemini API or the Vertex AI API on Google Cloud. Add one of the following configuration blocks to chat_agent/__init__.py:
# chat_agent/__init__.py
import os
# Only run this block for ML Developer API. Use your own API key.
os.environ["GOOGLE_GENAI_USE_VERTEXAI"] = "0"
os.environ["GOOGLE_API_KEY"] = "FILL YOUR API KEY"
# Only run this block for Vertex AI API Use your own project / location.
import os
os.environ["GOOGLE_GENAI_USE_VERTEXAI"] = "1"
os.environ["GOOGLE_CLOUD_PROJECT"] = "GOOGLE_CLOUD_PROJECT_ID"
os.environ["GOOGLE_CLOUD_LOCATION"] = "us-central1"
# important to make ADK work
from . import agent
The Hello World of the ADK is very straightforward, just use an LlmAgent with a model like Gemini 2.5-Pro, a name and instructions, which gives you the equivalent of a modern LLM Chat interface in 10 lines of code.
from google.adk.agents import Agent
root_agent = Agent(
name="chat_agent",
model="gemini-2.0-flash-exp",
instruction="""
You are a friendly chat companion, answer the user's questions stay social.
When asked about facts only answer for well known topics,
otherwise state that you don't know. Don't make up stuff!
"""
)
Then run uv run adk web and your're greeted by a really nice ADK web UI where you can start interacting with your newly defined agent, just select it from the top-left menu and chat. If we run uv run adk api_server, we can launch the backend API powering the Agent interface directly and access it via curl or postman.
uv run adk web
# or run just the backend API server
# uv run adk api_server
The UI hat shows a lot of debug information, including tool invocations, parameters, errors etc. You can click on most elements and they display behind-the scenes detail. A seen after the next step.
In Agents, the LLM selects and executes tools (functions) in a loop and integrates their output with reasoning and generation until the user's task is fulfilled. An LLM needs to be grounded in data to perform more reliably. That data can come from a Python function that reads a file, accesses an API or a database. To become an active agent, other tools can take action on the agent's (and indirectly the user's behalf).
We can define a few functions and make it available as a tool to our agent.
from google.adk.agents import Agent
import random
import base64
def dice(count: int = 6) -> int:
"Rolls an n-sided dice. n is the parameter as an integer"
return int(random.random() * int(count))
def base64_encode(text: str) -> str:
""" Encodes a string parameter into base64 format. """
return base64.b64encode(text.encode()).decode()
def base64_decode(text: str) -> str:
""" Decodes a base64 parameter string. """
return base64.b64decode(text.encode()).decode()
root_agent = Agent(
name="chat_agent",
model="gemini-2.0-flash-exp",
instruction="""
You are a friendly chat companion, answer the user's questions stay social.
When asked about facts only answer for well known topics,
otherwise state that you don't know. Don't make up stuff!
""",
tools=[dice,base64_encode, base64_decode]
)
Our example will again be our Company and News graph that we already used in the MCP Toolbox example. This allows us to reuse the tools we defined in that blog post.
Our example dataset will be a small subset (250k entities) of diffbot's global Knowledge Graph (50bn entities).
It contains organizations, people in their leadership, locations and industries.
Additionally articles mentioning these companies, which are chunked and indexed as vector embeddings.
You can access and query a read only version of that graph with the following credentials:
Now let's take that to the next level and create a few functions to access our database and create our first agent, a Database Agent.
We define a small class neo4jDatabase to handle the connection to Neo4j, ensure that we can only call readonly queries and format the results.
import os
import random
import sys
from neo4j import GraphDatabase
from typing import Any
import re
class neo4jDatabase:
def __init__(self, neo4j_uri: str, neo4j_username: str, neo4j_password: str):
d = GraphDatabase.driver(neo4j_uri, auth=(neo4j_username, neo4j_password))
d.verify_connectivity()
self.driver = d
def is_write_query(self, query: str) -> bool:
return re.search(r"\b(MERGE|CREATE|SET|DELETE|REMOVE|ADD)\b", query, re.IGNORECASE) is not None
def _execute_query(self, query: str, params: dict[str, Any] | None = None) -> list[dict[str, Any]]:
if self.is_write_query(query):
raise "Write Queries are not supported in this agent"
else:
result = self.driver.execute_query(query, params)
return = [dict(r) for r in result.records]
db = neo4jDatabase("neo4j+s://demo.neo4jlabs.com","companies","companies")
# Nodes in the database
db._execute_query("MATCH () RETURN count(*) as nodes")
# {'nodes': 237358 }
We can now make use of this class to add two functions for our generic Graph Database Agent.
We define two functions, one for retrieving the schema from the graph database and the other for executing a read Cypher graph query.
Note that we're adding a lot of documentation to each of them as this will be important for the agent to select and run the right tools.
def get_schema() -> list[dict[str,Any]]:
"""Get the schema of the database, returns node-types(labels) with their types and attributes and relationships between node-labels
Args: None
Returns:
list[dict[str,Any]]: A list of dictionaries representing the schema of the database
For example
```
[{'label': 'Person','attributes': {'summary': 'STRING','id': 'STRING unique indexed', 'name': 'STRING indexed'},
'relationships': {'HAS_PARENT': 'Person', 'HAS_CHILD': 'Person'}}]
```
"""
try:
results = db._execute_query(
"""
call apoc.meta.data() yield label, property, type, other, unique, index, elementType
where elementType = 'node' and not label starts with '_'
with label,
collect(case when type = 'RELATIONSHIP' then [property, head(other)] end) as relationships,
collect(case when type <> 'RELATIONSHIP' then [property, type + case when unique
then " unique" else "" end + case when index then " indexed" else "" end] end)
as attributes,
RETURN label, apoc.map.fromPairs(attributes) as attributes, apoc.map.fromPairs(relationships) as relationships
"""
)
return results
except Exception as e:
return [{"error":str(e)}]
def execute_read_query(query: str, params: dict[str, Any] = {}) -> list[dict[str, Any]]:
"""
Execute a Neo4j Cypher query and return results as a list of dictionaries
Args:
query (str): The Cypher query to execute
params (dict[str, Any], optional): The parameters to pass to the query or None.
Raises:
Exception: If there is an error executing the query
Returns:
list[dict[str, Any]]: A list of dictionaries representing the query results
"""
try:
if params is None:
params = {}
results = db._execute_query(query, params)
return results
except Exception as e:
return [{"error":str(e)}]
# get_schema()
# execute_read_query("RETURN 1", None)
Now we can define our first agents and put them to the test.
The agent takes actions via access to tools, deciding how and when to invoke a tool. The agent also manages orchestration, creating a plan for answering a user query and adapting to responses that aren't quite correct.
As a reminder - Agent tools can be Python functions, or Vertex AI Extensions, or MCP Server tools.
We use Gemini 2.5 Pro as our LLM for our agents. We could use different, specific models for the different agents, but for simplicity will use the latest and best Vertex AI model available.
MODEL="gemini-2.5-pro-exp-03-25"
We provide comprehensive instructions to our graph database agent and tools to read the schema and execute a Cypher read query.
graph_database_agent = Agent(
model=MODEL,
name='graph_database_agent',
instruction="""
You are an Neo4j graph database and Cypher query expert, that must use the database schema with a
user question and repeatedly generate valid cypher statements to execute on the database and answer
the user's questions in a friendly manner in natural language.
If in doubt the database schema is always prioritized when it comes to nodes-types (labels) or
relationship-types or property names, never take the user's input at face value.
If the user requests also render tables, charts or other artifacts with the query results.
Always validate the correct node-labels at the end of a relationship based on the schema.
If a query fails or doesn't return data, use the error response 3 times to try to fix the
generated query and re-run it, don't return the error to the user.
If you cannot fix the query, explain the issue to the user and apologize.
Fetch the graph database schema first and keep it in session memory to access for query generation.
Keep results of previous executions in session memory and access if needed,
for instance ids or other attributes of nodes to find them again removing the need to ask the user.
This also allows for generating shorter, more focused and less error-prone queries
to for drill downs, sequences and loops.
If possible resolve names to primary keys or ids and use those for looking up entities.
The schema always indicates *outgoing* relationship-types from an entity to another entity, the
graph patterns read like english language.
`company has supplier` would be the pattern `(o:Organization)-[:HAS_SUPPLIER]-(s:Organization)`
To get the schema of a database use the `get_schema` tool without parameters.
Store the response of the schema tool in session context to access later for query generation.
To answer a user question generate one or more Cypher statements based on the database schema
and the parts of the user question.
If necessary resolve categorical attributes (like names, countries, industries, publications)
first by retrieving them for a set of entities to translate from the user's request.
Use the `execute_query` tool repeatedly with the Cypher statements, you MUST generate
statements that use named query parameters with `$parameter` style names
and MUST pass them as a second dictionary parameter to the tool, even if empty.
Parameter data can come from the users requests, prior query results or additional lookup queries.
After the data for the question has been sufficiently retrieved, pass the data and control
back to the parent agent.
""",
tools=[
get_schema, execute_read_query
]
)
Now we can try it out, either interactively as above or via the low-level run loop as shown below.
APP_NAME = 'Neo4j Investment Researcher'
USER_ID = 'Michael Hunger'
from google.adk.runners import InMemoryRunner
from google.genai.types import Part, UserContent
runner = InMemoryRunner(app_name=APP_NAME, agent=graph_database_agent)
session = runner.session_service.create_session( app_name=runner.app_name, user_id=USER_ID)
async def run_prompt(new_message: str):
content = UserContent(parts=[Part(text=new_message)])
result = None
async for event in runner.run_async(user_id=session.user_id,
session_id=session.id, new_message=content):
for part in event.content.parts:
print(part.text, part.function_call, part.function_response)
if part.text:
result = part.text
return result
await run_prompt('How many people are in the database?')
Which results in this thinking and execution flow
None id='adk-af40c5f1-b747-4b01-a3ea-c1c7521c2ace' args={'agent_name': 'graph_database_agent'} name='transfer_to_agent' None
None None id='adk-af40c5f1-b747-4b01-a3ea-c1c7521c2ace' name='transfer_to_agent' response={}
Okay, I can help with that. First, I need to check the database schema to see how people are represented.
None None
None id='adk-03f904c5-cb65-47f5-9cda-a132eb543dfd' args={} name='get_schema' None
None None id='adk-03f904c5-cb65-47f5-9cda-a132eb543dfd' name='get_schema' response={'result': [{'label': 'Person', 'attributes': {'summary': 'STRING', 'id': 'STRING unique indexed', 'name': 'STRING indexed'}, 'relationships': {'HAS_PARENT': 'Person', 'HAS_CHILD': 'Person'}}, {'label': 'Organization', 'attributes': {'summary': 'STRING', 'isDissolved': 'BOOLEAN', 'id': 'STRING unique indexed', 'diffbotId': 'STRING', 'nbrEmployees': 'INTEGER', 'name': 'STRING indexed', 'isPublic': 'BOOLEAN', 'motto': 'STRING', 'revenue': 'FLOAT'}, 'relationships': {'HAS_COMPETITOR': 'Organization', 'HAS_BOARD_MEMBER': 'Person', 'HAS_CEO': 'Person', 'HAS_SUBSIDIARY': 'Organization', 'HAS_INVESTOR': 'Person', 'HAS_CATEGORY': 'IndustryCategory', 'HAS_SUPPLIER': 'Organization', 'IN_CITY': 'City'}}, ....}
Okay, the schema shows that people are represented by the `Person` label. I can now count how many `Person` nodes exist in the database.
None id='adk-52b98b75-3766-4d1c-a949-05720fd03401' args={'query': 'MATCH (p:Person) RETURN count(p) AS numberOfPeople', 'params': {}} name='execute_read_query' None
None None id='adk-52b98b75-3766-4d1c-a949-05720fd03401' name='execute_read_query' response={'result': [{'numberOfPeople': 8064}]}
There are 8,064 people in the database. None None
'There are 8,064 people in the database.'
Now let's take the next step and add our first specific GraphRAG tool based Agent, an Investment Research which runs a very specific database query to find investors in a company.
def get_investors(company: str) -> list[dict[str, Any]]:
"""
Returns the investor in the company with this name or id.
Args:
company (str): name of the company to find investors in
Returns:
list[dict[str, Any]]: A list of investor ids, names (and their types Organization or Person)
"""
try:
results = db._execute_query("""
MATCH p=(o:Organization)<-[r:HAS_INVESTOR]-(i)
WHERE o.name=$company OR o.id=$company
AND NOT exists { (o)<-[:SUBSIDARY_OF]-() }
RETURN i.id as id, i.name as name, head(labels(i)) as type
""", {"company":company})
return results
except Exception as e:
return [{"error":str(e)}]
We can test our function quickly to see that our query performs correctly.
get_investors("Cloudflare")
[{'id': 'EIBehdsQ4ME-yr8WK8tiC1w', 'name': 'Atlas Financial Group', 'type': 'Organization'},
{'id': 'E-qHiBRsMNb2wOsqdSx2CfQ', 'name': 'Pelion Venture Partners', 'type': 'Organization'},
{'id': 'E7obn7yRnMP6HjTQ4xOL6Dg', 'name': 'Venrock', 'type': 'Organization'},
{'id': 'EfCgjTBhgNdyHkynILGypYg', 'name': 'New Enterprise Associates (NEA)', 'type': 'Organization'},
{'id': 'EptIVnF6gNuOmOmAdBku7Uw', 'name': 'National Science Foundation (NSF)', 'type': 'Organization'},
{'id': 'EdE6JQUuvMf6rTFv2qMJZqQ', 'name': 'Union Square Ventures', 'type': 'Organization'},
{'id': 'E0aVDBaUAMSWgjywIrE4IwA', 'name': 'Greenspring Associates', 'type': 'Organization'},
{'id': 'EThF8yM26NOmTZYsND2DqRQ', 'name': 'Microsoft Accelerator', 'type': 'Organization'},
{'id': 'EUFq-3WlpNsq0pvfUYWXOEA', 'name': 'Google', 'type': 'Organization'},
{'id': 'EmuHMD_KVM1OUKHkQE21s5A', 'name': 'Qualcomm Ventures', 'type': 'Organization'},
{'id': 'E1XokcZs2NqGwLR47G92lFQ', 'name': 'Baidu', 'type': 'Organization'},
{'id': 'E_z0hoZzCMJawSGOzcGJpEA', 'name': 'Fidelity Investments', 'type': 'Organization'},
{'id': 'EQ0sXW3JHP1iVGViigR65Yg', 'name': 'CapitalG', 'type': 'Organization'},
{'id': 'EKBYcQJj_PEGkleEDZnKQrg', 'name': 'ESO Fund', 'type': 'Organization'},
{'id': 'Ef-HCFsSjOrGoIWH4dcdpXA', 'name': 'Franklin Templeton Investments', 'type': 'Organization'}]
Now we can create our investor research agent using these two functions.
investor_research_agent = Agent(
model=MODEL,
name='investment_research_agent',
instruction="""
You are an agent that has access to a database of investment relationships between companies and indivduals.
Use the get_investors tool when asked to find the investors of a company by id and name.
If you do so, try to always return not just the factual attribute data but also
investor ids to allow the other agents to investigate them more.
""",
tools=[
get_schema, get_investors
]
)
Now comes my favorite part - integrating a Neo4j GraphRAG MCP Server deployed in cloud run with MCP Toolbox that we worked on last year with Kurtis Van Gent.
MCP Toolbox for Databases is a service that allows declarative definition of tools for agentic systems. It supports popular databases like postgres, mysql, neo4j, spanner both for their local and cloud hosted deployments. For each source connection declaration, a number of tools can be defined as parameterized database queries (read and write).
You can think of these tools as (complex) queries that a subject matter expert (SME) had written for a specific domain and that would go beyond the LLMs capability to generate. They are vetted for correctness and performance and return exactly the data that's needed for the use-case.
Originally launched with LangChain and LlamaIndex support, the latest release supports MCP, but only as a hosted server, not STDIO.
We covered the integration and also the example server in our joint blog post and livestream.
Here is the example for our investment research tool also running against our company news database.
sources:
companies-graph:
kind: "neo4j"
uri: "neo4j+s://demo.neo4jlabs.com"
user: "companies"
password: "companies"
database: "companies"
tools:
companies_in_industry:
kind: neo4j-cypher
source: companies-graph
statement: |
MATCH (:IndustryCategory {name:$industry})<-[:HAS_CATEGORY]-(c)
WHERE NOT EXISTS { (c)<-[:HAS_SUBSIDARY]-() }
RETURN c.id as company_id, c.name as name, c.summary as summary
description: Companies (company_id, name, summary) in a given industry by industry
parameters:
- name: industry
type: string
description: Industry name to filter companies by
# ...
articles_in_month:
kind: neo4j-cypher
source: companies-graph
statement: |
MATCH (a:Article)
WHERE date($date) <= date(a.date) < date($date) + duration('P1M')
RETURN a.id as article_id, a.author as author, a.title as title, toString(a.date) as date, a.sentiment as sentiment
LIMIT 25
description: List of Articles (article_id, author, title, date, sentiment) in a month timeframe from the given date
parameters:
- name: date
type: string
description: Start date in yyyy-mm-dd format
As thankfully the ADK added MCPTools just in time for the launch, we can use our hosted server that runs on HTTPS/SSE transport of MCP, which you can find at: https://toolbox-990868019953.us-central1.run.app/mcp/sse
Which you can also test in the really useful MCP Inspector tool.
npx @modelcontextprotocol/inspector
Starting MCP inspector...
Proxy server listening on port 6277
MCP Inspector is up and running at http://127.0.0.1:6274
Fran demoed MCP integration during the developer keynote of Google Cloud Next 2025 albeit in an unexpected way.
You can find a general example of using MCP Tools in the example repository (but only for STDIO transport).
Now we can use the ADK's new MCP wrappers which exist for STDIO and HTTP/SSE MCP servers to make these MCP Server tools available to one of our agents.
from google.adk.tools.mcp_tool.mcp_toolset import MCPToolset, SseServerParams
async def load_tools(mcp_url):
async with MCPToolset( connection_params=SseServerParams(url=mcp_url)) as toolset:
tools = await toolset.load_tools()
return tools
tools = await load_tools("https://toolbox-990868019953.us-central1.run.app/mcp/sse")
tools.extend([get_schema])
With the Neo4j Integration in Google's MCP Toolbox we can quickly declaratively define sources and tools backed by database queries and deploy that toolset to Cloud Run.
Our tool definitions are here for these tools:
Now we can use our set of tools for our Investment Research Agent.
investment_research_agent = Agent(
model=MODEL,
name='investment_research_agent',
instruction="""
You are an agent that has access to a knowledge graph of companies (organizations), people involved with them, articles about companies,
and industry categories and technologies.
You have a set of tools to access different aspects of the investment database.
You will be tasked by other agents to fetch certain information from that knowledge graph.
If you do so, try to always return not just the factual attribute data but also
ids of companies, articles, people to allow the other tools to investigate them more.
""",
tools=tools
)
As we have built the 3 sub-agents of our multi-agent system, we now need the root agent that supervises them and handles transfer of control and validation of our results until they satisfy the user's task.
This agent is pretty straightforward, you just configure the 3 agents as sub_agents in the Agent definition, they will automatically get a parent_agent field set that points to their single parent (will be validated).
root_agent = Agent(
model=MODEL,
name='investment_agent',
global_instruction = "",
instruction="""
You are an agent that has access to a knowledge graph of companies (organizations), people involved with them, articles about companies,
and industry categories and technologies.
You have a set of agents to retrieve information from that knowledge graph, if possible prefer the research agents over the database agent.
If the user requests it, do render tables, charts or other artifacts with the research results.
""",
sub_agents=[investor_research_agent, investment_research_agent, database_agent]
)
Now we're done and can put our agents to the test.
We use the same run loop as before wrapped in a function, as that works best in a Jupyter notebook.
APP_NAME = 'Neo4j Investment Researcher'
USER_ID = 'Michael Hunger'
from google.adk.runners import InMemoryRunner
from google.genai.types import Part, UserContent
runner = InMemoryRunner(app_name=APP_NAME, agent=root_agent)
session = runner.session_service.create_session( app_name=runner.app_name, user_id=USER_ID)
async def run_prompt(new_message: str):
content = UserContent(parts=[Part(text=new_message)])
result = None
async for event in runner.run_async(user_id=session.user_id,
session_id=session.id, new_message=content):
for part in event.content.parts:
print(part.text, part.function_call, part.function_response)
if part.text:
result = part.text
return result
And then just start asking some questions. You see in the outputs that the agents take multiple steps to reach the conclusions.
await run_prompt('What are the main competitors of YouTube?')
Okay, I can look that up for you. First, I need to find YouTube in the database and then check for its competitors using the `HAS_COMPETITOR` relationship.
None None
None id='adk-33b02783-a9b5-4378-b4a6-2989b0700052' args={'params': {'name': 'YouTube'}, 'query': 'MATCH (o:Organization {name: $name})-[:HAS_COMPETITOR]-(c:Organization) RETURN c.name AS competitorName'} name='execute_read_query' None
None None id='adk-33b02783-a9b5-4378-b4a6-2989b0700052' name='execute_read_query' response={'result': [{'competitorName': 'Mixer'}, {'competitorName': 'BYTEDANCE'}, {'competitorName': 'BuzzFeed'}, {'competitorName': 'TikTok'}, {'competitorName': 'TikTok'}, {'competitorName': 'OpenAI'}, {'competitorName': 'Dailymotion'}, {'competitorName': 'Twitter'}, {'competitorName': 'Fox Broadcasting'}, {'competitorName': 'Violin Systems'}, {'competitorName': 'Oxygen'}]}
Okay, I found the main competitors for YouTube listed in the database. They are:
* Mixer
* BYTEDANCE
* BuzzFeed
* TikTok
* OpenAI
* Dailymotion
* Twitter
* Fox Broadcasting
* Violin Systems
* Oxygen None None
await run_prompt('What are 5 companies mentioned in articles from January 2023 with good sentiment and who are the people working there?')
Okay, I can search for that. I will look for articles published between January 1st, 2023, and January 31st, 2023, with a positive sentiment (let's define "good" as sentiment greater than 0.5). Then, I'll identify up to 5 distinct companies mentioned in those articles and find the names of people associated with them (CEOs and Board Members based on the schema).
None id='adk-00e13c39-b0ac-4b55-8257-01bf3396310f' args={'params': {'endDate': '2023-02-01T00:00:00', 'lowerSentiment': 0.3, 'startDate': '2023-01-01T00:00:00'}, 'query': '\n MATCH (a:Article)-[:MENTIONS]-(o:Organization)\n WHERE a.date >= datetime($startDate) AND a.date < datetime($endDate) AND a.sentiment > $lowerSentiment\n WITH DISTINCT o\n LIMIT 5\n OPTIONAL MATCH (o)-[:HAS_CEO]-(ceo:Person)\n OPTIONAL MATCH (o)-[:HAS_BOARD_MEMBER]-(bm:Person)\n WITH o, collect(DISTINCT ceo.name) AS ceoNames, collect(DISTINCT bm.name) AS bmNames\n WITH o.name AS companyName, [name IN ceoNames + bmNames WHERE name IS NOT NULL] AS associatedPeople\n RETURN companyName, associatedPeople\n '} name='execute_read_query' None
None None id='adk-00e13c39-b0ac-4b55-8257-01bf3396310f' name='execute_read_query' response={'result': [{'companyName': 'BackBox', 'associatedPeople': ['Andrew Kahl', 'Chris Pacitti']}, {'companyName': 'Pocket App', 'associatedPeople': ['Paul Swaddle']}, {'companyName': 'Mozilla', 'associatedPeople': []}, {'companyName': 'Wipro', 'associatedPeople': []}, {'companyName': 'Wipro', 'associatedPeople': []}]}
Okay, here are 5 companies mentioned in articles with positive sentiment (greater than 0.3) between January 1st and January 31st, 2023, along with people associated with them (CEOs or Board Members found in the database):
1. **BackBox:**
* Andrew Kahl
* Chris Pacitti
2. **Pocket App:**
* Paul Swaddle
3. **Mozilla:**
* (No associated people found in the database)
4. **Wipro:**
* (No associated people found in the database)
await run_prompt('Who has invested in BYTEDANCE and where else have they invested?')
Okay, I can help you with that request. I'll first find the investors for BYTEDANCE and then look for other companies those same investors have put money into.
Let's find the investors for BYTEDANCE first.
None None
None id='adk-ba9ce6de-2afd-4956-9347-6d549d655cf1' args={'params': {'name': 'BYTEDANCE'}, 'query': 'MATCH (o:Organization {name: $name})-[:HAS_INVESTOR]-(investor:Person) RETURN investor.name AS investorName, investor.id AS investorId'} name='execute_read_query' None
None None id='adk-ba9ce6de-2afd-4956-9347-6d549d655cf1' name='execute_read_query' response={'result': [{'investorName': 'Rong Yue', 'investorId': 'EHwJNEbHuN9S0OtngP6cFKQ'}, {'investorName': 'Wendi Murdoch', 'investorId': 'En8xLxQ3oNnWcR6ZwsWol3w'}]}
Okay, I found two investors for BYTEDANCE: Rong Yue and Wendi Murdoch.
Now, let's see where else these individuals have invested.
None None
None id='adk-15b1e3d1-0af0-4f88-8c62-de879a43f444' args={'query': 'MATCH (investor:Person)-[:HAS_INVESTOR]-(otherCompany:Organization)\nWHERE investor.id IN $investorIds AND otherCompany.name <> $excludeName\nRETURN investor.name AS investorName, collect(distinct otherCompany.name) AS otherInvestments', 'params': {'excludeName': 'BYTEDANCE', 'investorIds': ['EHwJNEbHuN9S0OtngP6cFKQ', 'En8xLxQ3oNnWcR6ZwsWol3w']}} name='execute_read_query' None
None None id='adk-15b1e3d1-0af0-4f88-8c62-de879a43f444' name='execute_read_query' response={'result': [{'investorName': 'Rong Yue', 'otherInvestments': ['Inspur']}]}
Okay, I found the investors for BYTEDANCE and their other investments:
* **Rong Yue** invested in BYTEDANCE and also in **Inspur**.
* **Wendi Murdoch** invested in BYTEDANCE. According to the database, there are no other investments listed for Wendi Murdoch. None None
Out[49]:
'Okay, I found the investors for BYTEDANCE and their other investments:\n\n* **Rong Yue** invested in BYTEDANCE and also in **Inspur**.\n* **Wendi Murdoch** invested in BYTEDANCE. According to the database, there are no other investments listed for Wendi Murdoch.'
It can even summarize the session so far:
result = await run_prompt("Summarize the results of the previous research questions")
result
Okay, here's a summary of our findings so far:
1. We first determined that there are **8,064 people** represented in the database.
2. Next, we looked up the competitors for **YouTube** and found several, including Mixer, BYTEDANCE, BuzzFeed, TikTok, OpenAI, Dailymotion, Twitter, Fox Broadcasting, Violin Systems, and Oxygen.
3. Finally, we identified the investors in **BYTEDANCE** as **Rong Yue** and **Wendi Murdoch**. We also found that Rong Yue has invested in **Inspur**, while no other investments were listed for Wendi Murdoch in the database besides BYTEDANCE. None None
runner.session_service.list_sessions(app_name=APP_NAME, user_id=USER_ID)
ListSessionsResponse(sessions=[Session(id='3375d095-3f4d-4725-97c6-9f7318c6574a', app_name='Neo4j Investment Researcher', user_id='Michael Hunger', state={}, events=[], last_update_time=1744237962.688923)])
for session in runner.session_service.list_sessions(app_name=APP_NAME, user_id=USER_ID).sessions:
print(session.model_dump())
{'id': '3375d095-3f4d-4725-97c6-9f7318c6574a', 'app_name': 'Neo4j Investment Researcher', 'user_id': 'Michael Hunger', 'state': {}, 'events': [], 'last_update_time': 1744237962.688923}
for session in runner.session_service.list_sessions(app_name=APP_NAME, user_id=USER_ID).sessions:
print(f"Deleting session {session}")
runner.session_service.delete_session(app_name=APP_NAME, user_id=USER_ID, session_id=session.id)
Deleting session id='3375d095-3f4d-4725-97c6-9f7318c6574a' app_name='Neo4j Investment Researcher' user_id='Michael Hunger' state={} events=[] last_update_time=1744237973.959884
The integration of Google Cloud's Vertex AI Agent Development Kit (ADK) with Neo4j graph databases and the Model Context Protocol (MCP), particularly using MCP Toolbox deployed on Cloud Run, provides a robust framework for building sophisticated, data-grounded multi-agent systems.
This article demonstrated configuring agents within the Vertex AI Agent Builder framework, enabling them to interact with complex knowledge graphs via custom Python functions and standardized MCP tools. ADK's features for defining agent instructions, tools (including Python functions, Vertex AI Extensions, and MCP tools), and hierarchical sub-agent structures facilitate the creation of complex, orchestrated workflows suitable for enterprise applications.
The examples illustrated the development of database-specific and research-focused agents, coordinated by a root agent, showcasing the framework's modularity and extensibility. Leveraging MCP Toolbox on Google Cloud Run allows for the incorporation of pre-validated, performant database queries, enhancing the reliability and capability of the agent system within a scalable, serverless architecture.
Utilizing ADK with Vertex AI's Gemini models and Neo4j allows developers to construct intelligent applications that effectively bridge natural language interaction with structured graph data, paving the way for advanced, data-driven AI solutions deployable and manageable within the Google Cloud ecosystem, supported by GCP's security and observability tools like IAM, VPC-SC, and the Cloud Operations Suite.