jarviscore-framework 0.1.0__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.
Files changed (55) hide show
  1. examples/calculator_agent_example.py +77 -0
  2. examples/multi_agent_workflow.py +132 -0
  3. examples/research_agent_example.py +76 -0
  4. jarviscore/__init__.py +54 -0
  5. jarviscore/cli/__init__.py +7 -0
  6. jarviscore/cli/__main__.py +33 -0
  7. jarviscore/cli/check.py +404 -0
  8. jarviscore/cli/smoketest.py +371 -0
  9. jarviscore/config/__init__.py +7 -0
  10. jarviscore/config/settings.py +128 -0
  11. jarviscore/core/__init__.py +7 -0
  12. jarviscore/core/agent.py +163 -0
  13. jarviscore/core/mesh.py +463 -0
  14. jarviscore/core/profile.py +64 -0
  15. jarviscore/docs/API_REFERENCE.md +932 -0
  16. jarviscore/docs/CONFIGURATION.md +753 -0
  17. jarviscore/docs/GETTING_STARTED.md +600 -0
  18. jarviscore/docs/TROUBLESHOOTING.md +424 -0
  19. jarviscore/docs/USER_GUIDE.md +983 -0
  20. jarviscore/execution/__init__.py +94 -0
  21. jarviscore/execution/code_registry.py +298 -0
  22. jarviscore/execution/generator.py +268 -0
  23. jarviscore/execution/llm.py +430 -0
  24. jarviscore/execution/repair.py +283 -0
  25. jarviscore/execution/result_handler.py +332 -0
  26. jarviscore/execution/sandbox.py +555 -0
  27. jarviscore/execution/search.py +281 -0
  28. jarviscore/orchestration/__init__.py +18 -0
  29. jarviscore/orchestration/claimer.py +101 -0
  30. jarviscore/orchestration/dependency.py +143 -0
  31. jarviscore/orchestration/engine.py +292 -0
  32. jarviscore/orchestration/status.py +96 -0
  33. jarviscore/p2p/__init__.py +23 -0
  34. jarviscore/p2p/broadcaster.py +353 -0
  35. jarviscore/p2p/coordinator.py +364 -0
  36. jarviscore/p2p/keepalive.py +361 -0
  37. jarviscore/p2p/swim_manager.py +290 -0
  38. jarviscore/profiles/__init__.py +6 -0
  39. jarviscore/profiles/autoagent.py +264 -0
  40. jarviscore/profiles/customagent.py +137 -0
  41. jarviscore_framework-0.1.0.dist-info/METADATA +136 -0
  42. jarviscore_framework-0.1.0.dist-info/RECORD +55 -0
  43. jarviscore_framework-0.1.0.dist-info/WHEEL +5 -0
  44. jarviscore_framework-0.1.0.dist-info/licenses/LICENSE +21 -0
  45. jarviscore_framework-0.1.0.dist-info/top_level.txt +3 -0
  46. tests/conftest.py +44 -0
  47. tests/test_agent.py +165 -0
  48. tests/test_autoagent.py +140 -0
  49. tests/test_autoagent_day4.py +186 -0
  50. tests/test_customagent.py +248 -0
  51. tests/test_integration.py +293 -0
  52. tests/test_llm_fallback.py +185 -0
  53. tests/test_mesh.py +356 -0
  54. tests/test_p2p_integration.py +375 -0
  55. tests/test_remote_sandbox.py +116 -0
