memgraph-toolbox 0.2.1__tar.gz → 0.2.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. {memgraph_toolbox-0.2.1 → memgraph_toolbox-0.2.3}/.gitignore +3 -0
  2. {memgraph_toolbox-0.2.1 → memgraph_toolbox-0.2.3}/PKG-INFO +1 -1
  3. {memgraph_toolbox-0.2.1 → memgraph_toolbox-0.2.3}/pyproject.toml +1 -1
  4. {memgraph_toolbox-0.2.1 → memgraph_toolbox-0.2.3}/src/memgraph_toolbox/api/memgraph.py +48 -4
  5. {memgraph_toolbox-0.2.1 → memgraph_toolbox-0.2.3}/src/memgraph_toolbox/client/mcp_prompt.py +14 -8
  6. {memgraph_toolbox-0.2.1 → memgraph_toolbox-0.2.3}/src/memgraph_toolbox/tests/test_async_memgraph.py +5 -0
  7. memgraph_toolbox-0.2.3/src/memgraph_toolbox/tests/test_merge_with_retry.py +69 -0
  8. memgraph_toolbox-0.2.1/uv.lock +0 -3399
  9. {memgraph_toolbox-0.2.1 → memgraph_toolbox-0.2.3}/LICENSE +0 -0
  10. {memgraph_toolbox-0.2.1 → memgraph_toolbox-0.2.3}/README.md +0 -0
  11. {memgraph_toolbox-0.2.1 → memgraph_toolbox-0.2.3}/src/memgraph_toolbox/__init__.py +0 -0
  12. {memgraph_toolbox-0.2.1 → memgraph_toolbox-0.2.3}/src/memgraph_toolbox/api/__init__.py +0 -0
  13. {memgraph_toolbox-0.2.1 → memgraph_toolbox-0.2.3}/src/memgraph_toolbox/api/tool.py +0 -0
  14. {memgraph_toolbox-0.2.1 → memgraph_toolbox-0.2.3}/src/memgraph_toolbox/api/toolbox.py +0 -0
  15. {memgraph_toolbox-0.2.1 → memgraph_toolbox-0.2.3}/src/memgraph_toolbox/client/__init__.py +0 -0
  16. {memgraph_toolbox-0.2.1 → memgraph_toolbox-0.2.3}/src/memgraph_toolbox/evals/__init__.py +0 -0
  17. {memgraph_toolbox-0.2.1 → memgraph_toolbox-0.2.3}/src/memgraph_toolbox/evals/coherence.py +0 -0
  18. {memgraph_toolbox-0.2.1 → memgraph_toolbox-0.2.3}/src/memgraph_toolbox/memgraph_toolbox.py +0 -0
  19. {memgraph_toolbox-0.2.1 → memgraph_toolbox-0.2.3}/src/memgraph_toolbox/py.typed +0 -0
  20. {memgraph_toolbox-0.2.1 → memgraph_toolbox-0.2.3}/src/memgraph_toolbox/tests/__init__.py +0 -0
  21. {memgraph_toolbox-0.2.1 → memgraph_toolbox-0.2.3}/src/memgraph_toolbox/tests/test_embedding.py +0 -0
  22. {memgraph_toolbox-0.2.1 → memgraph_toolbox-0.2.3}/src/memgraph_toolbox/tests/test_eval_coherence.py +0 -0
  23. {memgraph_toolbox-0.2.1 → memgraph_toolbox-0.2.3}/src/memgraph_toolbox/tests/test_mcp_prompt.py +0 -0
  24. {memgraph_toolbox-0.2.1 → memgraph_toolbox-0.2.3}/src/memgraph_toolbox/tests/test_memgraph_config.py +0 -0
  25. {memgraph_toolbox-0.2.1 → memgraph_toolbox-0.2.3}/src/memgraph_toolbox/tests/test_toolbox.py +0 -0
  26. {memgraph_toolbox-0.2.1 → memgraph_toolbox-0.2.3}/src/memgraph_toolbox/tests/test_tools.py +0 -0
  27. {memgraph_toolbox-0.2.1 → memgraph_toolbox-0.2.3}/src/memgraph_toolbox/tools/__init__.py +0 -0
  28. {memgraph_toolbox-0.2.1 → memgraph_toolbox-0.2.3}/src/memgraph_toolbox/tools/cypher.py +0 -0
  29. {memgraph_toolbox-0.2.1 → memgraph_toolbox-0.2.3}/src/memgraph_toolbox/tools/schema.py +0 -0
  30. {memgraph_toolbox-0.2.1 → memgraph_toolbox-0.2.3}/src/memgraph_toolbox/utils/embedding.py +0 -0
  31. {memgraph_toolbox-0.2.1 → memgraph_toolbox-0.2.3}/src/memgraph_toolbox/utils/logger.py +0 -0
  32. {memgraph_toolbox-0.2.1 → memgraph_toolbox-0.2.3}/src/memgraph_toolbox/utils/serialization.py +0 -0
