genxai-framework 0.1.0__py3-none-any.whl → 0.1.1__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/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/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/utils/tokens.py +6 -0
- {genxai_framework-0.1.0.dist-info → genxai_framework-0.1.1.dist-info}/METADATA +63 -12
- {genxai_framework-0.1.0.dist-info → genxai_framework-0.1.1.dist-info}/RECORD +50 -21
- {genxai_framework-0.1.0.dist-info → genxai_framework-0.1.1.dist-info}/WHEEL +0 -0
- {genxai_framework-0.1.0.dist-info → genxai_framework-0.1.1.dist-info}/entry_points.txt +0 -0
- {genxai_framework-0.1.0.dist-info → genxai_framework-0.1.1.dist-info}/licenses/LICENSE +0 -0
- {genxai_framework-0.1.0.dist-info → genxai_framework-0.1.1.dist-info}/top_level.txt +0 -0
genxai/core/graph/executor.py
CHANGED
|
@@ -2,12 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import copy
|
|
5
|
-
from typing import Any, Dict, List, Optional
|
|
5
|
+
from typing import Any, Callable, Dict, List, Optional
|
|
6
6
|
import logging
|
|
7
7
|
from pathlib import Path
|
|
8
8
|
|
|
9
9
|
from genxai.core.graph.engine import Graph
|
|
10
|
-
from genxai.core.
|
|
10
|
+
from genxai.core.memory.shared import SharedMemoryBus
|
|
11
|
+
from genxai.core.graph.nodes import (
|
|
12
|
+
InputNode,
|
|
13
|
+
OutputNode,
|
|
14
|
+
AgentNode,
|
|
15
|
+
ConditionNode,
|
|
16
|
+
ToolNode,
|
|
17
|
+
SubgraphNode,
|
|
18
|
+
LoopNode,
|
|
19
|
+
Node,
|
|
20
|
+
NodeConfig,
|
|
21
|
+
NodeType,
|
|
22
|
+
)
|
|
11
23
|
from genxai.core.graph.edges import Edge, ConditionalEdge
|
|
12
24
|
from genxai.core.agent.base import Agent, AgentFactory
|
|
13
25
|
from genxai.core.agent.registry import AgentRegistry
|
|
@@ -30,7 +42,9 @@ class EnhancedGraph(Graph):
|
|
|
30
42
|
in GenXAI.
|
|
31
43
|
"""
|
|
32
44
|
|
|
33
|
-
async def _execute_node_logic(
|
|
45
|
+
async def _execute_node_logic(
|
|
46
|
+
self, node: Any, state: Dict[str, Any], max_iterations: int = 100
|
|
47
|
+
) -> Any:
|
|
34
48
|
"""Execute node logic with actual agent execution.
|
|
35
49
|
|
|
36
50
|
Args:
|
|
@@ -74,8 +88,7 @@ class EnhancedGraph(Graph):
|
|
|
74
88
|
return result
|
|
75
89
|
|
|
76
90
|
else:
|
|
77
|
-
|
|
78
|
-
return {"node_id": node.id, "type": node.type.value}
|
|
91
|
+
return await super()._execute_node_logic(node, state, max_iterations)
|
|
79
92
|
|
|
80
93
|
async def _execute_agent_with_tools(
|
|
81
94
|
self, agent: Agent, task: str, state: Dict[str, Any]
|
|
@@ -95,7 +108,15 @@ class EnhancedGraph(Graph):
|
|
|
95
108
|
# Use AgentRuntime for full integration
|
|
96
109
|
from genxai.core.agent.runtime import AgentRuntime
|
|
97
110
|
|
|
98
|
-
runtime
|
|
111
|
+
# Pass both API keys to runtime so it can select the correct one based on model
|
|
112
|
+
runtime = AgentRuntime(
|
|
113
|
+
agent=agent,
|
|
114
|
+
llm_provider=getattr(self, "llm_provider", None),
|
|
115
|
+
openai_api_key=getattr(self, "openai_api_key", None),
|
|
116
|
+
anthropic_api_key=getattr(self, "anthropic_api_key", None),
|
|
117
|
+
enable_memory=True,
|
|
118
|
+
shared_memory=getattr(self, "shared_memory", None),
|
|
119
|
+
)
|
|
99
120
|
|
|
100
121
|
# Load tools from registry
|
|
101
122
|
if agent.config.tools:
|
|
@@ -108,7 +129,10 @@ class EnhancedGraph(Graph):
|
|
|
108
129
|
logger.debug(f"Loaded {len(tools)} tools for agent")
|
|
109
130
|
|
|
110
131
|
# Execute agent with full runtime support
|
|
111
|
-
|
|
132
|
+
context = dict(state)
|
|
133
|
+
if getattr(self, "shared_memory", None) is not None:
|
|
134
|
+
context["shared_memory"] = getattr(self, "shared_memory")
|
|
135
|
+
result = await runtime.execute(task, context=context)
|
|
112
136
|
|
|
113
137
|
return result
|
|
114
138
|
|
|
@@ -256,11 +280,14 @@ class WorkflowExecutor:
|
|
|
256
280
|
Constructed graph
|
|
257
281
|
"""
|
|
258
282
|
graph = EnhancedGraph(name="workflow")
|
|
283
|
+
graph.openai_api_key = self.openai_api_key
|
|
284
|
+
graph.anthropic_api_key = self.anthropic_api_key
|
|
259
285
|
|
|
260
286
|
# Add nodes
|
|
261
287
|
for node in nodes:
|
|
262
288
|
node_id = node.get("id")
|
|
263
289
|
node_type = node.get("type")
|
|
290
|
+
config = node.get("config", {})
|
|
264
291
|
|
|
265
292
|
# Support some common aliases used by the Studio UI
|
|
266
293
|
# - "start" behaves like an input node
|
|
@@ -271,6 +298,29 @@ class WorkflowExecutor:
|
|
|
271
298
|
graph.add_node(OutputNode(id=node_id))
|
|
272
299
|
elif node_type == "agent":
|
|
273
300
|
graph.add_node(AgentNode(id=node_id, agent_id=node_id))
|
|
301
|
+
elif node_type == "tool":
|
|
302
|
+
tool_name = config.get("tool_name") or config.get("name") or "tool"
|
|
303
|
+
graph.add_node(ToolNode(id=node_id, tool_name=tool_name))
|
|
304
|
+
elif node_type == "decision":
|
|
305
|
+
condition = config.get("condition", "")
|
|
306
|
+
graph.add_node(ConditionNode(id=node_id, condition=condition))
|
|
307
|
+
elif node_type == "subgraph":
|
|
308
|
+
workflow_id = config.get("workflow_id") or config.get("subgraph_id") or config.get("workflow")
|
|
309
|
+
if workflow_id:
|
|
310
|
+
graph.add_node(SubgraphNode(id=node_id, workflow_id=workflow_id))
|
|
311
|
+
else:
|
|
312
|
+
graph.add_node(
|
|
313
|
+
Node(
|
|
314
|
+
id=node_id,
|
|
315
|
+
type=NodeType.SUBGRAPH,
|
|
316
|
+
config=NodeConfig(type=NodeType.SUBGRAPH, data={"workflow_id": ""}),
|
|
317
|
+
)
|
|
318
|
+
)
|
|
319
|
+
logger.warning(f"Subgraph node '{node_id}' missing workflow_id")
|
|
320
|
+
elif node_type == "loop":
|
|
321
|
+
condition = config.get("condition", "")
|
|
322
|
+
max_iterations = int(config.get("max_iterations", 5))
|
|
323
|
+
graph.add_node(LoopNode(id=node_id, condition=condition, max_iterations=max_iterations))
|
|
274
324
|
else:
|
|
275
325
|
logger.warning(f"Unknown node type: {node_type}")
|
|
276
326
|
|
|
@@ -321,6 +371,10 @@ class WorkflowExecutor:
|
|
|
321
371
|
run_id: Optional[str] = None,
|
|
322
372
|
checkpoint_dir: Optional[str] = None,
|
|
323
373
|
resume_from: Optional[str] = None,
|
|
374
|
+
model_override: Optional[str] = None,
|
|
375
|
+
event_callback: Optional[Callable[[Dict[str, Any]], Any]] = None,
|
|
376
|
+
shared_memory: bool = False,
|
|
377
|
+
llm_provider: Optional[Any] = None,
|
|
324
378
|
) -> Dict[str, Any]:
|
|
325
379
|
"""Execute a workflow.
|
|
326
380
|
|
|
@@ -338,11 +392,23 @@ class WorkflowExecutor:
|
|
|
338
392
|
try:
|
|
339
393
|
logger.info("Starting workflow execution")
|
|
340
394
|
|
|
395
|
+
# Apply model override if provided
|
|
396
|
+
if model_override:
|
|
397
|
+
for node in nodes:
|
|
398
|
+
if node.get("type") == "agent":
|
|
399
|
+
config = node.setdefault("config", {})
|
|
400
|
+
config["llm_model"] = model_override
|
|
401
|
+
|
|
341
402
|
# Create agents from nodes
|
|
342
403
|
self._create_agents_from_nodes(nodes)
|
|
343
404
|
|
|
344
405
|
# Build graph
|
|
345
406
|
graph = self._build_graph(nodes, edges)
|
|
407
|
+
graph.llm_provider = llm_provider
|
|
408
|
+
if shared_memory:
|
|
409
|
+
graph.set_shared_memory(SharedMemoryBus())
|
|
410
|
+
graph.shared_memory = graph.shared_memory
|
|
411
|
+
graph.shared_memory_enabled = True
|
|
346
412
|
|
|
347
413
|
# Validate graph
|
|
348
414
|
graph.validate()
|
|
@@ -364,7 +430,11 @@ class WorkflowExecutor:
|
|
|
364
430
|
status="allowed",
|
|
365
431
|
)
|
|
366
432
|
)
|
|
367
|
-
result = await graph.run(
|
|
433
|
+
result = await graph.run(
|
|
434
|
+
input_data=input_data,
|
|
435
|
+
resume_from=checkpoint,
|
|
436
|
+
event_callback=event_callback,
|
|
437
|
+
)
|
|
368
438
|
|
|
369
439
|
logger.info("Workflow execution completed successfully")
|
|
370
440
|
|
|
@@ -378,6 +448,7 @@ class WorkflowExecutor:
|
|
|
378
448
|
"status": "success",
|
|
379
449
|
"run_id": run_id,
|
|
380
450
|
"result": result,
|
|
451
|
+
"node_events": result.get("node_events", []),
|
|
381
452
|
"nodes_executed": len(graph.nodes),
|
|
382
453
|
"message": "Workflow executed successfully"
|
|
383
454
|
}
|
|
@@ -410,6 +481,7 @@ class WorkflowExecutor:
|
|
|
410
481
|
run_id: Optional[str] = None,
|
|
411
482
|
checkpoint_dir: Optional[str] = None,
|
|
412
483
|
resume_from: Optional[str] = None,
|
|
484
|
+
model_override: Optional[str] = None,
|
|
413
485
|
) -> str:
|
|
414
486
|
"""Enqueue workflow execution using a worker queue engine."""
|
|
415
487
|
if not self.queue_engine:
|
|
@@ -430,6 +502,7 @@ class WorkflowExecutor:
|
|
|
430
502
|
run_id=payload["run_id"],
|
|
431
503
|
checkpoint_dir=payload.get("checkpoint_dir"),
|
|
432
504
|
resume_from=payload.get("resume_from"),
|
|
505
|
+
model_override=payload.get("model_override"),
|
|
433
506
|
)
|
|
434
507
|
|
|
435
508
|
await self.queue_engine.start()
|
|
@@ -441,6 +514,7 @@ class WorkflowExecutor:
|
|
|
441
514
|
"run_id": run_id,
|
|
442
515
|
"checkpoint_dir": checkpoint_dir,
|
|
443
516
|
"resume_from": resume_from,
|
|
517
|
+
"model_override": model_override,
|
|
444
518
|
},
|
|
445
519
|
_handler,
|
|
446
520
|
metadata={"workflow": "queued"},
|
|
@@ -454,6 +528,8 @@ def execute_workflow_sync(
|
|
|
454
528
|
input_data: Dict[str, Any],
|
|
455
529
|
openai_api_key: Optional[str] = None,
|
|
456
530
|
anthropic_api_key: Optional[str] = None,
|
|
531
|
+
model_override: Optional[str] = None,
|
|
532
|
+
shared_memory: bool = False,
|
|
457
533
|
) -> Dict[str, Any]:
|
|
458
534
|
"""Synchronous wrapper for workflow execution.
|
|
459
535
|
|
|
@@ -480,7 +556,13 @@ def execute_workflow_sync(
|
|
|
480
556
|
asyncio.set_event_loop(loop)
|
|
481
557
|
try:
|
|
482
558
|
result = loop.run_until_complete(
|
|
483
|
-
executor.execute(
|
|
559
|
+
executor.execute(
|
|
560
|
+
nodes,
|
|
561
|
+
edges,
|
|
562
|
+
input_data,
|
|
563
|
+
model_override=model_override,
|
|
564
|
+
shared_memory=shared_memory,
|
|
565
|
+
)
|
|
484
566
|
)
|
|
485
567
|
return result
|
|
486
568
|
finally:
|
|
@@ -493,6 +575,9 @@ async def execute_workflow_async(
|
|
|
493
575
|
input_data: Dict[str, Any],
|
|
494
576
|
openai_api_key: Optional[str] = None,
|
|
495
577
|
anthropic_api_key: Optional[str] = None,
|
|
578
|
+
model_override: Optional[str] = None,
|
|
579
|
+
event_callback: Optional[Callable[[Dict[str, Any]], Any]] = None,
|
|
580
|
+
shared_memory: bool = False,
|
|
496
581
|
) -> Dict[str, Any]:
|
|
497
582
|
"""Async convenience function for workflow execution.
|
|
498
583
|
|
|
@@ -513,4 +598,12 @@ async def execute_workflow_async(
|
|
|
513
598
|
openai_api_key=openai_api_key,
|
|
514
599
|
anthropic_api_key=anthropic_api_key,
|
|
515
600
|
)
|
|
516
|
-
return await executor.execute(
|
|
601
|
+
return await executor.execute(
|
|
602
|
+
nodes,
|
|
603
|
+
edges,
|
|
604
|
+
input_data,
|
|
605
|
+
model_override=model_override,
|
|
606
|
+
event_callback=event_callback,
|
|
607
|
+
shared_memory=shared_memory,
|
|
608
|
+
)
|
|
609
|
+
|
genxai/core/graph/nodes.py
CHANGED
|
@@ -15,6 +15,7 @@ class NodeType(str, Enum):
|
|
|
15
15
|
HUMAN = "human"
|
|
16
16
|
INPUT = "input"
|
|
17
17
|
OUTPUT = "output"
|
|
18
|
+
LOOP = "loop"
|
|
18
19
|
|
|
19
20
|
|
|
20
21
|
class NodeConfig(BaseModel):
|
|
@@ -159,3 +160,30 @@ class OutputNode(Node):
|
|
|
159
160
|
super().__init__(
|
|
160
161
|
id=id, type=NodeType.OUTPUT, config=NodeConfig(type=NodeType.OUTPUT), **kwargs
|
|
161
162
|
)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class SubgraphNode(Node):
|
|
166
|
+
"""Node that executes a nested workflow."""
|
|
167
|
+
|
|
168
|
+
def __init__(self, id: str, workflow_id: str, **kwargs: Any) -> None:
|
|
169
|
+
super().__init__(
|
|
170
|
+
id=id,
|
|
171
|
+
type=NodeType.SUBGRAPH,
|
|
172
|
+
config=NodeConfig(type=NodeType.SUBGRAPH, data={"workflow_id": workflow_id}),
|
|
173
|
+
**kwargs,
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
class LoopNode(Node):
|
|
178
|
+
"""Node that represents a loop with a termination condition."""
|
|
179
|
+
|
|
180
|
+
def __init__(self, id: str, condition: str, max_iterations: int = 5, **kwargs: Any) -> None:
|
|
181
|
+
super().__init__(
|
|
182
|
+
id=id,
|
|
183
|
+
type=NodeType.LOOP,
|
|
184
|
+
config=NodeConfig(
|
|
185
|
+
type=NodeType.LOOP,
|
|
186
|
+
data={"condition": condition, "max_iterations": max_iterations},
|
|
187
|
+
),
|
|
188
|
+
**kwargs,
|
|
189
|
+
)
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"""Workflow YAML loading utilities."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any, Dict, List
|
|
7
|
+
|
|
8
|
+
import yaml
|
|
9
|
+
|
|
10
|
+
from genxai.core.agent.base import Agent
|
|
11
|
+
from genxai.core.agent.registry import AgentRegistry
|
|
12
|
+
from genxai.core.agent.config_io import import_agents_yaml
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def load_workflow_yaml(path: Path) -> Dict[str, Any]:
|
|
16
|
+
"""Load a workflow YAML file.
|
|
17
|
+
|
|
18
|
+
Supports `agents_ref` to pull reusable agent definitions from another YAML file.
|
|
19
|
+
Inline agents remain supported and will be merged with referenced agents.
|
|
20
|
+
"""
|
|
21
|
+
payload = yaml.safe_load(path.read_text())
|
|
22
|
+
if not isinstance(payload, dict) or "workflow" not in payload:
|
|
23
|
+
raise ValueError("Workflow YAML must contain a top-level 'workflow' mapping")
|
|
24
|
+
|
|
25
|
+
workflow = payload["workflow"]
|
|
26
|
+
if not isinstance(workflow, dict):
|
|
27
|
+
raise ValueError("workflow must be a mapping")
|
|
28
|
+
|
|
29
|
+
_merge_agents_ref(workflow, base_path=path.parent)
|
|
30
|
+
_validate_workflow_schema(workflow)
|
|
31
|
+
return workflow
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def register_workflow_agents(workflow: Dict[str, Any]) -> List[Agent]:
|
|
35
|
+
"""Register agents defined in a workflow dict and return them.
|
|
36
|
+
|
|
37
|
+
Accepts agent dictionaries with the `agents_ref` already resolved.
|
|
38
|
+
"""
|
|
39
|
+
agents_payload = workflow.get("agents", [])
|
|
40
|
+
if not agents_payload:
|
|
41
|
+
return []
|
|
42
|
+
|
|
43
|
+
workflow_memory = workflow.get("memory") if isinstance(workflow.get("memory"), dict) else {}
|
|
44
|
+
|
|
45
|
+
agents: List[Agent] = []
|
|
46
|
+
for agent_data in agents_payload:
|
|
47
|
+
if not isinstance(agent_data, dict):
|
|
48
|
+
raise ValueError("Invalid agent definition in workflow")
|
|
49
|
+
merged_agent = _apply_workflow_memory_defaults(agent_data, workflow_memory)
|
|
50
|
+
agent = _agent_from_workflow_dict(merged_agent)
|
|
51
|
+
AgentRegistry.register(agent)
|
|
52
|
+
agents.append(agent)
|
|
53
|
+
return agents
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _apply_workflow_memory_defaults(
|
|
57
|
+
agent_data: Dict[str, Any], workflow_memory: Dict[str, Any]
|
|
58
|
+
) -> Dict[str, Any]:
|
|
59
|
+
if not workflow_memory:
|
|
60
|
+
return agent_data
|
|
61
|
+
|
|
62
|
+
defaults: Dict[str, Any] = {}
|
|
63
|
+
if "enabled" in workflow_memory:
|
|
64
|
+
defaults["enabled"] = workflow_memory.get("enabled")
|
|
65
|
+
if "type" in workflow_memory:
|
|
66
|
+
defaults["type"] = workflow_memory.get("type")
|
|
67
|
+
|
|
68
|
+
if not defaults:
|
|
69
|
+
return agent_data
|
|
70
|
+
|
|
71
|
+
if isinstance(agent_data.get("memory"), dict):
|
|
72
|
+
memory_block = dict(agent_data.get("memory") or {})
|
|
73
|
+
memory_block.setdefault("enabled", defaults.get("enabled"))
|
|
74
|
+
memory_block.setdefault("type", defaults.get("type"))
|
|
75
|
+
agent_data = {**agent_data, "memory": memory_block}
|
|
76
|
+
return agent_data
|
|
77
|
+
|
|
78
|
+
if "enable_memory" in agent_data or "memory_type" in agent_data:
|
|
79
|
+
return agent_data
|
|
80
|
+
|
|
81
|
+
return {**agent_data, "memory": {k: v for k, v in defaults.items() if v is not None}}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _merge_agents_ref(workflow: Dict[str, Any], base_path: Path) -> None:
|
|
85
|
+
agents_ref = workflow.get("agents_ref")
|
|
86
|
+
if not agents_ref:
|
|
87
|
+
return
|
|
88
|
+
|
|
89
|
+
agents_ref_path = (base_path / agents_ref).resolve()
|
|
90
|
+
referenced = import_agents_yaml(agents_ref_path)
|
|
91
|
+
referenced_dicts = [
|
|
92
|
+
{"id": agent.id, **agent.config.model_dump(mode="json")}
|
|
93
|
+
if isinstance(agent, Agent)
|
|
94
|
+
else dict(agent)
|
|
95
|
+
for agent in referenced
|
|
96
|
+
]
|
|
97
|
+
|
|
98
|
+
inline_agents = workflow.get("agents", [])
|
|
99
|
+
if inline_agents is None:
|
|
100
|
+
inline_agents = []
|
|
101
|
+
if not isinstance(inline_agents, list):
|
|
102
|
+
raise ValueError("workflow.agents must be a list")
|
|
103
|
+
|
|
104
|
+
# Merge with inline agents taking precedence by id.
|
|
105
|
+
merged = {agent["id"]: agent for agent in referenced_dicts if isinstance(agent, dict)}
|
|
106
|
+
for agent in inline_agents:
|
|
107
|
+
if not isinstance(agent, dict) or "id" not in agent:
|
|
108
|
+
raise ValueError("Invalid inline agent definition")
|
|
109
|
+
merged[agent["id"]] = agent
|
|
110
|
+
|
|
111
|
+
workflow["agents"] = list(merged.values())
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _validate_workflow_schema(workflow: Dict[str, Any]) -> None:
|
|
115
|
+
if not workflow.get("name"):
|
|
116
|
+
raise ValueError("workflow.name is required")
|
|
117
|
+
|
|
118
|
+
if "memory" in workflow and not isinstance(workflow.get("memory"), dict):
|
|
119
|
+
raise ValueError("workflow.memory must be a mapping when provided")
|
|
120
|
+
|
|
121
|
+
graph = workflow.get("graph")
|
|
122
|
+
if not isinstance(graph, dict):
|
|
123
|
+
raise ValueError("workflow.graph must be a mapping")
|
|
124
|
+
|
|
125
|
+
nodes = graph.get("nodes")
|
|
126
|
+
if not isinstance(nodes, list) or not nodes:
|
|
127
|
+
raise ValueError("workflow.graph.nodes must be a non-empty list")
|
|
128
|
+
|
|
129
|
+
edges = graph.get("edges")
|
|
130
|
+
if not isinstance(edges, list):
|
|
131
|
+
raise ValueError("workflow.graph.edges must be a list")
|
|
132
|
+
|
|
133
|
+
node_ids = set()
|
|
134
|
+
for node in nodes:
|
|
135
|
+
if not isinstance(node, dict):
|
|
136
|
+
raise ValueError("workflow.graph.nodes entries must be mappings")
|
|
137
|
+
if "id" not in node or "type" not in node:
|
|
138
|
+
raise ValueError("Each node requires 'id' and 'type'")
|
|
139
|
+
node_ids.add(node["id"])
|
|
140
|
+
if node["type"] not in {"input", "start", "output", "end", "agent", "tool", "condition"}:
|
|
141
|
+
raise ValueError(f"Unsupported node type: {node['type']}")
|
|
142
|
+
|
|
143
|
+
for edge in edges:
|
|
144
|
+
if not isinstance(edge, dict):
|
|
145
|
+
raise ValueError("workflow.graph.edges entries must be mappings")
|
|
146
|
+
if "from" not in edge or "to" not in edge:
|
|
147
|
+
raise ValueError("Each edge requires 'from' and 'to'")
|
|
148
|
+
if edge["from"] not in node_ids or edge["to"] not in node_ids:
|
|
149
|
+
raise ValueError("Edges must reference existing node ids")
|
|
150
|
+
|
|
151
|
+
agent_ids = {agent.get("id") for agent in workflow.get("agents", []) if isinstance(agent, dict)}
|
|
152
|
+
for node in nodes:
|
|
153
|
+
if node.get("type") == "agent" and node.get("id") not in agent_ids:
|
|
154
|
+
raise ValueError(f"Agent node '{node['id']}' has no matching agent definition")
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _agent_from_workflow_dict(data: Dict[str, Any]) -> Agent:
|
|
158
|
+
config_payload = data.get("config") if isinstance(data.get("config"), dict) else {}
|
|
159
|
+
merged = {
|
|
160
|
+
**config_payload,
|
|
161
|
+
**{k: v for k, v in data.items() if k not in {"config"}},
|
|
162
|
+
}
|
|
163
|
+
return Agent(
|
|
164
|
+
id=data["id"],
|
|
165
|
+
config=_agent_config_from_workflow_dict(merged),
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _agent_config_from_workflow_dict(data: Dict[str, Any]):
|
|
170
|
+
from genxai.core.agent.base import AgentConfig
|
|
171
|
+
|
|
172
|
+
# Allow workflow agents to specify either `llm` (legacy) or `llm_model`.
|
|
173
|
+
llm_model = data.get("llm_model") or data.get("llm") or "gpt-4"
|
|
174
|
+
|
|
175
|
+
return AgentConfig(
|
|
176
|
+
role=data.get("role", "Agent"),
|
|
177
|
+
goal=data.get("goal", "Process tasks"),
|
|
178
|
+
backstory=data.get("backstory", ""),
|
|
179
|
+
llm_provider=data.get("llm_provider", "openai"),
|
|
180
|
+
llm_model=llm_model,
|
|
181
|
+
llm_temperature=data.get("llm_temperature", 0.7),
|
|
182
|
+
tools=data.get("tools", []),
|
|
183
|
+
enable_memory=data.get("memory", {}).get("enabled", True)
|
|
184
|
+
if isinstance(data.get("memory"), dict)
|
|
185
|
+
else data.get("enable_memory", True),
|
|
186
|
+
memory_type=data.get("memory", {}).get("type", "short_term")
|
|
187
|
+
if isinstance(data.get("memory"), dict)
|
|
188
|
+
else data.get("memory_type", "short_term"),
|
|
189
|
+
agent_type=data.get("behavior", {}).get("agent_type", "reactive")
|
|
190
|
+
if isinstance(data.get("behavior"), dict)
|
|
191
|
+
else data.get("agent_type", "reactive"),
|
|
192
|
+
max_iterations=data.get("behavior", {}).get("max_iterations", 10)
|
|
193
|
+
if isinstance(data.get("behavior"), dict)
|
|
194
|
+
else data.get("max_iterations", 10),
|
|
195
|
+
verbose=data.get("behavior", {}).get("verbose", False)
|
|
196
|
+
if isinstance(data.get("behavior"), dict)
|
|
197
|
+
else data.get("verbose", False),
|
|
198
|
+
metadata=data.get("metadata", {}),
|
|
199
|
+
)
|
genxai/flows/__init__.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Flow orchestrators for common agent coordination patterns."""
|
|
2
|
+
|
|
3
|
+
from genxai.flows.base import FlowOrchestrator
|
|
4
|
+
from genxai.flows.round_robin import RoundRobinFlow
|
|
5
|
+
from genxai.flows.selector import SelectorFlow
|
|
6
|
+
from genxai.flows.p2p import P2PFlow
|
|
7
|
+
from genxai.flows.parallel import ParallelFlow
|
|
8
|
+
from genxai.flows.conditional import ConditionalFlow
|
|
9
|
+
from genxai.flows.loop import LoopFlow
|
|
10
|
+
from genxai.flows.router import RouterFlow
|
|
11
|
+
from genxai.flows.ensemble_voting import EnsembleVotingFlow
|
|
12
|
+
from genxai.flows.critic_review import CriticReviewFlow
|
|
13
|
+
from genxai.flows.coordinator_worker import CoordinatorWorkerFlow
|
|
14
|
+
from genxai.flows.map_reduce import MapReduceFlow
|
|
15
|
+
from genxai.flows.subworkflow import SubworkflowFlow
|
|
16
|
+
from genxai.flows.auction import AuctionFlow
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"FlowOrchestrator",
|
|
20
|
+
"RoundRobinFlow",
|
|
21
|
+
"SelectorFlow",
|
|
22
|
+
"P2PFlow",
|
|
23
|
+
"ParallelFlow",
|
|
24
|
+
"ConditionalFlow",
|
|
25
|
+
"LoopFlow",
|
|
26
|
+
"RouterFlow",
|
|
27
|
+
"EnsembleVotingFlow",
|
|
28
|
+
"CriticReviewFlow",
|
|
29
|
+
"CoordinatorWorkerFlow",
|
|
30
|
+
"MapReduceFlow",
|
|
31
|
+
"SubworkflowFlow",
|
|
32
|
+
"AuctionFlow",
|
|
33
|
+
]
|
genxai/flows/auction.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Auction flow orchestrator."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, List, Optional
|
|
4
|
+
|
|
5
|
+
from genxai.core.agent.runtime import AgentRuntime
|
|
6
|
+
from genxai.flows.base import FlowOrchestrator
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class AuctionFlow(FlowOrchestrator):
|
|
10
|
+
"""Agents bid to handle a task; highest bid executes."""
|
|
11
|
+
|
|
12
|
+
def __init__(
|
|
13
|
+
self,
|
|
14
|
+
agents: List[Any],
|
|
15
|
+
name: str = "auction_flow",
|
|
16
|
+
llm_provider: Any = None,
|
|
17
|
+
) -> None:
|
|
18
|
+
super().__init__(agents=agents, name=name, llm_provider=llm_provider)
|
|
19
|
+
|
|
20
|
+
async def run(
|
|
21
|
+
self,
|
|
22
|
+
input_data: Any,
|
|
23
|
+
state: Optional[Dict[str, Any]] = None,
|
|
24
|
+
max_iterations: int = 100,
|
|
25
|
+
) -> Dict[str, Any]:
|
|
26
|
+
if state is None:
|
|
27
|
+
state = {}
|
|
28
|
+
state["input"] = input_data
|
|
29
|
+
state.setdefault("bids", {})
|
|
30
|
+
|
|
31
|
+
runtimes = {
|
|
32
|
+
agent.id: AgentRuntime(agent=agent, llm_provider=self.llm_provider)
|
|
33
|
+
for agent in self.agents
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
bid_task = state.get("bid_task", "Provide a numeric bid between 0 and 1")
|
|
37
|
+
tasks = [
|
|
38
|
+
self._execute_with_retry(
|
|
39
|
+
runtimes[agent.id],
|
|
40
|
+
task=bid_task,
|
|
41
|
+
context=state,
|
|
42
|
+
)
|
|
43
|
+
for agent in self.agents
|
|
44
|
+
]
|
|
45
|
+
results = await self._gather_tasks(tasks)
|
|
46
|
+
for agent, bid_result in zip(self.agents, results):
|
|
47
|
+
bid_value = 0.0
|
|
48
|
+
try:
|
|
49
|
+
bid_value = float(bid_result.get("output", 0))
|
|
50
|
+
except (TypeError, ValueError, AttributeError):
|
|
51
|
+
bid_value = 0.0
|
|
52
|
+
state["bids"][agent.id] = bid_value
|
|
53
|
+
|
|
54
|
+
if not state["bids"]:
|
|
55
|
+
raise ValueError("AuctionFlow requires at least one bid")
|
|
56
|
+
|
|
57
|
+
winner_id = max(state["bids"], key=state["bids"].get)
|
|
58
|
+
winner_runtime = runtimes[winner_id]
|
|
59
|
+
execution = await self._execute_with_retry(
|
|
60
|
+
winner_runtime,
|
|
61
|
+
task=state.get("task", "Execute the task"),
|
|
62
|
+
context={**state, "winner_id": winner_id},
|
|
63
|
+
)
|
|
64
|
+
state["winner_id"] = winner_id
|
|
65
|
+
state["winner_result"] = execution
|
|
66
|
+
return state
|