openfree commited on
Commit
6d9621e
·
verified ·
1 Parent(s): 9570fbb

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +921 -0
app.py ADDED
@@ -0,0 +1,921 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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, traceback
10
+ import gradio as gr
11
+ from gradio_workflowbuilder import WorkflowBuilder
12
+
13
+ # Optional imports for LLM APIs
14
+ try:
15
+ from openai import OpenAI
16
+ OPENAI_AVAILABLE = True
17
+ except ImportError:
18
+ OPENAI_AVAILABLE = False
19
+ print("OpenAI library not available. Install with: pip install openai")
20
+
21
+ # Anthropic 관련 코드 주석 처리
22
+ # try:
23
+ # import anthropic
24
+ # ANTHROPIC_AVAILABLE = True
25
+ # except ImportError:
26
+ # ANTHROPIC_AVAILABLE = False
27
+ # print("Anthropic library not available. Install with: pip install anthropic")
28
+ ANTHROPIC_AVAILABLE = False
29
+
30
+ try:
31
+ import requests
32
+ REQUESTS_AVAILABLE = True
33
+ except ImportError:
34
+ REQUESTS_AVAILABLE = False
35
+ print("Requests library not available. Install with: pip install requests")
36
+
37
+ # -------------------------------------------------------------------
38
+ # 🛠️ 헬퍼 함수들
39
+ # -------------------------------------------------------------------
40
+ def export_pretty(data: typing.Dict[str, typing.Any]) -> str:
41
+ return json.dumps(data, indent=2, ensure_ascii=False) if data else "No workflow to export"
42
+
43
+ def export_file(data: typing.Dict[str, typing.Any]) -> typing.Optional[str]:
44
+ """워크플로우를 JSON 파일로 내보내기"""
45
+ if not data:
46
+ return None
47
+ fd, path = tempfile.mkstemp(suffix=".json", prefix="workflow_")
48
+ try:
49
+ with os.fdopen(fd, "w", encoding="utf-8") as f:
50
+ json.dump(data, f, ensure_ascii=False, indent=2)
51
+ return path
52
+ except Exception as e:
53
+ print(f"Error exporting file: {e}")
54
+ return None
55
+
56
+ def load_json_from_text_or_file(json_text: str, file_obj) -> typing.Tuple[typing.Dict[str, typing.Any], str]:
57
+ """텍스트 또는 파일에서 JSON 로드"""
58
+ # 파일이 있으면 파일 우선
59
+ if file_obj is not None:
60
+ try:
61
+ with open(file_obj.name, "r", encoding="utf-8") as f:
62
+ json_text = f.read()
63
+ except Exception as e:
64
+ return None, f"❌ Error reading file: {str(e)}"
65
+
66
+ # JSON 텍스트가 없거나 비어있으면
67
+ if not json_text or json_text.strip() == "":
68
+ return None, "No JSON data provided"
69
+
70
+ try:
71
+ # JSON 파싱
72
+ data = json.loads(json_text.strip())
73
+
74
+ # 데이터 검증
75
+ if not isinstance(data, dict):
76
+ return None, "Invalid format: not a dictionary"
77
+
78
+ # 필수 필드 확인
79
+ if 'nodes' not in data:
80
+ data['nodes'] = []
81
+ if 'edges' not in data:
82
+ data['edges'] = []
83
+
84
+ nodes_count = len(data.get('nodes', []))
85
+ edges_count = len(data.get('edges', []))
86
+
87
+ return data, f"✅ Loaded: {nodes_count} nodes, {edges_count} edges"
88
+
89
+ except json.JSONDecodeError as e:
90
+ return None, f"❌ JSON parsing error: {str(e)}"
91
+ except Exception as e:
92
+ return None, f"❌ Error: {str(e)}"
93
+
94
+ def create_sample_workflow(example_type="basic"):
95
+ """샘플 워크플로우 생성"""
96
+
97
+ if example_type == "basic":
98
+ # 기본 예제: 간단한 Q&A
99
+ return {
100
+ "nodes": [
101
+ {
102
+ "id": "input_1",
103
+ "type": "ChatInput",
104
+ "position": {"x": 100, "y": 200},
105
+ "data": {
106
+ "label": "User Question",
107
+ "template": {
108
+ "input_value": {"value": "What is the capital of Korea?"}
109
+ }
110
+ }
111
+ },
112
+ {
113
+ "id": "llm_1",
114
+ "type": "llmNode",
115
+ "position": {"x": 400, "y": 200},
116
+ "data": {
117
+ "label": "AI Processing",
118
+ "template": {
119
+ "provider": {"value": "OpenAI"},
120
+ "model": {"value": "gpt-4.1-mini"},
121
+ "temperature": {"value": 0.7},
122
+ "system_prompt": {"value": "You are a helpful assistant."}
123
+ }
124
+ }
125
+ },
126
+ {
127
+ "id": "output_1",
128
+ "type": "ChatOutput",
129
+ "position": {"x": 700, "y": 200},
130
+ "data": {"label": "Answer"}
131
+ }
132
+ ],
133
+ "edges": [
134
+ {"id": "e1", "source": "input_1", "target": "llm_1"},
135
+ {"id": "e2", "source": "llm_1", "target": "output_1"}
136
+ ]
137
+ }
138
+
139
+ elif example_type == "vidraft":
140
+ # VIDraft 예제
141
+ return {
142
+ "nodes": [
143
+ {
144
+ "id": "input_1",
145
+ "type": "ChatInput",
146
+ "position": {"x": 100, "y": 200},
147
+ "data": {
148
+ "label": "User Input",
149
+ "template": {
150
+ "input_value": {"value": "AI와 머신러닝의 차이점을 설명해주세요."}
151
+ }
152
+ }
153
+ },
154
+ {
155
+ "id": "llm_1",
156
+ "type": "llmNode",
157
+ "position": {"x": 400, "y": 200},
158
+ "data": {
159
+ "label": "VIDraft AI (Gemma)",
160
+ "template": {
161
+ "provider": {"value": "VIDraft"},
162
+ "model": {"value": "Gemma-3-r1984-27B"},
163
+ "temperature": {"value": 0.8},
164
+ "system_prompt": {"value": "당신은 전문적이고 친절한 AI 교육자입니다. 복잡한 개념을 쉽게 설명해주세요."}
165
+ }
166
+ }
167
+ },
168
+ {
169
+ "id": "output_1",
170
+ "type": "ChatOutput",
171
+ "position": {"x": 700, "y": 200},
172
+ "data": {"label": "AI Explanation"}
173
+ }
174
+ ],
175
+ "edges": [
176
+ {"id": "e1", "source": "input_1", "target": "llm_1"},
177
+ {"id": "e2", "source": "llm_1", "target": "output_1"}
178
+ ]
179
+ }
180
+
181
+ elif example_type == "multi_input":
182
+ # 다중 입력 예제
183
+ return {
184
+ "nodes": [
185
+ {
186
+ "id": "name_input",
187
+ "type": "textInput",
188
+ "position": {"x": 100, "y": 100},
189
+ "data": {
190
+ "label": "Your Name",
191
+ "template": {
192
+ "input_value": {"value": "John"}
193
+ }
194
+ }
195
+ },
196
+ {
197
+ "id": "topic_input",
198
+ "type": "textInput",
199
+ "position": {"x": 100, "y": 250},
200
+ "data": {
201
+ "label": "Topic",
202
+ "template": {
203
+ "input_value": {"value": "Python programming"}
204
+ }
205
+ }
206
+ },
207
+ {
208
+ "id": "level_input",
209
+ "type": "textInput",
210
+ "position": {"x": 100, "y": 400},
211
+ "data": {
212
+ "label": "Skill Level",
213
+ "template": {
214
+ "input_value": {"value": "beginner"}
215
+ }
216
+ }
217
+ },
218
+ {
219
+ "id": "combiner",
220
+ "type": "textNode",
221
+ "position": {"x": 350, "y": 250},
222
+ "data": {
223
+ "label": "Combine Inputs",
224
+ "template": {
225
+ "text": {"value": "Create a personalized learning plan"}
226
+ }
227
+ }
228
+ },
229
+ {
230
+ "id": "llm_1",
231
+ "type": "llmNode",
232
+ "position": {"x": 600, "y": 250},
233
+ "data": {
234
+ "label": "Generate Learning Plan",
235
+ "template": {
236
+ "provider": {"value": "OpenAI"},
237
+ "model": {"value": "gpt-4.1-mini"},
238
+ "temperature": {"value": 0.7},
239
+ "system_prompt": {"value": "You are an expert educational consultant. Create personalized learning plans based on the user's name, topic of interest, and skill level."}
240
+ }
241
+ }
242
+ },
243
+ {
244
+ "id": "output_1",
245
+ "type": "ChatOutput",
246
+ "position": {"x": 900, "y": 250},
247
+ "data": {"label": "Your Learning Plan"}
248
+ }
249
+ ],
250
+ "edges": [
251
+ {"id": "e1", "source": "name_input", "target": "combiner"},
252
+ {"id": "e2", "source": "topic_input", "target": "combiner"},
253
+ {"id": "e3", "source": "level_input", "target": "combiner"},
254
+ {"id": "e4", "source": "combiner", "target": "llm_1"},
255
+ {"id": "e5", "source": "llm_1", "target": "output_1"}
256
+ ]
257
+ }
258
+
259
+ elif example_type == "chain":
260
+ # 체인 처리 예제
261
+ return {
262
+ "nodes": [
263
+ {
264
+ "id": "input_1",
265
+ "type": "ChatInput",
266
+ "position": {"x": 50, "y": 200},
267
+ "data": {
268
+ "label": "Original Text",
269
+ "template": {
270
+ "input_value": {"value": "The quick brown fox jumps over the lazy dog."}
271
+ }
272
+ }
273
+ },
274
+ {
275
+ "id": "translator",
276
+ "type": "llmNode",
277
+ "position": {"x": 300, "y": 200},
278
+ "data": {
279
+ "label": "Translate to Korean",
280
+ "template": {
281
+ "provider": {"value": "VIDraft"},
282
+ "model": {"value": "Gemma-3-r1984-27B"},
283
+ "temperature": {"value": 0.3},
284
+ "system_prompt": {"value": "You are a professional translator. Translate the given English text to Korean accurately."}
285
+ }
286
+ }
287
+ },
288
+ {
289
+ "id": "analyzer",
290
+ "type": "llmNode",
291
+ "position": {"x": 600, "y": 200},
292
+ "data": {
293
+ "label": "Analyze Translation",
294
+ "template": {
295
+ "provider": {"value": "OpenAI"},
296
+ "model": {"value": "gpt-4.1-mini"},
297
+ "temperature": {"value": 0.5},
298
+ "system_prompt": {"value": "You are a linguistic expert. Analyze the Korean translation and explain its nuances and cultural context."}
299
+ }
300
+ }
301
+ },
302
+ {
303
+ "id": "output_translation",
304
+ "type": "ChatOutput",
305
+ "position": {"x": 450, "y": 350},
306
+ "data": {"label": "Korean Translation"}
307
+ },
308
+ {
309
+ "id": "output_analysis",
310
+ "type": "ChatOutput",
311
+ "position": {"x": 900, "y": 200},
312
+ "data": {"label": "Translation Analysis"}
313
+ }
314
+ ],
315
+ "edges": [
316
+ {"id": "e1", "source": "input_1", "target": "translator"},
317
+ {"id": "e2", "source": "translator", "target": "analyzer"},
318
+ {"id": "e3", "source": "translator", "target": "output_translation"},
319
+ {"id": "e4", "source": "analyzer", "target": "output_analysis"}
320
+ ]
321
+ }
322
+
323
+ # 기본값은 basic
324
+ return create_sample_workflow("basic")
325
+
326
+ # UI 실행을 위한 실제 워크플로우 실행 함수
327
+ def execute_workflow_simple(workflow_data: dict, input_values: dict) -> dict:
328
+ """워크플로우 실제 실행"""
329
+ import traceback
330
+
331
+ # API 키 확인
332
+ vidraft_token = os.getenv("FRIENDLI_TOKEN") # VIDraft/Friendli token
333
+ openai_key = os.getenv("OPENAI_API_KEY")
334
+ # anthropic_key = os.getenv("ANTHROPIC_API_KEY") # 주석 처리
335
+
336
+ # OpenAI 라이브러리 확인
337
+ try:
338
+ from openai import OpenAI
339
+ openai_available = True
340
+ except ImportError:
341
+ openai_available = False
342
+ print("OpenAI library not available")
343
+
344
+ # Anthropic 라이브러리 확인 - 주석 처리
345
+ # try:
346
+ # import anthropic
347
+ # anthropic_available = True
348
+ # except ImportError:
349
+ # anthropic_available = False
350
+ # print("Anthropic library not available")
351
+ anthropic_available = False
352
+
353
+ results = {}
354
+ nodes = workflow_data.get("nodes", [])
355
+ edges = workflow_data.get("edges", [])
356
+
357
+ # 노드를 순서대로 처리
358
+ for node in nodes:
359
+ node_id = node.get("id")
360
+ node_type = node.get("type", "")
361
+ node_data = node.get("data", {})
362
+
363
+ try:
364
+ elif node_type in ["ChatInput", "textInput", "Input"]:
365
+ # UI에서 제공된 입력값 사용
366
+ if node_id in input_values:
367
+ results[node_id] = input_values[node_id]
368
+ else:
369
+ # 기본값 사용
370
+ template = node_data.get("template", {})
371
+ default_value = template.get("input_value", {}).get("value", "")
372
+ results[node_id] = default_value
373
+
374
+ elif node_type == "textNode":
375
+ # 텍스트 노드는 연결된 모든 입력을 결합
376
+ template = node_data.get("template", {})
377
+ base_text = template.get("text", {}).get("value", "")
378
+
379
+ # 연결된 입력들 수집
380
+ connected_inputs = []
381
+ for edge in edges:
382
+ if edge.get("target") == node_id:
383
+ source_id = edge.get("source")
384
+ if source_id in results:
385
+ connected_inputs.append(f"{source_id}: {results[source_id]}")
386
+
387
+ # 결합된 텍스트 생성
388
+ if connected_inputs:
389
+ combined_text = f"{base_text}\n\nInputs:\n" + "\n".join(connected_inputs)
390
+ results[node_id] = combined_text
391
+ else:
392
+ results[node_id] = base_text
393
+
394
+ elif node_type in ["llmNode", "OpenAIModel", "ChatModel"]:
395
+ # LLM 노드 처리
396
+ template = node_data.get("template", {})
397
+
398
+ # 프로바이더 정보 추출 - VIDraft 또는 OpenAI만 허용
399
+ provider_info = template.get("provider", {})
400
+ provider = provider_info.get("value", "OpenAI") if isinstance(provider_info, dict) else "OpenAI"
401
+
402
+ # provider가 VIDraft 또는 OpenAI가 아닌 경우 OpenAI로 기본 설정
403
+ if provider not in ["VIDraft", "OpenAI"]:
404
+ provider = "OpenAI"
405
+
406
+ # 모델 정보 추출
407
+ if provider == "OpenAI":
408
+ # OpenAI는 gpt-4.1-mini로 고정
409
+ model = "gpt-4.1-mini"
410
+ elif provider == "VIDraft":
411
+ # VIDraft는 Gemma-3-r1984-27B로 고정
412
+ model = "Gemma-3-r1984-27B"
413
+ else:
414
+ model = "gpt-4.1-mini" # 기본값
415
+
416
+ # 온도 정보 추출
417
+ temp_info = template.get("temperature", {})
418
+ temperature = temp_info.get("value", 0.7) if isinstance(temp_info, dict) else 0.7
419
+
420
+ # 시스템 프롬프트 추출
421
+ prompt_info = template.get("system_prompt", {})
422
+ system_prompt = prompt_info.get("value", "") if isinstance(prompt_info, dict) else ""
423
+
424
+ # 입력 텍스트 찾기
425
+ input_text = ""
426
+ for edge in edges:
427
+ if edge.get("target") == node_id:
428
+ source_id = edge.get("source")
429
+ if source_id in results:
430
+ input_text = results[source_id]
431
+ break
432
+
433
+ # 실제 API 호출
434
+ if provider == "OpenAI" and openai_key and openai_available:
435
+ try:
436
+ client = OpenAI(api_key=openai_key)
437
+
438
+ messages = []
439
+ if system_prompt:
440
+ messages.append({"role": "system", "content": system_prompt})
441
+ messages.append({"role": "user", "content": input_text})
442
+
443
+ response = client.chat.completions.create(
444
+ model="gpt-4.1-mini", # 고정된 모델명
445
+ messages=messages,
446
+ temperature=temperature,
447
+ max_tokens=1000
448
+ )
449
+
450
+ results[node_id] = response.choices[0].message.content
451
+
452
+ except Exception as e:
453
+ results[node_id] = f"[OpenAI Error: {str(e)}]"
454
+
455
+ # Anthropic 관련 코드 주석 처리
456
+ # elif provider == "Anthropic" and anthropic_key and anthropic_available:
457
+ # try:
458
+ # client = anthropic.Anthropic(api_key=anthropic_key)
459
+ #
460
+ # message = client.messages.create(
461
+ # model="claude-3-haiku-20240307",
462
+ # max_tokens=1000,
463
+ # temperature=temperature,
464
+ # system=system_prompt if system_prompt else None,
465
+ # messages=[{"role": "user", "content": input_text}]
466
+ # )
467
+ #
468
+ # results[node_id] = message.content[0].text
469
+ #
470
+ # except Exception as e:
471
+ # results[node_id] = f"[Anthropic Error: {str(e)}]"
472
+
473
+ elif provider == "VIDraft" and vidraft_token:
474
+ try:
475
+ import requests
476
+
477
+ headers = {
478
+ "Authorization": f"Bearer {vidraft_token}",
479
+ "Content-Type": "application/json"
480
+ }
481
+
482
+ # 메시지 구성
483
+ messages = []
484
+ if system_prompt:
485
+ messages.append({"role": "system", "content": system_prompt})
486
+ messages.append({"role": "user", "content": input_text})
487
+
488
+ payload = {
489
+ "model": "dep89a2fld32mcm", # VIDraft 모델 ID
490
+ "messages": messages,
491
+ "max_tokens": 16384,
492
+ "temperature": temperature,
493
+ "top_p": 0.8,
494
+ "stream": False # 동기 실행을 위해 False로 설정
495
+ }
496
+
497
+ # VIDraft API endpoint
498
+ response = requests.post(
499
+ "https://api.friendli.ai/dedicated/v1/chat/completions",
500
+ headers=headers,
501
+ json=payload,
502
+ timeout=30
503
+ )
504
+
505
+ if response.status_code == 200:
506
+ response_json = response.json()
507
+ results[node_id] = response_json["choices"][0]["message"]["content"]
508
+ else:
509
+ results[node_id] = f"[VIDraft API Error: {response.status_code} - {response.text}]"
510
+
511
+ except Exception as e:
512
+ results[node_id] = f"[VIDraft Error: {str(e)}]"
513
+
514
+ else:
515
+ # API 키가 없는 경우 시뮬레이션
516
+ results[node_id] = f"[Simulated {provider} Response to: {input_text[:50]}...]"
517
+
518
+ elif node_type in ["ChatOutput", "textOutput", "Output"]:
519
+ # 출력 노드는 연결된 노드의 결과를 가져옴
520
+ for edge in edges:
521
+ if edge.get("target") == node_id:
522
+ source_id = edge.get("source")
523
+ if source_id in results:
524
+ results[node_id] = results[source_id]
525
+ break
526
+
527
+ except Exception as e:
528
+ results[node_id] = f"[Node Error: {str(e)}]"
529
+ print(f"Error processing node {node_id}: {traceback.format_exc()}")
530
+
531
+ return results
532
+
533
+ # -------------------------------------------------------------------
534
+ # 🎨 CSS
535
+ # -------------------------------------------------------------------
536
+ CSS = """
537
+ .main-container{max-width:1600px;margin:0 auto;}
538
+ .workflow-section{margin-bottom:2rem;min-height:500px;}
539
+ .button-row{display:flex;gap:1rem;justify-content:center;margin:1rem 0;}
540
+ .status-box{
541
+ padding:10px;border-radius:5px;margin-top:10px;
542
+ background:#f0f9ff;border:1px solid #3b82f6;color:#1e40af;
543
+ }
544
+ .component-description{
545
+ padding:24px;background:linear-gradient(135deg,#f8fafc 0%,#e2e8f0 100%);
546
+ border-left:4px solid #3b82f6;border-radius:12px;
547
+ box-shadow:0 2px 8px rgba(0,0,0,.05);margin:16px 0;
548
+ }
549
+ .workflow-container{position:relative;}
550
+ .ui-execution-section{
551
+ background:linear-gradient(135deg,#f0fdf4 0%,#dcfce7 100%);
552
+ padding:24px;border-radius:12px;margin:24px 0;
553
+ border:1px solid #86efac;
554
+ }
555
+ .powered-by{
556
+ text-align:center;color:#64748b;font-size:14px;
557
+ margin-top:8px;font-style:italic;
558
+ }
559
+ .sample-buttons{
560
+ display:grid;grid-template-columns:1fr 1fr;gap:0.5rem;
561
+ margin-top:0.5rem;
562
+ }
563
+ """
564
+
565
+ # -------------------------------------------------------------------
566
+ # 🖥️ Gradio 앱
567
+ # -------------------------------------------------------------------
568
+ with gr.Blocks(title="🐭 MOUSE Workflow", theme=gr.themes.Soft(), css=CSS) as demo:
569
+
570
+ with gr.Column(elem_classes=["main-container"]):
571
+ gr.Markdown("# 🐭 MOUSE Workflow")
572
+ gr.Markdown("**Visual Workflow Builder with Interactive UI Execution**")
573
+ gr.HTML('<p class="powered-by">@Powered by VIDraft & Huggingface gradio</p>')
574
+
575
+ gr.HTML(
576
+ """
577
+ <div class="component-description">
578
+ <p style="font-size:16px;margin:0;">Build sophisticated workflows visually • Import/Export JSON • Generate interactive UI for end-users</p>
579
+ </div>
580
+ """
581
+ )
582
+
583
+ # API Status Display
584
+ with gr.Accordion("🔌 API Status", open=False):
585
+ gr.Markdown(f"""
586
+ **Available APIs:**
587
+ - FRIENDLI_TOKEN (VIDraft): {'✅ Connected' if os.getenv("FRIENDLI_TOKEN") else '❌ Not found'}
588
+ - OPENAI_API_KEY: {'✅ Connected' if os.getenv("OPENAI_API_KEY") else '❌ Not found'}
589
+
590
+ **Libraries:**
591
+ - OpenAI: {'✅ Installed' if OPENAI_AVAILABLE else '❌ Not installed'}
592
+ - Requests: {'✅ Installed' if REQUESTS_AVAILABLE else '❌ Not installed'}
593
+
594
+ **Available Models:**
595
+ - OpenAI: gpt-4.1-mini (fixed)
596
+ - VIDraft: Gemma-3-r1984-27B (model ID: dep89a2fld32mcm)
597
+
598
+ **Sample Workflows:**
599
+ - Basic Q&A: Simple question-answer flow
600
+ - VIDraft: Korean language example with Gemma model
601
+ - Multi-Input: Combine multiple inputs for personalized output
602
+ - Chain: Sequential processing with multiple outputs
603
+
604
+ *Note: Without API keys, the UI will simulate AI responses.*
605
+ """)
606
+
607
+ # State for storing workflow data
608
+ loaded_data = gr.State(None)
609
+ trigger_update = gr.State(False)
610
+
611
+ # ─── Dynamic Workflow Container ───
612
+ with gr.Column(elem_classes=["workflow-container"]):
613
+ @gr.render(inputs=[loaded_data, trigger_update])
614
+ def render_workflow(data, trigger):
615
+ """동적으로 WorkflowBuilder 렌더링"""
616
+ workflow_value = data if data else {"nodes": [], "edges": []}
617
+
618
+ return WorkflowBuilder(
619
+ label="🎨 Visual Workflow Designer",
620
+ info="Drag from sidebar → Connect nodes → Edit properties",
621
+ value=workflow_value,
622
+ elem_id="main_workflow"
623
+ )
624
+
625
+ # ─── Import Section ───
626
+ with gr.Accordion("📥 Import Workflow", open=True):
627
+ with gr.Row():
628
+ with gr.Column(scale=2):
629
+ import_json_text = gr.Code(
630
+ language="json",
631
+ label="Paste JSON here",
632
+ lines=8,
633
+ value='{\n "nodes": [],\n "edges": []\n}'
634
+ )
635
+ with gr.Column(scale=1):
636
+ file_upload = gr.File(
637
+ label="Or upload JSON file",
638
+ file_types=[".json"],
639
+ type="filepath"
640
+ )
641
+ btn_load = gr.Button("📥 Load Workflow", variant="primary", size="lg")
642
+
643
+ # Sample buttons
644
+ gr.Markdown("**Sample Workflows:**")
645
+ with gr.Row():
646
+ btn_sample_basic = gr.Button("🎯 Basic Q&A", variant="secondary", scale=1)
647
+ btn_sample_vidraft = gr.Button("🤖 VIDraft", variant="secondary", scale=1)
648
+ with gr.Row():
649
+ btn_sample_multi = gr.Button("📝 Multi-Input", variant="secondary", scale=1)
650
+ btn_sample_chain = gr.Button("🔗 Chain", variant="secondary", scale=1)
651
+
652
+ # Status
653
+ status_text = gr.Textbox(
654
+ label="Status",
655
+ value="Ready",
656
+ elem_classes=["status-box"],
657
+ interactive=False
658
+ )
659
+
660
+ # ─── Export Section ───
661
+ gr.Markdown("## 💾 Export")
662
+
663
+ with gr.Row():
664
+ with gr.Column(scale=3):
665
+ export_preview = gr.Code(
666
+ language="json",
667
+ label="Current Workflow JSON",
668
+ lines=8
669
+ )
670
+ with gr.Column(scale=1):
671
+ btn_preview = gr.Button("👁️ Preview JSON", size="lg")
672
+ btn_download = gr.DownloadButton("💾 Download JSON", size="lg")
673
+
674
+ # ─── UI Execution Section ───
675
+ with gr.Column(elem_classes=["ui-execution-section"]):
676
+ gr.Markdown("## 🚀 UI Execution")
677
+ gr.Markdown("Generate an interactive UI from your workflow for end-users")
678
+
679
+ btn_execute_ui = gr.Button("▶️ Generate & Run UI", variant="primary", size="lg")
680
+
681
+ # UI execution state
682
+ ui_workflow_data = gr.State(None)
683
+
684
+ # Dynamic UI container
685
+ @gr.render(inputs=[ui_workflow_data])
686
+ def render_execution_ui(workflow_data):
687
+ if not workflow_data or not workflow_data.get("nodes"):
688
+ gr.Markdown("*Load a workflow first, then click 'Generate & Run UI'*")
689
+ return
690
+
691
+ gr.Markdown("### 📋 Generated UI")
692
+
693
+ # Extract input and output nodes
694
+ input_nodes = []
695
+ output_nodes = []
696
+
697
+ for node in workflow_data.get("nodes", []):
698
+ node_type = node.get("type", "")
699
+ if node_type in ["ChatInput", "textInput", "Input", "numberInput"]:
700
+ input_nodes.append(node)
701
+ elif node_type in ["ChatOutput", "textOutput", "Output"]:
702
+ output_nodes.append(node)
703
+ elif node_type == "textNode":
704
+ # textNode는 중간 처리 노드로, UI에는 표시하지 않음
705
+ pass
706
+
707
+ # Create input components
708
+ input_components = {}
709
+
710
+ if input_nodes:
711
+ gr.Markdown("#### 📥 Inputs")
712
+ for node in input_nodes:
713
+ node_id = node.get("id")
714
+ label = node.get("data", {}).get("label", node_id)
715
+ node_type = node.get("type")
716
+
717
+ # Get default value
718
+ template = node.get("data", {}).get("template", {})
719
+ default_value = template.get("input_value", {}).get("value", "")
720
+
721
+ if node_type == "numberInput":
722
+ input_components[node_id] = gr.Number(
723
+ label=label,
724
+ value=float(default_value) if default_value else 0
725
+ )
726
+ else:
727
+ input_components[node_id] = gr.Textbox(
728
+ label=label,
729
+ value=default_value,
730
+ lines=2,
731
+ placeholder="Enter your input..."
732
+ )
733
+
734
+ # Execute button
735
+ execute_btn = gr.Button("🎯 Execute", variant="primary")
736
+
737
+ # Create output components
738
+ output_components = {}
739
+
740
+ if output_nodes:
741
+ gr.Markdown("#### 📤 Outputs")
742
+ for node in output_nodes:
743
+ node_id = node.get("id")
744
+ label = node.get("data", {}).get("label", node_id)
745
+
746
+ output_components[node_id] = gr.Textbox(
747
+ label=label,
748
+ interactive=False,
749
+ lines=3
750
+ )
751
+
752
+ # Execution log
753
+ gr.Markdown("#### 📊 Execution Log")
754
+ log_output = gr.Textbox(
755
+ label="Log",
756
+ interactive=False,
757
+ lines=5
758
+ )
759
+
760
+ # Define execution handler
761
+ def execute_ui_workflow(*input_values):
762
+ # Create input dictionary
763
+ inputs_dict = {}
764
+ input_keys = list(input_components.keys())
765
+ for i, key in enumerate(input_keys):
766
+ if i < len(input_values):
767
+ inputs_dict[key] = input_values[i]
768
+
769
+ # Check API status
770
+ log = "=== Workflow Execution Started ===\n"
771
+ log += f"Inputs provided: {len(inputs_dict)}\n"
772
+
773
+ # API 상태 확인
774
+ vidraft_token = os.getenv("FRIENDLI_TOKEN")
775
+ openai_key = os.getenv("OPENAI_API_KEY")
776
+
777
+ log += "\nAPI Status:\n"
778
+ log += f"- FRIENDLI_TOKEN (VIDraft): {'✅ Found' if vidraft_token else '❌ Not found'}\n"
779
+ log += f"- OPENAI_API_KEY: {'✅ Found' if openai_key else '❌ Not found'}\n"
780
+
781
+ if not vidraft_token and not openai_key:
782
+ log += "\n⚠️ No API keys found. Results will be simulated.\n"
783
+ log += "To get real AI responses, set API keys in environment variables.\n"
784
+
785
+ log += "\n--- Processing Nodes ---\n"
786
+
787
+ try:
788
+ results = execute_workflow_simple(workflow_data, inputs_dict)
789
+
790
+ # Prepare outputs
791
+ output_values = []
792
+ for node_id in output_components.keys():
793
+ value = results.get(node_id, "No output")
794
+ output_values.append(value)
795
+
796
+ # Log 길이 제한
797
+ display_value = value[:100] + "..." if len(str(value)) > 100 else value
798
+ log += f"\nOutput [{node_id}]: {display_value}\n"
799
+
800
+ log += "\n=== Execution Completed Successfully! ===\n"
801
+ output_values.append(log)
802
+
803
+ return output_values
804
+
805
+ except Exception as e:
806
+ error_msg = f"❌ Error: {str(e)}"
807
+ log += f"\n{error_msg}\n"
808
+ log += "=== Execution Failed ===\n"
809
+ return [error_msg] * len(output_components) + [log]
810
+
811
+ # Connect execution
812
+ all_inputs = list(input_components.values())
813
+ all_outputs = list(output_components.values()) + [log_output]
814
+
815
+ execute_btn.click(
816
+ fn=execute_ui_workflow,
817
+ inputs=all_inputs,
818
+ outputs=all_outputs
819
+ )
820
+
821
+ # ─── Event Handlers ───
822
+
823
+ # Load workflow (from text or file)
824
+ def load_workflow(json_text, file_obj):
825
+ data, status = load_json_from_text_or_file(json_text, file_obj)
826
+ if data:
827
+ return data, status, json_text if not file_obj else export_pretty(data)
828
+ else:
829
+ return None, status, gr.update()
830
+
831
+ btn_load.click(
832
+ fn=load_workflow,
833
+ inputs=[import_json_text, file_upload],
834
+ outputs=[loaded_data, status_text, import_json_text]
835
+ ).then(
836
+ fn=lambda current_trigger: not current_trigger,
837
+ inputs=trigger_update,
838
+ outputs=trigger_update
839
+ )
840
+
841
+ # Auto-load when file is uploaded
842
+ file_upload.change(
843
+ fn=load_workflow,
844
+ inputs=[import_json_text, file_upload],
845
+ outputs=[loaded_data, status_text, import_json_text]
846
+ ).then(
847
+ fn=lambda current_trigger: not current_trigger,
848
+ inputs=trigger_update,
849
+ outputs=trigger_update
850
+ )
851
+
852
+ # Load samples
853
+ btn_sample_basic.click(
854
+ fn=lambda: (create_sample_workflow("basic"), "✅ Basic Q&A sample loaded", export_pretty(create_sample_workflow("basic"))),
855
+ outputs=[loaded_data, status_text, import_json_text]
856
+ ).then(
857
+ fn=lambda current_trigger: not current_trigger,
858
+ inputs=trigger_update,
859
+ outputs=trigger_update
860
+ )
861
+
862
+ btn_sample_vidraft.click(
863
+ fn=lambda: (create_sample_workflow("vidraft"), "✅ VIDraft sample loaded", export_pretty(create_sample_workflow("vidraft"))),
864
+ outputs=[loaded_data, status_text, import_json_text]
865
+ ).then(
866
+ fn=lambda current_trigger: not current_trigger,
867
+ inputs=trigger_update,
868
+ outputs=trigger_update
869
+ )
870
+
871
+ btn_sample_multi.click(
872
+ fn=lambda: (create_sample_workflow("multi_input"), "✅ Multi-input sample loaded", export_pretty(create_sample_workflow("multi_input"))),
873
+ outputs=[loaded_data, status_text, import_json_text]
874
+ ).then(
875
+ fn=lambda current_trigger: not current_trigger,
876
+ inputs=trigger_update,
877
+ outputs=trigger_update
878
+ )
879
+
880
+ btn_sample_chain.click(
881
+ fn=lambda: (create_sample_workflow("chain"), "✅ Chain processing sample loaded", export_pretty(create_sample_workflow("chain"))),
882
+ outputs=[loaded_data, status_text, import_json_text]
883
+ ).then(
884
+ fn=lambda current_trigger: not current_trigger,
885
+ inputs=trigger_update,
886
+ outputs=trigger_update
887
+ )
888
+
889
+ # Preview current workflow
890
+ btn_preview.click(
891
+ fn=export_pretty,
892
+ inputs=loaded_data,
893
+ outputs=export_preview
894
+ )
895
+
896
+ # Download workflow
897
+ btn_download.click(
898
+ fn=export_file,
899
+ inputs=loaded_data
900
+ )
901
+
902
+ # Generate UI execution
903
+ btn_execute_ui.click(
904
+ fn=lambda data: data,
905
+ inputs=loaded_data,
906
+ outputs=ui_workflow_data
907
+ )
908
+
909
+ # Auto-update export preview when workflow changes
910
+ loaded_data.change(
911
+ fn=export_pretty,
912
+ inputs=loaded_data,
913
+ outputs=export_preview
914
+ )
915
+
916
+
917
+ # -------------------------------------------------------------------
918
+ # 🚀 실행
919
+ # -------------------------------------------------------------------
920
+ if __name__ == "__main__":
921
+ demo.launch(server_name="0.0.0.0", show_error=True)