@@ -187,3 +187,6 @@ cython_debug/
187
187
 
188
188
  # deepeval writes run caches and telemetry here; build artifacts, not source.
189
189
  .deepeval/
190
+
191
+ # Eval runs hold benchmark text (questions, answers, retrieval); only published baselines are committed.
192
+ context-graph/eval/runs/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: memgraph-toolbox
3
- Version: 0.2.1
3
+ Version: 0.2.3
4
4
  Summary: Memgraph toolbox library for Memgraph AI tools and utilities
5
5
  Project-URL: Homepage, https://github.com/memgraph/ai-toolkit
6
6
  Project-URL: Source, https://github.com/memgraph/ai-toolkit
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "memgraph-toolbox"
3
- version = "0.2.1"
3
+ version = "0.2.3"
4
4
  description = "Memgraph toolbox library for Memgraph AI tools and utilities"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -1,4 +1,5 @@
1
1
  import os
2
+ import time
2
3
  from collections.abc import Mapping
3
4
  from typing import Any, cast
4
5
 
@@ -78,6 +79,49 @@ def memgraph_env(
78
79
  }
79
80
 
80
81
 
82
+ def merge_with_retry(db: Any, query: str, params: dict | None = None, *, attempts: int = 3) -> list[dict[str, Any]]:
83
+ """Run *query*, a MERGE of a uniqueness-constrained node, through *db*, surviving a concurrent create.
84
+
85
+ Two transactions that MERGE the same new node both find it missing and
86
+ both create it; the later commit fails the uniqueness constraint. Run
87
+ again, its MERGE matches the node the other one committed. Writers that
88
+ race this way are typically one process per event (agent hooks).
89
+
90
+ Args:
91
+ db: Anything with ``query(query, params)``, normally a :class:`Memgraph`.
92
+ query: The Cypher to run; it must be safe to run more than once.
93
+ params: Its parameters.
94
+ attempts: Runs before giving up, the first included.
95
+
96
+ Returns:
97
+ The rows of the run that committed.
98
+
99
+ Raises:
100
+ neo4j.exceptions.ClientError: a uniqueness violation on the last
101
+ attempt, or any other client error at once.
102
+ """
103
+ from neo4j.exceptions import ClientError
104
+
105
+ for attempt in range(attempts):
106
+ try:
107
+ return db.query(query, params=params)
108
+ except ClientError as exc:
109
+ if "unique constraint" not in str(exc) or attempt == attempts - 1:
110
+ raise
111
+ time.sleep(0.05 * (attempt + 1))
112
+ raise ValueError(f"attempts must be at least 1, got {attempts}")
113
+
114
+
115
+ def _driver_database(name: str) -> str | None:
116
+ """The database to hand the driver: *name*, or None for an empty one (the login's main database).
117
+
118
+ Over a direct ``bolt://`` connection ``""`` and None both leave the database to the
119
+ server. A routing ``neo4j://`` driver instead asks the cluster for the routing table
120
+ of a database named ``""``, which has no writer, so every write fails.
121
+ """
122
+ return name or None
123
+
124
+
81
125
  class Memgraph:
