monoco-toolkit 0.3.10__py3-none-any.whl → 0.3.12__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.
- monoco/__main__.py +8 -0
- monoco/core/artifacts/__init__.py +16 -0
- monoco/core/artifacts/manager.py +575 -0
- monoco/core/artifacts/models.py +161 -0
- monoco/core/automation/__init__.py +51 -0
- monoco/core/automation/config.py +338 -0
- monoco/core/automation/field_watcher.py +296 -0
- monoco/core/automation/handlers.py +723 -0
- monoco/core/config.py +31 -4
- monoco/core/executor/__init__.py +38 -0
- monoco/core/executor/agent_action.py +254 -0
- monoco/core/executor/git_action.py +303 -0
- monoco/core/executor/im_action.py +309 -0
- monoco/core/executor/pytest_action.py +218 -0
- monoco/core/git.py +38 -0
- monoco/core/hooks/context.py +74 -13
- monoco/core/ingestion/__init__.py +20 -0
- monoco/core/ingestion/discovery.py +248 -0
- monoco/core/ingestion/watcher.py +343 -0
- monoco/core/ingestion/worker.py +436 -0
- monoco/core/loader.py +633 -0
- monoco/core/registry.py +34 -25
- monoco/core/router/__init__.py +55 -0
- monoco/core/router/action.py +341 -0
- monoco/core/router/router.py +392 -0
- monoco/core/scheduler/__init__.py +63 -0
- monoco/core/scheduler/base.py +152 -0
- monoco/core/scheduler/engines.py +175 -0
- monoco/core/scheduler/events.py +171 -0
- monoco/core/scheduler/local.py +377 -0
- monoco/core/skills.py +119 -80
- monoco/core/watcher/__init__.py +57 -0
- monoco/core/watcher/base.py +365 -0
- monoco/core/watcher/dropzone.py +152 -0
- monoco/core/watcher/issue.py +303 -0
- monoco/core/watcher/memo.py +200 -0
- monoco/core/watcher/task.py +238 -0
- monoco/daemon/app.py +77 -1
- monoco/daemon/commands.py +10 -0
- monoco/daemon/events.py +34 -0
- monoco/daemon/mailroom_service.py +196 -0
- monoco/daemon/models.py +1 -0
- monoco/daemon/scheduler.py +207 -0
- monoco/daemon/services.py +27 -58
- monoco/daemon/triggers.py +55 -0
- monoco/features/agent/__init__.py +25 -7
- monoco/features/agent/adapter.py +17 -7
- monoco/features/agent/cli.py +91 -57
- monoco/features/agent/engines.py +31 -170
- monoco/{core/resources/en/skills/monoco_core → features/agent/resources/en/skills/monoco_atom_core}/SKILL.md +2 -2
- monoco/features/agent/resources/en/skills/{flow_engineer → monoco_workflow_agent_engineer}/SKILL.md +2 -2
- monoco/features/agent/resources/en/skills/{flow_manager → monoco_workflow_agent_manager}/SKILL.md +2 -2
- monoco/features/agent/resources/en/skills/{flow_planner → monoco_workflow_agent_planner}/SKILL.md +2 -2
- monoco/features/agent/resources/en/skills/{flow_reviewer → monoco_workflow_agent_reviewer}/SKILL.md +2 -2
- monoco/features/agent/resources/{roles/role-engineer.yaml → zh/roles/monoco_role_engineer.yaml} +3 -3
- monoco/features/agent/resources/{roles/role-manager.yaml → zh/roles/monoco_role_manager.yaml} +8 -8
- monoco/features/agent/resources/{roles/role-planner.yaml → zh/roles/monoco_role_planner.yaml} +8 -8
- monoco/features/agent/resources/{roles/role-reviewer.yaml → zh/roles/monoco_role_reviewer.yaml} +8 -8
- monoco/{core/resources/zh/skills/monoco_core → features/agent/resources/zh/skills/monoco_atom_core}/SKILL.md +2 -2
- monoco/features/agent/resources/zh/skills/{flow_engineer → monoco_workflow_agent_engineer}/SKILL.md +2 -2
- monoco/features/agent/resources/zh/skills/{flow_manager → monoco_workflow_agent_manager}/SKILL.md +2 -2
- monoco/features/agent/resources/zh/skills/{flow_planner → monoco_workflow_agent_planner}/SKILL.md +2 -2
- monoco/features/agent/resources/zh/skills/{flow_reviewer → monoco_workflow_agent_reviewer}/SKILL.md +2 -2
- monoco/features/agent/worker.py +1 -1
- monoco/features/artifact/__init__.py +0 -0
- monoco/features/artifact/adapter.py +33 -0
- monoco/features/artifact/resources/zh/AGENTS.md +14 -0
- monoco/features/artifact/resources/zh/skills/monoco_atom_artifact/SKILL.md +278 -0
- monoco/features/glossary/adapter.py +18 -7
- monoco/features/glossary/resources/en/skills/{monoco_glossary → monoco_atom_glossary}/SKILL.md +2 -2
- monoco/features/glossary/resources/zh/skills/{monoco_glossary → monoco_atom_glossary}/SKILL.md +2 -2
- monoco/features/hooks/__init__.py +11 -0
- monoco/features/hooks/adapter.py +67 -0
- monoco/features/hooks/commands.py +309 -0
- monoco/features/hooks/core.py +441 -0
- monoco/features/hooks/resources/ADDING_HOOKS.md +234 -0
- monoco/features/i18n/adapter.py +18 -5
- monoco/features/i18n/core.py +482 -17
- monoco/features/i18n/resources/en/skills/{monoco_i18n → monoco_atom_i18n}/SKILL.md +2 -2
- monoco/features/i18n/resources/en/skills/{i18n_scan_workflow → monoco_workflow_i18n_scan}/SKILL.md +2 -2
- monoco/features/i18n/resources/zh/skills/{monoco_i18n → monoco_atom_i18n}/SKILL.md +2 -2
- monoco/features/i18n/resources/zh/skills/{i18n_scan_workflow → monoco_workflow_i18n_scan}/SKILL.md +2 -2
- monoco/features/issue/adapter.py +19 -6
- monoco/features/issue/commands.py +352 -20
- monoco/features/issue/core.py +475 -16
- monoco/features/issue/engine/machine.py +114 -4
- monoco/features/issue/linter.py +60 -5
- monoco/features/issue/models.py +2 -2
- monoco/features/issue/resources/en/AGENTS.md +109 -0
- monoco/features/issue/resources/en/skills/{monoco_issue → monoco_atom_issue}/SKILL.md +2 -2
- monoco/features/issue/resources/en/skills/{issue_create_workflow → monoco_workflow_issue_creation}/SKILL.md +2 -2
- monoco/features/issue/resources/en/skills/{issue_develop_workflow → monoco_workflow_issue_development}/SKILL.md +2 -2
- monoco/features/issue/resources/en/skills/{issue_lifecycle_workflow → monoco_workflow_issue_management}/SKILL.md +2 -2
- monoco/features/issue/resources/en/skills/{issue_refine_workflow → monoco_workflow_issue_refinement}/SKILL.md +2 -2
- monoco/features/issue/resources/hooks/post-checkout.sh +39 -0
- monoco/features/issue/resources/hooks/pre-commit.sh +41 -0
- monoco/features/issue/resources/hooks/pre-push.sh +35 -0
- monoco/features/issue/resources/zh/AGENTS.md +109 -0
- monoco/features/issue/resources/zh/skills/{monoco_issue → monoco_atom_issue_lifecycle}/SKILL.md +2 -2
- monoco/features/issue/resources/zh/skills/{issue_create_workflow → monoco_workflow_issue_creation}/SKILL.md +2 -2
- monoco/features/issue/resources/zh/skills/{issue_develop_workflow → monoco_workflow_issue_development}/SKILL.md +2 -2
- monoco/features/issue/resources/zh/skills/{issue_lifecycle_workflow → monoco_workflow_issue_management}/SKILL.md +2 -2
- monoco/features/issue/resources/zh/skills/{issue_refine_workflow → monoco_workflow_issue_refinement}/SKILL.md +2 -2
- monoco/features/issue/validator.py +101 -1
- monoco/features/memo/adapter.py +21 -8
- monoco/features/memo/cli.py +103 -10
- monoco/features/memo/core.py +178 -92
- monoco/features/memo/models.py +53 -0
- monoco/features/memo/resources/en/skills/{monoco_memo → monoco_atom_memo}/SKILL.md +2 -2
- monoco/features/memo/resources/en/skills/{note_processing_workflow → monoco_workflow_note_processing}/SKILL.md +2 -2
- monoco/features/memo/resources/zh/skills/{monoco_memo → monoco_atom_memo}/SKILL.md +2 -2
- monoco/features/memo/resources/zh/skills/{note_processing_workflow → monoco_workflow_note_processing}/SKILL.md +2 -2
- monoco/features/spike/adapter.py +18 -5
- monoco/features/spike/commands.py +5 -3
- monoco/features/spike/resources/en/skills/{monoco_spike → monoco_atom_spike}/SKILL.md +2 -2
- monoco/features/spike/resources/en/skills/{research_workflow → monoco_workflow_research}/SKILL.md +2 -2
- monoco/features/spike/resources/zh/skills/{monoco_spike → monoco_atom_spike}/SKILL.md +2 -2
- monoco/features/spike/resources/zh/skills/{research_workflow → monoco_workflow_research}/SKILL.md +2 -2
- monoco/main.py +38 -1
- {monoco_toolkit-0.3.10.dist-info → monoco_toolkit-0.3.12.dist-info}/METADATA +7 -1
- monoco_toolkit-0.3.12.dist-info/RECORD +202 -0
- monoco/features/agent/apoptosis.py +0 -44
- monoco/features/agent/manager.py +0 -91
- monoco/features/agent/session.py +0 -121
- monoco_toolkit-0.3.10.dist-info/RECORD +0 -156
- /monoco/{core → features/agent}/resources/en/AGENTS.md +0 -0
- /monoco/{core → features/agent}/resources/zh/AGENTS.md +0 -0
- {monoco_toolkit-0.3.10.dist-info → monoco_toolkit-0.3.12.dist-info}/WHEEL +0 -0
- {monoco_toolkit-0.3.10.dist-info → monoco_toolkit-0.3.12.dist-info}/entry_points.txt +0 -0
- {monoco_toolkit-0.3.10.dist-info → monoco_toolkit-0.3.12.dist-info}/licenses/LICENSE +0 -0
monoco/core/registry.py
CHANGED
|
@@ -1,45 +1,54 @@
|
|
|
1
|
-
from typing import Dict, List
|
|
1
|
+
from typing import Dict, List, Optional
|
|
2
2
|
from monoco.core.feature import MonocoFeature
|
|
3
|
+
from monoco.core.loader import FeatureLoader, FeatureRegistry as LoaderFeatureRegistry
|
|
3
4
|
|
|
4
5
|
|
|
5
6
|
class FeatureRegistry:
|
|
6
|
-
|
|
7
|
+
"""
|
|
8
|
+
Feature registry that wraps the new unified FeatureLoader.
|
|
9
|
+
|
|
10
|
+
This class provides backward compatibility while delegating to the
|
|
11
|
+
new FeatureLoader for dynamic discovery and lifecycle management.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
_loader: Optional[FeatureLoader] = None
|
|
15
|
+
|
|
16
|
+
@classmethod
|
|
17
|
+
def _get_loader(cls) -> FeatureLoader:
|
|
18
|
+
"""Get or create the default feature loader."""
|
|
19
|
+
if cls._loader is None:
|
|
20
|
+
cls._loader = FeatureLoader()
|
|
21
|
+
# Discover and load all features
|
|
22
|
+
cls._loader.discover()
|
|
23
|
+
cls._loader.load_all()
|
|
24
|
+
return cls._loader
|
|
7
25
|
|
|
8
26
|
@classmethod
|
|
9
27
|
def register(cls, feature: MonocoFeature):
|
|
10
28
|
"""Register a feature instance."""
|
|
11
|
-
|
|
29
|
+
loader = cls._get_loader()
|
|
30
|
+
loader.registry.register(feature) # type: ignore
|
|
12
31
|
|
|
13
32
|
@classmethod
|
|
14
33
|
def get_features(cls) -> List[MonocoFeature]:
|
|
15
34
|
"""Get all registered features."""
|
|
16
|
-
|
|
35
|
+
loader = cls._get_loader()
|
|
36
|
+
return loader.registry.get_all() # type: ignore
|
|
17
37
|
|
|
18
38
|
@classmethod
|
|
19
|
-
def get_feature(cls, name: str) -> MonocoFeature:
|
|
39
|
+
def get_feature(cls, name: str) -> Optional[MonocoFeature]:
|
|
20
40
|
"""Get a specific feature by name."""
|
|
21
|
-
|
|
41
|
+
loader = cls._get_loader()
|
|
42
|
+
return loader.registry.get(name) # type: ignore
|
|
22
43
|
|
|
23
44
|
@classmethod
|
|
24
45
|
def load_defaults(cls):
|
|
25
46
|
"""
|
|
26
|
-
Load default core features.
|
|
27
|
-
TODO: In the future, this could be dynamic via entry points.
|
|
28
|
-
"""
|
|
29
|
-
# Import here to avoid circular dependencies at module level
|
|
30
|
-
from monoco.features.issue.adapter import IssueFeature
|
|
31
|
-
from monoco.features.spike.adapter import SpikeFeature
|
|
32
|
-
from monoco.features.i18n.adapter import I18nFeature
|
|
33
|
-
from monoco.features.memo.adapter import MemoFeature
|
|
34
|
-
|
|
35
|
-
cls.register(IssueFeature())
|
|
36
|
-
cls.register(SpikeFeature())
|
|
37
|
-
cls.register(I18nFeature())
|
|
38
|
-
cls.register(MemoFeature())
|
|
39
|
-
|
|
47
|
+
Load default core features using the unified FeatureLoader.
|
|
40
48
|
|
|
41
|
-
from monoco
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
49
|
+
This method discovers and loads all features from monoco/features/
|
|
50
|
+
automatically, replacing the manual registration approach.
|
|
51
|
+
"""
|
|
52
|
+
loader = cls._get_loader()
|
|
53
|
+
# Features are already discovered and loaded in _get_loader
|
|
54
|
+
# This method is kept for backward compatibility
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Router Module - Layer 2 of the Event Automation Framework.
|
|
3
|
+
|
|
4
|
+
This module provides event routing capabilities that map events to actions.
|
|
5
|
+
It is part of the three-layer architecture:
|
|
6
|
+
- Layer 1: File Watcher
|
|
7
|
+
- Layer 2: Action Router (this module)
|
|
8
|
+
- Layer 3: Action Executor
|
|
9
|
+
|
|
10
|
+
Example Usage:
|
|
11
|
+
>>> from monoco.core.router import ActionRouter, Action, ActionResult
|
|
12
|
+
>>> from monoco.core.scheduler import AgentEventType
|
|
13
|
+
>>>
|
|
14
|
+
>>> router = ActionRouter()
|
|
15
|
+
>>>
|
|
16
|
+
>>> # Register action for issue creation
|
|
17
|
+
>>> router.register(AgentEventType.ISSUE_CREATED, my_action)
|
|
18
|
+
>>>
|
|
19
|
+
>>> # Register with condition
|
|
20
|
+
>>> router.register(
|
|
21
|
+
... AgentEventType.ISSUE_STAGE_CHANGED,
|
|
22
|
+
... engineer_action,
|
|
23
|
+
... condition=lambda e: e.payload.get("new_stage") == "doing"
|
|
24
|
+
... )
|
|
25
|
+
>>>
|
|
26
|
+
>>> await router.start()
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from .action import (
|
|
30
|
+
Action,
|
|
31
|
+
ActionChain,
|
|
32
|
+
ActionRegistry,
|
|
33
|
+
ActionResult,
|
|
34
|
+
ActionStatus,
|
|
35
|
+
ConditionalAction,
|
|
36
|
+
)
|
|
37
|
+
from .router import (
|
|
38
|
+
ActionRouter,
|
|
39
|
+
ConditionalRouter,
|
|
40
|
+
RoutingRule,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
__all__ = [
|
|
44
|
+
# Actions
|
|
45
|
+
"Action",
|
|
46
|
+
"ActionChain",
|
|
47
|
+
"ActionRegistry",
|
|
48
|
+
"ActionResult",
|
|
49
|
+
"ActionStatus",
|
|
50
|
+
"ConditionalAction",
|
|
51
|
+
# Router
|
|
52
|
+
"ActionRouter",
|
|
53
|
+
"ConditionalRouter",
|
|
54
|
+
"RoutingRule",
|
|
55
|
+
]
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Action Abstractions - Layer 2 & 3 of the Event Automation Framework.
|
|
3
|
+
|
|
4
|
+
This module defines the Action ABC and related classes for the execution layer.
|
|
5
|
+
Actions are the units of work that respond to events.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
from abc import ABC, abstractmethod
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from datetime import datetime
|
|
14
|
+
from enum import Enum
|
|
15
|
+
from typing import Any, Callable, Dict, List, Optional, Union
|
|
16
|
+
|
|
17
|
+
from monoco.core.scheduler import AgentEvent, AgentEventType
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ActionStatus(Enum):
|
|
23
|
+
"""Status of an Action execution."""
|
|
24
|
+
PENDING = "pending"
|
|
25
|
+
RUNNING = "running"
|
|
26
|
+
SUCCESS = "success"
|
|
27
|
+
FAILED = "failed"
|
|
28
|
+
SKIPPED = "skipped"
|
|
29
|
+
CANCELLED = "cancelled"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class ActionResult:
|
|
34
|
+
"""
|
|
35
|
+
Result of an Action execution.
|
|
36
|
+
|
|
37
|
+
Attributes:
|
|
38
|
+
success: Whether the action succeeded
|
|
39
|
+
status: Detailed status
|
|
40
|
+
output: Output data from the action
|
|
41
|
+
error: Error message if failed
|
|
42
|
+
metadata: Additional metadata
|
|
43
|
+
started_at: Execution start time
|
|
44
|
+
completed_at: Execution completion time
|
|
45
|
+
"""
|
|
46
|
+
success: bool
|
|
47
|
+
status: ActionStatus
|
|
48
|
+
output: Any = None
|
|
49
|
+
error: Optional[str] = None
|
|
50
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
51
|
+
started_at: Optional[datetime] = None
|
|
52
|
+
completed_at: Optional[datetime] = None
|
|
53
|
+
|
|
54
|
+
@classmethod
|
|
55
|
+
def success_result(
|
|
56
|
+
cls,
|
|
57
|
+
output: Any = None,
|
|
58
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
59
|
+
) -> "ActionResult":
|
|
60
|
+
"""Create a success result."""
|
|
61
|
+
return cls(
|
|
62
|
+
success=True,
|
|
63
|
+
status=ActionStatus.SUCCESS,
|
|
64
|
+
output=output,
|
|
65
|
+
metadata=metadata or {},
|
|
66
|
+
completed_at=datetime.now(),
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
@classmethod
|
|
70
|
+
def failure_result(
|
|
71
|
+
cls,
|
|
72
|
+
error: str,
|
|
73
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
74
|
+
) -> "ActionResult":
|
|
75
|
+
"""Create a failure result."""
|
|
76
|
+
return cls(
|
|
77
|
+
success=False,
|
|
78
|
+
status=ActionStatus.FAILED,
|
|
79
|
+
error=error,
|
|
80
|
+
metadata=metadata or {},
|
|
81
|
+
completed_at=datetime.now(),
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
@classmethod
|
|
85
|
+
def skipped_result(
|
|
86
|
+
cls,
|
|
87
|
+
reason: str = "",
|
|
88
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
89
|
+
) -> "ActionResult":
|
|
90
|
+
"""Create a skipped result."""
|
|
91
|
+
return cls(
|
|
92
|
+
success=True,
|
|
93
|
+
status=ActionStatus.SKIPPED,
|
|
94
|
+
metadata={"reason": reason, **(metadata or {})},
|
|
95
|
+
completed_at=datetime.now(),
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class Action(ABC):
|
|
100
|
+
"""
|
|
101
|
+
Abstract base class for Actions (Layer 3).
|
|
102
|
+
|
|
103
|
+
Actions are the units of work that respond to events.
|
|
104
|
+
They are executed by the ActionRouter when events match their conditions.
|
|
105
|
+
|
|
106
|
+
Responsibilities:
|
|
107
|
+
- Define execution conditions (can_execute)
|
|
108
|
+
- Implement execution logic (execute)
|
|
109
|
+
- Return execution results
|
|
110
|
+
|
|
111
|
+
Example:
|
|
112
|
+
>>> class MyAction(Action):
|
|
113
|
+
... @property
|
|
114
|
+
... def name(self) -> str:
|
|
115
|
+
... return "MyAction"
|
|
116
|
+
...
|
|
117
|
+
... async def can_execute(self, event: AgentEvent) -> bool:
|
|
118
|
+
... return event.type == AgentEventType.ISSUE_CREATED
|
|
119
|
+
...
|
|
120
|
+
... async def execute(self, event: AgentEvent) -> ActionResult:
|
|
121
|
+
... # Do something
|
|
122
|
+
... return ActionResult.success_result()
|
|
123
|
+
"""
|
|
124
|
+
|
|
125
|
+
def __init__(self, config: Optional[Dict[str, Any]] = None):
|
|
126
|
+
self.config = config or {}
|
|
127
|
+
self._execution_count = 0
|
|
128
|
+
self._last_execution: Optional[datetime] = None
|
|
129
|
+
|
|
130
|
+
@property
|
|
131
|
+
@abstractmethod
|
|
132
|
+
def name(self) -> str:
|
|
133
|
+
"""Return the unique name of this action."""
|
|
134
|
+
pass
|
|
135
|
+
|
|
136
|
+
@abstractmethod
|
|
137
|
+
async def can_execute(self, event: AgentEvent) -> bool:
|
|
138
|
+
"""
|
|
139
|
+
Check if this action should execute for the given event.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
event: The event to check
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
True if the action should execute, False otherwise
|
|
146
|
+
"""
|
|
147
|
+
pass
|
|
148
|
+
|
|
149
|
+
@abstractmethod
|
|
150
|
+
async def execute(self, event: AgentEvent) -> ActionResult:
|
|
151
|
+
"""
|
|
152
|
+
Execute the action.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
event: The event that triggered this action
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
ActionResult indicating success/failure
|
|
159
|
+
"""
|
|
160
|
+
pass
|
|
161
|
+
|
|
162
|
+
async def __call__(self, event: AgentEvent) -> ActionResult:
|
|
163
|
+
"""
|
|
164
|
+
Make action callable - checks conditions then executes.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
event: The event to process
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
ActionResult
|
|
171
|
+
"""
|
|
172
|
+
self._last_execution = datetime.now()
|
|
173
|
+
|
|
174
|
+
try:
|
|
175
|
+
if not await self.can_execute(event):
|
|
176
|
+
return ActionResult.skipped_result(
|
|
177
|
+
reason="Conditions not met",
|
|
178
|
+
metadata={"action": self.name, "event_type": event.type.value},
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
self._execution_count += 1
|
|
182
|
+
result = await self.execute(event)
|
|
183
|
+
|
|
184
|
+
# Ensure result has timestamps
|
|
185
|
+
if result.started_at is None:
|
|
186
|
+
result.started_at = self._last_execution
|
|
187
|
+
|
|
188
|
+
return result
|
|
189
|
+
|
|
190
|
+
except Exception as e:
|
|
191
|
+
logger.error(f"Action {self.name} failed: {e}", exc_info=True)
|
|
192
|
+
return ActionResult.failure_result(
|
|
193
|
+
error=str(e),
|
|
194
|
+
metadata={"action": self.name, "event_type": event.type.value},
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
def get_stats(self) -> Dict[str, Any]:
|
|
198
|
+
"""Get action statistics."""
|
|
199
|
+
return {
|
|
200
|
+
"name": self.name,
|
|
201
|
+
"execution_count": self._execution_count,
|
|
202
|
+
"last_execution": self._last_execution.isoformat() if self._last_execution else None,
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
class ConditionalAction(Action):
|
|
207
|
+
"""
|
|
208
|
+
Action with configurable conditions.
|
|
209
|
+
|
|
210
|
+
Allows defining conditions declaratively without subclassing.
|
|
211
|
+
"""
|
|
212
|
+
|
|
213
|
+
def __init__(
|
|
214
|
+
self,
|
|
215
|
+
name: str,
|
|
216
|
+
execute_fn: Callable[[AgentEvent], Any],
|
|
217
|
+
condition_fn: Optional[Callable[[AgentEvent], bool]] = None,
|
|
218
|
+
config: Optional[Dict[str, Any]] = None,
|
|
219
|
+
):
|
|
220
|
+
super().__init__(config)
|
|
221
|
+
self._name = name
|
|
222
|
+
self._execute_fn = execute_fn
|
|
223
|
+
self._condition_fn = condition_fn
|
|
224
|
+
|
|
225
|
+
@property
|
|
226
|
+
def name(self) -> str:
|
|
227
|
+
return self._name
|
|
228
|
+
|
|
229
|
+
async def can_execute(self, event: AgentEvent) -> bool:
|
|
230
|
+
if self._condition_fn is None:
|
|
231
|
+
return True
|
|
232
|
+
|
|
233
|
+
if hasattr(self._condition_fn, '__await__'):
|
|
234
|
+
return await self._condition_fn(event)
|
|
235
|
+
return self._condition_fn(event)
|
|
236
|
+
|
|
237
|
+
async def execute(self, event: AgentEvent) -> ActionResult:
|
|
238
|
+
try:
|
|
239
|
+
if hasattr(self._execute_fn, '__await__'):
|
|
240
|
+
result = await self._execute_fn(event)
|
|
241
|
+
else:
|
|
242
|
+
result = self._execute_fn(event)
|
|
243
|
+
|
|
244
|
+
if isinstance(result, ActionResult):
|
|
245
|
+
return result
|
|
246
|
+
return ActionResult.success_result(output=result)
|
|
247
|
+
|
|
248
|
+
except Exception as e:
|
|
249
|
+
return ActionResult.failure_result(error=str(e))
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
class ActionChain:
|
|
253
|
+
"""
|
|
254
|
+
Chain of actions that execute sequentially.
|
|
255
|
+
|
|
256
|
+
Each action in the chain receives the output of the previous action.
|
|
257
|
+
If any action fails, the chain stops.
|
|
258
|
+
"""
|
|
259
|
+
|
|
260
|
+
def __init__(self, name: str, actions: Optional[List[Action]] = None):
|
|
261
|
+
self.name = name
|
|
262
|
+
self.actions = actions or []
|
|
263
|
+
self._results: List[ActionResult] = []
|
|
264
|
+
|
|
265
|
+
def add(self, action: Action) -> "ActionChain":
|
|
266
|
+
"""Add an action to the chain."""
|
|
267
|
+
self.actions.append(action)
|
|
268
|
+
return self
|
|
269
|
+
|
|
270
|
+
async def execute(self, event: AgentEvent) -> List[ActionResult]:
|
|
271
|
+
"""
|
|
272
|
+
Execute all actions in the chain.
|
|
273
|
+
|
|
274
|
+
Args:
|
|
275
|
+
event: The triggering event
|
|
276
|
+
|
|
277
|
+
Returns:
|
|
278
|
+
List of ActionResults for each action
|
|
279
|
+
"""
|
|
280
|
+
self._results = []
|
|
281
|
+
context = {"event": event, "chain_name": self.name}
|
|
282
|
+
|
|
283
|
+
for action in self.actions:
|
|
284
|
+
# Check if previous action failed
|
|
285
|
+
if self._results and not self._results[-1].success:
|
|
286
|
+
self._results.append(ActionResult.skipped_result(
|
|
287
|
+
reason="Previous action in chain failed"
|
|
288
|
+
))
|
|
289
|
+
continue
|
|
290
|
+
|
|
291
|
+
# Execute action with context
|
|
292
|
+
result = await action(event)
|
|
293
|
+
result.metadata["chain_context"] = context.copy()
|
|
294
|
+
self._results.append(result)
|
|
295
|
+
|
|
296
|
+
# Update context with output
|
|
297
|
+
if result.success and result.output is not None:
|
|
298
|
+
context[f"{action.name}_output"] = result.output
|
|
299
|
+
|
|
300
|
+
return self._results
|
|
301
|
+
|
|
302
|
+
def get_stats(self) -> Dict[str, Any]:
|
|
303
|
+
"""Get chain statistics."""
|
|
304
|
+
return {
|
|
305
|
+
"name": self.name,
|
|
306
|
+
"actions": len(self.actions),
|
|
307
|
+
"action_names": [a.name for a in self.actions],
|
|
308
|
+
"last_results": len(self._results),
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
class ActionRegistry:
|
|
313
|
+
"""
|
|
314
|
+
Registry for managing actions.
|
|
315
|
+
|
|
316
|
+
Provides action lookup and management.
|
|
317
|
+
"""
|
|
318
|
+
|
|
319
|
+
def __init__(self):
|
|
320
|
+
self._actions: Dict[str, Action] = {}
|
|
321
|
+
|
|
322
|
+
def register(self, action: Action) -> None:
|
|
323
|
+
"""Register an action."""
|
|
324
|
+
self._actions[action.name] = action
|
|
325
|
+
logger.debug(f"Registered action: {action.name}")
|
|
326
|
+
|
|
327
|
+
def unregister(self, name: str) -> Optional[Action]:
|
|
328
|
+
"""Unregister an action."""
|
|
329
|
+
return self._actions.pop(name, None)
|
|
330
|
+
|
|
331
|
+
def get(self, name: str) -> Optional[Action]:
|
|
332
|
+
"""Get an action by name."""
|
|
333
|
+
return self._actions.get(name)
|
|
334
|
+
|
|
335
|
+
def list_actions(self) -> List[str]:
|
|
336
|
+
"""List all registered action names."""
|
|
337
|
+
return list(self._actions.keys())
|
|
338
|
+
|
|
339
|
+
def clear(self) -> None:
|
|
340
|
+
"""Clear all registered actions."""
|
|
341
|
+
self._actions.clear()
|