fantaxy commited on
Commit
794a971
·
verified ·
1 Parent(s): e3cf867

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +498 -0
app.py ADDED
@@ -0,0 +1,498 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ MOUSE Workflow - Visual Workflow Builder with UI Execution
3
+ @Powered by VIDraft
4
+ ✓ Visual workflow designer with drag-and-drop
5
+ ✓ Import/Export JSON with copy-paste support
6
+ ✓ Auto-generate UI from workflow for end-user execution
7
+ """
8
+
9
+ import os, json, typing, tempfile
10
+ import gradio as gr
11
+ from gradio_workflowbuilder import WorkflowBuilder
12
+
13
+ # -------------------------------------------------------------------
14
+ # 🛠️ 헬퍼 함수들
15
+ # -------------------------------------------------------------------
16
+ def export_pretty(data: typing.Dict[str, typing.Any]) -> str:
17
+ return json.dumps(data, indent=2, ensure_ascii=False) if data else "No workflow to export"
18
+
19
+ def export_file(data: typing.Dict[str, typing.Any]) -> typing.Optional[str]:
20
+ """워크플로우를 JSON 파일로 내보내기"""
21
+ if not data:
22
+ return None
23
+ fd, path = tempfile.mkstemp(suffix=".json", prefix="workflow_")
24
+ try:
25
+ with os.fdopen(fd, "w", encoding="utf-8") as f:
26
+ json.dump(data, f, ensure_ascii=False, indent=2)
27
+ return path
28
+ except Exception as e:
29
+ print(f"Error exporting file: {e}")
30
+ return None
31
+
32
+ def load_json_from_text_or_file(json_text: str, file_obj) -> typing.Tuple[typing.Dict[str, typing.Any], str]:
33
+ """텍스트 또는 파일에서 JSON 로드"""
34
+ # 파일이 있으면 파일 우선
35
+ if file_obj is not None:
36
+ try:
37
+ with open(file_obj.name, "r", encoding="utf-8") as f:
38
+ json_text = f.read()
39
+ except Exception as e:
40
+ return None, f"❌ Error reading file: {str(e)}"
41
+
42
+ # JSON 텍스트가 없거나 비어있으면
43
+ if not json_text or json_text.strip() == "":
44
+ return None, "No JSON data provided"
45
+
46
+ try:
47
+ # JSON 파싱
48
+ data = json.loads(json_text.strip())
49
+
50
+ # 데이터 검증
51
+ if not isinstance(data, dict):
52
+ return None, "Invalid format: not a dictionary"
53
+
54
+ # 필수 필드 확인
55
+ if 'nodes' not in data:
56
+ data['nodes'] = []
57
+ if 'edges' not in data:
58
+ data['edges'] = []
59
+
60
+ nodes_count = len(data.get('nodes', []))
61
+ edges_count = len(data.get('edges', []))
62
+
63
+ return data, f"✅ Loaded: {nodes_count} nodes, {edges_count} edges"
64
+
65
+ except json.JSONDecodeError as e:
66
+ return None, f"❌ JSON parsing error: {str(e)}"
67
+ except Exception as e:
68
+ return None, f"❌ Error: {str(e)}"
69
+
70
+ def create_sample_workflow():
71
+ """샘플 워크플로우 생성"""
72
+ return {
73
+ "nodes": [
74
+ {
75
+ "id": "input_1",
76
+ "type": "ChatInput",
77
+ "position": {"x": 100, "y": 200},
78
+ "data": {
79
+ "label": "User Question",
80
+ "template": {
81
+ "input_value": {"value": "What is the capital of Korea?"}
82
+ }
83
+ }
84
+ },
85
+ {
86
+ "id": "llm_1",
87
+ "type": "llmNode",
88
+ "position": {"x": 400, "y": 200},
89
+ "data": {
90
+ "label": "AI Processing",
91
+ "template": {
92
+ "model": {"value": "gpt-3.5-turbo"},
93
+ "temperature": {"value": 0.7},
94
+ "system_prompt": {"value": "You are a helpful assistant."}
95
+ }
96
+ }
97
+ },
98
+ {
99
+ "id": "output_1",
100
+ "type": "ChatOutput",
101
+ "position": {"x": 700, "y": 200},
102
+ "data": {"label": "Answer"}
103
+ }
104
+ ],
105
+ "edges": [
106
+ {"id": "e1", "source": "input_1", "target": "llm_1"},
107
+ {"id": "e2", "source": "llm_1", "target": "output_1"}
108
+ ]
109
+ }
110
+
111
+ # UI 실행을 위한 간단한 실행 함수
112
+ def execute_workflow_simple(workflow_data: dict, input_values: dict) -> dict:
113
+ """워크플로우 실행 시뮬레이션"""
114
+ results = {}
115
+
116
+ # 입력 노드 처리
117
+ for node in workflow_data.get("nodes", []):
118
+ node_id = node.get("id")
119
+ node_type = node.get("type", "")
120
+
121
+ if node_type in ["ChatInput", "textInput", "Input"]:
122
+ # UI에서 제공된 입력값 사용
123
+ if node_id in input_values:
124
+ results[node_id] = input_values[node_id]
125
+ else:
126
+ # 기본값 사용
127
+ template = node.get("data", {}).get("template", {})
128
+ default_value = template.get("input_value", {}).get("value", "")
129
+ results[node_id] = default_value
130
+
131
+ elif node_type == "llmNode":
132
+ # LLM 처리 시뮬레이션
133
+ # 실제로는 여기서 API 호출을 하겠지만, 지금은 시뮬레이션
134
+ input_text = ""
135
+
136
+ # 연결된 입력 찾기
137
+ for edge in workflow_data.get("edges", []):
138
+ if edge.get("target") == node_id:
139
+ source_id = edge.get("source")
140
+ if source_id in results:
141
+ input_text = results[source_id]
142
+ break
143
+
144
+ # 간단한 응답 생성
145
+ results[node_id] = f"[AI Response to: {input_text}]"
146
+
147
+ elif node_type in ["ChatOutput", "textOutput", "Output"]:
148
+ # 출력 노드는 연결된 노드의 결과를 가져옴
149
+ for edge in workflow_data.get("edges", []):
150
+ if edge.get("target") == node_id:
151
+ source_id = edge.get("source")
152
+ if source_id in results:
153
+ results[node_id] = results[source_id]
154
+ break
155
+
156
+ return results
157
+
158
+ # -------------------------------------------------------------------
159
+ # 🎨 CSS
160
+ # -------------------------------------------------------------------
161
+ CSS = """
162
+ .main-container{max-width:1600px;margin:0 auto;}
163
+ .workflow-section{margin-bottom:2rem;min-height:500px;}
164
+ .button-row{display:flex;gap:1rem;justify-content:center;margin:1rem 0;}
165
+ .status-box{
166
+ padding:10px;border-radius:5px;margin-top:10px;
167
+ background:#f0f9ff;border:1px solid #3b82f6;color:#1e40af;
168
+ }
169
+ .component-description{
170
+ padding:24px;background:linear-gradient(135deg,#f8fafc 0%,#e2e8f0 100%);
171
+ border-left:4px solid #3b82f6;border-radius:12px;
172
+ box-shadow:0 2px 8px rgba(0,0,0,.05);margin:16px 0;
173
+ }
174
+ .workflow-container{position:relative;}
175
+ .ui-execution-section{
176
+ background:linear-gradient(135deg,#f0fdf4 0%,#dcfce7 100%);
177
+ padding:24px;border-radius:12px;margin:24px 0;
178
+ border:1px solid #86efac;
179
+ }
180
+ .powered-by{
181
+ text-align:center;color:#64748b;font-size:14px;
182
+ margin-top:8px;font-style:italic;
183
+ }
184
+ """
185
+
186
+ # -------------------------------------------------------------------
187
+ # 🖥️ Gradio 앱
188
+ # -------------------------------------------------------------------
189
+ with gr.Blocks(title="🐭 MOUSE Workflow", theme=gr.themes.Soft(), css=CSS) as demo:
190
+
191
+ with gr.Column(elem_classes=["main-container"]):
192
+ gr.Markdown("# 🐭 MOUSE Workflow")
193
+ gr.Markdown("**Visual Workflow Builder with Interactive UI Execution**")
194
+ gr.HTML('<p class="powered-by">@Powered by VIDraft</p>')
195
+
196
+ gr.HTML(
197
+ """
198
+ <div class="component-description">
199
+ <p style="font-size:16px;margin:0;">Build sophisticated workflows visually • Import/Export JSON • Generate interactive UI for end-users</p>
200
+ </div>
201
+ """
202
+ )
203
+
204
+ # State for storing workflow data
205
+ loaded_data = gr.State(None)
206
+ trigger_update = gr.State(False)
207
+
208
+ # ─── Dynamic Workflow Container ───
209
+ with gr.Column(elem_classes=["workflow-container"]):
210
+ @gr.render(inputs=[loaded_data, trigger_update])
211
+ def render_workflow(data, trigger):
212
+ """동적으로 WorkflowBuilder 렌더링"""
213
+ workflow_value = data if data else {"nodes": [], "edges": []}
214
+
215
+ return WorkflowBuilder(
216
+ label="🎨 Visual Workflow Designer",
217
+ info="Drag from sidebar → Connect nodes → Edit properties",
218
+ value=workflow_value,
219
+ elem_id="main_workflow"
220
+ )
221
+
222
+ # ─── Import Section ───
223
+ with gr.Accordion("📥 Import Workflow", open=True):
224
+ with gr.Row():
225
+ with gr.Column(scale=2):
226
+ import_json_text = gr.Code(
227
+ language="json",
228
+ label="Paste JSON here",
229
+ lines=8,
230
+ value='{\n "nodes": [],\n "edges": []\n}'
231
+ )
232
+ with gr.Column(scale=1):
233
+ file_upload = gr.File(
234
+ label="Or upload JSON file",
235
+ file_types=[".json"],
236
+ type="filepath"
237
+ )
238
+ btn_load = gr.Button("📥 Load Workflow", variant="primary", size="lg")
239
+ btn_sample = gr.Button("🎯 Load Sample", variant="secondary")
240
+
241
+ # Status
242
+ status_text = gr.Textbox(
243
+ label="Status",
244
+ value="Ready",
245
+ elem_classes=["status-box"],
246
+ interactive=False
247
+ )
248
+
249
+ # ─── Export Section ───
250
+ gr.Markdown("## 💾 Export")
251
+
252
+ with gr.Row():
253
+ with gr.Column(scale=3):
254
+ export_preview = gr.Code(
255
+ language="json",
256
+ label="Current Workflow JSON",
257
+ lines=8
258
+ )
259
+ with gr.Column(scale=1):
260
+ btn_preview = gr.Button("👁️ Preview JSON", size="lg")
261
+ btn_download = gr.DownloadButton("💾 Download JSON", size="lg")
262
+
263
+ # ─── UI Execution Section ───
264
+ with gr.Column(elem_classes=["ui-execution-section"]):
265
+ gr.Markdown("## 🚀 UI Execution")
266
+ gr.Markdown("Generate an interactive UI from your workflow for end-users")
267
+
268
+ btn_execute_ui = gr.Button("▶️ Generate & Run UI", variant="primary", size="lg")
269
+
270
+ # UI execution state
271
+ ui_workflow_data = gr.State(None)
272
+
273
+ # Dynamic UI container
274
+ @gr.render(inputs=[ui_workflow_data])
275
+ def render_execution_ui(workflow_data):
276
+ if not workflow_data or not workflow_data.get("nodes"):
277
+ gr.Markdown("*Load a workflow first, then click 'Generate & Run UI'*")
278
+ return
279
+
280
+ gr.Markdown("### 📋 Generated UI")
281
+
282
+ # Extract input and output nodes
283
+ input_nodes = []
284
+ output_nodes = []
285
+
286
+ for node in workflow_data.get("nodes", []):
287
+ node_type = node.get("type", "")
288
+ if node_type in ["ChatInput", "textInput", "Input", "numberInput"]:
289
+ input_nodes.append(node)
290
+ elif node_type in ["ChatOutput", "textOutput", "Output"]:
291
+ output_nodes.append(node)
292
+
293
+ # Create input components
294
+ input_components = {}
295
+
296
+ if input_nodes:
297
+ gr.Markdown("#### 📥 Inputs")
298
+ for node in input_nodes:
299
+ node_id = node.get("id")
300
+ label = node.get("data", {}).get("label", node_id)
301
+ node_type = node.get("type")
302
+
303
+ # Get default value
304
+ template = node.get("data", {}).get("template", {})
305
+ default_value = template.get("input_value", {}).get("value", "")
306
+
307
+ if node_type == "numberInput":
308
+ input_components[node_id] = gr.Number(
309
+ label=label,
310
+ value=float(default_value) if default_value else 0
311
+ )
312
+ else:
313
+ input_components[node_id] = gr.Textbox(
314
+ label=label,
315
+ value=default_value,
316
+ lines=2,
317
+ placeholder="Enter your input..."
318
+ )
319
+
320
+ # Execute button
321
+ execute_btn = gr.Button("🎯 Execute", variant="primary")
322
+
323
+ # Create output components
324
+ output_components = {}
325
+
326
+ if output_nodes:
327
+ gr.Markdown("#### 📤 Outputs")
328
+ for node in output_nodes:
329
+ node_id = node.get("id")
330
+ label = node.get("data", {}).get("label", node_id)
331
+
332
+ output_components[node_id] = gr.Textbox(
333
+ label=label,
334
+ interactive=False,
335
+ lines=3
336
+ )
337
+
338
+ # Execution log
339
+ gr.Markdown("#### 📊 Execution Log")
340
+ log_output = gr.Textbox(
341
+ label="Log",
342
+ interactive=False,
343
+ lines=5
344
+ )
345
+
346
+ # Define execution handler
347
+ def execute_ui_workflow(*input_values):
348
+ # Create input dictionary
349
+ inputs_dict = {}
350
+ input_keys = list(input_components.keys())
351
+ for i, key in enumerate(input_keys):
352
+ if i < len(input_values):
353
+ inputs_dict[key] = input_values[i]
354
+
355
+ # Execute workflow
356
+ log = f"Executing workflow with {len(inputs_dict)} inputs...\n"
357
+
358
+ try:
359
+ results = execute_workflow_simple(workflow_data, inputs_dict)
360
+
361
+ # Prepare outputs
362
+ output_values = []
363
+ for node_id in output_components.keys():
364
+ value = results.get(node_id, "No output")
365
+ output_values.append(value)
366
+ log += f"Output {node_id}: {value}\n"
367
+
368
+ log += "\n✅ Execution completed!"
369
+ output_values.append(log)
370
+
371
+ return output_values
372
+
373
+ except Exception as e:
374
+ error_msg = f"❌ Error: {str(e)}"
375
+ log += error_msg
376
+ return [error_msg] * len(output_components) + [log]
377
+
378
+ # Connect execution
379
+ all_inputs = list(input_components.values())
380
+ all_outputs = list(output_components.values()) + [log_output]
381
+
382
+ execute_btn.click(
383
+ fn=execute_ui_workflow,
384
+ inputs=all_inputs,
385
+ outputs=all_outputs
386
+ )
387
+
388
+ # ─── Event Handlers ───
389
+
390
+ # Load workflow (from text or file)
391
+ def load_workflow(json_text, file_obj):
392
+ data, status = load_json_from_text_or_file(json_text, file_obj)
393
+ if data:
394
+ return data, status, json_text if not file_obj else export_pretty(data)
395
+ else:
396
+ return None, status, gr.update()
397
+
398
+ btn_load.click(
399
+ fn=load_workflow,
400
+ inputs=[import_json_text, file_upload],
401
+ outputs=[loaded_data, status_text, import_json_text]
402
+ ).then(
403
+ fn=lambda current_trigger: not current_trigger,
404
+ inputs=trigger_update,
405
+ outputs=trigger_update
406
+ )
407
+
408
+ # Auto-load when file is uploaded
409
+ file_upload.change(
410
+ fn=load_workflow,
411
+ inputs=[import_json_text, file_upload],
412
+ outputs=[loaded_data, status_text, import_json_text]
413
+ ).then(
414
+ fn=lambda current_trigger: not current_trigger,
415
+ inputs=trigger_update,
416
+ outputs=trigger_update
417
+ )
418
+
419
+ # Load sample
420
+ btn_sample.click(
421
+ fn=lambda: (create_sample_workflow(), "✅ Sample loaded", export_pretty(create_sample_workflow())),
422
+ outputs=[loaded_data, status_text, import_json_text]
423
+ ).then(
424
+ fn=lambda current_trigger: not current_trigger,
425
+ inputs=trigger_update,
426
+ outputs=trigger_update
427
+ )
428
+
429
+ # Preview current workflow
430
+ btn_preview.click(
431
+ fn=export_pretty,
432
+ inputs=loaded_data,
433
+ outputs=export_preview
434
+ )
435
+
436
+ # Download workflow
437
+ btn_download.click(
438
+ fn=export_file,
439
+ inputs=loaded_data
440
+ )
441
+
442
+ # Generate UI execution
443
+ btn_execute_ui.click(
444
+ fn=lambda data: data,
445
+ inputs=loaded_data,
446
+ outputs=ui_workflow_data
447
+ )
448
+
449
+ # Auto-update export preview when workflow changes
450
+ loaded_data.change(
451
+ fn=export_pretty,
452
+ inputs=loaded_data,
453
+ outputs=export_preview
454
+ )
455
+
456
+ # ─── Instructions ───
457
+ with gr.Accordion("📖 How to Use", open=False):
458
+ gr.Markdown(
459
+ """
460
+ ### 🚀 Quick Start
461
+
462
+ 1. **Import Workflow**
463
+ - Paste JSON directly in the code editor
464
+ - Or upload a JSON file
465
+ - Click "Load Workflow" to visualize
466
+
467
+ 2. **Design Workflow**
468
+ - Drag components from sidebar
469
+ - Connect nodes (output → input)
470
+ - Edit node properties
471
+
472
+ 3. **Export Workflow**
473
+ - Preview JSON with "Preview JSON" button
474
+ - Download with "Download JSON" button
475
+
476
+ 4. **Generate UI**
477
+ - Click "Generate & Run UI" button
478
+ - Fill in the auto-generated inputs
479
+ - Click "Execute" to run the workflow
480
+
481
+ ### 💡 Features
482
+ - Visual workflow designer
483
+ - JSON import/export with copy-paste
484
+ - Auto-generate UI for end-users
485
+ - Real-time workflow execution
486
+
487
+ ### 🎯 Node Types
488
+ - **Input**: ChatInput, textInput, numberInput
489
+ - **Processing**: llmNode, textProcessor
490
+ - **Output**: ChatOutput, textOutput
491
+ """
492
+ )
493
+
494
+ # -------------------------------------------------------------------
495
+ # 🚀 실행
496
+ # -------------------------------------------------------------------
497
+ if __name__ == "__main__":
498
+ demo.launch(server_name="0.0.0.0", show_error=True)