82
126
  """
83
127
  Base Memgraph client for interacting with Memgraph database.
@@ -107,7 +151,7 @@ class Memgraph:
107
151
  url: The Memgraph connection URL
108
152
  username: Username for authentication
109
153
  password: Password for authentication
110
- database: The database name to connect to (default: "memgraph")
154
+ database: The database name to connect to (default: "memgraph"); "" for the login's main database
111
155
  driver_config: Additional Neo4j driver configuration
112
156
  user_agent: Client name sent to the server (e.g. "mcp-memgraph", "langchain-memgraph", "sql2graph", etc.)
113
157
  """
@@ -123,7 +167,7 @@ class Memgraph:
123
167
 
124
168
  self.driver = GraphDatabase.driver(url, auth=(username, password), **config)
125
169
 
126
- self.database = env["MEMGRAPH_DATABASE"]
170
+ self.database = _driver_database(env["MEMGRAPH_DATABASE"])
127
171
 
128
172
  try:
129
173
  import neo4j
@@ -217,7 +261,7 @@ class AsyncMemgraph:
217
261
  url: The Memgraph connection URL
218
262
  username: Username for authentication
219
263
  password: Password for authentication
220
- database: The database name to connect to (default: "memgraph")
264
+ database: The database name to connect to (default: "memgraph"); "" for the login's main database
221
265
  driver_config: Additional Neo4j driver configuration
222
266
  user_agent: Client name sent to the server (e.g. "mcp-memgraph", "langchain-memgraph", "sql2graph", etc.)
223
267
  """
@@ -232,7 +276,7 @@ class AsyncMemgraph:
232
276
  config.setdefault("user_agent", user_agent or self.DEFAULT_USER_AGENT)
233
277
 
234
278
  self.driver = AsyncGraphDatabase.driver(url, auth=(username, password), **config)
235
- self.database = env["MEMGRAPH_DATABASE"]
279
+ self.database = _driver_database(env["MEMGRAPH_DATABASE"])
236
280
 
237
281
  async def verify_connectivity(self) -> None:
238
282
  """
@@ -6,7 +6,7 @@ import sys
6
6
  from typing import cast
7
7
 
8
8
  try:
9
- from litellm import acompletion, experimental_mcp_client
9
+ from litellm import ModelResponse, acompletion, experimental_mcp_client
10
10
  from mcp import ClientSession, StdioServerParameters
11
11
  from mcp.client.stdio import stdio_client
12
12
  except ImportError as e:
