aury-agent 0.0.4__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.
- aury/__init__.py +2 -0
- aury/agents/__init__.py +55 -0
- aury/agents/a2a/__init__.py +168 -0
- aury/agents/backends/__init__.py +196 -0
- aury/agents/backends/artifact/__init__.py +9 -0
- aury/agents/backends/artifact/memory.py +130 -0
- aury/agents/backends/artifact/types.py +133 -0
- aury/agents/backends/code/__init__.py +65 -0
- aury/agents/backends/file/__init__.py +11 -0
- aury/agents/backends/file/local.py +66 -0
- aury/agents/backends/file/types.py +40 -0
- aury/agents/backends/invocation/__init__.py +8 -0
- aury/agents/backends/invocation/memory.py +81 -0
- aury/agents/backends/invocation/types.py +110 -0
- aury/agents/backends/memory/__init__.py +8 -0
- aury/agents/backends/memory/memory.py +179 -0
- aury/agents/backends/memory/types.py +136 -0
- aury/agents/backends/message/__init__.py +9 -0
- aury/agents/backends/message/memory.py +122 -0
- aury/agents/backends/message/types.py +124 -0
- aury/agents/backends/sandbox.py +275 -0
- aury/agents/backends/session/__init__.py +8 -0
- aury/agents/backends/session/memory.py +93 -0
- aury/agents/backends/session/types.py +124 -0
- aury/agents/backends/shell/__init__.py +11 -0
- aury/agents/backends/shell/local.py +110 -0
- aury/agents/backends/shell/types.py +55 -0
- aury/agents/backends/shell.py +209 -0
- aury/agents/backends/snapshot/__init__.py +19 -0
- aury/agents/backends/snapshot/git.py +95 -0
- aury/agents/backends/snapshot/hybrid.py +125 -0
- aury/agents/backends/snapshot/memory.py +86 -0
- aury/agents/backends/snapshot/types.py +59 -0
- aury/agents/backends/state/__init__.py +29 -0
- aury/agents/backends/state/composite.py +49 -0
- aury/agents/backends/state/file.py +57 -0
- aury/agents/backends/state/memory.py +52 -0
- aury/agents/backends/state/sqlite.py +262 -0
- aury/agents/backends/state/types.py +178 -0
- aury/agents/backends/subagent/__init__.py +165 -0
- aury/agents/cli/__init__.py +41 -0
- aury/agents/cli/chat.py +239 -0
- aury/agents/cli/config.py +236 -0
- aury/agents/cli/extensions.py +460 -0
- aury/agents/cli/main.py +189 -0
- aury/agents/cli/session.py +337 -0
- aury/agents/cli/workflow.py +276 -0
- aury/agents/context_providers/__init__.py +66 -0
- aury/agents/context_providers/artifact.py +299 -0
- aury/agents/context_providers/base.py +177 -0
- aury/agents/context_providers/memory.py +70 -0
- aury/agents/context_providers/message.py +130 -0
- aury/agents/context_providers/skill.py +50 -0
- aury/agents/context_providers/subagent.py +46 -0
- aury/agents/context_providers/tool.py +68 -0
- aury/agents/core/__init__.py +83 -0
- aury/agents/core/base.py +573 -0
- aury/agents/core/context.py +797 -0
- aury/agents/core/context_builder.py +303 -0
- aury/agents/core/event_bus/__init__.py +15 -0
- aury/agents/core/event_bus/bus.py +203 -0
- aury/agents/core/factory.py +169 -0
- aury/agents/core/isolator.py +97 -0
- aury/agents/core/logging.py +95 -0
- aury/agents/core/parallel.py +194 -0
- aury/agents/core/runner.py +139 -0
- aury/agents/core/services/__init__.py +5 -0
- aury/agents/core/services/file_session.py +144 -0
- aury/agents/core/services/message.py +53 -0
- aury/agents/core/services/session.py +53 -0
- aury/agents/core/signals.py +109 -0
- aury/agents/core/state.py +363 -0
- aury/agents/core/types/__init__.py +107 -0
- aury/agents/core/types/action.py +176 -0
- aury/agents/core/types/artifact.py +135 -0
- aury/agents/core/types/block.py +736 -0
- aury/agents/core/types/message.py +350 -0
- aury/agents/core/types/recall.py +144 -0
- aury/agents/core/types/session.py +257 -0
- aury/agents/core/types/subagent.py +154 -0
- aury/agents/core/types/tool.py +205 -0
- aury/agents/eval/__init__.py +331 -0
- aury/agents/hitl/__init__.py +57 -0
- aury/agents/hitl/ask_user.py +242 -0
- aury/agents/hitl/compaction.py +230 -0
- aury/agents/hitl/exceptions.py +87 -0
- aury/agents/hitl/permission.py +617 -0
- aury/agents/hitl/revert.py +216 -0
- aury/agents/llm/__init__.py +31 -0
- aury/agents/llm/adapter.py +367 -0
- aury/agents/llm/openai.py +294 -0
- aury/agents/llm/provider.py +476 -0
- aury/agents/mcp/__init__.py +153 -0
- aury/agents/memory/__init__.py +46 -0
- aury/agents/memory/compaction.py +394 -0
- aury/agents/memory/manager.py +465 -0
- aury/agents/memory/processor.py +177 -0
- aury/agents/memory/store.py +187 -0
- aury/agents/memory/types.py +137 -0
- aury/agents/messages/__init__.py +40 -0
- aury/agents/messages/config.py +47 -0
- aury/agents/messages/raw_store.py +224 -0
- aury/agents/messages/store.py +118 -0
- aury/agents/messages/types.py +88 -0
- aury/agents/middleware/__init__.py +31 -0
- aury/agents/middleware/base.py +341 -0
- aury/agents/middleware/chain.py +342 -0
- aury/agents/middleware/message.py +129 -0
- aury/agents/middleware/message_container.py +126 -0
- aury/agents/middleware/raw_message.py +153 -0
- aury/agents/middleware/truncation.py +139 -0
- aury/agents/middleware/types.py +81 -0
- aury/agents/plugin.py +162 -0
- aury/agents/react/__init__.py +4 -0
- aury/agents/react/agent.py +1923 -0
- aury/agents/sandbox/__init__.py +23 -0
- aury/agents/sandbox/local.py +239 -0
- aury/agents/sandbox/remote.py +200 -0
- aury/agents/sandbox/types.py +115 -0
- aury/agents/skill/__init__.py +16 -0
- aury/agents/skill/loader.py +180 -0
- aury/agents/skill/types.py +83 -0
- aury/agents/tool/__init__.py +39 -0
- aury/agents/tool/builtin/__init__.py +23 -0
- aury/agents/tool/builtin/ask_user.py +155 -0
- aury/agents/tool/builtin/bash.py +107 -0
- aury/agents/tool/builtin/delegate.py +726 -0
- aury/agents/tool/builtin/edit.py +121 -0
- aury/agents/tool/builtin/plan.py +277 -0
- aury/agents/tool/builtin/read.py +91 -0
- aury/agents/tool/builtin/thinking.py +111 -0
- aury/agents/tool/builtin/yield_result.py +130 -0
- aury/agents/tool/decorator.py +252 -0
- aury/agents/tool/set.py +204 -0
- aury/agents/usage/__init__.py +12 -0
- aury/agents/usage/tracker.py +236 -0
- aury/agents/workflow/__init__.py +85 -0
- aury/agents/workflow/adapter.py +268 -0
- aury/agents/workflow/dag.py +116 -0
- aury/agents/workflow/dsl.py +575 -0
- aury/agents/workflow/executor.py +659 -0
- aury/agents/workflow/expression.py +136 -0
- aury/agents/workflow/parser.py +182 -0
- aury/agents/workflow/state.py +145 -0
- aury/agents/workflow/types.py +86 -0
- aury_agent-0.0.4.dist-info/METADATA +90 -0
- aury_agent-0.0.4.dist-info/RECORD +149 -0
- aury_agent-0.0.4.dist-info/WHEEL +4 -0
- aury_agent-0.0.4.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""Expression evaluator for workflow DSL."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import re
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ExpressionError(Exception):
|
|
9
|
+
"""Expression evaluation error."""
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class DotDict(dict):
|
|
14
|
+
"""Dict that supports dot notation access.
|
|
15
|
+
|
|
16
|
+
Allows expressions like `inputs.json_data` instead of `inputs["json_data"]`.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def __getattr__(self, key: str) -> Any:
|
|
20
|
+
try:
|
|
21
|
+
value = self[key]
|
|
22
|
+
# Recursively wrap nested dicts
|
|
23
|
+
if isinstance(value, dict) and not isinstance(value, DotDict):
|
|
24
|
+
return DotDict(value)
|
|
25
|
+
return value
|
|
26
|
+
except KeyError:
|
|
27
|
+
raise AttributeError(f"'{type(self).__name__}' has no attribute '{key}'")
|
|
28
|
+
|
|
29
|
+
def __setattr__(self, key: str, value: Any) -> None:
|
|
30
|
+
self[key] = value
|
|
31
|
+
|
|
32
|
+
def __delattr__(self, key: str) -> None:
|
|
33
|
+
try:
|
|
34
|
+
del self[key]
|
|
35
|
+
except KeyError:
|
|
36
|
+
raise AttributeError(f"'{type(self).__name__}' has no attribute '{key}'")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class ExpressionEvaluator:
|
|
40
|
+
"""Expression evaluator for ${{ ... }} syntax."""
|
|
41
|
+
|
|
42
|
+
EXPR_PATTERN = re.compile(r'\$\{\{\s*(.+?)\s*\}\}')
|
|
43
|
+
|
|
44
|
+
def evaluate(
|
|
45
|
+
self,
|
|
46
|
+
expression: str,
|
|
47
|
+
context: dict[str, Any],
|
|
48
|
+
) -> Any:
|
|
49
|
+
"""Evaluate expression.
|
|
50
|
+
|
|
51
|
+
Supports:
|
|
52
|
+
- Full expressions: ${{ inputs.name }}
|
|
53
|
+
- Template strings: "Hello ${{ inputs.name }}!"
|
|
54
|
+
- Comparisons: ${{ inputs.count > 10 }}
|
|
55
|
+
"""
|
|
56
|
+
if expression.startswith("${{") and expression.endswith("}}"):
|
|
57
|
+
inner = expression[3:-2].strip()
|
|
58
|
+
return self._eval_inner(inner, context)
|
|
59
|
+
|
|
60
|
+
def replace(match: re.Match) -> str:
|
|
61
|
+
inner = match.group(1)
|
|
62
|
+
result = self._eval_inner(inner, context)
|
|
63
|
+
return str(result) if result is not None else ""
|
|
64
|
+
|
|
65
|
+
return self.EXPR_PATTERN.sub(replace, expression)
|
|
66
|
+
|
|
67
|
+
def _eval_inner(self, expr: str, context: dict[str, Any]) -> Any:
|
|
68
|
+
"""Evaluate inner expression.
|
|
69
|
+
|
|
70
|
+
Uses DotDict to allow dot notation access like `inputs.json_data`.
|
|
71
|
+
"""
|
|
72
|
+
# Wrap dicts in DotDict to support dot notation
|
|
73
|
+
inputs_data = context.get("inputs", {})
|
|
74
|
+
state_data = context.get("state", {})
|
|
75
|
+
|
|
76
|
+
safe_context = {
|
|
77
|
+
"inputs": DotDict(inputs_data) if isinstance(inputs_data, dict) else inputs_data,
|
|
78
|
+
"state": DotDict(state_data.to_dict() if hasattr(state_data, 'to_dict') else state_data) if state_data else DotDict({}),
|
|
79
|
+
"true": True,
|
|
80
|
+
"false": False,
|
|
81
|
+
"null": None,
|
|
82
|
+
"True": True,
|
|
83
|
+
"False": False,
|
|
84
|
+
"None": None,
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
# Add extra context variables (also wrap dicts)
|
|
88
|
+
for key, value in context.items():
|
|
89
|
+
if key not in ("inputs", "state"):
|
|
90
|
+
if isinstance(value, dict):
|
|
91
|
+
safe_context[key] = DotDict(value)
|
|
92
|
+
else:
|
|
93
|
+
safe_context[key] = value
|
|
94
|
+
|
|
95
|
+
try:
|
|
96
|
+
return eval(expr, {"__builtins__": {}}, safe_context)
|
|
97
|
+
except Exception as e:
|
|
98
|
+
raise ExpressionError(f"Failed to evaluate: {expr}") from e
|
|
99
|
+
|
|
100
|
+
def evaluate_condition(self, expr: str | None, context: dict[str, Any]) -> bool:
|
|
101
|
+
"""Evaluate condition expression."""
|
|
102
|
+
if not expr:
|
|
103
|
+
return True
|
|
104
|
+
result = self.evaluate(expr, context)
|
|
105
|
+
return bool(result)
|
|
106
|
+
|
|
107
|
+
def has_expression(self, value: str) -> bool:
|
|
108
|
+
"""Check if string contains expression."""
|
|
109
|
+
if not isinstance(value, str):
|
|
110
|
+
return False
|
|
111
|
+
return bool(self.EXPR_PATTERN.search(value))
|
|
112
|
+
|
|
113
|
+
def resolve_inputs(
|
|
114
|
+
self,
|
|
115
|
+
inputs: dict[str, Any],
|
|
116
|
+
context: dict[str, Any],
|
|
117
|
+
) -> dict[str, Any]:
|
|
118
|
+
"""Resolve all expressions in inputs dict."""
|
|
119
|
+
resolved = {}
|
|
120
|
+
|
|
121
|
+
for key, value in inputs.items():
|
|
122
|
+
if isinstance(value, str) and self.has_expression(value):
|
|
123
|
+
resolved[key] = self.evaluate(value, context)
|
|
124
|
+
elif isinstance(value, dict):
|
|
125
|
+
resolved[key] = self.resolve_inputs(value, context)
|
|
126
|
+
elif isinstance(value, list):
|
|
127
|
+
resolved[key] = [
|
|
128
|
+
self.evaluate(v, context)
|
|
129
|
+
if isinstance(v, str) and self.has_expression(v)
|
|
130
|
+
else v
|
|
131
|
+
for v in value
|
|
132
|
+
]
|
|
133
|
+
else:
|
|
134
|
+
resolved[key] = value
|
|
135
|
+
|
|
136
|
+
return resolved
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"""Workflow DSL parser."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import yaml
|
|
8
|
+
|
|
9
|
+
from .types import (
|
|
10
|
+
NodeType,
|
|
11
|
+
Position,
|
|
12
|
+
NodeSpec,
|
|
13
|
+
EdgeSpec,
|
|
14
|
+
WorkflowSpec,
|
|
15
|
+
Workflow,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class WorkflowValidationError(Exception):
|
|
20
|
+
"""Workflow validation error."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, errors: list[str]):
|
|
23
|
+
self.errors = errors
|
|
24
|
+
super().__init__(f"Workflow validation failed: {'; '.join(errors)}")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class WorkflowParser:
|
|
28
|
+
"""Workflow DSL parser."""
|
|
29
|
+
|
|
30
|
+
def parse(self, source: str | Path) -> Workflow:
|
|
31
|
+
"""Parse YAML file or string."""
|
|
32
|
+
raw = self._load_yaml(source)
|
|
33
|
+
spec = self._parse_spec(raw)
|
|
34
|
+
|
|
35
|
+
errors = self.validate(spec)
|
|
36
|
+
if errors:
|
|
37
|
+
raise WorkflowValidationError(errors)
|
|
38
|
+
|
|
39
|
+
return self._build_workflow(spec)
|
|
40
|
+
|
|
41
|
+
def _load_yaml(self, source: str | Path) -> dict[str, Any]:
|
|
42
|
+
"""Load YAML from file or string."""
|
|
43
|
+
if isinstance(source, Path):
|
|
44
|
+
with open(source, 'r') as f:
|
|
45
|
+
return yaml.safe_load(f)
|
|
46
|
+
elif isinstance(source, str):
|
|
47
|
+
if source.endswith('.yaml') or source.endswith('.yml'):
|
|
48
|
+
with open(source, 'r') as f:
|
|
49
|
+
return yaml.safe_load(f)
|
|
50
|
+
else:
|
|
51
|
+
return yaml.safe_load(source)
|
|
52
|
+
raise ValueError(f"Invalid source type: {type(source)}")
|
|
53
|
+
|
|
54
|
+
def _parse_spec(self, raw: dict[str, Any]) -> WorkflowSpec:
|
|
55
|
+
"""Parse raw dict to WorkflowSpec."""
|
|
56
|
+
nodes = []
|
|
57
|
+
for n in raw.get("nodes", []):
|
|
58
|
+
pos_data = n.get("position", {"x": 0, "y": 0})
|
|
59
|
+
position = Position(x=pos_data.get("x", 0), y=pos_data.get("y", 0))
|
|
60
|
+
|
|
61
|
+
node = NodeSpec(
|
|
62
|
+
id=n["id"],
|
|
63
|
+
type=NodeType(n["type"]),
|
|
64
|
+
position=position,
|
|
65
|
+
agent=n.get("agent"),
|
|
66
|
+
config=n.get("config", {}),
|
|
67
|
+
inputs=n.get("inputs", {}),
|
|
68
|
+
output=n.get("output"),
|
|
69
|
+
when=n.get("when"),
|
|
70
|
+
branches=n.get("branches"),
|
|
71
|
+
steps=n.get("steps"),
|
|
72
|
+
expression=n.get("expression"),
|
|
73
|
+
then_node=n.get("then"),
|
|
74
|
+
else_node=n.get("else"),
|
|
75
|
+
)
|
|
76
|
+
nodes.append(node)
|
|
77
|
+
|
|
78
|
+
edges = []
|
|
79
|
+
for e in raw.get("edges", []):
|
|
80
|
+
edge = EdgeSpec(
|
|
81
|
+
from_node=e["from"],
|
|
82
|
+
to_node=e["to"],
|
|
83
|
+
when=e.get("when"),
|
|
84
|
+
)
|
|
85
|
+
edges.append(edge)
|
|
86
|
+
|
|
87
|
+
return WorkflowSpec(
|
|
88
|
+
name=raw["name"],
|
|
89
|
+
version=raw.get("version", "1.0"),
|
|
90
|
+
description=raw.get("description"),
|
|
91
|
+
state=raw.get("state", {}),
|
|
92
|
+
inputs=raw.get("inputs", {}),
|
|
93
|
+
nodes=nodes,
|
|
94
|
+
edges=edges,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
def validate(self, spec: WorkflowSpec) -> list[str]:
|
|
98
|
+
"""Validate workflow spec."""
|
|
99
|
+
errors = []
|
|
100
|
+
node_ids = {n.id for n in spec.nodes}
|
|
101
|
+
|
|
102
|
+
# Check edge references
|
|
103
|
+
for edge in spec.edges:
|
|
104
|
+
if edge.from_node not in node_ids:
|
|
105
|
+
errors.append(f"Edge references unknown node: {edge.from_node}")
|
|
106
|
+
if edge.to_node not in node_ids:
|
|
107
|
+
errors.append(f"Edge references unknown node: {edge.to_node}")
|
|
108
|
+
|
|
109
|
+
# Check for trigger node
|
|
110
|
+
triggers = [n for n in spec.nodes if n.type == NodeType.TRIGGER]
|
|
111
|
+
if not triggers:
|
|
112
|
+
errors.append("Workflow must have at least one trigger node")
|
|
113
|
+
|
|
114
|
+
# Check for cycles
|
|
115
|
+
if self._has_cycle(spec):
|
|
116
|
+
errors.append("Workflow contains cycles")
|
|
117
|
+
|
|
118
|
+
# Check agent nodes
|
|
119
|
+
for node in spec.nodes:
|
|
120
|
+
if node.type == NodeType.AGENT and not node.agent:
|
|
121
|
+
errors.append(f"Agent node '{node.id}' must specify 'agent' type")
|
|
122
|
+
|
|
123
|
+
# Check condition nodes
|
|
124
|
+
for node in spec.nodes:
|
|
125
|
+
if node.type == NodeType.CONDITION:
|
|
126
|
+
if not node.expression:
|
|
127
|
+
errors.append(f"Condition node '{node.id}' must have 'expression'")
|
|
128
|
+
if not node.then_node:
|
|
129
|
+
errors.append(f"Condition node '{node.id}' must have 'then'")
|
|
130
|
+
|
|
131
|
+
return errors
|
|
132
|
+
|
|
133
|
+
def _has_cycle(self, spec: WorkflowSpec) -> bool:
|
|
134
|
+
"""Detect cycles using DFS."""
|
|
135
|
+
visited: set[str] = set()
|
|
136
|
+
rec_stack: set[str] = set()
|
|
137
|
+
|
|
138
|
+
adj: dict[str, list[str]] = {n.id: [] for n in spec.nodes}
|
|
139
|
+
for edge in spec.edges:
|
|
140
|
+
adj[edge.from_node].append(edge.to_node)
|
|
141
|
+
|
|
142
|
+
def dfs(node_id: str) -> bool:
|
|
143
|
+
visited.add(node_id)
|
|
144
|
+
rec_stack.add(node_id)
|
|
145
|
+
|
|
146
|
+
for neighbor in adj.get(node_id, []):
|
|
147
|
+
if neighbor not in visited:
|
|
148
|
+
if dfs(neighbor):
|
|
149
|
+
return True
|
|
150
|
+
elif neighbor in rec_stack:
|
|
151
|
+
return True
|
|
152
|
+
|
|
153
|
+
rec_stack.remove(node_id)
|
|
154
|
+
return False
|
|
155
|
+
|
|
156
|
+
for node in spec.nodes:
|
|
157
|
+
if node.id not in visited:
|
|
158
|
+
if dfs(node.id):
|
|
159
|
+
return True
|
|
160
|
+
|
|
161
|
+
return False
|
|
162
|
+
|
|
163
|
+
def _build_workflow(self, spec: WorkflowSpec) -> Workflow:
|
|
164
|
+
"""Build executable workflow."""
|
|
165
|
+
node_map = {n.id: n for n in spec.nodes}
|
|
166
|
+
|
|
167
|
+
incoming_edges: dict[str, list[str]] = {n.id: [] for n in spec.nodes}
|
|
168
|
+
outgoing_edges: dict[str, list[str]] = {n.id: [] for n in spec.nodes}
|
|
169
|
+
edge_conditions: dict[tuple[str, str], str | None] = {}
|
|
170
|
+
|
|
171
|
+
for edge in spec.edges:
|
|
172
|
+
incoming_edges[edge.to_node].append(edge.from_node)
|
|
173
|
+
outgoing_edges[edge.from_node].append(edge.to_node)
|
|
174
|
+
edge_conditions[(edge.from_node, edge.to_node)] = edge.when
|
|
175
|
+
|
|
176
|
+
return Workflow(
|
|
177
|
+
spec=spec,
|
|
178
|
+
node_map=node_map,
|
|
179
|
+
incoming_edges=incoming_edges,
|
|
180
|
+
outgoing_edges=outgoing_edges,
|
|
181
|
+
edge_conditions=edge_conditions,
|
|
182
|
+
)
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""Workflow state management with branch isolation."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from collections import ChainMap
|
|
5
|
+
from typing import Any, Callable, Iterator, Protocol
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class MergeStrategy(Protocol):
|
|
9
|
+
"""Merge strategy protocol."""
|
|
10
|
+
|
|
11
|
+
def merge(self, results: list[Any]) -> Any:
|
|
12
|
+
"""Merge parallel results."""
|
|
13
|
+
...
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class CollectListStrategy:
|
|
17
|
+
"""Collect results as list."""
|
|
18
|
+
|
|
19
|
+
def merge(self, results: list[Any]) -> list[Any]:
|
|
20
|
+
return [r for r in results if r is not None]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class CollectDictStrategy:
|
|
24
|
+
"""Collect results as dict."""
|
|
25
|
+
|
|
26
|
+
def __init__(self, key_fn: Callable[[Any], str] | None = None):
|
|
27
|
+
self.key_fn = key_fn or (lambda x: str(id(x)))
|
|
28
|
+
|
|
29
|
+
def merge(self, results: list[Any]) -> dict[str, Any]:
|
|
30
|
+
return {self.key_fn(r): r for r in results if r is not None}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class FirstSuccessStrategy:
|
|
34
|
+
"""Take first non-None result."""
|
|
35
|
+
|
|
36
|
+
def merge(self, results: list[Any]) -> Any:
|
|
37
|
+
return next((r for r in results if r is not None), None)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class CustomMergeStrategy:
|
|
41
|
+
"""Custom merge function."""
|
|
42
|
+
|
|
43
|
+
def __init__(self, merge_fn: Callable[[list[Any]], Any]):
|
|
44
|
+
self.merge_fn = merge_fn
|
|
45
|
+
|
|
46
|
+
def merge(self, results: list[Any]) -> Any:
|
|
47
|
+
return self.merge_fn(results)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class WorkflowState:
|
|
51
|
+
"""Workflow state with branch isolation.
|
|
52
|
+
|
|
53
|
+
Uses ChainMap for copy-on-write semantics.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
def __init__(self, parent: WorkflowState | None = None):
|
|
57
|
+
self._local: dict[str, Any] = {}
|
|
58
|
+
self._parent = parent
|
|
59
|
+
|
|
60
|
+
if parent:
|
|
61
|
+
self._chain: ChainMap[str, Any] = ChainMap(self._local, parent._chain)
|
|
62
|
+
else:
|
|
63
|
+
self._chain = ChainMap(self._local)
|
|
64
|
+
|
|
65
|
+
def __getitem__(self, key: str) -> Any:
|
|
66
|
+
return self._chain[key]
|
|
67
|
+
|
|
68
|
+
def __setitem__(self, key: str, value: Any) -> None:
|
|
69
|
+
self._local[key] = value
|
|
70
|
+
|
|
71
|
+
def __contains__(self, key: str) -> bool:
|
|
72
|
+
return key in self._chain
|
|
73
|
+
|
|
74
|
+
def __iter__(self) -> Iterator[str]:
|
|
75
|
+
return iter(self._chain)
|
|
76
|
+
|
|
77
|
+
def get(self, key: str, default: Any = None) -> Any:
|
|
78
|
+
return self._chain.get(key, default)
|
|
79
|
+
|
|
80
|
+
def keys(self) -> Any:
|
|
81
|
+
return self._chain.keys()
|
|
82
|
+
|
|
83
|
+
def values(self) -> Any:
|
|
84
|
+
return self._chain.values()
|
|
85
|
+
|
|
86
|
+
def items(self) -> Any:
|
|
87
|
+
return self._chain.items()
|
|
88
|
+
|
|
89
|
+
def create_branch(self) -> WorkflowState:
|
|
90
|
+
"""Create branch for parallel execution."""
|
|
91
|
+
return WorkflowState(parent=self)
|
|
92
|
+
|
|
93
|
+
def get_local_changes(self) -> dict[str, Any]:
|
|
94
|
+
"""Get local changes."""
|
|
95
|
+
return dict(self._local)
|
|
96
|
+
|
|
97
|
+
def merge_from(
|
|
98
|
+
self,
|
|
99
|
+
other: WorkflowState,
|
|
100
|
+
strategy: str = "overwrite",
|
|
101
|
+
) -> None:
|
|
102
|
+
"""Merge changes from another state."""
|
|
103
|
+
changes = other.get_local_changes()
|
|
104
|
+
|
|
105
|
+
for key, value in changes.items():
|
|
106
|
+
if strategy == "overwrite":
|
|
107
|
+
self._local[key] = value
|
|
108
|
+
elif strategy == "append":
|
|
109
|
+
if key in self._local:
|
|
110
|
+
existing = self._local[key]
|
|
111
|
+
if isinstance(existing, list):
|
|
112
|
+
existing.append(value)
|
|
113
|
+
else:
|
|
114
|
+
self._local[key] = [existing, value]
|
|
115
|
+
else:
|
|
116
|
+
self._local[key] = [value]
|
|
117
|
+
|
|
118
|
+
def to_dict(self) -> dict[str, Any]:
|
|
119
|
+
"""Convert to regular dict."""
|
|
120
|
+
return dict(self._chain)
|
|
121
|
+
|
|
122
|
+
@classmethod
|
|
123
|
+
def from_dict(cls, data: dict[str, Any]) -> "WorkflowState":
|
|
124
|
+
"""Create from dict."""
|
|
125
|
+
state = cls()
|
|
126
|
+
state._local.update(data)
|
|
127
|
+
return state
|
|
128
|
+
|
|
129
|
+
def clear(self) -> None:
|
|
130
|
+
"""Clear local state."""
|
|
131
|
+
self._local.clear()
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def get_merge_strategy(strategy: str, **kwargs: Any) -> MergeStrategy:
|
|
135
|
+
"""Get merge strategy by name."""
|
|
136
|
+
match strategy:
|
|
137
|
+
case "collect_list":
|
|
138
|
+
return CollectListStrategy()
|
|
139
|
+
case "collect_dict":
|
|
140
|
+
key_fn = kwargs.get("key_fn")
|
|
141
|
+
return CollectDictStrategy(key_fn=key_fn)
|
|
142
|
+
case "first_success":
|
|
143
|
+
return FirstSuccessStrategy()
|
|
144
|
+
case _:
|
|
145
|
+
return CollectListStrategy()
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""Workflow DSL types."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from enum import Enum
|
|
6
|
+
from typing import Any, TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from ..middleware import Middleware, MiddlewareChain
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class NodeType(Enum):
|
|
13
|
+
"""Workflow node types."""
|
|
14
|
+
TRIGGER = "trigger"
|
|
15
|
+
AGENT = "agent"
|
|
16
|
+
PARALLEL = "parallel"
|
|
17
|
+
SEQUENCE = "sequence"
|
|
18
|
+
CONDITION = "condition"
|
|
19
|
+
TERMINAL = "terminal"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class Position:
|
|
24
|
+
"""Node position for visual editors."""
|
|
25
|
+
x: float = 0.0
|
|
26
|
+
y: float = 0.0
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class NodeSpec:
|
|
31
|
+
"""Node specification."""
|
|
32
|
+
id: str
|
|
33
|
+
type: NodeType
|
|
34
|
+
position: Position = field(default_factory=Position)
|
|
35
|
+
agent: str | None = None
|
|
36
|
+
config: dict[str, Any] = field(default_factory=dict)
|
|
37
|
+
inputs: dict[str, Any] = field(default_factory=dict)
|
|
38
|
+
output: str | None = None
|
|
39
|
+
when: str | None = None # Conditional expression
|
|
40
|
+
|
|
41
|
+
# For parallel/sequence nodes
|
|
42
|
+
branches: list[dict[str, Any]] | None = None
|
|
43
|
+
steps: list[str] | None = None
|
|
44
|
+
|
|
45
|
+
# For condition nodes
|
|
46
|
+
expression: str | None = None
|
|
47
|
+
then_node: str | None = None
|
|
48
|
+
else_node: str | None = None
|
|
49
|
+
|
|
50
|
+
# Node-level middleware (overrides/extends workflow middleware)
|
|
51
|
+
middleware: list["Middleware"] | None = None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass
|
|
55
|
+
class EdgeSpec:
|
|
56
|
+
"""Edge specification."""
|
|
57
|
+
from_node: str
|
|
58
|
+
to_node: str
|
|
59
|
+
when: str | None = None # Conditional edge
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass
|
|
63
|
+
class WorkflowSpec:
|
|
64
|
+
"""Workflow specification from DSL."""
|
|
65
|
+
name: str
|
|
66
|
+
version: str = "1.0"
|
|
67
|
+
description: str | None = None
|
|
68
|
+
state: dict[str, str] = field(default_factory=dict)
|
|
69
|
+
inputs: dict[str, dict[str, Any]] = field(default_factory=dict)
|
|
70
|
+
nodes: list[NodeSpec] = field(default_factory=list)
|
|
71
|
+
edges: list[EdgeSpec] = field(default_factory=list)
|
|
72
|
+
|
|
73
|
+
# Workflow-level middleware (applied to all nodes)
|
|
74
|
+
middleware: "MiddlewareChain | None" = None
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@dataclass
|
|
78
|
+
class Workflow:
|
|
79
|
+
"""Parsed executable workflow."""
|
|
80
|
+
spec: WorkflowSpec
|
|
81
|
+
|
|
82
|
+
# Computed properties
|
|
83
|
+
node_map: dict[str, NodeSpec] = field(default_factory=dict)
|
|
84
|
+
incoming_edges: dict[str, list[str]] = field(default_factory=dict)
|
|
85
|
+
outgoing_edges: dict[str, list[str]] = field(default_factory=dict)
|
|
86
|
+
edge_conditions: dict[tuple[str, str], str | None] = field(default_factory=dict)
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: aury-agent
|
|
3
|
+
Version: 0.0.4
|
|
4
|
+
Summary: Aury Agent Framework - React Agent and Workflow orchestration
|
|
5
|
+
Author: Aury Team
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: agent,llm,react,workflow
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
13
|
+
Requires-Python: >=3.13
|
|
14
|
+
Requires-Dist: aiofiles>=23.0.0
|
|
15
|
+
Requires-Dist: aury-ai-model[all]
|
|
16
|
+
Requires-Dist: pydantic>=2.0.0
|
|
17
|
+
Requires-Dist: python-dotenv>=1.0.0
|
|
18
|
+
Requires-Dist: pyyaml>=6.0.0
|
|
19
|
+
Requires-Dist: rich>=13.0.0
|
|
20
|
+
Requires-Dist: typer>=0.9.0
|
|
21
|
+
Provides-Extra: all
|
|
22
|
+
Requires-Dist: anthropic>=0.20.0; extra == 'all'
|
|
23
|
+
Requires-Dist: openai>=1.0.0; extra == 'all'
|
|
24
|
+
Provides-Extra: anthropic
|
|
25
|
+
Requires-Dist: anthropic>=0.20.0; extra == 'anthropic'
|
|
26
|
+
Provides-Extra: memory
|
|
27
|
+
Requires-Dist: chromadb>=0.4.0; extra == 'memory'
|
|
28
|
+
Provides-Extra: openai
|
|
29
|
+
Requires-Dist: openai>=1.0.0; extra == 'openai'
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
|
|
32
|
+
# Aury Agents
|
|
33
|
+
|
|
34
|
+
A flexible Agent framework supporting both React Agent (autonomous loop) and Workflow (DAG orchestration) patterns.
|
|
35
|
+
|
|
36
|
+
## Features
|
|
37
|
+
|
|
38
|
+
- **React Agent**: Autonomous agent with tool calling loop
|
|
39
|
+
- **Workflow**: DAG-based orchestration for complex pipelines
|
|
40
|
+
- **Session Management**: Event-sourced session with snapshot and revert
|
|
41
|
+
- **Permission System**: Fine-grained tool execution control
|
|
42
|
+
- **Memory System**: Semantic memory with vector storage
|
|
43
|
+
- **Compaction**: Token-efficient session history management
|
|
44
|
+
|
|
45
|
+
## Installation
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pip install aury-agent
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Quick Start
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
from aury.agents import ReactAgent, Tool
|
|
55
|
+
|
|
56
|
+
# Define a tool
|
|
57
|
+
@tool
|
|
58
|
+
def search(query: str) -> str:
|
|
59
|
+
"""Search the web."""
|
|
60
|
+
return f"Results for: {query}"
|
|
61
|
+
|
|
62
|
+
# Create agent
|
|
63
|
+
agent = ReactAgent(
|
|
64
|
+
tools=[search],
|
|
65
|
+
llm_provider=your_llm_provider,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
# Run
|
|
69
|
+
result = await agent.run("What is the weather today?")
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Development
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
# Install dev dependencies
|
|
76
|
+
uv sync
|
|
77
|
+
|
|
78
|
+
# Run tests
|
|
79
|
+
pytest
|
|
80
|
+
|
|
81
|
+
# Type check
|
|
82
|
+
mypy aury
|
|
83
|
+
|
|
84
|
+
# Lint
|
|
85
|
+
ruff check aury
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## License
|
|
89
|
+
|
|
90
|
+
MIT
|