jvserve 2.0.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.
Potentially problematic release.
This version of jvserve might be problematic. Click here for more details.
- jvserve/__init__.py +8 -0
- jvserve/cli.py +138 -0
- jvserve/lib/__init__.py +1 -0
- jvserve/lib/agent_interface.py +680 -0
- jvserve/lib/agent_pulse.py +63 -0
- jvserve/lib/jvlogger.py +108 -0
- jvserve-2.0.0.dist-info/LICENSE +201 -0
- jvserve-2.0.0.dist-info/METADATA +146 -0
- jvserve-2.0.0.dist-info/RECORD +12 -0
- jvserve-2.0.0.dist-info/WHEEL +5 -0
- jvserve-2.0.0.dist-info/entry_points.txt +2 -0
- jvserve-2.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,680 @@
|
|
|
1
|
+
"""Agent Interface class and methods for interaction with Jivas."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
import os
|
|
6
|
+
import string
|
|
7
|
+
import time
|
|
8
|
+
import traceback
|
|
9
|
+
from typing import Any, Dict, Optional
|
|
10
|
+
from urllib.parse import quote, unquote
|
|
11
|
+
|
|
12
|
+
import aiohttp
|
|
13
|
+
import requests
|
|
14
|
+
from fastapi import Form, Request, UploadFile
|
|
15
|
+
from fastapi.responses import JSONResponse
|
|
16
|
+
from jac_cloud.core.architype import AnchorState, Permission, Root
|
|
17
|
+
from jac_cloud.core.context import (
|
|
18
|
+
JASECI_CONTEXT,
|
|
19
|
+
SUPER_ROOT,
|
|
20
|
+
SUPER_ROOT_ID,
|
|
21
|
+
ExecutionContext,
|
|
22
|
+
JaseciContext,
|
|
23
|
+
)
|
|
24
|
+
from jac_cloud.core.memory import MongoDB
|
|
25
|
+
from jac_cloud.plugin.jaseci import NodeAnchor
|
|
26
|
+
from jaclang.plugin.feature import JacFeature as _Jac
|
|
27
|
+
from jaclang.runtimelib.machine import JacMachine
|
|
28
|
+
from pydantic import BaseModel
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class AgentInterface:
|
|
32
|
+
"""Agent Interface for Jivas."""
|
|
33
|
+
|
|
34
|
+
HOST = "localhost"
|
|
35
|
+
PORT = 8000
|
|
36
|
+
ROOT_ID = ""
|
|
37
|
+
TOKEN = ""
|
|
38
|
+
EXPIRATION = ""
|
|
39
|
+
LOGGER = logging.getLogger(__name__)
|
|
40
|
+
|
|
41
|
+
@staticmethod
|
|
42
|
+
def spawn_walker(
|
|
43
|
+
walker_name: str, module_name: str, attributes: dict
|
|
44
|
+
) -> _Jac.Walker:
|
|
45
|
+
"""Spawn any walker by name, located in module"""
|
|
46
|
+
return JacMachine.get().spawn_walker(walker_name, attributes, module_name)
|
|
47
|
+
|
|
48
|
+
@staticmethod
|
|
49
|
+
async def webhook_exec(key: str, request: Request) -> JSONResponse:
|
|
50
|
+
"""
|
|
51
|
+
Execute a walker by name within context
|
|
52
|
+
The key combines the walker name, module name and agent_id in an encoded string
|
|
53
|
+
"""
|
|
54
|
+
params = {}
|
|
55
|
+
response = JSONResponse(status_code=200, content="200 OK")
|
|
56
|
+
|
|
57
|
+
# Capture query parameters dynamically
|
|
58
|
+
|
|
59
|
+
if query_params := request.query_params:
|
|
60
|
+
params = query_params
|
|
61
|
+
|
|
62
|
+
# Capture JSON body dynamically
|
|
63
|
+
if request.method == "POST":
|
|
64
|
+
try:
|
|
65
|
+
params = await request.json()
|
|
66
|
+
|
|
67
|
+
except Exception as e:
|
|
68
|
+
AgentInterface.LOGGER.warning(
|
|
69
|
+
f"Missing or invalid JSON served via webhook call: {e}"
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
# decode the arguments
|
|
73
|
+
args = AgentInterface.decrypt_webhook_key(key=key)
|
|
74
|
+
|
|
75
|
+
if args:
|
|
76
|
+
agent_id = args.get("agent_id")
|
|
77
|
+
module_root = args.get("module_root")
|
|
78
|
+
walker = args.get("walker")
|
|
79
|
+
|
|
80
|
+
if not agent_id or not walker or not module_root:
|
|
81
|
+
AgentInterface.LOGGER.error("malformed webhook key")
|
|
82
|
+
return response
|
|
83
|
+
else:
|
|
84
|
+
AgentInterface.LOGGER.error("malformed webhook key")
|
|
85
|
+
return response
|
|
86
|
+
|
|
87
|
+
ctx = await AgentInterface.load_context_async()
|
|
88
|
+
if ctx:
|
|
89
|
+
# compose full module_path
|
|
90
|
+
module = f"{module_root}.{walker}"
|
|
91
|
+
try:
|
|
92
|
+
response = _Jac.spawn_call(
|
|
93
|
+
ctx.entry_node.architype,
|
|
94
|
+
AgentInterface.spawn_walker(
|
|
95
|
+
walker_name=walker,
|
|
96
|
+
attributes={
|
|
97
|
+
"agent_id": agent_id,
|
|
98
|
+
"params": params,
|
|
99
|
+
"reporting": False,
|
|
100
|
+
},
|
|
101
|
+
module_name=module,
|
|
102
|
+
),
|
|
103
|
+
).response
|
|
104
|
+
|
|
105
|
+
if response:
|
|
106
|
+
if isinstance(response, str):
|
|
107
|
+
response = json.loads(response)
|
|
108
|
+
response = JSONResponse(
|
|
109
|
+
status_code=200, content=response, media_type="application/json"
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
except Exception as e:
|
|
113
|
+
AgentInterface.EXPIRATION = ""
|
|
114
|
+
AgentInterface.LOGGER.error(
|
|
115
|
+
f"an exception occurred: {e}, {traceback.format_exc()}"
|
|
116
|
+
)
|
|
117
|
+
else:
|
|
118
|
+
AgentInterface.LOGGER.error(f"unable to execute {walker}")
|
|
119
|
+
|
|
120
|
+
ctx.close()
|
|
121
|
+
|
|
122
|
+
return response
|
|
123
|
+
|
|
124
|
+
@staticmethod
|
|
125
|
+
async def action_walker_exec(
|
|
126
|
+
request: Request,
|
|
127
|
+
agent_id: str = Form(...), # noqa: B008
|
|
128
|
+
module_root: str = Form(...), # noqa: B008
|
|
129
|
+
walker: str = Form(...), # noqa: B008
|
|
130
|
+
args: Optional[str] = Form(None), # noqa: B008
|
|
131
|
+
attachments: Optional[list[UploadFile]] = None,
|
|
132
|
+
) -> JSONResponse:
|
|
133
|
+
"""Execute a named walker exposed by an action within context; capable of handling JSON or file data depending on request"""
|
|
134
|
+
|
|
135
|
+
response = JSONResponse(status_code=500, content="unable to complete request")
|
|
136
|
+
|
|
137
|
+
try:
|
|
138
|
+
|
|
139
|
+
# add agent id as a standard
|
|
140
|
+
attributes: Dict[str, Any] = {"agent_id": agent_id}
|
|
141
|
+
|
|
142
|
+
# add any other args
|
|
143
|
+
if args:
|
|
144
|
+
attributes.update(json.loads(args))
|
|
145
|
+
|
|
146
|
+
# Processing files if any were uploaded
|
|
147
|
+
if attachments:
|
|
148
|
+
attributes["files"] = []
|
|
149
|
+
for file in attachments:
|
|
150
|
+
attributes["files"].append(
|
|
151
|
+
{
|
|
152
|
+
"name": file.filename,
|
|
153
|
+
"type": file.content_type,
|
|
154
|
+
"content": await file.read(),
|
|
155
|
+
}
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
if not agent_id or not walker or not module_root:
|
|
159
|
+
AgentInterface.LOGGER.error("missing parameters")
|
|
160
|
+
return JSONResponse(
|
|
161
|
+
status_code=401, content="missing required parameters"
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
except Exception as e:
|
|
165
|
+
AgentInterface.LOGGER.error(
|
|
166
|
+
f"an exception occurred: {e}, {traceback.format_exc()}"
|
|
167
|
+
)
|
|
168
|
+
return JSONResponse(status_code=500, content="internal server error")
|
|
169
|
+
|
|
170
|
+
ctx = await AgentInterface.load_context_async()
|
|
171
|
+
if ctx:
|
|
172
|
+
# compose full module_path
|
|
173
|
+
module = f"{module_root}.{walker}"
|
|
174
|
+
|
|
175
|
+
try:
|
|
176
|
+
response = _Jac.spawn_call(
|
|
177
|
+
ctx.entry_node.architype,
|
|
178
|
+
AgentInterface.spawn_walker(
|
|
179
|
+
walker_name=walker,
|
|
180
|
+
attributes=attributes,
|
|
181
|
+
module_name=module,
|
|
182
|
+
),
|
|
183
|
+
).response
|
|
184
|
+
|
|
185
|
+
except Exception as e:
|
|
186
|
+
AgentInterface.EXPIRATION = ""
|
|
187
|
+
AgentInterface.LOGGER.error(
|
|
188
|
+
f"an exception occurred: {e}, {traceback.format_exc()}"
|
|
189
|
+
)
|
|
190
|
+
else:
|
|
191
|
+
AgentInterface.LOGGER.error(f"unable to execute {walker}")
|
|
192
|
+
|
|
193
|
+
ctx.close()
|
|
194
|
+
|
|
195
|
+
return response
|
|
196
|
+
|
|
197
|
+
class InteractPayload(BaseModel):
|
|
198
|
+
"""Payload for interacting with the agent."""
|
|
199
|
+
|
|
200
|
+
agent_id: str
|
|
201
|
+
utterance: str
|
|
202
|
+
session_id: str
|
|
203
|
+
tts: bool
|
|
204
|
+
verbose: bool
|
|
205
|
+
|
|
206
|
+
@staticmethod
|
|
207
|
+
def interact(payload: InteractPayload) -> dict:
|
|
208
|
+
"""Interact with the agent."""
|
|
209
|
+
|
|
210
|
+
response = None
|
|
211
|
+
ctx = AgentInterface.load_context()
|
|
212
|
+
session_id = payload.session_id if payload.session_id else ""
|
|
213
|
+
|
|
214
|
+
if not ctx:
|
|
215
|
+
return {}
|
|
216
|
+
|
|
217
|
+
AgentInterface.LOGGER.debug(
|
|
218
|
+
f"attempting to interact with agent {payload.agent_id} with user root {ctx.root}..."
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
try:
|
|
222
|
+
response = _Jac.spawn_call(
|
|
223
|
+
ctx.entry_node.architype,
|
|
224
|
+
AgentInterface.spawn_walker(
|
|
225
|
+
walker_name="interact",
|
|
226
|
+
attributes={
|
|
227
|
+
"agent_id": payload.agent_id,
|
|
228
|
+
"utterance": payload.utterance,
|
|
229
|
+
"session_id": session_id,
|
|
230
|
+
"tts": payload.tts,
|
|
231
|
+
"verbose": payload.verbose,
|
|
232
|
+
"reporting": False,
|
|
233
|
+
},
|
|
234
|
+
module_name="agent.action.interact",
|
|
235
|
+
),
|
|
236
|
+
).response
|
|
237
|
+
except Exception as e:
|
|
238
|
+
AgentInterface.EXPIRATION = ""
|
|
239
|
+
AgentInterface.LOGGER.error(
|
|
240
|
+
f"an exception occurred: {e}, {traceback.format_exc()}"
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
ctx.close()
|
|
244
|
+
return response if response else {}
|
|
245
|
+
|
|
246
|
+
@staticmethod
|
|
247
|
+
def pulse(action_label: str, agent_id: str = "") -> dict:
|
|
248
|
+
"""Interact with the agent."""
|
|
249
|
+
|
|
250
|
+
response = None
|
|
251
|
+
ctx = AgentInterface.load_context()
|
|
252
|
+
|
|
253
|
+
if not ctx:
|
|
254
|
+
return {}
|
|
255
|
+
|
|
256
|
+
# let's do some cleanup on the way schedule passes params; it includes in the value the param=
|
|
257
|
+
# we need to take this out if it exists..
|
|
258
|
+
action_label = action_label.replace("action_label=", "")
|
|
259
|
+
agent_id = agent_id.replace("agent_id=", "")
|
|
260
|
+
|
|
261
|
+
# TODO : raise error in the event agent id is invalid
|
|
262
|
+
AgentInterface.LOGGER.debug(
|
|
263
|
+
f"attempting to interact with agent {agent_id} with user root {ctx.root}..."
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
try:
|
|
267
|
+
response = _Jac.spawn_call(
|
|
268
|
+
ctx.entry_node.architype,
|
|
269
|
+
AgentInterface.spawn_walker(
|
|
270
|
+
walker_name="pulse",
|
|
271
|
+
attributes={
|
|
272
|
+
"action_label": action_label,
|
|
273
|
+
"agent_id": agent_id,
|
|
274
|
+
"reporting": True,
|
|
275
|
+
},
|
|
276
|
+
module_name="agent.action.pulse",
|
|
277
|
+
),
|
|
278
|
+
).response
|
|
279
|
+
except Exception as e:
|
|
280
|
+
AgentInterface.EXPIRATION = ""
|
|
281
|
+
AgentInterface.LOGGER.error(
|
|
282
|
+
f"an exception occurred: {e}, {traceback.format_exc()}"
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
ctx.close()
|
|
286
|
+
return response if response else {}
|
|
287
|
+
|
|
288
|
+
@staticmethod
|
|
289
|
+
def api_pulse(action_label: str, agent_id: str) -> dict:
|
|
290
|
+
"""Interact with the agent pulse using API"""
|
|
291
|
+
|
|
292
|
+
host = AgentInterface.HOST
|
|
293
|
+
port = AgentInterface.PORT
|
|
294
|
+
ctx = AgentInterface.get_user_context()
|
|
295
|
+
|
|
296
|
+
if not ctx:
|
|
297
|
+
return {}
|
|
298
|
+
|
|
299
|
+
# let's do some cleanup on the way schedule passes params; it includes in the value the param=
|
|
300
|
+
# we need to take this out if it exists..
|
|
301
|
+
action_label = action_label.replace("action_label=", "")
|
|
302
|
+
agent_id = agent_id.replace("agent_id=", "")
|
|
303
|
+
|
|
304
|
+
endpoint = f"http://{host}:{port}/walker/pulse"
|
|
305
|
+
|
|
306
|
+
if AgentInterface.TOKEN:
|
|
307
|
+
|
|
308
|
+
try:
|
|
309
|
+
headers = {}
|
|
310
|
+
json = {"action_label": action_label, "agent_id": agent_id}
|
|
311
|
+
headers["Authorization"] = "Bearer " + AgentInterface.TOKEN
|
|
312
|
+
|
|
313
|
+
# call interact
|
|
314
|
+
response = requests.post(endpoint, json=json, headers=headers)
|
|
315
|
+
|
|
316
|
+
if response.status_code == 200:
|
|
317
|
+
result = response.json()
|
|
318
|
+
return result.get("reports", {})
|
|
319
|
+
|
|
320
|
+
if response.status_code == 401:
|
|
321
|
+
AgentInterface.EXPIRATION = ""
|
|
322
|
+
return {}
|
|
323
|
+
|
|
324
|
+
except Exception as e:
|
|
325
|
+
AgentInterface.EXPIRATION = ""
|
|
326
|
+
AgentInterface.LOGGER.error(
|
|
327
|
+
f"an exception occurred: {e}, {traceback.format_exc()}"
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
return {}
|
|
331
|
+
|
|
332
|
+
@staticmethod
|
|
333
|
+
def api_interact(payload: InteractPayload) -> dict:
|
|
334
|
+
"""Interact with the agent using API"""
|
|
335
|
+
|
|
336
|
+
host = AgentInterface.HOST
|
|
337
|
+
port = AgentInterface.PORT
|
|
338
|
+
ctx = AgentInterface.get_user_context()
|
|
339
|
+
session_id = payload.session_id if payload.session_id else ""
|
|
340
|
+
|
|
341
|
+
if not ctx:
|
|
342
|
+
return {}
|
|
343
|
+
|
|
344
|
+
endpoint = f"http://{host}:{port}/walker/interact"
|
|
345
|
+
|
|
346
|
+
if ctx["token"]:
|
|
347
|
+
|
|
348
|
+
try:
|
|
349
|
+
headers = {}
|
|
350
|
+
json = {
|
|
351
|
+
"agent_id": payload.agent_id,
|
|
352
|
+
"utterance": payload.utterance,
|
|
353
|
+
"session_id": session_id,
|
|
354
|
+
}
|
|
355
|
+
headers["Authorization"] = "Bearer " + AgentInterface.TOKEN
|
|
356
|
+
|
|
357
|
+
# call interact
|
|
358
|
+
response = requests.post(endpoint, json=json, headers=headers)
|
|
359
|
+
|
|
360
|
+
if response.status_code == 200:
|
|
361
|
+
result = response.json()
|
|
362
|
+
return result["reports"]
|
|
363
|
+
|
|
364
|
+
if response.status_code == 401:
|
|
365
|
+
AgentInterface.EXPIRATION = ""
|
|
366
|
+
return {}
|
|
367
|
+
|
|
368
|
+
except Exception as e:
|
|
369
|
+
AgentInterface.EXPIRATION = ""
|
|
370
|
+
AgentInterface.LOGGER.error(
|
|
371
|
+
f"an exception occurred: {e}, {traceback.format_exc()}"
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
return {}
|
|
375
|
+
|
|
376
|
+
@staticmethod
|
|
377
|
+
def load_context(entry: NodeAnchor | None = None) -> Optional[ExecutionContext]:
|
|
378
|
+
"""Load the execution context synchronously."""
|
|
379
|
+
return AgentInterface.get_jaseci_context(entry, AgentInterface.ROOT_ID)
|
|
380
|
+
|
|
381
|
+
@staticmethod
|
|
382
|
+
async def load_context_async(
|
|
383
|
+
entry: NodeAnchor | None = None,
|
|
384
|
+
) -> Optional[ExecutionContext]:
|
|
385
|
+
"""Load the execution context asynchronously."""
|
|
386
|
+
ctx = await AgentInterface.get_user_context_async()
|
|
387
|
+
if ctx:
|
|
388
|
+
AgentInterface.ROOT_ID = ctx["root_id"]
|
|
389
|
+
AgentInterface.TOKEN = ctx["token"]
|
|
390
|
+
AgentInterface.EXPIRATION = ctx["expiration"]
|
|
391
|
+
return AgentInterface.get_jaseci_context(entry, AgentInterface.ROOT_ID)
|
|
392
|
+
|
|
393
|
+
@staticmethod
|
|
394
|
+
def get_jaseci_context(entry: NodeAnchor | None, root_id: str) -> ExecutionContext:
|
|
395
|
+
"""Build the execution context for the agent."""
|
|
396
|
+
|
|
397
|
+
try:
|
|
398
|
+
ctx = JaseciContext()
|
|
399
|
+
ctx.base = ExecutionContext.get()
|
|
400
|
+
except Exception as e:
|
|
401
|
+
AgentInterface.LOGGER.error(
|
|
402
|
+
f"an exception occurred: {e}, {traceback.format_exc()}"
|
|
403
|
+
)
|
|
404
|
+
return None
|
|
405
|
+
|
|
406
|
+
ctx.mem = MongoDB()
|
|
407
|
+
ctx.reports = []
|
|
408
|
+
ctx.status = 200
|
|
409
|
+
|
|
410
|
+
# load the user root graph
|
|
411
|
+
user_root = NodeAnchor.ref(f"n:root:{root_id}")
|
|
412
|
+
|
|
413
|
+
if not isinstance(system_root := ctx.mem.find_by_id(SUPER_ROOT), NodeAnchor):
|
|
414
|
+
system_root = NodeAnchor(
|
|
415
|
+
architype=object.__new__(Root),
|
|
416
|
+
id=SUPER_ROOT_ID,
|
|
417
|
+
access=Permission(),
|
|
418
|
+
state=AnchorState(connected=True),
|
|
419
|
+
persistent=True,
|
|
420
|
+
edges=[],
|
|
421
|
+
)
|
|
422
|
+
system_root.architype.__jac__ = system_root
|
|
423
|
+
NodeAnchor.Collection.insert_one(system_root.serialize())
|
|
424
|
+
system_root.sync_hash()
|
|
425
|
+
ctx.mem.set(system_root.id, system_root)
|
|
426
|
+
|
|
427
|
+
ctx.system_root = system_root
|
|
428
|
+
ctx.root = user_root if user_root else system_root
|
|
429
|
+
ctx.entry_node = entry if entry else ctx.root
|
|
430
|
+
|
|
431
|
+
if _ctx := JASECI_CONTEXT.get(None):
|
|
432
|
+
_ctx.close()
|
|
433
|
+
JASECI_CONTEXT.set(ctx)
|
|
434
|
+
|
|
435
|
+
return ctx
|
|
436
|
+
|
|
437
|
+
@staticmethod
|
|
438
|
+
def get_user_context() -> Optional[dict]:
|
|
439
|
+
"""Set graph context for JIVAS if user is not logged in; attempt registration if login fails."""
|
|
440
|
+
ctx: dict = {}
|
|
441
|
+
host = AgentInterface.HOST
|
|
442
|
+
port = AgentInterface.PORT
|
|
443
|
+
|
|
444
|
+
# if user context still active, return it
|
|
445
|
+
now = int(time.time())
|
|
446
|
+
if (
|
|
447
|
+
AgentInterface.EXPIRATION
|
|
448
|
+
and AgentInterface.EXPIRATION.isdigit()
|
|
449
|
+
and int(AgentInterface.EXPIRATION) > now
|
|
450
|
+
):
|
|
451
|
+
return {
|
|
452
|
+
"root_id": AgentInterface.ROOT_ID,
|
|
453
|
+
"token": AgentInterface.TOKEN,
|
|
454
|
+
"expiration": AgentInterface.EXPIRATION,
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
user = os.environ.get("JIVAS_USER")
|
|
458
|
+
password = os.environ.get("JIVAS_PASSWORD")
|
|
459
|
+
if not user or not password:
|
|
460
|
+
AgentInterface.LOGGER.error(
|
|
461
|
+
"JIVAS_USER and or JIVAS_PASSWORD environment variable is not set."
|
|
462
|
+
)
|
|
463
|
+
return ctx
|
|
464
|
+
|
|
465
|
+
login_url = f"http://{host}:{port}/user/login"
|
|
466
|
+
register_url = f"http://{host}:{port}/user/register"
|
|
467
|
+
|
|
468
|
+
try:
|
|
469
|
+
# Attempt to log in
|
|
470
|
+
response = requests.post(
|
|
471
|
+
login_url, json={"email": user, "password": password}
|
|
472
|
+
)
|
|
473
|
+
|
|
474
|
+
if response.status_code == 200:
|
|
475
|
+
# Login successful, set the ROOT_ID
|
|
476
|
+
ctx["root_id"] = AgentInterface.ROOT_ID = response.json()["user"][
|
|
477
|
+
"root_id"
|
|
478
|
+
]
|
|
479
|
+
ctx["token"] = AgentInterface.TOKEN = response.json()["token"]
|
|
480
|
+
ctx["expiration"] = AgentInterface.EXPIRATION = response.json()["user"][
|
|
481
|
+
"expiration"
|
|
482
|
+
]
|
|
483
|
+
|
|
484
|
+
else:
|
|
485
|
+
AgentInterface.LOGGER.info(
|
|
486
|
+
f"Login failed with status code {response.status_code}, attempting registration..."
|
|
487
|
+
)
|
|
488
|
+
|
|
489
|
+
# Attempt to register the user
|
|
490
|
+
register_response = requests.post(
|
|
491
|
+
register_url, json={"email": user, "password": password}
|
|
492
|
+
)
|
|
493
|
+
|
|
494
|
+
if register_response.status_code == 201:
|
|
495
|
+
# Registration successful, now log in again
|
|
496
|
+
AgentInterface.LOGGER.info(
|
|
497
|
+
f"Registration successful for user {user}, attempting login again..."
|
|
498
|
+
)
|
|
499
|
+
|
|
500
|
+
# Re-attempt login after successful registration
|
|
501
|
+
login_response = requests.post(
|
|
502
|
+
login_url, json={"email": user, "password": password}
|
|
503
|
+
)
|
|
504
|
+
|
|
505
|
+
if login_response.status_code == 200:
|
|
506
|
+
AgentInterface.LOGGER.info(
|
|
507
|
+
f"Login successful after registration, ROOT_ID ({ctx['root_id']}) set for user {user}."
|
|
508
|
+
)
|
|
509
|
+
else:
|
|
510
|
+
AgentInterface.LOGGER.error(
|
|
511
|
+
f"Login failed after registration with status code {login_response.status_code}."
|
|
512
|
+
)
|
|
513
|
+
else:
|
|
514
|
+
AgentInterface.LOGGER.error(
|
|
515
|
+
f"Registration failed with status code {register_response.status_code}."
|
|
516
|
+
)
|
|
517
|
+
|
|
518
|
+
except Exception as e:
|
|
519
|
+
AgentInterface.EXPIRATION = ""
|
|
520
|
+
AgentInterface.LOGGER.error(
|
|
521
|
+
f"an exception occurred: {e}, {traceback.format_exc()}"
|
|
522
|
+
)
|
|
523
|
+
|
|
524
|
+
return ctx
|
|
525
|
+
|
|
526
|
+
@staticmethod
|
|
527
|
+
async def get_user_context_async() -> Optional[dict]:
|
|
528
|
+
"""Set graph context for JIVAS if user is not logged in; attempt registration if login fails."""
|
|
529
|
+
ctx: dict = {}
|
|
530
|
+
host = AgentInterface.HOST
|
|
531
|
+
port = AgentInterface.PORT
|
|
532
|
+
|
|
533
|
+
# if user context still active, return it
|
|
534
|
+
now = int(time.time())
|
|
535
|
+
if (
|
|
536
|
+
AgentInterface.EXPIRATION
|
|
537
|
+
and AgentInterface.EXPIRATION.isdigit()
|
|
538
|
+
and int(AgentInterface.EXPIRATION) > now
|
|
539
|
+
):
|
|
540
|
+
return {
|
|
541
|
+
"root_id": AgentInterface.ROOT_ID,
|
|
542
|
+
"token": AgentInterface.TOKEN,
|
|
543
|
+
"expiration": AgentInterface.EXPIRATION,
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
user = os.environ.get("JIVAS_USER")
|
|
547
|
+
password = os.environ.get("JIVAS_PASSWORD")
|
|
548
|
+
if not user or not password:
|
|
549
|
+
AgentInterface.LOGGER.error(
|
|
550
|
+
"JIVAS_USER and or JIVAS_PASSWORD environment variable is not set."
|
|
551
|
+
)
|
|
552
|
+
return ctx
|
|
553
|
+
|
|
554
|
+
login_url = f"http://{host}:{port}/user/login"
|
|
555
|
+
register_url = f"http://{host}:{port}/user/register"
|
|
556
|
+
|
|
557
|
+
async with aiohttp.ClientSession() as session:
|
|
558
|
+
try:
|
|
559
|
+
# Attempt to log in
|
|
560
|
+
async with session.post(
|
|
561
|
+
login_url, json={"email": user, "password": password}
|
|
562
|
+
) as response:
|
|
563
|
+
if response.status == 200:
|
|
564
|
+
# Login successful, set the ROOT_ID
|
|
565
|
+
data = await response.json()
|
|
566
|
+
ctx["root_id"] = AgentInterface.ROOT_ID = data["user"][
|
|
567
|
+
"root_id"
|
|
568
|
+
]
|
|
569
|
+
ctx["token"] = AgentInterface.TOKEN = data["token"]
|
|
570
|
+
ctx["expiration"] = AgentInterface.EXPIRATION = data["user"][
|
|
571
|
+
"expiration"
|
|
572
|
+
]
|
|
573
|
+
else:
|
|
574
|
+
AgentInterface.LOGGER.info(
|
|
575
|
+
f"Login failed with status code {response.status}, attempting registration..."
|
|
576
|
+
)
|
|
577
|
+
|
|
578
|
+
# Attempt to register the user
|
|
579
|
+
async with session.post(
|
|
580
|
+
register_url, json={"email": user, "password": password}
|
|
581
|
+
) as register_response:
|
|
582
|
+
if register_response.status == 201:
|
|
583
|
+
AgentInterface.LOGGER.info(
|
|
584
|
+
f"Registration successful for user {user}, attempting login again..."
|
|
585
|
+
)
|
|
586
|
+
|
|
587
|
+
# Re-attempt login after successful registration
|
|
588
|
+
async with session.post(
|
|
589
|
+
login_url,
|
|
590
|
+
json={"email": user, "password": password},
|
|
591
|
+
) as login_response:
|
|
592
|
+
if login_response.status == 200:
|
|
593
|
+
data = await login_response.json()
|
|
594
|
+
root_id = data["user"]["root_id"]
|
|
595
|
+
ctx["root_id"] = root_id
|
|
596
|
+
ctx["token"] = data["token"]
|
|
597
|
+
ctx["expiration"] = data["user"]["expiration"]
|
|
598
|
+
AgentInterface.LOGGER.info(
|
|
599
|
+
f"Login successful after registration, ROOT_ID ({ctx['root_id']}) set for user {user}."
|
|
600
|
+
)
|
|
601
|
+
else:
|
|
602
|
+
AgentInterface.LOGGER.error(
|
|
603
|
+
f"Login failed after registration with status code {login_response.status}."
|
|
604
|
+
)
|
|
605
|
+
else:
|
|
606
|
+
AgentInterface.LOGGER.error(
|
|
607
|
+
f"Registration failed with status code {register_response.status}."
|
|
608
|
+
)
|
|
609
|
+
|
|
610
|
+
except Exception as e:
|
|
611
|
+
AgentInterface.EXPIRATION = ""
|
|
612
|
+
AgentInterface.LOGGER.error(
|
|
613
|
+
f"an exception occurred: {e}, {traceback.format_exc()}"
|
|
614
|
+
)
|
|
615
|
+
|
|
616
|
+
return ctx
|
|
617
|
+
|
|
618
|
+
@staticmethod
|
|
619
|
+
def generate_cipher_alphabet() -> tuple[str, str]:
|
|
620
|
+
"""Generate a cipher alphabet for encryption."""
|
|
621
|
+
# TODO: make this more secure
|
|
622
|
+
secret_key = os.environ.get("JIVAS_WEBHOOK_SECRET_KEY", "ABCDEFGHIJK")
|
|
623
|
+
secret_key = secret_key.lower() + secret_key.upper()
|
|
624
|
+
seen = set()
|
|
625
|
+
key_unique = "".join(
|
|
626
|
+
seen.add(c) or c for c in secret_key if c not in seen and c.isalpha() # type: ignore
|
|
627
|
+
)
|
|
628
|
+
remaining = "".join(
|
|
629
|
+
c
|
|
630
|
+
for c in string.ascii_lowercase + string.ascii_uppercase
|
|
631
|
+
if c not in seen and c.isalpha()
|
|
632
|
+
)
|
|
633
|
+
return key_unique, remaining
|
|
634
|
+
|
|
635
|
+
@staticmethod
|
|
636
|
+
def encrypt_webhook_key(agent_id: str, module_root: str, walker: str) -> str:
|
|
637
|
+
"""Encrypt the webhook key."""
|
|
638
|
+
lower_cipher_alphabet, upper_cipher_alphabet = (
|
|
639
|
+
AgentInterface.generate_cipher_alphabet()
|
|
640
|
+
)
|
|
641
|
+
table = str.maketrans(
|
|
642
|
+
string.ascii_lowercase + string.ascii_uppercase,
|
|
643
|
+
lower_cipher_alphabet + upper_cipher_alphabet,
|
|
644
|
+
)
|
|
645
|
+
key_text = json.dumps(
|
|
646
|
+
{"agent_id": agent_id, "module_root": module_root, "walker": walker},
|
|
647
|
+
separators=(",", ":"),
|
|
648
|
+
)
|
|
649
|
+
|
|
650
|
+
# Translate using the cipher alphabet
|
|
651
|
+
encoded_text = key_text.translate(table)
|
|
652
|
+
|
|
653
|
+
# URL encode the translated output
|
|
654
|
+
return quote(encoded_text)
|
|
655
|
+
|
|
656
|
+
@staticmethod
|
|
657
|
+
def decrypt_webhook_key(key: str) -> Optional[dict]:
|
|
658
|
+
"""Decrypt the webhook key."""
|
|
659
|
+
lower_cipher_alphabet, upper_cipher_alphabet = (
|
|
660
|
+
AgentInterface.generate_cipher_alphabet()
|
|
661
|
+
)
|
|
662
|
+
table = str.maketrans(
|
|
663
|
+
lower_cipher_alphabet + upper_cipher_alphabet,
|
|
664
|
+
string.ascii_lowercase + string.ascii_uppercase,
|
|
665
|
+
)
|
|
666
|
+
|
|
667
|
+
# Decode the URL-encoded string
|
|
668
|
+
decoded_text = unquote(key)
|
|
669
|
+
|
|
670
|
+
# Translate back using the cipher alphabet
|
|
671
|
+
key_text = decoded_text.translate(table)
|
|
672
|
+
|
|
673
|
+
# Convert the JSON string back to a dictionary
|
|
674
|
+
try:
|
|
675
|
+
return json.loads(key_text)
|
|
676
|
+
except Exception as e:
|
|
677
|
+
AgentInterface.LOGGER.error(
|
|
678
|
+
f"an exception occurred: {e}, {traceback.format_exc()}"
|
|
679
|
+
)
|
|
680
|
+
return {}
|