jvserve 2.0.16__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,901 +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
- def action_walker_exec(
204
- agent_id: Optional[str] = Form(None), # noqa: B008
205
- module_root: 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 module_root 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
- # Load execution context
235
- ctx = AgentInterface.load_context()
236
- if not ctx:
237
- AgentInterface.LOGGER.error(f"Unable to execute {walker}")
238
- return JSONResponse(
239
- status_code=500,
240
- content={"error": "Failed to load execution context"},
241
- )
242
-
243
- # Prepare attributes
244
- attributes: Dict[str, Any] = {"agent_id": agent_id}
245
-
246
- # Parse additional arguments if provided
247
- if args:
248
- try:
249
- attributes.update(json.loads(args))
250
- except json.JSONDecodeError as e:
251
- AgentInterface.LOGGER.error(f"Invalid JSON in args: {e}")
252
- return JSONResponse(
253
- status_code=400,
254
- content={"error": "Invalid JSON in arguments"},
255
- )
256
-
257
- # Process uploaded files
258
- if attachments:
259
- attributes["files"] = []
260
- for file in attachments:
261
- try:
262
- attributes["files"].append(
263
- {
264
- "name": file.filename,
265
- "type": file.content_type,
266
- "content": file.file.read(),
267
- }
268
- )
269
- except Exception as e:
270
- AgentInterface.LOGGER.error(
271
- f"Failed to process file {file.filename}: {e}"
272
- )
273
- continue # Skip problematic files or return error if critical
274
-
275
- # Execute the walker
276
- walker_response = _Jac.spawn_call(
277
- ctx.entry_node.architype,
278
- AgentInterface.spawn_walker(
279
- walker_name=walker,
280
- attributes=attributes,
281
- module_name=f"{module_root}.{walker}",
282
- ),
283
- ).response
284
- ctx.close()
285
-
286
- return walker_response
287
-
288
- except Exception as e:
289
- AgentInterface.EXPIRATION = None
290
- AgentInterface.LOGGER.error(
291
- f"Exception occurred: {str(e)}\n{traceback.format_exc()}"
292
- )
293
- return JSONResponse(
294
- status_code=500,
295
- content={"error": "Internal server error", "details": str(e)},
296
- )
297
- finally:
298
- if ctx:
299
- try:
300
- ctx.close()
301
- except Exception as e:
302
- AgentInterface.LOGGER.error(f"Error closing context: {str(e)}")
303
-
304
- class InteractPayload(BaseModel):
305
- """Payload for interacting with the agent."""
306
-
307
- agent_id: str
308
- utterance: Optional[str] = None
309
- channel: Optional[str] = None
310
- session_id: Optional[str] = None
311
- tts: Optional[bool] = None
312
- verbose: Optional[bool] = None
313
- data: Optional[list[dict]] = None
314
- streaming: Optional[bool] = None
315
-
316
- @staticmethod
317
- def interact(payload: InteractPayload, request: Request) -> dict:
318
- """Interact with the agent."""
319
- response = None
320
- ctx = AgentInterface.load_context()
321
- session_id = payload.session_id if payload.session_id else ""
322
-
323
- if not ctx:
324
- return {}
325
-
326
- AgentInterface.LOGGER.debug(
327
- f"attempting to interact with agent {payload.agent_id} with user root {ctx.root}..."
328
- )
329
-
330
- try:
331
- response = _Jac.spawn_call(
332
- ctx.entry_node.architype,
333
- AgentInterface.spawn_walker(
334
- walker_name="interact",
335
- attributes={
336
- "agent_id": payload.agent_id,
337
- "utterance": payload.utterance or "",
338
- "channel": payload.channel or "",
339
- "session_id": session_id or "",
340
- "tts": payload.tts or False,
341
- "verbose": payload.verbose or False,
342
- "data": payload.data or [],
343
- "streaming": payload.streaming or False,
344
- "reporting": False,
345
- },
346
- module_name="jivas.agent.action.interact",
347
- ),
348
- )
349
-
350
- if payload.streaming:
351
- # since streaming occurs asynchronously, we'll need to close the context for writebacks here
352
- # at this point of closure, there will be an open interaction_node without a response
353
- # our job hereafter is to stream to completion and then update and close this interaction_node with the final result
354
-
355
- ctx.close()
356
- if (
357
- response is not None
358
- and hasattr(response, "generator")
359
- and hasattr(response, "interaction_node")
360
- ):
361
-
362
- interaction_node = response.interaction_node
363
-
364
- async def generate(
365
- generator: Iterator, request: Request
366
- ) -> AsyncGenerator[str, None]:
367
- """
368
- Asynchronously yield data chunks from a response generator in Server-Sent Events (SSE) format.
369
-
370
- Accumulates the full text content and yields each chunk as a JSON-encoded SSE message.
371
- After all chunks are processed, updates the interaction node with the complete generated text and triggers an update in the graph context.
372
-
373
- Yields:
374
- str: A JSON-encoded string representing the current chunk of data in SSE format.
375
- """
376
- full_text = ""
377
- total_tokens = 0
378
-
379
- try:
380
- for chunk in generator:
381
- full_text += chunk.content
382
- total_tokens += 1 # each chunk is a token, let's tally
383
- yield (
384
- "data: "
385
- + json.dumps(
386
- {
387
- "id": interaction_node.id,
388
- "content": chunk.content,
389
- "session_id": interaction_node.response.get(
390
- "session_id"
391
- ),
392
- "type": chunk.type,
393
- "metadata": chunk.response_metadata,
394
- }
395
- )
396
- + "\n\n"
397
- )
398
- await sleep(0.025)
399
- # Update the interaction node with the fully generated text
400
- actx = await AgentInterface.load_context_async()
401
- try:
402
- interaction_node.set_text_message(message=full_text)
403
- interaction_node.add_tokens(total_tokens)
404
- _Jac.spawn_call(
405
- NodeAnchor.ref(interaction_node.id).architype,
406
- AgentInterface.spawn_walker(
407
- walker_name="update_interaction",
408
- attributes={
409
- "interaction_data": interaction_node.export(),
410
- },
411
- module_name="jivas.agent.memory.update_interaction",
412
- ),
413
- )
414
- finally:
415
- if actx:
416
- actx.close()
417
-
418
- except Exception as e:
419
- AgentInterface.LOGGER.error(
420
- f"Exception in streaming generator: {e}, {traceback.format_exc()}"
421
- )
422
- except asyncio.CancelledError:
423
- AgentInterface.LOGGER.error(
424
- "Client disconnected. Aborting stream."
425
- )
426
- actx = await AgentInterface.load_context_async()
427
- try:
428
- interaction_node.set_text_message(message=full_text)
429
- interaction_node.add_tokens(total_tokens)
430
- _Jac.spawn_call(
431
- NodeAnchor.ref(interaction_node.id).architype,
432
- AgentInterface.spawn_walker(
433
- walker_name="update_interaction",
434
- attributes={
435
- "interaction_data": interaction_node.export(),
436
- },
437
- module_name="jivas.agent.memory.update_interaction",
438
- ),
439
- )
440
- finally:
441
- if actx:
442
- actx.close()
443
-
444
- return StreamingResponse(
445
- generate(response.generator, request),
446
- media_type="text/event-stream",
447
- )
448
-
449
- else:
450
- AgentInterface.LOGGER.error(
451
- "Response is None or missing required attributes for streaming."
452
- )
453
- return {}
454
-
455
- else:
456
- response = response.response
457
- ctx.close()
458
- return response if response else {}
459
-
460
- except Exception as e:
461
- AgentInterface.EXPIRATION = None
462
- AgentInterface.LOGGER.error(
463
- f"an exception occurred: {e}, {traceback.format_exc()}"
464
- )
465
- ctx.close()
466
- return {}
467
-
468
- @staticmethod
469
- def pulse(action_label: str, agent_id: str = "") -> dict:
470
- """Interact with the agent."""
471
-
472
- response = None
473
- ctx = AgentInterface.load_context()
474
-
475
- if not ctx:
476
- return {}
477
-
478
- # let's do some cleanup on the way schedule passes params; it includes in the value the param=
479
- # we need to take this out if it exists..
480
- action_label = action_label.replace("action_label=", "")
481
- agent_id = agent_id.replace("agent_id=", "")
482
-
483
- # TODO : raise error in the event agent id is invalid
484
- AgentInterface.LOGGER.debug(
485
- f"attempting to interact with agent {agent_id} with user root {ctx.root}..."
486
- )
487
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"""
488
38
  try:
489
- response = _Jac.spawn_call(
490
- ctx.entry_node.architype,
491
- AgentInterface.spawn_walker(
492
- walker_name="pulse",
493
- attributes={
494
- "action_label": action_label,
495
- "agent_id": agent_id,
496
- "reporting": True,
497
- },
498
- module_name="agent.action.pulse",
499
- ),
500
- ).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")
501
45
  except Exception as e:
502
- AgentInterface.EXPIRATION = None
503
- AgentInterface.LOGGER.error(
504
- f"an exception occurred: {e}, {traceback.format_exc()}"
505
- )
506
-
507
- ctx.close()
508
- return response if response else {}
509
-
510
- @staticmethod
511
- def api_pulse(action_label: str, agent_id: str) -> dict:
512
- """Interact with the agent pulse using API"""
46
+ self._jac.reset()
47
+ self.logger.error(f"Init error: {e}\n{traceback.format_exc()}")
513
48
 
514
- host = AgentInterface.HOST
515
- port = AgentInterface.PORT
516
- ctx = AgentInterface.get_user_context()
517
-
518
- 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")
519
53
  return {}
520
54
 
521
- # let's do some cleanup on the way schedule passes params; it includes in the value the param=
522
- # we need to take this out if it exists..
55
+ # Clean parameters
523
56
  action_label = action_label.replace("action_label=", "")
524
57
  agent_id = agent_id.replace("agent_id=", "")
525
58
 
526
- endpoint = f"http://{host}:{port}/walker/pulse"
527
-
528
- if AgentInterface.TOKEN:
529
-
530
- try:
531
- headers = {}
532
- json = {"action_label": action_label, "agent_id": agent_id}
533
- headers["Authorization"] = "Bearer " + AgentInterface.TOKEN
534
-
535
- # call interact
536
- response = requests.post(endpoint, json=json, headers=headers)
537
-
538
- if response.status_code == 200:
539
- result = response.json()
540
- return result.get("reports", {})
541
-
542
- if response.status_code == 401:
543
- AgentInterface.EXPIRATION = None
544
- return {}
545
-
546
- except Exception as e:
547
- AgentInterface.EXPIRATION = None
548
- AgentInterface.LOGGER.error(
549
- f"an exception occurred: {e}, {traceback.format_exc()}"
550
- )
551
-
552
- return {}
553
-
554
- @staticmethod
555
- def api_interact(payload: InteractPayload) -> dict:
556
- """Interact with the agent using API"""
557
-
558
- host = AgentInterface.HOST
559
- port = AgentInterface.PORT
560
- ctx = AgentInterface.get_user_context()
561
- session_id = payload.session_id if payload.session_id else ""
562
-
563
- if not ctx:
564
- return {}
565
-
566
- endpoint = f"http://{host}:{port}/walker/interact"
567
-
568
- if ctx["token"]:
569
-
570
- try:
571
- headers = {}
572
- json = {
573
- "agent_id": payload.agent_id,
574
- "utterance": payload.utterance or "",
575
- "channel": payload.channel or "",
576
- "session_id": session_id or "",
577
- "tts": payload.tts or False,
578
- "verbose": payload.verbose or False,
579
- "data": payload.data or [],
580
- "streaming": payload.streaming or False,
581
- "reporting": False,
582
- }
583
- headers["Authorization"] = "Bearer " + AgentInterface.TOKEN
584
-
585
- # call interact
586
- response = requests.post(endpoint, json=json, headers=headers)
587
-
588
- if response.status_code == 200:
589
- result = response.json()
590
- return result["reports"]
591
-
592
- if response.status_code == 401:
593
- AgentInterface.EXPIRATION = None
594
- return {}
595
-
596
- except Exception as e:
597
- AgentInterface.EXPIRATION = None
598
- AgentInterface.LOGGER.error(
599
- f"an exception occurred: {e}, {traceback.format_exc()}"
600
- )
601
-
602
- return {}
603
-
604
- @staticmethod
605
- def load_context(entry: NodeAnchor | None = None) -> Optional[ExecutionContext]:
606
- """Load the execution context synchronously."""
607
- AgentInterface.get_user_context()
608
- return AgentInterface.get_jaseci_context(entry, AgentInterface.ROOT_ID)
609
-
610
- @staticmethod
611
- async def load_context_async(
612
- entry: NodeAnchor | None = None,
613
- ) -> Optional[ExecutionContext]:
614
- """Load the execution context asynchronously."""
615
- ctx = await AgentInterface.get_user_context_async()
616
- if ctx:
617
- AgentInterface.ROOT_ID = ctx["root_id"]
618
- AgentInterface.TOKEN = ctx["token"]
619
- AgentInterface.EXPIRATION = ctx["expiration"]
620
- return AgentInterface.get_jaseci_context(entry, AgentInterface.ROOT_ID)
621
-
622
- @staticmethod
623
- def get_jaseci_context(entry: NodeAnchor | None, root_id: str) -> ExecutionContext:
624
- """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}
625
62
 
