Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,23 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [2.0.5] - 2026-10-05

### Improved

- **`installQueries()` no longer holds the connection open while queries compile** — installation is submitted asynchronously, so the call returns as soon as the server accepts it instead of blocking for the whole compile and risking a gateway timeout. `wait=True` (the default for sync connections) still returns only once installation has finished; `wait=False` (the default for async connections) now returns the ID of the submitted request rather than a completion message.
- **`AI.query()` chat engine selection** — `query()` now accepts `mode` (`"agentic"` | `"classic"`), `rag_method` (agent style or retriever name), and `include_fields` to choose the GraphRAG chat engine and control what the response returns. Called with only a question it behaves exactly as before, deferring to the graph's configured default engine.

### Fixed

- **Declared minimum Python version corrected to 3.9** — installing on Python 3.8 previously succeeded and then failed at import. The package has in fact required 3.9 since it began using built-in generic type annotations; the conda recipe already required 3.9.
- **`AsyncTigerGraphConnection` use across event loops** — a connection is no longer tied to the first event loop it ran on. Reusing it after a loop has closed, which is what `asyncio.run()` leaves behind on every call, no longer fails with `RuntimeError: Event loop is closed`; and sharing one connection between threads that each run their own loop no longer lets one thread close the session another is still using. The HTTP session and connection pool are now kept per event loop, and those belonging to a closed loop are released rather than reported as leaked.
- **Untyped vertex IDs in `runInstalledQuery()` GET mode** — passing a vertex as `(id, "type")` no longer fails when the ID contains `&` or `#`; the ID is now escaped in the query string as it already was for every other vertex form.
- **Parameter values in async `runInterpretedQuery()`** — string and vertex values containing spaces, quotes, `&`, `=`, `%`, `#`, `+` or non-ASCII characters reached the query still percent-encoded (e.g. `"a b"` arrived as `"a%20b"`), and a vertex whose ID contained one of them could not be found. Values now arrive exactly as passed, matching the sync client.
- **Vertex IDs containing `%` in `runInstalledQuery()`** — a vertex whose primary ID contains a percent sign (e.g. `"50% done.pdf"`) could not be retrieved via POST. The ID is URL-decoded by the server, so the `%` was read as the start of an escape sequence; it is now escaped like every other string in the request body. Applies to both the sync and async clients.

---

## [2.0.4] - 2026-05-18

