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.
Files changed (120) hide show
  1. monoco/core/automation/__init__.py +0 -11
  2. monoco/core/automation/handlers.py +108 -26
  3. monoco/core/config.py +28 -10
  4. monoco/core/daemon/__init__.py +5 -0
  5. monoco/core/daemon/pid.py +290 -0
  6. monoco/core/injection.py +86 -8
  7. monoco/core/integrations.py +0 -24
  8. monoco/core/router/__init__.py +1 -39
  9. monoco/core/router/action.py +3 -142
  10. monoco/core/scheduler/events.py +28 -2
  11. monoco/core/setup.py +9 -0
  12. monoco/core/sync.py +199 -4
  13. monoco/core/watcher/__init__.py +6 -0
  14. monoco/core/watcher/base.py +18 -1
  15. monoco/core/watcher/im.py +460 -0
  16. monoco/core/watcher/memo.py +40 -48
  17. monoco/daemon/app.py +3 -60
  18. monoco/daemon/commands.py +459 -25
  19. monoco/daemon/scheduler.py +1 -16
  20. monoco/daemon/services.py +15 -0
  21. monoco/features/agent/resources/en/AGENTS.md +14 -14
  22. monoco/features/agent/resources/en/skills/monoco_role_engineer/SKILL.md +101 -0
  23. monoco/features/agent/resources/en/skills/monoco_role_manager/SKILL.md +95 -0
  24. monoco/features/agent/resources/en/skills/monoco_role_planner/SKILL.md +177 -0
  25. monoco/features/agent/resources/en/skills/monoco_role_reviewer/SKILL.md +139 -0
  26. monoco/features/agent/resources/zh/skills/monoco_role_engineer/SKILL.md +101 -0
  27. monoco/features/agent/resources/zh/skills/monoco_role_manager/SKILL.md +95 -0
  28. monoco/features/agent/resources/zh/skills/monoco_role_planner/SKILL.md +177 -0
  29. monoco/features/agent/resources/zh/skills/monoco_role_reviewer/SKILL.md +139 -0
  30. monoco/features/hooks/__init__.py +61 -6
  31. monoco/features/hooks/commands.py +281 -271
  32. monoco/features/hooks/dispatchers/__init__.py +23 -0
  33. monoco/features/hooks/dispatchers/agent_dispatcher.py +486 -0
  34. monoco/features/hooks/dispatchers/git_dispatcher.py +478 -0
  35. monoco/features/hooks/manager.py +357 -0
  36. monoco/features/hooks/models.py +262 -0
  37. monoco/features/hooks/parser.py +322 -0
  38. monoco/features/hooks/universal_interceptor.py +503 -0
  39. monoco/features/im/__init__.py +67 -0
  40. monoco/features/im/core.py +782 -0
  41. monoco/features/im/models.py +311 -0
  42. monoco/features/issue/commands.py +65 -50
  43. monoco/features/issue/core.py +199 -99
  44. monoco/features/issue/domain_commands.py +0 -19
  45. monoco/features/issue/resources/en/AGENTS.md +17 -122
  46. monoco/features/issue/resources/hooks/agent/before-tool.sh +102 -0
  47. monoco/features/issue/resources/hooks/agent/session-start.sh +88 -0
  48. monoco/features/issue/resources/hooks/{post-checkout.sh → git/git-post-checkout.sh} +10 -9
  49. monoco/features/issue/resources/hooks/git/git-pre-commit.sh +31 -0
  50. monoco/features/issue/resources/hooks/{pre-push.sh → git/git-pre-push.sh} +7 -13
  51. monoco/features/issue/resources/zh/AGENTS.md +18 -123
  52. monoco/features/memo/cli.py +15 -64
  53. monoco/features/memo/core.py +6 -34
  54. monoco/features/memo/models.py +24 -15
  55. monoco/features/memo/resources/en/AGENTS.md +31 -0
  56. monoco/features/memo/resources/zh/AGENTS.md +28 -5
  57. monoco/main.py +5 -3
  58. {monoco_toolkit-0.3.12.dist-info → monoco_toolkit-0.4.0.dist-info}/METADATA +1 -1
  59. monoco_toolkit-0.4.0.dist-info/RECORD +170 -0
  60. monoco/core/automation/config.py +0 -338
  61. monoco/core/execution.py +0 -67
  62. monoco/core/executor/__init__.py +0 -38
  63. monoco/core/executor/agent_action.py +0 -254
  64. monoco/core/executor/git_action.py +0 -303
  65. monoco/core/executor/im_action.py +0 -309
  66. monoco/core/executor/pytest_action.py +0 -218
  67. monoco/core/router/router.py +0 -392
  68. monoco/features/agent/resources/atoms/atom-code-dev.yaml +0 -61
  69. monoco/features/agent/resources/atoms/atom-issue-lifecycle.yaml +0 -73
  70. monoco/features/agent/resources/atoms/atom-knowledge.yaml +0 -55
  71. monoco/features/agent/resources/atoms/atom-review.yaml +0 -60
  72. monoco/features/agent/resources/en/skills/monoco_atom_core/SKILL.md +0 -99
  73. monoco/features/agent/resources/en/skills/monoco_workflow_agent_engineer/SKILL.md +0 -94
  74. monoco/features/agent/resources/en/skills/monoco_workflow_agent_manager/SKILL.md +0 -93
  75. monoco/features/agent/resources/en/skills/monoco_workflow_agent_planner/SKILL.md +0 -85
  76. monoco/features/agent/resources/en/skills/monoco_workflow_agent_reviewer/SKILL.md +0 -114
  77. monoco/features/agent/resources/workflows/workflow-dev.yaml +0 -83
  78. monoco/features/agent/resources/workflows/workflow-issue-create.yaml +0 -72
  79. monoco/features/agent/resources/workflows/workflow-review.yaml +0 -94
  80. monoco/features/agent/resources/zh/roles/monoco_role_engineer.yaml +0 -49
  81. monoco/features/agent/resources/zh/roles/monoco_role_manager.yaml +0 -46
  82. monoco/features/agent/resources/zh/roles/monoco_role_planner.yaml +0 -46
  83. monoco/features/agent/resources/zh/roles/monoco_role_reviewer.yaml +0 -47
  84. monoco/features/agent/resources/zh/skills/monoco_atom_core/SKILL.md +0 -99
  85. monoco/features/agent/resources/zh/skills/monoco_workflow_agent_engineer/SKILL.md +0 -94
  86. monoco/features/agent/resources/zh/skills/monoco_workflow_agent_manager/SKILL.md +0 -88
  87. monoco/features/agent/resources/zh/skills/monoco_workflow_agent_planner/SKILL.md +0 -259
  88. monoco/features/agent/resources/zh/skills/monoco_workflow_agent_reviewer/SKILL.md +0 -137
  89. monoco/features/artifact/resources/zh/skills/monoco_atom_artifact/SKILL.md +0 -278
  90. monoco/features/glossary/resources/en/skills/monoco_atom_glossary/SKILL.md +0 -35
  91. monoco/features/glossary/resources/zh/skills/monoco_atom_glossary/SKILL.md +0 -35
  92. monoco/features/hooks/adapter.py +0 -67
  93. monoco/features/hooks/core.py +0 -441
  94. monoco/features/i18n/resources/en/skills/monoco_atom_i18n/SKILL.md +0 -96
  95. monoco/features/i18n/resources/en/skills/monoco_workflow_i18n_scan/SKILL.md +0 -105
  96. monoco/features/i18n/resources/zh/skills/monoco_atom_i18n/SKILL.md +0 -96
  97. monoco/features/i18n/resources/zh/skills/monoco_workflow_i18n_scan/SKILL.md +0 -105
  98. monoco/features/issue/resources/en/skills/monoco_atom_issue/SKILL.md +0 -165
  99. monoco/features/issue/resources/en/skills/monoco_workflow_issue_creation/SKILL.md +0 -167
  100. monoco/features/issue/resources/en/skills/monoco_workflow_issue_development/SKILL.md +0 -224
  101. monoco/features/issue/resources/en/skills/monoco_workflow_issue_management/SKILL.md +0 -159
  102. monoco/features/issue/resources/en/skills/monoco_workflow_issue_refinement/SKILL.md +0 -203
  103. monoco/features/issue/resources/hooks/pre-commit.sh +0 -41
  104. monoco/features/issue/resources/zh/skills/monoco_atom_issue_lifecycle/SKILL.md +0 -190
  105. monoco/features/issue/resources/zh/skills/monoco_workflow_issue_creation/SKILL.md +0 -167
  106. monoco/features/issue/resources/zh/skills/monoco_workflow_issue_development/SKILL.md +0 -224
  107. monoco/features/issue/resources/zh/skills/monoco_workflow_issue_management/SKILL.md +0 -159
  108. monoco/features/issue/resources/zh/skills/monoco_workflow_issue_refinement/SKILL.md +0 -203
  109. monoco/features/memo/resources/en/skills/monoco_atom_memo/SKILL.md +0 -77
  110. monoco/features/memo/resources/en/skills/monoco_workflow_note_processing/SKILL.md +0 -140
  111. monoco/features/memo/resources/zh/skills/monoco_atom_memo/SKILL.md +0 -77
  112. monoco/features/memo/resources/zh/skills/monoco_workflow_note_processing/SKILL.md +0 -140
  113. monoco/features/spike/resources/en/skills/monoco_atom_spike/SKILL.md +0 -76
  114. monoco/features/spike/resources/en/skills/monoco_workflow_research/SKILL.md +0 -121
  115. monoco/features/spike/resources/zh/skills/monoco_atom_spike/SKILL.md +0 -76
  116. monoco/features/spike/resources/zh/skills/monoco_workflow_research/SKILL.md +0 -121
  117. monoco_toolkit-0.3.12.dist-info/RECORD +0 -202
  118. {monoco_toolkit-0.3.12.dist-info → monoco_toolkit-0.4.0.dist-info}/WHEEL +0 -0
  119. {monoco_toolkit-0.3.12.dist-info → monoco_toolkit-0.4.0.dist-info}/entry_points.txt +0 -0
  120. {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