626
63
  try:
627
- ctx = JaseciContext()
628
- ctx.base = ExecutionContext.get()
629
- except Exception as e:
630
- AgentInterface.LOGGER.error(
631
- f"an exception occurred: {e}, {traceback.format_exc()}"
632
- )
633
- return None
634
-
635
- ctx.mem = MongoDB()
636
- ctx.reports = []
637
- ctx.status = 200
638
-
639
- # load the user root graph
640
- user_root = NodeAnchor.ref(f"n:root:{root_id}")
641
-
642
- if not isinstance(system_root := ctx.mem.find_by_id(SUPER_ROOT), NodeAnchor):
643
- system_root = NodeAnchor(
644
- architype=object.__new__(Root),
645
- id=SUPER_ROOT_ID,
646
- access=Permission(),
647
- state=AnchorState(connected=True),
648
- persistent=True,
649
- edges=[],
650
- )
651
- system_root.architype.__jac__ = system_root
652
- NodeAnchor.Collection.insert_one(system_root.serialize())
653
- system_root.sync_hash()
654
- ctx.mem.set(system_root.id, system_root)
655
-
656
- ctx.system_root = system_root
657
- ctx.root = user_root if user_root else system_root
658
- ctx.entry_node = entry if entry else ctx.root
659
-
660
- if _ctx := JASECI_CONTEXT.get(None):
661
- _ctx.close()
662
- JASECI_CONTEXT.set(ctx)
663
-
664
- return ctx
665
-
666
- @staticmethod
667
- def get_user_context() -> Optional[dict]:
668
- """Set graph context for JIVAS if user is not logged in; attempt registration if login fails."""
669
- ctx: dict = {}
670
- host = AgentInterface.HOST
671
- port = AgentInterface.PORT
672
-
673
- # if user context still active, return it
674
- now = int(time.time())
675
- if AgentInterface.EXPIRATION and AgentInterface.EXPIRATION > now:
676
- return {
677
- "root_id": AgentInterface.ROOT_ID,
678
- "token": AgentInterface.TOKEN,
679
- "expiration": AgentInterface.EXPIRATION,
680
- }
681
-
682
- user = os.environ.get("JIVAS_USER")
683
- password = os.environ.get("JIVAS_PASSWORD")
684
- if not user or not password:
685
- AgentInterface.LOGGER.error(
686
- "JIVAS_USER and or JIVAS_PASSWORD environment variable is not set."
687
- )
688
- return ctx
689
-
690
- login_url = f"http://{host}:{port}/user/login"
691
- register_url = f"http://{host}:{port}/user/register"
692
-
693
- try:
694
- # Attempt to log in
695
64
  response = requests.post(
696
- login_url, json={"email": user, "password": password}
65
+ endpoint, json=payload, headers=headers, timeout=10
697
66
  )
