empathy-framework 4.6.3__py3-none-any.whl → 4.6.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.
Files changed (65) hide show
  1. {empathy_framework-4.6.3.dist-info → empathy_framework-4.6.5.dist-info}/METADATA +53 -11
  2. {empathy_framework-4.6.3.dist-info → empathy_framework-4.6.5.dist-info}/RECORD +32 -57
  3. empathy_llm_toolkit/agent_factory/crews/health_check.py +7 -4
  4. empathy_llm_toolkit/agent_factory/decorators.py +3 -2
  5. empathy_llm_toolkit/agent_factory/memory_integration.py +6 -2
  6. empathy_llm_toolkit/contextual_patterns.py +5 -2
  7. empathy_llm_toolkit/git_pattern_extractor.py +8 -4
  8. empathy_llm_toolkit/providers.py +4 -3
  9. empathy_os/__init__.py +1 -1
  10. empathy_os/cli/__init__.py +306 -0
  11. empathy_os/cli/__main__.py +26 -0
  12. empathy_os/cli/commands/__init__.py +8 -0
  13. empathy_os/cli/commands/inspection.py +48 -0
  14. empathy_os/cli/commands/memory.py +56 -0
  15. empathy_os/cli/commands/provider.py +86 -0
  16. empathy_os/cli/commands/utilities.py +94 -0
  17. empathy_os/cli/core.py +32 -0
  18. empathy_os/cli.py +18 -6
  19. empathy_os/cli_unified.py +19 -3
  20. empathy_os/memory/short_term.py +12 -2
  21. empathy_os/project_index/scanner.py +151 -49
  22. empathy_os/socratic/visual_editor.py +9 -4
  23. empathy_os/workflows/bug_predict.py +70 -1
  24. empathy_os/workflows/pr_review.py +6 -0
  25. empathy_os/workflows/security_audit.py +13 -0
  26. empathy_os/workflows/tier_tracking.py +50 -2
  27. wizards/discharge_summary_wizard.py +4 -2
  28. wizards/incident_report_wizard.py +4 -2
  29. empathy_os/meta_workflows/agent_creator 2.py +0 -254
  30. empathy_os/meta_workflows/builtin_templates 2.py +0 -567
  31. empathy_os/meta_workflows/cli_meta_workflows 2.py +0 -1551
  32. empathy_os/meta_workflows/form_engine 2.py +0 -304
  33. empathy_os/meta_workflows/intent_detector 2.py +0 -298
  34. empathy_os/meta_workflows/pattern_learner 2.py +0 -754
  35. empathy_os/meta_workflows/session_context 2.py +0 -398
  36. empathy_os/meta_workflows/template_registry 2.py +0 -229
  37. empathy_os/meta_workflows/workflow 2.py +0 -980
  38. empathy_os/orchestration/pattern_learner 2.py +0 -699
  39. empathy_os/orchestration/real_tools 2.py +0 -938
  40. empathy_os/socratic/__init__ 2.py +0 -273
  41. empathy_os/socratic/ab_testing 2.py +0 -969
  42. empathy_os/socratic/blueprint 2.py +0 -532
  43. empathy_os/socratic/cli 2.py +0 -689
  44. empathy_os/socratic/collaboration 2.py +0 -1112
  45. empathy_os/socratic/domain_templates 2.py +0 -916
  46. empathy_os/socratic/embeddings 2.py +0 -734
  47. empathy_os/socratic/engine 2.py +0 -729
  48. empathy_os/socratic/explainer 2.py +0 -663
  49. empathy_os/socratic/feedback 2.py +0 -767
  50. empathy_os/socratic/forms 2.py +0 -624
  51. empathy_os/socratic/generator 2.py +0 -716
  52. empathy_os/socratic/llm_analyzer 2.py +0 -635
  53. empathy_os/socratic/mcp_server 2.py +0 -751
  54. empathy_os/socratic/session 2.py +0 -306
  55. empathy_os/socratic/storage 2.py +0 -635
  56. empathy_os/socratic/success 2.py +0 -719
  57. empathy_os/socratic/visual_editor 2.py +0 -812
  58. empathy_os/socratic/web_ui 2.py +0 -925
  59. empathy_os/workflows/batch_processing 2.py +0 -310
  60. empathy_os/workflows/release_prep_crew 2.py +0 -968
  61. empathy_os/workflows/test_coverage_boost_crew 2.py +0 -848
  62. {empathy_framework-4.6.3.dist-info → empathy_framework-4.6.5.dist-info}/WHEEL +0 -0
  63. {empathy_framework-4.6.3.dist-info → empathy_framework-4.6.5.dist-info}/entry_points.txt +0 -0
  64. {empathy_framework-4.6.3.dist-info → empathy_framework-4.6.5.dist-info}/licenses/LICENSE +0 -0
  65. {empathy_framework-4.6.3.dist-info → empathy_framework-4.6.5.dist-info}/top_level.txt +0 -0
