webagents 0.1.12__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.
- webagents/__init__.py +18 -0
- webagents/agents/__init__.py +13 -0
- webagents/agents/core/__init__.py +19 -0
- webagents/agents/core/base_agent.py +1834 -0
- webagents/agents/core/handoffs.py +293 -0
- webagents/agents/handoffs/__init__.py +0 -0
- webagents/agents/interfaces/__init__.py +0 -0
- webagents/agents/lifecycle/__init__.py +0 -0
- webagents/agents/skills/__init__.py +109 -0
- webagents/agents/skills/base.py +136 -0
- webagents/agents/skills/core/__init__.py +8 -0
- webagents/agents/skills/core/guardrails/__init__.py +0 -0
- webagents/agents/skills/core/llm/__init__.py +0 -0
- webagents/agents/skills/core/llm/anthropic/__init__.py +1 -0
- webagents/agents/skills/core/llm/litellm/__init__.py +10 -0
- webagents/agents/skills/core/llm/litellm/skill.py +538 -0
- webagents/agents/skills/core/llm/openai/__init__.py +1 -0
- webagents/agents/skills/core/llm/xai/__init__.py +1 -0
- webagents/agents/skills/core/mcp/README.md +375 -0
- webagents/agents/skills/core/mcp/__init__.py +15 -0
- webagents/agents/skills/core/mcp/skill.py +731 -0
- webagents/agents/skills/core/memory/__init__.py +11 -0
- webagents/agents/skills/core/memory/long_term_memory/__init__.py +10 -0
- webagents/agents/skills/core/memory/long_term_memory/memory_skill.py +639 -0
- webagents/agents/skills/core/memory/short_term_memory/__init__.py +9 -0
- webagents/agents/skills/core/memory/short_term_memory/skill.py +341 -0
- webagents/agents/skills/core/memory/vector_memory/skill.py +447 -0
- webagents/agents/skills/core/planning/__init__.py +9 -0
- webagents/agents/skills/core/planning/planner.py +343 -0
- webagents/agents/skills/ecosystem/__init__.py +0 -0
- webagents/agents/skills/ecosystem/crewai/__init__.py +1 -0
- webagents/agents/skills/ecosystem/database/__init__.py +1 -0
- webagents/agents/skills/ecosystem/filesystem/__init__.py +0 -0
- webagents/agents/skills/ecosystem/google/__init__.py +0 -0
- webagents/agents/skills/ecosystem/google/calendar/__init__.py +6 -0
- webagents/agents/skills/ecosystem/google/calendar/skill.py +306 -0
- webagents/agents/skills/ecosystem/n8n/__init__.py +0 -0
- webagents/agents/skills/ecosystem/openai_agents/__init__.py +0 -0
- webagents/agents/skills/ecosystem/web/__init__.py +0 -0
- webagents/agents/skills/ecosystem/zapier/__init__.py +0 -0
- webagents/agents/skills/robutler/__init__.py +11 -0
- webagents/agents/skills/robutler/auth/README.md +63 -0
- webagents/agents/skills/robutler/auth/__init__.py +17 -0
- webagents/agents/skills/robutler/auth/skill.py +354 -0
- webagents/agents/skills/robutler/crm/__init__.py +18 -0
- webagents/agents/skills/robutler/crm/skill.py +368 -0
- webagents/agents/skills/robutler/discovery/README.md +281 -0
- webagents/agents/skills/robutler/discovery/__init__.py +16 -0
- webagents/agents/skills/robutler/discovery/skill.py +230 -0
- webagents/agents/skills/robutler/kv/__init__.py +6 -0
- webagents/agents/skills/robutler/kv/skill.py +80 -0
- webagents/agents/skills/robutler/message_history/__init__.py +9 -0
- webagents/agents/skills/robutler/message_history/skill.py +270 -0
- webagents/agents/skills/robutler/messages/__init__.py +0 -0
- webagents/agents/skills/robutler/nli/__init__.py +13 -0
- webagents/agents/skills/robutler/nli/skill.py +687 -0
- webagents/agents/skills/robutler/notifications/__init__.py +5 -0
- webagents/agents/skills/robutler/notifications/skill.py +141 -0
- webagents/agents/skills/robutler/payments/__init__.py +41 -0
- webagents/agents/skills/robutler/payments/exceptions.py +255 -0
- webagents/agents/skills/robutler/payments/skill.py +610 -0
- webagents/agents/skills/robutler/storage/__init__.py +10 -0
- webagents/agents/skills/robutler/storage/files/__init__.py +9 -0
- webagents/agents/skills/robutler/storage/files/skill.py +445 -0
- webagents/agents/skills/robutler/storage/json/__init__.py +9 -0
- webagents/agents/skills/robutler/storage/json/skill.py +336 -0
- webagents/agents/skills/robutler/storage/kv/skill.py +88 -0
- webagents/agents/skills/robutler/storage.py +389 -0
- webagents/agents/tools/__init__.py +0 -0
- webagents/agents/tools/decorators.py +426 -0
- webagents/agents/tracing/__init__.py +0 -0
- webagents/agents/workflows/__init__.py +0 -0
- webagents/api/__init__.py +17 -0
- webagents/api/client.py +1207 -0
- webagents/api/types.py +253 -0
- webagents/scripts/__init__.py +0 -0
- webagents/server/__init__.py +28 -0
- webagents/server/context/__init__.py +0 -0
- webagents/server/context/context_vars.py +121 -0
- webagents/server/core/__init__.py +0 -0
- webagents/server/core/app.py +843 -0
- webagents/server/core/middleware.py +69 -0
- webagents/server/core/models.py +98 -0
- webagents/server/core/monitoring.py +59 -0
- webagents/server/endpoints/__init__.py +0 -0
- webagents/server/interfaces/__init__.py +0 -0
- webagents/server/middleware.py +330 -0
- webagents/server/models.py +92 -0
- webagents/server/monitoring.py +659 -0
- webagents/utils/__init__.py +0 -0
- webagents/utils/logging.py +359 -0
- webagents-0.1.12.dist-info/METADATA +99 -0
- webagents-0.1.12.dist-info/RECORD +96 -0
- webagents-0.1.12.dist-info/WHEEL +4 -0
- webagents-0.1.12.dist-info/entry_points.txt +2 -0
- webagents-0.1.12.dist-info/licenses/LICENSE +1 -0
@@ -0,0 +1,336 @@
|
|
1
|
+
"""
|
2
|
+
RobutlerJSONSkill - JSON Data Storage for Long-term Memory
|
3
|
+
|
4
|
+
Provides JSON data storage capabilities for agent long-term memory and persistent data.
|
5
|
+
"""
|
6
|
+
|
7
|
+
import json
|
8
|
+
import os
|
9
|
+
from typing import Dict, List, Any, Optional, Union
|
10
|
+
from datetime import datetime
|
11
|
+
|
12
|
+
from ....base import Skill
|
13
|
+
from webagents.agents.tools.decorators import tool
|
14
|
+
from webagents.api.client import RobutlerClient
|
15
|
+
|
16
|
+
|
17
|
+
class RobutlerJSONSkill(Skill):
|
18
|
+
"""
|
19
|
+
Robutler portal JSON storage skill for long-term memory using RobutlerClient.
|
20
|
+
|
21
|
+
Features:
|
22
|
+
- Store/retrieve JSON data for long-term memory
|
23
|
+
- Update and manage JSON data files
|
24
|
+
- Context-based scope access controls
|
25
|
+
- Agent identity determined by API key
|
26
|
+
|
27
|
+
Scope Configurations (from auth context):
|
28
|
+
- 'owner' scope: Full access to all JSON operations
|
29
|
+
- 'all' scope: No access (restricted)
|
30
|
+
|
31
|
+
Tool Scope Restrictions:
|
32
|
+
- store_json_data: owner scope only
|
33
|
+
- retrieve_json_data: owner scope only
|
34
|
+
- update_json_data: owner scope only
|
35
|
+
- delete_json_file: owner scope only
|
36
|
+
"""
|
37
|
+
|
38
|
+
def __init__(self, config: Optional[Dict[str, Any]] = None):
|
39
|
+
super().__init__(config)
|
40
|
+
self.portal_url = config.get('portal_url', 'http://localhost:3000') if config else 'http://localhost:3000'
|
41
|
+
self.api_key = config.get('api_key', os.getenv('ROBUTLER_API_KEY', 'rok_testapikey')) if config else os.getenv('ROBUTLER_API_KEY', 'rok_testapikey')
|
42
|
+
|
43
|
+
# Initialize RobutlerClient
|
44
|
+
self.client = RobutlerClient(
|
45
|
+
api_key=self.api_key,
|
46
|
+
base_url=self.portal_url
|
47
|
+
)
|
48
|
+
|
49
|
+
async def initialize(self, agent_reference):
|
50
|
+
"""Initialize with agent reference"""
|
51
|
+
await super().initialize(agent_reference)
|
52
|
+
self.agent = agent_reference
|
53
|
+
|
54
|
+
async def cleanup(self):
|
55
|
+
"""Cleanup method to close client session"""
|
56
|
+
if self.client:
|
57
|
+
await self.client.close()
|
58
|
+
|
59
|
+
def _get_scope_from_context(self) -> str:
|
60
|
+
"""
|
61
|
+
Get scope from authentication context.
|
62
|
+
|
63
|
+
Returns:
|
64
|
+
'owner' or 'all' based on auth context
|
65
|
+
"""
|
66
|
+
try:
|
67
|
+
from webagents.server.context.context_vars import get_context
|
68
|
+
context = get_context()
|
69
|
+
if context:
|
70
|
+
# Check auth scope from context
|
71
|
+
auth_scope = context.get('auth_scope') or context.get('scope')
|
72
|
+
if auth_scope == 'owner':
|
73
|
+
return 'owner'
|
74
|
+
return 'all' # Default to 'all' scope
|
75
|
+
except Exception:
|
76
|
+
return 'all' # Fallback to 'all' scope
|
77
|
+
|
78
|
+
def _get_storage_visibility(self) -> str:
|
79
|
+
"""
|
80
|
+
Get the appropriate visibility for storing content based on scope.
|
81
|
+
|
82
|
+
Returns:
|
83
|
+
- 'private' for owner scope (private agent storage)
|
84
|
+
- 'public' for all scope (public agent storage)
|
85
|
+
"""
|
86
|
+
scope = self._get_scope_from_context()
|
87
|
+
if scope == 'owner':
|
88
|
+
return 'private' # Store privately for owner
|
89
|
+
else: # 'all' scope
|
90
|
+
return 'public' # Store publicly for general access
|
91
|
+
|
92
|
+
def _get_access_visibility(self) -> str:
|
93
|
+
"""
|
94
|
+
Get the appropriate visibility for accessing content based on scope.
|
95
|
+
|
96
|
+
Returns:
|
97
|
+
- 'both' for owner scope (access to public and private content)
|
98
|
+
- 'public' for all scope (access to only public content)
|
99
|
+
"""
|
100
|
+
scope = self._get_scope_from_context()
|
101
|
+
if scope == 'owner':
|
102
|
+
return 'both' # Access to both public and private content
|
103
|
+
else: # 'all' scope
|
104
|
+
return 'public' # Access to only public content
|
105
|
+
|
106
|
+
@tool(scope="owner")
|
107
|
+
async def store_json_data(
|
108
|
+
self,
|
109
|
+
filename: str,
|
110
|
+
data: Dict[str, Any],
|
111
|
+
description: Optional[str] = None
|
112
|
+
) -> str:
|
113
|
+
"""
|
114
|
+
Store JSON data for long-term memory.
|
115
|
+
|
116
|
+
Args:
|
117
|
+
filename: Name of the file (will add .json if not present)
|
118
|
+
data: JSON-serializable data to store
|
119
|
+
description: Optional description of the file
|
120
|
+
|
121
|
+
Returns:
|
122
|
+
JSON string with storage result
|
123
|
+
"""
|
124
|
+
try:
|
125
|
+
# Ensure filename has .json extension
|
126
|
+
if not filename.endswith('.json'):
|
127
|
+
filename = f"{filename}.json"
|
128
|
+
|
129
|
+
# Convert data to JSON string and bytes
|
130
|
+
json_content = json.dumps(data, indent=2)
|
131
|
+
json_bytes = json_content.encode('utf-8')
|
132
|
+
|
133
|
+
# Upload using RobutlerClient
|
134
|
+
response = await self.client.upload_content(
|
135
|
+
filename=filename,
|
136
|
+
content_data=json_bytes,
|
137
|
+
content_type='application/json',
|
138
|
+
visibility=self._get_storage_visibility(),
|
139
|
+
description=description or "JSON data for long-term memory",
|
140
|
+
tags=['json_data', 'memory']
|
141
|
+
)
|
142
|
+
|
143
|
+
if response.success and response.data:
|
144
|
+
file_info = response.data.get('file', {})
|
145
|
+
return json.dumps({
|
146
|
+
"success": True,
|
147
|
+
"file_id": file_info.get('id'),
|
148
|
+
"filename": file_info.get('fileName'),
|
149
|
+
"url": file_info.get('url'),
|
150
|
+
"size": file_info.get('size')
|
151
|
+
}, indent=2)
|
152
|
+
else:
|
153
|
+
return json.dumps({
|
154
|
+
"success": False,
|
155
|
+
"error": f"Upload failed: {response.error or response.message}"
|
156
|
+
})
|
157
|
+
|
158
|
+
except Exception as e:
|
159
|
+
return json.dumps({
|
160
|
+
"success": False,
|
161
|
+
"error": f"Failed to store JSON data: {str(e)}"
|
162
|
+
})
|
163
|
+
|
164
|
+
@tool(scope="owner")
|
165
|
+
async def retrieve_json_data(self, filename: str) -> str:
|
166
|
+
"""
|
167
|
+
Retrieve JSON data from long-term memory.
|
168
|
+
|
169
|
+
Args:
|
170
|
+
filename: Name of the file to retrieve
|
171
|
+
|
172
|
+
Returns:
|
173
|
+
JSON string with file content or error
|
174
|
+
"""
|
175
|
+
try:
|
176
|
+
# Ensure filename has .json extension
|
177
|
+
if not filename.endswith('.json'):
|
178
|
+
filename = f"{filename}.json"
|
179
|
+
|
180
|
+
# Get content using RobutlerClient
|
181
|
+
response = await self.client.get_content(filename)
|
182
|
+
|
183
|
+
if response.success and response.data:
|
184
|
+
return json.dumps({
|
185
|
+
"success": True,
|
186
|
+
"filename": response.data.get('filename'),
|
187
|
+
"data": response.data.get('content'),
|
188
|
+
"metadata": response.data.get('metadata', {})
|
189
|
+
}, indent=2)
|
190
|
+
else:
|
191
|
+
# Get available files for better error message
|
192
|
+
list_response = await self.client.list_content(visibility=self._get_access_visibility())
|
193
|
+
available_files = []
|
194
|
+
if list_response.success and list_response.data:
|
195
|
+
files = list_response.data.get('files', [])
|
196
|
+
for file_info in files:
|
197
|
+
if 'json_data' in file_info.get('tags', []):
|
198
|
+
available_files.append(file_info.get('fileName', ''))
|
199
|
+
|
200
|
+
return json.dumps({
|
201
|
+
"success": False,
|
202
|
+
"error": response.error or response.message or "File not found",
|
203
|
+
"available_json_files": available_files
|
204
|
+
})
|
205
|
+
|
206
|
+
except Exception as e:
|
207
|
+
return json.dumps({
|
208
|
+
"success": False,
|
209
|
+
"error": f"Failed to retrieve JSON data: {str(e)}"
|
210
|
+
})
|
211
|
+
|
212
|
+
@tool(scope="owner")
|
213
|
+
async def update_json_data(
|
214
|
+
self,
|
215
|
+
filename: str,
|
216
|
+
data: Dict[str, Any],
|
217
|
+
description: Optional[str] = None
|
218
|
+
) -> str:
|
219
|
+
"""
|
220
|
+
Update existing JSON data in long-term memory.
|
221
|
+
|
222
|
+
Args:
|
223
|
+
filename: Name of the file to update
|
224
|
+
data: New JSON data
|
225
|
+
description: Optional new description
|
226
|
+
|
227
|
+
Returns:
|
228
|
+
JSON string with update result
|
229
|
+
"""
|
230
|
+
try:
|
231
|
+
# Ensure filename has .json extension
|
232
|
+
if not filename.endswith('.json'):
|
233
|
+
filename = f"{filename}.json"
|
234
|
+
|
235
|
+
# Convert data to JSON string and bytes
|
236
|
+
json_content = json.dumps(data, indent=2)
|
237
|
+
json_bytes = json_content.encode('utf-8')
|
238
|
+
|
239
|
+
# Update using RobutlerClient
|
240
|
+
response = await self.client.update_content(
|
241
|
+
filename=filename,
|
242
|
+
content_data=json_bytes,
|
243
|
+
content_type='application/json',
|
244
|
+
description=description or "Updated JSON data for long-term memory",
|
245
|
+
tags=['json_data', 'memory'],
|
246
|
+
visibility=self._get_storage_visibility()
|
247
|
+
)
|
248
|
+
|
249
|
+
if response.success and response.data:
|
250
|
+
file_info = response.data.get('file', {})
|
251
|
+
return json.dumps({
|
252
|
+
"success": True,
|
253
|
+
"file_id": file_info.get('id'),
|
254
|
+
"filename": file_info.get('fileName'),
|
255
|
+
"url": file_info.get('url'),
|
256
|
+
"size": file_info.get('size')
|
257
|
+
}, indent=2)
|
258
|
+
else:
|
259
|
+
return json.dumps({
|
260
|
+
"success": False,
|
261
|
+
"error": f"Update failed: {response.error or response.message}"
|
262
|
+
})
|
263
|
+
|
264
|
+
except Exception as e:
|
265
|
+
return json.dumps({
|
266
|
+
"success": False,
|
267
|
+
"error": f"Failed to update JSON data: {str(e)}"
|
268
|
+
})
|
269
|
+
|
270
|
+
@tool(scope="owner")
|
271
|
+
async def delete_json_file(self, filename: str) -> str:
|
272
|
+
"""
|
273
|
+
Delete a JSON file from long-term memory.
|
274
|
+
|
275
|
+
Args:
|
276
|
+
filename: Name of the file to delete
|
277
|
+
|
278
|
+
Returns:
|
279
|
+
JSON string with deletion result
|
280
|
+
"""
|
281
|
+
try:
|
282
|
+
# Ensure filename has .json extension
|
283
|
+
if not filename.endswith('.json'):
|
284
|
+
filename = f"{filename}.json"
|
285
|
+
|
286
|
+
# Delete using RobutlerClient
|
287
|
+
response = await self.client.delete_content(filename)
|
288
|
+
|
289
|
+
if response.success:
|
290
|
+
return json.dumps({
|
291
|
+
"success": True,
|
292
|
+
"message": f"JSON file '{filename}' deleted successfully"
|
293
|
+
})
|
294
|
+
else:
|
295
|
+
return json.dumps({
|
296
|
+
"success": False,
|
297
|
+
"error": response.error or response.message or f"Delete failed"
|
298
|
+
})
|
299
|
+
|
300
|
+
except Exception as e:
|
301
|
+
return json.dumps({
|
302
|
+
"success": False,
|
303
|
+
"error": f"Failed to delete JSON file: {str(e)}"
|
304
|
+
})
|
305
|
+
|
306
|
+
def get_skill_info(self) -> Dict[str, Any]:
|
307
|
+
"""Get comprehensive skill information"""
|
308
|
+
return {
|
309
|
+
"name": "RobutlerJSONSkill",
|
310
|
+
"description": "JSON data storage for long-term memory",
|
311
|
+
"version": "1.0.0",
|
312
|
+
"capabilities": [
|
313
|
+
"Store JSON data for long-term memory (owner scope only)",
|
314
|
+
"Retrieve JSON data from memory (owner scope only)",
|
315
|
+
"Update and manage JSON files (owner scope only)",
|
316
|
+
"Delete JSON files (owner scope only)",
|
317
|
+
"Agent identity determined by API key",
|
318
|
+
"Integration with portal authentication via RobutlerClient",
|
319
|
+
"Owner scope: Full access to JSON operations",
|
320
|
+
"All scope: No access (restricted)"
|
321
|
+
],
|
322
|
+
"tools": [
|
323
|
+
"store_json_data",
|
324
|
+
"retrieve_json_data",
|
325
|
+
"update_json_data",
|
326
|
+
"delete_json_file",
|
327
|
+
],
|
328
|
+
"config": {
|
329
|
+
"portal_url": self.portal_url,
|
330
|
+
"api_key_configured": bool(self.api_key),
|
331
|
+
"client_type": "RobutlerClient",
|
332
|
+
"scope": self._get_scope_from_context(),
|
333
|
+
"storage_visibility": self._get_storage_visibility(),
|
334
|
+
"access_visibility": self._get_access_visibility()
|
335
|
+
}
|
336
|
+
}
|
@@ -0,0 +1,88 @@
|
|
1
|
+
"""
|
2
|
+
RobutlerKVSkill - Simple key/value storage backed by the portal /api/kv endpoint.
|
3
|
+
|
4
|
+
Used for storing small secrets and tokens like OAuth credentials per user/agent.
|
5
|
+
"""
|
6
|
+
|
7
|
+
from typing import Any, Dict, Optional
|
8
|
+
import os
|
9
|
+
|
10
|
+
from ....base import Skill
|
11
|
+
from webagents.api.client import RobutlerClient
|
12
|
+
from webagents.agents.tools.decorators import tool
|
13
|
+
|
14
|
+
|
15
|
+
class RobutlerKVSkill(Skill):
|
16
|
+
"""KV storage skill using the Robutler portal API"""
|
17
|
+
|
18
|
+
def __init__(self, config: Optional[Dict[str, Any]] = None):
|
19
|
+
super().__init__(config)
|
20
|
+
self.portal_url = config.get('portal_url') if config else os.getenv('ROBUTLER_API_URL', 'http://localhost:3000')
|
21
|
+
self.api_key = config.get('api_key') if config else os.getenv('ROBUTLER_API_KEY')
|
22
|
+
self.client: Optional[RobutlerClient] = None
|
23
|
+
self.agent = None
|
24
|
+
|
25
|
+
async def initialize(self, agent):
|
26
|
+
await super().initialize(agent)
|
27
|
+
self.agent = agent
|
28
|
+
# Prefer agent.api_key over env
|
29
|
+
api_key = getattr(agent, 'api_key', None) or self.api_key
|
30
|
+
self.client = RobutlerClient(api_key=api_key, base_url=self.portal_url)
|
31
|
+
|
32
|
+
async def cleanup(self):
|
33
|
+
if self.client:
|
34
|
+
await self.client.close()
|
35
|
+
|
36
|
+
# Internal helpers
|
37
|
+
def _subject(self) -> Dict[str, Optional[str]]:
|
38
|
+
# Use new subject addressing with type/id
|
39
|
+
agent_id = getattr(self.agent, 'id', None)
|
40
|
+
if agent_id:
|
41
|
+
return {"subjectType": "agent", "subjectId": agent_id}
|
42
|
+
# Fallback legacy agentId if missing (shouldn't happen)
|
43
|
+
return {"agentId": agent_id}
|
44
|
+
|
45
|
+
# Programmatic API
|
46
|
+
async def kv_set(self, key: str, value: str, namespace: Optional[str] = None) -> bool:
|
47
|
+
if not self.client:
|
48
|
+
raise RuntimeError("KV client not initialized")
|
49
|
+
body = {"key": key, "value": value}
|
50
|
+
body.update(self._subject())
|
51
|
+
if namespace is not None:
|
52
|
+
body["namespace"] = namespace
|
53
|
+
res = await self.client._make_request('POST', '/kv', data=body)
|
54
|
+
return bool(res.success)
|
55
|
+
|
56
|
+
async def kv_get(self, key: str, namespace: Optional[str] = None) -> Optional[str]:
|
57
|
+
if not self.client:
|
58
|
+
raise RuntimeError("KV client not initialized")
|
59
|
+
params = {"key": key}
|
60
|
+
params.update(self._subject())
|
61
|
+
if namespace is not None:
|
62
|
+
params["namespace"] = namespace
|
63
|
+
res = await self.client._make_request('GET', '/kv', params=params)
|
64
|
+
if not res.success:
|
65
|
+
return None
|
66
|
+
data = res.data or {}
|
67
|
+
if data.get('found'):
|
68
|
+
return data.get('value')
|
69
|
+
return None
|
70
|
+
|
71
|
+
# Expose minimal tools for manual ops (owner scope)
|
72
|
+
@tool(
|
73
|
+
description="Store a small piece of data (like credentials or settings) by key. Use sparingly - not for large data or temporary storage.",
|
74
|
+
scope="owner"
|
75
|
+
)
|
76
|
+
async def kv_store(self, key: str, value: str, namespace: Optional[str] = None) -> str:
|
77
|
+
ok = await self.kv_set(key, value, namespace)
|
78
|
+
return "stored" if ok else "failed"
|
79
|
+
|
80
|
+
@tool(
|
81
|
+
description="Retrieve a previously stored piece of data by key. Returns empty string if not found.",
|
82
|
+
scope="owner"
|
83
|
+
)
|
84
|
+
async def kv_read(self, key: str, namespace: Optional[str] = None) -> str:
|
85
|
+
val = await self.kv_get(key, namespace)
|
86
|
+
return val or ""
|
87
|
+
|
88
|
+
|