jvserve 2.0.15__py3-none-any.whl → 2.1.1__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.
Potentially problematic release.
This version of jvserve might be problematic. Click here for more details.
- jvserve/__init__.py +4 -2
- jvserve/cli.py +207 -221
- jvserve/lib/agent_interface.py +67 -935
- jvserve/lib/file_interface.py +1 -1
- jvserve/lib/jac_interface.py +222 -0
- {jvserve-2.0.15.dist-info → jvserve-2.1.1.dist-info}/METADATA +4 -3
- jvserve-2.1.1.dist-info/RECORD +13 -0
- jvserve/lib/agent_pulse.py +0 -63
- jvserve-2.0.15.dist-info/RECORD +0 -13
- {jvserve-2.0.15.dist-info → jvserve-2.1.1.dist-info}/WHEEL +0 -0
- {jvserve-2.0.15.dist-info → jvserve-2.1.1.dist-info}/entry_points.txt +0 -0
- {jvserve-2.0.15.dist-info → jvserve-2.1.1.dist-info}/licenses/LICENSE +0 -0
- {jvserve-2.0.15.dist-info → jvserve-2.1.1.dist-info}/top_level.txt +0 -0
jvserve/lib/agent_interface.py
CHANGED
|
@@ -1,965 +1,97 @@
|
|
|
1
1
|
"""Agent Interface class and methods for interaction with Jivas."""
|
|
2
2
|
|
|
3
|
-
import asyncio
|
|
4
|
-
import json
|
|
5
3
|
import logging
|
|
6
4
|
import os
|
|
7
|
-
import string
|
|
8
|
-
import time
|
|
9
5
|
import traceback
|
|
10
|
-
from
|
|
11
|
-
from typing import Any, AsyncGenerator, Dict, Iterator, List, Optional
|
|
12
|
-
from urllib.parse import quote, unquote
|
|
6
|
+
from typing import Any
|
|
13
7
|
|
|
14
|
-
import aiohttp
|
|
15
8
|
import requests
|
|
16
|
-
from fastapi import File, Form, Request, UploadFile
|
|
17
|
-
from fastapi.responses import JSONResponse, StreamingResponse
|
|
18
|
-
from jac_cloud.core.architype import AnchorState, Permission, Root
|
|
19
|
-
from jac_cloud.core.context import (
|
|
20
|
-
JASECI_CONTEXT,
|
|
21
|
-
SUPER_ROOT,
|
|
22
|
-
SUPER_ROOT_ID,
|
|
23
|
-
ExecutionContext,
|
|
24
|
-
JaseciContext,
|
|
25
|
-
)
|
|
26
|
-
from jac_cloud.core.memory import MongoDB
|
|
27
|
-
from jac_cloud.plugin.jaseci import NodeAnchor
|
|
28
|
-
from jaclang.plugin.feature import JacFeature as _Jac
|
|
29
|
-
from jaclang.runtimelib.machine import JacMachine
|
|
30
|
-
from pydantic import BaseModel
|
|
31
9
|
|
|
10
|
+
from jvserve.lib.jac_interface import JacInterface
|
|
32
11
|
|
|
33
|
-
class AgentInterface:
|
|
34
|
-
"""Agent Interface for Jivas."""
|
|
35
|
-
|
|
36
|
-
HOST = "localhost"
|
|
37
|
-
PORT = 8000
|
|
38
|
-
ROOT_ID = ""
|
|
39
|
-
TOKEN = ""
|
|
40
|
-
EXPIRATION = None
|
|
41
|
-
LOGGER = logging.getLogger(__name__)
|
|
42
|
-
|
|
43
|
-
@staticmethod
|
|
44
|
-
def spawn_walker(
|
|
45
|
-
walker_name: str, module_name: str, attributes: dict
|
|
46
|
-
) -> _Jac.Walker:
|
|
47
|
-
"""Spawn any walker by name, located in module"""
|
|
48
|
-
# Get the list of modules
|
|
49
|
-
modules = JacMachine.get().list_modules()
|
|
50
|
-
|
|
51
|
-
# Search for the exact module name in the list of modules
|
|
52
|
-
for mod in modules:
|
|
53
|
-
if mod.endswith(module_name):
|
|
54
|
-
module_name = mod
|
|
55
|
-
break
|
|
56
|
-
|
|
57
|
-
try:
|
|
58
|
-
walker = JacMachine.get().spawn_walker(walker_name, attributes, module_name)
|
|
59
|
-
return walker
|
|
60
|
-
except Exception as e:
|
|
61
|
-
raise ValueError(
|
|
62
|
-
f"Unable to spawn walker {walker_name} in module {module_name}: {e}"
|
|
63
|
-
)
|
|
64
|
-
|
|
65
|
-
@staticmethod
|
|
66
|
-
def spawn_node(node_name: str, module_name: str, attributes: dict) -> _Jac.Node:
|
|
67
|
-
"""Spawn any node by name, located in module"""
|
|
68
|
-
# Get the list of modules
|
|
69
|
-
modules = JacMachine.get().list_modules()
|
|
70
|
-
|
|
71
|
-
# Search for the exact module name in the list of modules
|
|
72
|
-
for mod in modules:
|
|
73
|
-
if mod.endswith(module_name):
|
|
74
|
-
module_name = mod
|
|
75
|
-
break
|
|
76
|
-
|
|
77
|
-
try:
|
|
78
|
-
node = JacMachine.get().spawn_node(node_name, attributes, module_name)
|
|
79
|
-
return node
|
|
80
|
-
except Exception as e:
|
|
81
|
-
raise ValueError(
|
|
82
|
-
f"Unable to spawn node {node_name} in module {module_name}: {e}"
|
|
83
|
-
)
|
|
84
|
-
|
|
85
|
-
@staticmethod
|
|
86
|
-
async def webhook_exec(key: str, request: Request) -> JSONResponse:
|
|
87
|
-
"""
|
|
88
|
-
Execute a walker by name within context
|
|
89
|
-
The key combines the walker name, module name and agent_id in an encoded string
|
|
90
|
-
"""
|
|
91
|
-
params = {}
|
|
92
|
-
response = JSONResponse(status_code=200, content="200 OK")
|
|
93
|
-
|
|
94
|
-
# Capture query parameters dynamically
|
|
95
|
-
|
|
96
|
-
if query_params := request.query_params:
|
|
97
|
-
params = query_params
|
|
98
|
-
|
|
99
|
-
# Capture JSON body dynamically
|
|
100
|
-
if request.method == "POST":
|
|
101
|
-
try:
|
|
102
|
-
params = await request.json()
|
|
103
|
-
|
|
104
|
-
except Exception as e:
|
|
105
|
-
AgentInterface.LOGGER.warning(
|
|
106
|
-
f"Missing or invalid JSON served via webhook call: {e}"
|
|
107
|
-
)
|
|
108
|
-
|
|
109
|
-
# decode the arguments
|
|
110
|
-
args = AgentInterface.decrypt_webhook_key(key=key)
|
|
111
|
-
|
|
112
|
-
if args:
|
|
113
|
-
agent_id = args.get("agent_id")
|
|
114
|
-
module_root = args.get("module_root")
|
|
115
|
-
walker = args.get("walker")
|
|
116
|
-
|
|
117
|
-
if not agent_id or not walker or not module_root:
|
|
118
|
-
AgentInterface.LOGGER.error("malformed webhook key")
|
|
119
|
-
return response
|
|
120
|
-
else:
|
|
121
|
-
AgentInterface.LOGGER.error("malformed webhook key")
|
|
122
|
-
return response
|
|
123
|
-
|
|
124
|
-
ctx = await AgentInterface.load_context_async()
|
|
125
|
-
if ctx:
|
|
126
|
-
# compose full module_path
|
|
127
|
-
module = f"{module_root}.{walker}"
|
|
128
|
-
try:
|
|
129
|
-
response = _Jac.spawn_call(
|
|
130
|
-
ctx.entry_node.architype,
|
|
131
|
-
AgentInterface.spawn_walker(
|
|
132
|
-
walker_name=walker,
|
|
133
|
-
attributes={
|
|
134
|
-
"headers": request.headers,
|
|
135
|
-
"agent_id": agent_id,
|
|
136
|
-
"params": params,
|
|
137
|
-
"reporting": False,
|
|
138
|
-
},
|
|
139
|
-
module_name=module,
|
|
140
|
-
),
|
|
141
|
-
).response
|
|
142
|
-
|
|
143
|
-
if response:
|
|
144
|
-
if isinstance(response, str):
|
|
145
|
-
response = json.loads(response)
|
|
146
|
-
response = JSONResponse(
|
|
147
|
-
status_code=200, content=response, media_type="application/json"
|
|
148
|
-
)
|
|
149
|
-
|
|
150
|
-
except Exception as e:
|
|
151
|
-
AgentInterface.EXPIRATION = None
|
|
152
|
-
AgentInterface.LOGGER.error(
|
|
153
|
-
f"an exception occurred: {e}, {traceback.format_exc()}"
|
|
154
|
-
)
|
|
155
|
-
else:
|
|
156
|
-
AgentInterface.LOGGER.error(f"unable to execute {walker}")
|
|
157
|
-
|
|
158
|
-
ctx.close()
|
|
159
|
-
|
|
160
|
-
return response
|
|
161
|
-
|
|
162
|
-
@staticmethod
|
|
163
|
-
def get_action_data(agent_id: str, action_label: str) -> dict:
|
|
164
|
-
"""Retrieves the data for a specific action of an agent."""
|
|
165
|
-
|
|
166
|
-
action_data = {}
|
|
167
|
-
ctx = AgentInterface.load_context()
|
|
168
|
-
|
|
169
|
-
if not ctx:
|
|
170
|
-
return {}
|
|
171
|
-
|
|
172
|
-
# TODO : raise error in the event agent id is invalid
|
|
173
|
-
AgentInterface.LOGGER.debug(
|
|
174
|
-
f"attempting to interact with agent {agent_id} with user root {ctx.root}..."
|
|
175
|
-
)
|
|
176
|
-
|
|
177
|
-
try:
|
|
178
|
-
actions = _Jac.spawn_call(
|
|
179
|
-
ctx.entry_node.architype,
|
|
180
|
-
AgentInterface.spawn_walker(
|
|
181
|
-
walker_name="list_actions",
|
|
182
|
-
attributes={"agent_id": agent_id},
|
|
183
|
-
module_name="agent.action.list_actions",
|
|
184
|
-
),
|
|
185
|
-
).actions
|
|
186
|
-
|
|
187
|
-
if actions:
|
|
188
|
-
for action in actions:
|
|
189
|
-
if action.get("label") == action_label:
|
|
190
|
-
action_data = action
|
|
191
|
-
break
|
|
192
|
-
|
|
193
|
-
except Exception as e:
|
|
194
|
-
AgentInterface.EXPIRATION = None
|
|
195
|
-
AgentInterface.LOGGER.error(
|
|
196
|
-
f"an exception occurred: {e}, {traceback.format_exc()}"
|
|
197
|
-
)
|
|
198
|
-
|
|
199
|
-
ctx.close()
|
|
200
|
-
return action_data
|
|
201
|
-
|
|
202
|
-
@staticmethod
|
|
203
|
-
async def action_walker_exec(
|
|
204
|
-
agent_id: Optional[str] = Form(None), # noqa: B008
|
|
205
|
-
action: Optional[str] = Form(None), # noqa: B008
|
|
206
|
-
walker: Optional[str] = Form(None), # noqa: B008
|
|
207
|
-
args: Optional[str] = Form(None), # noqa: B008
|
|
208
|
-
attachments: List[UploadFile] = File(default_factory=list), # noqa: B008
|
|
209
|
-
) -> JSONResponse:
|
|
210
|
-
"""
|
|
211
|
-
Execute a named walker exposed by an action within context.
|
|
212
|
-
Capable of handling JSON or file data depending on request.
|
|
213
|
-
|
|
214
|
-
Args:
|
|
215
|
-
agent_id: ID of the agent
|
|
216
|
-
action: Name of the action
|
|
217
|
-
walker: Name of the walker to execute
|
|
218
|
-
args: JSON string of additional arguments
|
|
219
|
-
attachments: List of uploaded files
|
|
220
|
-
|
|
221
|
-
Returns:
|
|
222
|
-
JSONResponse: Response containing walker output or error message
|
|
223
|
-
"""
|
|
224
|
-
ctx = None
|
|
225
|
-
try:
|
|
226
|
-
# Validate required parameters
|
|
227
|
-
if walker is None or agent_id is None or action is None:
|
|
228
|
-
AgentInterface.LOGGER.error("Missing required parameters")
|
|
229
|
-
return JSONResponse(
|
|
230
|
-
status_code=400, # 400 (Bad Request)
|
|
231
|
-
content={"error": "Missing required parameters"},
|
|
232
|
-
)
|
|
233
|
-
|
|
234
|
-
# Get action data to resolve module
|
|
235
|
-
if agent_id is None or action is None:
|
|
236
|
-
AgentInterface.LOGGER.error("agent_id and action must not be None")
|
|
237
|
-
return JSONResponse(
|
|
238
|
-
status_code=400,
|
|
239
|
-
content={"error": "agent_id and action must not be None"},
|
|
240
|
-
)
|
|
241
|
-
|
|
242
|
-
action_data = AgentInterface.get_action_data(agent_id, action)
|
|
243
|
-
if not action_data:
|
|
244
|
-
AgentInterface.LOGGER.error(
|
|
245
|
-
f"Action {action} not found for agent {agent_id}"
|
|
246
|
-
)
|
|
247
|
-
return JSONResponse(
|
|
248
|
-
status_code=404,
|
|
249
|
-
content={"error": "Action not found"},
|
|
250
|
-
)
|
|
251
|
-
|
|
252
|
-
module_root = (
|
|
253
|
-
action_data.get("_package", {}).get("config", {}).get("module_root", "")
|
|
254
|
-
)
|
|
255
|
-
if not module_root:
|
|
256
|
-
AgentInterface.LOGGER.error(
|
|
257
|
-
f"Module not found for action {action} of agent {agent_id}"
|
|
258
|
-
)
|
|
259
|
-
return JSONResponse(
|
|
260
|
-
status_code=404,
|
|
261
|
-
content={"error": "Module not found"},
|
|
262
|
-
)
|
|
263
|
-
|
|
264
|
-
# Load execution context
|
|
265
|
-
ctx = await AgentInterface.load_context_async()
|
|
266
|
-
if not ctx:
|
|
267
|
-
AgentInterface.LOGGER.error(f"Unable to execute {walker}")
|
|
268
|
-
return JSONResponse(
|
|
269
|
-
status_code=500,
|
|
270
|
-
content={"error": "Failed to load execution context"},
|
|
271
|
-
)
|
|
272
|
-
|
|
273
|
-
# Prepare attributes
|
|
274
|
-
attributes: Dict[str, Any] = {"agent_id": agent_id}
|
|
275
|
-
|
|
276
|
-
# Parse additional arguments if provided
|
|
277
|
-
if args:
|
|
278
|
-
try:
|
|
279
|
-
attributes.update(json.loads(args))
|
|
280
|
-
except json.JSONDecodeError as e:
|
|
281
|
-
AgentInterface.LOGGER.error(f"Invalid JSON in args: {e}")
|
|
282
|
-
return JSONResponse(
|
|
283
|
-
status_code=400,
|
|
284
|
-
content={"error": "Invalid JSON in arguments"},
|
|
285
|
-
)
|
|
286
|
-
|
|
287
|
-
# Process uploaded files
|
|
288
|
-
if attachments:
|
|
289
|
-
attributes["files"] = []
|
|
290
|
-
for file in attachments:
|
|
291
|
-
try:
|
|
292
|
-
attributes["files"].append(
|
|
293
|
-
{
|
|
294
|
-
"name": file.filename,
|
|
295
|
-
"type": file.content_type,
|
|
296
|
-
"content": await file.read(),
|
|
297
|
-
}
|
|
298
|
-
)
|
|
299
|
-
except Exception as e:
|
|
300
|
-
AgentInterface.LOGGER.error(
|
|
301
|
-
f"Failed to process file {file.filename}: {e}"
|
|
302
|
-
)
|
|
303
|
-
continue # Skip problematic files or return error if critical
|
|
304
|
-
|
|
305
|
-
# Execute the walker
|
|
306
|
-
walker_response = _Jac.spawn_call(
|
|
307
|
-
ctx.entry_node.architype,
|
|
308
|
-
AgentInterface.spawn_walker(
|
|
309
|
-
walker_name=walker,
|
|
310
|
-
attributes=attributes,
|
|
311
|
-
module_name=f"{module_root}.{walker}",
|
|
312
|
-
),
|
|
313
|
-
).response
|
|
314
|
-
|
|
315
|
-
# Handle different response types appropriately
|
|
316
|
-
try:
|
|
317
|
-
# If it's already a proper Response object, return as-is
|
|
318
|
-
if isinstance(walker_response, requests.Response):
|
|
319
|
-
return walker_response
|
|
320
|
-
|
|
321
|
-
# If it's a Pydantic model or similar complex object with dict representation
|
|
322
|
-
if hasattr(walker_response, "dict"):
|
|
323
|
-
return JSONResponse(status_code=200, content=walker_response.dict())
|
|
324
|
-
|
|
325
|
-
# If it's a list of complex objects
|
|
326
|
-
if (
|
|
327
|
-
isinstance(walker_response, list)
|
|
328
|
-
and len(walker_response) > 0
|
|
329
|
-
and hasattr(walker_response[0], "dict")
|
|
330
|
-
):
|
|
331
|
-
return JSONResponse(
|
|
332
|
-
status_code=200,
|
|
333
|
-
content=[item.dict() for item in walker_response],
|
|
334
|
-
)
|
|
335
|
-
|
|
336
|
-
# For other JSON-serializable types
|
|
337
|
-
try:
|
|
338
|
-
return JSONResponse(status_code=200, content=walker_response)
|
|
339
|
-
except TypeError:
|
|
340
|
-
# Fallback to string representation if not directly JSON-serializable
|
|
341
|
-
return JSONResponse(
|
|
342
|
-
status_code=200, content={"result": str(walker_response)}
|
|
343
|
-
)
|
|
344
|
-
|
|
345
|
-
except Exception as e:
|
|
346
|
-
AgentInterface.LOGGER.error(f"Failed to format walker response: {e}")
|
|
347
|
-
return JSONResponse(
|
|
348
|
-
status_code=500,
|
|
349
|
-
content={"error": "Failed to format response", "details": str(e)},
|
|
350
|
-
)
|
|
351
|
-
|
|
352
|
-
except Exception as e:
|
|
353
|
-
AgentInterface.EXPIRATION = None
|
|
354
|
-
AgentInterface.LOGGER.error(
|
|
355
|
-
f"Exception occurred: {str(e)}\n{traceback.format_exc()}"
|
|
356
|
-
)
|
|
357
|
-
return JSONResponse(
|
|
358
|
-
status_code=500,
|
|
359
|
-
content={"error": "Internal server error", "details": str(e)},
|
|
360
|
-
)
|
|
361
|
-
finally:
|
|
362
|
-
if ctx:
|
|
363
|
-
try:
|
|
364
|
-
ctx.close()
|
|
365
|
-
except Exception as e:
|
|
366
|
-
AgentInterface.LOGGER.error(f"Error closing context: {str(e)}")
|
|
367
|
-
|
|
368
|
-
class InteractPayload(BaseModel):
|
|
369
|
-
"""Payload for interacting with the agent."""
|
|
370
|
-
|
|
371
|
-
agent_id: str
|
|
372
|
-
utterance: Optional[str] = None
|
|
373
|
-
channel: Optional[str] = None
|
|
374
|
-
session_id: Optional[str] = None
|
|
375
|
-
tts: Optional[bool] = None
|
|
376
|
-
verbose: Optional[bool] = None
|
|
377
|
-
data: Optional[list[dict]] = None
|
|
378
|
-
streaming: Optional[bool] = None
|
|
379
|
-
|
|
380
|
-
@staticmethod
|
|
381
|
-
def interact(payload: InteractPayload, request: Request) -> dict:
|
|
382
|
-
"""Interact with the agent."""
|
|
383
|
-
response = None
|
|
384
|
-
ctx = AgentInterface.load_context()
|
|
385
|
-
session_id = payload.session_id if payload.session_id else ""
|
|
386
|
-
|
|
387
|
-
if not ctx:
|
|
388
|
-
return {}
|
|
389
|
-
|
|
390
|
-
AgentInterface.LOGGER.debug(
|
|
391
|
-
f"attempting to interact with agent {payload.agent_id} with user root {ctx.root}..."
|
|
392
|
-
)
|
|
393
|
-
|
|
394
|
-
try:
|
|
395
|
-
response = _Jac.spawn_call(
|
|
396
|
-
ctx.entry_node.architype,
|
|
397
|
-
AgentInterface.spawn_walker(
|
|
398
|
-
walker_name="interact",
|
|
399
|
-
attributes={
|
|
400
|
-
"agent_id": payload.agent_id,
|
|
401
|
-
"utterance": payload.utterance or "",
|
|
402
|
-
"channel": payload.channel or "",
|
|
403
|
-
"session_id": session_id or "",
|
|
404
|
-
"tts": payload.tts or False,
|
|
405
|
-
"verbose": payload.verbose or False,
|
|
406
|
-
"data": payload.data or [],
|
|
407
|
-
"streaming": payload.streaming or False,
|
|
408
|
-
"reporting": False,
|
|
409
|
-
},
|
|
410
|
-
module_name="jivas.agent.action.interact",
|
|
411
|
-
),
|
|
412
|
-
)
|
|
413
|
-
|
|
414
|
-
if payload.streaming:
|
|
415
|
-
# since streaming occurs asynchronously, we'll need to close the context for writebacks here
|
|
416
|
-
# at this point of closure, there will be an open interaction_node without a response
|
|
417
|
-
# our job hereafter is to stream to completion and then update and close this interaction_node with the final result
|
|
418
|
-
|
|
419
|
-
ctx.close()
|
|
420
|
-
if (
|
|
421
|
-
response is not None
|
|
422
|
-
and hasattr(response, "generator")
|
|
423
|
-
and hasattr(response, "interaction_node")
|
|
424
|
-
):
|
|
425
|
-
|
|
426
|
-
interaction_node = response.interaction_node
|
|
427
|
-
|
|
428
|
-
async def generate(
|
|
429
|
-
generator: Iterator, request: Request
|
|
430
|
-
) -> AsyncGenerator[str, None]:
|
|
431
|
-
"""
|
|
432
|
-
Asynchronously yield data chunks from a response generator in Server-Sent Events (SSE) format.
|
|
433
|
-
|
|
434
|
-
Accumulates the full text content and yields each chunk as a JSON-encoded SSE message.
|
|
435
|
-
After all chunks are processed, updates the interaction node with the complete generated text and triggers an update in the graph context.
|
|
436
|
-
|
|
437
|
-
Yields:
|
|
438
|
-
str: A JSON-encoded string representing the current chunk of data in SSE format.
|
|
439
|
-
"""
|
|
440
|
-
full_text = ""
|
|
441
|
-
total_tokens = 0
|
|
442
|
-
|
|
443
|
-
try:
|
|
444
|
-
for chunk in generator:
|
|
445
|
-
full_text += chunk.content
|
|
446
|
-
total_tokens += 1 # each chunk is a token, let's tally
|
|
447
|
-
yield (
|
|
448
|
-
"data: "
|
|
449
|
-
+ json.dumps(
|
|
450
|
-
{
|
|
451
|
-
"id": interaction_node.id,
|
|
452
|
-
"content": chunk.content,
|
|
453
|
-
"session_id": interaction_node.response.get(
|
|
454
|
-
"session_id"
|
|
455
|
-
),
|
|
456
|
-
"type": chunk.type,
|
|
457
|
-
"metadata": chunk.response_metadata,
|
|
458
|
-
}
|
|
459
|
-
)
|
|
460
|
-
+ "\n\n"
|
|
461
|
-
)
|
|
462
|
-
await sleep(0.025)
|
|
463
|
-
# Update the interaction node with the fully generated text
|
|
464
|
-
actx = await AgentInterface.load_context_async()
|
|
465
|
-
try:
|
|
466
|
-
interaction_node.set_text_message(message=full_text)
|
|
467
|
-
interaction_node.add_tokens(total_tokens)
|
|
468
|
-
_Jac.spawn_call(
|
|
469
|
-
NodeAnchor.ref(interaction_node.id).architype,
|
|
470
|
-
AgentInterface.spawn_walker(
|
|
471
|
-
walker_name="update_interaction",
|
|
472
|
-
attributes={
|
|
473
|
-
"interaction_data": interaction_node.export(),
|
|
474
|
-
},
|
|
475
|
-
module_name="jivas.agent.memory.update_interaction",
|
|
476
|
-
),
|
|
477
|
-
)
|
|
478
|
-
finally:
|
|
479
|
-
if actx:
|
|
480
|
-
actx.close()
|
|
481
|
-
|
|
482
|
-
except Exception as e:
|
|
483
|
-
AgentInterface.LOGGER.error(
|
|
484
|
-
f"Exception in streaming generator: {e}, {traceback.format_exc()}"
|
|
485
|
-
)
|
|
486
|
-
except asyncio.CancelledError:
|
|
487
|
-
AgentInterface.LOGGER.error(
|
|
488
|
-
"Client disconnected. Aborting stream."
|
|
489
|
-
)
|
|
490
|
-
actx = await AgentInterface.load_context_async()
|
|
491
|
-
try:
|
|
492
|
-
interaction_node.set_text_message(message=full_text)
|
|
493
|
-
interaction_node.add_tokens(total_tokens)
|
|
494
|
-
_Jac.spawn_call(
|
|
495
|
-
NodeAnchor.ref(interaction_node.id).architype,
|
|
496
|
-
AgentInterface.spawn_walker(
|
|
497
|
-
walker_name="update_interaction",
|
|
498
|
-
attributes={
|
|
499
|
-
"interaction_data": interaction_node.export(),
|
|
500
|
-
},
|
|
501
|
-
module_name="jivas.agent.memory.update_interaction",
|
|
502
|
-
),
|
|
503
|
-
)
|
|
504
|
-
finally:
|
|
505
|
-
if actx:
|
|
506
|
-
actx.close()
|
|
507
|
-
|
|
508
|
-
return StreamingResponse(
|
|
509
|
-
generate(response.generator, request),
|
|
510
|
-
media_type="text/event-stream",
|
|
511
|
-
)
|
|
512
|
-
|
|
513
|
-
else:
|
|
514
|
-
AgentInterface.LOGGER.error(
|
|
515
|
-
"Response is None or missing required attributes for streaming."
|
|
516
|
-
)
|
|
517
|
-
return {}
|
|
518
|
-
|
|
519
|
-
else:
|
|
520
|
-
response = response.response
|
|
521
|
-
ctx.close()
|
|
522
|
-
return response if response else {}
|
|
523
|
-
|
|
524
|
-
except Exception as e:
|
|
525
|
-
AgentInterface.EXPIRATION = None
|
|
526
|
-
AgentInterface.LOGGER.error(
|
|
527
|
-
f"an exception occurred: {e}, {traceback.format_exc()}"
|
|
528
|
-
)
|
|
529
|
-
ctx.close()
|
|
530
|
-
return {}
|
|
531
|
-
|
|
532
|
-
@staticmethod
|
|
533
|
-
def pulse(action_label: str, agent_id: str = "") -> dict:
|
|
534
|
-
"""Interact with the agent."""
|
|
535
|
-
|
|
536
|
-
response = None
|
|
537
|
-
ctx = AgentInterface.load_context()
|
|
538
|
-
|
|
539
|
-
if not ctx:
|
|
540
|
-
return {}
|
|
541
|
-
|
|
542
|
-
# let's do some cleanup on the way schedule passes params; it includes in the value the param=
|
|
543
|
-
# we need to take this out if it exists..
|
|
544
|
-
action_label = action_label.replace("action_label=", "")
|
|
545
|
-
agent_id = agent_id.replace("agent_id=", "")
|
|
546
|
-
|
|
547
|
-
# TODO : raise error in the event agent id is invalid
|
|
548
|
-
AgentInterface.LOGGER.debug(
|
|
549
|
-
f"attempting to interact with agent {agent_id} with user root {ctx.root}..."
|
|
550
|
-
)
|
|
551
12
|
|
|
13
|
+
class AgentInterface:
|
|
14
|
+
"""Agent Interface for Jivas with proper concurrency handling."""
|
|
15
|
+
|
|
16
|
+
_instance = None
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
def __init__(self, host: str = "localhost", port: int = 8000) -> None:
|
|
20
|
+
"""Initialize the AgentInterface with JacInterface."""
|
|
21
|
+
self._jac = JacInterface(host, port)
|
|
22
|
+
|
|
23
|
+
@classmethod
|
|
24
|
+
def get_instance(
|
|
25
|
+
cls, host: str = "localhost", port: int = 8000
|
|
26
|
+
) -> "AgentInterface":
|
|
27
|
+
"""Get a singleton instance of AgentInterface."""
|
|
28
|
+
if cls._instance is None:
|
|
29
|
+
env_host = os.environ.get("JIVAS_HOST", "localhost")
|
|
30
|
+
env_port = int(os.environ.get("JIVAS_PORT", "8000"))
|
|
31
|
+
host = host or env_host
|
|
32
|
+
port = port or env_port
|
|
33
|
+
cls._instance = cls(host, port)
|
|
34
|
+
return cls._instance
|
|
35
|
+
|
|
36
|
+
async def init_agents(self) -> None:
|
|
37
|
+
"""Initialize agents - async compatible"""
|
|
552
38
|
try:
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
"agent_id": agent_id,
|
|
560
|
-
"reporting": True,
|
|
561
|
-
},
|
|
562
|
-
module_name="agent.action.pulse",
|
|
563
|
-
),
|
|
564
|
-
).response
|
|
39
|
+
if not await self._jac.spawn_walker_async(
|
|
40
|
+
walker_name="init_agents",
|
|
41
|
+
module_name="jivas.agent.core.init_agents",
|
|
42
|
+
attributes={"reporting": False},
|
|
43
|
+
):
|
|
44
|
+
self.logger.error("Agent initialization failed")
|
|
565
45
|
except Exception as e:
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
f"an exception occurred: {e}, {traceback.format_exc()}"
|
|
569
|
-
)
|
|
570
|
-
|
|
571
|
-
ctx.close()
|
|
572
|
-
return response if response else {}
|
|
573
|
-
|
|
574
|
-
@staticmethod
|
|
575
|
-
def api_pulse(action_label: str, agent_id: str) -> dict:
|
|
576
|
-
"""Interact with the agent pulse using API"""
|
|
46
|
+
self._jac.reset()
|
|
47
|
+
self.logger.error(f"Init error: {e}\n{traceback.format_exc()}")
|
|
577
48
|
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
if not ctx:
|
|
49
|
+
def api_pulse(self, action_label: str, agent_id: str) -> dict:
|
|
50
|
+
"""Synchronous pulse API call"""
|
|
51
|
+
if not self._jac.is_valid():
|
|
52
|
+
self.logger.warning("Invalid API state for pulse")
|
|
583
53
|
return {}
|
|
584
54
|
|
|
585
|
-
#
|
|
586
|
-
# we need to take this out if it exists..
|
|
55
|
+
# Clean parameters
|
|
587
56
|
action_label = action_label.replace("action_label=", "")
|
|
588
57
|
agent_id = agent_id.replace("agent_id=", "")
|
|
589
58
|
|
|
590
|
-
endpoint = f"http://{host}:{port}/walker/
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
try:
|
|
595
|
-
headers = {}
|
|
596
|
-
json = {"action_label": action_label, "agent_id": agent_id}
|
|
597
|
-
headers["Authorization"] = "Bearer " + AgentInterface.TOKEN
|
|
598
|
-
|
|
599
|
-
# call interact
|
|
600
|
-
response = requests.post(endpoint, json=json, headers=headers)
|
|
601
|
-
|
|
602
|
-
if response.status_code == 200:
|
|
603
|
-
result = response.json()
|
|
604
|
-
return result.get("reports", {})
|
|
605
|
-
|
|
606
|
-
if response.status_code == 401:
|
|
607
|
-
AgentInterface.EXPIRATION = None
|
|
608
|
-
return {}
|
|
609
|
-
|
|
610
|
-
except Exception as e:
|
|
611
|
-
AgentInterface.EXPIRATION = None
|
|
612
|
-
AgentInterface.LOGGER.error(
|
|
613
|
-
f"an exception occurred: {e}, {traceback.format_exc()}"
|
|
614
|
-
)
|
|
615
|
-
|
|
616
|
-
return {}
|
|
617
|
-
|
|
618
|
-
@staticmethod
|
|
619
|
-
def api_interact(payload: InteractPayload) -> dict:
|
|
620
|
-
"""Interact with the agent using API"""
|
|
621
|
-
|
|
622
|
-
host = AgentInterface.HOST
|
|
623
|
-
port = AgentInterface.PORT
|
|
624
|
-
ctx = AgentInterface.get_user_context()
|
|
625
|
-
session_id = payload.session_id if payload.session_id else ""
|
|
626
|
-
|
|
627
|
-
if not ctx:
|
|
628
|
-
return {}
|
|
629
|
-
|
|
630
|
-
endpoint = f"http://{host}:{port}/walker/interact"
|
|
631
|
-
|
|
632
|
-
if ctx["token"]:
|
|
633
|
-
|
|
634
|
-
try:
|
|
635
|
-
headers = {}
|
|
636
|
-
json = {
|
|
637
|
-
"agent_id": payload.agent_id,
|
|
638
|
-
"utterance": payload.utterance or "",
|
|
639
|
-
"channel": payload.channel or "",
|
|
640
|
-
"session_id": session_id or "",
|
|
641
|
-
"tts": payload.tts or False,
|
|
642
|
-
"verbose": payload.verbose or False,
|
|
643
|
-
"data": payload.data or [],
|
|
644
|
-
"streaming": payload.streaming or False,
|
|
645
|
-
"reporting": False,
|
|
646
|
-
}
|
|
647
|
-
headers["Authorization"] = "Bearer " + AgentInterface.TOKEN
|
|
648
|
-
|
|
649
|
-
# call interact
|
|
650
|
-
response = requests.post(endpoint, json=json, headers=headers)
|
|
651
|
-
|
|
652
|
-
if response.status_code == 200:
|
|
653
|
-
result = response.json()
|
|
654
|
-
return result["reports"]
|
|
655
|
-
|
|
656
|
-
if response.status_code == 401:
|
|
657
|
-
AgentInterface.EXPIRATION = None
|
|
658
|
-
return {}
|
|
659
|
-
|
|
660
|
-
except Exception as e:
|
|
661
|
-
AgentInterface.EXPIRATION = None
|
|
662
|
-
AgentInterface.LOGGER.error(
|
|
663
|
-
f"an exception occurred: {e}, {traceback.format_exc()}"
|
|
664
|
-
)
|
|
665
|
-
|
|
666
|
-
return {}
|
|
667
|
-
|
|
668
|
-
@staticmethod
|
|
669
|
-
def load_context(entry: NodeAnchor | None = None) -> Optional[ExecutionContext]:
|
|
670
|
-
"""Load the execution context synchronously."""
|
|
671
|
-
AgentInterface.get_user_context()
|
|
672
|
-
return AgentInterface.get_jaseci_context(entry, AgentInterface.ROOT_ID)
|
|
673
|
-
|
|
674
|
-
@staticmethod
|
|
675
|
-
async def load_context_async(
|
|
676
|
-
entry: NodeAnchor | None = None,
|
|
677
|
-
) -> Optional[ExecutionContext]:
|
|
678
|
-
"""Load the execution context asynchronously."""
|
|
679
|
-
ctx = await AgentInterface.get_user_context_async()
|
|
680
|
-
if ctx:
|
|
681
|
-
AgentInterface.ROOT_ID = ctx["root_id"]
|
|
682
|
-
AgentInterface.TOKEN = ctx["token"]
|
|
683
|
-
AgentInterface.EXPIRATION = ctx["expiration"]
|
|
684
|
-
return AgentInterface.get_jaseci_context(entry, AgentInterface.ROOT_ID)
|
|
685
|
-
|
|
686
|
-
@staticmethod
|
|
687
|
-
def get_jaseci_context(entry: NodeAnchor | None, root_id: str) -> ExecutionContext:
|
|
688
|
-
"""Build the execution context for the agent."""
|
|
59
|
+
endpoint = f"http://{self._jac.host}:{self._jac.port}/walker/do_pulse"
|
|
60
|
+
headers = {"Authorization": f"Bearer {self._jac.token}"}
|
|
61
|
+
payload = {"action_label": action_label, "agent_id": agent_id}
|
|
689
62
|
|
|
690
63
|
try:
|
|
691
|
-
ctx = JaseciContext()
|
|
692
|
-
ctx.base = ExecutionContext.get()
|
|
693
|
-
except Exception as e:
|
|
694
|
-
AgentInterface.LOGGER.error(
|
|
695
|
-
f"an exception occurred: {e}, {traceback.format_exc()}"
|
|
696
|
-
)
|
|
697
|
-
return None
|
|
698
|
-
|
|
699
|
-
ctx.mem = MongoDB()
|
|
700
|
-
ctx.reports = []
|
|
701
|
-
ctx.status = 200
|
|
702
|
-
|
|
703
|
-
# load the user root graph
|
|
704
|
-
user_root = NodeAnchor.ref(f"n:root:{root_id}")
|
|
705
|
-
|
|
706
|
-
if not isinstance(system_root := ctx.mem.find_by_id(SUPER_ROOT), NodeAnchor):
|
|
707
|
-
system_root = NodeAnchor(
|
|
708
|
-
architype=object.__new__(Root),
|
|
709
|
-
id=SUPER_ROOT_ID,
|
|
710
|
-
access=Permission(),
|
|
711
|
-
state=AnchorState(connected=True),
|
|
712
|
-
persistent=True,
|
|
713
|
-
edges=[],
|
|
714
|
-
)
|
|
715
|
-
system_root.architype.__jac__ = system_root
|
|
716
|
-
NodeAnchor.Collection.insert_one(system_root.serialize())
|
|
717
|
-
system_root.sync_hash()
|
|
718
|
-
ctx.mem.set(system_root.id, system_root)
|
|
719
|
-
|
|
720
|
-
ctx.system_root = system_root
|
|
721
|
-
ctx.root = user_root if user_root else system_root
|
|
722
|
-
ctx.entry_node = entry if entry else ctx.root
|
|
723
|
-
|
|
724
|
-
if _ctx := JASECI_CONTEXT.get(None):
|
|
725
|
-
_ctx.close()
|
|
726
|
-
JASECI_CONTEXT.set(ctx)
|
|
727
|
-
|
|
728
|
-
return ctx
|
|
729
|
-
|
|
730
|
-
@staticmethod
|
|
731
|
-
def get_user_context() -> Optional[dict]:
|
|
732
|
-
"""Set graph context for JIVAS if user is not logged in; attempt registration if login fails."""
|
|
733
|
-
ctx: dict = {}
|
|
734
|
-
host = AgentInterface.HOST
|
|
735
|
-
port = AgentInterface.PORT
|
|
736
|
-
|
|
737
|
-
# if user context still active, return it
|
|
738
|
-
now = int(time.time())
|
|
739
|
-
if AgentInterface.EXPIRATION and AgentInterface.EXPIRATION > now:
|
|
740
|
-
return {
|
|
741
|
-
"root_id": AgentInterface.ROOT_ID,
|
|
742
|
-
"token": AgentInterface.TOKEN,
|
|
743
|
-
"expiration": AgentInterface.EXPIRATION,
|
|
744
|
-
}
|
|
745
|
-
|
|
746
|
-
user = os.environ.get("JIVAS_USER")
|
|
747
|
-
password = os.environ.get("JIVAS_PASSWORD")
|
|
748
|
-
if not user or not password:
|
|
749
|
-
AgentInterface.LOGGER.error(
|
|
750
|
-
"JIVAS_USER and or JIVAS_PASSWORD environment variable is not set."
|
|
751
|
-
)
|
|
752
|
-
return ctx
|
|
753
|
-
|
|
754
|
-
login_url = f"http://{host}:{port}/user/login"
|
|
755
|
-
register_url = f"http://{host}:{port}/user/register"
|
|
756
|
-
|
|
757
|
-
try:
|
|
758
|
-
# Attempt to log in
|
|
759
64
|
response = requests.post(
|
|
760
|
-
|
|
65
|
+
endpoint, json=payload, headers=headers, timeout=10
|
|
761
66
|
)
|
|
762
|
-
|
|
763
67
|
if response.status_code == 200:
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
]
|
|
768
|
-
ctx["token"] = AgentInterface.TOKEN = response.json()["token"]
|
|
769
|
-
ctx["expiration"] = AgentInterface.EXPIRATION = response.json()["user"][
|
|
770
|
-
"expiration"
|
|
771
|
-
]
|
|
772
|
-
|
|
773
|
-
else:
|
|
774
|
-
AgentInterface.LOGGER.info(
|
|
775
|
-
f"Login failed with status code {response.status_code}, attempting registration..."
|
|
776
|
-
)
|
|
777
|
-
|
|
778
|
-
# Attempt to register the user
|
|
779
|
-
register_response = requests.post(
|
|
780
|
-
register_url, json={"email": user, "password": password}
|
|
781
|
-
)
|
|
782
|
-
|
|
783
|
-
if register_response.status_code == 201:
|
|
784
|
-
# Registration successful, now log in again
|
|
785
|
-
AgentInterface.LOGGER.info(
|
|
786
|
-
f"Registration successful for user {user}, attempting login again..."
|
|
787
|
-
)
|
|
788
|
-
|
|
789
|
-
# Re-attempt login after successful registration
|
|
790
|
-
login_response = requests.post(
|
|
791
|
-
login_url, json={"email": user, "password": password}
|
|
792
|
-
)
|
|
793
|
-
|
|
794
|
-
if login_response.status_code == 200:
|
|
795
|
-
AgentInterface.LOGGER.info(
|
|
796
|
-
f"Login successful after registration, ROOT_ID ({ctx['root_id']}) set for user {user}."
|
|
797
|
-
)
|
|
798
|
-
else:
|
|
799
|
-
AgentInterface.LOGGER.error(
|
|
800
|
-
f"Login failed after registration with status code {login_response.status_code}."
|
|
801
|
-
)
|
|
802
|
-
else:
|
|
803
|
-
AgentInterface.LOGGER.error(
|
|
804
|
-
f"Registration failed with status code {register_response.status_code}."
|
|
805
|
-
)
|
|
806
|
-
|
|
68
|
+
return response.json().get("reports", {})
|
|
69
|
+
if response.status_code == 401:
|
|
70
|
+
self._jac.reset()
|
|
807
71
|
except Exception as e:
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
f"an exception occurred: {e}, {traceback.format_exc()}"
|
|
811
|
-
)
|
|
72
|
+
self._jac.reset()
|
|
73
|
+
self.logger.error(f"Pulse error: {e}\n{traceback.format_exc()}")
|
|
812
74
|
|
|
813
|
-
return
|
|
814
|
-
|
|
815
|
-
@staticmethod
|
|
816
|
-
async def get_user_context_async() -> Optional[dict]:
|
|
817
|
-
"""Set graph context for JIVAS if user is not logged in; attempt registration if login fails."""
|
|
818
|
-
ctx: dict = {}
|
|
819
|
-
host = AgentInterface.HOST
|
|
820
|
-
port = AgentInterface.PORT
|
|
75
|
+
return {}
|
|
821
76
|
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
}
|
|
77
|
+
async def _finalize_interaction(
|
|
78
|
+
self, interaction_node: Any, full_text: str, total_tokens: int
|
|
79
|
+
) -> None:
|
|
80
|
+
"""Finalize interaction in background"""
|
|
81
|
+
try:
|
|
82
|
+
interaction_node.set_text_message(message=full_text)
|
|
83
|
+
interaction_node.add_tokens(total_tokens)
|
|
830
84
|
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
"JIVAS_USER and or JIVAS_PASSWORD environment variable is not set."
|
|
85
|
+
await self._jac.spawn_walker_async(
|
|
86
|
+
walker_name="update_interaction",
|
|
87
|
+
module_name="jivas.agent.memory.update_interaction",
|
|
88
|
+
attributes={"interaction_data": interaction_node.export()},
|
|
836
89
|
)
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
login_url = f"http://{host}:{port}/user/login"
|
|
840
|
-
register_url = f"http://{host}:{port}/user/register"
|
|
841
|
-
|
|
842
|
-
async with aiohttp.ClientSession() as session:
|
|
843
|
-
try:
|
|
844
|
-
# Attempt to log in
|
|
845
|
-
async with session.post(
|
|
846
|
-
login_url, json={"email": user, "password": password}
|
|
847
|
-
) as response:
|
|
848
|
-
if response.status == 200:
|
|
849
|
-
# Login successful, set the ROOT_ID
|
|
850
|
-
data = await response.json()
|
|
851
|
-
ctx["root_id"] = AgentInterface.ROOT_ID = data["user"][
|
|
852
|
-
"root_id"
|
|
853
|
-
]
|
|
854
|
-
ctx["token"] = AgentInterface.TOKEN = data["token"]
|
|
855
|
-
ctx["expiration"] = AgentInterface.EXPIRATION = data["user"][
|
|
856
|
-
"expiration"
|
|
857
|
-
]
|
|
858
|
-
else:
|
|
859
|
-
AgentInterface.LOGGER.info(
|
|
860
|
-
f"Login failed with status code {response.status}, attempting registration..."
|
|
861
|
-
)
|
|
862
|
-
|
|
863
|
-
# Attempt to register the user
|
|
864
|
-
async with session.post(
|
|
865
|
-
register_url, json={"email": user, "password": password}
|
|
866
|
-
) as register_response:
|
|
867
|
-
if register_response.status == 201:
|
|
868
|
-
AgentInterface.LOGGER.info(
|
|
869
|
-
f"Registration successful for user {user}, attempting login again..."
|
|
870
|
-
)
|
|
871
|
-
|
|
872
|
-
# Re-attempt login after successful registration
|
|
873
|
-
async with session.post(
|
|
874
|
-
login_url,
|
|
875
|
-
json={"email": user, "password": password},
|
|
876
|
-
) as login_response:
|
|
877
|
-
if login_response.status == 200:
|
|
878
|
-
data = await login_response.json()
|
|
879
|
-
root_id = data["user"]["root_id"]
|
|
880
|
-
ctx["root_id"] = root_id
|
|
881
|
-
ctx["token"] = data["token"]
|
|
882
|
-
ctx["expiration"] = data["user"]["expiration"]
|
|
883
|
-
AgentInterface.LOGGER.info(
|
|
884
|
-
f"Login successful after registration, ROOT_ID ({ctx['root_id']}) set for user {user}."
|
|
885
|
-
)
|
|
886
|
-
else:
|
|
887
|
-
AgentInterface.LOGGER.error(
|
|
888
|
-
f"Login failed after registration with status code {login_response.status}."
|
|
889
|
-
)
|
|
890
|
-
else:
|
|
891
|
-
AgentInterface.LOGGER.error(
|
|
892
|
-
f"Registration failed with status code {register_response.status}."
|
|
893
|
-
)
|
|
894
|
-
|
|
895
|
-
except Exception as e:
|
|
896
|
-
AgentInterface.EXPIRATION = None
|
|
897
|
-
AgentInterface.LOGGER.error(
|
|
898
|
-
f"an exception occurred: {e}, {traceback.format_exc()}"
|
|
899
|
-
)
|
|
900
|
-
|
|
901
|
-
return ctx
|
|
902
|
-
|
|
903
|
-
@staticmethod
|
|
904
|
-
def generate_cipher_alphabet() -> tuple[str, str]:
|
|
905
|
-
"""Generate a cipher alphabet for encryption."""
|
|
906
|
-
# TODO: make this more secure
|
|
907
|
-
secret_key = os.environ.get("JIVAS_WEBHOOK_SECRET_KEY", "ABCDEFGHIJK")
|
|
908
|
-
secret_key = secret_key.lower() + secret_key.upper()
|
|
909
|
-
seen = set()
|
|
910
|
-
key_unique = "".join(
|
|
911
|
-
seen.add(c) or c for c in secret_key if c not in seen and c.isalpha() # type: ignore
|
|
912
|
-
)
|
|
913
|
-
remaining = "".join(
|
|
914
|
-
c
|
|
915
|
-
for c in string.ascii_lowercase + string.ascii_uppercase
|
|
916
|
-
if c not in seen and c.isalpha()
|
|
917
|
-
)
|
|
918
|
-
return key_unique, remaining
|
|
919
|
-
|
|
920
|
-
@staticmethod
|
|
921
|
-
def encrypt_webhook_key(agent_id: str, module_root: str, walker: str) -> str:
|
|
922
|
-
"""Encrypt the webhook key."""
|
|
923
|
-
lower_cipher_alphabet, upper_cipher_alphabet = (
|
|
924
|
-
AgentInterface.generate_cipher_alphabet()
|
|
925
|
-
)
|
|
926
|
-
table = str.maketrans(
|
|
927
|
-
string.ascii_lowercase + string.ascii_uppercase,
|
|
928
|
-
lower_cipher_alphabet + upper_cipher_alphabet,
|
|
929
|
-
)
|
|
930
|
-
key_text = json.dumps(
|
|
931
|
-
{"agent_id": agent_id, "module_root": module_root, "walker": walker},
|
|
932
|
-
separators=(",", ":"),
|
|
933
|
-
)
|
|
934
|
-
|
|
935
|
-
# Translate using the cipher alphabet
|
|
936
|
-
encoded_text = key_text.translate(table)
|
|
937
|
-
|
|
938
|
-
# URL encode the translated output
|
|
939
|
-
return quote(encoded_text)
|
|
940
|
-
|
|
941
|
-
@staticmethod
|
|
942
|
-
def decrypt_webhook_key(key: str) -> Optional[dict]:
|
|
943
|
-
"""Decrypt the webhook key."""
|
|
944
|
-
lower_cipher_alphabet, upper_cipher_alphabet = (
|
|
945
|
-
AgentInterface.generate_cipher_alphabet()
|
|
946
|
-
)
|
|
947
|
-
table = str.maketrans(
|
|
948
|
-
lower_cipher_alphabet + upper_cipher_alphabet,
|
|
949
|
-
string.ascii_lowercase + string.ascii_uppercase,
|
|
950
|
-
)
|
|
951
|
-
|
|
952
|
-
# Decode the URL-encoded string
|
|
953
|
-
decoded_text = unquote(key)
|
|
90
|
+
except Exception as e:
|
|
91
|
+
self.logger.error(f"Finalize error: {e}")
|
|
954
92
|
|
|
955
|
-
# Translate back using the cipher alphabet
|
|
956
|
-
key_text = decoded_text.translate(table)
|
|
957
93
|
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
AgentInterface.LOGGER.error(
|
|
963
|
-
f"an exception occurred: {e}, {traceback.format_exc()}"
|
|
964
|
-
)
|
|
965
|
-
return {}
|
|
94
|
+
# Module-level functions
|
|
95
|
+
def do_pulse(action_label: str, agent_id: str) -> dict:
|
|
96
|
+
"""Execute pulse action synchronously"""
|
|
97
|
+
return AgentInterface.get_instance().api_pulse(action_label, agent_id)
|