ralphx 0.2.2__py3-none-any.whl → 0.3.5__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.
- ralphx/__init__.py +1 -1
- ralphx/api/main.py +9 -1
- ralphx/api/routes/auth.py +730 -65
- ralphx/api/routes/config.py +3 -56
- ralphx/api/routes/export_import.py +795 -0
- ralphx/api/routes/loops.py +4 -4
- ralphx/api/routes/planning.py +19 -5
- ralphx/api/routes/projects.py +84 -2
- ralphx/api/routes/templates.py +115 -2
- ralphx/api/routes/workflows.py +22 -22
- ralphx/cli.py +21 -6
- ralphx/core/auth.py +346 -171
- ralphx/core/database.py +615 -167
- ralphx/core/executor.py +0 -3
- ralphx/core/loop.py +15 -2
- ralphx/core/loop_templates.py +69 -3
- ralphx/core/planning_service.py +109 -21
- ralphx/core/preview.py +9 -25
- ralphx/core/project_db.py +175 -75
- ralphx/core/project_export.py +469 -0
- ralphx/core/project_import.py +670 -0
- ralphx/core/sample_project.py +430 -0
- ralphx/core/templates.py +46 -9
- ralphx/core/workflow_executor.py +35 -5
- ralphx/core/workflow_export.py +606 -0
- ralphx/core/workflow_import.py +1149 -0
- ralphx/examples/sample_project/DESIGN.md +345 -0
- ralphx/examples/sample_project/README.md +37 -0
- ralphx/examples/sample_project/guardrails.md +57 -0
- ralphx/examples/sample_project/stories.jsonl +10 -0
- ralphx/mcp/__init__.py +6 -2
- ralphx/mcp/registry.py +3 -3
- ralphx/mcp/server.py +99 -29
- ralphx/mcp/tools/__init__.py +4 -0
- ralphx/mcp/tools/help.py +204 -0
- ralphx/mcp/tools/workflows.py +114 -32
- ralphx/mcp_server.py +6 -2
- ralphx/static/assets/index-0ovNnfOq.css +1 -0
- ralphx/static/assets/index-CY9s08ZB.js +251 -0
- ralphx/static/assets/index-CY9s08ZB.js.map +1 -0
- ralphx/static/index.html +14 -0
- {ralphx-0.2.2.dist-info → ralphx-0.3.5.dist-info}/METADATA +34 -12
- {ralphx-0.2.2.dist-info → ralphx-0.3.5.dist-info}/RECORD +45 -30
- {ralphx-0.2.2.dist-info → ralphx-0.3.5.dist-info}/WHEEL +0 -0
- {ralphx-0.2.2.dist-info → ralphx-0.3.5.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
"""Sample project creation for RalphX first-run experience.
|
|
2
|
+
|
|
3
|
+
Creates a sample "Excuse Generator" project when users first install RalphX,
|
|
4
|
+
demonstrating how workflows work with design documents, stories, and guardrails.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import logging
|
|
9
|
+
import shutil
|
|
10
|
+
import subprocess
|
|
11
|
+
import uuid
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Optional
|
|
14
|
+
|
|
15
|
+
from ralphx.core.workspace import get_workspace_path
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
# Sample project name and directory
|
|
20
|
+
SAMPLE_PROJECT_NAME = "Excuse-Generator"
|
|
21
|
+
SAMPLE_PROJECT_DIR_NAME = "excuse-generator"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def get_sample_project_source_path() -> Path:
|
|
25
|
+
"""Get path to bundled sample project files.
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
Path to ralphx/examples/sample_project/ directory.
|
|
29
|
+
"""
|
|
30
|
+
return Path(__file__).parent.parent / "examples" / "sample_project"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def get_samples_directory() -> Path:
|
|
34
|
+
"""Get the samples directory within the RalphX workspace.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
Path to ~/.ralphx/samples/
|
|
38
|
+
"""
|
|
39
|
+
samples_dir = get_workspace_path() / "samples"
|
|
40
|
+
samples_dir.mkdir(parents=True, exist_ok=True)
|
|
41
|
+
return samples_dir
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def get_sample_project_target_path() -> Path:
|
|
45
|
+
"""Get target path for sample project.
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
Path to ~/.ralphx/samples/excuse-generator/
|
|
49
|
+
"""
|
|
50
|
+
return get_samples_directory() / SAMPLE_PROJECT_DIR_NAME
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _init_git_repo(project_path: Path) -> bool:
|
|
54
|
+
"""Initialize a git repository in the project directory.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
project_path: Path to the project directory.
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
True if git repo was initialized, False if git not available.
|
|
61
|
+
"""
|
|
62
|
+
try:
|
|
63
|
+
# Check if git is available
|
|
64
|
+
result = subprocess.run(
|
|
65
|
+
["git", "--version"],
|
|
66
|
+
capture_output=True,
|
|
67
|
+
text=True,
|
|
68
|
+
timeout=5,
|
|
69
|
+
)
|
|
70
|
+
if result.returncode != 0:
|
|
71
|
+
return False
|
|
72
|
+
|
|
73
|
+
# Initialize git repo
|
|
74
|
+
subprocess.run(
|
|
75
|
+
["git", "init"],
|
|
76
|
+
cwd=project_path,
|
|
77
|
+
capture_output=True,
|
|
78
|
+
timeout=10,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
# Create initial commit with sample files
|
|
82
|
+
subprocess.run(
|
|
83
|
+
["git", "add", "."],
|
|
84
|
+
cwd=project_path,
|
|
85
|
+
capture_output=True,
|
|
86
|
+
timeout=10,
|
|
87
|
+
)
|
|
88
|
+
subprocess.run(
|
|
89
|
+
["git", "commit", "-m", "Initial commit: RalphX sample project"],
|
|
90
|
+
cwd=project_path,
|
|
91
|
+
capture_output=True,
|
|
92
|
+
timeout=10,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
return True
|
|
96
|
+
except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
|
|
97
|
+
return False
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _create_workflow_with_stories(
|
|
101
|
+
project_db,
|
|
102
|
+
workflow_id: str,
|
|
103
|
+
template_id: str,
|
|
104
|
+
stories_path: Path,
|
|
105
|
+
) -> Optional[str]:
|
|
106
|
+
"""Create a workflow from template and populate with sample stories.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
project_db: ProjectDatabase instance.
|
|
110
|
+
workflow_id: ID for the new workflow.
|
|
111
|
+
template_id: Template ID to base workflow on.
|
|
112
|
+
stories_path: Path to stories.jsonl file.
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
Workflow ID if created, None on error.
|
|
116
|
+
"""
|
|
117
|
+
try:
|
|
118
|
+
# Seed templates if empty
|
|
119
|
+
project_db.seed_workflow_templates_if_empty()
|
|
120
|
+
|
|
121
|
+
# Get template to create steps from
|
|
122
|
+
template = project_db.get_workflow_template(template_id)
|
|
123
|
+
if not template:
|
|
124
|
+
logger.warning(f"Template '{template_id}' not found")
|
|
125
|
+
return None
|
|
126
|
+
|
|
127
|
+
# Create workflow (namespace parameter removed in schema v16)
|
|
128
|
+
workflow = project_db.create_workflow(
|
|
129
|
+
id=workflow_id,
|
|
130
|
+
name="Build Excuse Generator",
|
|
131
|
+
template_id=template_id,
|
|
132
|
+
status="draft",
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
# Create steps from template phases
|
|
136
|
+
created_steps = []
|
|
137
|
+
for phase in template.get("phases", []):
|
|
138
|
+
step = project_db.create_workflow_step(
|
|
139
|
+
workflow_id=workflow_id,
|
|
140
|
+
step_number=phase["number"],
|
|
141
|
+
name=phase["name"],
|
|
142
|
+
step_type=phase["type"],
|
|
143
|
+
config={
|
|
144
|
+
"description": phase.get("description"),
|
|
145
|
+
"loopType": phase.get("loopType"),
|
|
146
|
+
"inputs": phase.get("inputs", []),
|
|
147
|
+
"outputs": phase.get("outputs", []),
|
|
148
|
+
"skippable": phase.get("skippable", False),
|
|
149
|
+
"skipCondition": phase.get("skipCondition"),
|
|
150
|
+
},
|
|
151
|
+
status="pending",
|
|
152
|
+
)
|
|
153
|
+
created_steps.append(step)
|
|
154
|
+
|
|
155
|
+
# Find the Story Generation step (step 1 in from-design-doc template)
|
|
156
|
+
story_step = None
|
|
157
|
+
for step in created_steps:
|
|
158
|
+
if step["step_number"] == 1:
|
|
159
|
+
story_step = step
|
|
160
|
+
break
|
|
161
|
+
|
|
162
|
+
if not story_step:
|
|
163
|
+
logger.warning("Could not find Story Generation step")
|
|
164
|
+
return workflow_id
|
|
165
|
+
|
|
166
|
+
# Import stories from JSONL file
|
|
167
|
+
if stories_path.exists():
|
|
168
|
+
story_count = _import_stories(
|
|
169
|
+
project_db,
|
|
170
|
+
workflow_id=workflow_id,
|
|
171
|
+
source_step_id=story_step["id"],
|
|
172
|
+
stories_path=stories_path,
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
# Create a completed run record to track the imported stories
|
|
176
|
+
if story_count > 0:
|
|
177
|
+
run_id = f"run-sample-{uuid.uuid4().hex[:8]}"
|
|
178
|
+
project_db.create_run(
|
|
179
|
+
id=run_id,
|
|
180
|
+
loop_name="sample-import",
|
|
181
|
+
workflow_id=workflow_id,
|
|
182
|
+
step_id=story_step["id"],
|
|
183
|
+
)
|
|
184
|
+
# Mark run as completed with story count
|
|
185
|
+
from datetime import datetime
|
|
186
|
+
project_db.update_run(
|
|
187
|
+
run_id,
|
|
188
|
+
status="completed",
|
|
189
|
+
completed_at=datetime.utcnow().isoformat(),
|
|
190
|
+
iterations_completed=1,
|
|
191
|
+
items_generated=story_count,
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
# Mark Story Generation step as completed since stories are pre-populated
|
|
195
|
+
project_db.update_workflow_step(
|
|
196
|
+
story_step["id"],
|
|
197
|
+
status="completed",
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
return workflow_id
|
|
201
|
+
|
|
202
|
+
except Exception as e:
|
|
203
|
+
logger.error(f"Failed to create workflow: {e}")
|
|
204
|
+
return None
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _import_stories(
|
|
208
|
+
project_db,
|
|
209
|
+
workflow_id: str,
|
|
210
|
+
source_step_id: int,
|
|
211
|
+
stories_path: Path,
|
|
212
|
+
) -> int:
|
|
213
|
+
"""Import stories from JSONL file as work items.
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
project_db: ProjectDatabase instance.
|
|
217
|
+
workflow_id: Parent workflow ID.
|
|
218
|
+
source_step_id: Step ID that "created" these items.
|
|
219
|
+
stories_path: Path to stories.jsonl file.
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
Number of stories imported.
|
|
223
|
+
"""
|
|
224
|
+
count = 0
|
|
225
|
+
try:
|
|
226
|
+
with open(stories_path, "r") as f:
|
|
227
|
+
for line in f:
|
|
228
|
+
line = line.strip()
|
|
229
|
+
if not line:
|
|
230
|
+
continue
|
|
231
|
+
|
|
232
|
+
try:
|
|
233
|
+
story = json.loads(line)
|
|
234
|
+
|
|
235
|
+
# Build content from story data
|
|
236
|
+
content_parts = [story.get("description", "")]
|
|
237
|
+
|
|
238
|
+
# Add acceptance criteria if present
|
|
239
|
+
criteria = story.get("acceptance_criteria", [])
|
|
240
|
+
if criteria:
|
|
241
|
+
content_parts.append("\n\n## Acceptance Criteria")
|
|
242
|
+
for criterion in criteria:
|
|
243
|
+
content_parts.append(f"- {criterion}")
|
|
244
|
+
|
|
245
|
+
content = "\n".join(content_parts)
|
|
246
|
+
|
|
247
|
+
# Determine priority (convert string to int if needed)
|
|
248
|
+
priority = story.get("priority")
|
|
249
|
+
if priority == "high":
|
|
250
|
+
priority = 1
|
|
251
|
+
elif priority == "medium":
|
|
252
|
+
priority = 2
|
|
253
|
+
elif priority == "low":
|
|
254
|
+
priority = 3
|
|
255
|
+
elif isinstance(priority, str):
|
|
256
|
+
priority = 2 # default
|
|
257
|
+
|
|
258
|
+
# Create work item with status "completed" so it's ready for Implementation
|
|
259
|
+
project_db.create_work_item(
|
|
260
|
+
id=story.get("id", f"story-{uuid.uuid4().hex[:8]}"),
|
|
261
|
+
workflow_id=workflow_id,
|
|
262
|
+
source_step_id=source_step_id,
|
|
263
|
+
content=content,
|
|
264
|
+
title=story.get("title"),
|
|
265
|
+
priority=priority,
|
|
266
|
+
category=story.get("category"),
|
|
267
|
+
item_type="story",
|
|
268
|
+
metadata={
|
|
269
|
+
"acceptance_criteria": story.get("acceptance_criteria", []),
|
|
270
|
+
"dependencies": story.get("dependencies", []),
|
|
271
|
+
},
|
|
272
|
+
status="completed", # Ready for next step to consume
|
|
273
|
+
)
|
|
274
|
+
count += 1
|
|
275
|
+
|
|
276
|
+
except json.JSONDecodeError as e:
|
|
277
|
+
logger.warning(f"Failed to parse story line: {e}")
|
|
278
|
+
continue
|
|
279
|
+
|
|
280
|
+
except Exception as e:
|
|
281
|
+
logger.error(f"Failed to import stories: {e}")
|
|
282
|
+
|
|
283
|
+
return count
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def create_sample_project() -> Optional[str]:
|
|
287
|
+
"""Create the sample project for first-run experience.
|
|
288
|
+
|
|
289
|
+
This function:
|
|
290
|
+
1. Creates ~/RalphX-Sample-Project/ directory
|
|
291
|
+
2. Copies sample files (README, DESIGN.md, guardrails, stories)
|
|
292
|
+
3. Initializes git repository (if git available)
|
|
293
|
+
4. Registers project with RalphX
|
|
294
|
+
5. Creates a workflow from "from-design-doc" template
|
|
295
|
+
6. Pre-populates with sample stories
|
|
296
|
+
|
|
297
|
+
Returns:
|
|
298
|
+
Project slug if created successfully, None on error.
|
|
299
|
+
"""
|
|
300
|
+
from ralphx.core.project import ProjectManager
|
|
301
|
+
from ralphx.core.project_db import ProjectDatabase
|
|
302
|
+
|
|
303
|
+
source_path = get_sample_project_source_path()
|
|
304
|
+
target_path = get_sample_project_target_path()
|
|
305
|
+
|
|
306
|
+
# Check if source files exist
|
|
307
|
+
if not source_path.exists():
|
|
308
|
+
logger.warning(f"Sample project source not found: {source_path}")
|
|
309
|
+
return None
|
|
310
|
+
|
|
311
|
+
# Check if target already exists
|
|
312
|
+
if target_path.exists():
|
|
313
|
+
logger.info(f"Sample project directory already exists: {target_path}")
|
|
314
|
+
return None
|
|
315
|
+
|
|
316
|
+
try:
|
|
317
|
+
# Create target directory
|
|
318
|
+
target_path.mkdir(parents=True, exist_ok=True)
|
|
319
|
+
|
|
320
|
+
# Copy sample files
|
|
321
|
+
for source_file in source_path.iterdir():
|
|
322
|
+
if source_file.is_file():
|
|
323
|
+
shutil.copy2(source_file, target_path / source_file.name)
|
|
324
|
+
|
|
325
|
+
logger.info(f"Created sample project at: {target_path}")
|
|
326
|
+
|
|
327
|
+
# Initialize git repo
|
|
328
|
+
git_initialized = _init_git_repo(target_path)
|
|
329
|
+
if git_initialized:
|
|
330
|
+
logger.info("Initialized git repository")
|
|
331
|
+
else:
|
|
332
|
+
logger.info("Git not available, skipping repository initialization")
|
|
333
|
+
|
|
334
|
+
# Register project with RalphX
|
|
335
|
+
pm = ProjectManager()
|
|
336
|
+
|
|
337
|
+
try:
|
|
338
|
+
project = pm.add_project(
|
|
339
|
+
path=target_path,
|
|
340
|
+
name=SAMPLE_PROJECT_NAME,
|
|
341
|
+
design_doc="DESIGN.md",
|
|
342
|
+
slug="excuse-generator",
|
|
343
|
+
)
|
|
344
|
+
logger.info(f"Registered sample project: {project.slug}")
|
|
345
|
+
|
|
346
|
+
# Create workflow with pre-populated stories
|
|
347
|
+
project_db = pm.get_project_db(target_path)
|
|
348
|
+
workflow_id = f"wf-sample-{uuid.uuid4().hex[:8]}"
|
|
349
|
+
|
|
350
|
+
_create_workflow_with_stories(
|
|
351
|
+
project_db,
|
|
352
|
+
workflow_id=workflow_id,
|
|
353
|
+
template_id="from-design-doc",
|
|
354
|
+
stories_path=target_path / "stories.jsonl",
|
|
355
|
+
)
|
|
356
|
+
logger.info("Created sample workflow with pre-populated stories")
|
|
357
|
+
|
|
358
|
+
# Add design doc as workflow resource
|
|
359
|
+
try:
|
|
360
|
+
design_doc_path = target_path / "DESIGN.md"
|
|
361
|
+
if design_doc_path.exists():
|
|
362
|
+
project_db.create_workflow_resource(
|
|
363
|
+
workflow_id=workflow_id,
|
|
364
|
+
resource_type="design_doc",
|
|
365
|
+
name="Excuse Generator Design",
|
|
366
|
+
content=design_doc_path.read_text(),
|
|
367
|
+
source="manual",
|
|
368
|
+
enabled=True,
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
# Add guardrails as workflow resource
|
|
372
|
+
guardrails_path = target_path / "guardrails.md"
|
|
373
|
+
if guardrails_path.exists():
|
|
374
|
+
project_db.create_workflow_resource(
|
|
375
|
+
workflow_id=workflow_id,
|
|
376
|
+
resource_type="guardrails",
|
|
377
|
+
name="Project Guardrails",
|
|
378
|
+
content=guardrails_path.read_text(),
|
|
379
|
+
source="manual",
|
|
380
|
+
enabled=True,
|
|
381
|
+
)
|
|
382
|
+
except Exception as e:
|
|
383
|
+
logger.warning(f"Failed to add workflow resources: {e}")
|
|
384
|
+
|
|
385
|
+
return project.slug
|
|
386
|
+
|
|
387
|
+
except FileExistsError:
|
|
388
|
+
logger.info("Sample project already registered")
|
|
389
|
+
return "excuse-generator"
|
|
390
|
+
|
|
391
|
+
except Exception as e:
|
|
392
|
+
logger.error(f"Failed to create sample project: {e}")
|
|
393
|
+
# Clean up on failure
|
|
394
|
+
if target_path.exists():
|
|
395
|
+
try:
|
|
396
|
+
shutil.rmtree(target_path)
|
|
397
|
+
except Exception:
|
|
398
|
+
pass
|
|
399
|
+
return None
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def ensure_sample_project_created() -> bool:
|
|
403
|
+
"""Ensure sample project is created on first run.
|
|
404
|
+
|
|
405
|
+
Uses global database settings to track if sample was already created.
|
|
406
|
+
|
|
407
|
+
Returns:
|
|
408
|
+
True if sample project was created or already exists, False on error.
|
|
409
|
+
"""
|
|
410
|
+
from ralphx.core.global_db import GlobalDatabase
|
|
411
|
+
|
|
412
|
+
global_db = GlobalDatabase()
|
|
413
|
+
|
|
414
|
+
# Check if already created
|
|
415
|
+
if global_db.get_setting("sample_project_created") is not None:
|
|
416
|
+
return True
|
|
417
|
+
|
|
418
|
+
# Create sample project
|
|
419
|
+
result = create_sample_project()
|
|
420
|
+
|
|
421
|
+
if result:
|
|
422
|
+
# Mark as created
|
|
423
|
+
global_db.set_setting("sample_project_created", "true")
|
|
424
|
+
logger.info("Sample project setup complete")
|
|
425
|
+
return True
|
|
426
|
+
|
|
427
|
+
# Even if creation failed (e.g., directory exists), mark as attempted
|
|
428
|
+
# to avoid repeated attempts
|
|
429
|
+
global_db.set_setting("sample_project_created", "attempted")
|
|
430
|
+
return False
|
ralphx/core/templates.py
CHANGED
|
@@ -9,15 +9,15 @@ from typing import Optional
|
|
|
9
9
|
|
|
10
10
|
# Base loop templates
|
|
11
11
|
TEMPLATES: dict[str, dict] = {
|
|
12
|
-
"
|
|
13
|
-
"name": "
|
|
14
|
-
"display_name": "
|
|
12
|
+
"extractgen_requirements": {
|
|
13
|
+
"name": "extractgen_requirements",
|
|
14
|
+
"display_name": "Extract Requirements Loop",
|
|
15
15
|
"description": "Discover and document user stories from design documents or web research",
|
|
16
16
|
"type": "generator",
|
|
17
17
|
"category": "discovery",
|
|
18
18
|
"config": {
|
|
19
|
-
"name": "
|
|
20
|
-
"display_name": "
|
|
19
|
+
"name": "extractgen_requirements",
|
|
20
|
+
"display_name": "Extract Requirements Loop",
|
|
21
21
|
"type": "generator",
|
|
22
22
|
"description": "Discover and document user stories from design documents",
|
|
23
23
|
"item_types": {
|
|
@@ -34,7 +34,7 @@ TEMPLATES: dict[str, dict] = {
|
|
|
34
34
|
"model": "claude-sonnet-4-20250514",
|
|
35
35
|
"timeout": 180,
|
|
36
36
|
"tools": [],
|
|
37
|
-
"prompt_template": "prompts/
|
|
37
|
+
"prompt_template": "prompts/extractgen_requirements_turbo.md",
|
|
38
38
|
},
|
|
39
39
|
{
|
|
40
40
|
"name": "deep",
|
|
@@ -42,7 +42,7 @@ TEMPLATES: dict[str, dict] = {
|
|
|
42
42
|
"model": "claude-sonnet-4-20250514",
|
|
43
43
|
"timeout": 900,
|
|
44
44
|
"tools": ["web_search"],
|
|
45
|
-
"prompt_template": "prompts/
|
|
45
|
+
"prompt_template": "prompts/extractgen_requirements_deep.md",
|
|
46
46
|
},
|
|
47
47
|
],
|
|
48
48
|
"mode_selection": {
|
|
@@ -57,6 +57,43 @@ TEMPLATES: dict[str, dict] = {
|
|
|
57
57
|
},
|
|
58
58
|
},
|
|
59
59
|
},
|
|
60
|
+
"webgen_requirements": {
|
|
61
|
+
"name": "webgen_requirements",
|
|
62
|
+
"display_name": "Web-Generated Requirements",
|
|
63
|
+
"description": "Discover missing requirements through web research on domain best practices",
|
|
64
|
+
"type": "generator",
|
|
65
|
+
"category": "discovery",
|
|
66
|
+
"config": {
|
|
67
|
+
"name": "webgen_requirements",
|
|
68
|
+
"display_name": "Web-Generated Requirements",
|
|
69
|
+
"type": "generator",
|
|
70
|
+
"description": "Research industry best practices to find requirements NOT in the design doc",
|
|
71
|
+
"item_types": {
|
|
72
|
+
"output": {
|
|
73
|
+
"singular": "story",
|
|
74
|
+
"plural": "stories",
|
|
75
|
+
"description": "User stories discovered through domain research",
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
"modes": [
|
|
79
|
+
{
|
|
80
|
+
"name": "research",
|
|
81
|
+
"description": "Web research for best practices and gaps",
|
|
82
|
+
"model": "claude-sonnet-4-20250514",
|
|
83
|
+
"timeout": 900,
|
|
84
|
+
"tools": ["web_search"],
|
|
85
|
+
"prompt_template": "prompts/webgen_requirements.md",
|
|
86
|
+
}
|
|
87
|
+
],
|
|
88
|
+
"mode_selection": {"strategy": "fixed", "fixed_mode": "research"},
|
|
89
|
+
"limits": {
|
|
90
|
+
"max_iterations": 15,
|
|
91
|
+
"max_runtime_seconds": 14400,
|
|
92
|
+
"max_consecutive_errors": 3,
|
|
93
|
+
"cooldown_between_iterations": 15,
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
},
|
|
60
97
|
"implementation": {
|
|
61
98
|
"name": "implementation",
|
|
62
99
|
"display_name": "Implementation Loop",
|
|
@@ -72,7 +109,7 @@ TEMPLATES: dict[str, dict] = {
|
|
|
72
109
|
"input": {
|
|
73
110
|
"singular": "story",
|
|
74
111
|
"plural": "stories",
|
|
75
|
-
"source": "
|
|
112
|
+
"source": "extractgen_requirements",
|
|
76
113
|
"description": "Stories to implement",
|
|
77
114
|
},
|
|
78
115
|
"output": {
|
|
@@ -182,7 +219,7 @@ def get_template(name: str) -> Optional[dict]:
|
|
|
182
219
|
"""Get a template by name.
|
|
183
220
|
|
|
184
221
|
Args:
|
|
185
|
-
name: Template name (e.g., '
|
|
222
|
+
name: Template name (e.g., 'extractgen_requirements', 'implementation')
|
|
186
223
|
|
|
187
224
|
Returns:
|
|
188
225
|
Template dict or None if not found
|
ralphx/core/workflow_executor.py
CHANGED
|
@@ -305,7 +305,7 @@ class WorkflowExecutor:
|
|
|
305
305
|
config_path.write_text(config_yaml)
|
|
306
306
|
|
|
307
307
|
# Create prompt files (required by the loop config)
|
|
308
|
-
self._create_loop_prompt_files(loop_name, loop_type)
|
|
308
|
+
self._create_loop_prompt_files(loop_name, loop_type, step)
|
|
309
309
|
|
|
310
310
|
# Load and return config
|
|
311
311
|
loader = LoopLoader(db=self.db)
|
|
@@ -323,31 +323,61 @@ class WorkflowExecutor:
|
|
|
323
323
|
loader = LoopLoader(db=self.db)
|
|
324
324
|
return loader.get_loop(loop_record["name"])
|
|
325
325
|
|
|
326
|
-
|
|
326
|
+
# Maximum allowed size for custom prompts (50KB)
|
|
327
|
+
MAX_CUSTOM_PROMPT_LENGTH = 50000
|
|
328
|
+
|
|
329
|
+
def _create_loop_prompt_files(
|
|
330
|
+
self, loop_name: str, loop_type: str, step: dict
|
|
331
|
+
) -> None:
|
|
327
332
|
"""Create prompt template files for an auto-generated loop.
|
|
328
333
|
|
|
329
334
|
The simple config generators reference prompt files that don't exist.
|
|
330
|
-
This method creates those files with default content
|
|
335
|
+
This method creates those files with default content, or uses a custom
|
|
336
|
+
prompt if specified in the step config.
|
|
331
337
|
|
|
332
338
|
Args:
|
|
333
339
|
loop_name: Name of the loop.
|
|
334
340
|
loop_type: Type of loop (generator, consumer).
|
|
341
|
+
step: The step dict containing config (may include customPrompt or template).
|
|
335
342
|
"""
|
|
336
343
|
from ralphx.core.loop_templates import (
|
|
337
344
|
PLANNING_EXTRACT_PROMPT,
|
|
338
345
|
IMPLEMENTATION_IMPLEMENT_PROMPT,
|
|
346
|
+
WEBGEN_REQUIREMENTS_PROMPT,
|
|
339
347
|
)
|
|
340
348
|
|
|
341
349
|
loop_dir = Path(self.project.path) / ".ralphx" / "loops" / loop_name
|
|
342
350
|
prompts_dir = loop_dir / "prompts"
|
|
343
351
|
prompts_dir.mkdir(parents=True, exist_ok=True)
|
|
344
352
|
|
|
353
|
+
# Check for custom prompt in step config
|
|
354
|
+
step_config = step.get("config", {})
|
|
355
|
+
custom_prompt = step_config.get("customPrompt")
|
|
356
|
+
|
|
357
|
+
# Validate custom prompt size if provided
|
|
358
|
+
if custom_prompt:
|
|
359
|
+
if len(custom_prompt) > self.MAX_CUSTOM_PROMPT_LENGTH:
|
|
360
|
+
raise ValueError(
|
|
361
|
+
f"Custom prompt exceeds maximum length of {self.MAX_CUSTOM_PROMPT_LENGTH} characters"
|
|
362
|
+
)
|
|
363
|
+
# Treat empty/whitespace-only as no custom prompt
|
|
364
|
+
if not custom_prompt.strip():
|
|
365
|
+
custom_prompt = None
|
|
366
|
+
|
|
367
|
+
# Determine prompt content and file based on template or loop_type
|
|
368
|
+
template = step_config.get("template")
|
|
369
|
+
|
|
345
370
|
if loop_type == "generator":
|
|
346
371
|
prompt_file = prompts_dir / "planning.md"
|
|
347
|
-
|
|
372
|
+
if custom_prompt:
|
|
373
|
+
prompt_content = custom_prompt
|
|
374
|
+
elif template == "webgen_requirements":
|
|
375
|
+
prompt_content = WEBGEN_REQUIREMENTS_PROMPT
|
|
376
|
+
else:
|
|
377
|
+
prompt_content = PLANNING_EXTRACT_PROMPT
|
|
348
378
|
else:
|
|
349
379
|
prompt_file = prompts_dir / "implement.md"
|
|
350
|
-
prompt_content = IMPLEMENTATION_IMPLEMENT_PROMPT
|
|
380
|
+
prompt_content = custom_prompt or IMPLEMENTATION_IMPLEMENT_PROMPT
|
|
351
381
|
|
|
352
382
|
prompt_file.write_text(prompt_content.strip())
|
|
353
383
|
|