698
-
699
67
  if response.status_code == 200:
700
- # Login successful, set the ROOT_ID
701
- ctx["root_id"] = AgentInterface.ROOT_ID = response.json()["user"][
702
- "root_id"
703
- ]
704
- ctx["token"] = AgentInterface.TOKEN = response.json()["token"]
705
- ctx["expiration"] = AgentInterface.EXPIRATION = response.json()["user"][
706
- "expiration"
707
- ]
708
-
709
- else:
710
- AgentInterface.LOGGER.info(
711
- f"Login failed with status code {response.status_code}, attempting registration..."
712
- )
713
-
714
- # Attempt to register the user
715
- register_response = requests.post(
716
- register_url, json={"email": user, "password": password}
717
- )
718
-
719
- if register_response.status_code == 201:
720
- # Registration successful, now log in again
721
- AgentInterface.LOGGER.info(
722
- f"Registration successful for user {user}, attempting login again..."
723
- )
724
-
725
- # Re-attempt login after successful registration
726
- login_response = requests.post(
727
- login_url, json={"email": user, "password": password}
728
- )
729
-
730
- if login_response.status_code == 200:
731
- AgentInterface.LOGGER.info(
732
- f"Login successful after registration, ROOT_ID ({ctx['root_id']}) set for user {user}."
733
- )
734
- else:
735
- AgentInterface.LOGGER.error(
736
- f"Login failed after registration with status code {login_response.status_code}."
737
- )
738
- else:
739
- AgentInterface.LOGGER.error(
740
- f"Registration failed with status code {register_response.status_code}."
741
- )
742
-
68
+ return response.json().get("reports", {})
69
+ if response.status_code == 401:
70
+ self._jac.reset()
743
71
  except Exception as e:
