contextstore-sdk 0.1.0__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.
- contextstore_sdk-0.1.0/PKG-INFO +86 -0
- contextstore_sdk-0.1.0/README.md +72 -0
- contextstore_sdk-0.1.0/contextstore/__init__.py +26 -0
- contextstore_sdk-0.1.0/contextstore/_models.py +117 -0
- contextstore_sdk-0.1.0/contextstore/_transport.py +150 -0
- contextstore_sdk-0.1.0/contextstore/client.py +54 -0
- contextstore_sdk-0.1.0/contextstore/company.py +75 -0
- contextstore_sdk-0.1.0/contextstore/context.py +28 -0
- contextstore_sdk-0.1.0/contextstore/memory.py +86 -0
- contextstore_sdk-0.1.0/contextstore/permissions.py +47 -0
- contextstore_sdk-0.1.0/contextstore/py.typed +2 -0
- contextstore_sdk-0.1.0/contextstore_sdk.egg-info/PKG-INFO +86 -0
- contextstore_sdk-0.1.0/contextstore_sdk.egg-info/SOURCES.txt +15 -0
- contextstore_sdk-0.1.0/contextstore_sdk.egg-info/dependency_links.txt +1 -0
- contextstore_sdk-0.1.0/contextstore_sdk.egg-info/top_level.txt +1 -0
- contextstore_sdk-0.1.0/pyproject.toml +27 -0
- contextstore_sdk-0.1.0/setup.cfg +4 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: contextstore-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Client SDK for the ContextStore company brain — compiled memory and agent team, accessed via a typed Python API.
|
|
5
|
+
Author: ContextStore
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://contextstore.oritm.tech
|
|
8
|
+
Keywords: memory,mcp,second-brain,agents,knowledge-base
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
12
|
+
Requires-Python: >=3.8
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
# ContextStore Python SDK
|
|
16
|
+
|
|
17
|
+
Talk to your ContextStore **company brain** — compiled memory + agent team — from
|
|
18
|
+
Python. The SDK is a typed client over your backend's MCP JSON-RPC layer.
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pip install contextstore-sdk
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Quickstart
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
import os
|
|
30
|
+
from contextstore import ContextStore
|
|
31
|
+
|
|
32
|
+
client = ContextStore(api_key=os.environ["CONTEXTSTORE_KEY"])
|
|
33
|
+
|
|
34
|
+
# Remember something
|
|
35
|
+
client.memory.add(
|
|
36
|
+
content="Board approved usage-based pricing on Sep 12",
|
|
37
|
+
metadata={"project": "pricing", "owner": "maya"},
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
# Ask the company brain — with sources
|
|
41
|
+
res = client.memory.query("What did we decide about pricing?")
|
|
42
|
+
print(res.answer)
|
|
43
|
+
print(res.sources)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Local / self-hosted
|
|
47
|
+
|
|
48
|
+
Point at your own backend (default is the hosted one):
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
client = ContextStore(
|
|
52
|
+
api_key=os.environ["CONTEXTSTORE_KEY"],
|
|
53
|
+
base_url="https://contextstore.onrender.com",
|
|
54
|
+
)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
For local development you can pass a JWT instead of an api_key:
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
client = ContextStore(token="<jwt>")
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Namespaces / methods
|
|
64
|
+
|
|
65
|
+
| Python | Backend MCP tool |
|
|
66
|
+
|---|---|
|
|
67
|
+
| `client.memory.add(...)` | `memory_store` |
|
|
68
|
+
| `client.memory.query(...)` | `memory_recall` |
|
|
69
|
+
| `client.memory.log_turn(...)` | `log_conversation_turn` |
|
|
70
|
+
| `client.context.snapshot()` | `get_context_snapshot` |
|
|
71
|
+
| `client.context.checkin(...)` | `smart_checkin` |
|
|
72
|
+
| `client.company.learn(...)` | `company_learn` |
|
|
73
|
+
| `client.company.map()` / `.set_map(...)` | `company_map` |
|
|
74
|
+
| `client.company.review(run=True)` | `company_review` |
|
|
75
|
+
| `client.company.approve(...)` / `.reject(...)` | `review_propagation` |
|
|
76
|
+
| `client.company.pending(...)` | `list_propagation_queue` |
|
|
77
|
+
| `client.permissions.grant/revoke/list(...)` | permission admin tools |
|
|
78
|
+
| `client.permissions.check_authority(...)` | `check_action_authority` |
|
|
79
|
+
|
|
80
|
+
## Errors
|
|
81
|
+
|
|
82
|
+
- `AuthenticationError` — bad/expired/scoped-out API key
|
|
83
|
+
- `ApiError` — the MCP tool returned an error
|
|
84
|
+
- `ContextStoreError` — network / transport issues
|
|
85
|
+
|
|
86
|
+
No external dependencies — uses only the Python standard library.
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# ContextStore Python SDK
|
|
2
|
+
|
|
3
|
+
Talk to your ContextStore **company brain** — compiled memory + agent team — from
|
|
4
|
+
Python. The SDK is a typed client over your backend's MCP JSON-RPC layer.
|
|
5
|
+
|
|
6
|
+
## Install
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
pip install contextstore-sdk
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Quickstart
|
|
13
|
+
|
|
14
|
+
```python
|
|
15
|
+
import os
|
|
16
|
+
from contextstore import ContextStore
|
|
17
|
+
|
|
18
|
+
client = ContextStore(api_key=os.environ["CONTEXTSTORE_KEY"])
|
|
19
|
+
|
|
20
|
+
# Remember something
|
|
21
|
+
client.memory.add(
|
|
22
|
+
content="Board approved usage-based pricing on Sep 12",
|
|
23
|
+
metadata={"project": "pricing", "owner": "maya"},
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
# Ask the company brain — with sources
|
|
27
|
+
res = client.memory.query("What did we decide about pricing?")
|
|
28
|
+
print(res.answer)
|
|
29
|
+
print(res.sources)
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Local / self-hosted
|
|
33
|
+
|
|
34
|
+
Point at your own backend (default is the hosted one):
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
client = ContextStore(
|
|
38
|
+
api_key=os.environ["CONTEXTSTORE_KEY"],
|
|
39
|
+
base_url="https://contextstore.onrender.com",
|
|
40
|
+
)
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
For local development you can pass a JWT instead of an api_key:
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
client = ContextStore(token="<jwt>")
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Namespaces / methods
|
|
50
|
+
|
|
51
|
+
| Python | Backend MCP tool |
|
|
52
|
+
|---|---|
|
|
53
|
+
| `client.memory.add(...)` | `memory_store` |
|
|
54
|
+
| `client.memory.query(...)` | `memory_recall` |
|
|
55
|
+
| `client.memory.log_turn(...)` | `log_conversation_turn` |
|
|
56
|
+
| `client.context.snapshot()` | `get_context_snapshot` |
|
|
57
|
+
| `client.context.checkin(...)` | `smart_checkin` |
|
|
58
|
+
| `client.company.learn(...)` | `company_learn` |
|
|
59
|
+
| `client.company.map()` / `.set_map(...)` | `company_map` |
|
|
60
|
+
| `client.company.review(run=True)` | `company_review` |
|
|
61
|
+
| `client.company.approve(...)` / `.reject(...)` | `review_propagation` |
|
|
62
|
+
| `client.company.pending(...)` | `list_propagation_queue` |
|
|
63
|
+
| `client.permissions.grant/revoke/list(...)` | permission admin tools |
|
|
64
|
+
| `client.permissions.check_authority(...)` | `check_action_authority` |
|
|
65
|
+
|
|
66
|
+
## Errors
|
|
67
|
+
|
|
68
|
+
- `AuthenticationError` — bad/expired/scoped-out API key
|
|
69
|
+
- `ApiError` — the MCP tool returned an error
|
|
70
|
+
- `ContextStoreError` — network / transport issues
|
|
71
|
+
|
|
72
|
+
No external dependencies — uses only the Python standard library.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""ContextStore — Python SDK for the ContextStore company brain.
|
|
2
|
+
|
|
3
|
+
Talk to your compiled memory and agent team over a clean, typed API.
|
|
4
|
+
See the ContextStore class for usage.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .client import ContextStore
|
|
8
|
+
from ._transport import (
|
|
9
|
+
ApiError,
|
|
10
|
+
AuthenticationError,
|
|
11
|
+
ContextStoreError,
|
|
12
|
+
)
|
|
13
|
+
from ._models import ActionResult, QueryResult, SnapshotResult, Source
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"ContextStore",
|
|
17
|
+
"ActionResult",
|
|
18
|
+
"QueryResult",
|
|
19
|
+
"SnapshotResult",
|
|
20
|
+
"Source",
|
|
21
|
+
"ContextStoreError",
|
|
22
|
+
"AuthenticationError",
|
|
23
|
+
"ApiError",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""ContextStore SDK — typed result models for MCP tool responses."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from typing import Any, Dict, List, Optional
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _load_text(result: Dict[str, Any]) -> Dict[str, Any]:
|
|
11
|
+
"""Extract the JSON payload the server returns inside content[].text."""
|
|
12
|
+
for block in result.get("content", []):
|
|
13
|
+
if isinstance(block, dict) and block.get("type") == "text":
|
|
14
|
+
text = str(block.get("text", ""))
|
|
15
|
+
try:
|
|
16
|
+
parsed = json.loads(text)
|
|
17
|
+
if isinstance(parsed, dict):
|
|
18
|
+
return parsed
|
|
19
|
+
except (ValueError, TypeError):
|
|
20
|
+
return {"text": text}
|
|
21
|
+
return {}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class Source:
|
|
26
|
+
"""A single source returned by the memory recall."""
|
|
27
|
+
|
|
28
|
+
text: str
|
|
29
|
+
source: Optional[str] = None
|
|
30
|
+
memory_type: Optional[str] = None
|
|
31
|
+
confidence: Optional[float] = None
|
|
32
|
+
extra: Dict[str, Any] = field(default_factory=dict)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class QueryResult:
|
|
37
|
+
"""Typed result of `client.memory.query(...)`."""
|
|
38
|
+
|
|
39
|
+
answer: str = ""
|
|
40
|
+
sources: List[Source] = field(default_factory=list)
|
|
41
|
+
raw: Dict[str, Any] = field(default_factory=dict)
|
|
42
|
+
|
|
43
|
+
@classmethod
|
|
44
|
+
def from_result(cls, result: Dict[str, Any]) -> "QueryResult":
|
|
45
|
+
data = _load_text(result)
|
|
46
|
+
answer = data.get("answer") or data.get("summary") or _raw_text(result)
|
|
47
|
+
if isinstance(answer, list):
|
|
48
|
+
answer = "\n".join(str(a) for a in answer)
|
|
49
|
+
sources_raw = data.get("sources") or data.get("results") or []
|
|
50
|
+
sources: List[Source] = []
|
|
51
|
+
if isinstance(sources_raw, list):
|
|
52
|
+
for s in sources_raw:
|
|
53
|
+
if isinstance(s, dict):
|
|
54
|
+
sources.append(
|
|
55
|
+
Source(
|
|
56
|
+
text=str(s.get("content") or s.get("text") or s.get("summary") or s),
|
|
57
|
+
source=s.get("source") or s.get("memory_type") or None,
|
|
58
|
+
memory_type=s.get("memory_type") or s.get("type") or None,
|
|
59
|
+
confidence=s.get("confidence") or s.get("score") or None,
|
|
60
|
+
extra=s,
|
|
61
|
+
)
|
|
62
|
+
)
|
|
63
|
+
else:
|
|
64
|
+
sources.append(Source(text=str(s)))
|
|
65
|
+
elif isinstance(sources_raw, dict):
|
|
66
|
+
for key, val in sources_raw.items():
|
|
67
|
+
sources.append(
|
|
68
|
+
Source(
|
|
69
|
+
text=str(val.get("content") or val if isinstance(val, dict) else val),
|
|
70
|
+
source=key,
|
|
71
|
+
extra=val if isinstance(val, dict) else {},
|
|
72
|
+
)
|
|
73
|
+
)
|
|
74
|
+
return cls(answer=str(answer), sources=sources, raw=data)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@dataclass
|
|
78
|
+
class ActionResult:
|
|
79
|
+
"""Generic result of a tool that returns a JSON-ish payload."""
|
|
80
|
+
|
|
81
|
+
data: Dict[str, Any] = field(default_factory=dict)
|
|
82
|
+
text: str = ""
|
|
83
|
+
|
|
84
|
+
@classmethod
|
|
85
|
+
def from_result(cls, result: Dict[str, Any]) -> "ActionResult":
|
|
86
|
+
return cls(data=_load_text(result), text=_raw_text(result))
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _raw_text(result: Dict[str, Any]) -> str:
|
|
90
|
+
parts = []
|
|
91
|
+
for block in result.get("content", []):
|
|
92
|
+
if isinstance(block, dict) and block.get("type") == "text":
|
|
93
|
+
parts.append(str(block.get("text", "")))
|
|
94
|
+
return "\n".join(p for p in parts if p)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@dataclass
|
|
98
|
+
class SnapshotResult:
|
|
99
|
+
"""Typed result of `client.snapshot()`."""
|
|
100
|
+
|
|
101
|
+
data: Dict[str, Any] = field(default_factory=dict)
|
|
102
|
+
matched_playbooks: List[str] = field(default_factory=list)
|
|
103
|
+
has_profile: bool = False
|
|
104
|
+
text: str = ""
|
|
105
|
+
|
|
106
|
+
@classmethod
|
|
107
|
+
def from_result(cls, result: Dict[str, Any]) -> "SnapshotResult":
|
|
108
|
+
data = _load_text(result)
|
|
109
|
+
playbooks = data.get("matched_playbooks") or []
|
|
110
|
+
if isinstance(playbooks, str):
|
|
111
|
+
playbooks = [playbooks]
|
|
112
|
+
return cls(
|
|
113
|
+
data=data,
|
|
114
|
+
matched_playbooks=list(playbooks),
|
|
115
|
+
has_profile=bool(data.get("has_profile") or data.get("user_profile")),
|
|
116
|
+
text=_raw_text(result),
|
|
117
|
+
)
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"""ContextStore SDK — MCP JSON-RPC transport over Streamable HTTP."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import time
|
|
7
|
+
import urllib.error
|
|
8
|
+
import urllib.request
|
|
9
|
+
from typing import Any, Dict, Optional
|
|
10
|
+
|
|
11
|
+
DEFAULT_BASE_URL = "https://contextstore.onrender.com"
|
|
12
|
+
DEFAULT_MCP_PATH = "/mcp"
|
|
13
|
+
DEFAULT_TIMEOUT = 60.0
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ContextStoreError(Exception):
|
|
17
|
+
"""Base error for ContextStore SDK."""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class AuthenticationError(ContextStoreError):
|
|
21
|
+
"""Raised when the API key / token is invalid, expired, or lacks scope."""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ApiError(ContextStoreError):
|
|
25
|
+
"""Raised when the MCP call returns an error or an isError result."""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Transport:
|
|
29
|
+
"""Minimal MCP Streamable HTTP (JSON-RPC) client with no external deps."""
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
*,
|
|
34
|
+
api_key: Optional[str] = None,
|
|
35
|
+
token: Optional[str] = None,
|
|
36
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
37
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
38
|
+
) -> None:
|
|
39
|
+
if not api_key and not token:
|
|
40
|
+
raise ValueError("Either api_key or token (JWT) must be provided")
|
|
41
|
+
self._api_key = api_key
|
|
42
|
+
self._token = token
|
|
43
|
+
self._base_url = base_url.rstrip("/")
|
|
44
|
+
self._timeout = timeout
|
|
45
|
+
self._headers_cache: Optional[Dict[str, str]] = None
|
|
46
|
+
|
|
47
|
+
def _headers(self) -> Dict[str, str]:
|
|
48
|
+
headers = {
|
|
49
|
+
"Content-Type": "application/json",
|
|
50
|
+
"Accept": "application/json, text/event-stream",
|
|
51
|
+
}
|
|
52
|
+
if self._api_key:
|
|
53
|
+
headers["Authorization"] = f"Bearer {self._api_key}"
|
|
54
|
+
elif self._token:
|
|
55
|
+
headers["Authorization"] = f"Bearer {self._token}"
|
|
56
|
+
return headers
|
|
57
|
+
|
|
58
|
+
def call_tool(self, name: str, arguments: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
59
|
+
"""Send a JSON-RPC tools/call and return the result payload (dict)."""
|
|
60
|
+
request_id = int(time.time() * 1000) % (2 ** 31)
|
|
61
|
+
payload = {
|
|
62
|
+
"jsonrpc": "2.0",
|
|
63
|
+
"id": request_id,
|
|
64
|
+
"method": "tools/call",
|
|
65
|
+
"params": {"name": name, "arguments": arguments or {}},
|
|
66
|
+
}
|
|
67
|
+
url = f"{self._base_url}{DEFAULT_MCP_PATH}"
|
|
68
|
+
data = json.dumps(payload).encode("utf-8")
|
|
69
|
+
req = urllib.request.Request(url, data=data, headers=self._headers(), method="POST")
|
|
70
|
+
try:
|
|
71
|
+
with urllib.request.urlopen(req, timeout=self._timeout) as resp:
|
|
72
|
+
raw = resp.read().decode("utf-8")
|
|
73
|
+
except urllib.error.HTTPError as e:
|
|
74
|
+
body = ""
|
|
75
|
+
try:
|
|
76
|
+
body = e.read().decode("utf-8")
|
|
77
|
+
except Exception:
|
|
78
|
+
pass
|
|
79
|
+
if e.code in (401, 403):
|
|
80
|
+
raise AuthenticationError(
|
|
81
|
+
f"Authentication failed ({e.code}): {body or e.reason}"
|
|
82
|
+
)
|
|
83
|
+
raise ApiError(f"HTTP {e.code}: {body or e.reason}")
|
|
84
|
+
except urllib.error.URLError as e:
|
|
85
|
+
raise ContextStoreError(f"Network error: {e.reason}")
|
|
86
|
+
|
|
87
|
+
# The response may be either a single JSON object or an SSE stream.
|
|
88
|
+
obj = self._parse_response(raw)
|
|
89
|
+
if "error" in obj:
|
|
90
|
+
err = obj["error"]
|
|
91
|
+
raise ApiError(str(err.get("message", err)))
|
|
92
|
+
result = obj.get("result", {})
|
|
93
|
+
if result.get("isError"):
|
|
94
|
+
text = self._text_of(result)
|
|
95
|
+
raise ApiError(text or "MCP tool returned an error")
|
|
96
|
+
return result
|
|
97
|
+
|
|
98
|
+
def list_tools(self) -> list:
|
|
99
|
+
payload = {
|
|
100
|
+
"jsonrpc": "2.0",
|
|
101
|
+
"id": int(time.time() * 1000) % (2 ** 31),
|
|
102
|
+
"method": "tools/list",
|
|
103
|
+
"params": {},
|
|
104
|
+
}
|
|
105
|
+
url = f"{self._base_url}{DEFAULT_MCP_PATH}"
|
|
106
|
+
req = urllib.request.Request(
|
|
107
|
+
url,
|
|
108
|
+
data=json.dumps(payload).encode("utf-8"),
|
|
109
|
+
headers=self._headers(),
|
|
110
|
+
method="POST",
|
|
111
|
+
)
|
|
112
|
+
try:
|
|
113
|
+
with urllib.request.urlopen(req, timeout=self._timeout) as resp:
|
|
114
|
+
raw = resp.read().decode("utf-8")
|
|
115
|
+
except urllib.error.HTTPError as e:
|
|
116
|
+
if e.code in (401, 403):
|
|
117
|
+
raise AuthenticationError(f"Authentication failed ({e.code})")
|
|
118
|
+
raise ApiError(f"HTTP {e.code}")
|
|
119
|
+
except urllib.error.URLError as e:
|
|
120
|
+
raise ContextStoreError(f"Network error: {e.reason}")
|
|
121
|
+
obj = self._parse_response(raw)
|
|
122
|
+
if "error" in obj:
|
|
123
|
+
raise ApiError(str(obj["error"]))
|
|
124
|
+
return obj.get("result", {}).get("tools", [])
|
|
125
|
+
|
|
126
|
+
@staticmethod
|
|
127
|
+
def _parse_response(raw: str) -> Dict[str, Any]:
|
|
128
|
+
raw = raw.strip()
|
|
129
|
+
if not raw:
|
|
130
|
+
raise ApiError("Empty response from server")
|
|
131
|
+
if raw.startswith("event:"):
|
|
132
|
+
# SSE stream: take the last data: line.
|
|
133
|
+
for line in raw.splitlines():
|
|
134
|
+
line = line.strip()
|
|
135
|
+
if line.startswith("data:") and line[5:].strip():
|
|
136
|
+
raw = line[5:].strip()
|
|
137
|
+
if not raw.startswith("{"):
|
|
138
|
+
raise ApiError(f"Unexpected response format: {raw[:200]}")
|
|
139
|
+
try:
|
|
140
|
+
return json.loads(raw)
|
|
141
|
+
except json.JSONDecodeError as e:
|
|
142
|
+
raise ApiError(f"Invalid JSON response: {e}")
|
|
143
|
+
|
|
144
|
+
@staticmethod
|
|
145
|
+
def _text_of(result: Dict[str, Any]) -> str:
|
|
146
|
+
texts = []
|
|
147
|
+
for block in result.get("content", []):
|
|
148
|
+
if isinstance(block, dict) and block.get("type") == "text":
|
|
149
|
+
texts.append(str(block.get("text", "")))
|
|
150
|
+
return "\n".join(t for t in texts if t)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""ContextStore SDK — the public client.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
import os
|
|
5
|
+
from contextstore import ContextStore
|
|
6
|
+
|
|
7
|
+
client = ContextStore(api_key=os.environ["CONTEXTSTORE_KEY"])
|
|
8
|
+
|
|
9
|
+
client.memory.add("Board approved usage-based pricing on Sep 12",
|
|
10
|
+
metadata={"project": "pricing", "owner": "maya"})
|
|
11
|
+
res = client.memory.query("What did we decide about pricing?")
|
|
12
|
+
print(res.answer, res.sources)
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from typing import Optional
|
|
18
|
+
|
|
19
|
+
from ._transport import Transport
|
|
20
|
+
from .company import CompanyBrainNamespace
|
|
21
|
+
from .context import ContextNamespace
|
|
22
|
+
from .memory import MemoryNamespace
|
|
23
|
+
from .permissions import PermissionsNamespace
|
|
24
|
+
|
|
25
|
+
__all__ = ["ContextStore"]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ContextStore:
|
|
29
|
+
"""Root client. Points at the ContextStore backend and exposes namespaced,
|
|
30
|
+
typed access to the MCP tool catalog over JSON-RPC."""
|
|
31
|
+
|
|
32
|
+
def __init__(
|
|
33
|
+
self,
|
|
34
|
+
*,
|
|
35
|
+
api_key: Optional[str] = None,
|
|
36
|
+
token: Optional[str] = None,
|
|
37
|
+
base_url: str = "https://contextstore.onrender.com",
|
|
38
|
+
timeout: float = 60.0,
|
|
39
|
+
) -> None:
|
|
40
|
+
transport = Transport(
|
|
41
|
+
api_key=api_key,
|
|
42
|
+
token=token,
|
|
43
|
+
base_url=base_url,
|
|
44
|
+
timeout=timeout,
|
|
45
|
+
)
|
|
46
|
+
self._transport = transport
|
|
47
|
+
self.memory = MemoryNamespace(transport)
|
|
48
|
+
self.company = CompanyBrainNamespace(transport)
|
|
49
|
+
self.context = ContextNamespace(transport)
|
|
50
|
+
self.permissions = PermissionsNamespace(transport)
|
|
51
|
+
|
|
52
|
+
def ping(self) -> bool:
|
|
53
|
+
"""Quick connectivity/auth check: list the available MCP tools."""
|
|
54
|
+
return self._transport.list_tools() is not None
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""ContextStore SDK — Company Brain namespace (learn, map, review, approve)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Dict, List, Optional
|
|
6
|
+
|
|
7
|
+
from ._models import ActionResult
|
|
8
|
+
from ._transport import Transport
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class CompanyBrainNamespace:
|
|
12
|
+
"""Access to the Company Brain: corrections, the live map, and the review
|
|
13
|
+
ritual that gates what propagates company-wide."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, transport: Transport) -> None:
|
|
16
|
+
self._transport = transport
|
|
17
|
+
|
|
18
|
+
def learn(
|
|
19
|
+
self,
|
|
20
|
+
correction: str,
|
|
21
|
+
*,
|
|
22
|
+
correction_type: str = "fact",
|
|
23
|
+
reviewer: Optional[str] = None,
|
|
24
|
+
reason: Optional[str] = None,
|
|
25
|
+
playbook_name: Optional[str] = None,
|
|
26
|
+
when_to_use: Optional[str] = None,
|
|
27
|
+
) -> ActionResult:
|
|
28
|
+
"""Record a correction/lesson. It is stored as pending_review and must pass
|
|
29
|
+
human sign-off (via review()) before it spreads company-wide."""
|
|
30
|
+
arguments: Dict[str, Any] = {"correction": correction, "correction_type": correction_type}
|
|
31
|
+
if reviewer:
|
|
32
|
+
arguments["reviewer"] = reviewer
|
|
33
|
+
if reason:
|
|
34
|
+
arguments["reason"] = reason
|
|
35
|
+
if playbook_name:
|
|
36
|
+
arguments["playbook_name"] = playbook_name
|
|
37
|
+
if when_to_use:
|
|
38
|
+
arguments["when_to_use"] = when_to_use
|
|
39
|
+
result = self._transport.call_tool("company_learn", arguments)
|
|
40
|
+
return ActionResult.from_result(result)
|
|
41
|
+
|
|
42
|
+
def map(self, *, playbooks: bool = True) -> ActionResult:
|
|
43
|
+
"""Read the current company map (front door: priorities, source order, nav)."""
|
|
44
|
+
result = self._transport.call_tool("company_map", {"playbooks": playbooks})
|
|
45
|
+
return ActionResult.from_result(result)
|
|
46
|
+
|
|
47
|
+
def set_map(self, map_text: str) -> ActionResult:
|
|
48
|
+
"""Upsert a new company map. Map is stored as a tagged, recallable memory."""
|
|
49
|
+
result = self._transport.call_tool("company_map", {"set_map": True, "map_text": map_text})
|
|
50
|
+
return ActionResult.from_result(result)
|
|
51
|
+
|
|
52
|
+
def review(self, run: bool = True) -> ActionResult:
|
|
53
|
+
"""List pending learnings awaiting human sign-off. run=True lists them."""
|
|
54
|
+
result = self._transport.call_tool("company_review", {"run": run})
|
|
55
|
+
return ActionResult.from_result(result)
|
|
56
|
+
|
|
57
|
+
def approve(self, queue_id: str) -> ActionResult:
|
|
58
|
+
"""Approve a pending propagation request so it spreads company-wide."""
|
|
59
|
+
result = self._transport.call_tool("review_propagation", {"queue_id": queue_id, "action": "approve"})
|
|
60
|
+
return ActionResult.from_result(result)
|
|
61
|
+
|
|
62
|
+
def reject(self, queue_id: str) -> ActionResult:
|
|
63
|
+
"""Reject a pending propagation request (stays restricted)."""
|
|
64
|
+
result = self._transport.call_tool("review_propagation", {"queue_id": queue_id, "action": "reject"})
|
|
65
|
+
return ActionResult.from_result(result)
|
|
66
|
+
|
|
67
|
+
def pending(self, org_id: Optional[str] = None) -> List[Dict[str, Any]]:
|
|
68
|
+
"""List the raw pending propagation queue entries."""
|
|
69
|
+
arguments: Dict[str, Any] = {}
|
|
70
|
+
if org_id:
|
|
71
|
+
arguments["org_id"] = org_id
|
|
72
|
+
result = self._transport.call_tool("list_propagation_queue", arguments)
|
|
73
|
+
data = ActionResult.from_result(result).data
|
|
74
|
+
items = data.get("pending_requests") or []
|
|
75
|
+
return items if isinstance(items, list) else []
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""ContextStore SDK — context namespace (snapshot, checkin)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Dict, Optional
|
|
6
|
+
|
|
7
|
+
from ._models import ActionResult, SnapshotResult
|
|
8
|
+
from ._transport import Transport
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ContextNamespace:
|
|
12
|
+
"""Startup + session context (get_context_snapshot / smart_checkin)."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, transport: Transport) -> None:
|
|
15
|
+
self._transport = transport
|
|
16
|
+
|
|
17
|
+
def snapshot(self) -> SnapshotResult:
|
|
18
|
+
"""Load user profile, project conventions, and recent history. Call first."""
|
|
19
|
+
result = self._transport.call_tool("get_context_snapshot", {})
|
|
20
|
+
return SnapshotResult.from_result(result)
|
|
21
|
+
|
|
22
|
+
def checkin(self, user_intent: Optional[str] = None) -> ActionResult:
|
|
23
|
+
"""Optional richer startup loader: active_thread, asks, suggestions."""
|
|
24
|
+
arguments: Dict[str, Any] = {}
|
|
25
|
+
if user_intent:
|
|
26
|
+
arguments["user_intent"] = user_intent
|
|
27
|
+
result = self._transport.call_tool("smart_checkin", arguments)
|
|
28
|
+
return ActionResult.from_result(result)
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""ContextStore SDK — memory namespace (store, recall, log, list)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Dict, List, Optional
|
|
6
|
+
|
|
7
|
+
from ._models import ActionResult, QueryResult
|
|
8
|
+
from ._transport import Transport
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class MemoryNamespace:
|
|
12
|
+
"""Typesafe access to the brain's memory layer (memory_store / memory_recall)."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, transport: Transport) -> None:
|
|
15
|
+
self._transport = transport
|
|
16
|
+
|
|
17
|
+
def add(
|
|
18
|
+
self,
|
|
19
|
+
content: str,
|
|
20
|
+
*,
|
|
21
|
+
memory_type: str = "general",
|
|
22
|
+
tags: Optional[List[str]] = None,
|
|
23
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
24
|
+
project: Optional[str] = None,
|
|
25
|
+
source: Optional[str] = "sdk",
|
|
26
|
+
team_id: Optional[str] = None,
|
|
27
|
+
confidence_score: Optional[float] = None,
|
|
28
|
+
) -> ActionResult:
|
|
29
|
+
"""Remember a durable fact. Maps to the server's memory_store tool.
|
|
30
|
+
|
|
31
|
+
`metadata` is a convenience map merged into `tags`/`source`; keys other
|
|
32
|
+
than the tool's known params are passed through as tags so nothing is lost.
|
|
33
|
+
"""
|
|
34
|
+
arguments: Dict[str, Any] = {
|
|
35
|
+
"content": content,
|
|
36
|
+
"memory_type": memory_type,
|
|
37
|
+
"tags": tags or [],
|
|
38
|
+
"source": source or "sdk",
|
|
39
|
+
}
|
|
40
|
+
if project:
|
|
41
|
+
arguments["project"] = project
|
|
42
|
+
if team_id:
|
|
43
|
+
arguments["team_id"] = team_id
|
|
44
|
+
if confidence_score is not None:
|
|
45
|
+
arguments["confidence_score"] = confidence_score
|
|
46
|
+
if metadata:
|
|
47
|
+
for key, value in metadata.items():
|
|
48
|
+
if key in arguments:
|
|
49
|
+
arguments[key] = value
|
|
50
|
+
elif isinstance(value, str):
|
|
51
|
+
arguments.setdefault("tags", []).append(f"{key}:{value}")
|
|
52
|
+
result = self._transport.call_tool("memory_store", arguments)
|
|
53
|
+
return ActionResult.from_result(result)
|
|
54
|
+
|
|
55
|
+
def query(
|
|
56
|
+
self,
|
|
57
|
+
query: str,
|
|
58
|
+
*,
|
|
59
|
+
top_k: int = 5,
|
|
60
|
+
memory_type: Optional[str] = None,
|
|
61
|
+
project: Optional[str] = None,
|
|
62
|
+
user_intent: Optional[str] = None,
|
|
63
|
+
include_scratchpad: bool = False,
|
|
64
|
+
) -> QueryResult:
|
|
65
|
+
"""Ask the company brain a question; returns `answer` + `sources`."""
|
|
66
|
+
arguments: Dict[str, Any] = {"query": query, "top_k": top_k}
|
|
67
|
+
if memory_type:
|
|
68
|
+
arguments["memory_type"] = memory_type
|
|
69
|
+
if project:
|
|
70
|
+
arguments["project"] = project
|
|
71
|
+
if user_intent:
|
|
72
|
+
arguments["user_intent"] = user_intent
|
|
73
|
+
if include_scratchpad:
|
|
74
|
+
arguments["include_scratchpad"] = True
|
|
75
|
+
result = self._transport.call_tool("memory_recall", arguments)
|
|
76
|
+
return QueryResult.from_result(result)
|
|
77
|
+
|
|
78
|
+
def log_turn(self, role: str, content: str, *, thread_id: Optional[str] = None, thread_label: Optional[str] = None) -> ActionResult:
|
|
79
|
+
"""Log a conversation turn (role='user' or 'assistant')."""
|
|
80
|
+
arguments: Dict[str, Any] = {"role": role, "content": content}
|
|
81
|
+
if thread_id:
|
|
82
|
+
arguments["thread_id"] = thread_id
|
|
83
|
+
if thread_label:
|
|
84
|
+
arguments["thread_label"] = thread_label
|
|
85
|
+
result = self._transport.call_tool("log_conversation_turn", arguments)
|
|
86
|
+
return ActionResult.from_result(result)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""ContextStore SDK — permissions & action-authority namespace (admin tools)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Dict, Optional
|
|
6
|
+
|
|
7
|
+
from ._models import ActionResult
|
|
8
|
+
from ._transport import Transport
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class PermissionsNamespace:
|
|
12
|
+
"""Admin controls: grant/revoke tool permissions and check action authority.
|
|
13
|
+
These require an API key with the 'admin' scope."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, transport: Transport) -> None:
|
|
16
|
+
self._transport = transport
|
|
17
|
+
|
|
18
|
+
def grant(self, *, agent_id: str, tool_name: str, allowed: bool = True) -> ActionResult:
|
|
19
|
+
result = self._transport.call_tool(
|
|
20
|
+
"grant_permission",
|
|
21
|
+
{"agent_id": agent_id, "tool_name": tool_name, "allowed": allowed},
|
|
22
|
+
)
|
|
23
|
+
return ActionResult.from_result(result)
|
|
24
|
+
|
|
25
|
+
def revoke(self, *, agent_id: str, tool_name: str) -> ActionResult:
|
|
26
|
+
result = self._transport.call_tool(
|
|
27
|
+
"revoke_permission",
|
|
28
|
+
{"agent_id": agent_id, "tool_name": tool_name},
|
|
29
|
+
)
|
|
30
|
+
return ActionResult.from_result(result)
|
|
31
|
+
|
|
32
|
+
def list(self, agent_id: Optional[str] = None) -> ActionResult:
|
|
33
|
+
arguments: Dict[str, Any] = {}
|
|
34
|
+
if agent_id:
|
|
35
|
+
arguments["agent_id"] = agent_id
|
|
36
|
+
result = self._transport.call_tool("list_permissions", arguments)
|
|
37
|
+
return ActionResult.from_result(result)
|
|
38
|
+
|
|
39
|
+
def check_authority(self, memory_id: str, requested_action: str) -> ActionResult:
|
|
40
|
+
"""Verify whether a memory's stored action_authority allows an action
|
|
41
|
+
(e.g. 'act:auto_email', 'act:deploy', 'act:refund'). Queues for human
|
|
42
|
+
sign-off if unauthorized."""
|
|
43
|
+
result = self._transport.call_tool(
|
|
44
|
+
"check_action_authority",
|
|
45
|
+
{"memory_id": memory_id, "requested_action": requested_action},
|
|
46
|
+
)
|
|
47
|
+
return ActionResult.from_result(result)
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: contextstore-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Client SDK for the ContextStore company brain — compiled memory and agent team, accessed via a typed Python API.
|
|
5
|
+
Author: ContextStore
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://contextstore.oritm.tech
|
|
8
|
+
Keywords: memory,mcp,second-brain,agents,knowledge-base
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
12
|
+
Requires-Python: >=3.8
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
# ContextStore Python SDK
|
|
16
|
+
|
|
17
|
+
Talk to your ContextStore **company brain** — compiled memory + agent team — from
|
|
18
|
+
Python. The SDK is a typed client over your backend's MCP JSON-RPC layer.
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pip install contextstore-sdk
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Quickstart
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
import os
|
|
30
|
+
from contextstore import ContextStore
|
|
31
|
+
|
|
32
|
+
client = ContextStore(api_key=os.environ["CONTEXTSTORE_KEY"])
|
|
33
|
+
|
|
34
|
+
# Remember something
|
|
35
|
+
client.memory.add(
|
|
36
|
+
content="Board approved usage-based pricing on Sep 12",
|
|
37
|
+
metadata={"project": "pricing", "owner": "maya"},
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
# Ask the company brain — with sources
|
|
41
|
+
res = client.memory.query("What did we decide about pricing?")
|
|
42
|
+
print(res.answer)
|
|
43
|
+
print(res.sources)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Local / self-hosted
|
|
47
|
+
|
|
48
|
+
Point at your own backend (default is the hosted one):
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
client = ContextStore(
|
|
52
|
+
api_key=os.environ["CONTEXTSTORE_KEY"],
|
|
53
|
+
base_url="https://contextstore.onrender.com",
|
|
54
|
+
)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
For local development you can pass a JWT instead of an api_key:
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
client = ContextStore(token="<jwt>")
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Namespaces / methods
|
|
64
|
+
|
|
65
|
+
| Python | Backend MCP tool |
|
|
66
|
+
|---|---|
|
|
67
|
+
| `client.memory.add(...)` | `memory_store` |
|
|
68
|
+
| `client.memory.query(...)` | `memory_recall` |
|
|
69
|
+
| `client.memory.log_turn(...)` | `log_conversation_turn` |
|
|
70
|
+
| `client.context.snapshot()` | `get_context_snapshot` |
|
|
71
|
+
| `client.context.checkin(...)` | `smart_checkin` |
|
|
72
|
+
| `client.company.learn(...)` | `company_learn` |
|
|
73
|
+
| `client.company.map()` / `.set_map(...)` | `company_map` |
|
|
74
|
+
| `client.company.review(run=True)` | `company_review` |
|
|
75
|
+
| `client.company.approve(...)` / `.reject(...)` | `review_propagation` |
|
|
76
|
+
| `client.company.pending(...)` | `list_propagation_queue` |
|
|
77
|
+
| `client.permissions.grant/revoke/list(...)` | permission admin tools |
|
|
78
|
+
| `client.permissions.check_authority(...)` | `check_action_authority` |
|
|
79
|
+
|
|
80
|
+
## Errors
|
|
81
|
+
|
|
82
|
+
- `AuthenticationError` — bad/expired/scoped-out API key
|
|
83
|
+
- `ApiError` — the MCP tool returned an error
|
|
84
|
+
- `ContextStoreError` — network / transport issues
|
|
85
|
+
|
|
86
|
+
No external dependencies — uses only the Python standard library.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
contextstore/__init__.py
|
|
4
|
+
contextstore/_models.py
|
|
5
|
+
contextstore/_transport.py
|
|
6
|
+
contextstore/client.py
|
|
7
|
+
contextstore/company.py
|
|
8
|
+
contextstore/context.py
|
|
9
|
+
contextstore/memory.py
|
|
10
|
+
contextstore/permissions.py
|
|
11
|
+
contextstore/py.typed
|
|
12
|
+
contextstore_sdk.egg-info/PKG-INFO
|
|
13
|
+
contextstore_sdk.egg-info/SOURCES.txt
|
|
14
|
+
contextstore_sdk.egg-info/dependency_links.txt
|
|
15
|
+
contextstore_sdk.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
contextstore
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "contextstore-sdk"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Client SDK for the ContextStore company brain — compiled memory and agent team, accessed via a typed Python API."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "ContextStore" }]
|
|
13
|
+
keywords = ["memory", "mcp", "second-brain", "agents", "knowledge-base"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Operating System :: OS Independent",
|
|
17
|
+
"Topic :: Software Development :: Libraries",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[project.urls]
|
|
21
|
+
Homepage = "https://contextstore.oritm.tech"
|
|
22
|
+
|
|
23
|
+
[tool.setuptools]
|
|
24
|
+
packages = ["contextstore"]
|
|
25
|
+
|
|
26
|
+
[tool.setuptools.package-data]
|
|
27
|
+
contextstore = ["py.typed"]
|