### Fixed
Expand Down Expand Up @@ -74,7 +91,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- **`showSecrets()` removed.** Use `getSecrets()` instead.
- **`runInstalledQuery()` now auto-selects GET or POST** based on `params` type (`dict` → POST, `str` → GET). Passing a raw query string with `usePost=True` raises `TigerGraphException`.
- **MCP tools moved to [`pytigergraph-mcp`](https://github.com/tigergraph/pytigergraph-mcp).** Do not import from `pyTigerGraph.mcp` directly.
- **MCP tools moved to [`tigergraph-mcp`](https://github.com/tigergraph/tigergraph-mcp).** Do not import from `pyTigerGraph.mcp` directly.

### New Features

Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,8 +164,9 @@ with TigerGraphConnection(...) as conn:

### Asynchronous mode (`AsyncTigerGraphConnection`)

- Uses a single `aiohttp.ClientSession` with an unbounded connection pool shared across all concurrent coroutines — no GIL, no thread-scheduling overhead.
- Uses a single `aiohttp.ClientSession` per event loop, with an unbounded connection pool shared across all concurrent coroutines — no GIL, no thread-scheduling overhead.
- Typically achieves higher QPS and lower tail latency than the threaded sync mode for I/O-bound workloads.
- A connection may be used from more than one event loop: reused across separate `asyncio.run()` calls, or shared by threads that each run their own loop. Each loop gets its own session and pool. Use `async with` (or `await conn.aclose()`) to release sockets when finished.

```python
import asyncio
Expand Down
2 changes: 1 addition & 1 deletion pyTigerGraph/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
try:
__version__ = _pkg_version("pyTigerGraph")
except PackageNotFoundError:
__version__ = "2.0.4"
__version__ = "2.0.5"

__license__ = "Apache 2"

Expand Down
21 changes: 20 additions & 1 deletion pyTigerGraph/ai/ai.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,17 +201,36 @@ def retrieveDocs(self, query: str, top_k: int = 3):
"/retrieve_docs?top_k="+str(top_k)
return self.conn._req("POST", url, authMode="pwd", data=data, jsonData=True, resKey=None, skipCheck=True)

def query(self, query):
def query(self, query, mode: str = None, rag_method: str = None, include_fields: list = None):
""" Query the database with natural language.
Args:
query (str):
Natural language query to ask about the database.
mode (str):
Chat engine to use: "agentic", "classic", or None to defer
to the graph's configured default.
rag_method (str):
Engine variant. When agentic: "auto", "planned", or
"reactive". When classic: "auto" or a retriever name
(e.g. "hybrid", "similarity", "contextual",
"entityrelationship", "community"). None defers to the
configured default.
include_fields (list):
Extra response fields beyond the answer. None returns the
answer only; pass field names (e.g. ["query_sources"]) or
["all"] to include the supporting sources / trace.
Returns:
JSON including the natural language response, a answered_question flag, and answer sources.
"""
data = {
"query": query
}
if mode is not None:
data["mode"] = mode
if rag_method is not None:
data["rag_method"] = rag_method
if include_fields is not None:
data["include_fields"] = include_fields

url = self.nlqs_host+"/"+self.conn.graphname+"/query"
return self.conn._req("POST", url, authMode="pwd", data=data, jsonData=True, resKey=None)
Expand Down
2 changes: 1 addition & 1 deletion pyTigerGraph/common/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ def __init__(self, host: str = "http://127.0.0.1", graphname: str = "",
if inputHost.scheme not in ["http", "https"]:
raise TigerGraphException("Invalid URL scheme. Supported schemes are http and https.",
"E-0003")
# Extract port from URL if present (e.g. http://192.168.11.11:14240)
# Extract port from URL if present (e.g. http://127.0.0.1:14240)
# Use hostname (without port) to avoid double-port URLs later.
hostOnly = inputHost.hostname
if not hostOnly:
Expand Down
26 changes: 20 additions & 6 deletions pyTigerGraph/common/query.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ def _parse_query_parameters(params: dict) -> str:
f"Invalid vertex parameter '{k}': vertex type string must not be empty. "
"Use (id,) for VERTEX<T> or (id, 'type') for untyped VERTEX.")
# VERTEX (untyped): (id, "type") → k=id&k.type=type
parts.append(k + "=" + str(v[0]))
parts.append(k + "=" + _safe_char(v[0]))
parts.append(k + ".type=" + _safe_char(v[1]))
else:
raise TigerGraphException(
Expand All @@ -88,7 +88,7 @@ def _parse_query_parameters(params: dict) -> str:
"Use (id,) for VERTEX<T> or (id, 'type') for untyped VERTEX.")
# SET<VERTEX>: (id, "type") → k[i]=id&k[i].type=type
parts.append(k + "[" + str(i) + "]=" + _safe_char(vv[0]))
parts.append(k + "[" + str(i) + "].type=" + vv[1])
parts.append(k + "[" + str(i) + "].type=" + _safe_char(vv[1]))
else:
raise TigerGraphException(
f"Invalid vertex parameter '{k}[{i}]': expected (id,) for VERTEX<T> "
Expand Down Expand Up @@ -120,6 +120,20 @@ def _encode_str_for_post(value: str) -> str:
return value.replace("%", "%25")


def _encode_vertex_id_for_post(value):
"""Apply the same ``%`` escaping to a string vertex ID as to every other
string in the POST ``/query`` body.

Vertex IDs go through the same URL-decoding as plain string parameters, so
a bare ``%`` in an ID is read as the start of a percent-escape and the ID
fails to resolve. Other reserved characters — spaces, ``/``, ``:``, ``@``,
``+`` — and non-ASCII need no escaping and are left alone.

Non-string IDs are returned unchanged so INT primary IDs keep their JSON type.
"""
return _encode_str_for_post(value) if isinstance(value, str) else value


def _prep_query_parameters_json(params: dict) -> dict:
"""Converts a parameter dictionary into the JSON format expected by TigerGraph's
POST /query endpoint.
Expand Down Expand Up @@ -160,14 +174,14 @@ def _prep_query_parameters_json(params: dict) -> dict:
if isinstance(v, tuple):
if len(v) == 1:
# VERTEX<T> (typed): (id,) → {"id": id}
converted[k] = {"id": v[0]}
converted[k] = {"id": _encode_vertex_id_for_post(v[0])}
elif len(v) == 2 and isinstance(v[1], str):
if not v[1]:
raise TigerGraphException(
f"Invalid vertex parameter '{k}': vertex type string must not be empty. "
"Use (id,) for VERTEX<T> or (id, 'type') for untyped VERTEX.")
# VERTEX (untyped): (id, "type") → {"id": id, "type": "type"}
converted[k] = {"id": v[0], "type": v[1]}
converted[k] = {"id": _encode_vertex_id_for_post(v[0]), "type": v[1]}
else:
raise TigerGraphException(
f"Invalid vertex parameter '{k}': expected (id,) for VERTEX<T> "
Expand All @@ -188,14 +202,14 @@ def _prep_query_parameters_json(params: dict) -> dict:
if isinstance(vv, tuple):
if len(vv) == 1:
# SET<VERTEX<T>>: (id,) → {"id": id}
new_list.append({"id": vv[0]})
new_list.append({"id": _encode_vertex_id_for_post(vv[0])})
elif len(vv) == 2 and isinstance(vv[1], str):
if not vv[1]:
raise TigerGraphException(
f"Invalid vertex parameter '{k}': vertex type string must not be empty. "
"Use (id,) for VERTEX<T> or (id, 'type') for untyped VERTEX.")
# SET<VERTEX>: (id, "type") → {"id": id, "type": "type"}
new_list.append({"id": vv[0], "type": vv[1]})
new_list.append({"id": _encode_vertex_id_for_post(vv[0]), "type": vv[1]})
else:
raise TigerGraphException(
f"Invalid vertex parameter '{k}': expected (id,) for VERTEX<T> "
Expand Down
4 changes: 4 additions & 0 deletions pyTigerGraph/pyTigerGraphQuery.py
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,10 @@ def installQueries(self, queries: Union[str, list], flag: Union[str, list] = Non
flag = ",".join(flag)
params["flag"] = flag

# Install asynchronously so the server returns a requestId immediately
# instead of holding the request open for the whole compile.
params["async"] = "true"

res = self._req("GET", self.gsUrl + "/gsql/v1/queries/install", params=params, authMode="pwd", resKey=None)

if wait:
Expand Down
Loading