Csplk commited on
Commit
3b243d5
Β·
1 Parent(s): 592efa4

dagrgen ui

Browse files
Files changed (2) hide show
  1. app.py β†’ daggr3d.py +0 -0
  2. misc/app.py +976 -0
app.py β†’ daggr3d.py RENAMED
File without changes
misc/app.py ADDED
@@ -0,0 +1,976 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Complete Daggr Generator Suite
3
+ ==============================
4
+ Implements GradioNode, InferenceNode, and FnNode generators with a web UI.
5
+
6
+ Usage:
7
+ python daggr_suite.py # Launch UI
8
+ python daggr_suite.py --cli "space/name" # CLI mode
9
+ """
10
+
11
+ import argparse
12
+ import ast
13
+ import inspect
14
+ import json
15
+ import re
16
+ import sys
17
+ import textwrap
18
+ from abc import ABC, abstractmethod
19
+ from dataclasses import dataclass, field, asdict
20
+ from pathlib import Path
21
+ from typing import Any, Callable, Dict, List, Optional, Tuple, Union, get_type_hints
22
+ from urllib.parse import urlparse
23
+
24
+ try:
25
+ from gradio_client import Client, handle_file
26
+ import gradio as gr
27
+ import huggingface_hub as hf_api
28
+ except ImportError:
29
+ print("Installing required packages...")
30
+ import subprocess
31
+ subprocess.check_call([sys.executable, "-m", "pip", "install", "gradio", "gradio-client", "huggingface-hub"])
32
+ from gradio_client import Client
33
+ import gradio as gr
34
+ import huggingface_hub as hf_api
35
+
36
+
37
+ # ==============================================================================
38
+ # DATA CLASSES
39
+ # ==============================================================================
40
+
41
+ @dataclass
42
+ class PortSchema:
43
+ name: str
44
+ python_type: str
45
+ component_type: Optional[str] = None
46
+ label: Optional[str] = None
47
+ default: Any = None
48
+ description: Optional[str] = None
49
+ choices: Optional[List] = None
50
+
51
+ def to_dict(self):
52
+ return asdict(self)
53
+
54
+ def to_gradio_component(self) -> str:
55
+ type_mapping = {
56
+ "str": "gr.Textbox",
57
+ "int": "gr.Number",
58
+ "float": "gr.Number",
59
+ "bool": "gr.Checkbox",
60
+ "filepath": "gr.File",
61
+ "file": "gr.File",
62
+ "image": "gr.Image",
63
+ "audio": "gr.Audio",
64
+ "video": "gr.Video",
65
+ "dict": "gr.JSON",
66
+ "list": "gr.JSON",
67
+ "dataframe": "gr.Dataframe",
68
+ "model3d": "gr.Model3D",
69
+ "downloadbutton": "gr.File",
70
+ "annotatedimage": "gr.AnnotatedImage",
71
+ }
72
+
73
+ comp_base = type_mapping.get(self.python_type, "gr.Textbox")
74
+ params = []
75
+
76
+ if self.label:
77
+ params.append(f'label="{self.label}"')
78
+ if self.default is not None and self.default != "":
79
+ if isinstance(self.default, str):
80
+ params.append(f'value="{self.default}"')
81
+ else:
82
+ params.append(f'value={self.default}')
83
+ if self.choices:
84
+ params.append(f'choices={self.choices}')
85
+
86
+ if comp_base == "gr.Textbox" and self.python_type == "str":
87
+ if len(str(self.default or "")) > 50:
88
+ params.append("lines=3")
89
+
90
+ return f"{comp_base}({', '.join(params)})" if params else comp_base
91
+
92
+
93
+ @dataclass
94
+ class APIEndpoint:
95
+ name: str
96
+ route: str
97
+ inputs: List[PortSchema] = field(default_factory=list)
98
+ outputs: List[PortSchema] = field(default_factory=list)
99
+ description: Optional[str] = None
100
+
101
+
102
+ @dataclass
103
+ class NodeTemplate:
104
+ node_type: str # 'gradio', 'inference', 'function'
105
+ name: str
106
+ imports: List[str]
107
+ node_code: str
108
+ wiring_docs: List[str]
109
+ metadata: Dict = field(default_factory=dict)
110
+ dependencies: List[str] = field(default_factory=list)
111
+
112
+
113
+ # ==============================================================================
114
+ # ABSTRACT BASE
115
+ # ==============================================================================
116
+
117
+ class NodeGenerator(ABC):
118
+ @abstractmethod
119
+ def generate(self, **kwargs) -> NodeTemplate:
120
+ pass
121
+
122
+
123
+ # ==============================================================================
124
+ # GRADIO NODE GENERATOR
125
+ # ==============================================================================
126
+
127
+ class GradioNodeGenerator(NodeGenerator):
128
+ COMPONENT_TYPE_MAP = {
129
+ "textbox": "str", "number": "float", "slider": "float",
130
+ "checkbox": "bool", "checkboxgroup": "list", "radio": "str",
131
+ "dropdown": "str", "image": "filepath", "file": "filepath",
132
+ "audio": "filepath", "video": "filepath", "dataframe": "dataframe",
133
+ "json": "dict", "gallery": "list", "chatbot": "list",
134
+ "code": "str", "colorpicker": "str", "model3d": "model3d",
135
+ "downloadbutton": "filepath", "annotatedimage": "annotatedimage",
136
+ }
137
+
138
+ def _normalize_type(self, type_val) -> str:
139
+ if type_val is None:
140
+ return "str"
141
+ if isinstance(type_val, str):
142
+ return type_val.lower()
143
+ if isinstance(type_val, dict):
144
+ if "type" in type_val:
145
+ t = type_val["type"]
146
+ if t == "filepath": return "filepath"
147
+ elif t == "integer": return "int"
148
+ elif t == "float": return "float"
149
+ elif t == "boolean": return "bool"
150
+ if type_val.get("type") == "union":
151
+ choices = type_val.get("choices", [])
152
+ non_none = [c for c in choices if self._normalize_type(c) != "none"]
153
+ if non_none:
154
+ return self._normalize_type(non_none[0])
155
+ return "str"
156
+
157
+ def _extract_space_id(self, url_or_id: str) -> str:
158
+ if url_or_id.startswith("http"):
159
+ parsed = urlparse(url_or_id)
160
+ if "huggingface.co" in parsed.netloc:
161
+ parts = parsed.path.strip("/").split("/")
162
+ if len(parts) >= 3 and parts[0] == "spaces":
163
+ return "/".join(parts[1:3])
164
+ return parsed.path.strip("/").split("/")[0]
165
+ return url_or_id
166
+
167
+ def get_endpoints(self, space_id: str) -> List[Dict]:
168
+ """Fetch available endpoints for a space."""
169
+ try:
170
+ client = Client(space_id)
171
+ api_info = client.view_api(return_format="dict")
172
+ endpoints = []
173
+ for route, info in api_info.get("named_endpoints", {}).items():
174
+ endpoints.append({
175
+ "route": route,
176
+ "fn": info.get("fn", route),
177
+ "num_params": len(info.get("parameters", [])),
178
+ "num_returns": len(info.get("returns", []))
179
+ })
180
+ return endpoints
181
+ except Exception as e:
182
+ return [{"error": str(e)}]
183
+
184
+ def generate(self, space_url: str, api_name: Optional[str] = None,
185
+ node_name: Optional[str] = None, **kwargs) -> NodeTemplate:
186
+ space_id = self._extract_space_id(space_url)
187
+ var_name = node_name or self._to_snake_case(space_id.split("/")[-1])
188
+
189
+ client = Client(space_id)
190
+ api_info = client.view_api(return_format="dict")
191
+
192
+ endpoints = []
193
+ for route, info in api_info.get("named_endpoints", {}).items():
194
+ ep = APIEndpoint(
195
+ name=info.get("fn", route),
196
+ route=route,
197
+ description=info.get("description", "")
198
+ )
199
+
200
+ for param in info.get("parameters", []):
201
+ comp_type = self._detect_component_type(param)
202
+ python_type = self._parse_type(param)
203
+
204
+ port = PortSchema(
205
+ name=param.get("parameter_name", "input"),
206
+ python_type=self.COMPONENT_TYPE_MAP.get(comp_type, python_type),
207
+ component_type=comp_type,
208
+ label=param.get("label"),
209
+ default=param.get("default"),
210
+ description=param.get("description", "")[:100] if param.get("description") else None,
211
+ choices=param.get("choices")
212
+ )
213
+ ep.inputs.append(port)
214
+
215
+ for i, ret in enumerate(info.get("returns", [])):
216
+ comp_type = self._detect_component_type(ret)
217
+ python_type = self._parse_type(ret)
218
+
219
+ ret_name = ret.get("label", f"output_{i}" if len(info.get("returns", [])) > 1 else "result")
220
+ ret_name = re.sub(r'[^a-zA-Z0-9_]', '_', ret_name).lower()
221
+ if ret_name[0].isdigit():
222
+ ret_name = "out_" + ret_name
223
+
224
+ port = PortSchema(
225
+ name=ret_name,
226
+ python_type=self.COMPONENT_TYPE_MAP.get(comp_type, python_type),
227
+ component_type=comp_type,
228
+ label=ret.get("label", f"Output {i+1}"),
229
+ description=ret.get("description", "")[:100] if ret.get("description") else None
230
+ )
231
+ ep.outputs.append(port)
232
+
233
+ endpoints.append(ep)
234
+
235
+ if not endpoints:
236
+ raise ValueError("No endpoints found")
237
+
238
+ if api_name:
239
+ selected = next((e for e in endpoints if e.route == api_name), None)
240
+ if not selected:
241
+ available = ", ".join([e.route for e in endpoints])
242
+ raise ValueError(f"Endpoint {api_name} not found. Available: {available}")
243
+ else:
244
+ candidates = [e for e in endpoints if (e.inputs or e.outputs) and not e.route.startswith("/lambda")]
245
+ selected = candidates[0] if candidates else endpoints[0]
246
+
247
+ wiring = self._generate_wiring_docs(selected, var_name)
248
+ code = self._render_code(space_id, var_name, selected)
249
+
250
+ return NodeTemplate(
251
+ node_type="gradio",
252
+ name=var_name,
253
+ imports=["from daggr import GradioNode", "import gradio as gr"],
254
+ node_code=code,
255
+ wiring_docs=wiring,
256
+ metadata={"space_id": space_id, "endpoint": selected.route, "endpoints": [e.route for e in endpoints]}
257
+ )
258
+
259
+ def _parse_type(self, param: Dict) -> str:
260
+ raw_type = param.get("python_type")
261
+ if isinstance(raw_type, dict) and raw_type.get("type") == "union":
262
+ choices = raw_type.get("choices", [])
263
+ non_none = [c for c in choices if isinstance(c, str) and c.lower() != "none"]
264
+ if non_none:
265
+ return non_none[0].lower()
266
+ return self._normalize_type(raw_type)
267
+
268
+ def _detect_component_type(self, param: Dict) -> str:
269
+ label = (param.get("label", "") or "").lower()
270
+ component = param.get("component", "")
271
+ if component and isinstance(component, str):
272
+ return component.lower()
273
+
274
+ python_type = self._parse_type(param)
275
+ if "filepath" in python_type or "path" in label:
276
+ if "image" in label: return "image"
277
+ if "3d" in label or "model" in label: return "model3d"
278
+ return "file"
279
+ if "image" in python_type: return "image"
280
+ return "textbox"
281
+
282
+ def _to_snake_case(self, name: str) -> str:
283
+ clean = re.sub(r'[^a-zA-Z0-9]', '_', name)
284
+ clean = re.sub(r'([A-Z])', r'_\1', clean).lower()
285
+ clean = re.sub(r'_+', '_', clean).strip('_')
286
+ return clean or "node"
287
+
288
+ def _generate_wiring_docs(self, endpoint: APIEndpoint, var_name: str) -> List[str]:
289
+ docs = [f"# Wiring for {var_name}", "# Inputs:"]
290
+ for inp in endpoint.inputs:
291
+ docs.append(f"# {inp.name}: {inp.python_type}")
292
+ docs.append("# Outputs:")
293
+ for out in endpoint.outputs:
294
+ docs.append(f"# {out.name}: {out.python_type}")
295
+ return docs
296
+
297
+ def _render_code(self, space_id: str, var_name: str, endpoint: APIEndpoint) -> str:
298
+ lines = [f'{var_name} = GradioNode(']
299
+ lines.append(f' space_or_url="{space_id}",')
300
+ lines.append(f' api_name="{endpoint.route}",')
301
+ lines.append('')
302
+
303
+ if endpoint.inputs:
304
+ lines.append(' inputs={')
305
+ for inp in endpoint.inputs:
306
+ if inp.default is not None:
307
+ val = f'"{inp.default}"' if isinstance(inp.default, str) else str(inp.default)
308
+ lines.append(f' "{inp.name}": {val}, # Fixed')
309
+ else:
310
+ comp = inp.to_gradio_component()
311
+ lines.append(f' "{inp.name}": {comp},')
312
+ lines.append(' },')
313
+ else:
314
+ lines.append(' inputs={},')
315
+ lines.append('')
316
+
317
+ if endpoint.outputs:
318
+ lines.append(' outputs={')
319
+ for out in endpoint.outputs:
320
+ comp = out.to_gradio_component()
321
+ lines.append(f' "{out.name}": {comp},')
322
+ lines.append(' },')
323
+ else:
324
+ lines.append(' outputs={},')
325
+
326
+ lines.append(')')
327
+ return "\n".join(lines)
328
+
329
+
330
+ # ==============================================================================
331
+ # INFERENCE NODE GENERATOR
332
+ # ==============================================================================
333
+
334
+ class InferenceNodeGenerator(NodeGenerator):
335
+ """Generator for HF Inference Providers (serverless inference)."""
336
+
337
+ TASK_INPUTS = {
338
+ "text-generation": {"prompt": ("str", "gr.Textbox(lines=3, label='Prompt')")},
339
+ "text2text-generation": {"text": ("str", "gr.Textbox(lines=3, label='Input Text')")},
340
+ "summarization": {"text": ("str", "gr.Textbox(lines=5, label='Text to Summarize')")},
341
+ "translation": {"text": ("str", "gr.Textbox(label='Text to Translate')")},
342
+ "question-answering": {
343
+ "context": ("str", "gr.Textbox(lines=5, label='Context')"),
344
+ "question": ("str", "gr.Textbox(label='Question')")
345
+ },
346
+ "image-classification": {"image": ("filepath", "gr.Image(label='Input Image')")},
347
+ "object-detection": {"image": ("filepath", "gr.Image(label='Input Image')")},
348
+ "image-segmentation": {"image": ("filepath", "gr.Image(label='Input Image')")},
349
+ "text-to-image": {"prompt": ("str", "gr.Textbox(lines=3, label='Prompt')")},
350
+ "image-to-text": {"image": ("filepath", "gr.Image(label='Input Image')")},
351
+ "automatic-speech-recognition": {"audio": ("filepath", "gr.Audio(label='Input Audio')")},
352
+ "text-to-speech": {"text": ("str", "gr.Textbox(label='Text to Speak')")},
353
+ "zero-shot-classification": {
354
+ "text": ("str", "gr.Textbox(label='Text')"),
355
+ "candidate_labels": ("str", "gr.Textbox(label='Candidate Labels (comma-separated)')")
356
+ },
357
+ }
358
+
359
+ TASK_OUTPUTS = {
360
+ "text-generation": {"generated_text": ("str", "gr.Textbox(label='Generated Text')")},
361
+ "text2text-generation": {"generated_text": ("str", "gr.Textbox(label='Output')")},
362
+ "summarization": {"summary": ("str", "gr.Textbox(label='Summary')")},
363
+ "translation": {"translation": ("str", "gr.Textbox(label='Translation')")},
364
+ "question-answering": {"answer": ("str", "gr.Textbox(label='Answer'))},
365
+ "image-classification": {"labels": ("list", "gr.JSON(label='Predictions')")},
366
+ "object-detection": {"objects": ("list", "gr.JSON(label='Detections')")},
367
+ "image-segmentation": {"masks": ("list", "gr.JSON(label='Segments')")},
368
+ "text-to-image": {"image": ("filepath", "gr.Image(label='Generated Image')")},
369
+ "image-to-text": {"text": ("str", "gr.Textbox(label='Description')")},
370
+ "automatic-speech-recognition": {"text": ("str", "gr.Textbox(label='Transcription')")},
371
+ "text-to-speech": {"audio": ("filepath", "gr.Audio(label='Generated Audio')")},
372
+ "zero-shot-classification": {"scores": ("list", "gr.JSON(label='Scores'))},
373
+ }
374
+
375
+ def get_model_info(self, model_id: str) -> Optional[Dict]:
376
+ """Fetch model info from HF Hub."""
377
+ try:
378
+ api = hf_api.HfApi()
379
+ info = api.model_info(model_id)
380
+ return {
381
+ "id": model_id,
382
+ "pipeline_tag": info.pipeline_tag,
383
+ "tags": info.tags,
384
+ "library_name": info.library_name,
385
+ }
386
+ except Exception as e:
387
+ return None
388
+
389
+ def generate(self, model_id: str, task: Optional[str] = None,
390
+ node_name: Optional[str] = None, **kwargs) -> NodeTemplate:
391
+ var_name = node_name or self._to_snake_case(model_id.split("/")[-1])
392
+
393
+ # Try to detect task
394
+ if not task:
395
+ info = self.get_model_info(model_id)
396
+ if info and info.get("pipeline_tag"):
397
+ task = info["pipeline_tag"]
398
+ else:
399
+ task = "text-generation" # Default
400
+
401
+ inputs_def = self.TASK_INPUTS.get(task, {"input": ("str", "gr.Textbox()")})
402
+ outputs_def = self.TASK_OUTPUTS.get(task, {"output": ("str", "gr.Textbox()")})
403
+
404
+ # Build code
405
+ lines = [f'{var_name} = InferenceNode(']
406
+ lines.append(f' model="{model_id}",')
407
+ if task:
408
+ lines.append(f' # Task: {task}')
409
+ lines.append('')
410
+ lines.append(' inputs={')
411
+ for name, (ptype, comp) in inputs_def.items():
412
+ lines.append(f' "{name}": {comp},')
413
+ lines.append(' },')
414
+ lines.append('')
415
+ lines.append(' outputs={')
416
+ for name, (ptype, comp) in outputs_def.items():
417
+ lines.append(f' "{name}": {comp},')
418
+ lines.append(' },')
419
+ lines.append(')')
420
+
421
+ wiring = [
422
+ f"# InferenceNode: {model_id}",
423
+ f"# Task: {task}",
424
+ "# Inputs: " + ", ".join(inputs_def.keys()),
425
+ "# Outputs: " + ", ".join(outputs_def.keys())
426
+ ]
427
+
428
+ return NodeTemplate(
429
+ node_type="inference",
430
+ name=var_name,
431
+ imports=["from daggr import InferenceNode", "import gradio as gr"],
432
+ node_code="\n".join(lines),
433
+ wiring_docs=wiring,
434
+ metadata={"model_id": model_id, "task": task}
435
+ )
436
+
437
+ def _to_snake_case(self, name: str) -> str:
438
+ clean = re.sub(r'[^a-zA-Z0-9]', '_', name)
439
+ clean = re.sub(r'([A-Z])', r'_\1', clean).lower()
440
+ clean = re.sub(r'_+', '_', clean).strip('_')
441
+ return clean or "model"
442
+
443
+
444
+ # ==============================================================================
445
+ # FN NODE GENERATOR
446
+ # ==============================================================================
447
+
448
+ class FnNodeGenerator(NodeGenerator):
449
+ """Generator for custom Python functions."""
450
+
451
+ def _type_to_gradio(self, py_type: type) -> Tuple[str, str]:
452
+ """Map Python type to (python_type, gradio_component)."""
453
+ type_map = {
454
+ str: ("str", "gr.Textbox"),
455
+ int: ("int", "gr.Number"),
456
+ float: ("float", "gr.Number"),
457
+ bool: ("bool", "gr.Checkbox"),
458
+ list: ("list", "gr.JSON"),
459
+ dict: ("dict", "gr.JSON"),
460
+ }
461
+ return type_map.get(py_type, ("str", "gr.Textbox"))
462
+
463
+ def generate(self, function_source: str, node_name: Optional[str] = None,
464
+ **kwargs) -> NodeTemplate:
465
+ """
466
+ Generate from function source code or callable.
467
+ function_source can be:
468
+ - A callable function
469
+ - A string containing function definition
470
+ """
471
+ if callable(function_source):
472
+ func = function_source
473
+ source = inspect.getsource(func)
474
+ else:
475
+ # Parse from string
476
+ source = function_source
477
+ # Extract function name
478
+ match = re.search(r'def\s+(\w+)', source)
479
+ if not match:
480
+ raise ValueError("No function definition found")
481
+ func_name = match.group(1)
482
+ # Execute to get callable (sandboxed)
483
+ namespace = {}
484
+ exec(source, namespace)
485
+ func = namespace.get(func_name)
486
+ if not func:
487
+ raise ValueError(f"Function {func_name} not found in source")
488
+
489
+ # Introspect
490
+ sig = inspect.signature(func)
491
+ type_hints = get_type_hints(func)
492
+
493
+ func_name = func.__name__
494
+ var_name = node_name or func_name
495
+
496
+ # Build inputs
497
+ inputs = {}
498
+ for name, param in sig.parameters.items():
499
+ if param.default != inspect.Parameter.empty:
500
+ default = param.default
501
+ else:
502
+ default = None
503
+
504
+ py_type = type_hints.get(name, str)
505
+ ptype, comp = self._type_to_gradio(py_type)
506
+
507
+ inputs[name] = {
508
+ "name": name,
509
+ "type": ptype,
510
+ "component": comp,
511
+ "default": default
512
+ }
513
+
514
+ # Build outputs from return annotation
515
+ outputs = {"result": ("str", "gr.Textbox(label='Result')")}
516
+ return_hint = type_hints.get('return')
517
+ if return_hint:
518
+ if hasattr(return_hint, '__origin__') and return_hint.__origin__ is tuple:
519
+ # Multiple outputs
520
+ outputs = {}
521
+ for i, _ in enumerate(return_hint.__args__):
522
+ outputs[f"output_{i}"] = ("str", f"gr.Textbox(label='Output {i}')")
523
+ else:
524
+ ptype, comp = self._type_to_gradio(return_hint)
525
+ outputs = {"result": (ptype, f"{comp}(label='Result')")}
526
+
527
+ # Generate code
528
+ lines = [f'def {func_name}(', ' # Function defined above', '):']
529
+ lines.append(' """Custom function node"""')
530
+ lines.append(' pass # Implement your logic here')
531
+ lines.append('')
532
+ lines.append(f'{var_name} = FnNode(')
533
+ lines.append(f' fn={func_name},')
534
+ lines.append(' inputs={')
535
+ for name, info in inputs.items():
536
+ if info["default"] is not None:
537
+ val = f'"{info["default"]}"' if isinstance(info["default"], str) else str(info["default"])
538
+ lines.append(f' "{name}": {val},')
539
+ else:
540
+ lines.append(f' "{name}": {info["component"]}(label="{name.title()}"),')
541
+ lines.append(' },')
542
+ lines.append(' outputs={')
543
+ for name, (ptype, comp) in outputs.items():
544
+ lines.append(f' "{name}": {comp},')
545
+ lines.append(' },')
546
+ lines.append(')')
547
+
548
+ wiring = [
549
+ f"# FnNode: {func_name}",
550
+ f"# Inputs: " + ", ".join(inputs.keys()),
551
+ f"# Outputs: " + ", ".join(outputs.keys())
552
+ ]
553
+
554
+ return NodeTemplate(
555
+ node_type="function",
556
+ name=var_name,
557
+ imports=["from daggr import FnNode", "import gradio as gr"],
558
+ node_code="\n".join(lines),
559
+ wiring_docs=wiring,
560
+ metadata={"function_name": func_name, "source": source[:200]}
561
+ )
562
+
563
+
564
+ # ==============================================================================
565
+ # WORKFLOW BUILDER
566
+ # ==============================================================================
567
+
568
+ class WorkflowBuilder:
569
+ """Helps build multi-node workflows."""
570
+
571
+ def __init__(self):
572
+ self.nodes = []
573
+ self.connections = []
574
+
575
+ def add_node(self, template: NodeTemplate):
576
+ self.nodes.append(template)
577
+
578
+ def generate_workflow(self, name: str = "My Workflow") -> str:
579
+ lines = ['"""', f'{name}', 'Generated Daggr Workflow', '"""', '']
580
+
581
+ # Collect all imports
582
+ all_imports = set(["from daggr import Graph"])
583
+ for node in self.nodes:
584
+ for imp in node.imports:
585
+ all_imports.add(imp)
586
+ lines.extend(sorted(all_imports))
587
+ lines.append('')
588
+
589
+ # Add node definitions
590
+ for node in self.nodes:
591
+ lines.extend(node.wiring_docs)
592
+ lines.append(node.node_code)
593
+ lines.append('')
594
+
595
+ # Add graph
596
+ lines.append(f'graph = Graph(')
597
+ lines.append(f' name="{name}",')
598
+ node_names = [n.name for n in self.nodes]
599
+ lines.append(f' nodes=[{", ".join(node_names)}]')
600
+ lines.append(f')')
601
+ lines.append('')
602
+ lines.append('if __name__ == "__main__":')
603
+ lines.append(' graph.launch()')
604
+
605
+ return "\n".join(lines)
606
+
607
+
608
+ # ==============================================================================
609
+ # GRADIO UI
610
+ # ==============================================================================
611
+
612
+ def create_ui():
613
+ """Create the Gradio interface for the Daggr Generator."""
614
+
615
+ gradio_gen = GradioNodeGenerator()
616
+ inference_gen = InferenceNodeGenerator()
617
+ fn_gen = FnNodeGenerator()
618
+ builder = WorkflowBuilder()
619
+
620
+ def fetch_endpoints(space_id):
621
+ """Fetch endpoints for a space."""
622
+ if not space_id:
623
+ return gr.Dropdown(choices=[], value=None), "Enter a space ID"
624
+ try:
625
+ endpoints = gradio_gen.get_endpoints(space_id)
626
+ if "error" in endpoints[0]:
627
+ return gr.Dropdown(choices=[], value=None), f"Error: {endpoints[0]['error']}"
628
+
629
+ choices = [f"{e['route']} ({e['num_params']} in, {e['num_returns']} out)" for e in endpoints]
630
+ return gr.Dropdown(choices=choices, value=choices[0] if choices else None), f"Found {len(endpoints)} endpoints"
631
+ except Exception as e:
632
+ return gr.Dropdown(choices=[], value=None), f"Error: {str(e)}"
633
+
634
+ def generate_gradio_node(space_id, endpoint_selection, node_name, include_wiring):
635
+ """Generate GradioNode code."""
636
+ if not space_id:
637
+ return "Please enter a Space ID"
638
+
639
+ try:
640
+ if endpoint_selection:
641
+ api_name = endpoint_selection.split(" ")[0]
642
+ else:
643
+ api_name = None
644
+
645
+ template = gradio_gen.generate(space_id, api_name=api_name, node_name=node_name or None)
646
+
647
+ lines = []
648
+ if include_wiring:
649
+ lines.extend(template.wiring_docs)
650
+ lines.append("")
651
+ lines.append(template.node_code)
652
+
653
+ return "\n".join(lines)
654
+ except Exception as e:
655
+ return f"Error: {str(e)}\n\nMake sure the space is public and has an API."
656
+
657
+ def generate_inference_node(model_id, task, node_name):
658
+ """Generate InferenceNode code."""
659
+ if not model_id:
660
+ return "Please enter a Model ID"
661
+
662
+ try:
663
+ template = inference_gen.generate(model_id, task=task if task else None, node_name=node_name or None)
664
+ return "\n".join(template.wiring_docs + ["", template.node_code])
665
+ except Exception as e:
666
+ return f"Error: {str(e)}"
667
+
668
+ def generate_function_node(func_source, node_name):
669
+ """Generate FnNode code."""
670
+ if not func_source:
671
+ return "Please enter function code"
672
+
673
+ try:
674
+ template = fn_gen.generate(func_source, node_name=node_name or None)
675
+ return "\n".join(template.wiring_docs + ["", template.node_code])
676
+ except Exception as e:
677
+ return f"Error: {str(e)}"
678
+
679
+ def add_to_workflow(code, current_workflow):
680
+ """Add generated code to workflow builder."""
681
+ if not code or code.startswith("Error"):
682
+ return current_workflow
683
+
684
+ # Simple parsing to extract node variable name
685
+ match = re.search(r'^(\w+)\s*=', code, re.MULTILINE)
686
+ if match:
687
+ node_name = match.group(1)
688
+ else:
689
+ node_name = "unknown_node"
690
+
691
+ # Append to workflow
692
+ if current_workflow:
693
+ new_workflow = current_workflow + "\n\n# --- New Node ---\n" + code
694
+ else:
695
+ new_workflow = code
696
+
697
+ return new_workflow
698
+
699
+ def export_full_workflow(workflow_code, workflow_name):
700
+ """Export complete workflow with Graph."""
701
+ if not workflow_code:
702
+ return "No workflow to export"
703
+
704
+ # Check if already has Graph
705
+ if "Graph(" in workflow_code:
706
+ return workflow_code
707
+
708
+ lines = ['"""', f'{workflow_name}', '"""', '']
709
+ lines.append('from daggr import Graph')
710
+ lines.append('import gradio as gr')
711
+ lines.append('')
712
+ lines.append(workflow_code)
713
+ lines.append('')
714
+ lines.append(f'workflow = Graph(')
715
+ lines.append(f' name="{workflow_name}",')
716
+ # Extract node names
717
+ nodes = re.findall(r'^(\w+)\s*=', workflow_code, re.MULTILINE)
718
+ lines.append(f' nodes=[{", ".join(nodes)}]')
719
+ lines.append(')')
720
+ lines.append('')
721
+ lines.append('if __name__ == "__main__":')
722
+ lines.append(' workflow.launch()')
723
+
724
+ return "\n".join(lines)
725
+
726
+ # Custom CSS for better appearance
727
+ css = """
728
+ .container { max-width: 1200px; margin: 0 auto; }
729
+ .header { text-align: center; margin-bottom: 2rem; }
730
+ .code-output { font-family: monospace; background: #f5f5f5; }
731
+ """
732
+
733
+ with gr.Blocks(css=css, title="Daggr Generator") as demo:
734
+ gr.Markdown("""
735
+ # πŸ•ΈοΈ Daggr Workflow Generator
736
+ Generate daggr nodes for Hugging Face Spaces, Inference Models, and Custom Functions.
737
+ Build AI workflows without writing boilerplate code.
738
+ """)
739
+
740
+ with gr.Tab("Gradio Space"):
741
+ with gr.Row():
742
+ with gr.Column(scale=1):
743
+ gr.Markdown("### Space Configuration")
744
+ space_input = gr.Textbox(
745
+ label="Space ID or URL",
746
+ placeholder="e.g., black-forest-labs/FLUX.1-schnell",
747
+ info="Enter Hugging Face Space ID or full URL"
748
+ )
749
+ fetch_btn = gr.Button("πŸ” Fetch Endpoints", variant="primary")
750
+ endpoint_status = gr.Textbox(label="Status", interactive=False)
751
+
752
+ endpoint_dropdown = gr.Dropdown(
753
+ label="Select API Endpoint",
754
+ choices=[],
755
+ info="Choose which endpoint to use"
756
+ )
757
+
758
+ node_name_input = gr.Textbox(
759
+ label="Node Variable Name (optional)",
760
+ placeholder="Auto-generated from space name"
761
+ )
762
+
763
+ include_wiring = gr.Checkbox(
764
+ label="Include Wiring Documentation",
765
+ value=True,
766
+ info="Add comments showing how to connect nodes"
767
+ )
768
+
769
+ generate_btn = gr.Button("⚑ Generate Code", variant="primary")
770
+
771
+ with gr.Column(scale=2):
772
+ gr.Markdown("### Generated Code")
773
+ gradio_output = gr.Code(
774
+ label="Python Code",
775
+ language="python",
776
+ lines=20
777
+ )
778
+
779
+ with gr.Row():
780
+ add_to_workflow_btn = gr.Button("βž• Add to Workflow")
781
+ copy_btn = gr.Button("πŸ“‹ Copy to Clipboard")
782
+
783
+ with gr.Tab("Inference Model"):
784
+ with gr.Row():
785
+ with gr.Column(scale=1):
786
+ gr.Markdown("### Model Configuration")
787
+ model_input = gr.Textbox(
788
+ label="Model ID",
789
+ placeholder="e.g., meta-llama/Llama-3.1-8B-Instruct"
790
+ )
791
+
792
+ task_dropdown = gr.Dropdown(
793
+ label="Task Type (auto-detected if empty)",
794
+ choices=[
795
+ "text-generation",
796
+ "text2text-generation",
797
+ "summarization",
798
+ "translation",
799
+ "question-answering",
800
+ "image-classification",
801
+ "object-detection",
802
+ "text-to-image",
803
+ "text-to-speech",
804
+ "automatic-speech-recognition"
805
+ ],
806
+ value=None,
807
+ allow_custom_value=True
808
+ )
809
+
810
+ inf_node_name = gr.Textbox(
811
+ label="Node Variable Name (optional)",
812
+ placeholder="Auto-generated from model name"
813
+ )
814
+
815
+ gen_inference_btn = gr.Button("⚑ Generate Code", variant="primary")
816
+
817
+ with gr.Column(scale=2):
818
+ inference_output = gr.Code(
819
+ label="Python Code",
820
+ language="python",
821
+ lines=15
822
+ )
823
+
824
+ with gr.Row():
825
+ add_inf_btn = gr.Button("βž• Add to Workflow")
826
+
827
+ with gr.Tab("Custom Function"):
828
+ with gr.Row():
829
+ with gr.Column(scale=1):
830
+ gr.Markdown("### Function Definition")
831
+ function_input = gr.Code(
832
+ label="Python Function",
833
+ language="python",
834
+ value="""def my_processor(text: str, temperature: float = 0.7) -> str:
835
+ \"\"\"Process input text with given temperature.\"\"\"
836
+ # Your processing logic here
837
+ return text.upper()""",
838
+ lines=10
839
+ )
840
+
841
+ fn_node_name = gr.Textbox(
842
+ label="Node Variable Name (optional)",
843
+ placeholder="Auto-generated from function name"
844
+ )
845
+
846
+ gen_fn_btn = gr.Button("⚑ Generate Code", variant="primary")
847
+
848
+ with gr.Column(scale=2):
849
+ fn_output = gr.Code(
850
+ label="Python Code",
851
+ language="python",
852
+ lines=15
853
+ )
854
+
855
+ with gr.Row():
856
+ add_fn_btn = gr.Button("βž• Add to Workflow")
857
+
858
+ with gr.Tab("Workflow Builder"):
859
+ gr.Markdown("### Assemble Multi-Node Workflow")
860
+
861
+ workflow_code = gr.Code(
862
+ label="Workflow Code (accumulated from tabs above)",
863
+ language="python",
864
+ lines=25,
865
+ value="# Generated nodes will appear here\n# Add nodes from other tabs to build a pipeline"
866
+ )
867
+
868
+ with gr.Row():
869
+ workflow_name = gr.Textbox(
870
+ label="Workflow Name",
871
+ value="My AI Workflow",
872
+ scale=2
873
+ )
874
+ export_btn = gr.Button("πŸ“¦ Export Full Workflow", variant="primary", scale=1)
875
+
876
+ final_output = gr.Code(
877
+ label="Complete Export (with Graph setup)",
878
+ language="python",
879
+ lines=30
880
+ )
881
+
882
+ download_btn = gr.File(label="Download Workflow")
883
+
884
+ # Event handlers
885
+ fetch_btn.click(
886
+ fn=fetch_endpoints,
887
+ inputs=space_input,
888
+ outputs=[endpoint_dropdown, endpoint_status]
889
+ )
890
+
891
+ generate_btn.click(
892
+ fn=generate_gradio_node,
893
+ inputs=[space_input, endpoint_dropdown, node_name_input, include_wiring],
894
+ outputs=gradio_output
895
+ )
896
+
897
+ gen_inference_btn.click(
898
+ fn=generate_inference_node,
899
+ inputs=[model_input, task_dropdown, inf_node_name],
900
+ outputs=inference_output
901
+ )
902
+
903
+ gen_fn_btn.click(
904
+ fn=generate_function_node,
905
+ inputs=[function_input, fn_node_name],
906
+ outputs=fn_output
907
+ )
908
+
909
+ # Workflow building
910
+ add_to_workflow_btn.click(
911
+ fn=add_to_workflow,
912
+ inputs=[gradio_output, workflow_code],
913
+ outputs=workflow_code
914
+ )
915
+
916
+ add_inf_btn.click(
917
+ fn=add_to_workflow,
918
+ inputs=[inference_output, workflow_code],
919
+ outputs=workflow_code
920
+ )
921
+
922
+ add_fn_btn.click(
923
+ fn=add_to_workflow,
924
+ inputs=[fn_output, workflow_code],
925
+ outputs=workflow_code
926
+ )
927
+
928
+ export_btn.click(
929
+ fn=export_full_workflow,
930
+ inputs=[workflow_code, workflow_name],
931
+ outputs=final_output
932
+ )
933
+
934
+ return demo
935
+
936
+
937
+ # ==============================================================================
938
+ # MAIN
939
+ # ==============================================================================
940
+
941
+ def main():
942
+ parser = argparse.ArgumentParser(description="Daggr Generator Suite")
943
+ parser.add_argument("--cli", help="CLI mode: generate from space ID")
944
+ parser.add_argument("--api-name", "-a", help="API endpoint for CLI mode")
945
+ parser.add_argument("--output", "-o", help="Output file for CLI mode")
946
+ parser.add_argument("--type", choices=["gradio", "inference", "function"],
947
+ default="gradio", help="Node type to generate")
948
+ parser.add_argument("--port", "-p", type=int, default=7860, help="Port for UI")
949
+
950
+ args = parser.parse_args()
951
+
952
+ if args.cli:
953
+ # CLI mode
954
+ gen = GradioNodeGenerator() if args.type == "gradio" else InferenceNodeGenerator()
955
+
956
+ if args.type == "gradio":
957
+ template = gen.generate(args.cli, api_name=args.api_name)
958
+ else:
959
+ template = gen.generate(args.cli)
960
+
961
+ code = "\n".join(template.imports + ["", "\n".join(template.wiring_docs), "", template.node_code])
962
+
963
+ if args.output:
964
+ Path(args.output).write_text(code)
965
+ print(f"βœ… Generated: {args.output}")
966
+ else:
967
+ print(code)
968
+ else:
969
+ # UI mode
970
+ print(f"πŸš€ Starting Daggr Generator UI on port {args.port}")
971
+ demo = create_ui()
972
+ demo.launch(server_port=args.port, share=False)
973
+
974
+
975
+ if __name__ == "__main__":
976
+ main()