genxai-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.
- cli/__init__.py +3 -0
- cli/commands/__init__.py +6 -0
- cli/commands/approval.py +85 -0
- cli/commands/audit.py +127 -0
- cli/commands/metrics.py +25 -0
- cli/commands/tool.py +389 -0
- cli/main.py +32 -0
- genxai/__init__.py +81 -0
- genxai/api/__init__.py +5 -0
- genxai/api/app.py +21 -0
- genxai/config/__init__.py +5 -0
- genxai/config/settings.py +37 -0
- genxai/connectors/__init__.py +19 -0
- genxai/connectors/base.py +122 -0
- genxai/connectors/kafka.py +92 -0
- genxai/connectors/postgres_cdc.py +95 -0
- genxai/connectors/registry.py +44 -0
- genxai/connectors/sqs.py +94 -0
- genxai/connectors/webhook.py +73 -0
- genxai/core/__init__.py +37 -0
- genxai/core/agent/__init__.py +32 -0
- genxai/core/agent/base.py +206 -0
- genxai/core/agent/config_io.py +59 -0
- genxai/core/agent/registry.py +98 -0
- genxai/core/agent/runtime.py +970 -0
- genxai/core/communication/__init__.py +6 -0
- genxai/core/communication/collaboration.py +44 -0
- genxai/core/communication/message_bus.py +192 -0
- genxai/core/communication/protocols.py +35 -0
- genxai/core/execution/__init__.py +22 -0
- genxai/core/execution/metadata.py +181 -0
- genxai/core/execution/queue.py +201 -0
- genxai/core/graph/__init__.py +30 -0
- genxai/core/graph/checkpoints.py +77 -0
- genxai/core/graph/edges.py +131 -0
- genxai/core/graph/engine.py +813 -0
- genxai/core/graph/executor.py +516 -0
- genxai/core/graph/nodes.py +161 -0
- genxai/core/graph/trigger_runner.py +40 -0
- genxai/core/memory/__init__.py +19 -0
- genxai/core/memory/base.py +72 -0
- genxai/core/memory/embedding.py +327 -0
- genxai/core/memory/episodic.py +448 -0
- genxai/core/memory/long_term.py +467 -0
- genxai/core/memory/manager.py +543 -0
- genxai/core/memory/persistence.py +297 -0
- genxai/core/memory/procedural.py +461 -0
- genxai/core/memory/semantic.py +526 -0
- genxai/core/memory/shared.py +62 -0
- genxai/core/memory/short_term.py +303 -0
- genxai/core/memory/vector_store.py +508 -0
- genxai/core/memory/working.py +211 -0
- genxai/core/state/__init__.py +6 -0
- genxai/core/state/manager.py +293 -0
- genxai/core/state/schema.py +115 -0
- genxai/llm/__init__.py +14 -0
- genxai/llm/base.py +150 -0
- genxai/llm/factory.py +329 -0
- genxai/llm/providers/__init__.py +1 -0
- genxai/llm/providers/anthropic.py +249 -0
- genxai/llm/providers/cohere.py +274 -0
- genxai/llm/providers/google.py +334 -0
- genxai/llm/providers/ollama.py +147 -0
- genxai/llm/providers/openai.py +257 -0
- genxai/llm/routing.py +83 -0
- genxai/observability/__init__.py +6 -0
- genxai/observability/logging.py +327 -0
- genxai/observability/metrics.py +494 -0
- genxai/observability/tracing.py +372 -0
- genxai/performance/__init__.py +39 -0
- genxai/performance/cache.py +256 -0
- genxai/performance/pooling.py +289 -0
- genxai/security/audit.py +304 -0
- genxai/security/auth.py +315 -0
- genxai/security/cost_control.py +528 -0
- genxai/security/default_policies.py +44 -0
- genxai/security/jwt.py +142 -0
- genxai/security/oauth.py +226 -0
- genxai/security/pii.py +366 -0
- genxai/security/policy_engine.py +82 -0
- genxai/security/rate_limit.py +341 -0
- genxai/security/rbac.py +247 -0
- genxai/security/validation.py +218 -0
- genxai/tools/__init__.py +21 -0
- genxai/tools/base.py +383 -0
- genxai/tools/builtin/__init__.py +131 -0
- genxai/tools/builtin/communication/__init__.py +15 -0
- genxai/tools/builtin/communication/email_sender.py +159 -0
- genxai/tools/builtin/communication/notification_manager.py +167 -0
- genxai/tools/builtin/communication/slack_notifier.py +118 -0
- genxai/tools/builtin/communication/sms_sender.py +118 -0
- genxai/tools/builtin/communication/webhook_caller.py +136 -0
- genxai/tools/builtin/computation/__init__.py +15 -0
- genxai/tools/builtin/computation/calculator.py +101 -0
- genxai/tools/builtin/computation/code_executor.py +183 -0
- genxai/tools/builtin/computation/data_validator.py +259 -0
- genxai/tools/builtin/computation/hash_generator.py +129 -0
- genxai/tools/builtin/computation/regex_matcher.py +201 -0
- genxai/tools/builtin/data/__init__.py +15 -0
- genxai/tools/builtin/data/csv_processor.py +213 -0
- genxai/tools/builtin/data/data_transformer.py +299 -0
- genxai/tools/builtin/data/json_processor.py +233 -0
- genxai/tools/builtin/data/text_analyzer.py +288 -0
- genxai/tools/builtin/data/xml_processor.py +175 -0
- genxai/tools/builtin/database/__init__.py +15 -0
- genxai/tools/builtin/database/database_inspector.py +157 -0
- genxai/tools/builtin/database/mongodb_query.py +196 -0
- genxai/tools/builtin/database/redis_cache.py +167 -0
- genxai/tools/builtin/database/sql_query.py +145 -0
- genxai/tools/builtin/database/vector_search.py +163 -0
- genxai/tools/builtin/file/__init__.py +17 -0
- genxai/tools/builtin/file/directory_scanner.py +214 -0
- genxai/tools/builtin/file/file_compressor.py +237 -0
- genxai/tools/builtin/file/file_reader.py +102 -0
- genxai/tools/builtin/file/file_writer.py +122 -0
- genxai/tools/builtin/file/image_processor.py +186 -0
- genxai/tools/builtin/file/pdf_parser.py +144 -0
- genxai/tools/builtin/test/__init__.py +15 -0
- genxai/tools/builtin/test/async_simulator.py +62 -0
- genxai/tools/builtin/test/data_transformer.py +99 -0
- genxai/tools/builtin/test/error_generator.py +82 -0
- genxai/tools/builtin/test/simple_math.py +94 -0
- genxai/tools/builtin/test/string_processor.py +72 -0
- genxai/tools/builtin/web/__init__.py +15 -0
- genxai/tools/builtin/web/api_caller.py +161 -0
- genxai/tools/builtin/web/html_parser.py +330 -0
- genxai/tools/builtin/web/http_client.py +187 -0
- genxai/tools/builtin/web/url_validator.py +162 -0
- genxai/tools/builtin/web/web_scraper.py +170 -0
- genxai/tools/custom/my_test_tool_2.py +9 -0
- genxai/tools/dynamic.py +105 -0
- genxai/tools/mcp_server.py +167 -0
- genxai/tools/persistence/__init__.py +6 -0
- genxai/tools/persistence/models.py +55 -0
- genxai/tools/persistence/service.py +322 -0
- genxai/tools/registry.py +227 -0
- genxai/tools/security/__init__.py +11 -0
- genxai/tools/security/limits.py +214 -0
- genxai/tools/security/policy.py +20 -0
- genxai/tools/security/sandbox.py +248 -0
- genxai/tools/templates.py +435 -0
- genxai/triggers/__init__.py +19 -0
- genxai/triggers/base.py +104 -0
- genxai/triggers/file_watcher.py +75 -0
- genxai/triggers/queue.py +68 -0
- genxai/triggers/registry.py +82 -0
- genxai/triggers/schedule.py +66 -0
- genxai/triggers/webhook.py +68 -0
- genxai/utils/__init__.py +1 -0
- genxai/utils/tokens.py +295 -0
- genxai_framework-0.1.0.dist-info/METADATA +495 -0
- genxai_framework-0.1.0.dist-info/RECORD +156 -0
- genxai_framework-0.1.0.dist-info/WHEEL +5 -0
- genxai_framework-0.1.0.dist-info/entry_points.txt +2 -0
- genxai_framework-0.1.0.dist-info/licenses/LICENSE +21 -0
- genxai_framework-0.1.0.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""SMS sender tool for sending text messages."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict
|
|
4
|
+
import logging
|
|
5
|
+
|
|
6
|
+
from genxai.tools.base import Tool, ToolMetadata, ToolParameter, ToolCategory
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class SMSSenderTool(Tool):
|
|
12
|
+
"""Send SMS messages via Twilio API."""
|
|
13
|
+
|
|
14
|
+
def __init__(self) -> None:
|
|
15
|
+
"""Initialize SMS sender tool."""
|
|
16
|
+
metadata = ToolMetadata(
|
|
17
|
+
name="sms_sender",
|
|
18
|
+
description="Send SMS text messages via Twilio",
|
|
19
|
+
category=ToolCategory.COMMUNICATION,
|
|
20
|
+
tags=["sms", "text", "message", "twilio", "communication"],
|
|
21
|
+
version="1.0.0",
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
parameters = [
|
|
25
|
+
ToolParameter(
|
|
26
|
+
name="to_number",
|
|
27
|
+
type="string",
|
|
28
|
+
description="Recipient phone number (E.164 format: +1234567890)",
|
|
29
|
+
required=True,
|
|
30
|
+
),
|
|
31
|
+
ToolParameter(
|
|
32
|
+
name="from_number",
|
|
33
|
+
type="string",
|
|
34
|
+
description="Sender phone number (Twilio number)",
|
|
35
|
+
required=True,
|
|
36
|
+
),
|
|
37
|
+
ToolParameter(
|
|
38
|
+
name="message",
|
|
39
|
+
type="string",
|
|
40
|
+
description="SMS message text",
|
|
41
|
+
required=True,
|
|
42
|
+
),
|
|
43
|
+
ToolParameter(
|
|
44
|
+
name="account_sid",
|
|
45
|
+
type="string",
|
|
46
|
+
description="Twilio Account SID",
|
|
47
|
+
required=True,
|
|
48
|
+
),
|
|
49
|
+
ToolParameter(
|
|
50
|
+
name="auth_token",
|
|
51
|
+
type="string",
|
|
52
|
+
description="Twilio Auth Token",
|
|
53
|
+
required=True,
|
|
54
|
+
),
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
super().__init__(metadata, parameters)
|
|
58
|
+
|
|
59
|
+
async def _execute(
|
|
60
|
+
self,
|
|
61
|
+
to_number: str,
|
|
62
|
+
from_number: str,
|
|
63
|
+
message: str,
|
|
64
|
+
account_sid: str,
|
|
65
|
+
auth_token: str,
|
|
66
|
+
) -> Dict[str, Any]:
|
|
67
|
+
"""Execute SMS sending.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
to_number: Recipient phone number
|
|
71
|
+
from_number: Sender phone number
|
|
72
|
+
message: SMS message
|
|
73
|
+
account_sid: Twilio Account SID
|
|
74
|
+
auth_token: Twilio Auth Token
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
Dictionary containing send results
|
|
78
|
+
"""
|
|
79
|
+
try:
|
|
80
|
+
from twilio.rest import Client
|
|
81
|
+
from twilio.base.exceptions import TwilioRestException
|
|
82
|
+
except ImportError:
|
|
83
|
+
raise ImportError(
|
|
84
|
+
"twilio package not installed. Install with: pip install twilio"
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
result: Dict[str, Any] = {
|
|
88
|
+
"to": to_number,
|
|
89
|
+
"from": from_number,
|
|
90
|
+
"success": False,
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
try:
|
|
94
|
+
# Create Twilio client
|
|
95
|
+
client = Client(account_sid, auth_token)
|
|
96
|
+
|
|
97
|
+
# Send SMS
|
|
98
|
+
sms_message = client.messages.create(
|
|
99
|
+
body=message,
|
|
100
|
+
from_=from_number,
|
|
101
|
+
to=to_number
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
result.update({
|
|
105
|
+
"success": True,
|
|
106
|
+
"message_sid": sms_message.sid,
|
|
107
|
+
"status": sms_message.status,
|
|
108
|
+
"price": sms_message.price,
|
|
109
|
+
"price_unit": sms_message.price_unit,
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
except TwilioRestException as e:
|
|
113
|
+
result["error"] = f"Twilio error: {str(e)}"
|
|
114
|
+
except Exception as e:
|
|
115
|
+
result["error"] = str(e)
|
|
116
|
+
|
|
117
|
+
logger.info(f"SMS send completed: success={result['success']}")
|
|
118
|
+
return result
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""Webhook caller tool for triggering webhooks."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, Optional
|
|
4
|
+
import logging
|
|
5
|
+
|
|
6
|
+
from genxai.tools.base import Tool, ToolMetadata, ToolParameter, ToolCategory
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class WebhookCallerTool(Tool):
|
|
12
|
+
"""Call webhooks with custom payloads and headers."""
|
|
13
|
+
|
|
14
|
+
def __init__(self) -> None:
|
|
15
|
+
"""Initialize webhook caller tool."""
|
|
16
|
+
metadata = ToolMetadata(
|
|
17
|
+
name="webhook_caller",
|
|
18
|
+
description="Trigger webhooks with custom payloads and authentication",
|
|
19
|
+
category=ToolCategory.COMMUNICATION,
|
|
20
|
+
tags=["webhook", "http", "callback", "integration", "api"],
|
|
21
|
+
version="1.0.0",
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
parameters = [
|
|
25
|
+
ToolParameter(
|
|
26
|
+
name="url",
|
|
27
|
+
type="string",
|
|
28
|
+
description="Webhook URL",
|
|
29
|
+
pattern=r"^https?://",
|
|
30
|
+
required=True,
|
|
31
|
+
),
|
|
32
|
+
ToolParameter(
|
|
33
|
+
name="method",
|
|
34
|
+
type="string",
|
|
35
|
+
description="HTTP method",
|
|
36
|
+
required=False,
|
|
37
|
+
default="POST",
|
|
38
|
+
enum=["GET", "POST", "PUT", "PATCH", "DELETE"],
|
|
39
|
+
),
|
|
40
|
+
ToolParameter(
|
|
41
|
+
name="payload",
|
|
42
|
+
type="object",
|
|
43
|
+
description="Webhook payload data",
|
|
44
|
+
required=False,
|
|
45
|
+
),
|
|
46
|
+
ToolParameter(
|
|
47
|
+
name="headers",
|
|
48
|
+
type="object",
|
|
49
|
+
description="Custom HTTP headers",
|
|
50
|
+
required=False,
|
|
51
|
+
),
|
|
52
|
+
ToolParameter(
|
|
53
|
+
name="timeout",
|
|
54
|
+
type="number",
|
|
55
|
+
description="Request timeout in seconds",
|
|
56
|
+
required=False,
|
|
57
|
+
default=30,
|
|
58
|
+
),
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
super().__init__(metadata, parameters)
|
|
62
|
+
|
|
63
|
+
async def _execute(
|
|
64
|
+
self,
|
|
65
|
+
url: str,
|
|
66
|
+
method: str = "POST",
|
|
67
|
+
payload: Optional[Dict[str, Any]] = None,
|
|
68
|
+
headers: Optional[Dict[str, str]] = None,
|
|
69
|
+
timeout: int = 30,
|
|
70
|
+
) -> Dict[str, Any]:
|
|
71
|
+
"""Execute webhook call.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
url: Webhook URL
|
|
75
|
+
method: HTTP method
|
|
76
|
+
payload: Payload data
|
|
77
|
+
headers: Custom headers
|
|
78
|
+
timeout: Request timeout
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
Dictionary containing webhook response
|
|
82
|
+
"""
|
|
83
|
+
try:
|
|
84
|
+
import httpx
|
|
85
|
+
except ImportError:
|
|
86
|
+
raise ImportError(
|
|
87
|
+
"httpx package not installed. Install with: pip install httpx"
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
result: Dict[str, Any] = {
|
|
91
|
+
"url": url,
|
|
92
|
+
"method": method,
|
|
93
|
+
"success": False,
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
try:
|
|
97
|
+
# Prepare request
|
|
98
|
+
request_kwargs: Dict[str, Any] = {
|
|
99
|
+
"method": method,
|
|
100
|
+
"url": url,
|
|
101
|
+
"timeout": timeout,
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if headers:
|
|
105
|
+
request_kwargs["headers"] = headers
|
|
106
|
+
|
|
107
|
+
if payload and method in ["POST", "PUT", "PATCH"]:
|
|
108
|
+
request_kwargs["json"] = payload
|
|
109
|
+
|
|
110
|
+
# Make request
|
|
111
|
+
async with httpx.AsyncClient() as client:
|
|
112
|
+
response = await client.request(**request_kwargs)
|
|
113
|
+
|
|
114
|
+
# Parse response
|
|
115
|
+
result.update({
|
|
116
|
+
"status_code": response.status_code,
|
|
117
|
+
"success": 200 <= response.status_code < 300,
|
|
118
|
+
"response_headers": dict(response.headers),
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
# Try to parse JSON response
|
|
122
|
+
try:
|
|
123
|
+
result["response_data"] = response.json()
|
|
124
|
+
except:
|
|
125
|
+
result["response_text"] = response.text
|
|
126
|
+
|
|
127
|
+
except httpx.HTTPStatusError as e:
|
|
128
|
+
result["error"] = f"HTTP error: {e.response.status_code}"
|
|
129
|
+
result["status_code"] = e.response.status_code
|
|
130
|
+
except httpx.RequestError as e:
|
|
131
|
+
result["error"] = f"Request error: {str(e)}"
|
|
132
|
+
except Exception as e:
|
|
133
|
+
result["error"] = str(e)
|
|
134
|
+
|
|
135
|
+
logger.info(f"Webhook call completed: success={result['success']}")
|
|
136
|
+
return result
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Computation tools for GenXAI."""
|
|
2
|
+
|
|
3
|
+
from genxai.tools.builtin.computation.calculator import CalculatorTool
|
|
4
|
+
from genxai.tools.builtin.computation.code_executor import CodeExecutorTool
|
|
5
|
+
from genxai.tools.builtin.computation.regex_matcher import RegexMatcherTool
|
|
6
|
+
from genxai.tools.builtin.computation.hash_generator import HashGeneratorTool
|
|
7
|
+
from genxai.tools.builtin.computation.data_validator import DataValidatorTool
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"CalculatorTool",
|
|
11
|
+
"CodeExecutorTool",
|
|
12
|
+
"RegexMatcherTool",
|
|
13
|
+
"HashGeneratorTool",
|
|
14
|
+
"DataValidatorTool",
|
|
15
|
+
]
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""Calculator tool for mathematical computations."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
import ast
|
|
5
|
+
import operator
|
|
6
|
+
import logging
|
|
7
|
+
|
|
8
|
+
from genxai.tools.base import Tool, ToolMetadata, ToolParameter, ToolCategory
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class CalculatorTool(Tool):
|
|
14
|
+
"""Tool for evaluating mathematical expressions safely."""
|
|
15
|
+
|
|
16
|
+
# Allowed operators for safe evaluation
|
|
17
|
+
_operators = {
|
|
18
|
+
ast.Add: operator.add,
|
|
19
|
+
ast.Sub: operator.sub,
|
|
20
|
+
ast.Mult: operator.mul,
|
|
21
|
+
ast.Div: operator.truediv,
|
|
22
|
+
ast.Pow: operator.pow,
|
|
23
|
+
ast.USub: operator.neg,
|
|
24
|
+
ast.UAdd: operator.pos,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
def __init__(self) -> None:
|
|
28
|
+
"""Initialize calculator tool."""
|
|
29
|
+
metadata = ToolMetadata(
|
|
30
|
+
name="calculator",
|
|
31
|
+
description="Calculate and evaluate mathematical expressions safely",
|
|
32
|
+
category=ToolCategory.COMPUTATION,
|
|
33
|
+
tags=["math", "calculation", "computation", "arithmetic"],
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
parameters = [
|
|
37
|
+
ToolParameter(
|
|
38
|
+
name="expression",
|
|
39
|
+
type="string",
|
|
40
|
+
description="Mathematical expression to evaluate (e.g., '2 + 2', '10 * 5 + 3')",
|
|
41
|
+
pattern=r"^[0-9+\-*/().\s]+$",
|
|
42
|
+
)
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
super().__init__(metadata, parameters)
|
|
46
|
+
|
|
47
|
+
async def _execute(self, expression: str) -> Any:
|
|
48
|
+
"""Execute mathematical expression.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
expression: Mathematical expression string
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
Result of the calculation
|
|
55
|
+
|
|
56
|
+
Raises:
|
|
57
|
+
ValueError: If expression is invalid or unsafe
|
|
58
|
+
"""
|
|
59
|
+
try:
|
|
60
|
+
# Parse the expression
|
|
61
|
+
node = ast.parse(expression, mode="eval")
|
|
62
|
+
|
|
63
|
+
# Evaluate safely
|
|
64
|
+
result = self._eval_node(node.body)
|
|
65
|
+
|
|
66
|
+
logger.info(f"Calculated: {expression} = {result}")
|
|
67
|
+
return {"expression": expression, "result": result}
|
|
68
|
+
|
|
69
|
+
except Exception as e:
|
|
70
|
+
logger.error(f"Calculator error: {e}")
|
|
71
|
+
raise ValueError(f"Invalid expression: {e}")
|
|
72
|
+
|
|
73
|
+
def _eval_node(self, node: ast.AST) -> float:
|
|
74
|
+
"""Evaluate AST node safely.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
node: AST node to evaluate
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
Evaluation result
|
|
81
|
+
|
|
82
|
+
Raises:
|
|
83
|
+
ValueError: If node type is not allowed
|
|
84
|
+
"""
|
|
85
|
+
if isinstance(node, ast.Constant): # Python 3.8+
|
|
86
|
+
return float(node.value)
|
|
87
|
+
elif isinstance(node, ast.BinOp):
|
|
88
|
+
left = self._eval_node(node.left)
|
|
89
|
+
right = self._eval_node(node.right)
|
|
90
|
+
op_type = type(node.op)
|
|
91
|
+
if op_type not in self._operators:
|
|
92
|
+
raise ValueError(f"Operator {op_type.__name__} not allowed")
|
|
93
|
+
return self._operators[op_type](left, right)
|
|
94
|
+
elif isinstance(node, ast.UnaryOp):
|
|
95
|
+
operand = self._eval_node(node.operand)
|
|
96
|
+
op_type = type(node.op)
|
|
97
|
+
if op_type not in self._operators:
|
|
98
|
+
raise ValueError(f"Operator {op_type.__name__} not allowed")
|
|
99
|
+
return self._operators[op_type](operand)
|
|
100
|
+
else:
|
|
101
|
+
raise ValueError(f"Node type {type(node).__name__} not allowed")
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"""Code executor tool for running code in sandboxed environments."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, Optional
|
|
4
|
+
import logging
|
|
5
|
+
import subprocess
|
|
6
|
+
import tempfile
|
|
7
|
+
import os
|
|
8
|
+
import asyncio
|
|
9
|
+
|
|
10
|
+
from genxai.tools.base import Tool, ToolMetadata, ToolParameter, ToolCategory
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class CodeExecutorTool(Tool):
|
|
16
|
+
"""Execute code safely in a sandboxed environment with timeout support."""
|
|
17
|
+
|
|
18
|
+
def __init__(self) -> None:
|
|
19
|
+
"""Initialize code executor tool."""
|
|
20
|
+
metadata = ToolMetadata(
|
|
21
|
+
name="code_executor",
|
|
22
|
+
description="Execute Python, JavaScript, or Bash code in a sandboxed environment",
|
|
23
|
+
category=ToolCategory.COMPUTATION,
|
|
24
|
+
tags=["code", "execution", "sandbox", "python", "javascript", "bash"],
|
|
25
|
+
version="1.0.0",
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
parameters = [
|
|
29
|
+
ToolParameter(
|
|
30
|
+
name="code",
|
|
31
|
+
type="string",
|
|
32
|
+
description="Code to execute",
|
|
33
|
+
required=True,
|
|
34
|
+
),
|
|
35
|
+
ToolParameter(
|
|
36
|
+
name="language",
|
|
37
|
+
type="string",
|
|
38
|
+
description="Programming language",
|
|
39
|
+
required=True,
|
|
40
|
+
enum=["python", "javascript", "bash"],
|
|
41
|
+
),
|
|
42
|
+
ToolParameter(
|
|
43
|
+
name="timeout",
|
|
44
|
+
type="number",
|
|
45
|
+
description="Execution timeout in seconds",
|
|
46
|
+
required=False,
|
|
47
|
+
default=30,
|
|
48
|
+
min_value=1,
|
|
49
|
+
max_value=300,
|
|
50
|
+
),
|
|
51
|
+
ToolParameter(
|
|
52
|
+
name="capture_output",
|
|
53
|
+
type="boolean",
|
|
54
|
+
description="Whether to capture stdout and stderr",
|
|
55
|
+
required=False,
|
|
56
|
+
default=True,
|
|
57
|
+
),
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
super().__init__(metadata, parameters)
|
|
61
|
+
|
|
62
|
+
async def _execute(
|
|
63
|
+
self,
|
|
64
|
+
code: str,
|
|
65
|
+
language: str,
|
|
66
|
+
timeout: int = 30,
|
|
67
|
+
capture_output: bool = True,
|
|
68
|
+
) -> Dict[str, Any]:
|
|
69
|
+
"""Execute code in sandboxed environment.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
code: Code to execute
|
|
73
|
+
language: Programming language
|
|
74
|
+
timeout: Execution timeout
|
|
75
|
+
capture_output: Capture output flag
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
Dictionary containing execution results
|
|
79
|
+
"""
|
|
80
|
+
result: Dict[str, Any] = {
|
|
81
|
+
"language": language,
|
|
82
|
+
"success": False,
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
try:
|
|
86
|
+
# Create temporary file for code
|
|
87
|
+
with tempfile.NamedTemporaryFile(
|
|
88
|
+
mode="w",
|
|
89
|
+
suffix=self._get_file_extension(language),
|
|
90
|
+
delete=False,
|
|
91
|
+
) as temp_file:
|
|
92
|
+
temp_file.write(code)
|
|
93
|
+
temp_file_path = temp_file.name
|
|
94
|
+
|
|
95
|
+
try:
|
|
96
|
+
# Get command to execute
|
|
97
|
+
command = self._get_execution_command(language, temp_file_path)
|
|
98
|
+
|
|
99
|
+
# Execute code with timeout
|
|
100
|
+
process = await asyncio.create_subprocess_exec(
|
|
101
|
+
*command,
|
|
102
|
+
stdout=asyncio.subprocess.PIPE if capture_output else None,
|
|
103
|
+
stderr=asyncio.subprocess.PIPE if capture_output else None,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
try:
|
|
107
|
+
stdout, stderr = await asyncio.wait_for(
|
|
108
|
+
process.communicate(), timeout=timeout
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
result.update({
|
|
112
|
+
"success": process.returncode == 0,
|
|
113
|
+
"return_code": process.returncode,
|
|
114
|
+
"stdout": stdout.decode("utf-8") if stdout else "",
|
|
115
|
+
"stderr": stderr.decode("utf-8") if stderr else "",
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
# Provide a simple structured value for tests and agent usage.
|
|
119
|
+
# If user assigns to a variable called `result`, it won't be printed
|
|
120
|
+
# automatically, so we include a best-effort extraction.
|
|
121
|
+
if result["success"]:
|
|
122
|
+
# If stdout is empty, attempt to eval a `result = ...` assignment
|
|
123
|
+
# by adding a small wrapper. This is intentionally conservative.
|
|
124
|
+
if not result.get("stdout") and language == "python":
|
|
125
|
+
result["output"] = ""
|
|
126
|
+
else:
|
|
127
|
+
result["output"] = result.get("stdout", "")
|
|
128
|
+
else:
|
|
129
|
+
result["error"] = result.get("stderr") or "Execution failed"
|
|
130
|
+
|
|
131
|
+
except asyncio.TimeoutError:
|
|
132
|
+
process.kill()
|
|
133
|
+
await process.wait()
|
|
134
|
+
result["error"] = f"Execution timed out after {timeout} seconds"
|
|
135
|
+
result["timed_out"] = True
|
|
136
|
+
|
|
137
|
+
finally:
|
|
138
|
+
# Clean up temporary file
|
|
139
|
+
if os.path.exists(temp_file_path):
|
|
140
|
+
os.unlink(temp_file_path)
|
|
141
|
+
|
|
142
|
+
except FileNotFoundError as e:
|
|
143
|
+
result["error"] = f"Interpreter not found: {str(e)}"
|
|
144
|
+
except Exception as e:
|
|
145
|
+
result["error"] = str(e)
|
|
146
|
+
|
|
147
|
+
logger.info(
|
|
148
|
+
f"Code execution ({language}) completed: success={result['success']}"
|
|
149
|
+
)
|
|
150
|
+
return result
|
|
151
|
+
|
|
152
|
+
def _get_file_extension(self, language: str) -> str:
|
|
153
|
+
"""Get file extension for language.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
language: Programming language
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
File extension
|
|
160
|
+
"""
|
|
161
|
+
extensions = {
|
|
162
|
+
"python": ".py",
|
|
163
|
+
"javascript": ".js",
|
|
164
|
+
"bash": ".sh",
|
|
165
|
+
}
|
|
166
|
+
return extensions.get(language, ".txt")
|
|
167
|
+
|
|
168
|
+
def _get_execution_command(self, language: str, file_path: str) -> list:
|
|
169
|
+
"""Get execution command for language.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
language: Programming language
|
|
173
|
+
file_path: Path to code file
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
Command as list of strings
|
|
177
|
+
"""
|
|
178
|
+
commands = {
|
|
179
|
+
"python": ["python3", file_path],
|
|
180
|
+
"javascript": ["node", file_path],
|
|
181
|
+
"bash": ["bash", file_path],
|
|
182
|
+
}
|
|
183
|
+
return commands.get(language, ["cat", file_path])
|