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.
- jvserve/__init__.py +4 -2
- jvserve/cli.py +207 -221
- jvserve/lib/agent_interface.py +67 -871
- jvserve/lib/file_interface.py +1 -1
- jvserve/lib/jac_interface.py +222 -0
- {jvserve-2.0.16.dist-info → jvserve-2.1.1.dist-info}/METADATA +4 -3
- jvserve-2.1.1.dist-info/RECORD +13 -0
- jvserve/lib/agent_pulse.py +0 -63
- jvserve-2.0.16.dist-info/RECORD +0 -13
- {jvserve-2.0.16.dist-info → jvserve-2.1.1.dist-info}/WHEEL +0 -0
- {jvserve-2.0.16.dist-info → jvserve-2.1.1.dist-info}/entry_points.txt +0 -0
- {jvserve-2.0.16.dist-info → jvserve-2.1.1.dist-info}/licenses/LICENSE +0 -0
- {jvserve-2.0.16.dist-info → jvserve-2.1.1.dist-info}/top_level.txt +0 -0
jvserve/lib/file_interface.py
CHANGED
|
@@ -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:
|
|
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.
|
|
3
|
+
Version: 2.1.1
|
|
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
|
|
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: `
|
|
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.1.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
9
|
+
jvserve-2.1.1.dist-info/METADATA,sha256=NaMM0u6bH8WokLVjNGeuwj35kvs6yX-L20SYooPDvBA,4814
|
|
10
|
+
jvserve-2.1.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
11
|
+
jvserve-2.1.1.dist-info/entry_points.txt,sha256=HYyg1QXoLs0JRb004L300VeLOZyDLY27ynD1tnTnEN4,35
|
|
12
|
+
jvserve-2.1.1.dist-info/top_level.txt,sha256=afoCXZv-zXNBuhVIvfJGjafXKEiJl_ooy4BtgQwAG4Q,8
|
|
13
|
+
jvserve-2.1.1.dist-info/RECORD,,
|
jvserve/lib/agent_pulse.py
DELETED
|
@@ -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()
|
jvserve-2.0.16.dist-info/RECORD
DELETED
|
@@ -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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|