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,435 @@
|
|
|
1
|
+
"""Tool templates for quick tool creation."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, List
|
|
4
|
+
import logging
|
|
5
|
+
import httpx
|
|
6
|
+
from genxai.tools.base import Tool, ToolMetadata, ToolParameter, ToolCategory
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
# Template definitions
|
|
12
|
+
TEMPLATES = {
|
|
13
|
+
"api_call": {
|
|
14
|
+
"name": "API Call Tool",
|
|
15
|
+
"description": "Make HTTP requests to external APIs",
|
|
16
|
+
"parameters": ["url", "method", "headers", "body"],
|
|
17
|
+
"config_schema": {
|
|
18
|
+
"url": {"type": "string", "required": True, "description": "API endpoint URL"},
|
|
19
|
+
"method": {"type": "string", "required": True, "enum": ["GET", "POST", "PUT", "DELETE", "PATCH"]},
|
|
20
|
+
"headers": {"type": "object", "required": False, "description": "HTTP headers"},
|
|
21
|
+
"timeout": {"type": "number", "required": False, "default": 30},
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"text_processor": {
|
|
25
|
+
"name": "Text Processing Tool",
|
|
26
|
+
"description": "Process and transform text data",
|
|
27
|
+
"parameters": ["text", "operation"],
|
|
28
|
+
"config_schema": {
|
|
29
|
+
"operation": {
|
|
30
|
+
"type": "string",
|
|
31
|
+
"required": True,
|
|
32
|
+
"enum": ["uppercase", "lowercase", "reverse", "word_count", "char_count"]
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"data_transformer": {
|
|
37
|
+
"name": "Data Transformation Tool",
|
|
38
|
+
"description": "Transform data between formats",
|
|
39
|
+
"parameters": ["data", "from_format", "to_format"],
|
|
40
|
+
"config_schema": {
|
|
41
|
+
"from_format": {"type": "string", "required": True, "enum": ["json", "csv", "xml"]},
|
|
42
|
+
"to_format": {"type": "string", "required": True, "enum": ["json", "csv", "xml"]},
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"file_processor": {
|
|
46
|
+
"name": "File Processing Tool",
|
|
47
|
+
"description": "Read and process files",
|
|
48
|
+
"parameters": ["file_path", "operation"],
|
|
49
|
+
"config_schema": {
|
|
50
|
+
"operation": {"type": "string", "required": True, "enum": ["read", "write", "append", "delete"]},
|
|
51
|
+
"encoding": {"type": "string", "required": False, "default": "utf-8"},
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class APICallTool(Tool):
|
|
58
|
+
"""Template tool for making API calls."""
|
|
59
|
+
|
|
60
|
+
def __init__(
|
|
61
|
+
self,
|
|
62
|
+
name: str,
|
|
63
|
+
description: str,
|
|
64
|
+
category: ToolCategory,
|
|
65
|
+
tags: List[str],
|
|
66
|
+
config: Dict[str, Any]
|
|
67
|
+
):
|
|
68
|
+
"""Initialize API call tool."""
|
|
69
|
+
metadata = ToolMetadata(
|
|
70
|
+
name=name,
|
|
71
|
+
description=description,
|
|
72
|
+
category=category,
|
|
73
|
+
tags=tags + ["api", "http", "template"],
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
parameters = [
|
|
77
|
+
ToolParameter(
|
|
78
|
+
name="url",
|
|
79
|
+
type="string",
|
|
80
|
+
description="API endpoint URL (overrides default)",
|
|
81
|
+
required=False,
|
|
82
|
+
),
|
|
83
|
+
ToolParameter(
|
|
84
|
+
name="params",
|
|
85
|
+
type="object",
|
|
86
|
+
description="Query parameters",
|
|
87
|
+
required=False,
|
|
88
|
+
),
|
|
89
|
+
ToolParameter(
|
|
90
|
+
name="body",
|
|
91
|
+
type="object",
|
|
92
|
+
description="Request body (for POST/PUT/PATCH)",
|
|
93
|
+
required=False,
|
|
94
|
+
),
|
|
95
|
+
]
|
|
96
|
+
|
|
97
|
+
super().__init__(metadata, parameters)
|
|
98
|
+
self.config = config
|
|
99
|
+
self.default_url = config.get("url", "")
|
|
100
|
+
self.method = config.get("method", "GET").upper()
|
|
101
|
+
self.default_headers = config.get("headers", {})
|
|
102
|
+
self.timeout = config.get("timeout", 30)
|
|
103
|
+
|
|
104
|
+
async def _execute(self, **kwargs: Any) -> Any:
|
|
105
|
+
"""Execute API call."""
|
|
106
|
+
url = kwargs.get("url", self.default_url)
|
|
107
|
+
params = kwargs.get("params", {})
|
|
108
|
+
body = kwargs.get("body", None)
|
|
109
|
+
|
|
110
|
+
if not url:
|
|
111
|
+
raise ValueError("URL is required")
|
|
112
|
+
|
|
113
|
+
try:
|
|
114
|
+
async with httpx.AsyncClient(timeout=self.timeout) as client:
|
|
115
|
+
response = await client.request(
|
|
116
|
+
method=self.method,
|
|
117
|
+
url=url,
|
|
118
|
+
params=params,
|
|
119
|
+
json=body if body else None,
|
|
120
|
+
headers=self.default_headers,
|
|
121
|
+
)
|
|
122
|
+
response.raise_for_status()
|
|
123
|
+
|
|
124
|
+
# Try to parse as JSON, fallback to text
|
|
125
|
+
try:
|
|
126
|
+
data = response.json()
|
|
127
|
+
except Exception:
|
|
128
|
+
data = response.text
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
"status_code": response.status_code,
|
|
132
|
+
"data": data,
|
|
133
|
+
"headers": dict(response.headers),
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
except httpx.HTTPError as e:
|
|
137
|
+
logger.error(f"API call failed: {e}")
|
|
138
|
+
raise RuntimeError(f"API call failed: {e}")
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class TextProcessorTool(Tool):
|
|
142
|
+
"""Template tool for text processing."""
|
|
143
|
+
|
|
144
|
+
def __init__(
|
|
145
|
+
self,
|
|
146
|
+
name: str,
|
|
147
|
+
description: str,
|
|
148
|
+
category: ToolCategory,
|
|
149
|
+
tags: List[str],
|
|
150
|
+
config: Dict[str, Any]
|
|
151
|
+
):
|
|
152
|
+
"""Initialize text processor tool."""
|
|
153
|
+
metadata = ToolMetadata(
|
|
154
|
+
name=name,
|
|
155
|
+
description=description,
|
|
156
|
+
category=category,
|
|
157
|
+
tags=tags + ["text", "processing", "template"],
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
parameters = [
|
|
161
|
+
ToolParameter(
|
|
162
|
+
name="text",
|
|
163
|
+
type="string",
|
|
164
|
+
description="Text to process",
|
|
165
|
+
required=True,
|
|
166
|
+
),
|
|
167
|
+
ToolParameter(
|
|
168
|
+
name="operation",
|
|
169
|
+
type="string",
|
|
170
|
+
description="Operation to perform",
|
|
171
|
+
required=False,
|
|
172
|
+
enum=["uppercase", "lowercase", "reverse", "word_count", "char_count"],
|
|
173
|
+
),
|
|
174
|
+
]
|
|
175
|
+
|
|
176
|
+
super().__init__(metadata, parameters)
|
|
177
|
+
self.config = config
|
|
178
|
+
self.default_operation = config.get("operation", "uppercase")
|
|
179
|
+
|
|
180
|
+
async def _execute(self, text: str, operation: str = None, **kwargs: Any) -> Any:
|
|
181
|
+
"""Execute text processing."""
|
|
182
|
+
op = operation or self.default_operation
|
|
183
|
+
|
|
184
|
+
operations = {
|
|
185
|
+
"uppercase": lambda t: t.upper(),
|
|
186
|
+
"lowercase": lambda t: t.lower(),
|
|
187
|
+
"reverse": lambda t: t[::-1],
|
|
188
|
+
"word_count": lambda t: len(t.split()),
|
|
189
|
+
"char_count": lambda t: len(t),
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if op not in operations:
|
|
193
|
+
raise ValueError(f"Unknown operation: {op}")
|
|
194
|
+
|
|
195
|
+
result = operations[op](text)
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
"operation": op,
|
|
199
|
+
"input": text,
|
|
200
|
+
"result": result,
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
class DataTransformerTool(Tool):
|
|
205
|
+
"""Template tool for data transformation."""
|
|
206
|
+
|
|
207
|
+
def __init__(
|
|
208
|
+
self,
|
|
209
|
+
name: str,
|
|
210
|
+
description: str,
|
|
211
|
+
category: ToolCategory,
|
|
212
|
+
tags: List[str],
|
|
213
|
+
config: Dict[str, Any]
|
|
214
|
+
):
|
|
215
|
+
"""Initialize data transformer tool."""
|
|
216
|
+
metadata = ToolMetadata(
|
|
217
|
+
name=name,
|
|
218
|
+
description=description,
|
|
219
|
+
category=category,
|
|
220
|
+
tags=tags + ["data", "transformation", "template"],
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
parameters = [
|
|
224
|
+
ToolParameter(
|
|
225
|
+
name="data",
|
|
226
|
+
type="string",
|
|
227
|
+
description="Data to transform",
|
|
228
|
+
required=True,
|
|
229
|
+
),
|
|
230
|
+
ToolParameter(
|
|
231
|
+
name="from_format",
|
|
232
|
+
type="string",
|
|
233
|
+
description="Source format",
|
|
234
|
+
required=False,
|
|
235
|
+
enum=["json", "csv", "xml"],
|
|
236
|
+
),
|
|
237
|
+
ToolParameter(
|
|
238
|
+
name="to_format",
|
|
239
|
+
type="string",
|
|
240
|
+
description="Target format",
|
|
241
|
+
required=False,
|
|
242
|
+
enum=["json", "csv", "xml"],
|
|
243
|
+
),
|
|
244
|
+
]
|
|
245
|
+
|
|
246
|
+
super().__init__(metadata, parameters)
|
|
247
|
+
self.config = config
|
|
248
|
+
self.default_from = config.get("from_format", "json")
|
|
249
|
+
self.default_to = config.get("to_format", "json")
|
|
250
|
+
|
|
251
|
+
async def _execute(
|
|
252
|
+
self,
|
|
253
|
+
data: str,
|
|
254
|
+
from_format: str = None,
|
|
255
|
+
to_format: str = None,
|
|
256
|
+
**kwargs: Any
|
|
257
|
+
) -> Any:
|
|
258
|
+
"""Execute data transformation."""
|
|
259
|
+
import json
|
|
260
|
+
import csv
|
|
261
|
+
import io
|
|
262
|
+
|
|
263
|
+
from_fmt = from_format or self.default_from
|
|
264
|
+
to_fmt = to_format or self.default_to
|
|
265
|
+
|
|
266
|
+
# Parse input
|
|
267
|
+
if from_fmt == "json":
|
|
268
|
+
parsed_data = json.loads(data)
|
|
269
|
+
elif from_fmt == "csv":
|
|
270
|
+
reader = csv.DictReader(io.StringIO(data))
|
|
271
|
+
parsed_data = list(reader)
|
|
272
|
+
else:
|
|
273
|
+
raise ValueError(f"Unsupported format: {from_fmt}")
|
|
274
|
+
|
|
275
|
+
# Convert output
|
|
276
|
+
if to_fmt == "json":
|
|
277
|
+
result = json.dumps(parsed_data, indent=2)
|
|
278
|
+
elif to_fmt == "csv":
|
|
279
|
+
if not isinstance(parsed_data, list):
|
|
280
|
+
parsed_data = [parsed_data]
|
|
281
|
+
|
|
282
|
+
output = io.StringIO()
|
|
283
|
+
if parsed_data:
|
|
284
|
+
writer = csv.DictWriter(output, fieldnames=parsed_data[0].keys())
|
|
285
|
+
writer.writeheader()
|
|
286
|
+
writer.writerows(parsed_data)
|
|
287
|
+
result = output.getvalue()
|
|
288
|
+
else:
|
|
289
|
+
raise ValueError(f"Unsupported format: {to_fmt}")
|
|
290
|
+
|
|
291
|
+
return {
|
|
292
|
+
"from_format": from_fmt,
|
|
293
|
+
"to_format": to_fmt,
|
|
294
|
+
"result": result,
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
class FileProcessorTool(Tool):
|
|
299
|
+
"""Template tool for file processing."""
|
|
300
|
+
|
|
301
|
+
def __init__(
|
|
302
|
+
self,
|
|
303
|
+
name: str,
|
|
304
|
+
description: str,
|
|
305
|
+
category: ToolCategory,
|
|
306
|
+
tags: List[str],
|
|
307
|
+
config: Dict[str, Any]
|
|
308
|
+
):
|
|
309
|
+
"""Initialize file processor tool."""
|
|
310
|
+
metadata = ToolMetadata(
|
|
311
|
+
name=name,
|
|
312
|
+
description=description,
|
|
313
|
+
category=category,
|
|
314
|
+
tags=tags + ["file", "io", "template"],
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
parameters = [
|
|
318
|
+
ToolParameter(
|
|
319
|
+
name="file_path",
|
|
320
|
+
type="string",
|
|
321
|
+
description="Path to file",
|
|
322
|
+
required=True,
|
|
323
|
+
),
|
|
324
|
+
ToolParameter(
|
|
325
|
+
name="operation",
|
|
326
|
+
type="string",
|
|
327
|
+
description="Operation to perform",
|
|
328
|
+
required=False,
|
|
329
|
+
enum=["read", "write", "append"],
|
|
330
|
+
),
|
|
331
|
+
ToolParameter(
|
|
332
|
+
name="content",
|
|
333
|
+
type="string",
|
|
334
|
+
description="Content to write (for write/append operations)",
|
|
335
|
+
required=False,
|
|
336
|
+
),
|
|
337
|
+
]
|
|
338
|
+
|
|
339
|
+
super().__init__(metadata, parameters)
|
|
340
|
+
self.config = config
|
|
341
|
+
self.encoding = config.get("encoding", "utf-8")
|
|
342
|
+
|
|
343
|
+
async def _execute(
|
|
344
|
+
self,
|
|
345
|
+
file_path: str,
|
|
346
|
+
operation: str = "read",
|
|
347
|
+
content: str = None,
|
|
348
|
+
**kwargs: Any
|
|
349
|
+
) -> Any:
|
|
350
|
+
"""Execute file operation."""
|
|
351
|
+
import aiofiles
|
|
352
|
+
import os
|
|
353
|
+
|
|
354
|
+
try:
|
|
355
|
+
if operation == "read":
|
|
356
|
+
async with aiofiles.open(file_path, 'r', encoding=self.encoding) as f:
|
|
357
|
+
data = await f.read()
|
|
358
|
+
return {"operation": "read", "file_path": file_path, "content": data}
|
|
359
|
+
|
|
360
|
+
elif operation == "write":
|
|
361
|
+
if content is None:
|
|
362
|
+
raise ValueError("Content is required for write operation")
|
|
363
|
+
async with aiofiles.open(file_path, 'w', encoding=self.encoding) as f:
|
|
364
|
+
await f.write(content)
|
|
365
|
+
return {"operation": "write", "file_path": file_path, "bytes_written": len(content)}
|
|
366
|
+
|
|
367
|
+
elif operation == "append":
|
|
368
|
+
if content is None:
|
|
369
|
+
raise ValueError("Content is required for append operation")
|
|
370
|
+
async with aiofiles.open(file_path, 'a', encoding=self.encoding) as f:
|
|
371
|
+
await f.write(content)
|
|
372
|
+
return {"operation": "append", "file_path": file_path, "bytes_written": len(content)}
|
|
373
|
+
|
|
374
|
+
else:
|
|
375
|
+
raise ValueError(f"Unknown operation: {operation}")
|
|
376
|
+
|
|
377
|
+
except Exception as e:
|
|
378
|
+
logger.error(f"File operation failed: {e}")
|
|
379
|
+
raise RuntimeError(f"File operation failed: {e}")
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def create_tool_from_template(
|
|
383
|
+
name: str,
|
|
384
|
+
description: str,
|
|
385
|
+
category: ToolCategory,
|
|
386
|
+
tags: List[str],
|
|
387
|
+
template: str,
|
|
388
|
+
config: Dict[str, Any]
|
|
389
|
+
) -> Tool:
|
|
390
|
+
"""Create a tool from a template.
|
|
391
|
+
|
|
392
|
+
Args:
|
|
393
|
+
name: Tool name
|
|
394
|
+
description: Tool description
|
|
395
|
+
category: Tool category
|
|
396
|
+
tags: Tool tags
|
|
397
|
+
template: Template name
|
|
398
|
+
config: Template configuration
|
|
399
|
+
|
|
400
|
+
Returns:
|
|
401
|
+
Tool instance
|
|
402
|
+
|
|
403
|
+
Raises:
|
|
404
|
+
ValueError: If template is unknown
|
|
405
|
+
"""
|
|
406
|
+
template_classes = {
|
|
407
|
+
"api_call": APICallTool,
|
|
408
|
+
"text_processor": TextProcessorTool,
|
|
409
|
+
"data_transformer": DataTransformerTool,
|
|
410
|
+
"file_processor": FileProcessorTool,
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
if template not in template_classes:
|
|
414
|
+
raise ValueError(f"Unknown template: {template}")
|
|
415
|
+
|
|
416
|
+
tool_class = template_classes[template]
|
|
417
|
+
return tool_class(name, description, category, tags, config)
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def get_available_templates() -> List[Dict[str, Any]]:
|
|
421
|
+
"""Get list of available templates.
|
|
422
|
+
|
|
423
|
+
Returns:
|
|
424
|
+
List of template definitions
|
|
425
|
+
"""
|
|
426
|
+
return [
|
|
427
|
+
{
|
|
428
|
+
"id": template_id,
|
|
429
|
+
"name": template_data["name"],
|
|
430
|
+
"description": template_data["description"],
|
|
431
|
+
"parameters": template_data["parameters"],
|
|
432
|
+
"config_schema": template_data["config_schema"],
|
|
433
|
+
}
|
|
434
|
+
for template_id, template_data in TEMPLATES.items()
|
|
435
|
+
]
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Trigger system for GenXAI workflows."""
|
|
2
|
+
|
|
3
|
+
from genxai.triggers.base import BaseTrigger, TriggerEvent, TriggerStatus
|
|
4
|
+
from genxai.triggers.registry import TriggerRegistry
|
|
5
|
+
from genxai.triggers.webhook import WebhookTrigger
|
|
6
|
+
from genxai.triggers.schedule import ScheduleTrigger
|
|
7
|
+
from genxai.triggers.queue import QueueTrigger
|
|
8
|
+
from genxai.triggers.file_watcher import FileWatcherTrigger
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"BaseTrigger",
|
|
12
|
+
"TriggerEvent",
|
|
13
|
+
"TriggerStatus",
|
|
14
|
+
"TriggerRegistry",
|
|
15
|
+
"WebhookTrigger",
|
|
16
|
+
"ScheduleTrigger",
|
|
17
|
+
"FileWatcherTrigger",
|
|
18
|
+
"QueueTrigger",
|
|
19
|
+
]
|
genxai/triggers/base.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""Base trigger abstractions for GenXAI workflows."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from enum import Enum
|
|
9
|
+
from typing import Any, Awaitable, Callable, Dict, Optional
|
|
10
|
+
import asyncio
|
|
11
|
+
import logging
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class TriggerStatus(str, Enum):
|
|
17
|
+
"""Lifecycle status for triggers."""
|
|
18
|
+
|
|
19
|
+
STOPPED = "stopped"
|
|
20
|
+
STARTING = "starting"
|
|
21
|
+
RUNNING = "running"
|
|
22
|
+
STOPPING = "stopping"
|
|
23
|
+
ERROR = "error"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class TriggerEvent:
|
|
28
|
+
"""Event emitted by triggers to start workflows."""
|
|
29
|
+
|
|
30
|
+
trigger_id: str
|
|
31
|
+
payload: Dict[str, Any]
|
|
32
|
+
timestamp: datetime = field(default_factory=datetime.utcnow)
|
|
33
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class BaseTrigger(ABC):
|
|
37
|
+
"""Abstract base class for workflow triggers."""
|
|
38
|
+
|
|
39
|
+
def __init__(self, trigger_id: str, name: Optional[str] = None) -> None:
|
|
40
|
+
self.trigger_id = trigger_id
|
|
41
|
+
self.name = name or trigger_id
|
|
42
|
+
self.status: TriggerStatus = TriggerStatus.STOPPED
|
|
43
|
+
self._callbacks: list[Callable[[TriggerEvent], Awaitable[None]]] = []
|
|
44
|
+
self._lock = asyncio.Lock()
|
|
45
|
+
|
|
46
|
+
def on_event(self, callback: Callable[[TriggerEvent], Awaitable[None]]) -> None:
|
|
47
|
+
"""Register a callback to receive trigger events."""
|
|
48
|
+
self._callbacks.append(callback)
|
|
49
|
+
|
|
50
|
+
async def emit(self, payload: Dict[str, Any], metadata: Optional[Dict[str, Any]] = None) -> None:
|
|
51
|
+
"""Emit a trigger event to registered callbacks."""
|
|
52
|
+
event = TriggerEvent(
|
|
53
|
+
trigger_id=self.trigger_id,
|
|
54
|
+
payload=payload,
|
|
55
|
+
metadata=metadata or {},
|
|
56
|
+
)
|
|
57
|
+
if not self._callbacks:
|
|
58
|
+
logger.warning("Trigger %s emitted event with no subscribers", self.trigger_id)
|
|
59
|
+
return
|
|
60
|
+
|
|
61
|
+
await asyncio.gather(*[callback(event) for callback in self._callbacks])
|
|
62
|
+
|
|
63
|
+
async def start(self) -> None:
|
|
64
|
+
"""Start the trigger."""
|
|
65
|
+
async with self._lock:
|
|
66
|
+
if self.status in {TriggerStatus.RUNNING, TriggerStatus.STARTING}:
|
|
67
|
+
return
|
|
68
|
+
self.status = TriggerStatus.STARTING
|
|
69
|
+
try:
|
|
70
|
+
await self._start()
|
|
71
|
+
self.status = TriggerStatus.RUNNING
|
|
72
|
+
logger.info("Trigger started: %s", self.trigger_id)
|
|
73
|
+
except Exception as exc:
|
|
74
|
+
self.status = TriggerStatus.ERROR
|
|
75
|
+
logger.error("Failed to start trigger %s: %s", self.trigger_id, exc)
|
|
76
|
+
raise
|
|
77
|
+
|
|
78
|
+
async def stop(self) -> None:
|
|
79
|
+
"""Stop the trigger."""
|
|
80
|
+
async with self._lock:
|
|
81
|
+
if self.status in {TriggerStatus.STOPPED, TriggerStatus.STOPPING}:
|
|
82
|
+
return
|
|
83
|
+
self.status = TriggerStatus.STOPPING
|
|
84
|
+
try:
|
|
85
|
+
await self._stop()
|
|
86
|
+
self.status = TriggerStatus.STOPPED
|
|
87
|
+
logger.info("Trigger stopped: %s", self.trigger_id)
|
|
88
|
+
except Exception as exc:
|
|
89
|
+
self.status = TriggerStatus.ERROR
|
|
90
|
+
logger.error("Failed to stop trigger %s: %s", self.trigger_id, exc)
|
|
91
|
+
raise
|
|
92
|
+
|
|
93
|
+
@abstractmethod
|
|
94
|
+
async def _start(self) -> None:
|
|
95
|
+
"""Implement trigger-specific start logic."""
|
|
96
|
+
raise NotImplementedError
|
|
97
|
+
|
|
98
|
+
@abstractmethod
|
|
99
|
+
async def _stop(self) -> None:
|
|
100
|
+
"""Implement trigger-specific stop logic."""
|
|
101
|
+
raise NotImplementedError
|
|
102
|
+
|
|
103
|
+
def __repr__(self) -> str:
|
|
104
|
+
return f"{self.__class__.__name__}(id={self.trigger_id}, status={self.status})"
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""File watcher trigger implementation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any, Dict, Optional
|
|
7
|
+
import logging
|
|
8
|
+
|
|
9
|
+
from genxai.triggers.base import BaseTrigger
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class FileWatcherTrigger(BaseTrigger):
|
|
15
|
+
"""Trigger that emits events on filesystem changes.
|
|
16
|
+
|
|
17
|
+
Requires `watchdog` to be installed.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(
|
|
21
|
+
self,
|
|
22
|
+
trigger_id: str,
|
|
23
|
+
watch_path: str | Path,
|
|
24
|
+
recursive: bool = True,
|
|
25
|
+
name: Optional[str] = None,
|
|
26
|
+
) -> None:
|
|
27
|
+
super().__init__(trigger_id=trigger_id, name=name)
|
|
28
|
+
self.watch_path = Path(watch_path)
|
|
29
|
+
self.recursive = recursive
|
|
30
|
+
self._observer = None
|
|
31
|
+
|
|
32
|
+
async def _start(self) -> None:
|
|
33
|
+
try:
|
|
34
|
+
from watchdog.observers import Observer
|
|
35
|
+
from watchdog.events import FileSystemEventHandler
|
|
36
|
+
except ImportError as exc:
|
|
37
|
+
raise ImportError(
|
|
38
|
+
"watchdog is required for FileWatcherTrigger. Install with: pip install watchdog"
|
|
39
|
+
) from exc
|
|
40
|
+
|
|
41
|
+
class _Handler(FileSystemEventHandler):
|
|
42
|
+
def __init__(self, outer: "FileWatcherTrigger") -> None:
|
|
43
|
+
self.outer = outer
|
|
44
|
+
|
|
45
|
+
def on_any_event(self, event):
|
|
46
|
+
payload: Dict[str, Any] = {
|
|
47
|
+
"event_type": event.event_type,
|
|
48
|
+
"src_path": event.src_path,
|
|
49
|
+
"is_directory": event.is_directory,
|
|
50
|
+
}
|
|
51
|
+
if getattr(event, "dest_path", None):
|
|
52
|
+
payload["dest_path"] = event.dest_path
|
|
53
|
+
try:
|
|
54
|
+
import asyncio
|
|
55
|
+
|
|
56
|
+
asyncio.run_coroutine_threadsafe(
|
|
57
|
+
self.outer.emit(payload=payload),
|
|
58
|
+
asyncio.get_event_loop(),
|
|
59
|
+
)
|
|
60
|
+
except Exception as exc:
|
|
61
|
+
logger.error("Failed to emit file event: %s", exc)
|
|
62
|
+
|
|
63
|
+
handler = _Handler(self)
|
|
64
|
+
observer = Observer()
|
|
65
|
+
observer.schedule(handler, str(self.watch_path), recursive=self.recursive)
|
|
66
|
+
observer.start()
|
|
67
|
+
self._observer = observer
|
|
68
|
+
logger.info("FileWatcherTrigger %s started", self.trigger_id)
|
|
69
|
+
|
|
70
|
+
async def _stop(self) -> None:
|
|
71
|
+
if self._observer:
|
|
72
|
+
self._observer.stop()
|
|
73
|
+
self._observer.join(timeout=5)
|
|
74
|
+
self._observer = None
|
|
75
|
+
logger.info("FileWatcherTrigger %s stopped", self.trigger_id)
|
genxai/triggers/queue.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Queue-based trigger implementation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Dict, Optional
|
|
6
|
+
import asyncio
|
|
7
|
+
import logging
|
|
8
|
+
|
|
9
|
+
from genxai.triggers.base import BaseTrigger
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class QueueTrigger(BaseTrigger):
|
|
15
|
+
"""Async queue trigger.
|
|
16
|
+
|
|
17
|
+
This trigger listens to an asyncio.Queue instance. It can be swapped
|
|
18
|
+
for custom queue backends by feeding messages into the queue.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
trigger_id: str,
|
|
24
|
+
queue: Optional[asyncio.Queue] = None,
|
|
25
|
+
name: Optional[str] = None,
|
|
26
|
+
poll_interval: float = 0.1,
|
|
27
|
+
) -> None:
|
|
28
|
+
super().__init__(trigger_id=trigger_id, name=name)
|
|
29
|
+
self.queue = queue or asyncio.Queue()
|
|
30
|
+
self.poll_interval = poll_interval
|
|
31
|
+
self._task: Optional[asyncio.Task] = None
|
|
32
|
+
|
|
33
|
+
async def _start(self) -> None:
|
|
34
|
+
self._task = asyncio.create_task(self._listen())
|
|
35
|
+
logger.info("QueueTrigger %s started", self.trigger_id)
|
|
36
|
+
|
|
37
|
+
async def _stop(self) -> None:
|
|
38
|
+
if self._task:
|
|
39
|
+
self._task.cancel()
|
|
40
|
+
try:
|
|
41
|
+
await self._task
|
|
42
|
+
except asyncio.CancelledError:
|
|
43
|
+
pass
|
|
44
|
+
self._task = None
|
|
45
|
+
logger.info("QueueTrigger %s stopped", self.trigger_id)
|
|
46
|
+
|
|
47
|
+
async def _listen(self) -> None:
|
|
48
|
+
while True:
|
|
49
|
+
try:
|
|
50
|
+
message = await asyncio.wait_for(self.queue.get(), timeout=self.poll_interval)
|
|
51
|
+
payload: Dict[str, Any]
|
|
52
|
+
if isinstance(message, dict):
|
|
53
|
+
payload = message
|
|
54
|
+
else:
|
|
55
|
+
payload = {"message": message}
|
|
56
|
+
await self.emit(payload=payload)
|
|
57
|
+
self.queue.task_done()
|
|
58
|
+
except asyncio.TimeoutError:
|
|
59
|
+
continue
|
|
60
|
+
except asyncio.CancelledError:
|
|
61
|
+
break
|
|
62
|
+
except Exception as exc:
|
|
63
|
+
logger.error("QueueTrigger %s error: %s", self.trigger_id, exc)
|
|
64
|
+
await asyncio.sleep(self.poll_interval)
|
|
65
|
+
|
|
66
|
+
async def enqueue(self, payload: Dict[str, Any]) -> None:
|
|
67
|
+
"""Helper to push payloads into the trigger queue."""
|
|
68
|
+
await self.queue.put(payload)
|