jvserve 2.0.12__tar.gz → 2.0.15__tar.gz

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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jvserve
3
- Version: 2.0.12
3
+ Version: 2.0.15
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.
@@ -4,5 +4,5 @@ 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.12"
7
+ __version__ = "2.0.15"
8
8
  __supported__jivas__versions__ = ["2.0.0"]
@@ -7,14 +7,13 @@ from contextlib import asynccontextmanager
7
7
  from typing import AsyncIterator, Optional
8
8
 
9
9
  from dotenv import load_dotenv
10
- from fastapi.responses import FileResponse, StreamingResponse
10
+ from fastapi.responses import FileResponse, Response, StreamingResponse
11
11
  from jac_cloud.jaseci.security import authenticator
12
12
  from jac_cloud.plugin.jaseci import NodeAnchor
13
13
  from jaclang.cli.cmdreg import cmd_registry
14
14
  from jaclang.plugin.default import hookimpl
15
15
  from jaclang.runtimelib.context import ExecutionContext
16
16
  from jaclang.runtimelib.machine import JacMachine
17
- from requests import Response
18
17
  from uvicorn import run as _run
19
18
 
20
19
  from jvserve.lib.agent_interface import AgentInterface
@@ -216,8 +215,7 @@ class JacCmd:
216
215
  ) -> None:
217
216
  """Launch the file proxy server for remote files."""
218
217
  # load FastAPI
219
- from bson import ObjectId
220
- from fastapi import FastAPI, HTTPException
218
+ from fastapi import FastAPI
221
219
  from fastapi.middleware.cors import CORSMiddleware
222
220
 
223
221
  # Setup custom routes
@@ -238,21 +236,32 @@ class JacCmd:
238
236
  @app.get("/files/{file_path:path}", response_model=None)
239
237
  async def serve_file(
240
238
  file_path: str,
241
- ) -> Response:
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
+
242
244
  return serve_proxied_file(file_path)
243
245
 
244
246
  @app.get("/f/{file_id:path}", response_model=None)
245
247
  async def get_proxied_file(
246
248
  file_id: str,
247
- ) -> Response:
249
+ ) -> FileResponse | StreamingResponse | Response:
250
+ from bson import ObjectId
251
+ from fastapi import HTTPException
252
+
248
253
  params = file_id.split("/")
249
254
  object_id = params[0]
250
255
 
251
256
  # mongo db collection
252
257
  collection = NodeAnchor.Collection.get_collection("url_proxies")
253
258
  file_details = collection.find_one({"_id": ObjectId(object_id)})
259
+ descriptor_path = os.environ["JIVAS_DESCRIPTOR_ROOT_PATH"]
254
260
 
255
261
  if file_details:
262
+ if descriptor_path and descriptor_path in file_details["path"]:
263
+ return Response(status_code=403)
264
+
256
265
  return serve_proxied_file(file_details["path"])
257
266
 
258
267
  raise HTTPException(status_code=404, detail="File not found")
@@ -1,5 +1,6 @@
1
1
  """Agent Interface class and methods for interaction with Jivas."""
2
2
 
3
+ import asyncio
3
4
  import json
4
5
  import logging
5
6
  import os
@@ -158,77 +159,211 @@ class AgentInterface:
158
159
 
159
160
  return response
160
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
+
161
202
  @staticmethod
162
203
  async def action_walker_exec(
163
- agent_id: str = Form(...), # noqa: B008
164
- module_root: str = Form(...), # noqa: B008
165
- walker: str = Form(...), # noqa: B008
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
166
207
  args: Optional[str] = Form(None), # noqa: B008
167
208
  attachments: List[UploadFile] = File(default_factory=list), # noqa: B008
168
209
  ) -> JSONResponse:
169
- """Execute a named walker exposed by an action within context; capable of handling JSON or file data depending on request"""
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
+ )
170
233
 
171
- response = JSONResponse(status_code=500, content="unable to complete request")
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
+ )
172
241
 
173
- try:
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
+ )
174
263
 
175
- # add agent id as a standard
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
176
274
  attributes: Dict[str, Any] = {"agent_id": agent_id}
177
275
 
178
- # add any other args
276
+ # Parse additional arguments if provided
179
277
  if args:
