hsafa-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,82 @@
1
+ Metadata-Version: 2.4
2
+ Name: hsafa-sdk
3
+ Version: 0.1.0
4
+ Summary: Hsafa SDK v7 — connect any service to a Haseef brain
5
+ Requires-Python: >=3.9
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: httpx>=0.27.0
8
+
9
+ # hsafa-sdk
10
+
11
+ Python SDK for **Hsafa Core v7**. Connect any service to a Haseef brain — register tools, handle tool calls over SSE, push events, and read/write the haseef's memory and profile.
12
+
13
+ ## Install
14
+
15
+ ```bash
16
+ pip install hsafa-sdk
17
+ ```
18
+
19
+ Or from source:
20
+
21
+ ```bash
22
+ cd sdks/python
23
+ pip install -e .
24
+ ```
25
+
26
+ ## Quick Start
27
+
28
+ ```python
29
+ import asyncio
30
+ import os
31
+ from hsafa_sdk import HsafaSDK, SdkOptions
32
+
33
+ async def main():
34
+ sdk = HsafaSDK(SdkOptions(
35
+ core_url="http://localhost:3001",
36
+ api_key=os.environ.get("HSAFA_CORE_KEY", "test-key"),
37
+ skill="weather",
38
+ ))
39
+
40
+ # 1. Register tools
41
+ await sdk.register_tools([{
42
+ "name": "get_weather",
43
+ "description": "Get current weather for a city",
44
+ "input": {"city": "string", "units": "string?"},
45
+ }])
46
+
47
+ # 2. Handle tool calls
48
+ async def handle_weather(args, ctx):
49
+ print(f"{ctx['haseef']['name']} wants weather for {args.get('city')}")
50
+ return {"temperature": 72, "conditions": "sunny", "city": args.get("city")}
51
+
52
+ sdk.on_tool_call("get_weather", handle_weather)
53
+
54
+ # 3. Listen to lifecycle events
55
+ def on_run_started(e):
56
+ print(f"Run started for {e['haseef']['name']}")
57
+
58
+ sdk.on("run.started", on_run_started)
59
+
60
+ # 4. Connect the SSE stream (blocks until disconnect)
61
+ # For background usage alongside a web server:
62
+ # asyncio.create_task(sdk.connect())
63
+ try:
64
+ await sdk.connect()
65
+ except KeyboardInterrupt:
66
+ await sdk.disconnect()
67
+
68
+ if __name__ == "__main__":
69
+ asyncio.run(main())
70
+ ```
71
+
72
+ ## Namespaces
73
+
74
+ | Namespace | Methods |
75
+ |-----------|---------|
76
+ | `sdk.haseef` | `list()`, `get(id)`, `create(data)`, `update(id, patch)`, `delete(id)`, `get_profile(id)`, `update_profile(id, patch)`, `add_skill(id, name)`, `remove_skill(id, name)`, `status(id)` |
77
+ | `sdk.memory` | `list(id)`, `search(id, query, limit)`, `set(id, memories)`, `delete(id, keys)`, `episodes(id, limit)`, `search_episodes(id, query, limit)`, `social(id)`, `procedural(id)`, `stats(id)` |
78
+ | `sdk.runs` | `list(haseef_id?, status?, limit?)`, `get(run_id)` |
79
+
80
+ ## License
81
+
82
+ MIT
@@ -0,0 +1,74 @@
1
+ # hsafa-sdk
2
+
3
+ Python SDK for **Hsafa Core v7**. Connect any service to a Haseef brain — register tools, handle tool calls over SSE, push events, and read/write the haseef's memory and profile.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install hsafa-sdk
9
+ ```
10
+
11
+ Or from source:
12
+
13
+ ```bash
14
+ cd sdks/python
15
+ pip install -e .
16
+ ```
17
+
18
+ ## Quick Start
19
+
20
+ ```python
21
+ import asyncio
22
+ import os
23
+ from hsafa_sdk import HsafaSDK, SdkOptions
24
+
25
+ async def main():
26
+ sdk = HsafaSDK(SdkOptions(
27
+ core_url="http://localhost:3001",
28
+ api_key=os.environ.get("HSAFA_CORE_KEY", "test-key"),
29
+ skill="weather",
30
+ ))
31
+
32
+ # 1. Register tools
33
+ await sdk.register_tools([{
34
+ "name": "get_weather",
35
+ "description": "Get current weather for a city",
36
+ "input": {"city": "string", "units": "string?"},
37
+ }])
38
+
39
+ # 2. Handle tool calls
40
+ async def handle_weather(args, ctx):
41
+ print(f"{ctx['haseef']['name']} wants weather for {args.get('city')}")
42
+ return {"temperature": 72, "conditions": "sunny", "city": args.get("city")}
43
+
44
+ sdk.on_tool_call("get_weather", handle_weather)
45
+
46
+ # 3. Listen to lifecycle events
47
+ def on_run_started(e):
48
+ print(f"Run started for {e['haseef']['name']}")
49
+
50
+ sdk.on("run.started", on_run_started)
51
+
52
+ # 4. Connect the SSE stream (blocks until disconnect)
53
+ # For background usage alongside a web server:
54
+ # asyncio.create_task(sdk.connect())
55
+ try:
56
+ await sdk.connect()
57
+ except KeyboardInterrupt:
58
+ await sdk.disconnect()
59
+
60
+ if __name__ == "__main__":
61
+ asyncio.run(main())
62
+ ```
63
+
64
+ ## Namespaces
65
+
66
+ | Namespace | Methods |
67
+ |-----------|---------|
68
+ | `sdk.haseef` | `list()`, `get(id)`, `create(data)`, `update(id, patch)`, `delete(id)`, `get_profile(id)`, `update_profile(id, patch)`, `add_skill(id, name)`, `remove_skill(id, name)`, `status(id)` |
69
+ | `sdk.memory` | `list(id)`, `search(id, query, limit)`, `set(id, memories)`, `delete(id, keys)`, `episodes(id, limit)`, `search_episodes(id, query, limit)`, `social(id)`, `procedural(id)`, `stats(id)` |
70
+ | `sdk.runs` | `list(haseef_id?, status?, limit?)`, `get(run_id)` |
71
+
72
+ ## License
73
+
74
+ MIT
@@ -0,0 +1,16 @@
1
+ [project]
2
+ name = "hsafa-sdk"
3
+ version = "0.1.0"
4
+ description = "Hsafa SDK v7 — connect any service to a Haseef brain"
5
+ readme = "README.md"
6
+ requires-python = ">=3.9"
7
+ dependencies = [
8
+ "httpx>=0.27.0",
9
+ ]
10
+
11
+ [build-system]
12
+ requires = ["setuptools>=61.0"]
13
+ build-backend = "setuptools.build_meta"
14
+
15
+ [tool.setuptools.packages.find]
16
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,59 @@
1
+ from .sdk import HsafaSDK
2
+ from .types import (
3
+ SdkOptions,
4
+ ToolDefinition,
5
+ ToolHandler,
6
+ ToolCallContext,
7
+ HaseefContext,
8
+ PushEventPayload,
9
+ Attachment,
10
+ ToolInputStartEvent,
11
+ ToolInputDeltaEvent,
12
+ ToolCallEvent,
13
+ ToolResultEvent,
14
+ ToolErrorEvent,
15
+ RunStartedEvent,
16
+ RunCompletedEvent,
17
+ Haseef,
18
+ CreateHaseefInput,
19
+ UpdateHaseefInput,
20
+ SemanticMemory,
21
+ SemanticMemoryInput,
22
+ EpisodicMemory,
23
+ SocialMemory,
24
+ ProceduralMemory,
25
+ MemoryStats,
26
+ Run,
27
+ ListRunsOptions,
28
+ )
29
+ from .schema import input_to_json_schema
30
+
31
+ __all__ = [
32
+ "HsafaSDK",
33
+ "SdkOptions",
34
+ "ToolDefinition",
35
+ "ToolHandler",
36
+ "ToolCallContext",
37
+ "HaseefContext",
38
+ "PushEventPayload",
39
+ "Attachment",
40
+ "ToolInputStartEvent",
41
+ "ToolInputDeltaEvent",
42
+ "ToolCallEvent",
43
+ "ToolResultEvent",
44
+ "ToolErrorEvent",
45
+ "RunStartedEvent",
46
+ "RunCompletedEvent",
47
+ "Haseef",
48
+ "CreateHaseefInput",
49
+ "UpdateHaseefInput",
50
+ "SemanticMemory",
51
+ "SemanticMemoryInput",
52
+ "EpisodicMemory",
53
+ "SocialMemory",
54
+ "ProceduralMemory",
55
+ "MemoryStats",
56
+ "Run",
57
+ "ListRunsOptions",
58
+ "input_to_json_schema",
59
+ ]
@@ -0,0 +1,54 @@
1
+ import json
2
+ from typing import Dict, Any
3
+
4
+
5
+ def input_to_json_schema(input_types: Dict[str, str]) -> Dict[str, Any]:
6
+ properties: Dict[str, Any] = {}
7
+ required: list[str] = []
8
+
9
+ for key, type_str in input_types.items():
10
+ optional = type_str.endswith('?')
11
+ base_type = type_str[:-1] if optional else type_str
12
+
13
+ if not optional:
14
+ required.append(key)
15
+
16
+ if base_type == 'string[]':
17
+ properties[key] = {'type': 'array', 'items': {'type': 'string'}}
18
+ elif base_type == 'number[]':
19
+ properties[key] = {'type': 'array', 'items': {'type': 'number'}}
20
+ elif base_type == 'boolean[]':
21
+ properties[key] = {'type': 'array', 'items': {'type': 'boolean'}}
22
+ elif base_type == 'object':
23
+ properties[key] = {'type': 'object', 'additionalProperties': True}
24
+ else:
25
+ properties[key] = {'type': base_type}
26
+
27
+ schema: Dict[str, Any] = {
28
+ 'type': 'object',
29
+ 'properties': properties,
30
+ 'additionalProperties': False,
31
+ }
32
+
33
+ if required:
34
+ schema['required'] = required
35
+
36
+ return schema
37
+
38
+
39
+ def parse_partial_json(accumulated: str) -> Dict[str, Any]:
40
+ try:
41
+ return json.loads(accumulated)
42
+ except json.JSONDecodeError:
43
+ attempts = [
44
+ accumulated + '}',
45
+ accumulated + '"}',
46
+ accumulated + '"}]',
47
+ accumulated + '"}]}',
48
+ ]
49
+ for attempt in attempts:
50
+ try:
51
+ return json.loads(attempt)
52
+ except json.JSONDecodeError:
53
+ continue
54
+ return {}
@@ -0,0 +1,323 @@
1
+ import asyncio
2
+ import inspect
3
+ import json
4
+ from typing import Dict, Any, List, Optional, Callable, Set
5
+ from urllib.parse import urlencode
6
+
7
+ from .types import (
8
+ SdkOptions,
9
+ ToolDefinition,
10
+ ToolHandler,
11
+ PushEventPayload,
12
+ ToolCallContext,
13
+ HaseefContext,
14
+ )
15
+ from .schema import input_to_json_schema, parse_partial_json
16
+
17
+ DEFAULT_RECONNECT_DELAY = 2.0
18
+ MAX_RECONNECT_DELAY = 30.0
19
+ DEFAULT_API_BASE = '/api/v7'
20
+
21
+
22
+ class HaseefAPI:
23
+ def __init__(self, sdk: 'HsafaSDK'):
24
+ self._sdk = sdk
25
+
26
+ async def list(self) -> List[Dict[str, Any]]:
27
+ res = await self._sdk._request("GET", f"{self._sdk.api_base}/haseefs")
28
+ return res.get("haseefs", [])
29
+
30
+ async def get(self, haseef_id: str) -> Dict[str, Any]:
31
+ res = await self._sdk._request("GET", f"{self._sdk.api_base}/haseefs/{haseef_id}")
32
+ return res.get("haseef", {})
33
+
34
+ async def create(self, input_data: Dict[str, Any]) -> Dict[str, Any]:
35
+ res = await self._sdk._request("POST", f"{self._sdk.api_base}/haseefs", body=input_data)
36
+ return res.get("haseef", {})
37
+
38
+ async def update(self, haseef_id: str, patch: Dict[str, Any]) -> Dict[str, Any]:
39
+ res = await self._sdk._request("PATCH", f"{self._sdk.api_base}/haseefs/{haseef_id}", body=patch)
40
+ return res.get("haseef", {})
41
+
42
+ async def delete(self, haseef_id: str) -> None:
43
+ await self._sdk._request("DELETE", f"{self._sdk.api_base}/haseefs/{haseef_id}")
44
+
45
+ async def get_profile(self, haseef_id: str) -> Dict[str, Any]:
46
+ res = await self._sdk._request("GET", f"{self._sdk.api_base}/haseefs/{haseef_id}/profile")
47
+ return res.get("profile", {})
48
+
49
+ async def update_profile(self, haseef_id: str, patch: Dict[str, Any]) -> Dict[str, Any]:
50
+ res = await self._sdk._request("PATCH", f"{self._sdk.api_base}/haseefs/{haseef_id}/profile", body=patch)
51
+ return res.get("profile", {})
52
+
53
+ async def add_skill(self, haseef_id: str, skill_name: str) -> Dict[str, Any]:
54
+ haseef = await self.get(haseef_id)
55
+ current = haseef.get("skills") or []
56
+ if skill_name in current:
57
+ return haseef
58
+ return await self.update(haseef_id, {"skills": [*current, skill_name]})
59
+
60
+ async def remove_skill(self, haseef_id: str, skill_name: str) -> Dict[str, Any]:
61
+ haseef = await self.get(haseef_id)
62
+ current = haseef.get("skills") or []
63
+ if skill_name not in current:
64
+ return haseef
65
+ return await self.update(haseef_id, {"skills": [s for s in current if s != skill_name]})
66
+
67
+ async def status(self, haseef_id: str) -> Dict[str, Any]:
68
+ return await self._sdk._request("GET", f"{self._sdk.api_base}/haseefs/{haseef_id}/status")
69
+
70
+
71
+ class MemoryAPI:
72
+ def __init__(self, sdk: 'HsafaSDK'):
73
+ self._sdk = sdk
74
+
75
+ async def list(self, haseef_id: str) -> List[Dict[str, Any]]:
76
+ res = await self._sdk._request("GET", f"{self._sdk.api_base}/memory/{haseef_id}/semantic")
77
+ return res.get("memories", [])
78
+
79
+ async def search(self, haseef_id: str, query: str, limit: int = 20) -> List[Dict[str, Any]]:
80
+ qs = urlencode({"q": query, "limit": limit})
81
+ res = await self._sdk._request("GET", f"{self._sdk.api_base}/memory/{haseef_id}/semantic/search?{qs}")
82
+ return res.get("results", [])
83
+
84
+ async def set(self, haseef_id: str, memories: List[Dict[str, Any]]) -> Dict[str, Any]:
85
+ return await self._sdk._request("POST", f"{self._sdk.api_base}/memory/{haseef_id}/semantic", body={"memories": memories})
86
+
87
+ async def delete(self, haseef_id: str, keys: List[str]) -> Dict[str, Any]:
88
+ return await self._sdk._request("DELETE", f"{self._sdk.api_base}/memory/{haseef_id}/semantic", body={"keys": keys})
89
+
90
+ async def episodes(self, haseef_id: str, limit: int = 20) -> List[Dict[str, Any]]:
91
+ res = await self._sdk._request("GET", f"{self._sdk.api_base}/memory/{haseef_id}/episodic?limit={limit}")
92
+ return res.get("episodes", [])
93
+
94
+ async def search_episodes(self, haseef_id: str, query: str, limit: int = 10) -> List[Dict[str, Any]]:
95
+ qs = urlencode({"q": query, "limit": limit})
96
+ res = await self._sdk._request("GET", f"{self._sdk.api_base}/memory/{haseef_id}/episodic/search?{qs}")
97
+ return res.get("results", [])
98
+
99
+ async def social(self, haseef_id: str) -> List[Dict[str, Any]]:
100
+ res = await self._sdk._request("GET", f"{self._sdk.api_base}/memory/{haseef_id}/social")
101
+ return res.get("people", [])
102
+
103
+ async def procedural(self, haseef_id: str) -> List[Dict[str, Any]]:
104
+ res = await self._sdk._request("GET", f"{self._sdk.api_base}/memory/{haseef_id}/procedural")
105
+ return res.get("patterns", [])
106
+
107
+ async def stats(self, haseef_id: str) -> Dict[str, Any]:
108
+ return await self._sdk._request("GET", f"{self._sdk.api_base}/memory/{haseef_id}/stats")
109
+
110
+
111
+ class RunsAPI:
112
+ def __init__(self, sdk: 'HsafaSDK'):
113
+ self._sdk = sdk
114
+
115
+ async def list(
116
+ self,
117
+ haseef_id: Optional[str] = None,
118
+ status: Optional[str] = None,
119
+ limit: Optional[int] = None,
120
+ ) -> List[Dict[str, Any]]:
121
+ params: Dict[str, Any] = {}
122
+ if haseef_id:
123
+ params["haseefId"] = haseef_id
124
+ if status:
125
+ params["status"] = status
126
+ if limit is not None:
127
+ params["limit"] = limit
128
+ qs = urlencode(params)
129
+ path = f"{self._sdk.api_base}/runs" + (f"?{qs}" if qs else "")
130
+ res = await self._sdk._request("GET", path)
131
+ return res.get("runs", [])
132
+
133
+ async def get(self, run_id: str) -> Dict[str, Any]:
134
+ res = await self._sdk._request("GET", f"{self._sdk.api_base}/runs/{run_id}")
135
+ return res.get("run", {})
136
+
137
+
138
+ class HsafaSDK:
139
+ def __init__(self, opts: SdkOptions):
140
+ self.core_url = opts.get("core_url", "http://localhost:3001").rstrip("/")
141
+ self.api_key = opts.get("api_key", "")
142
+ self.skill = opts.get("skill", "")
143
+ self.api_base = (opts.get("api_base") or DEFAULT_API_BASE).rstrip("/")
144
+
145
+ self._tool_handlers: Dict[str, ToolHandler] = {}
146
+ self._event_listeners: Dict[str, Set[Callable[[Any], Any]]] = {}
147
+ self._is_connected = False
148
+ self._client = httpx.AsyncClient()
149
+
150
+ self.haseef = HaseefAPI(self)
151
+ self.memory = MemoryAPI(self)
152
+ self.runs = RunsAPI(self)
153
+
154
+ async def register_tools(self, tools: List[ToolDefinition]) -> None:
155
+ body = []
156
+ for t in tools:
157
+ schema = t.get("inputSchema") or input_to_json_schema(t.get("input") or {})
158
+ body.append({
159
+ "name": t["name"],
160
+ "description": t["description"],
161
+ "inputSchema": schema,
162
+ })
163
+
164
+ path = f"{self.api_base}/skills/{self.skill}/tools"
165
+ await self._request("PUT", path, body={"tools": body})
166
+
167
+ def on_tool_call(self, name: str, handler: ToolHandler) -> None:
168
+ self._tool_handlers[name] = handler
169
+
170
+ async def push_event(self, event: PushEventPayload) -> None:
171
+ payload = {"skill": self.skill, **event}
172
+ await self._request("POST", f"{self.api_base}/events", body=payload)
173
+
174
+ def on(self, event_name: str, listener: Callable[[Any], Any]) -> None:
175
+ if event_name not in self._event_listeners:
176
+ self._event_listeners[event_name] = set()
177
+ self._event_listeners[event_name].add(listener)
178
+
179
+ def off(self, event_name: str, listener: Callable[[Any], Any]) -> None:
180
+ if event_name in self._event_listeners:
181
+ self._event_listeners[event_name].discard(listener)
182
+
183
+ async def _emit(self, event_name: str, data: Any) -> None:
184
+ listeners = self._event_listeners.get(event_name, set())
185
+ for listener in listeners:
186
+ try:
187
+ if inspect.iscoroutinefunction(listener):
188
+ await listener(data)
189
+ else:
190
+ listener(data)
191
+ except Exception as e:
192
+ print(f"[HsafaSDK] Listener error on {event_name}: {e}")
193
+
194
+ async def connect(self) -> None:
195
+ """
196
+ Connects to the SSE stream and blocks until disconnect() is called.
197
+ Wrap in asyncio.create_task() to run in background alongside other code:
198
+ asyncio.create_task(sdk.connect())
199
+ """
200
+ if self._is_connected:
201
+ return
202
+ self._is_connected = True
203
+
204
+ delay = DEFAULT_RECONNECT_DELAY
205
+ while self._is_connected:
206
+ try:
207
+ await self._open_sse()
208
+ delay = DEFAULT_RECONNECT_DELAY
209
+ except asyncio.CancelledError:
210
+ break
211
+ except Exception as e:
212
+ if not self._is_connected:
213
+ break
214
+ print(f"[HsafaSDK] SSE Connection lost ({e}). Reconnecting in {delay}s...")
215
+ await asyncio.sleep(delay)
216
+ delay = min(delay * 2, MAX_RECONNECT_DELAY)
217
+
218
+ async def disconnect(self) -> None:
219
+ self._is_connected = False
220
+ await self._client.aclose()
221
+
222
+ async def _open_sse(self) -> None:
223
+ url = f"{self.core_url}{self.api_base}/skills/{self.skill}/actions/stream"
224
+ headers = {
225
+ "x-api-key": self.api_key,
226
+ "Accept": "text/event-stream",
227
+ }
228
+
229
+ async with self._client.stream("GET", url, headers=headers, timeout=None) as response:
230
+ response.raise_for_status()
231
+
232
+ data_line = ""
233
+ async for line in response.aiter_lines():
234
+ if line.startswith("data: "):
235
+ data_line = line[6:].strip()
236
+ elif line == "" and data_line:
237
+ try:
238
+ msg = json.loads(data_line)
239
+ asyncio.create_task(self._handle_message(msg))
240
+ except json.JSONDecodeError:
241
+ pass
242
+ data_line = ""
243
+
244
+ async def _handle_message(self, msg: Dict[str, Any]) -> None:
245
+ msg_type = msg.get("type")
246
+
247
+ lifecycle_events = [
248
+ "tool.input.start",
249
+ "tool.input.delta",
250
+ "tool.call",
251
+ "tool.result",
252
+ "tool.error",
253
+ "run.started",
254
+ "run.completed",
255
+ ]
256
+
257
+ if msg_type in lifecycle_events:
258
+ await self._emit(msg_type, msg.get("data"))
259
+ return
260
+
261
+ if msg_type == "action":
262
+ action_id = msg.get("actionId")
263
+ tool_name = msg.get("toolName")
264
+ args = msg.get("args", {})
265
+ haseef = msg.get("haseef", {})
266
+
267
+ handler = self._tool_handlers.get(tool_name)
268
+ if not handler:
269
+ await self._post_result(action_id, {"error": f'No handler registered for tool "{tool_name}"'})
270
+ return
271
+
272
+ try:
273
+ ctx: ToolCallContext = {"actionId": action_id, "haseef": haseef}
274
+ result = await handler(args, ctx)
275
+ await self._post_result(action_id, result)
276
+ except Exception as e:
277
+ await self._post_result(action_id, {"error": str(e)})
278
+
279
+ if msg_type == "tool.input.delta.raw":
280
+ data = msg.get("data", {})
281
+ partial_text = data.get("accumulatedText", "")
282
+ await self._emit("tool.input.delta", {
283
+ "actionId": data.get("actionId"),
284
+ "toolName": data.get("toolName"),
285
+ "delta": partial_text,
286
+ "partialArgs": parse_partial_json(partial_text),
287
+ "haseef": data.get("haseef"),
288
+ })
289
+
290
+ async def _post_result(self, action_id: str, result: Any) -> None:
291
+ try:
292
+ path = f"{self.api_base}/actions/{action_id}/result"
293
+ await self._request("POST", path, body={"result": result})
294
+ except Exception as e:
295
+ print(f"[HsafaSDK:{self.skill}] Failed to submit result for action {action_id}: {e}")
296
+
297
+ async def _request(
298
+ self,
299
+ method: str,
300
+ path: str,
301
+ body: Optional[Dict[str, Any]] = None,
302
+ ) -> Any:
303
+ url = f"{self.core_url}{path}"
304
+ headers = {
305
+ "x-api-key": self.api_key,
306
+ "Content-Type": "application/json",
307
+ }
308
+
309
+ response = await self._client.request(method, url, headers=headers, json=body)
310
+
311
+ if not response.is_success:
312
+ raise Exception(f"{method} {path} failed ({response.status_code}): {response.text}")
313
+
314
+ if response.status_code == 204 or not response.content:
315
+ return None
316
+
317
+ if "application/json" in response.headers.get("content-type", ""):
318
+ return response.json()
319
+
320
+ return None
321
+
322
+
323
+ import httpx
@@ -0,0 +1,206 @@
1
+ from typing import TypedDict, Dict, Any, List, Optional, Callable, Awaitable, Union
2
+
3
+
4
+ class SdkOptions(TypedDict, total=False):
5
+ core_url: str
6
+ api_key: str
7
+ skill: str
8
+ api_base: Optional[str]
9
+
10
+
11
+ class ToolDefinition(TypedDict, total=False):
12
+ name: str
13
+ description: str
14
+ input: Optional[Dict[str, str]]
15
+ inputSchema: Optional[Any]
16
+
17
+
18
+ class HaseefContext(TypedDict):
19
+ id: str
20
+ name: str
21
+ profile: Dict[str, Any]
22
+
23
+
24
+ class ToolCallContext(TypedDict):
25
+ actionId: str
26
+ haseef: HaseefContext
27
+
28
+
29
+ ToolHandler = Callable[[Dict[str, Any], ToolCallContext], Awaitable[Any]]
30
+
31
+
32
+ class Attachment(TypedDict, total=False):
33
+ type: str # 'image' | 'audio' | 'file'
34
+ mimeType: str
35
+ url: Optional[str]
36
+ base64: Optional[str]
37
+ name: Optional[str]
38
+
39
+
40
+ class PushEventPayload(TypedDict, total=False):
41
+ type: str
42
+ data: Dict[str, Any]
43
+ attachments: Optional[List[Attachment]]
44
+ haseefId: Optional[str]
45
+ target: Optional[Dict[str, str]]
46
+
47
+
48
+ # ── Lifecycle Events ───────────────────────────────────────────────────────────
49
+
50
+ class ToolInputStartEvent(TypedDict):
51
+ actionId: str
52
+ toolName: str
53
+ haseef: HaseefContext
54
+
55
+
56
+ class ToolInputDeltaEvent(TypedDict):
57
+ actionId: str
58
+ toolName: str
59
+ delta: str
60
+ partialArgs: Dict[str, Any]
61
+ haseef: HaseefContext
62
+
63
+
64
+ class ToolCallEvent(TypedDict):
65
+ actionId: str
66
+ toolName: str
67
+ args: Dict[str, Any]
68
+ haseef: HaseefContext
69
+
70
+
71
+ class ToolResultEvent(TypedDict):
72
+ actionId: str
73
+ toolName: str
74
+ args: Dict[str, Any]
75
+ result: Any
76
+ durationMs: int
77
+ haseef: HaseefContext
78
+
79
+
80
+ class ToolErrorEvent(TypedDict):
81
+ actionId: str
82
+ toolName: str
83
+ error: str
84
+ haseef: HaseefContext
85
+
86
+
87
+ class RunStartedEvent(TypedDict):
88
+ runId: str
89
+ haseef: Dict[str, str]
90
+ triggerSkill: Optional[str]
91
+ triggerType: Optional[str]
92
+
93
+
94
+ class RunCompletedEvent(TypedDict):
95
+ runId: str
96
+ haseef: Dict[str, str]
97
+ summary: Optional[str]
98
+ durationMs: int
99
+
100
+
101
+ SdkEventType = Union[
102
+ str,
103
+ ]
104
+
105
+ SdkEventMap = Dict[str, Any]
106
+
107
+
108
+ # ── Haseef API ───────────────────────────────────────────────────────────────
109
+
110
+ class Haseef(TypedDict, total=False):
111
+ id: str
112
+ name: str
113
+ description: Optional[str]
114
+ profileJson: Optional[Dict[str, Any]]
115
+ configJson: Optional[Dict[str, Any]]
116
+ skills: Optional[List[str]]
117
+ createdAt: Optional[str]
118
+ updatedAt: Optional[str]
119
+
120
+
121
+ class CreateHaseefInput(TypedDict, total=False):
122
+ name: str
123
+ description: Optional[str]
124
+ configJson: Dict[str, Any]
125
+ profileJson: Optional[Dict[str, Any]]
126
+ skills: Optional[List[str]]
127
+
128
+
129
+ class UpdateHaseefInput(TypedDict, total=False):
130
+ name: Optional[str]
131
+ description: Optional[str]
132
+ configJson: Optional[Dict[str, Any]]
133
+ profileJson: Optional[Dict[str, Any]]
134
+ skills: Optional[List[str]]
135
+
136
+
137
+ # ── Memory API ───────────────────────────────────────────────────────────────
138
+
139
+ class SemanticMemoryInput(TypedDict, total=False):
140
+ key: str
141
+ value: str
142
+ importance: Optional[int]
143
+
144
+
145
+ class SemanticMemory(TypedDict):
146
+ id: str
147
+ haseefId: str
148
+ key: str
149
+ value: str
150
+ importance: int
151
+ recalledAt: Optional[str]
152
+ createdAt: str
153
+ updatedAt: str
154
+
155
+
156
+ class EpisodicMemory(TypedDict):
157
+ id: str
158
+ haseefId: str
159
+ runId: Optional[str]
160
+ summary: str
161
+ context: Optional[Dict[str, Any]]
162
+ createdAt: str
163
+
164
+
165
+ class SocialMemory(TypedDict):
166
+ id: str
167
+ haseefId: str
168
+ personKey: str
169
+ observations: Any
170
+ updatedAt: str
171
+
172
+
173
+ class ProceduralMemory(TypedDict):
174
+ id: str
175
+ haseefId: str
176
+ pattern: str
177
+ confidence: float
178
+ updatedAt: str
179
+
180
+
181
+ class MemoryStats(TypedDict):
182
+ haseefId: str
183
+ counts: Dict[str, int]
184
+ total: int
185
+
186
+
187
+ # ── Runs API ───────────────────────────────────────────────────────────────────
188
+
189
+ class Run(TypedDict, total=False):
190
+ id: str
191
+ haseefId: str
192
+ status: str
193
+ triggerSkill: Optional[str]
194
+ triggerType: Optional[str]
195
+ startedAt: str
196
+ completedAt: Optional[str]
197
+ durationMs: Optional[int]
198
+ summary: Optional[str]
199
+ tokensUsed: Optional[int]
200
+ toolCallCount: Optional[int]
201
+
202
+
203
+ class ListRunsOptions(TypedDict, total=False):
204
+ haseefId: Optional[str]
205
+ status: Optional[str]
206
+ limit: Optional[int]
@@ -0,0 +1,82 @@
1
+ Metadata-Version: 2.4
2
+ Name: hsafa-sdk
3
+ Version: 0.1.0
4
+ Summary: Hsafa SDK v7 — connect any service to a Haseef brain
5
+ Requires-Python: >=3.9
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: httpx>=0.27.0
8
+
9
+ # hsafa-sdk
10
+
11
+ Python SDK for **Hsafa Core v7**. Connect any service to a Haseef brain — register tools, handle tool calls over SSE, push events, and read/write the haseef's memory and profile.
12
+
13
+ ## Install
14
+
15
+ ```bash
16
+ pip install hsafa-sdk
17
+ ```
18
+
19
+ Or from source:
20
+
21
+ ```bash
22
+ cd sdks/python
23
+ pip install -e .
24
+ ```
25
+
26
+ ## Quick Start
27
+
28
+ ```python
29
+ import asyncio
30
+ import os
31
+ from hsafa_sdk import HsafaSDK, SdkOptions
32
+
33
+ async def main():
34
+ sdk = HsafaSDK(SdkOptions(
35
+ core_url="http://localhost:3001",
36
+ api_key=os.environ.get("HSAFA_CORE_KEY", "test-key"),
37
+ skill="weather",
38
+ ))
39
+
40
+ # 1. Register tools
41
+ await sdk.register_tools([{
42
+ "name": "get_weather",
43
+ "description": "Get current weather for a city",
44
+ "input": {"city": "string", "units": "string?"},
45
+ }])
46
+
47
+ # 2. Handle tool calls
48
+ async def handle_weather(args, ctx):
49
+ print(f"{ctx['haseef']['name']} wants weather for {args.get('city')}")
50
+ return {"temperature": 72, "conditions": "sunny", "city": args.get("city")}
51
+
52
+ sdk.on_tool_call("get_weather", handle_weather)
53
+
54
+ # 3. Listen to lifecycle events
55
+ def on_run_started(e):
56
+ print(f"Run started for {e['haseef']['name']}")
57
+
58
+ sdk.on("run.started", on_run_started)
59
+
60
+ # 4. Connect the SSE stream (blocks until disconnect)
61
+ # For background usage alongside a web server:
62
+ # asyncio.create_task(sdk.connect())
63
+ try:
64
+ await sdk.connect()
65
+ except KeyboardInterrupt:
66
+ await sdk.disconnect()
67
+
68
+ if __name__ == "__main__":
69
+ asyncio.run(main())
70
+ ```
71
+
72
+ ## Namespaces
73
+
74
+ | Namespace | Methods |
75
+ |-----------|---------|
76
+ | `sdk.haseef` | `list()`, `get(id)`, `create(data)`, `update(id, patch)`, `delete(id)`, `get_profile(id)`, `update_profile(id, patch)`, `add_skill(id, name)`, `remove_skill(id, name)`, `status(id)` |
77
+ | `sdk.memory` | `list(id)`, `search(id, query, limit)`, `set(id, memories)`, `delete(id, keys)`, `episodes(id, limit)`, `search_episodes(id, query, limit)`, `social(id)`, `procedural(id)`, `stats(id)` |
78
+ | `sdk.runs` | `list(haseef_id?, status?, limit?)`, `get(run_id)` |
79
+
80
+ ## License
81
+
82
+ MIT
@@ -0,0 +1,11 @@
1
+ README.md
2
+ pyproject.toml
3
+ src/hsafa_sdk/__init__.py
4
+ src/hsafa_sdk/schema.py
5
+ src/hsafa_sdk/sdk.py
6
+ src/hsafa_sdk/types.py
7
+ src/hsafa_sdk.egg-info/PKG-INFO
8
+ src/hsafa_sdk.egg-info/SOURCES.txt
9
+ src/hsafa_sdk.egg-info/dependency_links.txt
10
+ src/hsafa_sdk.egg-info/requires.txt
11
+ src/hsafa_sdk.egg-info/top_level.txt
@@ -0,0 +1 @@
1
+ httpx>=0.27.0
@@ -0,0 +1 @@
1
+ hsafa_sdk