From 54b1cfeb97fb731c1a07448520103e5ca01f6522 Mon Sep 17 00:00:00 2001 From: Willie Doran Date: Tue, 11 Aug 2026 11:46:16 +0200 Subject: [PATCH 1/2] docs(examples): document what the examples need in order to run examples/README.md is a pure link index: no mention of API keys, extras, Docker, or the fact that 17 examples talk to a remote demo database. What setup knowledge exists is spread across the root README's Tests section and three per-store READMEs. Adds examples/SETUP.md covering credentials, the services, which providers are free or have a local equivalent, and the traps that cost the most time - running from the repo root, the three examples needing PYTHONPATH=examples/data, and the examples that cannot work as written against a read-only demo database. Adds examples/.env.example as a credential template, and gitignores .env variants while keeping the template tracked. Declares python-dotenv and requests in the examples extra. Fourteen examples call load_dotenv() and tools_retriever_example.py calls a web API with requests, but neither was declared, so both resolved only transitively. The three Ollama examples now take the model as an argument rather than shipping a placeholder to edit, since which model you have depends on what you pulled. Fixes the Weaviate and Pinecone READMEs, which gave `python -m` a slash path. Every count in SETUP.md was checked against the code rather than estimated. Co-Authored-By: Claude Opus 5 (1M context) --- .gitignore | 2 + CHANGELOG.md | 2 + examples/.env.example | 64 +++++++++ examples/README.md | 3 + examples/SETUP.md | 134 ++++++++++++++++++ .../customize/embeddings/ollama_embeddings.py | 20 ++- examples/customize/llms/ollama_llm.py | 19 ++- examples/customize/llms/ollama_tool_calls.py | 22 ++- .../retrievers/external/pinecone/README.md | 2 +- .../retrievers/external/weaviate/README.md | 2 +- pyproject.toml | 5 + uv.lock | 4 + 12 files changed, 271 insertions(+), 8 deletions(-) create mode 100644 examples/.env.example create mode 100644 examples/SETUP.md diff --git a/.gitignore b/.gitignore index 935632db2..b902cad76 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,8 @@ dist/ htmlcov/ .idea/ .env +.env.* +!.env.example docs/build/ .vscode/ .python-version diff --git a/CHANGELOG.md b/CHANGELOG.md index 58903531f..381263450 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ ### Added +- Added `examples/SETUP.md` and `examples/.env.example`, documenting what the examples need in order to run — extras, API keys and services — and which providers are free or have a local equivalent. +- The `examples` extra now declares `python-dotenv` and `requests`. Fourteen examples call `load_dotenv()` and `tools_retriever_example.py` calls a web API with `requests`, but neither package was declared, so both resolved only transitively. - `AnthropicLLM` now supports structured output via the `response_format` argument, accepting a Pydantic model or an Anthropic `output_config` dict, alongside `OpenAILLM` and `VertexAILLM`. - Added `neo4j_graphrag.llm.utils.split_http_client_kwargs`, a shared helper that routes a constructor's `http_client` kwarg to whichever of the sync/async SDK clients it matches. `AnthropicLLM`, `OpenAILLM`, and `AzureOpenAILLM` now all use this single implementation instead of three separately maintained copies of the same logic. Custom subclasses that construct their own SDK clients can call it to get the same behavior; it is exported from `neo4j_graphrag.llm` for that purpose. - Added `BaseAnthropicLLM`, a new base class holding all of `AnthropicLLM`'s shared message-building, schema-conversion, and response-parsing logic, mirroring `BaseOpenAILLM`. Both `BaseAnthropicLLM` and `BaseOpenAILLM` are now exported from `neo4j_graphrag.llm` as documented, supported extension points for subclassing to reach custom Anthropic/OpenAI-compatible endpoints. diff --git a/examples/.env.example b/examples/.env.example new file mode 100644 index 000000000..91c09f371 --- /dev/null +++ b/examples/.env.example @@ -0,0 +1,64 @@ +# Template for the examples' configuration. +# +# cp examples/.env.example .env +# +# `.env` at the repo root is gitignored; this template is the only env file that +# is tracked. Never put a real credential in this file, and never commit `.env`. +# +# Credential lines are commented out on purpose: an empty `FOO=` would blank out +# a variable you already exported when this file is sourced. Uncomment a line +# only when you are putting a real value on it. +# +# 14 examples call load_dotenv(); the rest read the environment directly, so +# export the file before running one: +# +# set -a; source .env; set +a + + +# --- Neo4j ------------------------------------------------------------------ +# The local database, as started by tests/e2e/docker-compose.yml. Examples that +# use the public demo database hardcode its URI and ignore these. +NEO4J_URI=bolt://localhost:7687 +NEO4J_USER=neo4j +NEO4J_PASSWORD=password + + +# --- API keys --------------------------------------------------------------- + +# OpenAI. No free tier. https://platform.openai.com/api-keys +# OPENAI_API_KEY= + +# Google Gemini via AI Studio. Free, no credit card required. +# https://aistudio.google.com/apikey +# GOOGLE_API_KEY= + +# Cohere. Free trial key: 1,000 calls/month, not for production. +# https://dashboard.cohere.com/api-keys +# CO_API_KEY= + +# Mistral AI. Free "Experiment" tier; phone verification, no card. +# https://console.mistral.ai/api-keys +# MISTRAL_API_KEY= + +# Anthropic. No free tier - the account needs purchased credits. +# https://console.anthropic.com/settings/keys +# ANTHROPIC_API_KEY= + + +# --- Cloud providers -------------------------------------------------------- + +# Google Vertex AI. Credentials come from application-default login, not from +# here; this only selects the project. +# gcloud auth application-default login +# gcloud auth application-default set-quota-project +# The quota project is required for VertexAIEmbeddings but not for VertexAILLM. +# GOOGLE_CLOUD_PROJECT= + +# AWS Bedrock. Credentials come from the standard AWS chain (~/.aws/credentials, +# SSO, or AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY); the examples hardcode the +# region. Anthropic models on Bedrock also need a one-time use-case form in the +# Bedrock console. +# If your login wrote a named profile rather than the default (SSO usually +# does), name it here - otherwise boto3 reports "Unable to locate credentials" +# even though the login succeeded. +# AWS_PROFILE= diff --git a/examples/README.md b/examples/README.md index dcbc44021..9b13fba8f 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,5 +1,8 @@ # Examples Index +> **New here?** [SETUP.md](SETUP.md) covers what you need in order to run these - +> extras, API keys and services - and which providers are free. + This folder contains examples usage for the different features supported by the `neo4j-graphrag` package: diff --git a/examples/SETUP.md b/examples/SETUP.md new file mode 100644 index 000000000..789547e63 --- /dev/null +++ b/examples/SETUP.md @@ -0,0 +1,134 @@ +# Running the examples + +[README.md](README.md) indexes what the examples demonstrate. This file covers what you need +installed and configured to actually run them. + +## Quick start + +```bash +uv sync --all-extras --group dev +cp examples/.env.example .env # then add an OPENAI_API_KEY +docker compose -f tests/e2e/docker-compose.yml up -d --wait +set -a; source .env; set +a +python examples/question_answering/graphrag.py +``` + +That covers about two thirds of the examples. The rest need another provider's key, a vector +store, or a local runtime — see below. + +## Credentials + +**Never commit a credential to this repository.** Keys belong in `.env` at the repo root, which is +gitignored. `examples/.env.example` is a template of placeholders and is the only env file that is +tracked. + +Only 14 examples call `load_dotenv()`. The rest read the environment directly, so export the file +before running one: + +```bash +set -a; source .env; set +a +``` + +Two examples expect a key edited directly into their source rather than read from the +environment: `customize/embeddings/azure_openai_embeddings.py`, and the Pinecone examples. If you +edit them, do not commit the edit — for Pinecone, use Pinecone Local instead, which ignores keys +entirely. + +## Running an example + +Run examples from the repo root — several resolve data files relative to it. + +**Three examples need `examples/data` on the import path.** They load a pre-computed vector as a +bare module (`from embedding_avatar import ...`), which does not resolve from the repo root: + +```bash +PYTHONPATH=examples/data python examples/retrieve/similarity_search_for_vector.py +PYTHONPATH=examples/data python examples/customize/retrievers/external/qdrant/qdrant_vector_search.py +PYTHONPATH=examples/data python examples/customize/retrievers/external/weaviate/weaviate_vector_search.py +``` + +The Ollama examples take the model name as an argument, since it depends on what you have pulled: + +```bash +ollama pull llama3.2 +python examples/customize/llms/ollama_llm.py llama3.2 +``` + +## Services + +`tests/e2e/docker-compose.yml` starts everything the examples can talk to locally. The vector +stores sit behind a `vectordb` profile: + +```bash +docker compose -f tests/e2e/docker-compose.yml up -d --wait # Neo4j + APOC +docker compose -f tests/e2e/docker-compose.yml --profile vectordb up -d --wait # + vector stores +``` + +17 examples instead use the public read-only demo database at `demo.neo4jlabs.com`. Those need no +local setup, just network access — and because it is read-only, the two examples that write +message history to it fail with `Forbidden`. + +Some retriever examples assume a vector or fulltext index already exists on the local database. +`examples/database_operations/` has scripts that create them. + +The vector-store examples need their store populated first: + +```bash +uv run python -m tests.e2e.weaviate_e2e.populate_dbs +uv run python -m tests.e2e.qdrant_e2e.populate_dbs +``` + +## Providers: what is free, and what has a local equivalent + +| Provider | Free without a credit card? | Local / Docker equivalent | +|---|---|---| +| **Google Gemini** (AI Studio) | **Yes.** Free tier on the Flash models; Pro is paid-only. A new key cannot reach every model the API lists — `gemini-flash-latest` is the safe choice. Prompts may be used to improve Google's products | — | +| **Cohere** | **Yes.** Trial key, rate-limited and not for production | — | +| **Mistral** | **Yes.** "Experiment" tier. Phone verification, no card | — | +| **Ollama** | **Yes.** Entirely local | `brew install ollama` | +| **Weaviate / Qdrant** | n/a | Docker, in the `vectordb` profile | +| **Pinecone** | Hosted free "Starter" tier exists | **Pinecone Local** — an in-memory emulator in the `vectordb` profile. Ignores API keys, keeps nothing after it stops | +| **sentence-transformers / spaCy** | n/a | Local model download (spaCy `en_core_web_lg` is ~560 MB) | +| **OpenAI** | No free tier | — | +| **Anthropic** | No free tier; the account needs purchased credits | Claude is also reachable through Bedrock | +| **Vertex AI** | No standing free tier; new GCP accounts get trial credits | Gemini via AI Studio reaches the same model family for free | +| **AWS Bedrock** | No standing free tier | — | +| **Azure OpenAI** | No free tier; needs a deployed resource | — | + +So every non-OpenAI provider has a free or local path. + +## Cloud providers + +- **Vertex AI** authenticates through `gcloud auth application-default login`, not an API key. + `gcloud auth application-default set-quota-project ` is required for + `VertexAIEmbeddings` but not for `VertexAILLM`. +- **Bedrock** enables serverless models by default, but Anthropic models need a one-time use-case + form submitted from the Bedrock console before the first call. Credentials come from the standard + AWS chain — if you use named profiles (SSO commonly writes them), export `AWS_PROFILE=` or + boto3 will report "Unable to locate credentials" despite a successful login. +- **Azure OpenAI** needs a deployed resource; the example hardcodes its endpoint and key. + +If you work at Neo4j, an Aura dev environment already has these cloud accounts provisioned, and +`omni` will print their coordinates. Vertex AI is a managed API, so it works while the environment +is scaled down. + +## Python extras + +Each provider is behind an extra in `pyproject.toml` — `pip install "neo4j-graphrag[openai]"`, or +`uv sync --all-extras` to get everything at once. + +Note that examples import library symbols (`OpenAILLM`) rather than provider SDKs, so which extra +an example needs is not visible from its imports alone. + +## Known issues in the examples + +These are properties of the examples, not of your setup. + +- `customize/retrievers/text2cypher_custom_prompt.py` declares `(:User)-[:REVIEWED]->(:Movie)` in + its schema, which the `recommendations` database does not have. +- `customize/llms/llm_with_neo4j_message_history.py` and + `question_answering/graphrag_with_neo4j_message_history.py` write message history to the + **read-only** demo database, so they fail with `Forbidden` as written. +- `customize/build_graph/components/splitters/langhchain_splitter.py` and `llamaindex_splitter.py` + are empty files, though `README.md` links to them. +- `customize/embeddings/azure_openai_embeddings.py` reads no environment variables at all. diff --git a/examples/customize/embeddings/ollama_embeddings.py b/examples/customize/embeddings/ollama_embeddings.py index 7a460146c..065de275c 100644 --- a/examples/customize/embeddings/ollama_embeddings.py +++ b/examples/customize/embeddings/ollama_embeddings.py @@ -1,11 +1,29 @@ """This example demonstrate how to embed a text into a vector using a local model served by Ollama. + +The model is a command-line argument, because which models you have depends on +what you have pulled locally. Pull an embedding model first, then name it: + + ollama pull nomic-embed-text + python examples/customize/embeddings/ollama_embeddings.py nomic-embed-text """ +import argparse + from neo4j_graphrag.embeddings import OllamaEmbeddings +parser = argparse.ArgumentParser(description=__doc__) +parser.add_argument( + "model", + nargs="?", + default="nomic-embed-text", + help="an embedding model you have pulled with `ollama pull` " + "(default: %(default)s)", +) +args = parser.parse_args() + embeder = OllamaEmbeddings( - model="", + model=args.model, # host="...", # if using a remote server ) res = embeder.embed_query("my question") diff --git a/examples/customize/llms/ollama_llm.py b/examples/customize/llms/ollama_llm.py index bfa27f442..f6ef35412 100644 --- a/examples/customize/llms/ollama_llm.py +++ b/examples/customize/llms/ollama_llm.py @@ -1,11 +1,28 @@ """This example demonstrate how to invoke an LLM using a local model served by Ollama. + +The model is a command-line argument, because which models you have depends on +what you have pulled locally. Pull one first, then name it: + + ollama pull llama3.2 + python examples/customize/llms/ollama_llm.py llama3.2 """ +import argparse + from neo4j_graphrag.llm import LLMResponse, OllamaLLM +parser = argparse.ArgumentParser(description=__doc__) +parser.add_argument( + "model", + nargs="?", + default="llama3.2", + help="name of a model you have pulled with `ollama pull` (default: %(default)s)", +) +args = parser.parse_args() + with OllamaLLM( - model_name="", + model_name=args.model, # model_params={"options": {"temperature": 0}, "format": "json"}, # host="...", # if using a remote server ) as llm: diff --git a/examples/customize/llms/ollama_tool_calls.py b/examples/customize/llms/ollama_tool_calls.py index 91d08eb7f..0789c2020 100644 --- a/examples/customize/llms/ollama_tool_calls.py +++ b/examples/customize/llms/ollama_tool_calls.py @@ -4,9 +4,14 @@ To run this example: 1. Make sure you have `ollama serve` running -2. Run: python examples/tool_calls/ollama_tool_calls.py +2. Pull a model that supports tool calling: `ollama pull llama3.2` +3. Run: python examples/customize/llms/ollama_tool_calls.py llama3.2 + +The model is a command-line argument because not every local model supports +tool calling - check the model's page in the Ollama library before using it. """ +import argparse import asyncio import json from typing import Dict, Any @@ -61,9 +66,9 @@ def process_tool_calls(response: ToolCallResponse) -> Dict[str, Any]: return results[0] if results else {} -async def main() -> None: +async def main(model: str) -> None: async with OllamaLLM( - model_name="mistral:latest", model_params={"options": {"temperature": 0}} + model_name=model, model_params={"options": {"temperature": 0}} ) as llm: # Example text containing information about a person text = "Stella Hane is a 35-year-old software engineer who loves coding." @@ -91,5 +96,14 @@ async def main() -> None: if __name__ == "__main__": + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "model", + nargs="?", + default="llama3.2", + help="a tool-calling model you have pulled with `ollama pull` " + "(default: %(default)s)", + ) + args = parser.parse_args() # Run the async main function - asyncio.run(main()) + asyncio.run(main(args.model)) diff --git a/examples/customize/retrievers/external/pinecone/README.md b/examples/customize/retrievers/external/pinecone/README.md index c4d236ef4..c013e7544 100644 --- a/examples/customize/retrievers/external/pinecone/README.md +++ b/examples/customize/retrievers/external/pinecone/README.md @@ -7,7 +7,7 @@ You will need both a Pinecone vector database and a Neo4j database to use this r Update `NEO4J_AUTH`, `NEO4J_URL`, and `PC_API_KEY` variables in the `tests/e2e/pinecone_e2e/populate_dbs.py` script then run this from the project root to write test data to both dbs. ``` -uv run python -m tests/e2e/pinecone_e2e/populate_dbs.py +uv run python -m tests.e2e.pinecone_e2e.populate_dbs ``` ### Install Pinecone client diff --git a/examples/customize/retrievers/external/weaviate/README.md b/examples/customize/retrievers/external/weaviate/README.md index 3a1b67680..4e05faea7 100644 --- a/examples/customize/retrievers/external/weaviate/README.md +++ b/examples/customize/retrievers/external/weaviate/README.md @@ -15,7 +15,7 @@ docker compose -f tests/e2e/docker-compose.yml --profile vectordb up -d --wait Run this from the project root to write data to both dbs. ``` -uv run python -m tests/e2e/weaviate_e2e/populate_dbs.py +uv run python -m tests.e2e.weaviate_e2e.populate_dbs ``` ### Install Weaviate client diff --git a/pyproject.toml b/pyproject.toml index 44bd11345..da29ebac6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -66,6 +66,11 @@ experimental = [ examples = [ "langchain-openai>=0.2.2,<2.0.0", "langchain-huggingface>=0.1.0,<2.0.0", + # 14 examples call load_dotenv(); without this it resolves only by accident, + # transitively through another extra. + "python-dotenv>=1.0.0,<2.0.0", + # tools_retriever_example.py calls the open-meteo API with it. + "requests>=2.31.0,<3.0.0", ] # NOTE: spaCy currently has a Python 3.14 import-time failure via its pydantic v1 path # (see https://github.com/explosion/spaCy/issues/13895). Until upstream resolves this, diff --git a/uv.lock b/uv.lock index 37696253a..25001a60a 100644 --- a/uv.lock +++ b/uv.lock @@ -3146,6 +3146,8 @@ cohere = [ examples = [ { name = "langchain-huggingface" }, { name = "langchain-openai" }, + { name = "python-dotenv" }, + { name = "requests" }, ] experimental = [ { name = "langchain-text-splitters" }, @@ -3232,9 +3234,11 @@ requires-dist = [ { name = "pyarrow", marker = "extra == 'experimental'", specifier = ">=20.0.0" }, { name = "pydantic", specifier = ">=2.6.3,<3.0.0" }, { name = "pypdf", specifier = ">=6.0.0,<7.0.0" }, + { name = "python-dotenv", marker = "extra == 'examples'", specifier = ">=1.0.0,<2.0.0" }, { name = "pyyaml", specifier = ">=6.0.2,<7.0.0" }, { name = "qdrant-client", marker = "extra == 'qdrant'", specifier = ">=1.11.3,<2.0.0" }, { name = "rapidfuzz", marker = "extra == 'fuzzy-matching'", specifier = ">=3.12.2,<4.0.0" }, + { name = "requests", marker = "extra == 'examples'", specifier = ">=2.31.0,<3.0.0" }, { name = "scipy", marker = "python_full_version >= '3.9' and python_full_version < '3.13'", specifier = ">=1.13.0,<2.0.0" }, { name = "scipy", marker = "python_full_version >= '3.13' and python_full_version < '3.15'", specifier = ">=1.15.0,<2.0.0" }, { name = "sentence-transformers", marker = "extra == 'sentence-transformers'", specifier = ">=3.0.0,<4.0.0" }, From a841139622d52305779e44f2619c6fe843ab808f Mon Sep 17 00:00:00 2001 From: Willie Doran Date: Thu, 13 Aug 2026 09:45:56 +0200 Subject: [PATCH 2/2] docs(examples): drop the dead splitter links, and say what Azure needs Review feedback on #596. The two LangChain/LlamaIndex splitter examples are empty files, and SETUP.md documented that the README linked to them. Removing the links is the better half of that trade: nothing points at an empty file now, so the note has nothing left to warn about. The Azure note said only what the example does not do - read the environment. It now says what you actually have to do: its endpoint, key and API version are placeholders in the file, so it has to be edited before it runs, and that edit must not be committed. Co-Authored-By: Claude Opus 5 (1M context) --- examples/README.md | 2 -- examples/SETUP.md | 6 +++--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/examples/README.md b/examples/README.md index 9b13fba8f..6e8cabd6b 100644 --- a/examples/README.md +++ b/examples/README.md @@ -122,8 +122,6 @@ are listed in [the last section of this file](#customize). - [Custom](./customize/build_graph/components/loaders/custom_loader.py) - Text Splitter: - [Fixed size splitter](./customize/build_graph/components/splitters/fixed_size_splitter.py) - - [Splitter from LangChain](./customize/build_graph/components/splitters/langhchain_splitter.py) - - [Splitter from LLamaIndex](./customize/build_graph/components/splitters/llamaindex_splitter.py) - [Custom](./customize/build_graph/components/splitters/custom_splitter.py) - [Chunk embedder]() - Schema Builder: diff --git a/examples/SETUP.md b/examples/SETUP.md index 789547e63..84dcb10a4 100644 --- a/examples/SETUP.md +++ b/examples/SETUP.md @@ -129,6 +129,6 @@ These are properties of the examples, not of your setup. - `customize/llms/llm_with_neo4j_message_history.py` and `question_answering/graphrag_with_neo4j_message_history.py` write message history to the **read-only** demo database, so they fail with `Forbidden` as written. -- `customize/build_graph/components/splitters/langhchain_splitter.py` and `llamaindex_splitter.py` - are empty files, though `README.md` links to them. -- `customize/embeddings/azure_openai_embeddings.py` reads no environment variables at all. +- `customize/embeddings/azure_openai_embeddings.py` reads no environment variables. Its endpoint, + key and API version are placeholders in the file itself (`api_key=""`), so it has to be + edited before it will run — and that edit must not be committed.