cogspace 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.
- cogspace-0.1.0/.gitignore +65 -0
- cogspace-0.1.0/PKG-INFO +131 -0
- cogspace-0.1.0/README.md +102 -0
- cogspace-0.1.0/cogspace/__init__.py +203 -0
- cogspace-0.1.0/cogspace/_async_client.py +93 -0
- cogspace-0.1.0/cogspace/_async_space.py +105 -0
- cogspace-0.1.0/cogspace/_client.py +96 -0
- cogspace-0.1.0/cogspace/_space.py +127 -0
- cogspace-0.1.0/cogspace/exceptions.py +46 -0
- cogspace-0.1.0/cogspace/types.py +113 -0
- cogspace-0.1.0/pyproject.toml +52 -0
- cogspace-0.1.0/tests/__init__.py +0 -0
- cogspace-0.1.0/tests/conftest.py +54 -0
- cogspace-0.1.0/tests/test_async.py +65 -0
- cogspace-0.1.0/tests/test_sync.py +111 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.so
|
|
6
|
+
.Python
|
|
7
|
+
build/
|
|
8
|
+
develop-eggs/
|
|
9
|
+
dist/
|
|
10
|
+
downloads/
|
|
11
|
+
eggs/
|
|
12
|
+
.eggs/
|
|
13
|
+
lib/
|
|
14
|
+
lib64/
|
|
15
|
+
parts/
|
|
16
|
+
sdist/
|
|
17
|
+
var/
|
|
18
|
+
wheels/
|
|
19
|
+
*.egg-info/
|
|
20
|
+
.installed.cfg
|
|
21
|
+
*.egg
|
|
22
|
+
venv/
|
|
23
|
+
ENV/
|
|
24
|
+
env/
|
|
25
|
+
.venv
|
|
26
|
+
|
|
27
|
+
# Node
|
|
28
|
+
node_modules/
|
|
29
|
+
npm-debug.log
|
|
30
|
+
yarn-error.log
|
|
31
|
+
.next/
|
|
32
|
+
out/
|
|
33
|
+
.turbo/
|
|
34
|
+
|
|
35
|
+
# IDE
|
|
36
|
+
.vscode/
|
|
37
|
+
.idea/
|
|
38
|
+
*.swp
|
|
39
|
+
*.swo
|
|
40
|
+
*~
|
|
41
|
+
.DS_Store
|
|
42
|
+
|
|
43
|
+
# Environment
|
|
44
|
+
.env
|
|
45
|
+
.env.local
|
|
46
|
+
.env.*.local
|
|
47
|
+
|
|
48
|
+
# Data
|
|
49
|
+
*.db
|
|
50
|
+
*.sqlite
|
|
51
|
+
*.sqlite3
|
|
52
|
+
data/
|
|
53
|
+
.cogspace/
|
|
54
|
+
examples/*/memory.db
|
|
55
|
+
examples/*/bm25_index/
|
|
56
|
+
examples/*/lancedb/
|
|
57
|
+
|
|
58
|
+
# Coverage
|
|
59
|
+
.coverage
|
|
60
|
+
htmlcov/
|
|
61
|
+
.pytest_cache/
|
|
62
|
+
|
|
63
|
+
# Logs
|
|
64
|
+
*.log
|
|
65
|
+
logs/
|
cogspace-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cogspace
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Official Cogspace SDK — add a knowledge layer to any AI agent
|
|
5
|
+
Project-URL: Homepage, https://cogspace.ai
|
|
6
|
+
Project-URL: Documentation, https://docs.cogspace.ai
|
|
7
|
+
Project-URL: Repository, https://github.com/Jack-Pision/cogspace-ai
|
|
8
|
+
Project-URL: Issues, https://github.com/Jack-Pision/cogspace-ai/issues
|
|
9
|
+
Author-email: Cogspace <sdk@cogspace.ai>
|
|
10
|
+
License: MIT
|
|
11
|
+
Keywords: agents,ai,knowledge,memory,rag,sdk
|
|
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.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
20
|
+
Classifier: Typing :: Typed
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Requires-Dist: httpx>=0.27
|
|
23
|
+
Requires-Dist: pydantic>=2.0
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
26
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
27
|
+
Requires-Dist: respx>=0.21; extra == 'dev'
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
# cogspace
|
|
31
|
+
|
|
32
|
+
Official Python SDK for [Cogspace](https://cogspace.ai) — add a persistent knowledge layer to any AI agent.
|
|
33
|
+
|
|
34
|
+
## Install
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install cogspace
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Quickstart
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
from cogspace import Cogspace
|
|
44
|
+
|
|
45
|
+
cog = Cogspace(api_key="cs-...")
|
|
46
|
+
|
|
47
|
+
# Get a space-scoped client
|
|
48
|
+
space = cog.space("my-agent")
|
|
49
|
+
|
|
50
|
+
# Search
|
|
51
|
+
results = space.search("retry logic with backoff")
|
|
52
|
+
for r in results.results:
|
|
53
|
+
print(r.file_path, r.score)
|
|
54
|
+
|
|
55
|
+
# Write knowledge
|
|
56
|
+
space.write("expertise/retry.md", """
|
|
57
|
+
# Retry patterns
|
|
58
|
+
Always use exponential backoff with jitter.
|
|
59
|
+
""")
|
|
60
|
+
|
|
61
|
+
# Read the hot layer (always-injected context)
|
|
62
|
+
context = space.hot_layer()
|
|
63
|
+
|
|
64
|
+
# Memory
|
|
65
|
+
space.write_memory("Currently working on the payment service refactor.")
|
|
66
|
+
memory = space.read_memory()
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Async
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
from cogspace import AsyncCogspace
|
|
73
|
+
|
|
74
|
+
async def main():
|
|
75
|
+
cog = AsyncCogspace(api_key="cs-...")
|
|
76
|
+
space = await cog.space("my-agent")
|
|
77
|
+
results = await space.search("retry logic")
|
|
78
|
+
await cog.aclose()
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Or use as a context manager:
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
async with AsyncCogspace(api_key="cs-...") as cog:
|
|
85
|
+
space = await cog.space("my-agent")
|
|
86
|
+
await space.write("expertise/notes.md", "...")
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## API Reference
|
|
90
|
+
|
|
91
|
+
### `Cogspace(api_key, base_url, timeout, max_retries)`
|
|
92
|
+
|
|
93
|
+
| Method | Description |
|
|
94
|
+
|---|---|
|
|
95
|
+
| `cog.space(name_or_id)` | Get a space client |
|
|
96
|
+
| `cog.list_spaces()` | List all your spaces |
|
|
97
|
+
| `cog.create_space(name)` | Create a new space |
|
|
98
|
+
|
|
99
|
+
### `SpaceClient`
|
|
100
|
+
|
|
101
|
+
| Method | Description |
|
|
102
|
+
|---|---|
|
|
103
|
+
| `space.search(query, mode, top_k, layer)` | Hybrid / vector / keyword search |
|
|
104
|
+
| `space.read(path)` | Read a file by path |
|
|
105
|
+
| `space.write(path, content, metadata)` | Write or update a file |
|
|
106
|
+
| `space.list(folder)` | List files in a folder |
|
|
107
|
+
| `space.read_memory()` | Read `memory.md` |
|
|
108
|
+
| `space.write_memory(content, confidence)` | Update `memory.md` |
|
|
109
|
+
| `space.read_context()` | Read `memory/user/` context files |
|
|
110
|
+
| `space.hot_layer()` | Get full hot layer injection string |
|
|
111
|
+
| `space.graph_traverse(path, depth, rel_type)` | Traverse knowledge graph |
|
|
112
|
+
| `space.health()` | Space health and storage stats |
|
|
113
|
+
|
|
114
|
+
## Errors
|
|
115
|
+
|
|
116
|
+
All exceptions inherit from `CogspaceError`:
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
from cogspace.exceptions import AuthError, NotFoundError, RateLimitError
|
|
120
|
+
|
|
121
|
+
try:
|
|
122
|
+
space.search("query")
|
|
123
|
+
except AuthError:
|
|
124
|
+
print("Invalid API key")
|
|
125
|
+
except NotFoundError:
|
|
126
|
+
print("Space not found")
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Get an API key
|
|
130
|
+
|
|
131
|
+
Sign in at [platform.cogspace.ai](https://platform.cogspace.ai), go to **Settings → API keys**, and create a key.
|
cogspace-0.1.0/README.md
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# cogspace
|
|
2
|
+
|
|
3
|
+
Official Python SDK for [Cogspace](https://cogspace.ai) — add a persistent knowledge layer to any AI agent.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install cogspace
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quickstart
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from cogspace import Cogspace
|
|
15
|
+
|
|
16
|
+
cog = Cogspace(api_key="cs-...")
|
|
17
|
+
|
|
18
|
+
# Get a space-scoped client
|
|
19
|
+
space = cog.space("my-agent")
|
|
20
|
+
|
|
21
|
+
# Search
|
|
22
|
+
results = space.search("retry logic with backoff")
|
|
23
|
+
for r in results.results:
|
|
24
|
+
print(r.file_path, r.score)
|
|
25
|
+
|
|
26
|
+
# Write knowledge
|
|
27
|
+
space.write("expertise/retry.md", """
|
|
28
|
+
# Retry patterns
|
|
29
|
+
Always use exponential backoff with jitter.
|
|
30
|
+
""")
|
|
31
|
+
|
|
32
|
+
# Read the hot layer (always-injected context)
|
|
33
|
+
context = space.hot_layer()
|
|
34
|
+
|
|
35
|
+
# Memory
|
|
36
|
+
space.write_memory("Currently working on the payment service refactor.")
|
|
37
|
+
memory = space.read_memory()
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Async
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
from cogspace import AsyncCogspace
|
|
44
|
+
|
|
45
|
+
async def main():
|
|
46
|
+
cog = AsyncCogspace(api_key="cs-...")
|
|
47
|
+
space = await cog.space("my-agent")
|
|
48
|
+
results = await space.search("retry logic")
|
|
49
|
+
await cog.aclose()
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Or use as a context manager:
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
async with AsyncCogspace(api_key="cs-...") as cog:
|
|
56
|
+
space = await cog.space("my-agent")
|
|
57
|
+
await space.write("expertise/notes.md", "...")
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## API Reference
|
|
61
|
+
|
|
62
|
+
### `Cogspace(api_key, base_url, timeout, max_retries)`
|
|
63
|
+
|
|
64
|
+
| Method | Description |
|
|
65
|
+
|---|---|
|
|
66
|
+
| `cog.space(name_or_id)` | Get a space client |
|
|
67
|
+
| `cog.list_spaces()` | List all your spaces |
|
|
68
|
+
| `cog.create_space(name)` | Create a new space |
|
|
69
|
+
|
|
70
|
+
### `SpaceClient`
|
|
71
|
+
|
|
72
|
+
| Method | Description |
|
|
73
|
+
|---|---|
|
|
74
|
+
| `space.search(query, mode, top_k, layer)` | Hybrid / vector / keyword search |
|
|
75
|
+
| `space.read(path)` | Read a file by path |
|
|
76
|
+
| `space.write(path, content, metadata)` | Write or update a file |
|
|
77
|
+
| `space.list(folder)` | List files in a folder |
|
|
78
|
+
| `space.read_memory()` | Read `memory.md` |
|
|
79
|
+
| `space.write_memory(content, confidence)` | Update `memory.md` |
|
|
80
|
+
| `space.read_context()` | Read `memory/user/` context files |
|
|
81
|
+
| `space.hot_layer()` | Get full hot layer injection string |
|
|
82
|
+
| `space.graph_traverse(path, depth, rel_type)` | Traverse knowledge graph |
|
|
83
|
+
| `space.health()` | Space health and storage stats |
|
|
84
|
+
|
|
85
|
+
## Errors
|
|
86
|
+
|
|
87
|
+
All exceptions inherit from `CogspaceError`:
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
from cogspace.exceptions import AuthError, NotFoundError, RateLimitError
|
|
91
|
+
|
|
92
|
+
try:
|
|
93
|
+
space.search("query")
|
|
94
|
+
except AuthError:
|
|
95
|
+
print("Invalid API key")
|
|
96
|
+
except NotFoundError:
|
|
97
|
+
print("Space not found")
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Get an API key
|
|
101
|
+
|
|
102
|
+
Sign in at [platform.cogspace.ai](https://platform.cogspace.ai), go to **Settings → API keys**, and create a key.
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Cogspace SDK — add a knowledge layer to any AI agent.
|
|
3
|
+
|
|
4
|
+
Quickstart (sync):
|
|
5
|
+
from cogspace import Cogspace
|
|
6
|
+
|
|
7
|
+
cog = Cogspace(api_key="cs-...")
|
|
8
|
+
space = cog.space("my-agent")
|
|
9
|
+
results = space.search("retry logic with backoff")
|
|
10
|
+
space.write("expertise/retry.md", "# Retry patterns\\n...")
|
|
11
|
+
context = space.hot_layer()
|
|
12
|
+
|
|
13
|
+
Quickstart (async):
|
|
14
|
+
from cogspace import AsyncCogspace
|
|
15
|
+
|
|
16
|
+
cog = AsyncCogspace(api_key="cs-...")
|
|
17
|
+
space = cog.space("my-agent")
|
|
18
|
+
results = await space.search("retry logic with backoff")
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
from typing import Optional
|
|
24
|
+
|
|
25
|
+
from cogspace._async_client import AsyncClient
|
|
26
|
+
from cogspace._async_space import AsyncSpaceClient
|
|
27
|
+
from cogspace._client import SyncClient
|
|
28
|
+
from cogspace._space import SpaceClient
|
|
29
|
+
from cogspace.exceptions import (
|
|
30
|
+
AuthError,
|
|
31
|
+
CogspaceError,
|
|
32
|
+
NotFoundError,
|
|
33
|
+
RateLimitError,
|
|
34
|
+
ServerError,
|
|
35
|
+
TimeoutError,
|
|
36
|
+
)
|
|
37
|
+
from cogspace.types import (
|
|
38
|
+
ContextFile,
|
|
39
|
+
GraphEdge,
|
|
40
|
+
GraphNode,
|
|
41
|
+
GraphTraverseResponse,
|
|
42
|
+
HealthResponse,
|
|
43
|
+
ListResponse,
|
|
44
|
+
ReadContextResponse,
|
|
45
|
+
ReadResponse,
|
|
46
|
+
SearchItem,
|
|
47
|
+
SearchResponse,
|
|
48
|
+
Space,
|
|
49
|
+
WriteResponse,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
DEFAULT_BASE_URL = "https://platform.cogspace.ai"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class Cogspace:
|
|
56
|
+
"""
|
|
57
|
+
Sync Cogspace client.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
api_key: Your Cogspace API key (starts with 'cs-').
|
|
61
|
+
base_url: Platform URL. Defaults to https://platform.cogspace.ai.
|
|
62
|
+
timeout: Request timeout in seconds. Default 30.
|
|
63
|
+
max_retries: Number of retries on 429/5xx. Default 3.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
def __init__(
|
|
67
|
+
self,
|
|
68
|
+
api_key: str,
|
|
69
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
70
|
+
timeout: float = 30.0,
|
|
71
|
+
max_retries: int = 3,
|
|
72
|
+
) -> None:
|
|
73
|
+
self._client = SyncClient(
|
|
74
|
+
api_key=api_key,
|
|
75
|
+
base_url=base_url,
|
|
76
|
+
timeout=timeout,
|
|
77
|
+
max_retries=max_retries,
|
|
78
|
+
)
|
|
79
|
+
self._space_cache: dict[str, str] = {} # name -> id
|
|
80
|
+
|
|
81
|
+
def _resolve_space_id(self, space_name_or_id: str) -> str:
|
|
82
|
+
"""Resolve a space name to its ID, caching the result."""
|
|
83
|
+
if space_name_or_id in self._space_cache:
|
|
84
|
+
return self._space_cache[space_name_or_id]
|
|
85
|
+
spaces = self._client.get("/spaces")
|
|
86
|
+
for s in spaces:
|
|
87
|
+
if s["name"] == space_name_or_id or s["id"] == space_name_or_id:
|
|
88
|
+
self._space_cache[s["name"]] = s["id"]
|
|
89
|
+
self._space_cache[s["id"]] = s["id"]
|
|
90
|
+
return s["id"]
|
|
91
|
+
raise NotFoundError(f"Space not found: {space_name_or_id!r}")
|
|
92
|
+
|
|
93
|
+
def space(self, name_or_id: str) -> SpaceClient:
|
|
94
|
+
"""Get a space-scoped client by space name or ID."""
|
|
95
|
+
space_id = self._resolve_space_id(name_or_id)
|
|
96
|
+
return SpaceClient(self._client, space_id)
|
|
97
|
+
|
|
98
|
+
def list_spaces(self) -> list[Space]:
|
|
99
|
+
"""List all your spaces."""
|
|
100
|
+
data = self._client.get("/spaces")
|
|
101
|
+
return [Space.model_validate(s) for s in data]
|
|
102
|
+
|
|
103
|
+
def create_space(self, name: str) -> Space:
|
|
104
|
+
"""Create a new space."""
|
|
105
|
+
data = self._client.post("/spaces", json={"name": name})
|
|
106
|
+
return Space.model_validate(data)
|
|
107
|
+
|
|
108
|
+
def close(self) -> None:
|
|
109
|
+
self._client.close()
|
|
110
|
+
|
|
111
|
+
def __enter__(self) -> "Cogspace":
|
|
112
|
+
return self
|
|
113
|
+
|
|
114
|
+
def __exit__(self, *_) -> None:
|
|
115
|
+
self.close()
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class AsyncCogspace:
|
|
119
|
+
"""
|
|
120
|
+
Async Cogspace client.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
api_key: Your Cogspace API key (starts with 'cs-').
|
|
124
|
+
base_url: Platform URL. Defaults to https://platform.cogspace.ai.
|
|
125
|
+
timeout: Request timeout in seconds. Default 30.
|
|
126
|
+
max_retries: Number of retries on 429/5xx. Default 3.
|
|
127
|
+
"""
|
|
128
|
+
|
|
129
|
+
def __init__(
|
|
130
|
+
self,
|
|
131
|
+
api_key: str,
|
|
132
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
133
|
+
timeout: float = 30.0,
|
|
134
|
+
max_retries: int = 3,
|
|
135
|
+
) -> None:
|
|
136
|
+
self._client = AsyncClient(
|
|
137
|
+
api_key=api_key,
|
|
138
|
+
base_url=base_url,
|
|
139
|
+
timeout=timeout,
|
|
140
|
+
max_retries=max_retries,
|
|
141
|
+
)
|
|
142
|
+
self._space_cache: dict[str, str] = {}
|
|
143
|
+
|
|
144
|
+
async def _resolve_space_id(self, space_name_or_id: str) -> str:
|
|
145
|
+
if space_name_or_id in self._space_cache:
|
|
146
|
+
return self._space_cache[space_name_or_id]
|
|
147
|
+
spaces = await self._client.get("/spaces")
|
|
148
|
+
for s in spaces:
|
|
149
|
+
if s["name"] == space_name_or_id or s["id"] == space_name_or_id:
|
|
150
|
+
self._space_cache[s["name"]] = s["id"]
|
|
151
|
+
self._space_cache[s["id"]] = s["id"]
|
|
152
|
+
return s["id"]
|
|
153
|
+
raise NotFoundError(f"Space not found: {space_name_or_id!r}")
|
|
154
|
+
|
|
155
|
+
async def space(self, name_or_id: str) -> AsyncSpaceClient:
|
|
156
|
+
"""Get a space-scoped async client by space name or ID."""
|
|
157
|
+
space_id = await self._resolve_space_id(name_or_id)
|
|
158
|
+
return AsyncSpaceClient(self._client, space_id)
|
|
159
|
+
|
|
160
|
+
async def list_spaces(self) -> list[Space]:
|
|
161
|
+
data = await self._client.get("/spaces")
|
|
162
|
+
return [Space.model_validate(s) for s in data]
|
|
163
|
+
|
|
164
|
+
async def create_space(self, name: str) -> Space:
|
|
165
|
+
data = await self._client.post("/spaces", json={"name": name})
|
|
166
|
+
return Space.model_validate(data)
|
|
167
|
+
|
|
168
|
+
async def aclose(self) -> None:
|
|
169
|
+
await self._client.aclose()
|
|
170
|
+
|
|
171
|
+
async def __aenter__(self) -> "AsyncCogspace":
|
|
172
|
+
return self
|
|
173
|
+
|
|
174
|
+
async def __aexit__(self, *_) -> None:
|
|
175
|
+
await self.aclose()
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
__all__ = [
|
|
179
|
+
"Cogspace",
|
|
180
|
+
"AsyncCogspace",
|
|
181
|
+
"SpaceClient",
|
|
182
|
+
"AsyncSpaceClient",
|
|
183
|
+
"CogspaceError",
|
|
184
|
+
"AuthError",
|
|
185
|
+
"NotFoundError",
|
|
186
|
+
"RateLimitError",
|
|
187
|
+
"ServerError",
|
|
188
|
+
"TimeoutError",
|
|
189
|
+
"SearchResponse",
|
|
190
|
+
"SearchItem",
|
|
191
|
+
"ReadResponse",
|
|
192
|
+
"WriteResponse",
|
|
193
|
+
"ListResponse",
|
|
194
|
+
"ReadContextResponse",
|
|
195
|
+
"ContextFile",
|
|
196
|
+
"GraphTraverseResponse",
|
|
197
|
+
"GraphNode",
|
|
198
|
+
"GraphEdge",
|
|
199
|
+
"HealthResponse",
|
|
200
|
+
"Space",
|
|
201
|
+
]
|
|
202
|
+
|
|
203
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""Async HTTP client for the Cogspace SDK."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
|
|
10
|
+
from cogspace.exceptions import RateLimitError, TimeoutError, _raise_for_status
|
|
11
|
+
|
|
12
|
+
DEFAULT_BASE_URL = "https://platform.cogspace.ai"
|
|
13
|
+
DEFAULT_TIMEOUT = 30.0
|
|
14
|
+
DEFAULT_MAX_RETRIES = 3
|
|
15
|
+
_RETRY_STATUSES = {429, 500, 502, 503, 504}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class AsyncClient:
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
api_key: str,
|
|
22
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
23
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
24
|
+
max_retries: int = DEFAULT_MAX_RETRIES,
|
|
25
|
+
) -> None:
|
|
26
|
+
self._api_key = api_key
|
|
27
|
+
self._base_url = base_url.rstrip("/")
|
|
28
|
+
self._timeout = timeout
|
|
29
|
+
self._max_retries = max_retries
|
|
30
|
+
self._http = httpx.AsyncClient(
|
|
31
|
+
base_url=self._base_url,
|
|
32
|
+
headers={"Authorization": f"Bearer {api_key}", "User-Agent": "cogspace-python/0.1.0"},
|
|
33
|
+
timeout=timeout,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
async def aclose(self) -> None:
|
|
37
|
+
await self._http.aclose()
|
|
38
|
+
|
|
39
|
+
async def __aenter__(self) -> "AsyncClient":
|
|
40
|
+
return self
|
|
41
|
+
|
|
42
|
+
async def __aexit__(self, *_: Any) -> None:
|
|
43
|
+
await self.aclose()
|
|
44
|
+
|
|
45
|
+
async def _request(self, method: str, path: str, **kwargs: Any) -> dict:
|
|
46
|
+
last_exc: Exception | None = None
|
|
47
|
+
for attempt in range(self._max_retries):
|
|
48
|
+
try:
|
|
49
|
+
response = await self._http.request(method, path, **kwargs)
|
|
50
|
+
except httpx.TimeoutException as exc:
|
|
51
|
+
raise TimeoutError(f"Request timed out: {path}") from exc
|
|
52
|
+
|
|
53
|
+
if response.status_code not in _RETRY_STATUSES:
|
|
54
|
+
if response.status_code >= 400:
|
|
55
|
+
try:
|
|
56
|
+
body = response.json()
|
|
57
|
+
except Exception:
|
|
58
|
+
body = {}
|
|
59
|
+
_raise_for_status(response.status_code, body)
|
|
60
|
+
return response.json() if response.content else {}
|
|
61
|
+
|
|
62
|
+
if attempt < self._max_retries - 1:
|
|
63
|
+
wait = 2 ** attempt
|
|
64
|
+
if response.status_code == 429:
|
|
65
|
+
retry_after = response.headers.get("Retry-After")
|
|
66
|
+
if retry_after:
|
|
67
|
+
wait = float(retry_after)
|
|
68
|
+
await asyncio.sleep(wait)
|
|
69
|
+
try:
|
|
70
|
+
body = response.json()
|
|
71
|
+
except Exception:
|
|
72
|
+
body = {}
|
|
73
|
+
last_exc = RateLimitError(
|
|
74
|
+
body.get("detail", f"HTTP {response.status_code}"),
|
|
75
|
+
status=response.status_code,
|
|
76
|
+
)
|
|
77
|
+
else:
|
|
78
|
+
try:
|
|
79
|
+
body = response.json()
|
|
80
|
+
except Exception:
|
|
81
|
+
body = {}
|
|
82
|
+
_raise_for_status(response.status_code, body)
|
|
83
|
+
|
|
84
|
+
raise last_exc or RateLimitError("Max retries exceeded")
|
|
85
|
+
|
|
86
|
+
async def get(self, path: str, params: dict | None = None) -> dict:
|
|
87
|
+
return await self._request("GET", path, params=params)
|
|
88
|
+
|
|
89
|
+
async def post(self, path: str, json: dict | None = None) -> dict:
|
|
90
|
+
return await self._request("POST", path, json=json)
|
|
91
|
+
|
|
92
|
+
async def delete(self, path: str) -> dict:
|
|
93
|
+
return await self._request("DELETE", path)
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""AsyncSpaceClient — async space-scoped operations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Literal, Optional
|
|
6
|
+
|
|
7
|
+
from cogspace._async_client import AsyncClient
|
|
8
|
+
from cogspace.types import (
|
|
9
|
+
GraphTraverseResponse,
|
|
10
|
+
HealthResponse,
|
|
11
|
+
ListResponse,
|
|
12
|
+
ReadContextResponse,
|
|
13
|
+
ReadResponse,
|
|
14
|
+
SearchResponse,
|
|
15
|
+
WriteResponse,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class AsyncSpaceClient:
|
|
20
|
+
"""Async client scoped to a single Cogspace space."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, client: AsyncClient, space_id: str) -> None:
|
|
23
|
+
self._c = client
|
|
24
|
+
self._space_id = space_id
|
|
25
|
+
|
|
26
|
+
def _path(self, endpoint: str) -> str:
|
|
27
|
+
return f"/spaces/{self._space_id}/{endpoint}"
|
|
28
|
+
|
|
29
|
+
async def search(
|
|
30
|
+
self,
|
|
31
|
+
query: str,
|
|
32
|
+
mode: Literal["hybrid", "vector", "bm25"] = "hybrid",
|
|
33
|
+
top_k: int = 5,
|
|
34
|
+
layer: Optional[str] = None,
|
|
35
|
+
min_score: float = 0.0,
|
|
36
|
+
) -> SearchResponse:
|
|
37
|
+
endpoint = {
|
|
38
|
+
"hybrid": "search/semantic",
|
|
39
|
+
"vector": "search/semantic",
|
|
40
|
+
"bm25": "search/keyword",
|
|
41
|
+
}[mode]
|
|
42
|
+
data = await self._c.post(self._path(endpoint), json={
|
|
43
|
+
"query": query,
|
|
44
|
+
"top_k": top_k,
|
|
45
|
+
"layer": layer,
|
|
46
|
+
"min_score": min_score,
|
|
47
|
+
"mode": mode,
|
|
48
|
+
})
|
|
49
|
+
return SearchResponse.model_validate(data)
|
|
50
|
+
|
|
51
|
+
async def read(self, path: str) -> ReadResponse:
|
|
52
|
+
data = await self._c.get(self._path("read"), params={"path": path})
|
|
53
|
+
return ReadResponse.model_validate(data)
|
|
54
|
+
|
|
55
|
+
async def write(
|
|
56
|
+
self,
|
|
57
|
+
path: str,
|
|
58
|
+
content: str,
|
|
59
|
+
metadata: Optional[dict] = None,
|
|
60
|
+
agent_id: str = "default",
|
|
61
|
+
) -> WriteResponse:
|
|
62
|
+
data = await self._c.post(self._path("write"), json={
|
|
63
|
+
"path": path,
|
|
64
|
+
"content": content,
|
|
65
|
+
"metadata": metadata,
|
|
66
|
+
"agent_id": agent_id,
|
|
67
|
+
})
|
|
68
|
+
return WriteResponse.model_validate(data)
|
|
69
|
+
|
|
70
|
+
async def list(self, folder: str = "") -> ListResponse:
|
|
71
|
+
data = await self._c.get(self._path("list"), params={"folder": folder})
|
|
72
|
+
return ListResponse.model_validate(data)
|
|
73
|
+
|
|
74
|
+
async def read_memory(self) -> ReadResponse:
|
|
75
|
+
data = await self._c.get(self._path("memory"))
|
|
76
|
+
return ReadResponse.model_validate(data)
|
|
77
|
+
|
|
78
|
+
async def write_memory(self, content: str, confidence: float = 0.9) -> WriteResponse:
|
|
79
|
+
data = await self._c.post(self._path("memory"), json={"content": content, "confidence": confidence})
|
|
80
|
+
return WriteResponse.model_validate(data)
|
|
81
|
+
|
|
82
|
+
async def read_context(self) -> ReadContextResponse:
|
|
83
|
+
data = await self._c.get(self._path("memory/context"))
|
|
84
|
+
return ReadContextResponse.model_validate(data)
|
|
85
|
+
|
|
86
|
+
async def graph_traverse(
|
|
87
|
+
self,
|
|
88
|
+
path: str,
|
|
89
|
+
depth: int = 2,
|
|
90
|
+
rel_type: Optional[str] = None,
|
|
91
|
+
) -> GraphTraverseResponse:
|
|
92
|
+
data = await self._c.post(self._path("graph/traverse"), json={
|
|
93
|
+
"path": path,
|
|
94
|
+
"depth": depth,
|
|
95
|
+
"rel_type": rel_type,
|
|
96
|
+
})
|
|
97
|
+
return GraphTraverseResponse.model_validate(data)
|
|
98
|
+
|
|
99
|
+
async def hot_layer(self) -> str:
|
|
100
|
+
data = await self._c.get(self._path("hot-layer"))
|
|
101
|
+
return data["hot_layer"]
|
|
102
|
+
|
|
103
|
+
async def health(self) -> HealthResponse:
|
|
104
|
+
data = await self._c.get(self._path("health"))
|
|
105
|
+
return HealthResponse.model_validate(data)
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""Sync HTTP client for the Cogspace SDK."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
from typing import Any, TypeVar
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
|
|
10
|
+
from cogspace.exceptions import RateLimitError, TimeoutError, _raise_for_status
|
|
11
|
+
|
|
12
|
+
T = TypeVar("T")
|
|
13
|
+
|
|
14
|
+
DEFAULT_BASE_URL = "https://platform.cogspace.ai"
|
|
15
|
+
DEFAULT_TIMEOUT = 30.0
|
|
16
|
+
DEFAULT_MAX_RETRIES = 3
|
|
17
|
+
_RETRY_STATUSES = {429, 500, 502, 503, 504}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class SyncClient:
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
api_key: str,
|
|
24
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
25
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
26
|
+
max_retries: int = DEFAULT_MAX_RETRIES,
|
|
27
|
+
) -> None:
|
|
28
|
+
self._api_key = api_key
|
|
29
|
+
self._base_url = base_url.rstrip("/")
|
|
30
|
+
self._timeout = timeout
|
|
31
|
+
self._max_retries = max_retries
|
|
32
|
+
self._http = httpx.Client(
|
|
33
|
+
base_url=self._base_url,
|
|
34
|
+
headers={"Authorization": f"Bearer {api_key}", "User-Agent": "cogspace-python/0.1.0"},
|
|
35
|
+
timeout=timeout,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
def close(self) -> None:
|
|
39
|
+
self._http.close()
|
|
40
|
+
|
|
41
|
+
def __enter__(self) -> "SyncClient":
|
|
42
|
+
return self
|
|
43
|
+
|
|
44
|
+
def __exit__(self, *_: Any) -> None:
|
|
45
|
+
self.close()
|
|
46
|
+
|
|
47
|
+
def _request(self, method: str, path: str, **kwargs: Any) -> dict:
|
|
48
|
+
last_exc: Exception | None = None
|
|
49
|
+
for attempt in range(self._max_retries):
|
|
50
|
+
try:
|
|
51
|
+
response = self._http.request(method, path, **kwargs)
|
|
52
|
+
except httpx.TimeoutException as exc:
|
|
53
|
+
raise TimeoutError(f"Request timed out: {path}") from exc
|
|
54
|
+
|
|
55
|
+
if response.status_code not in _RETRY_STATUSES:
|
|
56
|
+
if response.status_code >= 400:
|
|
57
|
+
try:
|
|
58
|
+
body = response.json()
|
|
59
|
+
except Exception:
|
|
60
|
+
body = {}
|
|
61
|
+
_raise_for_status(response.status_code, body)
|
|
62
|
+
return response.json() if response.content else {}
|
|
63
|
+
|
|
64
|
+
# Retryable status
|
|
65
|
+
if attempt < self._max_retries - 1:
|
|
66
|
+
wait = 2 ** attempt
|
|
67
|
+
if response.status_code == 429:
|
|
68
|
+
retry_after = response.headers.get("Retry-After")
|
|
69
|
+
if retry_after:
|
|
70
|
+
wait = float(retry_after)
|
|
71
|
+
time.sleep(wait)
|
|
72
|
+
try:
|
|
73
|
+
body = response.json()
|
|
74
|
+
except Exception:
|
|
75
|
+
body = {}
|
|
76
|
+
last_exc = RateLimitError(
|
|
77
|
+
body.get("detail", f"HTTP {response.status_code}"),
|
|
78
|
+
status=response.status_code,
|
|
79
|
+
)
|
|
80
|
+
else:
|
|
81
|
+
try:
|
|
82
|
+
body = response.json()
|
|
83
|
+
except Exception:
|
|
84
|
+
body = {}
|
|
85
|
+
_raise_for_status(response.status_code, body)
|
|
86
|
+
|
|
87
|
+
raise last_exc or RateLimitError("Max retries exceeded")
|
|
88
|
+
|
|
89
|
+
def get(self, path: str, params: dict | None = None) -> dict:
|
|
90
|
+
return self._request("GET", path, params=params)
|
|
91
|
+
|
|
92
|
+
def post(self, path: str, json: dict | None = None) -> dict:
|
|
93
|
+
return self._request("POST", path, json=json)
|
|
94
|
+
|
|
95
|
+
def delete(self, path: str) -> dict:
|
|
96
|
+
return self._request("DELETE", path)
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""SpaceClient — sync space-scoped operations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Literal, Optional
|
|
6
|
+
|
|
7
|
+
from cogspace._client import SyncClient
|
|
8
|
+
from cogspace.types import (
|
|
9
|
+
GraphTraverseResponse,
|
|
10
|
+
HealthResponse,
|
|
11
|
+
ListResponse,
|
|
12
|
+
ReadContextResponse,
|
|
13
|
+
ReadResponse,
|
|
14
|
+
SearchResponse,
|
|
15
|
+
WriteResponse,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class SpaceClient:
|
|
20
|
+
"""Sync client scoped to a single Cogspace space."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, client: SyncClient, space_id: str) -> None:
|
|
23
|
+
self._c = client
|
|
24
|
+
self._space_id = space_id
|
|
25
|
+
|
|
26
|
+
def _path(self, endpoint: str) -> str:
|
|
27
|
+
return f"/spaces/{self._space_id}/{endpoint}"
|
|
28
|
+
|
|
29
|
+
# ── Search ────────────────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
def search(
|
|
32
|
+
self,
|
|
33
|
+
query: str,
|
|
34
|
+
mode: Literal["hybrid", "vector", "bm25"] = "hybrid",
|
|
35
|
+
top_k: int = 5,
|
|
36
|
+
layer: Optional[str] = None,
|
|
37
|
+
min_score: float = 0.0,
|
|
38
|
+
) -> SearchResponse:
|
|
39
|
+
"""Search the knowledge space."""
|
|
40
|
+
endpoint = {
|
|
41
|
+
"hybrid": "search/semantic",
|
|
42
|
+
"vector": "search/semantic",
|
|
43
|
+
"bm25": "search/keyword",
|
|
44
|
+
}[mode]
|
|
45
|
+
data = self._c.post(self._path(endpoint), json={
|
|
46
|
+
"query": query,
|
|
47
|
+
"top_k": top_k,
|
|
48
|
+
"layer": layer,
|
|
49
|
+
"min_score": min_score,
|
|
50
|
+
"mode": mode,
|
|
51
|
+
})
|
|
52
|
+
return SearchResponse.model_validate(data)
|
|
53
|
+
|
|
54
|
+
# ── Core ──────────────────────────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
def read(self, path: str) -> ReadResponse:
|
|
57
|
+
"""Read a file by path."""
|
|
58
|
+
data = self._c.get(self._path("read"), params={"path": path})
|
|
59
|
+
return ReadResponse.model_validate(data)
|
|
60
|
+
|
|
61
|
+
def write(
|
|
62
|
+
self,
|
|
63
|
+
path: str,
|
|
64
|
+
content: str,
|
|
65
|
+
metadata: Optional[dict] = None,
|
|
66
|
+
agent_id: str = "default",
|
|
67
|
+
) -> WriteResponse:
|
|
68
|
+
"""Write (create or update) a file."""
|
|
69
|
+
data = self._c.post(self._path("write"), json={
|
|
70
|
+
"path": path,
|
|
71
|
+
"content": content,
|
|
72
|
+
"metadata": metadata,
|
|
73
|
+
"agent_id": agent_id,
|
|
74
|
+
})
|
|
75
|
+
return WriteResponse.model_validate(data)
|
|
76
|
+
|
|
77
|
+
def list(self, folder: str = "") -> ListResponse:
|
|
78
|
+
"""List files in a folder."""
|
|
79
|
+
data = self._c.get(self._path("list"), params={"folder": folder})
|
|
80
|
+
return ListResponse.model_validate(data)
|
|
81
|
+
|
|
82
|
+
# ── Memory ────────────────────────────────────────────────────────────────
|
|
83
|
+
|
|
84
|
+
def read_memory(self) -> ReadResponse:
|
|
85
|
+
"""Read the agent's memory.md."""
|
|
86
|
+
data = self._c.get(self._path("memory"))
|
|
87
|
+
return ReadResponse.model_validate(data)
|
|
88
|
+
|
|
89
|
+
def write_memory(self, content: str, confidence: float = 0.9) -> WriteResponse:
|
|
90
|
+
"""Update the agent's memory."""
|
|
91
|
+
data = self._c.post(self._path("memory"), json={"content": content, "confidence": confidence})
|
|
92
|
+
return WriteResponse.model_validate(data)
|
|
93
|
+
|
|
94
|
+
def read_context(self) -> ReadContextResponse:
|
|
95
|
+
"""Read user context files (memory/user/)."""
|
|
96
|
+
data = self._c.get(self._path("memory/context"))
|
|
97
|
+
return ReadContextResponse.model_validate(data)
|
|
98
|
+
|
|
99
|
+
# ── Graph ─────────────────────────────────────────────────────────────────
|
|
100
|
+
|
|
101
|
+
def graph_traverse(
|
|
102
|
+
self,
|
|
103
|
+
path: str,
|
|
104
|
+
depth: int = 2,
|
|
105
|
+
rel_type: Optional[str] = None,
|
|
106
|
+
) -> GraphTraverseResponse:
|
|
107
|
+
"""Traverse the knowledge graph from a file."""
|
|
108
|
+
data = self._c.post(self._path("graph/traverse"), json={
|
|
109
|
+
"path": path,
|
|
110
|
+
"depth": depth,
|
|
111
|
+
"rel_type": rel_type,
|
|
112
|
+
})
|
|
113
|
+
return GraphTraverseResponse.model_validate(data)
|
|
114
|
+
|
|
115
|
+
# ── Hot layer ─────────────────────────────────────────────────────────────
|
|
116
|
+
|
|
117
|
+
def hot_layer(self) -> str:
|
|
118
|
+
"""Get the full hot layer injection string (always-injected context)."""
|
|
119
|
+
data = self._c.get(self._path("hot-layer"))
|
|
120
|
+
return data["hot_layer"]
|
|
121
|
+
|
|
122
|
+
# ── Health ────────────────────────────────────────────────────────────────
|
|
123
|
+
|
|
124
|
+
def health(self) -> HealthResponse:
|
|
125
|
+
"""Get space health and storage layer stats."""
|
|
126
|
+
data = self._c.get(self._path("health"))
|
|
127
|
+
return HealthResponse.model_validate(data)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Cogspace SDK exception hierarchy."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class CogspaceError(Exception):
|
|
7
|
+
"""Base exception for all Cogspace SDK errors."""
|
|
8
|
+
|
|
9
|
+
def __init__(self, message: str, status: int | None = None, code: str | None = None) -> None:
|
|
10
|
+
super().__init__(message)
|
|
11
|
+
self.status = status
|
|
12
|
+
self.code = code
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class AuthError(CogspaceError):
|
|
16
|
+
"""Invalid or revoked API key."""
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class NotFoundError(CogspaceError):
|
|
20
|
+
"""Requested resource does not exist."""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class RateLimitError(CogspaceError):
|
|
24
|
+
"""Rate limit exceeded. The SDK will retry automatically."""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ServerError(CogspaceError):
|
|
28
|
+
"""Unexpected server-side error (5xx)."""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class TimeoutError(CogspaceError):
|
|
32
|
+
"""Request timed out."""
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _raise_for_status(status: int, body: dict) -> None:
|
|
36
|
+
message = body.get("detail") or body.get("message") or f"HTTP {status}"
|
|
37
|
+
code = body.get("error")
|
|
38
|
+
if status == 401:
|
|
39
|
+
raise AuthError(message, status=status, code=code)
|
|
40
|
+
if status == 404:
|
|
41
|
+
raise NotFoundError(message, status=status, code=code)
|
|
42
|
+
if status == 429:
|
|
43
|
+
raise RateLimitError(message, status=status, code=code)
|
|
44
|
+
if status >= 500:
|
|
45
|
+
raise ServerError(message, status=status, code=code)
|
|
46
|
+
raise CogspaceError(message, status=status, code=code)
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""Response types for the Cogspace SDK."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Literal, Optional
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class SearchItem(BaseModel):
|
|
11
|
+
file_path: str
|
|
12
|
+
content: str
|
|
13
|
+
score: float
|
|
14
|
+
layer: str
|
|
15
|
+
file_type: str
|
|
16
|
+
topic: str
|
|
17
|
+
confidence: float
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class SearchResponse(BaseModel):
|
|
21
|
+
results: list[SearchItem]
|
|
22
|
+
total: int
|
|
23
|
+
query: str
|
|
24
|
+
mode: str
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ReadResponse(BaseModel):
|
|
28
|
+
path: str
|
|
29
|
+
frontmatter: dict[str, Any]
|
|
30
|
+
content: str
|
|
31
|
+
layer: str
|
|
32
|
+
size_bytes: int
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class WriteResponse(BaseModel):
|
|
36
|
+
path: str
|
|
37
|
+
status: Literal["created", "updated"]
|
|
38
|
+
reindex_queued: bool
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class FileTreeNode(BaseModel):
|
|
42
|
+
model_config = {"extra": "allow"}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class ListResponse(BaseModel):
|
|
46
|
+
tree: dict[str, Any]
|
|
47
|
+
file_count: int
|
|
48
|
+
folder: str
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class ContextFile(BaseModel):
|
|
52
|
+
path: str
|
|
53
|
+
content: str
|
|
54
|
+
frontmatter: dict[str, Any]
|
|
55
|
+
confidence: float
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class ReadContextResponse(BaseModel):
|
|
59
|
+
files: list[ContextFile]
|
|
60
|
+
count: int
|
|
61
|
+
folder: str
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class GraphNode(BaseModel):
|
|
65
|
+
path: str
|
|
66
|
+
file_type: str
|
|
67
|
+
topic: str
|
|
68
|
+
summary: Optional[str] = None
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class GraphEdge(BaseModel):
|
|
72
|
+
from_: str
|
|
73
|
+
to: str
|
|
74
|
+
rel_type: str
|
|
75
|
+
|
|
76
|
+
class Config:
|
|
77
|
+
populate_by_name = True
|
|
78
|
+
fields = {"from_": "from"}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class GraphTraverseResponse(BaseModel):
|
|
82
|
+
source: str
|
|
83
|
+
depth: int
|
|
84
|
+
rel_type: Optional[str] = None
|
|
85
|
+
nodes: list[GraphNode]
|
|
86
|
+
edges: list[GraphEdge]
|
|
87
|
+
graph_available: bool
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class HealthResponse(BaseModel):
|
|
91
|
+
status: str
|
|
92
|
+
space_id: str
|
|
93
|
+
vector_count: int
|
|
94
|
+
bm25_doc_count: int
|
|
95
|
+
graph_available: bool
|
|
96
|
+
graph_node_count: int = 0
|
|
97
|
+
graph_edge_count: int = 0
|
|
98
|
+
hot_layer_files: dict[str, bool] = {}
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class Space(BaseModel):
|
|
102
|
+
id: str
|
|
103
|
+
user_id: str
|
|
104
|
+
name: str
|
|
105
|
+
created_at: str
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class ApiKey(BaseModel):
|
|
109
|
+
id: str
|
|
110
|
+
name: str
|
|
111
|
+
key_prefix: str
|
|
112
|
+
created_at: str
|
|
113
|
+
last_used: Optional[str] = None
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "cogspace"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Official Cogspace SDK — add a knowledge layer to any AI agent"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
authors = [{ name = "Cogspace", email = "sdk@cogspace.ai" }]
|
|
12
|
+
requires-python = ">=3.10"
|
|
13
|
+
keywords = ["ai", "agents", "knowledge", "memory", "rag", "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.10",
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
23
|
+
"Typing :: Typed",
|
|
24
|
+
]
|
|
25
|
+
dependencies = [
|
|
26
|
+
"httpx>=0.27",
|
|
27
|
+
"pydantic>=2.0",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[project.optional-dependencies]
|
|
31
|
+
dev = [
|
|
32
|
+
"pytest>=8",
|
|
33
|
+
"pytest-asyncio>=0.23",
|
|
34
|
+
"respx>=0.21",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
[project.urls]
|
|
38
|
+
Homepage = "https://cogspace.ai"
|
|
39
|
+
Documentation = "https://docs.cogspace.ai"
|
|
40
|
+
Repository = "https://github.com/Jack-Pision/cogspace-ai"
|
|
41
|
+
Issues = "https://github.com/Jack-Pision/cogspace-ai/issues"
|
|
42
|
+
|
|
43
|
+
[tool.hatch.build.targets.wheel]
|
|
44
|
+
packages = ["cogspace"]
|
|
45
|
+
|
|
46
|
+
[tool.pytest.ini_options]
|
|
47
|
+
asyncio_mode = "auto"
|
|
48
|
+
testpaths = ["tests"]
|
|
49
|
+
|
|
50
|
+
[tool.ruff]
|
|
51
|
+
target-version = "py310"
|
|
52
|
+
line-length = 100
|
|
File without changes
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Shared pytest fixtures for Cogspace SDK tests."""
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
import respx
|
|
5
|
+
import httpx
|
|
6
|
+
|
|
7
|
+
BASE_URL = "http://test.cogspace.local"
|
|
8
|
+
API_KEY = "cs-testkey1234567890abcdef1234567890abcdef12"
|
|
9
|
+
|
|
10
|
+
MOCK_SPACES = [
|
|
11
|
+
{"id": "space-001", "user_id": "user-1", "name": "code-patterns", "created_at": "2026-01-01T00:00:00"}
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
MOCK_SEARCH = {
|
|
15
|
+
"results": [
|
|
16
|
+
{
|
|
17
|
+
"file_path": "expertise/retry.md",
|
|
18
|
+
"content": "Retry with exponential backoff...",
|
|
19
|
+
"score": 0.91,
|
|
20
|
+
"layer": "expertise",
|
|
21
|
+
"file_type": "knowledge",
|
|
22
|
+
"topic": "retry",
|
|
23
|
+
"confidence": 0.9,
|
|
24
|
+
}
|
|
25
|
+
],
|
|
26
|
+
"total": 1,
|
|
27
|
+
"query": "retry logic",
|
|
28
|
+
"mode": "hybrid",
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
MOCK_READ = {
|
|
32
|
+
"path": "expertise/retry.md",
|
|
33
|
+
"frontmatter": {"type": "knowledge", "topic": "retry"},
|
|
34
|
+
"content": "# Retry patterns\n...",
|
|
35
|
+
"layer": "expertise",
|
|
36
|
+
"size_bytes": 42,
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
MOCK_WRITE = {
|
|
40
|
+
"path": "expertise/retry.md",
|
|
41
|
+
"status": "created",
|
|
42
|
+
"reindex_queued": True,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
MOCK_HEALTH = {
|
|
46
|
+
"status": "ok",
|
|
47
|
+
"space_id": "space-001",
|
|
48
|
+
"vector_count": 10,
|
|
49
|
+
"bm25_doc_count": 5,
|
|
50
|
+
"graph_available": False,
|
|
51
|
+
"graph_node_count": 0,
|
|
52
|
+
"graph_edge_count": 0,
|
|
53
|
+
"hot_layer_files": {"cogspace_md": True, "memory_md": False},
|
|
54
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Tests for the async Cogspace SDK client."""
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
import respx
|
|
5
|
+
import httpx
|
|
6
|
+
|
|
7
|
+
from tests.conftest import BASE_URL, API_KEY, MOCK_SPACES, MOCK_SEARCH, MOCK_WRITE, MOCK_HEALTH
|
|
8
|
+
from cogspace import AsyncCogspace
|
|
9
|
+
from cogspace.exceptions import AuthError, NotFoundError
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@pytest.mark.asyncio
|
|
13
|
+
@respx.mock
|
|
14
|
+
async def test_async_search():
|
|
15
|
+
respx.get(f"{BASE_URL}/spaces").mock(return_value=httpx.Response(200, json=MOCK_SPACES))
|
|
16
|
+
respx.post(f"{BASE_URL}/spaces/space-001/search/semantic").mock(
|
|
17
|
+
return_value=httpx.Response(200, json=MOCK_SEARCH)
|
|
18
|
+
)
|
|
19
|
+
cog = AsyncCogspace(api_key=API_KEY, base_url=BASE_URL)
|
|
20
|
+
space = await cog.space("code-patterns")
|
|
21
|
+
results = await space.search("retry logic")
|
|
22
|
+
assert results.total == 1
|
|
23
|
+
await cog.aclose()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@pytest.mark.asyncio
|
|
27
|
+
@respx.mock
|
|
28
|
+
async def test_async_write():
|
|
29
|
+
respx.get(f"{BASE_URL}/spaces").mock(return_value=httpx.Response(200, json=MOCK_SPACES))
|
|
30
|
+
respx.post(f"{BASE_URL}/spaces/space-001/write").mock(return_value=httpx.Response(200, json=MOCK_WRITE))
|
|
31
|
+
async with AsyncCogspace(api_key=API_KEY, base_url=BASE_URL) as cog:
|
|
32
|
+
space = await cog.space("code-patterns")
|
|
33
|
+
result = await space.write("expertise/retry.md", "# Retry\n...")
|
|
34
|
+
assert result.status == "created"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@pytest.mark.asyncio
|
|
38
|
+
@respx.mock
|
|
39
|
+
async def test_async_space_not_found():
|
|
40
|
+
respx.get(f"{BASE_URL}/spaces").mock(return_value=httpx.Response(200, json=MOCK_SPACES))
|
|
41
|
+
cog = AsyncCogspace(api_key=API_KEY, base_url=BASE_URL)
|
|
42
|
+
with pytest.raises(NotFoundError):
|
|
43
|
+
await cog.space("nonexistent")
|
|
44
|
+
await cog.aclose()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@pytest.mark.asyncio
|
|
48
|
+
@respx.mock
|
|
49
|
+
async def test_async_auth_error():
|
|
50
|
+
respx.get(f"{BASE_URL}/spaces").mock(
|
|
51
|
+
return_value=httpx.Response(401, json={"detail": "Invalid or revoked API key"})
|
|
52
|
+
)
|
|
53
|
+
cog = AsyncCogspace(api_key="cs-badkey", base_url=BASE_URL)
|
|
54
|
+
with pytest.raises(AuthError):
|
|
55
|
+
await cog.list_spaces()
|
|
56
|
+
await cog.aclose()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@pytest.mark.asyncio
|
|
60
|
+
@respx.mock
|
|
61
|
+
async def test_async_context_manager():
|
|
62
|
+
respx.get(f"{BASE_URL}/spaces").mock(return_value=httpx.Response(200, json=MOCK_SPACES))
|
|
63
|
+
async with AsyncCogspace(api_key=API_KEY, base_url=BASE_URL) as cog:
|
|
64
|
+
spaces = await cog.list_spaces()
|
|
65
|
+
assert len(spaces) == 1
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""Tests for the sync Cogspace SDK client."""
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
import respx
|
|
5
|
+
import httpx
|
|
6
|
+
|
|
7
|
+
from tests.conftest import BASE_URL, API_KEY, MOCK_SPACES, MOCK_SEARCH, MOCK_READ, MOCK_WRITE, MOCK_HEALTH
|
|
8
|
+
from cogspace import Cogspace
|
|
9
|
+
from cogspace.exceptions import AuthError, NotFoundError
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@respx.mock
|
|
13
|
+
def test_space_by_name_resolves():
|
|
14
|
+
respx.get(f"{BASE_URL}/spaces").mock(return_value=httpx.Response(200, json=MOCK_SPACES))
|
|
15
|
+
respx.post(f"{BASE_URL}/spaces/space-001/search/semantic").mock(
|
|
16
|
+
return_value=httpx.Response(200, json=MOCK_SEARCH)
|
|
17
|
+
)
|
|
18
|
+
cog = Cogspace(api_key=API_KEY, base_url=BASE_URL)
|
|
19
|
+
space = cog.space("code-patterns")
|
|
20
|
+
results = space.search("retry logic")
|
|
21
|
+
assert results.total == 1
|
|
22
|
+
assert results.results[0].file_path == "expertise/retry.md"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@respx.mock
|
|
26
|
+
def test_space_by_id_resolves():
|
|
27
|
+
respx.get(f"{BASE_URL}/spaces").mock(return_value=httpx.Response(200, json=MOCK_SPACES))
|
|
28
|
+
cog = Cogspace(api_key=API_KEY, base_url=BASE_URL)
|
|
29
|
+
space = cog.space("space-001")
|
|
30
|
+
assert space._space_id == "space-001"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@respx.mock
|
|
34
|
+
def test_space_name_cached():
|
|
35
|
+
route = respx.get(f"{BASE_URL}/spaces").mock(return_value=httpx.Response(200, json=MOCK_SPACES))
|
|
36
|
+
cog = Cogspace(api_key=API_KEY, base_url=BASE_URL)
|
|
37
|
+
cog.space("code-patterns")
|
|
38
|
+
cog.space("code-patterns")
|
|
39
|
+
assert route.call_count == 1 # only resolved once
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@respx.mock
|
|
43
|
+
def test_space_not_found():
|
|
44
|
+
respx.get(f"{BASE_URL}/spaces").mock(return_value=httpx.Response(200, json=MOCK_SPACES))
|
|
45
|
+
cog = Cogspace(api_key=API_KEY, base_url=BASE_URL)
|
|
46
|
+
with pytest.raises(NotFoundError):
|
|
47
|
+
cog.space("nonexistent-space")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@respx.mock
|
|
51
|
+
def test_search():
|
|
52
|
+
respx.get(f"{BASE_URL}/spaces").mock(return_value=httpx.Response(200, json=MOCK_SPACES))
|
|
53
|
+
respx.post(f"{BASE_URL}/spaces/space-001/search/semantic").mock(
|
|
54
|
+
return_value=httpx.Response(200, json=MOCK_SEARCH)
|
|
55
|
+
)
|
|
56
|
+
cog = Cogspace(api_key=API_KEY, base_url=BASE_URL)
|
|
57
|
+
space = cog.space("code-patterns")
|
|
58
|
+
results = space.search("retry logic")
|
|
59
|
+
assert len(results.results) == 1
|
|
60
|
+
assert results.results[0].score == 0.91
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@respx.mock
|
|
64
|
+
def test_read():
|
|
65
|
+
respx.get(f"{BASE_URL}/spaces").mock(return_value=httpx.Response(200, json=MOCK_SPACES))
|
|
66
|
+
respx.get(f"{BASE_URL}/spaces/space-001/read").mock(return_value=httpx.Response(200, json=MOCK_READ))
|
|
67
|
+
cog = Cogspace(api_key=API_KEY, base_url=BASE_URL)
|
|
68
|
+
space = cog.space("code-patterns")
|
|
69
|
+
result = space.read("expertise/retry.md")
|
|
70
|
+
assert result.path == "expertise/retry.md"
|
|
71
|
+
assert result.layer == "expertise"
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@respx.mock
|
|
75
|
+
def test_write():
|
|
76
|
+
respx.get(f"{BASE_URL}/spaces").mock(return_value=httpx.Response(200, json=MOCK_SPACES))
|
|
77
|
+
respx.post(f"{BASE_URL}/spaces/space-001/write").mock(return_value=httpx.Response(200, json=MOCK_WRITE))
|
|
78
|
+
cog = Cogspace(api_key=API_KEY, base_url=BASE_URL)
|
|
79
|
+
space = cog.space("code-patterns")
|
|
80
|
+
result = space.write("expertise/retry.md", "# Retry patterns\n...")
|
|
81
|
+
assert result.status == "created"
|
|
82
|
+
assert result.reindex_queued is True
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@respx.mock
|
|
86
|
+
def test_health():
|
|
87
|
+
respx.get(f"{BASE_URL}/spaces").mock(return_value=httpx.Response(200, json=MOCK_SPACES))
|
|
88
|
+
respx.get(f"{BASE_URL}/spaces/space-001/health").mock(return_value=httpx.Response(200, json=MOCK_HEALTH))
|
|
89
|
+
cog = Cogspace(api_key=API_KEY, base_url=BASE_URL)
|
|
90
|
+
space = cog.space("code-patterns")
|
|
91
|
+
h = space.health()
|
|
92
|
+
assert h.status == "ok"
|
|
93
|
+
assert h.vector_count == 10
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@respx.mock
|
|
97
|
+
def test_auth_error():
|
|
98
|
+
respx.get(f"{BASE_URL}/spaces").mock(
|
|
99
|
+
return_value=httpx.Response(401, json={"detail": "Invalid or revoked API key"})
|
|
100
|
+
)
|
|
101
|
+
cog = Cogspace(api_key="cs-badkey", base_url=BASE_URL)
|
|
102
|
+
with pytest.raises(AuthError):
|
|
103
|
+
cog.list_spaces()
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@respx.mock
|
|
107
|
+
def test_context_manager():
|
|
108
|
+
respx.get(f"{BASE_URL}/spaces").mock(return_value=httpx.Response(200, json=MOCK_SPACES))
|
|
109
|
+
with Cogspace(api_key=API_KEY, base_url=BASE_URL) as cog:
|
|
110
|
+
spaces = cog.list_spaces()
|
|
111
|
+
assert len(spaces) == 1
|