You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
- nb03: add client.health() verification before 'Connected' print in
both gRPC branches (COORDINODE_ADDR env and _port_open fallback)
- nb03: scope find_related intermediate hops to session — use path
variable p and WHERE ALL(x IN nodes(p) WHERE x.session = $sess);
switch type(last(r)) to type(last(relationships(p)))
- tests: add test_create_label_schema_mode_validated asserting
schema_mode == 2 (VALIDATED) to complement flexible/invalid tests
Copy file name to clipboardExpand all lines: demo/notebooks/03_langgraph_agent.ipynb
+135-3Lines changed: 135 additions & 3 deletions
Original file line number
Diff line number
Diff line change
@@ -56,7 +56,48 @@
56
56
"id": "d4e5f6a7-0003-0000-0000-000000000005",
57
57
"metadata": {},
58
58
"outputs": [],
59
-
"source": "import os, socket\n\n\ndef _port_open(port):\n try:\n with socket.create_connection((\"127.0.0.1\", port), timeout=1):\n return True\n except OSError:\n return False\n\n\nGRPC_PORT = int(os.environ.get(\"COORDINODE_PORT\", \"7080\"))\n\nif os.environ.get(\"COORDINODE_ADDR\"):\n COORDINODE_ADDR = os.environ[\"COORDINODE_ADDR\"]\n from coordinode import CoordinodeClient\n\n client = CoordinodeClient(COORDINODE_ADDR)\n print(f\"Connected to {COORDINODE_ADDR}\")\nelif _port_open(GRPC_PORT):\n COORDINODE_ADDR = f\"localhost:{GRPC_PORT}\"\n from coordinode import CoordinodeClient\n\n client = CoordinodeClient(COORDINODE_ADDR)\n print(f\"Connected to {COORDINODE_ADDR}\")\nelse:\n # No server available — use the embedded in-process engine.\n try:\n from coordinode_embedded import LocalClient\n except ImportError as exc:\n raise RuntimeError(\n \"coordinode-embedded is not installed. \"\n \"Run: pip install git+https://github.com/structured-world/coordinode-python.git@b23c833#subdirectory=coordinode-embedded\"\n \" — or start a CoordiNode server and set COORDINODE_ADDR.\"\n ) from exc\n\n client = LocalClient(\":memory:\")\n print(\"Using embedded LocalClient (in-process)\")"
59
+
"source": [
60
+
"import os, socket\n",
61
+
"\n",
62
+
"\n",
63
+
"def _port_open(port):\n",
64
+
" try:\n",
65
+
" with socket.create_connection((\"127.0.0.1\", port), timeout=1):\n",
"source": "import os, re, uuid\nfrom langchain_core.tools import tool\n\nSESSION = uuid.uuid4().hex[:8] # isolates this demo's data from other sessions\n\n_REL_TYPE_RE = re.compile(r\"[A-Z_][A-Z0-9_]*\")\n# Regex guards for query_facts (demo safety guard).\n_WRITE_CLAUSE_RE = re.compile(\n r\"\\b(CREATE|MERGE|DELETE|DETACH|SET|REMOVE|DROP|CALL|LOAD)\\b\",\n re.IGNORECASE | re.DOTALL,\n)\n# NOTE: this guard checks that AT LEAST ONE node pattern carries session scope.\n# A Cartesian-product query such as `MATCH (n), (m {session: $sess}) RETURN n`\n# would pass yet return unscoped rows for `n`. A complete per-alias check would\n# require parsing the Cypher AST, which is out of scope for a demo safety guard.\n# In production code, use server-side row-level security instead of client regex.\n_SESSION_WHERE_SCOPE_RE = re.compile(\n r\"WHERE\\b[^;{}]*\\.session\\s*=\\s*\\$sess\",\n re.IGNORECASE | re.DOTALL,\n)\n_SESSION_NODE_SCOPE_RE = re.compile(\n r\"\\([^)]*\\{[^}]*session\\s*:\\s*\\$sess[^}]*\\}[^)]*\\)\",\n re.IGNORECASE | re.DOTALL,\n)\n\n\n@tool\ndef save_fact(subject: str, relation: str, obj: str) -> str:\n \"\"\"Save a fact (subject → relation → object) into the knowledge graph.\n Example: save_fact('Alice', 'WORKS_AT', 'Acme Corp')\"\"\"\n rel_type = relation.upper().replace(\" \", \"_\")\n # Validate rel_type before interpolating into Cypher to prevent injection.\n if not _REL_TYPE_RE.fullmatch(rel_type):\n return f\"Invalid relation type {relation!r}: only letters, digits, and underscores allowed\"\n client.cypher(\n f\"MERGE (a:Entity {{name: $s, session: $sess}}) \"\n f\"MERGE (b:Entity {{name: $o, session: $sess}}) \"\n f\"MERGE (a)-[r:{rel_type}]->(b)\",\n params={\"s\": subject, \"o\": obj, \"sess\": SESSION},\n )\n return f\"Saved: {subject} -[{rel_type}]-> {obj}\"\n\n\n@tool\ndef query_facts(cypher: str) -> str:\n \"\"\"Run a read-only Cypher MATCH query against the knowledge graph.\n Must scope reads via either WHERE <alias>.session = $sess\n or a node pattern {session: $sess}.\"\"\"\n q = cypher.strip()\n if _WRITE_CLAUSE_RE.search(q):\n return \"Only read-only Cypher is allowed in query_facts.\"\n # Require $sess in a WHERE clause or node pattern, not just anywhere.\n # Accepts both: WHERE n.session = $sess and MATCH (n {session: $sess})\n if not (_SESSION_WHERE_SCOPE_RE.search(q) or _SESSION_NODE_SCOPE_RE.search(q)):\n return \"Query must scope reads to the current session with either WHERE <alias>.session = $sess or {session: $sess}\"\n rows = client.cypher(q, params={\"sess\": SESSION})\n return str(rows[:20]) if rows else \"No results\"\n\n\n@tool\ndef find_related(entity_name: str, depth: int = 1) -> str:\n \"\"\"Find all entities reachable from entity_name within the given number of hops (max 3).\"\"\"\n safe_depth = max(1, min(int(depth), 3))\n rows = client.cypher(\n f\"MATCH (n:Entity {{name: $name, session: $sess}})-[r*1..{safe_depth}]->(m:Entity {{session: $sess}}) \"\n \"RETURN m.name AS related, type(last(r)) AS via LIMIT 20\",\n params={\"name\": entity_name, \"sess\": SESSION},\n )\n if not rows:\n return f\"No related entities found for {entity_name}\"\n return \"\\n\".join(f\"{r['via']} -> {r['related']}\" for r in rows)\n\n\n@tool\ndef list_all_facts() -> str:\n \"\"\"List every fact stored in the current session's knowledge graph.\"\"\"\n rows = client.cypher(\n \"MATCH (a:Entity {session: $sess})-[r]->(b:Entity {session: $sess}) \"\n \"RETURN a.name AS subject, type(r) AS relation, b.name AS object\",\n params={\"sess\": SESSION},\n )\n if not rows:\n return \"No facts stored yet\"\n return \"\\n\".join(f\"{r['subject']} -[{r['relation']}]-> {r['object']}\" for r in rows)\n\n\ntools = [save_fact, query_facts, find_related, list_all_facts]\nprint(f\"Session: {SESSION}\")\nprint(\"Tools:\", [t.name for t in tools])"
119
+
"source": [
120
+
"import os, re, uuid\n",
121
+
"from langchain_core.tools import tool\n",
122
+
"\n",
123
+
"SESSION = uuid.uuid4().hex[:8] # isolates this demo's data from other sessions\n",
0 commit comments