@@ -69,10 +69,13 @@ async def prompt_with_tools(
69
69
  logger.info(f"{len(tools)} MCP tools loaded")
70
70
 
71
71
  messages = [*tool_selection_system_messages, {"role": "user", "content": prompt}]
72
- resp = await acompletion(
73
- model=tool_selection_model_name,
74
- messages=messages,
75
- tools=tools,
72
+ resp = cast(
73
+ "ModelResponse",
74
+ await acompletion(
75
+ model=tool_selection_model_name,
76
+ messages=messages,
77
+ tools=tools,
78
+ ),
76
79
  )
77
80
  msg = resp["choices"][0]["message"]
78
81
  tool_calls = msg.get("tool_calls", [])
@@ -147,9 +150,12 @@ async def prompt_with_tools(
147
150
  )
148
151
 
149
152
  messages = messages + response_system_messages
150
- final_resp = await acompletion(
151
- model=response_model_name,
152
- messages=messages,
153
+ final_resp = cast(
154
+ "ModelResponse",
155
+ await acompletion(
156
+ model=response_model_name,
157
+ messages=messages,
158
+ ),
153
159
  )
154
160
  return final_resp["choices"][0]["message"].content
155
161
 
@@ -32,6 +32,11 @@ def test_async_memgraph_resolves_config_from_env(monkeypatch):
32
32
  assert explicit.database == "arg-db"
33
33
 
34
34
 
35
+ def test_an_empty_database_reaches_the_driver_as_none():
36
+ """An empty name means the login's main database; a routing driver would look up a database named ""."""
37
+ assert AsyncMemgraph(url="neo4j://localhost:7687", database="").database is None
38
+
39
+
35
40
  def test_async_memgraph_mirrors_sync_defaults():
36
41
  """AsyncMemgraph shares the sync client's user-agent default."""
37
42
  assert AsyncMemgraph.DEFAULT_USER_AGENT == Memgraph.DEFAULT_USER_AGENT
@@ -0,0 +1,69 @@
1
+ """Tests for merge_with_retry against a live Memgraph; they skip when none is reachable."""
2
+
3
+ from concurrent.futures import ThreadPoolExecutor
4
+
5
+ import pytest
6
+ from neo4j.exceptions import ClientError
7
+
8
+ from ..api.memgraph import Memgraph, merge_with_retry
9
+
10
+ _LABEL = "MergeWithRetryProbe"
11
+
12
+
13
+ @pytest.fixture
14
+ def db():
15
+ try:
16
+ client = Memgraph()
17
+ except Exception as exc:
18
+ pytest.skip(f"No Memgraph server reachable at the configured MEMGRAPH_URL: {exc}")
19
+ client.query(f"MATCH (n:{_LABEL}) DETACH DELETE n")
20
+ client.query(f"CREATE CONSTRAINT ON (n:{_LABEL}) ASSERT n.key IS UNIQUE;")
21
+ yield client
22
+ client.query(f"MATCH (n:{_LABEL}) DETACH DELETE n")
23
+ client.query(f"DROP CONSTRAINT ON (n:{_LABEL}) ASSERT n.key IS UNIQUE;")
24
+ client.close()
25
+
26
+
27
+ def test_concurrent_merges_of_new_nodes_converge_on_one_node_each(db):
28
+ keys = [f"k{i}" for i in range(10)]
29
+
30
+ def merge(key: str) -> None:
31
+ merge_with_retry(db, f"MERGE (n:{_LABEL} {{key: $key}})", {"key": key})
32
+
33
+ with ThreadPoolExecutor(max_workers=16) as pool:
34
+ list(pool.map(merge, keys * 16))
35
+
36
+ rows = db.query(f"MATCH (n:{_LABEL}) RETURN n.key AS key, count(*) AS c ORDER BY key")
37
+ assert rows == [{"key": key, "c": 1} for key in sorted(keys)]
38
+
39
+
40
+ def test_returns_the_rows_of_the_run_that_committed(db):
41
+ rows = merge_with_retry(db, f"MERGE (n:{_LABEL} {{key: 'a'}}) RETURN n.key AS key")
42
+ assert rows == [{"key": "a"}]
43
+
44
+
45
+ def test_any_other_client_error_is_raised_without_retrying(db):
46
+ calls = []
47
+
48
+ class Counting:
49
+ def query(self, query, params=None):
50
+ calls.append(query)
51
+ return db.query(query, params)
52
+
53
+ with pytest.raises(ClientError):
54
+ merge_with_retry(Counting(), "MERGE (n:")
55
+ assert len(calls) == 1
56
+
57
+
58
+ def test_a_conflict_on_every_attempt_is_raised_after_the_last():
59
+ """A conflict that never clears can't be produced on demand by a real server."""
60
+ calls = []
61
+
62
+ class AlwaysConflicting:
63
+ def query(self, query, params=None):
64
+ calls.append(query)
65
+ raise ClientError("Unable to commit due to unique constraint violation on :X(key)")
66
+
67
+ with pytest.raises(ClientError, match="unique constraint"):
68
+ merge_with_retry(AlwaysConflicting(), "MERGE (n:X {key: 1})", attempts=2)
69
+ assert len(calls) == 2