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.
Files changed (156) hide show
  1. cli/__init__.py +3 -0
  2. cli/commands/__init__.py +6 -0
  3. cli/commands/approval.py +85 -0
  4. cli/commands/audit.py +127 -0
  5. cli/commands/metrics.py +25 -0
  6. cli/commands/tool.py +389 -0
  7. cli/main.py +32 -0
  8. genxai/__init__.py +81 -0
  9. genxai/api/__init__.py +5 -0
  10. genxai/api/app.py +21 -0
  11. genxai/config/__init__.py +5 -0
  12. genxai/config/settings.py +37 -0
  13. genxai/connectors/__init__.py +19 -0
  14. genxai/connectors/base.py +122 -0
  15. genxai/connectors/kafka.py +92 -0
  16. genxai/connectors/postgres_cdc.py +95 -0
  17. genxai/connectors/registry.py +44 -0
  18. genxai/connectors/sqs.py +94 -0
  19. genxai/connectors/webhook.py +73 -0
  20. genxai/core/__init__.py +37 -0
  21. genxai/core/agent/__init__.py +32 -0
  22. genxai/core/agent/base.py +206 -0
  23. genxai/core/agent/config_io.py +59 -0
  24. genxai/core/agent/registry.py +98 -0
  25. genxai/core/agent/runtime.py +970 -0
  26. genxai/core/communication/__init__.py +6 -0
  27. genxai/core/communication/collaboration.py +44 -0
  28. genxai/core/communication/message_bus.py +192 -0
  29. genxai/core/communication/protocols.py +35 -0
  30. genxai/core/execution/__init__.py +22 -0
  31. genxai/core/execution/metadata.py +181 -0
  32. genxai/core/execution/queue.py +201 -0
  33. genxai/core/graph/__init__.py +30 -0
  34. genxai/core/graph/checkpoints.py +77 -0
  35. genxai/core/graph/edges.py +131 -0
  36. genxai/core/graph/engine.py +813 -0
  37. genxai/core/graph/executor.py +516 -0
  38. genxai/core/graph/nodes.py +161 -0
  39. genxai/core/graph/trigger_runner.py +40 -0
  40. genxai/core/memory/__init__.py +19 -0
  41. genxai/core/memory/base.py +72 -0
  42. genxai/core/memory/embedding.py +327 -0
  43. genxai/core/memory/episodic.py +448 -0
  44. genxai/core/memory/long_term.py +467 -0
  45. genxai/core/memory/manager.py +543 -0
  46. genxai/core/memory/persistence.py +297 -0
  47. genxai/core/memory/procedural.py +461 -0
  48. genxai/core/memory/semantic.py +526 -0
  49. genxai/core/memory/shared.py +62 -0
  50. genxai/core/memory/short_term.py +303 -0
  51. genxai/core/memory/vector_store.py +508 -0
  52. genxai/core/memory/working.py +211 -0
  53. genxai/core/state/__init__.py +6 -0
  54. genxai/core/state/manager.py +293 -0
  55. genxai/core/state/schema.py +115 -0
  56. genxai/llm/__init__.py +14 -0
  57. genxai/llm/base.py +150 -0
  58. genxai/llm/factory.py +329 -0
  59. genxai/llm/providers/__init__.py +1 -0
  60. genxai/llm/providers/anthropic.py +249 -0
  61. genxai/llm/providers/cohere.py +274 -0
  62. genxai/llm/providers/google.py +334 -0
  63. genxai/llm/providers/ollama.py +147 -0
  64. genxai/llm/providers/openai.py +257 -0
  65. genxai/llm/routing.py +83 -0
  66. genxai/observability/__init__.py +6 -0
  67. genxai/observability/logging.py +327 -0
  68. genxai/observability/metrics.py +494 -0
  69. genxai/observability/tracing.py +372 -0
  70. genxai/performance/__init__.py +39 -0
  71. genxai/performance/cache.py +256 -0
  72. genxai/performance/pooling.py +289 -0
  73. genxai/security/audit.py +304 -0
  74. genxai/security/auth.py +315 -0
  75. genxai/security/cost_control.py +528 -0
  76. genxai/security/default_policies.py +44 -0
  77. genxai/security/jwt.py +142 -0
  78. genxai/security/oauth.py +226 -0
  79. genxai/security/pii.py +366 -0
  80. genxai/security/policy_engine.py +82 -0
  81. genxai/security/rate_limit.py +341 -0
  82. genxai/security/rbac.py +247 -0
  83. genxai/security/validation.py +218 -0
  84. genxai/tools/__init__.py +21 -0
  85. genxai/tools/base.py +383 -0
  86. genxai/tools/builtin/__init__.py +131 -0
  87. genxai/tools/builtin/communication/__init__.py +15 -0
  88. genxai/tools/builtin/communication/email_sender.py +159 -0
  89. genxai/tools/builtin/communication/notification_manager.py +167 -0
  90. genxai/tools/builtin/communication/slack_notifier.py +118 -0
  91. genxai/tools/builtin/communication/sms_sender.py +118 -0
  92. genxai/tools/builtin/communication/webhook_caller.py +136 -0
  93. genxai/tools/builtin/computation/__init__.py +15 -0
  94. genxai/tools/builtin/computation/calculator.py +101 -0
  95. genxai/tools/builtin/computation/code_executor.py +183 -0
  96. genxai/tools/builtin/computation/data_validator.py +259 -0
  97. genxai/tools/builtin/computation/hash_generator.py +129 -0
  98. genxai/tools/builtin/computation/regex_matcher.py +201 -0
  99. genxai/tools/builtin/data/__init__.py +15 -0
  100. genxai/tools/builtin/data/csv_processor.py +213 -0
  101. genxai/tools/builtin/data/data_transformer.py +299 -0
  102. genxai/tools/builtin/data/json_processor.py +233 -0
  103. genxai/tools/builtin/data/text_analyzer.py +288 -0
  104. genxai/tools/builtin/data/xml_processor.py +175 -0
  105. genxai/tools/builtin/database/__init__.py +15 -0
  106. genxai/tools/builtin/database/database_inspector.py +157 -0
  107. genxai/tools/builtin/database/mongodb_query.py +196 -0
  108. genxai/tools/builtin/database/redis_cache.py +167 -0
  109. genxai/tools/builtin/database/sql_query.py +145 -0
  110. genxai/tools/builtin/database/vector_search.py +163 -0
  111. genxai/tools/builtin/file/__init__.py +17 -0
  112. genxai/tools/builtin/file/directory_scanner.py +214 -0
  113. genxai/tools/builtin/file/file_compressor.py +237 -0
  114. genxai/tools/builtin/file/file_reader.py +102 -0
  115. genxai/tools/builtin/file/file_writer.py +122 -0
  116. genxai/tools/builtin/file/image_processor.py +186 -0
  117. genxai/tools/builtin/file/pdf_parser.py +144 -0
  118. genxai/tools/builtin/test/__init__.py +15 -0
  119. genxai/tools/builtin/test/async_simulator.py +62 -0
  120. genxai/tools/builtin/test/data_transformer.py +99 -0
  121. genxai/tools/builtin/test/error_generator.py +82 -0
  122. genxai/tools/builtin/test/simple_math.py +94 -0
  123. genxai/tools/builtin/test/string_processor.py +72 -0
  124. genxai/tools/builtin/web/__init__.py +15 -0
  125. genxai/tools/builtin/web/api_caller.py +161 -0
  126. genxai/tools/builtin/web/html_parser.py +330 -0
  127. genxai/tools/builtin/web/http_client.py +187 -0
  128. genxai/tools/builtin/web/url_validator.py +162 -0
  129. genxai/tools/builtin/web/web_scraper.py +170 -0
  130. genxai/tools/custom/my_test_tool_2.py +9 -0
  131. genxai/tools/dynamic.py +105 -0
  132. genxai/tools/mcp_server.py +167 -0
  133. genxai/tools/persistence/__init__.py +6 -0
  134. genxai/tools/persistence/models.py +55 -0
  135. genxai/tools/persistence/service.py +322 -0
  136. genxai/tools/registry.py +227 -0
  137. genxai/tools/security/__init__.py +11 -0
  138. genxai/tools/security/limits.py +214 -0
  139. genxai/tools/security/policy.py +20 -0
  140. genxai/tools/security/sandbox.py +248 -0
  141. genxai/tools/templates.py +435 -0
  142. genxai/triggers/__init__.py +19 -0
  143. genxai/triggers/base.py +104 -0
  144. genxai/triggers/file_watcher.py +75 -0
  145. genxai/triggers/queue.py +68 -0
  146. genxai/triggers/registry.py +82 -0
  147. genxai/triggers/schedule.py +66 -0
  148. genxai/triggers/webhook.py +68 -0
  149. genxai/utils/__init__.py +1 -0
  150. genxai/utils/tokens.py +295 -0
  151. genxai_framework-0.1.0.dist-info/METADATA +495 -0
  152. genxai_framework-0.1.0.dist-info/RECORD +156 -0
  153. genxai_framework-0.1.0.dist-info/WHEEL +5 -0
  154. genxai_framework-0.1.0.dist-info/entry_points.txt +2 -0
  155. genxai_framework-0.1.0.dist-info/licenses/LICENSE +21 -0
  156. 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
+ ]
@@ -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)
@@ -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)