sessions-graph 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,39 @@
1
+ """Sessions Graph: Cross-session memory store for agents, backed by Memgraph.
2
+
3
+ Quick start::
4
+
5
+ from sessions_graph import SessionsGraph, Memory
6
+
7
+ graph = SessionsGraph()
8
+ graph.setup()
9
+
10
+ mem = graph.save_memory(user_id="alice", content="Prefers Python over TypeScript")
11
+ results = graph.search_memories(user_id="alice", query="Python")
12
+
13
+ Integration with Agent Context Graph::
14
+
15
+ from sessions_graph import SessionsGraph
16
+ from sessions_graph.connector import SessionsGraphConnector
17
+ from agent_context_graph import AgentLink
18
+ from agent_context_graph.adapters.claude import ClaudeAdapter
19
+
20
+ graph = SessionsGraph()
21
+ graph.setup()
22
+
23
+ connector = SessionsGraphConnector(graph)
24
+ link = AgentLink()
25
+ link.add_connector(connector)
26
+ adapter = ClaudeAdapter(link, session_id="s-1", session_kwargs={"user_id": "alice"})
27
+
28
+ # Memory writes and recalls go through the Python API:
29
+ graph.save_memory(
30
+ user_id=connector.active_user_id,
31
+ content="User works in the ai-toolkit repo",
32
+ session_id=connector.active_session_id,
33
+ )
34
+ """
35
+
36
+ from .core import SessionsGraph
37
+ from .models import Memory, MemoryValidationError
38
+
39
+ __all__ = ["Memory", "MemoryValidationError", "SessionsGraph"]
@@ -0,0 +1,117 @@
1
+ """Agent Context Graph connector for SessionsGraph.
2
+
3
+ The connector is intentionally thin: it watches ``SessionStartEvent`` and
4
+ ``SessionEndEvent`` to MERGE ``(:User)`` and ``(:Session)`` nodes so that
5
+ provenance relationships can be wired when memories are saved. Memory writes
6
+ themselves happen through the :class:`SessionsGraph` Python API directly,
7
+ not through the event stream.
8
+
9
+ Usage::
10
+
11
+ from sessions_graph import SessionsGraph
12
+ from sessions_graph.connector import SessionsGraphConnector
13
+ from agent_context_graph import AgentLink
14
+ from agent_context_graph.adapters.claude import ClaudeAdapter
15
+
16
+ graph = SessionsGraph()
17
+ graph.setup()
18
+
19
+ connector = SessionsGraphConnector(graph)
20
+
21
+ link = AgentLink()
22
+ link.add_connector(connector)
23
+ adapter = ClaudeAdapter(link, session_id="s-1", session_kwargs={"user_id": "alice"})
24
+
25
+ # Later, from within the agent:
26
+ graph.save_memory(
27
+ user_id=connector.active_user_id,
28
+ content="User prefers concise answers",
29
+ session_id=connector.active_session_id,
30
+ )
31
+ """
32
+
33
+ from __future__ import annotations
34
+
35
+ from typing import TYPE_CHECKING
36
+
37
+ from agent_context_graph.events import Event, EventType, SessionEndEvent, SessionStartEvent
38
+ from agent_context_graph.protocols import GraphConnector
39
+
40
+ if TYPE_CHECKING:
41
+ from .core import SessionsGraph
42
+
43
+
44
+ _SUPPORTED_EVENTS = {EventType.SESSION_START, EventType.SESSION_END}
45
+
46
+
47
+ class SessionsGraphConnector(GraphConnector):
48
+ """Receives Agent Context Graph session events for memory provenance.
49
+
50
+ On ``SESSION_START``:
51
+ - MERGEs ``(:User {user_id})`` and ``(:Session {session_id})`` nodes.
52
+ - Tracks ``active_user_id`` and ``active_session_id`` for use in API calls.
53
+
54
+ On ``SESSION_END``:
55
+ - Clears the tracked active session context.
56
+
57
+ Args:
58
+ graph: An initialised :class:`SessionsGraph` instance.
59
+ """
60
+
61
+ def __init__(self, graph: SessionsGraph) -> None:
62
+ self._graph = graph
63
+ self._active_user_id: str | None = None
64
+ self._active_session_id: str | None = None
65
+
66
+ # ------------------------------------------------------------------
67
+ # GraphConnector interface
68
+ # ------------------------------------------------------------------
69
+
70
+ def supports(self, event: Event) -> bool:
71
+ return event.event_type in _SUPPORTED_EVENTS
72
+
73
+ def on_event(self, event: Event) -> None:
74
+ if isinstance(event, SessionStartEvent):
75
+ self._on_session_start(event)
76
+ elif isinstance(event, SessionEndEvent):
77
+ self._on_session_end(event)
78
+
79
+ # ------------------------------------------------------------------
80
+ # Active session context (convenience for callers)
81
+ # ------------------------------------------------------------------
82
+
83
+ @property
84
+ def active_user_id(self) -> str | None:
85
+ """The ``user_id`` from the most recent ``SessionStartEvent``, if any."""
86
+ return self._active_user_id
87
+
88
+ @property
89
+ def active_session_id(self) -> str | None:
90
+ """The ``session_id`` from the most recent ``SessionStartEvent``, if any."""
91
+ return self._active_session_id
92
+
93
+ # ------------------------------------------------------------------
94
+ # Internals
95
+ # ------------------------------------------------------------------
96
+
97
+ def _on_session_start(self, event: SessionStartEvent) -> None:
98
+ user_id: str | None = getattr(event, "user_id", None)
99
+
100
+ if user_id:
101
+ self._active_user_id = user_id
102
+ # Ensure the User node exists before any save_memory call
103
+ self._graph._db.query(
104
+ "MERGE (:User {user_id: $user_id});",
105
+ params={"user_id": user_id},
106
+ )
107
+
108
+ self._active_session_id = event.session_id
109
+ # Ensure the Session node exists for provenance wiring
110
+ self._graph._db.query(
111
+ "MERGE (:Session {session_id: $session_id});",
112
+ params={"session_id": event.session_id},
113
+ )
114
+
115
+ def _on_session_end(self, event: SessionEndEvent) -> None:
116
+ self._active_user_id = None
117
+ self._active_session_id = None
sessions_graph/core.py ADDED
@@ -0,0 +1,237 @@
1
+ """Core SessionsGraph class for storing and recalling agent memories in Memgraph.
2
+
3
+ Graph schema
4
+ ------------
5
+ Nodes:
6
+ (:User {user_id})
7
+ (:Memory {memory_id, user_id, content, created_at, session_id?})
8
+ (:Session {session_id})
9
+
10
+ Relationships:
11
+ (:User)-[:HAS_MEMORY]->(:Memory)
12
+ (:Session)-[:PRODUCED_MEMORY]->(:Memory) — only when session_id is provided
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import contextlib
18
+ from typing import Any
19
+
20
+ from memgraph_toolbox.api.memgraph import Memgraph
21
+
22
+ from .models import Memory, validate_content, validate_memory_id, validate_user_id
23
+
24
+ _FULLTEXT_INDEX = "memory_content_index"
25
+
26
+
27
+ class SessionsGraph:
28
+ """Store and recall agent memories in Memgraph.
29
+
30
+ Provides:
31
+ - :meth:`save_memory` — persist a new Memory for a user
32
+ - :meth:`get_memories` — retrieve all Memories for a user
33
+ - :meth:`search_memories` — full-text search over Memory content
34
+ - :meth:`update_memory` — replace the content of an existing Memory
35
+ - :meth:`delete_memory` — remove a Memory by ID
36
+ """
37
+
38
+ def __init__(self, memgraph: Memgraph | None = None, **kwargs: Any) -> None:
39
+ """Initialise SessionsGraph.
40
+
41
+ Args:
42
+ memgraph: An existing Memgraph client instance. When *None* a new
43
+ one is created from *kwargs* / environment variables.
44
+ **kwargs: Forwarded to :class:`Memgraph` when *memgraph* is ``None``.
45
+ """
46
+ self._db = memgraph or Memgraph(**kwargs)
47
+
48
+ # ------------------------------------------------------------------
49
+ # Schema setup
50
+ # ------------------------------------------------------------------
51
+
52
+ def setup(self) -> None:
53
+ """Create constraints, indexes, and the full-text index."""
54
+ self._db.query("CREATE CONSTRAINT ON (u:User) ASSERT u.user_id IS UNIQUE;")
55
+ self._db.query("CREATE CONSTRAINT ON (m:Memory) ASSERT m.memory_id IS UNIQUE;")
56
+ self._db.query("CREATE INDEX ON :Memory(user_id);")
57
+ self._db.query("CREATE INDEX ON :Memory(created_at);")
58
+ self._db.query(f"CREATE TEXT INDEX {_FULLTEXT_INDEX} ON :Memory(content);")
59
+
60
+ def drop(self) -> None:
61
+ """Remove all Memory-related constraints and indexes."""
62
+ with contextlib.suppress(Exception):
63
+ self._db.query("DROP CONSTRAINT ON (u:User) ASSERT u.user_id IS UNIQUE;")
64
+ with contextlib.suppress(Exception):
65
+ self._db.query("DROP CONSTRAINT ON (m:Memory) ASSERT m.memory_id IS UNIQUE;")
66
+ with contextlib.suppress(Exception):
67
+ self._db.query(f"DROP TEXT INDEX {_FULLTEXT_INDEX};")
68
+
69
+ # ------------------------------------------------------------------
70
+ # Write
71
+ # ------------------------------------------------------------------
72
+
73
+ def save_memory(
74
+ self,
75
+ user_id: str,
76
+ content: str,
77
+ *,
78
+ session_id: str | None = None,
79
+ memory_id: str | None = None,
80
+ ) -> Memory:
81
+ """Persist a new Memory for *user_id*.
82
+
83
+ Args:
84
+ user_id: The owning user identity.
85
+ content: The free-form text assertion to store.
86
+ session_id: Optional session that produced this memory (for provenance).
87
+ memory_id: Override the auto-generated UUID (useful in tests).
88
+
89
+ Returns:
90
+ The persisted :class:`Memory` instance.
91
+ """
92
+ memory = Memory(
93
+ user_id=validate_user_id(user_id),
94
+ content=validate_content(content),
95
+ session_id=session_id,
96
+ **({"memory_id": memory_id} if memory_id else {}),
97
+ )
98
+
99
+ # MERGE user, CREATE memory, wire ownership
100
+ self._db.query(
101
+ """
102
+ MERGE (u:User {user_id: $user_id})
103
+ CREATE (m:Memory {
104
+ memory_id: $memory_id,
105
+ user_id: $user_id,
106
+ content: $content,
107
+ created_at: $created_at
108
+ })
109
+ CREATE (u)-[:HAS_MEMORY]->(m)
110
+ """,
111
+ params={
112
+ "user_id": memory.user_id,
113
+ "memory_id": memory.memory_id,
114
+ "content": memory.content,
115
+ "created_at": memory.created_at,
116
+ },
117
+ )
118
+
119
+ # Wire session provenance when a session_id is supplied
120
+ if session_id:
121
+ self._db.query(
122
+ """
123
+ MERGE (s:Session {session_id: $session_id})
124
+ WITH s
125
+ MATCH (m:Memory {memory_id: $memory_id})
126
+ CREATE (s)-[:PRODUCED_MEMORY]->(m)
127
+ """,
128
+ params={"session_id": session_id, "memory_id": memory.memory_id},
129
+ )
130
+
131
+ return memory
132
+
133
+ # ------------------------------------------------------------------
134
+ # Read
135
+ # ------------------------------------------------------------------
136
+
137
+ def get_memories(self, user_id: str) -> list[Memory]:
138
+ """Return all Memories owned by *user_id*, newest first."""
139
+ validate_user_id(user_id)
140
+ rows = self._db.query(
141
+ """
142
+ MATCH (u:User {user_id: $user_id})-[:HAS_MEMORY]->(m:Memory)
143
+ OPTIONAL MATCH (s:Session)-[:PRODUCED_MEMORY]->(m)
144
+ RETURN m.memory_id AS memory_id,
145
+ m.user_id AS user_id,
146
+ m.content AS content,
147
+ m.created_at AS created_at,
148
+ s.session_id AS session_id
149
+ ORDER BY m.created_at DESC
150
+ """,
151
+ params={"user_id": user_id},
152
+ )
153
+ return [self._row_to_memory(r) for r in rows]
154
+
155
+ def search_memories(self, user_id: str, query: str, *, limit: int = 10) -> list[Memory]:
156
+ """Full-text search over Memory content for *user_id*.
157
+
158
+ Args:
159
+ user_id: Only return Memories owned by this user.
160
+ query: Full-text search query string.
161
+ limit: Maximum number of results to return.
162
+
163
+ Returns:
164
+ Matching :class:`Memory` instances ordered by relevance score.
165
+ """
166
+ validate_user_id(user_id)
167
+ if not query or not query.strip():
168
+ return []
169
+
170
+ rows = self._db.query(
171
+ f"""
172
+ CALL text_search.search_all('{_FULLTEXT_INDEX}', $query)
173
+ YIELD node AS m, score
174
+ WHERE m.user_id = $user_id
175
+ OPTIONAL MATCH (s:Session)-[:PRODUCED_MEMORY]->(m)
176
+ RETURN m.memory_id AS memory_id,
177
+ m.user_id AS user_id,
178
+ m.content AS content,
179
+ m.created_at AS created_at,
180
+ s.session_id AS session_id
181
+ ORDER BY score DESC
182
+ LIMIT {int(limit)}
183
+ """,
184
+ params={"user_id": user_id, "query": query.strip()},
185
+ )
186
+ return [self._row_to_memory(r) for r in rows]
187
+
188
+ # ------------------------------------------------------------------
189
+ # Update / Delete
190
+ # ------------------------------------------------------------------
191
+
192
+ def update_memory(self, memory_id: str, content: str) -> Memory | None:
193
+ """Replace the content of an existing Memory.
194
+
195
+ Returns the updated :class:`Memory`, or ``None`` if not found.
196
+ """
197
+ validate_memory_id(memory_id)
198
+ validate_content(content)
199
+
200
+ rows = self._db.query(
201
+ """
202
+ MATCH (m:Memory {memory_id: $memory_id})
203
+ SET m.content = $content
204
+ OPTIONAL MATCH (s:Session)-[:PRODUCED_MEMORY]->(m)
205
+ RETURN m.memory_id AS memory_id,
206
+ m.user_id AS user_id,
207
+ m.content AS content,
208
+ m.created_at AS created_at,
209
+ s.session_id AS session_id
210
+ """,
211
+ params={"memory_id": memory_id, "content": content},
212
+ )
213
+ if not rows:
214
+ return None
215
+ return self._row_to_memory(rows[0])
216
+
217
+ def delete_memory(self, memory_id: str) -> None:
218
+ """Remove a Memory and all its relationships by ID."""
219
+ validate_memory_id(memory_id)
220
+ self._db.query(
221
+ "MATCH (m:Memory {memory_id: $memory_id}) DETACH DELETE m;",
222
+ params={"memory_id": memory_id},
223
+ )
224
+
225
+ # ------------------------------------------------------------------
226
+ # Helpers
227
+ # ------------------------------------------------------------------
228
+
229
+ @staticmethod
230
+ def _row_to_memory(row: dict) -> Memory:
231
+ return Memory(
232
+ memory_id=row["memory_id"],
233
+ user_id=row["user_id"],
234
+ content=row["content"],
235
+ created_at=row["created_at"],
236
+ session_id=row.get("session_id"),
237
+ )
@@ -0,0 +1,69 @@
1
+ """Data models for Sessions Graph.
2
+
3
+ Defines the core data structure for a Memory — a cross-session-durable
4
+ free-form text assertion written explicitly by an agent.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import re
10
+ from dataclasses import dataclass, field
11
+ from datetime import datetime, timezone
12
+ from uuid import uuid4
13
+
14
+ _USER_ID_RE = re.compile(r"^[a-zA-Z0-9_@.\-]{1,256}$")
15
+ _MEMORY_ID_RE = re.compile(r"^[a-zA-Z0-9_-]{1,128}$")
16
+
17
+
18
+ def _utc_now() -> str:
19
+ return datetime.now(timezone.utc).isoformat()
20
+
21
+
22
+ def _generate_id() -> str:
23
+ return str(uuid4())
24
+
25
+
26
+ class MemoryValidationError(ValueError):
27
+ """Raised when a Memory field violates validation rules."""
28
+
29
+
30
+ def validate_user_id(user_id: str) -> str:
31
+ if not user_id or not _USER_ID_RE.match(user_id):
32
+ raise MemoryValidationError(f"user_id must match pattern {_USER_ID_RE.pattern!r}, got: {user_id!r}")
33
+ return user_id
34
+
35
+
36
+ def validate_memory_id(memory_id: str) -> str:
37
+ if not memory_id or not _MEMORY_ID_RE.match(memory_id):
38
+ raise MemoryValidationError(f"memory_id must match pattern {_MEMORY_ID_RE.pattern!r}, got: {memory_id!r}")
39
+ return memory_id
40
+
41
+
42
+ def validate_content(content: str) -> str:
43
+ if not content or not content.strip():
44
+ raise MemoryValidationError("content must be a non-empty string")
45
+ return content
46
+
47
+
48
+ @dataclass
49
+ class Memory:
50
+ """A cross-session-durable free-form text assertion owned by a user.
51
+
52
+ Attributes:
53
+ user_id: The identity of the user this memory belongs to.
54
+ content: The free-form text assertion.
55
+ memory_id: Unique identifier (auto-generated UUID by default).
56
+ created_at: ISO-format UTC timestamp of when the memory was written.
57
+ session_id: The session that produced this memory (optional provenance).
58
+ """
59
+
60
+ user_id: str
61
+ content: str
62
+ memory_id: str = field(default_factory=_generate_id)
63
+ created_at: str = field(default_factory=_utc_now)
64
+ session_id: str | None = None
65
+
66
+ def __post_init__(self) -> None:
67
+ validate_user_id(self.user_id)
68
+ validate_memory_id(self.memory_id)
69
+ validate_content(self.content)
@@ -0,0 +1,135 @@
1
+ Metadata-Version: 2.4
2
+ Name: sessions-graph
3
+ Version: 0.1.0
4
+ Summary: Cross-session memory store for agents, backed by Memgraph
5
+ License: MIT
6
+ Classifier: Programming Language :: Python :: 3.10
7
+ Classifier: Programming Language :: Python :: 3.11
8
+ Classifier: Programming Language :: Python :: 3.12
9
+ Classifier: Programming Language :: Python :: 3.13
10
+ Requires-Python: >=3.10
11
+ Requires-Dist: memgraph-toolbox
12
+ Provides-Extra: agent-context-graph
13
+ Requires-Dist: agent-context-graph>=0.1.2; extra == 'agent-context-graph'
14
+ Provides-Extra: test
15
+ Requires-Dist: pytest>=9.0.3; extra == 'test'
16
+ Description-Content-Type: text/markdown
17
+
18
+ # Sessions Graph
19
+
20
+ Sessions Graph is the [Context Graph](../CONTEXT-MAP.md) component for session context and cross-session recall. It is the **authority on `(:Session)` nodes** in the Context Graph family. It stores free-form text assertions — called **Memories** — written explicitly by agents, and makes them searchable in future sessions.
21
+
22
+ Requires **Memgraph ≥ 3.6** (text search is stable from that release).
23
+
24
+ ## Installation
25
+
26
+ ```bash
27
+ pip install sessions-graph
28
+ ```
29
+
30
+ To use with Agent Context Graph:
31
+
32
+ ```bash
33
+ pip install sessions-graph[agent-context-graph]
34
+ ```
35
+
36
+ ## Quick start
37
+
38
+ ```python
39
+ from sessions_graph import SessionsGraph
40
+
41
+ graph = SessionsGraph() # connects via MEMGRAPH_HOST / MEMGRAPH_PORT env vars
42
+ graph.setup() # creates constraints and the text index (run once)
43
+
44
+ # Write a memory
45
+ mem = graph.save_memory(
46
+ user_id="alice",
47
+ content="Prefers Python over TypeScript",
48
+ session_id="s-abc123", # optional — links memory to a session for provenance
49
+ )
50
+
51
+ # Retrieve all memories for a user
52
+ memories = graph.get_memories("alice")
53
+
54
+ # Search memories by content (full-text, powered by Tantivy)
55
+ results = graph.search_memories("alice", "Python")
56
+
57
+ # Update or delete
58
+ graph.update_memory(mem.memory_id, "Prefers Python, especially for data tooling")
59
+ graph.delete_memory(mem.memory_id)
60
+ ```
61
+
62
+ ## Integration with Agent Context Graph
63
+
64
+ Wire the `SessionsGraphConnector` into an `AgentLink` to get automatic session provenance — the connector tracks the active `session_id` and `user_id` from `SessionStartEvent` so you can reference them when saving memories.
65
+
66
+ ```python
67
+ from sessions_graph import SessionsGraph
68
+ from sessions_graph.connector import SessionsGraphConnector
69
+ from agent_context_graph import AgentLink
70
+ from agent_context_graph.adapters.claude import ClaudeAdapter
71
+
72
+ graph = SessionsGraph()
73
+ graph.setup()
74
+
75
+ connector = SessionsGraphConnector(graph)
76
+ link = AgentLink()
77
+ link.add_connector(connector)
78
+
79
+ adapter = ClaudeAdapter(
80
+ link,
81
+ session_id="s-abc123",
82
+ session_kwargs={"user_id": "alice"},
83
+ )
84
+
85
+ # During the session, save memories via the Python API:
86
+ graph.save_memory(
87
+ user_id=connector.active_user_id,
88
+ content="User works primarily in the ai-toolkit repository",
89
+ session_id=connector.active_session_id,
90
+ )
91
+ ```
92
+
93
+ ## Graph schema
94
+
95
+ ```
96
+ (:User {user_id})
97
+ └─[:HAS_MEMORY]─▶ (:Memory {memory_id, user_id, content, created_at})
98
+ ▲
99
+ [:PRODUCED_MEMORY]
100
+ │
101
+ (:Session {session_id}) ← shared idempotent coordination point
102
+ ```
103
+
104
+ ## Text search
105
+
106
+ Sessions Graph uses [Memgraph text search](https://memgraph.com/docs/querying/text-search) (powered by Tantivy) for `search_memories`. The text index is created on `setup()`:
107
+
108
+ ```cypher
109
+ CREATE TEXT INDEX memory_content_index ON :Memory(content);
110
+ ```
111
+
112
+ Searches run as:
113
+
114
+ ```cypher
115
+ CALL text_search.search_all('memory_content_index', 'Python')
116
+ YIELD node AS m, score
117
+ WHERE m.user_id = 'alice'
118
+ RETURN m.content, score
119
+ ORDER BY score DESC
120
+ LIMIT 10;
121
+ ```
122
+
123
+ The query string follows [Tantivy query syntax](https://docs.rs/tantivy/latest/tantivy/query/struct.QueryParser.html).
124
+
125
+ ## API reference
126
+
127
+ | Method | Description |
128
+ |---|---|
129
+ | `setup()` | Create constraints and text index. Run once on first use. |
130
+ | `drop()` | Remove all Memory-related constraints and indexes. |
131
+ | `save_memory(user_id, content, *, session_id, memory_id)` | Persist a new Memory. Returns the stored `Memory` object. |
132
+ | `get_memories(user_id)` | Return all Memories for a user, newest first. |
133
+ | `search_memories(user_id, query, *, limit=10)` | Full-text search over Memory content. |
134
+ | `update_memory(memory_id, content)` | Replace the content of an existing Memory. Returns `None` if not found. |
135
+ | `delete_memory(memory_id)` | Remove a Memory and all its relationships. |
@@ -0,0 +1,7 @@
1
+ sessions_graph/__init__.py,sha256=w45gncXgA_L7fqggHLH7VgK70mvnAinqK-XFVATFJXk,1236
2
+ sessions_graph/connector.py,sha256=67h-lRqt0-egJFw3Y61KSjrNbsLj3Iq8lXaInXZeN5k,4108
3
+ sessions_graph/core.py,sha256=DI3c9IcyfefupZ5vgAATq-5DniU_yRebbv5HxNgi_64,8647
4
+ sessions_graph/models.py,sha256=lzUAwnRcQ-smFer0f-fbyM8VURI0fsbZZswbZ_0za2k,2132
5
+ sessions_graph-0.1.0.dist-info/METADATA,sha256=q5e2uln6bhLENvZrFcRhDZNDFtOleLFrEZdkQF44kgc,4463
6
+ sessions_graph-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
7
+ sessions_graph-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any