jvserve 2.0.16__py3-none-any.whl → 2.1.2__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.

@@ -78,7 +78,7 @@ class LocalFileInterface(FileInterface):
78
78
  """Get URL for accessing local file via HTTP."""
79
79
  file_path = os.path.join(self.__root_dir, filename)
80
80
  if os.path.exists(file_path):
81
- return f"{os.environ.get('JIVAS_FILES_URL', 'http://localhost:9000/files')}/{filename}"
81
+ return f"{os.environ.get('JIVAS_FILES_URL', 'http://localhost:8000/files')}/{filename}"
82
82
  return None
83
83
 
84
84
 
@@ -0,0 +1,222 @@
1
+ """JacInterface: A connection and context state provider for Jac Runtime."""
2
+
3
+ import asyncio
4
+ import logging
5
+ import os
6
+ import threading
7
+ import time
8
+ import traceback
9
+ from typing import Optional, Tuple
10
+
11
+ import requests
12
+ from fastapi import Request
13
+ from jac_cloud.core.archetype import ( # type: ignore
14
+ NodeAnchor,
15
+ WalkerArchetype,
16
+ )
17
+ from jac_cloud.core.context import (
18
+ JASECI_CONTEXT,
19
+ JaseciContext,
20
+ )
21
+ from jac_cloud.plugin.jaseci import JacPlugin
22
+ from jaclang.runtimelib.machine import JacMachine
23
+
24
+
25
+ class JacInterface:
26
+ """Thread-safe connection and context state provider for Jac Runtime with auto-authentication."""
27
+
28
+ def __init__(self, host: str = "localhost", port: int = 8000) -> None:
29
+ """Initialize JacInterface with host and port."""
30
+ self.host = host
31
+ self.port = port
32
+ self.root_id = ""
33
+ self.token = ""
34
+ self.expiration = 0.0
35
+ self._lock = threading.RLock() # Thread-safe lock
36
+ self.logger = logging.getLogger(__name__)
37
+
38
+ def update(self, root_id: str, token: str, expiration: float) -> None:
39
+ """Thread-safe state update"""
40
+ with self._lock:
41
+ self.root_id = root_id
42
+ self.token = token
43
+ self.expiration = expiration
44
+
45
+ def reset(self) -> None:
46
+ """Thread-safe state reset"""
47
+ with self._lock:
48
+ self.root_id = ""
49
+ self.token = ""
50
+ self.expiration = 0.0
51
+
52
+ def is_valid(self) -> bool:
53
+ """Thread-safe validity check"""
54
+ with self._lock:
55
+ return bool(
56
+ self.token and self.expiration and self.expiration > time.time()
57
+ )
58
+
59
+ def get_state(self) -> Tuple[str, str, Optional[float]]:
60
+ """Get current state with auto-authentication"""
61
+ if not self.is_valid():
62
+ self._authenticate()
63
+ with self._lock:
64
+ return (self.root_id, self.token, self.expiration)
65
+
66
+ def get_context(self, request: Request | None = None) -> Optional[JaseciContext]:
67
+ """Get Jaseci context with proper thread safety."""
68
+
69
+ state = self.get_state()
70
+ if not state or not self.is_valid():
71
+ self.logger.error("Failed to get valid state for Jaseci context")
72
+ return None
73
+
74
+ try:
75
+ root_id = state[0]
76
+ entry_node = NodeAnchor.ref(f"n:root:{root_id}") # type: ignore
77
+ if not entry_node:
78
+ self.logger.error("Failed to resolve entry node from root_id")
79
+ return None
80
+
81
+ ctx = JaseciContext.create(request, entry_node)
82
+ if not ctx:
83
+ self.logger.error("Failed to create JaseciContext with entry node")
84
+ return None
85
+
86
+ ctx.system_root = entry_node
87
+ ctx.root_state = entry_node
88
+
89
+ # Clean up any existing context before setting new one
90
+ existing_ctx = JASECI_CONTEXT.get(None)
91
+ if existing_ctx:
92
+ try:
93
+ existing_ctx.close()
94
+ except Exception as e:
95
+ self.logger.warning(f"Error while closing existing context: {e}")
96
+
97
+ JASECI_CONTEXT.set(ctx)
98
+ return ctx
99
+
100
+ except Exception as e:
101
+ self.logger.error(
102
+ f"Failed to create JaseciContext: {e}\n{traceback.format_exc()}"
103
+ )
104
+ return None
105
+
106
+ def spawn_walker(
107
+ self,
108
+ walker_name: str | None,
109
+ module_name: str,
110
+ attributes: dict = {}, # noqa: B006
111
+ request: Request | None = None, # noqa: B006
112
+ ) -> Optional[WalkerArchetype]:
113
+ """Spawn walker with proper context handling and thread safety"""
114
+
115
+ if not all([walker_name, module_name]):
116
+ self.logger.error("Missing required parameters for spawning walker")
117
+ return None
118
+
119
+ ctx = self.get_context(request)
120
+ if not ctx:
121
+ return None
122
+
123
+ try:
124
+ if module_name not in JacMachine.list_modules():
125
+ self.logger.error(f"Module {module_name} not loaded")
126
+ return None
127
+
128
+ entry_node = ctx.entry_node.archetype
129
+
130
+ return JacPlugin.spawn(
131
+ JacMachine.spawn_walker(walker_name, attributes, module_name),
132
+ entry_node,
133
+ )
134
+ except Exception as e:
135
+ self.logger.error(f"Error spawning walker: {e}\n{traceback.format_exc()}")
136
+ return None
137
+ finally:
138
+ if ctx:
139
+ ctx.close()
140
+ if JASECI_CONTEXT.get(None) == ctx:
141
+ JASECI_CONTEXT.set(None)
142
+
143
+ def _authenticate(self) -> None:
144
+ """Thread-safe authentication with retry logic and improved error handling"""
145
+ user = os.environ.get("JIVAS_USER")
146
+ password = os.environ.get("JIVAS_PASSWORD")
147
+ if not user or not password:
148
+ self.logger.error("Missing JIVAS_USER or JIVAS_PASSWORD")
149
+ return
150
+
151
+ login_url = f"http://{self.host}:{self.port}/user/login"
152
+ register_url = f"http://{self.host}:{self.port}/user/register"
153
+
154
+ with self._lock:
155
+ try:
156
+ # Try login first
157
+ response = requests.post(
158
+ login_url, json={"email": user, "password": password}, timeout=15
159
+ )
160
+ self.logger.info(f"Login response status: {response.status_code}")
161
+ if response.status_code == 200:
162
+ self._process_auth_response(response.json())
163
+ return
164
+
165
+ # Register if login fails
166
+ reg_response = requests.post(
167
+ register_url, json={"email": user, "password": password}, timeout=15
168
+ )
169
+ self.logger.info(
170
+ f"Register response status: {reg_response.status_code}"
171
+ )
172
+ if reg_response.status_code == 201:
173
+ # Retry login after registration
174
+ login_response = requests.post(
175
+ login_url,
176
+ json={"email": user, "password": password},
177
+ timeout=15,
178
+ )
179
+ self.logger.info(
180
+ f"Retry login response status: {login_response.status_code}"
181
+ )
182
+ if login_response.status_code == 200:
183
+ self._process_auth_response(login_response.json())
184
+ except requests.exceptions.RequestException as e:
185
+ self.logger.error(f"Network error during authentication: {e}")
186
+ except Exception as e:
187
+ self.logger.error(
188
+ f"Authentication failed: {e}\n{traceback.format_exc()}"
189
+ )
190
+ self.reset()
191
+
192
+ def _process_auth_response(self, response_data: dict) -> None:
193
+ """Process authentication response and update state"""
194
+ user_data = response_data.get("user", {})
195
+ root_id = user_data.get("root_id", "")
196
+ token = response_data.get("token", "")
197
+ expiration = user_data.get("expiration")
198
+
199
+ if not all([root_id, token, expiration]):
200
+ self.logger.error("Invalid authentication response")
201
+ return
202
+
203
+ self.update(root_id, token, expiration)
204
+
205
+ # Async versions of methods
206
+ async def _authenticate_async(self) -> None:
207
+ """Asynchronous wrapper for authentication"""
208
+ loop = asyncio.get_running_loop()
209
+ await loop.run_in_executor(None, self._authenticate)
210
+
211
+ async def spawn_walker_async(
212
+ self,
213
+ walker_name: str,
214
+ module_name: str,
215
+ attributes: dict,
216
+ request: Request | None = None,
217
+ ) -> Optional[WalkerArchetype]:
218
+ """Asynchronous wrapper for walker spawning"""
219
+ loop = asyncio.get_running_loop()
220
+ return await loop.run_in_executor(
221
+ None, self.spawn_walker, walker_name, module_name, attributes, request
222
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jvserve
3
- Version: 2.0.16
3
+ Version: 2.1.2
4
4
  Summary: FastAPI webserver for loading and interaction with JIVAS agents.
5
5
  Home-page: https://github.com/TrueSelph/jvserve
6
6
  Author: TrueSelph Inc.
@@ -9,7 +9,8 @@ Keywords: jivas
9
9
  Requires-Python: >=3.12.0
10
10
  Description-Content-Type: text/markdown
11
11
  License-File: LICENSE
12
- Requires-Dist: jac-cloud==0.1.20
12
+ Requires-Dist: jac-cloud
13
+ Requires-Dist: jaclang==0.8.4
13
14
  Requires-Dist: pyaml>=25.1.0
14
15
  Requires-Dist: requests>=2.32.3
15
16
  Requires-Dist: aiohttp>=3.10.10
@@ -80,7 +81,7 @@ jac jvfileserve ./static
80
81
  ### Supported Arguments
81
82
 
82
83
  - **filename**: Path to your JAC file.
83
- - **host**: Host address to bind the server (default: `0.0.0.0`).
84
+ - **host**: Host address to bind the server (default: `localhost`).
84
85
  - **port**: Port number to bind the server (default: `8000`).
85
86
  - **loglevel**: Logging level (default: `INFO`).
86
87
  - **workers**: Number of worker processes (optional).
@@ -0,0 +1,13 @@
1
+ jvserve/__init__.py,sha256=Jd0pamSDn2wGTZkNk8I9qNYTFBHp7rasdYO0_Dvad_k,245
2
+ jvserve/cli.py,sha256=WTB_euv8R_cQ9fK_tlFHV_I6Jn-CYJrHKzjHsWSyWPY,9304
3
+ jvserve/lib/__init__.py,sha256=cnzfSHLoTWG9Ygut2nOpDys5aPlQz-m0BSkB-nd7OMs,31
4
+ jvserve/lib/agent_interface.py,sha256=Igv5Jb7i9Aq_7IbLDZ6jnldGKssAWKeb6iXoolX8u4k,3478
5
+ jvserve/lib/file_interface.py,sha256=VO9RBCtJwaBxu5eZjc57-uRbsVXXZt86wVRVq9R3KXY,6079
6
+ jvserve/lib/jac_interface.py,sha256=ydhXfYTsrhdvMXBTAd_vnAXJSSVBydQ3qavPU1-oodU,7973
7
+ jvserve/lib/jvlogger.py,sha256=RNiB9PHuBzTvNIQWhxoDgrDlNYA0PYm1SVpvzlqu8mE,4180
8
+ jvserve-2.1.2.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
9
+ jvserve-2.1.2.dist-info/METADATA,sha256=EO9HyWpAhJym2eK2j9ZQVdFDvgVDBxYBL-fbrVVtLUw,4814
10
+ jvserve-2.1.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
11
+ jvserve-2.1.2.dist-info/entry_points.txt,sha256=HYyg1QXoLs0JRb004L300VeLOZyDLY27ynD1tnTnEN4,35
12
+ jvserve-2.1.2.dist-info/top_level.txt,sha256=afoCXZv-zXNBuhVIvfJGjafXKEiJl_ooy4BtgQwAG4Q,8
13
+ jvserve-2.1.2.dist-info/RECORD,,
@@ -1,63 +0,0 @@
1
- """Agent Pulse class for scheduling and running agent jobs."""
2
-
3
- import logging
4
- import threading
5
- import time
6
-
7
- import schedule
8
-
9
-
10
- class AgentPulse:
11
- """Agent Pulse class for scheduling and running agent jobs."""
12
-
13
- EVENT = None
14
- THREAD = None
15
- LOGGER = logging.getLogger(__name__)
16
-
17
- @staticmethod
18
- def start(interval: int = 1) -> threading.Event:
19
- """Starts the agent pulse in a separate thread that executes
20
- pending jobs at each elapsed time interval.
21
-
22
- This method ensures that only one thread is running at a time
23
- to prevent duplication. If a thread is already running, it logs
24
- a message and returns without starting a new thread.
25
-
26
- @param interval: Time in seconds between each execution cycle of
27
- scheduled jobs.
28
- @return: threading.Event which can be set to stop the running
29
- thread.
30
-
31
- Note: It is intended behavior that run_continuously() does not
32
- run missed jobs. For instance, a job scheduled to run every
33
- minute with a run interval of one hour will only run once per
34
- hour, not 60 times at once.
35
- """
36
-
37
- if AgentPulse.THREAD and AgentPulse.THREAD.is_alive():
38
- AgentPulse.LOGGER.info("agent pulse is already running.")
39
- return AgentPulse.EVENT
40
-
41
- AgentPulse.EVENT = threading.Event()
42
-
43
- class ScheduleThread(threading.Thread):
44
- def run(self) -> None:
45
- while AgentPulse.EVENT and not AgentPulse.EVENT.is_set():
46
- schedule.run_pending()
47
- time.sleep(interval)
48
-
49
- AgentPulse.THREAD = ScheduleThread()
50
- AgentPulse.THREAD.start()
51
-
52
- AgentPulse.LOGGER.info("agent pulse started.")
53
-
54
- return AgentPulse.EVENT
55
-
56
- @staticmethod
57
- def stop() -> None:
58
- """Stops the agent pulse."""
59
- if AgentPulse.EVENT and not AgentPulse.EVENT.is_set():
60
- AgentPulse.LOGGER.info("agent pulse stopped.")
61
- AgentPulse.EVENT.set()
62
- if AgentPulse.THREAD:
63
- AgentPulse.THREAD.join()
@@ -1,13 +0,0 @@
1
- jvserve/__init__.py,sha256=pq6aKUL15KaEDAIo2zlj2RtTFVdN-vXUx3UVdOZxOeM,191
2
- jvserve/cli.py,sha256=NJD4ehtKnhOvsz-vrFh9lz3P4bnxqz-uqTr8QItljZc,8897
3
- jvserve/lib/__init__.py,sha256=cnzfSHLoTWG9Ygut2nOpDys5aPlQz-m0BSkB-nd7OMs,31
4
- jvserve/lib/agent_interface.py,sha256=-uewEL_M4cZpZVl0rIOXnVpZI-3O9aGKLljoCgKSW0M,34955
5
- jvserve/lib/agent_pulse.py,sha256=6hBF6KQYr6Z9Mi_yoWKGfdnW7gg84kK20Slu-bLR_m8,2067
6
- jvserve/lib/file_interface.py,sha256=sqwTmBnZsVFQUGt7mE1EWA-jHnZlRtBAFeb4Rb_QgQ8,6079
7
- jvserve/lib/jvlogger.py,sha256=RNiB9PHuBzTvNIQWhxoDgrDlNYA0PYm1SVpvzlqu8mE,4180
8
- jvserve-2.0.16.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
9
- jvserve-2.0.16.dist-info/METADATA,sha256=kRy2uTRK7zZBau0hAOt5BunMHO0m2OHHQPUzY9BGUOY,4791
10
- jvserve-2.0.16.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
11
- jvserve-2.0.16.dist-info/entry_points.txt,sha256=HYyg1QXoLs0JRb004L300VeLOZyDLY27ynD1tnTnEN4,35
12
- jvserve-2.0.16.dist-info/top_level.txt,sha256=afoCXZv-zXNBuhVIvfJGjafXKEiJl_ooy4BtgQwAG4Q,8
13
- jvserve-2.0.16.dist-info/RECORD,,