monoco-toolkit 0.3.9__py3-none-any.whl → 0.3.11__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/config.py +38 -4
- monoco/core/git.py +23 -0
- monoco/core/hooks/builtin/git_cleanup.py +1 -1
- 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/injection.py +63 -29
- monoco/core/integrations.py +2 -2
- monoco/core/loader.py +633 -0
- monoco/core/output.py +5 -5
- monoco/core/registry.py +34 -19
- monoco/core/resource/__init__.py +5 -0
- monoco/core/resource/finder.py +98 -0
- monoco/core/resource/manager.py +91 -0
- monoco/core/resource/models.py +35 -0
- monoco/core/skill_framework.py +292 -0
- monoco/core/skills.py +524 -385
- monoco/core/sync.py +73 -1
- monoco/core/workflow_converter.py +420 -0
- monoco/daemon/app.py +77 -1
- monoco/daemon/commands.py +10 -0
- monoco/daemon/mailroom_service.py +196 -0
- monoco/daemon/models.py +1 -0
- monoco/daemon/scheduler.py +236 -0
- monoco/daemon/services.py +185 -0
- monoco/daemon/triggers.py +55 -0
- monoco/features/agent/__init__.py +2 -2
- monoco/features/agent/adapter.py +41 -0
- monoco/features/agent/apoptosis.py +44 -0
- monoco/features/agent/cli.py +101 -144
- monoco/features/agent/config.py +35 -21
- monoco/features/agent/defaults.py +6 -49
- monoco/features/agent/engines.py +32 -6
- monoco/features/agent/manager.py +47 -6
- monoco/features/agent/models.py +2 -2
- monoco/features/agent/resources/atoms/atom-code-dev.yaml +61 -0
- monoco/features/agent/resources/atoms/atom-issue-lifecycle.yaml +73 -0
- monoco/features/agent/resources/atoms/atom-knowledge.yaml +55 -0
- monoco/features/agent/resources/atoms/atom-review.yaml +60 -0
- monoco/{core/resources/en → features/agent/resources/en/skills/monoco_atom_core}/SKILL.md +3 -1
- monoco/features/agent/resources/en/skills/monoco_workflow_agent_engineer/SKILL.md +94 -0
- monoco/features/agent/resources/en/skills/monoco_workflow_agent_manager/SKILL.md +93 -0
- monoco/features/agent/resources/en/skills/monoco_workflow_agent_planner/SKILL.md +85 -0
- monoco/features/agent/resources/en/skills/monoco_workflow_agent_reviewer/SKILL.md +114 -0
- monoco/features/agent/resources/workflows/workflow-dev.yaml +83 -0
- monoco/features/agent/resources/workflows/workflow-issue-create.yaml +72 -0
- monoco/features/agent/resources/workflows/workflow-review.yaml +94 -0
- monoco/features/agent/resources/zh/roles/monoco_role_engineer.yaml +49 -0
- monoco/features/agent/resources/zh/roles/monoco_role_manager.yaml +46 -0
- monoco/features/agent/resources/zh/roles/monoco_role_planner.yaml +46 -0
- monoco/features/agent/resources/zh/roles/monoco_role_reviewer.yaml +47 -0
- monoco/{core/resources/zh → features/agent/resources/zh/skills/monoco_atom_core}/SKILL.md +3 -1
- monoco/features/agent/resources/{skills/flow_engineer → zh/skills/monoco_workflow_agent_engineer}/SKILL.md +2 -2
- monoco/features/agent/resources/{skills/flow_manager → zh/skills/monoco_workflow_agent_manager}/SKILL.md +2 -2
- monoco/features/agent/resources/zh/skills/monoco_workflow_agent_planner/SKILL.md +259 -0
- monoco/features/agent/resources/zh/skills/monoco_workflow_agent_reviewer/SKILL.md +137 -0
- monoco/features/agent/session.py +59 -11
- monoco/features/agent/worker.py +38 -2
- 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/__init__.py +0 -0
- monoco/features/glossary/adapter.py +42 -0
- monoco/features/glossary/config.py +5 -0
- monoco/features/glossary/resources/en/AGENTS.md +29 -0
- monoco/features/glossary/resources/en/skills/monoco_atom_glossary/SKILL.md +35 -0
- monoco/features/glossary/resources/zh/AGENTS.md +29 -0
- monoco/features/glossary/resources/zh/skills/monoco_atom_glossary/SKILL.md +35 -0
- 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/{SKILL.md → skills/monoco_atom_i18n/SKILL.md} +3 -1
- monoco/features/i18n/resources/en/skills/monoco_workflow_i18n_scan/SKILL.md +105 -0
- monoco/features/i18n/resources/zh/{SKILL.md → skills/monoco_atom_i18n/SKILL.md} +3 -1
- monoco/features/i18n/resources/{skills/i18n_scan_workflow → zh/skills/monoco_workflow_i18n_scan}/SKILL.md +2 -2
- monoco/features/issue/adapter.py +19 -6
- monoco/features/issue/commands.py +281 -7
- monoco/features/issue/core.py +272 -19
- monoco/features/issue/engine/machine.py +118 -5
- monoco/features/issue/linter.py +60 -5
- monoco/features/issue/models.py +3 -2
- monoco/features/issue/resources/en/AGENTS.md +109 -0
- monoco/features/issue/resources/en/{SKILL.md → skills/monoco_atom_issue/SKILL.md} +3 -1
- monoco/features/issue/resources/en/skills/monoco_workflow_issue_creation/SKILL.md +167 -0
- monoco/features/issue/resources/en/skills/monoco_workflow_issue_development/SKILL.md +224 -0
- monoco/features/issue/resources/en/skills/monoco_workflow_issue_management/SKILL.md +159 -0
- monoco/features/issue/resources/en/skills/monoco_workflow_issue_refinement/SKILL.md +203 -0
- 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/{SKILL.md → skills/monoco_atom_issue_lifecycle/SKILL.md} +3 -1
- monoco/features/issue/resources/zh/skills/monoco_workflow_issue_creation/SKILL.md +167 -0
- monoco/features/issue/resources/zh/skills/monoco_workflow_issue_development/SKILL.md +224 -0
- monoco/features/issue/resources/{skills/issue_lifecycle_workflow → zh/skills/monoco_workflow_issue_management}/SKILL.md +2 -2
- monoco/features/issue/resources/zh/skills/monoco_workflow_issue_refinement/SKILL.md +203 -0
- 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_atom_memo/SKILL.md +77 -0
- monoco/features/memo/resources/en/skills/monoco_workflow_note_processing/SKILL.md +140 -0
- monoco/features/memo/resources/zh/{SKILL.md → skills/monoco_atom_memo/SKILL.md} +3 -1
- monoco/features/memo/resources/{skills/note_processing_workflow → zh/skills/monoco_workflow_note_processing}/SKILL.md +2 -2
- monoco/features/spike/adapter.py +18 -5
- monoco/features/spike/resources/en/{SKILL.md → skills/monoco_atom_spike/SKILL.md} +3 -1
- monoco/features/spike/resources/en/skills/monoco_workflow_research/SKILL.md +121 -0
- monoco/features/spike/resources/zh/{SKILL.md → skills/monoco_atom_spike/SKILL.md} +3 -1
- monoco/features/spike/resources/{skills/research_workflow → zh/skills/monoco_workflow_research}/SKILL.md +2 -2
- monoco/main.py +38 -1
- monoco_toolkit-0.3.11.dist-info/METADATA +130 -0
- monoco_toolkit-0.3.11.dist-info/RECORD +181 -0
- monoco/features/agent/reliability.py +0 -106
- monoco/features/agent/resources/skills/flow_reviewer/SKILL.md +0 -114
- monoco_toolkit-0.3.9.dist-info/METADATA +0 -127
- monoco_toolkit-0.3.9.dist-info/RECORD +0 -115
- /monoco/{core → features/agent}/resources/en/AGENTS.md +0 -0
- /monoco/{core → features/agent}/resources/zh/AGENTS.md +0 -0
- {monoco_toolkit-0.3.9.dist-info → monoco_toolkit-0.3.11.dist-info}/WHEEL +0 -0
- {monoco_toolkit-0.3.9.dist-info → monoco_toolkit-0.3.11.dist-info}/entry_points.txt +0 -0
- {monoco_toolkit-0.3.9.dist-info → monoco_toolkit-0.3.11.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Conversion Worker for Monoco Mailroom.
|
|
3
|
+
|
|
4
|
+
Handles document conversion tasks using discovered tools.
|
|
5
|
+
Supports concurrent processing with asyncio.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
import logging
|
|
12
|
+
import shutil
|
|
13
|
+
import subprocess
|
|
14
|
+
import tempfile
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
|
+
from datetime import datetime, timezone
|
|
17
|
+
from enum import Enum
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Optional, Callable, Any
|
|
20
|
+
|
|
21
|
+
from .discovery import EnvironmentDiscovery, ToolCapability, ConversionTool
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ConversionStatus(str, Enum):
|
|
27
|
+
"""Status of a conversion task."""
|
|
28
|
+
PENDING = "pending"
|
|
29
|
+
PROCESSING = "processing"
|
|
30
|
+
SUCCESS = "success"
|
|
31
|
+
FAILED = "failed"
|
|
32
|
+
CANCELLED = "cancelled"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class ConversionTask:
|
|
37
|
+
"""Represents a document conversion task."""
|
|
38
|
+
task_id: str
|
|
39
|
+
source_path: Path
|
|
40
|
+
target_format: str
|
|
41
|
+
output_dir: Path
|
|
42
|
+
options: dict[str, Any] = field(default_factory=dict)
|
|
43
|
+
created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def source_extension(self) -> str:
|
|
47
|
+
"""Get the source file extension."""
|
|
48
|
+
return self.source_path.suffix.lower()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass
|
|
52
|
+
class ConversionResult:
|
|
53
|
+
"""Result of a conversion operation."""
|
|
54
|
+
task_id: str
|
|
55
|
+
status: ConversionStatus
|
|
56
|
+
output_path: Optional[Path] = None
|
|
57
|
+
error_message: Optional[str] = None
|
|
58
|
+
processing_time_ms: float = 0.0
|
|
59
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class ConversionWorker:
|
|
63
|
+
"""
|
|
64
|
+
Worker for processing document conversion tasks.
|
|
65
|
+
|
|
66
|
+
Features:
|
|
67
|
+
- Async processing with semaphore-controlled concurrency
|
|
68
|
+
- Tool selection based on file type and capability
|
|
69
|
+
- Automatic cleanup of temporary files
|
|
70
|
+
- Progress callbacks
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
# File extension to required capability mapping
|
|
74
|
+
EXTENSION_CAPABILITIES = {
|
|
75
|
+
".docx": ToolCapability.DOCX_TO_MD,
|
|
76
|
+
".doc": ToolCapability.DOCX_TO_TEXT,
|
|
77
|
+
".odt": ToolCapability.ODT_TO_TEXT,
|
|
78
|
+
".pdf": ToolCapability.PDF_TO_TEXT,
|
|
79
|
+
".xlsx": ToolCapability.XLSX_TO_CSV,
|
|
80
|
+
".xls": ToolCapability.XLSX_TO_CSV,
|
|
81
|
+
".pptx": ToolCapability.PPTX_TO_TEXT,
|
|
82
|
+
".ppt": ToolCapability.PPTX_TO_TEXT,
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
def __init__(
|
|
86
|
+
self,
|
|
87
|
+
discovery: Optional[EnvironmentDiscovery] = None,
|
|
88
|
+
max_concurrent: int = 4,
|
|
89
|
+
timeout_seconds: float = 120.0,
|
|
90
|
+
):
|
|
91
|
+
"""
|
|
92
|
+
Initialize the conversion worker.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
discovery: EnvironmentDiscovery instance (creates new if None)
|
|
96
|
+
max_concurrent: Maximum concurrent conversion tasks
|
|
97
|
+
timeout_seconds: Timeout for individual conversions
|
|
98
|
+
"""
|
|
99
|
+
self.discovery = discovery or EnvironmentDiscovery()
|
|
100
|
+
self.discovery.discover()
|
|
101
|
+
|
|
102
|
+
self.max_concurrent = max_concurrent
|
|
103
|
+
self.timeout_seconds = timeout_seconds
|
|
104
|
+
self._semaphore = asyncio.Semaphore(max_concurrent)
|
|
105
|
+
self._active_tasks: dict[str, asyncio.Task] = {}
|
|
106
|
+
|
|
107
|
+
# Callbacks
|
|
108
|
+
self._on_progress: Optional[Callable[[str, ConversionStatus, float], None]] = None
|
|
109
|
+
self._on_complete: Optional[Callable[[ConversionResult], None]] = None
|
|
110
|
+
|
|
111
|
+
def set_callbacks(
|
|
112
|
+
self,
|
|
113
|
+
on_progress: Optional[Callable[[str, ConversionStatus, float], None]] = None,
|
|
114
|
+
on_complete: Optional[Callable[[ConversionResult], None]] = None,
|
|
115
|
+
) -> None:
|
|
116
|
+
"""Set progress and completion callbacks."""
|
|
117
|
+
self._on_progress = on_progress
|
|
118
|
+
self._on_complete = on_complete
|
|
119
|
+
|
|
120
|
+
def _notify_progress(self, task_id: str, status: ConversionStatus, progress: float) -> None:
|
|
121
|
+
"""Notify progress callback."""
|
|
122
|
+
if self._on_progress:
|
|
123
|
+
try:
|
|
124
|
+
self._on_progress(task_id, status, progress)
|
|
125
|
+
except Exception:
|
|
126
|
+
pass
|
|
127
|
+
|
|
128
|
+
def _notify_complete(self, result: ConversionResult) -> None:
|
|
129
|
+
"""Notify completion callback."""
|
|
130
|
+
if self._on_complete:
|
|
131
|
+
try:
|
|
132
|
+
self._on_complete(result)
|
|
133
|
+
except Exception:
|
|
134
|
+
pass
|
|
135
|
+
|
|
136
|
+
def can_convert(self, file_path: Path) -> bool:
|
|
137
|
+
"""
|
|
138
|
+
Check if a file can be converted.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
file_path: Path to the file to check
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
True if conversion is possible
|
|
145
|
+
"""
|
|
146
|
+
ext = file_path.suffix.lower()
|
|
147
|
+
if ext not in self.EXTENSION_CAPABILITIES:
|
|
148
|
+
return False
|
|
149
|
+
|
|
150
|
+
capability = self.EXTENSION_CAPABILITIES[ext]
|
|
151
|
+
return self.discovery.has_capability(capability)
|
|
152
|
+
|
|
153
|
+
def get_supported_extensions(self) -> list[str]:
|
|
154
|
+
"""Get list of supported file extensions."""
|
|
155
|
+
supported = []
|
|
156
|
+
for ext, capability in self.EXTENSION_CAPABILITIES.items():
|
|
157
|
+
if self.discovery.has_capability(capability):
|
|
158
|
+
supported.append(ext)
|
|
159
|
+
return supported
|
|
160
|
+
|
|
161
|
+
async def submit(self, task: ConversionTask) -> ConversionResult:
|
|
162
|
+
"""
|
|
163
|
+
Submit a conversion task for processing.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
task: The conversion task to process
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
ConversionResult with status and output details
|
|
170
|
+
"""
|
|
171
|
+
async with self._semaphore:
|
|
172
|
+
return await self._process_task(task)
|
|
173
|
+
|
|
174
|
+
async def submit_batch(
|
|
175
|
+
self,
|
|
176
|
+
tasks: list[ConversionTask],
|
|
177
|
+
) -> list[ConversionResult]:
|
|
178
|
+
"""
|
|
179
|
+
Submit multiple tasks for concurrent processing.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
tasks: List of conversion tasks
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
List of ConversionResults (order may vary)
|
|
186
|
+
"""
|
|
187
|
+
coroutines = [self.submit(task) for task in tasks]
|
|
188
|
+
return await asyncio.gather(*coroutines, return_exceptions=True)
|
|
189
|
+
|
|
190
|
+
async def _process_task(self, task: ConversionTask) -> ConversionResult:
|
|
191
|
+
"""Process a single conversion task."""
|
|
192
|
+
import time
|
|
193
|
+
start_time = time.time()
|
|
194
|
+
|
|
195
|
+
self._notify_progress(task.task_id, ConversionStatus.PROCESSING, 0.0)
|
|
196
|
+
|
|
197
|
+
try:
|
|
198
|
+
# Validate source file
|
|
199
|
+
if not task.source_path.exists():
|
|
200
|
+
return self._create_error_result(
|
|
201
|
+
task, "Source file does not exist"
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
# Get required capability
|
|
205
|
+
ext = task.source_extension
|
|
206
|
+
if ext not in self.EXTENSION_CAPABILITIES:
|
|
207
|
+
return self._create_error_result(
|
|
208
|
+
task, f"Unsupported file extension: {ext}"
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
capability = self.EXTENSION_CAPABILITIES[ext]
|
|
212
|
+
tool = self.discovery.get_best_tool(capability)
|
|
213
|
+
|
|
214
|
+
if not tool:
|
|
215
|
+
return self._create_error_result(
|
|
216
|
+
task, f"No tool available for {capability.value}"
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
# Perform conversion
|
|
220
|
+
result = await self._convert_with_tool(task, tool, capability)
|
|
221
|
+
|
|
222
|
+
processing_time = (time.time() - start_time) * 1000
|
|
223
|
+
result.processing_time_ms = processing_time
|
|
224
|
+
|
|
225
|
+
self._notify_complete(result)
|
|
226
|
+
return result
|
|
227
|
+
|
|
228
|
+
except asyncio.TimeoutError:
|
|
229
|
+
result = self._create_error_result(task, "Conversion timeout")
|
|
230
|
+
self._notify_complete(result)
|
|
231
|
+
return result
|
|
232
|
+
except Exception as e:
|
|
233
|
+
logger.exception(f"Conversion failed for task {task.task_id}")
|
|
234
|
+
result = self._create_error_result(task, str(e))
|
|
235
|
+
self._notify_complete(result)
|
|
236
|
+
return result
|
|
237
|
+
|
|
238
|
+
def _create_error_result(self, task: ConversionTask, message: str) -> ConversionResult:
|
|
239
|
+
"""Create a failed conversion result."""
|
|
240
|
+
return ConversionResult(
|
|
241
|
+
task_id=task.task_id,
|
|
242
|
+
status=ConversionStatus.FAILED,
|
|
243
|
+
error_message=message,
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
async def _convert_with_tool(
|
|
247
|
+
self,
|
|
248
|
+
task: ConversionTask,
|
|
249
|
+
tool: ConversionTool,
|
|
250
|
+
capability: ToolCapability,
|
|
251
|
+
) -> ConversionResult:
|
|
252
|
+
"""Convert using the specified tool."""
|
|
253
|
+
|
|
254
|
+
if tool.tool_type.value == "libreoffice":
|
|
255
|
+
return await self._convert_with_libreoffice(task, tool)
|
|
256
|
+
elif tool.tool_type.value == "pandoc":
|
|
257
|
+
return await self._convert_with_pandoc(task, tool)
|
|
258
|
+
elif tool.tool_type.value in ("pdf2text", "pdftohtml"):
|
|
259
|
+
return await self._convert_with_pdf_tool(task, tool)
|
|
260
|
+
else:
|
|
261
|
+
return self._create_error_result(task, f"Unknown tool type: {tool.tool_type}")
|
|
262
|
+
|
|
263
|
+
async def _convert_with_libreoffice(
|
|
264
|
+
self,
|
|
265
|
+
task: ConversionTask,
|
|
266
|
+
tool: ConversionTool,
|
|
267
|
+
) -> ConversionResult:
|
|
268
|
+
"""Convert using LibreOffice."""
|
|
269
|
+
# Create temp directory for conversion
|
|
270
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
271
|
+
tmp_path = Path(tmpdir)
|
|
272
|
+
|
|
273
|
+
# Build LibreOffice command
|
|
274
|
+
cmd = [
|
|
275
|
+
str(tool.executable_path),
|
|
276
|
+
"--headless",
|
|
277
|
+
"--convert-to", "txt:Text",
|
|
278
|
+
"--outdir", str(tmp_path),
|
|
279
|
+
str(task.source_path),
|
|
280
|
+
]
|
|
281
|
+
|
|
282
|
+
# Run conversion
|
|
283
|
+
try:
|
|
284
|
+
process = await asyncio.create_subprocess_exec(
|
|
285
|
+
*cmd,
|
|
286
|
+
stdout=asyncio.subprocess.PIPE,
|
|
287
|
+
stderr=asyncio.subprocess.PIPE,
|
|
288
|
+
)
|
|
289
|
+
stdout, stderr = await asyncio.wait_for(
|
|
290
|
+
process.communicate(),
|
|
291
|
+
timeout=self.timeout_seconds,
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
if process.returncode != 0:
|
|
295
|
+
error_msg = stderr.decode() if stderr else "Unknown error"
|
|
296
|
+
return self._create_error_result(task, f"LibreOffice error: {error_msg}")
|
|
297
|
+
|
|
298
|
+
# Find output file
|
|
299
|
+
base_name = task.source_path.stem
|
|
300
|
+
output_file = tmp_path / f"{base_name}.txt"
|
|
301
|
+
|
|
302
|
+
if not output_file.exists():
|
|
303
|
+
return self._create_error_result(task, "Output file not created")
|
|
304
|
+
|
|
305
|
+
# Move to final destination
|
|
306
|
+
task.output_dir.mkdir(parents=True, exist_ok=True)
|
|
307
|
+
final_output = task.output_dir / f"{base_name}.txt"
|
|
308
|
+
shutil.move(str(output_file), str(final_output))
|
|
309
|
+
|
|
310
|
+
return ConversionResult(
|
|
311
|
+
task_id=task.task_id,
|
|
312
|
+
status=ConversionStatus.SUCCESS,
|
|
313
|
+
output_path=final_output,
|
|
314
|
+
metadata={
|
|
315
|
+
"tool": tool.name,
|
|
316
|
+
"tool_version": tool.version,
|
|
317
|
+
},
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
except asyncio.TimeoutError:
|
|
321
|
+
return self._create_error_result(task, "LibreOffice conversion timeout")
|
|
322
|
+
|
|
323
|
+
async def _convert_with_pandoc(
|
|
324
|
+
self,
|
|
325
|
+
task: ConversionTask,
|
|
326
|
+
tool: ConversionTool,
|
|
327
|
+
) -> ConversionResult:
|
|
328
|
+
"""Convert using Pandoc."""
|
|
329
|
+
task.output_dir.mkdir(parents=True, exist_ok=True)
|
|
330
|
+
|
|
331
|
+
base_name = task.source_path.stem
|
|
332
|
+
output_file = task.output_dir / f"{base_name}.md"
|
|
333
|
+
|
|
334
|
+
cmd = [
|
|
335
|
+
str(tool.executable_path),
|
|
336
|
+
str(task.source_path),
|
|
337
|
+
"-o", str(output_file),
|
|
338
|
+
"-t", "markdown",
|
|
339
|
+
]
|
|
340
|
+
|
|
341
|
+
try:
|
|
342
|
+
process = await asyncio.create_subprocess_exec(
|
|
343
|
+
*cmd,
|
|
344
|
+
stdout=asyncio.subprocess.PIPE,
|
|
345
|
+
stderr=asyncio.subprocess.PIPE,
|
|
346
|
+
)
|
|
347
|
+
stdout, stderr = await asyncio.wait_for(
|
|
348
|
+
process.communicate(),
|
|
349
|
+
timeout=self.timeout_seconds,
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
if process.returncode != 0:
|
|
353
|
+
error_msg = stderr.decode() if stderr else "Unknown error"
|
|
354
|
+
return self._create_error_result(task, f"Pandoc error: {error_msg}")
|
|
355
|
+
|
|
356
|
+
return ConversionResult(
|
|
357
|
+
task_id=task.task_id,
|
|
358
|
+
status=ConversionStatus.SUCCESS,
|
|
359
|
+
output_path=output_file,
|
|
360
|
+
metadata={
|
|
361
|
+
"tool": tool.name,
|
|
362
|
+
"tool_version": tool.version,
|
|
363
|
+
},
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
except asyncio.TimeoutError:
|
|
367
|
+
return self._create_error_result(task, "Pandoc conversion timeout")
|
|
368
|
+
|
|
369
|
+
async def _convert_with_pdf_tool(
|
|
370
|
+
self,
|
|
371
|
+
task: ConversionTask,
|
|
372
|
+
tool: ConversionTool,
|
|
373
|
+
) -> ConversionResult:
|
|
374
|
+
"""Convert PDF using pdftotext or similar."""
|
|
375
|
+
task.output_dir.mkdir(parents=True, exist_ok=True)
|
|
376
|
+
|
|
377
|
+
base_name = task.source_path.stem
|
|
378
|
+
output_file = task.output_dir / f"{base_name}.txt"
|
|
379
|
+
|
|
380
|
+
cmd = [
|
|
381
|
+
str(tool.executable_path),
|
|
382
|
+
str(task.source_path),
|
|
383
|
+
str(output_file),
|
|
384
|
+
]
|
|
385
|
+
|
|
386
|
+
# Add layout preservation for pdftotext
|
|
387
|
+
if "pdftotext" in tool.name:
|
|
388
|
+
cmd.insert(1, "-layout")
|
|
389
|
+
|
|
390
|
+
try:
|
|
391
|
+
process = await asyncio.create_subprocess_exec(
|
|
392
|
+
*cmd,
|
|
393
|
+
stdout=asyncio.subprocess.PIPE,
|
|
394
|
+
stderr=asyncio.subprocess.PIPE,
|
|
395
|
+
)
|
|
396
|
+
stdout, stderr = await asyncio.wait_for(
|
|
397
|
+
process.communicate(),
|
|
398
|
+
timeout=self.timeout_seconds,
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
if process.returncode != 0:
|
|
402
|
+
error_msg = stderr.decode() if stderr else "Unknown error"
|
|
403
|
+
return self._create_error_result(task, f"PDF tool error: {error_msg}")
|
|
404
|
+
|
|
405
|
+
return ConversionResult(
|
|
406
|
+
task_id=task.task_id,
|
|
407
|
+
status=ConversionStatus.SUCCESS,
|
|
408
|
+
output_path=output_file,
|
|
409
|
+
metadata={
|
|
410
|
+
"tool": tool.name,
|
|
411
|
+
"tool_version": tool.version,
|
|
412
|
+
},
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
except asyncio.TimeoutError:
|
|
416
|
+
return self._create_error_result(task, "PDF conversion timeout")
|
|
417
|
+
|
|
418
|
+
async def cancel_task(self, task_id: str) -> bool:
|
|
419
|
+
"""
|
|
420
|
+
Cancel an active task.
|
|
421
|
+
|
|
422
|
+
Args:
|
|
423
|
+
task_id: ID of the task to cancel
|
|
424
|
+
|
|
425
|
+
Returns:
|
|
426
|
+
True if cancelled, False if not found
|
|
427
|
+
"""
|
|
428
|
+
if task_id in self._active_tasks:
|
|
429
|
+
task = self._active_tasks[task_id]
|
|
430
|
+
task.cancel()
|
|
431
|
+
return True
|
|
432
|
+
return False
|
|
433
|
+
|
|
434
|
+
def get_active_count(self) -> int:
|
|
435
|
+
"""Get number of currently active tasks."""
|
|
436
|
+
return len(self._active_tasks)
|
monoco/core/injection.py
CHANGED
|
@@ -10,6 +10,8 @@ class PromptInjector:
|
|
|
10
10
|
"""
|
|
11
11
|
|
|
12
12
|
MANAGED_HEADER = "## Monoco Toolkit"
|
|
13
|
+
MANAGED_START = "<!-- MONOCO_GENERATED_START -->"
|
|
14
|
+
MANAGED_END = "<!-- MONOCO_GENERATED_END -->"
|
|
13
15
|
|
|
14
16
|
def __init__(self, target_file: Path):
|
|
15
17
|
self.target_file = target_file
|
|
@@ -52,19 +54,40 @@ class PromptInjector:
|
|
|
52
54
|
# Sanitize content: remove leading header if it matches the title
|
|
53
55
|
clean_content = content.strip()
|
|
54
56
|
# Regex to match optional leading hash header matching the title (case insensitive)
|
|
55
|
-
# e.g. "### Issue Management" or "# Issue Management"
|
|
56
57
|
pattern = r"^(#+\s*)" + re.escape(title) + r"\s*\n"
|
|
57
58
|
match = re.match(pattern, clean_content, re.IGNORECASE)
|
|
58
59
|
|
|
59
60
|
if match:
|
|
60
61
|
clean_content = clean_content[match.end() :].strip()
|
|
61
|
-
|
|
62
|
-
|
|
62
|
+
|
|
63
|
+
# Demote headers in content to be below ### (so start at ####)
|
|
64
|
+
# We assume the content headers start at # or ##.
|
|
65
|
+
# We map # -> ####, ## -> #####, etc. (+3 offset)
|
|
66
|
+
demoted_content = []
|
|
67
|
+
for line in clean_content.splitlines():
|
|
68
|
+
if line.lstrip().startswith("#"):
|
|
69
|
+
demoted_content.append("###" + line)
|
|
70
|
+
else:
|
|
71
|
+
demoted_content.append(line)
|
|
72
|
+
|
|
73
|
+
managed_block.append("\n".join(demoted_content))
|
|
63
74
|
managed_block.append("") # Blank line after section
|
|
64
75
|
|
|
65
76
|
managed_block_str = "\n".join(managed_block).strip() + "\n"
|
|
77
|
+
managed_block_str = f"{self.MANAGED_START}\n{managed_block_str}\n{self.MANAGED_END}\n"
|
|
66
78
|
|
|
67
79
|
# 2. Find and replace/append in the original content
|
|
80
|
+
# Check for delimiters first
|
|
81
|
+
if self.MANAGED_START in original and self.MANAGED_END in original:
|
|
82
|
+
try:
|
|
83
|
+
pre = original.split(self.MANAGED_START)[0]
|
|
84
|
+
post = original.split(self.MANAGED_END)[1]
|
|
85
|
+
# Reconstruct
|
|
86
|
+
return pre + managed_block_str.strip() + post
|
|
87
|
+
except IndexError:
|
|
88
|
+
# Fallback to header detection if delimiters malformed
|
|
89
|
+
pass
|
|
90
|
+
|
|
68
91
|
lines = original.splitlines()
|
|
69
92
|
start_idx = -1
|
|
70
93
|
end_idx = -1
|
|
@@ -74,31 +97,29 @@ class PromptInjector:
|
|
|
74
97
|
if line.strip() == self.MANAGED_HEADER:
|
|
75
98
|
start_idx = i
|
|
76
99
|
break
|
|
100
|
+
|
|
101
|
+
if start_idx == -1:
|
|
102
|
+
# Check if we have delimiters even if header is missing/changed?
|
|
103
|
+
# Handled above.
|
|
104
|
+
pass
|
|
77
105
|
|
|
78
106
|
if start_idx == -1:
|
|
79
107
|
# Block not found, append to end
|
|
80
108
|
if original and not original.endswith("\n"):
|
|
81
|
-
return original + "\n\n" + managed_block_str
|
|
109
|
+
return original + "\n\n" + managed_block_str.strip()
|
|
82
110
|
elif original:
|
|
83
|
-
return original + "\n" + managed_block_str
|
|
111
|
+
return original + "\n" + managed_block_str.strip()
|
|
84
112
|
else:
|
|
85
|
-
return managed_block_str
|
|
86
|
-
|
|
87
|
-
# Find end: Look for next header of level 1 (assuming Managed Header is H1)
|
|
88
|
-
# Or EOF
|
|
89
|
-
# Note: If MANAGED_HEADER is "# ...", we look for next "# ..."
|
|
90
|
-
# But allow "## ..." as children.
|
|
113
|
+
return managed_block_str.strip() + "\n"
|
|
91
114
|
|
|
115
|
+
# Find end: Look for next header of level 1 or 2 (siblings or parents)
|
|
92
116
|
header_level_match = re.match(r"^(#+)\s", self.MANAGED_HEADER)
|
|
93
|
-
header_level_prefix = header_level_match.group(1) if header_level_match else "
|
|
117
|
+
header_level_prefix = header_level_match.group(1) if header_level_match else "##"
|
|
94
118
|
|
|
95
119
|
for i in range(start_idx + 1, len(lines)):
|
|
96
120
|
line = lines[i]
|
|
97
121
|
# Check if this line is a header of the same level or higher (fewer #s)
|
|
98
|
-
# e.g. if Managed is "###", then "#" and "##" are higher/parents, "###" is sibling.
|
|
99
|
-
# We treat siblings as end of block too.
|
|
100
122
|
if line.startswith("#"):
|
|
101
|
-
# Match regex to get level
|
|
102
123
|
match = re.match(r"^(#+)\s", line)
|
|
103
124
|
if match:
|
|
104
125
|
level = match.group(1)
|
|
@@ -146,26 +167,39 @@ class PromptInjector:
|
|
|
146
167
|
|
|
147
168
|
# Find start
|
|
148
169
|
for i, line in enumerate(lines):
|
|
149
|
-
if
|
|
170
|
+
if self.MANAGED_START in line:
|
|
150
171
|
start_idx = i
|
|
172
|
+
# Look for end from here
|
|
173
|
+
for j in range(i, len(lines)):
|
|
174
|
+
if self.MANAGED_END in lines[j]:
|
|
175
|
+
end_idx = j + 1 # Include the end line
|
|
176
|
+
break
|
|
151
177
|
break
|
|
178
|
+
|
|
179
|
+
if start_idx == -1:
|
|
180
|
+
# Fallback to header logic
|
|
181
|
+
for i, line in enumerate(lines):
|
|
182
|
+
if line.strip() == self.MANAGED_HEADER:
|
|
183
|
+
start_idx = i
|
|
184
|
+
break
|
|
152
185
|
|
|
153
186
|
if start_idx == -1:
|
|
154
187
|
return False
|
|
155
188
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
189
|
+
if end_idx == -1:
|
|
190
|
+
# Find end: exact logic as in _merge_content
|
|
191
|
+
header_level_match = re.match(r"^(#+)\s", self.MANAGED_HEADER)
|
|
192
|
+
header_level_prefix = header_level_match.group(1) if header_level_match else "##"
|
|
193
|
+
|
|
194
|
+
for i in range(start_idx + 1, len(lines)):
|
|
195
|
+
line = lines[i]
|
|
196
|
+
if line.startswith("#"):
|
|
197
|
+
match = re.match(r"^(#+)\s", line)
|
|
198
|
+
if match:
|
|
199
|
+
level = match.group(1)
|
|
200
|
+
if len(level) <= len(header_level_prefix):
|
|
201
|
+
end_idx = i
|
|
202
|
+
break
|
|
169
203
|
|
|
170
204
|
if end_idx == -1:
|
|
171
205
|
end_idx = len(lines)
|
monoco/core/integrations.py
CHANGED
|
@@ -129,8 +129,8 @@ DEFAULT_INTEGRATIONS: Dict[str, AgentIntegration] = {
|
|
|
129
129
|
"kimi": AgentIntegration(
|
|
130
130
|
key="kimi",
|
|
131
131
|
name="Kimi CLI",
|
|
132
|
-
system_prompt_file="
|
|
133
|
-
skill_root_dir=".
|
|
132
|
+
system_prompt_file="AGENTS.md",
|
|
133
|
+
skill_root_dir=".agent/skills/",
|
|
134
134
|
bin_name="kimi",
|
|
135
135
|
version_cmd="--version",
|
|
136
136
|
),
|