Skip to content

Commit 379c8d4

Browse files
committed
feat: Python SDK — zero-dependency yaad client for pip install
sdk/python/yaad/__init__.py (218 lines): - Yaad class wrapping all REST API endpoints - Node, Edge, RecallResult dataclasses - remember, recall, context, forget, link, subgraph, impact - session_start, session_end, health, stats - approve, edit, discard (feedback) - compact, mental_model (advanced) - Zero dependencies (stdlib only: urllib, json, dataclasses) sdk/python/pyproject.toml: - pip install yaad - Python 3.10+ - MIT license This closes the last gap vs top 20 OSS projects. Yaad now has both TypeScript and Python SDKs.
1 parent b06daeb commit 379c8d4

3 files changed

Lines changed: 280 additions & 0 deletions

File tree

sdk/python/README.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# yaad — Python SDK
2+
3+
Persistent memory for coding agents. Zero dependencies.
4+
5+
```bash
6+
pip install yaad
7+
```
8+
9+
```python
10+
from yaad import Yaad
11+
12+
y = Yaad() # connects to yaad server on localhost:3456
13+
14+
# Store a memory
15+
y.remember(content="Use jose for JWT", type="convention")
16+
17+
# Search memories (graph-aware)
18+
results = y.recall("auth middleware")
19+
for node in results.nodes:
20+
print(f"[{node.type}] {node.content}")
21+
22+
# Get session context
23+
ctx = y.context()
24+
25+
# Impact analysis
26+
affected = y.impact("src/auth.ts")
27+
28+
# Mental model
29+
model = y.mental_model()
30+
```
31+
32+
Requires the `yaad` binary running: `yaad serve`
33+
34+
Docs: https://github.com/GrayCodeAI/yaad

sdk/python/pyproject.toml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
[build-system]
2+
requires = ["setuptools>=68.0"]
3+
build-backend = "setuptools.build_meta"
4+
5+
[project]
6+
name = "yaad"
7+
version = "0.1.0"
8+
description = "Python SDK for Yaad — persistent memory for coding agents"
9+
readme = "README.md"
10+
license = {text = "MIT"}
11+
requires-python = ">=3.10"
12+
authors = [{name = "GrayCodeAI", email = "hello@graycode.ai"}]
13+
keywords = ["yaad", "memory", "coding-agent", "mcp", "graph", "ai"]
14+
classifiers = [
15+
"Development Status :: 4 - Beta",
16+
"Intended Audience :: Developers",
17+
"License :: OSI Approved :: MIT License",
18+
"Programming Language :: Python :: 3",
19+
"Topic :: Software Development :: Libraries",
20+
]
21+
22+
[project.urls]
23+
Homepage = "https://github.com/GrayCodeAI/yaad"
24+
Repository = "https://github.com/GrayCodeAI/yaad"
25+
Issues = "https://github.com/GrayCodeAI/yaad/issues"
26+
27+
[tool.setuptools.packages.find]
28+
include = ["yaad*"]

