algomath-extract 1.0.0
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.
- package/README.md +260 -0
- package/bin/algo-extract.js +143 -0
- package/bin/algo-generate.js +102 -0
- package/bin/algo-help.js +136 -0
- package/bin/algo-list.js +56 -0
- package/bin/algo-run.js +141 -0
- package/bin/algo-status.js +88 -0
- package/bin/algo-verify.js +189 -0
- package/bin/install.js +349 -0
- package/package.json +57 -0
- package/requirements.txt +20 -0
- package/src/__pycache__/intent.cpython-313.pyc +0 -0
- package/src/cli/__pycache__/commands.cpython-313.pyc +0 -0
- package/src/cli/cli_entry.py +106 -0
- package/src/cli/commands.py +339 -0
- package/src/execution/__init__.py +74 -0
- package/src/execution/__pycache__/__init__.cpython-313.pyc +0 -0
- package/src/execution/__pycache__/display.cpython-313.pyc +0 -0
- package/src/execution/__pycache__/errors.cpython-313.pyc +0 -0
- package/src/execution/__pycache__/executor.cpython-313.pyc +0 -0
- package/src/execution/__pycache__/sandbox.cpython-313.pyc +0 -0
- package/src/execution/display.py +261 -0
- package/src/execution/errors.py +158 -0
- package/src/execution/executor.py +253 -0
- package/src/execution/sandbox.py +333 -0
- package/src/extraction/__init__.py +102 -0
- package/src/extraction/__pycache__/__init__.cpython-313.pyc +0 -0
- package/src/extraction/__pycache__/boundaries.cpython-313.pyc +0 -0
- package/src/extraction/__pycache__/errors.cpython-313.pyc +0 -0
- package/src/extraction/__pycache__/llm_extraction.cpython-313.pyc +0 -0
- package/src/extraction/__pycache__/notation.cpython-313.pyc +0 -0
- package/src/extraction/__pycache__/parser.cpython-313.pyc +0 -0
- package/src/extraction/__pycache__/pdf_processor.cpython-313.pyc +0 -0
- package/src/extraction/__pycache__/prompts.cpython-313.pyc +0 -0
- package/src/extraction/__pycache__/review.cpython-313.pyc +0 -0
- package/src/extraction/__pycache__/schema.cpython-313.pyc +0 -0
- package/src/extraction/__pycache__/validation.cpython-313.pyc +0 -0
- package/src/extraction/boundaries.py +281 -0
- package/src/extraction/errors.py +156 -0
- package/src/extraction/llm_extraction.py +225 -0
- package/src/extraction/notation.py +240 -0
- package/src/extraction/parser.py +402 -0
- package/src/extraction/pdf_processor.py +281 -0
- package/src/extraction/prompts.py +90 -0
- package/src/extraction/review.py +298 -0
- package/src/extraction/schema.py +173 -0
- package/src/extraction/validation.py +202 -0
- package/src/generation/__init__.py +79 -0
- package/src/generation/__pycache__/__init__.cpython-313.pyc +0 -0
- package/src/generation/__pycache__/code_generator.cpython-313.pyc +0 -0
- package/src/generation/__pycache__/errors.cpython-313.pyc +0 -0
- package/src/generation/__pycache__/hybrid.cpython-313.pyc +0 -0
- package/src/generation/__pycache__/llm_generator.cpython-313.pyc +0 -0
- package/src/generation/__pycache__/persistence.cpython-313.pyc +0 -0
- package/src/generation/__pycache__/prompts.cpython-313.pyc +0 -0
- package/src/generation/__pycache__/review.cpython-313.pyc +0 -0
- package/src/generation/__pycache__/templates.cpython-313.pyc +0 -0
- package/src/generation/__pycache__/types.cpython-313.pyc +0 -0
- package/src/generation/__pycache__/validation.cpython-313.pyc +0 -0
- package/src/generation/code_generator.py +375 -0
- package/src/generation/errors.py +84 -0
- package/src/generation/hybrid.py +210 -0
- package/src/generation/llm_generator.py +223 -0
- package/src/generation/persistence.py +221 -0
- package/src/generation/prompts.py +202 -0
- package/src/generation/review.py +254 -0
- package/src/generation/templates.py +208 -0
- package/src/generation/types.py +196 -0
- package/src/generation/validation.py +278 -0
- package/src/intent.py +323 -0
- package/src/verification/__init__.py +63 -0
- package/src/verification/__pycache__/__init__.cpython-313.pyc +0 -0
- package/src/verification/__pycache__/checker.cpython-313.pyc +0 -0
- package/src/verification/__pycache__/comparison.cpython-313.pyc +0 -0
- package/src/verification/__pycache__/explainer.cpython-313.pyc +0 -0
- package/src/verification/__pycache__/static_analysis.cpython-313.pyc +0 -0
- package/src/verification/checker.py +220 -0
- package/src/verification/comparison.py +492 -0
- package/src/verification/explainer.py +414 -0
- package/src/verification/static_analysis.py +540 -0
- package/src/workflows/__init__.py +21 -0
- package/src/workflows/__pycache__/__init__.cpython-313.pyc +0 -0
- package/src/workflows/__pycache__/extract.cpython-313.pyc +0 -0
- package/src/workflows/__pycache__/generate.cpython-313.pyc +0 -0
- package/src/workflows/__pycache__/run.cpython-313.pyc +0 -0
- package/src/workflows/__pycache__/verify.cpython-313.pyc +0 -0
- package/src/workflows/extract.py +181 -0
- package/src/workflows/generate.py +155 -0
- package/src/workflows/run.py +187 -0
- package/src/workflows/verify.py +334 -0
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
"""PDF and text file processing module.
|
|
2
|
+
|
|
3
|
+
Provides functionality to extract text from PDFs and text files,
|
|
4
|
+
auto-detect file types, and handle encoding issues.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import re
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Optional, Tuple
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class PDFExtractionResult:
|
|
16
|
+
"""Result of PDF/text extraction."""
|
|
17
|
+
text: str
|
|
18
|
+
file_type: str # 'pdf', 'text', or 'unknown'
|
|
19
|
+
page_count: int
|
|
20
|
+
encoding: str
|
|
21
|
+
success: bool
|
|
22
|
+
error: Optional[str] = None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class PDFProcessor:
|
|
26
|
+
"""Process PDF and text files to extract algorithm text."""
|
|
27
|
+
|
|
28
|
+
def __init__(self):
|
|
29
|
+
self.supported_extensions = {'.pdf', '.txt', '.md', '.markdown'}
|
|
30
|
+
self.text_extensions = {'.txt', '.md', '.markdown'}
|
|
31
|
+
|
|
32
|
+
def extract_text(self, file_path: str) -> PDFExtractionResult:
|
|
33
|
+
"""
|
|
34
|
+
Extract text from file (PDF or text).
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
file_path: Path to file
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
PDFExtractionResult with extracted text
|
|
41
|
+
"""
|
|
42
|
+
path = Path(file_path)
|
|
43
|
+
|
|
44
|
+
# Check if file exists
|
|
45
|
+
if not path.exists():
|
|
46
|
+
return PDFExtractionResult(
|
|
47
|
+
text="",
|
|
48
|
+
file_type="unknown",
|
|
49
|
+
page_count=0,
|
|
50
|
+
encoding="",
|
|
51
|
+
success=False,
|
|
52
|
+
error=f"File not found: {file_path}"
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
# Check extension
|
|
56
|
+
ext = path.suffix.lower()
|
|
57
|
+
if ext not in self.supported_extensions:
|
|
58
|
+
return PDFExtractionResult(
|
|
59
|
+
text="",
|
|
60
|
+
file_type="unknown",
|
|
61
|
+
page_count=0,
|
|
62
|
+
encoding="",
|
|
63
|
+
success=False,
|
|
64
|
+
error=f"Unsupported file type: {ext}. Supported: {self.supported_extensions}"
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# Route to appropriate handler
|
|
68
|
+
if ext == '.pdf':
|
|
69
|
+
return self._extract_pdf(file_path)
|
|
70
|
+
elif ext in self.text_extensions:
|
|
71
|
+
return self._read_text_file(file_path)
|
|
72
|
+
else:
|
|
73
|
+
return PDFExtractionResult(
|
|
74
|
+
text="",
|
|
75
|
+
file_type="unknown",
|
|
76
|
+
page_count=0,
|
|
77
|
+
encoding="",
|
|
78
|
+
success=False,
|
|
79
|
+
error=f"Unknown file type: {ext}"
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
def _extract_pdf(self, file_path: str) -> PDFExtractionResult:
|
|
83
|
+
"""
|
|
84
|
+
Extract text from PDF file.
|
|
85
|
+
|
|
86
|
+
Uses pdfplumber for text-based PDFs.
|
|
87
|
+
Falls back to PyMuPDF for complex layouts.
|
|
88
|
+
"""
|
|
89
|
+
try:
|
|
90
|
+
# Try pdfplumber first (best for text-based PDFs)
|
|
91
|
+
import pdfplumber
|
|
92
|
+
|
|
93
|
+
text_parts = []
|
|
94
|
+
page_count = 0
|
|
95
|
+
|
|
96
|
+
with pdfplumber.open(file_path) as pdf:
|
|
97
|
+
page_count = len(pdf.pages)
|
|
98
|
+
|
|
99
|
+
for i, page in enumerate(pdf.pages):
|
|
100
|
+
page_text = page.extract_text()
|
|
101
|
+
if page_text:
|
|
102
|
+
text_parts.append(page_text)
|
|
103
|
+
|
|
104
|
+
full_text = '\n\n'.join(text_parts)
|
|
105
|
+
|
|
106
|
+
# Check if we got any text
|
|
107
|
+
if not full_text.strip():
|
|
108
|
+
# Try PyMuPDF as fallback for image-based or complex PDFs
|
|
109
|
+
return self._extract_pdf_with_pymupdf(file_path)
|
|
110
|
+
|
|
111
|
+
return PDFExtractionResult(
|
|
112
|
+
text=full_text,
|
|
113
|
+
file_type='pdf',
|
|
114
|
+
page_count=page_count,
|
|
115
|
+
encoding='utf-8',
|
|
116
|
+
success=True
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
except ImportError:
|
|
120
|
+
# pdfplumber not installed, try PyMuPDF
|
|
121
|
+
return self._extract_pdf_with_pymupdf(file_path)
|
|
122
|
+
except Exception as e:
|
|
123
|
+
return PDFExtractionResult(
|
|
124
|
+
text="",
|
|
125
|
+
file_type='pdf',
|
|
126
|
+
page_count=0,
|
|
127
|
+
encoding="",
|
|
128
|
+
success=False,
|
|
129
|
+
error=f"PDF extraction failed: {str(e)}"
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
def _extract_pdf_with_pymupdf(self, file_path: str) -> PDFExtractionResult:
|
|
133
|
+
"""Fallback PDF extraction using PyMuPDF (fitz)."""
|
|
134
|
+
try:
|
|
135
|
+
import fitz # PyMuPDF
|
|
136
|
+
|
|
137
|
+
text_parts = []
|
|
138
|
+
page_count = 0
|
|
139
|
+
|
|
140
|
+
with fitz.open(file_path) as doc:
|
|
141
|
+
page_count = len(doc)
|
|
142
|
+
|
|
143
|
+
for page in doc:
|
|
144
|
+
text_parts.append(page.get_text())
|
|
145
|
+
|
|
146
|
+
full_text = '\n\n'.join(text_parts)
|
|
147
|
+
|
|
148
|
+
return PDFExtractionResult(
|
|
149
|
+
text=full_text,
|
|
150
|
+
file_type='pdf',
|
|
151
|
+
page_count=page_count,
|
|
152
|
+
encoding='utf-8',
|
|
153
|
+
success=True
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
except ImportError:
|
|
157
|
+
return PDFExtractionResult(
|
|
158
|
+
text="",
|
|
159
|
+
file_type='pdf',
|
|
160
|
+
page_count=0,
|
|
161
|
+
encoding="",
|
|
162
|
+
success=False,
|
|
163
|
+
error="PDF libraries not installed. Run: pip install pdfplumber pymupdf"
|
|
164
|
+
)
|
|
165
|
+
except Exception as e:
|
|
166
|
+
return PDFExtractionResult(
|
|
167
|
+
text="",
|
|
168
|
+
file_type='pdf',
|
|
169
|
+
page_count=0,
|
|
170
|
+
encoding="",
|
|
171
|
+
success=False,
|
|
172
|
+
error=f"PDF extraction failed: {str(e)}"
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
def _read_text_file(self, file_path: str) -> PDFExtractionResult:
|
|
176
|
+
"""Read plain text file."""
|
|
177
|
+
try:
|
|
178
|
+
# Try UTF-8 first
|
|
179
|
+
with open(file_path, 'r', encoding='utf-8') as f:
|
|
180
|
+
text = f.read()
|
|
181
|
+
|
|
182
|
+
return PDFExtractionResult(
|
|
183
|
+
text=text,
|
|
184
|
+
file_type='text',
|
|
185
|
+
page_count=1,
|
|
186
|
+
encoding='utf-8',
|
|
187
|
+
success=True
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
except UnicodeDecodeError:
|
|
191
|
+
# Try other encodings
|
|
192
|
+
encodings = ['latin-1', 'cp1252', 'iso-8859-1']
|
|
193
|
+
for encoding in encodings:
|
|
194
|
+
try:
|
|
195
|
+
with open(file_path, 'r', encoding=encoding) as f:
|
|
196
|
+
text = f.read()
|
|
197
|
+
|
|
198
|
+
return PDFExtractionResult(
|
|
199
|
+
text=text,
|
|
200
|
+
file_type='text',
|
|
201
|
+
page_count=1,
|
|
202
|
+
encoding=encoding,
|
|
203
|
+
success=True
|
|
204
|
+
)
|
|
205
|
+
except UnicodeDecodeError:
|
|
206
|
+
continue
|
|
207
|
+
|
|
208
|
+
return PDFExtractionResult(
|
|
209
|
+
text="",
|
|
210
|
+
file_type='text',
|
|
211
|
+
page_count=0,
|
|
212
|
+
encoding="",
|
|
213
|
+
success=False,
|
|
214
|
+
error="Could not decode file. Try specifying encoding."
|
|
215
|
+
)
|
|
216
|
+
except Exception as e:
|
|
217
|
+
return PDFExtractionResult(
|
|
218
|
+
text="",
|
|
219
|
+
file_type='text',
|
|
220
|
+
page_count=0,
|
|
221
|
+
encoding="",
|
|
222
|
+
success=False,
|
|
223
|
+
error=f"Text file read failed: {str(e)}"
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
def is_text_based_pdf(self, file_path: str) -> bool:
|
|
227
|
+
"""
|
|
228
|
+
Check if PDF contains extractable text vs just images.
|
|
229
|
+
|
|
230
|
+
Returns True if PDF has text content.
|
|
231
|
+
"""
|
|
232
|
+
try:
|
|
233
|
+
import pdfplumber
|
|
234
|
+
|
|
235
|
+
with pdfplumber.open(file_path) as pdf:
|
|
236
|
+
for page in pdf.pages:
|
|
237
|
+
text = page.extract_text()
|
|
238
|
+
if text and text.strip():
|
|
239
|
+
return True
|
|
240
|
+
return False
|
|
241
|
+
|
|
242
|
+
except Exception:
|
|
243
|
+
return False
|
|
244
|
+
|
|
245
|
+
def extract_algorithm_section(self, text: str) -> Tuple[str, int, int]:
|
|
246
|
+
"""
|
|
247
|
+
Attempt to find algorithm section within extracted text.
|
|
248
|
+
|
|
249
|
+
Args:
|
|
250
|
+
text: Full extracted text
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
Tuple of (algorithm_text, start_index, end_index)
|
|
254
|
+
"""
|
|
255
|
+
# Common patterns that indicate algorithm sections
|
|
256
|
+
patterns = [
|
|
257
|
+
r'(?i)(?:algorithm|procedure|function)\s+\w+.*?\n.*?\n(?:end|return)',
|
|
258
|
+
r'(?i)(?:input|output):.*?\n.*?\n(?:end|return)',
|
|
259
|
+
r'(?i)step\s*\d+[:.]\s*.+?(?:\n\n|\Z)',
|
|
260
|
+
]
|
|
261
|
+
|
|
262
|
+
for pattern in patterns:
|
|
263
|
+
matches = list(re.finditer(pattern, text, re.DOTALL))
|
|
264
|
+
if matches:
|
|
265
|
+
# Return the longest match (most likely complete algorithm)
|
|
266
|
+
longest = max(matches, key=lambda m: len(m.group()))
|
|
267
|
+
return (
|
|
268
|
+
longest.group(),
|
|
269
|
+
longest.start(),
|
|
270
|
+
longest.end()
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
# If no pattern matched, return first 5000 chars (heuristic)
|
|
274
|
+
return (text[:5000], 0, min(5000, len(text)))
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
# Convenience function
|
|
278
|
+
def extract_text_from_file(file_path: str) -> PDFExtractionResult:
|
|
279
|
+
"""Extract text from PDF or text file."""
|
|
280
|
+
processor = PDFProcessor()
|
|
281
|
+
return processor.extract_text(file_path)
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""LLM prompts for algorithm extraction.
|
|
2
|
+
|
|
3
|
+
This module provides system and user prompts for LLM-based algorithm extraction,
|
|
4
|
+
designed to guide the LLM to produce structured JSON output.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
EXTRACTION_SYSTEM_PROMPT = """You are an expert at parsing mathematical algorithms from natural language descriptions.
|
|
8
|
+
|
|
9
|
+
Your task is to analyze algorithm text and extract a structured JSON representation.
|
|
10
|
+
|
|
11
|
+
RULES:
|
|
12
|
+
1. Identify the algorithm name from headers like "Algorithm X", "Procedure Y"
|
|
13
|
+
2. Extract inputs and outputs from Input/Output sections
|
|
14
|
+
3. Parse each step with accurate type classification
|
|
15
|
+
4. Preserve line references to the original text
|
|
16
|
+
5. Handle mathematical notation (Σ, Π, subscripts, superscripts)
|
|
17
|
+
6. Output valid JSON only - no explanatory text
|
|
18
|
+
|
|
19
|
+
STEP TYPES (classify each step):
|
|
20
|
+
- assignment: Variable assignment (x = y, x ← y)
|
|
21
|
+
- loop_for: For loops (for each, for i from, repeat n times)
|
|
22
|
+
- loop_while: While loops (while condition, until condition)
|
|
23
|
+
- conditional: If statements (if, when, in case)
|
|
24
|
+
- return: Return statements (return x, output x, result)
|
|
25
|
+
- call: Function calls (call f(), invoke)
|
|
26
|
+
- comment: Annotations and explanations
|
|
27
|
+
|
|
28
|
+
JSON OUTPUT FORMAT:
|
|
29
|
+
{
|
|
30
|
+
"name": "algorithm name or 'unnamed'",
|
|
31
|
+
"description": "brief description",
|
|
32
|
+
"inputs": [{"name": "var", "type": "inferred", "description": ""}],
|
|
33
|
+
"outputs": [{"name": "var", "type": "inferred", "description": ""}],
|
|
34
|
+
"steps": [
|
|
35
|
+
{
|
|
36
|
+
"id": 1,
|
|
37
|
+
"type": "assignment",
|
|
38
|
+
"description": "human readable description",
|
|
39
|
+
"inputs": ["read vars"],
|
|
40
|
+
"outputs": ["written vars"],
|
|
41
|
+
"line_refs": [line_numbers],
|
|
42
|
+
"condition": null,
|
|
43
|
+
"body": [],
|
|
44
|
+
"else_body": [],
|
|
45
|
+
"iter_var": null,
|
|
46
|
+
"iter_range": null,
|
|
47
|
+
"expression": null,
|
|
48
|
+
"call_target": null,
|
|
49
|
+
"arguments": [],
|
|
50
|
+
"annotation": null
|
|
51
|
+
}
|
|
52
|
+
],
|
|
53
|
+
"source_text": "original text with line numbers"
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
Infer types: int, float, array, matrix, bool based on context.
|
|
57
|
+
Always include line_refs array showing which original lines this step came from."""
|
|
58
|
+
|
|
59
|
+
EXTRACTION_USER_PROMPT_TEMPLATE = """Extract the algorithm from this mathematical text:
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
{numbered_text}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Provide the structured JSON representation following the schema provided.
|
|
66
|
+
|
|
67
|
+
Return ONLY the JSON. No explanatory text outside the JSON."""
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def format_extraction_prompt(text: str) -> str:
|
|
71
|
+
"""
|
|
72
|
+
Format extraction prompt with numbered text.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
text: Raw algorithm text
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
Formatted prompt with line numbers for traceability
|
|
79
|
+
|
|
80
|
+
Per D-08 from 02-CONTEXT.md.
|
|
81
|
+
"""
|
|
82
|
+
lines = text.split('\n')
|
|
83
|
+
numbered_lines = []
|
|
84
|
+
|
|
85
|
+
for i, line in enumerate(lines, 1):
|
|
86
|
+
numbered_lines.append(f"{i:3d}: {line}")
|
|
87
|
+
|
|
88
|
+
numbered_text = '\n'.join(numbered_lines)
|
|
89
|
+
|
|
90
|
+
return EXTRACTION_USER_PROMPT_TEMPLATE.format(numbered_text=numbered_text)
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
"""User review interface for algorithm extraction."""
|
|
2
|
+
|
|
3
|
+
from typing import List, Dict, Any, Optional, Tuple
|
|
4
|
+
from copy import deepcopy
|
|
5
|
+
|
|
6
|
+
from .schema import Algorithm, Step, StepType
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def validate_step_edit(step: Step, edits: Dict[str, Any]) -> Tuple[bool, List[str]]:
|
|
10
|
+
"""
|
|
11
|
+
Validate proposed edits to a step.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
step: Original step
|
|
15
|
+
edits: Dictionary of proposed changes
|
|
16
|
+
|
|
17
|
+
Returns:
|
|
18
|
+
Tuple of (is_valid, list_of_errors)
|
|
19
|
+
|
|
20
|
+
Per D-20 from 02-CONTEXT.md.
|
|
21
|
+
"""
|
|
22
|
+
errors = []
|
|
23
|
+
|
|
24
|
+
# Validate step type
|
|
25
|
+
if "type" in edits:
|
|
26
|
+
try:
|
|
27
|
+
StepType(edits["type"])
|
|
28
|
+
except ValueError:
|
|
29
|
+
errors.append(f"Invalid step type: {edits['type']}")
|
|
30
|
+
|
|
31
|
+
# Validate id is positive integer
|
|
32
|
+
if "id" in edits:
|
|
33
|
+
if not isinstance(edits["id"], int) or edits["id"] < 1:
|
|
34
|
+
errors.append("Step ID must be a positive integer")
|
|
35
|
+
|
|
36
|
+
# Validate description is non-empty
|
|
37
|
+
if "description" in edits:
|
|
38
|
+
if not edits["description"] or not str(edits["description"]).strip():
|
|
39
|
+
errors.append("Step description cannot be empty")
|
|
40
|
+
|
|
41
|
+
# Validate inputs and outputs are lists of strings
|
|
42
|
+
if "inputs" in edits:
|
|
43
|
+
if not isinstance(edits["inputs"], list):
|
|
44
|
+
errors.append("Inputs must be a list")
|
|
45
|
+
elif not all(isinstance(x, str) for x in edits["inputs"]):
|
|
46
|
+
errors.append("All inputs must be strings")
|
|
47
|
+
|
|
48
|
+
if "outputs" in edits:
|
|
49
|
+
if not isinstance(edits["outputs"], list):
|
|
50
|
+
errors.append("Outputs must be a list")
|
|
51
|
+
elif not all(isinstance(x, str) for x in edits["outputs"]):
|
|
52
|
+
errors.append("All outputs must be strings")
|
|
53
|
+
|
|
54
|
+
return len(errors) == 0, errors
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class ReviewInterface:
|
|
58
|
+
"""
|
|
59
|
+
Interface for reviewing and editing extracted algorithms.
|
|
60
|
+
|
|
61
|
+
Per D-18, D-19 from 02-CONTEXT.md.
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
def __init__(self, algorithm: Algorithm):
|
|
65
|
+
self.original = algorithm
|
|
66
|
+
self.working = deepcopy(algorithm)
|
|
67
|
+
self.pending_edits: List[Dict] = []
|
|
68
|
+
|
|
69
|
+
def get_side_by_side(self) -> Dict[str, Any]:
|
|
70
|
+
"""
|
|
71
|
+
Get side-by-side view data for UI rendering.
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
Dict with original_text and structured_steps for display
|
|
75
|
+
|
|
76
|
+
Per D-18 from 02-CONTEXT.md.
|
|
77
|
+
"""
|
|
78
|
+
return {
|
|
79
|
+
"original_text": self.original.source_text,
|
|
80
|
+
"algorithm_name": self.working.name,
|
|
81
|
+
"inputs": self.working.inputs,
|
|
82
|
+
"outputs": self.working.outputs,
|
|
83
|
+
"steps": [
|
|
84
|
+
{
|
|
85
|
+
"id": step.id,
|
|
86
|
+
"type": step.type.value,
|
|
87
|
+
"description": step.description,
|
|
88
|
+
"inputs": step.inputs,
|
|
89
|
+
"outputs": step.outputs,
|
|
90
|
+
"line_refs": step.line_refs
|
|
91
|
+
}
|
|
92
|
+
for step in self.working.steps
|
|
93
|
+
],
|
|
94
|
+
"step_count": len(self.working.steps)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
def edit_step(self, step_id: int, edits: Dict[str, Any]) -> Tuple[bool, List[str]]:
|
|
98
|
+
"""
|
|
99
|
+
Edit a specific step.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
step_id: ID of step to edit
|
|
103
|
+
edits: Dictionary of changes {field: new_value}
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
Tuple of (success, errors)
|
|
107
|
+
|
|
108
|
+
Per D-19 from 02-CONTEXT.md.
|
|
109
|
+
"""
|
|
110
|
+
# Find step
|
|
111
|
+
step = self._find_step(step_id)
|
|
112
|
+
if not step:
|
|
113
|
+
return False, [f"Step {step_id} not found"]
|
|
114
|
+
|
|
115
|
+
# Validate edits
|
|
116
|
+
is_valid, errors = validate_step_edit(step, edits)
|
|
117
|
+
if not is_valid:
|
|
118
|
+
return False, errors
|
|
119
|
+
|
|
120
|
+
# Apply edits
|
|
121
|
+
if "type" in edits:
|
|
122
|
+
step.type = StepType(edits["type"])
|
|
123
|
+
if "description" in edits:
|
|
124
|
+
step.description = edits["description"]
|
|
125
|
+
if "inputs" in edits:
|
|
126
|
+
step.inputs = edits["inputs"]
|
|
127
|
+
if "outputs" in edits:
|
|
128
|
+
step.outputs = edits["outputs"]
|
|
129
|
+
if "expression" in edits:
|
|
130
|
+
step.expression = edits["expression"]
|
|
131
|
+
if "condition" in edits:
|
|
132
|
+
step.condition = edits["condition"]
|
|
133
|
+
|
|
134
|
+
self.pending_edits.append({"action": "edit", "step_id": step_id, "edits": edits})
|
|
135
|
+
return True, []
|
|
136
|
+
|
|
137
|
+
def reorder_step(self, step_id: int, new_position: int) -> Tuple[bool, str]:
|
|
138
|
+
"""
|
|
139
|
+
Move a step to a new position.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
step_id: ID of step to move
|
|
143
|
+
new_position: New position index (1-based)
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
Tuple of (success, message)
|
|
147
|
+
|
|
148
|
+
Per D-19 from 02-CONTEXT.md.
|
|
149
|
+
"""
|
|
150
|
+
steps = self.working.steps
|
|
151
|
+
|
|
152
|
+
# Find current index
|
|
153
|
+
current_idx = None
|
|
154
|
+
for i, step in enumerate(steps):
|
|
155
|
+
if step.id == step_id:
|
|
156
|
+
current_idx = i
|
|
157
|
+
break
|
|
158
|
+
|
|
159
|
+
if current_idx is None:
|
|
160
|
+
return False, f"Step {step_id} not found"
|
|
161
|
+
|
|
162
|
+
# Clamp position
|
|
163
|
+
new_position = max(1, min(new_position, len(steps)))
|
|
164
|
+
new_idx = new_position - 1
|
|
165
|
+
|
|
166
|
+
if current_idx == new_idx:
|
|
167
|
+
return True, "No change needed"
|
|
168
|
+
|
|
169
|
+
# Move step
|
|
170
|
+
step = steps.pop(current_idx)
|
|
171
|
+
steps.insert(new_idx, step)
|
|
172
|
+
|
|
173
|
+
# Renumber all steps
|
|
174
|
+
self._renumber_steps()
|
|
175
|
+
|
|
176
|
+
self.pending_edits.append({"action": "reorder", "step_id": step_id, "new_position": new_position})
|
|
177
|
+
return True, f"Step moved to position {new_position}"
|
|
178
|
+
|
|
179
|
+
def delete_step(self, step_id: int) -> Tuple[bool, str]:
|
|
180
|
+
"""
|
|
181
|
+
Delete a step.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
step_id: ID of step to delete
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
Tuple of (success, message)
|
|
188
|
+
|
|
189
|
+
Per D-19 from 02-CONTEXT.md.
|
|
190
|
+
"""
|
|
191
|
+
steps = self.working.steps
|
|
192
|
+
|
|
193
|
+
for i, step in enumerate(steps):
|
|
194
|
+
if step.id == step_id:
|
|
195
|
+
steps.pop(i)
|
|
196
|
+
self._renumber_steps()
|
|
197
|
+
self.pending_edits.append({"action": "delete", "step_id": step_id})
|
|
198
|
+
return True, f"Step {step_id} deleted"
|
|
199
|
+
|
|
200
|
+
return False, f"Step {step_id} not found"
|
|
201
|
+
|
|
202
|
+
def add_step(self, position: int, step_data: Dict[str, Any]) -> Tuple[bool, List[str]]:
|
|
203
|
+
"""
|
|
204
|
+
Add a new step at specified position.
|
|
205
|
+
|
|
206
|
+
Args:
|
|
207
|
+
position: Position to insert (1-based, or -1 for end)
|
|
208
|
+
step_data: Step data dictionary
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
Tuple of (success, errors_or_message)
|
|
212
|
+
|
|
213
|
+
Per D-19 from 02-CONTEXT.md.
|
|
214
|
+
"""
|
|
215
|
+
# Validate
|
|
216
|
+
temp_step = Step(id=0, type=StepType.COMMENT, description="")
|
|
217
|
+
is_valid, errors = validate_step_edit(temp_step, step_data)
|
|
218
|
+
if not is_valid:
|
|
219
|
+
return False, errors
|
|
220
|
+
|
|
221
|
+
# Create step
|
|
222
|
+
step = Step(
|
|
223
|
+
id=0, # Will be renumbered
|
|
224
|
+
type=StepType(step_data.get("type", "comment")),
|
|
225
|
+
description=step_data.get("description", ""),
|
|
226
|
+
inputs=step_data.get("inputs", []),
|
|
227
|
+
outputs=step_data.get("outputs", []),
|
|
228
|
+
line_refs=[]
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
# Insert
|
|
232
|
+
steps = self.working.steps
|
|
233
|
+
if position == -1 or position > len(steps):
|
|
234
|
+
steps.append(step)
|
|
235
|
+
else:
|
|
236
|
+
steps.insert(position - 1, step)
|
|
237
|
+
|
|
238
|
+
self._renumber_steps()
|
|
239
|
+
|
|
240
|
+
self.pending_edits.append({"action": "add", "position": position, "step_data": step_data})
|
|
241
|
+
return True, [f"Step added at position {position}"]
|
|
242
|
+
|
|
243
|
+
def _find_step(self, step_id: int) -> Optional[Step]:
|
|
244
|
+
"""Find step by ID."""
|
|
245
|
+
for step in self.working.steps:
|
|
246
|
+
if step.id == step_id:
|
|
247
|
+
return step
|
|
248
|
+
return None
|
|
249
|
+
|
|
250
|
+
def _renumber_steps(self):
|
|
251
|
+
"""Renumber all steps sequentially."""
|
|
252
|
+
for i, step in enumerate(self.working.steps, 1):
|
|
253
|
+
step.id = i
|
|
254
|
+
|
|
255
|
+
def get_pending_changes(self) -> List[Dict]:
|
|
256
|
+
"""Get list of pending edits."""
|
|
257
|
+
return self.pending_edits
|
|
258
|
+
|
|
259
|
+
def reset(self):
|
|
260
|
+
"""Reset to original algorithm."""
|
|
261
|
+
self.working = deepcopy(self.original)
|
|
262
|
+
self.pending_edits = []
|
|
263
|
+
|
|
264
|
+
def approve(self) -> Algorithm:
|
|
265
|
+
"""
|
|
266
|
+
Approve and return final algorithm.
|
|
267
|
+
|
|
268
|
+
Per D-22 from 02-CONTEXT.md.
|
|
269
|
+
"""
|
|
270
|
+
return self.working
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def apply_edits(algorithm: Algorithm, edits: List[Dict]) -> Algorithm:
|
|
274
|
+
"""
|
|
275
|
+
Apply a series of edits to an algorithm.
|
|
276
|
+
|
|
277
|
+
Args:
|
|
278
|
+
algorithm: Original algorithm
|
|
279
|
+
edits: List of edit operations
|
|
280
|
+
|
|
281
|
+
Returns:
|
|
282
|
+
Modified algorithm
|
|
283
|
+
"""
|
|
284
|
+
review = ReviewInterface(algorithm)
|
|
285
|
+
|
|
286
|
+
for edit in edits:
|
|
287
|
+
action = edit.get("action")
|
|
288
|
+
|
|
289
|
+
if action == "edit":
|
|
290
|
+
review.edit_step(edit["step_id"], edit["edits"])
|
|
291
|
+
elif action == "reorder":
|
|
292
|
+
review.reorder_step(edit["step_id"], edit["new_position"])
|
|
293
|
+
elif action == "delete":
|
|
294
|
+
review.delete_step(edit["step_id"])
|
|
295
|
+
elif action == "add":
|
|
296
|
+
review.add_step(edit["position"], edit["step_data"])
|
|
297
|
+
|
|
298
|
+
return review.approve()
|