744
- AgentInterface.EXPIRATION = None
745
- AgentInterface.LOGGER.error(
746
- f"an exception occurred: {e}, {traceback.format_exc()}"
747
- )
748
-
749
- return ctx
72
+ self._jac.reset()
73
+ self.logger.error(f"Pulse error: {e}\n{traceback.format_exc()}")
750
74
 
751
- @staticmethod
752
- async def get_user_context_async() -> Optional[dict]:
753
- """Set graph context for JIVAS if user is not logged in; attempt registration if login fails."""
754
- ctx: dict = {}
755
- host = AgentInterface.HOST
756
- port = AgentInterface.PORT
75
+ return {}
757
76
 
758
- # if user context still active, return it
759
- now = int(time.time())
760
- if AgentInterface.EXPIRATION and AgentInterface.EXPIRATION > now:
761
- return {
762
- "root_id": AgentInterface.ROOT_ID,
763
- "token": AgentInterface.TOKEN,
764
- "expiration": AgentInterface.EXPIRATION,
765
- }
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)
766
84
 
767
- user = os.environ.get("JIVAS_USER")
768
- password = os.environ.get("JIVAS_PASSWORD")
769
- if not user or not password:
770
- AgentInterface.LOGGER.error(
771
- "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()},
772
89
  )