@@ -1,812 +0,0 @@
1
- """Visual Workflow Editor
2
-
3
- Provides a visual editor for modifying generated workflow blueprints.
4
- Supports both terminal-based visualization and web-based React components.
5
-
6
- Features:
7
- - ASCII art workflow visualization for terminal
8
- - Drag-and-drop capable React schemas
9
- - Agent configuration panels
10
- - Stage dependency editing
11
- - Real-time validation
12
-
13
- Copyright 2026 Smart-AI-Memory
14
- Licensed under Fair Source License 0.9
15
- """
16
-
17
- from __future__ import annotations
18
-
19
- import json
20
- from dataclasses import dataclass, field
21
- from enum import Enum
22
- from typing import Any
23
-
24
- from .blueprint import StageSpec, WorkflowBlueprint
25
-
26
- # =============================================================================
27
- # DATA STRUCTURES
28
- # =============================================================================
29
-
30
-
31
- class NodeType(Enum):
32
- """Types of nodes in the visual editor."""
33
-
34
- AGENT = "agent"
35
- STAGE = "stage"
36
- START = "start"
37
- END = "end"
38
- CONNECTOR = "connector"
39
-
40
-
41
- @dataclass
42
- class Position:
43
- """Position in the visual editor."""
44
-
45
- x: int
46
- y: int
47
-
48
- def to_dict(self) -> dict[str, int]:
49
- return {"x": self.x, "y": self.y}
50
-
51
-
52
- @dataclass
53
- class EditorNode:
54
- """A node in the visual editor."""
55
-
56
- node_id: str
57
- node_type: NodeType
58
- label: str
59
- position: Position
60
- data: dict[str, Any] = field(default_factory=dict)
61
- selected: bool = False
62
- locked: bool = False
63
-
64
- def to_dict(self) -> dict[str, Any]:
65
- return {
66
- "id": self.node_id,
67
- "type": self.node_type.value,
68
- "label": self.label,
69
- "position": self.position.to_dict(),
70
- "data": self.data,
71
- "selected": self.selected,
72
- "locked": self.locked,
73
- }
74
-
75
-
76
- @dataclass
77
- class EditorEdge:
78
- """An edge (connection) in the visual editor."""
79
-
80
- edge_id: str
81
- source_id: str
82
- target_id: str
83
- label: str = ""
84
- animated: bool = False
85
-
86
- def to_dict(self) -> dict[str, Any]:
87
- return {
88
- "id": self.edge_id,
89
- "source": self.source_id,
90
- "target": self.target_id,
91
- "label": self.label,
92
- "animated": self.animated,
93
- }
94
-
95
-
96
- @dataclass
97
- class EditorState:
98
- """State of the visual editor."""
99
-
100
- nodes: list[EditorNode]
101
- edges: list[EditorEdge]
102
- selected_node_id: str | None = None
103
- zoom: float = 1.0
104
- pan_x: int = 0
105
- pan_y: int = 0
106
-
107
- def to_dict(self) -> dict[str, Any]:
108
- return {
109
- "nodes": [n.to_dict() for n in self.nodes],
110
- "edges": [e.to_dict() for e in self.edges],
111
- "selectedNodeId": self.selected_node_id,
112
- "zoom": self.zoom,
113
- "panX": self.pan_x,
114
- "panY": self.pan_y,
115
- }
116
-
117
-
118
- # =============================================================================
119
- # WORKFLOW TO EDITOR CONVERSION
120
- # =============================================================================
121
-
122
-
123
- class WorkflowVisualizer:
124
- """Converts workflow blueprints to visual editor state."""
125
-
126
- def __init__(self, node_spacing: int = 200, stage_spacing: int = 150):
127
- """Initialize visualizer.
128
-
129
- Args:
130
- node_spacing: Horizontal spacing between nodes
131
- stage_spacing: Vertical spacing between stages
132
- """
133
- self.node_spacing = node_spacing
134
- self.stage_spacing = stage_spacing
135
-
136
- def blueprint_to_editor(self, blueprint: WorkflowBlueprint) -> EditorState:
137
- """Convert a workflow blueprint to editor state.
138
-
139
- Args:
140
- blueprint: The workflow blueprint
141
-
142
- Returns:
143
- EditorState ready for visualization
144
- """
145
- nodes: list[EditorNode] = []
146
- edges: list[EditorEdge] = []
147
-
148
- # Create agent lookup
149
- agents_by_id = {a.agent_id: a for a in blueprint.agents}
150
-
151
- # Create start node
152
- start_node = EditorNode(
153
- node_id="start",
154
- node_type=NodeType.START,
155
- label="Start",
156
- position=Position(x=400, y=50),
157
- locked=True,
158
- )
159
- nodes.append(start_node)
160
-
161
- # Process stages
162
- y_offset = 150
163
- first_stage = True
164
-
165
- for stage in blueprint.stages:
166
- # Create stage node
167
- stage_node = EditorNode(
168
- node_id=stage.stage_id,
169
- node_type=NodeType.STAGE,
170
- label=stage.name,
171
- position=Position(x=400, y=y_offset),
172
- data={
173
- "parallel": stage.parallel,
174
- "timeout": stage.timeout_seconds,
175
- },
176
- )
177
- nodes.append(stage_node)
178
-
179
- # Connect from start or dependencies
180
- if first_stage:
181
- edges.append(EditorEdge(
182
- edge_id=f"start->{stage.stage_id}",
183
- source_id="start",
184
- target_id=stage.stage_id,
185
- animated=True,
186
- ))
187
- first_stage = False
188
- else:
189
- for dep in stage.dependencies:
190
- edges.append(EditorEdge(
191
- edge_id=f"{dep}->{stage.stage_id}",
192
- source_id=dep,
193
- target_id=stage.stage_id,
194
- ))
195
-
196
- # Create agent nodes for this stage
197
- agent_x_start = 200
198
- for i, agent_id in enumerate(stage.agent_ids):
199
- agent = agents_by_id.get(agent_id)
200
- if not agent:
201
- continue
202
-
203
- agent_node = EditorNode(
204
- node_id=agent_id,
205
- node_type=NodeType.AGENT,
206
- label=agent.name,
207
- position=Position(
208
- x=agent_x_start + (i * self.node_spacing),
209
- y=y_offset + 60,
210
- ),
211
- data={
212
- "role": agent.role.value,
213
- "tools": [t.tool_id for t in agent.tools],
214
- "description": agent.description,
215
- },
216
- )
217
- nodes.append(agent_node)
218
-
219
- # Connect stage to agent
220
- edges.append(EditorEdge(
221
- edge_id=f"{stage.stage_id}->{agent_id}",
222
- source_id=stage.stage_id,
223
- target_id=agent_id,
224
- ))
225
-
226
- y_offset += self.stage_spacing
227
-
228
- # Create end node
229
- end_node = EditorNode(
230
- node_id="end",
231
- node_type=NodeType.END,
232
- label="End",
233
- position=Position(x=400, y=y_offset),
234
- locked=True,
235
- )
236
- nodes.append(end_node)
237
-
238
- # Connect last stage to end
239
- if blueprint.stages:
240
- last_stage = blueprint.stages[-1]
241
- edges.append(EditorEdge(
242
- edge_id=f"{last_stage.stage_id}->end",
243
- source_id=last_stage.stage_id,
244
- target_id="end",
245
- animated=True,
246
- ))
247
-
248
- return EditorState(nodes=nodes, edges=edges)
249
-
250
- def editor_to_blueprint(
251
- self,
252
- state: EditorState,
253
- original_blueprint: WorkflowBlueprint,
254
- ) -> WorkflowBlueprint:
255
- """Convert editor state back to workflow blueprint.
256
-
257
- Args:
258
- state: The editor state
259
- original_blueprint: Original blueprint for reference
260
-
261
- Returns:
262
- Updated WorkflowBlueprint
263
- """
264
- # Extract stage nodes and their agents
265
- stage_nodes = [n for n in state.nodes if n.node_type == NodeType.STAGE]
266
- agent_nodes = [n for n in state.nodes if n.node_type == NodeType.AGENT]
267
-
268
- # Build agent lookup from original
269
- agents_by_id = {a.agent_id: a for a in original_blueprint.agents}
270
-
271
- # Build edge lookup
272
- edges_by_source: dict[str, list[str]] = {}
273
- edges_by_target: dict[str, list[str]] = {}
274
- for edge in state.edges:
275
- edges_by_source.setdefault(edge.source_id, []).append(edge.target_id)
276
- edges_by_target.setdefault(edge.target_id, []).append(edge.source_id)
277
-
278
- # Rebuild stages
279
- new_stages: list[StageSpec] = []
280
- for stage_node in stage_nodes:
281
- # Find agents connected to this stage
282
- agent_ids = [
283
- target
284
- for target in edges_by_source.get(stage_node.node_id, [])
285
- if any(a.node_id == target and a.node_type == NodeType.AGENT for a in agent_nodes)
286
- ]
287
-
288
- # Find dependencies (stages that connect TO this stage)
289
- dependencies = [
290
- source
291
- for source in edges_by_target.get(stage_node.node_id, [])
292
- if source != "start" and any(
293
- s.node_id == source and s.node_type == NodeType.STAGE
294
- for s in stage_nodes
295
- )
296
- ]
297
-
298
- new_stages.append(StageSpec(
299
- stage_id=stage_node.node_id,
300
- name=stage_node.label,
301
- agent_ids=agent_ids,
302
- dependencies=dependencies,
303
- parallel=stage_node.data.get("parallel", False),
304
- timeout_seconds=stage_node.data.get("timeout"),
305
- ))
306
-
307
- # Update blueprint
308
- return WorkflowBlueprint(
309
- blueprint_id=original_blueprint.blueprint_id,
310
- name=original_blueprint.name,
311
- description=original_blueprint.description,
312
- domains=original_blueprint.domains,
313
- agents=original_blueprint.agents,
314
- stages=new_stages,
315
- created_at=original_blueprint.created_at,
316
- )
317
-
318
-
319
- # =============================================================================
320
- # ASCII VISUALIZATION
321
- # =============================================================================
322
-
323
-
324
- class ASCIIVisualizer:
325
- """Creates ASCII art visualization of workflows for terminal display."""
326
-
327
- def __init__(self, width: int = 80):
328
- """Initialize ASCII visualizer.
329
-
330
- Args:
331
- width: Maximum width in characters
332
- """
333
- self.width = width
334
-
335
- def render(self, blueprint: WorkflowBlueprint) -> str:
336
- """Render workflow as ASCII art.
337
-
338
- Args:
339
- blueprint: The workflow blueprint
340
-
341
- Returns:
342
- ASCII art string
343
- """
344
- lines: list[str] = []
345
-
346
- # Header
347
- lines.append("=" * self.width)
348
- lines.append(self._center(f"Workflow: {blueprint.name}"))
349
- lines.append("=" * self.width)
350
- lines.append("")
351
-
352
- # Agents summary
353
- lines.append(self._box("Agents"))
354
- for agent in blueprint.agents:
355
- tools = ", ".join(t.tool_id for t in agent.tools[:3])
356
- if len(agent.tools) > 3:
357
- tools += f" (+{len(agent.tools) - 3} more)"
358
- lines.append(f" [{agent.role.value[:3].upper()}] {agent.name}")
359
- lines.append(f" Tools: {tools}")
360
- lines.append("")
361
-
362
- # Flow diagram
363
- lines.append(self._box("Workflow Flow"))
364
- lines.append("")
365
- lines.append(self._center("[ START ]"))
366
- lines.append(self._center("│"))
367
-
368
- for i, stage in enumerate(blueprint.stages):
369
- is_last = i == len(blueprint.stages) - 1
370
-
371
- # Stage box
372
- stage_line = f"┌{'─' * (len(stage.name) + 2)}┐"
373
- lines.append(self._center(stage_line))
374
- lines.append(self._center(f"│ {stage.name} │"))
375
- lines.append(self._center(f"└{'─' * (len(stage.name) + 2)}┘"))
376
-
377
- # Agents in stage
378
- if stage.agent_ids:
379
- agent_str = " → ".join(stage.agent_ids)
380
- if len(agent_str) > self.width - 10:
381
- agent_str = agent_str[:self.width - 13] + "..."
382
- lines.append(self._center(f"({agent_str})"))
383
-
384
- # Connector
385
- if not is_last:
386
- lines.append(self._center("│"))
387
- if blueprint.stages[i + 1].parallel:
388
- lines.append(self._center("║ (parallel)"))
389
- else:
390
- lines.append(self._center("│"))
391
-
392
- lines.append(self._center("│"))
393
- lines.append(self._center("[ END ]"))
394
- lines.append("")
395
-
396
- # Footer
397
- lines.append("=" * self.width)
398
-
399
- return "\n".join(lines)
400
-
401
- def render_compact(self, blueprint: WorkflowBlueprint) -> str:
402
- """Render a compact single-line representation.
403
-
404
- Args:
405
- blueprint: The workflow blueprint
406
-
407
- Returns:
408
- Compact string representation
409
- """
410
- stages = []
411
- for stage in blueprint.stages:
412
- agents = ",".join(a[:8] for a in stage.agent_ids)
413
- parallel = "∥" if stage.parallel else "→"
414
- stages.append(f"[{stage.name}:{agents}]")
415
-
416
- return f" {' → '.join(stages)} "
417
-
418
- def _center(self, text: str) -> str:
419
- """Center text within width."""
420
- if len(text) >= self.width:
421
- return text
422
- padding = (self.width - len(text)) // 2
423
- return " " * padding + text
424
-
425
- def _box(self, title: str) -> str:
426
- """Create a section header box."""
427
- return f"┌─ {title} {'─' * (self.width - len(title) - 5)}┐"
428
-
429
-
430
- # =============================================================================
431
- # REACT COMPONENT SCHEMAS
432
- # =============================================================================
433
-
434
-
435
- def generate_react_flow_schema(state: EditorState) -> dict[str, Any]:
436
- """Generate React Flow compatible schema.
437
-
438
- Args:
439
- state: Editor state
440
-
441
- Returns:
442
- Schema for React Flow library
443
- """
444
- # Node types for React Flow
445
- node_type_map = {
446
- NodeType.START: "input",
447
- NodeType.END: "output",
448
- NodeType.STAGE: "default",
449
- NodeType.AGENT: "default",
450
- }
451
-
452
- # Node styles
453
- node_styles = {
454
- NodeType.START: {
455
- "background": "#10b981",
456
- "color": "white",
457
- "border": "2px solid #059669",
458
- "borderRadius": "50%",
459
- "width": 80,
460
- "height": 80,
461
- },
462
- NodeType.END: {
463
- "background": "#ef4444",
464
- "color": "white",
465
- "border": "2px solid #dc2626",
466
- "borderRadius": "50%",
467
- "width": 80,
468
- "height": 80,
469
- },
470
- NodeType.STAGE: {
471
- "background": "#3b82f6",
472
- "color": "white",
473
- "border": "2px solid #2563eb",
474
- "borderRadius": "8px",
475
- "padding": "10px",
476
- },
477
- NodeType.AGENT: {
478
- "background": "#8b5cf6",
479
- "color": "white",
480
- "border": "2px solid #7c3aed",
481
- "borderRadius": "8px",
482
- "padding": "8px",
483
- },
484
- }
485
-
486
- nodes = []
487
- for node in state.nodes:
488
- rf_node = {
489
- "id": node.node_id,
490
- "type": node_type_map.get(node.node_type, "default"),
491
- "position": node.position.to_dict(),
492
- "data": {
493
- "label": node.label,
494
- **node.data,
495
- },
496
- "style": node_styles.get(node.node_type, {}),
497
- "draggable": not node.locked,
498
- "selectable": True,
499
- }
500
- nodes.append(rf_node)
501
-
502
- edges = []
503
- for edge in state.edges:
504
- rf_edge = {
505
- "id": edge.edge_id,
506
- "source": edge.source_id,
507
- "target": edge.target_id,
508
- "label": edge.label,
509
- "animated": edge.animated,
510
- "style": {"strokeWidth": 2},
511
- "markerEnd": {"type": "arrowclosed"},
512
- }
513
- edges.append(rf_edge)
514
-
515
- return {
516
- "nodes": nodes,
517
- "edges": edges,
518
- "defaultViewport": {
519
- "x": state.pan_x,
520
- "y": state.pan_y,
521
- "zoom": state.zoom,
522
- },
523
- }
524
-
525
-
526
- def generate_editor_html(
527
- blueprint: WorkflowBlueprint,
528
- title: str = "Workflow Editor",
529
- ) -> str:
530
- """Generate standalone HTML page with workflow editor.
531
-
532
- Args:
533
- blueprint: The workflow blueprint
534
- title: Page title
535
-
536
- Returns:
537
- Complete HTML page
538
- """
539
- visualizer = WorkflowVisualizer()
540
- state = visualizer.blueprint_to_editor(blueprint)
541
- react_schema = generate_react_flow_schema(state)
542
-
543
- return f"""<!DOCTYPE html>
544
- <html lang="en">
545
- <head>
546
- <meta charset="UTF-8">
547
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
548
- <title>{title}</title>
549
- <script src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
550
- <script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
551
- <script src="https://unpkg.com/reactflow@11/dist/umd/index.js"></script>
552
- <link href="https://unpkg.com/reactflow@11/dist/style.css" rel="stylesheet" />
553
- <style>
554
- * {{ margin: 0; padding: 0; box-sizing: border-box; }}
555
- body {{ font-family: system-ui, -apple-system, sans-serif; }}
556
- #root {{ width: 100vw; height: 100vh; }}
557
- .react-flow__node {{ font-size: 12px; }}
558
- .react-flow__edge-path {{ stroke-width: 2; }}
559
-
560
- .panel {{
561
- position: absolute;
562
- top: 10px;
563
- left: 10px;
564
- background: white;
565
- padding: 15px;
566
- border-radius: 8px;
567
- box-shadow: 0 2px 10px rgba(0,0,0,0.1);
568
- z-index: 1000;
569
- }}
570
-
571
- .panel h3 {{
572
- margin-bottom: 10px;
573
- font-size: 14px;
574
- color: #374151;
575
- }}
576
-
577
- .panel p {{
578
- font-size: 12px;
579
- color: #6b7280;
580
- margin-bottom: 5px;
581
- }}
582
-
583
- .export-btn {{
584
- margin-top: 10px;
585
- padding: 8px 16px;
586
- background: #3b82f6;
587
- color: white;
588
- border: none;
589
- border-radius: 4px;
590
- cursor: pointer;
591
- font-size: 12px;
592
- }}
593
-
594
- .export-btn:hover {{ background: #2563eb; }}
595
- </style>
596
- </head>
597
- <body>
598
- <div id="root"></div>
599
- <script>
600
- const {{ useState, useCallback }} = React;
601
- const {{ ReactFlow, Background, Controls, MiniMap }} = window.ReactFlow;
602
-
603
- const initialNodes = {json.dumps(react_schema['nodes'], indent=2)};
604
- const initialEdges = {json.dumps(react_schema['edges'], indent=2)};
605
-
606
- function WorkflowEditor() {{
607
- const [nodes, setNodes] = useState(initialNodes);
608
- const [edges, setEdges] = useState(initialEdges);
609
- const [selectedNode, setSelectedNode] = useState(null);
610
-
611
- const onNodesChange = useCallback((changes) => {{
612
- setNodes((nds) => {{
613
- return nds.map((node) => {{
614
- const change = changes.find(c => c.id === node.id);
615
- if (change && change.type === 'position' && change.position) {{
616
- return {{ ...node, position: change.position }};
617
- }}
618
- return node;
619
- }});
620
- }});
621
- }}, []);
622
-
623
- const onNodeClick = useCallback((event, node) => {{
624
- setSelectedNode(node);
625
- }}, []);
626
-
627
- const exportWorkflow = () => {{
628
- const data = {{ nodes, edges }};
629
- const blob = new Blob([JSON.stringify(data, null, 2)], {{ type: 'application/json' }});
630
- const url = URL.createObjectURL(blob);
631
- const a = document.createElement('a');
632
- a.href = url;
633
- a.download = 'workflow.json';
634
- a.click();
635
- }};
636
-
637
- return React.createElement('div', {{ style: {{ width: '100%', height: '100%' }} }},
638
- React.createElement(ReactFlow, {{
639
- nodes: nodes,
640
- edges: edges,
641
- onNodesChange: onNodesChange,
642
- onNodeClick: onNodeClick,
643
- fitView: true,
644
- }},
645
- React.createElement(Background, null),
646
- React.createElement(Controls, null),
647
- React.createElement(MiniMap, null)
648
- ),
649
- React.createElement('div', {{ className: 'panel' }},
650
- React.createElement('h3', null, '{blueprint.name}'),
651
- React.createElement('p', null, 'Agents: {len(blueprint.agents)}'),
652
- React.createElement('p', null, 'Stages: {len(blueprint.stages)}'),
653
- selectedNode && React.createElement('div', null,
654
- React.createElement('hr', {{ style: {{ margin: '10px 0' }} }}),
655
- React.createElement('p', null, 'Selected: ' + selectedNode.data.label),
656
- selectedNode.data.role && React.createElement('p', null, 'Role: ' + selectedNode.data.role),
657
- selectedNode.data.tools && React.createElement('p', null, 'Tools: ' + selectedNode.data.tools.length)
658
- ),
659
- React.createElement('button', {{
660
- className: 'export-btn',
661
- onClick: exportWorkflow
662
- }}, 'Export JSON')
663
- )
664
- );
665
- }}
666
-
667
- const root = ReactDOM.createRoot(document.getElementById('root'));
668
- root.render(React.createElement(WorkflowEditor));
669
- </script>
670
- </body>
671
- </html>"""
672
-
673
-
674
- # =============================================================================
675
- # VISUAL EDITOR CLASS
676
- # =============================================================================
677
-
678
-
679
- class VisualWorkflowEditor:
680
- """High-level API for visual workflow editing."""
681
-
682
- def __init__(self):
683
- """Initialize the editor."""
684
- self.visualizer = WorkflowVisualizer()
685
- self.ascii_visualizer = ASCIIVisualizer()
686
-
687
- def create_editor_state(self, blueprint: WorkflowBlueprint) -> EditorState:
688
- """Create editor state from blueprint.
689
-
690
- Args:
691
- blueprint: The workflow blueprint
692
-
693
- Returns:
694
- EditorState for the editor
695
- """
696
- return self.visualizer.blueprint_to_editor(blueprint)
697
-
698
- def apply_changes(
699
- self,
700
- state: EditorState,
701
- original_blueprint: WorkflowBlueprint,
702
- ) -> WorkflowBlueprint:
703
- """Apply editor changes to create updated blueprint.
704
-
705
- Args:
706
- state: Modified editor state
707
- original_blueprint: Original blueprint
708
-
709
- Returns:
710
- Updated WorkflowBlueprint
711
- """
712
- return self.visualizer.editor_to_blueprint(state, original_blueprint)
713
-
714
- def render_ascii(self, blueprint: WorkflowBlueprint) -> str:
715
- """Render workflow as ASCII art.
716
-
717
- Args:
718
- blueprint: The workflow blueprint
719
-
720
- Returns:
721
- ASCII art visualization
722
- """
723
- return self.ascii_visualizer.render(blueprint)
724
-
725
- def render_compact(self, blueprint: WorkflowBlueprint) -> str:
726
- """Render compact representation.
727
-
728
- Args:
729
- blueprint: The workflow blueprint
730
-
731
- Returns:
732
- Compact string
733
- """
734
- return self.ascii_visualizer.render_compact(blueprint)
735
-
736
- def generate_html_editor(self, blueprint: WorkflowBlueprint) -> str:
737
- """Generate HTML page with interactive editor.
738
-
739
- Args:
740
- blueprint: The workflow blueprint
741
-
742
- Returns:
743
- Complete HTML page
744
- """
745
- return generate_editor_html(blueprint)
746
-
747
- def generate_react_schema(self, blueprint: WorkflowBlueprint) -> dict[str, Any]:
748
- """Generate React Flow compatible schema.
749
-
750
- Args:
751
- blueprint: The workflow blueprint
752
-
753
- Returns:
754
- React Flow schema
755
- """
756
- state = self.visualizer.blueprint_to_editor(blueprint)
757
- return generate_react_flow_schema(state)
758
-
759
- def validate_state(self, state: EditorState) -> list[str]:
760
- """Validate editor state for errors.
761
-
762
- Args:
763
- state: The editor state
764
-
765
- Returns:
766
- List of validation errors (empty if valid)
767
- """
768
- errors: list[str] = []
769
-
770
- # Check for required nodes
771
- has_start = any(n.node_type == NodeType.START for n in state.nodes)
772
- has_end = any(n.node_type == NodeType.END for n in state.nodes)
773
-
774
- if not has_start:
775
- errors.append("Workflow must have a start node")
776
- if not has_end:
777
- errors.append("Workflow must have an end node")
778
-
779
- # Check for orphan nodes (no connections)
780
- node_ids = {n.node_id for n in state.nodes}
781
- connected_nodes: set[str] = set()
782
- for edge in state.edges:
783
- connected_nodes.add(edge.source_id)
784
- connected_nodes.add(edge.target_id)
785
-
786
- orphans = node_ids - connected_nodes - {"start", "end"}
787
- if orphans:
788
- errors.append(f"Orphan nodes (not connected): {', '.join(orphans)}")
789
-
790
- # Check for cycles (simple detection)
791
- # Note: A more robust implementation would use DFS
792
- visited: set[str] = set()
793
- def check_cycle(node_id: str, path: set[str]) -> bool:
794
- if node_id in path:
795
- return True
796
- if node_id in visited:
797
- return False
798
-
799
- visited.add(node_id)
800
- path.add(node_id)
801
-
802
- for edge in state.edges:
803
- if edge.source_id == node_id:
804
- if check_cycle(edge.target_id, path.copy()):
805
- return True
806
-
807
- return False
808
-
809
- if check_cycle("start", set()):
810
- errors.append("Workflow contains a cycle")
811
-
812
- return errors