sdk/python/yaad/__init__.py

Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
"""
2+
Yaad Python SDK — persistent memory for coding agents.
3+
4+
Usage:
5+
from yaad import Yaad
6+
7+
y = Yaad()
8+
y.remember(content="Use jose for JWT", type="convention")
9+
results = y.recall("auth middleware")
10+
context = y.context()
11+
"""
12+
13+
from __future__ import annotations
14+
15+
import json
16+
from dataclasses import dataclass, field
17+
from typing import Any, Optional
18+
from urllib.request import Request, urlopen
19+
from urllib.error import URLError
20+
21+
22+
@dataclass
23+
class Node:
24+
id: str = ""
25+
type: str = ""
26+
content: str = ""
27+
summary: str = ""
28+
scope: str = ""
29+
project: str = ""
30+
tier: int = 0
31+
tags: str = ""
32+
confidence: float = 0.0
33+
access_count: int = 0
34+
35+
36+
@dataclass
37+
class Edge:
38+
id: str = ""
39+
from_id: str = ""
40+
to_id: str = ""
41+
type: str = ""
42+
weight: float = 0.0
43+
44+
45+
@dataclass
46+
class RecallResult:
47+
nodes: list[Node] = field(default_factory=list)
48+
edges: list[Edge] = field(default_factory=list)
49+
50+
51+
class YaadError(Exception):
52+
pass
53+
54+
55+
class Yaad:
56+
"""Client for the Yaad memory layer REST API."""
57+
58+
def __init__(self, base_url: str = "http://127.0.0.1:3456"):
59+
self.base_url = base_url.rstrip("/")
60+
61+
# --- Core Memory ---
62+
63+
def remember(
64+
self,
65+
content: str,
66+
type: str = "decision",
67+
summary: str = "",
68+
tags: str = "",
69+
project: str = "",
70+
scope: str = "project",
71+
session: str = "",
72+
agent: str = "",
73+
) -> Node:
74+
"""Store a memory node."""
75+
data = self._post("/yaad/remember", {
76+
"content": content, "type": type, "summary": summary,
77+
"tags": tags, "project": project, "scope": scope,
78+
"session": session, "agent": agent,
79+
})
80+
return self._to_node(data)
81+
82+
def recall(
83+
self,
84+
query: str,
85+
depth: int = 2,
86+
limit: int = 10,
87+
type: str = "",
88+
project: str = "",
89+
) -> RecallResult:
90+
"""Graph-aware search: BM25 + vector + graph + temporal."""
91+
data = self._post("/yaad/recall", {
92+
"query": query, "depth": depth, "limit": limit,
93+
"type": type, "project": project,
94+
})
95+
return self._to_recall_result(data)
96+
97+
def context(self, project: str = "") -> RecallResult:
98+
"""Get hot-tier context for session start (~2K tokens)."""
99+
params = f"?project={project}" if project else ""
100+
data = self._get(f"/yaad/context{params}")
101+
return self._to_recall_result(data)
102+
103+
def forget(self, id: str) -> None:
104+
"""Archive a memory node."""
105+
self._delete(f"/yaad/forget/{id}")
106+
107+
# --- Graph ---
108+
109+
def link(self, from_id: str, to_id: str, type: str) -> Edge:
110+
"""Create an edge between two nodes."""
111+
data = self._post("/yaad/link", {
112+
"from_id": from_id, "to_id": to_id, "type": type,
113+
})
114+
return Edge(**{k: data.get(k, "") for k in Edge.__dataclass_fields__})
115+
116+
def subgraph(self, id: str, depth: int = 2) -> RecallResult:
117+
"""Get BFS subgraph around a node."""
118+
data = self._get(f"/yaad/subgraph/{id}?depth={depth}")
119+
return self._to_recall_result(data)
120+
121+
def impact(self, file: str) -> list[Node]:
122+
"""Impact analysis: what memories are affected if this file changes?"""
123+
data = self._get(f"/yaad/impact/{file}")
124+
if isinstance(data, list):
125+
return [self._to_node(n) for n in data]
126+
return []
127+
128+
# --- Sessions ---
129+
130+
def session_start(self, project: str = "", agent: str = "") -> dict:
131+
"""Start a session and get context."""
132+
return self._post("/yaad/session/start", {"project": project, "agent": agent})
133+
134+
def session_end(self, id: str, summary: str = "") -> None:
135+
"""End a session."""
136+
self._post("/yaad/session/end", {"id": id, "summary": summary})
137+
138+
# --- System ---
139+
140+
def health(self) -> dict:
141+
"""Health check."""
142+
return self._get("/yaad/health")
143+
144+
def stats(self, project: str = "") -> dict:
145+
"""Graph statistics."""
146+
params = f"?project={project}" if project else ""
147+
return self._get(f"/yaad/graph/stats{params}")
148+
149+
# --- Feedback ---
150+
151+
def approve(self, id: str) -> None:
152+
self._post("/yaad/feedback", {"id": id, "action": "approve"})
153+
154+
def edit(self, id: str, new_content: str) -> None:
155+
self._post("/yaad/feedback", {"id": id, "action": "edit", "new_content": new_content})
156+
157+
def discard(self, id: str) -> None:
158+
self._post("/yaad/feedback", {"id": id, "action": "discard"})
159+
160+
# --- Advanced ---
161+
162+
def compact(self, project: str = "") -> dict:
163+
"""Compact low-confidence memories."""
164+
return self._post(f"/yaad/compact?project={project}", {})
165+
166+
def mental_model(self, project: str = "") -> dict:
167+
"""Get auto-generated project mental model."""
168+
params = f"?project={project}" if project else ""
169+
return self._get(f"/yaad/mental-model{params}")
170+
171+
def intent(self, query: str) -> dict:
172+
"""Classify query intent (Why/When/Who/How/What)."""
173+
return self._post("/yaad/recall", {"query": query, "limit": 0})
174+
175+
# --- HTTP helpers ---
176+
177+
def _get(self, path: str) -> Any:
178+
try:
179+
req = Request(f"{self.base_url}{path}")
180+
with urlopen(req, timeout=10) as resp:
181+
return json.loads(resp.read())
182+
except URLError as e:
183+
raise YaadError(f"GET {path}: {e}") from e
184+
185+
def _post(self, path: str, body: dict) -> Any:
186+
try:
187+
data = json.dumps(body).encode()
188+
req = Request(f"{self.base_url}{path}", data=data, method="POST")
189+
req.add_header("Content-Type", "application/json")
190+
with urlopen(req, timeout=10) as resp:
191+
return json.loads(resp.read())
192+
except URLError as e:
193+
raise YaadError(f"POST {path}: {e}") from e
194+
195+
def _delete(self, path: str) -> None:
196+
try:
197+
req = Request(f"{self.base_url}{path}", method="DELETE")
198+
urlopen(req, timeout=10)
199+
except URLError as e:
200+
raise YaadError(f"DELETE {path}: {e}") from e
201+
202+
# --- Converters ---
203+
204+
@staticmethod
205+
def _to_node(data: dict) -> Node:
206+
if not data:
207+
return Node()
208+
return Node(**{k: data.get(k, Node.__dataclass_fields__[k].default)
209+
for k in Node.__dataclass_fields__})
210+
211+
@staticmethod
212+
def _to_recall_result(data: dict) -> RecallResult:
213+
if not data:
214+
return RecallResult()
215+
nodes = [Yaad._to_node(n) for n in (data.get("nodes") or [])]
216+
edges = [Edge(**{k: e.get(k, "") for k in Edge.__dataclass_fields__})
217+
for e in (data.get("edges") or [])]
218+
return RecallResult(nodes=nodes, edges=edges)

0 commit comments

Comments
 (0)