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.

@@ -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 {}