monoco-toolkit 0.3.11__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 +40 -0
- monoco/core/automation/field_watcher.py +296 -0
- monoco/core/automation/handlers.py +805 -0
- monoco/core/config.py +29 -11
- monoco/core/daemon/__init__.py +5 -0
- monoco/core/daemon/pid.py +290 -0
- monoco/core/git.py +15 -0
- monoco/core/hooks/context.py +74 -13
- monoco/core/injection.py +86 -8
- monoco/core/integrations.py +0 -24
- monoco/core/router/__init__.py +17 -0
- monoco/core/router/action.py +202 -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 +197 -0
- monoco/core/scheduler/local.py +377 -0
- monoco/core/setup.py +9 -0
- monoco/core/sync.py +199 -4
- monoco/core/watcher/__init__.py +63 -0
- monoco/core/watcher/base.py +382 -0
- monoco/core/watcher/dropzone.py +152 -0
- monoco/core/watcher/im.py +460 -0
- monoco/core/watcher/issue.py +303 -0
- monoco/core/watcher/memo.py +192 -0
- monoco/core/watcher/task.py +238 -0
- monoco/daemon/app.py +3 -60
- monoco/daemon/commands.py +459 -25
- monoco/daemon/events.py +34 -0
- monoco/daemon/scheduler.py +157 -201
- monoco/daemon/services.py +42 -243
- monoco/features/agent/__init__.py +25 -7
- monoco/features/agent/cli.py +91 -57
- monoco/features/agent/engines.py +31 -170
- 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/agent/worker.py +1 -1
- 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 +133 -60
- monoco/features/issue/core.py +385 -40
- 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/features/spike/commands.py +5 -3
- monoco/main.py +5 -3
- {monoco_toolkit-0.3.11.dist-info → monoco_toolkit-0.4.0.dist-info}/METADATA +1 -1
- monoco_toolkit-0.4.0.dist-info/RECORD +170 -0
- monoco/core/execution.py +0 -67
- monoco/features/agent/apoptosis.py +0 -44
- monoco/features/agent/manager.py +0 -127
- 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/agent/session.py +0 -169
- 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.11.dist-info/RECORD +0 -181
- {monoco_toolkit-0.3.11.dist-info → monoco_toolkit-0.4.0.dist-info}/WHEEL +0 -0
- {monoco_toolkit-0.3.11.dist-info → monoco_toolkit-0.4.0.dist-info}/entry_points.txt +0 -0
- {monoco_toolkit-0.3.11.dist-info → monoco_toolkit-0.4.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""
|
|
2
|
+
DropzoneWatcher - Adapter for the existing dropzone watcher.
|
|
3
|
+
|
|
4
|
+
Part of Layer 1 (File Watcher) in the event automation framework.
|
|
5
|
+
Wraps the existing ingestion watcher to fit the FilesystemWatcher interface.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
import logging
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any, Callable, Dict, Optional
|
|
14
|
+
|
|
15
|
+
from monoco.core.scheduler import AgentEventType, EventBus, event_bus
|
|
16
|
+
from monoco.core.ingestion.watcher import (
|
|
17
|
+
DropzoneWatcher as LegacyDropzoneWatcher,
|
|
18
|
+
IngestionEvent,
|
|
19
|
+
IngestionEventType,
|
|
20
|
+
)
|
|
21
|
+
from monoco.core.artifacts.manager import ArtifactManager
|
|
22
|
+
|
|
23
|
+
from .base import (
|
|
24
|
+
ChangeType,
|
|
25
|
+
FileEvent,
|
|
26
|
+
FilesystemWatcher,
|
|
27
|
+
WatchConfig,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
logger = logging.getLogger(__name__)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class DropzoneFileEvent(FileEvent):
|
|
34
|
+
"""FileEvent specific to Dropzone."""
|
|
35
|
+
|
|
36
|
+
def __init__(
|
|
37
|
+
self,
|
|
38
|
+
path: Path,
|
|
39
|
+
change_type: ChangeType,
|
|
40
|
+
ingestion_event_type: IngestionEventType,
|
|
41
|
+
artifact_id: Optional[str] = None,
|
|
42
|
+
**kwargs,
|
|
43
|
+
):
|
|
44
|
+
super().__init__(
|
|
45
|
+
path=path,
|
|
46
|
+
change_type=change_type,
|
|
47
|
+
watcher_name="DropzoneWatcher",
|
|
48
|
+
**kwargs,
|
|
49
|
+
)
|
|
50
|
+
self.ingestion_event_type = ingestion_event_type
|
|
51
|
+
self.artifact_id = artifact_id
|
|
52
|
+
|
|
53
|
+
def to_agent_event_type(self) -> Optional[AgentEventType]:
|
|
54
|
+
"""Dropzone events don't map directly to agent events."""
|
|
55
|
+
return None
|
|
56
|
+
|
|
57
|
+
def to_payload(self) -> Dict[str, Any]:
|
|
58
|
+
"""Convert to payload with Dropzone-specific fields."""
|
|
59
|
+
payload = super().to_payload()
|
|
60
|
+
payload["ingestion_event_type"] = self.ingestion_event_type.value
|
|
61
|
+
payload["artifact_id"] = self.artifact_id
|
|
62
|
+
return payload
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class DropzoneWatcher(FilesystemWatcher):
|
|
66
|
+
"""
|
|
67
|
+
Adapter for the existing DropzoneWatcher.
|
|
68
|
+
|
|
69
|
+
Wraps the legacy ingestion watcher to provide a unified interface
|
|
70
|
+
while maintaining backward compatibility.
|
|
71
|
+
|
|
72
|
+
Example:
|
|
73
|
+
>>> from monoco.core.artifacts.manager import ArtifactManager
|
|
74
|
+
>>> artifact_manager = ArtifactManager()
|
|
75
|
+
>>> config = WatchConfig(path=Path("./.monoco/dropzone"))
|
|
76
|
+
>>> watcher = DropzoneWatcher(config, artifact_manager)
|
|
77
|
+
>>> await watcher.start()
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
def __init__(
|
|
81
|
+
self,
|
|
82
|
+
config: WatchConfig,
|
|
83
|
+
artifact_manager: ArtifactManager,
|
|
84
|
+
event_bus: Optional[EventBus] = None,
|
|
85
|
+
name: str = "DropzoneWatcher",
|
|
86
|
+
):
|
|
87
|
+
super().__init__(config, event_bus, name)
|
|
88
|
+
self.artifact_manager = artifact_manager
|
|
89
|
+
|
|
90
|
+
# Create legacy watcher
|
|
91
|
+
self._legacy_watcher = LegacyDropzoneWatcher(
|
|
92
|
+
dropzone_path=config.path,
|
|
93
|
+
artifact_manager=artifact_manager,
|
|
94
|
+
process_existing=False,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
# Set up event forwarding
|
|
98
|
+
self._legacy_watcher.set_event_callback(self._on_ingestion_event)
|
|
99
|
+
|
|
100
|
+
async def start(self) -> None:
|
|
101
|
+
"""Start watching the dropzone."""
|
|
102
|
+
if self._running:
|
|
103
|
+
return
|
|
104
|
+
|
|
105
|
+
self._legacy_watcher.start()
|
|
106
|
+
self._running = True
|
|
107
|
+
logger.info(f"Started DropzoneWatcher: {self.config.path}")
|
|
108
|
+
|
|
109
|
+
async def stop(self) -> None:
|
|
110
|
+
"""Stop watching the dropzone."""
|
|
111
|
+
if not self._running:
|
|
112
|
+
return
|
|
113
|
+
|
|
114
|
+
self._legacy_watcher.stop()
|
|
115
|
+
self._running = False
|
|
116
|
+
logger.info(f"Stopped DropzoneWatcher: {self.config.path}")
|
|
117
|
+
|
|
118
|
+
def _on_ingestion_event(self, event: IngestionEvent) -> None:
|
|
119
|
+
"""Handle ingestion events from legacy watcher."""
|
|
120
|
+
# Map ingestion event type to change type
|
|
121
|
+
change_type_map = {
|
|
122
|
+
IngestionEventType.FILE_DETECTED: ChangeType.CREATED,
|
|
123
|
+
IngestionEventType.CONVERSION_STARTED: ChangeType.MODIFIED,
|
|
124
|
+
IngestionEventType.CONVERSION_COMPLETED: ChangeType.MODIFIED,
|
|
125
|
+
IngestionEventType.CONVERSION_FAILED: ChangeType.MODIFIED,
|
|
126
|
+
IngestionEventType.ARTIFACT_REGISTERED: ChangeType.MODIFIED,
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
change_type = change_type_map.get(event.event_type, ChangeType.MODIFIED)
|
|
130
|
+
|
|
131
|
+
# Create unified event
|
|
132
|
+
file_event = DropzoneFileEvent(
|
|
133
|
+
path=event.file_path,
|
|
134
|
+
change_type=change_type,
|
|
135
|
+
ingestion_event_type=event.event_type,
|
|
136
|
+
artifact_id=event.artifact_id,
|
|
137
|
+
metadata={
|
|
138
|
+
"task_id": event.task_id,
|
|
139
|
+
"error_message": event.error_message,
|
|
140
|
+
**event.metadata,
|
|
141
|
+
},
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
# Emit synchronously (called from sync context)
|
|
145
|
+
asyncio.create_task(self.emit(file_event))
|
|
146
|
+
|
|
147
|
+
def get_stats(self) -> Dict[str, Any]:
|
|
148
|
+
"""Get watcher statistics."""
|
|
149
|
+
stats = super().get_stats()
|
|
150
|
+
legacy_stats = self._legacy_watcher.get_stats()
|
|
151
|
+
stats.update(legacy_stats)
|
|
152
|
+
return stats
|
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
"""
|
|
2
|
+
IMWatcher - Monitors IM message flow for Agent triggers (FEAT-0167).
|
|
3
|
+
|
|
4
|
+
Part of Layer 1 (File Watcher) in the event automation framework.
|
|
5
|
+
Emits events when IM messages are received and need Agent processing.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
import json
|
|
12
|
+
import logging
|
|
13
|
+
from datetime import datetime
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any, Dict, List, Optional, Callable
|
|
16
|
+
|
|
17
|
+
from monoco.core.scheduler import AgentEventType, EventBus, event_bus
|
|
18
|
+
|
|
19
|
+
from .base import (
|
|
20
|
+
ChangeType,
|
|
21
|
+
FileEvent,
|
|
22
|
+
PollingWatcher,
|
|
23
|
+
WatchConfig,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class IMFileEvent(FileEvent):
|
|
30
|
+
"""FileEvent specific to IM message files."""
|
|
31
|
+
|
|
32
|
+
def __init__(
|
|
33
|
+
self,
|
|
34
|
+
path: Path,
|
|
35
|
+
change_type: ChangeType,
|
|
36
|
+
message_id: str,
|
|
37
|
+
channel_id: str,
|
|
38
|
+
platform: str,
|
|
39
|
+
event_type: str = "message_received",
|
|
40
|
+
**kwargs,
|
|
41
|
+
):
|
|
42
|
+
super().__init__(
|
|
43
|
+
path=path,
|
|
44
|
+
change_type=change_type,
|
|
45
|
+
watcher_name="IMWatcher",
|
|
46
|
+
**kwargs,
|
|
47
|
+
)
|
|
48
|
+
self.message_id = message_id
|
|
49
|
+
self.channel_id = channel_id
|
|
50
|
+
self.platform = platform
|
|
51
|
+
self.event_type = event_type
|
|
52
|
+
|
|
53
|
+
def to_agent_event_type(self) -> Optional[AgentEventType]:
|
|
54
|
+
"""Convert to appropriate AgentEventType."""
|
|
55
|
+
event_map = {
|
|
56
|
+
"message_received": AgentEventType.IM_MESSAGE_RECEIVED,
|
|
57
|
+
"message_replied": AgentEventType.IM_MESSAGE_REPLIED,
|
|
58
|
+
"agent_trigger": AgentEventType.IM_AGENT_TRIGGER,
|
|
59
|
+
"session_started": AgentEventType.IM_SESSION_STARTED,
|
|
60
|
+
"session_closed": AgentEventType.IM_SESSION_CLOSED,
|
|
61
|
+
}
|
|
62
|
+
return event_map.get(self.event_type)
|
|
63
|
+
|
|
64
|
+
def to_payload(self) -> Dict[str, Any]:
|
|
65
|
+
"""Convert to payload with IM-specific fields."""
|
|
66
|
+
payload = super().to_payload()
|
|
67
|
+
payload.update({
|
|
68
|
+
"message_id": self.message_id,
|
|
69
|
+
"channel_id": self.channel_id,
|
|
70
|
+
"platform": self.platform,
|
|
71
|
+
"event_type": self.event_type,
|
|
72
|
+
})
|
|
73
|
+
return payload
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class IMWatcher(PollingWatcher):
|
|
77
|
+
"""
|
|
78
|
+
Watcher for IM message storage.
|
|
79
|
+
|
|
80
|
+
Monitors the .monoco/im/messages/ directory for:
|
|
81
|
+
- New message files
|
|
82
|
+
- Message status changes
|
|
83
|
+
- Agent trigger conditions
|
|
84
|
+
|
|
85
|
+
Emits appropriate events to the EventBus for Agent scheduling.
|
|
86
|
+
|
|
87
|
+
Example:
|
|
88
|
+
>>> config = WatchConfig(
|
|
89
|
+
... path=Path("./.monoco/im/messages"),
|
|
90
|
+
... patterns=["*.jsonl"],
|
|
91
|
+
... poll_interval=2.0,
|
|
92
|
+
... )
|
|
93
|
+
>>> watcher = IMWatcher(config)
|
|
94
|
+
>>> await watcher.start()
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
def __init__(
|
|
98
|
+
self,
|
|
99
|
+
config: WatchConfig,
|
|
100
|
+
event_bus: Optional[EventBus] = None,
|
|
101
|
+
name: str = "IMWatcher",
|
|
102
|
+
trigger_on_mention: bool = True,
|
|
103
|
+
):
|
|
104
|
+
super().__init__(config, event_bus, name)
|
|
105
|
+
self.trigger_on_mention = trigger_on_mention
|
|
106
|
+
self._file_states: Dict[Path, Dict[str, Any]] = {}
|
|
107
|
+
self._processed_messages: set = set()
|
|
108
|
+
self._message_handlers: List[Callable[[Dict[str, Any]], None]] = []
|
|
109
|
+
|
|
110
|
+
def register_message_handler(
|
|
111
|
+
self,
|
|
112
|
+
handler: Callable[[Dict[str, Any]], None]
|
|
113
|
+
) -> None:
|
|
114
|
+
"""
|
|
115
|
+
Register a handler for new messages.
|
|
116
|
+
|
|
117
|
+
The handler will be called with the message data dictionary.
|
|
118
|
+
"""
|
|
119
|
+
self._message_handlers.append(handler)
|
|
120
|
+
logger.debug(f"Registered message handler: {handler.__name__}")
|
|
121
|
+
|
|
122
|
+
def unregister_message_handler(
|
|
123
|
+
self,
|
|
124
|
+
handler: Callable[[Dict[str, Any]], None]
|
|
125
|
+
) -> None:
|
|
126
|
+
"""Unregister a message handler."""
|
|
127
|
+
if handler in self._message_handlers:
|
|
128
|
+
self._message_handlers.remove(handler)
|
|
129
|
+
|
|
130
|
+
async def _check_changes(self) -> None:
|
|
131
|
+
"""Check for new messages in IM storage."""
|
|
132
|
+
if not self.config.path.exists():
|
|
133
|
+
return
|
|
134
|
+
|
|
135
|
+
try:
|
|
136
|
+
# Scan all message files
|
|
137
|
+
message_files = list(self.config.path.glob("*.jsonl"))
|
|
138
|
+
|
|
139
|
+
for file_path in message_files:
|
|
140
|
+
if not self._should_process(file_path):
|
|
141
|
+
continue
|
|
142
|
+
|
|
143
|
+
await self._process_message_file(file_path)
|
|
144
|
+
|
|
145
|
+
except Exception as e:
|
|
146
|
+
logger.error(f"Error checking IM messages: {e}")
|
|
147
|
+
|
|
148
|
+
async def _process_message_file(self, file_path: Path) -> None:
|
|
149
|
+
"""Process a message file for new entries."""
|
|
150
|
+
try:
|
|
151
|
+
# Get current file state
|
|
152
|
+
stat = file_path.stat()
|
|
153
|
+
current_size = stat.st_size
|
|
154
|
+
|
|
155
|
+
# Check if we've seen this file before
|
|
156
|
+
if file_path in self._file_states:
|
|
157
|
+
last_size = self._file_states[file_path].get("size", 0)
|
|
158
|
+
if current_size <= last_size:
|
|
159
|
+
# No new content
|
|
160
|
+
return
|
|
161
|
+
|
|
162
|
+
# Read new messages
|
|
163
|
+
messages = self._read_messages(file_path)
|
|
164
|
+
|
|
165
|
+
for message_data in messages:
|
|
166
|
+
message_id = message_data.get("message_id")
|
|
167
|
+
|
|
168
|
+
if not message_id:
|
|
169
|
+
continue
|
|
170
|
+
|
|
171
|
+
# Skip already processed messages
|
|
172
|
+
if message_id in self._processed_messages:
|
|
173
|
+
continue
|
|
174
|
+
|
|
175
|
+
# Process new message
|
|
176
|
+
await self._handle_new_message(file_path, message_data)
|
|
177
|
+
self._processed_messages.add(message_id)
|
|
178
|
+
|
|
179
|
+
# Update file state
|
|
180
|
+
self._file_states[file_path] = {
|
|
181
|
+
"size": current_size,
|
|
182
|
+
"mtime": stat.st_mtime,
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
except Exception as e:
|
|
186
|
+
logger.error(f"Error processing {file_path}: {e}")
|
|
187
|
+
|
|
188
|
+
def _read_messages(self, file_path: Path) -> List[Dict[str, Any]]:
|
|
189
|
+
"""Read all messages from a JSONL file."""
|
|
190
|
+
messages = []
|
|
191
|
+
|
|
192
|
+
try:
|
|
193
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
|
194
|
+
for line in f:
|
|
195
|
+
line = line.strip()
|
|
196
|
+
if not line:
|
|
197
|
+
continue
|
|
198
|
+
try:
|
|
199
|
+
data = json.loads(line)
|
|
200
|
+
messages.append(data)
|
|
201
|
+
except json.JSONDecodeError:
|
|
202
|
+
logger.warning(f"Invalid JSON in {file_path}")
|
|
203
|
+
|
|
204
|
+
except Exception as e:
|
|
205
|
+
logger.error(f"Error reading {file_path}: {e}")
|
|
206
|
+
|
|
207
|
+
return messages
|
|
208
|
+
|
|
209
|
+
async def _handle_new_message(
|
|
210
|
+
self,
|
|
211
|
+
file_path: Path,
|
|
212
|
+
message_data: Dict[str, Any]
|
|
213
|
+
) -> None:
|
|
214
|
+
"""Handle a new message."""
|
|
215
|
+
message_id = message_data.get("message_id")
|
|
216
|
+
channel_id = message_data.get("channel_id")
|
|
217
|
+
platform = message_data.get("platform", "unknown")
|
|
218
|
+
status = message_data.get("status", "received")
|
|
219
|
+
|
|
220
|
+
logger.debug(f"New IM message: {message_id} in {channel_id}")
|
|
221
|
+
|
|
222
|
+
# Call registered handlers
|
|
223
|
+
for handler in self._message_handlers:
|
|
224
|
+
try:
|
|
225
|
+
if asyncio.iscoroutinefunction(handler):
|
|
226
|
+
await handler(message_data)
|
|
227
|
+
else:
|
|
228
|
+
handler(message_data)
|
|
229
|
+
except Exception as e:
|
|
230
|
+
logger.error(f"Message handler error: {e}")
|
|
231
|
+
|
|
232
|
+
# Determine event type based on status
|
|
233
|
+
event_type = self._determine_event_type(message_data)
|
|
234
|
+
|
|
235
|
+
if event_type:
|
|
236
|
+
event = IMFileEvent(
|
|
237
|
+
path=file_path,
|
|
238
|
+
change_type=ChangeType.CREATED,
|
|
239
|
+
message_id=message_id,
|
|
240
|
+
channel_id=channel_id,
|
|
241
|
+
platform=platform,
|
|
242
|
+
event_type=event_type,
|
|
243
|
+
metadata={
|
|
244
|
+
"status": status,
|
|
245
|
+
"sender": message_data.get("sender", {}),
|
|
246
|
+
"content_preview": self._get_content_preview(message_data),
|
|
247
|
+
},
|
|
248
|
+
)
|
|
249
|
+
await self.emit(event)
|
|
250
|
+
logger.info(f"Emitted IM event: {event_type} for message {message_id}")
|
|
251
|
+
|
|
252
|
+
def _determine_event_type(self, message_data: Dict[str, Any]) -> Optional[str]:
|
|
253
|
+
"""
|
|
254
|
+
Determine the event type for a message.
|
|
255
|
+
|
|
256
|
+
Returns:
|
|
257
|
+
Event type string or None if no event should be emitted
|
|
258
|
+
"""
|
|
259
|
+
status = message_data.get("status", "received")
|
|
260
|
+
|
|
261
|
+
# Map status to event type
|
|
262
|
+
status_map = {
|
|
263
|
+
"received": "message_received",
|
|
264
|
+
"routing": "message_received",
|
|
265
|
+
"agent_processing": "agent_trigger",
|
|
266
|
+
"replied": "message_replied",
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
event_type = status_map.get(status)
|
|
270
|
+
|
|
271
|
+
# Check for agent trigger conditions
|
|
272
|
+
if event_type == "message_received" and self._should_trigger_agent(message_data):
|
|
273
|
+
event_type = "agent_trigger"
|
|
274
|
+
|
|
275
|
+
return event_type
|
|
276
|
+
|
|
277
|
+
def _should_trigger_agent(self, message_data: Dict[str, Any]) -> bool:
|
|
278
|
+
"""
|
|
279
|
+
Determine if this message should trigger an Agent.
|
|
280
|
+
|
|
281
|
+
Override this method for custom trigger logic.
|
|
282
|
+
"""
|
|
283
|
+
content = message_data.get("content", {})
|
|
284
|
+
text = content.get("text", "")
|
|
285
|
+
mentions = message_data.get("mentions", [])
|
|
286
|
+
mention_all = message_data.get("mention_all", False)
|
|
287
|
+
|
|
288
|
+
# Trigger if @mentioned
|
|
289
|
+
if mentions or mention_all:
|
|
290
|
+
return True
|
|
291
|
+
|
|
292
|
+
# Trigger on specific keywords
|
|
293
|
+
trigger_keywords = [
|
|
294
|
+
"@monoco", "@agent", "@bot",
|
|
295
|
+
"#task", "#issue", "#help",
|
|
296
|
+
]
|
|
297
|
+
|
|
298
|
+
text_lower = text.lower() if text else ""
|
|
299
|
+
for keyword in trigger_keywords:
|
|
300
|
+
if keyword in text_lower:
|
|
301
|
+
return True
|
|
302
|
+
|
|
303
|
+
return False
|
|
304
|
+
|
|
305
|
+
def _get_content_preview(self, message_data: Dict[str, Any], max_length: int = 50) -> str:
|
|
306
|
+
"""Get a preview of the message content."""
|
|
307
|
+
content = message_data.get("content", {})
|
|
308
|
+
text = content.get("text", "")
|
|
309
|
+
|
|
310
|
+
if not text:
|
|
311
|
+
return "[No text content]"
|
|
312
|
+
|
|
313
|
+
if len(text) > max_length:
|
|
314
|
+
return text[:max_length] + "..."
|
|
315
|
+
|
|
316
|
+
return text
|
|
317
|
+
|
|
318
|
+
def get_stats(self) -> Dict[str, Any]:
|
|
319
|
+
"""Get watcher statistics."""
|
|
320
|
+
stats = super().get_stats()
|
|
321
|
+
stats.update({
|
|
322
|
+
"processed_messages": len(self._processed_messages),
|
|
323
|
+
"monitored_files": len(self._file_states),
|
|
324
|
+
"trigger_on_mention": self.trigger_on_mention,
|
|
325
|
+
})
|
|
326
|
+
return stats
|
|
327
|
+
|
|
328
|
+
def clear_processed_cache(self) -> None:
|
|
329
|
+
"""Clear the processed message cache."""
|
|
330
|
+
self._processed_messages.clear()
|
|
331
|
+
logger.debug("Cleared IM message processed cache")
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
class IMInboundWatcher(IMWatcher):
|
|
335
|
+
"""
|
|
336
|
+
Specialized watcher for inbound IM messages.
|
|
337
|
+
|
|
338
|
+
Watches for messages that need Agent attention and emits
|
|
339
|
+
IM_AGENT_TRIGGER events.
|
|
340
|
+
"""
|
|
341
|
+
|
|
342
|
+
def __init__(
|
|
343
|
+
self,
|
|
344
|
+
config: WatchConfig,
|
|
345
|
+
event_bus: Optional[EventBus] = None,
|
|
346
|
+
name: str = "IMInboundWatcher",
|
|
347
|
+
):
|
|
348
|
+
super().__init__(config, event_bus, name, trigger_on_mention=True)
|
|
349
|
+
|
|
350
|
+
def _should_trigger_agent(self, message_data: Dict[str, Any]) -> bool:
|
|
351
|
+
"""Always trigger for inbound messages that need processing."""
|
|
352
|
+
# Only trigger for messages in 'received' or 'routing' status
|
|
353
|
+
status = message_data.get("status", "")
|
|
354
|
+
if status not in ("received", "routing"):
|
|
355
|
+
return False
|
|
356
|
+
|
|
357
|
+
# Check if sender is not an agent/bot
|
|
358
|
+
sender = message_data.get("sender", {})
|
|
359
|
+
sender_type = sender.get("participant_type", "user")
|
|
360
|
+
if sender_type in ("agent", "bot"):
|
|
361
|
+
return False
|
|
362
|
+
|
|
363
|
+
# Check for mentions
|
|
364
|
+
mentions = message_data.get("mentions", [])
|
|
365
|
+
mention_all = message_data.get("mention_all", False)
|
|
366
|
+
|
|
367
|
+
if mentions or mention_all:
|
|
368
|
+
return True
|
|
369
|
+
|
|
370
|
+
# Check for trigger keywords
|
|
371
|
+
content = message_data.get("content", {})
|
|
372
|
+
text = content.get("text", "")
|
|
373
|
+
|
|
374
|
+
if text:
|
|
375
|
+
text_lower = text.lower()
|
|
376
|
+
trigger_keywords = [
|
|
377
|
+
"@monoco", "@agent", "@bot",
|
|
378
|
+
"#task", "#issue", "#help",
|
|
379
|
+
"请帮忙", "help me", " assistance",
|
|
380
|
+
]
|
|
381
|
+
for keyword in trigger_keywords:
|
|
382
|
+
if keyword in text_lower:
|
|
383
|
+
return True
|
|
384
|
+
|
|
385
|
+
return False
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
class IMWebhookWatcher(PollingWatcher):
|
|
389
|
+
"""
|
|
390
|
+
Watcher for IM webhook configuration changes.
|
|
391
|
+
|
|
392
|
+
Monitors the .monoco/im/webhooks/ directory for:
|
|
393
|
+
- New webhook configurations
|
|
394
|
+
- Webhook configuration updates
|
|
395
|
+
"""
|
|
396
|
+
|
|
397
|
+
def __init__(
|
|
398
|
+
self,
|
|
399
|
+
config: WatchConfig,
|
|
400
|
+
event_bus: Optional[EventBus] = None,
|
|
401
|
+
name: str = "IMWebhookWatcher",
|
|
402
|
+
):
|
|
403
|
+
super().__init__(config, event_bus, name)
|
|
404
|
+
self._webhook_configs: Dict[str, Dict[str, Any]] = {}
|
|
405
|
+
|
|
406
|
+
async def _check_changes(self) -> None:
|
|
407
|
+
"""Check for webhook configuration changes."""
|
|
408
|
+
if not self.config.path.exists():
|
|
409
|
+
return
|
|
410
|
+
|
|
411
|
+
try:
|
|
412
|
+
config_files = list(self.config.path.glob("*.json"))
|
|
413
|
+
|
|
414
|
+
for file_path in config_files:
|
|
415
|
+
if not self._should_process(file_path):
|
|
416
|
+
continue
|
|
417
|
+
|
|
418
|
+
await self._process_config_file(file_path)
|
|
419
|
+
|
|
420
|
+
except Exception as e:
|
|
421
|
+
logger.error(f"Error checking webhook configs: {e}")
|
|
422
|
+
|
|
423
|
+
async def _process_config_file(self, file_path: Path) -> None:
|
|
424
|
+
"""Process a webhook config file."""
|
|
425
|
+
try:
|
|
426
|
+
stat = file_path.stat()
|
|
427
|
+
mtime = stat.st_mtime
|
|
428
|
+
|
|
429
|
+
# Check if file has been modified
|
|
430
|
+
if file_path.name in self._webhook_configs:
|
|
431
|
+
last_mtime = self._webhook_configs[file_path.name].get("mtime", 0)
|
|
432
|
+
if mtime <= last_mtime:
|
|
433
|
+
return
|
|
434
|
+
|
|
435
|
+
# Read and validate config
|
|
436
|
+
config_data = json.loads(file_path.read_text(encoding="utf-8"))
|
|
437
|
+
|
|
438
|
+
# Store config state
|
|
439
|
+
self._webhook_configs[file_path.name] = {
|
|
440
|
+
"mtime": mtime,
|
|
441
|
+
"config": config_data,
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
# Emit event
|
|
445
|
+
event = FileEvent(
|
|
446
|
+
path=file_path,
|
|
447
|
+
change_type=ChangeType.MODIFIED,
|
|
448
|
+
watcher_name=self.name,
|
|
449
|
+
metadata={
|
|
450
|
+
"platform": config_data.get("platform"),
|
|
451
|
+
"channel_id": config_data.get("channel_id"),
|
|
452
|
+
"event_type": "webhook_config_updated",
|
|
453
|
+
},
|
|
454
|
+
)
|
|
455
|
+
await self.emit(event)
|
|
456
|
+
|
|
457
|
+
logger.info(f"Updated webhook config: {file_path.name}")
|
|
458
|
+
|
|
459
|
+
except Exception as e:
|
|
460
|
+
logger.error(f"Error processing {file_path}: {e}")
|