monoco-toolkit 0.3.12__py3-none-any.whl → 0.4.0__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/core/automation/__init__.py +0 -11
- monoco/core/automation/handlers.py +108 -26
- monoco/core/config.py +28 -10
- monoco/core/daemon/__init__.py +5 -0
- monoco/core/daemon/pid.py +290 -0
- monoco/core/injection.py +86 -8
- monoco/core/integrations.py +0 -24
- monoco/core/router/__init__.py +1 -39
- monoco/core/router/action.py +3 -142
- monoco/core/scheduler/events.py +28 -2
- monoco/core/setup.py +9 -0
- monoco/core/sync.py +199 -4
- monoco/core/watcher/__init__.py +6 -0
- monoco/core/watcher/base.py +18 -1
- monoco/core/watcher/im.py +460 -0
- monoco/core/watcher/memo.py +40 -48
- monoco/daemon/app.py +3 -60
- monoco/daemon/commands.py +459 -25
- monoco/daemon/scheduler.py +1 -16
- monoco/daemon/services.py +15 -0
- monoco/features/agent/resources/en/AGENTS.md +14 -14
- monoco/features/agent/resources/en/skills/monoco_role_engineer/SKILL.md +101 -0
- monoco/features/agent/resources/en/skills/monoco_role_manager/SKILL.md +95 -0
- monoco/features/agent/resources/en/skills/monoco_role_planner/SKILL.md +177 -0
- monoco/features/agent/resources/en/skills/monoco_role_reviewer/SKILL.md +139 -0
- monoco/features/agent/resources/zh/skills/monoco_role_engineer/SKILL.md +101 -0
- monoco/features/agent/resources/zh/skills/monoco_role_manager/SKILL.md +95 -0
- monoco/features/agent/resources/zh/skills/monoco_role_planner/SKILL.md +177 -0
- monoco/features/agent/resources/zh/skills/monoco_role_reviewer/SKILL.md +139 -0
- monoco/features/hooks/__init__.py +61 -6
- monoco/features/hooks/commands.py +281 -271
- monoco/features/hooks/dispatchers/__init__.py +23 -0
- monoco/features/hooks/dispatchers/agent_dispatcher.py +486 -0
- monoco/features/hooks/dispatchers/git_dispatcher.py +478 -0
- monoco/features/hooks/manager.py +357 -0
- monoco/features/hooks/models.py +262 -0
- monoco/features/hooks/parser.py +322 -0
- monoco/features/hooks/universal_interceptor.py +503 -0
- monoco/features/im/__init__.py +67 -0
- monoco/features/im/core.py +782 -0
- monoco/features/im/models.py +311 -0
- monoco/features/issue/commands.py +65 -50
- monoco/features/issue/core.py +199 -99
- monoco/features/issue/domain_commands.py +0 -19
- monoco/features/issue/resources/en/AGENTS.md +17 -122
- monoco/features/issue/resources/hooks/agent/before-tool.sh +102 -0
- monoco/features/issue/resources/hooks/agent/session-start.sh +88 -0
- monoco/features/issue/resources/hooks/{post-checkout.sh → git/git-post-checkout.sh} +10 -9
- monoco/features/issue/resources/hooks/git/git-pre-commit.sh +31 -0
- monoco/features/issue/resources/hooks/{pre-push.sh → git/git-pre-push.sh} +7 -13
- monoco/features/issue/resources/zh/AGENTS.md +18 -123
- monoco/features/memo/cli.py +15 -64
- monoco/features/memo/core.py +6 -34
- monoco/features/memo/models.py +24 -15
- monoco/features/memo/resources/en/AGENTS.md +31 -0
- monoco/features/memo/resources/zh/AGENTS.md +28 -5
- monoco/main.py +5 -3
- {monoco_toolkit-0.3.12.dist-info → monoco_toolkit-0.4.0.dist-info}/METADATA +1 -1
- monoco_toolkit-0.4.0.dist-info/RECORD +170 -0
- monoco/core/automation/config.py +0 -338
- monoco/core/execution.py +0 -67
- monoco/core/executor/__init__.py +0 -38
- monoco/core/executor/agent_action.py +0 -254
- monoco/core/executor/git_action.py +0 -303
- monoco/core/executor/im_action.py +0 -309
- monoco/core/executor/pytest_action.py +0 -218
- monoco/core/router/router.py +0 -392
- monoco/features/agent/resources/atoms/atom-code-dev.yaml +0 -61
- monoco/features/agent/resources/atoms/atom-issue-lifecycle.yaml +0 -73
- monoco/features/agent/resources/atoms/atom-knowledge.yaml +0 -55
- monoco/features/agent/resources/atoms/atom-review.yaml +0 -60
- monoco/features/agent/resources/en/skills/monoco_atom_core/SKILL.md +0 -99
- monoco/features/agent/resources/en/skills/monoco_workflow_agent_engineer/SKILL.md +0 -94
- monoco/features/agent/resources/en/skills/monoco_workflow_agent_manager/SKILL.md +0 -93
- monoco/features/agent/resources/en/skills/monoco_workflow_agent_planner/SKILL.md +0 -85
- monoco/features/agent/resources/en/skills/monoco_workflow_agent_reviewer/SKILL.md +0 -114
- monoco/features/agent/resources/workflows/workflow-dev.yaml +0 -83
- monoco/features/agent/resources/workflows/workflow-issue-create.yaml +0 -72
- monoco/features/agent/resources/workflows/workflow-review.yaml +0 -94
- monoco/features/agent/resources/zh/roles/monoco_role_engineer.yaml +0 -49
- monoco/features/agent/resources/zh/roles/monoco_role_manager.yaml +0 -46
- monoco/features/agent/resources/zh/roles/monoco_role_planner.yaml +0 -46
- monoco/features/agent/resources/zh/roles/monoco_role_reviewer.yaml +0 -47
- monoco/features/agent/resources/zh/skills/monoco_atom_core/SKILL.md +0 -99
- monoco/features/agent/resources/zh/skills/monoco_workflow_agent_engineer/SKILL.md +0 -94
- monoco/features/agent/resources/zh/skills/monoco_workflow_agent_manager/SKILL.md +0 -88
- monoco/features/agent/resources/zh/skills/monoco_workflow_agent_planner/SKILL.md +0 -259
- monoco/features/agent/resources/zh/skills/monoco_workflow_agent_reviewer/SKILL.md +0 -137
- monoco/features/artifact/resources/zh/skills/monoco_atom_artifact/SKILL.md +0 -278
- monoco/features/glossary/resources/en/skills/monoco_atom_glossary/SKILL.md +0 -35
- monoco/features/glossary/resources/zh/skills/monoco_atom_glossary/SKILL.md +0 -35
- monoco/features/hooks/adapter.py +0 -67
- monoco/features/hooks/core.py +0 -441
- monoco/features/i18n/resources/en/skills/monoco_atom_i18n/SKILL.md +0 -96
- monoco/features/i18n/resources/en/skills/monoco_workflow_i18n_scan/SKILL.md +0 -105
- monoco/features/i18n/resources/zh/skills/monoco_atom_i18n/SKILL.md +0 -96
- monoco/features/i18n/resources/zh/skills/monoco_workflow_i18n_scan/SKILL.md +0 -105
- monoco/features/issue/resources/en/skills/monoco_atom_issue/SKILL.md +0 -165
- monoco/features/issue/resources/en/skills/monoco_workflow_issue_creation/SKILL.md +0 -167
- monoco/features/issue/resources/en/skills/monoco_workflow_issue_development/SKILL.md +0 -224
- monoco/features/issue/resources/en/skills/monoco_workflow_issue_management/SKILL.md +0 -159
- monoco/features/issue/resources/en/skills/monoco_workflow_issue_refinement/SKILL.md +0 -203
- monoco/features/issue/resources/hooks/pre-commit.sh +0 -41
- monoco/features/issue/resources/zh/skills/monoco_atom_issue_lifecycle/SKILL.md +0 -190
- monoco/features/issue/resources/zh/skills/monoco_workflow_issue_creation/SKILL.md +0 -167
- monoco/features/issue/resources/zh/skills/monoco_workflow_issue_development/SKILL.md +0 -224
- monoco/features/issue/resources/zh/skills/monoco_workflow_issue_management/SKILL.md +0 -159
- monoco/features/issue/resources/zh/skills/monoco_workflow_issue_refinement/SKILL.md +0 -203
- monoco/features/memo/resources/en/skills/monoco_atom_memo/SKILL.md +0 -77
- monoco/features/memo/resources/en/skills/monoco_workflow_note_processing/SKILL.md +0 -140
- monoco/features/memo/resources/zh/skills/monoco_atom_memo/SKILL.md +0 -77
- monoco/features/memo/resources/zh/skills/monoco_workflow_note_processing/SKILL.md +0 -140
- monoco/features/spike/resources/en/skills/monoco_atom_spike/SKILL.md +0 -76
- monoco/features/spike/resources/en/skills/monoco_workflow_research/SKILL.md +0 -121
- monoco/features/spike/resources/zh/skills/monoco_atom_spike/SKILL.md +0 -76
- monoco/features/spike/resources/zh/skills/monoco_workflow_research/SKILL.md +0 -121
- monoco_toolkit-0.3.12.dist-info/RECORD +0 -202
- {monoco_toolkit-0.3.12.dist-info → monoco_toolkit-0.4.0.dist-info}/WHEEL +0 -0
- {monoco_toolkit-0.3.12.dist-info → monoco_toolkit-0.4.0.dist-info}/entry_points.txt +0 -0
- {monoco_toolkit-0.3.12.dist-info → monoco_toolkit-0.4.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,303 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Git Actions - Actions for git operations.
|
|
3
|
-
|
|
4
|
-
Part of Layer 3 (Action Executor) in the event automation framework.
|
|
5
|
-
Provides actions for git commit and push operations.
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
from __future__ import annotations
|
|
9
|
-
|
|
10
|
-
import asyncio
|
|
11
|
-
import logging
|
|
12
|
-
from pathlib import Path
|
|
13
|
-
from typing import Any, Dict, List, Optional
|
|
14
|
-
|
|
15
|
-
from monoco.core.scheduler import AgentEvent
|
|
16
|
-
from monoco.core.router import Action, ActionResult
|
|
17
|
-
|
|
18
|
-
logger = logging.getLogger(__name__)
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
class GitResult:
|
|
22
|
-
"""Result of a git command execution."""
|
|
23
|
-
|
|
24
|
-
def __init__(
|
|
25
|
-
self,
|
|
26
|
-
returncode: int,
|
|
27
|
-
stdout: str,
|
|
28
|
-
stderr: str,
|
|
29
|
-
):
|
|
30
|
-
self.returncode = returncode
|
|
31
|
-
self.stdout = stdout
|
|
32
|
-
self.stderr = stderr
|
|
33
|
-
|
|
34
|
-
@property
|
|
35
|
-
def success(self) -> bool:
|
|
36
|
-
return self.returncode == 0
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
class GitCommitAction(Action):
|
|
40
|
-
"""
|
|
41
|
-
Action that performs git commit.
|
|
42
|
-
|
|
43
|
-
This action stages files and creates a git commit with a message.
|
|
44
|
-
|
|
45
|
-
Example:
|
|
46
|
-
>>> action = GitCommitAction(
|
|
47
|
-
... message="Auto-commit: {issue_id}",
|
|
48
|
-
... files=["*.py"],
|
|
49
|
-
... add_all=False,
|
|
50
|
-
... )
|
|
51
|
-
>>> result = await action(event)
|
|
52
|
-
"""
|
|
53
|
-
|
|
54
|
-
def __init__(
|
|
55
|
-
self,
|
|
56
|
-
message: str,
|
|
57
|
-
files: Optional[List[str]] = None,
|
|
58
|
-
add_all: bool = False,
|
|
59
|
-
working_dir: Optional[Path] = None,
|
|
60
|
-
timeout: int = 30,
|
|
61
|
-
config: Optional[Dict[str, Any]] = None,
|
|
62
|
-
):
|
|
63
|
-
super().__init__(config)
|
|
64
|
-
self.message = message
|
|
65
|
-
self.files = files or []
|
|
66
|
-
self.add_all = add_all
|
|
67
|
-
self.working_dir = working_dir or Path.cwd()
|
|
68
|
-
self.timeout = timeout
|
|
69
|
-
self._last_result: Optional[GitResult] = None
|
|
70
|
-
|
|
71
|
-
@property
|
|
72
|
-
def name(self) -> str:
|
|
73
|
-
return "GitCommitAction"
|
|
74
|
-
|
|
75
|
-
async def can_execute(self, event: AgentEvent) -> bool:
|
|
76
|
-
"""Check if we're in a git repository."""
|
|
77
|
-
git_dir = self.working_dir / ".git"
|
|
78
|
-
return git_dir.exists()
|
|
79
|
-
|
|
80
|
-
async def execute(self, event: AgentEvent) -> ActionResult:
|
|
81
|
-
"""Perform git commit."""
|
|
82
|
-
# Format message with event data
|
|
83
|
-
formatted_message = self._format_message(event)
|
|
84
|
-
|
|
85
|
-
logger.info(f"Performing git commit: {formatted_message[:50]}...")
|
|
86
|
-
|
|
87
|
-
try:
|
|
88
|
-
# Stage files
|
|
89
|
-
if self.add_all:
|
|
90
|
-
await self._run_git_command(["git", "add", "-A"])
|
|
91
|
-
elif self.files:
|
|
92
|
-
for file_pattern in self.files:
|
|
93
|
-
await self._run_git_command(["git", "add", file_pattern])
|
|
94
|
-
|
|
95
|
-
# Check if there are changes to commit
|
|
96
|
-
status_result = await self._run_git_command(
|
|
97
|
-
["git", "status", "--porcelain"]
|
|
98
|
-
)
|
|
99
|
-
|
|
100
|
-
if not status_result.stdout.strip():
|
|
101
|
-
logger.info("No changes to commit")
|
|
102
|
-
return ActionResult.success_result(
|
|
103
|
-
output={"committed": False, "reason": "no_changes"},
|
|
104
|
-
)
|
|
105
|
-
|
|
106
|
-
# Commit
|
|
107
|
-
commit_result = await self._run_git_command(
|
|
108
|
-
["git", "commit", "-m", formatted_message]
|
|
109
|
-
)
|
|
110
|
-
self._last_result = commit_result
|
|
111
|
-
|
|
112
|
-
if commit_result.success:
|
|
113
|
-
# Get commit hash
|
|
114
|
-
hash_result = await self._run_git_command(
|
|
115
|
-
["git", "rev-parse", "HEAD"]
|
|
116
|
-
)
|
|
117
|
-
commit_hash = hash_result.stdout.strip()
|
|
118
|
-
|
|
119
|
-
return ActionResult.success_result(
|
|
120
|
-
output={
|
|
121
|
-
"committed": True,
|
|
122
|
-
"commit_hash": commit_hash,
|
|
123
|
-
"message": formatted_message,
|
|
124
|
-
},
|
|
125
|
-
)
|
|
126
|
-
else:
|
|
127
|
-
return ActionResult.failure_result(
|
|
128
|
-
error=f"Git commit failed: {commit_result.stderr}",
|
|
129
|
-
)
|
|
130
|
-
|
|
131
|
-
except Exception as e:
|
|
132
|
-
logger.error(f"Git commit failed: {e}")
|
|
133
|
-
return ActionResult.failure_result(error=str(e))
|
|
134
|
-
|
|
135
|
-
def _format_message(self, event: AgentEvent) -> str:
|
|
136
|
-
"""Format commit message with event data."""
|
|
137
|
-
try:
|
|
138
|
-
return self.message.format(**event.payload)
|
|
139
|
-
except (KeyError, ValueError):
|
|
140
|
-
# If formatting fails, return original message
|
|
141
|
-
return self.message
|
|
142
|
-
|
|
143
|
-
async def _run_git_command(self, cmd: List[str]) -> GitResult:
|
|
144
|
-
"""Execute a git command."""
|
|
145
|
-
process = await asyncio.create_subprocess_exec(
|
|
146
|
-
*cmd,
|
|
147
|
-
stdout=asyncio.subprocess.PIPE,
|
|
148
|
-
stderr=asyncio.subprocess.PIPE,
|
|
149
|
-
cwd=self.working_dir,
|
|
150
|
-
)
|
|
151
|
-
|
|
152
|
-
try:
|
|
153
|
-
stdout, stderr = await asyncio.wait_for(
|
|
154
|
-
process.communicate(),
|
|
155
|
-
timeout=self.timeout,
|
|
156
|
-
)
|
|
157
|
-
except asyncio.TimeoutError:
|
|
158
|
-
process.kill()
|
|
159
|
-
raise RuntimeError(f"Git command timed out: {' '.join(cmd)}")
|
|
160
|
-
|
|
161
|
-
return GitResult(
|
|
162
|
-
returncode=process.returncode,
|
|
163
|
-
stdout=stdout.decode("utf-8", errors="replace"),
|
|
164
|
-
stderr=stderr.decode("utf-8", errors="replace"),
|
|
165
|
-
)
|
|
166
|
-
|
|
167
|
-
def get_stats(self) -> Dict[str, Any]:
|
|
168
|
-
"""Get action statistics."""
|
|
169
|
-
stats = super().get_stats()
|
|
170
|
-
stats.update({
|
|
171
|
-
"working_dir": str(self.working_dir),
|
|
172
|
-
"message_template": self.message,
|
|
173
|
-
})
|
|
174
|
-
return stats
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
class GitPushAction(Action):
|
|
178
|
-
"""
|
|
179
|
-
Action that performs git push.
|
|
180
|
-
|
|
181
|
-
This action pushes commits to a remote repository.
|
|
182
|
-
|
|
183
|
-
Example:
|
|
184
|
-
>>> action = GitPushAction(
|
|
185
|
-
... remote="origin",
|
|
186
|
-
... branch="main",
|
|
187
|
-
... )
|
|
188
|
-
>>> result = await action(event)
|
|
189
|
-
"""
|
|
190
|
-
|
|
191
|
-
def __init__(
|
|
192
|
-
self,
|
|
193
|
-
remote: str = "origin",
|
|
194
|
-
branch: Optional[str] = None,
|
|
195
|
-
force: bool = False,
|
|
196
|
-
working_dir: Optional[Path] = None,
|
|
197
|
-
timeout: int = 60,
|
|
198
|
-
config: Optional[Dict[str, Any]] = None,
|
|
199
|
-
):
|
|
200
|
-
super().__init__(config)
|
|
201
|
-
self.remote = remote
|
|
202
|
-
self.branch = branch
|
|
203
|
-
self.force = force
|
|
204
|
-
self.working_dir = working_dir or Path.cwd()
|
|
205
|
-
self.timeout = timeout
|
|
206
|
-
self._last_result: Optional[GitResult] = None
|
|
207
|
-
|
|
208
|
-
@property
|
|
209
|
-
def name(self) -> str:
|
|
210
|
-
return "GitPushAction"
|
|
211
|
-
|
|
212
|
-
async def can_execute(self, event: AgentEvent) -> bool:
|
|
213
|
-
"""Check if we're in a git repository with a remote."""
|
|
214
|
-
git_dir = self.working_dir / ".git"
|
|
215
|
-
if not git_dir.exists():
|
|
216
|
-
return False
|
|
217
|
-
|
|
218
|
-
# Check if remote exists
|
|
219
|
-
try:
|
|
220
|
-
result = await self._run_git_command(
|
|
221
|
-
["git", "remote", "get-url", self.remote]
|
|
222
|
-
)
|
|
223
|
-
return result.success
|
|
224
|
-
except Exception:
|
|
225
|
-
return False
|
|
226
|
-
|
|
227
|
-
async def execute(self, event: AgentEvent) -> ActionResult:
|
|
228
|
-
"""Perform git push."""
|
|
229
|
-
# Determine branch
|
|
230
|
-
branch = self.branch
|
|
231
|
-
if not branch:
|
|
232
|
-
# Get current branch
|
|
233
|
-
result = await self._run_git_command(
|
|
234
|
-
["git", "rev-parse", "--abbrev-ref", "HEAD"]
|
|
235
|
-
)
|
|
236
|
-
if result.success:
|
|
237
|
-
branch = result.stdout.strip()
|
|
238
|
-
else:
|
|
239
|
-
return ActionResult.failure_result(
|
|
240
|
-
error="Could not determine current branch"
|
|
241
|
-
)
|
|
242
|
-
|
|
243
|
-
logger.info(f"Pushing to {self.remote}/{branch}")
|
|
244
|
-
|
|
245
|
-
try:
|
|
246
|
-
# Build command
|
|
247
|
-
cmd = ["git", "push", self.remote, branch]
|
|
248
|
-
if self.force:
|
|
249
|
-
cmd.append("--force-with-lease")
|
|
250
|
-
|
|
251
|
-
result = await self._run_git_command(cmd)
|
|
252
|
-
self._last_result = result
|
|
253
|
-
|
|
254
|
-
if result.success:
|
|
255
|
-
return ActionResult.success_result(
|
|
256
|
-
output={
|
|
257
|
-
"pushed": True,
|
|
258
|
-
"remote": self.remote,
|
|
259
|
-
"branch": branch,
|
|
260
|
-
},
|
|
261
|
-
)
|
|
262
|
-
else:
|
|
263
|
-
return ActionResult.failure_result(
|
|
264
|
-
error=f"Git push failed: {result.stderr}",
|
|
265
|
-
)
|
|
266
|
-
|
|
267
|
-
except Exception as e:
|
|
268
|
-
logger.error(f"Git push failed: {e}")
|
|
269
|
-
return ActionResult.failure_result(error=str(e))
|
|
270
|
-
|
|
271
|
-
async def _run_git_command(self, cmd: List[str]) -> GitResult:
|
|
272
|
-
"""Execute a git command."""
|
|
273
|
-
process = await asyncio.create_subprocess_exec(
|
|
274
|
-
*cmd,
|
|
275
|
-
stdout=asyncio.subprocess.PIPE,
|
|
276
|
-
stderr=asyncio.subprocess.PIPE,
|
|
277
|
-
cwd=self.working_dir,
|
|
278
|
-
)
|
|
279
|
-
|
|
280
|
-
try:
|
|
281
|
-
stdout, stderr = await asyncio.wait_for(
|
|
282
|
-
process.communicate(),
|
|
283
|
-
timeout=self.timeout,
|
|
284
|
-
)
|
|
285
|
-
except asyncio.TimeoutError:
|
|
286
|
-
process.kill()
|
|
287
|
-
raise RuntimeError(f"Git command timed out: {' '.join(cmd)}")
|
|
288
|
-
|
|
289
|
-
return GitResult(
|
|
290
|
-
returncode=process.returncode,
|
|
291
|
-
stdout=stdout.decode("utf-8", errors="replace"),
|
|
292
|
-
stderr=stderr.decode("utf-8", errors="replace"),
|
|
293
|
-
)
|
|
294
|
-
|
|
295
|
-
def get_stats(self) -> Dict[str, Any]:
|
|
296
|
-
"""Get action statistics."""
|
|
297
|
-
stats = super().get_stats()
|
|
298
|
-
stats.update({
|
|
299
|
-
"working_dir": str(self.working_dir),
|
|
300
|
-
"remote": self.remote,
|
|
301
|
-
"branch": self.branch or "auto",
|
|
302
|
-
})
|
|
303
|
-
return stats
|
|
@@ -1,309 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
SendIMAction - Action for sending notifications.
|
|
3
|
-
|
|
4
|
-
Part of Layer 3 (Action Executor) in the event automation framework.
|
|
5
|
-
Provides action for sending IM/webhook notifications.
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
from __future__ import annotations
|
|
9
|
-
|
|
10
|
-
import asyncio
|
|
11
|
-
import json
|
|
12
|
-
import logging
|
|
13
|
-
from typing import Any, Dict, List, Optional
|
|
14
|
-
|
|
15
|
-
from monoco.core.scheduler import AgentEvent
|
|
16
|
-
from monoco.core.router import Action, ActionResult
|
|
17
|
-
|
|
18
|
-
logger = logging.getLogger(__name__)
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
class NotificationResult:
|
|
22
|
-
"""Result of a notification send."""
|
|
23
|
-
|
|
24
|
-
def __init__(
|
|
25
|
-
self,
|
|
26
|
-
success: bool,
|
|
27
|
-
message: str,
|
|
28
|
-
response: Optional[Any] = None,
|
|
29
|
-
):
|
|
30
|
-
self.success = success
|
|
31
|
-
self.message = message
|
|
32
|
-
self.response = response
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
class SendIMAction(Action):
|
|
36
|
-
"""
|
|
37
|
-
Action that sends notifications via IM or webhook.
|
|
38
|
-
|
|
39
|
-
This action sends notifications to various channels:
|
|
40
|
-
- Webhook (HTTP POST)
|
|
41
|
-
- Console (stdout)
|
|
42
|
-
- File (append to log file)
|
|
43
|
-
|
|
44
|
-
Future: Slack, Discord, Email, etc.
|
|
45
|
-
|
|
46
|
-
Example:
|
|
47
|
-
>>> action = SendIMAction(
|
|
48
|
-
... channel="webhook",
|
|
49
|
-
... webhook_url="https://hooks.example.com/notify",
|
|
50
|
-
... message_template="Issue {issue_id} updated to {new_stage}",
|
|
51
|
-
... )
|
|
52
|
-
>>> result = await action(event)
|
|
53
|
-
"""
|
|
54
|
-
|
|
55
|
-
def __init__(
|
|
56
|
-
self,
|
|
57
|
-
channel: str = "console",
|
|
58
|
-
message_template: str = "{event_type}: {payload}",
|
|
59
|
-
webhook_url: Optional[str] = None,
|
|
60
|
-
webhook_headers: Optional[Dict[str, str]] = None,
|
|
61
|
-
log_file: Optional[str] = None,
|
|
62
|
-
timeout: int = 30,
|
|
63
|
-
config: Optional[Dict[str, Any]] = None,
|
|
64
|
-
):
|
|
65
|
-
super().__init__(config)
|
|
66
|
-
self.channel = channel
|
|
67
|
-
self.message_template = message_template
|
|
68
|
-
self.webhook_url = webhook_url
|
|
69
|
-
self.webhook_headers = webhook_headers or {}
|
|
70
|
-
self.log_file = log_file
|
|
71
|
-
self.timeout = timeout
|
|
72
|
-
self._last_result: Optional[NotificationResult] = None
|
|
73
|
-
|
|
74
|
-
@property
|
|
75
|
-
def name(self) -> str:
|
|
76
|
-
return f"SendIMAction({self.channel})"
|
|
77
|
-
|
|
78
|
-
async def can_execute(self, event: AgentEvent) -> bool:
|
|
79
|
-
"""Check if the channel is available."""
|
|
80
|
-
if self.channel == "webhook":
|
|
81
|
-
return self.webhook_url is not None
|
|
82
|
-
elif self.channel == "file":
|
|
83
|
-
return self.log_file is not None
|
|
84
|
-
elif self.channel == "console":
|
|
85
|
-
return True
|
|
86
|
-
return False
|
|
87
|
-
|
|
88
|
-
async def execute(self, event: AgentEvent) -> ActionResult:
|
|
89
|
-
"""Send notification."""
|
|
90
|
-
# Format message
|
|
91
|
-
message = self._format_message(event)
|
|
92
|
-
|
|
93
|
-
logger.debug(f"Sending {self.channel} notification: {message[:100]}...")
|
|
94
|
-
|
|
95
|
-
try:
|
|
96
|
-
if self.channel == "webhook":
|
|
97
|
-
result = await self._send_webhook(message, event)
|
|
98
|
-
elif self.channel == "file":
|
|
99
|
-
result = await self._write_to_file(message)
|
|
100
|
-
else: # console
|
|
101
|
-
result = await self._send_console(message)
|
|
102
|
-
|
|
103
|
-
self._last_result = result
|
|
104
|
-
|
|
105
|
-
if result.success:
|
|
106
|
-
return ActionResult.success_result(
|
|
107
|
-
output={
|
|
108
|
-
"channel": self.channel,
|
|
109
|
-
"message_sent": True,
|
|
110
|
-
},
|
|
111
|
-
metadata={
|
|
112
|
-
"message_preview": message[:200],
|
|
113
|
-
},
|
|
114
|
-
)
|
|
115
|
-
else:
|
|
116
|
-
return ActionResult.failure_result(
|
|
117
|
-
error=result.message,
|
|
118
|
-
metadata={
|
|
119
|
-
"channel": self.channel,
|
|
120
|
-
},
|
|
121
|
-
)
|
|
122
|
-
|
|
123
|
-
except Exception as e:
|
|
124
|
-
logger.error(f"Failed to send notification: {e}")
|
|
125
|
-
return ActionResult.failure_result(error=str(e))
|
|
126
|
-
|
|
127
|
-
def _format_message(self, event: AgentEvent) -> str:
|
|
128
|
-
"""Format notification message with event data."""
|
|
129
|
-
try:
|
|
130
|
-
return self.message_template.format(
|
|
131
|
-
event_type=event.type.value,
|
|
132
|
-
timestamp=event.timestamp.isoformat(),
|
|
133
|
-
source=event.source or "unknown",
|
|
134
|
-
**event.payload,
|
|
135
|
-
)
|
|
136
|
-
except (KeyError, ValueError) as e:
|
|
137
|
-
# If formatting fails, return a simple message
|
|
138
|
-
return f"Event: {event.type.value} at {event.timestamp.isoformat()}"
|
|
139
|
-
|
|
140
|
-
async def _send_webhook(
|
|
141
|
-
self,
|
|
142
|
-
message: str,
|
|
143
|
-
event: AgentEvent,
|
|
144
|
-
) -> NotificationResult:
|
|
145
|
-
"""Send notification via webhook."""
|
|
146
|
-
try:
|
|
147
|
-
import aiohttp
|
|
148
|
-
except ImportError:
|
|
149
|
-
# Fallback to sync requests
|
|
150
|
-
return await self._send_webhook_sync(message, event)
|
|
151
|
-
|
|
152
|
-
payload = {
|
|
153
|
-
"message": message,
|
|
154
|
-
"event_type": event.type.value,
|
|
155
|
-
"timestamp": event.timestamp.isoformat(),
|
|
156
|
-
"source": event.source,
|
|
157
|
-
"payload": event.payload,
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
headers = {
|
|
161
|
-
"Content-Type": "application/json",
|
|
162
|
-
**self.webhook_headers,
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
try:
|
|
166
|
-
async with aiohttp.ClientSession() as session:
|
|
167
|
-
async with session.post(
|
|
168
|
-
self.webhook_url,
|
|
169
|
-
json=payload,
|
|
170
|
-
headers=headers,
|
|
171
|
-
timeout=aiohttp.ClientTimeout(total=self.timeout),
|
|
172
|
-
) as response:
|
|
173
|
-
if response.status < 400:
|
|
174
|
-
return NotificationResult(
|
|
175
|
-
success=True,
|
|
176
|
-
message=f"Webhook sent: HTTP {response.status}",
|
|
177
|
-
response={
|
|
178
|
-
"status": response.status,
|
|
179
|
-
"body": await response.text(),
|
|
180
|
-
},
|
|
181
|
-
)
|
|
182
|
-
else:
|
|
183
|
-
return NotificationResult(
|
|
184
|
-
success=False,
|
|
185
|
-
message=f"Webhook failed: HTTP {response.status}",
|
|
186
|
-
response={"status": response.status},
|
|
187
|
-
)
|
|
188
|
-
except Exception as e:
|
|
189
|
-
return NotificationResult(
|
|
190
|
-
success=False,
|
|
191
|
-
message=f"Webhook error: {str(e)}",
|
|
192
|
-
)
|
|
193
|
-
|
|
194
|
-
async def _send_webhook_sync(
|
|
195
|
-
self,
|
|
196
|
-
message: str,
|
|
197
|
-
event: AgentEvent,
|
|
198
|
-
) -> NotificationResult:
|
|
199
|
-
"""Send webhook using sync requests (fallback)."""
|
|
200
|
-
try:
|
|
201
|
-
import requests
|
|
202
|
-
except ImportError:
|
|
203
|
-
return NotificationResult(
|
|
204
|
-
success=False,
|
|
205
|
-
message="Neither aiohttp nor requests available for webhook",
|
|
206
|
-
)
|
|
207
|
-
|
|
208
|
-
payload = {
|
|
209
|
-
"message": message,
|
|
210
|
-
"event_type": event.type.value,
|
|
211
|
-
"timestamp": event.timestamp.isoformat(),
|
|
212
|
-
"source": event.source,
|
|
213
|
-
"payload": event.payload,
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
headers = {
|
|
217
|
-
"Content-Type": "application/json",
|
|
218
|
-
**self.webhook_headers,
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
try:
|
|
222
|
-
response = requests.post(
|
|
223
|
-
self.webhook_url,
|
|
224
|
-
json=payload,
|
|
225
|
-
headers=headers,
|
|
226
|
-
timeout=self.timeout,
|
|
227
|
-
)
|
|
228
|
-
|
|
229
|
-
if response.status_code < 400:
|
|
230
|
-
return NotificationResult(
|
|
231
|
-
success=True,
|
|
232
|
-
message=f"Webhook sent: HTTP {response.status_code}",
|
|
233
|
-
response={
|
|
234
|
-
"status": response.status_code,
|
|
235
|
-
"body": response.text,
|
|
236
|
-
},
|
|
237
|
-
)
|
|
238
|
-
else:
|
|
239
|
-
return NotificationResult(
|
|
240
|
-
success=False,
|
|
241
|
-
message=f"Webhook failed: HTTP {response.status_code}",
|
|
242
|
-
)
|
|
243
|
-
except Exception as e:
|
|
244
|
-
return NotificationResult(
|
|
245
|
-
success=False,
|
|
246
|
-
message=f"Webhook error: {str(e)}",
|
|
247
|
-
)
|
|
248
|
-
|
|
249
|
-
async def _write_to_file(self, message: str) -> NotificationResult:
|
|
250
|
-
"""Write notification to log file."""
|
|
251
|
-
try:
|
|
252
|
-
import aiofiles
|
|
253
|
-
except ImportError:
|
|
254
|
-
# Fallback to sync file write
|
|
255
|
-
return await self._write_to_file_sync(message)
|
|
256
|
-
|
|
257
|
-
try:
|
|
258
|
-
timestamp = asyncio.get_event_loop().time()
|
|
259
|
-
log_line = f"[{timestamp}] {message}\n"
|
|
260
|
-
|
|
261
|
-
async with aiofiles.open(self.log_file, "a") as f:
|
|
262
|
-
await f.write(log_line)
|
|
263
|
-
|
|
264
|
-
return NotificationResult(
|
|
265
|
-
success=True,
|
|
266
|
-
message=f"Written to {self.log_file}",
|
|
267
|
-
)
|
|
268
|
-
except Exception as e:
|
|
269
|
-
return NotificationResult(
|
|
270
|
-
success=False,
|
|
271
|
-
message=f"File write error: {str(e)}",
|
|
272
|
-
)
|
|
273
|
-
|
|
274
|
-
async def _write_to_file_sync(self, message: str) -> NotificationResult:
|
|
275
|
-
"""Write to file synchronously (fallback)."""
|
|
276
|
-
try:
|
|
277
|
-
import time
|
|
278
|
-
log_line = f"[{time.time()}] {message}\n"
|
|
279
|
-
|
|
280
|
-
with open(self.log_file, "a") as f:
|
|
281
|
-
f.write(log_line)
|
|
282
|
-
|
|
283
|
-
return NotificationResult(
|
|
284
|
-
success=True,
|
|
285
|
-
message=f"Written to {self.log_file}",
|
|
286
|
-
)
|
|
287
|
-
except Exception as e:
|
|
288
|
-
return NotificationResult(
|
|
289
|
-
success=False,
|
|
290
|
-
message=f"File write error: {str(e)}",
|
|
291
|
-
)
|
|
292
|
-
|
|
293
|
-
async def _send_console(self, message: str) -> NotificationResult:
|
|
294
|
-
"""Print notification to console."""
|
|
295
|
-
print(f"[NOTIFICATION] {message}")
|
|
296
|
-
return NotificationResult(
|
|
297
|
-
success=True,
|
|
298
|
-
message="Printed to console",
|
|
299
|
-
)
|
|
300
|
-
|
|
301
|
-
def get_stats(self) -> Dict[str, Any]:
|
|
302
|
-
"""Get action statistics."""
|
|
303
|
-
stats = super().get_stats()
|
|
304
|
-
stats.update({
|
|
305
|
-
"channel": self.channel,
|
|
306
|
-
"webhook_configured": self.webhook_url is not None,
|
|
307
|
-
"log_file": self.log_file,
|
|
308
|
-
})
|
|
309
|
-
return stats
|