Showing preview only (694K chars total). Download the full file or copy to clipboard to get everything.
Repository: gradio-app/daggr
Branch: main
Commit: 73cca141ccda
Files: 113
Total size: 657.4 KB
Directory structure:
gitextract_z_vlte2q/
├── .agents/
│ └── skills/
│ └── daggr/
│ └── SKILL.md
├── .changeset/
│ ├── README.md
│ ├── changeset.cjs
│ ├── config.json
│ └── fix_changelogs.cjs
├── .github/
│ ├── assets/
│ │ └── run-mode-dropdown-demo.webm
│ ├── pull_request_template.md
│ └── workflows/
│ ├── comment-queue.yml
│ ├── format.yml
│ ├── generate-changeset.yml
│ ├── publish.yml
│ ├── test.yml
│ └── trigger-changeset.yml
├── .gitignore
├── CHANGELOG.md
├── CLAUDE.md
├── CONTRIBUTING.md
├── LICENSE
├── MANIFEST.in
├── README.md
├── RELEASE.md
├── build_pypi.sh
├── daggr/
│ ├── CHANGELOG.md
│ ├── __init__.py
│ ├── _client_cache.py
│ ├── _utils.py
│ ├── cli.py
│ ├── edge.py
│ ├── executor.py
│ ├── frontend/
│ │ ├── index.html
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── App.svelte
│ │ │ ├── components/
│ │ │ │ ├── Audio.svelte
│ │ │ │ ├── AudioPlayer.svelte
│ │ │ │ ├── Button.svelte
│ │ │ │ ├── Checkbox.svelte
│ │ │ │ ├── CheckboxGroup.svelte
│ │ │ │ ├── Code.svelte
│ │ │ │ ├── ColorPicker.svelte
│ │ │ │ ├── Dataframe.svelte
│ │ │ │ ├── Dialogue.svelte
│ │ │ │ ├── Dropdown.svelte
│ │ │ │ ├── EmbeddedComponent.svelte
│ │ │ │ ├── File.svelte
│ │ │ │ ├── Gallery.svelte
│ │ │ │ ├── HighlightedText.svelte
│ │ │ │ ├── Html.svelte
│ │ │ │ ├── Image.svelte
│ │ │ │ ├── ImageSlider.svelte
│ │ │ │ ├── ItemListSection.svelte
│ │ │ │ ├── Json.svelte
│ │ │ │ ├── Label.svelte
│ │ │ │ ├── MapItemsSection.svelte
│ │ │ │ ├── Markdown.svelte
│ │ │ │ ├── Model3D.svelte
│ │ │ │ ├── Number.svelte
│ │ │ │ ├── Radio.svelte
│ │ │ │ ├── Slider.svelte
│ │ │ │ ├── Textbox.svelte
│ │ │ │ ├── Video.svelte
│ │ │ │ └── index.ts
│ │ │ ├── main.ts
│ │ │ └── types.ts
│ │ ├── svelte.config.js
│ │ └── vite.config.ts
│ ├── graph.py
│ ├── local_space.py
│ ├── node.py
│ ├── ops.py
│ ├── package.json
│ ├── port.py
│ ├── py.typed
│ ├── server.py
│ ├── session.py
│ └── state.py
├── examples/
│ ├── 01_quickstart.py
│ ├── 02_voice_design_comparator_app.py
│ ├── 03_mock_podcast_app.py
│ ├── 04_complete_podcast_app.py
│ ├── 05_local_translation_app.py
│ ├── 06_pig_latin_voice_app.py
│ ├── 07_image_to_3d_app.py
│ ├── 08_text_to_3d_app.py
│ ├── 09_slideshow_app.py
│ ├── 10_real_podcast_app.py
│ ├── 11_viral_content_generator_app.py
│ ├── 12_ecommerce_product_generator_app.py
│ ├── 13_accessible_image_description_app.py
│ ├── 14_food_nutrition_analyzer_app.py
│ └── 15_background_removal_with_input_node.py
├── package.json
├── pnpm-workspace.yaml
├── pyproject.toml
└── tests/
├── README.md
├── conftest.py
├── test_api.py
├── test_basic.py
├── test_cache.py
├── test_executor.py
├── test_nodes.py
├── test_persistence.py
├── test_server.py
└── ui/
├── __init__.py
├── conftest.py
├── helpers.py
├── test_basic.py
├── test_cancel.py
├── test_dependency_hash.py
├── test_image_fix.py
├── test_images.py
├── test_run_mode.py
├── test_sheets.py
└── test_theme.py
================================================
FILE CONTENTS
================================================
================================================
FILE: .agents/skills/daggr/SKILL.md
================================================
---
name: daggr
description: |
Build DAG-based AI pipelines connecting Gradio Spaces, HuggingFace models, and Python functions into visual workflows. Use when asked to create a workflow, build a pipeline, connect AI models, chain Gradio Spaces, create a daggr app, build multi-step AI applications, or orchestrate ML models. Triggers on: "build a workflow", "create a pipeline", "connect models", "daggr", "chain Spaces", "AI pipeline".
license: MIT
metadata:
author: gradio-app
version: "1.1"
---
# daggr
Build visual DAG pipelines connecting Gradio Spaces, HF Inference Providers, and Python functions.
Full docs: https://raw.githubusercontent.com/gradio-app/daggr/refs/heads/main/README.md
## Quick Start
```python
from daggr import GradioNode, FnNode, InferenceNode, Graph, ItemList
import gradio as gr
graph = Graph(name="My Workflow", nodes=[node1, node2, ...])
graph.launch() # Starts web server with visual DAG UI
```
## Node Types
### GradioNode - Gradio Spaces
```python
node = GradioNode(
space_or_url="owner/space-name",
api_name="/endpoint",
inputs={
"param": gr.Textbox(label="Input"), # UI input
"other": other_node.output_port, # Port connection
"fixed": "constant_value", # Fixed value
},
postprocess=lambda *returns: returns[0], # Transform response
outputs={"result": gr.Image(label="Output")},
)
# Example: image generation
img = GradioNode("Tongyi-MAI/Z-Image-Turbo", api_name="/generate",
inputs={"prompt": gr.Textbox(), "resolution": "1024x1024 ( 1:1 )"},
postprocess=lambda imgs, *_: imgs[0]["image"],
outputs={"image": gr.Image()})
```
Find Spaces with semantic queries (describe what you need): `https://huggingface.co/api/spaces/semantic-search?q=generate+music+for+a+video&sdk=gradio&includeNonRunning=false`
Or by category: `https://huggingface.co/api/spaces/semantic-search?category=image-generation&sdk=gradio&includeNonRunning=false`
(categories: image-generation | video-generation | text-generation | speech-synthesis | music-generation | voice-cloning | image-editing | background-removal | image-upscaling | ocr | style-transfer | image-captioning)
### FnNode - Python Functions
```python
def process(input1: str, input2: int) -> str:
return f"{input1}: {input2}"
node = FnNode(
fn=process,
inputs={"input1": gr.Textbox(), "input2": other_node.port},
outputs={"result": gr.Textbox()},
)
```
### InferenceNode - [HF Inference Providers](https://huggingface.co/docs/inference-providers)
Find models: `https://huggingface.co/api/models?inference_provider=all&pipeline_tag=text-to-image`
(swap pipeline_tag: text-to-image | image-to-image | image-to-text | image-to-video | text-to-video | text-to-speech | automatic-speech-recognition)
VLM/LLM models: https://router.huggingface.co/v1/models
```python
node = InferenceNode(
model="org/model:provider", # model:provider (fal-ai, replicate, together, etc.)
inputs={"image": other_node.image, "prompt": gr.Textbox()},
outputs={"image": gr.Image()},
)
```
**Auth:** InferenceNode and ZeroGPU Spaces require a HF token. If not in env, ask user to create one:
`https://huggingface.co/settings/tokens/new?ownUserPermissions=inference.serverless.write&tokenType=fineGrained`
Out of quota? Pro gives 8x ZeroGPU + 10x inference: `https://huggingface.co/subscribe/pro`
## Port Connections
Pass ports via `inputs={...}`:
```python
inputs={"param": previous_node.output_port} # Basic connection
inputs={"item": items_node.items.field_name} # Scattered (per-item)
inputs={"all": scattered_node.output.all()} # Gathered (collect list)
```
## ItemList - Dynamic Lists
```python
def gen_items(n: int) -> list:
return [{"text": f"Item {i}"} for i in range(n)]
items = FnNode(fn=gen_items,
outputs={"items": ItemList(text=gr.Textbox())})
# Runs once per item
process = FnNode(fn=process_item,
inputs={"text": items.items.text},
outputs={"result": gr.Textbox()})
# Collect all results
final = FnNode(fn=combine,
inputs={"all": process.result.all()},
outputs={"out": gr.Textbox()})
```
## Checklist
1. **Check API** before using a Space:
```bash
curl -s "https://<space-subdomain>.hf.space/gradio_api/openapi.json"
```
Replace `<space-subdomain>` with the Space's subdomain (e.g., `Tongyi-MAI/Z-Image-Turbo` → `tongyi-mai-z-image-turbo`).
(Spaces also have "Use via API" link in footer with endpoints and code snippets)
2. **Handle files** (Gradio returns dicts):
```python
path = file.get("path") if isinstance(file, dict) else file
```
3. **Use postprocess** for multi-return APIs:
```python
postprocess=lambda imgs, seed, num: imgs[0]["image"]
```
4. **Debug with `.test()`** to validate a node in isolation:
```python
node.test(param="value")
```
## Common Patterns
```python
# Image Generation
GradioNode("Tongyi-MAI/Z-Image-Turbo", api_name="/generate",
inputs={"prompt": gr.Textbox(), "resolution": "1024x1024 ( 1:1 )"},
postprocess=lambda imgs, *_: imgs[0]["image"],
outputs={"image": gr.Image()})
# Text-to-Speech
GradioNode("Qwen/Qwen3-TTS", api_name="/generate_voice_design",
inputs={"text": gr.Textbox(), "language": "English", "voice_description": "..."},
postprocess=lambda audio, status: audio,
outputs={"audio": gr.Audio()})
# Image-to-Video
GradioNode("alexnasa/ltx-2-TURBO", api_name="/generate_video",
inputs={"input_image": img.image, "prompt": gr.Textbox(), "duration": 5},
postprocess=lambda video, seed: video,
outputs={"video": gr.Video()})
# ffmpeg composition (import tempfile, subprocess)
def combine(video: str|dict, audio: str|dict) -> str:
v = video.get("path") if isinstance(video, dict) else video
a = audio.get("path") if isinstance(audio, dict) else audio
out = tempfile.mktemp(suffix=".mp4")
subprocess.run(["ffmpeg","-y","-i",v,"-i",a,"-shortest",out])
return out
```
## Run
```bash
uvx --python 3.12 daggr workflow.py & # Launch in background, hot reloads on file changes
```
## Authentication
**Local development:** Use `hf auth login` or set `HF_TOKEN` env var. This enables ZeroGPU quota tracking, private Spaces access, and gated models.
**Deployed Spaces:** Users can click "Login" in the UI and paste their HF token. This enables persistence (sheets) so they can save outputs and resume work later. The token is stored in browser localStorage.
**When deploying:** Pass secrets via `--secret HF_TOKEN=xxx` if your workflow needs server-side auth (e.g., for gated models in FnNode). Warning: this uses the deployer's token for all users.
## Deploy to Hugging Face Spaces
Only deploy if the user has explicitly asked to publish/deploy their workflow.
```bash
daggr deploy workflow.py
```
This extracts the Graph, creates a Space named after it, and uploads everything.
**Options:**
```bash
daggr deploy workflow.py --name my-space # Custom Space name
daggr deploy workflow.py --org huggingface # Deploy to an organization
daggr deploy workflow.py --private # Private Space
daggr deploy workflow.py --hardware t4-small # GPU (t4-small, t4-medium, a10g-small, etc.)
daggr deploy workflow.py --secret KEY=value # Add secrets (repeatable)
daggr deploy workflow.py --dry-run # Preview without deploying
```
================================================
FILE: .changeset/README.md
================================================
# Changesets
Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
with multi-package repos, or single-package repos to help you version and publish your code. You can
find the full documentation for it [in our repository](https://github.com/changesets/changesets)
We have a quick list of common questions to get you started engaging with this project in
[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
================================================
FILE: .changeset/changeset.cjs
================================================
const { getPackagesSync } = require("@manypkg/get-packages");
const dependents_graph = require("@changesets/get-dependents-graph");
const gh = require("@changesets/get-github-info");
const { existsSync, readFileSync, writeFileSync } = require("fs");
const { join } = require("path");
const { getInfo, getInfoFromPullRequest } = gh;
const pkg_data = getPackagesSync(process.cwd());
const { packages, rootDir } = pkg_data;
const dependents = dependents_graph.getDependentsGraph({
packages,
root: pkg_data.rootPackage
});
/**
* @typedef {{packageJson: {name: string, python?: boolean}, dir: string}} Package
*/
/**
* @typedef {{summary: string, id: string, commit: string, releases: {name: string}}} Changeset
*/
/**
*
* @param {string} package_name The name of the package to find the directories for
* @returns {string[]} The directories for the package
*/
function find_packages_dirs(package_name) {
/** @type {string[]} */
let package_dirs = [];
/** @type {Package | undefined} */
const _package = packages.find((p) => p.packageJson.name === package_name);
if (!_package) throw new Error(`Package ${package_name} not found`);
package_dirs.push(_package.dir);
if (_package.packageJson.python) {
package_dirs.push(join(_package.dir, ".."));
}
return package_dirs;
}
let lines = {
_handled: []
};
const changelogFunctions = {
/**
*
* @param {Changeset[]} changesets The changesets that have been created
* @param {any} dependenciesUpdated The dependencies that have been updated
* @param {any} options The options passed to the changelog generator
* @returns {Promise<string>} The release line for the dependencies
*/
getDependencyReleaseLine: async (
changesets,
dependenciesUpdated,
options
) => {
if (!options.repo) {
throw new Error(
'Please provide a repo to this changelog generator like this:\n"changelog": ["@changesets/changelog-github", { "repo": "org/repo" }]'
);
}
if (dependenciesUpdated.length === 0) return "";
const changesetLink = `- Updated dependencies [${(
await Promise.all(
changesets.map(async (cs) => {
if (cs.commit) {
let { links } = await getInfo({
repo: options.repo,
commit: cs.commit
});
return links.commit;
}
})
)
)
.filter((_) => _)
.join(", ")}]:`;
const updatedDepenenciesList = dependenciesUpdated.map(
/**
*
* @param {any} dependency The dependency that has been updated
* @returns {string} The formatted dependency
*/
(dependency) => {
const updates = dependents.get(dependency.name);
if (updates && updates.length > 0) {
updates.forEach((update) => {
if (!lines[update]) {
lines[update] = {
dirs: find_packages_dirs(update),
current_changelog: "",
feat: [],
fix: [],
highlight: [],
previous_version: packages.find(
(p) => p.packageJson.name === update
).packageJson.version,
dependencies: []
};
const changelog_path = join(
//@ts-ignore
lines[update].dirs[1] || lines[update].dirs[0],
"CHANGELOG.md"
);
if (existsSync(changelog_path)) {
//@ts-ignore
lines[update].current_changelog = readFileSync(
changelog_path,
"utf-8"
)
.replace(`# ${update}`, "")
.trim();
}
}
lines[update].dependencies.push(
` - ${dependency.name}@${dependency.newVersion}`
);
});
}
return ` - ${dependency.name}@${dependency.newVersion}`;
}
);
writeFileSync(
join(rootDir, ".changeset", "_changelog.json"),
JSON.stringify(lines, null, 2)
);
return [changesetLink, ...updatedDepenenciesList].join("\n");
},
/**
*
* @param {{summary: string, id: string, commit: string, releases: {name: string}[]}} changeset The changeset that has been created
* @param {any} type The type of changeset
* @param {any} options The options passed to the changelog generator
* @returns {Promise<string>} The release line for the changeset
*/
getReleaseLine: async (changeset, type, options) => {
if (!options || !options.repo) {
throw new Error(
'Please provide a repo to this changelog generator like this:\n"changelog": ["@changesets/changelog-github", { "repo": "org/repo" }]'
);
}
let prFromSummary;
let commitFromSummary;
/**
* @type {string[]}
*/
let usersFromSummary = [];
const replacedChangelog = changeset.summary
.replace(/^\s*(?:pr|pull|pull\s+request):\s*#?(\d+)/im, (_, pr) => {
let num = Number(pr);
if (!isNaN(num)) prFromSummary = num;
return "";
})
.replace(/^\s*commit:\s*([^\s]+)/im, (_, commit) => {
commitFromSummary = commit;
return "";
})
.replace(/^\s*(?:author|user):\s*@?([^\s]+)/gim, (_, user) => {
usersFromSummary.push(user);
return "";
})
.trim();
const [firstLine, ...futureLines] = replacedChangelog
.split("\n")
.map((l) => l.trimRight());
const links = await (async () => {
if (prFromSummary !== undefined) {
let { links } = await getInfoFromPullRequest({
repo: options.repo,
pull: prFromSummary
});
if (commitFromSummary) {
links = {
...links,
commit: `[\`${commitFromSummary}\`](https://github.com/${options.repo}/commit/${commitFromSummary})`
};
}
return links;
}
const commitToFetchFrom = commitFromSummary || changeset.commit;
if (commitToFetchFrom) {
let { links } = await getInfo({
repo: options.repo,
commit: commitToFetchFrom
});
return links;
}
return {
commit: null,
pull: null,
user: null
};
})();
const user_link = /\[(@[^]+)\]/.exec(links.user);
const users =
usersFromSummary && usersFromSummary.length
? usersFromSummary
.map((userFromSummary) => `@${userFromSummary}`)
.join(", ")
: user_link
? user_link[1]
: links.user;
const prefix = [
links.pull === null ? "" : `${links.pull}`,
links.commit === null ? "" : `${links.commit}`
]
.join(" ")
.trim();
const suffix = users === null ? "" : ` Thanks ${users}!`;
/**
* @typedef {{[key: string]: string[] | {dirs: string[], current_changelog: string, feat: {summary: string}[], fix: {summary: string}[], highlight: {summary: string}[]}}} ChangesetMeta
*/
/**
* @type { ChangesetMeta & { _handled: string[] } }}
*/
if (lines._handled.includes(changeset.id)) {
return "done";
}
lines._handled.push(changeset.id);
changeset.releases.forEach((release) => {
if (!lines[release.name]) {
lines[release.name] = {
dirs: find_packages_dirs(release.name),
current_changelog: "",
feat: [],
fix: [],
highlight: [],
previous_version: packages.find(
(p) => p.packageJson.name === release.name
).packageJson.version,
dependencies: []
};
}
const changelog_path = join(
//@ts-ignore
lines[release.name].dirs[1] || lines[release.name].dirs[0],
"CHANGELOG.md"
);
if (existsSync(changelog_path)) {
//@ts-ignore
lines[release.name].current_changelog = readFileSync(
changelog_path,
"utf-8"
)
.replace(`# ${release.name}`, "")
.trim();
}
const [, _type, summary] = changeset.summary
.trim()
.match(/^(feat|fix|highlight)\s*:\s*([^]*)/im) || [
,
"feat",
changeset.summary
];
let formatted_summary = "";
if (_type === "highlight") {
const [heading, ...rest] = summary.trim().split("\n");
const _heading = `${heading} ${prefix ? `(${prefix})` : ""}`;
const _rest = rest.concat(["", suffix]);
formatted_summary = `${_heading}\n${_rest.join("\n")}`;
} else {
formatted_summary = handle_line(summary, prefix, suffix);
}
//@ts-ignore
lines[release.name][_type].push({
summary: formatted_summary
});
});
writeFileSync(
join(rootDir, ".changeset", "_changelog.json"),
JSON.stringify(lines, null, 2)
);
return `\n\n-${prefix ? `${prefix} -` : ""} ${firstLine}\n${futureLines
.map((l) => ` ${l}`)
.join("\n")}`;
}
};
/**
* @param {string} str The changelog entry
* @param {string} prefix The prefix to add to the first line
* @param {string} suffix The suffix to add to the last line
* @returns {string} The formatted changelog entry
*/
function handle_line(str, prefix, suffix) {
const [_s, ...lines] = str.split("\n").filter(Boolean);
const desc = `${prefix ? `${prefix} -` : ""} ${_s.replace(
/[\s\.]$/,
""
)}. ${suffix}`;
if (_s.length === 1) {
return desc;
}
return [desc, ...lines.map((l) => ` ${l}`)].join("/n");
}
module.exports = changelogFunctions;
================================================
FILE: .changeset/config.json
================================================
{
"$schema": "https://unpkg.com/@changesets/config@2.3.0/schema.json",
"changelog": ["./changeset.cjs", { "repo": "abidlabs/daggr" }],
"commit": false,
"fixed": [],
"linked": [],
"access": "public",
"baseBranch": "main",
"updateInternalDependencies": "patch"
}
================================================
FILE: .changeset/fix_changelogs.cjs
================================================
const { join } = require("path");
const { readFileSync, existsSync, writeFileSync, unlinkSync } = require("fs");
const { getPackagesSync } = require("@manypkg/get-packages");
const RE_PKG_NAME = /^[\w-]+\b/;
const pkg_meta = getPackagesSync(process.cwd());
/**
* @typedef {{dirs: string[], highlight: {summary: string}[], feat: {summary: string}[], fix: {summary: string}[], current_changelog: string}} ChangesetMeta
*/
/**
* @typedef {{[key: string]: ChangesetMeta}} ChangesetMetaCollection
*/
function run() {
if (!existsSync(join(pkg_meta.rootDir, ".changeset", "_changelog.json"))) {
console.warn("No changesets to process");
return;
}
/**
* @type { ChangesetMetaCollection & { _handled: string[] } }}
*/
const { _handled, ...packages } = JSON.parse(
readFileSync(
join(pkg_meta.rootDir, ".changeset", "_changelog.json"),
"utf-8"
)
);
/**
* @typedef { {packageJson: {name: string, version: string, python: boolean}, dir: string} } PackageMeta
*/
/**
* @type { {[key:string]: PackageMeta} }
*/
const all_packages = pkg_meta.packages.reduce((acc, pkg) => {
acc[pkg.packageJson.name] = /**@type {PackageMeta} */ (
/** @type {unknown} */ (pkg)
);
return acc;
}, /** @type {{[key:string] : PackageMeta}} */ ({}));
for (const pkg_name in packages) {
const { dirs, highlight, feat, fix, current_changelog, dependencies } =
/**@type {ChangesetMeta} */ (packages[pkg_name]);
const { version, python } = all_packages[pkg_name].packageJson;
const highlights = highlight?.map((h) => `${h.summary}`) || [];
const features = feat?.map((f) => `- ${f.summary}`) || [];
const fixes = fix?.map((f) => `- ${f.summary}`) || [];
const deps = Array.from(new Set(dependencies?.map((d) => d.trim()))) || [];
const release_notes = /** @type {[string[], string][]} */ ([
[highlights, "### Highlights"],
[features, "### Features"],
[fixes, "### Fixes"],
[deps, "### Dependency updates"],
])
.filter(([s], i) => s.length > 0)
.map(([lines, title]) => {
if (title === "### Highlights") {
return `${title}\n\n${lines.join("\n\n")}`;
}
return `${title}\n\n${lines.join("\n")}`;
})
.join("\n\n");
const new_changelog = `# ${pkg_name}
## ${version}
${release_notes}
${current_changelog.replace(`# ${pkg_name}`, "").trim()}
`.trim();
dirs.forEach((dir) => {
writeFileSync(join(dir, "CHANGELOG.md"), new_changelog);
});
if (python) {
bump_local_dependents(pkg_name, version);
}
}
unlinkSync(join(pkg_meta.rootDir, ".changeset", "_changelog.json"));
/**
* @param {string} pkg_to_bump The name of the package to bump
* @param {string} version The version to bump to
* @returns {void}
* */
function bump_local_dependents(pkg_to_bump, version) {
for (const pkg_name in all_packages) {
const {
dir,
packageJson: { python },
} = all_packages[pkg_name];
if (!python) continue;
const requirements_path = join(dir, "..", "requirements.txt");
if (!existsSync(requirements_path)) continue;
const requirements = readFileSync(requirements_path, "utf-8").split("\n");
const pkg_index = requirements.findIndex((line) => {
const m = line.trim().match(RE_PKG_NAME);
if (!m) return false;
return m[0] === pkg_to_bump;
});
if (pkg_index !== -1) {
requirements[pkg_index] = `${pkg_to_bump}==${version}`;
writeFileSync(requirements_path, requirements.join("\n"));
}
}
}
}
run();
================================================
FILE: .github/pull_request_template.md
================================================
Thank you for your contribution! All PRs should include the following sections. PRs missing these sections may be closed immediately.
## Short description
This PR... *[fill here]*
## AI Disclosure
We encourage the use of AI tooling in creating PRs, but the any non-trivial use of AI needs be disclosed. E.g. if you used Claude to write a first draft, you should mention that. Trivial tab-completion doesn't need to be disclosed. **You should self-review all PRs, especially if they were generated with AI**.
-----
- [ ] I used AI to... *[fill here]*
- [ ] I did not use AI
----
## Type of Change
- [ ] Bug fix
- [ ] New feature (non-breaking)
- [ ] New feature (breaking change)
- [ ] Documentation update
- [ ] Test improvements
## Related Issues
If this PR closes an issue, please link it below:
Closes:
## Testing and linting
Please run tests before submitting changes:
```bash
python -m pytest
```
and format your code using Ruff:
```bash
ruff check --fix --select I && ruff format
```
================================================
FILE: .github/workflows/comment-queue.yml
================================================
name: Comment on pull request without race conditions
on:
workflow_call:
inputs:
pr_number:
type: string
message:
required: true
type: string
tag:
required: false
type: string
default: "previews"
additional_text:
required: false
type: string
default: ""
secrets:
gh_token:
required: true
jobs:
comment:
environment: comment_pr
concurrency:
group: ${{inputs.pr_number || inputs.tag}}
runs-on: ubuntu-latest
steps:
- name: comment on pr
uses: "gradio-app/github/actions/comment-pr@main"
with:
gh_token: ${{ secrets.gh_token }}
tag: ${{ inputs.tag }}
pr_number: ${{ inputs.pr_number}}
message: ${{ inputs.message }}
additional_text: ${{ inputs.additional_text }}
================================================
FILE: .github/workflows/format.yml
================================================
name: Format
on:
pull_request:
branches: [ main ]
push:
branches: [ main ]
jobs:
format:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.10'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install ruff
- name: Check formatting with ruff
run: |
ruff format --check .
- name: Check code with ruff
run: |
ruff check .
================================================
FILE: .github/workflows/generate-changeset.yml
================================================
name: Generate changeset
on:
workflow_run:
workflows: ["trigger-changeset"]
types:
- completed
env:
CI: true
concurrency:
group: "${{ github.event.workflow_run.head_repository.full_name }}-${{ github.event.workflow_run.head_branch }}"
cancel-in-progress: false
permissions: {}
jobs:
get-pr:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
if: github.event.workflow_run.conclusion == 'success'
outputs:
found_pr: ${{ steps.pr_details.outputs.found_pr }}
pr_number: ${{ steps.pr_details.outputs.pr_number }}
source_repo: ${{ steps.pr_details.outputs.source_repo }}
source_branch: ${{ steps.pr_details.outputs.source_branch }}
actor: ${{ steps.pr_details.outputs.actor }}
sha: ${{ steps.pr_details.outputs.sha }}
steps:
- name: get pr details
id: pr_details
uses: gradio-app/github/actions/find-pr@main
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
comment-changes-start:
uses: "./.github/workflows/comment-queue.yml"
needs: get-pr
secrets:
gh_token: ${{ secrets.GRADIO_PAT }}
with:
pr_number: ${{ needs.get-pr.outputs.pr_number }}
message: changes~pending~null
version:
permissions:
contents: read
name: version
needs: get-pr
runs-on: ubuntu-latest
if: needs.get-pr.outputs.found_pr == 'true'
outputs:
skipped: ${{ steps.version.outputs.skipped }}
comment_url: ${{ steps.version.outputs.comment_url }}
approved: ${{ steps.version.outputs.approved }}
steps:
- uses: actions/checkout@v4
with:
repository: ${{ needs.get-pr.outputs.source_repo }}
ref: ${{ needs.get-pr.outputs.source_branch }}
fetch-depth: 0
token: ${{ secrets.GRADIO_PAT }}
- name: generate changeset
id: version
uses: "gradio-app/github/actions/generate-changeset@main"
with:
github_token: ${{ secrets.GRADIO_PAT }}
main_pkg: daggr
pr_number: ${{ needs.get-pr.outputs.pr_number }}
branch_name: ${{ needs.get-pr.outputs.source_branch }}
actor: ${{ needs.get-pr.outputs.actor }}
update-status:
permissions:
actions: read
statuses: write
runs-on: ubuntu-latest
needs: [version, get-pr]
steps:
- name: update status
uses: gradio-app/github/actions/commit-status@main
with:
sha: ${{ needs.get-pr.outputs.sha }}
token: ${{ secrets.COMMIT_STATUS }}
name: "Changeset Results"
pr: ${{ needs.get-pr.outputs.pr_number }}
result: 'success'
type: all
comment-changes-skipped:
uses: "./.github/workflows/comment-queue.yml"
needs: [get-pr, version]
if: needs.version.result == 'success' && needs.version.outputs.skipped == 'true'
secrets:
gh_token: ${{ secrets.GRADIO_PAT }}
with:
pr_number: ${{ needs.get-pr.outputs.pr_number }}
message: changes~warning~https://github.com/abidlabs/daggr/actions/runs/${{github.run_id}}/
comment-changes-success:
uses: "./.github/workflows/comment-queue.yml"
needs: [get-pr, version]
if: needs.version.result == 'success' && needs.version.outputs.skipped == 'false'
secrets:
gh_token: ${{ secrets.GRADIO_PAT }}
with:
pr_number: ${{ needs.get-pr.outputs.pr_number }}
message: changes~success~${{ needs.version.outputs.comment_url }}
comment-changes-failure:
uses: "./.github/workflows/comment-queue.yml"
needs: [get-pr, version]
if: always() && needs.version.result == 'failure'
secrets:
gh_token: ${{ secrets.GRADIO_PAT }}
with:
pr_number: ${{ needs.get-pr.outputs.pr_number }}
message: changes~failure~https://github.com/abidlabs/daggr/actions/runs/${{github.run_id}}/
================================================
FILE: .github/workflows/publish.yml
================================================
# safe runs from main
name: publish
on:
push:
branches:
- main
jobs:
version_or_publish:
runs-on: ubuntu-22.04
environment: publish
permissions:
contents: read
id-token: write
steps:
- name: checkout repo
uses: actions/checkout@v4
with:
fetch-depth: 0
persist-credentials: false
- name: Install pnpm
uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 # @v4
with:
version: 9.1.x
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
cache-dependency-path: pnpm-lock.yaml
- name: Install changesets
run: pnpm i --frozen-lockfile
- name: create and publish versions
id: changesets
uses: changesets/action@aba318e9165b45b7948c60273e0b72fce0a64eb9 # @v1
with:
version: pnpm ci:version
commit: "chore: update versions"
title: "chore: update versions"
publish: pnpm ci:tag
env:
GITHUB_TOKEN: ${{ secrets.GRADIO_PAT }}
- name: Build frontend
if: steps.changesets.outputs.hasChangesets != 'true'
run: |
cd daggr/frontend
npm ci
npm run build
- name: publish to pypi
if: steps.changesets.outputs.hasChangesets != 'true'
uses: "gradio-app/github/actions/publish-pypi@main"
with:
use-oidc: true
================================================
FILE: .github/workflows/test.yml
================================================
name: Unit Tests
on:
pull_request:
branches: [ main ]
push:
branches: [ main ]
jobs:
test:
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.10'
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install uv
uses: astral-sh/setup-uv@v3
with:
version: "latest"
- name: Cache uv packages
uses: actions/cache@v3
with:
path: ~/.cache/uv
key: ${{ runner.os }}-uv-${{ hashFiles('pyproject.toml') }}
restore-keys: |
${{ runner.os }}-uv-
- name: Build frontend
run: |
cd daggr/frontend
npm ci
npm run build
- name: Install Python dependencies
run: |
uv pip install --system -e .[dev] --prerelease=allow
uv pip install --system pytest --prerelease=allow
- name: Install Playwright browsers (Ubuntu only)
if: runner.os == 'Linux'
run: |
playwright install chromium --with-deps
- name: Run all tests (Ubuntu)
if: runner.os == 'Linux'
run: |
pytest
- name: Run tests without UI (Windows)
if: runner.os == 'Windows'
run: |
pytest --ignore=tests/ui/
================================================
FILE: .github/workflows/trigger-changeset.yml
================================================
name: trigger-changeset
on:
pull_request:
types: [opened, synchronize, reopened, edited, labeled, unlabeled]
branches:
- main
issue_comment:
types: [edited]
permissions: {}
jobs:
changeset:
runs-on: ubuntu-22.04
if: github.event.sender.login != 'gradio-pr-bot'
steps:
- run: echo "Requesting changeset"
================================================
FILE: .gitignore
================================================
trackio/__pycache__/
tests/__pycache__/
test-results/
.trackio/
.gradio/
trackio.db
*.pyc
*.pyi
*.claude/
.venv/
**/.DS_Store
*.daggr_sessions.db
node_modules/
/dist/
examples/test.py
daggr/frontend/dist/
build/
tf_test_run/
venv
.vscode/
================================================
FILE: CHANGELOG.md
================================================
# daggr
## 0.8.0
### Features
- [#79](https://github.com/gradio-app/daggr/pull/79) [`d32cec2`](https://github.com/gradio-app/daggr/commit/d32cec21e93532ce7a7e66815922985c0bd5cb75) - feat: Introduce `InputNode` for grouping UI controls. Thanks @elismasilva!
- [#81](https://github.com/gradio-app/daggr/pull/81) [`34267c9`](https://github.com/gradio-app/daggr/commit/34267c9f93729cc4fa4839b15feb44a239897ec9) - stylized thin scrollbars. Thanks @elismasilva!
- [#77](https://github.com/gradio-app/daggr/pull/77) [`9002cca`](https://github.com/gradio-app/daggr/commit/9002cca2b312731572d4d80d7efe3cd9483e19b3) - Feature: Toggle Dark/Light Mode (based on @elismasilva's changes). Thanks @abidlabs!
## 0.7.0
### Features
- [#69](https://github.com/gradio-app/daggr/pull/69) [`297d104`](https://github.com/gradio-app/daggr/commit/297d104d0e3fbd6b59dfcd1f69c9a478de81bc3d) - Add stop button to cancel running nodes. Thanks @abidlabs!
- [#64](https://github.com/gradio-app/daggr/pull/64) [`a9e53c6`](https://github.com/gradio-app/daggr/commit/a9e53c64db6b3beede0b19b3876a3e50ab572233) - Fix ChoiceNode: public .name property and respect explicit names. Thanks @abidlabs!
- [#62](https://github.com/gradio-app/daggr/pull/62) [`695411c`](https://github.com/gradio-app/daggr/commit/695411ce94bc8fedd3320ac941ab41233bf8f887) - Standardize file handling: all files are path strings. Thanks @abidlabs!
- [#66](https://github.com/gradio-app/daggr/pull/66) [`1ed16f8`](https://github.com/gradio-app/daggr/commit/1ed16f806d535413c3718fc47d81d79d93d73ee0) - Fix gr.JSON rendering (use @render snippet syntax). Thanks @abidlabs!
- [#63](https://github.com/gradio-app/daggr/pull/63) [`00b05ac`](https://github.com/gradio-app/daggr/commit/00b05ac526642c91560cbebb258626a4511082c2) - Fix file downloads from private HF Spaces. Thanks @abidlabs!
- [#70](https://github.com/gradio-app/daggr/pull/70) [`33ccb74`](https://github.com/gradio-app/daggr/commit/33ccb7470a482ee1b09fddf1d51de81b1e2c4a40) - Fix gr.Image not rendering with initial value or None input. Thanks @abidlabs!
- [#68](https://github.com/gradio-app/daggr/pull/68) [`4b76dca`](https://github.com/gradio-app/daggr/commit/4b76dca815e2802b70ac03bb95fb03f21d81a8fa) - Add dependency hash tracking for upstream Spaces and models. Thanks @abidlabs!
## 0.6.0
### Features
- [#54](https://github.com/gradio-app/daggr/pull/54) [`c1abb26`](https://github.com/gradio-app/daggr/commit/c1abb260b254af6ca2060292232049ea89f0f944) - Fix cache. Thanks @abidlabs!
- [#56](https://github.com/gradio-app/daggr/pull/56) [`6e3dfc0`](https://github.com/gradio-app/daggr/commit/6e3dfc0a585b673adb77bb11ab1dcfd80d01da5a) - Add paste from clipboard button to Image component. Thanks @abidlabs!
- [#57](https://github.com/gradio-app/daggr/pull/57) [`76855ba`](https://github.com/gradio-app/daggr/commit/76855ba967e3f3132e8ec0590ae037d3151af310) - Fix dropdown options being clipped inside node. Thanks @abidlabs!
- [#58](https://github.com/gradio-app/daggr/pull/58) [`eb52b72`](https://github.com/gradio-app/daggr/commit/eb52b725b17d277e85f6eac1cc9d07f8068b011b) - Add theme support to daggr. Thanks @abidlabs!
- [#59](https://github.com/gradio-app/daggr/pull/59) [`78189a4`](https://github.com/gradio-app/daggr/commit/78189a4163b4041c814e52110b65754dc4dbf863) - Add run mode dropdown to control node execution scope. Thanks @abidlabs!
- [#39](https://github.com/gradio-app/daggr/pull/39) [`e8792ad`](https://github.com/gradio-app/daggr/commit/e8792ad1b5818ff8d13660b0b156f329bbc1c33a) - feat: add --state-db-path CLI arg and DAGGR_DB_PATH env var support. Thanks @leith-bartrich!
## 0.5.4
### Features
- [#27](https://github.com/gradio-app/daggr/pull/27) [`3952b2c`](https://github.com/gradio-app/daggr/commit/3952b2ccf30e7d18994f23049c2a2e84b323cfd6) - changes. Thanks @abidlabs!
## 0.5.3
### Features
- [#19](https://github.com/gradio-app/daggr/pull/19) [`cd956fe`](https://github.com/gradio-app/daggr/commit/cd956fe29945bdfd31bbe76fcb80d3f9c97cc301) - Add daggr tag to deployed Spaces. Thanks @gary149!
## 0.5.2
### Features
- [#14](https://github.com/gradio-app/daggr/pull/14) [`3fa412d`](https://github.com/gradio-app/daggr/commit/3fa412d678988608d49d46d99d193a05469892d2) - Fixes. Thanks @abidlabs!
## 0.5.1
### Features
- [#11](https://github.com/gradio-app/daggr/pull/11) [`ce1d5f4`](https://github.com/gradio-app/daggr/commit/ce1d5f4deaac60d95d9a021b0aa057bc2941b018) - Fixes. Thanks @abidlabs!
- [#13](https://github.com/gradio-app/daggr/pull/13) [`3246921`](https://github.com/gradio-app/daggr/commit/32469213dad5fd29a7ac85938dffbd976e2c6643) - fixes. Thanks @abidlabs!
## 0.5.0
### Features
- [#8](https://github.com/gradio-app/daggr/pull/8) [`e480065`](https://github.com/gradio-app/daggr/commit/e480065dd058dbf19053a80956dbfc90cf3e3caf) - Improving security around executor and various bug fixes. Thanks @abidlabs!
## 0.4.0
### Features
- [#1](https://github.com/gradio-app/daggr/pull/1) [`23538c8`](https://github.com/gradio-app/daggr/commit/23538c884fb3f2d84bbe4bf14f475dc85fa17c79) - Refactor files, add Dialogue component, and implement fully working podcast example. Thanks @abidlabs!
## 0.1.0
Initial release
================================================
FILE: CLAUDE.md
================================================
# CLAUDE.md
## Code Style
- AVOID inline comments
## Commands
```bash
pip install -e .[dev] # Install dev dependencies
pytest # Run tests
ruff check --fix --select I && ruff format # Lint and format
```
## Architecture
DAG-based workflow library connecting Gradio Spaces, ML models, and Python functions.
### Core Files
- **`graph.py`**: `Graph` class - holds nodes, edges, launches server
- **`node.py`**: `GradioNode`, `FnNode`, `InferenceNode`, `InteractionNode`
- **`port.py`**: `Port`, `ScatteredPort`, `GatheredPort` for parallel execution
- **`edge.py`**: `Edge` class for node connections
- **`executor.py`**: Executes workflow nodes
- **`server.py`**: FastAPI server with WebSocket for real-time UI updates
- **`state.py`**: Session state persistence with SQLite
- **`frontend/`**: Svelte frontend for workflow visualization
### Node Definition Pattern
Nodes use `inputs` and `outputs` dicts:
```python
node = GradioNode(
space_or_url="owner/space",
api_name="/endpoint",
inputs={
"param": gr.Textbox(label="Label"), # UI input
"other": other_node.output_port, # Port connection
"fixed": "constant_value", # Fixed value
},
outputs={
"result": gr.Audio(label="Result"),
"hidden": gr.Text(visible=False), # Hidden output
},
)
```
### Key Patterns
1. **Port access**: `node.port_name` returns a `Port` for connections
2. **Graph creation**: `Graph(nodes=[...])` auto-wires from port connections in inputs
3. **Internal attrs**: Use `_` prefix (`_name`, `_fn`) to avoid port name conflicts
## Issue Workflow
1. Create branch: `git checkout -b fix-issue-NUMBER`
2. Implement fix
3. Run: `pytest && ruff check --fix --select I && ruff format`
4. Do not commit - leave for user
================================================
FILE: CONTRIBUTING.md
================================================
# Contributing
Thank you for your interest in contributing! This document provides guidelines and information for contributing to the project.
## Contribution Guidelines
We welcome contributions that:
- Improve or enhance core functionality
- Fix bugs in existing features
- Add essential features that align with the project's goals
## Development Setup
1. Fork and clone the repository
2. Install the package with development dependencies
```bash
pip install -e ".[dev]"
```
3. Run tests before submitting changes:
```bash
python -m pytest
```
4. Build the frontend:
The project includes a Svelte-based frontend that must be built for the app to function correctly (requires Node 24+ and npm 11+).
```bash
cd daggr/frontend
npm install
npm run build
cd ../..
```
5. Format your code using Ruff:
```bash
ruff check --fix --select I && ruff format
```
## Pull Request Process
1. Ensure your code passes all tests
2. Format your code using Ruff
3. Update documentation if necessary
4. Submit a pull request with a clear description of your changes
Thank you for contributing!
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2024 Your Name
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: MANIFEST.in
================================================
include LICENSE
include README.md
include daggr/package.json
recursive-include daggr *.py *.pyi
recursive-include daggr/frontend/dist *
recursive-include daggr/assets *
prune daggr/canvas-component
prune daggr/frontend/node_modules
prune daggr/frontend/src
================================================
FILE: README.md
================================================
<h3 align="center">
<div style="display:flex;flex-direction:row;">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="daggr/assets/logo_dark.png">
<source media="(prefers-color-scheme: light)" srcset="daggr/assets/logo_light.png">
<img width="75%" alt="daggr Logo" src="daggr/assets/logo_light.png">
</picture>
<p>DAG-based Gradio workflows!</p>
</div>
</h3>
`daggr` is a Python library for building AI workflows that connect [Gradio](https://github.com/gradio-app/gradio) apps, ML models (through [Hugging Face Inference Providers](https://huggingface.co/docs/inference-providers/en/index)), and custom Python functions. It automatically generates a visual canvas for your workflow allowing you to inspect intermediate outputs, rerun any step any number of times, and preserves state for complex or long-running workflows. Daggr also tracks provenance: when you browse through previous results, it automatically restores the exact inputs that produced each output, and visually indicates which parts of your workflow are stale.
<img width="1462" height="508" alt="Screenshot 2026-01-26 at 1 01 58 PM" src="https://github.com/user-attachments/assets/b751abd8-e143-4882-817b-036fb66a6d92" />
## Installation
```bash
pip install daggr
```
(requires Python 3.10 or higher).
## Quick Start
After installing `daggr`, create a new Python file, say `app.py`, and paste this code:
```python
import random
import gradio as gr
from daggr import GradioNode, Graph
glm_image = GradioNode(
"hf-applications/Z-Image-Turbo",
api_name="/generate_image",
inputs={
"prompt": gr.Textbox( # An input node is created for the prompt
label="Prompt",
value="A cheetah in the grassy savanna.",
lines=3,
),
"height": 1024, # Fixed value (does not appear in the canvas)
"width": 1024, # Fixed value (does not appear in the canvas)
"seed": random.random, # Functions are rerun every time the workflow is run (not shown in the canvas)
},
outputs={
"image": gr.Image(
label="Image" # Display original image
),
},
)
background_remover = GradioNode(
"hf-applications/background-removal",
api_name="/image",
inputs={
"image": glm_image.image,
},
postprocess=lambda _, final: final,
outputs={
"image": gr.Image(label="Final Image"), # Display only final image
},
)
graph = Graph(
name="Transparent Background Image Generator", nodes=[glm_image, background_remover]
)
graph.launch()
```
Run `daggr app.py` to start the app with hot reloading (or `python app.py` for standard execution). You should see a Daggr app like the one shown above that you can use to generate images with a transparent background!
## When to (Not) Use Daggr
Use Daggr when:
* You want to define an AI workflow in Python involving Gradio Spaces, inference providers, or custom functions
* The workflow is complex enough that inspecting intermediate outputs or rerunning individual steps is useful
* You need a fixed pipeline that you or others can run with different inputs
* You want to explore variations: generate multiple outputs, compare them, and always know exactly what inputs produced each result
**Why not... ComfyUI?** ComfyUI is a visual node editor where you build workflows by dragging and connecting nodes. Daggr takes a code-first approach: you define workflows in Python and the visual canvas is generated automatically. If you prefer writing code over visual editing, Daggr may be a better fit. In addition, Daggr works with Gradio Spaces and Hugging Face models directly, no need for specialized nodes.
**Why not... Airflow/Prefect?** Daggr was inspired by Airflow/Prefect, but whereas the focus of these orchestration platforms is scheduling, monitoring, and managing pipelines at scale, Daggr is built for interactive AI/ML workflows with real-time visual feedback and immediate execution, making it ideal for prototyping, demos, and workflows where you want to inspect intermediate outputs and rerun individual steps on the fly.
**Why not... Gradio?** Gradio creates web UIs for individual ML models and demos. While complex workflows can be built in Gradio, they often fail in ways that are hard to debug when using the Gradio app. Daggr tries to provide a transparent, easily-inspectable way to chain multiple Gradio apps, custom Python functions, and inference providers through a visual canvas.
Don't use Daggr when:
* You need a simple UI for a single model or function - consider using Gradio directly
* You want a node-based editor for building workflows visually - consider using ComfyUI instead
## How It Works
A Daggr workflow consists of **nodes** connected in a directed graph. Each node represents a computation: a Gradio Space API call, an inference call to a model, or a Python function.
Each node has **input ports** and **output ports**, which correspond to the node's parameters and return values. Ports are how data flows between nodes.
**Input ports** can be connected to:
- A previous node's output port → creates an edge, data flows automatically
- A Gradio component → creates a standalone input in the UI
- A fixed value → passed directly, doesn't appear in UI
- A `Callable` → called each time the node runs (useful for random seeds)
**Output ports** can be:
- A Gradio component → displays the output in the node's card
- `None` → output not displayed in the node's card but port can still connect to downstream nodes
### Node Types
#### `GradioNode`
Calls a Gradio Space API endpoint. Use this to connect to any Gradio app on Hugging Face Spaces or running locally.
```python
from daggr import GradioNode
import gradio as gr
image_gen = GradioNode(
space_or_url="black-forest-labs/FLUX.1-schnell", # HF Space ID or URL
api_name="/infer", # API endpoint name
inputs={
"prompt": gr.Textbox(label="Prompt"), # Creates UI input
"seed": 42, # Fixed value
"width": 1024,
"height": 1024,
},
outputs={
"image": gr.Image(label="Generated Image"), # Display in node card
},
)
```
**Finding the right inputs:** To find what parameters a GradioNode expects, go to the Gradio Space and click "Use via API" at the bottom of the page. This shows you the API endpoints and their parameters. For example, if the API page shows:
```python
from gradio_client import Client
client = Client("black-forest-labs/FLUX.1-schnell")
result = client.predict(
prompt="Hello!!",
seed=0,
randomize_seed=True,
width=1024,
height=1024,
num_inference_steps=4,
api_name="/infer"
)
```
Then your GradioNode inputs should use the same parameter names: `prompt`, `seed`, `randomize_seed`, `width`, `height`, `num_inference_steps`.
**Outputs:** Output port names can be anything you choose—they simply map to the return values of the API endpoint in order. If an endpoint returns `(image, seed)`, you might define:
```python
outputs={
"generated_image": gr.Image(), # Maps to first return value
"used_seed": gr.Number(), # Maps to second return value
}
```
#### `FnNode`
Runs a Python function. Input ports are automatically discovered from the function signature.
```python
from daggr import FnNode
import gradio as gr
def summarize(text: str, max_words: int = 100) -> str:
words = text.split()[:max_words]
return " ".join(words) + "..."
summarizer = FnNode(
fn=summarize,
inputs={
"text": gr.Textbox(label="Text to Summarize", lines=5),
"max_words": gr.Slider(minimum=10, maximum=500, value=100, label="Max Words"),
},
outputs={
"summary": gr.Textbox(label="Summary"),
},
)
```
**Inputs:** Keys in the `inputs` dict must match the function's parameter names. If you don't specify an input, it uses the function's default value (if available).
**Outputs:** Return values are mapped to output ports in the same order they are defined in the `outputs` dict—just like GradioNode. For a single output, simply return the value. For multiple outputs, return a tuple:
```python
def process(text: str) -> tuple[str, int]:
return text.upper(), len(text)
node = FnNode(
fn=process,
inputs={"text": gr.Textbox()},
outputs={
"uppercase": gr.Textbox(), # First return value
"length": gr.Number(), # Second return value
},
)
```
Note: If you return a dict or list, it will be treated as a single value (mapped to the first output port), not as a mapping to output ports.
**Concurrency:** By default, FnNodes execute sequentially (one at a time per user session) to prevent resource contention from concurrent function calls. If your function is safe to run in parallel, you can enable concurrent execution:
```python
# Allow this node to run in parallel with other nodes
node = FnNode(my_func, concurrent=True)
# Share a resource limit with other nodes (e.g., GPU memory)
gpu_node_1 = FnNode(process_image, concurrency_group="gpu", max_concurrent=2)
gpu_node_2 = FnNode(enhance_image, concurrency_group="gpu", max_concurrent=2)
```
| Parameter | Default | Description |
|-----------|---------|-------------|
| `concurrent` | `False` | If `True`, allow parallel execution |
| `concurrency_group` | `None` | Name of a group sharing a concurrency limit |
| `max_concurrent` | `1` | Max parallel executions in the group |
> **Tip:** When possible, prefer `GradioNode` or `InferenceNode` over `FnNode`. These nodes automatically run concurrently (they're external API calls), and your Hugging Face token is automatically passed through for ZeroGPU quota tracking, private Spaces access, and gated model access.
#### `InferenceNode`
Calls a model via [Hugging Face Inference Providers](https://huggingface.co/docs/inference-providers/en/index). This lets you use models hosted on the Hugging Face Hub without downloading them.
```python
from daggr import InferenceNode
import gradio as gr
llm = InferenceNode(
model="meta-llama/Llama-3.1-8B-Instruct",
inputs={
"prompt": gr.Textbox(label="Prompt", lines=3),
},
outputs={
"response": gr.Textbox(label="Response"),
},
)
```
**Inputs:** The expected inputs depend on the model's task type. For text generation models, use `prompt`. For other tasks, check the model's documentation on the Hub.
**Outputs:** Like other nodes, output names are arbitrary and map to return values in order.
> **Tip:** `InferenceNode` and `GradioNode` automatically run concurrently and pass your HF token for ZeroGPU, private Spaces, and gated models. Prefer these over `FnNode` when possible.
### Preprocessing and Postprocessing
`GradioNode`, `FnNode`, and `InferenceNode` all support optional `preprocess` and `postprocess` hooks that transform data on the way in and out of a node.
**`preprocess`** receives the input dict and returns a modified dict before the node executes. This is useful when an upstream node outputs data in a different format than the downstream node expects:
```python
def fix_image_input(inputs):
img = inputs.get("image")
if isinstance(img, dict) and "path" in img:
inputs["image"] = img["path"]
return inputs
describer = GradioNode(
"vikhyatk/moondream2",
api_name="/answer_question",
preprocess=fix_image_input,
inputs={"image": image_gen.result, "prompt": "Describe this image."},
outputs={"description": gr.Textbox()},
)
```
**`postprocess`** receives the raw return values from the node and lets you reshape them before they are mapped to output ports. If the node returns multiple values (a tuple), each value is passed as a separate argument. This is essential when working with Spaces that return extra values you don't need:
```python
background_remover = GradioNode(
"hf-applications/background-removal",
api_name="/image",
inputs={"image": some_node.image},
postprocess=lambda original, final: final, # Space returns (original, processed); keep only processed
outputs={"image": gr.Image(label="Result")},
)
```
Another common pattern is extracting a specific item from a complex return value:
```python
image_gen = GradioNode(
"multimodalart/stable-cascade",
api_name="/run",
inputs={...},
postprocess=lambda images, seed_used, seed_number: images[0]["image"], # Extract first image
outputs={"image": gr.Image(label="Generated Image")},
)
```
### File Handling
> **Key difference from Gradio:** In daggr, all file-based data (images, audio, video, 3D models) is passed between nodes as **file path strings**. Gradio's `type` parameter (e.g., `Image(type="numpy")`) is ignored — daggr does not convert files to numpy arrays, PIL images, or any other in-memory format.
This means:
- **Inputs** to your node arrive as file path strings (e.g., `"/tmp/daggr/abc123.png"`)
- **Outputs** from your node should be file path strings pointing to a file on disk
If your node expects a different format, use `preprocess` to convert file paths on the way in, and `postprocess` to convert back to file paths on the way out. This works with all node types:
```python
from PIL import Image
def load_image(inputs):
inputs["image"] = Image.open(inputs["image"])
return inputs
def save_image(result):
out_path = "/tmp/processed.png"
result.save(out_path)
return out_path
node = FnNode(
lambda image: image.rotate(90),
preprocess=load_image,
postprocess=save_image,
inputs={"image": gr.Image(label="Input")},
outputs={"output": gr.Image(label="Rotated")},
)
```
For audio:
```python
import soundfile as sf
def load_audio(inputs):
data, sr = sf.read(inputs["audio"])
inputs["audio"] = (sr, data)
return inputs
def save_audio(result):
sr, data = result
out_path = "/tmp/processed.wav"
sf.write(out_path, data, sr)
return out_path
```
#### `InputNode`
The `InputNode` allows you to group multiple Gradio input components (such as `gr.Textbox`, `gr.Slider`, or `gr.Dropdown`) into a single, organized block on the canvas. This is particularly useful for workflows with many parameters, preventing the UI from becoming cluttered with numerous individual input nodes.
**How it works:** Each key defined in the `ports` dictionary automatically becomes a distinct **output port** on the node. You can then wire these ports to downstream processing nodes.
```python
import gradio as gr
from daggr import InputNode, GradioNode, Graph
parameters = InputNode(
name="Parameters",
ports={
"prompt": gr.Textbox(
label="Prompt",
value="A cheetah in the grassy savanna.",
lines=3,
),
"height": gr.Slider(
label="Height", value=1024, minimum=512, maximum=2048, step=128
),
"width": gr.Slider(
label="Width", value=1024, minimum=512, maximum=2048, step=128
)
},
)
glm_image = GradioNode(
"hf-applications/Z-Image-Turbo",
api_name="/generate_image",
inputs={
"prompt": parameters.prompt,
"height": parameters.height,
"width": parameters.width,
},
outputs={
"image": gr.Image(label="Image"),
},
)
graph = Graph(name="Grouped Inputs Demo", nodes=[glm_image])
graph.launch()
```
### Node Concurrency
Different node types have different concurrency behaviors:
| Node Type | Concurrency | Why |
|-----------|-------------|-----|
| `GradioNode` | **Concurrent** | External API calls—safe to parallelize |
| `InferenceNode` | **Concurrent** | External API calls—safe to parallelize |
| `FnNode` | **Sequential** (default) | Local Python code may have resource constraints |
**Why sequential by default for FnNode?** Local Python functions often:
- Access shared resources (files, databases, GPU memory)
- Use libraries that aren't thread-safe
- Consume significant CPU/memory
By running FnNodes sequentially per session, daggr prevents race conditions and resource contention. If your function is safe to run in parallel, opt in with `concurrent=True`.
**Concurrency groups** let multiple nodes share a resource limit:
```python
# Both nodes share GPU—at most 2 concurrent executions total
upscale = FnNode(upscale_image, concurrency_group="gpu", max_concurrent=2)
enhance = FnNode(enhance_image, concurrency_group="gpu", max_concurrent=2)
```
### Testing Nodes
You can test-run any node in isolation using the `.test()` method:
```python
tts = GradioNode("mrfakename/MeloTTS", api_name="/synthesize", ...)
result = tts.test(text="Hello world", speaker="EN-US")
# Returns: {"audio": "/path/to/audio.wav"}
```
If called without arguments, `.test()` auto-generates example values using each input component's `.example_value()` method:
```python
result = tts.test() # Uses gr.Textbox().example_value(), etc.
```
This is useful for quickly checking what format a node returns without wiring up a full workflow.
### Input Types
Each node's `inputs` dict accepts four types of values:
| Type | Example | Result |
|------|---------|--------|
| **Gradio component** | `gr.Textbox(label="Topic")` | Creates UI input |
| **Port reference** | `other_node.output_name` | Connects nodes |
| **Fixed value** | `"Auto"` or `42` | Constant, no UI |
| **Callable** | `random.random` | Called each run, no UI |
### Output Types
Each node's `outputs` dict accepts two types of values:
| Type | Example | Result |
|------|---------|--------|
| **Gradio component** | `gr.Image(label="Result")` | Displays output in node card |
| **None** | `None` | Hidden, but can connect to downstream nodes |
### Scatter / Gather (experimental)
When a node outputs a list and you want to process each item individually, use `.each` to scatter and `.all()` to gather:
```python
script = FnNode(fn=generate_script, inputs={...}, outputs={"lines": gr.JSON()})
tts = FnNode(
fn=text_to_speech,
inputs={
"text": script.lines.each["text"], # Scatter: run once per item
"speaker": script.lines.each["speaker"],
},
outputs={"audio": gr.Audio()},
)
final = FnNode(
fn=combine_audio,
inputs={"audio_files": tts.audio.all()}, # Gather: collect all outputs
outputs={"audio": gr.Audio()},
)
```
### Choice Nodes (experimental)
Sometimes you want to offer multiple alternatives for the same step in your workflow—for example, two different TTS providers or image generators. Use the `|` operator to create a **choice node** that lets users switch between variants in the UI:
```python
host_voice = GradioNode(
space_or_url="abidlabs/tts",
api_name="/generate_voice_design",
inputs={
"voice_description": gr.Textbox(label="Host Voice"),
"language": "Auto",
"text": "Hi! I'm the host!",
},
outputs={"audio": gr.Audio(label="Host Voice")},
) | GradioNode(
space_or_url="mrfakename/E2-F5-TTS",
api_name="/basic_tts",
inputs={
"ref_audio_input": gr.Audio(label="Reference Audio"),
"gen_text_input": gr.Textbox(label="Text to Generate"),
},
outputs={"audio": gr.Audio(label="Host Voice")},
)
# Downstream nodes connect to host_voice.audio regardless of which variant is selected
dialogue = FnNode(
fn=generate_dialogue,
inputs={"host_voice": host_voice.audio, ...},
...
)
```
In the canvas, choice nodes display an accordion UI where you can:
- See all available variants
- Click to select which variant to use
- View the selected variant's input components
The selected variant is persisted per sheet, so your choice is remembered across page refreshes. All variants must have the same output ports (so downstream connections work regardless of selection), but they can have different input ports.
## Putting It Together: A Mock Podcast Generator
```python
import gradio as gr
from daggr import FnNode, GradioNode, Graph
# Generate voice profiles
host_voice = GradioNode(
space_or_url="abidlabs/tts",
api_name="/generate_voice_design",
inputs={
"voice_description": gr.Textbox(label="Host Voice", value="Deep British voice..."),
"language": "Auto",
"text": "Hi! I'm the host.",
},
outputs={"audio": gr.Audio(label="Host Voice")},
)
guest_voice = GradioNode(
space_or_url="abidlabs/tts",
api_name="/generate_voice_design",
inputs={
"voice_description": gr.Textbox(label="Guest Voice", value="Friendly American voice..."),
"language": "Auto",
"text": "Hi! I'm the guest.",
},
outputs={"audio": gr.Audio(label="Guest Voice")},
)
# Generate dialogue (would be an LLM call in production)
def generate_dialogue(topic: str, host_voice: str, guest_voice: str) -> tuple[list, str]:
dialogue = [
{"voice": host_voice, "text": "Hello, how are you?"},
{"voice": guest_voice, "text": "I'm great, thanks!"},
]
html = "<b>Host:</b> Hello!<br><b>Guest:</b> I'm great!"
return dialogue, html # Returns tuple: first value -> "json", second -> "html"
dialogue = FnNode(
fn=generate_dialogue,
inputs={
"topic": gr.Textbox(label="Topic", value="AI"),
"host_voice": host_voice.audio,
"guest_voice": guest_voice.audio,
},
outputs={
"json": gr.JSON(visible=False), # Maps to first return value
"html": gr.HTML(label="Script"), # Maps to second return value
},
)
# Generate audio for each line (scatter)
def text_to_speech(text: str, audio: str) -> str:
return audio # Would call TTS model in production
samples = FnNode(
fn=text_to_speech,
inputs={
"text": dialogue.json.each["text"],
"audio": dialogue.json.each["voice"],
},
outputs={"audio": gr.Audio(label="Sample")},
)
# Combine all audio (gather)
def combine_audio(audio_files: list[str]) -> str:
from pydub import AudioSegment
combined = AudioSegment.empty()
for path in audio_files:
combined += AudioSegment.from_file(path)
combined.export("output.mp3", format="mp3")
return "output.mp3"
final = FnNode(
fn=combine_audio,
inputs={"audio_files": samples.audio.all()},
outputs={"audio": gr.Audio(label="Full Podcast")},
)
graph = Graph(name="Podcast Generator", nodes=[host_voice, guest_voice, dialogue, samples, final])
graph.launch()
```
## Sharing and Hosting
Create a public URL to share your workflow with others:
```python
graph.launch(share=True)
```
This generates a temporary public URL (expires in 1 week) using Gradio's tunneling infrastructure.
### Deploying to Hugging Face Spaces
For permanent hosting, use `daggr deploy` to deploy your app to [Hugging Face Spaces](https://huggingface.co/spaces):
```bash
daggr deploy my_app.py
```
Assuming you are logged in locally with your [Hugging Face token](https://huggingface.co/settings/tokens), this command:
1. Extracts the Graph from your script
2. Creates a Space named after your Graph (e.g., "Podcast Generator" → `podcast-generator`)
3. Uploads your script and dependencies
4. Configures the Space with the Gradio SDK
#### Deploy Options
```bash
# Custom Space name
daggr deploy my_app.py --name my-custom-space
# Deploy to an organization
daggr deploy my_app.py --org huggingface
# Private Space with GPU
daggr deploy my_app.py --private --hardware t4-small
# Add secrets (e.g., API keys)
daggr deploy my_app.py --secret HF_TOKEN=xxx --secret OPENAI_KEY=yyy
# Preview without deploying
daggr deploy my_app.py --dry-run
```
| Option | Short | Description |
|--------|-------|-------------|
| `--name` | `-n` | Space name (default: derived from Graph name) |
| `--title` | `-t` | Display title (default: Graph name) |
| `--org` | `-o` | Organization to deploy under |
| `--private` | `-p` | Make the Space private |
| `--hardware` | | Hardware tier: `cpu-basic`, `cpu-upgrade`, `t4-small`, `t4-medium`, `a10g-small`, etc. |
| `--secret` | `-s` | Add secrets (repeatable) |
| `--requirements` | `-r` | Custom requirements.txt path |
| `--dry-run` | | Preview what would be deployed |
The deploy command automatically:
- Detects local Python imports and includes them
- Uses existing `requirements.txt` if present, or generates one with `daggr`
- Renames your script to `app.py` (HF Spaces convention)
- Generates the required `README.md` with Space metadata
### Manual Deployment
You can also deploy manually by creating a new Space with the Gradio SDK, adding your workflow code to `app.py`, and including `daggr` in your `requirements.txt`.
Daggr automatically reads the `GRADIO_SERVER_NAME` and `GRADIO_SERVER_PORT` environment variables, which Hugging Face Spaces sets automatically for Gradio apps. This means your daggr app will work on Spaces without any additional configuration.
## Persistence and Sheets
Daggr automatically saves your workflow state—input values, node results, and canvas position—so you can pick up where you left off after a page reload.
### Sheets
**Sheets** are like separate workspaces within a single Daggr app. Each sheet has its own:
- Input values for all nodes
- Cached results from previous runs
- Canvas zoom and pan position
Use sheets to work on multiple projects within the same workflow. For example, in a podcast generator app, each sheet could represent a different podcast episode you're working on.
The sheet selector appears in the title bar. Click to switch between sheets, create new ones, rename them (double-click), or delete them.
### Result History and Provenance Tracking
Every time a node runs, Daggr saves not just the output, but also a snapshot of all input values at that moment. This enables powerful exploratory workflows:
**Browsing previous results**: Use the `‹` and `›` arrows in the node footer to navigate through all cached results for that node (shown as "1/3", "2/3", etc.).
**Automatic input restoration**: When you select a previous result, Daggr automatically restores the input values that produced it. This means you can:
1. Generate multiple variations by running a node several times with different inputs
2. Browse through your results to find the best one
3. When you select a result, see exactly what inputs created it
4. Continue your workflow from that point with all the original context intact
**Cascading restoration**: When you toggle through results on a node, Daggr also automatically selects the matching result on downstream nodes (if one exists). For example, if you generated 3 images and removed the background from 2 of them, selecting image #1 will automatically show background-removal result #1.
#### Visual Staleness Indicators
Daggr uses edge colors to show you which parts of your workflow are up-to-date:
| Edge Color | Meaning |
|------------|---------|
| **Orange** | Fresh—the downstream node ran with this exact upstream value |
| **Gray** | Stale—the upstream value has changed, or the downstream hasn't run yet |
<img width="957" height="418" alt="image" src="https://github.com/user-attachments/assets/4acd0ec2-9561-44fc-8a40-d09ab972d717" />
<img width="957" height="418" alt="image" src="https://github.com/user-attachments/assets/683e7cbe-779f-44a9-9401-a6aafc57a936" />
Edges are stale when:
- You edit an input value (e.g., change a text prompt)
- You select a different cached result on an upstream node
- A downstream node hasn't been run yet
This visual feedback helps you understand at a glance which results are current and which need to be re-run. It's especially useful in long workflows where you might forget which steps you've already executed with your current inputs.
**Example workflow:**
1. Generate an image with prompt "A cheetah in the savanna" → edge turns orange
2. Edit the prompt to "A lion in the jungle" → edge turns gray (stale)
3. Re-run the image generation → edge turns orange again
4. Run the background removal node → that edge also turns orange
This provenance tracking is particularly valuable for creative workflows where you're exploring variations and want to always know exactly what inputs produced each output.
### How Persistence Works
| Environment | User Status | Persistence |
|-------------|-------------|-------------|
| **Local** | Not logged in | ✅ Saved as "local" user |
| **Local** | HF logged in | ✅ Saved under your HF username |
When running locally, your data is stored in a SQLite database at `~/.cache/huggingface/daggr/sessions.db`.
### The `persist_key` Parameter
By default, the `persist_key` is derived from your graph's `name`:
```python
Graph(name="My Podcast Generator") # persist_key = "my_podcast_generator"
```
If you later rename your app but want to keep the existing saved data, set `persist_key` explicitly:
```python
Graph(name="Podcast Generator v2", persist_key="my_podcast_generator")
```
### Disabling Persistence
For scratch workflows or demos where you don't want data saved:
```python
Graph(name="Quick Demo", persist_key=False)
```
This disables all persistence—no sheets UI, no saved state.
## Hugging Face Authentication
Daggr automatically uses your local Hugging Face token for both `GradioNode` and `InferenceNode`. This enables:
- **ZeroGPU quota tracking**: Your HF token is sent to Gradio Spaces running on ZeroGPU, so your usage is tracked against your account's quota
- **Private Spaces access**: Connect to private Gradio Spaces you have access to
- **Gated models**: Use gated models on Hugging Face that require accepting terms of service
To log in with your Hugging Face account:
```bash
pip install huggingface_hub
hf auth login
```
You'll be prompted to enter your token, which you can find at https://huggingface.co/settings/tokens.
Once logged in, the token is saved locally and daggr will automatically use it for all `GradioNode` and `InferenceNode` calls—no additional configuration needed.
Alternatively, you can set the `HF_TOKEN` environment variable directly:
```bash
export HF_TOKEN=hf_xxxxx
```
## LLM-Friendly Error Messages
Daggr is designed to be LLM-friendly, making it easy for AI coding assistants to generate and debug workflows. To give your AI coding assistant context on how to use daggr, you can install the daggr skill:
```bash
npx skills add gradio-app/daggr
```
<img width="3444" height="2342" alt="image" src="https://github.com/user-attachments/assets/b054d648-0f75-43a8-9335-1480d6bf9263" />
When you (or an LLM) make a mistake, Daggr provides detailed, actionable error messages with suggestions:
**Invalid API endpoint:**
```
ValueError: API endpoint '/infer' not found in 'hf-applications/background-removal'.
Available endpoints: ['/image', '/text', '/png']. Did you mean '/image'?
```
**Typo in parameter name:**
```
ValueError: Invalid parameter(s) {'promt'} for endpoint '/generate_image' in
'hf-applications/Z-Image-Turbo'. Did you mean: 'promt' -> 'prompt'?
Valid parameters: {'width', 'height', 'seed', 'prompt'}
```
**Missing required parameter:**
```
ValueError: Missing required parameter(s) {'prompt'} for endpoint '/generate_image'
in 'hf-applications/Z-Image-Turbo'. These parameters have no default values.
```
**Invalid output port reference:**
```
ValueError: Output port 'img' not found on node 'Z-Image-Turbo'.
Available outputs: image. Did you mean 'image'?
```
**Invalid function parameter:**
```
ValueError: Invalid input(s) {'toppic'} for function 'generate_dialogue'.
Did you mean: 'toppic' -> 'topic'? Valid parameters: {'topic', 'host_voice', 'guest_voice'}
```
**Invalid model name:**
```
ValueError: Model 'meta-llama/nonexistent-model' not found on Hugging Face Hub.
Please check the model name is correct (format: 'username/model-name').
```
These errors make it easy for LLMs to understand what went wrong and fix the generated code automatically, enabling a smoother AI-assisted development experience.
### Discovering Output Formats
When building workflows, LLMs can use `.test()` to discover a node's actual output format:
```python
# LLM wants to understand what whisper returns
whisper = InferenceNode("openai/whisper-large-v3", inputs={"audio": gr.Audio()})
result = whisper.test(audio="sample.wav")
# Returns: {"text": "Hello, how are you?"}
```
This helps LLMs:
- Understand the structure of node outputs
- Apply `postprocess` functions to extract specific values
- Create intermediate `FnNode`s to transform data between nodes
For example, if a node returns multiple values but you only need one:
```python
# After discovering the output format with .test()
bg_remover = GradioNode(
"hf-applications/background-removal",
api_name="/image",
inputs={"image": some_image.output},
postprocess=lambda original, final: final, # Keep only the second output
outputs={"image": gr.Image()},
)
```
## Running Locally
While in our examples above, we've seen how Daggr works with remote Gradio Spaces and Hugging Face Inference Providers, it's also well-suited for completely local, offline workflows.
### Automatic Local Execution
The easiest way to run a Space locally is to set `run_locally=True` on any `GradioNode`. Daggr will automatically clone the Space, install dependencies in an isolated virtual environment, and launch the Gradio app:
```python
from daggr import GradioNode, Graph
import gradio as gr
# Automatically clone and run the Space locally
background_remover = GradioNode(
"hf-applications/background-removal",
api_name="/image",
run_locally=True, # Run locally instead of calling the remote API
inputs={"image": gr.Image(label="Input Image")},
outputs={"final_image": gr.Image(label="Output")},
)
graph = Graph(name="Local Background Removal", nodes=[background_remover])
graph.launch()
```
On first run, daggr will:
1. Clone the Space repository to `~/.cache/huggingface/daggr/spaces/`
2. Create an isolated virtual environment with the Space's dependencies
3. Launch the Gradio app on an available port
4. Connect to it automatically
Subsequent runs reuse the cached clone and venv, making startup much faster.
### Graceful Fallback
If local execution fails (missing dependencies, GPU requirements, etc.), daggr automatically falls back to the remote API and prints helpful guidance:
```
⚠️ Local execution failed for 'owner/space-name'
Reason: Failed to install dependencies
Logs: ~/.cache/huggingface/daggr/logs/owner_space-name_pip_install_2026-01-27.log
Falling back to remote API...
```
To disable fallback and see the full error (useful for debugging):
```bash
export DAGGR_LOCAL_NO_FALLBACK=1
```
### Environment Variables
| Variable | Default | Description |
|----------|---------|-------------|
| `DAGGR_LOCAL_TIMEOUT` | `120` | Seconds to wait for the app to start |
| `DAGGR_LOCAL_VERBOSE` | `0` | Set to `1` to show app stdout/stderr |
| `DAGGR_LOCAL_NO_FALLBACK` | `0` | Set to `1` to disable fallback to remote |
| `DAGGR_UPDATE_SPACES` | `0` | Set to `1` to re-clone cached Spaces |
| `DAGGR_DEPENDENCY_CHECK` | *(unset)* | `skip`, `update`, or `error` — controls upstream hash checking |
| `GRADIO_SERVER_NAME` | `127.0.0.1` | Host to bind to. Set to `0.0.0.0` on HF Spaces |
| `GRADIO_SERVER_PORT` | `7860` | Port to bind to |
### Manual Local URL
You can also run a Gradio app yourself and point to it directly:
```python
from daggr import GradioNode, Graph
import gradio as gr
# Connect to a Gradio app you're running locally
local_model = GradioNode(
"http://localhost:7860", # Local URL instead of Space ID
api_name="/predict",
inputs={"text": gr.Textbox(label="Input")},
outputs={"result": gr.Textbox(label="Output")},
)
graph = Graph(name="Local Workflow", nodes=[local_model])
graph.launch()
```
This approach lets you run your entire workflow offline, use custom or fine-tuned models, and avoid API rate limits.
### API Access
Daggr workflows can be called programmatically via REST API, making it easy to integrate workflows into other applications or run automated tests.
#### Discovering the API Schema
First, get the API schema to see available inputs and outputs:
```bash
curl http://localhost:7860/api/schema
```
Response:
```json
{
"subgraphs": [
{
"id": "main",
"inputs": [
{"node": "image_gen", "port": "prompt", "type": "textbox", "id": "image_gen__prompt"}
],
"outputs": [
{"node": "background_remover", "port": "image", "type": "image"}
]
}
]
}
```
#### Calling the Workflow
Execute the entire workflow by POSTing inputs to `/api/call`:
```bash
curl -X POST http://localhost:7860/api/call \
-H "Content-Type: application/json" \
-d '{"inputs": {"image_gen__prompt": "A mountain landscape"}}'
```
Response:
```json
{
"outputs": {
"background_remover": {
"image": "/file/path/to/output.png"
}
}
}
```
Input keys follow the format `{node_name}__{port_name}` (with spaces/dashes replaced by underscores).
#### Disconnected Subgraphs
If your workflow has multiple disconnected subgraphs, use `/api/call/{subgraph_id}`:
```bash
# List available subgraphs
curl http://localhost:7860/api/schema
# Call a specific subgraph
curl -X POST http://localhost:7860/api/call/subgraph_0 \
-H "Content-Type: application/json" \
-d '{"inputs": {...}}'
```
#### Python Example
```python
import requests
# Get schema
schema = requests.get("http://localhost:7860/api/schema").json()
# Execute workflow
response = requests.post(
"http://localhost:7860/api/call",
json={"inputs": {"my_node__text": "Hello world"}}
)
outputs = response.json()["outputs"]
```
## Hot Reload Mode
During development, you can use the `daggr` CLI to run your app with automatic hot reloading. When you make changes to your Python file or its dependencies, the app automatically restarts:
```bash
daggr examples/01_quickstart.py
```
This is much faster than manually stopping and restarting your app each time you make a change.
### CLI Options
```bash
daggr <script> [options]
```
| Option | Description |
|--------|-------------|
| `--host` | Host to bind to (default: `127.0.0.1`) |
| `--port` | Port to bind to (default: `7860`) |
| `--no-reload` | Disable auto-reload |
| `--no-watch-daggr` | Don't watch daggr source for changes |
### What Gets Watched
By default, the CLI watches for changes in:
- **Your script file** and its directory
- **Local imports** from your script
- **The daggr source code** itself (useful when developing daggr)
To disable watching the daggr source (e.g., in production-like testing):
```bash
daggr examples/01_quickstart.py --no-watch-daggr
```
### API Caching
To speed up reloads, daggr caches Gradio Space API info in `~/.cache/huggingface/daggr/`. This means:
- **First run**: Connects to each Gradio Space to fetch API info (cached to disk)
- **Subsequent reloads**: Loads from cache, no network calls needed
If you change a Space's API or encounter stale cache issues, clear the cache:
```bash
rm -rf ~/.cache/huggingface/daggr
```
### When to Use Hot Reload
Use `daggr <script>` when you're actively developing and want instant feedback on changes.
Use `python <script>` when you want the standard behavior (no file watching, direct execution).
## Upstream Dependency Tracking
When your workflow references external Gradio Spaces or Hugging Face models, those dependencies can change at any time—a Space author might update the model, change the API, or alter default behavior. This can silently break reproducibility: the same workflow with the same inputs may produce different results weeks later.
To address this, daggr tracks the commit SHA of every upstream Space and model the first time your app launches. On subsequent launches, daggr compares the cached SHA against the current version. If an upstream dependency has changed, you'll see a terminal warning:
```
⚠️ Upstream dependency changes detected:
• space 'mrfakename/MeloTTS' (node: MeloTTS)
cached: a1b2c3d4e5f6
current: f6e5d4c3b2a1
How would you like to handle 'mrfakename/MeloTTS'?
[1] Duplicate the original version under your namespace (safer)
[2] Update to the latest version
```
**Option 1 (Spaces only)** downloads the Space at the exact commit you originally built against and re-uploads it under your Hugging Face namespace, so your workflow continues using the known-good version. This requires being logged in via `huggingface-cli login`.
**Option 2** updates the cached hash to accept the new version.
For CI/CD or non-interactive environments, set the `DAGGR_DEPENDENCY_CHECK` environment variable:
| Value | Behavior |
|-------|----------|
| `skip` | Skip all dependency checks |
| `update` | Auto-accept upstream changes |
| `error` | Fail if any dependency has changed |
Dependency hashes are stored in `~/.cache/huggingface/daggr/_dependency_hashes.json`.
## Beta Status
> [!WARNING]
> Daggr is in active development. APIs may change between versions, and while we persist workflow state locally, data loss is possible during updates. We recommend not relying on daggr for production-critical workflows yet. Please [report issues](https://github.com/gradio-app/daggr/issues) if you encounter bugs!
## Development
```bash
pip install -e ".[dev]"
ruff check --fix --select I && ruff format
```
## License
MIT License
================================================
FILE: RELEASE.md
================================================
# Making a release
> [!NOTE]
> VERSION needs to be formatted following the `v{major}.{minor}.{patch}` convention. We need to follow this convention to be able to retrieve versioned scripts.
## Major/Minor Release
### 1. Ensure your local repository is up to date with the upstream repository
```bash
git checkout main
git pull origin main
```
> [!WARNING]
> Do not merge other pull requests into `main` until the release is done. This is to ensure that the release is stable and does not include any untested changes. Announce internally to other maintainers that you are doing a release and that they must not merge PRs until the release is done.
### 2. Create a release branch from main
```bash
git checkout -b release-v{major}.{minor}
```
### 3. Change the version in the following file
- `daggr/package.json`:
```diff
- "version": "{major}.{minor}.0.dev0"
+ "version": "{major}.{minor}.0"
```
### 4. Commit and push these changes
```shell
git add daggr/package.json
git commit -m 'Release: {major}.{minor}'
git push origin release-v{major}.{minor}
```
### 5. Create a pull request
from `release-v{major}.{minor}` to `main`, named `Release: v{major}.{minor}`, wait for tests to pass, and request a review.
### 6. Once the pull request is approved, merge it into `main`
This will automatically trigger the CI to publish the package to PyPI.
### 7. Add a tag in git to mark the release
```shell
git checkout main
git pull origin main
git tag -a v{major}.{minor}.0 -m 'Adds tag v{major}.{minor}.0 for PyPI'
git push origin v{major}.{minor}.0
```
### 8. Create a branch `v{major}.{minor}-release` for future patch releases
```shell
git checkout -b v{major}.{minor}-release
git push origin v{major}.{minor}-release
```
This ensures that future patch releases (`v{major}.{minor}.1`, `v{major}.{minor}.2`, etc.) can be made separately from `main`.
### 9. Create a GitHub Release
1. Go to the repo's releases section on GitHub.
2. Click **Draft a new release**.
3. Select the `v{major}.{minor}.0` tag you just created in step 7.
4. Add a title (`v{major}.{minor}.0`) and a short description of what's new.
5. Click **Publish Release**.
### 10. Bump to dev version
1. Create a branch `bump-dev-version-{major}.{minor+1}` from `main` and checkout to it.
```shell
git checkout -b bump-dev-version-{major}.{minor+1}
```
2. Change the version in `daggr/package.json`
```diff
- "version": "{major}.{minor}.0"
+ "version": "{major}.{minor+1}.0.dev0"
```
3. Commit and push these changes
```shell
git add daggr/package.json
git commit -m '⬆️ Bump dev version'
git push origin bump-dev-version-{major}.{minor+1}
```
4. Create a pull request from `bump-dev-version-{major}.{minor+1}` to `main`, named `⬆️ Bump dev version`, and request urgent review.
5. Once the pull request is approved, merge it into `main`.
6. The codebase is now ready for the next development cycle.
## Making a patch release
### 1. Ensure your local repository is up to date with the upstream repository
```bash
git checkout v{major}.{minor}-release
git pull origin main
```
### 2. Cherry-pick the changes you want to include in the patch release
```bash
git cherry-pick <commit-hash-0>
git cherry-pick <commit-hash-1>
...
```
### 3. Change the version in the following files
- `daggr/package.json`:
```diff
- "version": "{major}.{minor}.{patch-1}"
+ "version": "{major}.{minor}.{patch}"
```
### 4. Commit and push these changes
```shell
git add daggr/package.json
git commit -m 'Release: {major}.{minor}.{patch}'
git push origin v{major}.{minor}-release
```
### 5. Wait for the CI to pass
This will automatically trigger the CI to publish the package to PyPI.
### 6. Add a tag in git to mark the release
```shell
git tag -a v{major}.{minor}.{patch} -m 'Adds tag v{major}.{minor}.{patch} for PyPI'
git push origin v{major}.{minor}.{patch}
```
### 7. Create a GitHub Release
1. Go to the repo's releases section on GitHub.
2. Click **Draft a new release**.
3. Select the `v{major}.{minor}.{patch}` tag you just created in step 6.
4. Add a title (`v{major}.{minor}.{patch}`) and a short description of what's new.
5. Click **Publish Release**.
================================================
FILE: build_pypi.sh
================================================
#!/bin/bash
set -e
cd "$(dirname ${0})"
python3 -m pip install build
rm -rf dist/*
rm -rf build/*
python3 -m build -w
================================================
FILE: daggr/CHANGELOG.md
================================================
# daggr
## 0.8.0
### Features
- [#79](https://github.com/gradio-app/daggr/pull/79) [`d32cec2`](https://github.com/gradio-app/daggr/commit/d32cec21e93532ce7a7e66815922985c0bd5cb75) - feat: Introduce `InputNode` for grouping UI controls. Thanks @elismasilva!
- [#81](https://github.com/gradio-app/daggr/pull/81) [`34267c9`](https://github.com/gradio-app/daggr/commit/34267c9f93729cc4fa4839b15feb44a239897ec9) - stylized thin scrollbars. Thanks @elismasilva!
- [#77](https://github.com/gradio-app/daggr/pull/77) [`9002cca`](https://github.com/gradio-app/daggr/commit/9002cca2b312731572d4d80d7efe3cd9483e19b3) - Feature: Toggle Dark/Light Mode (based on @elismasilva's changes). Thanks @abidlabs!
## 0.7.0
### Features
- [#69](https://github.com/gradio-app/daggr/pull/69) [`297d104`](https://github.com/gradio-app/daggr/commit/297d104d0e3fbd6b59dfcd1f69c9a478de81bc3d) - Add stop button to cancel running nodes. Thanks @abidlabs!
- [#64](https://github.com/gradio-app/daggr/pull/64) [`a9e53c6`](https://github.com/gradio-app/daggr/commit/a9e53c64db6b3beede0b19b3876a3e50ab572233) - Fix ChoiceNode: public .name property and respect explicit names. Thanks @abidlabs!
- [#62](https://github.com/gradio-app/daggr/pull/62) [`695411c`](https://github.com/gradio-app/daggr/commit/695411ce94bc8fedd3320ac941ab41233bf8f887) - Standardize file handling: all files are path strings. Thanks @abidlabs!
- [#66](https://github.com/gradio-app/daggr/pull/66) [`1ed16f8`](https://github.com/gradio-app/daggr/commit/1ed16f806d535413c3718fc47d81d79d93d73ee0) - Fix gr.JSON rendering (use @render snippet syntax). Thanks @abidlabs!
- [#63](https://github.com/gradio-app/daggr/pull/63) [`00b05ac`](https://github.com/gradio-app/daggr/commit/00b05ac526642c91560cbebb258626a4511082c2) - Fix file downloads from private HF Spaces. Thanks @abidlabs!
- [#70](https://github.com/gradio-app/daggr/pull/70) [`33ccb74`](https://github.com/gradio-app/daggr/commit/33ccb7470a482ee1b09fddf1d51de81b1e2c4a40) - Fix gr.Image not rendering with initial value or None input. Thanks @abidlabs!
- [#68](https://github.com/gradio-app/daggr/pull/68) [`4b76dca`](https://github.com/gradio-app/daggr/commit/4b76dca815e2802b70ac03bb95fb03f21d81a8fa) - Add dependency hash tracking for upstream Spaces and models. Thanks @abidlabs!
## 0.6.0
### Features
- [#54](https://github.com/gradio-app/daggr/pull/54) [`c1abb26`](https://github.com/gradio-app/daggr/commit/c1abb260b254af6ca2060292232049ea89f0f944) - Fix cache. Thanks @abidlabs!
- [#56](https://github.com/gradio-app/daggr/pull/56) [`6e3dfc0`](https://github.com/gradio-app/daggr/commit/6e3dfc0a585b673adb77bb11ab1dcfd80d01da5a) - Add paste from clipboard button to Image component. Thanks @abidlabs!
- [#57](https://github.com/gradio-app/daggr/pull/57) [`76855ba`](https://github.com/gradio-app/daggr/commit/76855ba967e3f3132e8ec0590ae037d3151af310) - Fix dropdown options being clipped inside node. Thanks @abidlabs!
- [#58](https://github.com/gradio-app/daggr/pull/58) [`eb52b72`](https://github.com/gradio-app/daggr/commit/eb52b725b17d277e85f6eac1cc9d07f8068b011b) - Add theme support to daggr. Thanks @abidlabs!
- [#59](https://github.com/gradio-app/daggr/pull/59) [`78189a4`](https://github.com/gradio-app/daggr/commit/78189a4163b4041c814e52110b65754dc4dbf863) - Add run mode dropdown to control node execution scope. Thanks @abidlabs!
- [#39](https://github.com/gradio-app/daggr/pull/39) [`e8792ad`](https://github.com/gradio-app/daggr/commit/e8792ad1b5818ff8d13660b0b156f329bbc1c33a) - feat: add --state-db-path CLI arg and DAGGR_DB_PATH env var support. Thanks @leith-bartrich!
## 0.5.4
### Features
- [#27](https://github.com/gradio-app/daggr/pull/27) [`3952b2c`](https://github.com/gradio-app/daggr/commit/3952b2ccf30e7d18994f23049c2a2e84b323cfd6) - changes. Thanks @abidlabs!
## 0.5.3
### Features
- [#19](https://github.com/gradio-app/daggr/pull/19) [`cd956fe`](https://github.com/gradio-app/daggr/commit/cd956fe29945bdfd31bbe76fcb80d3f9c97cc301) - Add daggr tag to deployed Spaces. Thanks @gary149!
## 0.5.2
### Features
- [#14](https://github.com/gradio-app/daggr/pull/14) [`3fa412d`](https://github.com/gradio-app/daggr/commit/3fa412d678988608d49d46d99d193a05469892d2) - Fixes. Thanks @abidlabs!
## 0.5.1
### Features
- [#11](https://github.com/gradio-app/daggr/pull/11) [`ce1d5f4`](https://github.com/gradio-app/daggr/commit/ce1d5f4deaac60d95d9a021b0aa057bc2941b018) - Fixes. Thanks @abidlabs!
- [#13](https://github.com/gradio-app/daggr/pull/13) [`3246921`](https://github.com/gradio-app/daggr/commit/32469213dad5fd29a7ac85938dffbd976e2c6643) - fixes. Thanks @abidlabs!
## 0.5.0
### Features
- [#8](https://github.com/gradio-app/daggr/pull/8) [`e480065`](https://github.com/gradio-app/daggr/commit/e480065dd058dbf19053a80956dbfc90cf3e3caf) - Improving security around executor and various bug fixes. Thanks @abidlabs!
## 0.4.0
### Features
- [#1](https://github.com/gradio-app/daggr/pull/1) [`23538c8`](https://github.com/gradio-app/daggr/commit/23538c884fb3f2d84bbe4bf14f475dc85fa17c79) - Refactor files, add Dialogue component, and implement fully working podcast example. Thanks @abidlabs!
## 0.1.0
Initial release
================================================
FILE: daggr/__init__.py
================================================
"""daggr - Build visual, node-based AI pipelines with Gradio Spaces.
daggr lets you create DAG (directed acyclic graph) pipelines that connect
Gradio Spaces, Hugging Face models, and Python functions into interactive
applications.
Example:
>>> from daggr import Graph, GradioNode, FnNode
>>> import gradio as gr
>>>
>>> tts = GradioNode(
... "mrfakename/MeloTTS",
... inputs={"text": gr.Textbox()},
... outputs={"audio": gr.Audio()},
... )
>>> graph = Graph("TTS Demo", nodes=[tts])
>>> graph.launch()
"""
import json
from pathlib import Path
__version__ = json.loads((Path(__file__).parent / "package.json").read_text())[
"version"
]
from daggr.edge import Edge
from daggr.graph import Graph
from daggr.node import (
ChoiceNode,
FnNode,
GradioNode,
InferenceNode,
InputNode,
InteractionNode,
Node,
)
from daggr.port import ItemList, Port
from daggr.server import DaggrServer
__all__ = [
"__version__",
"ChoiceNode",
"Edge",
"Graph",
"Node",
"FnNode",
"GradioNode",
"InferenceNode",
"InteractionNode",
"InputNode",
"ItemList",
"Port",
"DaggrServer",
]
================================================
FILE: daggr/_client_cache.py
================================================
from __future__ import annotations
import hashlib
import json
import os
from pathlib import Path
from typing import Any
from daggr.state import get_daggr_cache_dir
_client_cache: dict[str, Any] = {}
_api_memory_cache: dict[str, dict] = {}
_validated_set: set[str] = set()
_model_task_cache: dict[str, str] = {}
_dependency_hash_cache: dict[str, str] = {}
_dependency_hash_loaded: bool = False
def _is_hot_reload() -> bool:
return os.environ.get("DAGGR_HOT_RELOAD") == "1"
def _get_cache_path(src: str) -> Path:
src_hash = hashlib.md5(src.encode()).hexdigest()[:16]
return get_daggr_cache_dir() / f"{src_hash}.json"
def _get_validated_file() -> Path:
return get_daggr_cache_dir() / "_validated.json"
def _load_validated_set() -> None:
global _validated_set
if _validated_set:
return
if not _is_hot_reload():
return
validated_file = _get_validated_file()
if validated_file.exists():
try:
_validated_set = set(json.loads(validated_file.read_text()))
except (json.JSONDecodeError, OSError):
_validated_set = set()
def _save_validated_set() -> None:
if not _is_hot_reload():
return
try:
get_daggr_cache_dir().mkdir(parents=True, exist_ok=True)
_get_validated_file().write_text(json.dumps(list(_validated_set)))
except OSError:
pass
def is_validated(cache_key: tuple) -> bool:
if not _is_hot_reload():
return False
_load_validated_set()
return str(cache_key) in _validated_set
def mark_validated(cache_key: tuple) -> None:
if not _is_hot_reload():
return
_load_validated_set()
_validated_set.add(str(cache_key))
_save_validated_set()
def get_api_info(src: str) -> dict | None:
if src in _api_memory_cache:
return _api_memory_cache[src]
if not _is_hot_reload():
return None
cache_path = _get_cache_path(src)
if cache_path.exists():
try:
data = json.loads(cache_path.read_text())
_api_memory_cache[src] = data
return data
except (json.JSONDecodeError, OSError):
pass
return None
def set_api_info(src: str, info: dict) -> None:
_api_memory_cache[src] = info
if not _is_hot_reload():
return
try:
get_daggr_cache_dir().mkdir(parents=True, exist_ok=True)
cache_path = _get_cache_path(src)
cache_path.write_text(json.dumps(info))
except OSError:
pass
def get_client(src: str):
return _client_cache.get(src)
def set_client(src: str, client) -> None:
_client_cache[src] = client
def _get_model_task_cache_path() -> Path:
return get_daggr_cache_dir() / "_model_tasks.json"
def _load_model_task_cache() -> None:
global _model_task_cache
if _model_task_cache:
return
if not _is_hot_reload():
return
cache_path = _get_model_task_cache_path()
if cache_path.exists():
try:
_model_task_cache = json.loads(cache_path.read_text())
except (json.JSONDecodeError, OSError):
_model_task_cache = {}
def _save_model_task_cache() -> None:
if not _is_hot_reload():
return
try:
get_daggr_cache_dir().mkdir(parents=True, exist_ok=True)
_get_model_task_cache_path().write_text(json.dumps(_model_task_cache))
except OSError:
pass
def get_model_task(model: str) -> tuple[bool, str | None]:
"""Get cached task for a model.
Returns:
(found_in_cache, task) where:
- found_in_cache is True if we have cached info for this model
- task is the pipeline_tag (can be None if model has no task, or "__NOT_FOUND__" if model doesn't exist)
"""
if model in _model_task_cache:
return True, _model_task_cache[model]
if not _is_hot_reload():
return False, None
_load_model_task_cache()
if model in _model_task_cache:
return True, _model_task_cache[model]
return False, None
def set_model_task(model: str, task: str | None) -> None:
_model_task_cache[model] = task
_save_model_task_cache()
def set_model_not_found(model: str) -> None:
_model_task_cache[model] = "__NOT_FOUND__"
_save_model_task_cache()
def _get_dependency_hash_path() -> Path:
return get_daggr_cache_dir() / "_dependency_hashes.json"
def _load_dependency_hash_cache() -> None:
global _dependency_hash_cache, _dependency_hash_loaded
if _dependency_hash_loaded:
return
cache_path = _get_dependency_hash_path()
if cache_path.exists():
try:
_dependency_hash_cache = json.loads(cache_path.read_text())
except (json.JSONDecodeError, OSError):
_dependency_hash_cache = {}
_dependency_hash_loaded = True
def _save_dependency_hash_cache() -> None:
try:
get_daggr_cache_dir().mkdir(parents=True, exist_ok=True)
_get_dependency_hash_path().write_text(json.dumps(_dependency_hash_cache))
except OSError:
pass
def get_dependency_hash(src: str) -> str | None:
_load_dependency_hash_cache()
return _dependency_hash_cache.get(src)
def set_dependency_hash(src: str, sha: str) -> None:
_load_dependency_hash_cache()
_dependency_hash_cache[src] = sha
_save_dependency_hash_cache()
================================================
FILE: daggr/_utils.py
================================================
"""Internal utilities for daggr."""
from __future__ import annotations
import difflib
def suggest_similar(invalid: str, valid_options: set[str]) -> str | None:
"""Find a similar string from valid_options using fuzzy matching.
Args:
invalid: The invalid string to find matches for.
valid_options: Set of valid options to search through.
Returns:
The closest matching string if found with >= 60% similarity, else None.
"""
matches = difflib.get_close_matches(invalid, valid_options, n=1, cutoff=0.6)
return matches[0] if matches else None
================================================
FILE: daggr/cli.py
================================================
from __future__ import annotations
import argparse
import ast
import importlib.util
import os
import re
import shutil
import socket
import sqlite3
import sys
import tempfile
import threading
import time
import webbrowser
from pathlib import Path
INITIAL_PORT_VALUE = int(os.getenv("DAGGR_SERVER_PORT", "7860"))
TRY_NUM_PORTS = int(os.getenv("DAGGR_NUM_PORTS", "100"))
def _find_available_port(host: str, start_port: int) -> int:
"""Find an available port starting from start_port."""
for port in range(start_port, start_port + TRY_NUM_PORTS):
try:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind((host if host != "0.0.0.0" else "127.0.0.1", port))
s.close()
return port
except OSError:
continue
raise OSError(
f"Cannot find empty port in range: {start_port}-{start_port + TRY_NUM_PORTS - 1}. "
f"You can specify a different port by setting the DAGGR_SERVER_PORT environment variable "
f"or passing the --port parameter."
)
def find_python_imports(file_path: Path) -> list[Path]:
"""Find local Python files imported by the given file."""
imports = []
try:
with open(file_path) as f:
content = f.read()
tree = ast.parse(content)
file_dir = file_path.parent
for node in ast.walk(tree):
if isinstance(node, ast.Import):
for alias in node.names:
module_path = file_dir / f"{alias.name.replace('.', '/')}.py"
if module_path.exists():
imports.append(module_path)
elif isinstance(node, ast.ImportFrom):
if node.module:
module_path = file_dir / f"{node.module.replace('.', '/')}.py"
if module_path.exists():
imports.append(module_path)
package_init = (
file_dir / node.module.replace(".", "/") / "__init__.py"
)
if package_init.exists():
imports.append(package_init.parent)
except Exception:
pass
return imports
def main():
if len(sys.argv) > 1 and sys.argv[1] == "deploy":
_deploy_main()
return
parser = argparse.ArgumentParser(
prog="daggr",
description="Run a daggr app with hot reload",
)
parser.add_argument(
"script",
help="Path to the Python script containing the daggr Graph",
)
parser.add_argument(
"--host",
default="127.0.0.1",
help="Host to bind to (default: 127.0.0.1)",
)
parser.add_argument(
"--port",
type=int,
default=7860,
help="Port to bind to (default: 7860)",
)
parser.add_argument(
"--no-reload",
action="store_true",
help="Disable auto-reload",
)
parser.add_argument(
"--watch-daggr",
action="store_true",
default=True,
help="Watch daggr source for changes (default: True, useful for development)",
)
parser.add_argument(
"--no-watch-daggr",
action="store_true",
help="Don't watch daggr source for changes",
)
parser.add_argument(
"--delete-sheets",
action="store_true",
help="Delete all cached data (sheets, results, downloaded files) for this project and exit",
)
parser.add_argument(
"--force",
"-f",
action="store_true",
help="Skip confirmation prompts (use with --delete-sheets)",
)
parser.add_argument(
"--state-db-path",
help="Optional path to SQLite state database. Overrides DAGGR_DB_PATH env var. Defaults to HuggingFace cache.",
)
args = parser.parse_args()
script_path = Path(args.script).resolve()
if not script_path.exists():
print(f"Error: Script not found: {script_path}")
sys.exit(1)
if not script_path.suffix == ".py":
print(f"Error: Script must be a Python file: {script_path}")
sys.exit(1)
if args.delete_sheets:
_delete_sheets(script_path, force=args.force)
sys.exit(0)
watch_daggr = args.watch_daggr and not args.no_watch_daggr
os.environ["DAGGR_SCRIPT_PATH"] = str(script_path)
os.environ["DAGGR_HOST"] = args.host
os.environ["DAGGR_PORT"] = str(args.port)
if args.state_db_path:
os.environ["DAGGR_DB_PATH"] = str(Path(args.state_db_path).resolve())
if args.no_reload:
_run_script(script_path, args.host, args.port)
else:
os.environ["DAGGR_HOT_RELOAD"] = "1"
_run_with_reload(script_path, args.host, args.port, watch_daggr)
def _deploy_main():
"""Entry point for the deploy subcommand."""
parser = argparse.ArgumentParser(
prog="daggr deploy",
description="Deploy a daggr app to Hugging Face Spaces",
)
parser.add_argument(
"script",
help="Path to the Python script containing the daggr Graph",
)
parser.add_argument(
"--name",
"-n",
help="Space name (default: derived from Graph name)",
)
parser.add_argument(
"--title",
"-t",
help="Display title for the Space (default: Graph name)",
)
parser.add_argument(
"--org",
"-o",
help="Organization or username to deploy under (default: your HF account)",
)
parser.add_argument(
"--private",
"-p",
action="store_true",
help="Make the Space private",
)
parser.add_argument(
"--hardware",
default="cpu-basic",
help="Hardware tier (default: cpu-basic). Options: cpu-basic, cpu-upgrade, t4-small, t4-medium, a10g-small, etc.",
)
parser.add_argument(
"--secret",
"-s",
action="append",
dest="secrets",
metavar="KEY=VALUE",
help="Add a secret (can be repeated). Example: --secret HF_TOKEN=xxx",
)
parser.add_argument(
"--requirements",
"-r",
help="Path to requirements.txt (default: auto-detect or generate)",
)
parser.add_argument(
"--dry-run",
action="store_true",
help="Preview what would be deployed without actually deploying",
)
args = parser.parse_args(sys.argv[2:])
script_path = Path(args.script).resolve()
if not script_path.exists():
print(f"Error: Script not found: {script_path}")
sys.exit(1)
if not script_path.suffix == ".py":
print(f"Error: Script must be a Python file: {script_path}")
sys.exit(1)
secrets = {}
if args.secrets:
for secret in args.secrets:
if "=" not in secret:
print(f"Error: Invalid secret format '{secret}'. Use KEY=VALUE")
sys.exit(1)
key, value = secret.split("=", 1)
secrets[key] = value
_deploy(
script_path=script_path,
name=args.name,
title=args.title,
org=args.org,
private=args.private,
hardware=args.hardware,
secrets=secrets,
requirements_path=args.requirements,
dry_run=args.dry_run,
)
def _extract_graph(script_path: Path):
"""Extract the Graph object from a script without running it."""
from daggr.graph import Graph
sys.path.insert(0, str(script_path.parent))
original_launch = Graph.launch
captured_graph = None
def capture_launch(self, **kwargs):
nonlocal captured_graph
captured_graph = self
Graph.launch = capture_launch
try:
spec = importlib.util.spec_from_file_location("__daggr_deploy__", script_path)
if spec is None or spec.loader is None:
print(f"Error: Could not load script: {script_path}")
sys.exit(1)
module = importlib.util.module_from_spec(spec)
sys.modules["__daggr_deploy__"] = module
spec.loader.exec_module(module)
finally:
Graph.launch = original_launch
if captured_graph is None:
for name in dir(module):
obj = getattr(module, name)
if isinstance(obj, Graph):
captured_graph = obj
break
if captured_graph is None:
print(f"Error: No Graph found in {script_path}")
sys.exit(1)
return captured_graph
def _sanitize_space_name(name: str) -> str:
"""Convert a Graph name to a valid HF Space name."""
sanitized = re.sub(r"[^a-zA-Z0-9\s-]", "", name)
sanitized = re.sub(r"[\s_]+", "-", sanitized)
sanitized = sanitized.lower().strip("-")
return sanitized or "daggr-app"
def _deploy(
script_path: Path,
name: str | None,
title: str | None,
org: str | None,
private: bool,
hardware: str,
secrets: dict[str, str],
requirements_path: str | None,
dry_run: bool,
):
"""Deploy a daggr app to Hugging Face Spaces."""
import huggingface_hub
from huggingface_hub import HfApi
import daggr
print("\n Extracting Graph from script...")
graph = _extract_graph(script_path)
space_name = name or _sanitize_space_name(graph.name)
space_title = title or graph.name
print(f" Graph name: {graph.name}")
print(f" Space name: {space_name}")
print(f" Space title: {space_title}")
hf_api = HfApi()
whoami = None
login_needed = False
try:
whoami = hf_api.whoami()
if whoami["auth"]["accessToken"]["role"] != "write":
login_needed = True
except Exception:
login_needed = True
if login_needed:
print("\n Need 'write' access token to create a Spaces repo.")
huggingface_hub.login(add_to_git_credential=False)
whoami = hf_api.whoami()
username = whoami["name"]
namespace = org or username
repo_id = f"{namespace}/{space_name}"
print(f"\n Target: https://huggingface.co/spaces/{repo_id}")
print(f" Hardware: {hardware}")
print(f" Private: {private}")
if secrets:
print(f" Secrets: {list(secrets.keys())}")
local_imports = find_python_imports(script_path)
print("\n Files to upload:")
print(f" • app.py (from {script_path.name})")
print(" • requirements.txt")
print(" • README.md")
for imp in local_imports:
if imp.is_file():
print(f" • {imp.name}")
else:
print(f" • {imp.name}/ (package)")
if dry_run:
print("\n [Dry run] No changes made.")
return
with tempfile.TemporaryDirectory() as tmpdir:
tmpdir = Path(tmpdir)
shutil.copy(script_path, tmpdir / "app.py")
for imp in local_imports:
if imp.is_file():
shutil.copy(imp, tmpdir / imp.name)
else:
shutil.copytree(imp, tmpdir / imp.name)
if requirements_path:
req_path = Path(requirements_path)
if not req_path.exists():
print(f"Error: Requirements file not found: {req_path}")
sys.exit(1)
shutil.copy(req_path, tmpdir / "requirements.txt")
with open(tmpdir / "requirements.txt", "r") as f:
req_content = f.read()
if "daggr" not in req_content:
with open(tmpdir / "requirements.txt", "a") as f:
f.write(f"\ndaggr>={daggr.__version__}\n")
else:
script_dir = script_path.parent
existing_req = script_dir / "requirements.txt"
if existing_req.exists():
shutil.copy(existing_req, tmpdir / "requirements.txt")
with open(tmpdir / "requirements.txt", "r") as f:
req_content = f.read()
if "daggr" not in req_content:
with open(tmpdir / "requirements.txt", "a") as f:
f.write(f"\ndaggr>={daggr.__version__}\n")
else:
with open(tmpdir / "requirements.txt", "w") as f:
f.write(f"daggr>={daggr.__version__}\n")
readme_content = f"""---
title: {space_title}
emoji: 🔀
colorFrom: blue
colorTo: purple
sdk: gradio
sdk_version: "{_get_gradio_version()}"
app_file: app.py
pinned: false
tags:
- daggr
---
# {space_title}
This Space was deployed using [daggr](https://github.com/gradio-app/daggr).
"""
with open(tmpdir / "README.md", "w") as f:
f.write(readme_content)
print("\n Creating Space repository...")
try:
hf_api.create_repo(
repo_id=repo_id,
repo_type="space",
space_sdk="gradio",
space_hardware=hardware,
private=private,
exist_ok=True,
)
except Exception as e:
print(f"Error creating repository: {e}")
sys.exit(1)
print(" Uploading files...")
try:
hf_api.upload_folder(
repo_id=repo_id,
repo_type="space",
folder_path=str(tmpdir),
)
except Exception as e:
print(f"Error uploading files: {e}")
sys.exit(1)
if secrets:
print(" Adding secrets...")
for secret_name, secret_value in secrets.items():
try:
hf_api.add_space_secret(repo_id, secret_name, secret_value)
except Exception as e:
print(f" Warning: Could not add secret '{secret_name}': {e}")
print(f"\n ✓ Deployed to https://huggingface.co/spaces/{repo_id}")
print(" The Space may take a few minutes to build and start.\n")
def _get_gradio_version() -> str:
"""Get the installed Gradio version."""
try:
import gradio
return gradio.__version__
except ImportError:
return "5.0.0"
def _delete_sheets(script_path: Path, force: bool = False):
"""Delete all cached data for the project defined in the script."""
from daggr.graph import Graph
from daggr.state import get_daggr_cache_dir
sys.path.insert(0, str(script_path.parent))
original_launch = Graph.launch
captured_graph = None
def capture_launch(self, **kwargs):
nonlocal captured_graph
captured_graph = self
Graph.launch = capture_launch
try:
spec = importlib.util.spec_from_file_location("__daggr_reset__", script_path)
if spec is None or spec.loader is None:
print(f"Error: Could not load script: {script_path}")
sys.exit(1)
module = importlib.util.module_from_spec(spec)
sys.modules["__daggr_reset__"] = module
spec.loader.exec_module(module)
finally:
Graph.launch = original_launch
if captured_graph is None:
for name in dir(module):
obj = getattr(module, name)
if isinstance(obj, Graph):
captured_graph = obj
break
if captured_graph is None:
print(f"Error: No Graph found in {script_path}")
sys.exit(1)
persist_key = captured_graph.persist_key
if not persist_key:
print("Error: Graph has no persist_key (persistence is disabled)")
sys.exit(1)
cache_dir = get_daggr_cache_dir()
db_path = cache_dir / "sessions.db"
if not db_path.exists():
print(f"No cache found for project '{persist_key}'")
return
conn = sqlite3.connect(str(db_path))
cursor = conn.cursor()
cursor.execute(
"SELECT sheet_id FROM sheets WHERE graph_name = ?",
(persist_key,),
)
sheet_ids = [row[0] for row in cursor.fetchall()]
if not sheet_ids:
print(f"No cached data found for project '{persist_key}'")
conn.close()
return
print(f"\nProject: {persist_key}")
print(f"This will delete {len(sheet_ids)} sheet(s) and all associated data.")
print(f"Cache location: {cache_dir}\n")
if not force:
try:
response = (
input("Are you sure you want to continue? [y/N] ").strip().lower()
)
except (EOFError, KeyboardInterrupt):
print("\nAborted.")
conn.close()
return
if response not in ("y", "yes"):
print("Aborted.")
conn.close()
return
for sheet_id in sheet_ids:
cursor.execute("DELETE FROM node_inputs WHERE sheet_id = ?", (sheet_id,))
cursor.execute("DELETE FROM node_results WHERE sheet_id = ?", (sheet_id,))
cursor.execute("DELETE FROM sheets WHERE sheet_id = ?", (sheet_id,))
conn.commit()
conn.close()
print(f"\n✓ Deleted {len(sheet_ids)} sheet(s) for project '{persist_key}'")
def _run_script(script_path: Path, host: str, port: int):
"""Run the script directly without reload."""
spec = importlib.util.spec_from_file_location("__daggr_main__", script_path)
if spec is None or spec.loader is None:
print(f"Error: Could not load script: {script_path}")
sys.exit(1)
sys.path.insert(0, str(script_path.parent))
module = importlib.util.module_from_spec(spec)
sys.modules["__daggr_main__"] = module
spec.loader.exec_module(module)
def _run_with_reload(script_path: Path, host: str, port: int, watch_daggr: bool):
"""Run the script with uvicorn hot reload."""
import uvicorn
actual_port = _find_available_port(host, port)
if actual_port != port:
print(f"\n Port {port} is in use, using {actual_port} instead.")
reload_dirs = [str(script_path.parent)]
local_imports = find_python_imports(script_path)
for imp in local_imports:
imp_dir = str(imp if imp.is_dir() else imp.parent)
if imp_dir not in reload_dirs:
reload_dirs.append(imp_dir)
if watch_daggr:
daggr_dir = Path(__file__).parent
daggr_src = str(daggr_dir)
if daggr_src not in reload_dirs:
reload_dirs.append(daggr_src)
reload_includes = ["*.py"]
print("\n daggr dev server starting...")
print(" Watching for changes in:")
for d in reload_dirs:
print(f" • {d}")
print()
os.environ["DAGGR_PORT"] = str(actual_port)
def open_browser():
time.sleep(1.0)
webbrowser.open_new_tab(f"http://{host}:{actual_port}")
threading.Thread(target=open_browser, daemon=True).start()
uvicorn.run(
"daggr.cli:_create_app",
factory=True,
host=host,
port=actual_port,
reload=True,
reload_dirs=reload_dirs,
reload_includes=reload_includes,
log_level="warning",
)
def _create_app():
"""Factory function for uvicorn to create the FastAPI app."""
from daggr.graph import Graph
from daggr.server import DaggrServer
script_path = Path(os.environ["DAGGR_SCRIPT_PATH"])
if str(script_path.parent) not in sys.path:
sys.path.insert(0, str(script_path.parent))
modules_to_remove = [m for m in sys.modules if m.startswith("__daggr_user_script_")]
for m in modules_to_remove:
del sys.modules[m]
module_name = f"__daggr_user_script_{id(script_path)}__"
spec = importlib.util.spec_from_file_location(module_name, script_path)
if spec is None or spec.loader is None:
raise RuntimeError(f"Could not load script: {script_path}")
original_launch = Graph.launch
captured_graph = None
launch_kwargs = {}
def capture_launch(self, **kwargs):
nonlocal captured_graph, launch_kwargs
captured_graph = self
launch_kwargs = kwargs
Graph.launch = capture_launch
try:
module = importlib.util.module_from_spec(spec)
sys.modules[module_name] = module
spec.loader.exec_module(module)
finally:
Graph.launch = original_launch
if captured_graph is None:
for name in dir(module):
obj = getattr(module, name)
if isinstance(obj, Graph):
captured_graph = obj
break
if captured_graph is None:
raise RuntimeError(
f"No Graph found in {script_path}. "
"Make sure your script defines a Graph and calls graph.launch() "
"or has a Graph instance at module level."
)
captured_graph._validate_edges()
server = DaggrServer(captured_graph)
base_url = f"http://{os.environ['DAGGR_HOST']}:{os.environ['DAGGR_PORT']}"
print(f"\n UI running at: {base_url}")
print(f" API server at: {base_url}/api\n")
return server.app
if __name__ == "__main__":
main()
================================================
FILE: daggr/edge.py
================================================
"""Edge module for connecting ports between nodes."""
from __future__ import annotations
from typing import TYPE_CHECKING
from daggr.port import GatheredPort, ScatteredPort
if TYPE_CHECKING:
from daggr.port import PortLike
class Edge:
"""Represents a connection between two ports in a graph.
Edges connect an output port of one node to an input port of another,
defining how data flows through the graph.
Attributes:
source_node: The node providing the output.
source_port: Name of the output port.
target_node: The node receiving the input.
target_port: Name of the input port.
is_scattered: True if this edge scatters a list to multiple executions.
is_gathered: True if this edge gathers results back into a list.
item_key: For scattered edges, the key to extract from each item.
"""
def __init__(self, source: PortLike, target: PortLike):
self.is_scattered = isinstance(source, ScatteredPort)
self.is_gathered = isinstance(source, GatheredPort)
self.item_key: str | None = None
if self.is_scattered:
self.item_key = source.item_key
self.source_node = source.node
self.source_port = source.name
self.target_node = target.node
self.target_port = target.name
def __repr__(self):
prefix = ""
if self.is_scattered:
key_info = f"['{self.item_key}']" if self.item_key else ""
prefix = f"scatter{key_info}:"
elif self.is_gathered:
prefix = "gather:"
return (
f"Edge({prefix}{self.source_node._name}.{self.source_port} -> "
f"{self.target_node._name}.{self.target_port})"
)
def as_tuple(self) -> tuple[str, str, str, str]:
return (
self.source_node._name,
self.source_port,
self.target_node._name,
self.target_port,
)
================================================
FILE: daggr/executor.py
================================================
"""Executor for daggr graphs.
This module provides the AsyncExecutor for running graph nodes with proper
concurrency control and session isolation.
"""
from __future__ import annotations
import asyncio
import base64
import hashlib
import uuid
from pathlib import Path
from typing import TYPE_CHECKING, Any
from urllib.parse import urlparse
from gradio_client.utils import is_file_obj_with_meta, traverse
from daggr.node import (
ChoiceNode,
FnNode,
GradioNode,
InferenceNode,
InputNode,
InteractionNode,
)
from daggr.session import ExecutionSession
from daggr.state import get_daggr_files_dir
if TYPE_CHECKING:
from daggr.graph import Graph
class FileValue(str):
"""A string subclass that marks a value as a file URL/path from Gradio output."""
pass
def _download_file(url: str, hf_token: str | None = None) -> str:
import httpx
parsed = urlparse(url)
ext = Path(parsed.path).suffix or ".bin"
url_hash = hashlib.md5(url.encode()).hexdigest()[:16]
filename = f"{url_hash}{ext}"
files_dir = get_daggr_files_dir()
local_path = files_dir / filename
if not local_path.exists():
headers = {}
if hf_token:
headers["Authorization"] = f"Bearer {hf_token}"
with httpx.Client(follow_redirects=True) as client:
response = client.get(url, headers=headers)
response.raise_for_status()
local_path.write_bytes(response.content)
return str(local_path)
def _postprocess_inference_result(task: str | None, result: Any) -> Any:
"""Unwrap HF Inference Client result objects to get the actual data."""
if result is None:
return None
if task == "automatic-speech-recognition":
return getattr(result, "text", result)
elif task == "translation":
return getattr(result, "translation_text", result)
elif task == "summarization":
return getattr(result, "summary_text", result)
elif task in (
"audio-classification",
"image-classification",
"text-classification",
):
if isinstance(result, list) and result:
return {item.label: item.score for item in result if hasattr(item, "label")}
return result
elif task == "image-to-text":
return getattr(result, "generated_text", result)
elif task == "question-answering":
if hasattr(result, "answer"):
return result.answer
return result
elif task in ("text-to-speech", "text-to-audio"):
if isinstance(result, bytes):
file_path = get_daggr_files_dir() / f"{uuid.uuid4()}.wav"
file_path.write_bytes(result)
return str(file_path)
return result
elif task in ("text-to-image", "image-to-image"):
if isinstance(result, dict):
if "images" in result:
result = result["images"][0] if result["images"] else result
elif "image" in result:
result = result["image"]
if hasattr(result, "save"):
file_path = get_daggr_files_dir() / f"{uuid.uuid4()}.png"
result.save(file_path)
return str(file_path)
return result
return result
def _call_inference_task(client: Any, task: str | None, inputs: dict[str, Any]) -> Any:
primary_input = None
if task in (
"image-to-image",
"image-classification",
"image-to-text",
"object-detection",
"image-segmentation",
"visual-question-answering",
"document-question-answering",
):
primary_input = inputs.get("image")
elif task in (
"automatic-speech-recognition",
"audio-classification",
"audio-to-audio",
):
primary_input = inputs.get("audio")
if primary_input is None:
primary_input = next(iter(inputs.values()), None) if inputs else None
if primary_input is None:
return None
task_method_map = {
"text-generation": "text_generation",
"text2text-generation": "text_generation",
"text-to-image": "text_to_image",
"image-to-image": "image_to_image",
"image-to-text": "image_to_text",
"image-to-video": "image_to_video",
"text-to-video": "text_to_video",
"text-to-speech": "text_to_speech",
"text-to-audio": "text_to_audio",
"automatic-speech-recognition": "automatic_speech_recognition",
"audio-to-audio": "audio_to_audio",
"audio-classification": "audio_classification",
"image-classification": "image_classification",
"object-detection": "object_detection",
"image-segmentation": "image_segmentation",
"translation": "translation",
"summarization": "summarization",
"feature-extraction": "feature_extraction",
"fill-mask": "fill_mask",
"question-answering": "question_answering",
"table-question-answering": "table_question_answering",
"sentence-similarity": "sentence_similarity",
"zero-shot-classification": "zero_shot_classification",
"zero-shot-image-classification": "zero_shot_image_classification",
"document-question-answering": "document_question_answering",
"visual-question-answering": "visual_question_answering",
}
method_name = (
task_method_map.get(task, "text_generation") if task else "text_generation"
)
method = getattr(client, method_name, None)
file_input_tasks = {
"image-to-image",
"image-classification",
"image-to-text",
"object-detection",
"image-segmentation",
"visual-question-answering",
"document-question-answering",
"automatic-speech-recognition",
"audio-classification",
"audio-to-audio",
}
if task in file_input_tasks and isinstance(primary_input, str):
primary_input = _read_file_as_bytes(primary_input)
try:
if method is None:
result = client.text_generation(primary_input)
elif task in ("image-to-image",):
prompt = inputs.get("prompt", "")
result = method(primary_input, prompt=prompt)
elif task in ("visual-question-answering", "document-question-answering"):
question = inputs.get("question", inputs.get("prompt", ""))
result = method(primary_input, question=question)
else:
result = method(primary_input)
except KeyError as e:
raise RuntimeError(
f"Provider returned unexpected response format for task '{task}'. "
f"Missing key: {e}. This model may require a specific provider "
f"(e.g., 'model_name:fal-ai' or 'model_name:replicate')."
) from e
return _postprocess_inference_result(task, result)
def _read_file_as_bytes(file_path: str) -> bytes:
"""Read a file path or data URL as bytes."""
if file_path.startswith("data:"):
try:
_, encoded = file_path.split(",", 1)
return base64.b64decode(encoded)
except Exception:
pass
path = Path(file_path)
if path.exists():
return path.read_bytes()
return file_path
class AsyncExecutor:
"""Async executor for graph nodes.
This executor is stateless - all state is held in the ExecutionSession.
It handles concurrency control:
- GradioNode/InferenceNode: run concurrently (external API calls)
- FnNode: sequential by default, configurable via concurrent/concurrency_group
"""
def __init__(self, graph: Graph):
self.graph = graph
def _get_client_for_gradio_node(
self, session: ExecutionSession, gradio_node, cache_key: str
):
from daggr import _client_cache
token_cache_key = f"{cache_key}__token_{hash(session.hf_token or '')}"
if token_cache_key in session.clients:
return session.clients[token_cache_key]
if gradio_node._run_locally:
from daggr.local_space import get_local_client
client = get_local_client(gradio_node)
if client is not None:
session.clients[token_cache_key] = client
return client
if session.hf_token:
from gradio_client import Client
client = Client(
gradio_node._src,
download_files=False,
verbose=False,
token=session.hf_token,
)
else:
client = _client_cache.get_client(gradio_node._src)
if client is None:
from gradio_client import Client
client = Client(
gradio_node._src,
download_files=False,
verbose=False,
)
_client_cache.set_client(gradio_node._src, client)
session.clients[token_cache_key] = client
return client
def _get_client(self, session: ExecutionSession, node_name: str):
node = self.graph.nodes[node_name]
if isinstance(node, ChoiceNode):
variant_idx = session.selected_variants.get(node_name, 0)
variant = node._variants[variant_idx]
if isinstance(variant, GradioNode):
cache_key = f"{node_name}__variant_{variant_idx}"
return self._get_client_for_gradio_node(session, variant, cache_key)
return None
if not isinstance(node, GradioNode):
return None
return self._get_client_for_gradio_node(session, node, node_name)
def _get_scattered_input_edges(self, node_name: str) -> list:
scattered = []
for edge in self.graph._edges:
if edge.target_node._name == node_name and edge.is_scattered:
scattered.append(edge)
return scattered
def _get_gathered_input_edges(self, node_name: str) -> list:
gathered = []
for edge in self.graph._edges:
if edge.target_node._name == node_name and edge.is_gathered:
gathered.append(edge)
return gathered
def _prepare_inputs(
self, session: ExecutionSession, node_name: str, skip_scattered: bool = False
) -> dict[str, Any]:
inputs = {}
for edge in self.graph._edges:
if edge.target_node._name == node_name:
if skip_scattered and edge.is_scattered:
continue
source_name = edge.source_node._name
source_output = edge.source_port
target_input = edge.target_port
if source_name in session.results:
source_result = session.results[source_name]
if (
edge.is_gathered
and isinstance(source_result, dict)
and "_scattered_results" in source_result
):
scattered_results = source_result["_scattered_results"]
extracted = []
for item_result in scattered_results:
if (
isinstance(item_result, dict)
and source_output in item_result
):
extracted.append(item_result[source_output])
else:
extracted.append(item_result)
inputs[target_input] = extracted
elif (
isinstance(source_result, dict)
and source_output in source_result
):
inputs[target_input] = source_result[source_output]
elif isinstance(source_result, (list, tuple)):
try:
output_idx = int(
source_output.replace("output_", "").replace(
"output", "0"
)
)
if 0 <= output_idx < len(source_result):
inputs[target_input] = source_result[output_idx]
except (ValueError, TypeError):
if len(source_result) > 0:
inputs[target_input] = source_result[0]
else:
inputs[target_input] = source_result
return inputs
def _execute_single_node_sync(
self, session: ExecutionSession, node_name: str, inputs: dict[str, Any]
) -> Any:
"""Synchronous node execution (called from thread pool for FnNode)."""
node = self.graph.nodes[node_name]
if isinstance(node, ChoiceNode):
variant_idx = session.selected_variants.get(node_name, 0)
variant = node._variants[variant_idx]
return self._execute_variant_node_sync(session, node_name, variant, inputs)
all_inputs = {}
for port_name, value in node._fixed_inputs.items():
all_inputs[port_name] = value() if callable(value) else value
for port_name, component in node._input_components.items():
if hasattr(component, "value"):
val = component.value
if is_file_obj_with_meta(val):
val = val["path"]
all_inputs[port_name] = val
all_inputs.update(inputs)
if isinstance(node, GradioNode):
client = self._get_client(session, node_name)
if client:
api_name = node._api_name or "/predict"
if not api_name.startswith("/"):
api_name = "/" + api_name
call_inputs = {
k: self._wrap_file_input(v)
for k, v in all_inputs.items()
if k in node._input_ports
}
if node._preprocess:
call_inputs = node._preprocess(call_inputs)
raw_result = client.predict(api_name=api_name, **call_inputs)
if node._postprocess:
raw_result = self._apply_postprocess(node._postprocess, raw_result)
result = self._map_gradio_result(
node, raw_result, hf_token=session.hf_token
)
else:
result = None
elif isinstance(node, FnNode):
fn_kwargs = {}
for port_name in node._input_ports:
if port_name in all_inputs:
fn_kwargs[port_name] = all_inputs[port_name]
if node._preprocess:
fn_kwargs = node._preprocess(fn_kwargs)
raw_result = node._fn(**fn_kwargs)
if node._postprocess:
raw_result = self._apply_postprocess(node._postprocess, raw_result)
result = self._map_fn_result(node, raw_result)
elif isinstance(node, InferenceNode):
from huggingface_hub import InferenceClient
if not node._task_fetched:
node._fetch_model_info()
client = InferenceClient(
model=node._model_name_for_hub,
provider=node._provider,
token=session.hf_token,
)
inference_inputs = {
k: v for k, v in all_inputs.items() if k in node._input_ports
}
if node._preprocess:
inference_inputs = node._preprocess(inference_inputs)
raw_result = _call_inference_task(client, node._task, inference_inputs)
if node._postprocess:
raw_result = self._apply_postprocess(node._postprocess, raw_result)
result = self._map_inference_result(node, raw_result)
elif isinstance(node, InteractionNode):
result = all_inputs.get(
"input",
all_inputs.get(node._input_ports[0]) if node._input_ports else None,
)
elif isinstance(node, InputNode):
result = {}
for port in node._output_ports:
result[port] = all_inputs.get(port)
return result
else:
result = None
return result
def _execute_variant_node_sync(
self,
session: ExecutionSession,
node_name: str,
variant,
inputs: dict[str, Any],
) -> Any:
all_inputs = {}
for port_name, value in variant._fixed_inputs.items():
all_inputs[port_name] = value() if callable(value) else value
for port_name, component in variant._input_components.items():
if hasattr(component, "value"):
val = component.value
if is_file_obj_with_meta(val):
val = val["path"]
all_inputs[port_name] = val
all_inputs.update(inputs)
if isinstance(variant, GradioNode):
client = self._get_client(session, node_name)
if client:
api_name = variant._api_name or "/predict"
if not api_name.startswith("/"):
api_name = "/" + api_name
call_inputs = {
k: self._wrap_file_input(v)
for k, v in all_inputs.items()
if k in variant._input_ports
}
if variant._preprocess:
call_inputs = variant._preprocess(call_inputs)
raw_result = client.predict(api_name=api_name, **call_inputs)
if variant._postprocess:
raw_result = self._apply_postprocess(
variant._postprocess, raw_result
)
result = self._map_gradio_result(
variant, raw_result, hf_token=session.hf_token
)
else:
result = None
elif isinstance(variant, FnNode):
fn_kwargs = {}
for port_name in variant._input_ports:
if port_name in all_inputs:
fn_kwargs[port_name] = all_inputs[port_name]
if variant._preprocess:
fn_kwargs = variant._preprocess(fn_kwargs)
raw_result = variant._fn(**fn_kwargs)
if variant._postprocess:
raw_result = self._apply_postprocess(variant._postprocess, raw_result)
result = self._map_fn_result(variant, raw_result)
elif isinstance(variant, InferenceNode):
from huggingface_hub import InferenceClient
if not variant._task_fetched:
variant._fetch_model_info()
client = InferenceClient(
model=variant._model_name_for_hub,
provider=variant._provider,
token=session.hf_token,
)
inference_inputs = {
k: v for k, v in all_inputs.items() if k in variant._input_ports
}
if variant._preprocess:
inference_inputs = variant._preprocess(inference_inputs)
raw_result = _call_inference_task(client, variant._task, inference_inputs)
if variant._postprocess:
raw_result = self._apply_postprocess(variant._postprocess, raw_result)
result = self._map_inference_result(variant, raw_result)
elif isinstance(variant, InputNode):
result = {}
for port in variant._output_ports:
result[port] = all_inputs.get(port)
return result
else:
result = None
return result
async def execute_node(
self,
session: ExecutionSession,
node_name: str,
user_inputs: dict[str, Any] | None = None,
) -> Any:
"""Execute a single node with proper concurrency control."""
node = self.graph.nodes[node_name]
scattered_edges = self._get_scattered_input_edges(node_name)
if scattered_edges:
result = await self._execute_scattered_node(
session, node_name, scattered_edges, user_inputs
)
else:
inputs = self._prepare_inputs(session, node_name)
if user_inputs:
if isinstance(user_inputs, dict):
inputs.update(user_inputs)
else:
if node._input_ports:
inputs[node._input_ports[0]] = user_inputs
else:
inputs["input"] = user_inputs
try:
if isinstance(node, (GradioNode, InferenceNode)):
result = await asyncio.to_thread(
self._execute_single_node_sync, session, node_name, inputs
)
elif isinstance(node, FnNode):
semaphore = await session.concurrency.get_semaphore(
node._concurrent,
node._concurrency_group,
node._max_concurrent,
)
if semaphore:
async with semaphore:
result = await asyncio.to_thread(
self._execute_single_node_sync,
session,
node_name,
inputs,
)
else:
result = await asyncio.to_thread(
self._execute_single_node_sync, session, node_name, inputs
)
else:
result = await asyncio.to_thread(
self._execute_single_node_sync, session, node_name, inputs
)
except Exception as e:
raise RuntimeError(f"Error executing node '{node_name}': {e}")
session.results[node_name] = result
return result
async def _execute_scattered_node(
self,
session: ExecutionSession,
node_name: str,
scattered_edges: list,
user_inputs: dict[str, Any] | None = None,
) -> dict[str, list[Any]]:
first_edge = scattered_edges[0]
source_name = first_edge.source_node._name
source_port = first_edge.source_port
source_result = session.results.get(source_name)
if source_result is None:
items = []
elif isinstance(source_result, dict) and source_port in source_result:
items = source_result[source_port]
else:
items = source_result
if not isinstance(items, list):
items = [items]
context_inputs = self._prepare_inputs(session, node_name, skip_scattered=True)
if user_inputs:
context_inputs.update(user_inputs)
node = self.graph.nodes[node_name]
async def execute_item(item, idx):
item_inputs = dict(context_inputs)
for edge in scattered_edges:
target_port = edge.target_port
item_key = edge.item_key
if item_key and isinstance(item, dict):
item_inputs[target_port] = item.get(item_key)
else:
item_inputs[target_port] = item
try:
if isinstance(node, (GradioNode, InferenceNode)):
return await asyncio.to_thread(
self._execute_single_node_sync, session, node_name, item_inputs
)
elif isinstance(node, FnNode):
semaphore = await session.concurrency.get_semaphore(
node._concurrent,
node._concurrency_group,
node._max_concurrent,
)
if semaphore:
async with semaphore:
return await asyncio.to_thread(
self._execute_single_node_sync,
session,
node_name,
item_inputs,
)
else:
return await asyncio.to_thread(
self._execute_single_node_sync,
session,
node_name,
item_inputs,
)
else:
return await asyncio.to_thread(
self._execute_single_node_sync, session, node_name, item_inputs
)
except Exception as e:
return {"error": str(e)}
if isinstance(node, (GradioNode, InferenceNode)):
tasks = [execute_item(item, i) for i, item in enumerate(items)]
results = await asyncio.gather(*tasks)
else:
results = []
for i, item in enumerate(items):
result = await execute_item(item, i)
results.append(result)
session.scattered_results[node_name] = list(results)
return {"_scattered_results": list(results), "_items": items}
def _wrap_file_input(self, value: Any) -> Any:
from gradio_client import handle_file
if isinstance(value, FileValue):
return handle_file(str(value))
if isinstance(value, str):
if value.startswith("data:"):
file_path = self._save_data_url_to_file(value)
if file_path:
return handle_file(file_path)
elif Path(value).exists():
return handle_file(value)
return value
def _save_data_url_to_file(self, data_url: str) -> str | None:
"""Convert a base64 data URL to a file and return the path."""
if not data_url.startswith("data:"):
return None
try:
header, encoded = data_url.split(",", 1)
media_type = header.split(":")[1].split(";")[0]
ext_map = {
"image/png": ".png",
"image/jpeg": ".jpg",
"image/jpg": ".jpg",
"image/gif": ".gif",
"image/webp": ".webp",
"audio/wav": ".wav",
"audio/mpeg": ".mp3",
"audio/mp3": ".mp3",
"audio/ogg": ".ogg",
"audio/webm": ".webm",
"video/mp4": ".mp4",
"video/webm": ".webm",
}
ext = ext_map.get(media_type, ".bin")
data = base64.b64decode(encoded)
file_path = get_daggr_files_dir() / f"{uuid.uuid4()}{ext}"
file_path.write_bytes(data)
return str(file_path)
except Exception:
return None
def _apply_postprocess(self, postprocess, raw_result: Any) -> Any:
if isinstance(raw_result, (list, tuple)):
return postprocess(*raw_result)
return postprocess(raw_result)
def _extract_file_urls(self, data: Any, hf_token: str | None = None) -> Any:
def download_and_wrap(file_obj: dict) -> FileValue:
url = file_obj.get("url")
if url:
local_path = _download_file(url, hf_token=hf_token)
return FileValue(local_path)
path = file_obj.get("path", "")
return FileValue(path)
return traverse(data, download_and_wrap, is_file_obj_with_meta)
def _map_gradio_result(
self, node, raw_result: Any, hf_token: str | None = None
) -> dict[str, Any]:
if raw_result is None:
return {}
raw_result = self._extract_file_urls(raw_result, hf_token=hf_token)
output_ports = node._output_ports
if not output_ports:
return {"output": raw_result}
if isinstance(raw_result, (list, tuple)):
result = {}
for i, port_name in enumerate(output_ports):
if i < len(raw_result):
result[port_name] = raw_result[i]
else:
result[port_name] = None
return result
elif len(output_ports) == 1:
return {output_ports[0]: raw_result}
else:
return {output_ports[0]: raw_result}
def _map_fn_result(self, node, raw_result: Any) -> dict[str, Any]:
if raw_result is None:
return {}
output_ports = node._output_ports
if not output_ports:
return {"output": raw_result}
if isinstance(raw_result, tuple):
result = {}
for i, port_name in enumerate(output_ports):
if i < len(raw_result):
result[port_name] = raw_result[i]
else:
result[port_name] = None
return result
else:
return {output_ports[0]: raw_result}
def _map_inference_result(self, node, raw_result: Any) -> dict[str, Any]:
"""Map inference API result to output ports."""
if raw_result is None:
return {}
output_ports = node._output_ports
if not output_ports:
return {"output": raw_result}
return {output_ports[0]: raw_result}
async def execute_all(
self, session: ExecutionSession, entry_inputs: dict[str, dict[str, Any]]
) -> dict[str, Any]:
execution_order = self.graph.get_execution_order()
session.results = {}
for node_name in execution_order:
user_input = entry_inputs.get(node_name, {})
await self.execute_node(session, node_name, user_input)
return session.results
class SequentialExecutor:
"""Legacy synchronous executor for backwards compatibility.
This wraps the AsyncExecutor for use in synchronous contexts like node.test().
For production use, prefer AsyncExecutor with proper session management.
"""
def __init__(self, graph: Graph, hf_token: str | None = None):
self.graph = graph
self._async_executor = AsyncExecutor(graph)
self._session = ExecutionSession(graph, hf_token)
@property
def results(self) -> dict[str, Any]:
return self._session.results
@results.setter
def results(self, value: dict[str, Any]):
self._session.results = value
@property
def selected_variants(self) -> dict[str, int]:
return self._session.selected_variants
@selected_variants.setter
def selected_variants(self, value: dict[str, int]):
self._session.selected_variants = value
def set_hf_token(self, token: str | None):
self._session.set_hf_token(token)
def execute_node(
self, node_name: str, user_inputs: dict[str, Any] | None = None
) -> Any:
"""Synchronous wrapper around async execute_node."""
loop = asyncio.new_event_loop()
try:
return loop.run_until_complete(
self._async_executor.execute_node(self._session, node_name, user_inputs)
)
finally:
loop.close()
def execute_all(self, entry_inputs: dict[str, dict[str, Any]]) -> dict[str, Any]:
"""Synchronous wrapper around async execute_all."""
loop = asyncio.new_event_loop()
try:
return loop.run_until_complete(
self._async_executor.execute_all(self._session, entry_inputs)
)
finally:
loop.close()
================================================
FILE: daggr/frontend/index.html
================================================
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>daggr</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/theme.css">
<style>
* { margin: 0; box-sizing: border-box; }
body {
background: var(--body-background-fill, #000);
min-height: 100vh;
font-family: 'Space Grotesk', -apple-system, BlinkMacSystemFont, sans-serif;
overflow: hidden;
color: var(--body-text-color, #fff);
}
</style>
</head>
<body class="dark">
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
================================================
FILE: daggr/frontend/package.json
================================================
{
"name": "daggr-frontend",
"private": true,
"version": "0.0.1",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@babylonjs/viewer": "^8.47.2"
},
"devDependencies": {
"@sveltejs/vite-plugin-svelte": "^5.0.3",
"svelte": "^5.16.0",
"vite": "^6.0.7"
}
}
================================================
FILE: daggr/frontend/src/App.svelte
================================================
<script lang="ts">
import { onMount } from 'svelte';
import { EmbeddedComponent, MapItemsSection, ItemListSection } from './components';
import type { GraphNode, GraphEdge, CanvasData, GradioComponentData } from './types';
interface Sheet {
sheet_id: string;
name: string;
created_at: string;
updated_at: string;
}
let canvasEl: HTMLDivElement;
let transform = $state({ x: 0, y: 0, scale: 1 });
let isPanning = $state(false);
let startPan = $state({ x: 0, y: 0 });
let graphData = $state<CanvasData | null>(null);
let sessionId = $state<string | null>(null);
let ws: WebSocket | null = null;
let wsConnected = $state(false);
let reconnectAttempts = 0;
let maxReconnectAttempts = 10;
let isConnecting = false;
let reconnectTimer: number | null = null;
let inputValues = $state<Record<string, Record<string, any>>>({});
let runningNodes = $state<Set<string>>(new Set());
let nodeResults = $state<Record<string, any[]>>({});
let nodeInputsSnapshots = $state<Record<string, (Record<string, any> | null)[]>>({});
let selectedResultIndex = $state<Record<string, number>>({});
let itemListValues = $state<Record<string, Record<number, Record<string, any>>>>({});
let selectedVariants = $state<Record<string, number>>({});
let nodeExecutionTimes = $state<Record<string, number>>({});
let nodeStartTimes = $state<Record<string, number>>({});
let nodeAvgTimes = $state<Record<string, { total: number; count: number }>>({});
let nodeErrors = $state<Record<string, string>>({});
let timerTick = $state(0);
let hfUser = $state<{ username: string; fullname: string; avatar_url: string } | null>(null);
let nodeRunModes = $state<Record<string, 'step' | 'toHere'>>({});
let runModeMenuOpen = $state<string | null>(null);
let runModeVersion = $state(0);
let highlightedNodes = $state<Set<string>>(new Set());
let nodeRunIds = $state<Record<string, string>>({});
let sheets = $state<Sheet[]>([]);
let currentSheetId = $state<string | null>(null);
let userId = $state<string | null>(null);
let canPersist = $state(false);
let isOnSpaces = $state(false);
let sheetDropdownOpen = $state(false);
let editingSheetName = $state(false);
let editSheetNameValue = $state('');
let saveDebounceTimer: number | null = null;
let transformDebounceTimer: number | null = null;
let showLoginTooltip = $state(false);
let tokenInputValue = $state('');
let loginLoading = $state(false);
let loginError = $state('');
let hasShownPersistencePrompt = $state(false);
const HF_TOKEN_KEY = 'daggr_hf_token';
let isDark = $state(true);
function applyTheme(dark: boolean) {
if (dark) {
document.documentElement.classList.add('dark');
document.body.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
document.body.classList.remove('dark');
}
}
function toggleTheme() {
isDark = !isDark;
localStorage.setItem('theme', isDark ? 'dark' : 'light');
applyTheme(isDark);
}
function getStoredToken(): string | null {
try {
return localStorage.getItem(HF_TOKEN_KEY);
} catch {
return null;
}
}
function storeToken(token: string) {
try {
localStorage.setItem(HF_TOKEN_KEY, token);
} catch {
console.warn('[daggr] Could not store token in localStorage');
}
}
function clearStoredToken() {
try {
localStorage.removeItem(HF_TOKEN_KEY);
} catch {
console.warn('[daggr] Could not clear token from localStorage');
}
}
let timerInterval: number | null = null;
let nodes = $derived(graphData?.nodes || []);
let edges = $derived(graphData?.edges || []);
let currentSheet = $derived(sheets.find(s => s.sheet_id === currentSheetId));
function startTimer() {
if (timerInterval) return;
timerInterval = window.setInterval(() => {
timerTick++;
}, 100);
}
function stopTimerIfNoRunning() {
if (runningNodes.size === 0 && timerInterval) {
clearInterval(timerInterval);
timerInterval = null;
}
}
const NODE_WIDTH = 280;
const HEADER_HEIGHT = 36;
const HEADER_BORDER = 1;
const BODY_PADDING_TOP = 8;
const PORT_ROW_HEIGHT = 22;
const EMBEDDED_COMPONENT_HEIGHT = 60;
function generateSessionId(): string {
return 'session_' + Math.random().toString(36).slice(2) + Date.now().toString(36);
}
async function fetchUserInfo() {
try {
const token = getStoredToken();
const headers: Record<string, string> = {};
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
const response = await fetch('/api/user_info', { headers });
if (response.ok) {
const data = await response.json();
hfUser = data.hf_user;
userId = data.user_id;
canPersist = data.can_persist;
isOnSpaces = data.is_on_spaces;
return data;
}
} catch (e) {
console.log('[daggr] Could not fetch user info');
}
return null;
}
async function handleLogin() {
if (!tokenInputValue.trim()) {
loginError = 'Please enter a token';
return;
}
loginLoading = true;
loginError = '';
try {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token: tokenInputValue.trim() })
});
const data = await response.json();
if (response.ok && data.success) {
storeToken(tokenInputValue.trim());
hfUser = data.hf_user;
showLoginTooltip = false;
tokenInputValue = '';
await fetchUserInfo();
await fetchSheets();
if (sheets.length > 0) {
currentSheetId = sheets[0].sheet_id;
} else if (canPersist) {
await createSheet('Sheet 1');
}
if (ws && wsConnected) {
const token = getStoredToken();
ws.send(JSON.stringify({ action: 'set_sheet', sheet_id: currentSheetId, hf_token: token }));
ws.send(JSON.stringify({ action: 'get_graph', sheet_id: currentSheetId, hf_token: token }));
}
} else {
loginError = data.error || 'Invalid token';
}
} catch (e) {
loginError = 'Failed to verify token';
} finally {
loginLoading = false;
}
}
async function handleLogout() {
clearStoredToken();
hfUser = null;
await fetchUserInfo();
await fetchSheets();
if (sheets.length > 0) {
currentSheetId = sheets[0].sheet_id;
} else {
currentSheetId = null;
}
if (ws && wsConnected) {
ws.send(JSON.stringify({ action: 'set_sheet', sheet_id: currentSheetId, hf_token: null }));
ws.send(JSON.stringify({ action: 'get_graph', sheet_id: currentSheetId, hf_token: null }));
}
}
async function fetchSheets() {
if (!canPersist) return;
try {
const token = getStoredToken();
const headers: Record<string, string> = {};
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
const response = await fetch('/api/sheets', { headers });
if (response.ok) {
const data = await response.json();
sheets = data.sheets || [];
}
} catch (e) {
console.log('[daggr] Could not fetch sheets');
}
}
async function createSheet(name?: string) {
if (!canPersist) return;
try {
const token = getStoredToken();
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
const response = await fetch('/api/sheets', {
method: 'POST',
headers,
body: JSON.stringify({ name })
});
if (response.ok) {
const data = await response.json();
const newSheet = data.sheet;
sheets = [newSheet, ...sheets];
await selectSheet(newSheet.sheet_id);
}
} catch (e) {
console.error('[daggr] Failed to create sheet:', e);
}
}
async function renameSheet(sheetId: string, newName: string) {
try {
const token = getStoredToken();
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
const response = await fetch(`/api/sheets/${sheetId}`, {
method: 'PATCH',
headers,
body: JSON.stringify({ name: newName })
});
if (response.ok) {
sheets = sheets.map(s => s.sheet_id === sheetId ? { ...s, name: newName } : s);
}
} catch (e) {
console.error('[daggr] Failed to rename sheet:', e);
}
}
async function deleteSheet(sheetId: string) {
if (!confirm('Delete this sheet and all its data?')) return;
try {
const token = getStoredToken();
const headers: Record<string, string> = {};
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
const response = await fetch(`/api/sheets/${sheetId}`, { method: 'DELETE', headers });
if (response.ok) {
sheets = sheets.filter(s => s.sheet_id !== sheetId);
if (currentSheetId === sheetId) {
if (sheets.length > 0) {
await selectSheet(sheets[0].sheet_id);
} else {
await createSheet();
}
}
}
} catch (e) {
console.error('[daggr] Failed to delete sheet:', e);
}
}
async function selectSheet(sheetId: string) {
currentSheetId = sheetId;
nodeResults = {};
selectedResultIndex = {};
inputValues = {};
itemListValues = {};
selectedVariants = {};
runningNodes = new Set();
nodeStartTimes = {};
nodeExecutionTimes = {};
nodeErrors = {};
if (timerInterval) {
clearInterval(timerInterval);
timerInterval = null;
}
if (transformDebounceTimer) {
clearTimeout(transformDebounceTimer);
transformDebounceTimer = null;
}
timerTick = 0;
transform = { x: 0, y: 0, scale: 1 };
if (ws && wsConnected) {
const token = getStoredToken();
ws.send(JSON.stringify({ action: 'set_sheet', sheet_id: sheetId, hf_token: token }));
ws.send(JSON.stringify({ action: 'get_graph', sheet_id: sheetId, hf_token: token }));
}
sheetDropdownOpen = false;
}
function connectWebSocket() {
if (isConnecting) return;
if (reconnectAttempts >= maxReconnectAttempts) {
console.error('[daggr] Max reconnection attempts reached');
return;
}
isConnecting = true;
if (!sessionId) {
sessionId = generateSessionId();
}
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsUrl = `${protocol}//${window.location.host}/ws/${sessionId}`;
console.log('[daggr] Connecting to', wsUrl);
try {
ws = new WebSocket(wsUrl);
} catch (e) {
console.error('[daggr] Failed to create WebSocket:', e);
isConnecting = false;
scheduleReconnect();
return;
}
ws.onopen = async () => {
console.log('[daggr] WebSocket connected');
isConnecting = false;
wsConnected = true;
reconnectAttempts = 0;
const token = getStoredToken();
if (canPersist && currentSheetId) {
ws?.send(JSON.stringify({ action: 'get_graph', sheet_id: currentSheetId, hf_token: token }));
} else {
ws?.send(JSON.stringify({ action: 'get_graph', hf_token: token }));
}
};
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
handleMessage(data);
};
ws.onclose = () => {
isConnecting = false;
wsConnected = false;
scheduleReconnect();
};
ws.onerror = () => {
console.error('[daggr] WebSocket error');
isConnecting = false;
};
}
function scheduleReconnect() {
if (reconnectTimer) return;
reconnectAttempts++;
const delay = reconnectAttempts === 1 ? 0 : reconnectAttempts <= 5 ? 50 : Math.min(1000 * Math.pow(2, reconnectAttempts - 5), 30000);
console.log(`[daggr] Reconnecting in ${delay}ms (attempt ${reconnectAttempts}/${maxReconnectAttempts})`);
reconnectTimer = window.setTimeout(() => {
reconnectTimer = null;
connectWebSocket();
}, delay);
}
function handleMessage(data: any) {
if (data.type === 'graph') {
const newUserId = data.data.user_id;
const newSheetId = data.data.sheet_id;
const userOrSheetChanged = newUserId !== userId || newSheetId !== currentSheetId;
if (userOrSheetChanged) {
nodeResults = {};
nodeInputsSnapshots = {};
selectedResultIndex = {};
nodeErrors = {};
inputValues = {};
itemListValues = {};
selectedVariants = {};
nodeExecutionTimes = {};
}
graphData = data.data;
userId = newUserId;
if (newSheetId) {
currentSheetId = newSheetId;
}
if (data.data.nodes) {
let hasNewErrors = false;
for (const node of data.data.nodes) {
if (node.validation_error) {
nodeErrors[node.name] = node.validation_error;
hasNewErrors = true;
}
}
if (hasNewErrors) {
nodeErrors = { ...nodeErrors };
}
}
if (data.data.persisted_results) {
for (const [nodeName, results] of Object.entries(data.data.persisted_results as Record<string, any[]>)) {
if (results && results.length > 0) {
const node = data.data.nodes?.find((n: GraphNode) => n.name === nodeName);
if (node && node.output_components?.length > 0) {
const snapshots: (Record<string, any> | null)[] = [];
nodeResults[nodeName] = results.map((entry: any) => {
const result = entry?.result !== undefined ? entry.result : entry;
const inputsSnapshot = entry?.inputs_snapshot || null;
snapshots.push(inputsSnapshot);
return node.output_components.map((comp: GradioComponentData) => {
if (result === null || result === undefined) {
return { ...comp, value: comp.value };
}
if (typeof result !== 'object' || Array.isArray(result)) {
const expectedKeys = node.output_components.map((c: GradioComponentData) => c.port_name).join(', ');
nodeErrors[nodeName] = `Function must return a dict with keys: {${expectedKeys}}. Got ${Array.isArray(result) ? 'list' : typeof result} instead.`;
return { ...comp, value: comp.value };
}
if (!(comp.port_name in result)) {
const expectedKeys = node.output_components.map((c: GradioComponentData) => c.port_name).join(', ');
const gotKeys = Object.keys(result).join(', ');
nodeErrors[nodeName] = `Missing key "${comp.port_name}" in return value. Expected: {${expectedKeys}}, got: {${gotKeys}}`;
return { ...comp, value: comp.value };
}
return { ...comp, value: result[comp.port_name] };
});
});
nodeInputsSnapshots[nodeName] = snapshots;
selectedResultIndex[nodeName] = nodeResults[nodeName].length - 1;
}
}
}
}
if (data.data.inputs) {
inputValues = data.data.inputs;
for (const [nodeId, nodeInputs] of Object.entries(inputValues)) {
const variant = (nodeInputs as Record<string, any>)['_selected_variant'];
if (variant !== undefined) {
selectedVariants[nodeId] = variant;
}
}
}
if (data.data.transform) {
transform = {
x: data.data.transform.x ?? 0,
y: data.data.transform.y ?? 0,
scale: data.data.transform.scale ?? 1
};
}
} else if (data.type === 'node_started') {
const startedNode = data.started_node;
if (startedNode) {
runningNodes.add(startedNode);
runningNodes = new Set(runningNodes);
if (data.run_id) {
nodeRunIds[startedNode] = data.run_id;
}
nodeStartTimes[startedNode] = Date.now();
delete nodeErrors[startedNode];
startTimer();
}
} else if (data.type === 'cancelled') {
const cancelledRunId = data.run_id;
for (const [nodeName, runId] of Object.entries(nodeRunIds)) {
if (runId === cancelledRunId) {
runningNodes.delete(nodeName);
delete nodeStartTimes[nodeName];
delete nodeRunIds[nodeName];
}
}
runningNodes = new Set(runningNodes);
stopTimerIfNoRunning();
} else if (data.type === 'error' && data.error) {
console.error('[daggr] server error:', data.error);
const errorNode = data.node || data.completed_node;
if (errorNode) {
nodeErrors[errorNode] = data.error;
}
const nodesToClear = data.nodes_to_clear || (errorNode ? [errorNode] : []);
for (const nodeName of nodesToClear) {
delete nodeStartTimes[nodeName];
delete nodeRunIds[nodeName];
runningNodes.delete(nodeName);
}
runningNodes = new Set(runningNodes);
stopTimerIfNoRunning();
} else if (data.type === 'node_complete' || data.type === 'error') {
const completedNode = data.completed_node;
if (completedNode) {
runningNodes.delete(completedNode);
runningNodes = new Set(runningNodes);
delete nodeRunIds[completedNode];
}
if (completedNode && data.execution_time_ms != null) {
nodeExecutionTimes[completedNode] = data.execution_time_ms;
delete nodeStartTimes[completedNode];
if (!nodeAvgTimes[completedNode]) {
nodeAvgTimes[completedNode] = { total: 0, count: 0 };
}
nodeAvgTimes[completedNode].total += data.execution_time_ms;
nodeAvgTimes[completedNode].count++;
stopTimerIfNoRunning();
}
if (data.nodes) {
graphData = { ...graphData!, nodes: data.nodes, edges: data.edges || graphData!.edges };
let hasNewErrors = false;
for (const node of data.nodes) {
if (node.validation_error) {
nodeErrors[node.name] = node.validation_error;
hasNewErrors = true;
}
}
if (hasNewErrors) {
nodeErrors = { ...nodeErrors };
}
if (completedNode) {
const node = data.nodes?.find((n: GraphNode) => n.name === completedNode);
if (node && node.output_components?.length > 0) {
const hasResult = node.output_components.some((c: GradioComponentData) => c.value != null);
if (hasResult) {
if (!nodeResults[completedNode]) {
nodeResults[completedNode] = [];
}
if (!nodeInputsSnapshots[completedNode]) {
nodeInputsSnapshots[completedNode] = [];
}
const resultSnapshot = node.output_components.map((c: GradioComponentData) => ({ ...c }));
nodeResults[completedNode] = [...nodeResults[completedNode], resultSnapshot];
const snapshot = data.inputs || data.selected_results ? {
inputs: data.inputs || {},
selected_results: data.selected_results || {},
} : null;
nodeInputsSnapshots[completedNode] = [...nodeInputsSnapshots[completedNode], snapshot];
selectedResultIndex[completedNode] = nodeResults[completedNode].length - 1;
if (isOnSpaces && !hfUser && !hasShownPersistencePrompt) {
hasShownPersistencePrompt = true;
showLoginTooltip = true;
}
}
}
}
}
}
}
onMount(() => {
const savedTheme = localStorage.getItem('theme');
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
isDark = savedTheme ? savedTheme === 'dark' : prefersDark;
applyTheme(isDark);
async function initialize() {
await fetchUserInfo();
if (canPersist) {
await fetchSheets();
if (sheets.length === 0) {
await createSheet();
} else {
currentSheetId = sheets[0].sheet_id;
}
}
connectWebSocket();
}
initialize();
return () => {
if (reconnectTimer) {
clearTimeout(reconnectTimer);
reconnectTimer = null;
}
if (timerInterval) {
clearInterval(timerInterval);
timerInterval = null;
}
if (saveDebounceTimer) {
clearTimeout(saveDebounceTimer);
saveDebounceTimer = null;
}
if (transformDebounceTimer) {
clearTimeout(transformDebounceTimer);
transformDebounceTimer = null;
}
if (ws) {
ws.onclose = null;
ws.onerror = null;
ws.close();
ws = null;
}
};
});
function getAncestors(nodeName: string): string[] {
const ancestors = new Set<string>();
const toVisit = [nodeName];
while (toVisit.length > 0) {
const current = toVisit.pop()!;
for (const edge of edges) {
if (edge.to_node === current.replace(/ /g, '_').replace(/-/g, '_')) {
const sourceNode = nodes.find(n => n.id === edge.from_node);
if (sourceNode && !ancestors.has(sourceNode.name) && !sourceNode.is_input_node) {
ancestors.add(sourceNode.name);
toVisit.push(sourceNode.name);
}
}
}
}
return Array.from(ancestors);
}
function debounceSaveInput(nodeId: string, portName: string, value: any) {
if (!canPersist || !currentSheetId) return;
if (saveDebounceTimer) {
clearTimeout(saveDebounceTimer);
}
saveDebounceTimer = window.setTimeout(() => {
if (ws && wsConnected) {
ws.send(JSON.stringify({
action: 'save_input',
node_id: nodeId,
port_name: portName,
value: value
}));
}
}, 500);
}
function debounceSaveTransform() {
if (!canPersist || !currentSheetId) return;
if (transformDebounceTimer) {
clearTimeout(transformDebounceTimer);
}
transformDebounceTimer = window.setTimeout(() => {
if (ws && wsConnected) {
ws.send(JSON.stringify({
action: 'save_transform',
x: transform.x,
y: transform.y,
scale: transform.scale
}));
}
}, 300);
}
async function handleInputChange(nodeId: string, portName: string, value: any) {
if (!inputValues[nodeId]) {
inputValues[nodeId] = {};
}
if (value instanceof Blob || value instanceof File) {
const dataUrl = await blobToDataUrl(value);
inputValues[nodeId][portName] = dataUrl;
debounceSaveInput(nodeId, portName, dataUrl);
} else {
inputValues[nodeId][portName] = value;
debounceSaveInput(nodeId, portName, value);
}
}
function blobToDataUrl(blob: Blob): Promise<string> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result as string);
reader.onerror = reject;
reader.readAsDataURL(blob);
});
}
function getComponentValue(node: GraphNode, comp: GradioComponentData): any {
const nodeInputs = inputValues[node.id];
if (nodeInputs && comp.port_name in nodeInputs) {
return nodeInputs[comp.port_name];
}
return comp.value ?? '';
}
function handleItemListChange(nodeId: string, itemIndex: number, fieldName: string, value: any) {
if (!itemListValues[nodeId]) {
itemListValues[nodeId] = {};
}
if (!itemListValues[nodeId][itemIndex]) {
itemListValues[nodeId][itemIndex] = {};
}
itemListValues[nodeId][itemIndex][fieldName] = value;
}
function getItemListValue(nodeId: string, itemIndex: number, fieldName: string): any {
const edited = itemListValues[nodeId]?.[itemIndex]?.[fieldName];
if (edited !== undefined) return edited;
const node = nodes.find(n => n.id === nodeId);
const item = node?.item_list_items?.find(i => i.index === itemIndex);
return item?.fields?.[fieldName] ?? '';
}
function handleVariantSelect(nodeId: string, variantIndex: number) {
selectedVariants[nodeId] = variantIndex;
if (!inputValues[nodeId]) {
inputValues[nodeId] = {};
}
inputValues[nodeId]['_selected_variant'] = variantIndex;
if (ws && wsConnected) {
ws.send(JSON.stringify({
action: 'save_variant_selection',
node_id: nodeId,
variant_index: variantIndex
}));
}
}
function getSelectedVariant(node: GraphNode): number {
if (selectedVariants[node.id] !== undefined) {
return selectedVariants[node.id];
}
return node.selected_variant ?? 0;
}
function getComponentsToRender(node: GraphNode): GradioComponentData[] {
if (node.is_input_node && node.input_components?.length) {
return node.input_components;
}
return getSelectedResults(node);
}
function hasUserProvidedOutput(node: GraphNode): boolean {
if (!node.output_components || node.output_components.length === 0) return false;
const nodeInputs = inputValues[node.id];
if (!nodeInputs) return false;
for (const comp of node.output_components) {
if (nodeInputs[comp.port_name] != null) return true;
}
return false;
}
function getNodeHeight(node: GraphNode): number {
const portRows = Math.max(node.inputs.length, node.outputs.length, 1);
const componentsToRender = getComponentsToRender(node);
const embeddedHeight = componentsToRender.length * EMBEDDED_COMPONENT_HEIGHT;
return HEADER_HEIGHT + HEADER_BORDER + BODY_PADDING_TOP + (portRows * PORT_ROW_HEIGHT) + embeddedHeight + BODY_PADDING_TOP;
}
let nodeMap = $derived.by(() => {
const map = new Map<string, GraphNode>();
for (const node of nodes) {
map.set(node.id, node);
}
return map;
});
function getPortY(portIndex: number): number {
return HEADER_HEIGHT + HEADER_BORDER + BODY_PADDING_TOP + (portIndex * PORT_ROW_HEIGHT) + (PORT_ROW_HEIGHT / 2);
}
let edgePaths = $derived.by(() => {
const paths: {
id: string;
d: string;
is_scattered: boolean;
is_gathered: boolean;
isStale: boolean;
forkPaths?: string[];
fromNodeName: string;
toNodeName: string;
}[] = [];
for (const edge of edges) {
const fromNode = nodeMap.get(edge.from_node);
const toNode = nodeMap.get(edge.to_node);
if (!fromNode || !toNode) continue;
const fromPortIdx = fromNode.outputs.indexOf(edge.from_port);
const toPortIdx = toNode.inputs.findIndex(p => p.name === edge.to_port);
if (fromPortIdx === -1 || toPortIdx === -1) continue;
const fromPortY = getPortY(fromPortIdx);
const toPortY = getPortY(toPortIdx);
const x1 = fromNode.x + NODE_WIDTH;
const y1 = fromNode.y + fromPortY;
const x2 = toNode.x;
const y2 = toNode.y + toPortY;
const dx = Math.abs(x2 - x1);
const cp = Math.max(dx * 0.4, 50);
const is_scattered = edge.is_scattered || false;
const is_gathered = edge.is_gathered || false;
const toNodeSelectedIdx = selectedResultIndex[toNode.name];
const toNodeSnapshot = nodeInputsSnapshots[toNode.name]?.[toNodeSelectedIdx];
let isStale = false;
if (toNodeSnapshot == null) {
isStale = true;
} else {
if (selectedResultIndex[fromNode.name] !== toNodeSnapshot.selected_results?.[fromNode.name]) {
isStale = true;
}
if (JSON.stringify(inputValues[fromNode.id]) !== JSON.stringify(toNodeSnapshot.inputs?.[fromNode.id])) {
isStale = true;
}
}
let forkPaths: string[] = [];
if (is_scattered) {
const forkStart = x2 - 30;
const forkSpread = 8;
const d = `M ${x1} ${y1} C ${x1 + cp} ${y1}, ${forkStart - 20} ${y2}, ${forkStart} ${y2}`;
forkPaths = [
`M ${forkStart} ${y2} L ${x2} ${y2 - forkSpread}`,
`M ${forkStart} ${y2} L ${x2} ${y2}`,
`M ${forkStart} ${y2} L ${x2} ${y2 + forkSpread}`,
];
paths.push({ id: edge.id, d, is_scattered, is_gathered, isStale, forkPaths, fromNodeName: fromNode.name, toNodeName: toNode.name });
} else if (is_gathered) {
const forkEnd = x1 + 30;
const forkSpread = 8;
forkPaths = [
`M ${x1} ${y1 - forkSpread} L ${forkEnd} ${y1}`,
`M ${x1} ${y1} L ${forkEnd} ${y1}`,
`M ${x1} ${y1 + forkSpread} L ${forkEnd} ${y1}`,
];
const d = `M ${forkEnd} ${y1} C ${forkEnd + cp - 30} ${y1}, ${x2 - cp} ${y2}, ${x2} ${y2}`;
paths.push({ id: edge.id, d, is_scattered, is_gathered, isStale, forkPaths, fromNodeName: fromNode.name, toNodeName: toNode.name });
} else {
const d = `M ${x1} ${y1} C ${x1 + cp} ${y1}, ${x2 - cp} ${y2}, ${x2} ${y2}`;
paths.push({ id: edge.id, d, is_scattered, is_gathered, isStale, fromNodeName: fromNode.name, toNodeName: toNode.name });
}
}
return paths;
});
function zoomToFit() {
if (nodes.length === 0 || !canvasEl) return;
const padding = 40;
const canvasRect = canvasEl.getBoundingClientRect();
const canvasWidth = canvasRect.width;
const canvasHeight = canvasRect.height;
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
for (const node of nodes) {
const nodeHeight = getNodeHeight(node);
minX = Math.min(minX, node.x);
minY = Math.min(minY, node.y);
maxX = Math.max(maxX, node.x + NODE_WIDTH);
maxY = Math.max(maxY, node.y + nodeHeight);
}
const contentWidth = maxX - minX;
const contentHeight = maxY - minY;
const scaleX = (canvasWidth - padding * 2) / contentWidth;
const scaleY = (canvasHeight - padding * 2) / contentHeight;
const newScale = Math.min(scaleX, scaleY, 1.5);
const centerX = (minX + maxX) / 2;
const centerY = (minY + maxY) / 2;
const newX = canvasWidth / 2 - centerX * newScale;
const newY = canvasHeight / 2 - centerY * newScale;
transform = { x: newX, y: newY, scale: Math.max(0.2, newScale) };
debounceSaveTransform();
}
function zoomIn() {
transform.scale = Math.min(3, transform.scale * 1.2);
debounceSaveTransform();
}
function zoomOut() {
transform.scale = Math.max(0.2, transform.scale / 1.2);
debounceSaveTransform();
}
function handleMouseDown(e: MouseEvent) {
if (e.button === 0 && e.target === canvasEl) {
isPanning = true;
startPan = { x: e.clientX - transform.x, y: e.clientY - transform.y };
}
const target = e.target as HTMLElement;
if (!target.closest('.run-controls')) {
runModeMenuOpen = null;
}
if (!target.closest('.sheet-selector')) {
sheetDropdownOpen = false;
}
}
function handleMouseMove(e: MouseEvent) {
if (isPanning) {
transform.x = e.clientX - startPan.x;
transform.y = e.clientY - startPan.y;
}
}
function handleMouseUp() {
if (isPanning) {
isPanning = false;
debounceSaveTransform();
}
}
function handleWheel(e: WheelEvent) {
const target = e.target as HTMLElement;
const scrollableParent = target.closest('.item-list-items, .map-items-list, .embedded-components');
if (scrollableParent && !e.ctrlKey && !e.metaKey) {
const el = scrollableParent as HTMLElement;
const canScrollUp = el.scrollTop > 0;
const canScrollDown = el.scrollTop < el.scrollHeight - el.clientHeight;
const scrollingDown = e.deltaY > 0;
const scrollingUp = e.deltaY < 0;
if ((scrollingDown && canScrollDown) || (scrollingUp && canScrollUp)) {
return;
}
}
e.preventDefault();
if (e.ctrlKey || e.metaKey) {
const rect = canvasEl.getBoundingClientRect();
const mouseX = e.clientX - rect.left;
const mouseY = e.clientY - rect.top;
const canvasX = (mouseX - transform.x) / transform.scale;
const canvasY = (mouseY - transform.y) / transform.scale;
const delta = e.deltaY > 0 ? 0.97 : 1.03;
const newScale = Math.max(0.2, Math.min(3, transform.scale * delta));
transform = {
x: mouseX - canvasX * newScale,
y: mouseY - canvasY * newScale,
scale: newScale
};
} else {
transform = {
...transform,
x: transform.x - e.deltaX,
y: transform.y - e.deltaY
};
}
debounceSaveTransform();
}
function handleRunNode(e: MouseEvent, nodeName: string, runMode?: 'step' | 'toHere') {
e.stopPropagation();
const mode = runMode ?? nodeRunModes[nodeName] ?? 'toHere';
const runId = `${nodeName}_${Date.now()}_${Math.random().toString(36).slice(2)}`;
runningNodes.add(nodeName);
runningNodes = new Set(runningNodes);
nodeRunIds[nodeName] = runId;
delete nodeExecutionTimes[nodeName];
if (ws && wsConnected) {
ws.send(JSON.stringify({
action: 'run',
node_name: nodeName,
inputs: inputValues,
item_list_values: itemListValues,
selected_results: selectedResultIndex,
run_id: runId,
sheet_id: currentSheetId,
hf_token: getStoredToken(),
run_ancestors: mode === 'toHere'
}));
}
}
function handleCancelNode(e: MouseEvent, nodeName: string) {
e.stopPropagation();
const runId = nodeRunIds[nodeName];
if (runId && ws && wsConnected) {
ws.send(JSON.stringify({
action: 'cancel',
run_id: runId,
node_name: nodeName,
}));
}
}
function setRunMode(nodeName: string, mode: 'step' | 'toHere') {
nodeRunModes[nodeName] = mode;
nodeRunModes = { ...nodeRunModes };
runModeVersion++;
runModeMenuOpen = null;
}
function highlightRunTargets(nodeName: string, mode: 'step' | 'toHere') {
if (mode === 'step') {
highlightedNodes = new Set([nodeName]);
} else {
const ancestors = getAncestors(nodeName).filter(a => !nodeResults[a]?.length);
highlightedNodes = new Set([nodeName, ...ancestors]);
}
}
function clearHighlight() {
highlightedNodes = new Set();
}
function toggleRunModeMenu(e: MouseEvent, nodeName: string) {
e.stopPropagation();
if (runModeMenuOpen === nodeName) {
runModeMenuOpen = null;
} else {
runModeMenuOpen = nodeName;
}
}
function getRunMode(nodeName: string): 'step' | 'toHere' {
void runModeVersion;
return nodeRunModes[nodeName] ?? 'toHere';
}
function getBadgeStyle(type: string): string {
const colors: Record<string, string> = {
'FN': 'var(--color-accent)',
'INPUT': 'var(--secondary-500, #06b6d4)',
'MAP': 'var(--primary-400, #a855f7)',
'GRADIO': 'var(--color-accent)',
'MODEL': 'var(--primary-500, #22c55e)',
'CHOICE': 'var(--primary-400, #8b5cf6)',
};
return `background: ${colors[type] || 'var(--neutral-500)'};`;
}
function getSelectedResults(node: GraphNode): GradioComponentData[] {
const results = nodeResults[node.name];
if (!results || results.length === 0) {
return node.output_components || [];
}
const idx = selectedResultIndex[node.name] ?? results.length - 1;
return results[idx] || node.output_components || [];
}
function getResultCount(nodeName: string): number {
return nodeResults[nodeName]?.length || 0;
}
function restoreInputsSnapshot(nodeName: string, index: number) {
const snapshots = nodeInputsSnapshots[nodeName];
if (!snapshots || !snapshots[index]) return;
const snapshot = snapshots[index];
const inputs = snapshot.inputs || snapshot;
for (const [inputNodeId, nodeInputs] of Object.entries(inputs)) {
if (typeof nodeInputs === 'object' && nodeInputs !== null) {
inputValues[inputNodeId] = { ...inputValues[inputNodeId], ...nodeInputs };
}
}
if (snapshot.selected_results) {
for (const [upstreamNode, resultIdx] of Object.entries(snapshot.selected_results)) {
if (upstreamNode === nodeName) continue;
if (typeof resultIdx === 'number') {
selectedResultIndex[upstreamNode] = resultIdx;
}
}
}
}
function autoMatchDownstream(changedNode: string, newIndex: number) {
for (const [nodeName, snapshots] of Object.entries(nodeInputsSnapshots)) {
if (!snapshots || nodeName === changedNode) continue;
const matchIdx = snapshots.findIndex(
s => s?.selected_results?.[changedNode] === newIndex
);
if (matchIdx !== -1) {
selectedResultIndex[nodeName] = matchIdx;
autoMatchDownstream(nodeName, matchIdx);
}
}
}
function prevResult(e: MouseEvent, nodeName: string) {
e.stopPropagation();
const current = selectedResultIndex[nodeName] ?? 0;
if (current > 0) {
const newIndex = current - 1;
selectedResultIndex[nodeName] = newIndex;
selectedResultIndex = { ...selectedResultIndex };
restoreInputsSnapshot(nodeName, newIndex);
autoMatchDownstream(nodeName, newIndex);
}
}
function nextResult(e: MouseEvent, nodeName: string) {
e.stopPropagation();
const total = getResultCount(nodeName);
const current = selectedResultIndex[nodeName] ?? 0;
if (current < total - 1) {
const newIndex = current + 1;
selectedResultIndex[nodeName] = newIndex;
selectedResultIndex = { ...selectedResultIndex };
restoreInputsSnapshot(nodeName, newIndex);
autoMatchDownstream(nodeName, newIndex);
}
}
function handleReplayItem(nodeName: string, itemIndex: number) {
}
let zoomPercent = $derived(Math.round(transform.scale * 100));
function formatTime(ms: number): string {
if (ms < 1000) {
return `${(ms / 1000).toFixed(1)}s`;
} else if (ms < 60000) {
return `${(ms / 1000).toFixed(1)}s`;
} else {
const mins = Math.floor(ms / 60000);
const secs = ((ms % 60000) / 1000).toFixed(0);
return `${mins}m ${secs}s`;
}
}
function getNodeTimeDisplay(nodeName: string): { text: string; isRunning: boolean; isError: boolean } | null {
void timerTick;
if (nodeErrors[nodeName]) {
return { text: 'Error', isRunning: false, isError: true };
}
const isRunning = runningNodes.has(nodeName);
const startTime = nodeStartTimes[nodeName];
const finalTime = nodeExecutionTimes[nodeName];
const avgData = nodeAvgTimes[nodeName];
const avgTime = avgData ? avgData.total / avgData.count : null;
if (isRunning && startTime) {
const elapsed = Date.now() - startTime;
if (avgTime) {
return { text: `${formatTime(elapsed)}/${formatTime(avgTime)}`, isRunning: true, isError: false };
}
return { text: formatTime(elapsed), isRunning: true, isError: false };
}
if (finalTime != null) {
return { text: formatTime(finalTime), isRunning: false, isError: false };
}
return null;
}
function startEditingSheetName() {
if (currentSheet) {
editSheetNameValue = currentSheet.name;
editingSheetName = true;
}
}
function finishEditingSheetName() {
if (editingSheetName && currentSheetId && editSheetNameValue.trim()) {
renameSheet(currentSheetId, editSheetNameValue.trim());
}
editingSheetName = false;
}
function handleSheetNameKeydown(e: KeyboardEvent) {
if (e.key === 'Enter') {
finishEditingSheetName();
} else if (e.key === 'Escape') {
editingSheetName = false;
}
}
function resetSheetValues() {
if (confirm('Are you sure you want to reset all component values? This cannot be undone.')) {
inputValues = {};
nodeResults = {};
selectedResultIndex = {};
itemListValues = {};
selectedVariants = {};
nodeErrors = {};
nodeExecutionTimes = {};
nodeInputsSnapshots = {};
nodeAvgTimes = {};
nodeStartTimes = {};
if (graphData?.nodes) {
for (const node of graphData.nodes) {
if (node.output_components) {
for (const comp of node.output_components) {
comp.value = null;
}
}
}
graphData = { ...graphData };
}
if (ws && wsConnected && canPersist && currentSheetId) {
ws.send(JSON.stringify({ action: 'clear_sheet' }));
}
}
}
</script>
<div
class="canvas"
class:dark={isDark}
bind:this={canvasEl}
onmousedown={handleMouseDown}
onmousemove={handleMouseMove}
onmouseup={handleMouseUp}
onmouseleave={handleMouseUp}
onwheel={handleWheel}
role="application"
>
<div class="grid-bg"></div>
<div
class="canvas-transform"
style="transform: translate({transform.x}px, {transform.y}px) scale({transform.scale})"
>
<svg class="edges-svg">
{#each edgePaths as edge (edge.id)}
<path d={edge.d} class="edge-path" class:stale={edge.isStale} class:will-run={highlightedNodes.has(edge.fromNodeName) && highlightedNodes.has(edge.toNodeName)} />
{#if edge.forkPaths}
{#each edge.forkPaths as forkD}
<path d={forkD} class="edge-path edge-fork" class:stale={edge.isStale} class:will-run={highlightedNodes.has(edge.fromNodeName) && highlightedNodes.has(edge.toNodeName)} />
{/each}
{/if}
{/each}
</svg>
{#each nodes as node (node.id)}
{@const componentsToRender = getComponentsToRender(node)}
{@const timeDisplay = getNodeTimeDisplay(node.name)}
<div
class="node"
class:will-run={highlightedNodes.has(node.name)}
style="left: {node.x}px; top: {node.y}px; width: {NODE_WIDTH}px;"
>
{#if timeDisplay}
<div class="exec-time" class:running={timeDisplay.isRunning} class:error={timeDisplay.isError}>{timeDisplay.text}</div>
{/if}
<div class="node-header">
<span class="type-badge" style={getBadgeStyle(node.type)}>{node.type}{#if node.is_local} ⚡{/if}</span>
{#if node.url}
<a class="node-name node-link" href={node.url} target="_blank" rel="noopener noreferrer" title="Open on Hugging Face">{node.name}</a>
{:else}
<span class="node-name">{node.name}</span>
{/if}
{#if !node.is_input_node}
{#key runModeVersion}
<div class="run-controls">
{#if runningNodes.has(node.name)}
<span
class="run-btn running"
onclick={(e) => handleCancelNode(e, node.name)}
title="Stop"
role="button"
tabindex="0"
>
<svg class="run-icon-svg" viewBox="0 0 12 12" fill="currentColor">
<rect x="2" y="2" width="8" height="8" rx="1"/>
</svg>
</span>
{:else}
<span
class="run-btn"
onclick={(e) => handleRunNode(e, node.name)}
onmouseenter={() => highlightRunTargets(node.name, getRunMode(node.name))}
onmouseleave={() => clearHighlight()}
title={(nodeRunModes[node.name] ?? 'toHere') === 'toHere' ? "Run to here" : "Run this step"}
role="button"
tabindex="0"
>
{#if node.is_map_node || (nodeRunModes[node.name] ?? 'toHere') === 'toHere'}
<svg class="run-icon-svg run-icon-double" viewBox="0 0 14 12" fill="currentColor">
<path d="M2 1 L10 6 L2 11 Z" opacity="0.5" transform="translate(-2, 0)"/>
<path d="M2 1 L10 6 L2 11 Z" transform="translate(2, 0)"/>
</svg>
{:else}
<svg class="run-icon-svg" viewBox="0 0 14 12" fill="currentColor">
<path d="M3 1 L11 6 L3 11 Z"/>
</svg>
{/if}
</span>
{/if}
<span
class="run-mode-toggle"
onclick={(e) => toggleRunModeMenu(e, node.name)}
role="button"
tabindex="0"
title="Run options"
>
<svg viewBox="0 0 10 6" fill="currentColor">
<path d="M1 1 L5 5 L9 1" stroke="currentColor" stroke-width="1.5" fill="none"/>
</svg>
</span>
{#if runModeMenuOpen === node.name}
<div class="run-mode-menu" onmouseleave={() => clearHighlight()}>
<button
class="run-mode-option"
class:active={(nodeRunModes[node.name] ?? 'toHere') === 'step'}
onclick={(e) => { e.stopPropagation(); setRunMode(node.name, 'step'); clearHighlight(); }}
onmouseenter={() => highlightRunTargets(node.name, 'step')}
>
<svg class="run-mode-icon" viewBox="0 0 10 12" fill="currentColor">
<path d="M1 1 L9 6 L1 11 Z"/>
</svg>
<span>Run this step</span>
</button>
<button
class="run-mode-option"
class:active={(nodeRunModes[node.name] ?? 'toHere') === 'toHere'}
onclick={(e) => { e.stopPropagation(); setRunMode(node.name, 'toHere'); clearHighlight(); }}
onmouseenter={() => highlightRunTargets(node.name, 'toHere')}
>
<svg class="run-mode-icon run-mode-icon-double" viewBox="0 0 14 12" fill="currentColor">
<path d="M2 1 L10 6 L2 11 Z" opacity="0.5" transform="translate(-2, 0)"/>
<path d="M2 1 L10 6 L2 11 Z" transform="translate(2, 0)"/>
</svg>
<span>Run to here</span>
</button>
</div>
{/if}
</div>
{/key}
{/if}
</div>
<div class="node-body">
<div class="ports-left">
{#each node.inputs as port (port.name)}
<div class="port-row">
<span class="port-dot input"></span>
<span class="port-label">{port.name}</span>
</div>
{/each}
</div>
<div class="ports-right">
{#each node.outputs as portName (portName)}
<div class="port-row">
<span class="port-label">{portName}</span>
<span class="port-dot output"></span>
gitextract_z_vlte2q/
├── .agents/
│ └── skills/
│ └── daggr/
│ └── SKILL.md
├── .changeset/
│ ├── README.md
│ ├── changeset.cjs
│ ├── config.json
│ └── fix_changelogs.cjs
├── .github/
│ ├── assets/
│ │ └── run-mode-dropdown-demo.webm
│ ├── pull_request_template.md
│ └── workflows/
│ ├── comment-queue.yml
│ ├── format.yml
│ ├── generate-changeset.yml
│ ├── publish.yml
│ ├── test.yml
│ └── trigger-changeset.yml
├── .gitignore
├── CHANGELOG.md
├── CLAUDE.md
├── CONTRIBUTING.md
├── LICENSE
├── MANIFEST.in
├── README.md
├── RELEASE.md
├── build_pypi.sh
├── daggr/
│ ├── CHANGELOG.md
│ ├── __init__.py
│ ├── _client_cache.py
│ ├── _utils.py
│ ├── cli.py
│ ├── edge.py
│ ├── executor.py
│ ├── frontend/
│ │ ├── index.html
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── App.svelte
│ │ │ ├── components/
│ │ │ │ ├── Audio.svelte
│ │ │ │ ├── AudioPlayer.svelte
│ │ │ │ ├── Button.svelte
│ │ │ │ ├── Checkbox.svelte
│ │ │ │ ├── CheckboxGroup.svelte
│ │ │ │ ├── Code.svelte
│ │ │ │ ├── ColorPicker.svelte
│ │ │ │ ├── Dataframe.svelte
│ │ │ │ ├── Dialogue.svelte
│ │ │ │ ├── Dropdown.svelte
│ │ │ │ ├── EmbeddedComponent.svelte
│ │ │ │ ├── File.svelte
│ │ │ │ ├── Gallery.svelte
│ │ │ │ ├── HighlightedText.svelte
│ │ │ │ ├── Html.svelte
│ │ │ │ ├── Image.svelte
│ │ │ │ ├── ImageSlider.svelte
│ │ │ │ ├── ItemListSection.svelte
│ │ │ │ ├── Json.svelte
│ │ │ │ ├── Label.svelte
│ │ │ │ ├── MapItemsSection.svelte
│ │ │ │ ├── Markdown.svelte
│ │ │ │ ├── Model3D.svelte
│ │ │ │ ├── Number.svelte
│ │ │ │ ├── Radio.svelte
│ │ │ │ ├── Slider.svelte
│ │ │ │ ├── Textbox.svelte
│ │ │ │ ├── Video.svelte
│ │ │ │ └── index.ts
│ │ │ ├── main.ts
│ │ │ └── types.ts
│ │ ├── svelte.config.js
│ │ └── vite.config.ts
│ ├── graph.py
│ ├── local_space.py
│ ├── node.py
│ ├── ops.py
│ ├── package.json
│ ├── port.py
│ ├── py.typed
│ ├── server.py
│ ├── session.py
│ └── state.py
├── examples/
│ ├── 01_quickstart.py
│ ├── 02_voice_design_comparator_app.py
│ ├── 03_mock_podcast_app.py
│ ├── 04_complete_podcast_app.py
│ ├── 05_local_translation_app.py
│ ├── 06_pig_latin_voice_app.py
│ ├── 07_image_to_3d_app.py
│ ├── 08_text_to_3d_app.py
│ ├── 09_slideshow_app.py
│ ├── 10_real_podcast_app.py
│ ├── 11_viral_content_generator_app.py
│ ├── 12_ecommerce_product_generator_app.py
│ ├── 13_accessible_image_description_app.py
│ ├── 14_food_nutrition_analyzer_app.py
│ └── 15_background_removal_with_input_node.py
├── package.json
├── pnpm-workspace.yaml
├── pyproject.toml
└── tests/
├── README.md
├── conftest.py
├── test_api.py
├── test_basic.py
├── test_cache.py
├── test_executor.py
├── test_nodes.py
├── test_persistence.py
├── test_server.py
└── ui/
├── __init__.py
├── conftest.py
├── helpers.py
├── test_basic.py
├── test_cancel.py
├── test_dependency_hash.py
├── test_image_fix.py
├── test_images.py
├── test_run_mode.py
├── test_sheets.py
└── test_theme.py
SYMBOL INDEX (402 symbols across 43 files)
FILE: .changeset/changeset.cjs
function find_packages_dirs (line 29) | function find_packages_dirs(package_name) {
function handle_line (line 317) | function handle_line(str, prefix, suffix) {
FILE: .changeset/fix_changelogs.cjs
constant RE_PKG_NAME (line 5) | const RE_PKG_NAME = /^[\w-]+\b/;
function run (line 16) | function run() {
FILE: daggr/_client_cache.py
function _is_hot_reload (line 19) | def _is_hot_reload() -> bool:
function _get_cache_path (line 23) | def _get_cache_path(src: str) -> Path:
function _get_validated_file (line 28) | def _get_validated_file() -> Path:
function _load_validated_set (line 32) | def _load_validated_set() -> None:
function _save_validated_set (line 46) | def _save_validated_set() -> None:
function is_validated (line 56) | def is_validated(cache_key: tuple) -> bool:
function mark_validated (line 63) | def mark_validated(cache_key: tuple) -> None:
function get_api_info (line 71) | def get_api_info(src: str) -> dict | None:
function set_api_info (line 89) | def set_api_info(src: str, info: dict) -> None:
function get_client (line 101) | def get_client(src: str):
function set_client (line 105) | def set_client(src: str, client) -> None:
function _get_model_task_cache_path (line 109) | def _get_model_task_cache_path() -> Path:
function _load_model_task_cache (line 113) | def _load_model_task_cache() -> None:
function _save_model_task_cache (line 127) | def _save_model_task_cache() -> None:
function get_model_task (line 137) | def get_model_task(model: str) -> tuple[bool, str | None]:
function set_model_task (line 157) | def set_model_task(model: str, task: str | None) -> None:
function set_model_not_found (line 162) | def set_model_not_found(model: str) -> None:
function _get_dependency_hash_path (line 167) | def _get_dependency_hash_path() -> Path:
function _load_dependency_hash_cache (line 171) | def _load_dependency_hash_cache() -> None:
function _save_dependency_hash_cache (line 184) | def _save_dependency_hash_cache() -> None:
function get_dependency_hash (line 192) | def get_dependency_hash(src: str) -> str | None:
function set_dependency_hash (line 197) | def set_dependency_hash(src: str, sha: str) -> None:
FILE: daggr/_utils.py
function suggest_similar (line 8) | def suggest_similar(invalid: str, valid_options: set[str]) -> str | None:
FILE: daggr/cli.py
function _find_available_port (line 22) | def _find_available_port(host: str, start_port: int) -> int:
function find_python_imports (line 40) | def find_python_imports(file_path: Path) -> list[Path]:
function main (line 72) | def main():
function _deploy_main (line 158) | def _deploy_main():
function _extract_graph (line 246) | def _extract_graph(script_path: Path):
function _sanitize_space_name (line 287) | def _sanitize_space_name(name: str) -> str:
function _deploy (line 295) | def _deploy(
function _get_gradio_version (line 457) | def _get_gradio_version() -> str:
function _delete_sheets (line 467) | def _delete_sheets(script_path: Path, force: bool = False):
function _run_script (line 562) | def _run_script(script_path: Path, host: str, port: int):
function _run_with_reload (line 576) | def _run_with_reload(script_path: Path, host: str, port: int, watch_dagg...
function _create_app (line 626) | def _create_app():
FILE: daggr/edge.py
class Edge (line 13) | class Edge:
method __init__ (line 29) | def __init__(self, source: PortLike, target: PortLike):
method __repr__ (line 42) | def __repr__(self):
method as_tuple (line 54) | def as_tuple(self) -> tuple[str, str, str, str]:
FILE: daggr/executor.py
class FileValue (line 34) | class FileValue(str):
function _download_file (line 40) | def _download_file(url: str, hf_token: str | None = None) -> str:
function _postprocess_inference_result (line 63) | def _postprocess_inference_result(task: str | None, result: Any) -> Any:
function _call_inference_task (line 109) | def _call_inference_task(client: Any, task: str | None, inputs: dict[str...
function _read_file_as_bytes (line 205) | def _read_file_as_bytes(file_path: str) -> bytes:
class AsyncExecutor (line 221) | class AsyncExecutor:
method __init__ (line 230) | def __init__(self, graph: Graph):
method _get_client_for_gradio_node (line 233) | def _get_client_for_gradio_node(
method _get_client (line 274) | def _get_client(self, session: ExecutionSession, node_name: str):
method _get_scattered_input_edges (line 290) | def _get_scattered_input_edges(self, node_name: str) -> list:
method _get_gathered_input_edges (line 297) | def _get_gathered_input_edges(self, node_name: str) -> list:
method _prepare_inputs (line 304) | def _prepare_inputs(
method _execute_single_node_sync (line 359) | def _execute_single_node_sync(
method _execute_variant_node_sync (line 451) | def _execute_variant_node_sync(
method execute_node (line 536) | async def execute_node(
method _execute_scattered_node (line 594) | async def _execute_scattered_node(
method _wrap_file_input (line 677) | def _wrap_file_input(self, value: Any) -> Any:
method _save_data_url_to_file (line 693) | def _save_data_url_to_file(self, data_url: str) -> str | None:
method _apply_postprocess (line 723) | def _apply_postprocess(self, postprocess, raw_result: Any) -> Any:
method _extract_file_urls (line 728) | def _extract_file_urls(self, data: Any, hf_token: str | None = None) -...
method _map_gradio_result (line 739) | def _map_gradio_result(
method _map_fn_result (line 764) | def _map_fn_result(self, node, raw_result: Any) -> dict[str, Any]:
method _map_inference_result (line 783) | def _map_inference_result(self, node, raw_result: Any) -> dict[str, Any]:
method execute_all (line 794) | async def execute_all(
class SequentialExecutor (line 807) | class SequentialExecutor:
method __init__ (line 814) | def __init__(self, graph: Graph, hf_token: str | None = None):
method results (line 820) | def results(self) -> dict[str, Any]:
method results (line 824) | def results(self, value: dict[str, Any]):
method selected_variants (line 828) | def selected_variants(self) -> dict[str, int]:
method selected_variants (line 832) | def selected_variants(self, value: dict[str, int]):
method set_hf_token (line 835) | def set_hf_token(self, token: str | None):
method execute_node (line 838) | def execute_node(
method execute_all (line 850) | def execute_all(self, entry_inputs: dict[str, dict[str, Any]]) -> dict...
FILE: daggr/frontend/src/types.ts
type Port (line 1) | interface Port {
type GradioComponentData (line 6) | interface GradioComponentData {
type MapItem (line 14) | interface MapItem {
type ItemListItem (line 22) | interface ItemListItem {
type NodeVariant (line 27) | interface NodeVariant {
type GraphNode (line 33) | interface GraphNode {
type GraphEdge (line 56) | interface GraphEdge {
type CanvasData (line 66) | interface CanvasData {
FILE: daggr/graph.py
function _parse_space_id (line 29) | def _parse_space_id(src: str) -> str | None:
function _get_dependency_id (line 40) | def _get_dependency_id(node) -> tuple[str | None, str]:
function _fetch_current_sha (line 49) | def _fetch_current_sha(dep_id: str, dep_type: str) -> str | None:
function _duplicate_space_at_revision (line 66) | def _duplicate_space_at_revision(
function _prompt_dependency_changes (line 104) | def _prompt_dependency_changes(changed: list[dict]) -> None:
function _get_hf_username (line 183) | def _get_hf_username() -> str | None:
class _Spinner (line 196) | class _Spinner:
method __init__ (line 199) | def __init__(self, message: str):
method _spin (line 207) | def _spin(self):
method _finish (line 214) | def _finish(self, symbol: str, suffix: str = ""):
method succeed (line 226) | def succeed(self, suffix: str = ""):
method warn (line 229) | def warn(self, suffix: str = ""):
function _get_node_display_label (line 233) | def _get_node_display_label(node) -> str:
class Graph (line 244) | class Graph:
method __init__ (line 260) | def __init__(
method add (line 298) | def add(self, node: Node) -> Graph:
method edge (line 313) | def edge(self, source: Port, target: Port) -> Graph:
method _add_node (line 330) | def _add_node(self, node: Node) -> None:
method _create_edges_from_port_connections (line 338) | def _create_edges_from_port_connections(self, node: Node) -> None:
method _add_edge (line 363) | def _add_edge(self, edge: Edge) -> None:
method get_entry_nodes (line 375) | def get_entry_nodes(self) -> list[Node]:
method get_execution_order (line 383) | def get_execution_order(self) -> list[str]:
method get_connections (line 387) | def get_connections(self) -> list[tuple]:
method _validate_edges (line 391) | def _validate_edges(self) -> None:
method launch (line 426) | def launch(
method _prepare_local_nodes (line 472) | def _prepare_local_nodes(self) -> None:
method _check_dependency_hashes (line 481) | def _check_dependency_hashes(self) -> None:
method _startup_display (line 550) | def _startup_display(self) -> None:
method get_subgraphs (line 656) | def get_subgraphs(self) -> list[set[str]]:
method get_output_nodes (line 665) | def get_output_nodes(self) -> list[str]:
method get_api_schema (line 673) | def get_api_schema(self) -> dict:
method _get_component_type (line 735) | def _get_component_type(self, component) -> str:
method __repr__ (line 766) | def __repr__(self):
FILE: daggr/local_space.py
function _get_spaces_cache_dir (line 26) | def _get_spaces_cache_dir() -> Path:
function _get_logs_dir (line 30) | def _get_logs_dir() -> Path:
function _get_space_dir (line 37) | def _get_space_dir(space_id: str) -> Path:
function _get_metadata_path (line 46) | def _get_metadata_path(space_dir: Path) -> Path:
function _hash_file (line 50) | def _hash_file(file_path: Path) -> str:
function _find_free_port (line 56) | def _find_free_port(start: int = 7861, end: int = 7960) -> int:
function _is_space_id (line 67) | def _is_space_id(src: str) -> bool:
class LocalSpaceManager (line 73) | class LocalSpaceManager:
method __init__ (line 74) | def __init__(self, node: GradioNode):
method ensure_ready (line 84) | def ensure_ready(self) -> str:
method _ensure_cloned (line 100) | def _ensure_cloned(self) -> None:
method _get_sdk_version (line 133) | def _get_sdk_version(self) -> str | None:
method _ensure_venv (line 155) | def _ensure_venv(self) -> None:
method _launch_app (line 257) | def _launch_app(self) -> str:
method _find_app_file (line 338) | def _find_app_file(self) -> Path | None:
method _wait_for_ready (line 345) | def _wait_for_ready(
method _load_metadata (line 416) | def _load_metadata(self) -> dict[str, Any] | None:
method _save_metadata (line 424) | def _save_metadata(self, metadata: dict[str, Any]) -> None:
method _get_log_path (line 428) | def _get_log_path(self, log_type: str) -> Path:
method _log_to_file (line 435) | def _log_to_file(self, log_type: str, content: str) -> None:
method _log_error (line 445) | def _log_error(self, error: Exception) -> None:
function prepare_local_node (line 449) | def prepare_local_node(node: GradioNode) -> None:
function get_local_client (line 479) | def get_local_client(node: GradioNode) -> Any:
function cleanup_local_processes (line 491) | def cleanup_local_processes() -> None:
FILE: daggr/node.py
function _warn_if_type_set (line 33) | def _warn_if_type_set(component: Any, port_name: str) -> None:
function _is_gradio_component (line 52) | def _is_gradio_component(obj: Any) -> bool:
class Node (line 77) | class Node(ABC):
method __init__ (line 92) | def __init__(self, name: str | None = None):
method name (line 106) | def name(self) -> str:
method name (line 110) | def name(self, value: str) -> None:
method __getattr__ (line 114) | def __getattr__(self, name: str) -> Port:
method __dir__ (line 119) | def __dir__(self) -> list[str]:
method __or__ (line 123) | def __or__(self, other: Node) -> ChoiceNode:
method _inputs (line 137) | def _inputs(self) -> PortNamespace:
method _outputs (line 141) | def _outputs(self) -> PortNamespace:
method _default_output_port (line 144) | def _default_output_port(self) -> Port:
method _default_input_port (line 149) | def _default_input_port(self) -> Port:
method _validate_ports (line 154) | def _validate_ports(self):
method _process_inputs (line 163) | def _process_inputs(self, inputs: dict[str, Any]) -> None:
method _process_outputs (line 174) | def _process_outputs(self, outputs: dict[str, Any]) -> None:
method test (line 181) | def test(self, **inputs) -> dict[str, Any]:
method _generate_example_inputs (line 214) | def _generate_example_inputs(self) -> dict[str, Any]:
method __repr__ (line 239) | def __repr__(self):
class ChoiceNode (line 243) | class ChoiceNode(Node):
method __init__ (line 262) | def __init__(
method _compute_union_output_ports (line 285) | def _compute_union_output_ports(self) -> list[str]:
method _compute_union_output_components (line 295) | def _compute_union_output_components(self) -> dict[str, Any]:
method __or__ (line 303) | def __or__(self, other: Node) -> ChoiceNode:
method __repr__ (line 308) | def __repr__(self):
class GradioNode (line 313) | class GradioNode(Node):
method __init__ (line 342) | def __init__(
method _validate_space_format (line 382) | def _validate_space_format(self) -> None:
method _get_api_info (line 390) | def _get_api_info(self) -> dict:
method _validate_gradio_api (line 408) | def _validate_gradio_api(
class InferenceNode (line 508) | class InferenceNode(Node):
method __init__ (line 530) | def __init__(
method _model_name_for_hub (line 567) | def _model_name_for_hub(self) -> str:
method _provider (line 574) | def _provider(self) -> str | None:
method _fetch_model_info (line 579) | def _fetch_model_info(self) -> None:
class FnNode (line 612) | class FnNode(Node):
method __init__ (line 660) | def __init__(
method _discover_signature (line 696) | def _discover_signature(self):
method _validate_fn_inputs (line 700) | def _validate_fn_inputs(self, inputs: dict[str, Any]) -> None:
method _process_outputs (line 724) | def _process_outputs(self, outputs: dict[str, Any]) -> None:
class InteractionNode (line 735) | class InteractionNode(Node):
method __init__ (line 749) | def __init__(
class InputNode (line 775) | class InputNode(Node):
method __init__ (line 783) | def __init__(
FILE: daggr/ops.py
class ChooseOne (line 6) | class ChooseOne(InteractionNode):
method __init__ (line 9) | def __init__(self, name: str | None = None):
class Approve (line 19) | class Approve(InteractionNode):
method __init__ (line 22) | def __init__(self, name: str | None = None):
class TextInput (line 32) | class TextInput(InteractionNode):
method __init__ (line 35) | def __init__(self, name: str | None = None, label: str = "Input"):
class ImageInput (line 46) | class ImageInput(InteractionNode):
method __init__ (line 49) | def __init__(self, name: str | None = None, label: str = "Image"):
FILE: daggr/port.py
class Port (line 15) | class Port:
method __init__ (line 26) | def __init__(self, node: Node, name: str):
method __repr__ (line 30) | def __repr__(self):
method _as_source (line 33) | def _as_source(self) -> tuple[Node, str]:
method _as_target (line 36) | def _as_target(self) -> tuple[Node, str]:
method __getattr__ (line 39) | def __getattr__(self, attr: str) -> ScatteredPort:
method each (line 52) | def each(self) -> ScatteredPort:
method all (line 56) | def all(self) -> GatheredPort:
class ScatteredPort (line 61) | class ScatteredPort:
method __init__ (line 68) | def __init__(self, port: Port, item_key: str | None = None):
method node (line 73) | def node(self):
method name (line 77) | def name(self):
method __getitem__ (line 80) | def __getitem__(self, key: str) -> ScatteredPort:
method __repr__ (line 84) | def __repr__(self):
class GatheredPort (line 90) | class GatheredPort:
method __init__ (line 97) | def __init__(self, port: Port):
method node (line 101) | def node(self):
method name (line 105) | def name(self):
method __repr__ (line 108) | def __repr__(self):
function is_port (line 115) | def is_port(obj: Any) -> bool:
class PortNamespace (line 120) | class PortNamespace:
method __init__ (line 127) | def __init__(self, node: Node, port_names: list[str]):
method __getattr__ (line 131) | def __getattr__(self, name: str) -> Port:
method __dir__ (line 136) | def __dir__(self) -> list[str]:
method __repr__ (line 139) | def __repr__(self):
class ItemList (line 143) | class ItemList:
method __init__ (line 157) | def __init__(self, **schema):
FILE: daggr/server.py
function _find_available_port (line 54) | def _find_available_port(host: str, start_port: int) -> int:
function _get_theme (line 72) | def _get_theme(theme: "Theme | str | None") -> "Theme":
class DaggrServer (line 109) | class DaggrServer:
method __init__ (line 110) | def __init__(
method _extract_token_from_header (line 126) | def _extract_token_from_header(self, authorization: str | None) -> str...
method _validate_hf_token (line 131) | def _validate_hf_token(self, token: str) -> dict | None:
method _setup_routes (line 144) | def _setup_routes(self):
method _get_dev_html (line 619) | def _get_dev_html(self) -> str:
method _get_node_url (line 644) | def _get_node_url(self, node) -> str | None:
method _get_node_type (line 655) | def _get_node_type(self, node, node_name: str) -> str:
method _has_scattered_input (line 674) | def _has_scattered_input(self, node_name: str) -> bool:
method _get_scattered_edge (line 680) | def _get_scattered_edge(self, node_name: str):
method _is_output_node (line 686) | def _is_output_node(self, node_name: str) -> bool:
method _is_running_locally (line 689) | def _is_running_locally(self, node) -> bool:
method _build_variant_data (line 694) | def _build_variant_data(self, variant, input_values: dict) -> dict[str...
method _get_component_type (line 722) | def _get_component_type(self, component) -> str:
method _serialize_component (line 752) | def _serialize_component(self, comp, port_name: str) -> dict[str, Any]:
method _file_to_url (line 798) | def _file_to_url(self, value: Any) -> Any:
method _validate_file_value (line 808) | def _validate_file_value(self, value: Any, comp_type: str) -> str | None:
method _transform_file_paths (line 830) | def _transform_file_paths(self, data: Any) -> Any:
method _transform_persisted_results (line 839) | def _transform_persisted_results(
method _build_input_components (line 859) | def _build_input_components(self, node) -> list[dict[str, Any]]:
method _build_output_components (line 867) | def _build_output_components(
method _build_scattered_items (line 901) | def _build_scattered_items(
method _serialize_item_list_schema (line 955) | def _serialize_item_list_schema(
method _build_item_list_items (line 964) | def _build_item_list_items(
method _apply_item_list_edits (line 983) | def _apply_item_list_edits(
method _compute_node_depths (line 1005) | def _compute_node_depths(self) -> dict[str, int]:
method _get_hf_user_info (line 1029) | def _get_hf_user_info(self) -> dict | None:
method _build_graph_data (line 1046) | def _build_graph_data(
method _get_ancestors (line 1393) | def _get_ancestors(self, node_name: str) -> list[str]:
method _get_user_provided_output (line 1404) | def _get_user_provided_output(
method _save_data_url_as_gradio_file (line 1429) | def _save_data_url_as_gradio_file(self, data_url: str):
method _convert_urls_to_file_values (line 1454) | def _convert_urls_to_file_values(self, data: Any) -> Any:
method _execute_to_node (line 1479) | async def _execute_to_node(
method _execute_to_node_streaming (line 1576) | async def _execute_to_node_streaming(
method _execute_workflow_api (line 1786) | async def _execute_workflow_api(
method run (line 1883) | def run(
class _Server (line 1972) | class _Server(uvicorn.Server):
method install_signal_handlers (line 1973) | def install_signal_handlers(self):
method run_in_thread (line 1976) | def run_in_thread(self):
method close (line 1987) | def close(self):
FILE: daggr/session.py
class ConcurrencyManager (line 12) | class ConcurrencyManager:
method __init__ (line 20) | def __init__(self):
method get_semaphore (line 25) | async def get_semaphore(
class ExecutionSession (line 50) | class ExecutionSession:
method __init__ (line 61) | def __init__(self, graph: Graph, hf_token: str | None = None):
method set_hf_token (line 73) | def set_hf_token(self, token: str | None):
method clear_results (line 79) | def clear_results(self):
method wait_for_node (line 84) | async def wait_for_node(self, node_name: str) -> bool:
method start_node_execution (line 97) | async def start_node_execution(self, node_name: str) -> bool:
method finish_node_execution (line 109) | async def finish_node_execution(self, node_name: str):
FILE: daggr/state.py
function get_daggr_cache_dir (line 14) | def get_daggr_cache_dir() -> Path:
function get_daggr_files_dir (line 21) | def get_daggr_files_dir() -> Path:
class SessionState (line 27) | class SessionState:
method __init__ (line 28) | def __init__(self, db_path: str | None = None):
method _init_db (line 34) | def _init_db(self):
method _migrate_legacy_schema (line 105) | def _migrate_legacy_schema(self, cursor):
method get_effective_user_id (line 170) | def get_effective_user_id(self, hf_user: dict | None = None) -> str | ...
method create_sheet (line 178) | def create_sheet(
method get_sheet_count (line 199) | def get_sheet_count(self, user_id: str, graph_name: str) -> int:
method list_sheets (line 210) | def list_sheets(self, user_id: str, graph_name: str) -> list[dict[str,...
method get_sheet (line 232) | def get_sheet(self, sheet_id: str) -> dict[str, Any] | None:
method save_transform (line 260) | def save_transform(self, sheet_id: str, x: float, y: float, scale: flo...
method rename_sheet (line 274) | def rename_sheet(self, sheet_id: str, new_name: str) -> bool:
method delete_sheet (line 287) | def delete_sheet(self, sheet_id: str) -> bool:
method get_or_create_sheet (line 298) | def get_or_create_sheet(
method save_input (line 312) | def save_input(self, sheet_id: str, node_name: str, port_name: str, va...
method get_inputs (line 331) | def get_inputs(self, sheet_id: str) -> dict[str, dict[str, Any]]:
method save_result (line 347) | def save_result(
method get_latest_result (line 372) | def get_latest_result(self, sheet_id: str, node_name: str) -> Any | None:
method get_result_count (line 387) | def get_result_count(self, sheet_id: str, node_name: str) -> int:
method get_result_by_index (line 398) | def get_result_by_index(
method get_all_results (line 417) | def get_all_results(self, sheet_id: str) -> dict[str, list[Any]]:
method get_sheet_state (line 439) | def get_sheet_state(self, sheet_id: str) -> dict[str, Any]:
method clear_sheet_data (line 445) | def clear_sheet_data(self, sheet_id: str):
method create_session (line 453) | def create_session(self, graph_name: str) -> str:
method get_or_create_session (line 456) | def get_or_create_session(self, session_id: str | None, graph_name: st...
FILE: examples/03_mock_podcast_app.py
function generate_dialogue (line 50) | def generate_dialogue(topic: str) -> list:
function chatterbox (line 74) | def chatterbox(text: str, speaker: str, host_audio: str, guest_audio: st...
function combine_audio_files (line 93) | def combine_audio_files(audio_files: list[str]) -> str:
FILE: examples/04_complete_podcast_app.py
function generate_dialogue (line 50) | def generate_dialogue(topic: str) -> list:
function chatterbox (line 74) | def chatterbox(text: str, speaker: str, host_audio: str, guest_audio: st...
function combine_audio_files (line 93) | def combine_audio_files(audio_files: list[str]) -> str:
FILE: examples/06_pig_latin_voice_app.py
function pig_latin_sentence (line 17) | def pig_latin_sentence(text: str) -> str:
FILE: examples/07_image_to_3d_app.py
function downscale_image_to_file (line 12) | def downscale_image_to_file(image: Any, scale: float = 0.25) -> str | None:
FILE: examples/09_slideshow_app.py
function resize_image (line 11) | def resize_image(image_path: str, size: int = 256) -> str:
function concat_videos (line 256) | def concat_videos(v1: str, v2: str, v3: str, v4: str) -> str:
FILE: examples/10_real_podcast_app.py
function extract_content (line 7) | def extract_content(url: str, custom_text: str) -> tuple[str, str]:
function generate_dialogue (line 76) | def generate_dialogue(
function generate_all_voice_segments (line 217) | def generate_all_voice_segments(dialogue: list) -> list:
function combine_podcast (line 257) | def combine_podcast(
FILE: examples/11_viral_content_generator_app.py
function expand_content_idea (line 9) | def expand_content_idea(
function package_content (line 159) | def package_content(
FILE: examples/12_ecommerce_product_generator_app.py
function ensure_image_path (line 7) | def ensure_image_path(inputs, key="image"):
function ensure_image_dict (line 15) | def ensure_image_dict(inputs, key="f"):
function postprocess_flux (line 31) | def postprocess_flux(result, seed):
FILE: examples/13_accessible_image_description_app.py
function postprocess_flux (line 7) | def postprocess_flux(result, seed):
function preprocess_moondream (line 22) | def preprocess_moondream(inputs):
FILE: examples/14_food_nutrition_analyzer_app.py
function ensure_image_path (line 8) | def ensure_image_path(inputs, key="image"):
function create_nutrition_report (line 15) | def create_nutrition_report(food_items: str, nutrition_analysis: str) ->...
function extract_calorie_summary (line 100) | def extract_calorie_summary(nutrition_analysis: str) -> str:
FILE: tests/test_api.py
class TestWorkflowAPI (line 8) | class TestWorkflowAPI:
method test_simple_two_node_workflow_api (line 9) | def test_simple_two_node_workflow_api(self):
method test_multi_node_chain_workflow_api (line 51) | def test_multi_node_chain_workflow_api(self):
FILE: tests/test_basic.py
function test_basic (line 7) | def test_basic():
function test_edge_api_with_typed_ports (line 11) | def test_edge_api_with_typed_ports():
function test_port_validation (line 33) | def test_port_validation():
FILE: tests/test_cache.py
function test_cache_directories_respect_hf_home_env_var (line 15) | def test_cache_directories_respect_hf_home_env_var():
FILE: tests/test_executor.py
class TestSequentialExecutor (line 5) | class TestSequentialExecutor:
method test_execute_single_fn_node (line 6) | def test_execute_single_fn_node(self):
method test_execute_chain (line 16) | def test_execute_chain(self):
method test_execute_all (line 31) | def test_execute_all(self):
method test_fn_result_mapping_tuple (line 46) | def test_fn_result_mapping_tuple(self):
method test_user_input_override (line 59) | def test_user_input_override(self):
method test_callable_fixed_input (line 69) | def test_callable_fixed_input(self):
FILE: tests/test_nodes.py
class TestComponentTypeWarning (line 7) | class TestComponentTypeWarning:
method test_warns_when_type_explicitly_set (line 8) | def test_warns_when_type_explicitly_set(self):
method test_no_warning_when_type_not_set (line 18) | def test_no_warning_when_type_not_set(self):
class TestFnNode (line 32) | class TestFnNode:
method test_creates_from_function (line 33) | def test_creates_from_function(self):
method test_custom_name (line 43) | def test_custom_name(self):
method test_explicit_inputs (line 50) | def test_explicit_inputs(self):
method test_invalid_input_raises_error (line 59) | def test_invalid_input_raises_error(self):
method test_item_list_output (line 67) | def test_item_list_output(self):
class TestInteractionNode (line 77) | class TestInteractionNode:
method test_default_ports (line 78) | def test_default_ports(self):
method test_custom_interaction_type (line 83) | def test_custom_interaction_type(self):
class TestChoiceNodeName (line 88) | class TestChoiceNodeName:
method test_choice_node_uses_custom_name_in_graph (line 89) | def test_choice_node_uses_custom_name_in_graph(self):
class TestPort (line 106) | class TestPort:
method test_port_access (line 107) | def test_port_access(self):
method test_scattered_port (line 117) | def test_scattered_port(self):
method test_scattered_port_with_key (line 126) | def test_scattered_port_with_key(self):
class TestGraphConstruction (line 136) | class TestGraphConstruction:
method test_requires_name (line 137) | def test_requires_name(self):
method test_persist_key_derived_from_name (line 143) | def test_persist_key_derived_from_name(self):
method test_persist_key_disabled (line 147) | def test_persist_key_disabled(self):
method test_persist_key_custom (line 151) | def test_persist_key_custom(self):
method test_add_nodes_from_init (line 155) | def test_add_nodes_from_init(self):
method test_cycle_detection (line 168) | def test_cycle_detection(self):
method test_execution_order (line 182) | def test_execution_order(self):
method test_get_connections (line 202) | def test_get_connections(self):
FILE: tests/test_persistence.py
function state (line 10) | def state():
function test_create_sheet (line 18) | def test_create_sheet(state):
function test_list_sheets_by_user (line 29) | def test_list_sheets_by_user(state):
function test_rename_sheet (line 43) | def test_rename_sheet(state):
function test_save_and_load_inputs (line 53) | def test_save_and_load_inputs(state):
function test_save_and_load_results (line 67) | def test_save_and_load_results(state):
function test_user_isolation (line 83) | def test_user_isolation(state):
function test_local_user_fallback (line 103) | def test_local_user_fallback(state, monkeypatch):
function test_spaces_requires_login (line 113) | def test_spaces_requires_login(state, monkeypatch):
function test_delete_sheet (line 123) | def test_delete_sheet(state):
FILE: tests/test_server.py
function server (line 11) | def server():
function test_file_to_url_converts_windows_paths (line 16) | def test_file_to_url_converts_windows_paths(server):
function test_file_to_url_converts_real_file_paths (line 27) | def test_file_to_url_converts_real_file_paths(server, tmp_path):
FILE: tests/ui/conftest.py
function temp_db (line 10) | def temp_db():
function browser (line 21) | def browser() -> Generator[Browser, None, None]:
function page (line 29) | def page(
function pytest_addoption (line 48) | def pytest_addoption(parser: pytest.Parser):
FILE: tests/ui/helpers.py
function find_available_port (line 13) | def find_available_port() -> int:
class TestServer (line 19) | class TestServer(uvicorn.Server):
method install_signal_handlers (line 20) | def install_signal_handlers(self):
method run_in_thread (line 23) | def run_in_thread(self):
method close (line 32) | def close(self):
function launch_daggr_server (line 37) | def launch_daggr_server(
function wait_for_graph_load (line 55) | def wait_for_graph_load(page: Page, timeout: int = 15000):
FILE: tests/ui/test_basic.py
function test_nodes_and_edges_render (line 8) | def test_nodes_and_edges_render(page: Page, temp_db: str):
function test_run_workflow_produces_output (line 49) | def test_run_workflow_produces_output(page: Page, temp_db: str):
function test_input_node_accepts_value (line 87) | def test_input_node_accepts_value(page: Page, temp_db: str):
FILE: tests/ui/test_cancel.py
function test_cancel_running_node (line 10) | def test_cancel_running_node(page: Page, temp_db: str):
FILE: tests/ui/test_dependency_hash.py
function test_dependency_hash_auto_update_on_stale_cache (line 10) | def test_dependency_hash_auto_update_on_stale_cache(page: Page, temp_db:...
FILE: tests/ui/test_image_fix.py
function test_image_initial_value_and_none_input (line 16) | def test_image_initial_value_and_none_input(page: Page, temp_db: str):
FILE: tests/ui/test_images.py
function test_image_output_displays (line 12) | def test_image_output_displays(page: Page, temp_db: str):
function test_image_input_and_output (line 55) | def test_image_input_and_output(page: Page, temp_db: str):
function test_multiple_outputs_with_image (line 82) | def test_multiple_outputs_with_image(page: Page, temp_db: str):
FILE: tests/ui/test_run_mode.py
function test_run_mode_dropdown_and_single_step (line 10) | def test_run_mode_dropdown_and_single_step(page: Page, temp_db: str):
FILE: tests/ui/test_sheets.py
function test_sheets_ui_elements_present (line 8) | def test_sheets_ui_elements_present(page: Page, temp_db: str):
function test_create_new_sheet (line 36) | def test_create_new_sheet(page: Page, temp_db: str):
function test_switch_between_sheets (line 74) | def test_switch_between_sheets(page: Page, temp_db: str):
function test_result_persists_on_sheet (line 149) | def test_result_persists_on_sheet(page: Page, temp_db: str):
FILE: tests/ui/test_theme.py
function test_theme_support (line 8) | def test_theme_support(page: Page, temp_db: str):
Condensed preview — 113 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (735K chars).
[
{
"path": ".agents/skills/daggr/SKILL.md",
"chars": 7342,
"preview": "---\nname: daggr\ndescription: |\n Build DAG-based AI pipelines connecting Gradio Spaces, HuggingFace models, and Python f"
},
{
"path": ".changeset/README.md",
"chars": 510,
"preview": "# Changesets\n\nHello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that wo"
},
{
"path": ".changeset/changeset.cjs",
"chars": 8701,
"preview": "const { getPackagesSync } = require(\"@manypkg/get-packages\");\nconst dependents_graph = require(\"@changesets/get-dependen"
},
{
"path": ".changeset/config.json",
"chars": 269,
"preview": "{\n\t\"$schema\": \"https://unpkg.com/@changesets/config@2.3.0/schema.json\",\n\t\"changelog\": [\"./changeset.cjs\", { \"repo\": \"abi"
},
{
"path": ".changeset/fix_changelogs.cjs",
"chars": 3453,
"preview": "const { join } = require(\"path\");\nconst { readFileSync, existsSync, writeFileSync, unlinkSync } = require(\"fs\");\nconst {"
},
{
"path": ".github/pull_request_template.md",
"chars": 1027,
"preview": "Thank you for your contribution! All PRs should include the following sections. PRs missing these sections may be closed"
},
{
"path": ".github/workflows/comment-queue.yml",
"chars": 878,
"preview": "name: Comment on pull request without race conditions\n\non:\n workflow_call:\n inputs:\n pr_number:\n type: s"
},
{
"path": ".github/workflows/format.yml",
"chars": 581,
"preview": "name: Format\n\non:\n pull_request:\n branches: [ main ]\n push:\n branches: [ main ]\n\njobs:\n format:\n runs-on: ub"
},
{
"path": ".github/workflows/generate-changeset.yml",
"chars": 3869,
"preview": "name: Generate changeset\non:\n workflow_run:\n workflows: [\"trigger-changeset\"]\n types:\n - completed\n\nenv:\n C"
},
{
"path": ".github/workflows/publish.yml",
"chars": 1464,
"preview": "# safe runs from main\n\nname: publish\non:\n push:\n branches:\n - main\n\njobs:\n version_or_publish:\n runs-on: ub"
},
{
"path": ".github/workflows/test.yml",
"chars": 1460,
"preview": "name: Unit Tests\n\non:\n pull_request:\n branches: [ main ]\n push:\n branches: [ main ]\n\njobs:\n test:\n strategy:"
},
{
"path": ".github/workflows/trigger-changeset.yml",
"chars": 347,
"preview": "name: trigger-changeset\non:\n pull_request:\n types: [opened, synchronize, reopened, edited, labeled, unlabeled]\n b"
},
{
"path": ".gitignore",
"chars": 239,
"preview": "trackio/__pycache__/\ntests/__pycache__/\ntest-results/\n.trackio/\n.gradio/\ntrackio.db\n*.pyc\n*.pyi\n*.claude/\n.venv/\n**/.DS_"
},
{
"path": "CHANGELOG.md",
"chars": 5195,
"preview": "# daggr\n\n## 0.8.0\n\n### Features\n\n- [#79](https://github.com/gradio-app/daggr/pull/79) [`d32cec2`](https://github.com/gra"
},
{
"path": "CLAUDE.md",
"chars": 1808,
"preview": "# CLAUDE.md\n\n## Code Style\n\n- AVOID inline comments\n\n## Commands\n\n```bash\npip install -e .[dev] # Install dev depe"
},
{
"path": "CONTRIBUTING.md",
"chars": 1139,
"preview": "# Contributing\n\nThank you for your interest in contributing! This document provides guidelines and information for contr"
},
{
"path": "LICENSE",
"chars": 1066,
"preview": "MIT License\n\nCopyright (c) 2024 Your Name\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\n"
},
{
"path": "MANIFEST.in",
"chars": 257,
"preview": "include LICENSE\ninclude README.md\ninclude daggr/package.json\nrecursive-include daggr *.py *.pyi\nrecursive-include daggr/"
},
{
"path": "README.md",
"chars": 41286,
"preview": "<h3 align=\"center\">\n <div style=\"display:flex;flex-direction:row;\">\n <picture>\n <source media=\"(prefers-color-s"
},
{
"path": "RELEASE.md",
"chars": 4201,
"preview": "# Making a release\n\n> [!NOTE]\n> VERSION needs to be formatted following the `v{major}.{minor}.{patch}` convention. We ne"
},
{
"path": "build_pypi.sh",
"chars": 120,
"preview": "#!/bin/bash\nset -e\n\ncd \"$(dirname ${0})\"\n\npython3 -m pip install build\nrm -rf dist/*\nrm -rf build/*\npython3 -m build -w\n"
},
{
"path": "daggr/CHANGELOG.md",
"chars": 5195,
"preview": "# daggr\n\n## 0.8.0\n\n### Features\n\n- [#79](https://github.com/gradio-app/daggr/pull/79) [`d32cec2`](https://github.com/gra"
},
{
"path": "daggr/__init__.py",
"chars": 1194,
"preview": "\"\"\"daggr - Build visual, node-based AI pipelines with Gradio Spaces.\n\ndaggr lets you create DAG (directed acyclic graph)"
},
{
"path": "daggr/_client_cache.py",
"chars": 5316,
"preview": "from __future__ import annotations\n\nimport hashlib\nimport json\nimport os\nfrom pathlib import Path\nfrom typing import Any"
},
{
"path": "daggr/_utils.py",
"chars": 591,
"preview": "\"\"\"Internal utilities for daggr.\"\"\"\n\nfrom __future__ import annotations\n\nimport difflib\n\n\ndef suggest_similar(invalid: s"
},
{
"path": "daggr/cli.py",
"chars": 20722,
"preview": "from __future__ import annotations\n\nimport argparse\nimport ast\nimport importlib.util\nimport os\nimport re\nimport shutil\ni"
},
{
"path": "daggr/edge.py",
"chars": 1957,
"preview": "\"\"\"Edge module for connecting ports between nodes.\"\"\"\n\nfrom __future__ import annotations\n\nfrom typing import TYPE_CHECK"
},
{
"path": "daggr/executor.py",
"chars": 31671,
"preview": "\"\"\"Executor for daggr graphs.\n\nThis module provides the AsyncExecutor for running graph nodes with proper\nconcurrency co"
},
{
"path": "daggr/frontend/index.html",
"chars": 892,
"preview": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, in"
},
{
"path": "daggr/frontend/package.json",
"chars": 368,
"preview": "{\n \"name\": \"daggr-frontend\",\n \"private\": true,\n \"version\": \"0.0.1\",\n \"type\": \"module\",\n \"scripts\": {\n \"dev\": \"vi"
},
{
"path": "daggr/frontend/src/App.svelte",
"chars": 74133,
"preview": "<script lang=\"ts\">\n\timport { onMount } from 'svelte';\n\timport { EmbeddedComponent, MapItemsSection, ItemListSection } fr"
},
{
"path": "daggr/frontend/src/components/Audio.svelte",
"chars": 7716,
"preview": "<script lang=\"ts\">\n\timport AudioPlayer from './AudioPlayer.svelte';\n\n\tinterface Props {\n\t\tlabel: string;\n\t\tvalue: any;\n\t"
},
{
"path": "daggr/frontend/src/components/AudioPlayer.svelte",
"chars": 3759,
"preview": "<script lang=\"ts\">\n\tinterface Props {\n\t\tsrc: string;\n\t\tid: string;\n\t\tcompact?: boolean;\n\t}\n\n\tlet { src, id, compact = fa"
},
{
"path": "daggr/frontend/src/components/Button.svelte",
"chars": 1914,
"preview": "<script lang=\"ts\">\n\tinterface Props {\n\t\tvalue: string;\n\t\tvariant?: 'primary' | 'secondary' | 'stop';\n\t\tsize?: 'sm' | 'md"
},
{
"path": "daggr/frontend/src/components/Checkbox.svelte",
"chars": 2196,
"preview": "<script lang=\"ts\">\n\tinterface Props {\n\t\tlabel: string;\n\t\tvalue: boolean;\n\t\tdisabled?: boolean;\n\t\tonchange?: (value: bool"
},
{
"path": "daggr/frontend/src/components/CheckboxGroup.svelte",
"chars": 2968,
"preview": "<script lang=\"ts\">\n\tinterface Props {\n\t\tlabel: string;\n\t\tchoices: [string, string | number][];\n\t\tvalue: (string | number"
},
{
"path": "daggr/frontend/src/components/Code.svelte",
"chars": 6260,
"preview": "<script lang=\"ts\">\n\tinterface Props {\n\t\tlabel: string;\n\t\tvalue: string;\n\t\tlanguage?: string;\n\t\tlineNumbers?: boolean;\n\t\t"
},
{
"path": "daggr/frontend/src/components/ColorPicker.svelte",
"chars": 2447,
"preview": "<script lang=\"ts\">\n\tinterface Props {\n\t\tlabel: string;\n\t\tvalue: string;\n\t\tdisabled?: boolean;\n\t\tonchange?: (value: strin"
},
{
"path": "daggr/frontend/src/components/Dataframe.svelte",
"chars": 6860,
"preview": "<script lang=\"ts\">\n\tinterface DataframeValue {\n\t\theaders: string[];\n\t\tdata: (string | number | boolean | null)[][];\n\t}\n\n"
},
{
"path": "daggr/frontend/src/components/Dialogue.svelte",
"chars": 10130,
"preview": "<script lang=\"ts\">\n\tinterface DialogueLine {\n\t\tspeaker: string;\n\t\ttext: string;\n\t}\n\n\tinterface Props {\n\t\tlabel: string;\n"
},
{
"path": "daggr/frontend/src/components/Dropdown.svelte",
"chars": 5517,
"preview": "<script lang=\"ts\">\n\tinterface Props {\n\t\tlabel: string;\n\t\tchoices: [string, string | number][];\n\t\tvalue: string | number "
},
{
"path": "daggr/frontend/src/components/EmbeddedComponent.svelte",
"chars": 8256,
"preview": "<script lang=\"ts\">\n\timport Audio from './Audio.svelte';\n\timport Textbox from './Textbox.svelte';\n\timport Image from './I"
},
{
"path": "daggr/frontend/src/components/File.svelte",
"chars": 8338,
"preview": "<script lang=\"ts\">\n\tinterface FileValue {\n\t\tname: string;\n\t\tsize: number;\n\t\turl?: string;\n\t\tdata?: File | Blob;\n\t}\n\n\tint"
},
{
"path": "daggr/frontend/src/components/Gallery.svelte",
"chars": 7877,
"preview": "<script lang=\"ts\">\n\tinterface GalleryItem {\n\t\timage?: { url: string };\n\t\tvideo?: { url: string };\n\t\tcaption?: string | n"
},
{
"path": "daggr/frontend/src/components/HighlightedText.svelte",
"chars": 3288,
"preview": "<script lang=\"ts\">\n\tinterface TextSpan {\n\t\ttext: string;\n\t\tlabel?: string | null;\n\t}\n\n\tinterface Props {\n\t\tlabel: string"
},
{
"path": "daggr/frontend/src/components/Html.svelte",
"chars": 2357,
"preview": "<script lang=\"ts\">\n\tinterface Props {\n\t\tlabel?: string;\n\t\tvalue: string;\n\t\tshowLabel?: boolean;\n\t}\n\n\tlet { \n\t\tlabel = ''"
},
{
"path": "daggr/frontend/src/components/Image.svelte",
"chars": 7857,
"preview": "<script lang=\"ts\">\n\tinterface Props {\n\t\tlabel: string;\n\t\tvalue: any;\n\t\teditable?: boolean;\n\t\tonchange?: (value: any) => "
},
{
"path": "daggr/frontend/src/components/ImageSlider.svelte",
"chars": 7256,
"preview": "<script lang=\"ts\">\n\tinterface ImageValue {\n\t\turl: string;\n\t}\n\n\tinterface Props {\n\t\tlabel: string;\n\t\tvalue: [ImageValue |"
},
{
"path": "daggr/frontend/src/components/ItemListSection.svelte",
"chars": 4591,
"preview": "<script lang=\"ts\">\n\timport type { GradioComponentData, ItemListItem } from '../types';\n\n\tinterface Props {\n\t\tnodeId: str"
},
{
"path": "daggr/frontend/src/components/Json.svelte",
"chars": 8870,
"preview": "<script lang=\"ts\">\n\tinterface Props {\n\t\tlabel: string;\n\t\tvalue: any;\n\t\topen?: boolean | number;\n\t\tshowIndices?: boolean;"
},
{
"path": "daggr/frontend/src/components/Label.svelte",
"chars": 2910,
"preview": "<script lang=\"ts\">\n\tinterface LabelValue {\n\t\tlabel: string;\n\t\tconfidences?: { label: string; confidence: number }[];\n\t}\n"
},
{
"path": "daggr/frontend/src/components/MapItemsSection.svelte",
"chars": 3237,
"preview": "<script lang=\"ts\">\n\timport AudioPlayer from './AudioPlayer.svelte';\n\timport type { MapItem } from '../types';\n\n\tinterfac"
},
{
"path": "daggr/frontend/src/components/Markdown.svelte",
"chars": 5837,
"preview": "<script lang=\"ts\">\n\tinterface Props {\n\t\tlabel?: string;\n\t\tvalue: string;\n\t\tshowLabel?: boolean;\n\t}\n\n\tlet { \n\t\tlabel = ''"
},
{
"path": "daggr/frontend/src/components/Model3D.svelte",
"chars": 9789,
"preview": "<script lang=\"ts\">\n\timport { onMount } from \"svelte\";\n\n\tinterface Props {\n\t\tlabel: string;\n\t\tvalue: string | null;\n\t}\n\n\t"
},
{
"path": "daggr/frontend/src/components/Number.svelte",
"chars": 3699,
"preview": "<script lang=\"ts\">\n\tinterface Props {\n\t\tlabel: string;\n\t\tvalue: number | null;\n\t\tminimum?: number;\n\t\tmaximum?: number;\n\t"
},
{
"path": "daggr/frontend/src/components/Radio.svelte",
"chars": 2649,
"preview": "<script lang=\"ts\">\n\tinterface Props {\n\t\tlabel: string;\n\t\tchoices: [string, string | number][];\n\t\tvalue: string | number "
},
{
"path": "daggr/frontend/src/components/Slider.svelte",
"chars": 3663,
"preview": "<script lang=\"ts\">\n\tinterface Props {\n\t\tlabel: string;\n\t\tvalue: number;\n\t\tmin?: number;\n\t\tmax?: number;\n\t\tstep?: number;"
},
{
"path": "daggr/frontend/src/components/Textbox.svelte",
"chars": 1596,
"preview": "<script lang=\"ts\">\n\tinterface Props {\n\t\tlabel: string;\n\t\tplaceholder?: string;\n\t\tlines?: number;\n\t\tdisabled?: boolean;\n\t"
},
{
"path": "daggr/frontend/src/components/Video.svelte",
"chars": 7711,
"preview": "<script lang=\"ts\">\n\tinterface Props {\n\t\tlabel: string;\n\t\tvalue: any;\n\t\teditable?: boolean;\n\t\tautoplay?: boolean;\n\t\tloop?"
},
{
"path": "daggr/frontend/src/components/index.ts",
"chars": 1606,
"preview": "export { default as AudioPlayer } from './AudioPlayer.svelte';\nexport { default as Audio } from './Audio.svelte';\nexport"
},
{
"path": "daggr/frontend/src/main.ts",
"chars": 154,
"preview": "import App from './App.svelte'\nimport { mount } from 'svelte'\n\nconst app = mount(App, {\n target: document.getElementByI"
},
{
"path": "daggr/frontend/src/types.ts",
"chars": 1444,
"preview": "export interface Port {\n\tname: string;\n\thistory_count?: number;\n}\n\nexport interface GradioComponentData {\n\tcomponent: st"
},
{
"path": "daggr/frontend/svelte.config.js",
"chars": 114,
"preview": "import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'\n\nexport default {\n preprocess: vitePreprocess()\n}\n\n"
},
{
"path": "daggr/frontend/vite.config.ts",
"chars": 376,
"preview": "import { defineConfig } from 'vite'\nimport { svelte } from '@sveltejs/vite-plugin-svelte'\n\nexport default defineConfig({"
},
{
"path": "daggr/graph.py",
"chars": 27331,
"preview": "\"\"\"Graph module for daggr.\n\nA Graph represents a directed acyclic graph (DAG) of nodes that can be\nexecuted to process d"
},
{
"path": "daggr/local_space.py",
"chars": 17059,
"preview": "from __future__ import annotations\n\nimport atexit\nimport hashlib\nimport json\nimport os\nimport re\nimport select\nimport sh"
},
{
"path": "daggr/node.py",
"chars": 29570,
"preview": "\"\"\"Node types for daggr graphs.\n\nThis module defines the various node types that can be used in a daggr graph:\n- Node: A"
},
{
"path": "daggr/ops.py",
"chars": 1675,
"preview": "from __future__ import annotations\n\nfrom daggr.node import InteractionNode\n\n\nclass ChooseOne(InteractionNode):\n _inst"
},
{
"path": "daggr/package.json",
"chars": 81,
"preview": "{\n\t\"name\": \"daggr\",\n\t\"version\": \"0.8.0\",\n\t\"description\": \"\",\n\t\"python\": \"true\"\n}\n"
},
{
"path": "daggr/port.py",
"chars": 4381,
"preview": "\"\"\"Port module for node input/output definitions.\n\nPorts are named connection points on nodes. Output ports can be conne"
},
{
"path": "daggr/py.typed",
"chars": 0,
"preview": ""
},
{
"path": "daggr/server.py",
"chars": 79048,
"preview": "from __future__ import annotations\n\nimport asyncio\nimport base64\nimport json\nimport mimetypes\nimport os\nimport secrets\ni"
},
{
"path": "daggr/session.py",
"chars": 3865,
"preview": "\"\"\"Session management for daggr, including per-session execution contexts for security isolation and concurrency managem"
},
{
"path": "daggr/state.py",
"chars": 16508,
"preview": "from __future__ import annotations\n\nimport json\nimport os\nimport sqlite3\nimport uuid\nfrom datetime import datetime\nfrom "
},
{
"path": "examples/01_quickstart.py",
"chars": 1238,
"preview": "# Showcases basic GradioNode chaining: generate an image then remove its background.\nimport random\n\nimport gradio as gr\n"
},
{
"path": "examples/02_voice_design_comparator_app.py",
"chars": 1279,
"preview": "# Showcases parallel execution by comparing two TTS services (Qwen and Maya) with the same input.\nimport gradio as gr\n\nf"
},
{
"path": "examples/03_mock_podcast_app.py",
"chars": 3862,
"preview": "# Showcases scatter/gather with ItemList: generate dialogue items and process each with TTS, then combine.\nimport ssl\nim"
},
{
"path": "examples/04_complete_podcast_app.py",
"chars": 3925,
"preview": "# Showcases a complete podcast generator using real TTS (Qwen3-TTS) with scatter/gather for multi-speaker audio.\nimport "
},
{
"path": "examples/05_local_translation_app.py",
"chars": 593,
"preview": "# Showcases running a GradioNode locally with run_locally=True instead of calling a remote Space.\nimport gradio as gr\n\nf"
},
{
"path": "examples/06_pig_latin_voice_app.py",
"chars": 1022,
"preview": "# Showcases InferenceNode for speech-to-text and text-to-speech with an FnNode transformation in between.\nimport gradio "
},
{
"path": "examples/07_image_to_3d_app.py",
"chars": 2281,
"preview": "# Showcases a multi-step image-to-3D pipeline: background removal → downscaling → FLUX enhancement → TRELLIS 3D.\nimport "
},
{
"path": "examples/08_text_to_3d_app.py",
"chars": 1850,
"preview": "# Showcases a text-to-3D pipeline: FLUX image generation → background removal → TRELLIS mesh extraction.\nimport gradio a"
},
{
"path": "examples/09_slideshow_app.py",
"chars": 8035,
"preview": "# Showcases parallel image generation, video transitions between scenes, and ffmpeg concatenation into a slideshow.\nimpo"
},
{
"path": "examples/10_real_podcast_app.py",
"chars": 11170,
"preview": "# Showcases a full document-to-podcast pipeline: URL extraction → dialogue generation → TTS → audio combining.\nimport gr"
},
{
"path": "examples/11_viral_content_generator_app.py",
"chars": 9834,
"preview": "# Showcases a social media content pipeline: idea expansion → parallel image/video generation → content packaging.\nimpor"
},
{
"path": "examples/12_ecommerce_product_generator_app.py",
"chars": 6924,
"preview": "# Showcases e-commerce product content automation: image generation → parallel processing (background removal, enhanceme"
},
{
"path": "examples/13_accessible_image_description_app.py",
"chars": 2796,
"preview": "# Showcases accessible content creation: generate an image, describe it with a vision model, then convert to speech for "
},
{
"path": "examples/14_food_nutrition_analyzer_app.py",
"chars": 6126,
"preview": "import random\n\nimport gradio as gr\n\nfrom daggr import FnNode, GradioNode, Graph\n\n\ndef ensure_image_path(inputs, key=\"ima"
},
{
"path": "examples/15_background_removal_with_input_node.py",
"chars": 1416,
"preview": "# Showcases basic GradioNode chaining: generate an image then remove its background.\nimport random\n\nimport gradio as gr\n"
},
{
"path": "package.json",
"chars": 465,
"preview": "{\n\t\"name\": \"daggr\",\n\t\"version\": \"0.1.0\",\n\t\"description\": \"\",\n\t\"private\": true,\n\t\"scripts\": {\n\t\t\"ci:version\": \"pnpm chang"
},
{
"path": "pnpm-workspace.yaml",
"chars": 22,
"preview": "packages:\n - \"daggr\"\n"
},
{
"path": "pyproject.toml",
"chars": 2640,
"preview": "[build-system]\nrequires = [\"hatchling\"]\nbuild-backend = \"hatchling.build\"\n\n[project]\nname = \"daggr\"\ndescription = \"A Pyt"
},
{
"path": "tests/README.md",
"chars": 182,
"preview": "# Tests\n\nThis directory contains Python unit tests which can be run by running `pytest` in the root directory.\n\nAdd your"
},
{
"path": "tests/conftest.py",
"chars": 0,
"preview": ""
},
{
"path": "tests/test_api.py",
"chars": 3477,
"preview": "import gradio as gr\nfrom fastapi.testclient import TestClient\n\nfrom daggr import FnNode, Graph\nfrom daggr.server import "
},
{
"path": "tests/test_basic.py",
"chars": 1383,
"preview": "import pytest\n\nimport daggr\nfrom daggr import FnNode, Graph\n\n\ndef test_basic():\n assert daggr.__version__\n\n\ndef test_"
},
{
"path": "tests/test_cache.py",
"chars": 861,
"preview": "\"\"\"Tests for cache directory resolution.\"\"\"\n\nimport importlib\nimport os\nimport tempfile\nfrom pathlib import Path\nfrom un"
},
{
"path": "tests/test_executor.py",
"chars": 2715,
"preview": "from daggr import FnNode, Graph\nfrom daggr.executor import SequentialExecutor\n\n\nclass TestSequentialExecutor:\n def te"
},
{
"path": "tests/test_nodes.py",
"chars": 6389,
"preview": "import pytest\n\nfrom daggr import FnNode, Graph, InteractionNode\nfrom daggr.port import ItemList, Port, ScatteredPort\n\n\nc"
},
{
"path": "tests/test_persistence.py",
"chars": 4105,
"preview": "import os\nimport tempfile\n\nimport pytest\n\nfrom daggr.state import SessionState\n\n\n@pytest.fixture\ndef state():\n with t"
},
{
"path": "tests/test_server.py",
"chars": 998,
"preview": "from pathlib import Path\nfrom unittest.mock import patch\n\nimport pytest\n\nfrom daggr import Graph\nfrom daggr.server impor"
},
{
"path": "tests/ui/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "tests/ui/conftest.py",
"chars": 1284,
"preview": "import os\nimport tempfile\nfrom typing import Generator\n\nimport pytest\nfrom playwright.sync_api import Browser, Page, syn"
},
{
"path": "tests/ui/helpers.py",
"chars": 1681,
"preview": "import os\nimport socket\nimport threading\nimport time\n\nimport uvicorn\nfrom playwright.sync_api import Page\n\nfrom daggr im"
},
{
"path": "tests/ui/test_basic.py",
"chars": 3655,
"preview": "import gradio as gr\nfrom playwright.sync_api import Page, expect\n\nfrom daggr import FnNode, Graph\nfrom tests.ui.helpers "
},
{
"path": "tests/ui/test_cancel.py",
"chars": 2293,
"preview": "import time\n\nimport gradio as gr\nfrom playwright.sync_api import Page, expect\n\nfrom daggr import FnNode, Graph\nfrom test"
},
{
"path": "tests/ui/test_dependency_hash.py",
"chars": 1642,
"preview": "import os\n\nimport gradio as gr\nfrom playwright.sync_api import Page, expect\n\nfrom daggr import GradioNode, Graph, _clien"
},
{
"path": "tests/ui/test_image_fix.py",
"chars": 2153,
"preview": "import tempfile\nfrom pathlib import Path\n\nimport gradio as gr\nfrom PIL import Image\nfrom playwright.sync_api import Page"
},
{
"path": "tests/ui/test_images.py",
"chars": 4157,
"preview": "import tempfile\nfrom pathlib import Path\n\nimport gradio as gr\nfrom PIL import Image\nfrom playwright.sync_api import Page"
},
{
"path": "tests/ui/test_run_mode.py",
"chars": 3430,
"preview": "import re\n\nimport gradio as gr\nfrom playwright.sync_api import Page, expect\n\nfrom daggr import FnNode, Graph\nfrom tests."
},
{
"path": "tests/ui/test_sheets.py",
"chars": 5899,
"preview": "import gradio as gr\nfrom playwright.sync_api import Page, expect\n\nfrom daggr import FnNode, Graph\nfrom tests.ui.helpers "
},
{
"path": "tests/ui/test_theme.py",
"chars": 1403,
"preview": "import gradio as gr\nfrom playwright.sync_api import Page\n\nfrom daggr import FnNode, Graph\nfrom tests.ui.helpers import l"
}
]
// ... and 1 more files (download for full content)
About this extraction
This page contains the full source code of the gradio-app/daggr GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 113 files (657.4 KB), approximately 175.8k tokens, and a symbol index with 402 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.