180
- attributes.update(json.loads(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
+ )
181
286
 
182
- # Processing files if any were uploaded
287
+ # Process uploaded files
183
288
  if attachments:
184
289
  attributes["files"] = []
185
290
  for file in attachments:
186
- attributes["files"].append(
187
- {
188
- "name": file.filename,
189
- "type": file.content_type,
190
- "content": await file.read(),
191
- }
192
- )
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
193
304
 
194
- if not agent_id or not walker or not module_root:
195
- AgentInterface.LOGGER.error("missing parameters")
196
- return JSONResponse(
197
- status_code=401, content="missing required parameters"
198
- )
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
199
314
 
200
- except Exception as e:
201
- AgentInterface.LOGGER.error(
202
- f"an exception occurred: {e}, {traceback.format_exc()}"
203
- )
204
- return JSONResponse(status_code=500, content="internal server error")
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
205
320
 
206
- ctx = await AgentInterface.load_context_async()
207
- if ctx:
208
- # compose full module_path
209
- module = f"{module_root}.{walker}"
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())
210
324
 
211
- try:
212
- response = _Jac.spawn_call(
213
- ctx.entry_node.architype,
214
- AgentInterface.spawn_walker(
215
- walker_name=walker,
216
- attributes=attributes,
217
- module_name=module,
218
- ),
219
- ).response
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
+ )
220
344
 
221
345
  except Exception as e:
222
- AgentInterface.EXPIRATION = None
223
- AgentInterface.LOGGER.error(
224
- f"an exception occurred: {e}, {traceback.format_exc()}"
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)},
225
350
  )
226
- else:
227
- AgentInterface.LOGGER.error(f"unable to execute {walker}")
228
351
 
229
- ctx.close()
230
-
231
- return response
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)}")
232
367
 
233
368
  class InteractPayload(BaseModel):
234
369
  """Payload for interacting with the agent."""
@@ -243,7 +378,7 @@ class AgentInterface:
243
378
  streaming: Optional[bool] = None
244
379
 
245
380
  @staticmethod
246
- def interact(payload: InteractPayload) -> dict:
381
+ def interact(payload: InteractPayload, request: Request) -> dict:
247
382
  """Interact with the agent."""
248
383
  response = None
249
384
  ctx = AgentInterface.load_context()
@@ -291,7 +426,7 @@ class AgentInterface:
291
426
  interaction_node = response.interaction_node
292
427
 
293
428
  async def generate(
294
- generator: Iterator,
429
+ generator: Iterator, request: Request
295
430
  ) -> AsyncGenerator[str, None]:
296
431
  """
297
432
  Asynchronously yield data chunks from a response generator in Server-Sent Events (SSE) format.
@@ -304,11 +439,11 @@ class AgentInterface:
304
439
  """
305
440
  full_text = ""
306
441
  total_tokens = 0
442
+
307
443
  try:
308
444
  for chunk in generator:
309
445
  full_text += chunk.content
310
446
  total_tokens += 1 # each chunk is a token, let's tally
311
- await sleep(0.025)
312
447
  yield (
313
448
  "data: "
314
449
  + json.dumps(
@@ -324,6 +459,7 @@ class AgentInterface:
324
459
  )
325
460
  + "\n\n"
326
461
  )
462
+ await sleep(0.025)
327
463
  # Update the interaction node with the fully generated text
328
464
  actx = await AgentInterface.load_context_async()
329
465
  try:
@@ -336,7 +472,7 @@ class AgentInterface:
336
472
  attributes={
337
473
  "interaction_data": interaction_node.export(),
338
474
  },
339
- module_name="jivas.agent.action.update_interaction",
475
+ module_name="jivas.agent.memory.update_interaction",
340
476
  ),
341
477
  )
342
478
  finally:
@@ -347,9 +483,30 @@ class AgentInterface:
347
483
  AgentInterface.LOGGER.error(
348
484
  f"Exception in streaming generator: {e}, {traceback.format_exc()}"
349
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()
350
507
 
351
508
  return StreamingResponse(
352
- generate(response.generator),
509
+ generate(response.generator, request),
353
510
  media_type="text/event-stream",
354
511
  )
355
512
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jvserve
3
- Version: 2.0.12
3
+ Version: 2.0.15
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.
File without changes
File without changes
File without changes
File without changes
File without changes