stacklink-sdk 0.2.0__py3-none-any.whl

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.
stacklink/mcp.py ADDED
@@ -0,0 +1,76 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from typing import Any
5
+
6
+ from .errors import StacklinkError
7
+
8
+
9
+ class McpResource:
10
+ def __init__(self, client):
11
+ self.client = client
12
+
13
+ def request(self, body: dict[str, Any] | list[dict[str, Any]]):
14
+ return self.client.request("POST", "/v1/mcp", json_body=body)
15
+
16
+ def session_request(self, session_id: str, body: dict[str, Any] | list[dict[str, Any]]):
17
+ return self.client.request("POST", f"/v1/sessions/{session_id}/mcp", json_body=body)
18
+
19
+ def list_tools(self):
20
+ return self.request({"jsonrpc": "2.0", "id": "tools-list", "method": "tools/list"})
21
+
22
+ def call_tool(self, name: str, arguments: dict[str, Any]):
23
+ return self.request({
24
+ "jsonrpc": "2.0",
25
+ "id": f"{name}-call",
26
+ "method": "tools/call",
27
+ "params": {"name": name, "arguments": arguments},
28
+ })
29
+
30
+ def call_session_tool(self, session_id: str, name: str, arguments: dict[str, Any]):
31
+ return self.session_request(session_id, {
32
+ "jsonrpc": "2.0",
33
+ "id": f"{name}-call",
34
+ "method": "tools/call",
35
+ "params": {"name": name, "arguments": arguments},
36
+ })
37
+
38
+ def call_session_tool_result(self, session_id: str, name: str, arguments: dict[str, Any]):
39
+ return unwrap_tool_result(self.call_session_tool(session_id, name, arguments))
40
+
41
+
42
+ def unwrap_tool_result(response: Any):
43
+ if not isinstance(response, dict):
44
+ return response
45
+ error = response.get("error")
46
+ if isinstance(error, dict):
47
+ raise StacklinkError(
48
+ str(error.get("message") or "Stacklink MCP tool call failed."),
49
+ status_code=400,
50
+ code=str(error.get("code")) if error.get("code") is not None else None,
51
+ details=error.get("data"),
52
+ )
53
+ result = response.get("result")
54
+ if not isinstance(result, dict):
55
+ return result
56
+ if result.get("isError") is True:
57
+ text = "\n".join(
58
+ str(item.get("text"))
59
+ for item in result.get("content", [])
60
+ if isinstance(item, dict) and item.get("type") == "text" and isinstance(item.get("text"), str)
61
+ )
62
+ raise StacklinkError(text or "Stacklink MCP tool call failed.", status_code=400, code="mcp_tool_error", details=result)
63
+ if "structuredContent" in result:
64
+ return result["structuredContent"]
65
+ content = result.get("content")
66
+ text_items = [
67
+ item["text"]
68
+ for item in content or []
69
+ if isinstance(item, dict) and item.get("type") == "text" and isinstance(item.get("text"), str)
70
+ ]
71
+ if len(text_items) != 1:
72
+ return result
73
+ try:
74
+ return json.loads(text_items[0])
75
+ except json.JSONDecodeError:
76
+ return text_items[0]
stacklink/memory.py ADDED
@@ -0,0 +1,235 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+ from urllib.parse import quote
5
+
6
+
7
+ class MemoryResource:
8
+ def __init__(self, client):
9
+ self.client = client
10
+
11
+ def list_policies(self):
12
+ return self.client.request("GET", "/v1/memory/policies")
13
+
14
+ def create_policy(self, body: dict[str, Any]):
15
+ return self.client.request("POST", "/v1/memory/policies", json_body=body)
16
+
17
+ def get_policy(self, policy_key: str = "default"):
18
+ return self.client.request("GET", f"/v1/memory/policies/{quote(policy_key, safe='')}")
19
+
20
+ def policy_history(self, policy_key: str = "default"):
21
+ return self.client.request("GET", f"/v1/memory/policies/{quote(policy_key, safe='')}/history")
22
+
23
+ def preview_policy(self, body: dict[str, Any], policy_key: str = "default"):
24
+ return self.client.request(
25
+ "POST",
26
+ f"/v1/memory/policies/{quote(policy_key, safe='')}/preview",
27
+ json_body=body,
28
+ )
29
+
30
+ def update_policy(self, policy_key: str, body: dict[str, Any]):
31
+ return self.client.request(
32
+ "PATCH",
33
+ f"/v1/memory/policies/{quote(policy_key, safe='')}",
34
+ json_body=body,
35
+ )
36
+
37
+ def delete_policy(self, policy_key: str):
38
+ return self.client.request("DELETE", f"/v1/memory/policies/{quote(policy_key, safe='')}")
39
+
40
+ def ingest(self, body: dict[str, Any]):
41
+ return self.client.request("POST", "/v1/memory/ingest", json_body=body)
42
+
43
+ def ingest_rich(self, body: dict[str, Any]):
44
+ return self.client.request("POST", "/v1/memory/ingest-rich", json_body=body)
45
+
46
+ def get_rich_ingest(self, job_id: str):
47
+ return self.client.request("GET", f"/v1/memory/ingest-rich/{job_id}")
48
+
49
+ def record_conversation_turn(self, body: dict[str, Any]):
50
+ return self.client.request(
51
+ "POST",
52
+ "/v1/memory/events/conversation-turn-completed",
53
+ json_body=body,
54
+ )
55
+
56
+ def add(self, body: dict[str, Any]):
57
+ return self.ingest({**body, "mode": "exact"})
58
+
59
+ def search(self, body: dict[str, Any]):
60
+ return self.client.request("POST", "/v1/memory/search", json_body=body)
61
+
62
+ def context(self, body: dict[str, Any]):
63
+ return self.client.request("POST", "/v1/memory/context", json_body=body)
64
+
65
+ def list_records(self, **query):
66
+ return self.client.request("GET", "/v1/memory/records", query=query)
67
+
68
+ def list_forgotten_records(self, **query):
69
+ return self.client.request("GET", "/v1/memory/admin/forgotten-records", query=query)
70
+
71
+ def get(self, memory_id: str, **query):
72
+ return self.client.request("GET", f"/v1/memory/records/{memory_id}", query=query)
73
+
74
+ def history(self, memory_id: str, **query):
75
+ return self.client.request("GET", f"/v1/memory/records/{memory_id}/history", query=query)
76
+
77
+ def update(self, memory_id: str, body: dict[str, Any]):
78
+ return self.client.request("PATCH", f"/v1/memory/records/{memory_id}", json_body=body)
79
+
80
+ def forget(self, memory_id: str, body: dict[str, Any]):
81
+ return self.client.request("POST", f"/v1/memory/records/{memory_id}/forget", json_body=body)
82
+
83
+ def restore(self, memory_id: str, body: dict[str, Any]):
84
+ return self.client.request("POST", f"/v1/memory/records/{memory_id}/restore", json_body=body)
85
+
86
+ def erase(self, memory_id: str, body: dict[str, Any]):
87
+ return self.client.request("DELETE", f"/v1/memory/records/{memory_id}", json_body=body)
88
+
89
+ def preview_erasure(self, memory_id: str, body: dict[str, Any]):
90
+ return self.client.request(
91
+ "POST",
92
+ f"/v1/memory/records/{memory_id}/erasure-preview",
93
+ json_body=body,
94
+ )
95
+
96
+ def get_erasure(self, erasure_job_id: str):
97
+ return self.client.request("GET", f"/v1/memory/erasures/{erasure_job_id}")
98
+
99
+ def profile(self, **query):
100
+ return self.client.request("GET", "/v1/memory/profile", query=query)
101
+
102
+ def subject_profile(self, subject_id: str, **query):
103
+ return self.client.request("GET", f"/v1/memory/subjects/{subject_id}/profile", query=query)
104
+
105
+ def list_subjects(self, **query):
106
+ return self.client.request("GET", "/v1/memory/subjects", query=query)
107
+
108
+ def erase_subject(self, subject_id: str, body: dict[str, Any]):
109
+ return self.client.request("POST", f"/v1/memory/subjects/{subject_id}/erase", json_body=body)
110
+
111
+ def create_block(self, body: dict[str, Any]):
112
+ return self.client.request("POST", "/v1/memory/blocks", json_body=body)
113
+
114
+ def list_blocks(self, **query):
115
+ return self.client.request("GET", "/v1/memory/blocks", query=query)
116
+
117
+ def update_block(self, block_id: str, body: dict[str, Any]):
118
+ return self.client.request("PATCH", f"/v1/memory/blocks/{block_id}", json_body=body)
119
+
120
+ def revoke_block(self, block_id: str, body: dict[str, Any]):
121
+ return self.client.request("DELETE", f"/v1/memory/blocks/{block_id}", json_body=body)
122
+
123
+ def query_graph(self, body: dict[str, Any]):
124
+ return self.client.request("POST", "/v1/memory/graph/query", json_body=body)
125
+
126
+ def create_source(self, body: dict[str, Any]):
127
+ return self.client.request("POST", "/v1/memory/sources", json_body=body)
128
+
129
+ def preview_connected_source(self, body: dict[str, Any]):
130
+ return self.client.request(
131
+ "POST",
132
+ "/v1/memory/sources/connected/preview",
133
+ json_body=body,
134
+ )
135
+
136
+ def create_connected_source(self, body: dict[str, Any]):
137
+ return self.client.request(
138
+ "POST",
139
+ "/v1/memory/sources/connected",
140
+ json_body=body,
141
+ )
142
+
143
+ def list_sources(self, **query):
144
+ return self.client.request("GET", "/v1/memory/sources", query=query)
145
+
146
+ def get_source(self, binding_id: str):
147
+ return self.client.request("GET", f"/v1/memory/sources/{binding_id}")
148
+
149
+ def set_source_status(self, binding_id: str, status: str):
150
+ return self.update_source(binding_id, {"status": status})
151
+
152
+ def update_source(self, binding_id: str, body: dict[str, Any]):
153
+ return self.client.request(
154
+ "PATCH",
155
+ f"/v1/memory/sources/{binding_id}",
156
+ json_body=body,
157
+ )
158
+
159
+ def delete_source(self, binding_id: str):
160
+ return self.client.request("DELETE", f"/v1/memory/sources/{binding_id}")
161
+
162
+ def replay_source_item(self, binding_id: str, item_id: str):
163
+ return self.client.request(
164
+ "POST",
165
+ f"/v1/memory/sources/{binding_id}/items/{item_id}/replay",
166
+ )
167
+
168
+ def list_source_deliveries(self, **query):
169
+ return self.client.request("GET", "/v1/memory/source-deliveries", query=query)
170
+
171
+ def replay_source_delivery(self, delivery_id: str):
172
+ return self.client.request("POST", f"/v1/memory/source-deliveries/{delivery_id}/replay")
173
+
174
+ def export_memory(self, body: dict[str, Any]):
175
+ return self.client.request("POST", "/v1/memory/exports", json_body=body)
176
+
177
+ def get_export(self, export_id: str):
178
+ return self.client.request("GET", f"/v1/memory/exports/{export_id}")
179
+
180
+ def data_management(self, **query):
181
+ return self.client.request("GET", "/v1/memory/data-management", query=query)
182
+
183
+ def create_legal_hold(self, body: dict[str, Any]):
184
+ return self.client.request("POST", "/v1/memory/legal-holds", json_body=body)
185
+
186
+ def release_legal_hold(self, hold_id: str):
187
+ return self.client.request("POST", f"/v1/memory/legal-holds/{hold_id}/release")
188
+
189
+
190
+ class SessionMemoryResource:
191
+ def __init__(self, memory: MemoryResource, session_id: str):
192
+ self.memory = memory
193
+ self.session_id = session_id
194
+
195
+ def add(self, body: dict[str, Any]):
196
+ return self.memory.add({**body, "sessionId": self.session_id})
197
+
198
+ def ingest(self, body: dict[str, Any]):
199
+ return self.memory.ingest({**body, "sessionId": self.session_id})
200
+
201
+ def ingest_rich(self, body: dict[str, Any]):
202
+ return self.memory.ingest_rich({**body, "sessionId": self.session_id})
203
+
204
+ def get_rich_ingest(self, job_id: str):
205
+ return self.memory.get_rich_ingest(job_id)
206
+
207
+ def record_conversation_turn(self, body: dict[str, Any]):
208
+ return self.memory.record_conversation_turn({**body, "sessionId": self.session_id})
209
+
210
+ def search(self, body: dict[str, Any]):
211
+ return self.memory.search({**body, "sessionId": self.session_id})
212
+
213
+ def context(self, body: dict[str, Any]):
214
+ return self.memory.context({**body, "sessionId": self.session_id})
215
+
216
+ def recall(self, body: dict[str, Any]):
217
+ return self.context(body)
218
+
219
+ def profile(self):
220
+ return self.memory.profile(sessionId=self.session_id)
221
+
222
+ def get(self, memory_id: str):
223
+ return self.memory.get(memory_id, sessionId=self.session_id)
224
+
225
+ def history(self, memory_id: str):
226
+ return self.memory.history(memory_id, sessionId=self.session_id)
227
+
228
+ def update(self, memory_id: str, patch: dict[str, Any]):
229
+ return self.memory.update(memory_id, {"sessionId": self.session_id, "patch": patch})
230
+
231
+ def forget(self, memory_id: str, reason: str | None = None):
232
+ body: dict[str, Any] = {"sessionId": self.session_id}
233
+ if reason is not None:
234
+ body["reason"] = reason
235
+ return self.memory.forget(memory_id, body)
@@ -0,0 +1,40 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from typing import TYPE_CHECKING, Any
5
+
6
+ if TYPE_CHECKING:
7
+ from agents import FunctionTool
8
+
9
+ from .sessions import Session, SessionTool
10
+
11
+
12
+ def create_stacklink_openai_agents_tools(session: "Session") -> list["FunctionTool"]:
13
+ try:
14
+ from agents import FunctionTool
15
+ except ImportError as error:
16
+ raise ImportError(
17
+ "OpenAI Agents SDK support requires the 'openai-agents' extra: "
18
+ "pip install 'stacklink-sdk[openai-agents]'"
19
+ ) from error
20
+
21
+ return [
22
+ FunctionTool(
23
+ name=tool.name,
24
+ description=tool.description,
25
+ params_json_schema=tool.parameters,
26
+ on_invoke_tool=_tool_invoker(tool),
27
+ strict_json_schema=False,
28
+ )
29
+ for tool in session.tools().values()
30
+ ]
31
+
32
+
33
+ def _tool_invoker(tool: "SessionTool"):
34
+ async def invoke(_context: Any, arguments_json: str) -> Any:
35
+ arguments = json.loads(arguments_json) if arguments_json else {}
36
+ if not isinstance(arguments, dict):
37
+ raise ValueError("Stacklink tool input must be a JSON object.")
38
+ return tool.call(arguments)
39
+
40
+ return invoke