@@ -0,0 +1,555 @@
1
+ """
2
+ Sandbox Executor - Safe execution of generated code with resource limits
3
+ Supports async code and provides internet search access
4
+
5
+ Modes:
6
+ - local: In-process execution (development/testing)
7
+ - remote: HTTP POST to sandbox service (production)
8
+ """
9
+ import asyncio
10
+ import aiohttp
11
+ import base64
12
+ import json
13
+ import logging
14
+ import signal
15
+ import sys
16
+ import time
17
+ from typing import Dict, Any, Optional
18
+ from contextlib import contextmanager
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+
23
+ class ExecutionTimeout(Exception):
24
+ """Raised when code execution times out."""
25
+ pass
26
+
27
+
28
+ @contextmanager
29
+ def time_limit(seconds: int):
30
+ """Context manager for enforcing time limits (Unix only)."""
31
+ def signal_handler(signum, frame):
32
+ raise ExecutionTimeout(f"Execution exceeded {seconds} seconds")
33
+
34
+ # Only works on Unix systems
35
+ if hasattr(signal, 'SIGALRM'):
36
+ signal.signal(signal.SIGALRM, signal_handler)
37
+ signal.alarm(seconds)
38
+ try:
39
+ yield
40
+ finally:
41
+ signal.alarm(0)
42
+ else:
43
+ # Windows fallback - no timeout enforcement
44
+ logger.warning("Timeout enforcement not available on Windows")
45
+ yield
46
+
47
+
48
+ class SandboxExecutor:
49
+ """
50
+ Safe code executor with resource limits and internet access.
51
+
52
+ Modes:
53
+ - local: In-process exec() (fast, for development)
54
+ - remote: HTTP POST to sandbox service (isolated, for production)
55
+
56
+ Philosophy:
57
+ - Execute generated code in isolated namespace
58
+ - Enforce timeout limits
59
+ - Provide search tools if available
60
+ - Capture all output and errors
61
+ - Extract 'result' variable
62
+
63
+ Example:
64
+ # Local mode (development)
65
+ executor = SandboxExecutor(mode="local")
66
+
67
+ # Remote mode (production)
68
+ executor = SandboxExecutor(
69
+ mode="remote",
70
+ sandbox_url="https://sandbox.mycompany.com/execute"
71
+ )
72
+ """
73
+
74
+ def __init__(
75
+ self,
76
+ timeout: int = 300,
77
+ search_client=None,
78
+ config: Optional[Dict] = None
79
+ ):
80
+ """
81
+ Initialize sandbox executor.
82
+
83
+ Args:
84
+ timeout: Max execution time in seconds (default 300 = 5 min)
85
+ search_client: Optional InternetSearch for web access
86
+ config: Optional config dict with:
87
+ - sandbox_mode: "local" or "remote"
88
+ - sandbox_service_url: URL for remote sandbox
89
+ """
90
+ self.timeout = timeout
91
+ self.search = search_client
92
+ self.config = config or {}
93
+
94
+ # Determine execution mode
95
+ self.mode = self.config.get('sandbox_mode', 'local').lower()
96
+ self.sandbox_url = self.config.get('sandbox_service_url')
97
+
98
+ if self.mode == 'remote' and not self.sandbox_url:
99
+ logger.warning(
100
+ "Remote sandbox mode requires sandbox_service_url. "
101
+ "Falling back to local mode."
102
+ )
103
+ self.mode = 'local'
104
+
105
+ logger.info(f"Sandbox initialized: mode={self.mode}, timeout={timeout}s")
106
+
107
+ async def execute(
108
+ self,
109
+ code: str,
110
+ timeout: Optional[int] = None,
111
+ context: Optional[Dict] = None
112
+ ) -> Dict[str, Any]:
113
+ """
114
+ Execute Python code in sandbox (local or remote).
115
+
116
+ Args:
117
+ code: Python code string to execute
118
+ timeout: Optional timeout override (seconds)
119
+ context: Optional context variables to inject
120
+
121
+ Returns:
122
+ {
123
+ "status": "success" | "failure",
124
+ "output": Any, # Value of 'result' variable
125
+ "error": str, # Error message if failed
126
+ "error_type": str, # Exception type
127
+ "execution_time": float, # Seconds taken
128
+ "mode": "local" | "remote" # Execution mode used
129
+ }
130
+
131
+ Example:
132
+ result = await executor.execute("result = 2 + 2")
133
+ print(result['output']) # 4
134
+ """
135
+ timeout = timeout or self.timeout
136
+ start_time = time.time()
137
+
138
+ logger.info(f"Executing code ({self.mode} mode, {timeout}s timeout)")
139
+ logger.debug(f"Code length: {len(code)} chars")
140
+
141
+ try:
142
+ # Route to appropriate execution method
143
+ if self.mode == 'remote':
144
+ result = await self._execute_remote(code, timeout, context)
145
+ else:
146
+ result = await self._execute_local(code, timeout, context)
147
+
148
+ # Add execution metadata
149
+ execution_time = time.time() - start_time
150
+ result['execution_time'] = execution_time
151
+ result['mode'] = self.mode
152
+
153
+ logger.info(f"Code execution successful ({execution_time:.3f}s)")
154
+ return result
155
+
156
+ except Exception as e:
157
+ execution_time = time.time() - start_time
158
+ logger.error(f"Execution failed: {type(e).__name__}: {e}")
159
+ return {
160
+ "status": "failure",
161
+ "error": str(e),
162
+ "error_type": type(e).__name__,
163
+ "execution_time": execution_time,
164
+ "mode": self.mode
165
+ }
166
+
167
+ async def _execute_local(
168
+ self,
169
+ code: str,
170
+ timeout: int,
171
+ context: Optional[Dict] = None
172
+ ) -> Dict[str, Any]:
173
+ """Execute code locally in-process."""
174
+ # Create isolated namespace
175
+ namespace = self._create_namespace(context)
176
+
177
+ # Check if code is async
178
+ is_async = 'async def' in code or 'await ' in code or 'asyncio' in code
179
+
180
+ if is_async:
181
+ return await self._execute_async(code, namespace, timeout)
182
+ else:
183
+ return await self._execute_sync(code, namespace, timeout)
184
+
185
+ async def _execute_remote(
186
+ self,
187
+ code: str,
188
+ timeout: int,
189
+ context: Optional[Dict] = None
190
+ ) -> Dict[str, Any]:
191
+ """
192
+ Execute code via remote sandbox service (Azure Container Apps).
193
+
194
+ Matches integration-agent format:
195
+ {
196
+ "STEP_DATA": {
197
+ "id": "job_id",
198
+ "function_name": "generated_code",
199
+ "parameters": {},
200
+ "options": {}
201
+ },
202
+ "TASK_CODE_B64": "base64_encoded_code"
203
+ }
204
+
205
+ Expects response:
206
+ {
207
+ "success": true/false,
208
+ "result": ...,
209
+ "error": "...",
210
+ ...
211
+ }
212
+ """
213
+ # Wrap code to capture result (matching integration agent behavior)
214
+ wrapped_code = self._wrap_code_for_sandbox(code, context)
215
+
216
+ # Encode code to base64
217
+ code_b64 = base64.b64encode(wrapped_code.encode('utf-8')).decode('utf-8')
218
+
219
+ # Prepare payload in Azure Container Apps format
220
+ payload = {
221
+ "STEP_DATA": {
222
+ "id": f"jarviscore_{int(time.time())}",
223
+ "function_name": "generated_code",
224
+ "parameters": context or {},
225
+ "options": {"timeout": timeout}
226
+ },
227
+ "TASK_CODE_B64": code_b64
228
+ }
229
+
230
+ try:
231
+ # Make HTTP request to sandbox service
232
+ # Use /normal endpoint for API tasks
233
+ endpoint_url = f"{self.sandbox_url}/normal"
234
+
235
+ async with aiohttp.ClientSession() as session:
236
+ async with session.post(
237
+ endpoint_url,
238
+ json=payload,
239
+ headers={"Content-Type": "application/json"},
240
+ timeout=aiohttp.ClientTimeout(total=timeout + 10) # Buffer
241
+ ) as response:
242
+ if response.status != 200:
243
+ error_text = await response.text()
244
+ raise RuntimeError(
245
+ f"Sandbox service error ({response.status}): {error_text}"
246
+ )
247
+
248
+ sandbox_response = await response.json()
249
+
250
+ logger.debug(f"Remote sandbox response: {sandbox_response}")
251
+
252
+ # Extract result using robust method (matching integration agent)
253
+ actual_result = self._extract_sandbox_result(sandbox_response)
254
+
255
+ # Convert to our format
256
+ if actual_result.get('success') is False:
257
+ # Error case
258
+ return {
259
+ 'status': 'failure',
260
+ 'error': actual_result.get('error', 'Unknown error'),
261
+ 'error_type': 'RemoteSandboxError'
262
+ }
263
+ else:
264
+ # Success case
265
+ return {
266
+ 'status': 'success',
267
+ 'output': actual_result.get('result', actual_result.get('data', actual_result.get('output')))
268
+ }
269
+
270
+ except asyncio.TimeoutError:
271
+ logger.error(f"Remote sandbox timeout after {timeout}s")
272
+ raise ExecutionTimeout(f"Remote execution exceeded {timeout} seconds")
273
+
274
+ except aiohttp.ClientError as e:
275
+ # Network/HTTP errors
276
+ logger.warning(f"Remote sandbox connection error: {e}. Falling back to local execution.")
277
+ return await self._execute_local(code, timeout, context)
278
+
279
+ except Exception as e:
280
+ # Only fallback for actual execution errors, not during cleanup
281
+ if "object has no attribute" not in str(e):
282
+ logger.warning(f"Remote sandbox failed: {e}. Falling back to local execution.")
283
+ return await self._execute_local(code, timeout, context)
284
+ else:
285
+ # This is likely a cleanup issue, just log and don't fallback
286
+ logger.debug(f"Ignoring cleanup error: {e}")
287
+ raise
288
+
289
+ def _wrap_code_for_sandbox(self, code: str, context: Optional[Dict] = None) -> str:
290
+ """
291
+ Wrap code to capture and print result as JSON (matches integration agent).
292
+
293
+ The sandbox executes code and captures stdout. We need to:
294
+ 1. Execute the code
295
+ 2. Extract the 'result' variable
296
+ 3. Print it as JSON to stdout
297
+
298
+ Args:
299
+ code: Python code to wrap
300
+ context: Optional context variables
301
+
302
+ Returns:
303
+ Wrapped code that prints result as JSON
304
+ """
305
+ # Add imports if needed
306
+ imports = []
307
+ if 'import json' not in code:
308
+ imports.append('import json')
309
+ if 'import sys' not in code:
310
+ imports.append('import sys')
311
+
312
+ imports_str = '\n'.join(imports) + '\n' if imports else ''
313
+
314
+ # Wrap code to capture and print result
315
+ wrapper = f'''{imports_str}{code}
316
+
317
+ # JarvisCore: Capture and print result
318
+ if __name__ == "__main__":
319
+ try:
320
+ # Check if result variable exists
321
+ if 'result' in locals() or 'result' in globals():
322
+ output = {{"success": True, "result": result}}
323
+ else:
324
+ output = {{"success": False, "error": "No 'result' variable found"}}
325
+
326
+ # Print as JSON to stdout (sandbox captures this)
327
+ print(json.dumps(output))
328
+ sys.exit(0)
329
+ except Exception as e:
330
+ error_output = {{
331
+ "success": False,
332
+ "error": str(e),
333
+ "error_type": type(e).__name__
334
+ }}
335
+ print(json.dumps(error_output))
336
+ sys.exit(1)
337
+ '''
338
+ return wrapper
339
+
340
+ def _extract_sandbox_result(self, sandbox_response: Any) -> Dict[str, Any]:
341
+ """
342
+ Extract the actual function result from sandbox response.
343
+ Matches integration agent's robust extraction logic.
344
+
345
+ Args:
346
+ sandbox_response: Raw response from sandbox service
347
+
348
+ Returns:
349
+ Extracted result dict
350
+ """
351
+ # Handle None response
352
+ if sandbox_response is None:
353
+ logger.warning("Sandbox returned None response")
354
+ return {
355
+ "success": False,
356
+ "error": "Sandbox returned null response",
357
+ "error_type": "null_response"
358
+ }
359
+
360
+ # Handle non-dict response
361
+ if not isinstance(sandbox_response, dict):
362
+ logger.warning(f"Sandbox returned non-dict response: {type(sandbox_response)}")
363
+ return {
364
+ "success": False,
365
+ "error": f"Sandbox returned unexpected response type: {type(sandbox_response)}",
366
+ "error_type": "invalid_response_type"
367
+ }
368
+
369
+ # Try to parse 'output' field if it's a JSON string
370
+ if 'output' in sandbox_response and isinstance(sandbox_response.get('output'), str):
371
+ output_str = sandbox_response['output'].strip()
372
+ if output_str:
373
+ try:
374
+ parsed_output = json.loads(output_str)
375
+ if isinstance(parsed_output, dict):
376
+ logger.debug("Successfully parsed result from output field")
377
+ return parsed_output
378
+ except json.JSONDecodeError as e:
379
+ logger.debug(f"JSON parse failed: {e}, trying line-by-line")
380
+ lines = output_str.strip().split('\n')
381
+ for line in reversed(lines):
382
+ line = line.strip()
383
+ if line.startswith('{') and line.endswith('}'):
384
+ try:
385
+ parsed_output = json.loads(line)
386
+ if isinstance(parsed_output, dict) and 'success' in parsed_output:
387
+ logger.debug("Successfully parsed result from last JSON line")
388
+ return parsed_output
389
+ except json.JSONDecodeError:
390
+ continue
391
+
392
+ logger.warning("Could not parse any JSON from output")
393
+ return {
394
+ "success": sandbox_response.get('success', False),
395
+ "output": output_str,
396
+ "error": sandbox_response.get('error') or "Failed to parse output as JSON"
397
+ }
398
+
399
+ # If response has 'success' field but no nested result fields, return as-is
400
+ if 'success' in sandbox_response:
401
+ wrapper_fields = {'result', 'function_result', 'execution_result'}
402
+ if not any(field in sandbox_response for field in wrapper_fields):
403
+ return sandbox_response
404
+
405
+ # Try common result field names
406
+ result_candidates = ['result', 'function_result', 'execution_result', 'data', 'response']
407
+ for field in result_candidates:
408
+ if field in sandbox_response and sandbox_response[field] is not None:
409
+ candidate = sandbox_response[field]
410
+ if isinstance(candidate, dict):
411
+ return candidate
412
+
413
+ logger.debug(f"No specific result field found, returning whole response")
414
+ return sandbox_response
415
+
416
+ def _create_namespace(self, context: Optional[Dict] = None) -> Dict:
417
+ """
418
+ Create isolated namespace with safe built-ins and tools.
419
+
420
+ Args:
421
+ context: Optional context variables to inject
422
+
423
+ Returns:
424
+ Namespace dict for code execution
425
+ """
426
+ # Get all built-ins except dangerous ones
427
+ safe_builtins = {}
428
+ for name in dir(__builtins__):
429
+ if name.startswith('_'):
430
+ continue
431
+ # Exclude dangerous functions
432
+ if name in ['eval', 'exec', 'compile', 'open', 'input', 'file']:
433
+ continue
434
+ try:
435
+ safe_builtins[name] = getattr(__builtins__, name)
436
+ except AttributeError:
437
+ pass
438
+
439
+ # Ensure critical built-ins are present
440
+ critical_builtins = [
441
+ 'print', '__import__', 'len', 'range', 'str', 'int', 'float',
442
+ 'list', 'dict', 'set', 'tuple', 'bool', 'type', 'isinstance',
443
+ 'min', 'max', 'sum', 'sorted', 'enumerate', 'zip', 'map', 'filter',
444
+ 'Exception', 'ValueError', 'TypeError', 'KeyError', 'IndexError',
445
+ 'NameError', 'AttributeError', 'RuntimeError', 'ZeroDivisionError'
446
+ ]
447
+
448
+ for builtin in critical_builtins:
449
+ if builtin not in safe_builtins:
450
+ try:
451
+ safe_builtins[builtin] = eval(builtin)
452
+ except:
453
+ logger.warning(f"Could not add built-in: {builtin}")
454
+
455
+ namespace = {
456
+ '__builtins__': safe_builtins,
457
+ 'result': None, # Where code should store output
458
+ }
459
+
460
+ # Inject search client if available
461
+ if self.search:
462
+ namespace['search'] = self.search
463
+ logger.debug("Injected search client into namespace")
464
+
465
+ # Inject context variables
466
+ if context:
467
+ namespace.update(context)
468
+ logger.debug(f"Injected {len(context)} context variables")
469
+
470
+ return namespace
471
+
472
+ async def _execute_sync(
473
+ self,
474
+ code: str,
475
+ namespace: Dict,
476
+ timeout: int
477
+ ) -> Dict[str, Any]:
478
+ """Execute synchronous code."""
479
+ try:
480
+ # Run in thread pool to enforce timeout
481
+ loop = asyncio.get_event_loop()
482
+ await asyncio.wait_for(
483
+ loop.run_in_executor(None, exec, code, namespace),
484
+ timeout=timeout
485
+ )
486
+
487
+ # Extract result
488
+ result = namespace.get('result')
489
+
490
+ return {
491
+ "status": "success",
492
+ "output": result
493
+ }
494
+
495
+ except asyncio.TimeoutError:
496
+ raise ExecutionTimeout(f"Execution exceeded {timeout} seconds")
497
+
498
+ async def _execute_async(
499
+ self,
500
+ code: str,
501
+ namespace: Dict,
502
+ timeout: int
503
+ ) -> Dict[str, Any]:
504
+ """Execute asynchronous code."""
505
+ # Inject asyncio and search for async code
506
+ namespace['asyncio'] = asyncio
507
+ if self.search:
508
+ namespace['search'] = self.search
509
+
510
+ try:
511
+ # Execute code to define functions
512
+ exec(code, namespace)
513
+
514
+ # Look for main() or run() function
515
+ if 'main' in namespace and callable(namespace['main']):
516
+ # Run main() with timeout
517
+ result_value = await asyncio.wait_for(
518
+ namespace['main'](),
519
+ timeout=timeout
520
+ )
521
+ elif 'run' in namespace and callable(namespace['run']):
522
+ result_value = await asyncio.wait_for(
523
+ namespace['run'](),
524
+ timeout=timeout
525
+ )
526
+ else:
527
+ # Check if result was set directly
528
+ result_value = namespace.get('result')
529
+
530
+ return {
531
+ "status": "success",
532
+ "output": result_value
533
+ }
534
+
535
+ except asyncio.TimeoutError:
536
+ raise ExecutionTimeout(f"Async execution exceeded {timeout} seconds")
537
+
538
+
539
+ def create_sandbox_executor(
540
+ timeout: int = 300,
541
+ search_client=None,
542
+ config: Optional[Dict] = None
543
+ ) -> SandboxExecutor:
544
+ """
545
+ Factory function to create sandbox executor.
546
+
547
+ Args:
548
+ timeout: Max execution time (default 300s)
549
+ search_client: Optional search client for web access
550
+ config: Optional configuration
551
+
552
+ Returns:
553
+ SandboxExecutor instance
554
+ """
555
+ return SandboxExecutor(timeout, search_client, config)