puffinflow 2.dev0__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.
- puffinflow/__init__.py +132 -0
- puffinflow/core/__init__.py +110 -0
- puffinflow/core/agent/__init__.py +320 -0
- puffinflow/core/agent/base.py +1635 -0
- puffinflow/core/agent/checkpoint.py +50 -0
- puffinflow/core/agent/context.py +521 -0
- puffinflow/core/agent/decorators/__init__.py +90 -0
- puffinflow/core/agent/decorators/builder.py +454 -0
- puffinflow/core/agent/decorators/flexible.py +714 -0
- puffinflow/core/agent/decorators/inspection.py +144 -0
- puffinflow/core/agent/dependencies.py +57 -0
- puffinflow/core/agent/scheduling/__init__.py +21 -0
- puffinflow/core/agent/scheduling/builder.py +160 -0
- puffinflow/core/agent/scheduling/exceptions.py +35 -0
- puffinflow/core/agent/scheduling/inputs.py +137 -0
- puffinflow/core/agent/scheduling/parser.py +209 -0
- puffinflow/core/agent/scheduling/scheduler.py +413 -0
- puffinflow/core/agent/state.py +141 -0
- puffinflow/core/config.py +62 -0
- puffinflow/core/coordination/__init__.py +137 -0
- puffinflow/core/coordination/agent_group.py +359 -0
- puffinflow/core/coordination/agent_pool.py +629 -0
- puffinflow/core/coordination/agent_team.py +577 -0
- puffinflow/core/coordination/coordinator.py +720 -0
- puffinflow/core/coordination/deadlock.py +1759 -0
- puffinflow/core/coordination/fluent_api.py +421 -0
- puffinflow/core/coordination/primitives.py +478 -0
- puffinflow/core/coordination/rate_limiter.py +520 -0
- puffinflow/core/observability/__init__.py +47 -0
- puffinflow/core/observability/agent.py +139 -0
- puffinflow/core/observability/alerting.py +73 -0
- puffinflow/core/observability/config.py +127 -0
- puffinflow/core/observability/context.py +88 -0
- puffinflow/core/observability/core.py +147 -0
- puffinflow/core/observability/decorators.py +105 -0
- puffinflow/core/observability/events.py +71 -0
- puffinflow/core/observability/interfaces.py +196 -0
- puffinflow/core/observability/metrics.py +137 -0
- puffinflow/core/observability/tracing.py +209 -0
- puffinflow/core/reliability/__init__.py +27 -0
- puffinflow/core/reliability/bulkhead.py +96 -0
- puffinflow/core/reliability/circuit_breaker.py +149 -0
- puffinflow/core/reliability/leak_detector.py +122 -0
- puffinflow/core/resources/__init__.py +77 -0
- puffinflow/core/resources/allocation.py +790 -0
- puffinflow/core/resources/pool.py +645 -0
- puffinflow/core/resources/quotas.py +567 -0
- puffinflow/core/resources/requirements.py +217 -0
- puffinflow/version.py +21 -0
- puffinflow-2.dev0.dist-info/METADATA +334 -0
- puffinflow-2.dev0.dist-info/RECORD +55 -0
- puffinflow-2.dev0.dist-info/WHEEL +5 -0
- puffinflow-2.dev0.dist-info/entry_points.txt +3 -0
- puffinflow-2.dev0.dist-info/licenses/LICENSE +21 -0
- puffinflow-2.dev0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"""Utilities for inspecting decorated states."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Callable, Optional
|
|
4
|
+
|
|
5
|
+
from ...coordination.rate_limiter import RateLimitStrategy
|
|
6
|
+
from ...resources.requirements import ResourceRequirements
|
|
7
|
+
from ..state import Priority
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def is_puffinflow_state(func: Callable) -> bool:
|
|
11
|
+
"""Check if a function is a PuffinFlow state."""
|
|
12
|
+
return getattr(func, "_puffinflow_state", False)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_state_config(func: Callable) -> Optional[dict[str, Any]]:
|
|
16
|
+
"""Get the configuration of a PuffinFlow state."""
|
|
17
|
+
return getattr(func, "_state_config", None)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def get_state_requirements(func: Callable) -> Optional[ResourceRequirements]:
|
|
21
|
+
"""Get the resource requirements of a PuffinFlow state."""
|
|
22
|
+
return getattr(func, "_resource_requirements", None)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def get_state_rate_limit(func: Callable) -> Optional[dict[str, Any]]:
|
|
26
|
+
"""Get the rate limiting configuration of a PuffinFlow state."""
|
|
27
|
+
if not hasattr(func, "_rate_limit"):
|
|
28
|
+
return None
|
|
29
|
+
|
|
30
|
+
strategy = getattr(func, "_rate_strategy", RateLimitStrategy.TOKEN_BUCKET)
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
"rate": func._rate_limit,
|
|
34
|
+
"burst": getattr(func, "_burst_limit", None),
|
|
35
|
+
"strategy": strategy,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def get_state_coordination(func: Callable) -> Optional[dict[str, Any]]:
|
|
40
|
+
"""Get the coordination configuration of a PuffinFlow state."""
|
|
41
|
+
primitive = getattr(func, "_coordination_primitive", None)
|
|
42
|
+
if primitive is None:
|
|
43
|
+
return None
|
|
44
|
+
|
|
45
|
+
return {"type": primitive, "config": getattr(func, "_coordination_config", {})}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def list_state_metadata(func: Callable) -> dict[str, Any]:
|
|
49
|
+
"""Get all metadata for a PuffinFlow state."""
|
|
50
|
+
if not is_puffinflow_state(func):
|
|
51
|
+
return {}
|
|
52
|
+
|
|
53
|
+
# Get description, falling back to docstring if not set
|
|
54
|
+
description = getattr(func, "_state_description", "")
|
|
55
|
+
if not description and func.__doc__:
|
|
56
|
+
description = func.__doc__.strip()
|
|
57
|
+
|
|
58
|
+
# If still no description, use fallback
|
|
59
|
+
if not description:
|
|
60
|
+
description = f"State: {func.__name__}"
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
"name": getattr(func, "_state_name", func.__name__),
|
|
64
|
+
"description": description,
|
|
65
|
+
"tags": getattr(func, "_state_tags", {}),
|
|
66
|
+
"priority": getattr(func, "_priority", Priority.NORMAL),
|
|
67
|
+
"requirements": get_state_requirements(func),
|
|
68
|
+
"rate_limit": get_state_rate_limit(func),
|
|
69
|
+
"coordination": get_state_coordination(func),
|
|
70
|
+
"dependencies": getattr(func, "_dependency_configs", {}),
|
|
71
|
+
"preemptible": getattr(func, "_preemptible", False),
|
|
72
|
+
"checkpoint_interval": getattr(func, "_checkpoint_interval", None),
|
|
73
|
+
"cleanup_on_failure": getattr(func, "_cleanup_on_failure", True),
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def compare_states(func1: Callable, func2: Callable) -> dict[str, Any]:
|
|
78
|
+
"""Compare two state configurations."""
|
|
79
|
+
config1 = get_state_config(func1) or {}
|
|
80
|
+
config2 = get_state_config(func2) or {}
|
|
81
|
+
|
|
82
|
+
differences = {}
|
|
83
|
+
all_keys = set(config1.keys()) | set(config2.keys())
|
|
84
|
+
|
|
85
|
+
for key in all_keys:
|
|
86
|
+
val1 = config1.get(key)
|
|
87
|
+
val2 = config2.get(key)
|
|
88
|
+
if val1 != val2:
|
|
89
|
+
differences[key] = {"func1": val1, "func2": val2}
|
|
90
|
+
|
|
91
|
+
return differences
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def get_state_summary(func: Callable) -> str:
|
|
95
|
+
"""Get a human-readable summary of state configuration."""
|
|
96
|
+
if not is_puffinflow_state(func):
|
|
97
|
+
return f"{func.__name__}: Not a PuffinFlow state"
|
|
98
|
+
|
|
99
|
+
config = get_state_config(func)
|
|
100
|
+
if config is None or not isinstance(config, dict):
|
|
101
|
+
return f"{func.__name__}: No configuration found"
|
|
102
|
+
|
|
103
|
+
summary_parts = [f"{func.__name__}:"]
|
|
104
|
+
|
|
105
|
+
# Resources
|
|
106
|
+
resources = []
|
|
107
|
+
cpu = config.get("cpu", 0)
|
|
108
|
+
if cpu is not None and cpu > 0:
|
|
109
|
+
resources.append(f"CPU={cpu}")
|
|
110
|
+
memory = config.get("memory", 0)
|
|
111
|
+
if memory is not None and memory > 0:
|
|
112
|
+
resources.append(f"Memory={memory}MB")
|
|
113
|
+
gpu = config.get("gpu", 0)
|
|
114
|
+
if gpu is not None and gpu > 0:
|
|
115
|
+
resources.append(f"GPU={gpu}")
|
|
116
|
+
|
|
117
|
+
if resources:
|
|
118
|
+
summary_parts.append(f" Resources: {', '.join(resources)}")
|
|
119
|
+
|
|
120
|
+
# Priority
|
|
121
|
+
priority = config.get("priority")
|
|
122
|
+
if priority and priority != Priority.NORMAL:
|
|
123
|
+
summary_parts.append(f" Priority: {priority.name}")
|
|
124
|
+
|
|
125
|
+
# Coordination
|
|
126
|
+
coord_info = []
|
|
127
|
+
if config.get("mutex"):
|
|
128
|
+
coord_info.append("Mutex")
|
|
129
|
+
if config.get("semaphore"):
|
|
130
|
+
coord_info.append(f"Semaphore({config['semaphore']})")
|
|
131
|
+
if config.get("barrier"):
|
|
132
|
+
coord_info.append(f"Barrier({config['barrier']})")
|
|
133
|
+
if config.get("rate_limit"):
|
|
134
|
+
coord_info.append(f"RateLimit({config['rate_limit']}/s)")
|
|
135
|
+
|
|
136
|
+
if coord_info:
|
|
137
|
+
summary_parts.append(f" Coordination: {', '.join(coord_info)}")
|
|
138
|
+
|
|
139
|
+
# Dependencies
|
|
140
|
+
deps = config.get("depends_on")
|
|
141
|
+
if deps:
|
|
142
|
+
summary_parts.append(f" Dependencies: {', '.join(deps)}")
|
|
143
|
+
|
|
144
|
+
return "\n".join(summary_parts)
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Dependency management types."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from typing import TYPE_CHECKING, Any, Callable, Optional
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from .base import Agent
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class DependencyType(Enum):
|
|
12
|
+
"""Types of dependencies between states."""
|
|
13
|
+
|
|
14
|
+
REQUIRED = "required" # Must complete before state can run
|
|
15
|
+
OPTIONAL = "optional" # Will wait if running, otherwise skips
|
|
16
|
+
PARALLEL = "parallel" # Can run in parallel with dependency
|
|
17
|
+
SEQUENTIAL = "sequential" # Must run after dependency completes
|
|
18
|
+
CONDITIONAL = "conditional" # Depends on condition function
|
|
19
|
+
TIMEOUT = "timeout" # Wait for max time then continue
|
|
20
|
+
XOR = "xor" # Only one dependency needs to be satisfied
|
|
21
|
+
AND = "and" # All dependencies must be satisfied
|
|
22
|
+
OR = "or" # At least one dependency must be satisfied
|
|
23
|
+
|
|
24
|
+
def __str__(self) -> str:
|
|
25
|
+
return f"DependencyType.{self.name}"
|
|
26
|
+
|
|
27
|
+
def __repr__(self) -> str:
|
|
28
|
+
return f"DependencyType.{self.name}"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class DependencyLifecycle(Enum):
|
|
32
|
+
"""Lifecycle management for dependencies."""
|
|
33
|
+
|
|
34
|
+
ONCE = "once" # Dependency only needs to be satisfied once
|
|
35
|
+
ALWAYS = "always" # Dependency must be satisfied every time
|
|
36
|
+
SESSION = "session" # Dependency valid for current run() execution
|
|
37
|
+
TEMPORARY = "temporary" # Dependency expires after specified time
|
|
38
|
+
PERIODIC = "periodic" # Must be re-satisfied after specified interval
|
|
39
|
+
|
|
40
|
+
def __str__(self) -> str:
|
|
41
|
+
return f"DependencyLifecycle.{self.name}"
|
|
42
|
+
|
|
43
|
+
def __repr__(self) -> str:
|
|
44
|
+
return f"DependencyLifecycle.{self.name}"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class DependencyConfig:
|
|
49
|
+
"""Configuration for state dependencies."""
|
|
50
|
+
|
|
51
|
+
type: DependencyType
|
|
52
|
+
lifecycle: DependencyLifecycle = DependencyLifecycle.ALWAYS
|
|
53
|
+
condition: Optional[Callable[["Agent"], bool]] = None
|
|
54
|
+
expiry: Optional[float] = None
|
|
55
|
+
interval: Optional[float] = None
|
|
56
|
+
timeout: Optional[float] = None
|
|
57
|
+
retry_policy: Optional[dict[str, Any]] = None
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Agent scheduling module for PuffinFlow."""
|
|
2
|
+
|
|
3
|
+
from .builder import ScheduleBuilder
|
|
4
|
+
from .exceptions import InvalidInputTypeError, InvalidScheduleError, SchedulingError
|
|
5
|
+
from .inputs import InputType, ScheduledInput, parse_magic_prefix
|
|
6
|
+
from .parser import ScheduleParser, parse_schedule_string
|
|
7
|
+
from .scheduler import GlobalScheduler, ScheduledAgent
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"GlobalScheduler",
|
|
11
|
+
"InputType",
|
|
12
|
+
"InvalidInputTypeError",
|
|
13
|
+
"InvalidScheduleError",
|
|
14
|
+
"ScheduleBuilder",
|
|
15
|
+
"ScheduleParser",
|
|
16
|
+
"ScheduledAgent",
|
|
17
|
+
"ScheduledInput",
|
|
18
|
+
"SchedulingError",
|
|
19
|
+
"parse_magic_prefix",
|
|
20
|
+
"parse_schedule_string",
|
|
21
|
+
]
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"""Fluent API builder for agent scheduling."""
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Any
|
|
4
|
+
|
|
5
|
+
from .inputs import ScheduledInput, parse_inputs
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from ..base import Agent
|
|
9
|
+
from .scheduler import ScheduledAgent
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ScheduleBuilder:
|
|
13
|
+
"""Fluent API builder for scheduling agents."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, agent: "Agent", schedule_string: str):
|
|
16
|
+
self._agent = agent
|
|
17
|
+
self._schedule_string = schedule_string
|
|
18
|
+
self._inputs: dict[str, ScheduledInput] = {}
|
|
19
|
+
|
|
20
|
+
def with_inputs(self, **inputs: Any) -> "ScheduleBuilder":
|
|
21
|
+
"""Add regular variable inputs.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
**inputs: Input key-value pairs
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
Self for chaining
|
|
28
|
+
"""
|
|
29
|
+
parsed = parse_inputs(**inputs)
|
|
30
|
+
self._inputs.update(parsed)
|
|
31
|
+
return self
|
|
32
|
+
|
|
33
|
+
def with_secrets(self, **secrets: Any) -> "ScheduleBuilder":
|
|
34
|
+
"""Add secret inputs.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
**secrets: Secret key-value pairs
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
Self for chaining
|
|
41
|
+
"""
|
|
42
|
+
for key, value in secrets.items():
|
|
43
|
+
prefixed_value = f"secret:{value}"
|
|
44
|
+
parsed = parse_inputs(**{key: prefixed_value})
|
|
45
|
+
self._inputs.update(parsed)
|
|
46
|
+
return self
|
|
47
|
+
|
|
48
|
+
def with_constants(self, **constants: Any) -> "ScheduleBuilder":
|
|
49
|
+
"""Add constant inputs.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
**constants: Constant key-value pairs
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
Self for chaining
|
|
56
|
+
"""
|
|
57
|
+
for key, value in constants.items():
|
|
58
|
+
prefixed_value = f"const:{value}"
|
|
59
|
+
parsed = parse_inputs(**{key: prefixed_value})
|
|
60
|
+
self._inputs.update(parsed)
|
|
61
|
+
return self
|
|
62
|
+
|
|
63
|
+
def with_cache(self, ttl: int, **cached_inputs: Any) -> "ScheduleBuilder":
|
|
64
|
+
"""Add cached inputs with TTL.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
ttl: Time to live in seconds
|
|
68
|
+
**cached_inputs: Cached input key-value pairs
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
Self for chaining
|
|
72
|
+
"""
|
|
73
|
+
for key, value in cached_inputs.items():
|
|
74
|
+
# Convert value to string for cache prefix
|
|
75
|
+
if isinstance(value, (dict, list)):
|
|
76
|
+
import json
|
|
77
|
+
|
|
78
|
+
value_str = json.dumps(value)
|
|
79
|
+
else:
|
|
80
|
+
value_str = str(value)
|
|
81
|
+
prefixed_value = f"cache:{ttl}:{value_str}"
|
|
82
|
+
parsed = parse_inputs(**{key: prefixed_value})
|
|
83
|
+
self._inputs.update(parsed)
|
|
84
|
+
return self
|
|
85
|
+
|
|
86
|
+
def with_typed(self, **typed_inputs: Any) -> "ScheduleBuilder":
|
|
87
|
+
"""Add typed inputs.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
**typed_inputs: Typed input key-value pairs
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
Self for chaining
|
|
94
|
+
"""
|
|
95
|
+
for key, value in typed_inputs.items():
|
|
96
|
+
# Convert value to string for typed prefix
|
|
97
|
+
if isinstance(value, (dict, list)):
|
|
98
|
+
import json
|
|
99
|
+
|
|
100
|
+
value_str = json.dumps(value)
|
|
101
|
+
else:
|
|
102
|
+
value_str = str(value)
|
|
103
|
+
prefixed_value = f"typed:{value_str}"
|
|
104
|
+
parsed = parse_inputs(**{key: prefixed_value})
|
|
105
|
+
self._inputs.update(parsed)
|
|
106
|
+
return self
|
|
107
|
+
|
|
108
|
+
def with_outputs(self, **outputs: Any) -> "ScheduleBuilder":
|
|
109
|
+
"""Add pre-set outputs.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
**outputs: Output key-value pairs
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
Self for chaining
|
|
116
|
+
"""
|
|
117
|
+
for key, value in outputs.items():
|
|
118
|
+
prefixed_value = f"output:{value}"
|
|
119
|
+
parsed = parse_inputs(**{key: prefixed_value})
|
|
120
|
+
self._inputs.update(parsed)
|
|
121
|
+
return self
|
|
122
|
+
|
|
123
|
+
def run(self) -> "ScheduledAgent":
|
|
124
|
+
"""Execute the scheduling with configured inputs.
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
ScheduledAgent instance
|
|
128
|
+
"""
|
|
129
|
+
# Convert ScheduledInput objects back to input format for schedule method
|
|
130
|
+
input_kwargs = {}
|
|
131
|
+
for key, scheduled_input in self._inputs.items():
|
|
132
|
+
if scheduled_input.input_type.value == "secret":
|
|
133
|
+
input_kwargs[key] = f"secret:{scheduled_input.value}"
|
|
134
|
+
elif scheduled_input.input_type.value == "const":
|
|
135
|
+
input_kwargs[key] = f"const:{scheduled_input.value}"
|
|
136
|
+
elif scheduled_input.input_type.value == "cache":
|
|
137
|
+
input_kwargs[
|
|
138
|
+
key
|
|
139
|
+
] = f"cache:{scheduled_input.ttl}:{scheduled_input.value}"
|
|
140
|
+
elif scheduled_input.input_type.value == "typed":
|
|
141
|
+
input_kwargs[key] = f"typed:{scheduled_input.value}"
|
|
142
|
+
elif scheduled_input.input_type.value == "output":
|
|
143
|
+
input_kwargs[key] = f"output:{scheduled_input.value}"
|
|
144
|
+
else: # variable
|
|
145
|
+
input_kwargs[key] = scheduled_input.value
|
|
146
|
+
|
|
147
|
+
return self._agent.schedule(self._schedule_string, **input_kwargs)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def create_schedule_builder(agent: "Agent", schedule_string: str) -> ScheduleBuilder:
|
|
151
|
+
"""Create a schedule builder for fluent API.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
agent: Agent to schedule
|
|
155
|
+
schedule_string: Schedule string
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
ScheduleBuilder instance
|
|
159
|
+
"""
|
|
160
|
+
return ScheduleBuilder(agent, schedule_string)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Scheduling exceptions."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class SchedulingError(Exception):
|
|
7
|
+
"""Base exception for scheduling errors."""
|
|
8
|
+
|
|
9
|
+
pass
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class InvalidScheduleError(SchedulingError):
|
|
13
|
+
"""Raised when a schedule string is invalid."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, schedule: str, message: Optional[str] = None):
|
|
16
|
+
self.schedule = schedule
|
|
17
|
+
if message is None:
|
|
18
|
+
message = (
|
|
19
|
+
f"Invalid schedule '{schedule}'. "
|
|
20
|
+
"Try: 'daily', 'hourly', 'every 5 minutes', or cron expression."
|
|
21
|
+
)
|
|
22
|
+
super().__init__(message)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class InvalidInputTypeError(SchedulingError):
|
|
26
|
+
"""Raised when an input type prefix is invalid."""
|
|
27
|
+
|
|
28
|
+
def __init__(self, prefix: str, message: Optional[str] = None):
|
|
29
|
+
self.prefix = prefix
|
|
30
|
+
if message is None:
|
|
31
|
+
message = (
|
|
32
|
+
f"Unknown input type '{prefix}'. "
|
|
33
|
+
"Supported: secret:, const:, cache:TTL:, typed:, output:"
|
|
34
|
+
)
|
|
35
|
+
super().__init__(message)
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""Input types and magic prefix parsing for scheduled agents."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from enum import Enum
|
|
6
|
+
from typing import Any, Optional
|
|
7
|
+
|
|
8
|
+
from .exceptions import InvalidInputTypeError
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class InputType(Enum):
|
|
12
|
+
"""Types of inputs for scheduled agents."""
|
|
13
|
+
|
|
14
|
+
VARIABLE = "variable" # Regular variables (no prefix)
|
|
15
|
+
SECRET = "secret" # secret:value
|
|
16
|
+
CONSTANT = "const" # const:value
|
|
17
|
+
CACHED = "cache" # cache:TTL:value
|
|
18
|
+
TYPED = "typed" # typed:value
|
|
19
|
+
OUTPUT = "output" # output:value
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class ScheduledInput:
|
|
24
|
+
"""Configuration for a scheduled input."""
|
|
25
|
+
|
|
26
|
+
key: str
|
|
27
|
+
value: Any
|
|
28
|
+
input_type: InputType
|
|
29
|
+
ttl: Optional[int] = None # For cached inputs
|
|
30
|
+
|
|
31
|
+
def apply_to_context(self, context: Any) -> None:
|
|
32
|
+
"""Apply this input to a context."""
|
|
33
|
+
if self.input_type == InputType.SECRET:
|
|
34
|
+
context.set_secret(self.key, self.value)
|
|
35
|
+
elif self.input_type == InputType.CONSTANT:
|
|
36
|
+
context.set_constant(self.key, self.value)
|
|
37
|
+
elif self.input_type == InputType.CACHED:
|
|
38
|
+
context.set_cached(self.key, self.value, self.ttl)
|
|
39
|
+
elif self.input_type == InputType.TYPED:
|
|
40
|
+
context.set_typed_variable(self.key, self.value)
|
|
41
|
+
elif self.input_type == InputType.OUTPUT:
|
|
42
|
+
context.set_output(self.key, self.value)
|
|
43
|
+
else: # VARIABLE
|
|
44
|
+
context.set_variable(self.key, self.value)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def parse_magic_prefix(key: str, value: Any) -> ScheduledInput:
|
|
48
|
+
"""Parse magic prefix from input value and return ScheduledInput.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
key: The input key name
|
|
52
|
+
value: The input value, potentially with magic prefix
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
ScheduledInput with parsed type and value
|
|
56
|
+
|
|
57
|
+
Raises:
|
|
58
|
+
InvalidInputTypeError: If prefix is invalid
|
|
59
|
+
"""
|
|
60
|
+
if not isinstance(value, str):
|
|
61
|
+
# Non-string values are treated as regular variables
|
|
62
|
+
return ScheduledInput(key, value, InputType.VARIABLE)
|
|
63
|
+
|
|
64
|
+
# Check for magic prefixes
|
|
65
|
+
if ":" not in value:
|
|
66
|
+
# No prefix, regular variable
|
|
67
|
+
return ScheduledInput(key, value, InputType.VARIABLE)
|
|
68
|
+
|
|
69
|
+
parts = value.split(":", 1)
|
|
70
|
+
prefix = parts[0].lower()
|
|
71
|
+
|
|
72
|
+
if prefix == "secret":
|
|
73
|
+
if len(parts) != 2 or not parts[1]:
|
|
74
|
+
raise InvalidInputTypeError(prefix, "Secret format: secret:value")
|
|
75
|
+
return ScheduledInput(key, parts[1], InputType.SECRET)
|
|
76
|
+
|
|
77
|
+
elif prefix == "const":
|
|
78
|
+
if len(parts) != 2 or not parts[1]:
|
|
79
|
+
raise InvalidInputTypeError(prefix, "Constant format: const:value")
|
|
80
|
+
return ScheduledInput(key, parts[1], InputType.CONSTANT)
|
|
81
|
+
|
|
82
|
+
elif prefix == "cache":
|
|
83
|
+
# Format: cache:TTL:value
|
|
84
|
+
cache_parts = value.split(":", 2)
|
|
85
|
+
if len(cache_parts) != 3:
|
|
86
|
+
raise InvalidInputTypeError(prefix, "Cache format: cache:TTL:value")
|
|
87
|
+
|
|
88
|
+
try:
|
|
89
|
+
ttl = int(cache_parts[1])
|
|
90
|
+
except ValueError as e:
|
|
91
|
+
raise InvalidInputTypeError(prefix, "Cache TTL must be an integer") from e
|
|
92
|
+
|
|
93
|
+
# Try to parse value as JSON, fall back to string
|
|
94
|
+
raw_value = cache_parts[2]
|
|
95
|
+
try:
|
|
96
|
+
parsed_value = json.loads(raw_value)
|
|
97
|
+
except json.JSONDecodeError:
|
|
98
|
+
parsed_value = raw_value
|
|
99
|
+
|
|
100
|
+
return ScheduledInput(key, parsed_value, InputType.CACHED, ttl=ttl)
|
|
101
|
+
|
|
102
|
+
elif prefix == "typed":
|
|
103
|
+
if len(parts) != 2 or not parts[1]:
|
|
104
|
+
raise InvalidInputTypeError(prefix, "Typed format: typed:value")
|
|
105
|
+
|
|
106
|
+
# Try to parse value as JSON for complex types
|
|
107
|
+
raw_value = parts[1]
|
|
108
|
+
try:
|
|
109
|
+
parsed_value = json.loads(raw_value)
|
|
110
|
+
except json.JSONDecodeError:
|
|
111
|
+
parsed_value = raw_value
|
|
112
|
+
|
|
113
|
+
return ScheduledInput(key, parsed_value, InputType.TYPED)
|
|
114
|
+
|
|
115
|
+
elif prefix == "output":
|
|
116
|
+
if len(parts) != 2 or not parts[1]:
|
|
117
|
+
raise InvalidInputTypeError(prefix, "Output format: output:value")
|
|
118
|
+
return ScheduledInput(key, parts[1], InputType.OUTPUT)
|
|
119
|
+
|
|
120
|
+
else:
|
|
121
|
+
# Unknown prefix, treat as regular variable
|
|
122
|
+
return ScheduledInput(key, value, InputType.VARIABLE)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def parse_inputs(**inputs: Any) -> dict[str, ScheduledInput]:
|
|
126
|
+
"""Parse all inputs with magic prefixes.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
**inputs: Input key-value pairs
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
Dictionary mapping keys to ScheduledInput objects
|
|
133
|
+
"""
|
|
134
|
+
parsed_inputs = {}
|
|
135
|
+
for key, value in inputs.items():
|
|
136
|
+
parsed_inputs[key] = parse_magic_prefix(key, value)
|
|
137
|
+
return parsed_inputs
|