cluestate-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.
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.egg-info/
|
|
6
|
+
dist/
|
|
7
|
+
build/
|
|
8
|
+
*.egg
|
|
9
|
+
.eggs/
|
|
10
|
+
|
|
11
|
+
# Virtual environments
|
|
12
|
+
venv/
|
|
13
|
+
.venv/
|
|
14
|
+
env/
|
|
15
|
+
|
|
16
|
+
# IDE
|
|
17
|
+
.idea/
|
|
18
|
+
.vscode/
|
|
19
|
+
*.swp
|
|
20
|
+
*.swo
|
|
21
|
+
|
|
22
|
+
# Environment
|
|
23
|
+
.env
|
|
24
|
+
|
|
25
|
+
# Database
|
|
26
|
+
*.db
|
|
27
|
+
*.sqlite
|
|
28
|
+
*.sqlite3
|
|
29
|
+
|
|
30
|
+
# OS
|
|
31
|
+
.DS_Store
|
|
32
|
+
Thumbs.db
|
|
33
|
+
|
|
34
|
+
# Node (admin UI & TypeScript SDK)
|
|
35
|
+
node_modules/
|
|
36
|
+
admin/node_modules/
|
|
37
|
+
admin/dist/
|
|
38
|
+
sdks/typescript/dist/
|
|
39
|
+
|
|
40
|
+
# Distribution
|
|
41
|
+
*.whl
|
|
42
|
+
*.tar.gz
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: cluestate-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Lightweight Python SDK for ClueState Customer Intelligence Infrastructure
|
|
5
|
+
Project-URL: Homepage, https://cluestate.com
|
|
6
|
+
Project-URL: Documentation, https://github.com/cluestate/cluestate/blob/main/docs/python-sdk.md
|
|
7
|
+
Project-URL: Repository, https://github.com/cluestate/cluestate
|
|
8
|
+
Author: ClueState Contributors
|
|
9
|
+
License-Expression: Apache-2.0
|
|
10
|
+
Keywords: ai,cluestate,customer-intelligence,entity-state,sdk
|
|
11
|
+
Requires-Python: >=3.9
|
|
12
|
+
Requires-Dist: httpx>=0.25.0
|
|
13
|
+
Requires-Dist: pydantic>=2.0.0
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
# ClueState Python SDK
|
|
17
|
+
|
|
18
|
+
Lightweight, typed Python client (sync & async) for [ClueState](https://cluestate.com).
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
# Lightweight client-only package (depends only on httpx and pydantic)
|
|
24
|
+
pip install ./sdks/python
|
|
25
|
+
|
|
26
|
+
# Or if you have the full ClueState engine installed:
|
|
27
|
+
pip install .
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Quick Start
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
from cluestate_sdk import ClueState
|
|
34
|
+
|
|
35
|
+
cs = ClueState(
|
|
36
|
+
api_key="cs_live_YOUR_API_KEY",
|
|
37
|
+
base_url="https://app.cluestate.com",
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
# 1. Define a schema topic
|
|
41
|
+
cs.topics.create(
|
|
42
|
+
name="annual_budget",
|
|
43
|
+
description="Approved annual software budget",
|
|
44
|
+
data_type="number",
|
|
45
|
+
cardinality="single",
|
|
46
|
+
update_policy="replace",
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
# 2. Ingest an interaction
|
|
50
|
+
result = cs.ingest(
|
|
51
|
+
external_id="cust_402",
|
|
52
|
+
entity_name="Marcus Vance",
|
|
53
|
+
content="Our approved software budget for Q4 is $25,000.",
|
|
54
|
+
source_type="support_ticket",
|
|
55
|
+
idempotency_key="evt_ticket_8841",
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
for mem in result.memories:
|
|
59
|
+
print(f"[{mem.topic_name}] {mem.claim} (value={mem.structured_value})")
|
|
60
|
+
|
|
61
|
+
# 3. Retrieve compiled customer state
|
|
62
|
+
intel = cs.get_intel("cust_402")
|
|
63
|
+
print(intel.formatted_prompt)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Async Client (`AsyncClueState`)
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
import asyncio
|
|
70
|
+
from cluestate_sdk import AsyncClueState
|
|
71
|
+
|
|
72
|
+
async def main():
|
|
73
|
+
async with AsyncClueState(api_key="cs_live_YOUR_API_KEY") as cs:
|
|
74
|
+
intel = await cs.get_intel("cust_402")
|
|
75
|
+
print(intel.formatted_prompt)
|
|
76
|
+
|
|
77
|
+
asyncio.run(main())
|
|
78
|
+
```
|
|
79
|
+
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# ClueState Python SDK
|
|
2
|
+
|
|
3
|
+
Lightweight, typed Python client (sync & async) for [ClueState](https://cluestate.com).
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# Lightweight client-only package (depends only on httpx and pydantic)
|
|
9
|
+
pip install ./sdks/python
|
|
10
|
+
|
|
11
|
+
# Or if you have the full ClueState engine installed:
|
|
12
|
+
pip install .
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Quick Start
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
from cluestate_sdk import ClueState
|
|
19
|
+
|
|
20
|
+
cs = ClueState(
|
|
21
|
+
api_key="cs_live_YOUR_API_KEY",
|
|
22
|
+
base_url="https://app.cluestate.com",
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
# 1. Define a schema topic
|
|
26
|
+
cs.topics.create(
|
|
27
|
+
name="annual_budget",
|
|
28
|
+
description="Approved annual software budget",
|
|
29
|
+
data_type="number",
|
|
30
|
+
cardinality="single",
|
|
31
|
+
update_policy="replace",
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
# 2. Ingest an interaction
|
|
35
|
+
result = cs.ingest(
|
|
36
|
+
external_id="cust_402",
|
|
37
|
+
entity_name="Marcus Vance",
|
|
38
|
+
content="Our approved software budget for Q4 is $25,000.",
|
|
39
|
+
source_type="support_ticket",
|
|
40
|
+
idempotency_key="evt_ticket_8841",
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
for mem in result.memories:
|
|
44
|
+
print(f"[{mem.topic_name}] {mem.claim} (value={mem.structured_value})")
|
|
45
|
+
|
|
46
|
+
# 3. Retrieve compiled customer state
|
|
47
|
+
intel = cs.get_intel("cust_402")
|
|
48
|
+
print(intel.formatted_prompt)
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Async Client (`AsyncClueState`)
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
import asyncio
|
|
55
|
+
from cluestate_sdk import AsyncClueState
|
|
56
|
+
|
|
57
|
+
async def main():
|
|
58
|
+
async with AsyncClueState(api_key="cs_live_YOUR_API_KEY") as cs:
|
|
59
|
+
intel = await cs.get_intel("cust_402")
|
|
60
|
+
print(intel.formatted_prompt)
|
|
61
|
+
|
|
62
|
+
asyncio.run(main())
|
|
63
|
+
```
|
|
64
|
+
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "cluestate-sdk"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Lightweight Python SDK for ClueState Customer Intelligence Infrastructure"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "Apache-2.0"
|
|
11
|
+
requires-python = ">=3.9"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "ClueState Contributors" },
|
|
14
|
+
]
|
|
15
|
+
keywords = ["cluestate", "customer-intelligence", "entity-state", "sdk", "ai"]
|
|
16
|
+
dependencies = [
|
|
17
|
+
"httpx>=0.25.0",
|
|
18
|
+
"pydantic>=2.0.0",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
[project.urls]
|
|
22
|
+
Homepage = "https://cluestate.com"
|
|
23
|
+
Documentation = "https://github.com/cluestate/cluestate/blob/main/docs/python-sdk.md"
|
|
24
|
+
Repository = "https://github.com/cluestate/cluestate"
|
|
25
|
+
|
|
26
|
+
[tool.hatch.build.targets.wheel]
|
|
27
|
+
packages = ["src/cluestate_sdk"]
|
|
28
|
+
|
|
@@ -0,0 +1,636 @@
|
|
|
1
|
+
"""Standalone lightweight Python SDK for ClueState (depends only on httpx and pydantic)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from typing import Any, Literal, Optional, Union
|
|
8
|
+
|
|
9
|
+
import httpx
|
|
10
|
+
from pydantic import BaseModel, Field
|
|
11
|
+
|
|
12
|
+
DEFAULT_BASE_URL = "https://app.cluestate.com"
|
|
13
|
+
|
|
14
|
+
EntityTypeLiteral = Literal["person", "organization", "product", "location", "custom"]
|
|
15
|
+
MemoryStatusLiteral = Literal["active", "confirmed", "rejected", "superseded"]
|
|
16
|
+
ClaimTypeLiteral = Literal["explicit", "inferred", "observed"]
|
|
17
|
+
TopicCardinalityLiteral = Literal["single", "multiple"]
|
|
18
|
+
UpdatePolicyLiteral = Literal["replace", "append", "merge", "review"]
|
|
19
|
+
SourceTypeLiteral = Literal[
|
|
20
|
+
"conversation",
|
|
21
|
+
"email",
|
|
22
|
+
"survey",
|
|
23
|
+
"purchase",
|
|
24
|
+
"support_ticket",
|
|
25
|
+
"call_transcript",
|
|
26
|
+
"app_interaction",
|
|
27
|
+
"review",
|
|
28
|
+
"social_media",
|
|
29
|
+
"note",
|
|
30
|
+
"custom",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class ClueStateError(Exception):
|
|
35
|
+
"""Raised when the ClueState API returns an error response."""
|
|
36
|
+
|
|
37
|
+
def __init__(self, message: str, status_code: Optional[int] = None, body: Any = None):
|
|
38
|
+
super().__init__(message)
|
|
39
|
+
self.message = message
|
|
40
|
+
self.status_code = status_code
|
|
41
|
+
self.body = body
|
|
42
|
+
|
|
43
|
+
def __str__(self) -> str:
|
|
44
|
+
if self.status_code:
|
|
45
|
+
return f"[HTTP {self.status_code}] {self.message}"
|
|
46
|
+
return self.message
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# ──────────────────────────────────────────────
|
|
50
|
+
# Response Models
|
|
51
|
+
# ──────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
class Evidence(BaseModel):
|
|
54
|
+
id: int
|
|
55
|
+
episode_id: int
|
|
56
|
+
excerpt: str
|
|
57
|
+
is_verified: bool = False
|
|
58
|
+
char_start: Optional[int] = None
|
|
59
|
+
char_end: Optional[int] = None
|
|
60
|
+
content_hash: str = ""
|
|
61
|
+
created_at: datetime
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class Memory(BaseModel):
|
|
65
|
+
id: int
|
|
66
|
+
entity_id: int
|
|
67
|
+
topic_id: Optional[int] = None
|
|
68
|
+
topic_name: Optional[str] = None
|
|
69
|
+
claim: str
|
|
70
|
+
value: str
|
|
71
|
+
structured_value: Optional[Any] = None
|
|
72
|
+
claim_type: str = "explicit"
|
|
73
|
+
confidence: float
|
|
74
|
+
status: str
|
|
75
|
+
superseded_by_id: Optional[int] = None
|
|
76
|
+
valid_from: Optional[datetime] = None
|
|
77
|
+
valid_to: Optional[datetime] = None
|
|
78
|
+
created_at: datetime
|
|
79
|
+
updated_at: datetime
|
|
80
|
+
evidence: list[Evidence] = Field(default_factory=list)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class Entity(BaseModel):
|
|
84
|
+
id: int
|
|
85
|
+
external_id: str
|
|
86
|
+
name: str
|
|
87
|
+
entity_type: str
|
|
88
|
+
meta: dict[str, Any] = Field(default_factory=dict)
|
|
89
|
+
created_at: datetime
|
|
90
|
+
updated_at: datetime
|
|
91
|
+
memory_count: int = 0
|
|
92
|
+
memories: list[Memory] = Field(default_factory=list)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class Topic(BaseModel):
|
|
96
|
+
id: int
|
|
97
|
+
name: str
|
|
98
|
+
description: str
|
|
99
|
+
data_type: str
|
|
100
|
+
cardinality: str = "multiple"
|
|
101
|
+
update_policy: str = "append"
|
|
102
|
+
temporal: bool = False
|
|
103
|
+
enum_values: list[str] = Field(default_factory=list)
|
|
104
|
+
schema_version: int = 1
|
|
105
|
+
is_active: bool = True
|
|
106
|
+
created_at: datetime
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class Episode(BaseModel):
|
|
110
|
+
id: int
|
|
111
|
+
entity_id: int
|
|
112
|
+
source_type: str
|
|
113
|
+
source_record_id: Optional[str] = None
|
|
114
|
+
content: str
|
|
115
|
+
content_hash: str = ""
|
|
116
|
+
source_description: str = ""
|
|
117
|
+
meta: dict[str, Any] = Field(default_factory=dict)
|
|
118
|
+
occurred_at: datetime
|
|
119
|
+
created_at: datetime
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class IngestResponse(BaseModel):
|
|
123
|
+
episode_id: int
|
|
124
|
+
entity_id: int
|
|
125
|
+
memories_extracted: int
|
|
126
|
+
memories_updated: int
|
|
127
|
+
memories_new: int
|
|
128
|
+
idempotent_replay: bool = False
|
|
129
|
+
memories: list[Memory] = Field(default_factory=list)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class IntelResponse(BaseModel):
|
|
133
|
+
entity_id: int
|
|
134
|
+
entity_name: str
|
|
135
|
+
entity_type: str
|
|
136
|
+
formatted_prompt: str
|
|
137
|
+
memories: list[Memory] = Field(default_factory=list)
|
|
138
|
+
total_memories: int = 0
|
|
139
|
+
total_episodes: int = 0
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _build_headers(api_key: str) -> dict[str, str]:
|
|
143
|
+
headers = {
|
|
144
|
+
"Content-Type": "application/json",
|
|
145
|
+
"Accept": "application/json",
|
|
146
|
+
"User-Agent": "cluestate-sdk-python/0.1.0",
|
|
147
|
+
}
|
|
148
|
+
if api_key:
|
|
149
|
+
headers["Authorization"] = f"Bearer {api_key}"
|
|
150
|
+
return headers
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _normalize_base_url(base_url: str) -> str:
|
|
154
|
+
url = base_url.rstrip("/")
|
|
155
|
+
if not url.endswith("/api"):
|
|
156
|
+
url = f"{url}/api"
|
|
157
|
+
return url
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _raise_for_response(response: httpx.Response) -> None:
|
|
161
|
+
if response.is_success:
|
|
162
|
+
return
|
|
163
|
+
detail = response.reason_phrase or "Request failed"
|
|
164
|
+
body = None
|
|
165
|
+
try:
|
|
166
|
+
body = response.json()
|
|
167
|
+
if isinstance(body, dict) and "detail" in body:
|
|
168
|
+
detail = str(body["detail"])
|
|
169
|
+
except Exception:
|
|
170
|
+
detail = response.text or detail
|
|
171
|
+
raise ClueStateError(message=detail, status_code=response.status_code, body=body)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
# ──────────────────────────────────────────────
|
|
175
|
+
# Sync Resources & Client
|
|
176
|
+
# ──────────────────────────────────────────────
|
|
177
|
+
|
|
178
|
+
class EntitiesResource:
|
|
179
|
+
def __init__(self, client: "ClueState"):
|
|
180
|
+
self._client = client
|
|
181
|
+
|
|
182
|
+
def create(
|
|
183
|
+
self,
|
|
184
|
+
external_id: str,
|
|
185
|
+
name: str = "",
|
|
186
|
+
entity_type: EntityTypeLiteral = "person",
|
|
187
|
+
meta: Optional[dict[str, Any]] = None,
|
|
188
|
+
) -> Entity:
|
|
189
|
+
data = self._client._request(
|
|
190
|
+
"POST",
|
|
191
|
+
"/entities",
|
|
192
|
+
json={"external_id": external_id, "name": name, "entity_type": entity_type, "meta": meta or {}},
|
|
193
|
+
)
|
|
194
|
+
return Entity.model_validate(data)
|
|
195
|
+
|
|
196
|
+
def list(self, search: str = "", limit: int = 50, offset: int = 0) -> list[Entity]:
|
|
197
|
+
data = self._client._request("GET", "/entities", params={"search": search, "limit": limit, "offset": offset})
|
|
198
|
+
return [Entity.model_validate(item) for item in data]
|
|
199
|
+
|
|
200
|
+
def get(self, entity_id: int) -> Entity:
|
|
201
|
+
data = self._client._request("GET", f"/entities/{entity_id}")
|
|
202
|
+
return Entity.model_validate(data)
|
|
203
|
+
|
|
204
|
+
def episodes(self, entity_id: int, limit: int = 50) -> list[Episode]:
|
|
205
|
+
data = self._client._request("GET", f"/entities/{entity_id}/episodes", params={"limit": limit})
|
|
206
|
+
return [Episode.model_validate(item) for item in data]
|
|
207
|
+
|
|
208
|
+
def delete(self, entity_id: int) -> None:
|
|
209
|
+
self._client._request("DELETE", f"/entities/{entity_id}")
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
class MemoriesResource:
|
|
213
|
+
def __init__(self, client: "ClueState"):
|
|
214
|
+
self._client = client
|
|
215
|
+
|
|
216
|
+
def list(
|
|
217
|
+
self,
|
|
218
|
+
entity_id: int,
|
|
219
|
+
status: Optional[MemoryStatusLiteral] = None,
|
|
220
|
+
topic_id: Optional[int] = None,
|
|
221
|
+
) -> list[Memory]:
|
|
222
|
+
params: dict[str, Any] = {}
|
|
223
|
+
if status is not None:
|
|
224
|
+
params["status"] = status
|
|
225
|
+
if topic_id is not None:
|
|
226
|
+
params["topic_id"] = topic_id
|
|
227
|
+
data = self._client._request("GET", f"/entities/{entity_id}/memories", params=params)
|
|
228
|
+
return [Memory.model_validate(item) for item in data]
|
|
229
|
+
|
|
230
|
+
def update(
|
|
231
|
+
self,
|
|
232
|
+
memory_id: int,
|
|
233
|
+
status: Optional[MemoryStatusLiteral] = None,
|
|
234
|
+
confidence: Optional[float] = None,
|
|
235
|
+
) -> Memory:
|
|
236
|
+
payload: dict[str, Any] = {}
|
|
237
|
+
if status is not None:
|
|
238
|
+
payload["status"] = status
|
|
239
|
+
if confidence is not None:
|
|
240
|
+
payload["confidence"] = confidence
|
|
241
|
+
data = self._client._request("PATCH", f"/memories/{memory_id}", json=payload)
|
|
242
|
+
return Memory.model_validate(data)
|
|
243
|
+
|
|
244
|
+
def confirm(self, memory_id: int) -> Memory:
|
|
245
|
+
return self.update(memory_id=memory_id, status="confirmed")
|
|
246
|
+
|
|
247
|
+
def reject(self, memory_id: int) -> Memory:
|
|
248
|
+
return self.update(memory_id=memory_id, status="rejected")
|
|
249
|
+
|
|
250
|
+
def delete(self, memory_id: int) -> None:
|
|
251
|
+
self._client._request("DELETE", f"/memories/{memory_id}")
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
class TopicsResource:
|
|
255
|
+
def __init__(self, client: "ClueState"):
|
|
256
|
+
self._client = client
|
|
257
|
+
|
|
258
|
+
def create(
|
|
259
|
+
self,
|
|
260
|
+
name: str,
|
|
261
|
+
description: str = "",
|
|
262
|
+
data_type: str = "text",
|
|
263
|
+
cardinality: TopicCardinalityLiteral = "multiple",
|
|
264
|
+
update_policy: UpdatePolicyLiteral = "append",
|
|
265
|
+
temporal: bool = False,
|
|
266
|
+
enum_values: Optional[list[str]] = None,
|
|
267
|
+
) -> Topic:
|
|
268
|
+
data = self._client._request(
|
|
269
|
+
"POST",
|
|
270
|
+
"/topics",
|
|
271
|
+
json={
|
|
272
|
+
"name": name,
|
|
273
|
+
"description": description,
|
|
274
|
+
"data_type": data_type,
|
|
275
|
+
"cardinality": cardinality,
|
|
276
|
+
"update_policy": update_policy,
|
|
277
|
+
"temporal": temporal,
|
|
278
|
+
"enum_values": enum_values or [],
|
|
279
|
+
},
|
|
280
|
+
)
|
|
281
|
+
return Topic.model_validate(data)
|
|
282
|
+
|
|
283
|
+
def list(self, active_only: bool = True) -> list[Topic]:
|
|
284
|
+
data = self._client._request("GET", "/topics", params={"active_only": active_only})
|
|
285
|
+
return [Topic.model_validate(item) for item in data]
|
|
286
|
+
|
|
287
|
+
def update(self, topic_id: int, **kwargs: Any) -> Topic:
|
|
288
|
+
payload = {k: v for k, v in kwargs.items() if v is not None}
|
|
289
|
+
data = self._client._request("PUT", f"/topics/{topic_id}", json=payload)
|
|
290
|
+
return Topic.model_validate(data)
|
|
291
|
+
|
|
292
|
+
def delete(self, topic_id: int) -> None:
|
|
293
|
+
self._client._request("DELETE", f"/topics/{topic_id}")
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
class ClueState:
|
|
297
|
+
"""Synchronous ClueState SDK Client."""
|
|
298
|
+
|
|
299
|
+
def __init__(
|
|
300
|
+
self,
|
|
301
|
+
api_key: Optional[str] = None,
|
|
302
|
+
base_url: Optional[str] = None,
|
|
303
|
+
timeout: float = 60.0,
|
|
304
|
+
http_client: Optional[httpx.Client] = None,
|
|
305
|
+
):
|
|
306
|
+
self.api_key = api_key if api_key is not None else os.getenv("CLUESTATE_API_KEY", "")
|
|
307
|
+
raw_url = base_url or os.getenv("CLUESTATE_API_URL") or DEFAULT_BASE_URL
|
|
308
|
+
self.base_url = _normalize_base_url(raw_url)
|
|
309
|
+
self._owns_client = http_client is None
|
|
310
|
+
self._http = http_client or httpx.Client(
|
|
311
|
+
base_url=self.base_url,
|
|
312
|
+
headers=_build_headers(self.api_key),
|
|
313
|
+
timeout=timeout,
|
|
314
|
+
)
|
|
315
|
+
self.entities = EntitiesResource(self)
|
|
316
|
+
self.memories = MemoriesResource(self)
|
|
317
|
+
self.topics = TopicsResource(self)
|
|
318
|
+
|
|
319
|
+
def _request(
|
|
320
|
+
self,
|
|
321
|
+
method: str,
|
|
322
|
+
path: str,
|
|
323
|
+
params: Optional[dict[str, Any]] = None,
|
|
324
|
+
json: Optional[dict[str, Any]] = None,
|
|
325
|
+
) -> Any:
|
|
326
|
+
res = self._http.request(method, path, params=params, json=json)
|
|
327
|
+
_raise_for_response(res)
|
|
328
|
+
if res.status_code == 204 or not res.content:
|
|
329
|
+
return None
|
|
330
|
+
return res.json()
|
|
331
|
+
|
|
332
|
+
def ingest(
|
|
333
|
+
self,
|
|
334
|
+
content: str,
|
|
335
|
+
*,
|
|
336
|
+
external_id: Optional[str] = None,
|
|
337
|
+
entity_id: Optional[int] = None,
|
|
338
|
+
entity_name: str = "",
|
|
339
|
+
source_type: SourceTypeLiteral = "conversation",
|
|
340
|
+
source_record_id: Optional[str] = None,
|
|
341
|
+
idempotency_key: Optional[str] = None,
|
|
342
|
+
occurred_at: Optional[Union[datetime, str]] = None,
|
|
343
|
+
source_description: str = "",
|
|
344
|
+
meta: Optional[dict[str, Any]] = None,
|
|
345
|
+
auto_create_entity: bool = True,
|
|
346
|
+
) -> IngestResponse:
|
|
347
|
+
if not external_id and not entity_id:
|
|
348
|
+
raise ValueError("Provide either 'external_id' or 'entity_id'")
|
|
349
|
+
|
|
350
|
+
payload: dict[str, Any] = {
|
|
351
|
+
"content": content,
|
|
352
|
+
"entity_name": entity_name,
|
|
353
|
+
"source_type": source_type,
|
|
354
|
+
"source_description": source_description,
|
|
355
|
+
"meta": meta or {},
|
|
356
|
+
"auto_create_entity": auto_create_entity,
|
|
357
|
+
}
|
|
358
|
+
if external_id is not None:
|
|
359
|
+
payload["external_id"] = external_id
|
|
360
|
+
if entity_id is not None:
|
|
361
|
+
payload["entity_id"] = entity_id
|
|
362
|
+
if source_record_id is not None:
|
|
363
|
+
payload["source_record_id"] = source_record_id
|
|
364
|
+
if idempotency_key is not None:
|
|
365
|
+
payload["idempotency_key"] = idempotency_key
|
|
366
|
+
if occurred_at is not None:
|
|
367
|
+
payload["occurred_at"] = (
|
|
368
|
+
occurred_at.isoformat() if isinstance(occurred_at, datetime) else occurred_at
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
data = self._request("POST", "/ingest", json=payload)
|
|
372
|
+
return IngestResponse.model_validate(data)
|
|
373
|
+
|
|
374
|
+
def get_intel(
|
|
375
|
+
self,
|
|
376
|
+
external_id: Optional[str] = None,
|
|
377
|
+
*,
|
|
378
|
+
entity_id: Optional[int] = None,
|
|
379
|
+
) -> IntelResponse:
|
|
380
|
+
if external_id:
|
|
381
|
+
data = self._request("GET", f"/intel/by-external-id/{external_id}")
|
|
382
|
+
elif entity_id is not None:
|
|
383
|
+
data = self._request("GET", f"/entities/{entity_id}/intel")
|
|
384
|
+
else:
|
|
385
|
+
raise ValueError("Provide either 'external_id' or 'entity_id'")
|
|
386
|
+
return IntelResponse.model_validate(data)
|
|
387
|
+
|
|
388
|
+
def close(self) -> None:
|
|
389
|
+
if self._owns_client:
|
|
390
|
+
self._http.close()
|
|
391
|
+
|
|
392
|
+
def __enter__(self) -> "ClueState":
|
|
393
|
+
return self
|
|
394
|
+
|
|
395
|
+
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
396
|
+
self.close()
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
# ──────────────────────────────────────────────
|
|
400
|
+
# Async Resources & Client
|
|
401
|
+
# ──────────────────────────────────────────────
|
|
402
|
+
|
|
403
|
+
class AsyncEntitiesResource:
|
|
404
|
+
def __init__(self, client: "AsyncClueState"):
|
|
405
|
+
self._client = client
|
|
406
|
+
|
|
407
|
+
async def create(
|
|
408
|
+
self,
|
|
409
|
+
external_id: str,
|
|
410
|
+
name: str = "",
|
|
411
|
+
entity_type: EntityTypeLiteral = "person",
|
|
412
|
+
meta: Optional[dict[str, Any]] = None,
|
|
413
|
+
) -> Entity:
|
|
414
|
+
data = await self._client._request(
|
|
415
|
+
"POST",
|
|
416
|
+
"/entities",
|
|
417
|
+
json={"external_id": external_id, "name": name, "entity_type": entity_type, "meta": meta or {}},
|
|
418
|
+
)
|
|
419
|
+
return Entity.model_validate(data)
|
|
420
|
+
|
|
421
|
+
async def list(self, search: str = "", limit: int = 50, offset: int = 0) -> list[Entity]:
|
|
422
|
+
data = await self._client._request("GET", "/entities", params={"search": search, "limit": limit, "offset": offset})
|
|
423
|
+
return [Entity.model_validate(item) for item in data]
|
|
424
|
+
|
|
425
|
+
async def get(self, entity_id: int) -> Entity:
|
|
426
|
+
data = await self._client._request("GET", f"/entities/{entity_id}")
|
|
427
|
+
return Entity.model_validate(data)
|
|
428
|
+
|
|
429
|
+
async def episodes(self, entity_id: int, limit: int = 50) -> list[Episode]:
|
|
430
|
+
data = await self._client._request("GET", f"/entities/{entity_id}/episodes", params={"limit": limit})
|
|
431
|
+
return [Episode.model_validate(item) for item in data]
|
|
432
|
+
|
|
433
|
+
async def delete(self, entity_id: int) -> None:
|
|
434
|
+
await self._client._request("DELETE", f"/entities/{entity_id}")
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
class AsyncMemoriesResource:
|
|
438
|
+
def __init__(self, client: "AsyncClueState"):
|
|
439
|
+
self._client = client
|
|
440
|
+
|
|
441
|
+
async def list(
|
|
442
|
+
self,
|
|
443
|
+
entity_id: int,
|
|
444
|
+
status: Optional[MemoryStatusLiteral] = None,
|
|
445
|
+
topic_id: Optional[int] = None,
|
|
446
|
+
) -> list[Memory]:
|
|
447
|
+
params: dict[str, Any] = {}
|
|
448
|
+
if status is not None:
|
|
449
|
+
params["status"] = status
|
|
450
|
+
if topic_id is not None:
|
|
451
|
+
params["topic_id"] = topic_id
|
|
452
|
+
data = await self._client._request("GET", f"/entities/{entity_id}/memories", params=params)
|
|
453
|
+
return [Memory.model_validate(item) for item in data]
|
|
454
|
+
|
|
455
|
+
async def update(
|
|
456
|
+
self,
|
|
457
|
+
memory_id: int,
|
|
458
|
+
status: Optional[MemoryStatusLiteral] = None,
|
|
459
|
+
confidence: Optional[float] = None,
|
|
460
|
+
) -> Memory:
|
|
461
|
+
payload: dict[str, Any] = {}
|
|
462
|
+
if status is not None:
|
|
463
|
+
payload["status"] = status
|
|
464
|
+
if confidence is not None:
|
|
465
|
+
payload["confidence"] = confidence
|
|
466
|
+
data = await self._client._request("PATCH", f"/memories/{memory_id}", json=payload)
|
|
467
|
+
return Memory.model_validate(data)
|
|
468
|
+
|
|
469
|
+
async def confirm(self, memory_id: int) -> Memory:
|
|
470
|
+
return await self.update(memory_id=memory_id, status="confirmed")
|
|
471
|
+
|
|
472
|
+
async def reject(self, memory_id: int) -> Memory:
|
|
473
|
+
return await self.update(memory_id=memory_id, status="rejected")
|
|
474
|
+
|
|
475
|
+
async def delete(self, memory_id: int) -> None:
|
|
476
|
+
await self._client._request("DELETE", f"/memories/{memory_id}")
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
class AsyncTopicsResource:
|
|
480
|
+
def __init__(self, client: "AsyncClueState"):
|
|
481
|
+
self._client = client
|
|
482
|
+
|
|
483
|
+
async def create(
|
|
484
|
+
self,
|
|
485
|
+
name: str,
|
|
486
|
+
description: str = "",
|
|
487
|
+
data_type: str = "text",
|
|
488
|
+
cardinality: TopicCardinalityLiteral = "multiple",
|
|
489
|
+
update_policy: UpdatePolicyLiteral = "append",
|
|
490
|
+
temporal: bool = False,
|
|
491
|
+
enum_values: Optional[list[str]] = None,
|
|
492
|
+
) -> Topic:
|
|
493
|
+
data = await self._client._request(
|
|
494
|
+
"POST",
|
|
495
|
+
"/topics",
|
|
496
|
+
json={
|
|
497
|
+
"name": name,
|
|
498
|
+
"description": description,
|
|
499
|
+
"data_type": data_type,
|
|
500
|
+
"cardinality": cardinality,
|
|
501
|
+
"update_policy": update_policy,
|
|
502
|
+
"temporal": temporal,
|
|
503
|
+
"enum_values": enum_values or [],
|
|
504
|
+
},
|
|
505
|
+
)
|
|
506
|
+
return Topic.model_validate(data)
|
|
507
|
+
|
|
508
|
+
async def list(self, active_only: bool = True) -> list[Topic]:
|
|
509
|
+
data = await self._client._request("GET", "/topics", params={"active_only": active_only})
|
|
510
|
+
return [Topic.model_validate(item) for item in data]
|
|
511
|
+
|
|
512
|
+
async def update(self, topic_id: int, **kwargs: Any) -> Topic:
|
|
513
|
+
payload = {k: v for k, v in kwargs.items() if v is not None}
|
|
514
|
+
data = await self._client._request("PUT", f"/topics/{topic_id}", json=payload)
|
|
515
|
+
return Topic.model_validate(data)
|
|
516
|
+
|
|
517
|
+
async def delete(self, topic_id: int) -> None:
|
|
518
|
+
await self._client._request("DELETE", f"/topics/{topic_id}")
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
class AsyncClueState:
|
|
522
|
+
"""Asynchronous ClueState SDK Client."""
|
|
523
|
+
|
|
524
|
+
def __init__(
|
|
525
|
+
self,
|
|
526
|
+
api_key: Optional[str] = None,
|
|
527
|
+
base_url: Optional[str] = None,
|
|
528
|
+
timeout: float = 60.0,
|
|
529
|
+
http_client: Optional[httpx.AsyncClient] = None,
|
|
530
|
+
):
|
|
531
|
+
self.api_key = api_key if api_key is not None else os.getenv("CLUESTATE_API_KEY", "")
|
|
532
|
+
raw_url = base_url or os.getenv("CLUESTATE_API_URL") or DEFAULT_BASE_URL
|
|
533
|
+
self.base_url = _normalize_base_url(raw_url)
|
|
534
|
+
self._owns_client = http_client is None
|
|
535
|
+
self._http = http_client or httpx.AsyncClient(
|
|
536
|
+
base_url=self.base_url,
|
|
537
|
+
headers=_build_headers(self.api_key),
|
|
538
|
+
timeout=timeout,
|
|
539
|
+
)
|
|
540
|
+
self.entities = AsyncEntitiesResource(self)
|
|
541
|
+
self.memories = AsyncMemoriesResource(self)
|
|
542
|
+
self.topics = AsyncTopicsResource(self)
|
|
543
|
+
|
|
544
|
+
async def _request(
|
|
545
|
+
self,
|
|
546
|
+
method: str,
|
|
547
|
+
path: str,
|
|
548
|
+
params: Optional[dict[str, Any]] = None,
|
|
549
|
+
json: Optional[dict[str, Any]] = None,
|
|
550
|
+
) -> Any:
|
|
551
|
+
res = await self._http.request(method, path, params=params, json=json)
|
|
552
|
+
_raise_for_response(res)
|
|
553
|
+
if res.status_code == 204 or not res.content:
|
|
554
|
+
return None
|
|
555
|
+
return res.json()
|
|
556
|
+
|
|
557
|
+
async def ingest(
|
|
558
|
+
self,
|
|
559
|
+
content: str,
|
|
560
|
+
*,
|
|
561
|
+
external_id: Optional[str] = None,
|
|
562
|
+
entity_id: Optional[int] = None,
|
|
563
|
+
entity_name: str = "",
|
|
564
|
+
source_type: SourceTypeLiteral = "conversation",
|
|
565
|
+
source_record_id: Optional[str] = None,
|
|
566
|
+
idempotency_key: Optional[str] = None,
|
|
567
|
+
occurred_at: Optional[Union[datetime, str]] = None,
|
|
568
|
+
source_description: str = "",
|
|
569
|
+
meta: Optional[dict[str, Any]] = None,
|
|
570
|
+
auto_create_entity: bool = True,
|
|
571
|
+
) -> IngestResponse:
|
|
572
|
+
if not external_id and not entity_id:
|
|
573
|
+
raise ValueError("Provide either 'external_id' or 'entity_id'")
|
|
574
|
+
|
|
575
|
+
payload: dict[str, Any] = {
|
|
576
|
+
"content": content,
|
|
577
|
+
"entity_name": entity_name,
|
|
578
|
+
"source_type": source_type,
|
|
579
|
+
"source_description": source_description,
|
|
580
|
+
"meta": meta or {},
|
|
581
|
+
"auto_create_entity": auto_create_entity,
|
|
582
|
+
}
|
|
583
|
+
if external_id is not None:
|
|
584
|
+
payload["external_id"] = external_id
|
|
585
|
+
if entity_id is not None:
|
|
586
|
+
payload["entity_id"] = entity_id
|
|
587
|
+
if source_record_id is not None:
|
|
588
|
+
payload["source_record_id"] = source_record_id
|
|
589
|
+
if idempotency_key is not None:
|
|
590
|
+
payload["idempotency_key"] = idempotency_key
|
|
591
|
+
if occurred_at is not None:
|
|
592
|
+
payload["occurred_at"] = (
|
|
593
|
+
occurred_at.isoformat() if isinstance(occurred_at, datetime) else occurred_at
|
|
594
|
+
)
|
|
595
|
+
|
|
596
|
+
data = await self._request("POST", "/ingest", json=payload)
|
|
597
|
+
return IngestResponse.model_validate(data)
|
|
598
|
+
|
|
599
|
+
async def get_intel(
|
|
600
|
+
self,
|
|
601
|
+
external_id: Optional[str] = None,
|
|
602
|
+
*,
|
|
603
|
+
entity_id: Optional[int] = None,
|
|
604
|
+
) -> IntelResponse:
|
|
605
|
+
if external_id:
|
|
606
|
+
data = await self._request("GET", f"/intel/by-external-id/{external_id}")
|
|
607
|
+
elif entity_id is not None:
|
|
608
|
+
data = await self._request("GET", f"/entities/{entity_id}/intel")
|
|
609
|
+
else:
|
|
610
|
+
raise ValueError("Provide either 'external_id' or 'entity_id'")
|
|
611
|
+
return IntelResponse.model_validate(data)
|
|
612
|
+
|
|
613
|
+
async def close(self) -> None:
|
|
614
|
+
if self._owns_client:
|
|
615
|
+
await self._http.aclose()
|
|
616
|
+
|
|
617
|
+
async def __aenter__(self) -> "AsyncClueState":
|
|
618
|
+
return self
|
|
619
|
+
|
|
620
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
621
|
+
await self.close()
|
|
622
|
+
|
|
623
|
+
|
|
624
|
+
__all__ = [
|
|
625
|
+
"ClueState",
|
|
626
|
+
"AsyncClueState",
|
|
627
|
+
"ClueStateError",
|
|
628
|
+
"Entity",
|
|
629
|
+
"Memory",
|
|
630
|
+
"Evidence",
|
|
631
|
+
"Topic",
|
|
632
|
+
"Episode",
|
|
633
|
+
"IngestResponse",
|
|
634
|
+
"IntelResponse",
|
|
635
|
+
]
|
|
636
|
+
|