773
- return ctx
774
-
775
- login_url = f"http://{host}:{port}/user/login"
776
- register_url = f"http://{host}:{port}/user/register"
777
-
778
- async with aiohttp.ClientSession() as session:
779
- try:
780
- # Attempt to log in
781
- async with session.post(
782
- login_url, json={"email": user, "password": password}
783
- ) as response:
784
- if response.status == 200:
785
- # Login successful, set the ROOT_ID
786
- data = await response.json()
787
- ctx["root_id"] = AgentInterface.ROOT_ID = data["user"][
788
- "root_id"
789
- ]
790
- ctx["token"] = AgentInterface.TOKEN = data["token"]
791
- ctx["expiration"] = AgentInterface.EXPIRATION = data["user"][
792
- "expiration"
793
- ]
794
- else:
795
- AgentInterface.LOGGER.info(
796
- f"Login failed with status code {response.status}, attempting registration..."
797
- )
798
-
799
- # Attempt to register the user
800
- async with session.post(
801
- register_url, json={"email": user, "password": password}
802
- ) as register_response:
803
- if register_response.status == 201:
804
- AgentInterface.LOGGER.info(
805
- f"Registration successful for user {user}, attempting login again..."
806
- )
807
-
808
- # Re-attempt login after successful registration
809
- async with session.post(
810
- login_url,
811
- json={"email": user, "password": password},
812
- ) as login_response:
813
- if login_response.status == 200:
814
- data = await login_response.json()
815
- root_id = data["user"]["root_id"]
816
- ctx["root_id"] = root_id
817
- ctx["token"] = data["token"]
818
- ctx["expiration"] = data["user"]["expiration"]
819
- AgentInterface.LOGGER.info(
820
- f"Login successful after registration, ROOT_ID ({ctx['root_id']}) set for user {user}."
821
- )
822
- else:
823
- AgentInterface.LOGGER.error(
824
- f"Login failed after registration with status code {login_response.status}."
825
- )
826
- else:
827
- AgentInterface.LOGGER.error(
828
- f"Registration failed with status code {register_response.status}."
829
- )
830
-
831
- except Exception as e:
832
- AgentInterface.EXPIRATION = None
833
- AgentInterface.LOGGER.error(
834
- f"an exception occurred: {e}, {traceback.format_exc()}"
835
- )
836
-
837
- return ctx
838
-
839
- @staticmethod
840
- def generate_cipher_alphabet() -> tuple[str, str]:
841
- """Generate a cipher alphabet for encryption."""
842
- # TODO: make this more secure
843
- secret_key = os.environ.get("JIVAS_WEBHOOK_SECRET_KEY", "ABCDEFGHIJK")
844
- secret_key = secret_key.lower() + secret_key.upper()
845
- seen = set()
846
- key_unique = "".join(
847
- seen.add(c) or c for c in secret_key if c not in seen and c.isalpha() # type: ignore
848
- )
849
- remaining = "".join(
850
- c
851
- for c in string.ascii_lowercase + string.ascii_uppercase
852
- if c not in seen and c.isalpha()
853
- )
854
- return key_unique, remaining
855
-
856
- @staticmethod
857
- def encrypt_webhook_key(agent_id: str, module_root: str, walker: str) -> str:
858
- """Encrypt the webhook key."""
859
- lower_cipher_alphabet, upper_cipher_alphabet = (
860
- AgentInterface.generate_cipher_alphabet()
861
- )
862
- table = str.maketrans(
863
- string.ascii_lowercase + string.ascii_uppercase,
864
- lower_cipher_alphabet + upper_cipher_alphabet,
865
- )
866
- key_text = json.dumps(
867
- {"agent_id": agent_id, "module_root": module_root, "walker": walker},
868
- separators=(",", ":"),
869
- )
870
-
871
- # Translate using the cipher alphabet
872
- encoded_text = key_text.translate(table)
873
-
874
- # URL encode the translated output
875
- return quote(encoded_text)
876
-
877
- @staticmethod
878
- def decrypt_webhook_key(key: str) -> Optional[dict]:
879
- """Decrypt the webhook key."""
880
- lower_cipher_alphabet, upper_cipher_alphabet = (
881
- AgentInterface.generate_cipher_alphabet()
882
- )
883
- table = str.maketrans(
884
- lower_cipher_alphabet + upper_cipher_alphabet,
885
- string.ascii_lowercase + string.ascii_uppercase,
886
- )
887
-
888
- # Decode the URL-encoded string
889
- decoded_text = unquote(key)
90
+ except Exception as e:
91
+ self.logger.error(f"Finalize error: {e}")
890
92
 
891
- # Translate back using the cipher alphabet
892
- key_text = decoded_text.translate(table)
893
93
 
894
- # Convert the JSON string back to a dictionary
895
- try:
896
- return json.loads(key_text)
897
- except Exception as e:
898
- AgentInterface.LOGGER.error(
899
- f"an exception occurred: {e}, {traceback.format_exc()}"
900
- )
901
- 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)