Metadata-Version: 2.5
Name: llama-index-agensgraph
Version: 0.3.1
Summary: LlamaIndex graph + vector store integration for AgensGraph.
Project-URL: Homepage, https://github.com/skaiworldwide-oss/agensgraph-ai/tree/main/llama-index
Project-URL: Repository, https://github.com/skaiworldwide-oss/agensgraph-ai
Project-URL: Issues, https://github.com/skaiworldwide-oss/agensgraph-ai/issues
Author-email: Muhammad Taha Naveed <skaisw@skaiworldwide.com>
License-Expression: Apache-2.0
License-File: LICENSE
Keywords: agensgraph,graph store,llama-index,property graph,rag,vector store
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Database
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
Requires-Python: <3.15,>=3.11
Requires-Dist: agensgraph-python>=2.0.0
Requires-Dist: llama-index-core<0.15,>=0.12
Requires-Dist: psycopg[binary]<4.0.0,>=3.3.4
Description-Content-Type: text/markdown

# LlamaIndex AgensGraph

This plugin integrates [AgensGraph](https://github.com/skaiworldwide-oss/agensgraph)
with [LlamaIndex](https://www.llamaindex.ai/), persisting graphs and vectors
directly in AgensGraph. It powers `PropertyGraphIndex` and `VectorStoreIndex`,
so you can store and query property graphs and embeddings in one database.

- Property Graph Store: `AgensPropertyGraphStore`
- Vector Store: `AgensgraphVectorStore`
- Connection pool: `AgensEngine` (optional, shared across stores)

## Requirements

- Python 3.11+
- `agensgraph-python` 2.0, installed with the package
- AgensGraph 2.17 or later with the `vector` extension (for vector / HNSW search). The `meta`
  extension is used for schema introspection when present, with a catalog
  fallback otherwise.

## Installation

```shell
pip install llama-index llama-index-agensgraph
```

## Usage

### Property Graph Store

```python
import os
import urllib.request
import nest_asyncio
from llama_index.core import SimpleDirectoryReader, PropertyGraphIndex
from llama_index.embeddings.openai import OpenAIEmbedding
from llama_index.llms.openai import OpenAI
from llama_index.core.indices.property_graph import SchemaLLMPathExtractor

from llama_index_agensgraph.graph_stores.agensgraph import AgensPropertyGraphStore

os.environ[
    "OPENAI_API_KEY"
] = "<YOUR_API_KEY>"  # Replace with your OpenAI API key

url = (
    "https://raw.githubusercontent.com/run-llama/llama_index/main/docs/"
    "examples/data/paul_graham/paul_graham_essay.txt"
)
output_path = "data/paul_graham/paul_graham_essay.txt"
os.makedirs("data/paul_graham/", exist_ok=True)
urllib.request.urlretrieve(url, output_path)

nest_asyncio.apply()

# Nothing to escape: every value reaches the server as a bound parameter, so an
# apostrophe in the text is just an apostrophe.
documents = SimpleDirectoryReader("./data/paul_graham/").load_data()

# Setup AgensGraph connection (ensure AgensGraph is running)
conf = {
    "dbname": "",
    "user": "",
    "password": "",
    "host": "",
    "port": 5432,
}

# Pass vector_dimension to enable the HNSW vector index (match your embedding
# model's dimension, e.g. 1536 for text-embedding-3-small). Without it, vector
# search still works but is unindexed.
graph_store = AgensPropertyGraphStore(
    graph_name="graph",
    conf=conf,
    vector_dimension=1536,
)

index = PropertyGraphIndex.from_documents(
    documents,
    embed_model=OpenAIEmbedding(model_name="text-embedding-3-small"),
    kg_extractors=[
        SchemaLLMPathExtractor(
            llm=OpenAI(model="gpt-4o-mini", temperature=0.0),
            # strict=True can yield zero triplets with some models; strict=False is more forgiving
            strict=False,
        )
    ],
    property_graph_store=graph_store,
    show_progress=True,
)

query_engine = index.as_query_engine(include_text=True)

response = query_engine.query("What happened at Interleaf and Viaweb?")
print("\nDetailed Query Response:")
print(str(response))
```

### Natural-language queries (Text2Cypher)

`TextToCypherRetriever` turns a question into AgensGraph Cypher using the store's
built-in dialect prompt — no custom prompt needed:

```python
from llama_index.core.indices.property_graph import TextToCypherRetriever

retriever = TextToCypherRetriever(
    graph_store=graph_store, llm=OpenAI(model="gpt-4o-mini")
)
nodes = retriever.retrieve("How many entities of each type are there?")
print(nodes[0].node.text)  # the generated Cypher and its result
```

### Vector Store
```python
import os
import urllib.request
from llama_index.core import SimpleDirectoryReader, VectorStoreIndex, StorageContext
from llama_index_agensgraph.vector_stores.agensgraph import AgensgraphVectorStore

# Set your OpenAI API key
os.environ["OPENAI_API_KEY"] = "<YOUR_API_KEY>"  # Replace with your key

# Download example data
os.makedirs("data/paul_graham/", exist_ok=True)
url = (
    "https://raw.githubusercontent.com/run-llama/llama_index/main/docs/"
    "examples/data/paul_graham/paul_graham_essay.txt"
)
output_path = "data/paul_graham/paul_graham_essay.txt"
urllib.request.urlretrieve(url, output_path)

# Load documents
documents = SimpleDirectoryReader("./data/paul_graham").load_data()

# Setup AgensGraph connection (ensure AgensGraph is running)
url = "postgresql://username:password@host:port/database_name"
embed_dim = 1536

# Initialize vector store
vector_store = AgensgraphVectorStore(url=url, embedding_dimension=embed_dim)

# Build index
storage_context = StorageContext.from_defaults(vector_store=vector_store)
index = VectorStoreIndex.from_documents(documents, storage_context=storage_context)

# Query
query_engine = index.as_query_engine()
response = query_engine.query("What happened at Interleaf?")
print("\nQuery Response:")
print(str(response))
```

For hybrid (vector + keyword) search, build the store with `hybrid_search=True`
and query with a `query_str`:

```python
hybrid_store = AgensgraphVectorStore(
    url=url, embedding_dimension=embed_dim, hybrid_search=True
)
index = VectorStoreIndex.from_vector_store(hybrid_store)
index.as_retriever(vector_store_query_mode="hybrid").retrieve(
    "What happened at Interleaf?"
)
```

## Async & connection pooling

By default each store opens a single dedicated connection. For concurrent
workloads, share an `AgensEngine` (a driver connection pool) across stores so
each request checks out its own connection instead of serializing on one:

```python
from llama_index_agensgraph.engine import AgensEngine
from llama_index_agensgraph.graph_stores.agensgraph import AgensPropertyGraphStore
from llama_index_agensgraph.vector_stores.agensgraph import AgensgraphVectorStore

engine = AgensEngine.from_url(
    "postgresql://user:pwd@host:5432/db", min_size=2, max_size=20
)

graph_store = AgensPropertyGraphStore(graph_name="graph", conf=conf, engine=engine)
vector_store = AgensgraphVectorStore(
    url="postgresql://user:pwd@host:5432/db",
    embedding_dimension=1536,
    engine=engine,
)

# ... use the stores ...
engine.close()  # await engine.aclose() if you used the async pool
```

The stores also provide true-async hot paths backed by the driver's `AsyncConnection`
(no thread-pool wrapping):

- Vector store: `async_add`, `aquery`, `adelete`
- Property graph store: `aupsert_nodes`, `aupsert_relations`, `aget`,
  `avector_query`, `astructured_query`

These work with or without an `AgensEngine`; with one, they draw from the async
pool.

## Performance & indexing

The stores are indexed for their hot paths out of the box:

- **Ingest** (`MERGE`-by-`id`) is backed by a btree index on `id`, so bulk
  `upsert`/`add` stays near-linear rather than O(N²).
- **Vector search** uses the HNSW index on the embedding.
- **Lookups by id** (`get` / `get_nodes` / `delete_nodes`) and the vector
  store's **`delete(ref_doc_id)`** are index-backed.
- **Relation upserts** are UNWIND-batched per relationship type (not one query
  per relation).

**Metadata-filtered vector search.** A metadata filter cannot use the HNSW index
for the filter itself, so a filter on an *un-indexed* property degrades to a
sequential scan over the embedded nodes. Index the keys you filter on to keep it
fast:

```python
# Property graph store
graph_store.create_property_index("country")

# Vector store
vector_store.create_property_index("topic")
```

With the index present, the planner preselects matching rows via an index/bitmap
scan and then ranks them — instead of scanning every node.

**Counting and type filters.** Prefer `count(*)` over `count(n)` in aggregations:
`count(n)` materializes each matched node (including its embedding), so it is much
slower on a graph that stores embeddings. For "all nodes of type X", match the label
itself with `MATCH (n:X)`, which reads only that label's storage.

## Demos & guides

**Start here:** the [**`examples/demos/`**](https://github.com/skaiworldwide-oss/agensgraph-ai/tree/main/llama-index/examples/demos) suite — runnable,
end-to-end demos on real datasets (arXiv, Wikipedia, CC-News) that show how to
build with this integration at realistic scale. Its
[README](https://github.com/skaiworldwide-oss/agensgraph-ai/tree/main/llama-index/examples/demos/README.md) has a quickstart and copy-paste building
blocks.

| Demo | What you build |
|------|----------------|
| [01 · arXiv](https://github.com/skaiworldwide-oss/agensgraph-ai/tree/main/llama-index/examples/demos/01_arxiv_pg) | a property graph + vector search + GraphRAG in one store, with `get`/`get_triplets` and an upsert→delete lifecycle |
| [02 · Wikipedia](https://github.com/skaiworldwide-oss/agensgraph-ai/tree/main/llama-index/examples/demos/02_wikipedia_pgindex) | an LLM-built knowledge graph + natural-language (Text2Cypher) Q&A |
| [03 · News](https://github.com/skaiworldwide-oss/agensgraph-ai/tree/main/llama-index/examples/demos/03_news_vector_rag) | vector RAG: semantic, metadata-filtered (full operator set), hybrid, and cited, plus the store-mutation lifecycle |
| [04 · Router](https://github.com/skaiworldwide-oss/agensgraph-ai/tree/main/llama-index/examples/demos/04_router) | one `AgensEngine` routing questions to the graph or the vector store — router *and* `FunctionAgent` |

Each demo folder also ships a **pre-executed notebook** — a narrated, end-to-end
tour with real embedded outputs.

Short, single-feature notebooks:

- [Property graph store](https://github.com/skaiworldwide-oss/agensgraph-ai/tree/main/llama-index/examples/property_graph/property_graph_agensgraph.ipynb)
- [Vector store](https://github.com/skaiworldwide-oss/agensgraph-ai/tree/main/llama-index/examples/vector_stores/AgensgraphVectorDemo.ipynb)
- [Vector store — metadata filters](https://github.com/skaiworldwide-oss/agensgraph-ai/tree/main/llama-index/examples/vector_stores/agensgraph_metadata_filter.ipynb)

## What's new in 0.3.0

Every statement goes through the [`agensgraph-python`](https://github.com/skaiworldwide-oss/agensgraph-python) 2.0 driver.

- **An element is written on the label naming what it is.** `MATCH (n:Author)`
  reads that label's storage and nothing else, so nothing has to keep a list of
  types or a scalar copy of one beside every element. Measured on twenty thousand
  of each of two types, counting one of them: 217 buffers by label against 335
  through a btree over such a copy, which also cost an index entry on every
  write. Each label carries its own uniqueness on `id`, because a constraint on
  the label they inherit does not reach them.
- **The embedding has a column of its own.** Read out of the property map it is
  text in a bag that has to come out of TOAST and be parsed before a distance can
  be taken, once per element a filter kept -- which is where a metadata-filtered
  search spent its time. Over 20,000 entities with a filter keeping one in ten:
  1,209 ms against 171 ms for the same complete answer -- 7x, and 111x against
  the un-promoted path once both are asked for every row they were asked for.
  The 180x first published here compared a complete answer against a partial one.
  Decline it with `promote_embedding=False`.
- **The query's mode is read.** `VectorStoreQuery.mode` was never looked at, so
  every mode got a plain vector search. Hybrid, text search and MMR are answered
  now, `alpha`/`sparse_top_k`/`hybrid_top_k` do what they say, a metadata filter
  reaches both halves of a hybrid search, and `distance_strategy` takes `l2` and
  `inner_product` as well as `cosine`.
- **A generated statement runs read-only.** `SafeTextToCypherRetriever` runs what
  a model wrote in a transaction the server will not let write, so what it may do
  is the server's decision. A list of Cypher's write keywords is not that: it is
  PostgreSQL underneath, so `INSERT`, `TRUNCATE`, `GRANT` and `COPY` are all
  available and none of them is on such a list, while a read whose text merely
  mentions DELETE looks like a write.
- **True async.** All twelve async methods the contract declares are implemented
  here. The base class answers most of them by calling the synchronous one, which
  holds the event loop for the whole round trip: twelve concurrent rel maps ran in
  76.9 ms with the loop doing nothing else at all, against 31.8 ms with it still
  running other work. See [Async & connection pooling](#async--connection-pooling).
- **Nothing is installed in your database.** Opening a store used to create three
  plpgsql functions there. The catalogs answer the same questions, and faster: on
  twenty thousand elements carrying embeddings, 1,213 ms of walking every one of
  them against 19 ms.
- **Performance.** Ingest is index-backed and near-linear (a btree index on the
  `id` MERGE key; bulk `add`/`upsert` batched). Id-keyed lookups (`get`,
  `get_nodes`, `get_triplets`, `get_rel_map`, `delete_nodes`) and the vector
  store's `delete(ref_doc_id)` are index-backed rather than sequential scans,
  relation upserts are UNWIND-batched per type, and schema introspection no
  longer materializes every distinct property value. Metadata-filter keys can be
  indexed with `create_property_index(...)`. See
  [Performance & indexing](#performance--indexing).
- AgensGraph 2.17 or later; an older server is refused at connect. Python 3.11 to 3.14.

## Changes in 0.2.0

- **`AgensPropertyGraphStore.vector_query` works.** It hard-coded a 3-dimension
  cast and ordered by a fixed literal vector, so results ignored the query
  embedding and it errored at any other dimension. Repairing that left it
  emitting `ORDER BY` after a `WITH` inside a SQL sub-query, which the grammar
  does not allow, so every call raised until the ordering moved onto the final
  `RETURN` -- where it still reaches the HNSW index, which is the part worth
  checking rather than assuming.
- **Metadata-filtered vector search.** Both `AgensPropertyGraphStore.vector_query`
  and `AgensgraphVectorStore.query` honor `MetadataFilters`, translated into a
  fully parameterized (injection-safe) Cypher `WHERE`. All 14 `FilterOperator`
  values are supported — `EQ`, `NE`, `GT`, `GTE`, `LT`, `LTE`, `IN`, `NIN`,
  `CONTAINS`, `TEXT_MATCH`, `TEXT_MATCH_INSENSITIVE`, `ANY`, `ALL`, `IS_EMPTY` —
  along with `AND`/`OR`/`NOT` conditions and nested filter groups.
- **Hybrid search.** `AgensgraphVectorStore(hybrid_search=True)` fuses HNSW
  semantic search with full-text keyword search by reciprocal rank fusion — each
  modality is queried against its own index (so both stay index-backed) and the
  two rankings are merged.
- **AgensGraph-dialect Text2Cypher.** The property graph store sets a default
  `text_to_cypher_template` that knows the storage model -- an element is written
  on the label naming what it is, and every such label inherits `"__Node__"` --
  and avoids Neo4j-only syntax, so `TextToCypherRetriever` generates runnable
  Cypher out of the box.
- **Lazy schema introspection.** `AgensPropertyGraphStore(refresh_schema=False)`
  defers the (O(N)) schema scan to the first `get_schema()`/`get_schema_str()`
  call, so opening a large existing graph is instant.
- **Correctness fixes.** Entity embeddings are persisted on `upsert_nodes` even
  when the entity has no source chunk; `get(ids=[])` returns nothing (instead of
  the whole graph); and depth-1 `get_rel_map` uses a fixed pattern (AgensGraph's
  variable-length edges are far slower).
- **Modern vector-store node management.** `AgensgraphVectorStore` implements
  `get_nodes(node_ids, filters)`, `delete_nodes(node_ids, filters)` and `clear()`
  (plus async `aget_nodes` / `adelete_nodes` / `aclear`).
- **Richer enhanced schema.** With `enhanced_schema=True`, numeric properties get
  `min` / `max` / `distinct_count`, list properties get `min_size` / `max_size`,
  and other properties get example values + `distinct_count` (computed
  exhaustively under a row threshold, sampled above it).
- **Breaking change.** The deprecated triplet `AgensGraphStore` (Knowledge Graph
  Store) has been removed. Use `AgensPropertyGraphStore` with `PropertyGraphIndex`.

## License

Apache-2.0.
