genxai-framework 0.1.0__py3-none-any.whl → 0.1.2__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/commands/__init__.py +3 -1
- cli/commands/connector.py +309 -0
- cli/commands/workflow.py +80 -0
- cli/main.py +3 -1
- genxai/__init__.py +33 -0
- genxai/agents/__init__.py +8 -0
- genxai/agents/presets.py +53 -0
- genxai/connectors/__init__.py +10 -0
- genxai/connectors/base.py +3 -3
- genxai/connectors/config_store.py +106 -0
- genxai/connectors/github.py +117 -0
- genxai/connectors/google_workspace.py +124 -0
- genxai/connectors/jira.py +108 -0
- genxai/connectors/notion.py +97 -0
- genxai/connectors/slack.py +121 -0
- genxai/core/agent/config_io.py +32 -1
- genxai/core/agent/runtime.py +41 -4
- genxai/core/graph/__init__.py +3 -0
- genxai/core/graph/engine.py +218 -11
- genxai/core/graph/executor.py +103 -10
- genxai/core/graph/nodes.py +28 -0
- genxai/core/graph/workflow_io.py +199 -0
- genxai/flows/__init__.py +33 -0
- genxai/flows/auction.py +66 -0
- genxai/flows/base.py +134 -0
- genxai/flows/conditional.py +45 -0
- genxai/flows/coordinator_worker.py +62 -0
- genxai/flows/critic_review.py +62 -0
- genxai/flows/ensemble_voting.py +49 -0
- genxai/flows/loop.py +42 -0
- genxai/flows/map_reduce.py +61 -0
- genxai/flows/p2p.py +146 -0
- genxai/flows/parallel.py +27 -0
- genxai/flows/round_robin.py +24 -0
- genxai/flows/router.py +45 -0
- genxai/flows/selector.py +63 -0
- genxai/flows/subworkflow.py +35 -0
- genxai/llm/factory.py +17 -10
- genxai/llm/providers/anthropic.py +116 -1
- genxai/observability/logging.py +2 -2
- genxai/security/auth.py +10 -6
- genxai/security/cost_control.py +6 -6
- genxai/security/jwt.py +2 -2
- genxai/security/pii.py +2 -2
- genxai/tools/builtin/__init__.py +3 -0
- genxai/tools/builtin/communication/human_input.py +32 -0
- genxai/tools/custom/test-2.py +19 -0
- genxai/tools/custom/test_tool_ui.py +9 -0
- genxai/tools/persistence/service.py +3 -3
- genxai/triggers/schedule.py +2 -2
- genxai/utils/tokens.py +6 -0
- {genxai_framework-0.1.0.dist-info → genxai_framework-0.1.2.dist-info}/METADATA +63 -12
- {genxai_framework-0.1.0.dist-info → genxai_framework-0.1.2.dist-info}/RECORD +57 -28
- {genxai_framework-0.1.0.dist-info → genxai_framework-0.1.2.dist-info}/WHEEL +0 -0
- {genxai_framework-0.1.0.dist-info → genxai_framework-0.1.2.dist-info}/entry_points.txt +0 -0
- {genxai_framework-0.1.0.dist-info → genxai_framework-0.1.2.dist-info}/licenses/LICENSE +0 -0
- {genxai_framework-0.1.0.dist-info → genxai_framework-0.1.2.dist-info}/top_level.txt +0 -0
cli/commands/__init__.py
CHANGED
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
"""Connector management CLI commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import json
|
|
7
|
+
from typing import Any, Dict, Optional
|
|
8
|
+
|
|
9
|
+
import click
|
|
10
|
+
from rich.console import Console
|
|
11
|
+
from rich.table import Table
|
|
12
|
+
|
|
13
|
+
from genxai.connectors import (
|
|
14
|
+
Connector,
|
|
15
|
+
KafkaConnector,
|
|
16
|
+
SQSConnector,
|
|
17
|
+
PostgresCDCConnector,
|
|
18
|
+
WebhookConnector,
|
|
19
|
+
SlackConnector,
|
|
20
|
+
GitHubConnector,
|
|
21
|
+
NotionConnector,
|
|
22
|
+
JiraConnector,
|
|
23
|
+
GoogleWorkspaceConnector,
|
|
24
|
+
)
|
|
25
|
+
from genxai.connectors.config_store import ConnectorConfigEntry, ConnectorConfigStore
|
|
26
|
+
|
|
27
|
+
console = Console()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
CONNECTOR_CATALOG: Dict[str, Dict[str, Any]] = {
|
|
31
|
+
"kafka": {
|
|
32
|
+
"class": KafkaConnector,
|
|
33
|
+
"required": ["topic", "bootstrap_servers"],
|
|
34
|
+
"description": "Kafka consumer connector",
|
|
35
|
+
},
|
|
36
|
+
"sqs": {
|
|
37
|
+
"class": SQSConnector,
|
|
38
|
+
"required": ["queue_url"],
|
|
39
|
+
"description": "AWS SQS connector",
|
|
40
|
+
},
|
|
41
|
+
"postgres_cdc": {
|
|
42
|
+
"class": PostgresCDCConnector,
|
|
43
|
+
"required": ["dsn"],
|
|
44
|
+
"description": "Postgres CDC connector",
|
|
45
|
+
},
|
|
46
|
+
"webhook": {
|
|
47
|
+
"class": WebhookConnector,
|
|
48
|
+
"required": [],
|
|
49
|
+
"description": "Inbound webhook connector",
|
|
50
|
+
},
|
|
51
|
+
"slack": {
|
|
52
|
+
"class": SlackConnector,
|
|
53
|
+
"required": ["bot_token"],
|
|
54
|
+
"description": "Slack Web API connector",
|
|
55
|
+
},
|
|
56
|
+
"github": {
|
|
57
|
+
"class": GitHubConnector,
|
|
58
|
+
"required": ["token"],
|
|
59
|
+
"description": "GitHub REST API connector",
|
|
60
|
+
},
|
|
61
|
+
"notion": {
|
|
62
|
+
"class": NotionConnector,
|
|
63
|
+
"required": ["token"],
|
|
64
|
+
"description": "Notion API connector",
|
|
65
|
+
},
|
|
66
|
+
"jira": {
|
|
67
|
+
"class": JiraConnector,
|
|
68
|
+
"required": ["email", "api_token", "base_url"],
|
|
69
|
+
"description": "Jira Cloud REST API connector",
|
|
70
|
+
},
|
|
71
|
+
"google_workspace": {
|
|
72
|
+
"class": GoogleWorkspaceConnector,
|
|
73
|
+
"required": ["access_token"],
|
|
74
|
+
"description": "Google Workspace APIs connector",
|
|
75
|
+
},
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@click.group()
|
|
80
|
+
def connector() -> None:
|
|
81
|
+
"""Manage GenXAI connectors."""
|
|
82
|
+
pass
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@connector.command("list")
|
|
86
|
+
@click.option("--format", "output_format", type=click.Choice(["table", "json"]), default="table")
|
|
87
|
+
def list_connectors(output_format: str) -> None:
|
|
88
|
+
"""List available connector types."""
|
|
89
|
+
if output_format == "json":
|
|
90
|
+
payload = {
|
|
91
|
+
name: {
|
|
92
|
+
"required": meta["required"],
|
|
93
|
+
"description": meta["description"],
|
|
94
|
+
}
|
|
95
|
+
for name, meta in CONNECTOR_CATALOG.items()
|
|
96
|
+
}
|
|
97
|
+
click.echo(json.dumps(payload, indent=2))
|
|
98
|
+
return
|
|
99
|
+
|
|
100
|
+
table = Table(title="GenXAI Connectors")
|
|
101
|
+
table.add_column("Type", style="cyan")
|
|
102
|
+
table.add_column("Required Fields", style="green")
|
|
103
|
+
table.add_column("Description", style="white")
|
|
104
|
+
for name, meta in CONNECTOR_CATALOG.items():
|
|
105
|
+
table.add_row(name, ", ".join(meta["required"]) or "(none)", meta["description"])
|
|
106
|
+
console.print(table)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@connector.command("validate")
|
|
110
|
+
@click.option("--type", "connector_type", required=True, help="Connector type")
|
|
111
|
+
@click.option("--connector-id", default="connector", show_default=True)
|
|
112
|
+
@click.option("--config", help="JSON config payload for the connector")
|
|
113
|
+
@click.option("--config-name", help="Use a saved connector config")
|
|
114
|
+
def validate(
|
|
115
|
+
connector_type: str,
|
|
116
|
+
connector_id: str,
|
|
117
|
+
config: Optional[str],
|
|
118
|
+
config_name: Optional[str],
|
|
119
|
+
) -> None:
|
|
120
|
+
"""Validate connector configuration without starting it."""
|
|
121
|
+
connector_meta = CONNECTOR_CATALOG.get(connector_type)
|
|
122
|
+
if not connector_meta:
|
|
123
|
+
raise click.ClickException(
|
|
124
|
+
f"Unknown connector type '{connector_type}'. Use 'genxai connector list' to see options."
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
config_data = _load_config(config, config_name)
|
|
128
|
+
missing = [field for field in connector_meta["required"] if field not in config_data]
|
|
129
|
+
if missing:
|
|
130
|
+
raise click.ClickException(f"Missing required fields: {', '.join(missing)}")
|
|
131
|
+
|
|
132
|
+
connector_instance = _build_connector(connector_id, connector_meta["class"], config_data)
|
|
133
|
+
try:
|
|
134
|
+
asyncio.run(connector_instance.validate_config())
|
|
135
|
+
console.print("[green]✓ Connector configuration valid[/green]")
|
|
136
|
+
except Exception as exc:
|
|
137
|
+
raise click.ClickException(str(exc)) from exc
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@connector.command("start")
|
|
141
|
+
@click.option("--type", "connector_type", required=True, help="Connector type")
|
|
142
|
+
@click.option("--connector-id", default="connector", show_default=True)
|
|
143
|
+
@click.option("--config", help="JSON config payload for the connector")
|
|
144
|
+
@click.option("--config-name", help="Use a saved connector config")
|
|
145
|
+
def start(connector_type: str, connector_id: str, config: Optional[str], config_name: Optional[str]) -> None:
|
|
146
|
+
"""Start a connector instance for quick validation."""
|
|
147
|
+
connector_instance = _build_from_cli(connector_type, connector_id, config, config_name)
|
|
148
|
+
try:
|
|
149
|
+
asyncio.run(connector_instance.start())
|
|
150
|
+
console.print("[green]✓ Connector started[/green]")
|
|
151
|
+
except Exception as exc:
|
|
152
|
+
raise click.ClickException(str(exc)) from exc
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
@connector.command("stop")
|
|
156
|
+
@click.option("--type", "connector_type", required=True, help="Connector type")
|
|
157
|
+
@click.option("--connector-id", default="connector", show_default=True)
|
|
158
|
+
@click.option("--config", help="JSON config payload for the connector")
|
|
159
|
+
@click.option("--config-name", help="Use a saved connector config")
|
|
160
|
+
def stop(connector_type: str, connector_id: str, config: Optional[str], config_name: Optional[str]) -> None:
|
|
161
|
+
"""Stop a connector instance for quick validation."""
|
|
162
|
+
connector_instance = _build_from_cli(connector_type, connector_id, config, config_name)
|
|
163
|
+
try:
|
|
164
|
+
asyncio.run(connector_instance.stop())
|
|
165
|
+
console.print("[green]✓ Connector stopped[/green]")
|
|
166
|
+
except Exception as exc:
|
|
167
|
+
raise click.ClickException(str(exc)) from exc
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
@connector.command("health")
|
|
171
|
+
@click.option("--type", "connector_type", required=True, help="Connector type")
|
|
172
|
+
@click.option("--connector-id", default="connector", show_default=True)
|
|
173
|
+
@click.option("--config", help="JSON config payload for the connector")
|
|
174
|
+
@click.option("--format", "output_format", type=click.Choice(["json", "table"]), default="json")
|
|
175
|
+
@click.option("--config-name", help="Use a saved connector config")
|
|
176
|
+
def health(
|
|
177
|
+
connector_type: str,
|
|
178
|
+
connector_id: str,
|
|
179
|
+
config: Optional[str],
|
|
180
|
+
output_format: str,
|
|
181
|
+
config_name: Optional[str],
|
|
182
|
+
) -> None:
|
|
183
|
+
"""Run a connector health check without starting it."""
|
|
184
|
+
connector_instance = _build_from_cli(connector_type, connector_id, config, config_name)
|
|
185
|
+
try:
|
|
186
|
+
payload = asyncio.run(connector_instance.health_check())
|
|
187
|
+
except Exception as exc:
|
|
188
|
+
raise click.ClickException(str(exc)) from exc
|
|
189
|
+
|
|
190
|
+
if output_format == "json":
|
|
191
|
+
click.echo(json.dumps(payload, indent=2))
|
|
192
|
+
return
|
|
193
|
+
|
|
194
|
+
table = Table(title="Connector Health")
|
|
195
|
+
table.add_column("Field", style="cyan")
|
|
196
|
+
table.add_column("Value", style="white")
|
|
197
|
+
for key, value in payload.items():
|
|
198
|
+
table.add_row(str(key), str(value))
|
|
199
|
+
console.print(table)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
@connector.command("save")
|
|
203
|
+
@click.option("--name", "config_name", required=True, help="Name for the saved config")
|
|
204
|
+
@click.option("--type", "connector_type", required=True, help="Connector type")
|
|
205
|
+
@click.option("--config", required=True, help="JSON config payload")
|
|
206
|
+
def save(config_name: str, connector_type: str, config: str) -> None:
|
|
207
|
+
"""Save a connector config for reuse."""
|
|
208
|
+
connector_meta = CONNECTOR_CATALOG.get(connector_type)
|
|
209
|
+
if not connector_meta:
|
|
210
|
+
raise click.ClickException(
|
|
211
|
+
f"Unknown connector type '{connector_type}'. Use 'genxai connector list' to see options."
|
|
212
|
+
)
|
|
213
|
+
config_data = _load_config(config, None)
|
|
214
|
+
missing = [field for field in connector_meta["required"] if field not in config_data]
|
|
215
|
+
if missing:
|
|
216
|
+
raise click.ClickException(f"Missing required fields: {', '.join(missing)}")
|
|
217
|
+
|
|
218
|
+
store = ConnectorConfigStore()
|
|
219
|
+
store.save(
|
|
220
|
+
ConnectorConfigEntry(
|
|
221
|
+
name=config_name,
|
|
222
|
+
connector_type=connector_type,
|
|
223
|
+
config=config_data,
|
|
224
|
+
)
|
|
225
|
+
)
|
|
226
|
+
console.print(f"[green]✓ Saved connector config '{config_name}'[/green]")
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
@connector.command("saved")
|
|
230
|
+
@click.option("--format", "output_format", type=click.Choice(["table", "json"]), default="table")
|
|
231
|
+
def list_saved(output_format: str) -> None:
|
|
232
|
+
"""List saved connector configs."""
|
|
233
|
+
store = ConnectorConfigStore()
|
|
234
|
+
entries = store.list()
|
|
235
|
+
if output_format == "json":
|
|
236
|
+
payload = {
|
|
237
|
+
name: {"connector_type": entry.connector_type, "config": entry.config}
|
|
238
|
+
for name, entry in entries.items()
|
|
239
|
+
}
|
|
240
|
+
click.echo(json.dumps(payload, indent=2))
|
|
241
|
+
return
|
|
242
|
+
|
|
243
|
+
table = Table(title="Saved Connector Configs")
|
|
244
|
+
table.add_column("Name", style="cyan")
|
|
245
|
+
table.add_column("Type", style="green")
|
|
246
|
+
table.add_column("Config", style="white")
|
|
247
|
+
for name, entry in entries.items():
|
|
248
|
+
table.add_row(name, entry.connector_type, json.dumps(entry.config))
|
|
249
|
+
console.print(table)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
@connector.command("remove")
|
|
253
|
+
@click.option("--name", "config_name", required=True, help="Saved config name")
|
|
254
|
+
def remove(config_name: str) -> None:
|
|
255
|
+
"""Remove a saved connector config."""
|
|
256
|
+
store = ConnectorConfigStore()
|
|
257
|
+
if not store.delete(config_name):
|
|
258
|
+
raise click.ClickException(f"Config '{config_name}' not found")
|
|
259
|
+
console.print(f"[green]✓ Removed connector config '{config_name}'[/green]")
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
@connector.command("keygen")
|
|
263
|
+
def keygen() -> None:
|
|
264
|
+
"""Generate a Fernet key for encrypted connector configs."""
|
|
265
|
+
try:
|
|
266
|
+
from cryptography.fernet import Fernet
|
|
267
|
+
except ImportError as exc:
|
|
268
|
+
raise click.ClickException(
|
|
269
|
+
"cryptography is required for key generation. Install with: pip install cryptography"
|
|
270
|
+
) from exc
|
|
271
|
+
key = Fernet.generate_key().decode()
|
|
272
|
+
click.echo(key)
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def _build_connector(connector_id: str, connector_class: type[Connector], config: Dict[str, Any]) -> Connector:
|
|
276
|
+
params = {"connector_id": connector_id, **config}
|
|
277
|
+
return connector_class(**params)
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _build_from_cli(
|
|
281
|
+
connector_type: str,
|
|
282
|
+
connector_id: str,
|
|
283
|
+
config: Optional[str],
|
|
284
|
+
config_name: Optional[str],
|
|
285
|
+
) -> Connector:
|
|
286
|
+
connector_meta = CONNECTOR_CATALOG.get(connector_type)
|
|
287
|
+
if not connector_meta:
|
|
288
|
+
raise click.ClickException(
|
|
289
|
+
f"Unknown connector type '{connector_type}'. Use 'genxai connector list' to see options."
|
|
290
|
+
)
|
|
291
|
+
config_data = _load_config(config, config_name)
|
|
292
|
+
missing = [field for field in connector_meta["required"] if field not in config_data]
|
|
293
|
+
if missing:
|
|
294
|
+
raise click.ClickException(f"Missing required fields: {', '.join(missing)}")
|
|
295
|
+
return _build_connector(connector_id, connector_meta["class"], config_data)
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def _load_config(config: Optional[str], config_name: Optional[str]) -> Dict[str, Any]:
|
|
299
|
+
if config_name:
|
|
300
|
+
store = ConnectorConfigStore()
|
|
301
|
+
entry = store.get(config_name)
|
|
302
|
+
if not entry:
|
|
303
|
+
raise click.ClickException(f"Config '{config_name}' not found")
|
|
304
|
+
return entry.config
|
|
305
|
+
return json.loads(config) if config else {}
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
if __name__ == "__main__":
|
|
309
|
+
connector()
|
cli/commands/workflow.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Workflow CLI commands."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any, Dict
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
|
|
8
|
+
from genxai.core.graph import load_workflow_yaml, register_workflow_agents
|
|
9
|
+
from genxai.core.graph.executor import WorkflowExecutor
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@click.group()
|
|
13
|
+
def workflow() -> None:
|
|
14
|
+
"""Manage and run workflows."""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@workflow.command("run")
|
|
18
|
+
@click.argument("workflow_path", type=click.Path(exists=True, dir_okay=False, path_type=Path))
|
|
19
|
+
@click.option("--input", "input_payload", required=True, help="JSON input payload")
|
|
20
|
+
def run_workflow(workflow_path: Path, input_payload: str) -> None:
|
|
21
|
+
"""Run a workflow from a YAML file."""
|
|
22
|
+
import json
|
|
23
|
+
|
|
24
|
+
try:
|
|
25
|
+
workflow = load_workflow_yaml(workflow_path)
|
|
26
|
+
except (ValueError, FileNotFoundError) as exc:
|
|
27
|
+
raise click.ClickException(str(exc)) from exc
|
|
28
|
+
register_workflow_agents(workflow)
|
|
29
|
+
|
|
30
|
+
nodes = _build_nodes_from_workflow(workflow)
|
|
31
|
+
edges = _build_edges_from_workflow(workflow)
|
|
32
|
+
|
|
33
|
+
executor = WorkflowExecutor()
|
|
34
|
+
input_data = json.loads(input_payload)
|
|
35
|
+
shared_memory = workflow.get("memory", {}).get("shared", False)
|
|
36
|
+
|
|
37
|
+
result = _run_executor(executor, nodes, edges, input_data, shared_memory=shared_memory)
|
|
38
|
+
click.echo(json.dumps(result, indent=2))
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _build_nodes_from_workflow(workflow: Dict[str, Any]):
|
|
42
|
+
nodes = workflow.get("graph", {}).get("nodes", [])
|
|
43
|
+
if not isinstance(nodes, list):
|
|
44
|
+
raise click.ClickException("workflow.graph.nodes must be a list")
|
|
45
|
+
return nodes
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _build_edges_from_workflow(workflow: Dict[str, Any]):
|
|
49
|
+
edges = workflow.get("graph", {}).get("edges", [])
|
|
50
|
+
if not isinstance(edges, list):
|
|
51
|
+
raise click.ClickException("workflow.graph.edges must be a list")
|
|
52
|
+
# Map YAML edge keys to executor expectations.
|
|
53
|
+
return [
|
|
54
|
+
{
|
|
55
|
+
"source": edge.get("from"),
|
|
56
|
+
"target": edge.get("to"),
|
|
57
|
+
"condition": edge.get("condition"),
|
|
58
|
+
}
|
|
59
|
+
for edge in edges
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _run_executor(
|
|
64
|
+
executor: WorkflowExecutor,
|
|
65
|
+
nodes,
|
|
66
|
+
edges,
|
|
67
|
+
input_data,
|
|
68
|
+
shared_memory: bool = False,
|
|
69
|
+
):
|
|
70
|
+
import asyncio
|
|
71
|
+
|
|
72
|
+
async def _execute():
|
|
73
|
+
return await executor.execute(
|
|
74
|
+
nodes=nodes,
|
|
75
|
+
edges=edges,
|
|
76
|
+
input_data=input_data,
|
|
77
|
+
shared_memory=shared_memory,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
return asyncio.run(_execute())
|
cli/main.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""GenXAI CLI - Main entry point."""
|
|
2
2
|
|
|
3
3
|
import click
|
|
4
|
-
from cli.commands import tool, metrics
|
|
4
|
+
from cli.commands import tool, metrics, connector, workflow
|
|
5
5
|
from cli.commands.approval import approval
|
|
6
6
|
from cli.commands.audit import audit
|
|
7
7
|
|
|
@@ -19,6 +19,8 @@ def cli():
|
|
|
19
19
|
# Register command groups
|
|
20
20
|
cli.add_command(tool)
|
|
21
21
|
cli.add_command(metrics)
|
|
22
|
+
cli.add_command(connector)
|
|
23
|
+
cli.add_command(workflow)
|
|
22
24
|
cli.add_command(approval)
|
|
23
25
|
cli.add_command(audit)
|
|
24
26
|
|
genxai/__init__.py
CHANGED
|
@@ -17,6 +17,7 @@ from genxai.core.agent import (
|
|
|
17
17
|
AgentRuntime,
|
|
18
18
|
AgentType,
|
|
19
19
|
)
|
|
20
|
+
from genxai.agents import AssistantAgent, UserProxyAgent
|
|
20
21
|
from genxai.core.graph import (
|
|
21
22
|
Edge,
|
|
22
23
|
EnhancedGraph,
|
|
@@ -27,6 +28,22 @@ from genxai.core.graph import (
|
|
|
27
28
|
WorkflowExecutor,
|
|
28
29
|
execute_workflow_sync,
|
|
29
30
|
)
|
|
31
|
+
from genxai.flows import (
|
|
32
|
+
FlowOrchestrator,
|
|
33
|
+
RoundRobinFlow,
|
|
34
|
+
SelectorFlow,
|
|
35
|
+
P2PFlow,
|
|
36
|
+
ParallelFlow,
|
|
37
|
+
ConditionalFlow,
|
|
38
|
+
LoopFlow,
|
|
39
|
+
RouterFlow,
|
|
40
|
+
EnsembleVotingFlow,
|
|
41
|
+
CriticReviewFlow,
|
|
42
|
+
CoordinatorWorkerFlow,
|
|
43
|
+
MapReduceFlow,
|
|
44
|
+
SubworkflowFlow,
|
|
45
|
+
AuctionFlow,
|
|
46
|
+
)
|
|
30
47
|
from genxai.core.memory.manager import MemorySystem
|
|
31
48
|
from genxai.tools import (
|
|
32
49
|
DynamicTool,
|
|
@@ -55,6 +72,8 @@ __all__ = [
|
|
|
55
72
|
"AgentRegistry",
|
|
56
73
|
"AgentRuntime",
|
|
57
74
|
"AgentType",
|
|
75
|
+
"AssistantAgent",
|
|
76
|
+
"UserProxyAgent",
|
|
58
77
|
"Graph",
|
|
59
78
|
"EnhancedGraph",
|
|
60
79
|
"WorkflowExecutor",
|
|
@@ -71,6 +90,20 @@ __all__ = [
|
|
|
71
90
|
"ToolResult",
|
|
72
91
|
"DynamicTool",
|
|
73
92
|
"MemorySystem",
|
|
93
|
+
"FlowOrchestrator",
|
|
94
|
+
"RoundRobinFlow",
|
|
95
|
+
"SelectorFlow",
|
|
96
|
+
"P2PFlow",
|
|
97
|
+
"ParallelFlow",
|
|
98
|
+
"ConditionalFlow",
|
|
99
|
+
"LoopFlow",
|
|
100
|
+
"RouterFlow",
|
|
101
|
+
"EnsembleVotingFlow",
|
|
102
|
+
"CriticReviewFlow",
|
|
103
|
+
"CoordinatorWorkerFlow",
|
|
104
|
+
"MapReduceFlow",
|
|
105
|
+
"SubworkflowFlow",
|
|
106
|
+
"AuctionFlow",
|
|
74
107
|
"BaseTrigger",
|
|
75
108
|
"TriggerEvent",
|
|
76
109
|
"TriggerRegistry",
|
genxai/agents/presets.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Preset agent wrappers for convenience usage."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Optional
|
|
6
|
+
|
|
7
|
+
from genxai.core.agent.base import Agent, AgentFactory, AgentType
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class AssistantAgent(Agent):
|
|
11
|
+
"""Thin wrapper around AgentFactory for assistant-style agents."""
|
|
12
|
+
|
|
13
|
+
@classmethod
|
|
14
|
+
def create(
|
|
15
|
+
cls,
|
|
16
|
+
id: str,
|
|
17
|
+
goal: str,
|
|
18
|
+
role: str = "Assistant",
|
|
19
|
+
**kwargs: Any,
|
|
20
|
+
) -> Agent:
|
|
21
|
+
return AgentFactory.create_agent(
|
|
22
|
+
id=id,
|
|
23
|
+
role=role,
|
|
24
|
+
goal=goal,
|
|
25
|
+
agent_type=kwargs.pop("agent_type", AgentType.DELIBERATIVE),
|
|
26
|
+
**kwargs,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class UserProxyAgent(Agent):
|
|
31
|
+
"""Thin wrapper for user-proxy style agents.
|
|
32
|
+
|
|
33
|
+
This is intended to be paired with a human-input tool and runtime execution.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
@classmethod
|
|
37
|
+
def create(
|
|
38
|
+
cls,
|
|
39
|
+
id: str,
|
|
40
|
+
goal: str = "Provide user input",
|
|
41
|
+
role: str = "User",
|
|
42
|
+
tools: Optional[list[str]] = None,
|
|
43
|
+
**kwargs: Any,
|
|
44
|
+
) -> Agent:
|
|
45
|
+
tool_list = tools or []
|
|
46
|
+
return AgentFactory.create_agent(
|
|
47
|
+
id=id,
|
|
48
|
+
role=role,
|
|
49
|
+
goal=goal,
|
|
50
|
+
tools=tool_list,
|
|
51
|
+
agent_type=kwargs.pop("agent_type", AgentType.REACTIVE),
|
|
52
|
+
**kwargs,
|
|
53
|
+
)
|
genxai/connectors/__init__.py
CHANGED
|
@@ -6,6 +6,11 @@ from genxai.connectors.webhook import WebhookConnector
|
|
|
6
6
|
from genxai.connectors.kafka import KafkaConnector
|
|
7
7
|
from genxai.connectors.sqs import SQSConnector
|
|
8
8
|
from genxai.connectors.postgres_cdc import PostgresCDCConnector
|
|
9
|
+
from genxai.connectors.slack import SlackConnector
|
|
10
|
+
from genxai.connectors.github import GitHubConnector
|
|
11
|
+
from genxai.connectors.notion import NotionConnector
|
|
12
|
+
from genxai.connectors.jira import JiraConnector
|
|
13
|
+
from genxai.connectors.google_workspace import GoogleWorkspaceConnector
|
|
9
14
|
|
|
10
15
|
__all__ = [
|
|
11
16
|
"Connector",
|
|
@@ -16,4 +21,9 @@ __all__ = [
|
|
|
16
21
|
"KafkaConnector",
|
|
17
22
|
"SQSConnector",
|
|
18
23
|
"PostgresCDCConnector",
|
|
24
|
+
"SlackConnector",
|
|
25
|
+
"GitHubConnector",
|
|
26
|
+
"NotionConnector",
|
|
27
|
+
"JiraConnector",
|
|
28
|
+
"GoogleWorkspaceConnector",
|
|
19
29
|
]
|
genxai/connectors/base.py
CHANGED
|
@@ -4,7 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
from abc import ABC, abstractmethod
|
|
6
6
|
from dataclasses import dataclass, field
|
|
7
|
-
from datetime import datetime
|
|
7
|
+
from datetime import datetime, UTC
|
|
8
8
|
from enum import Enum
|
|
9
9
|
from typing import Any, Awaitable, Callable, Dict, Optional
|
|
10
10
|
import asyncio
|
|
@@ -29,7 +29,7 @@ class ConnectorEvent:
|
|
|
29
29
|
|
|
30
30
|
connector_id: str
|
|
31
31
|
payload: Dict[str, Any]
|
|
32
|
-
timestamp: datetime = field(default_factory=datetime.
|
|
32
|
+
timestamp: datetime = field(default_factory=lambda: datetime.now(UTC))
|
|
33
33
|
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
34
34
|
|
|
35
35
|
|
|
@@ -106,7 +106,7 @@ class Connector(ABC):
|
|
|
106
106
|
"lifecycle": self.status.value,
|
|
107
107
|
"last_error": self._last_error,
|
|
108
108
|
}
|
|
109
|
-
self._last_healthcheck = datetime.
|
|
109
|
+
self._last_healthcheck = datetime.now(UTC).isoformat()
|
|
110
110
|
return payload
|
|
111
111
|
|
|
112
112
|
async def validate_config(self) -> None:
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""Connector configuration persistence helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any, Dict, Optional
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class ConnectorConfigEntry:
|
|
14
|
+
name: str
|
|
15
|
+
connector_type: str
|
|
16
|
+
config: Dict[str, Any]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ConnectorConfigStore:
|
|
20
|
+
"""Persist connector configurations as JSON on disk.
|
|
21
|
+
|
|
22
|
+
Supports optional encryption when a key is provided via the
|
|
23
|
+
`GENXAI_CONNECTOR_CONFIG_KEY` environment variable or `encryption_key`.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(self, path: Optional[Path] = None, encryption_key: Optional[str] = None) -> None:
|
|
27
|
+
self.path = path or Path(".genxai/connectors.json")
|
|
28
|
+
self.encryption_key = encryption_key or os.getenv("GENXAI_CONNECTOR_CONFIG_KEY")
|
|
29
|
+
|
|
30
|
+
def list(self) -> Dict[str, ConnectorConfigEntry]:
|
|
31
|
+
data = self._read_raw()
|
|
32
|
+
return {
|
|
33
|
+
name: ConnectorConfigEntry(
|
|
34
|
+
name=name,
|
|
35
|
+
connector_type=payload["connector_type"],
|
|
36
|
+
config=payload.get("config", {}),
|
|
37
|
+
)
|
|
38
|
+
for name, payload in data.items()
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
def get(self, name: str) -> Optional[ConnectorConfigEntry]:
|
|
42
|
+
data = self._read_raw()
|
|
43
|
+
payload = data.get(name)
|
|
44
|
+
if not payload:
|
|
45
|
+
return None
|
|
46
|
+
return ConnectorConfigEntry(
|
|
47
|
+
name=name,
|
|
48
|
+
connector_type=payload["connector_type"],
|
|
49
|
+
config=payload.get("config", {}),
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
def save(self, entry: ConnectorConfigEntry) -> None:
|
|
53
|
+
data = self._read_raw()
|
|
54
|
+
data[entry.name] = {
|
|
55
|
+
"connector_type": entry.connector_type,
|
|
56
|
+
"config": entry.config,
|
|
57
|
+
}
|
|
58
|
+
self._write_raw(data)
|
|
59
|
+
|
|
60
|
+
def delete(self, name: str) -> bool:
|
|
61
|
+
data = self._read_raw()
|
|
62
|
+
if name not in data:
|
|
63
|
+
return False
|
|
64
|
+
data.pop(name)
|
|
65
|
+
self._write_raw(data)
|
|
66
|
+
return True
|
|
67
|
+
|
|
68
|
+
def _read_raw(self) -> Dict[str, Any]:
|
|
69
|
+
if not self.path.exists():
|
|
70
|
+
return {}
|
|
71
|
+
raw = json.loads(self.path.read_text())
|
|
72
|
+
if isinstance(raw, dict) and raw.get("encrypted"):
|
|
73
|
+
payload = raw.get("payload")
|
|
74
|
+
if not payload:
|
|
75
|
+
raise ValueError("Encrypted connector config missing payload")
|
|
76
|
+
return json.loads(self._decrypt(payload))
|
|
77
|
+
return raw
|
|
78
|
+
|
|
79
|
+
def _write_raw(self, data: Dict[str, Any]) -> None:
|
|
80
|
+
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
81
|
+
if self.encryption_key:
|
|
82
|
+
encrypted = self._encrypt(json.dumps(data))
|
|
83
|
+
payload = {"encrypted": True, "payload": encrypted}
|
|
84
|
+
self.path.write_text(json.dumps(payload, indent=2))
|
|
85
|
+
return
|
|
86
|
+
self.path.write_text(json.dumps(data, indent=2))
|
|
87
|
+
|
|
88
|
+
def _get_fernet(self):
|
|
89
|
+
if not self.encryption_key:
|
|
90
|
+
raise ValueError("Encryption key not configured")
|
|
91
|
+
try:
|
|
92
|
+
from cryptography.fernet import Fernet
|
|
93
|
+
except ImportError as exc:
|
|
94
|
+
raise ImportError(
|
|
95
|
+
"cryptography is required for encrypted connector configs. "
|
|
96
|
+
"Install with: pip install cryptography"
|
|
97
|
+
) from exc
|
|
98
|
+
return Fernet(self.encryption_key.encode())
|
|
99
|
+
|
|
100
|
+
def _encrypt(self, payload: str) -> str:
|
|
101
|
+
fernet = self._get_fernet()
|
|
102
|
+
return fernet.encrypt(payload.encode()).decode()
|
|
103
|
+
|
|
104
|
+
def _decrypt(self, payload: str) -> str:
|
|
105
|
+
fernet = self._get_fernet()
|
|
106
|
+
return fernet.decrypt(payload.encode()).decode()
|