breeth 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.
breeth/__init__.py ADDED
@@ -0,0 +1,73 @@
1
+ """Breeth: the official Python SDK for the Breeth memory API.
2
+
3
+ from breeth import BreethClient
4
+
5
+ breeth = BreethClient(api_key="ck_live_...")
6
+ breeth.write("Recruiter prefers candidates with prior HR-tech roles.")
7
+ results = breeth.retrieve("HR-tech background")
8
+ """
9
+ from __future__ import annotations
10
+
11
+ from .client import AsyncBreethClient, BreethClient
12
+ from .errors import BreethError
13
+ from .types import (
14
+ CacheStats,
15
+ CogramTaskEnvelope,
16
+ DirectorProfileBlock,
17
+ EdgeHit,
18
+ EntityEdgeRow,
19
+ EntityEpisodeRow,
20
+ EntityMode,
21
+ EntityNarrativeRow,
22
+ EntityResponse,
23
+ EpisodeMentionRow,
24
+ ExtractedCounts,
25
+ GraphEdgeListResponse,
26
+ GraphEdgeRow,
27
+ GraphEntityListResponse,
28
+ GraphEntityRow,
29
+ GraphEpisodeListResponse,
30
+ GraphEpisodeRow,
31
+ GroupRow,
32
+ GroupsListResponse,
33
+ IntentSuggestion,
34
+ NeighborRow,
35
+ NodeDetailsResponse,
36
+ PatternEvidence,
37
+ RetrieveResponse,
38
+ WriteResponse,
39
+ )
40
+
41
+ __version__ = "0.1.0"
42
+
43
+ __all__ = [
44
+ "AsyncBreethClient",
45
+ "BreethClient",
46
+ "BreethError",
47
+ "CacheStats",
48
+ "CogramTaskEnvelope",
49
+ "DirectorProfileBlock",
50
+ "EdgeHit",
51
+ "EntityEdgeRow",
52
+ "EntityEpisodeRow",
53
+ "EntityMode",
54
+ "EntityNarrativeRow",
55
+ "EntityResponse",
56
+ "EpisodeMentionRow",
57
+ "ExtractedCounts",
58
+ "GraphEdgeListResponse",
59
+ "GraphEdgeRow",
60
+ "GraphEntityListResponse",
61
+ "GraphEntityRow",
62
+ "GraphEpisodeListResponse",
63
+ "GraphEpisodeRow",
64
+ "GroupRow",
65
+ "GroupsListResponse",
66
+ "IntentSuggestion",
67
+ "NeighborRow",
68
+ "NodeDetailsResponse",
69
+ "PatternEvidence",
70
+ "RetrieveResponse",
71
+ "WriteResponse",
72
+ "__version__",
73
+ ]
breeth/_base.py ADDED
@@ -0,0 +1,103 @@
1
+ """Shared base utilities for the sync and async Breeth clients.
2
+
3
+ Both `BreethClient` and `AsyncBreethClient` are thin wrappers around
4
+ ``httpx.Client`` / ``httpx.AsyncClient``. This module centralises:
5
+
6
+ * Base URL resolution (env var ``COGRAM_API_URL`` overrides the default).
7
+ * Authorization header construction.
8
+ * Optional ``X-End-User-Id`` passthrough for multi-end-user apps.
9
+ * HTTP error to ``BreethError`` mapping.
10
+
11
+ Note: ``X-Cogram-Team-Id`` is intentionally NOT set. The server resolves
12
+ the team from the ``ck_live_`` API key.
13
+ """
14
+ from __future__ import annotations
15
+
16
+ import os
17
+ from typing import Any, Mapping, Optional
18
+
19
+ import httpx
20
+
21
+ from .errors import BreethError
22
+
23
+
24
+ DEFAULT_BASE_URL = "https://api.thebreeth.com"
25
+ API_PREFIX = "/v1"
26
+ DEFAULT_TIMEOUT = 30.0
27
+ USER_AGENT = "breeth-python/0.1.0"
28
+
29
+
30
+ def resolve_base_url(explicit: Optional[str]) -> str:
31
+ """Pick the base URL in order: explicit arg, ``COGRAM_API_URL``,
32
+ SDK default. Trailing slashes are stripped so URL joining is
33
+ predictable."""
34
+ raw = explicit or os.environ.get("COGRAM_API_URL") or DEFAULT_BASE_URL
35
+ return raw.rstrip("/")
36
+
37
+
38
+ def resolve_api_key(explicit: Optional[str]) -> str:
39
+ key = explicit or os.environ.get("BREETH_API_KEY") or os.environ.get("COGRAM_API_KEY")
40
+ if not key:
41
+ raise BreethError(
42
+ "Missing API key. Pass api_key=... or set BREETH_API_KEY.",
43
+ status=0,
44
+ slug="missing_api_key",
45
+ )
46
+ return key
47
+
48
+
49
+ def build_headers(api_key: str, end_user_id: Optional[str]) -> dict[str, str]:
50
+ headers = {
51
+ "Authorization": f"Bearer {api_key}",
52
+ "User-Agent": USER_AGENT,
53
+ "Accept": "application/json",
54
+ }
55
+ if end_user_id:
56
+ headers["X-End-User-Id"] = end_user_id
57
+ return headers
58
+
59
+
60
+ def build_url(base_url: str, path: str) -> str:
61
+ """Join the base URL, the /v1 prefix, and a route path. The path
62
+ can be supplied with or without a leading slash."""
63
+ suffix = path if path.startswith("/") else f"/{path}"
64
+ return f"{base_url}{API_PREFIX}{suffix}"
65
+
66
+
67
+ def raise_for_status(response: httpx.Response) -> None:
68
+ """Map any 4xx / 5xx response to a ``BreethError``.
69
+
70
+ The API returns JSON shaped like
71
+ ``{"detail": {"error": "<slug>", "message": "..."}}`` for handled
72
+ errors, and FastAPI's default ``{"detail": "..."}`` for unhandled
73
+ validation issues. We accept both and extract whatever we can.
74
+ """
75
+ if response.is_success:
76
+ return
77
+
78
+ slug: Optional[str] = None
79
+ message: str = response.reason_phrase or "Request failed"
80
+ body: Any = None
81
+ try:
82
+ body = response.json()
83
+ except Exception:
84
+ body = response.text or None
85
+
86
+ if isinstance(body, dict):
87
+ detail = body.get("detail", body)
88
+ if isinstance(detail, dict):
89
+ slug = detail.get("error") or detail.get("slug") or slug
90
+ message = detail.get("message") or detail.get("detail") or message
91
+ elif isinstance(detail, str):
92
+ message = detail
93
+
94
+ raise BreethError(message, status=response.status_code, slug=slug, body=body)
95
+
96
+
97
+ def merge_query(params: Optional[Mapping[str, Any]]) -> Optional[dict[str, Any]]:
98
+ """Drop None-valued query parameters so callers can pass them
99
+ unconditionally without polluting the URL."""
100
+ if not params:
101
+ return None
102
+ out = {k: v for k, v in params.items() if v is not None}
103
+ return out or None
breeth/client.py ADDED
@@ -0,0 +1,369 @@
1
+ """Sync and async clients for the Breeth API.
2
+
3
+ Usage:
4
+
5
+ from breeth import BreethClient
6
+
7
+ with BreethClient(api_key="ck_live_...") as breeth:
8
+ breeth.write("Recruiter prefers candidates with HR-tech experience.")
9
+ hits = breeth.retrieve("HR-tech experience")
10
+
11
+ # Async
12
+ from breeth import AsyncBreethClient
13
+
14
+ async with AsyncBreethClient(api_key="ck_live_...") as breeth:
15
+ await breeth.write("...")
16
+ """
17
+ from __future__ import annotations
18
+
19
+ from typing import Any, Optional
20
+
21
+ import httpx
22
+
23
+ from ._base import (
24
+ DEFAULT_TIMEOUT,
25
+ build_headers,
26
+ build_url,
27
+ merge_query,
28
+ raise_for_status,
29
+ resolve_api_key,
30
+ resolve_base_url,
31
+ )
32
+ from .types import (
33
+ EntityMode,
34
+ EntityResponse,
35
+ GraphEdgeListResponse,
36
+ GraphEntityListResponse,
37
+ GraphEpisodeListResponse,
38
+ GroupsListResponse,
39
+ NodeDetailsResponse,
40
+ RetrieveResponse,
41
+ WriteResponse,
42
+ )
43
+
44
+
45
+ # ---------------------------------------------------------------------------
46
+ # Sync client
47
+ # ---------------------------------------------------------------------------
48
+
49
+ class _GraphNamespace:
50
+ """Nested namespace exposed as ``client.graph``. Reuses the parent
51
+ client's httpx.Client so we don't open extra connections."""
52
+
53
+ def __init__(self, client: "BreethClient") -> None:
54
+ self._client = client
55
+
56
+ def node_details(self, identifier: str) -> NodeDetailsResponse:
57
+ data = self._client._get(f"/graph/nodes/{identifier}/details")
58
+ return NodeDetailsResponse.model_validate(data)
59
+
60
+ def list_entities(
61
+ self,
62
+ *,
63
+ query: Optional[str] = None,
64
+ limit: Optional[int] = None,
65
+ offset: Optional[int] = None,
66
+ ) -> GraphEntityListResponse:
67
+ params = merge_query({"query": query, "limit": limit, "offset": offset})
68
+ data = self._client._get("/graph/entities", params=params)
69
+ return GraphEntityListResponse.model_validate(data)
70
+
71
+ def list_edges(
72
+ self,
73
+ *,
74
+ query: Optional[str] = None,
75
+ limit: Optional[int] = None,
76
+ offset: Optional[int] = None,
77
+ ) -> GraphEdgeListResponse:
78
+ params = merge_query({"query": query, "limit": limit, "offset": offset})
79
+ data = self._client._get("/graph/edges", params=params)
80
+ return GraphEdgeListResponse.model_validate(data)
81
+
82
+ def list_episodes(
83
+ self,
84
+ *,
85
+ query: Optional[str] = None,
86
+ limit: Optional[int] = None,
87
+ offset: Optional[int] = None,
88
+ ) -> GraphEpisodeListResponse:
89
+ params = merge_query({"query": query, "limit": limit, "offset": offset})
90
+ data = self._client._get("/graph/episodes", params=params)
91
+ return GraphEpisodeListResponse.model_validate(data)
92
+
93
+
94
+ class BreethClient:
95
+ """Synchronous client for the Breeth API.
96
+
97
+ Args:
98
+ api_key: ``ck_live_...`` token. Falls back to ``BREETH_API_KEY``
99
+ then ``COGRAM_API_KEY``.
100
+ base_url: Override the API host. Falls back to ``COGRAM_API_URL``
101
+ then the SDK default (``https://api.thebreeth.com``).
102
+ end_user_id: Optional ``X-End-User-Id`` header forwarded on
103
+ every request. Useful when one API key fronts many end users.
104
+ timeout: httpx timeout in seconds.
105
+ http_client: Inject a pre-configured ``httpx.Client`` (for
106
+ advanced transport, proxies, custom TLS, etc.). When supplied
107
+ the SDK does not close it on ``__exit__``.
108
+ """
109
+
110
+ def __init__(
111
+ self,
112
+ api_key: Optional[str] = None,
113
+ *,
114
+ base_url: Optional[str] = None,
115
+ end_user_id: Optional[str] = None,
116
+ timeout: float = DEFAULT_TIMEOUT,
117
+ http_client: Optional[httpx.Client] = None,
118
+ ) -> None:
119
+ self._api_key = resolve_api_key(api_key)
120
+ self._base_url = resolve_base_url(base_url)
121
+ self._end_user_id = end_user_id
122
+ self._headers = build_headers(self._api_key, end_user_id)
123
+ self._owns_http = http_client is None
124
+ self._http = http_client or httpx.Client(timeout=timeout)
125
+ self.graph = _GraphNamespace(self)
126
+
127
+ # ---- context manager ------------------------------------------------
128
+
129
+ def __enter__(self) -> "BreethClient":
130
+ return self
131
+
132
+ def __exit__(self, exc_type, exc, tb) -> None:
133
+ self.close()
134
+
135
+ def close(self) -> None:
136
+ if self._owns_http:
137
+ self._http.close()
138
+
139
+ # ---- low-level helpers ---------------------------------------------
140
+
141
+ def _get(self, path: str, *, params: Optional[dict[str, Any]] = None) -> Any:
142
+ response = self._http.get(
143
+ build_url(self._base_url, path),
144
+ headers=self._headers,
145
+ params=params,
146
+ )
147
+ raise_for_status(response)
148
+ return response.json()
149
+
150
+ def _post(self, path: str, *, json: dict[str, Any]) -> Any:
151
+ response = self._http.post(
152
+ build_url(self._base_url, path),
153
+ headers=self._headers,
154
+ json=json,
155
+ )
156
+ raise_for_status(response)
157
+ return response.json()
158
+
159
+ # ---- public methods ------------------------------------------------
160
+
161
+ def write(
162
+ self,
163
+ content: str,
164
+ *,
165
+ group_id: str = "default",
166
+ source_description: str = "api",
167
+ extract_intent: bool = False,
168
+ ) -> WriteResponse:
169
+ """Ingest a prose episode. The graph pipeline runs in the
170
+ background server-side; this returns once the episode is
171
+ persisted."""
172
+ payload = {
173
+ "content": content,
174
+ "group_id": group_id,
175
+ "source_description": source_description,
176
+ "extract_intent": extract_intent,
177
+ }
178
+ data = self._post("/episodes", json=payload)
179
+ return WriteResponse.model_validate(data)
180
+
181
+ def retrieve(
182
+ self,
183
+ query: str,
184
+ *,
185
+ group_id: str = "default",
186
+ limit: int = 10,
187
+ ) -> RetrieveResponse:
188
+ """Hybrid semantic + graph search. Returns edges plus, on
189
+ first-person queries, the director profile block."""
190
+ payload = {"query": query, "group_id": group_id, "limit": limit}
191
+ data = self._post("/search", json=payload)
192
+ return RetrieveResponse.model_validate(data)
193
+
194
+ def entity(
195
+ self,
196
+ name: str,
197
+ *,
198
+ mode: Optional[EntityMode] = None,
199
+ limit: Optional[int] = None,
200
+ ) -> EntityResponse:
201
+ """Look up an entity by name substring. ``mode`` selects which
202
+ view to return (``narrative`` is the server default)."""
203
+ params = merge_query({"mode": mode, "limit": limit})
204
+ data = self._get(f"/entities/{name}", params=params)
205
+ return EntityResponse.model_validate(data)
206
+
207
+ def groups(
208
+ self,
209
+ *,
210
+ query: Optional[str] = None,
211
+ limit: Optional[int] = None,
212
+ offset: Optional[int] = None,
213
+ ) -> GroupsListResponse:
214
+ """List the memory groups visible to the current team / member."""
215
+ params = merge_query({"query": query, "limit": limit, "offset": offset})
216
+ data = self._get("/groups", params=params)
217
+ return GroupsListResponse.model_validate(data)
218
+
219
+
220
+ # ---------------------------------------------------------------------------
221
+ # Async client
222
+ # ---------------------------------------------------------------------------
223
+
224
+ class _AsyncGraphNamespace:
225
+ def __init__(self, client: "AsyncBreethClient") -> None:
226
+ self._client = client
227
+
228
+ async def node_details(self, identifier: str) -> NodeDetailsResponse:
229
+ data = await self._client._get(f"/graph/nodes/{identifier}/details")
230
+ return NodeDetailsResponse.model_validate(data)
231
+
232
+ async def list_entities(
233
+ self,
234
+ *,
235
+ query: Optional[str] = None,
236
+ limit: Optional[int] = None,
237
+ offset: Optional[int] = None,
238
+ ) -> GraphEntityListResponse:
239
+ params = merge_query({"query": query, "limit": limit, "offset": offset})
240
+ data = await self._client._get("/graph/entities", params=params)
241
+ return GraphEntityListResponse.model_validate(data)
242
+
243
+ async def list_edges(
244
+ self,
245
+ *,
246
+ query: Optional[str] = None,
247
+ limit: Optional[int] = None,
248
+ offset: Optional[int] = None,
249
+ ) -> GraphEdgeListResponse:
250
+ params = merge_query({"query": query, "limit": limit, "offset": offset})
251
+ data = await self._client._get("/graph/edges", params=params)
252
+ return GraphEdgeListResponse.model_validate(data)
253
+
254
+ async def list_episodes(
255
+ self,
256
+ *,
257
+ query: Optional[str] = None,
258
+ limit: Optional[int] = None,
259
+ offset: Optional[int] = None,
260
+ ) -> GraphEpisodeListResponse:
261
+ params = merge_query({"query": query, "limit": limit, "offset": offset})
262
+ data = await self._client._get("/graph/episodes", params=params)
263
+ return GraphEpisodeListResponse.model_validate(data)
264
+
265
+
266
+ class AsyncBreethClient:
267
+ """Asynchronous client for the Breeth API. Same surface as
268
+ ``BreethClient`` but every method is a coroutine."""
269
+
270
+ def __init__(
271
+ self,
272
+ api_key: Optional[str] = None,
273
+ *,
274
+ base_url: Optional[str] = None,
275
+ end_user_id: Optional[str] = None,
276
+ timeout: float = DEFAULT_TIMEOUT,
277
+ http_client: Optional[httpx.AsyncClient] = None,
278
+ ) -> None:
279
+ self._api_key = resolve_api_key(api_key)
280
+ self._base_url = resolve_base_url(base_url)
281
+ self._end_user_id = end_user_id
282
+ self._headers = build_headers(self._api_key, end_user_id)
283
+ self._owns_http = http_client is None
284
+ self._http = http_client or httpx.AsyncClient(timeout=timeout)
285
+ self.graph = _AsyncGraphNamespace(self)
286
+
287
+ # ---- context manager ------------------------------------------------
288
+
289
+ async def __aenter__(self) -> "AsyncBreethClient":
290
+ return self
291
+
292
+ async def __aexit__(self, exc_type, exc, tb) -> None:
293
+ await self.aclose()
294
+
295
+ async def aclose(self) -> None:
296
+ if self._owns_http:
297
+ await self._http.aclose()
298
+
299
+ # ---- low-level helpers ---------------------------------------------
300
+
301
+ async def _get(self, path: str, *, params: Optional[dict[str, Any]] = None) -> Any:
302
+ response = await self._http.get(
303
+ build_url(self._base_url, path),
304
+ headers=self._headers,
305
+ params=params,
306
+ )
307
+ raise_for_status(response)
308
+ return response.json()
309
+
310
+ async def _post(self, path: str, *, json: dict[str, Any]) -> Any:
311
+ response = await self._http.post(
312
+ build_url(self._base_url, path),
313
+ headers=self._headers,
314
+ json=json,
315
+ )
316
+ raise_for_status(response)
317
+ return response.json()
318
+
319
+ # ---- public methods ------------------------------------------------
320
+
321
+ async def write(
322
+ self,
323
+ content: str,
324
+ *,
325
+ group_id: str = "default",
326
+ source_description: str = "api",
327
+ extract_intent: bool = False,
328
+ ) -> WriteResponse:
329
+ payload = {
330
+ "content": content,
331
+ "group_id": group_id,
332
+ "source_description": source_description,
333
+ "extract_intent": extract_intent,
334
+ }
335
+ data = await self._post("/episodes", json=payload)
336
+ return WriteResponse.model_validate(data)
337
+
338
+ async def retrieve(
339
+ self,
340
+ query: str,
341
+ *,
342
+ group_id: str = "default",
343
+ limit: int = 10,
344
+ ) -> RetrieveResponse:
345
+ payload = {"query": query, "group_id": group_id, "limit": limit}
346
+ data = await self._post("/search", json=payload)
347
+ return RetrieveResponse.model_validate(data)
348
+
349
+ async def entity(
350
+ self,
351
+ name: str,
352
+ *,
353
+ mode: Optional[EntityMode] = None,
354
+ limit: Optional[int] = None,
355
+ ) -> EntityResponse:
356
+ params = merge_query({"mode": mode, "limit": limit})
357
+ data = await self._get(f"/entities/{name}", params=params)
358
+ return EntityResponse.model_validate(data)
359
+
360
+ async def groups(
361
+ self,
362
+ *,
363
+ query: Optional[str] = None,
364
+ limit: Optional[int] = None,
365
+ offset: Optional[int] = None,
366
+ ) -> GroupsListResponse:
367
+ params = merge_query({"query": query, "limit": limit, "offset": offset})
368
+ data = await self._get("/groups", params=params)
369
+ return GroupsListResponse.model_validate(data)
breeth/errors.py ADDED
@@ -0,0 +1,38 @@
1
+ """Exception types raised by the Breeth SDK."""
2
+ from __future__ import annotations
3
+
4
+ from typing import Any, Optional
5
+
6
+
7
+ class BreethError(Exception):
8
+ """Base error for every non-2xx response from the Breeth API.
9
+
10
+ Attributes:
11
+ status: HTTP status code (e.g. 401, 429, 500).
12
+ slug: Stable machine-readable error code from the API
13
+ (e.g. "quota_exceeded", "invalid_token"). May be None if
14
+ the server returned a non-standard payload.
15
+ message: Human-readable message from the API, falls back to a
16
+ generic string.
17
+ body: The full parsed response body for advanced inspection.
18
+ """
19
+
20
+ def __init__(
21
+ self,
22
+ message: str,
23
+ *,
24
+ status: int,
25
+ slug: Optional[str] = None,
26
+ body: Any = None,
27
+ ) -> None:
28
+ super().__init__(message)
29
+ self.status = status
30
+ self.slug = slug
31
+ self.message = message
32
+ self.body = body
33
+
34
+ def __repr__(self) -> str:
35
+ return (
36
+ f"BreethError(status={self.status}, slug={self.slug!r}, "
37
+ f"message={self.message!r})"
38
+ )
breeth/types.py ADDED
@@ -0,0 +1,237 @@
1
+ """Pydantic v2 request and response models for the Breeth API.
2
+
3
+ These mirror the server schemas in cogram-core. Fields with a leading
4
+ underscore on the wire (e.g. ``_cache``, ``_tier``, ``_source``,
5
+ ``_note``) use aliases so Python users can access them as normal
6
+ attributes (``response.cache`` rather than ``response["_cache"]``).
7
+ """
8
+ from __future__ import annotations
9
+
10
+ from typing import Any, Literal, Optional
11
+
12
+ from pydantic import BaseModel, ConfigDict, Field
13
+
14
+
15
+ # ---------------------------------------------------------------------------
16
+ # Write (POST /v1/episodes)
17
+ # ---------------------------------------------------------------------------
18
+
19
+ class ExtractedCounts(BaseModel):
20
+ entities: int
21
+ edges: int
22
+
23
+
24
+ class CogramTaskEnvelope(BaseModel):
25
+ mode: str
26
+ status: str
27
+ task_id: Optional[str] = None
28
+ note: Optional[str] = None
29
+
30
+
31
+ class IntentSuggestion(BaseModel):
32
+ should_extract: bool = True
33
+ confidence: float
34
+ reason: str
35
+
36
+
37
+ class WriteResponse(BaseModel):
38
+ ok: bool = True
39
+ episode_name: str
40
+ extracted: ExtractedCounts
41
+ group_id: str
42
+ warning: Optional[str] = None
43
+ cogram: Optional[CogramTaskEnvelope] = None
44
+ intent_suggestion: Optional[IntentSuggestion] = None
45
+
46
+
47
+ # ---------------------------------------------------------------------------
48
+ # Retrieve (POST /v1/search)
49
+ # ---------------------------------------------------------------------------
50
+
51
+ class EdgeHit(BaseModel):
52
+ model_config = ConfigDict(populate_by_name=True)
53
+
54
+ edge_uuid: Optional[str] = None
55
+ source_node: str
56
+ target_node: str
57
+ fact: str
58
+ name: Optional[str] = None
59
+ intent_meta: Any = None
60
+ tier: str = Field(alias="_tier")
61
+
62
+
63
+ class CacheStats(BaseModel):
64
+ tier: str
65
+ hot_hits: int
66
+ cold_hits: int
67
+ group_id: str
68
+
69
+
70
+ class PatternEvidence(BaseModel):
71
+ name: str
72
+ stored_confidence: float
73
+ effective_confidence: float
74
+ examples: list[dict] = Field(default_factory=list)
75
+
76
+
77
+ class DirectorProfileBlock(BaseModel):
78
+ model_config = ConfigDict(populate_by_name=True)
79
+
80
+ summary: str
81
+ visions: list[str] = Field(default_factory=list)
82
+ top_patterns: list[PatternEvidence] = Field(default_factory=list)
83
+ source: str = Field(alias="_source")
84
+ note: str = Field(alias="_note")
85
+
86
+
87
+ class RetrieveResponse(BaseModel):
88
+ model_config = ConfigDict(populate_by_name=True)
89
+
90
+ director_profile: Optional[DirectorProfileBlock] = None
91
+ edges: list[EdgeHit] = Field(default_factory=list)
92
+ cache: CacheStats = Field(alias="_cache")
93
+ note: Optional[str] = None
94
+
95
+
96
+ # ---------------------------------------------------------------------------
97
+ # Entity (GET /v1/entities/{name})
98
+ # ---------------------------------------------------------------------------
99
+
100
+ EntityMode = Literal["narrative", "edges", "episodes", "all"]
101
+
102
+
103
+ class EntityNarrativeRow(BaseModel):
104
+ uuid: str
105
+ name: str
106
+ degree: int
107
+ summary: str
108
+ narrative: Any
109
+ confidence_stored: float
110
+ confidence_decayed: float
111
+ confidence_label: str
112
+
113
+
114
+ class EntityEdgeRow(BaseModel):
115
+ a: str
116
+ b: str
117
+ fact: str
118
+ intent_meta: Any = None
119
+
120
+
121
+ class EntityEpisodeRow(BaseModel):
122
+ uuid: str
123
+ name: str
124
+ content_excerpt: str
125
+ timestamp: Any = None
126
+
127
+
128
+ class EntityResponse(BaseModel):
129
+ entity_name: str
130
+ mode: str
131
+ narrative: Optional[list[EntityNarrativeRow]] = None
132
+ edges: Optional[list[EntityEdgeRow]] = None
133
+ episodes: Optional[list[EntityEpisodeRow]] = None
134
+ note: Optional[str] = None
135
+
136
+
137
+ # ---------------------------------------------------------------------------
138
+ # Graph (GET /v1/graph/*)
139
+ # ---------------------------------------------------------------------------
140
+
141
+ class GraphEntityRow(BaseModel):
142
+ uuid: str
143
+ name: str
144
+ labels: list[str] = Field(default_factory=list)
145
+ summary: Optional[str] = None
146
+ edge_count: int = 0
147
+ episode_count: int = 0
148
+ space: str = "individual"
149
+ member_id: Optional[str] = None
150
+ created_at: Optional[str] = None
151
+ knot_narrative: Optional[str] = None
152
+ knot_score: Optional[float] = None
153
+ knot_synthesized_at: Optional[float] = None
154
+ knot_model: Optional[str] = None
155
+
156
+
157
+ class GraphEntityListResponse(BaseModel):
158
+ entities: list[GraphEntityRow]
159
+ count: int
160
+
161
+
162
+ class GraphEdgeRow(BaseModel):
163
+ uuid: str
164
+ fact: str
165
+ source_name: str
166
+ target_name: str
167
+ cognitive_pattern: Optional[str] = None
168
+ intent_meta: Optional[dict] = None
169
+ retracted_at: Optional[str] = None
170
+ space: str = "individual"
171
+ member_id: Optional[str] = None
172
+ created_at: Optional[str] = None
173
+
174
+
175
+ class GraphEdgeListResponse(BaseModel):
176
+ edges: list[GraphEdgeRow]
177
+ count: int
178
+
179
+
180
+ class GraphEpisodeRow(BaseModel):
181
+ uuid: str
182
+ name: str
183
+ source_description: Optional[str] = None
184
+ content_excerpt: str = ""
185
+ group_id: str = ""
186
+ valid_at: Optional[str] = None
187
+ space: str = "individual"
188
+ member_id: Optional[str] = None
189
+
190
+
191
+ class GraphEpisodeListResponse(BaseModel):
192
+ episodes: list[GraphEpisodeRow]
193
+ count: int
194
+
195
+
196
+ class NeighborRow(BaseModel):
197
+ peer: str
198
+ direction: str
199
+ fact: Optional[str] = None
200
+ cognitive_pattern: Optional[str] = None
201
+ intent_meta: Optional[dict] = None
202
+ edge_uuid: Optional[str] = None
203
+
204
+
205
+ class EpisodeMentionRow(BaseModel):
206
+ uuid: str
207
+ name: str
208
+ valid_at: Optional[str] = None
209
+
210
+
211
+ class NodeDetailsResponse(BaseModel):
212
+ entity: GraphEntityRow
213
+ neighbors: list[NeighborRow]
214
+ episodes: list[EpisodeMentionRow]
215
+
216
+
217
+ # ---------------------------------------------------------------------------
218
+ # Groups (GET /v1/groups)
219
+ # ---------------------------------------------------------------------------
220
+
221
+ class GroupRow(BaseModel):
222
+ group_id: str
223
+ episodes: int
224
+ entities: int
225
+ patterns: int
226
+ knots: int
227
+ has_director_profile: bool
228
+
229
+
230
+ class GroupsListResponse(BaseModel):
231
+ groups: list[GroupRow]
232
+ total: int
233
+ returned: int
234
+ query: Optional[str] = None
235
+ next_offset: Optional[int] = None
236
+ hint: Optional[str] = None
237
+ note: Optional[str] = None
@@ -0,0 +1,130 @@
1
+ Metadata-Version: 2.4
2
+ Name: breeth
3
+ Version: 0.1.0
4
+ Summary: Official Python SDK for Breeth, a memory layer for agents.
5
+ Project-URL: Homepage, https://thebreeth.com
6
+ Project-URL: Documentation, https://docs.thebreeth.com
7
+ Project-URL: Source, https://github.com/Gramies/cogram-sdk-python
8
+ Project-URL: Issues, https://github.com/Gramies/cogram-sdk-python/issues
9
+ Author-email: Breeth <team@thebreeth.com>
10
+ License: MIT
11
+ License-File: LICENSE
12
+ Keywords: agents,breeth,graph,knowledge,llm,memory
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
23
+ Requires-Python: >=3.10
24
+ Requires-Dist: httpx>=0.27
25
+ Requires-Dist: pydantic>=2
26
+ Provides-Extra: dev
27
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
28
+ Requires-Dist: pytest>=8; extra == 'dev'
29
+ Requires-Dist: respx>=0.21; extra == 'dev'
30
+ Description-Content-Type: text/markdown
31
+
32
+ # breeth
33
+
34
+ Official Python SDK for [Breeth](https://thebreeth.com), the memory layer for agents.
35
+
36
+ `breeth` is a thin, type-safe wrapper around the Breeth REST API. It ships with both a synchronous and an asynchronous client, runs on Python 3.10+, and depends only on `httpx` and `pydantic`.
37
+
38
+ ## Install
39
+
40
+ ```bash
41
+ pip install breeth
42
+ ```
43
+
44
+ ## Quickstart (sync)
45
+
46
+ ```python
47
+ from breeth import BreethClient
48
+
49
+ with BreethClient(api_key="ck_live_...") as breeth:
50
+ # Write a memory
51
+ write_resp = breeth.write(
52
+ "Candidate Jane Doe has 4 years HR-tech experience at Workday.",
53
+ group_id="recruiting",
54
+ )
55
+ print(write_resp.episode_name, write_resp.extracted.entities)
56
+
57
+ # Retrieve
58
+ results = breeth.retrieve("HR-tech background", group_id="recruiting", limit=5)
59
+ for edge in results.edges:
60
+ print(edge.fact)
61
+
62
+ # Inspect an entity
63
+ entity = breeth.entity("Workday", mode="narrative")
64
+
65
+ # Browse the graph
66
+ nodes = breeth.graph.list_entities(query="Jane", limit=25)
67
+ edges = breeth.graph.list_edges(limit=50)
68
+ eps = breeth.graph.list_episodes()
69
+ details = breeth.graph.node_details(nodes.entities[0].uuid)
70
+
71
+ # List groups visible to the team
72
+ groups = breeth.groups()
73
+ ```
74
+
75
+ ## Quickstart (async)
76
+
77
+ ```python
78
+ import asyncio
79
+ from breeth import AsyncBreethClient
80
+
81
+ async def main():
82
+ async with AsyncBreethClient(api_key="ck_live_...") as breeth:
83
+ await breeth.write("Recruiter prefers iterative sourcing.")
84
+ results = await breeth.retrieve("recruiter preferences")
85
+ print(results.edges)
86
+
87
+ asyncio.run(main())
88
+ ```
89
+
90
+ ## Configuration
91
+
92
+ | Setting | Source |
93
+ | --- | --- |
94
+ | API key | `api_key=` argument, then `BREETH_API_KEY` env var, then `COGRAM_API_KEY` |
95
+ | Base URL | `base_url=` argument, then `COGRAM_API_URL` env var, default `https://api.thebreeth.com` |
96
+ | End user passthrough | `end_user_id=` argument, sent as `X-End-User-Id` |
97
+
98
+ ## Errors
99
+
100
+ Every non-2xx response is raised as a `BreethError`:
101
+
102
+ ```python
103
+ from breeth import BreethClient, BreethError
104
+
105
+ try:
106
+ with BreethClient(api_key="ck_live_...") as breeth:
107
+ breeth.write("...")
108
+ except BreethError as err:
109
+ print(err.status) # 429
110
+ print(err.slug) # "quota_exceeded"
111
+ print(err.message) # "Monthly write quota exceeded."
112
+ print(err.body) # raw JSON payload
113
+ ```
114
+
115
+ The `slug` field is stable across API versions, so it is safe to branch on it.
116
+
117
+ ## Roadmap
118
+
119
+ - v0.1.0: sync + async clients for write, retrieve, entity, graph, groups.
120
+ - v0.2.0: NDJSON streaming endpoints (`/v1/graph/nodes`, `/v1/graph/links`).
121
+
122
+ ## Links
123
+
124
+ - Product: https://thebreeth.com
125
+ - Docs: https://docs.thebreeth.com
126
+ - Issues: https://github.com/Gramies/cogram-sdk-python/issues
127
+
128
+ ## License
129
+
130
+ MIT. See [LICENSE](LICENSE).
@@ -0,0 +1,9 @@
1
+ breeth/__init__.py,sha256=Nv8BnkurpD9FD5XuCHWDIC0oNd0kBCwjpRG3aUJfAc8,1660
2
+ breeth/_base.py,sha256=ysb6EwCTHSKY5iH6sk3nRteIujOUUSE7keaNrn0AtvI,3396
3
+ breeth/client.py,sha256=a8yGlX3cmqQiW2QiCrXGdCkoguZORQhoPSeuRVrN5Ww,12375
4
+ breeth/errors.py,sha256=kVW9vXnufRCSZ_frm3PTcXjdyhZJYdKY5rIVIuP0m-4,1127
5
+ breeth/types.py,sha256=IdMruQiMjbHvhTMLh_k2Y0OwtpqryeHkbIzBuA1Hmf4,6010
6
+ breeth-0.1.0.dist-info/METADATA,sha256=_ox-dSbAi_b6N0tHsIW-XtOr6kvanKfFlN4_J8bh3-Q,4020
7
+ breeth-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
8
+ breeth-0.1.0.dist-info/licenses/LICENSE,sha256=HLhqMVIwZmy5mQwhOgcZSCBUYBTsLcrBjIed2C_2Z-g,1063
9
+ breeth-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
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Breeth
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.