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.

jvserve/__init__.py CHANGED
@@ -4,5 +4,7 @@ jvserve package initialization.
4
4
  This package provides the webserver for loading and interacting with JIVAS agents.
5
5
  """
6
6
 
7
- __version__ = "2.0.15"
8
- __supported__jivas__versions__ = ["2.0.0"]
7
+ from importlib.metadata import version
8
+
9
+ __version__ = version("jvserve")
10
+ __supported__jivas__versions__ = [__version__]
jvserve/cli.py CHANGED
@@ -1,23 +1,29 @@
1
1
  """Module for registering CLI plugins for jaseci."""
2
2
 
3
+ import asyncio
3
4
  import logging
4
5
  import os
5
- import time
6
+ from concurrent.futures import ThreadPoolExecutor
6
7
  from contextlib import asynccontextmanager
8
+ from pickle import load
7
9
  from typing import AsyncIterator, Optional
8
10
 
11
+ import aiohttp
12
+ import pymongo
13
+ from bson import ObjectId
9
14
  from dotenv import load_dotenv
10
- from fastapi.responses import FileResponse, Response, StreamingResponse
11
- from jac_cloud.jaseci.security import authenticator
15
+ from fastapi import FastAPI, HTTPException, Response
16
+ from fastapi.middleware.cors import CORSMiddleware
17
+ from fastapi.responses import FileResponse, StreamingResponse
18
+ from jac_cloud.core.context import JaseciContext
19
+ from jac_cloud.jaseci.main import FastAPI as JaseciFastAPI # type: ignore
12
20
  from jac_cloud.plugin.jaseci import NodeAnchor
21
+ from jaclang import JacMachine as Jac
13
22
  from jaclang.cli.cmdreg import cmd_registry
14
- from jaclang.plugin.default import hookimpl
15
- from jaclang.runtimelib.context import ExecutionContext
16
- from jaclang.runtimelib.machine import JacMachine
17
- from uvicorn import run as _run
23
+ from jaclang.runtimelib.machine import hookimpl
24
+ from watchfiles import Change, run_process
18
25
 
19
26
  from jvserve.lib.agent_interface import AgentInterface
20
- from jvserve.lib.agent_pulse import AgentPulse
21
27
  from jvserve.lib.file_interface import (
22
28
  DEFAULT_FILES_ROOT,
23
29
  FILE_INTERFACE,
@@ -25,59 +31,207 @@ from jvserve.lib.file_interface import (
25
31
  )
26
32
  from jvserve.lib.jvlogger import JVLogger
27
33
 
34
+ # quiet the jac_cloud logger down to errors only
35
+ # jac cloud dumps payload details to console which makes it hard to debug in JIVAS
36
+ os.environ["LOGGER_LEVEL"] = "ERROR"
28
37
  load_dotenv(".env")
29
-
30
-
31
- def serve_proxied_file(file_path: str) -> FileResponse | StreamingResponse:
32
- """Serve a proxied file from a remote or local URL."""
33
- import mimetypes
34
-
35
- import requests
36
- from fastapi import HTTPException
37
-
38
+ # Set up logging
39
+ JVLogger.setup_logging(level="INFO")
40
+ logger = logging.getLogger(__name__)
41
+
42
+ # Global for MongoDB collection with thread-safe initialization
43
+ url_proxy_collection = None
44
+ collection_init_lock = asyncio.Lock()
45
+
46
+
47
+ async def get_url_proxy_collection() -> pymongo.collection.Collection:
48
+ """Thread-safe initialization of MongoDB collection"""
49
+ global url_proxy_collection
50
+ if url_proxy_collection is None:
51
+ async with collection_init_lock:
52
+ if url_proxy_collection is None: # Double-check locking
53
+ loop = asyncio.get_running_loop()
54
+ with ThreadPoolExecutor() as pool:
55
+ url_proxy_collection = await loop.run_in_executor(
56
+ pool,
57
+ lambda: NodeAnchor.Collection.get_collection("url_proxies"),
58
+ )
59
+ return url_proxy_collection
60
+
61
+
62
+ async def serve_proxied_file(
63
+ file_path: str,
64
+ ) -> FileResponse | StreamingResponse | Response:
65
+ """Serve a proxied file from a remote or local URL (async version)"""
38
66
  if FILE_INTERFACE == "local":
39
- return FileResponse(path=os.path.join(DEFAULT_FILES_ROOT, file_path))
67
+ root_path = os.environ.get("JIVAS_FILES_ROOT_PATH", DEFAULT_FILES_ROOT)
68
+ full_path = os.path.join(root_path, file_path)
69
+ if not os.path.exists(full_path):
70
+ raise HTTPException(status_code=404, detail="File not found")
71
+ return FileResponse(full_path)
40
72
 
41
73
  file_url = file_interface.get_file_url(file_path)
42
74
 
75
+ # Security check to prevent recursive calls
43
76
  if file_url and ("localhost" in file_url or "127.0.0.1" in file_url):
44
- # prevent recusive calls when env vars are not detected
45
- raise HTTPException(status_code=500, detail="Environment not set up correctly")
77
+ raise HTTPException(
78
+ status_code=500, detail="Environment misconfiguration detected"
79
+ )
46
80
 
47
81
  if not file_url:
48
82
  raise HTTPException(status_code=404, detail="File not found")
49
83
 
50
- file_extension = os.path.splitext(file_path)[1].lower()
84
+ try:
85
+ async with aiohttp.ClientSession() as session:
86
+ async with session.get(file_url) as response:
87
+ response.raise_for_status()
88
+
89
+ return StreamingResponse(
90
+ response.content.iter_chunked(8192),
91
+ media_type=response.headers.get(
92
+ "Content-Type", "application/octet-stream"
93
+ ),
94
+ )
95
+ except aiohttp.ClientError as e:
96
+ raise HTTPException(status_code=502, detail=f"File fetch error: {str(e)}")
97
+
98
+
99
+ def run_jivas(filename: str, host: str = "localhost", port: int = 8000) -> None:
100
+ """Starts JIVAS server with integrated file services"""
101
+
102
+ # Create agent interface instance with configuration
103
+ agent_interface = AgentInterface.get_instance(host=host, port=port)
104
+
105
+ base, mod = os.path.split(filename)
106
+ base = base if base else "./"
107
+ mod = mod[:-4]
108
+
109
+ JaseciFastAPI.enable()
110
+
111
+ ctx = JaseciContext.create(None)
112
+ if filename.endswith(".jac"):
113
+ Jac.jac_import(target=mod, base_path=base, override_name="__main__")
114
+ elif filename.endswith(".jir"):
115
+ with open(filename, "rb") as f:
116
+ Jac.attach_program(load(f))
117
+ Jac.jac_import(target=mod, base_path=base, override_name="__main__")
118
+ else:
119
+ raise ValueError("Not a valid file!\nOnly supports `.jac` and `.jir`")
120
+
121
+ # Define post-startup function to run AFTER server is ready
122
+ async def post_startup() -> None:
123
+ """Wait for server to be ready before initializing agents"""
124
+ health_url = f"http://{host}:{port}/healthz"
125
+ max_retries = 10
126
+ retry_delay = 1.0
127
+
128
+ for attempt in range(max_retries):
129
+ try:
130
+ async with aiohttp.ClientSession() as session:
131
+ async with session.get(health_url, timeout=1) as response:
132
+ if response.status == 200:
133
+ logger.info("Server is ready, initializing agents...")
134
+ await agent_interface.init_agents()
135
+ return
136
+ except (aiohttp.ClientConnectorError, asyncio.TimeoutError) as e:
137
+ logger.warning(
138
+ f"Server not ready yet (attempt {attempt + 1} / {max_retries}): {e}"
139
+ )
140
+ await asyncio.sleep(retry_delay)
141
+ retry_delay *= 1.5 # Exponential backoff
142
+
143
+ logger.error(
144
+ "Server did not become ready in time. Agent initialization skipped."
145
+ )
146
+
147
+ # set up lifespan events
148
+ async def on_startup() -> None:
149
+ logger.info("JIVAS is starting up...")
150
+ # Start initialization in background without blocking
151
+ asyncio.create_task(post_startup())
152
+
153
+ async def on_shutdown() -> None:
154
+ logger.info("JIVAS is shutting down...")
155
+
156
+ app = JaseciFastAPI.get()
157
+ app_lifespan = app.router.lifespan_context
158
+
159
+ @asynccontextmanager
160
+ async def lifespan_wrapper(app: FastAPI) -> AsyncIterator[Optional[str]]:
161
+ await on_startup()
162
+ async with app_lifespan(app) as maybe_state:
163
+ yield maybe_state
164
+ await on_shutdown()
165
+
166
+ app.router.lifespan_context = lifespan_wrapper
167
+
168
+ # Add CORS middleware to main app
169
+ app.add_middleware(
170
+ CORSMiddleware,
171
+ allow_origins=["*"],
172
+ allow_credentials=True,
173
+ allow_methods=["*"],
174
+ allow_headers=["*"],
175
+ )
176
+
177
+ # Ensure the local file directory exists if that's the interface
178
+ if FILE_INTERFACE == "local":
179
+ directory = os.environ.get("JIVAS_FILES_ROOT_PATH", DEFAULT_FILES_ROOT)
180
+ if not os.path.exists(directory):
181
+ os.makedirs(directory, exist_ok=True)
182
+
183
+ # Setup file serving endpoint for both local and S3
184
+ @app.get("/files/{file_path:path}", response_model=None)
185
+ async def serve_file(
186
+ file_path: str,
187
+ ) -> FileResponse | StreamingResponse | Response:
188
+ # The serve_proxied_file function already handles both local and S3 cases
189
+ return await serve_proxied_file(file_path)
190
+
191
+ # Setup URL proxy endpoint
192
+ @app.get("/f/{file_id:path}", response_model=None)
193
+ async def get_proxied_file(
194
+ file_id: str,
195
+ ) -> FileResponse | StreamingResponse | Response:
196
+ params = file_id.split("/")
197
+ object_id = params[0]
198
+
199
+ try:
200
+ # Get MongoDB collection (thread-safe initialization)
201
+ collection = await get_url_proxy_collection()
202
+
203
+ # Run blocking MongoDB operation in thread pool
204
+ loop = asyncio.get_running_loop()
205
+ file_details = await loop.run_in_executor(
206
+ None, lambda: collection.find_one({"_id": ObjectId(object_id)})
207
+ )
51
208
 
52
- # List of extensions to serve directly
53
- direct_serve_extensions = [
54
- ".pdf",
55
- ".html",
56
- ".txt",
57
- ".js",
58
- ".css",
59
- ".json",
60
- ".xml",
61
- ".svg",
62
- ".csv",
63
- ".ico",
64
- ]
209
+ descriptor_path = os.environ.get("JIVAS_DESCRIPTOR_ROOT_PATH")
65
210
 
66
- if file_extension in direct_serve_extensions:
67
- file_response = requests.get(file_url)
68
- file_response.raise_for_status() # Raise HTTPError for bad responses (4XX or 5XX)
211
+ if file_details:
212
+ if descriptor_path and descriptor_path in file_details["path"]:
213
+ return Response(status_code=403)
214
+ return await serve_proxied_file(file_details["path"])
69
215
 
70
- mime_type = mimetypes.guess_type(file_path)[0] or "application/octet-stream"
216
+ raise HTTPException(status_code=404, detail="File not found")
217
+ except Exception as e:
218
+ logger.error(f"Proxy error: {str(e)}")
219
+ raise HTTPException(status_code=500, detail="Internal server error")
71
220
 
72
- return StreamingResponse(iter([file_response.content]), media_type=mime_type)
221
+ ctx.close()
222
+ # Run the app
223
+ JaseciFastAPI.start(host=host, port=port)
73
224
 
74
- file_response = requests.get(file_url, stream=True)
75
- file_response.raise_for_status()
76
225
 
77
- return StreamingResponse(
78
- file_response.iter_content(chunk_size=1024),
79
- media_type="application/octet-stream",
226
+ def log_reload(changes: set[tuple[Change, str]]) -> None:
227
+ """Log changes."""
228
+ num_of_changes = len(changes)
229
+ logger.warning(
230
+ f'Detected {num_of_changes} change{"s" if num_of_changes > 1 else ""}'
80
231
  )
232
+ for change in changes:
233
+ logger.warning(f"{change[1]} ({change[0].name})")
234
+ logger.warning("Reloading ...")
81
235
 
82
236
 
83
237
  class JacCmd:
@@ -89,182 +243,14 @@ class JacCmd:
89
243
  """Create Jac CLI cmds."""
90
244
 
91
245
  @cmd_registry.register
92
- def jvserve(
93
- filename: str,
94
- host: str = "0.0.0.0",
95
- port: int = 8000,
96
- loglevel: str = "INFO",
97
- workers: Optional[int] = None,
98
- ) -> None:
99
- """Launch the jac application."""
100
- from jaclang import jac_import
101
-
102
- # set up logging
103
- JVLogger.setup_logging(level=loglevel)
104
- logger = logging.getLogger(__name__)
105
-
106
- # load FastAPI
107
- from jac_cloud import FastAPI
108
-
109
- FastAPI.enable()
110
-
111
- # load the JAC application
112
- jctx = ExecutionContext.create()
113
-
114
- base, mod = os.path.split(filename)
115
- base = base if base else "./"
116
- mod = mod[:-4]
117
-
118
- if filename.endswith(".jac"):
119
- start_time = time.time()
120
- jac_import(
121
- target=mod,
122
- base_path=base,
123
- cachable=True,
124
- override_name="__main__",
125
- )
126
- logger.info(f"Loading took {time.time() - start_time} seconds")
127
-
128
- AgentInterface.HOST = host
129
- AgentInterface.PORT = port
130
-
131
- # set up lifespan events
132
- async def on_startup() -> None:
133
- # Perform initialization actions here
134
- logger.info("JIVAS is starting up...")
135
-
136
- async def on_shutdown() -> None:
137
- # Perform initialization actions here
138
- logger.info("JIVAS is shutting down...")
139
- AgentPulse.stop()
140
- # await AgentRTC.on_shutdown()
141
- jctx.close()
142
- JacMachine.detach()
143
-
144
- app_lifespan = FastAPI.get().router.lifespan_context
145
-
146
- @asynccontextmanager
147
- async def lifespan_wrapper(app: FastAPI) -> AsyncIterator[Optional[str]]:
148
- await on_startup()
149
- async with app_lifespan(app) as maybe_state:
150
- yield maybe_state
151
- await on_shutdown()
152
-
153
- FastAPI.get().router.lifespan_context = lifespan_wrapper
154
-
155
- # Setup custom routes
156
- FastAPI.get().add_api_route(
157
- "/interact", endpoint=AgentInterface.interact, methods=["POST"]
246
+ def jvserve(filename: str, host: str = "localhost", port: int = 8000) -> None:
247
+ """Launch unified JIVAS server with file services"""
248
+ watchdir = os.path.join(
249
+ os.path.abspath(os.path.dirname(filename)), "actions", ""
158
250
  )
159
- FastAPI.get().add_api_route(
160
- "/webhook/{key}",
161
- endpoint=AgentInterface.webhook_exec,
162
- methods=["GET", "POST"],
251
+ run_process(
252
+ watchdir,
253
+ target=run_jivas,
254
+ args=(filename, host, port),
255
+ callback=log_reload,
163
256
  )
164
- FastAPI.get().add_api_route(
165
- "/action/walker",
166
- endpoint=AgentInterface.action_walker_exec,
167
- methods=["POST"],
168
- dependencies=authenticator,
169
- )
170
-
171
- # run the app
172
- _run(FastAPI.get(), host=host, port=port, lifespan="on", workers=workers)
173
-
174
- @cmd_registry.register
175
- def jvfileserve(
176
- directory: str, host: str = "0.0.0.0", port: int = 9000
177
- ) -> None:
178
- """Launch the file server for local files."""
179
- # load FastAPI
180
- from fastapi import FastAPI
181
- from fastapi.middleware.cors import CORSMiddleware
182
- from fastapi.staticfiles import StaticFiles
183
-
184
- # Setup custom routes
185
- app = FastAPI()
186
-
187
- # Add CORS middleware
188
- app.add_middleware(
189
- CORSMiddleware,
190
- allow_origins=["*"],
191
- allow_credentials=True,
192
- allow_methods=["*"],
193
- allow_headers=["*"],
194
- )
195
-
196
- if not os.path.exists(directory):
197
- os.makedirs(directory)
198
-
199
- # Set the environment variable for the file root path
200
- os.environ["JIVAS_FILES_ROOT_PATH"] = directory
201
-
202
- # Mount the static files directory
203
- app.mount(
204
- "/files",
205
- StaticFiles(directory=directory),
206
- name="files",
207
- )
208
-
209
- # run the app
210
- _run(app, host=host, port=port)
211
-
212
- @cmd_registry.register
213
- def jvproxyserve(
214
- directory: str, host: str = "0.0.0.0", port: int = 9000
215
- ) -> None:
216
- """Launch the file proxy server for remote files."""
217
- # load FastAPI
218
- from fastapi import FastAPI
219
- from fastapi.middleware.cors import CORSMiddleware
220
-
221
- # Setup custom routes
222
- app = FastAPI()
223
-
224
- # Add CORS middleware
225
- app.add_middleware(
226
- CORSMiddleware,
227
- allow_origins=["*"],
228
- allow_credentials=True,
229
- allow_methods=["*"],
230
- allow_headers=["*"],
231
- )
232
-
233
- # Add proxy routes only if using S3
234
- if FILE_INTERFACE == "s3":
235
-
236
- @app.get("/files/{file_path:path}", response_model=None)
237
- async def serve_file(
238
- file_path: str,
239
- ) -> FileResponse | StreamingResponse | Response:
240
- descriptor_path = os.environ["JIVAS_DESCRIPTOR_ROOT_PATH"]
241
- if descriptor_path and descriptor_path in file_path:
242
- return Response(status_code=403)
243
-
244
- return serve_proxied_file(file_path)
245
-
246
- @app.get("/f/{file_id:path}", response_model=None)
247
- async def get_proxied_file(
248
- file_id: str,
249
- ) -> FileResponse | StreamingResponse | Response:
250
- from bson import ObjectId
251
- from fastapi import HTTPException
252
-
253
- params = file_id.split("/")
254
- object_id = params[0]
255
-
256
- # mongo db collection
257
- collection = NodeAnchor.Collection.get_collection("url_proxies")
258
- file_details = collection.find_one({"_id": ObjectId(object_id)})
259
- descriptor_path = os.environ["JIVAS_DESCRIPTOR_ROOT_PATH"]
260
-
261
- if file_details:
262
- if descriptor_path and descriptor_path in file_details["path"]:
263
- return Response(status_code=403)
264
-
265
- return serve_proxied_file(file_details["path"])
266
-
267
- raise HTTPException(status_code=404, detail="File not found")
268
-
269
- # run the app
270
- _run(app, host=host, port=port)