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.

@@ -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 asyncio import sleep
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
- response = _Jac.spawn_call(
554
- ctx.entry_node.architype,
555
- AgentInterface.spawn_walker(
556
- walker_name="pulse",
557
- attributes={
558
- "action_label": action_label,
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
- AgentInterface.EXPIRATION = None
567
- AgentInterface.LOGGER.error(
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
- host = AgentInterface.HOST
579
- port = AgentInterface.PORT
580
- ctx = AgentInterface.get_user_context()
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
- # let's do some cleanup on the way schedule passes params; it includes in the value the param=
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/pulse"
591
-
592
- if AgentInterface.TOKEN:
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
- login_url, json={"email": user, "password": password}
65
+ endpoint, json=payload, headers=headers, timeout=10
761
66
  )
762
-
763
67
  if response.status_code == 200:
764
- # Login successful, set the ROOT_ID
765
- ctx["root_id"] = AgentInterface.ROOT_ID = response.json()["user"][
766
- "root_id"
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
- AgentInterface.EXPIRATION = None
809
- AgentInterface.LOGGER.error(
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 ctx
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
- # if user context still active, return it
823
- now = int(time.time())
824
- if AgentInterface.EXPIRATION and AgentInterface.EXPIRATION > now:
825
- return {
826
- "root_id": AgentInterface.ROOT_ID,
827
- "token": AgentInterface.TOKEN,
828
- "expiration": AgentInterface.EXPIRATION,
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
- user = os.environ.get("JIVAS_USER")
832
- password = os.environ.get("JIVAS_PASSWORD")
833
- if not user or not password:
834
- AgentInterface.LOGGER.error(
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
- return ctx
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
- # Convert the JSON string back to a dictionary
959
- try:
960
- return json.loads(key_text)
961
- except Exception as e:
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)