switchy-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.
- switchy_sdk-0.1.0/.gitignore +8 -0
- switchy_sdk-0.1.0/PKG-INFO +139 -0
- switchy_sdk-0.1.0/README.md +111 -0
- switchy_sdk-0.1.0/pyproject.toml +39 -0
- switchy_sdk-0.1.0/switchy/__init__.py +17 -0
- switchy_sdk-0.1.0/switchy/client.py +372 -0
- switchy_sdk-0.1.0/switchy/errors.py +39 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: switchy-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Official Python SDK for the Switchy AI memory and chat API
|
|
5
|
+
Project-URL: Homepage, https://switchy.build
|
|
6
|
+
Project-URL: Documentation, https://switchy.build/api-dashboard
|
|
7
|
+
Project-URL: Repository, https://github.com/Switchy-AI/switchy
|
|
8
|
+
Project-URL: Bug Tracker, https://github.com/Switchy-AI/switchy/issues
|
|
9
|
+
Author-email: Switchy AI <contact@switchy.build>
|
|
10
|
+
License: MIT
|
|
11
|
+
Keywords: ai,knowledge-graph,memory,openrouter,sdk,switchy
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
+
Requires-Python: >=3.9
|
|
22
|
+
Requires-Dist: httpx>=0.25.0
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: pytest-asyncio>=0.21; extra == 'dev'
|
|
25
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
26
|
+
Requires-Dist: ruff>=0.1.0; extra == 'dev'
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# switchy-sdk
|
|
30
|
+
|
|
31
|
+
Official Python SDK for the [Switchy AI](https://switchy.build) memory, knowledge-graph, and multi-model chat API.
|
|
32
|
+
|
|
33
|
+
## Install
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install switchy-sdk
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Quick start
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
from switchy import Switchy
|
|
43
|
+
|
|
44
|
+
client = Switchy(api_key="switchy_...")
|
|
45
|
+
|
|
46
|
+
response = client.chat.complete(
|
|
47
|
+
model="anthropic/claude-sonnet-4",
|
|
48
|
+
message="Summarise my recent project notes",
|
|
49
|
+
memory={"enabled": True, "extractMemories": True},
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
print(response["message"]["content"])
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Streaming
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
for chunk in client.chat.stream(
|
|
59
|
+
model="openai/gpt-5",
|
|
60
|
+
message="Write a haiku about memory",
|
|
61
|
+
):
|
|
62
|
+
if chunk.get("type") == "token":
|
|
63
|
+
print(chunk.get("content", ""), end="", flush=True)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Async
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
import asyncio
|
|
70
|
+
from switchy import AsyncSwitchy
|
|
71
|
+
|
|
72
|
+
async def main():
|
|
73
|
+
async with AsyncSwitchy(api_key="switchy_...") as client:
|
|
74
|
+
res = await client.chat.complete(
|
|
75
|
+
model="anthropic/claude-sonnet-4",
|
|
76
|
+
message="Hello",
|
|
77
|
+
)
|
|
78
|
+
print(res["message"]["content"])
|
|
79
|
+
|
|
80
|
+
asyncio.run(main())
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Memory
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
# Create a namespace
|
|
87
|
+
client.namespaces.create(name="my-project")
|
|
88
|
+
|
|
89
|
+
# Store a memory frame
|
|
90
|
+
client.memory.create_frame(
|
|
91
|
+
"my-project",
|
|
92
|
+
content="User prefers dark mode and TypeScript",
|
|
93
|
+
metadata={"source": "onboarding"},
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
# Contextual retrieval
|
|
97
|
+
relevant = client.memory.context("my-project", query="user preferences", limit=5)
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Knowledge graph
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
client.knowledge_graph.create_entity(
|
|
104
|
+
"my-project", name="AuthService", type="service"
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
client.knowledge_graph.create_relation(
|
|
108
|
+
"my-project", source="AuthService", target="User", type="authenticates"
|
|
109
|
+
)
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Error handling
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
from switchy import Switchy, SwitchyError, RateLimitError
|
|
116
|
+
|
|
117
|
+
try:
|
|
118
|
+
client.chat.complete(model="openai/gpt-5", message="hi")
|
|
119
|
+
except RateLimitError as e:
|
|
120
|
+
print(f"Rate limited, retry in {e.retry_after}s (limit={e.limit})")
|
|
121
|
+
except SwitchyError as e:
|
|
122
|
+
print(f"API error: {e.code} — {e}")
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## API reference
|
|
126
|
+
|
|
127
|
+
- `chat.complete(...)` — single-turn completion
|
|
128
|
+
- `chat.stream(...)` — SSE streaming iterator
|
|
129
|
+
- `models.list(featured=True)` — list available models (350+)
|
|
130
|
+
- `namespaces.{create,list,get,update,delete}` — memory namespace management
|
|
131
|
+
- `memory.{create_frame,list_frames,context,semantic,search,bridge,consolidate}` — memory operations
|
|
132
|
+
- `knowledge_graph.{create_entity,create_relation,query}` — graph operations
|
|
133
|
+
- `sessions.{create,list,get}` — session management
|
|
134
|
+
|
|
135
|
+
Full OpenAPI spec: https://switchy.build/api/v1/openapi.json
|
|
136
|
+
|
|
137
|
+
## License
|
|
138
|
+
|
|
139
|
+
MIT
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# switchy-sdk
|
|
2
|
+
|
|
3
|
+
Official Python SDK for the [Switchy AI](https://switchy.build) memory, knowledge-graph, and multi-model chat API.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install switchy-sdk
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick start
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from switchy import Switchy
|
|
15
|
+
|
|
16
|
+
client = Switchy(api_key="switchy_...")
|
|
17
|
+
|
|
18
|
+
response = client.chat.complete(
|
|
19
|
+
model="anthropic/claude-sonnet-4",
|
|
20
|
+
message="Summarise my recent project notes",
|
|
21
|
+
memory={"enabled": True, "extractMemories": True},
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
print(response["message"]["content"])
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Streaming
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
for chunk in client.chat.stream(
|
|
31
|
+
model="openai/gpt-5",
|
|
32
|
+
message="Write a haiku about memory",
|
|
33
|
+
):
|
|
34
|
+
if chunk.get("type") == "token":
|
|
35
|
+
print(chunk.get("content", ""), end="", flush=True)
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Async
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
import asyncio
|
|
42
|
+
from switchy import AsyncSwitchy
|
|
43
|
+
|
|
44
|
+
async def main():
|
|
45
|
+
async with AsyncSwitchy(api_key="switchy_...") as client:
|
|
46
|
+
res = await client.chat.complete(
|
|
47
|
+
model="anthropic/claude-sonnet-4",
|
|
48
|
+
message="Hello",
|
|
49
|
+
)
|
|
50
|
+
print(res["message"]["content"])
|
|
51
|
+
|
|
52
|
+
asyncio.run(main())
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Memory
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
# Create a namespace
|
|
59
|
+
client.namespaces.create(name="my-project")
|
|
60
|
+
|
|
61
|
+
# Store a memory frame
|
|
62
|
+
client.memory.create_frame(
|
|
63
|
+
"my-project",
|
|
64
|
+
content="User prefers dark mode and TypeScript",
|
|
65
|
+
metadata={"source": "onboarding"},
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
# Contextual retrieval
|
|
69
|
+
relevant = client.memory.context("my-project", query="user preferences", limit=5)
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Knowledge graph
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
client.knowledge_graph.create_entity(
|
|
76
|
+
"my-project", name="AuthService", type="service"
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
client.knowledge_graph.create_relation(
|
|
80
|
+
"my-project", source="AuthService", target="User", type="authenticates"
|
|
81
|
+
)
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Error handling
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
from switchy import Switchy, SwitchyError, RateLimitError
|
|
88
|
+
|
|
89
|
+
try:
|
|
90
|
+
client.chat.complete(model="openai/gpt-5", message="hi")
|
|
91
|
+
except RateLimitError as e:
|
|
92
|
+
print(f"Rate limited, retry in {e.retry_after}s (limit={e.limit})")
|
|
93
|
+
except SwitchyError as e:
|
|
94
|
+
print(f"API error: {e.code} — {e}")
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## API reference
|
|
98
|
+
|
|
99
|
+
- `chat.complete(...)` — single-turn completion
|
|
100
|
+
- `chat.stream(...)` — SSE streaming iterator
|
|
101
|
+
- `models.list(featured=True)` — list available models (350+)
|
|
102
|
+
- `namespaces.{create,list,get,update,delete}` — memory namespace management
|
|
103
|
+
- `memory.{create_frame,list_frames,context,semantic,search,bridge,consolidate}` — memory operations
|
|
104
|
+
- `knowledge_graph.{create_entity,create_relation,query}` — graph operations
|
|
105
|
+
- `sessions.{create,list,get}` — session management
|
|
106
|
+
|
|
107
|
+
Full OpenAPI spec: https://switchy.build/api/v1/openapi.json
|
|
108
|
+
|
|
109
|
+
## License
|
|
110
|
+
|
|
111
|
+
MIT
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "switchy-sdk"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Official Python SDK for the Switchy AI memory and chat API"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "Switchy AI", email = "contact@switchy.build" }]
|
|
13
|
+
keywords = ["switchy", "ai", "memory", "knowledge-graph", "openrouter", "sdk"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.9",
|
|
20
|
+
"Programming Language :: Python :: 3.10",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
24
|
+
]
|
|
25
|
+
dependencies = [
|
|
26
|
+
"httpx>=0.25.0",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[project.optional-dependencies]
|
|
30
|
+
dev = ["pytest>=7.0", "pytest-asyncio>=0.21", "ruff>=0.1.0"]
|
|
31
|
+
|
|
32
|
+
[project.urls]
|
|
33
|
+
Homepage = "https://switchy.build"
|
|
34
|
+
Documentation = "https://switchy.build/api-dashboard"
|
|
35
|
+
Repository = "https://github.com/Switchy-AI/switchy"
|
|
36
|
+
"Bug Tracker" = "https://github.com/Switchy-AI/switchy/issues"
|
|
37
|
+
|
|
38
|
+
[tool.hatch.build.targets.wheel]
|
|
39
|
+
packages = ["switchy"]
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Switchy AI Python SDK.
|
|
2
|
+
|
|
3
|
+
Install:
|
|
4
|
+
pip install switchy-sdk
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
from switchy import Switchy
|
|
8
|
+
client = Switchy(api_key="switchy_...")
|
|
9
|
+
res = client.chat.complete(model="anthropic/claude-sonnet-4", message="Hello")
|
|
10
|
+
print(res["message"]["content"])
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from .client import Switchy, AsyncSwitchy
|
|
14
|
+
from .errors import SwitchyError, RateLimitError
|
|
15
|
+
|
|
16
|
+
__version__ = "0.1.0"
|
|
17
|
+
__all__ = ["Switchy", "AsyncSwitchy", "SwitchyError", "RateLimitError"]
|
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
"""Synchronous and asynchronous Switchy API clients."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import Any, AsyncIterator, Dict, Iterator, List, Optional
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
|
|
10
|
+
from .errors import RateLimitError, SwitchyError
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
DEFAULT_BASE_URL = "https://switchy.build/api/v1"
|
|
14
|
+
DEFAULT_TIMEOUT = 60.0
|
|
15
|
+
USER_AGENT = "switchy-sdk-python/0.1.0"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# ---------------------------------------------------------------------------
|
|
19
|
+
# Shared helpers
|
|
20
|
+
# ---------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
def _unwrap(resp: httpx.Response) -> Any:
|
|
23
|
+
"""Parse a Switchy API envelope and return `data`, raising on error."""
|
|
24
|
+
if resp.status_code == 429:
|
|
25
|
+
try:
|
|
26
|
+
body = resp.json()
|
|
27
|
+
except Exception:
|
|
28
|
+
body = {}
|
|
29
|
+
err = body.get("error") or {}
|
|
30
|
+
raise RateLimitError(err.get("message") or "Rate limit exceeded", err.get("details"))
|
|
31
|
+
if resp.status_code >= 400:
|
|
32
|
+
try:
|
|
33
|
+
body = resp.json()
|
|
34
|
+
except Exception:
|
|
35
|
+
raise SwitchyError(f"HTTP {resp.status_code}", "HTTP_ERROR", resp.status_code)
|
|
36
|
+
err = body.get("error") or {}
|
|
37
|
+
raise SwitchyError(
|
|
38
|
+
err.get("message") or f"HTTP {resp.status_code}",
|
|
39
|
+
err.get("code") or "HTTP_ERROR",
|
|
40
|
+
resp.status_code,
|
|
41
|
+
err.get("details"),
|
|
42
|
+
)
|
|
43
|
+
body = resp.json()
|
|
44
|
+
if not body.get("success"):
|
|
45
|
+
err = body.get("error") or {}
|
|
46
|
+
raise SwitchyError(
|
|
47
|
+
err.get("message") or "Unknown error",
|
|
48
|
+
err.get("code") or "UNKNOWN",
|
|
49
|
+
resp.status_code,
|
|
50
|
+
err.get("details"),
|
|
51
|
+
)
|
|
52
|
+
return body.get("data")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _clean_params(params: Optional[Dict[str, Any]]) -> Dict[str, Any]:
|
|
56
|
+
if not params:
|
|
57
|
+
return {}
|
|
58
|
+
return {k: v for k, v in params.items() if v is not None}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# ---------------------------------------------------------------------------
|
|
62
|
+
# Sync client
|
|
63
|
+
# ---------------------------------------------------------------------------
|
|
64
|
+
|
|
65
|
+
class Switchy:
|
|
66
|
+
"""Synchronous Switchy API client."""
|
|
67
|
+
|
|
68
|
+
def __init__(
|
|
69
|
+
self,
|
|
70
|
+
api_key: str,
|
|
71
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
72
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
73
|
+
client: Optional[httpx.Client] = None,
|
|
74
|
+
) -> None:
|
|
75
|
+
if not api_key:
|
|
76
|
+
raise ValueError("api_key is required")
|
|
77
|
+
self.api_key = api_key
|
|
78
|
+
self.base_url = base_url.rstrip("/")
|
|
79
|
+
self._client = client or httpx.Client(
|
|
80
|
+
timeout=timeout,
|
|
81
|
+
headers={
|
|
82
|
+
"Authorization": f"Bearer {api_key}",
|
|
83
|
+
"Content-Type": "application/json",
|
|
84
|
+
"User-Agent": USER_AGENT,
|
|
85
|
+
},
|
|
86
|
+
)
|
|
87
|
+
self.chat = _Chat(self)
|
|
88
|
+
self.models = _Models(self)
|
|
89
|
+
self.memory = _Memory(self)
|
|
90
|
+
self.namespaces = _Namespaces(self)
|
|
91
|
+
self.sessions = _Sessions(self)
|
|
92
|
+
self.knowledge_graph = _KnowledgeGraph(self)
|
|
93
|
+
|
|
94
|
+
# ── internal ───────────────────────────────────────────────────────
|
|
95
|
+
|
|
96
|
+
def _request(
|
|
97
|
+
self,
|
|
98
|
+
method: str,
|
|
99
|
+
path: str,
|
|
100
|
+
json_body: Optional[Any] = None,
|
|
101
|
+
params: Optional[Dict[str, Any]] = None,
|
|
102
|
+
) -> Any:
|
|
103
|
+
resp = self._client.request(
|
|
104
|
+
method,
|
|
105
|
+
self.base_url + path,
|
|
106
|
+
json=json_body,
|
|
107
|
+
params=_clean_params(params),
|
|
108
|
+
)
|
|
109
|
+
return _unwrap(resp)
|
|
110
|
+
|
|
111
|
+
def _stream(self, path: str, json_body: Any) -> Iterator[Dict[str, Any]]:
|
|
112
|
+
with self._client.stream(
|
|
113
|
+
"POST",
|
|
114
|
+
self.base_url + path,
|
|
115
|
+
json=json_body,
|
|
116
|
+
headers={"Accept": "text/event-stream"},
|
|
117
|
+
) as resp:
|
|
118
|
+
if resp.status_code != 200:
|
|
119
|
+
resp.read()
|
|
120
|
+
_unwrap(resp)
|
|
121
|
+
for line in resp.iter_lines():
|
|
122
|
+
if not line or not line.startswith("data:"):
|
|
123
|
+
continue
|
|
124
|
+
data = line[5:].strip()
|
|
125
|
+
if not data or data == "[DONE]":
|
|
126
|
+
continue
|
|
127
|
+
try:
|
|
128
|
+
yield json.loads(data)
|
|
129
|
+
except json.JSONDecodeError:
|
|
130
|
+
continue
|
|
131
|
+
|
|
132
|
+
def close(self) -> None:
|
|
133
|
+
self._client.close()
|
|
134
|
+
|
|
135
|
+
def __enter__(self) -> "Switchy":
|
|
136
|
+
return self
|
|
137
|
+
|
|
138
|
+
def __exit__(self, *a: Any) -> None:
|
|
139
|
+
self.close()
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
# ---------------------------------------------------------------------------
|
|
143
|
+
# Async client
|
|
144
|
+
# ---------------------------------------------------------------------------
|
|
145
|
+
|
|
146
|
+
class AsyncSwitchy:
|
|
147
|
+
"""Asynchronous Switchy API client (asyncio)."""
|
|
148
|
+
|
|
149
|
+
def __init__(
|
|
150
|
+
self,
|
|
151
|
+
api_key: str,
|
|
152
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
153
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
154
|
+
client: Optional[httpx.AsyncClient] = None,
|
|
155
|
+
) -> None:
|
|
156
|
+
if not api_key:
|
|
157
|
+
raise ValueError("api_key is required")
|
|
158
|
+
self.api_key = api_key
|
|
159
|
+
self.base_url = base_url.rstrip("/")
|
|
160
|
+
self._client = client or httpx.AsyncClient(
|
|
161
|
+
timeout=timeout,
|
|
162
|
+
headers={
|
|
163
|
+
"Authorization": f"Bearer {api_key}",
|
|
164
|
+
"Content-Type": "application/json",
|
|
165
|
+
"User-Agent": USER_AGENT,
|
|
166
|
+
},
|
|
167
|
+
)
|
|
168
|
+
self.chat = _AsyncChat(self)
|
|
169
|
+
self.models = _AsyncModels(self)
|
|
170
|
+
self.memory = _AsyncMemory(self)
|
|
171
|
+
self.namespaces = _AsyncNamespaces(self)
|
|
172
|
+
self.sessions = _AsyncSessions(self)
|
|
173
|
+
self.knowledge_graph = _AsyncKnowledgeGraph(self)
|
|
174
|
+
|
|
175
|
+
async def _request(
|
|
176
|
+
self,
|
|
177
|
+
method: str,
|
|
178
|
+
path: str,
|
|
179
|
+
json_body: Optional[Any] = None,
|
|
180
|
+
params: Optional[Dict[str, Any]] = None,
|
|
181
|
+
) -> Any:
|
|
182
|
+
resp = await self._client.request(
|
|
183
|
+
method,
|
|
184
|
+
self.base_url + path,
|
|
185
|
+
json=json_body,
|
|
186
|
+
params=_clean_params(params),
|
|
187
|
+
)
|
|
188
|
+
return _unwrap(resp)
|
|
189
|
+
|
|
190
|
+
async def _stream(self, path: str, json_body: Any) -> AsyncIterator[Dict[str, Any]]:
|
|
191
|
+
async with self._client.stream(
|
|
192
|
+
"POST",
|
|
193
|
+
self.base_url + path,
|
|
194
|
+
json=json_body,
|
|
195
|
+
headers={"Accept": "text/event-stream"},
|
|
196
|
+
) as resp:
|
|
197
|
+
if resp.status_code != 200:
|
|
198
|
+
await resp.aread()
|
|
199
|
+
_unwrap(resp)
|
|
200
|
+
async for line in resp.aiter_lines():
|
|
201
|
+
if not line or not line.startswith("data:"):
|
|
202
|
+
continue
|
|
203
|
+
data = line[5:].strip()
|
|
204
|
+
if not data or data == "[DONE]":
|
|
205
|
+
continue
|
|
206
|
+
try:
|
|
207
|
+
yield json.loads(data)
|
|
208
|
+
except json.JSONDecodeError:
|
|
209
|
+
continue
|
|
210
|
+
|
|
211
|
+
async def close(self) -> None:
|
|
212
|
+
await self._client.aclose()
|
|
213
|
+
|
|
214
|
+
async def __aenter__(self) -> "AsyncSwitchy":
|
|
215
|
+
return self
|
|
216
|
+
|
|
217
|
+
async def __aexit__(self, *a: Any) -> None:
|
|
218
|
+
await self.close()
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
# ---------------------------------------------------------------------------
|
|
222
|
+
# Resources (sync)
|
|
223
|
+
# ---------------------------------------------------------------------------
|
|
224
|
+
|
|
225
|
+
class _Chat:
|
|
226
|
+
def __init__(self, c: Switchy) -> None: self._c = c
|
|
227
|
+
def complete(self, *, model: str, message: str, **kwargs: Any) -> Dict[str, Any]:
|
|
228
|
+
body = {"model": model, "message": message, "stream": False, **kwargs}
|
|
229
|
+
return self._c._request("POST", "/chat", body)
|
|
230
|
+
def stream(self, *, model: str, message: str, **kwargs: Any) -> Iterator[Dict[str, Any]]:
|
|
231
|
+
body = {"model": model, "message": message, "stream": True, **kwargs}
|
|
232
|
+
return self._c._stream("/chat", body)
|
|
233
|
+
|
|
234
|
+
class _Models:
|
|
235
|
+
def __init__(self, c: Switchy) -> None: self._c = c
|
|
236
|
+
def list(self, *, category: Optional[str] = None, q: Optional[str] = None, featured: Optional[bool] = None) -> Dict[str, Any]:
|
|
237
|
+
return self._c._request("GET", "/models", params={"category": category, "q": q, "featured": featured})
|
|
238
|
+
|
|
239
|
+
class _Namespaces:
|
|
240
|
+
def __init__(self, c: Switchy) -> None: self._c = c
|
|
241
|
+
def create(self, *, name: str, description: Optional[str] = None) -> Dict[str, Any]:
|
|
242
|
+
return self._c._request("POST", "/namespaces", {"name": name, "description": description})
|
|
243
|
+
def list(self) -> List[Dict[str, Any]]:
|
|
244
|
+
return self._c._request("GET", "/namespaces")
|
|
245
|
+
def get(self, id: str) -> Dict[str, Any]:
|
|
246
|
+
return self._c._request("GET", f"/namespaces/{id}")
|
|
247
|
+
def update(self, id: str, **data: Any) -> Dict[str, Any]:
|
|
248
|
+
return self._c._request("PATCH", f"/namespaces/{id}", data)
|
|
249
|
+
def delete(self, id: str) -> None:
|
|
250
|
+
self._c._request("DELETE", f"/namespaces/{id}")
|
|
251
|
+
|
|
252
|
+
class _Memory:
|
|
253
|
+
def __init__(self, c: Switchy) -> None: self._c = c
|
|
254
|
+
def create_frame(self, namespace: str, *, content: str, metadata: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
255
|
+
return self._c._request("POST", f"/memory/{namespace}/frames", {"content": content, "metadata": metadata})
|
|
256
|
+
def list_frames(self, namespace: str, *, limit: Optional[int] = None, offset: Optional[int] = None, status: Optional[str] = None) -> List[Dict[str, Any]]:
|
|
257
|
+
return self._c._request("GET", f"/memory/{namespace}/frames", params={"limit": limit, "offset": offset, "status": status})
|
|
258
|
+
def get_frame(self, namespace: str, id: str) -> Dict[str, Any]:
|
|
259
|
+
return self._c._request("GET", f"/memory/{namespace}/frames/{id}")
|
|
260
|
+
def update_frame(self, namespace: str, id: str, **data: Any) -> Dict[str, Any]:
|
|
261
|
+
return self._c._request("PATCH", f"/memory/{namespace}/frames/{id}", data)
|
|
262
|
+
def close_frame(self, namespace: str, id: str) -> Dict[str, Any]:
|
|
263
|
+
return self._c._request("POST", f"/memory/{namespace}/frames/{id}/close")
|
|
264
|
+
def context(self, namespace: str, *, query: str, limit: Optional[int] = None) -> Dict[str, Any]:
|
|
265
|
+
return self._c._request("POST", f"/memory/{namespace}/context", {"query": query, "limit": limit})
|
|
266
|
+
def semantic(self, namespace: str, *, query: str, limit: Optional[int] = None, threshold: Optional[float] = None) -> Dict[str, Any]:
|
|
267
|
+
return self._c._request("POST", f"/memory/{namespace}/semantic", {"query": query, "limit": limit, "threshold": threshold})
|
|
268
|
+
def search(self, *, query: str, namespaces: Optional[List[str]] = None, limit: Optional[int] = None) -> Dict[str, Any]:
|
|
269
|
+
return self._c._request("POST", "/memory/search", {"query": query, "namespaces": namespaces, "limit": limit})
|
|
270
|
+
def bridge(self, namespace: str, *, source_namespace: str, frame_ids: Optional[List[str]] = None, mode: str = "link") -> Dict[str, Any]:
|
|
271
|
+
return self._c._request("POST", f"/memory/{namespace}/bridge", {"sourceNamespace": source_namespace, "frameIds": frame_ids, "mode": mode})
|
|
272
|
+
def consolidate(self, namespace: str) -> Dict[str, Any]:
|
|
273
|
+
return self._c._request("POST", f"/memory/{namespace}/consolidate")
|
|
274
|
+
|
|
275
|
+
class _KnowledgeGraph:
|
|
276
|
+
def __init__(self, c: Switchy) -> None: self._c = c
|
|
277
|
+
def list_entities(self, namespace: str) -> List[Dict[str, Any]]:
|
|
278
|
+
return self._c._request("GET", f"/memory/{namespace}/graph/entities")
|
|
279
|
+
def create_entity(self, namespace: str, *, name: str, type: str, metadata: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
280
|
+
return self._c._request("POST", f"/memory/{namespace}/graph/entities", {"name": name, "type": type, "metadata": metadata})
|
|
281
|
+
def list_relations(self, namespace: str) -> List[Dict[str, Any]]:
|
|
282
|
+
return self._c._request("GET", f"/memory/{namespace}/graph/relations")
|
|
283
|
+
def create_relation(self, namespace: str, *, source: str, target: str, type: str, metadata: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
284
|
+
return self._c._request("POST", f"/memory/{namespace}/graph/relations", {"source": source, "target": target, "type": type, "metadata": metadata})
|
|
285
|
+
def query(self, *, query: str, depth: Optional[int] = None, namespace: Optional[str] = None) -> Dict[str, Any]:
|
|
286
|
+
return self._c._request("POST", "/knowledge-graph", {"query": query, "depth": depth, "namespace": namespace})
|
|
287
|
+
|
|
288
|
+
class _Sessions:
|
|
289
|
+
def __init__(self, c: Switchy) -> None: self._c = c
|
|
290
|
+
def create(self, **data: Any) -> Dict[str, Any]:
|
|
291
|
+
return self._c._request("POST", "/sessions", data)
|
|
292
|
+
def list(self) -> List[Dict[str, Any]]:
|
|
293
|
+
return self._c._request("GET", "/sessions")
|
|
294
|
+
def get(self, id: str) -> Dict[str, Any]:
|
|
295
|
+
return self._c._request("GET", f"/sessions/{id}")
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
# ---------------------------------------------------------------------------
|
|
299
|
+
# Resources (async) — mirrors the sync ones
|
|
300
|
+
# ---------------------------------------------------------------------------
|
|
301
|
+
|
|
302
|
+
class _AsyncChat:
|
|
303
|
+
def __init__(self, c: AsyncSwitchy) -> None: self._c = c
|
|
304
|
+
async def complete(self, *, model: str, message: str, **kwargs: Any) -> Dict[str, Any]:
|
|
305
|
+
body = {"model": model, "message": message, "stream": False, **kwargs}
|
|
306
|
+
return await self._c._request("POST", "/chat", body)
|
|
307
|
+
def stream(self, *, model: str, message: str, **kwargs: Any) -> AsyncIterator[Dict[str, Any]]:
|
|
308
|
+
body = {"model": model, "message": message, "stream": True, **kwargs}
|
|
309
|
+
return self._c._stream("/chat", body)
|
|
310
|
+
|
|
311
|
+
class _AsyncModels:
|
|
312
|
+
def __init__(self, c: AsyncSwitchy) -> None: self._c = c
|
|
313
|
+
async def list(self, *, category: Optional[str] = None, q: Optional[str] = None, featured: Optional[bool] = None) -> Dict[str, Any]:
|
|
314
|
+
return await self._c._request("GET", "/models", params={"category": category, "q": q, "featured": featured})
|
|
315
|
+
|
|
316
|
+
class _AsyncNamespaces:
|
|
317
|
+
def __init__(self, c: AsyncSwitchy) -> None: self._c = c
|
|
318
|
+
async def create(self, *, name: str, description: Optional[str] = None) -> Dict[str, Any]:
|
|
319
|
+
return await self._c._request("POST", "/namespaces", {"name": name, "description": description})
|
|
320
|
+
async def list(self) -> List[Dict[str, Any]]:
|
|
321
|
+
return await self._c._request("GET", "/namespaces")
|
|
322
|
+
async def get(self, id: str) -> Dict[str, Any]:
|
|
323
|
+
return await self._c._request("GET", f"/namespaces/{id}")
|
|
324
|
+
async def update(self, id: str, **data: Any) -> Dict[str, Any]:
|
|
325
|
+
return await self._c._request("PATCH", f"/namespaces/{id}", data)
|
|
326
|
+
async def delete(self, id: str) -> None:
|
|
327
|
+
await self._c._request("DELETE", f"/namespaces/{id}")
|
|
328
|
+
|
|
329
|
+
class _AsyncMemory:
|
|
330
|
+
def __init__(self, c: AsyncSwitchy) -> None: self._c = c
|
|
331
|
+
async def create_frame(self, namespace: str, *, content: str, metadata: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
332
|
+
return await self._c._request("POST", f"/memory/{namespace}/frames", {"content": content, "metadata": metadata})
|
|
333
|
+
async def list_frames(self, namespace: str, *, limit: Optional[int] = None, offset: Optional[int] = None, status: Optional[str] = None) -> List[Dict[str, Any]]:
|
|
334
|
+
return await self._c._request("GET", f"/memory/{namespace}/frames", params={"limit": limit, "offset": offset, "status": status})
|
|
335
|
+
async def get_frame(self, namespace: str, id: str) -> Dict[str, Any]:
|
|
336
|
+
return await self._c._request("GET", f"/memory/{namespace}/frames/{id}")
|
|
337
|
+
async def update_frame(self, namespace: str, id: str, **data: Any) -> Dict[str, Any]:
|
|
338
|
+
return await self._c._request("PATCH", f"/memory/{namespace}/frames/{id}", data)
|
|
339
|
+
async def close_frame(self, namespace: str, id: str) -> Dict[str, Any]:
|
|
340
|
+
return await self._c._request("POST", f"/memory/{namespace}/frames/{id}/close")
|
|
341
|
+
async def context(self, namespace: str, *, query: str, limit: Optional[int] = None) -> Dict[str, Any]:
|
|
342
|
+
return await self._c._request("POST", f"/memory/{namespace}/context", {"query": query, "limit": limit})
|
|
343
|
+
async def semantic(self, namespace: str, *, query: str, limit: Optional[int] = None, threshold: Optional[float] = None) -> Dict[str, Any]:
|
|
344
|
+
return await self._c._request("POST", f"/memory/{namespace}/semantic", {"query": query, "limit": limit, "threshold": threshold})
|
|
345
|
+
async def search(self, *, query: str, namespaces: Optional[List[str]] = None, limit: Optional[int] = None) -> Dict[str, Any]:
|
|
346
|
+
return await self._c._request("POST", "/memory/search", {"query": query, "namespaces": namespaces, "limit": limit})
|
|
347
|
+
async def bridge(self, namespace: str, *, source_namespace: str, frame_ids: Optional[List[str]] = None, mode: str = "link") -> Dict[str, Any]:
|
|
348
|
+
return await self._c._request("POST", f"/memory/{namespace}/bridge", {"sourceNamespace": source_namespace, "frameIds": frame_ids, "mode": mode})
|
|
349
|
+
async def consolidate(self, namespace: str) -> Dict[str, Any]:
|
|
350
|
+
return await self._c._request("POST", f"/memory/{namespace}/consolidate")
|
|
351
|
+
|
|
352
|
+
class _AsyncKnowledgeGraph:
|
|
353
|
+
def __init__(self, c: AsyncSwitchy) -> None: self._c = c
|
|
354
|
+
async def list_entities(self, namespace: str) -> List[Dict[str, Any]]:
|
|
355
|
+
return await self._c._request("GET", f"/memory/{namespace}/graph/entities")
|
|
356
|
+
async def create_entity(self, namespace: str, *, name: str, type: str, metadata: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
357
|
+
return await self._c._request("POST", f"/memory/{namespace}/graph/entities", {"name": name, "type": type, "metadata": metadata})
|
|
358
|
+
async def list_relations(self, namespace: str) -> List[Dict[str, Any]]:
|
|
359
|
+
return await self._c._request("GET", f"/memory/{namespace}/graph/relations")
|
|
360
|
+
async def create_relation(self, namespace: str, *, source: str, target: str, type: str, metadata: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
361
|
+
return await self._c._request("POST", f"/memory/{namespace}/graph/relations", {"source": source, "target": target, "type": type, "metadata": metadata})
|
|
362
|
+
async def query(self, *, query: str, depth: Optional[int] = None, namespace: Optional[str] = None) -> Dict[str, Any]:
|
|
363
|
+
return await self._c._request("POST", "/knowledge-graph", {"query": query, "depth": depth, "namespace": namespace})
|
|
364
|
+
|
|
365
|
+
class _AsyncSessions:
|
|
366
|
+
def __init__(self, c: AsyncSwitchy) -> None: self._c = c
|
|
367
|
+
async def create(self, **data: Any) -> Dict[str, Any]:
|
|
368
|
+
return await self._c._request("POST", "/sessions", data)
|
|
369
|
+
async def list(self) -> List[Dict[str, Any]]:
|
|
370
|
+
return await self._c._request("GET", "/sessions")
|
|
371
|
+
async def get(self, id: str) -> Dict[str, Any]:
|
|
372
|
+
return await self._c._request("GET", f"/sessions/{id}")
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Exception types raised by the Switchy SDK."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from typing import Any, Optional
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class SwitchyError(Exception):
|
|
10
|
+
"""Base error for all SDK failures."""
|
|
11
|
+
|
|
12
|
+
def __init__(
|
|
13
|
+
self,
|
|
14
|
+
message: str,
|
|
15
|
+
code: str = "UNKNOWN",
|
|
16
|
+
status: int = 0,
|
|
17
|
+
details: Optional[Any] = None,
|
|
18
|
+
) -> None:
|
|
19
|
+
super().__init__(message)
|
|
20
|
+
self.code = code
|
|
21
|
+
self.status = status
|
|
22
|
+
self.details = details
|
|
23
|
+
|
|
24
|
+
def __repr__(self) -> str:
|
|
25
|
+
return f"SwitchyError(code={self.code!r}, status={self.status}, message={self.args[0]!r})"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class RateLimitError(SwitchyError):
|
|
29
|
+
"""Raised when the API returns 429 Too Many Requests."""
|
|
30
|
+
|
|
31
|
+
def __init__(self, message: str, details: Optional[dict] = None) -> None:
|
|
32
|
+
super().__init__(message, "RATE_LIMIT_EXCEEDED", 429, details)
|
|
33
|
+
details = details or {}
|
|
34
|
+
self.retry_after: int = int(details.get("retryAfter") or 60)
|
|
35
|
+
self.limit: Optional[int] = details.get("limit")
|
|
36
|
+
reset_at = details.get("resetAt")
|
|
37
|
+
self.reset_at: Optional[datetime] = (
|
|
38
|
+
datetime.fromisoformat(reset_at.replace("Z", "+00:00")) if reset_at else None
|
|
39
|
+
)
|