From f99495d2e65b06260a05f6f30775c66fc5fbfef8 Mon Sep 17 00:00:00 2001 From: Surajit Dasgupta Date: Thu, 22 Jan 2026 20:00:58 +0530 Subject: [PATCH 1/2] feat(studio): studio UI refresh --- README.md | 21 +- apps/create_from_template.py | 1836 ----------------- apps/create_new_task.py | 1300 ------------ apps/models.py | 252 --- apps/sygra_app.py | 22 - apps/tasks.py | 448 ---- apps/utils.py | 146 -- docs/getting_started/create_task_ui.md | 711 ++++++- mkdocs.yml | 2 +- pyproject.toml | 8 - run_ui.sh | 7 - .../components/builder/WorkflowBuilder.svelte | 112 +- .../lib/components/common/ConfirmModal.svelte | 32 +- .../common/ConfirmationModal.svelte | 30 +- .../lib/components/common/CustomSelect.svelte | 38 +- .../components/common/SelectionCard.svelte | 14 +- .../src/lib/components/data/SourceCard.svelte | 58 +- .../lib/components/data/SourceEditor.svelte | 12 +- .../components/data/TransformSelector.svelte | 2 +- .../lib/components/editor/MonacoEditor.svelte | 6 +- .../execution/ExecutionPanel.svelte | 24 +- .../execution/RunDetailsModal.svelte | 64 +- .../components/graph/NodeDetailsPanel.svelte | 870 ++++---- .../graph/renderers/nodes/AgentNode.svelte | 10 +- .../graph/renderers/nodes/DataNode.svelte | 16 +- .../graph/renderers/nodes/LLMNode.svelte | 8 +- .../graph/renderers/nodes/MultiLLMNode.svelte | 10 +- .../graph/renderers/nodes/NodeWrapper.svelte | 37 +- .../graph/renderers/nodes/OutputNode.svelte | 22 +- .../graph/renderers/nodes/SubgraphNode.svelte | 75 +- .../nodes/WeightedSamplerNode.svelte | 12 +- .../src/lib/components/home/HomeView.svelte | 551 +++-- .../lib/components/library/LibraryView.svelte | 311 +-- .../components/library/RecipeLibrary.svelte | 166 +- .../library/RecipePreviewModal.svelte | 34 +- .../lib/components/library/ToolLibrary.svelte | 212 +- .../lib/components/models/ModelsView.svelte | 402 ++-- .../src/lib/components/runs/LogViewer.svelte | 12 +- .../src/lib/components/runs/RunCard.svelte | 176 +- .../lib/components/runs/RunDetailsView.svelte | 290 +-- .../runs/RunDetailsViewEnhanced.svelte | 262 +-- .../components/runs/RunExecutionGraph.svelte | 68 +- .../lib/components/runs/RunQuickStats.svelte | 74 +- .../lib/components/runs/RunsListView.svelte | 208 +- .../runs/RunsListViewEnhanced.svelte | 283 +-- .../components/settings/SettingsModal.svelte | 160 +- .../src/lib/components/sidebar/Sidebar.svelte | 303 +-- .../workflows/WorkflowsListView.svelte | 268 +-- .../frontend/src/lib/stores/recipe.svelte.ts | 12 +- studio/frontend/src/lib/stores/tool.svelte.ts | 10 +- studio/frontend/src/routes/+layout.svelte | 6 +- studio/frontend/src/styles/global.css | 973 +++++++-- studio/frontend/tailwind.config.js | 297 ++- uv.lock | 304 +-- 54 files changed, 4574 insertions(+), 7013 deletions(-) delete mode 100644 apps/create_from_template.py delete mode 100644 apps/create_new_task.py delete mode 100644 apps/models.py delete mode 100644 apps/sygra_app.py delete mode 100644 apps/tasks.py delete mode 100644 apps/utils.py delete mode 100755 run_ui.sh diff --git a/README.md b/README.md index 5a1df107..388dcbd4 100644 --- a/README.md +++ b/README.md @@ -109,12 +109,31 @@ workflow.run(num_records=1) --- +## SyGra Studio + +**SyGra Studio** is a visual workflow builder that replaces manual YAML editing with an interactive drag-and-drop interface: + +- **Visual Graph Builder** — Drag-and-drop nodes, connect them visually, configure with forms +- **Real-time Execution** — Watch your workflow run with live node status and streaming logs +- **Rich Analytics** — Track usage, tokens, latency, and success rates across runs +- **Multi-LLM Support** — Azure OpenAI, OpenAI, Ollama, vLLM, Mistral, and more + +```bash +# One command to start +make studio +# Then open http://localhost:8000 +``` + +> **[Read the full Studio documentation →](https://servicenow.github.io/SyGra/getting_started/create_task_ui/)** + +--- + ## Task Components SyGra supports extendability and ease of implementation—most tasks are defined as graph configuration YAML files. Each task consists of two major components: a graph configuration and Python code to define conditions and processors. YAML contains various parts: -- **Data configuration** : Configure file or huggingface or ServiceNow instance as source and sink for the task. +- **Data configuration** : Configure file or huggingface or ServiceNow instance as source and sink for the task. - **Data transformation** : Configuration to transform the data into the format it can be used in the graph. - **Node configuration** : Configure nodes and corresponding properties, preprocessor and post processor. - **Edge configuration** : Connect the nodes configured above with or without conditions. diff --git a/apps/create_from_template.py b/apps/create_from_template.py deleted file mode 100644 index 80197a65..00000000 --- a/apps/create_from_template.py +++ /dev/null @@ -1,1836 +0,0 @@ -import yaml -from pathlib import Path -from datasets import load_dataset, get_dataset_config_names - -try: - import streamlit as st - from streamlit_flow import streamlit_flow - from streamlit_flow.elements import StreamlitFlowNode, StreamlitFlowEdge - from streamlit_flow.state import StreamlitFlowState - from streamlit_flow.layouts import ( - TreeLayout, - RadialLayout, - LayeredLayout, - ForceLayout, - StressLayout, - RandomLayout, - ) -except ModuleNotFoundError: - raise ModuleNotFoundError( - "SyGra UI requires the optional 'ui' dependencies. " - "Install them with: pip install 'sygra[ui]'" - ) - -import os -import time -import json -import csv -import re -import pandas as pd - -st.set_page_config(page_title="SyGra UI", layout="wide") -TASKS_DIR = Path("tasks") - -TASKS_DIR.mkdir(exist_ok=True) -if "selected_task1" not in st.session_state: - st.session_state["selected_task1"] = None -if "selected_subtask1" not in st.session_state: - st.session_state["selected_subtask1"] = None -if "draft_data_config1" not in st.session_state: - st.session_state["draft_data_config1"] = {} -if "draft_output_config1" not in st.session_state: - st.session_state["draft_output_config1"] = {} -if "draft_graph_config1" not in st.session_state: - st.session_state["draft_graph_config1"] = {} -if "current_task1" not in st.session_state: - st.session_state["current_task1"] = "" -if "nodes1" not in st.session_state: - st.session_state.nodes1 = [] -if "edges1" not in st.session_state: - st.session_state.edges1 = [] -if "parsed_edges1" not in st.session_state: - st.session_state.parsed_edges1 = [] -if "static_flow_state2" not in st.session_state: - st.session_state["static_flow_state2"] = None -if "draft_yaml1" not in st.session_state: - st.session_state.draft_yaml1 = None -if "draft_executor1" not in st.session_state: - st.session_state.draft_executor1 = "" -if "load_yaml" not in st.session_state: - st.session_state["load_yaml"] = None -if "active_models" not in st.session_state: - st.session_state["active_models"] = [] - - -def process_graph_config(draft_graph_config): - nodes = [] - edges = [] - - # 1. Process Nodes - for idx, (node_name, node_info) in enumerate(draft_graph_config["nodes"].items()): - node_type = node_info.get("node_type", "unknown") - node_id = f"{node_type}_{idx + 1}" # index+1 - - node_data = {} - for k, v in node_info.items(): - if k in ["pre_process", "post_process"] and isinstance(v, str): - # Only take the function/class name - node_data[k] = v.split(".")[-1] - elif k != "node_type": - node_data[k] = v - node_data["label"] = node_name # Save node name as label - - node = { - "id": node_id, - "type": node_type, - "data": node_data, - } - nodes.append(node) - - # Helper to map node names to (label(id)) - node_name_to_label_id = { - node["data"]["label"]: f"{node['data']['label']}({node['id']})" - for node in nodes - } - - # 2. Process Edges - draft_edges = draft_graph_config.get("edges", []) - - for edge in draft_edges: - src = edge["from"] - tgt = edge.get("to") - - # Skip the first edge from START - if src == "START": - continue - - # Skip self-looping end edge (from node to itself) - if src == tgt: - continue - - edge_info = { - "source": node_name_to_label_id.get(src, src), - "target": node_name_to_label_id.get(tgt, tgt), - } - - if "condition" in edge: - # Only keep the function name, not full path - condition_func_name = edge["condition"].split(".")[-1] - - edge_info["is_conditional"] = True - edge_info["condition"] = condition_func_name - # Update path_map with formatted node IDs - raw_path_map = edge.get("path_map", {}) - formatted_path_map = { - cond: node_name_to_label_id.get(node_name, node_name) - for cond, node_name in raw_path_map.items() - } - edge_info["path_map"] = formatted_path_map - - edges.append(edge_info) - - return nodes, edges - - -def read_yaml_file(yaml_path): - with open(yaml_path, "r") as f: - config = yaml.safe_load(f) - if config is not None: - graph_config = config.get("graph_config", {}) - st.session_state.draft_graph_config1 = graph_config - - nodes, edges = process_graph_config(st.session_state.draft_graph_config1) - st.session_state.nodes1 = nodes - st.session_state.edges1 = edges - - data_config = config.get("data_config", {}) - st.session_state.draft_data_config1 = data_config - - output_config = config.get("output_config", {}) - st.session_state.draft_output_config1 = output_config - - st.session_state.load_yaml = True - - -def get_first_record(file_path, file_format, encoding): - try: - if file_format == "json": - with open(file_path, "r", encoding=encoding) as f: - data = json.load(f) - return data[0] if isinstance(data, list) else data - - elif file_format == "jsonl": - with open(file_path, "r", encoding=encoding) as f: - first_line = f.readline() - return json.loads(first_line) - - elif file_format == "csv": - with open(file_path, "r", encoding=encoding) as f: - reader = csv.DictReader(f) - return next(reader) - - elif file_format == "parquet": - df = pd.read_parquet(file_path) - return df.iloc[0].to_dict() - - else: - return {"error": f"Unsupported format: {file_format}"} - - except Exception as e: - return {"error": str(e)} - - -def setup_task(): - st.subheader("Task Setup") - task_name = st.text_input("Enter Task Name") - if st.button("Create"): - if task_name: - st.session_state["current_task1"] = task_name - task_folder = TASKS_DIR / task_name - if not task_folder.exists(): - task_folder.mkdir(parents=True) - (task_folder / "graph_config.yaml").write_text("# Graph config\n") - (task_folder / "task_executor.py").write_text("# Task executor\n") - st.success(f"Created folder: {task_folder}") - else: - st.warning("Task already exists.") - else: - st.warning("Please enter a task name.") - - -@st.fragment -def load_hf_dataset(): - source = st.session_state.draft_data_config1.get("source") or {} - - # Properly convert list -> comma-separated string for prefill - prefill_split = ( - ", ".join(source.get("split", [])) - if isinstance(source.get("split", []), list) - else source.get("split", "") - ) - - repo_id = st.text_input( - "repo_id", - placeholder="e.g., username/dataset-name", - value=source.get("repo_id", ""), - ) - config_name = st.text_input( - "config_name", placeholder="e.g., default", value=source.get("config_name", "") - ) - split_input = st.text_input( - "split (comma-separated)", - placeholder="e.g., train, test, validation", - value=prefill_split, - ) - token = st.text_input( - "token (optional)", placeholder="hf_token", value=source.get("token", "") - ) - streaming = st.checkbox( - "Streaming? (optional)", value=source.get("streaming", False) - ) - shard = st.text_input("shard (optional)", value=source.get("shard", "")) - - # After taking user input, split properly - split_list = [s.strip() for s in split_input.split(",") if s.strip()] - - load = st.button("Save & Load Dataset") - if load: - progress_bar = st.progress(0) - status_text = st.empty() - - for percent in range(0, 70, 10): - progress_bar.progress(percent) - status_text.text(f"Loading dataset... {percent}%") - time.sleep(0.1) - - try: - dataset = load_dataset( - path=repo_id, - name=config_name if config_name != "default" else None, - split=split_list[0] if split_list else None, - token=token or None, - streaming=streaming, - ) - - for percent in range(70, 101, 10): - progress_bar.progress(percent) - status_text.text(f"Finishing up... {percent}%") - time.sleep(0.05) - - st.success("Dataset loaded!") - st.session_state["draft_data_config1"].update( - { - "source": { - "type": "hf", - "repo_id": repo_id, - "config_name": config_name, - "split": split_list, - "token": token, - "streaming": streaming, - "shard": shard, - } - } - ) - st.json(dataset[0]) - - except Exception as e: - progress_bar.empty() - status_text.empty() - st.error(f"Error loading dataset: {e}") - - -@st.fragment -def load_disk_dataset(): - with st.form("disk_data"): - disk_data = { - "type": "disk", - "file_path": st.text_input( - "File Path", - value=(st.session_state.draft_data_config1.get("source") or {}).get( - "file_path", "" - ), - ), - "file_format": st.text_input( - "File Format (e.g., csv, json)", - value=(st.session_state.draft_data_config1.get("source") or {}).get( - "file_format", "" - ), - ), - "encoding": st.text_input( - "Encoding", - value=(st.session_state.draft_data_config1.get("source") or {}).get( - "encoding", "" - ), - ), - } - load = st.form_submit_button("Save & Load Dataset") - if load: - progress_bar = st.progress(0) - status_text = st.empty() - - for percent in range(0, 70, 10): - progress_bar.progress(percent) - status_text.text(f"Loading dataset... {percent}%") - time.sleep(0.1) - try: - first_record = get_first_record( - disk_data["file_path"], - disk_data["file_format"], - disk_data["encoding"], - ) - for percent in range(70, 101, 10): - progress_bar.progress(percent) - status_text.text(f"Finishing up... {percent}%") - time.sleep(0.05) - st.success("Dataset loaded!") - st.session_state["draft_data_config1"].update( - { - "source": { - "type": "disk", - "file_path": disk_data["file_path"], - "file_format": disk_data["file_format"], - "encoding": disk_data["encoding"], - } - } - ) - st.json(first_record) - except Exception as e: - progress_bar.empty() - status_text.empty() - st.error(f"Error loading dataset: {e}") - - -def dataset_source(): - with st.container(border=True): - data_config = st.checkbox("Data Source?", value=True) - if data_config: - st.subheader("📂 Data Source") - - # Safely get the current source type - source_type_existing = ( - st.session_state.get("draft_data_config1", {}) - .get("source", {}) - .get("type", "") - ) - - # Set index based on existing source_type - source_options = ["hf", "disk"] - if source_type_existing in source_options: - index = source_options.index(source_type_existing) - else: - index = 0 # Default to "hf" - - source_type = st.selectbox( - "Select Source Type", source_options, index=index - ) - - if source_type == "hf": - load_hf_dataset() - else: - load_disk_dataset() - - -@st.fragment -def transformation_form(): - with st.container(border=True): - transformation = st.checkbox("Transformation?", value=True) - if transformation: - # Check if existing transformations are present - existing_transformations = ( - st.session_state.get("draft_data_config1", {}) - .get("source", {}) - .get("transformations", []) - ) - existing_count = len(existing_transformations) - - # If transformations exist, prefill the number - transformations_count = st.number_input( - "No. of Transformations", - min_value=1, - step=1, - value=existing_count if existing_count > 0 else 1, - ) - - transformations = [] - - for i in range(int(transformations_count)): - # Prefill transform_path and params if available - if i < existing_count: - existing = existing_transformations[i] - transform_path_prefill = existing["transform"].split(".")[ - -1 - ] # Take only the class name - params_prefill = json.dumps(existing.get("params", {}), indent=2) - else: - transform_path_prefill = "" - params_prefill = "{}" - - transform_path = st.text_input( - f"#{i + 1} Transform class (e.g., ClassName)", - value=transform_path_prefill, - key=f"transform_path_{i}", - ) - - params = st.text_area( - f"#{i + 1} Transform parameters (Dict)", - value=params_prefill, - key=f"params_{i}", - ) - - if transform_path and params: - try: - params_dict = json.loads(params) - transformations.append( - { - "transform": f"processors.data_transform.{transform_path}", - "params": params_dict, - } - ) - except json.JSONDecodeError: - st.error( - f"Invalid JSON in parameters for transformation #{i + 1}" - ) - - transform_submit = st.button("Save Mappings") - if transform_submit: - st.session_state["draft_data_config1"]["source"].update( - {"transformations": transformations} - ) - st.success("Transformations saved") - - -@st.fragment -def sink_hf_form(sink_type): - with st.form("sink_hf_form"): - # Prefill if available - sink_data = st.session_state.get("draft_data_config1", {}).get("sink", {}) - - repo_id = st.text_input( - "repo_id", - value=sink_data.get("repo_id", ""), - placeholder="e.g., username/dataset-name", - ) - config_name = st.text_input( - "config_name", - placeholder="e.g., config-name", - value=sink_data.get("config_name", ""), - ) - split = st.text_input( - "split", - value=sink_data.get("split", ""), - placeholder="e.g., train, test, validation", - ) - private = st.checkbox("private", value=sink_data.get("private", False)) - hf_token = st.text_input( - "hf_token (optional)", - placeholder="hf-token", - value=sink_data.get("token", ""), - ) - - if st.form_submit_button("Save HF Sink"): - st.session_state["draft_data_config1"].update( - { - "sink": { - "type": sink_type, - "repo_id": repo_id, - "config_name": config_name, - "split": split, - "token": hf_token, - "private": private, - } - } - ) - st.success( - f"HF Sink Configured with repo_id: {repo_id}, split: {split}, private: {private}" - ) - - -@st.fragment -def sink_disk_form(sink_type): - with st.form("sink_disk_form"): - # Prefill if available - sink_data = st.session_state.get("draft_data_config1", {}).get("sink", {}) - - file_path = st.text_input( - "file_path", - value=sink_data.get("file_path", ""), - placeholder="e.g., /path/to/output/file.json", - ) - encoding = st.text_input("encoding", value=sink_data.get("encoding", "utf-8")) - - if st.form_submit_button("Save Disk Sink"): - st.session_state["draft_data_config1"].update( - { - "sink": { - "type": sink_type, - "file_path": file_path, - "encoding": encoding, - } - } - ) - st.success( - f"Disk Sink Configured with file_path: {file_path}, encoding: {encoding}" - ) - - -def dataset_sink(): - with st.container(border=True): - dataset_sink = st.checkbox("Dataset Sink?", value=True) - if dataset_sink: - st.subheader("📂 Data Sink (Optional)") - - # Safely get existing sink type - sink_data = st.session_state.get("draft_data_config1", {}).get("sink", {}) - sink_type_existing = sink_data.get("type", "") - - sink_options = ["hf", "json", "jsonl", "csv", "parquet"] - if sink_type_existing in sink_options: - index = sink_options.index(sink_type_existing) - else: - index = 0 # Default to "hf" - - sink_type = st.selectbox("Select sink Type", sink_options, index=index) - - if sink_type == "hf": - sink_hf_form(sink_type) - else: - sink_disk_form(sink_type) - - -@st.fragment -def output_config(): - with st.container(border=True): - output_config_enabled = st.checkbox("Output Config?", value=True) - - if output_config_enabled: - st.subheader("Output Config") - - # Safely get existing data - draft = st.session_state.get("draft_output_config1", {}) - generator_existing = draft.get("generator", "") - output_map_existing = draft.get("output_map", {}) - oasst_mapper_existing = draft.get("oasst_mapper", {}) - - # Prefill Generator Function - generator_display = ( - draft.get("generator", "").split(".")[-1] - if draft.get("generator") - else "" - ) - generator_function = st.text_input( - "Generator Function", value=generator_display - ) - - # Prefill Output Map - st.markdown("### Output Map") - existing_fields = list(output_map_existing.keys()) - output_mapping_count = st.number_input( - "Add field renames", - min_value=1, - step=1, - value=len(existing_fields) if existing_fields else 1, - key="mapping_count", - ) - - mappings = {} - for i in range(int(output_mapping_count)): - field_default = existing_fields[i] if i < len(existing_fields) else "" - field_data = ( - output_map_existing.get(field_default, {}) if field_default else {} - ) - - col1, col2 = st.columns([1, 3]) - with col1: - field_name = st.text_input( - f"Field Name #{i + 1}", - value=field_default, - key=f"field_name_{i}", - ) - with col2: - from_field = st.text_input( - f"From #{i + 1}", - value=field_data.get("from", ""), - key=f"from_{i}", - ) - transform = st.text_input( - f"Transform #{i + 1}", - value=field_data.get("transform", ""), - key=f"transform_{i}", - ) - value = st.text_input( - f"Value #{i + 1}", - value=json.dumps(field_data.get("value", "")) - if "value" in field_data - else "", - key=f"value_{i}", - ) - - if field_name: - # Note: Only add keys that have non-empty values - mappings[field_name] = {} - if from_field: - mappings[field_name]["from"] = from_field - if transform: - mappings[field_name]["transform"] = transform - if value: - try: - parsed_value = json.loads(value) - except json.JSONDecodeError: - parsed_value = value - mappings[field_name]["value"] = parsed_value - - # Prefill OASST Mapper - st.markdown("### OASST Mapper") - required = st.checkbox( - "Required?", - value=oasst_mapper_existing.get("required", "").lower() - in ["yes", "true"], - key="required_checkbox", - ) - oasst_type = st.selectbox( - "Type", - options=["sft", "dpo"], - index=0 if oasst_mapper_existing.get("type", "sft") == "sft" else 1, - key="oasst_type", - ) - intermediate_writing = st.checkbox( - "Intermediate Writing?", - value=oasst_mapper_existing.get("intermediate_writing", "").lower() - in ["yes", "true"], - key="intermediate_checkbox", - ) - - # Save Button - save = st.button("Save Output Config") - if save: - st.session_state["draft_output_config1"].update( - { - "generator": f"tasks.{st.session_state.get('current_task1', '')}.task_executor.{generator_function}", - "output_map": mappings, - "oasst_mapper": { - "required": "yes" if required else "no", - "type": oasst_type, - "intermediate_writing": "yes" - if intermediate_writing - else "no", - }, - } - ) - st.success("Output Config saved") - - -def show_graph_builder(): - st.subheader("Create Graph") - - # Split into two columns - col1, col2 = st.columns([2, 1]) - - with col2: - st.markdown("---") - st.subheader("Current Nodes and Edges") - - if st.button("Show Nodes and Edges"): - # Display all nodes - st.markdown("**Nodes:**") - for i, node in enumerate(st.session_state.nodes1): - with st.expander(f"{node['data']['label']}: {node['id']}"): - st.json(node) - - # Display all edges - if "edges1" in st.session_state and st.session_state.edges1: - st.markdown("**Edges:**") - for i, edge in enumerate(st.session_state.edges1): - markdown = "" - if "is_conditional" in edge and edge["is_conditional"]: - markdown = f"idx={i} | {edge['source']} (conditional)" - else: - markdown = f"idx={i} | {edge['source']} ----→ {edge['target']}" - with st.expander(markdown): - st.json(edge) - - with col1: - # Node palette and configuration - st.markdown("---") - st.subheader("Add Node") - node_type = st.selectbox( - "Node Type", ["llm", "weighted_sampler", "lambda", "multi_llm", "agent"] - ) - - node_label = st.text_input("Node Label") - - if st.button("Add Node"): - if add_node(node_type, node_label): - st.info(f"Node added: {node_label}") - - # Node configuration - if st.session_state.nodes1: - st.subheader("Configure Node") - selected_node = st.selectbox( - "Select Node", - [ - node["data"]["label"] + "(" + node["id"] + ")" - for node in st.session_state.nodes1 - ], - ) - node_id = selected_node.split("(")[-1].rstrip(")") - configure_node(node_id) - - # Edge creation - if len(st.session_state.nodes1) >= 2: - st.markdown("---") - st.subheader("Add Edge") - source = st.selectbox( - "From", - [ - node["data"]["label"] + "(" + node["id"] + ")" - for node in st.session_state.nodes1 - ], - key="edge_source", - ) - - is_conditional = st.checkbox("Conditional Edge") - - if is_conditional: - condition = st.text_input("Condition Function") - num_paths = st.number_input("Number of Paths", min_value=1, value=2) - - paths = {} - for i in range(num_paths): - col1, col2 = st.columns(2) - with col1: - condition_value = st.text_input( - f"Condition {i + 1}", key=f"cond_{i}" - ) - with col2: - target_node = st.selectbox( - "Target", - ["END"] - + [ - node["data"]["label"] + "(" + node["id"] + ")" - for node in st.session_state.nodes1 - ], - key=f"target_{i}", - ) - paths[condition_value] = target_node - - else: - target = st.selectbox( - "To", - [ - node["data"]["label"] + "(" + node["id"] + ")" - for node in st.session_state.nodes1 - ], - key="edge_target", - ) - - if st.button("Add Edge"): - if not is_conditional: - add_edge( - source, - target, - is_conditional, - condition if is_conditional else None, - paths if is_conditional else None, - ) - else: - add_edge_conditional( - source, - is_conditional, - condition if is_conditional else None, - paths if is_conditional else None, - ) - - if len(st.session_state.edges1) >= 1: - edge_to_remove = st.number_input( - "Edge Index to Remove", - min_value=0, - max_value=len(st.session_state.edges1) - 1, - step=1, - ) - if st.button("Remove Edge"): - delete_edge(edge_to_remove) - - -@st.dialog("Add Node Failed") -def failed_node_message(item, reason: str = "None"): - st.error(f"Node {item} failed. Reason: {reason}") - - -def check_node_exists(node_label): - for node in st.session_state.nodes: - label = node["data"]["label"] - if label == node_label: - return True - return False - - -def add_node(node_type: str, label: str): - if check_node_exists(label): - failed_node_message(label, "duplicate node found.") - return False - node_id = f"{node_type}_{len(st.session_state.nodes1) + 1}" - st.session_state.nodes1.append( - {"id": node_id, "type": node_type, "data": {"label": label}} - ) - return True - - -def delete_edge(index: int): - if "edges1" in st.session_state and 0 <= index < len(st.session_state.edges1): - del st.session_state.edges1[index] - st.rerun() - - -def add_edge( - source: str, - target: str, - is_conditional: bool, - condition: str = None, - paths: dict[str, str] = None, -): - edge = { - "source": source, - "target": target, - } - - if is_conditional: - edge.update( - { - "is_conditional": is_conditional, - "condition": condition, - "path_map": paths, - } - ) - - st.session_state.edges1.append(edge) - - -def add_edge_conditional( - source: str, - is_conditional: bool, - condition: str = None, - paths: dict[str, str] = None, -): - edge = { - "source": source, - } - - if is_conditional: - edge.update( - { - "is_conditional": is_conditional, - "condition": condition, - "path_map": paths, - } - ) - - st.session_state.edges1.append(edge) - - -def configure_node(node_id: str): - node = next(node for node in st.session_state.nodes1 if node["id"] == node_id) - - if node["type"] == "llm": - configure_llm_node(node) - elif node["type"] == "weighted_sampler": - configure_sampler_node(node) - elif node["type"] == "lambda": - configure_lambda_node(node) - elif node["type"] == "multi_llm": - configure_multi_llm_node(node) - elif node["type"] == "agent": - configure_agent_node(node) - - -def configure_agent_node(node: dict): - data = node.get("data", {}) - model_name = data.get("model", "") - parameters = data.get("parameters", {}) - temperature = float(parameters.get("temperature", 0.7)) - max_tokens = int(parameters.get("max_tokens", 500)) - - pre_process = data.get("pre_process", "") - post_process = data.get("post_process", "") - chat_history = data.get("chat_history", False) - default_messages = data.get("prompt", []) - default_tools = data.get("tools", []) - - # UI Components - model = st.selectbox( - "Model", - st.session_state.active_models, - index=st.session_state.active_models.index(model_name) - if model_name in st.session_state.active_models - else 0, - ) - - temperature = st.slider("Temperature", 0.0, 1.0, temperature) - max_tokens = st.number_input("Max Tokens", 100, 2000, max_tokens) - output_keys = st.text_input( - "Output Keys", - placeholder="e.g. ai_answer", - value=",".join(data.get("output_keys", [])) - if isinstance(data.get("output_keys", []), list) - else data.get("output_keys", ""), - ) - output_keys = [key.strip() for key in output_keys.split(",") if key.strip()] - # Tools Input - tools_input = st.text_area( - "Tools (comma-separated paths)", - value=", ".join(default_tools), - placeholder="eg. tasks.agent_task.tools.func_name, tasks.agent_task.tools_from_module, tasks.agent_task.tools_from_class", - help="Provide import paths for tools the agent will use", - ) - tools = [tool.strip() for tool in tools_input.split(",") if tool.strip()] - - pre_process = st.text_input( - "Pre-Process (optional)", - placeholder="eg. CritiqueAnsNodePreProcessor", - help="write the name of the pre_processor function", - value=pre_process, - ) - - post_process = st.text_input( - "Post-Process (optional)", - placeholder="eg. CritiqueAnsNodePostProcessor", - help="write the name of the post_processor function", - value=post_process, - ) - - chat_history = st.checkbox("Enable Chat History", value=chat_history) - - # Prompt Messages - st.subheader("Prompt Messages") - num_messages = st.number_input( - "Number of Messages", 1, 5, max(1, len(default_messages)) - ) - - messages = [] - for i in range(int(num_messages)): - default_message = default_messages[i] if i < len(default_messages) else {} - default_role = list(default_message.keys())[0] if default_message else "system" - default_content = ( - default_message.get(default_role, "") if default_message else "" - ) - - col1, col2 = st.columns([1, 3]) - with col1: - role = st.selectbox( - f"Role {i + 1}", - ["system", "user", "assistant"], - index=["system", "user", "assistant"].index(default_role), - key=f"agent_role_{i}", - ) - with col2: - content = st.text_area( - f"Content {i + 1}", value=default_content, key=f"agent_content_{i}" - ) - messages.append({role: content}) - - # Save - if st.button("Save"): - node["data"].update( - { - "node_type": "agent", - "model": model, - "parameters": {"temperature": temperature, "max_tokens": max_tokens}, - "prompt": messages, - "pre_process": pre_process, - "post_process": post_process, - "chat_history": chat_history, - "tools": tools, - "output_keys": output_keys, - } - ) - st.info("Saved Successfully") - - # Delete - match = re.search(r"(\d+)$", node["id"]) - index = int(match.group(1)) - 1 - if index >= len(st.session_state.nodes1): - index = index - 1 - if st.button("Delete Node"): - del st.session_state["nodes1"][index] - st.rerun() - - -def configure_llm_node(node: dict): - data = node.get("data", {}) - model_data = data.get("model", {}) - params = model_data.get("parameters", {}) - - model = st.selectbox( - "Model", - st.session_state.active_models, - index=st.session_state.active_models.index(model_data.get("name", "gpt4")) - if model_data.get("name", "gpt4") in st.session_state.active_models - else 1, - ) - output_keys = st.text_input( - "Output Keys", - placeholder="e.g. ai_answer", - value=",".join(data.get("output_keys", [])) - if isinstance(data.get("output_keys", []), list) - else data.get("output_keys", ""), - ) - output_keys = [key.strip() for key in output_keys.split(",") if key.strip()] - - temperature = st.slider( - "Temperature", 0.0, 1.0, float(params.get("temperature", 0.7)) - ) - max_tokens = st.number_input( - "Max Tokens", 100, 2000, int(params.get("max_tokens", 500)) - ) - chat_history = st.checkbox( - "Enable Chat History", value=data.get("chat_history", False) - ) - pre_process = st.text_input( - "Pre-Process (optional)", - placeholder="eg. NodeNamePreProcessor", - help="write the name of the pre_processor function", - value=data.get("pre_process", ""), - ) - post_process = st.text_input( - "Post-Process (optional)", - placeholder="eg. NodeNamePostProcessor", - help="write the name of the post_processor function", - value=data.get("post_process", ""), - ) - - st.subheader("Prompt Messages") - default_messages = data.get("prompt", []) - num_messages = st.number_input( - "Number of Messages", 1, 5, max(1, len(default_messages)) - ) - - messages = [] - for i in range(int(num_messages)): - default_message = default_messages[i] if i < len(default_messages) else {} - default_role = list(default_message.keys())[0] if default_message else "system" - default_content = ( - default_message.get(default_role, "") if default_message else "" - ) - - col1, col2 = st.columns([1, 3]) - with col1: - role = st.selectbox( - f"Role {i + 1}", - ["system", "user", "assistant"], - index=["system", "user", "assistant"].index(default_role), - key=f"role_{i}", - ) - with col2: - content = st.text_area( - f"Content {i + 1}", value=default_content, key=f"content_{i}" - ) - messages.append({role: content}) - - if st.button("Save"): - node["data"].update( - { - "node_type": "llm", - "output_keys": output_keys, - "model": { - "name": model, - "parameters": { - "temperature": temperature, - "max_tokens": max_tokens, - }, - }, - "prompt": messages, - "pre_process": pre_process, - "post_process": post_process, - "chat_history": chat_history, - } - ) - st.info("Saved Successfully") - - match = re.search(r"(\d+)$", node["id"]) - index = int(match.group(1)) - 1 - if index >= len(st.session_state.nodes1): - index = index - 1 - if st.button("Delete Node"): - del st.session_state["nodes1"][index] - st.rerun() - - -def configure_sampler_node(node: dict): - st.subheader("Attributes") - data = node.get("data", {}) - existing_attrs = data.get("attributes", {}) - num_attrs = st.number_input( - "Number of Attributes", - 1, - 20, - value=len(existing_attrs) if existing_attrs else 1, - ) - - attributes = {} - attr_keys = list(existing_attrs.keys()) - - for i in range(int(num_attrs)): - default_name = attr_keys[i] if i < len(attr_keys) else "" - default_values = ( - existing_attrs.get(default_name, {}).get("values", []) - if default_name - else [] - ) - default_weights = ( - existing_attrs.get(default_name, {}).get("weights", []) - if default_name - else [] - ) - - attr_name = st.text_input( - f"Attribute {i + 1} Name", value=default_name, key=f"attr_name_{i}" - ) - values = st.text_input( - f"Values (comma-separated)", - value=", ".join(map(str, default_values)), - key=f"values_{i}", - ) - weights = st.text_input( - f"Weights (comma-separated, optional)", - value=", ".join(map(str, default_weights)) if default_weights else "", - key=f"weights_{i}", - ) - - if attr_name: - attributes[attr_name] = { - "values": [v.strip() for v in values.split(",")], - "weights": [float(w.strip()) for w in weights.split(",")] - if weights - else None, - } - - if st.button("Save"): - node["data"].update( - { - "node_type": "weighted_sampler", - "attributes": attributes, - } - ) - st.info("Saved Successfully") - - match = re.search(r"(\d+)$", node["id"]) - index = int(match.group(1)) - 1 - if index >= len(st.session_state.nodes1): - index = index - 1 - if st.button("Delete Node"): - del st.session_state["nodes1"][index] - st.rerun() - - -def configure_lambda_node(node: dict): - data = node.get("data", {}) - default_lambda = data.get("lambda", "") - default_node_state = data.get("node_state", "") - - function_path = st.text_input( - "Function Path", value=default_lambda, help="e.g., path.to.module.function_name" - ) - node_state = st.text_input("Node State", value=default_node_state) - output_keys = st.text_input( - "Output Keys", - placeholder="e.g. ai_answer", - value=",".join(data.get("output_keys", [])) - if isinstance(data.get("output_keys", []), list) - else data.get("output_keys", ""), - ) - output_keys = [key.strip() for key in output_keys.split(",") if key.strip()] - - if st.button("Save"): - node["data"].update( - { - "node_type": "lambda", - "lambda": function_path, - "node_state": node_state, - "output_keys": output_keys, - } - ) - st.info("Saved Successfully") - - match = re.search(r"(\d+)$", node["id"]) - index = int(match.group(1)) - 1 - if index >= len(st.session_state.nodes1): - index = index - 1 - if st.button("Delete Node"): - del st.session_state["nodes1"][index] - st.rerun() - - -def configure_multi_llm_node(node: dict): - data = node.get("data", {}) - existing_prompt = data.get("prompt", []) - pre_process_multi_llm = data.get("pre_process", "") - post_process_multi_llm = data.get( - "multi_llm_post_process", data.get("post_process", "") - ) - output_keys = st.text_input( - "Output Keys", - placeholder="e.g. ai_answer", - value=",".join(data.get("output_keys", [])) - if isinstance(data.get("output_keys", []), list) - else data.get("output_keys", ""), - ) - output_keys = [key.strip() for key in output_keys.split(",") if key.strip()] - existing_models = data.get("models", {}) - - st.subheader("Prompt Messages") - num_messages = st.number_input( - "Number of Messages", 1, 5, max(1, len(existing_prompt)) - ) - - messages = [] - for i in range(num_messages): - col1, col2 = st.columns([1, 3]) - default_role = ( - list(existing_prompt[i].keys())[0] if i < len(existing_prompt) else "user" - ) - default_content = ( - list(existing_prompt[i].values())[0] if i < len(existing_prompt) else "" - ) - with col1: - role = st.selectbox( - f"Role {i + 1}", - ["system", "user", "assistant"], - key=f"role_{i}", - index=["system", "user", "assistant"].index(default_role), - ) - with col2: - content = st.text_area( - f"Content {i + 1}", value=default_content, key=f"content_{i}" - ) - messages.append({role: content}) - - pre_process_multi_llm = st.text_input( - "Pre-Process MultiLLM (optional)", - placeholder="eg. generate_samples_pre_process", - help="write the name of the pre_processor function", - value=pre_process_multi_llm, - ) - post_process_multi_llm = st.text_input( - "Post-Process MultiLLM (optional)", - placeholder="eg. generate_samples_post_process", - help="write the name of the post_processor function", - value=post_process_multi_llm, - ) - - st.subheader("Models") - num_models = st.number_input("Number of Models", 1, 5, max(1, len(existing_models))) - - models = {} - model_keys = list(existing_models.keys()) - - for i in range(num_models): - st.markdown(f"**Model {i + 1}**") - default_model_key = model_keys[i] if i < len(model_keys) else "" - default_model_data = existing_models.get(default_model_key, {}) - default_name = default_model_data.get("name", "") - default_temperature = default_model_data.get("parameters", {}).get( - "temperature", 0.7 - ) - default_max_tokens = default_model_data.get("parameters", {}).get( - "max_tokens", 500 - ) - - model_name = st.text_input( - "Name", key=f"model_name_{i}", value=default_model_key - ) - model_type = st.selectbox( - "Type", - st.session_state.active_models, - key=f"model_type_{i}", - index=st.session_state.active_models.index(default_name) - if default_name in st.session_state.active_models - else 0, - ) - temperature = st.slider( - "Temperature", 0.0, 1.0, default_temperature, key=f"temp_{i}" - ) - max_tokens = st.number_input( - "Max Tokens", 100, 2000, default_max_tokens, key=f"tokens_{i}" - ) - - if model_name: - models[model_name] = { - "name": model_type, - "parameters": {"temperature": temperature, "max_tokens": max_tokens}, - } - - if st.button("Save"): - node["data"].update( - { - "node_type": "multi_llm", - "prompt": messages, - "models": models, - "pre_process": pre_process_multi_llm, - "multi_llm_post_process": post_process_multi_llm, - "output_keys": output_keys, - } - ) - st.info("Saved Successfully") - - match = re.search(r"(\d+)$", node["id"]) - index = int(match.group(1)) - 1 - if index >= len(st.session_state.nodes1): - index = index - 1 - if st.button("Delete Node"): - del st.session_state["nodes1"][index] - st.rerun() - - -def parse_edges(raw_edges): - edge_list = [] - for edge in raw_edges: - from_node = edge["source"] - to_node = edge.get("target") - condition = edge.get("condition") - path_map = edge.get("path_map") - is_conditional = edge.get("is_conditional") - - if to_node: - edge_list.append({"source": from_node, "target": to_node}) - elif path_map: - for label, target in path_map.items(): - edge_list.append( - { - "source": from_node, - "target": target, - "label": "conditional", - "condition": condition, - "is_conditional": is_conditional, - } - ) - - st.session_state.parsed_edges1 = edge_list - - -def create_graph(nodes_list, edges_list): - nodes = [] - edges = [] - style_llm = {"backgroundColor": "#52c2fa", "color": "#041a75"} - style_weighted_sampler = {"backgroundColor": "#ebd22f", "color": "#0d0800"} - style_lambda = {"backgroundColor": "#dcb1fa", "color": "#5e059c"} - style_multillm = {"backgroundColor": "#c99999", "color": "#381515"} - style_agent = {"backgroundColor": "#e37a30", "color": "#401b01"} - nodes.append( - StreamlitFlowNode( - id="START", - pos=(0, 0), - data={ - "content": "__start__", - }, - node_type="input", - source_position="right", - draggable=True, - style={ - "color": "white", - "backgroundColor": "#00c04b", - "border": "2px solid white", - }, - ) - ) - - for node in nodes_list: - style = {} - if node["type"] == "llm": - style = style_llm - elif node["type"] == "weighted_sampler": - style = style_weighted_sampler - elif node["type"] == "lambda": - style = style_lambda - elif node["type"] == "multi_llm": - style = style_multillm - elif node["type"] == "agent": - style = style_agent - else: - st.warning(f"Not implemented for node type : {node['type']}") - id = node["data"]["label"] + "(" + node["id"] + ")" - nodes.append( - StreamlitFlowNode( - id=id, - pos=(0, 0), - data={ - "content": node["data"]["label"], - "node_type": node["type"], - }, - node_type="default", - source_position="right", - target_position="left", - draggable=True, - style=style, - ) - ) - - nodes.append( - StreamlitFlowNode( - id="END", - pos=(0, 0), - data={ - "content": "__end__", - }, - node_type="output", - target_position="left", - draggable=True, - style={ - "color": "white", - "backgroundColor": "#d95050", - "border": "2px solid white", - }, - ) - ) - - first_node_label = nodes_list[0]["data"]["label"] - first_node_id = nodes_list[0]["id"] - edges.append( - StreamlitFlowEdge( - id=f"START-{first_node_label}({first_node_id})", - source="START", - target=f"{first_node_label}({first_node_id})", - edge_type="default", - animated=False, - marker_end={"type": "arrowclosed", "width": 25, "height": 25}, - ) - ) - - for edge in edges_list: - from_node = edge["source"] - to_node = edge.get("target") - id = f"{from_node}-{to_node}" - is_conditional = edge.get("is_conditional") - label = "" - if is_conditional: - label = "(conditional)" - edges.append( - StreamlitFlowEdge( - id=id, - source=from_node, - target=to_node, - edge_type="default", - label=label, - label_show_bg=True, - label_bg_style={"fill": "gray"}, - animated=False, - marker_end={"type": "arrowclosed", "width": 25, "height": 25}, - ) - ) - last_node_id = nodes_list[-1]["id"] - last_node_label = nodes_list[-1]["data"]["label"] - edges.append( - StreamlitFlowEdge( - id=f"{last_node_label}({last_node_id})-END", - source=f"{last_node_label}({last_node_id})", - target="END", - edge_type="default", - animated=False, - marker_end={"type": "arrowclosed", "width": 25, "height": 25}, - ) - ) - - return nodes, edges - - -@st.fragment -def show_graph(): - if st.button("Refresh Graph", key="refresh_graph1"): - del st.session_state.static_flow_state2 - st.rerun() - parse_edges(st.session_state.edges1) - nodes, edges = create_graph(st.session_state.nodes1, st.session_state.parsed_edges1) - if st.session_state["static_flow_state2"] is None: - st.session_state.static_flow_state2 = StreamlitFlowState(nodes, edges) - - streamlit_flow( - "static_flow2", - st.session_state.static_flow_state2, - fit_view=True, - show_minimap=True, - show_controls=True, - pan_on_drag=True, - allow_zoom=True, - hide_watermark=True, - layout=StressLayout(), - ) - - st.markdown("### Legend") - st.markdown("- 🟩 **Green**: START node") - st.markdown("- 🟥 **Red**: END node") - st.markdown("- 🟦 **Blue Box**: LLM node") - st.markdown("- 🟨 **Yellow Box**: Sampler node") - st.markdown("- 🟪 **Purple Box**: Lambda node") - st.markdown("- 🟫 **Brown Box**: Multi-LLM node") - st.markdown("- 🟧 **Orange Box**: Agent node") - - st.markdown("**Edges:**") - st.markdown("- Solid line: Direct flow") - st.markdown("- Labelled line: Conditional path") - - -def strip_node_id(name): - """Removes ID like '(llm_1)' from 'paraphrase_answer(llm_1)'.""" - return re.split(r"\(", name)[0] - - -@st.fragment -def publish(yaml_str, executor_code): - if st.button("Publish"): - task_name = st.session_state.get("current_task1") - if task_name: - task_folder = Path("tasks") / task_name - yaml_path = task_folder / "graph_config.yaml" - yaml_path.write_text(yaml_str) - task_path = Path("tasks") / task_name / "task_executor.py" - task_path.write_text(executor_code) - st.success(f"Published to {yaml_path} and {task_path}") - else: - st.warning("No task selected. Please create a task first.") - - -@st.fragment -def generate_yaml_and_executor(): - st.markdown("---") - if st.button("Generate Yaml and Task Executor file"): - # ----------- YAML Generation ------------ - data_config = st.session_state.get("draft_data_config1", {}) - output_config = st.session_state.get("draft_output_config1", {}) - current_task = st.session_state.get("current_task1", "example_task") - - yaml_file = { - "data_config": data_config, - "graph_config": {"nodes": {}, "edges": []}, - "output_config": output_config, - } - - nodes = st.session_state.get("nodes1", []) - edges = st.session_state.get("edges1", []) - - for node in nodes: - node_type = node.get("type") - data = node.get("data", {}) - label = data.get("label") - output_keys = data.get("output_keys", []) - prompts_list = data.get("prompt", []) - for p in prompts_list: - if p.get("system"): - p["system"] = " |\n" + p.get("system") - if p.get("user"): - p["user"] = " |\n" + p.get("user") - if p.get("assistant"): - p["assistant"] = " |\n" + p.get("assistant") - if not label: - continue - - node_entry = { - "node_type": node_type, - "output_keys": output_keys, - "prompt": data.get("prompt", []), - } - - if node_type in ["llm", "multi_llm"]: - if node_type == "llm": - node_entry["model"] = { - "name": data.get("model", ""), - "parameters": data.get("parameters", {}), - } - if data.get("chat_history") == True: - node_entry["chat_history"] = data.get("chat_history", False) - - elif node_type == "multi_llm": - node_entry["models"] = data.get("models", []) - - # Add pre_process if present - if data.get("pre_process"): - node_entry["pre_process"] = ( - f"tasks.{current_task}.task_executor.{data.get('pre_process')}" - ) - - # Add post_process or multi_llm_post_process accordingly - if data.get("post_process"): - key = ( - "multi_llm_post_process" - if node_type == "multi_llm" - else "post_process" - ) - node_entry[key] = ( - f"tasks.{current_task}.task_executor.{data.get('post_process')}" - ) - - elif node_type == "weighted_sampler": - node_entry = { - "node_type": node_type, - "attributes": data.get("attributes", {}), - } - elif node_type == "lambda": - node_entry = { - "node_type": node_type, - "lambda": data.get("lambda", ""), - "node_state": data.get("node_state", ""), - } - elif node_type == "agent": - node_entry["model"] = { - "name": data.get("model", ""), - "parameters": data.get("parameters", {}), - } - node_entry["chat_history"] = data.get("chat_history", False) - if data.get("tools"): - node_entry["tools"] = data.get("tools", []) - if data.get("pre_process"): - node_entry["pre_process"] = ( - f"tasks.{current_task}.task_executor.{data.get('pre_process')}" - ) - if data.get("post_process"): - node_entry["post_process"] = ( - f"tasks.{current_task}.task_executor.{data.get('post_process')}" - ) - - yaml_file["graph_config"]["nodes"].update({label: node_entry}) - - # ----- Build graph_config.edges ----- - all_edges = [] - - if nodes: - # Add START edge to first node - first_node_label = nodes[0]["data"]["label"] - all_edges.append({"from": "START", "to": first_node_label}) - - for edge in edges: - if edge.get("is_conditional"): - path_map = { - k: strip_node_id(v) for k, v in edge.get("path_map", {}).items() - } - all_edges.append( - { - "from": strip_node_id(edge.get("source")), - "condition": f"tasks.{current_task}.task_executor.{edge.get('condition', '')}", - "path_map": path_map, - } - ) - else: - all_edges.append( - { - "from": strip_node_id(edge.get("source")), - "to": strip_node_id(edge.get("target")), - } - ) - - # Add END edge logic - if len(nodes) == 1: - # Only one node, connect to END - only_node_label = nodes[0]["data"]["label"] - all_edges.append({"from": only_node_label, "to": "END"}) - # Add END edge if last edge is not conditional - elif edges: - last_edge = edges[-1] - if not last_edge.get("is_conditional"): - last_target = strip_node_id(last_edge.get("target")) - all_edges.append({"from": last_target, "to": "END"}) - - yaml_file["graph_config"]["edges"] = all_edges - - yaml_str = yaml.dump(yaml_file, sort_keys=False) - st.session_state.draft_yaml1 = yaml_str - - # ---------- Task Executor Generation ------------ - imports = [ - "from core.graph.functions.node_processor import NodePreProcessor, NodePostProcessor", - "from core.graph.functions.edge_condition import EdgeCondition", - "from processors.output_record_generator import BaseOutputGenerator", - "from core.graph.sygra_state import SygraState", - "from core.base_task_executor import BaseTaskExecutor", - "from utils import utils, constants", - ] - - classes = [] - for node in nodes: - data = node.get("data", {}) - pre = data.get("pre_process") - post = data.get("post_process") - - if pre and not any(f"class {pre}(" in c for c in classes): - classes.append(f""" - class {pre}(NodePreProcessor): - def apply(self, state:SygraState) -> SygraState: - return state - """) - - if post and not any(f"class {post}(" in c for c in classes): - classes.append(f""" - class {post}(NodePostProcessorWithState): - def apply(self, resp:SygraMessage, state:SygraState) -> SygraState: - return resp - """) - - for edge in edges: - condition = edge.get("condition") - if condition and not any(f"class {condition}(" in c for c in classes): - classes.append(f""" - class {condition}(EdgeCondition): - def apply(state:SygraState) -> str: - return "END" - """) - - generator = output_config.get("generator") - if generator: - class_name = generator.split(".")[-1] - if not any(f"class {class_name}(" in c for c in classes): - classes.append(f""" - class {class_name}(BaseOutputGenerator): - def map_function(data: Any, state: SygraState): - return None - """) - - executor_code = "\n\n".join(imports + classes) - st.session_state.draft_executor1 = executor_code - - # ---------- Tabs Display ---------- - tab1, tab2 = st.tabs(["YAML", "Task Executor"]) - with tab1: - st.code(yaml_str, language="yaml") - with tab2: - st.code(executor_code, language="python") - - st.markdown("---") - if st.session_state.draft_yaml1 and st.session_state.draft_executor1: - publish(st.session_state.draft_yaml1, st.session_state.draft_executor1) - - -def get_task_list(): - return [ - f for f in os.listdir(TASKS_DIR) if os.path.isdir(os.path.join(TASKS_DIR, f)) - ] - - -def find_valid_subfolders(task_path): - valid = [] - for dirpath, _, filenames in os.walk(task_path): - if "graph_config.yaml" in filenames and "task_executor.py" in filenames: - valid.append(dirpath) - return valid - - -def show_task_list(): - st.header("Select a Task") - tasks = get_task_list() - - if tasks: - for task in tasks: - if st.button(f"📂 {task}", key=f"task_{task}"): - st.session_state["selected_task1"] = task - st.session_state["selected_subtask1"] = None - st.rerun() - else: - st.info("No tasks created yet.") - - -def show_subtask_selector(task_name, subtasks): - st.header(f"Task: {task_name}") - if st.button("🔙 Back to Task List"): - st.session_state["selected_task1"] = None - st.rerun() - - st.markdown("### Choose a subtask:") - for subtask_path in subtasks: - subtask_name = os.path.relpath(subtask_path, os.path.join(TASKS_DIR, task_name)) - if st.button(f"📄 {subtask_name}", key=subtask_path): - st.session_state["selected_subtask1"] = subtask_path - st.rerun() - - -def show_subtask_details(subtask_path): - st.header(f"Subtask: {os.path.relpath(subtask_path, TASKS_DIR)}") - - if st.button("🔙 Back"): - st.session_state["selected_subtask1"] = None - st.session_state["selected_task1"] = None - st.session_state.load_yaml = False - st.rerun() - - graph_config_path = os.path.join(subtask_path, "graph_config.yaml") - - if not st.session_state.load_yaml: - read_yaml_file(graph_config_path) - - setup_task() - dataset_source() - transformation_form() - dataset_sink() - - show_graph_builder() - st.markdown("---") - if len(st.session_state.nodes1) >= 1: - show_graph() - output_config() - generate_yaml_and_executor() - - -def main(): - if st.session_state["selected_subtask1"]: - show_subtask_details(st.session_state["selected_subtask1"]) - - elif st.session_state["selected_task1"]: - task_path = os.path.join(TASKS_DIR, st.session_state["selected_task1"]) - valid_subtasks = find_valid_subfolders(task_path) - - if not valid_subtasks: - st.warning("No valid subfolders with both files found in this task.") - if st.button("🔙 Back to Task List"): - st.session_state["selected_task1"] = None - st.session_state["selected_subtask1"] = None - st.session_state["static_flow_state2"] = None - st.rerun() - elif len(valid_subtasks) == 1: - # Skip selection, show the only subtask directly - st.session_state["selected_subtask1"] = valid_subtasks[0] - st.session_state["static_flow_state2"] = None - st.rerun() - else: - show_subtask_selector(st.session_state["selected_task1"], valid_subtasks) - st.session_state["static_flow_state2"] = None - - else: - show_task_list() - - -if __name__ == "__main__": - main() diff --git a/apps/create_new_task.py b/apps/create_new_task.py deleted file mode 100644 index d36c2574..00000000 --- a/apps/create_new_task.py +++ /dev/null @@ -1,1300 +0,0 @@ -from pathlib import Path -import yaml -from datasets import load_dataset, get_dataset_config_names - -try: - import streamlit as st - from streamlit_flow import streamlit_flow - from streamlit_flow.elements import StreamlitFlowNode, StreamlitFlowEdge - from streamlit_flow.state import StreamlitFlowState - from streamlit_flow.layouts import ( - TreeLayout, - RadialLayout, - LayeredLayout, - ForceLayout, - StressLayout, - RandomLayout, - ) -except ModuleNotFoundError: - raise ModuleNotFoundError( - "SyGra UI requires the optional 'ui' dependencies. " - "Install them with: pip install 'sygra[ui]'" - ) -import time -import json -import csv -import pandas as pd -import re - - -# Constants -TASKS_DIR = Path("tasks") -TASKS_DIR.mkdir(exist_ok=True) -st.set_page_config(page_title="SyGra UI", layout="wide") - -st.title("Create New Task") - -if "draft_data_config" not in st.session_state: - st.session_state["draft_data_config"] = {} -if "draft_output_config" not in st.session_state: - st.session_state["draft_output_config"] = {} -if "current_task" not in st.session_state: - st.session_state["current_task"] = "" -if "nodes" not in st.session_state: - st.session_state.nodes = [] -if "edges" not in st.session_state: - st.session_state.edges = [] -if "parsed_edges" not in st.session_state: - st.session_state.parsed_edges = [] -if "static_flow_state" not in st.session_state: - st.session_state["static_flow_state"] = None -if "draft_yaml" not in st.session_state: - st.session_state.draft_yaml = None -if "draft_executor" not in st.session_state: - st.session_state.draft_executor = "" -if "hf_config_names" not in st.session_state: - st.session_state["hf_config_names"] = [] -if "active_models" not in st.session_state: - st.session_state.active_models = [] - - -def get_first_record(file_path, file_format, encoding): - try: - if file_format == "json": - with open(file_path, "r", encoding=encoding) as f: - data = json.load(f) - return data[0] if isinstance(data, list) else data - - elif file_format == "jsonl": - with open(file_path, "r", encoding=encoding) as f: - first_line = f.readline() - return json.loads(first_line) - - elif file_format == "csv": - with open(file_path, "r", encoding=encoding) as f: - reader = csv.DictReader(f) - return next(reader) - - elif file_format == "parquet": - df = pd.read_parquet(file_path) - return df.iloc[0].to_dict() - - else: - return {"error": f"Unsupported format: {file_format}"} - - except Exception as e: - return {"error": str(e)} - - -# 1. Task name + folder setup -@st.fragment -def setup_task(): - st.subheader("Task Setup") - task_name = st.text_input("Enter Task Name") - if st.button("Create"): - if task_name: - st.session_state["current_task"] = task_name - task_folder = TASKS_DIR / task_name - if not task_folder.exists(): - task_folder.mkdir(parents=True) - (task_folder / "graph_config.yaml").write_text("# Graph config\n") - (task_folder / "task_executor.py").write_text("# Task executor\n") - st.success(f"Created folder: {task_folder}") - else: - st.warning("Task already exists.") - else: - st.warning("Please enter a task name.") - - -# 2. Load dataset from HuggingFace -@st.fragment -def load_hf_dataset(): - repo_id = st.text_input( - "repo_id (type the repo_id and press Enter to get list of configs)", - placeholder="e.g., username/dataset-name", - ) - - # Fetch config names if repo_id is entered - if repo_id and not st.session_state.hf_config_names: - try: - st.session_state["hf_config_names"] = get_dataset_config_names(repo_id) - except Exception as e: - st.warning(f"Could not fetch config names: {e}") - st.session_state["hf_config_names"] = [] - - config_name = st.selectbox( - "config_name", - options=st.session_state["hf_config_names"] or ["default"], - index=0, - ) - - split = st.text_input( - "split (comma-separated)", placeholder="e.g., train, test, validation" - ) - split_list = [s.strip() for s in split.split(",")] - token = st.text_input("token (optional)", placeholder="hf_token") - streaming = st.checkbox("Streaming? (optional)") - shard = st.text_input("shard (optional)") - - load = st.button("Save & Load Dataset") - if load: - progress_bar = st.progress(0) - status_text = st.empty() - - for percent in range(0, 70, 10): - progress_bar.progress(percent) - status_text.text(f"Loading dataset... {percent}%") - time.sleep(0.1) - - try: - dataset = load_dataset( - path=repo_id, - name=config_name if config_name != "default" else None, - split=split_list[0], - token=token or None, - streaming=streaming, - ) - - for percent in range(70, 101, 10): - progress_bar.progress(percent) - status_text.text(f"Finishing up... {percent}%") - time.sleep(0.05) - - st.success("Dataset loaded!") - st.session_state["draft_data_config"].update( - { - "source": { - "type": "hf", - "repo_id": repo_id, - "config_name": config_name, - "split": split_list, - "token": token, - "streaming": streaming, - "shard": shard, - } - } - ) - st.json(dataset[0]) - except Exception as e: - progress_bar.empty() - status_text.empty() - st.error(f"Error loading dataset: {e}") - - -# 3. Load dataset from disk -@st.fragment -def load_disk_dataset(): - with st.form("disk_data"): - disk_data = { - "type": "disk", - "file_path": st.text_input("File Path"), - "file_format": st.text_input("File Format (e.g., csv, json)"), - "encoding": st.text_input("Encoding", value="utf-8"), - } - load = st.form_submit_button("Save & Load Dataset") - if load: - progress_bar = st.progress(0) - status_text = st.empty() - - for percent in range(0, 70, 10): - progress_bar.progress(percent) - status_text.text(f"Loading dataset... {percent}%") - time.sleep(0.1) - try: - first_record = get_first_record( - disk_data["file_path"], - disk_data["file_format"], - disk_data["encoding"], - ) - for percent in range(70, 101, 10): - progress_bar.progress(percent) - status_text.text(f"Finishing up... {percent}%") - time.sleep(0.05) - st.success("Dataset loaded!") - st.session_state["draft_data_config"].update( - { - "source": { - "type": "disk", - "file_path": disk_data["file_path"], - "file_format": disk_data["file_format"], - "encoding": disk_data["encoding"], - } - } - ) - st.json(first_record) - except Exception as e: - progress_bar.empty() - status_text.empty() - st.error(f"Error loading dataset: {e}") - - -# 4. Dataset source form -def dataset_source(): - with st.container(border=True): - data_config = st.checkbox("Data Source?", value=True) - if data_config: - st.subheader("📂 Data Source") - source_type = st.selectbox("Select Source Type", ["hf", "disk"]) - if source_type == "hf": - load_hf_dataset() - else: - load_disk_dataset() - - -# 5. Transformation section -@st.fragment -def transformation_form(): - with st.container(border=True): - transformation = st.checkbox("Transformation?", value=True) - if transformation: - transformations_count = st.number_input( - "No. of Transformations", min_value=1, step=1 - ) - transformations = [] - for i in range(int(transformations_count)): - transform_path = st.text_input( - f"#{i + 1} Transform class (e.g., ClassName)" - ) - params = st.text_area(f"#{i + 1} Transform parameters (Dict)", "{}") - if transform_path and params: - try: - params = json.loads(params) - transformations.append( - { - "transform": f"processors.data_transform.{transform_path}", - "params": params, - } - ) - except json.JSONDecodeError: - st.error("Invalid JSON in parameters") - - transform_submit = st.button("Save Mappings") - if transform_submit: - st.session_state["draft_data_config"]["source"].update( - {"transformations": transformations} - ) - st.success("Transformations saved") - - -# 6. Form for sink -@st.fragment -def sink_hf_form(sink_type): - with st.form("sink_hf_form"): - repo_id = st.text_input("repo_id", placeholder="e.g., username/dataset-name") - config_name = st.text_input("config_name", placeholder="e.g., config-name") - split = st.text_input("split", placeholder="e.g., train, test, validation") - hf_token = st.text_input("hf_token (optional)", placeholder="hf-token") - private = st.checkbox("private", value=False) - - # Display summary (optional) - if st.form_submit_button("Save HF Sink"): - st.session_state["draft_data_config"].update( - { - "sink": { - "type": sink_type, - "repo_id": repo_id, - "config": config_name, - "split": split, - "private": private, - "hf_token": hf_token, - } - } - ) - st.success( - f"HF Sink Configured with repo_id: {repo_id}, split: {split}, private: {private}" - ) - - -@st.fragment -def sink_disk_form(sink_type): - with st.form("sink_disk_form"): - file_path = st.text_input( - "file_path", placeholder="e.g., /path/to/output/file.json" - ) - encoding = st.text_input("encoding", value="utf-8") - - # Display summary (optional) - if st.form_submit_button("Save Disk Sink"): - st.session_state["draft_data_config"].update( - { - "sink": { - "type": sink_type, - "file_path": file_path, - "encoding": encoding, - } - } - ) - st.success( - f"Disk Sink Configured with file_path: {file_path}, encoding: {encoding}" - ) - - -def dataset_sink(): - with st.container(border=True): - dataset_sink = st.checkbox("Dataset Sink?", value=True) - if dataset_sink: - st.subheader("📂 Data Sink (Optional)") - sink_type = st.selectbox( - "Select sink Type", ["hf", "json", "jsonl", "csv", "parquet"] - ) - if sink_type == "hf": - sink_hf_form(sink_type) - else: - sink_disk_form(sink_type) - - -@st.fragment -def output_config(): - with st.container(border=True): - output_config = st.checkbox("Output Config?", value=True) - - if output_config: - st.subheader("Output Config") - - generator_function = st.text_input("Generator Function") - - st.markdown("### Output Map") - output_mapping_count = st.number_input( - "Add field renames", min_value=1, step=1, key="mapping_count" - ) - - mappings = {} - for i in range(int(output_mapping_count)): - col1, col2 = st.columns([1, 3]) - with col1: - field_name = st.text_input( - f"Field Name #{i + 1}", key=f"field_name_{i}" - ) - with col2: - from_field = st.text_input(f"From #{i + 1}", key=f"from_{i}") - transform = st.text_input( - f"Transform #{i + 1}", key=f"transform_{i}" - ) - value = st.text_input(f"Value #{i + 1}", key=f"value_{i}") - if field_name: - mappings[field_name] = { - "from": from_field, - "transform": transform, - "value": value, - } - - st.markdown("### OASST Mapper") - required = st.checkbox("Required?", value=False, key="required_checkbox") - oasst_type = st.selectbox("Type", options=["sft", "dpo"], key="oasst_type") - intermediate_writing = st.checkbox( - "Intermediate Writing?", value=False, key="intermediate_checkbox" - ) - - save = st.button("Save Output Config") - if save: - st.session_state.draft_output_config.update( - { - "generator": f"tasks.{st.session_state.current_task}.task_executor.{generator_function}", - "output_map": mappings, - "oasst_mapper": { - "required": required, - "type": oasst_type, - "intermediate_writing": intermediate_writing, - }, - } - ) - st.success("Output Config saved") - - -def show_graph_builder(): - st.subheader("Create Graph") - - # Split into two columns - col1, col2 = st.columns([2, 1]) - - with col2: - st.markdown("---") - st.subheader("Current Nodes and Edges") - - if st.button("Show Nodes and Edges"): - # Display all nodes - st.markdown("**Nodes:**") - for i, node in enumerate(st.session_state.nodes): - with st.expander(f"{node['data']['label']}: {node['id']}"): - st.json(node) - - # Display all edges - if "edges" in st.session_state and st.session_state.edges: - st.markdown("**Edges:**") - for i, edge in enumerate(st.session_state.edges): - markdown = "" - if "is_conditional" in edge and edge["is_conditional"]: - markdown = f"idx={i} | {edge['source']} (conditional)" - else: - markdown = f"idx={i} | {edge['source']} ----→ {edge['target']}" - with st.expander(markdown): - st.json(edge) - - with col1: - # Node palette and configuration - st.markdown("---") - st.subheader("Add Node") - node_type = st.selectbox( - "Node Type", ["llm", "weighted_sampler", "lambda", "multi_llm", "agent"] - ) - - node_label = st.text_input("Node Label") - - if st.button("Add Node"): - if add_node(node_type, node_label): - st.info(f"Node added: {node_label}") - - # Node configuration - if st.session_state.nodes: - st.subheader("Configure Node") - selected_node = st.selectbox( - "Select Node", - [ - node["data"]["label"] + "(" + node["id"] + ")" - for node in st.session_state.nodes - ], - ) - node_id = selected_node.split("(")[-1].rstrip(")") - configure_node(node_id) - - # Edge creation - if len(st.session_state.nodes) >= 2: - st.markdown("---") - st.subheader("Add Edge") - source = st.selectbox( - "From", - [ - node["data"]["label"] + "(" + node["id"] + ")" - for node in st.session_state.nodes - ], - key="edge_source", - ) - - is_conditional = st.checkbox("Conditional Edge") - - if is_conditional: - condition = st.text_input("Condition Function") - num_paths = st.number_input("Number of Paths", min_value=1, value=2) - - paths = {} - for i in range(num_paths): - col1, col2 = st.columns(2) - with col1: - condition_value = st.text_input( - f"Condition {i + 1}", key=f"cond_{i}" - ) - with col2: - target_node = st.selectbox( - "Target", - ["END"] - + [ - node["data"]["label"] + "(" + node["id"] + ")" - for node in st.session_state.nodes - ], - key=f"target_{i}", - ) - paths[condition_value] = target_node - - else: - target = st.selectbox( - "To", - [ - node["data"]["label"] + "(" + node["id"] + ")" - for node in st.session_state.nodes - ], - key="edge_target", - ) - - if st.button("Add Edge"): - if not is_conditional: - add_edge( - source, - target, - is_conditional, - condition if is_conditional else None, - paths if is_conditional else None, - ) - else: - add_edge_conditional( - source, - is_conditional, - condition if is_conditional else None, - paths if is_conditional else None, - ) - - if len(st.session_state.edges) >= 1: - edge_to_remove = st.number_input( - "Edge Index to Remove", - min_value=0, - max_value=len(st.session_state.edges) - 1, - step=1, - ) - if st.button("Remove Edge"): - delete_edge(edge_to_remove) - - -@st.dialog("Add Node Failed") -def failed_node_message(item, reason: str = "None"): - st.error(f"Node {item} failed. Reason: {reason}") - - -def check_node_exists(node_label): - for node in st.session_state.nodes: - label = node["data"]["label"] - if label == node_label: - return True - return False - - -def add_node(node_type: str, label: str): - if check_node_exists(label): - failed_node_message(label, "duplicate node found.") - return False - node_id = f"{node_type}_{len(st.session_state.nodes) + 1}" - st.session_state.nodes.append( - {"id": node_id, "type": node_type, "data": {"label": label}} - ) - return True - - -def delete_edge(index: int): - if "edges" in st.session_state and 0 <= index < len(st.session_state.edges): - del st.session_state.edges[index] - st.rerun() - - -def add_edge( - source: str, - target: str, - is_conditional: bool, - condition: str = None, - paths: dict[str, str] = None, -): - edge = { - "source": source, - "target": target, - } - - if is_conditional: - edge.update( - { - "is_conditional": is_conditional, - "condition": condition, - "path_map": paths, - } - ) - - st.session_state.edges.append(edge) - - -def add_edge_conditional( - source: str, - is_conditional: bool, - condition: str = None, - paths: dict[str, str] = None, -): - edge = { - "source": source, - } - - if is_conditional: - edge.update( - { - "is_conditional": is_conditional, - "condition": condition, - "path_map": paths, - } - ) - - st.session_state.edges.append(edge) - - -def configure_node(node_id: str): - node = next(node for node in st.session_state.nodes if node["id"] == node_id) - - if node["type"] == "llm": - configure_llm_node(node) - elif node["type"] == "weighted_sampler": - configure_sampler_node(node) - elif node["type"] == "lambda": - configure_lambda_node(node) - elif node["type"] == "multi_llm": - configure_multi_llm_node(node) - elif node["type"] == "agent": - configure_agent_node(node) - - -def configure_agent_node(node: dict): - output_keys = st.text_input("Output Keys", placeholder="e.g. ai_answer") - output_keys = output_keys.split(",") if "," in output_keys else output_keys - model = st.selectbox("Model", st.session_state.active_models) - temperature = st.slider("Temperature", 0.0, 1.0, 0.7) - max_tokens = st.number_input("Max Tokens", 100, 2000, 500) - pre_process = st.text_input( - "Pre-Process (optional)", - placeholder="eg. CritiqueAnsNodePreProcessor", - help="write the name of the pre_processor function", - ) - post_process = st.text_input( - "Post-Process (optional)", - placeholder="eg. CritiqueAnsNodePostProcessor", - help="write the name of the post_processor function", - ) - tools_input = st.text_area( - "Tools (comma-separated paths)", - placeholder="eg. tasks.agent_task.tools.func_name, tasks.agent_task.tools_from_module, tasks.agent_task.tools_from_class ", - help="Provide the import paths for the tools the agent will use", - ) - tools = [tool.strip() for tool in tools_input.split(",") if tool.strip()] - - chat_history = st.checkbox("Enable Chat History", value=False) - st.subheader("Prompt Messages") - num_messages = st.number_input("Number of Messages", 1, 5, 1) - - messages = [] - for i in range(num_messages): - col1, col2 = st.columns([1, 3]) - with col1: - role = st.selectbox( - f"Role {i + 1}", ["system", "user", "assistant"], key=f"role_{i}" - ) - with col2: - content = st.text_area(f"Content {i + 1}", key=f"content_{i}") - messages.append({role: content}) - - if st.button("Save"): - node["data"].update( - { - "node_type": "agent", - "model": model, - "parameters": {"temperature": temperature, "max_tokens": max_tokens}, - "prompt": messages, - "pre_process": pre_process, - "post_process": post_process, - "chat_history": chat_history, - "tools": tools, - "output_keys": output_keys, - } - ) - st.info("Saved Successfully") - match = re.search(r"(\d+)$", node["id"]) - index = int(match.group(1)) - 1 - if index >= len(st.session_state.nodes): - index = index - 1 - if st.button("Delete Node"): - del st.session_state["nodes"][index] - st.rerun() - - -def configure_llm_node(node: dict): - output_keys = st.text_input("Output Keys", placeholder="e.g. ai_answer") - output_keys = output_keys.split(",") if "," in output_keys else output_keys - model = st.selectbox("Model", st.session_state.active_models) - temperature = st.slider("Temperature", 0.0, 1.0, 0.7) - max_tokens = st.number_input("Max Tokens", 100, 2000, 500) - pre_process = st.text_input( - "Pre-Process (optional)", - placeholder="eg. CritiqueAnsNodePreProcessor", - help="write the name of the pre_processor function", - ) - post_process = st.text_input( - "Post-Process (optional)", - placeholder="eg. CritiqueAnsNodePostProcessor", - help="write the name of the post_processor function", - ) - chat_history = st.checkbox("Enable Chat History", value=False) - st.subheader("Prompt Messages") - num_messages = st.number_input("Number of Messages", 1, 5, 1) - - messages = [] - for i in range(num_messages): - col1, col2 = st.columns([1, 3]) - with col1: - role = st.selectbox( - f"Role {i + 1}", ["system", "user", "assistant"], key=f"role_{i}" - ) - with col2: - content = st.text_area(f"Content {i + 1}", key=f"content_{i}") - messages.append({role: content}) - - if st.button("Save"): - node["data"].update( - { - "node_type": "llm", - "output_keys": output_keys, - "model": model, - "parameters": {"temperature": temperature, "max_tokens": max_tokens}, - "prompt": messages, - "pre_process": pre_process, - "post_process": post_process, - "chat_history": chat_history, - } - ) - st.info("Saved Successfully") - match = re.search(r"(\d+)$", node["id"]) - index = int(match.group(1)) - 1 - if index >= len(st.session_state.nodes): - index = index - 1 - if st.button("Delete Node"): - del st.session_state["nodes"][index] - st.rerun() - - -def configure_sampler_node(node: dict): - st.subheader("Attributes") - num_attrs = st.number_input("Number of Attributes", 1, 5, 1) - - attributes = {} - for i in range(num_attrs): - attr_name = st.text_input(f"Attribute {i + 1} Name", key=f"attr_name_{i}") - values = st.text_input(f"Values (comma-separated)", key=f"values_{i}") - weights = st.text_input( - f"Weights (comma-separated, optional)", key=f"weights_{i}" - ) - - if attr_name: - attributes[attr_name] = { - "values": [v.strip() for v in values.split(",")], - "weights": [float(w.strip()) for w in weights.split(",")] - if weights - else None, - } - - if st.button("Save"): - node["data"].update( - { - "node_type": "weighted_sampler", - "attributes": attributes, - } - ) - st.info("Saved Successfully") - match = re.search(r"(\d+)$", node["id"]) - index = int(match.group(1)) - 1 - if index >= len(st.session_state.nodes): - index = index - 1 - if st.button("Delete Node"): - del st.session_state["nodes"][index] - st.rerun() - - -def configure_lambda_node(node: dict): - output_keys = st.text_input("Output Keys", placeholder="e.g. ai_answer") - output_keys = output_keys.split(",") if "," in output_keys else output_keys - function_path = st.text_input( - "Function Path", help="e.g., path.to.module.function_name" - ) - node_state = st.text_input("Node State") - - if st.button("Save"): - node["data"].update( - { - "node_type": "lambda", - "lambda": function_path, - "node_state": node_state, - "output_keys": output_keys, - } - ) - st.info("Saved Successfully") - match = re.search(r"(\d+)$", node["id"]) - index = int(match.group(1)) - 1 - if index >= len(st.session_state.nodes): - index = index - 1 - if st.button("Delete Node"): - del st.session_state["nodes"][index] - st.rerun() - - -def configure_multi_llm_node(node: dict): - st.subheader("Prompt Messages") - num_messages = st.number_input("Number of Messages", 1, 5, 1) - - messages = [] - for i in range(num_messages): - col1, col2 = st.columns([1, 3]) - with col1: - role = st.selectbox( - f"Role {i + 1}", ["system", "user", "assistant"], key=f"role_{i}" - ) - with col2: - content = st.text_area(f"Content {i + 1}", key=f"content_{i}") - messages.append({role: content}) - - pre_process_multi_llm = st.text_input( - "Pre-Process MultiLLM (optional)", - placeholder="eg. generate_samples_pre_process", - help="write the name of the pre_processor function", - ) - post_process_multi_llm = st.text_input( - "Post-Process MultiLLM (optional)", - placeholder="eg. generate_samples_post_process", - help="write the name of the post_processor function", - ) - output_keys = st.text_input("Output Keys", placeholder="e.g. ai_answer") - output_keys = output_keys.split(",") if "," in output_keys else output_keys - - st.subheader("Models") - num_models = st.number_input("Number of Models", 1, 5, 1) - - models = {} - for i in range(num_models): - st.markdown(f"**Model {i + 1}**") - model_name = st.text_input(f"Name", key=f"model_name_{i}") - model_type = st.selectbox( - f"Type", st.session_state.active_models, key=f"model_type_{i}" - ) - temperature = st.slider(f"Temperature", 0.0, 1.0, 0.7, key=f"temp_{i}") - max_tokens = st.number_input(f"Max Tokens", 100, 2000, 500, key=f"tokens_{i}") - - if model_name: - models[model_name] = { - "name": model_type, - "parameters": {"temperature": temperature, "max_tokens": max_tokens}, - } - - if st.button("Save"): - node["data"].update( - { - "node_type": "multi_llm", - "prompt": messages, - "models": models, - "pre_process": pre_process_multi_llm, - "post_process": post_process_multi_llm, - "output_keys": output_keys, - } - ) - st.info("Saved Successfully") - match = re.search(r"(\d+)$", node["id"]) - index = int(match.group(1)) - 1 - if index >= len(st.session_state.nodes): - index = index - 1 - if st.button("Delete Node"): - del st.session_state["nodes"][index] - st.rerun() - - -def parse_edges(raw_edges): - edge_list = [] - for edge in raw_edges: - from_node = edge["source"] - to_node = edge.get("target") - condition = edge.get("condition") - path_map = edge.get("path_map") - is_conditional = edge.get("is_conditional") - - if to_node: - edge_list.append({"source": from_node, "target": to_node}) - elif path_map: - for label, target in path_map.items(): - edge_list.append( - { - "source": from_node, - "target": target, - "label": "conditional", - "condition": condition, - "is_conditional": is_conditional, - } - ) - - st.session_state.parsed_edges = edge_list - - -def create_graph(nodes_list, edges_list): - nodes = [] - edges = [] - style_llm = {"backgroundColor": "#52c2fa", "color": "#041a75"} - style_weighted_sampler = {"backgroundColor": "#ebd22f", "color": "#0d0800"} - style_lambda = {"backgroundColor": "#dcb1fa", "color": "#5e059c"} - style_multillm = {"backgroundColor": "#c99999", "color": "#381515"} - style_agent = {"backgroundColor": "#e37a30", "color": "#401b01"} - nodes.append( - StreamlitFlowNode( - id="START", - pos=(0, 0), - data={ - "content": "__start__", - }, - node_type="input", - source_position="right", - draggable=True, - style={ - "color": "white", - "backgroundColor": "#00c04b", - "border": "2px solid white", - }, - ) - ) - - for node in nodes_list: - style = {} - if node["type"] == "llm": - style = style_llm - elif node["type"] == "weighted_sampler": - style = style_weighted_sampler - elif node["type"] == "lambda": - style = style_lambda - elif node["type"] == "multi_llm": - style = style_multillm - elif node["type"] == "agent": - style = style_agent - id = node["data"]["label"] + "(" + node["id"] + ")" - nodes.append( - StreamlitFlowNode( - id=id, - pos=(0, 0), - data={ - "content": node["data"]["label"], - "node_type": node["type"], - }, - node_type="default", - source_position="right", - target_position="left", - draggable=True, - style=style, - ) - ) - - nodes.append( - StreamlitFlowNode( - id="END", - pos=(0, 0), - data={ - "content": "__end__", - }, - node_type="output", - target_position="left", - draggable=True, - style={ - "color": "white", - "backgroundColor": "#d95050", - "border": "2px solid white", - }, - ) - ) - - first_node_id = nodes_list[0]["id"] - first_node_label = nodes_list[0]["data"]["label"] - edges.append( - StreamlitFlowEdge( - id=f"START-{first_node_label}({first_node_id})", - source="START", - target=f"{first_node_label}({first_node_id})", - edge_type="default", - animated=False, - marker_end={"type": "arrowclosed", "width": 25, "height": 25}, - ) - ) - - for edge in edges_list: - from_node = edge["source"] - to_node = edge.get("target") - id = f"{from_node}-{to_node}" - is_conditional = edge.get("is_conditional") - label = "" - if is_conditional: - label = "(conditional)" - edges.append( - StreamlitFlowEdge( - id=id, - source=from_node, - target=to_node, - edge_type="default", - label=label, - label_visibility=True if label else False, - label_show_bg=True, - label_bg_style={"fill": "gray"}, - animated=False, - marker_end={"type": "arrowclosed", "width": 25, "height": 25}, - ) - ) - end_node_id = nodes_list[-1]["id"] - end_node_label = nodes_list[-1]["data"]["label"] - edges.append( - StreamlitFlowEdge( - id=f"{end_node_label}({end_node_id})-END", - source=f"{end_node_label}({end_node_id})", - target="END", - edge_type="default", - animated=False, - marker_end={"type": "arrowclosed", "width": 25, "height": 25}, - ) - ) - - return nodes, edges - - -@st.fragment -def show_graph(): - if st.button("Refresh Graph", key="refresh_graph"): - del st.session_state.static_flow_state - st.rerun() - parse_edges(st.session_state.edges) - nodes, edges = create_graph(st.session_state.nodes, st.session_state.parsed_edges) - if st.session_state["static_flow_state"] is None: - st.session_state.static_flow_state = StreamlitFlowState(nodes, edges) - - streamlit_flow( - "static_flow", - st.session_state.static_flow_state, - fit_view=True, - show_minimap=True, - show_controls=True, - pan_on_drag=True, - allow_zoom=True, - hide_watermark=True, - layout=StressLayout(), - ) - - st.markdown("### Legend") - st.markdown("- 🟩 **Green**: START node") - st.markdown("- 🟥 **Red**: END node") - st.markdown("- 🟦 **Blue Box**: LLM node") - st.markdown("- 🟨 **Yellow Box**: Sampler node") - st.markdown("- 🟪 **Purple Box**: Lambda node") - st.markdown("- 🟫 **Brown Box**: Multi-LLM node") - st.markdown("- 🟧 **Orange Box**: Agent node") - - st.markdown("**Edges:**") - st.markdown("- Solid line: Direct flow") - st.markdown("- Labelled line: Conditional path") - - -def strip_node_id(name): - """Removes ID like '(llm_1)' from 'paraphrase_answer(llm_1)'.""" - return re.split(r"\(", name)[0] - - -@st.fragment -def publish(yaml_str, executor_code): - if st.button("Publish"): - task_name = st.session_state.get("current_task") - if task_name: - task_folder = Path("tasks") / task_name - yaml_path = task_folder / "graph_config.yaml" - yaml_path.write_text(yaml_str) - task_path = Path("tasks") / task_name / "task_executor.py" - task_path.write_text(executor_code) - st.success(f"Published to {yaml_path} and {task_path}") - else: - st.warning("No task selected. Please create a task first.") - - -def generate_yaml_and_executor(): - st.markdown("---") - if st.button("Generate Yaml and Task Executor file"): - # ----------- YAML Generation ------------ - data_config = st.session_state.get("draft_data_config", {}) - output_config = st.session_state.get("draft_output_config", {}) - current_task = st.session_state.get("current_task", "example_task") - - yaml_file = { - "data_config": data_config, - "graph_config": {"nodes": {}, "edges": []}, - "output_config": output_config, - } - - nodes = st.session_state.get("nodes", []) - edges = st.session_state.get("edges", []) - - for node in nodes: - node_type = node.get("type") - data = node.get("data", {}) - label = data.get("label") - output_keys = data.get("output_keys", []) - prompts_list = data.get("prompt", []) - for p in prompts_list: - if p.get("system"): - p["system"] = " |\n" + p.get("system") - if p.get("user"): - p["user"] = " |\n" + p.get("user") - if p.get("assistant"): - p["assistant"] = " |\n" + p.get("assistant") - if not label: - continue - - node_entry = { - "node_type": node_type, - "output_keys": output_keys, - "prompt": data.get("prompt", []), - } - - if node_type in ["llm", "multi_llm"]: - if node_type == "llm": - node_entry["model"] = { - "name": data.get("model", ""), - "parameters": data.get("parameters", {}), - } - if data.get("chat_history") == True: - node_entry["chat_history"] = data.get("chat_history", False) - - elif node_type == "multi_llm": - node_entry["models"] = data.get("models", []) - - if data.get("pre_process"): - node_entry["pre_process"] = ( - f"tasks.{current_task}.task_executor.{data.get('pre_process')}" - ) - if data.get("post_process"): - key = ( - "multi_llm_post_process" - if node_type == "multi_llm" - else "post_process" - ) - node_entry[key] = ( - f"tasks.{current_task}.task_executor.{data.get('post_process')}" - ) - - elif node_type == "weighted_sampler": - node_entry = { - "node_type": node_type, - "attributes": data.get("attributes", {}), - } - elif node_type == "lambda": - node_entry = { - "node_type": node_type, - "lambda": data.get("lambda", ""), - "node_state": data.get("node_state", ""), - } - elif node_type == "agent": - node_entry["model"] = { - "name": data.get("model", ""), - "parameters": data.get("parameters", {}), - } - node_entry["chat_history"] = data.get("chat_history", False) - if data.get("tools"): - node_entry["tools"] = data.get("tools", []) - if data.get("pre_process"): - node_entry["pre_process"] = ( - f"tasks.{current_task}.task_executor.{data.get('pre_process')}" - ) - if data.get("post_process"): - node_entry["post_process"] = ( - f"tasks.{current_task}.task_executor.{data.get('post_process')}" - ) - - yaml_file["graph_config"]["nodes"].update({label: node_entry}) - - # ----- Build graph_config.edges ----- - all_edges = [] - - if nodes: - # Add START edge to first node - first_node_label = nodes[0]["data"]["label"] - all_edges.append({"from": "START", "to": first_node_label}) - - for edge in edges: - if edge.get("is_conditional"): - path_map = { - k: strip_node_id(v) for k, v in edge.get("path_map", {}).items() - } - all_edges.append( - { - "from": strip_node_id(edge.get("source")), - "condition": f"tasks.{current_task}.task_executor.{edge.get('condition', '')}", - "path_map": path_map, - } - ) - else: - all_edges.append( - { - "from": strip_node_id(edge.get("source")), - "to": strip_node_id(edge.get("target")), - } - ) - - # Add END edge logic - if len(nodes) == 1: - # Only one node, connect to END - only_node_label = nodes[0]["data"]["label"] - all_edges.append({"from": only_node_label, "to": "END"}) - # Add END edge if last edge is not conditional - elif edges: - last_edge = edges[-1] - if not last_edge.get("is_conditional"): - last_target = strip_node_id(last_edge.get("target")) - all_edges.append({"from": last_target, "to": "END"}) - - yaml_file["graph_config"]["edges"] = all_edges - - yaml_str = yaml.dump(yaml_file, sort_keys=False) - st.session_state.draft_yaml = yaml_str - - # ---------- Task Executor Generation ------------ - imports = [ - "from core.graph.functions.node_processor import NodePreProcessor, NodePostProcessor", - "from core.graph.functions.edge_condition import EdgeCondition", - "from processors.output_record_generator import BaseOutputGenerator", - "from core.graph.sygra_state import SygraState", - "from core.base_task_executor import BaseTaskExecutor", - "from utils import utils, constants", - ] - - classes = [] - for node in nodes: - data = node.get("data", {}) - pre = data.get("pre_process") - post = data.get("post_process") - - if pre and not any(f"class {pre}(" in c for c in classes): - classes.append(f""" -class {pre}(NodePreProcessor): - def apply(self, state:SygraState) -> SygraState: - return state -""") - - if post and not any(f"class {post}(" in c for c in classes): - classes.append(f""" -class {post}(NodePostProcessorWithState): - def apply(self, resp:SygraMessage, state:SygraState) -> SygraState: - return state -""") - - for edge in edges: - condition = edge.get("condition") - if condition and not any(f"class {condition}(" in c for c in classes): - classes.append(f""" -class {condition}(EdgeCondition): - def apply(state:SygraState) -> str: - return "END" -""") - - generator = output_config.get("generator") - if generator: - class_name = generator.split(".")[-1] - if not any(f"class {class_name}(" in c for c in classes): - classes.append(f""" -class {class_name}(BaseOutputGenerator): - def map_function(data: Any, state: SygraState): - return None -""") - - executor_code = "\n\n".join(imports + classes) - st.session_state.draft_executor = executor_code - - # ---------- Tabs Display ---------- - tab1, tab2 = st.tabs(["YAML", "Task Executor"]) - with tab1: - st.code(yaml_str, language="yaml") - with tab2: - st.code(executor_code, language="python") - - -def main(): - setup_task() - dataset_source() - transformation_form() - dataset_sink() - - show_graph_builder() - st.markdown("---") - if len(st.session_state.nodes) >= 1: - show_graph() - - output_config() - generate_yaml_and_executor() - st.markdown("---") - if st.session_state.draft_yaml and st.session_state.draft_executor: - publish(st.session_state.draft_yaml, st.session_state.draft_executor) - - -if __name__ == "__main__": - main() diff --git a/apps/models.py b/apps/models.py deleted file mode 100644 index 7f6a91dd..00000000 --- a/apps/models.py +++ /dev/null @@ -1,252 +0,0 @@ -try: - import streamlit as st -except ModuleNotFoundError: - raise ModuleNotFoundError( - "SyGra UI requires the optional 'ui' dependencies. " - "Install them with: pip install 'sygra[ui]'" - ) -import yaml -import os -import asyncio -from pathlib import Path -import httpx -from dateutil.relativedelta import relativedelta -from datetime import datetime, timedelta, timezone -from utils import check_model_status -import sys - -sys.path.append(os.path.dirname(os.path.dirname(__file__))) -from sygra.utils.utils import load_model_config - -UTC = timezone.utc -YAML_FILE = Path("../sygra/config/models.yaml") -USER_TZ = UTC -st.set_page_config(page_title="SyGra UI", layout="wide") - -if "model_statuses" not in st.session_state: - st.session_state["model_statuses"] = {} - -if "last_updated" not in st.session_state: - st.session_state["last_updated"] = None - -if "draft_models" not in st.session_state: - st.session_state["draft_models"] = {} - -if "delete_confirm" not in st.session_state: - st.session_state["delete_confirm"] = {} - -if "new_params" not in st.session_state: - st.session_state["new_params"] = [] - -if "active_models" not in st.session_state: - st.session_state["active_models"] = [] - - -def load_models(): - if os.path.exists(YAML_FILE): - with open(YAML_FILE, "r") as file: - return yaml.safe_load(file) or {} - return {} - - -def save_models(models): - with open(YAML_FILE, "w") as file: - yaml.dump(models, file, default_flow_style=False) - - -def get_time_delta(delta: relativedelta): - """Get a human-readable time delta like "2 hours ago" or "just now".""" - for unit in ("years", "months", "days", "hours", "minutes", "seconds"): - value = getattr(delta, unit) - if value: - return f"{value} {unit if value > 1 else unit[:-1]} ago" - return "just now" - - -@st.fragment(run_every="1min") -def display_time_since(timestamp): - st.markdown( - f"🕒 Last updated {get_time_delta(relativedelta(datetime.now(UTC), timestamp))}", - help=timestamp.astimezone(USER_TZ).strftime("%Y-%m-%d %H:%M:%S %Z"), - ) - - -models = load_model_config() - - -async def update_model_statuses(models): - async with httpx.AsyncClient() as session: - tasks = [ - check_model_status(session, name, model) for name, model in models.items() - ] - statuses = await asyncio.gather(*tasks) - return { - name: ("🟢 Active" if is_active else "🔴 Inactive") - for name, is_active in statuses - } - - -async def initialize_model_statuses(): - st.session_state["model_statuses"] = await update_model_statuses(models) - st.session_state["last_updated"] = datetime.now(UTC) - - -# Run the initialization only if model statuses are empty -if "model_statuses" not in st.session_state or not st.session_state["model_statuses"]: - asyncio.run(initialize_model_statuses()) # ✅ This runs immediately on app start - - -st.markdown("# Models 🤖") - -if st.button("🔄 Refresh Status"): - st.session_state["model_statuses"] = asyncio.run(update_model_statuses(models)) - st.session_state["last_updated"] = datetime.now(UTC) - st.rerun() - -if st.session_state["last_updated"]: - display_time_since(st.session_state["last_updated"]) - -for model_name in list(models.keys()): - status = st.session_state["model_statuses"].get(model_name, "⚪ Checking...") - with st.expander( - f"**{model_name}** ({models[model_name].get('model_type', 'Unknown Type')}) {status}" - ): - st.json(models[model_name]) - - # Delete Confirmation - if model_name not in st.session_state["delete_confirm"]: - st.session_state["delete_confirm"][model_name] = False - - if not st.session_state["delete_confirm"][model_name]: - if st.button(f"🗑 Delete {model_name}", key=f"delete_{model_name}"): - st.session_state["delete_confirm"][model_name] = True - st.rerun() - else: - st.warning( - f"Are you sure you want to delete **{model_name}**? This action cannot be undone." - ) - if st.button( - f"✅ Confirm Delete {model_name}", key=f"confirm_{model_name}" - ): - del models[model_name] - save_models(models) - del st.session_state["delete_confirm"][model_name] - st.success(f"Model '{model_name}' deleted successfully!") - st.rerun() - - if st.button("❌ Cancel", key=f"cancel_delete_{model_name}"): - st.session_state["delete_confirm"][model_name] = False - st.rerun() - - -if st.button("➕ Add Model"): - st.session_state["show_add_model_form"] = True - st.session_state["new_params"] = [] # Reset new parameters when adding a new model - -if st.session_state.get("show_add_model_form", False): - st.markdown("## Add New Model") - - with st.form("add_model_form"): - new_model_name = st.text_input("Name").strip() - new_model_type = st.selectbox( - "Model Type", ["azure_openai", "tgi", "vllm", "mistralai"], index=0 - ) - new_model_model = st.text_input("Model") - new_model_url = st.text_input("URL") - new_model_auth_token = st.text_input("Auth Token", type="password") - new_model_api_version = st.text_input("API Version") - - if st.form_submit_button("➕ Add New Parameter"): - st.session_state["new_params"].append({"key": "", "value": ""}) - st.rerun() - - to_delete1 = [] - for i, param in enumerate(st.session_state["new_params"]): - colu1, colu2, colu3 = st.columns([3, 3, 1]) - with colu1: - param["key"] = st.text_input( - f"Param {i + 1}", param["key"], key=f"key_{i}" - ) - with colu2: - param["value"] = st.text_input( - f"Value {i + 1}", param["value"], key=f"value_{i}" - ) - with colu3: - if st.form_submit_button(f"❌ Remove {i + 1}"): - to_delete1.append(i) - - for i in reversed(to_delete1): - del st.session_state["new_params"][i] - st.rerun() - - st.markdown("### Parameters") - max_tokens = st.number_input("Max Tokens", min_value=1, value=500) - temperature = st.slider("Temperature", 0.0, 2.0, 1.0, step=0.1) - stop_tokens = st.text_area("Stop Tokens (comma-separated)").split(",") - - col1, col2 = st.columns(2) - with col1: - submitted = st.form_submit_button("Save as Draft") - with col2: - cancel = st.form_submit_button("Cancel") - - if cancel: - st.session_state["show_add_model_form"] = False - st.rerun() - - if submitted: - if not new_model_name: - st.error("Model name cannot be empty!") - elif ( - new_model_name in models - or new_model_name in st.session_state["draft_models"] - ): - st.error("A model with this name already exists!") - else: - new_params = { - param["key"]: param["value"] - for param in st.session_state["new_params"] - if param["key"] - } - - # Save as a draft - st.session_state["draft_models"][new_model_name] = { - "model_type": new_model_type, - "model": new_model_model, - "url": new_model_url, - "auth_token": new_model_auth_token, - "api_version": new_model_api_version, - **new_params, - "parameters": { - "max_tokens": max_tokens, - "temperature": temperature, - "stop": stop_tokens, - }, - } - st.success(f"Model '{new_model_name}' saved as draft!") - st.session_state["show_add_model_form"] = False - st.session_state["new_params"] = [] - st.rerun() - -if st.session_state["draft_models"]: - st.markdown("## Draft Models") - for draft_name, draft_model in list(st.session_state["draft_models"].items()): - with st.expander( - f"**{draft_name}** ({draft_model.get('model_type', 'Unknown Type')}) - DRAFT" - ): - st.json(draft_model) - - col1, col2 = st.columns(2) - with col1: - if st.button(f"✅ Publish {draft_name}", key=f"publish_{draft_name}"): - models[draft_name] = draft_model - save_models(models) - del st.session_state["draft_models"][draft_name] - st.success(f"Model '{draft_name}' published!") - st.rerun() - with col2: - if st.button( - f"❌ Delete Draft {draft_name}", key=f"delete_draft_{draft_name}" - ): - del st.session_state["draft_models"][draft_name] - st.rerun() diff --git a/apps/sygra_app.py b/apps/sygra_app.py deleted file mode 100644 index 2181fac2..00000000 --- a/apps/sygra_app.py +++ /dev/null @@ -1,22 +0,0 @@ -try: - import streamlit as st -except ModuleNotFoundError: - raise ModuleNotFoundError( - "SyGra UI requires the optional 'ui' dependencies. " - "Install them with: pip install 'sygra[ui]'" - ) - -# Define the pages -Models = st.Page("models.py", title="Models", icon="🤖") -Tasks = st.Page("tasks.py", title="Tasks", icon="🗒️") -CreateTask = st.Page("create_new_task.py", title="Create new task", icon="➕") -CreateTaskFromTemplate = st.Page( - "create_from_template.py", title="Create new task from template", icon="➕" -) - -# Set up navigation -pg = st.navigation([Models, Tasks, CreateTask, CreateTaskFromTemplate]) - - -# Run the selected page -pg.run() diff --git a/apps/tasks.py b/apps/tasks.py deleted file mode 100644 index 9b7f85be..00000000 --- a/apps/tasks.py +++ /dev/null @@ -1,448 +0,0 @@ -import os -import yaml -from pathlib import Path - -try: - import streamlit as st - from streamlit_flow import streamlit_flow - from streamlit_flow.elements import StreamlitFlowNode, StreamlitFlowEdge - from streamlit_flow.state import StreamlitFlowState - from streamlit_flow.layouts import ( - TreeLayout, - RadialLayout, - LayeredLayout, - ForceLayout, - StressLayout, - RandomLayout, - ) -except ModuleNotFoundError: - raise ModuleNotFoundError( - "SyGra UI requires the optional 'ui' dependencies. " - "Install them with: pip install 'sygra[ui]'" - ) - -TASKS_DIR = Path("tasks") - - -class GraphConfigVisualizer: - def __init__(self, yaml_path): - self.yaml_path = yaml_path - self.nodes = {} - self._load_yaml() - - def _load_yaml(self): - try: - with open(self.yaml_path, "r") as f: - self.config = yaml.safe_load(f) - self.nodes = self.config.get("graph_config", {}).get("nodes", {}) - except Exception as e: - self.config = {} - self.nodes = {} - - def get_node_ids(self): - return list(self.nodes.keys()) - - def get_node_details(self, node_id): - raw = self.nodes.get(node_id, {}) - - return { - "node_type": raw.get("node_type"), - "output_key": raw.get("output_key"), - "pre_process": raw.get("pre_process"), - "post_process": raw.get("post_process"), - "model": raw.get("model"), - "attributes": raw.get("attributes") or raw.get("sampling_attributes"), - "prompts": raw.get("prompt", []), - "raw": raw, - } - - -def parse_graph_config_from_yaml(yaml_path): - with open(yaml_path, "r") as f: - config = yaml.safe_load(f) - graph_config = config.get("graph_config", {}) - - # Extract nodes with specific details - nodes = graph_config.get("nodes", {}) - - node_list = [] - for node_id, node_data in nodes.items(): - node_info = { - "name": node_id, - "node_type": node_data.get("node_type"), - "model_name": node_data.get("model", {}).get("name") - if node_data.get("model") - else None, - "pre_process": node_data.get("pre_process"), - "post_process": node_data.get("post_process"), - } - node_list.append(node_info) - - # Extract edges (same as before) - raw_edges = graph_config.get("edges", []) - edge_list = [] - for edge in raw_edges: - from_node = edge["from"] - to_node = edge.get("to") - condition = edge.get("condition") - path_map = edge.get("path_map") - - if to_node: - edge_list.append({"from": from_node, "to": to_node}) - elif path_map: - for label, target in path_map.items(): - edge_list.append( - {"from": from_node, "to": target, "label": "conditional"} - ) - - return node_list, edge_list - - -def create_graph(nodes_list, edges_list): - nodes = [] - edges = [] - style_llm = {"backgroundColor": "#52c2fa", "color": "#041a75"} - style_weighted_sampler = {"backgroundColor": "#ebd22f", "color": "#0d0800"} - style_lamda = {"backgroundColor": "#dcb1fa", "color": "#5e059c"} - style_multillm = {"backgroundColor": "#c99999", "color": "#381515"} - style_agent = {"backgroundColor": "#e37a30", "color": "#401b01"} - nodes.append( - StreamlitFlowNode( - id="START", - pos=(0, 0), - data={ - "content": "__start__", - }, - node_type="input", - source_position="right", - draggable=True, - style={ - "color": "white", - "backgroundColor": "#00c04b", - "border": "2px solid white", - }, - ) - ) - - for node in nodes_list: - style = {} - if node["node_type"] == "llm": - style = style_llm - elif node["node_type"] == "weighted_sampler": - style = style_weighted_sampler - elif node["node_type"] == "lambda": - style = style_lamda - elif node["node_type"] == "multi_llm": - style = style_multillm - elif node["node_type"] == "agent": - style = style_agent - else: - st.warning(f"Not implemented for node type : {node['node_type']}") - return [], [] - - nodes.append( - StreamlitFlowNode( - id=node["name"], - pos=(0, 0), - data={ - "content": node["name"], - "node_type": node["node_type"], - "model_name": node["model_name"], - "pre_process": node["pre_process"], - "post_process": node["post_process"], - }, - node_type="default", - source_position="right", - target_position="left", - draggable=True, - style=style, - ) - ) - - nodes.append( - StreamlitFlowNode( - id="END", - pos=(0, 0), - data={ - "content": "__end__", - }, - node_type="output", - target_position="left", - draggable=True, - style={ - "color": "white", - "backgroundColor": "#d95050", - "border": "2px solid white", - }, - ) - ) - - for edge in edges_list: - from_node = edge["from"] - to_node = edge["to"] - id = f"{from_node}-{to_node}" - label = "" - if "label" in edge and edge["label"]: - label = edge["label"] - edges.append( - StreamlitFlowEdge( - id=id, - source=from_node, - target=to_node, - edge_type="default", - label=label, - label_visibility=True if label else False, - label_show_bg=True, - label_bg_style={"fill": "gray"}, - animated=False, - marker_end={"type": "arrowclosed", "width": 25, "height": 25}, - ) - ) - - return nodes, edges - - -# Get top-level task folders -def get_task_list(): - return [ - f for f in os.listdir(TASKS_DIR) if os.path.isdir(os.path.join(TASKS_DIR, f)) - ] - - -# Recursively find all folders with both required files -def find_valid_subfolders(task_path): - valid = [] - for dirpath, _, filenames in os.walk(task_path): - if "graph_config.yaml" in filenames and "task_executor.py" in filenames: - valid.append(dirpath) - return valid - - -def read_file(file_path): - try: - with open(file_path, "r") as f: - return f.read() - except Exception as e: - return f"Error reading file: {e}" - - -def show_task_list(): - st.markdown("# Tasks 🗒️") - tasks = get_task_list() - - if tasks: - for task in tasks: - if st.button(f"📂 {task}", key=f"task_{task}"): - st.session_state["selected_task"] = task - st.session_state["selected_subtask"] = None - st.rerun() - else: - st.info("No tasks created yet.") - - -def show_subtask_selector(task_name, subtasks): - st.header(f"Task: {task_name}") - if st.button("🔙 Back to Task List"): - st.session_state["selected_task"] = None - st.rerun() - - st.markdown("### Choose a subtask:") - for subtask_path in subtasks: - subtask_name = os.path.relpath(subtask_path, os.path.join(TASKS_DIR, task_name)) - if st.button(f"📄 {subtask_name}", key=subtask_path): - st.session_state["selected_subtask"] = subtask_path - st.rerun() - - -def show_subtask_details(subtask_path): - st.header(f"Subtask: {os.path.relpath(subtask_path, TASKS_DIR)}") - - if st.button("🔙 Back"): - st.session_state["selected_subtask"] = None - st.session_state["selected_task"] = ( - None # This line ensures clean back navigation - ) - st.rerun() - - graph_config_path = os.path.join(subtask_path, "graph_config.yaml") - task_executor_path = os.path.join(subtask_path, "task_executor.py") - visualizer = GraphConfigVisualizer(graph_config_path) - - tab1, tab2, tab3, tab4 = st.tabs( - [ - "Graph Visualization", - "Configuration Overview", - "Task Executor Overview", - "Node Details", - ] - ) - - with tab1: - st.subheader("Graph Visualization") - nodes_list, edges_list = parse_graph_config_from_yaml(graph_config_path) - nodes, edges = create_graph(nodes_list, edges_list) - - if st.session_state["static_flow_state1"] is None: - st.session_state.static_flow_state1 = StreamlitFlowState(nodes, edges) - - streamlit_flow( - "static_flow1", - st.session_state.static_flow_state1, - fit_view=True, - show_minimap=True, - show_controls=True, - pan_on_drag=True, - allow_zoom=True, - hide_watermark=True, - layout=StressLayout(), - ) - - st.markdown("### Legend") - st.markdown("- 🟩 **Green**: START node") - st.markdown("- 🟥 **Red**: END node") - st.markdown("- 🟦 **Blue Box**: LLM node") - st.markdown("- 🟨 **Yellow Box**: Sampler node") - st.markdown("- 🟪 **Purple Box**: Lamda node") - st.markdown("- 🟫 **Brown Box**: Multi-LLM node") - st.markdown("- 🟧 **Orange Box**: Agent node") - - st.markdown("**Edges:**") - st.markdown("- Solid line: Direct flow") - st.markdown("- Labelled line: Conditional path") - - with tab2: - st.subheader("graph_config.yaml") - - raw_content = read_file(graph_config_path) - - try: - parsed_yaml = yaml.safe_load(raw_content) - except Exception as e: - st.error(f"Failed to parse YAML: {e}") - st.code(raw_content, language="yaml") - st.stop() - - # Expander 1 - Data Config - if "data_config" in parsed_yaml: - with st.expander("Data Configuration", expanded=False): - st.code( - yaml.dump( - {"data_config": parsed_yaml["data_config"]}, sort_keys=False - ), - language="yaml", - ) - - # Expander 2 - Output Config - if "output_config" in parsed_yaml: - with st.expander("Output Configuration", expanded=False): - st.code( - yaml.dump( - {"output_config": parsed_yaml["output_config"]}, sort_keys=False - ), - language="yaml", - ) - - # Expander 3 - Full YAML - with st.expander("Complete Configuration", expanded=True): - st.code(raw_content, language="yaml") - - with tab3: - st.subheader("task_executor.py") - content = read_file(task_executor_path) - st.code(content, language="python") - - with tab4: - st.subheader("Node Configuration Details") - node_ids = visualizer.get_node_ids() - - if node_ids: - selected_node = st.selectbox("Select a node to view details:", node_ids) - - if selected_node: - node_info = visualizer.get_node_details(selected_node) - - with st.expander("Basic Information", expanded=True): - col1, col2 = st.columns(2) - with col1: - st.write(f"**Node Type:** {node_info['node_type']}") - if node_info["output_key"]: - st.write(f"**Output Key:** {node_info['output_key']}") - with col2: - if node_info["pre_process"]: - st.write(f"**Pre-processor:** {node_info['pre_process']}") - if node_info["post_process"]: - st.write(f"**Post-processor:** {node_info['post_process']}") - - if node_info["model"]: - with st.expander("Model Configuration", expanded=True): - st.json(node_info["model"]) - - if node_info["attributes"]: - with st.expander("Sampling Attributes", expanded=True): - for attr_name, attr_config in node_info["attributes"].items(): - st.subheader(f"Attribute: {attr_name}") - if ( - isinstance(attr_config, dict) - and "values" in attr_config - ): - st.write("Values:") - st.write(attr_config["values"]) - - if node_info["prompts"]: - with st.expander("Prompts", expanded=True): - for i, prompt in enumerate(node_info["prompts"], 1): - st.subheader(f"Prompt {i}") - for role, content in prompt.items(): - st.markdown(f"**{role.title()}:**") - st.text_area( - "", - value=content, - height=100, - key=f"{selected_node}_prompt_{i}_{role}", - disabled=True, - ) - - with st.expander("Raw Configuration"): - st.json(node_info["raw"]) - else: - st.info("No nodes available in the current configuration.") - - -def main(): - st.set_page_config(page_title="SyGra UI", layout="wide") - if "selected_task" not in st.session_state: - st.session_state["selected_task"] = None - if "selected_subtask" not in st.session_state: - st.session_state["selected_subtask"] = None - if "static_flow_state1" not in st.session_state: - st.session_state["static_flow_state1"] = None - - if st.session_state["selected_subtask"]: - show_subtask_details(st.session_state["selected_subtask"]) - - elif st.session_state["selected_task"]: - task_path = os.path.join(TASKS_DIR, st.session_state["selected_task"]) - valid_subtasks = find_valid_subfolders(task_path) - - if not valid_subtasks: - st.warning("No valid subfolders with both files found in this task.") - if st.button("🔙 Back to Task List"): - st.session_state["selected_task"] = None - st.session_state["selected_subtask"] = None - st.session_state["static_flow_state1"] = None - st.rerun() - elif len(valid_subtasks) == 1: - # Skip selection, show the only subtask directly - st.session_state["selected_subtask"] = valid_subtasks[0] - st.session_state["static_flow_state1"] = None - st.rerun() - else: - show_subtask_selector(st.session_state["selected_task"], valid_subtasks) - st.session_state["static_flow_state1"] = None - - else: - show_task_list() - - -if __name__ == "__main__": - main() diff --git a/apps/utils.py b/apps/utils.py deleted file mode 100644 index 9d946b54..00000000 --- a/apps/utils.py +++ /dev/null @@ -1,146 +0,0 @@ -try: - import streamlit as st -except ModuleNotFoundError: - raise ModuleNotFoundError( - "SyGra UI requires the optional 'ui' dependencies. " - "Install them with: pip install 'sygra[ui]'" - ) -import httpx -from openai import AsyncAzureOpenAI, AsyncOpenAI -from mistralai_azure import MistralAzure -from mistralai_azure.utils.retries import RetryConfig, BackoffStrategy -import aiohttp -import json - - -async def check_openai_status(session, model_name, model_data): - try: - client = AsyncAzureOpenAI( - azure_endpoint=model_data["url"], - api_key=model_data["auth_token"], - api_version=model_data["api_version"], - timeout=model_data.get("timeout", 10), - default_headers={"Connection": "close"}, - ) - - # Sending test request - completion = await client.chat.completions.create( - model=model_data["model"], - messages=[{"role": "system", "content": "Hello!"}], - max_tokens=5, - temperature=0.1, - ) - - # If no exception, model is active - st.session_state["active_models"].append(model_name) - return model_name, True - - except Exception: - return model_name, False - - -async def check_mistralai_status(session, model_name, model_data): - try: - async_httpx_client = httpx.AsyncClient( - http1=True, verify=True, timeout=model_data.get("timeout", 10) - ) - - retry_config = RetryConfig( - strategy="backoff", - retry_connection_errors=True, - backoff=BackoffStrategy( - initial_interval=1000, - max_interval=1000, - exponent=1.5, - max_elapsed_time=10, - ), - ) - - client = MistralAzure( - azure_api_key=model_data["auth_token"], - azure_endpoint=model_data["url"], - async_client=async_httpx_client, - retry_config=retry_config, - ) - - chat_response = await client.chat.complete_async( - model=model_data["model"], - messages=[{"role": "system", "content": "Hello!"}], - max_tokens=5, - ) - - if chat_response and chat_response.choices: - st.session_state["active_models"].append(model_name) - return model_name, True - - except Exception as e: - return model_name, False - - -async def check_tgi_status(session, model_name, model_data): - try: - payload = json.dumps({"inputs": "Hello!", "parameters": {"max_new_tokens": 5}}) - - # Headers with authentication - headers = { - "Authorization": f"Bearer {model_data.get('auth_token', '')}", - "Content-Type": "application/json", - } - - timeout = model_data.get("timeout", 10) - - async with aiohttp.ClientSession() as session: - async with session.post( - model_data["url"], - data=payload, - headers=headers, - timeout=timeout, - ssl=False, - ) as response: - if response.status == 200: - st.session_state["active_models"].append(model_name) - return model_name, True - else: - error_text = await response.text() - return model_name, False - - except Exception as e: - return model_name, False - - -async def check_vllm_status(session, model_name, model_data): - try: - client = AsyncOpenAI( - base_url=model_data["url"], - api_key=model_data["auth_token"], - timeout=model_data.get("timeout", 10), - default_headers={"Connection": "close"}, - ) - - # Sending test request - completion = await client.chat.completions.create( - model=model_data.get("model_serving_name", model_name), - messages=[{"role": "system", "content": "Hello!"}], - max_tokens=5, - temperature=0.1, - ) - - # If no exception, model is active - st.session_state["active_models"].append(model_name) - return model_name, True - - except Exception as e: - return model_name, False - - -async def check_model_status(session, model_name, model_data): - model_type = model_data.get("model_type", "unknown") - if model_type == "azure_openai": - return await check_openai_status(session, model_name, model_data) - elif model_type == "mistralai": - return await check_mistralai_status(session, model_name, model_data) - elif model_type == "tgi": - return await check_tgi_status(session, model_name, model_data) - elif model_type == "vllm": - return await check_vllm_status(session, model_name, model_data) - return model_name, False diff --git a/docs/getting_started/create_task_ui.md b/docs/getting_started/create_task_ui.md index 04bcf7a7..15e21eb9 100644 --- a/docs/getting_started/create_task_ui.md +++ b/docs/getting_started/create_task_ui.md @@ -1,45 +1,702 @@ +# SyGra Studio -### Run the UI Service +**Visual workflow builder and execution platform for SyGra synthetic data pipelines** -The UI for this project is built using Streamlit and is located in the `apps` directory. To launch the SyGra UI locally, use the provided shell script: +--- + +## Why This Exists + +SyGra is a graph-oriented framework for building synthetic data generation pipelines using LLMs. While powerful, creating and debugging YAML-based workflow configurations requires deep familiarity with the schema and manual iteration. + +**SyGra Studio** provides: +- A visual drag-and-drop interface for designing workflows +- Real-time execution monitoring with node-level progress +- Integrated code editing with syntax highlighting +- Data source preview and transformation testing +- Model management across multiple LLM providers + +It replaces the manual YAML editing workflow with an interactive builder while maintaining full compatibility with the SyGra framework. + +--- + +## Quickstart + +### Using Make (Recommended) + +```bash +# From repo root - one command does everything +make studio +``` + +This automatically builds the frontend (if needed) and starts the server at http://localhost:8000. + +### Manual Setup + +```bash +# 2. Build the frontend +cd studio/frontend +npm install +npm run build +cd ../.. + +# 3. Start the server +uv run python -m studio.server --tasks-dir ./tasks/examples --svelte + +# 4. Open browser +# Navigate to http://localhost:8000 +``` + +**Verification**: You should see the SyGra Studio interface with a sidebar listing available workflows from `tasks/examples/`. + +--- + +## Features + +| Feature | Description | +|---------|-------------| +| **Visual Graph Builder** | Drag-and-drop workflow creation with 12+ node types | +| **Multi-LLM Support** | Azure OpenAI, OpenAI, Ollama, vLLM, Mistral, Vertex AI, Bedrock | +| **Real-time Execution** | Live node status, logs, and output streaming | +| **Code Editor** | Monaco-based Python/YAML editing with syntax highlighting | +| **Data Preview** | Sample data loading with transformation preview | +| **Structured Outputs** | JSON schema validation for LLM responses | +| **Nested Workflows** | Subgraph support for modular workflow composition | +| **Execution History** | Full run tracking with comparison and analytics | +| **Export** | Generate production-ready YAML and Python code | + +### Non-Goals + +- Production job scheduler +- Multi-tenant platform +- Model training +- Distributed execution + +--- + +## Installation + +### Requirements + +| Component | Version | Notes | +|-----------|---------|-------| +| Python | 3.9, 3.10, 3.11 | 3.9.7 excluded due to bug | +| Node.js | 18+ | For frontend build | +| npm | 9+ | Package manager | + +### Install from Source ```bash -./run_ui.sh +# Clone repository +git clone https://github.com/ServiceNow/SyGra.git +cd SyGra + +# Install Python dependencies +pip install -e . + +# Build frontend +cd studio/frontend +npm install +npm run build +cd ../.. ``` -If you're running it for the first time, make sure the script is executable: +### Verify Installation + ```bash -chmod +x run_ui.sh +python -c "from studio import create_server; print('OK')" ``` -To run it on a custom port (e.g., 8502): +--- + +## Configuration + +### Environment Variables + +Studio uses environment variables for model credentials and settings. Variables are stored in `studio/.env` and can be managed via the Settings UI. + +### Model Credentials + +Model credentials follow the pattern `SYGRA_{MODEL_NAME}_{URL|TOKEN}`: + ```bash -./run_ui.sh 8502 +# Azure OpenAI +SYGRA_GPT-4O_URL=https://your-resource.openai.azure.com/ +SYGRA_GPT-4O_TOKEN=your-api-key + +# vLLM / Self-hosted +SYGRA_LLAMA_3_1_8B_URL=http://localhost:8001/v1 +SYGRA_LLAMA_3_1_8B_TOKEN=your-token + +# Ollama (local) +SYGRA_QWEN3_URL=http://localhost:11434 +``` + +### Custom Models + +Add models to `studio/config/custom_models.yaml`: + +```yaml +my_custom_model: + type: azure_openai + model_name: gpt-4o + deployment_name: my-deployment + api_version: "2024-02-15-preview" + parameters: + temperature: 0.7 + max_tokens: 4096 +``` + +--- + +## Usage + +### Make Commands (Recommended) + +The project includes a Makefile with convenient commands for Studio. Run from the repository root: + +| Command | Description | +|---------|-------------| +| `make studio` | Build frontend (if needed) and start server | +| `make studio-build` | Build frontend only (skips if already built) | +| `make studio-rebuild` | Force rebuild frontend | +| `make studio-dev` | Print instructions for development mode | +| `make studio-clean` | Remove frontend build artifacts and node_modules | + +**Configuration via environment variables:** + +```bash +# Custom tasks directory and port +make studio TASKS_DIR=./my/tasks PORT=9000 + +# Default values +# TASKS_DIR=./tasks/examples +# PORT=8000 +``` + +### CLI Reference + +```bash +python -m studio.server [OPTIONS] +``` + +| Flag | Short | Default | Description | +|------|-------|---------|-------------| +| `--tasks-dir` | `-t` | `None` | Directory containing workflow tasks | +| `--host` | `-H` | `0.0.0.0` | Host to bind | +| `--port` | `-p` | `8000` | Port to listen on | +| `--reload` | `-r` | `false` | Auto-reload on code changes | +| `--log-level` | `-l` | `info` | Logging level (debug/info/warning/error) | +| `--svelte` | `-s` | `false` | Use Svelte UI (requires build) | + +### Examples + +```bash +# Development with auto-reload +python -m studio.server -t ./tasks/examples --reload --svelte + +# Production +python -m studio.server -t /opt/sygra/tasks -p 8080 --svelte + +# Custom host binding +python -m studio.server -H 127.0.0.1 -p 3000 --svelte +``` + +### Programmatic Usage + +```python +from studio import run_server, create_app + +# Simple: run blocking server +run_server(tasks_dir="./tasks", port=8000, use_svelte_ui=True) + +# Advanced: get FastAPI app for custom middleware +app = create_app(tasks_dir="./tasks") +# Add custom routes, middleware, etc. +``` + +--- + +## API Reference + +### Workflow Endpoints + +| Method | Endpoint | Description | +|--------|----------|-------------| +| `GET` | `/api/workflows` | List all workflows | +| `GET` | `/api/workflows/{id}` | Get workflow graph | +| `POST` | `/api/workflows` | Create workflow | +| `PUT` | `/api/workflows/{id}` | Update workflow | +| `DELETE` | `/api/workflows/{id}` | Delete workflow | +| `GET` | `/api/workflows/{id}/yaml` | Export as YAML | +| `PUT` | `/api/workflows/{id}/nodes/{node_id}` | Update node | +| `DELETE` | `/api/workflows/{id}/nodes/{node_id}` | Delete node | +| `POST` | `/api/workflows/{id}/edges` | Add edge | +| `DELETE` | `/api/workflows/{id}/edges/{edge_id}` | Delete edge | + +### Execution Endpoints + +| Method | Endpoint | Description | +|--------|----------|-------------| +| `POST` | `/api/workflows/{id}/execute` | Start execution | +| `GET` | `/api/executions` | List executions (paginated) | +| `GET` | `/api/executions/{id}` | Get execution status | +| `POST` | `/api/executions/{id}/cancel` | Cancel execution | +| `DELETE` | `/api/executions/{id}` | Delete execution record | + +### Model Endpoints + +| Method | Endpoint | Description | +|--------|----------|-------------| +| `GET` | `/api/models` | List configured models | +| `POST` | `/api/models/{name}/ping` | Health check model | +| `POST` | `/api/models/ping-all` | Health check all models | + +### Execute Workflow Example + +```bash +curl -X POST http://localhost:8000/api/workflows/my_workflow/execute \ + -H "Content-Type: application/json" \ + -d '{ + "input_data": [{"question": "What is machine learning?"}], + "num_records": 1, + "batch_size": 25 + }' +``` + +Response: +```json +{ + "execution_id": "exec_abc123", + "status": "running", + "message": "Execution started" +} +``` + +### Poll Execution Status + +```bash +curl http://localhost:8000/api/executions/exec_abc123 +``` + +Response: +```json +{ + "id": "exec_abc123", + "workflow_id": "my_workflow", + "status": "completed", + "started_at": "2026-01-19T10:30:00Z", + "completed_at": "2026-01-19T10:30:45Z", + "duration_ms": 45000, + "node_states": { + "llm_1": {"status": "completed", "duration_ms": 2500} + }, + "output_data": [{"response": "Machine learning is..."}] +} +``` + +--- + +## User Interface Overview + +### Home Dashboard + +When you first open SyGra Studio, you'll see the **Home Dashboard** with: + +- **Quick Actions**: Create new workflow, browse workflows, view runs, access template library +- **Stats Overview**: Success rate, total tokens used, total cost, average duration, running executions +- **Recent Workflows**: Quick access to your most recent workflows +- **Recent Activity**: Latest execution runs with status indicators + +### Sidebar Navigation + +The sidebar provides navigation to all major sections: + +| Section | Description | +|---------|-------------| +| **Home** | Dashboard with stats and quick actions | +| **Workflows** | Browse and manage all workflows | +| **Models** | Configure and test LLM connections | +| **Runs** | View execution history and analytics | +| **Library** | Browse workflow templates | + +The sidebar also shows a badge indicating the number of currently running executions. + +--- + +## Creating a Workflow + +### Step 1: Start a New Workflow + +1. Click the **"+ Create Workflow"** button in the sidebar (or on the Home dashboard) +2. The visual **Workflow Builder** opens with a blank canvas +3. Your workflow auto-saves to local storage as you work (key: `sygra_workflow_draft`) + +### Step 2: Understanding the Builder Interface + +The Workflow Builder has several key areas: + +| Area | Description | +|------|-------------| +| **Canvas** | Main drag-and-drop area for building your workflow graph | +| **Node Palette** | Panel on the left with available node types to drag onto canvas | +| **Toolbar** | Top bar with Undo/Redo, Save, Run, and layout controls | +| **Details Panel** | Right panel showing configuration for the selected node | +| **Minimap** | Small overview of your workflow (bottom-right) | + +### Step 3: Add Nodes to Your Workflow + +**Available Node Types:** + +| Node Type | Icon | Purpose | +|-----------|------|---------| +| **Data** | Database | Load input data from HuggingFace, local files, or ServiceNow | +| **LLM** | Bot | Call a language model with prompts | +| **Agent** | Bot (pink) | LLM with tool-calling capabilities | +| **Lambda** | Lightning | Run custom Python code | +| **Branch** | Git Branch | Conditional routing based on state | +| **Connector** | Link | Connect to external services | +| **Subgraph** | Boxes | Embed another workflow as a node | +| **Output** | Download | Define output generation and sinks | +| **Multi-LLM** | Compare | Run multiple LLMs and compare outputs | +| **Web Agent** | Globe | Browser automation with Playwright | + +**To add a node:** + +1. **Drag and drop**: Drag a node type from the palette onto the canvas +2. **Double-click**: Double-click on the canvas to open node type selector +3. **Context menu**: Right-click on the canvas for options + +### Step 4: Connect Nodes + +1. Hover over a node to see its **connection handles** (small circles on edges) +2. Click and drag from an **output handle** (right side) to an **input handle** (left side) of another node +3. Release to create the connection (edge) +4. To delete an edge, click on it to select, then press **Delete** or **Backspace** + +**Keyboard shortcuts:** + +| Shortcut | Action | +|----------|--------| +| `Ctrl/Cmd + Z` | Undo | +| `Ctrl/Cmd + Shift + Z` | Redo | +| `Ctrl/Cmd + S` | Save workflow | +| `Delete` / `Backspace` | Delete selected node or edge | +| `Escape` | Deselect / Close panel | + +### Step 5: Configure Node Details + +Click on any node to open the **Node Details Panel** on the right: + +#### Overview Tab +- **Node ID**: Unique identifier (auto-generated, editable) +- **Summary**: Display name shown on the node +- **Description**: Optional documentation + +#### Prompt Tab (LLM/Agent nodes) +- Add **System**, **User**, or **Assistant** messages +- Use `{variable_name}` syntax to reference state variables +- Drag to reorder prompts +- Variable autocomplete shows available state variables from upstream nodes + +#### Models Tab (Multi-LLM nodes) +- Add multiple model configurations +- Set temperature and max tokens per model +- Define post-processing logic + +#### Tools Tab (LLM/Agent nodes) +- Add tool paths (Python functions) +- Set tool choice: Auto, Required, or None +- Browse available tools from the Tool Library + +#### Code Tab (Lambda/Branch/Data/Output nodes) +- Monaco editor with Python syntax highlighting +- Pre-process and post-process hooks for execution nodes +- Transform functions for data nodes +- Output generator code for output nodes + +#### Settings Tab +- **Structured Output**: Enable JSON schema validation +- **Chat History**: Enable conversation memory +- **Output Keys**: Define state variables to output + +### Step 6: Configure Data Sources + +For **Data** nodes, configure where your input data comes from: + +| Source Type | Configuration | +|-------------|---------------| +| **HuggingFace** | Repo ID, Config Name, Split | +| **Local File** | File path (CSV, JSON, JSONL, Parquet) | +| **ServiceNow** | Table name, Query filters | + +**Preview your data:** + +1. Select the Data node +2. In the Details Panel, find the data source section +3. Click **"Preview"** to load sample records +4. Verify columns and data format before running + +### Step 7: Save Your Workflow + +1. Click the **"Save"** button in the toolbar (or `Ctrl/Cmd + S`) +2. If this is a new workflow, enter a **workflow name** (becomes the folder name) +3. Choose a **save location** within your tasks directory +4. Studio generates: + - `task.yaml` - Workflow configuration + - `task_executor.py` - Python code for custom functions + +--- + +## Running a Workflow + +### Step 1: Open the Run Dialog + +From the Workflow Builder or Workflow detail view: + +1. Click the **"▶ Run Workflow"** button in the toolbar +2. The **Run Workflow Modal** opens with configuration options + +### Step 2: Configure Execution Parameters + +#### Basic Parameters + +| Parameter | Description | Default | +|-----------|-------------|---------| +| **Number of Records** | How many records to process from the data source | 10 | +| **Start Index** | Which record to start from (0-indexed) | 0 | +| **Batch Size** | Records processed in each batch | 25 | +| **Run Name** | Custom name for this execution (for tracking) | Auto-generated | + +#### Advanced Options + +Expand the **"Advanced Options"** section for additional settings: + +| Option | Description | +|--------|-------------| +| **Debug Mode** | Enable verbose logging | +| **Resume** | Resume from a previous checkpoint | +| **Quality Mode** | Enable quality checks and validation | +| **Disable Metadata** | Skip metadata collection (faster execution) | +| **Checkpoint Interval** | How often to save progress (records) | +| **Custom Run Args** | JSON object with additional parameters | + +### Step 3: Preview Input Data + +Before running, you can preview the data that will be processed: + +1. Click **"Show Preview"** in the Run modal +2. View sample records from your data source +3. Verify the data looks correct + +### Step 4: Start Execution + +1. Click the **"▶ Run Workflow"** button in the modal +2. The execution starts immediately +3. You'll see the **Execution Panel** appear at the bottom of the screen + +### Step 5: Monitor Live Progress + +The **Execution Panel** shows real-time progress: + +- **Status indicator**: Running (blue pulse), Completed (green), Failed (red) +- **Progress bar**: Visual progress through nodes +- **Current node**: Which node is currently executing +- **Duration**: Elapsed time +- **Logs**: Expandable panel showing execution logs (click chevron to expand) + +**View Results:** + +When execution completes (or fails), click **"View Results"** to see: +- Output data records +- Execution logs +- Error details (if failed) + +--- + +## Monitoring Workflows + +### Accessing the Runs View + +1. Click **"Runs"** in the sidebar +2. The **Runs List View** shows all execution history + +### Runs List Features + +#### Filtering and Search + +| Filter | Options | +|--------|---------| +| **Search** | Search by workflow name or run ID | +| **Status** | All, Completed, Running, Failed, Cancelled, Pending | +| **Workflow** | Filter by specific workflow | +| **Date** | All Time, Today, Since Yesterday, Last 7 Days, Last 30 Days | + +#### Sorting + +Click column headers to sort by: +- Workflow name +- Status +- Started time +- Duration + +#### Bulk Actions + +1. Use checkboxes to select multiple runs +2. Available actions: + - **Delete**: Remove selected runs from history + - **Compare**: Compare metrics across selected runs (2+ runs) + +### View Modes + +Toggle between three view modes using the icons in the top-right: + +| Mode | Description | +|------|-------------| +| **Table** | Traditional list view with all runs | +| **Analytics** | Dashboard with charts and aggregate statistics | +| **Compare** | Side-by-side comparison of selected runs | + +### Run Details View + +Click on any run to see detailed information: + +#### Overview Tab +- **Quick stats**: Start time, duration, output records, run ID +- **Node execution states**: Status and duration for each node +- **Error details**: If the run failed, see the error message + +#### Output Tab +- **Sample records**: View first 5 output records +- **Copy All**: Copy full output to clipboard +- **Expandable records**: Click to expand individual records + +#### Logs Tab +- **Full execution logs**: Scrollable log viewer with line numbers +- **Syntax highlighting**: Different colors for log levels + +#### Metadata Tab (for completed runs) +Rich analytics including: + +- **Cost breakdown**: Total cost, cost per record +- **Token usage**: Prompt vs completion tokens, distribution chart +- **Success rate**: Records processed vs failed +- **Request statistics**: Total requests, failures + +**Interactive Charts:** +- Token distribution (donut chart) +- Model token usage (stacked bar chart) +- Node latency (horizontal bar chart with color coding) + +**Model Performance:** +- Per-model statistics (requests, latency, throughput, P95) +- Latency distribution visualization + +**Node Statistics:** +- Table with execution count, average latency, tokens per node + +### Run Comparison + +To compare multiple runs: + +1. Select 2 or more runs using checkboxes +2. Click the **Compare** icon in the toolbar +3. View side-by-side comparison: + - Status + - Duration + - Total tokens + - Cost + - Records processed + - Success rate + - Models used + - Per-model performance breakdown + +### Analytics Dashboard + +Switch to **Analytics** view for aggregate insights: + +- **Run distribution**: Pie chart by status +- **Duration trends**: Line chart over time +- **Cost analysis**: Breakdown by workflow +- **Token usage patterns**: Over time and by model + +--- + +## Common Workflows + +### Add an LLM Node + +1. Drag **"LLM"** node onto canvas +2. In details panel: + - Set **Summary** (display name) + - Select **Model** from dropdown + - Add **System** and **User** prompts using `{variable}` syntax + - Set **Output Key** for the state variable to store the response +3. Connect to previous/next nodes + +### Configure Structured Output + +1. Select an LLM node +2. Go to **Settings** tab +3. Enable **"Structured Output"** +4. Choose schema mode: + - **Inline**: Define fields directly with name, type, description + - **Class Path**: Reference a Pydantic model (e.g., `mymodule.schemas.MyOutput`) +5. Set **Fallback Strategy**: + - `instruction`: Add schema to prompt (more reliable) + - `post_process`: Validate after generation +6. Configure retry settings for parse errors + +### Create a Data Pipeline + +1. Add a **Data** node → connect to **LLM** node → connect to **Output** node +2. Configure Data node with your source (HuggingFace, file, etc.) +3. Configure LLM node with prompts referencing data columns: `{column_name}` +4. Configure Output node with output mappings or generator code +5. Save and run + +### Use Pre/Post Processors + +For any execution node (LLM, Lambda, etc.): + +1. Select the node +2. Go to **Code** tab +3. Add **Pre-processor**: Modify state before node execution +4. Add **Post-processor**: Transform the node's response + +Example pre-processor: +```python +class MyPreProcessor(NodePreProcessor): + def apply(self, state: SygraState) -> SygraState: + state["formatted_input"] = state["raw_input"].upper() + return state ``` -By default, the app will be available at: http://localhost:8501 -### Steps to create task +### Group Nodes as Subgraph + +1. Select multiple nodes (Shift+click or drag to select) +2. Click **"Group as Subgraph"** in the toolbar +3. Enter a name for the subgraph +4. The selected nodes become a single subgraph node +5. Double-click the subgraph to edit its contents -The Streamlit-based user interface provides a comprehensive set of tools to manage models and configure task flows in an interactive manner. Below are the key features: +--- -#### 1. Model Management -Users can view all registered models along with their current status (active or inactive). The interface allows manual refreshing of model statuses to ensure accuracy. Additionally, users can register new models by providing essential details such as base URL, model name, type, and any custom configuration parameters. +## Operational Guide -#### 2. Review Existing Tasks -Users can explore previously defined task flows through an interactive visual interface. This includes: -- Viewing the task's directed graph structure -- Inspecting individual node configurations -- Understanding the data flow and logic for each task +### Execution Storage -#### 3. Create a New Task Flow from Scratch -The UI guides users through the complete process of creating a new task flow: -- Filling in `data_config` parameters -- Constructing the task graph by defining nodes and edges -- Defining the `output_config` section -- Automatically generating the required `graph_config.yaml` and `task_executor.py` files -- Reviewing and publishing the complete task setup +Executions are stored in `studio/.executions/`: -#### 4. Create a New Task Flow Based on Existing Flows -Users can use existing task flows as templates, modify them as needed, and publish new customized task flows. This streamlines the task creation process by leveraging previously defined components. +``` +.executions/ +├── index.json # Metadata index (loaded on startup) +└── runs/ + ├── exec_abc123.json + ├── exec_def456.json + └── ... +``` ---- \ No newline at end of file +**Index refresh**: If the index becomes stale: +```bash +curl -X POST http://localhost:8000/api/executions/storage/refresh +``` diff --git a/mkdocs.yml b/mkdocs.yml index be4c55a4..35a2ee0a 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -55,7 +55,7 @@ nav: - Graph Config Guide: getting_started/graph_config_guide.md - Adding Edges: getting_started/adding_edges.md - Create a Synthetic Datagen Pipeline: getting_started/create_new_pipeline.md - - Create a task with UI: getting_started/create_task_ui.md + - SyGra Studio: getting_started/create_task_ui.md - Concepts: - Data Handler: - Overview: concepts/data_handler/README.md diff --git a/pyproject.toml b/pyproject.toml index 060e5b2c..c707ccf4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -68,14 +68,6 @@ dependencies = [ ] [project.optional-dependencies] -ui = [ - "streamlit>=1.49,<2.0; python_version != '3.9.7'", - "streamlit-agraph>=0.0,<1.0", - "streamlit-autorefresh>=1.0,<2.0", - "streamlit-flow-component>=1.6,<2.0", - "watchdog>=6.0,<7.0", - "plotly>=6.5.0" -] dev = [ "pytest>=8.4,<9.0", "pytest-asyncio>=1.1,<2.0", diff --git a/run_ui.sh b/run_ui.sh deleted file mode 100755 index 1f47cdd2..00000000 --- a/run_ui.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/bash - -# Default port is 8501, override with: ./run_ui.sh 8502 -PORT=${1:-8501} - -# Run the Streamlit app -streamlit run apps/sygra_app.py --server.port=$PORT diff --git a/studio/frontend/src/lib/components/builder/WorkflowBuilder.svelte b/studio/frontend/src/lib/components/builder/WorkflowBuilder.svelte index 4e9c46be..94557e24 100644 --- a/studio/frontend/src/lib/components/builder/WorkflowBuilder.svelte +++ b/studio/frontend/src/lib/components/builder/WorkflowBuilder.svelte @@ -878,7 +878,7 @@ saveDraft(); // Show brief visual feedback const toast = document.createElement('div'); - toast.className = 'fixed bottom-4 right-4 px-4 py-2 bg-green-600 text-white rounded-lg shadow-lg z-50 animate-fade-in'; + toast.className = 'fixed bottom-4 right-4 px-4 py-2 bg-success text-white rounded-lg shadow-lg z-50 animate-fade-in'; toast.textContent = '✓ Draft saved'; document.body.appendChild(toast); setTimeout(() => toast.remove(), 2000); @@ -1137,28 +1137,28 @@ -
+
-
+
-
+
{#if hasChanges} - + Unsaved changes {/if} {#if lastAutoSave} - + Draft saved {lastAutoSave.toLocaleTimeString()} {/if} @@ -1170,12 +1170,12 @@ -
+
{/if} {#if selectedNodeIds.length >= 2} @@ -1183,25 +1183,25 @@ onclick={() => showGroupModal = true} disabled={!canGroupNodes()} title={canGroupNodes() ? `Group ${selectedNodeIds.length} nodes as subgraph (⌘G)` : 'Cannot group: includes START, END, Data, or Output nodes'} - class="flex items-center gap-2 px-3 py-2 bg-blue-600 hover:bg-blue-700 disabled:bg-gray-300 dark:disabled:bg-gray-600 text-white rounded-lg transition-colors disabled:cursor-not-allowed" + class="flex items-center gap-2 px-3 py-2 bg-info hover:bg-info-dark disabled:bg-surface-tertiary text-white rounded-lg transition-colors disabled:cursor-not-allowed" > Group ({selectedNodeIds.length}) -
+
{/if} -
+
@@ -1249,21 +1249,21 @@ {#each NODE_CATEGORIES as category} {@const categoryNodes = groupedNodeTypes()[category.id]} {#if categoryNodes && categoryNodes.length > 0} -
+
@@ -1280,7 +1280,7 @@ role="option" aria-selected={draggedNodeType === nodeType.type} tabindex="0" - class="flex items-center gap-2.5 p-2 rounded-lg border border-dashed border-gray-200 dark:border-gray-600 hover:border-[#52B8FF] dark:hover:border-[#52B8FF] cursor-grab active:cursor-grabbing transition-colors group {draggedNodeType === nodeType.type ? 'border-[#52B8FF] bg-sky-50 dark:bg-sky-900/20' : ''}" + class="flex items-center gap-2.5 p-2 rounded-lg border border-dashed border-surface-border hover:border-info cursor-grab active:cursor-grabbing transition-colors group {draggedNodeType === nodeType.type ? 'border-info bg-info-light' : ''}" >
-
+
{nodeType.label}
-
+
{nodeType.description}
- +
{/each}
@@ -1308,18 +1308,18 @@ {#if paletteSearch && filteredNodeTypes().length === 0} -
+

No nodes match "{paletteSearch}"

{/if} -
-

+
+

Tips

-
+

• Connect by dragging handle to handle

⌘/Ctrl+Click to multi-select

Delete/Backspace to delete selected

@@ -1385,19 +1385,19 @@ {#if showMinimap} -
+
t.type === node.type); return nodeType?.color ?? '#6b7280'; }} - bgColor={isDarkMode ? '#1e293b' : '#ffffff'} - maskColor={isDarkMode ? 'rgba(30, 41, 59, 0.6)' : 'rgba(240, 240, 240, 0.6)'} - maskStrokeColor={isDarkMode ? '#475569' : '#cbd5e1'} + bgColor={isDarkMode ? '#032D42' : '#ffffff'} + maskColor={isDarkMode ? 'rgba(3, 45, 66, 0.6)' : 'rgba(240, 240, 240, 0.6)'} + maskStrokeColor={isDarkMode ? '#0a4560' : '#cbd5e1'} maskStrokeWidth={1} pannable={true} zoomable={true} @@ -1422,8 +1422,8 @@ {#if draggedNodeType} -
-
+
+
Drop to add {NODE_TYPES.find(t => t.type === draggedNodeType)?.label} node
@@ -1433,7 +1433,7 @@ {#if selectedNodeIds.length >= 2 || selectedEdgeIds.length > 0} {@const totalSelected = selectedNodeIds.length + selectedEdgeIds.length} {@const canDelete = selectedNodeIds.filter(id => id !== 'START' && id !== 'END').length > 0 || selectedEdgeIds.length > 0} -
+
{#if selectedNodeIds.length > 0 && selectedEdgeIds.length > 0} @@ -1444,27 +1444,27 @@ {selectedEdgeIds.length} edge{selectedEdgeIds.length > 1 ? 's' : ''} selected {/if}
-
+
{#if selectedNodeIds.length >= 2 && canGroupNodes()} -
+
{/if} {#if canDelete} {/if} - Esc to clear + Esc to clear
{/if}
@@ -1529,29 +1529,29 @@ aria-modal="true" >
e.stopPropagation()} > -
-

+
+

Start New Workflow?

-

+

You have unsaved changes. Starting a new workflow will discard your current work.

-
+
diff --git a/studio/frontend/src/lib/components/common/ConfirmModal.svelte b/studio/frontend/src/lib/components/common/ConfirmModal.svelte index b9ec433d..abf7151a 100644 --- a/studio/frontend/src/lib/components/common/ConfirmModal.svelte +++ b/studio/frontend/src/lib/components/common/ConfirmModal.svelte @@ -32,23 +32,23 @@ const variantStyles = { danger: { icon: Trash2, - iconBg: 'bg-red-100 dark:bg-red-900/30', - iconColor: 'text-red-600 dark:text-red-400', - buttonBg: 'bg-red-600 hover:bg-red-700', + iconBg: 'bg-error-light', + iconColor: 'text-error', + buttonBg: 'bg-error hover:bg-error/90', buttonText: 'text-white' }, warning: { icon: AlertTriangle, - iconBg: 'bg-amber-100 dark:bg-amber-900/30', - iconColor: 'text-amber-600 dark:text-amber-400', - buttonBg: 'bg-amber-600 hover:bg-amber-700', + iconBg: 'bg-warning-light', + iconColor: 'text-warning', + buttonBg: 'bg-warning hover:bg-warning/90', buttonText: 'text-white' }, info: { icon: Check, - iconBg: 'bg-blue-100 dark:bg-blue-900/30', - iconColor: 'text-blue-600 dark:text-blue-400', - buttonBg: 'bg-blue-600 hover:bg-blue-700', + iconBg: 'bg-info-light', + iconColor: 'text-info', + buttonBg: 'bg-info hover:bg-info/90', buttonText: 'text-white' } }; @@ -64,22 +64,22 @@ onclick={() => dispatch('cancel')} >
e.stopPropagation()} > -
+
-

+

{title}

@@ -87,16 +87,16 @@
-

+

{message}

-
+
diff --git a/studio/frontend/src/lib/components/common/ConfirmationModal.svelte b/studio/frontend/src/lib/components/common/ConfirmationModal.svelte index f3350aad..275f4f90 100644 --- a/studio/frontend/src/lib/components/common/ConfirmationModal.svelte +++ b/studio/frontend/src/lib/components/common/ConfirmationModal.svelte @@ -34,21 +34,21 @@ // Variant styles const variantStyles = { danger: { - iconBg: 'bg-red-100 dark:bg-red-900/30', - iconColor: 'text-red-600 dark:text-red-400', - buttonBg: 'bg-red-600 hover:bg-red-700', + iconBg: 'bg-error-light', + iconColor: 'text-error', + buttonBg: 'bg-error hover:bg-error/90', buttonText: 'text-white' }, warning: { - iconBg: 'bg-amber-100 dark:bg-amber-900/30', - iconColor: 'text-amber-600 dark:text-amber-400', - buttonBg: 'bg-amber-600 hover:bg-amber-700', + iconBg: 'bg-warning-light', + iconColor: 'text-warning', + buttonBg: 'bg-warning hover:bg-warning/90', buttonText: 'text-white' }, info: { - iconBg: 'bg-blue-100 dark:bg-blue-900/30', - iconColor: 'text-blue-600 dark:text-blue-400', - buttonBg: 'bg-blue-600 hover:bg-blue-700', + iconBg: 'bg-info-light', + iconColor: 'text-info', + buttonBg: 'bg-info hover:bg-info/90', buttonText: 'text-white' } }; @@ -67,7 +67,7 @@ role="presentation" >
e.stopPropagation()} role="dialog" aria-modal="true" @@ -79,26 +79,26 @@
-

+

{title}

-

+

{message}

-
+
diff --git a/studio/frontend/src/lib/components/common/CustomSelect.svelte b/studio/frontend/src/lib/components/common/CustomSelect.svelte index c5937c5e..7a5a31ae 100644 --- a/studio/frontend/src/lib/components/common/CustomSelect.svelte +++ b/studio/frontend/src/lib/components/common/CustomSelect.svelte @@ -143,19 +143,19 @@ onkeydown={handleKeydown} disabled={disabled} class="w-full flex items-center justify-between gap-2 text-left transition-all - {compact ? 'px-2 py-1 text-xs rounded-md border-0 bg-transparent' : 'px-3 py-2 text-sm border border-gray-300 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-800'} - {disabled ? 'opacity-50 cursor-not-allowed' : compact ? 'hover:bg-gray-100 dark:hover:bg-gray-700' : 'hover:border-[#52B8FF] dark:hover:border-[#7661FF] focus:ring-2 focus:ring-[#52B8FF] focus:border-[#52B8FF]'} - {isOpen && !compact ? 'ring-2 ring-[#52B8FF] border-[#52B8FF]' : ''}" + {compact ? 'px-2 py-1 text-xs rounded-md border-0 bg-transparent' : 'px-3 py-2 text-sm border border-surface-border rounded-lg bg-surface'} + {disabled ? 'opacity-50 cursor-not-allowed' : compact ? 'hover:bg-surface-hover' : 'hover:border-info focus:ring-2 focus:ring-info focus:border-info'} + {isOpen && !compact ? 'ring-2 ring-info border-info' : ''}" > - + {#if selectedOption} {#if selectedOption.icon} - + {/if} {selectedOption.label} {#if selectedOption.subtitle && !compact} - ({selectedOption.subtitle}) + ({selectedOption.subtitle}) {/if} {:else} @@ -164,7 +164,7 @@ @@ -172,26 +172,26 @@ {#if isOpen}
{#if searchable} -
+
- + {#if searchQuery} @@ -203,7 +203,7 @@
{#if filteredOptions.length === 0} -
+
No options found
{:else} @@ -214,24 +214,24 @@ onclick={() => selectOption(opt)} onmouseenter={() => highlightedIndex = index} class="w-full flex items-center gap-2 px-3 py-2 text-left text-sm transition-colors - {opt.value === value ? 'bg-[#7661FF]/10 dark:bg-[#7661FF]/20 text-[#7661FF] dark:text-[#52B8FF]' : ''} - {index === highlightedIndex ? 'bg-gray-100 dark:bg-gray-700' : 'hover:bg-gray-50 dark:hover:bg-gray-700/50'}" + {opt.value === value ? 'bg-info-light text-info' : ''} + {index === highlightedIndex ? 'bg-surface-hover' : 'hover:bg-surface-secondary'}" > {#if opt.icon} - + {/if}
-
+
{opt.label}
{#if opt.subtitle} -
+
{opt.subtitle}
{/if}
{#if opt.value === value} - + {/if} {/each} diff --git a/studio/frontend/src/lib/components/common/SelectionCard.svelte b/studio/frontend/src/lib/components/common/SelectionCard.svelte index d05227a8..a4003fe9 100644 --- a/studio/frontend/src/lib/components/common/SelectionCard.svelte +++ b/studio/frontend/src/lib/components/common/SelectionCard.svelte @@ -32,11 +32,11 @@ class="group relative flex flex-col rounded-xl border-2 transition-all duration-200 overflow-hidden text-left w-full {disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'} {selected - ? 'border-[#7661FF] shadow-lg shadow-[#7661FF]/20 dark:shadow-[#7661FF]/10' - : 'border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600 hover:shadow-md'}" + ? 'border-info shadow-lg shadow-info/20' + : 'border-surface-border hover:border-info/50 hover:shadow-md'}" > -
+
{#if Icon}
@@ -48,24 +48,24 @@
-
+
{#if Icon} {/if}
- + {label} {#if description && !compact} - + {description} {/if}
{#if selected} -
+
{/if} diff --git a/studio/frontend/src/lib/components/data/SourceCard.svelte b/studio/frontend/src/lib/components/data/SourceCard.svelte index d655076c..c0a985ec 100644 --- a/studio/frontend/src/lib/components/data/SourceCard.svelte +++ b/studio/frontend/src/lib/components/data/SourceCard.svelte @@ -68,7 +68,7 @@ dispatch('previewRefresh', e.detail); } - // Source type configurations + // Source type configurations using design tokens const sourceTypeConfig: Record; label: string; @@ -79,39 +79,39 @@ hf: { icon: Cloud, label: 'HuggingFace', - color: 'amber', - bgClass: 'bg-amber-100 dark:bg-amber-900/30', - iconClass: 'text-amber-600 dark:text-amber-400' + color: 'warning', + bgClass: 'bg-warning-light', + iconClass: 'text-warning' }, servicenow: { icon: Server, label: 'ServiceNow', - color: 'emerald', - bgClass: 'bg-emerald-100 dark:bg-emerald-900/30', - iconClass: 'text-emerald-600 dark:text-emerald-400' + color: 'success', + bgClass: 'bg-success-light', + iconClass: 'text-success' }, disk: { icon: HardDrive, label: 'Local File', - color: 'blue', - bgClass: 'bg-blue-100 dark:bg-blue-900/30', - iconClass: 'text-blue-600 dark:text-blue-400' + color: 'info', + bgClass: 'bg-info-light', + iconClass: 'text-info' }, memory: { icon: MemoryStick, label: 'In Memory', - color: 'indigo', - bgClass: 'bg-[#BF71F2]/15 dark:bg-[#BF71F2]/20', - iconClass: 'text-[#BF71F2] dark:text-[#BF71F2]' + color: 'node-agent', + bgClass: 'bg-node-agent-bg', + iconClass: 'text-node-agent' } }; let config = $derived(sourceTypeConfig[source.type || ''] || { icon: Database, label: 'Unknown', - color: 'gray', - bgClass: 'bg-gray-100 dark:bg-gray-800', - iconClass: 'text-gray-500 dark:text-gray-400' + color: 'muted', + bgClass: 'bg-surface-tertiary', + iconClass: 'text-text-muted' }); let Icon = $derived(config.icon); @@ -152,10 +152,10 @@
-
+ ? 'border-warning' + : (showPreview ? 'border-info' : 'border-surface-border hover:border-[var(--border-hover)]')}">
@@ -169,11 +169,11 @@
{#if isPrimary} - + {/if} - + {#if showAlias && source.alias} {source.alias} {:else} @@ -183,14 +183,14 @@ {#if showAlias && source.alias} - + {config.label} {/if}
-
+
{getSourceDetail()}
@@ -199,7 +199,7 @@ {#if secondaryBadges.length > 0}
{#each secondaryBadges.slice(0, 2) as badge} - + {badge} {/each} @@ -220,7 +220,7 @@ {#if !isPrimary}
{#if outputKeyError} -

+

{outputKeyError}

@@ -2406,8 +2406,8 @@ class ${dataClassName}Transform(DataTransform): {/if} -

- Use {'{'}key_name{'}'} in downstream prompts to reference output values. +

+ Use {'{'}key_name{'}'} in downstream prompts to reference output values.

{/if} @@ -2415,10 +2415,10 @@ class ${dataClassName}Transform(DataTransform): {#if node.node_type === 'lambda' && node.function_path}
-
+
Function Path
-
+
{node.function_path}
@@ -2427,10 +2427,10 @@ class ${dataClassName}Transform(DataTransform): {#if node.node_type === 'subgraph' && node.subgraph_path}
-
+
Subgraph Path
-
+
{node.subgraph_path}
@@ -2480,9 +2480,9 @@ class ${dataClassName}Transform(DataTransform):
{#if isEditing} -
+
-
+
Output Mappings
@@ -2498,30 +2498,30 @@ class ${dataClassName}Transform(DataTransform): {#if editOutputMappings.length > 0}
{#each editOutputMappings as mapping, idx} -
+
- Mapping {idx + 1} + Mapping {idx + 1}
- Output Key * + Output Key * updateOutputMapping(idx, 'key', e.currentTarget.value)} placeholder="field_name" aria-label="Output key" - class="w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-800 dark:text-gray-200 font-mono focus:ring-2 focus:ring-[#52B8FF]" + class="w-full px-3 py-2 text-sm border border-surface-border rounded-lg bg-surface text-text-primary font-mono focus:ring-2 focus:ring-info" />
- From State + From State
- Static Value (JSON) + Static Value (JSON) updateOutputMapping(idx, 'value', e.currentTarget.value)} placeholder="JSON value" aria-label="Static value" - class="w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-800 dark:text-gray-200 font-mono focus:ring-2 focus:ring-[#52B8FF]" + class="w-full px-3 py-2 text-sm border border-surface-border rounded-lg bg-surface text-text-primary font-mono focus:ring-2 focus:ring-info" />
- Transform Function + Transform Function updateOutputMapping(idx, 'transform', e.currentTarget.value)} placeholder="transform_func" aria-label="Transform function" - class="w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-800 dark:text-gray-200 font-mono focus:ring-2 focus:ring-[#52B8FF]" + class="w-full px-3 py-2 text-sm border border-surface-border rounded-lg bg-surface text-text-primary font-mono focus:ring-2 focus:ring-info" />
@@ -2558,7 +2558,7 @@ class ${dataClassName}Transform(DataTransform): {/each}
{:else} -
+
No output mappings. Click "Add Mapping" to configure output fields.
{/if} @@ -2566,15 +2566,15 @@ class ${dataClassName}Transform(DataTransform): {:else} {#if node.output_config?.generator} -
+
Generator Class
-
+
{node.output_config.generator.split('.').pop()}
-
+
{node.output_config.generator}
@@ -2582,30 +2582,30 @@ class ${dataClassName}Transform(DataTransform): {#if outputKeys.length > 0} -
-
+
+
Output Mappings ({outputKeys.length})
{#each outputKeys as key} {@const mapping = outputMap[key]} -
+
- + {key.length > 15 ? key.slice(0, 15) + '...' : key} - + {#if mapping.from} {mapping.from} {:else if mapping.value !== undefined} - static + static {/if} {#if mapping.transform}
{#if mapping.transform && transformCodeExpanded[key]} -
+
{#if transformCodeLoading[key]} -
- - Loading... +
+ + Loading...
{:else if transformCodeMap[key]}
@@ -2637,7 +2637,7 @@ class ${dataClassName}Transform(DataTransform): />
{:else} -
+
Transform function not found
{/if} @@ -2648,7 +2648,7 @@ class ${dataClassName}Transform(DataTransform):
{:else} -
+
No output mappings configured.
{/if} @@ -2661,7 +2661,7 @@ class ${dataClassName}Transform(DataTransform): {@const attributes = node.sampler_config?.attributes || {}} {@const attributeNames = Object.keys(attributes)}
-
+
Sampler Attributes
@@ -2685,37 +2685,37 @@ class ${dataClassName}Transform(DataTransform): {#if editSamplerAttributes.length > 0}
{#each editSamplerAttributes as attr, idx} -
+
- Attribute {idx + 1} + Attribute {idx + 1}
- Name + Name updateSamplerAttribute(idx, 'name', e.currentTarget.value)} placeholder="num_turns" aria-label="Attribute name" - class="w-full px-2 py-1 text-xs border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-900 text-gray-800 dark:text-gray-200 font-mono" + class="w-full px-2 py-1 text-xs border border-surface-border rounded bg-surface text-text-primary font-mono" />
- Values (comma-separated) + Values (comma-separated) updateSamplerAttribute(idx, 'values', e.currentTarget.value)} placeholder="2, 3, 4, 5 or professional, casual, friendly" aria-label="Attribute values" - class="w-full px-2 py-1 text-xs border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-900 text-gray-800 dark:text-gray-200 font-mono" + class="w-full px-2 py-1 text-xs border border-surface-border rounded bg-surface text-text-primary font-mono" />
@@ -2723,7 +2723,7 @@ class ${dataClassName}Transform(DataTransform): {/each}
{:else} -
+
No attributes defined. Click "Add" to create sampler attributes.
{/if} @@ -2738,14 +2738,14 @@ class ${dataClassName}Transform(DataTransform):
{#each attributeNames as name} {@const attr = attributes[name]} -
+
- {name} + {name} {attr.values?.length ?? 0} values
-
+
{attr.values?.slice(0, 5).join(', ')}{attr.values?.length > 5 ? '...' : ''}
@@ -2753,7 +2753,7 @@ class ${dataClassName}Transform(DataTransform):
{:else} -
No attributes configured. Click Edit to add attributes.
+
No attributes configured. Click Edit to add attributes.
{/if} {/if}
@@ -2762,36 +2762,36 @@ class ${dataClassName}Transform(DataTransform): {#if nodeState}
-
+
Execution Status
- Status + Status {nodeState.status}
{#if nodeState.duration_ms}
- Duration - + Duration + {nodeState.duration_ms}ms
{/if} {#if nodeState.error} -
+
{nodeState.error}
{/if} @@ -2806,7 +2806,7 @@ class ${dataClassName}Transform(DataTransform):
{#if isEditing} -
+
@@ -2814,11 +2814,11 @@ class ${dataClassName}Transform(DataTransform):
-

Variables

-

Type {'{'} to autocomplete

+

Variables

+

Type {'{'} to autocomplete

- + {availableStateVariables().variables.length}
@@ -2827,12 +2827,12 @@ class ${dataClassName}Transform(DataTransform): {#if availableStateVariables().variables.length > 5}
- +
@@ -2847,8 +2847,8 @@ class ${dataClassName}Transform(DataTransform): {#if vars.bySource.data.length > 0}
- - Data + + Data
{#each vars.bySource.data as variable} @@ -2856,18 +2856,18 @@ class ${dataClassName}Transform(DataTransform): onclick={() => copyVariable(variable)} class="group relative inline-flex items-center gap-1.5 px-2.5 py-1 text-xs font-mono rounded-lg transition-all duration-200 {copiedVariable === variable.name - ? 'bg-green-100 dark:bg-green-900/50 text-green-700 dark:text-green-300 ring-2 ring-green-500/30' - : 'bg-white dark:bg-gray-800 text-blue-700 dark:text-blue-300 border border-blue-200 dark:border-blue-800/50 hover:border-blue-400 dark:hover:border-blue-600 hover:shadow-sm hover:shadow-blue-100 dark:hover:shadow-blue-900/20' + ? 'bg-success-light dark:bg-success/30 text-success dark:text-success ring-2 ring-success/30' + : 'bg-surface text-info dark:text-info border border-info-border dark:border-info/30 hover:border-info dark:hover:border-info hover:shadow-sm hover:shadow-info/10 dark:hover:shadow-info/20' }" title={variable.description || `Copy {${variable.name}}`} > {#if copiedVariable === variable.name} - + Copied! {:else} - {'{'} + {'{'} {variable.name} - {'}'} + {'}'} {/if} {/each} @@ -2879,8 +2879,8 @@ class ${dataClassName}Transform(DataTransform): {#if vars.bySource.output.length > 0}
- - Outputs + + Outputs
{#each vars.bySource.output as variable} @@ -2888,20 +2888,20 @@ class ${dataClassName}Transform(DataTransform): onclick={() => copyVariable(variable)} class="group relative inline-flex items-center gap-1 px-2.5 py-1 text-xs font-mono rounded-lg transition-all duration-200 {copiedVariable === variable.name - ? 'bg-green-100 dark:bg-green-900/50 text-green-700 dark:text-green-300 ring-2 ring-green-500/30' - : 'bg-white dark:bg-gray-800 text-emerald-700 dark:text-emerald-300 border border-emerald-200 dark:border-emerald-800/50 hover:border-emerald-400 dark:hover:border-emerald-600 hover:shadow-sm hover:shadow-emerald-100 dark:hover:shadow-emerald-900/20' + ? 'bg-success-light dark:bg-success/30 text-success dark:text-success ring-2 ring-success/30' + : 'bg-surface text-success dark:text-success border border-success-border dark:border-success/30 hover:border-success dark:hover:border-success hover:shadow-sm hover:shadow-success/10 dark:hover:shadow-success/20' }" title="{variable.description || `From ${variable.sourceNode}`}" > {#if copiedVariable === variable.name} - + Copied! {:else} - {'{'} + {'{'} {variable.name} - {'}'} + {'}'} {#if variable.sourceNode} - + {variable.sourceNode} {/if} @@ -2916,8 +2916,8 @@ class ${dataClassName}Transform(DataTransform): {#if vars.bySource.sampler.length > 0}
- - Sampler + + Sampler
{#each vars.bySource.sampler as variable} @@ -2925,18 +2925,18 @@ class ${dataClassName}Transform(DataTransform): onclick={() => copyVariable(variable)} class="group relative inline-flex items-center gap-1 px-2.5 py-1 text-xs font-mono rounded-lg transition-all duration-200 {copiedVariable === variable.name - ? 'bg-green-100 dark:bg-green-900/50 text-green-700 dark:text-green-300 ring-2 ring-green-500/30' - : 'bg-white dark:bg-gray-800 text-purple-700 dark:text-purple-300 border border-purple-200 dark:border-purple-800/50 hover:border-purple-400 dark:hover:border-purple-600 hover:shadow-sm hover:shadow-purple-100 dark:hover:shadow-purple-900/20' + ? 'bg-success-light dark:bg-success/30 text-success dark:text-success ring-2 ring-success/30' + : 'bg-surface text-node-llm dark:text-node-llm border border-node-llm/30 dark:border-node-llm/40 hover:border-node-llm dark:hover:border-node-llm hover:shadow-sm hover:shadow-node-llm/10 dark:hover:shadow-node-llm/20' }" title="{variable.description || `Copy {${variable.name}}`}" > {#if copiedVariable === variable.name} - + Copied! {:else} - {'{'} + {'{'} {variable.name} - {'}'} + {'}'} {/if} {/each} @@ -2949,7 +2949,7 @@ class ${dataClassName}Transform(DataTransform):
- Framework + Framework
{#each vars.bySource.framework as variable} @@ -2957,13 +2957,13 @@ class ${dataClassName}Transform(DataTransform): onclick={() => copyVariable(variable)} class="group relative inline-flex items-center gap-1 px-2.5 py-1 text-xs font-mono rounded-lg transition-all duration-200 {copiedVariable === variable.name - ? 'bg-green-100 dark:bg-green-900/50 text-green-700 dark:text-green-300 ring-2 ring-green-500/30' - : 'bg-white dark:bg-gray-800 text-slate-600 dark:text-slate-300 border border-slate-200 dark:border-slate-700/50 hover:border-slate-400 dark:hover:border-slate-600 hover:shadow-sm hover:shadow-slate-100 dark:hover:shadow-slate-900/20' + ? 'bg-success-light dark:bg-success/30 text-success dark:text-success ring-2 ring-success/30' + : 'bg-surface text-slate-600 dark:text-slate-300 border border-slate-200 dark:border-slate-700/50 hover:border-slate-400 dark:hover:border-slate-600 hover:shadow-sm hover:shadow-slate-100 dark:hover:shadow-slate-900/20' }" title="{variable.description}" > {#if copiedVariable === variable.name} - + Copied! {:else} {'{'} @@ -2980,17 +2980,17 @@ class ${dataClassName}Transform(DataTransform): {#if vars.variables.length === 0}
{#if variableFilter} -

+

No variables matching "{variableFilter}"

{:else}
-
- +
+
-

No variables yet

-

Add a data source or upstream nodes with output_keys

+

No variables yet

+

Add a data source or upstream nodes with output_keys

{/if} @@ -3003,17 +3003,17 @@ class ${dataClassName}Transform(DataTransform): {#if promptValidation().errors.length > 0} -
+
-
- +
+
-

+

{promptValidation().errors.length === 1 ? 'Undefined Variable' : `${promptValidation().errors.length} Undefined Variables`}

-

+

The following variables are referenced but not available in this node's context:

@@ -3022,11 +3022,11 @@ class ${dataClassName}Transform(DataTransform):
{#each promptValidation().invalidReferences as ref} -
- +
+ {'{' + ref.name + '}'} - + is not defined
@@ -3034,8 +3034,8 @@ class ${dataClassName}Transform(DataTransform):
-
-

+

+

How to fix: Ensure the variable is either a data column, an output_key from an upstream node, or a framework variable. Check that upstream nodes are connected and have the correct output_keys configured.

@@ -3044,8 +3044,8 @@ class ${dataClassName}Transform(DataTransform): {#if isEditing} {#each editPrompts as message, index} -
-
+
+
- + {#if isMultiModalContent(message.content)} -
+
{#each message.content as part, partIndex} -
+
{getMultiModalPartLabel(part.type)} @@ -3096,7 +3096,7 @@ class ${dataClassName}Transform(DataTransform): rows={3} placeholder={'Enter text content... Type \'{\' for variable autocomplete'} oninput={(val) => updateMultiModalPart(index, partIndex, val)} - class="border border-gray-200 dark:border-gray-700 rounded" + class="border border-surface-border rounded" /> {:else}
{/each} - +
@@ -3161,27 +3161,27 @@ class ${dataClassName}Transform(DataTransform):
{:else} {#each node.prompt ?? [] as message} -
-
+
+
{message.role} @@ -3191,12 +3191,12 @@ class ${dataClassName}Transform(DataTransform): {/if}
-
+
{#if isMultiModalContent(message.content)}
{#each message.content as part} -
+
{getMultiModalPartLabel(part.type)} @@ -3216,7 +3216,7 @@ class ${dataClassName}Transform(DataTransform): {/each} {#if !node.prompt?.length} -
+
No prompts defined for this node
{/if} @@ -3224,20 +3224,20 @@ class ${dataClassName}Transform(DataTransform): {#if node.node_type === 'llm' || node.node_type === 'agent'} -
+
- - Multi-Turn Conversations + + Multi-Turn Conversations
{#if isEditing} {:else} - + {node.chat_history ? 'Enabled' : 'Disabled'} {/if} @@ -3255,15 +3255,15 @@ class ${dataClassName}Transform(DataTransform): {#if isEditing && editChatHistoryEnabled} -

+

Track conversation history across turns. Inject system messages at specific turns to guide the conversation flow.

- Turn-Based System Injections - + Turn-Based System Injections + {editInjectSystemMessages.length} configured
@@ -3289,12 +3289,12 @@ class ${dataClassName}Transform(DataTransform): value={msg.message} oninput={(e) => updateSystemMessageInjection(msg.id, 'message', e.currentTarget.value)} placeholder="System message content..." - class="w-full px-3 py-1.5 text-sm border border-gray-200 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-800 text-gray-800 dark:text-gray-200 focus:ring-2 focus:ring-[#52B8FF] focus:border-transparent" + class="w-full px-3 py-1.5 text-sm border border-surface-border rounded-lg bg-surface text-text-primary focus:ring-2 focus:ring-info focus:border-transparent" />
@@ -3336,7 +3336,7 @@ class ${dataClassName}Transform(DataTransform): {:else if !isEditing && node.chat_history} -

+

Conversation history is tracked across turns.

{#if node.inject_system_messages && node.inject_system_messages.length > 0} @@ -3348,16 +3348,16 @@ class ${dataClassName}Transform(DataTransform): Turn {turn} - {message} + {message}
{/each}
{:else} -
No turn-based injections configured
+
No turn-based injections configured
{/if} {:else if !isEditing} -

+

Enable to track conversation history and inject messages at specific turns.

{/if} @@ -3372,11 +3372,11 @@ class ${dataClassName}Transform(DataTransform):
- + Parallel Models {#if isEditing} - + {editMultiLLMModels.length} model{editMultiLLMModels.length !== 1 ? 's' : ''} {/if} @@ -3387,24 +3387,24 @@ class ${dataClassName}Transform(DataTransform): {#if editMultiLLMModels.length > 0}
{#each editMultiLLMModels as model (model.id)} -
+
-
- +
+
updateMultiLLMModel(model.id, 'label', e.currentTarget.value)} placeholder="model_label" - class="px-2 py-1 text-sm font-semibold border border-transparent hover:border-gray-200 dark:hover:border-gray-600 focus:border-cyan-500 rounded bg-transparent text-gray-800 dark:text-gray-200 w-28" + class="px-2 py-1 text-sm font-semibold border border-transparent hover:border-surface-border focus:border-info rounded bg-transparent text-text-primary w-28" />
{#if transformCodeExpanded[key]} {#if transformCodeLoading[key]}
- +
{:else if transformCodeMap[key]}
@@ -3820,7 +3820,7 @@ class ${dataClassName}Transform(DataTransform): />
{:else} -
+
Transform function code not found
{/if} @@ -3837,33 +3837,33 @@ class ${dataClassName}Transform(DataTransform): {#if node.node_type === 'lambda'}
-
+
Lambda Function
- Primary function code + Primary function code
{#if isEditing}
- Function Path + Function Path
{:else if node.function_path} -
+
{node.function_path}
{:else} -
+
No function path defined
{/if} -
+
{isEditing ? 'Edit the lambda function code. This will be saved to task_executor.py.' : 'Lambda function code.'}
@@ -3886,12 +3886,12 @@ class ${dataClassName}Transform(DataTransform): {#if node.node_type === 'branch'}
-
+
Branch Condition
- Primary condition code + Primary condition code
-
+
{isEditing ? 'Edit the condition logic that determines which path to take.' : 'Condition logic that determines which path to take. This code will be saved to task_executor.py.'}
@@ -3912,14 +3912,14 @@ class ${dataClassName}Transform(DataTransform): {#if canHaveProcessors && (node.pre_process || isEditing)}
-
+
Pre-processor
- Optional hook + Optional hook
-
{isEditing ? 'Code Editor:' : 'Code Preview:'}
+
{isEditing ? 'Code Editor:' : 'Code Preview:'}
-
+
Post-processor
- Optional hook + Optional hook
-
{isEditing ? 'Code Editor:' : 'Code Preview:'}
+
{isEditing ? 'Code Editor:' : 'Code Preview:'}
+
No code configuration for this node
{/if} @@ -3979,12 +3979,12 @@ class ${dataClassName}Transform(DataTransform): type="text" bind:value={overrideSearchQuery} placeholder="Search inner nodes..." - class="w-full pl-3 pr-8 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-800 dark:text-gray-200 focus:ring-2 focus:ring-[#52B8FF] focus:border-transparent" + class="w-full pl-3 pr-8 py-2 text-sm border border-surface-border rounded-lg bg-surface text-text-primary focus:ring-2 focus:ring-info focus:border-transparent" /> {#if overrideSearchQuery} @@ -3994,10 +3994,10 @@ class ${dataClassName}Transform(DataTransform):
-
+
- -
+ +
Node Configuration Overrides allow you to customize inner node settings without modifying the original subgraph. Add overrides for model, placeholders, or processors.
@@ -4013,15 +4013,15 @@ class ${dataClassName}Transform(DataTransform): {@const override = editNodeConfigMap[innerNode.id]}
@@ -4138,7 +4138,7 @@ class ${dataClassName}Transform(DataTransform): {/each}
{:else} -
+
No placeholder mappings. Click "Add" to create one.
{/if} @@ -4148,7 +4148,7 @@ class ${dataClassName}Transform(DataTransform): {#if innerNode.node_type !== 'data' && innerNode.node_type !== 'output'}
-
-
{/if} -
+
{:else} -
+
No overrides configured for this node.
- Click Edit to add overrides. + Click Edit to add overrides.
{/if}
@@ -4271,7 +4271,7 @@ class ${dataClassName}Transform(DataTransform): {/each} {#if filteredInnerNodes().length === 0} -
+
{#if overrideSearchQuery} No inner nodes match "{overrideSearchQuery}" {:else} @@ -4283,8 +4283,8 @@ class ${dataClassName}Transform(DataTransform): {#if overrideCount > 0} -
-
+
+
{overrideCount} node{overrideCount !== 1 ? 's' : ''} with configuration overrides
@@ -4297,59 +4297,59 @@ class ${dataClassName}Transform(DataTransform): {#if activeTab === 'settings'}
-
-
+
+
Configuration Overview
-
- Node Type - {node.node_type} +
+ Node Type + {node.node_type}
-
- Node ID - {node.id} +
+ Node ID + {node.id}
{#if node.model?.name} -
- Model +
+ Model {node.model.name}
{/if} {#if node.model?.provider} -
- Provider - {node.model.provider} +
+ Provider + {node.model.provider}
{/if} {#if node.tools && node.tools.length > 0} -
- Tools - {node.tools.length} configured +
+ Tools + {node.tools.length} configured
{/if} {#if node.tool_choice} -
- Tool Choice - {node.tool_choice} +
+ Tool Choice + {node.tool_choice}
{/if} {#if node.pre_process} -
- Pre-processor - {node.pre_process} +
+ Pre-processor + {node.pre_process}
{/if} {#if node.post_process} -
- Post-processor - {node.post_process} +
+ Post-processor + {node.post_process}
{/if} {#if node.function_path} -
- Function Path - {node.function_path} +
+ Function Path + {node.function_path}
{/if}
@@ -4358,20 +4358,20 @@ class ${dataClassName}Transform(DataTransform): {#if nodeState}
-
+
Execution State
-
+
- + {nodeState.status?.toUpperCase() ?? 'UNKNOWN'} {#if nodeState.duration_ms} - {nodeState.duration_ms}ms + {nodeState.duration_ms}ms {/if}
{#if nodeState.error} -
+
{nodeState.error}
{/if} @@ -4383,7 +4383,7 @@ class ${dataClassName}Transform(DataTransform): {#if node.node_type === 'llm'}
-
+
Model Parameters
{#if isEditing} @@ -4400,24 +4400,24 @@ class ${dataClassName}Transform(DataTransform): {#if isEditing}
{#each Object.entries(editModelParameters) as [key, value]} -
+
renameModelParameter(key, e.currentTarget.value)} - class="flex-1 px-2 py-1 text-xs font-mono border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-800 dark:text-gray-200" + class="flex-1 px-2 py-1 text-xs font-mono border border-surface-border rounded bg-surface text-text-primary" placeholder="Parameter name" /> updateModelParameter(key, parseParameterValue(e.currentTarget.value))} - class="flex-1 px-2 py-1 text-xs font-mono border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-800 dark:text-gray-200" + class="flex-1 px-2 py-1 text-xs font-mono border border-surface-border rounded bg-surface text-text-primary" placeholder="Value" /> @@ -4425,21 +4425,21 @@ class ${dataClassName}Transform(DataTransform): {/each} {#if Object.keys(editModelParameters).length === 0} -
+
No parameters. Click "Add" to add one.
{/if}
-
-
Quick add common parameters:
+
+
Quick add common parameters:
{#each ['temperature', 'max_tokens', 'top_p', 'frequency_penalty', 'presence_penalty'] as param} {#if !editModelParameters[param]} @@ -4450,16 +4450,16 @@ class ${dataClassName}Transform(DataTransform): {:else}
{#each Object.entries(node.model?.parameters ?? {}) as [key, value]} -
- {key} - +
+ {key} + {typeof value === 'object' ? JSON.stringify(value) : value}
{/each} {#if !node.model?.parameters || Object.keys(node.model.parameters).length === 0} -
+
No parameters configured
{/if} @@ -4470,9 +4470,9 @@ class ${dataClassName}Transform(DataTransform): {#if node.node_type === 'llm' || node.node_type === 'agent'} -
+
-
+
Structured Output
@@ -4482,12 +4482,12 @@ class ${dataClassName}Transform(DataTransform): type="checkbox" bind:checked={editStructuredOutputEnabled} onchange={() => markChanged()} - class="w-4 h-4 text-[#7661FF] border-gray-300 rounded focus:ring-[#52B8FF]" + class="w-4 h-4 text-[#7661FF] border-surface-border rounded focus:ring-info" /> - Enable + Enable {:else} - + {node.model?.structured_output?.enabled ? 'Enabled' : 'Disabled'} {/if} @@ -4496,21 +4496,21 @@ class ${dataClassName}Transform(DataTransform): {#if isEditing && editStructuredOutputEnabled}
- + Schema Type -
+
@@ -4521,7 +4521,7 @@ class ${dataClassName}Transform(DataTransform):
- + Output Fields {#if showSchemaPreview} -
{generateSchemaPreview()}
+
{generateSchemaPreview()}
{/if}
-
- +
+ Advanced Options -
+
- +
- + markChanged()} - class="w-4 h-4 text-[#7661FF] border-gray-300 rounded focus:ring-[#52B8FF]" + class="w-4 h-4 text-[#7661FF] border-surface-border rounded focus:ring-info" />
- + markChanged()} min="0" max="10" - class="w-16 px-2 py-1 text-xs border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-800 dark:text-gray-200 focus:ring-2 focus:ring-[#52B8FF] focus:border-transparent" + class="w-16 px-2 py-1 text-xs border border-surface-border rounded-md bg-surface text-text-primary focus:ring-2 focus:ring-info focus:border-transparent" />
@@ -4701,36 +4701,36 @@ class ${dataClassName}Transform(DataTransform):
{#if typeof node.model.structured_output.schema === 'string'} -
+
-
Class Path
-
{node.model.structured_output.schema}
+
Class Path
+
{node.model.structured_output.schema}
{:else if node.model.structured_output.schema?.fields}
- + Output Fields
{#each Object.entries(node.model.structured_output.schema.fields) as [fieldName, fieldDef]} -
+
- {fieldName} + {fieldName} {fieldDef.type}
{#if fieldDef.description} -
{fieldDef.description}
+
{fieldDef.description}
{/if} {#if fieldDef.default !== undefined} -
+
Default: {JSON.stringify(fieldDef.default)}
{/if} @@ -4741,14 +4741,14 @@ class ${dataClassName}Transform(DataTransform):
{/if} {#if node.model.structured_output.fallback_strategy} -
- Fallback Strategy: - {node.model.structured_output.fallback_strategy} +
+ Fallback Strategy: + {node.model.structured_output.fallback_strategy}
{/if}
{:else if !isEditing} -
+
Structured output not configured
{/if} @@ -4758,32 +4758,32 @@ class ${dataClassName}Transform(DataTransform): {#if node.node_type === 'data' && node.data_source}
-
+
Data Source Configuration
{#if node.data_source.source_type} -
- Source Type - {node.data_source.source_type} +
+ Source Type + {node.data_source.source_type}
{/if} {#if node.data_source.repo_id} -
- Repository - {node.data_source.repo_id} +
+ Repository + {node.data_source.repo_id}
{/if} {#if node.data_source.file_path} -
- File Path - {node.data_source.file_path} +
+ File Path + {node.data_source.file_path}
{/if} {#if node.data_source.split} -
- Split - {node.data_source.split} +
+ Split + {node.data_source.split}
{/if}
@@ -4793,18 +4793,18 @@ class ${dataClassName}Transform(DataTransform): {#if node.node_type === 'subgraph' && node.subgraph}
-
+
Subgraph Configuration
-
- Subgraph Path - {node.subgraph} +
+ Subgraph Path + {node.subgraph}
{#if node.node_config_map && Object.keys(node.node_config_map).length > 0} -
- Overrides - {Object.keys(node.node_config_map).length} nodes +
+ Overrides + {Object.keys(node.node_config_map).length} nodes
{/if}
@@ -4815,17 +4815,17 @@ class ${dataClassName}Transform(DataTransform): {#if node.metadata && Object.keys(node.metadata).length > 0}
-
+
Metadata
- {Object.keys(node.metadata).length} fields + {Object.keys(node.metadata).length} fields
View full metadata -
-
{JSON.stringify(node.metadata, null, 2)}
+
+
{JSON.stringify(node.metadata, null, 2)}
@@ -4834,7 +4834,7 @@ class ${dataClassName}Transform(DataTransform):
-
+
Full Configuration
@@ -4842,8 +4842,8 @@ class ${dataClassName}Transform(DataTransform): View raw node configuration (JSON) -
-
{JSON.stringify(node, null, 2)}
+
+
{JSON.stringify(node, null, 2)}
diff --git a/studio/frontend/src/lib/components/graph/renderers/nodes/AgentNode.svelte b/studio/frontend/src/lib/components/graph/renderers/nodes/AgentNode.svelte index 3080b072..ae4b23c1 100644 --- a/studio/frontend/src/lib/components/graph/renderers/nodes/AgentNode.svelte +++ b/studio/frontend/src/lib/components/graph/renderers/nodes/AgentNode.svelte @@ -34,23 +34,23 @@
{#if data.model?.provider}
- - {data.model.provider} + + {data.model.provider}
{/if} {#if temperature !== undefined} -
+
{temperature}
{/if} {#if toolCount > 0} -
+
{toolCount} tool{toolCount !== 1 ? 's' : ''}
{:else} -
+
No tools configured
{/if} diff --git a/studio/frontend/src/lib/components/graph/renderers/nodes/DataNode.svelte b/studio/frontend/src/lib/components/graph/renderers/nodes/DataNode.svelte index 901f04e2..0ac882f0 100644 --- a/studio/frontend/src/lib/components/graph/renderers/nodes/DataNode.svelte +++ b/studio/frontend/src/lib/components/graph/renderers/nodes/DataNode.svelte @@ -126,17 +126,17 @@ {#if sources().length > 0}
-
+
Sources
{#each sources() as src} {@const Icon = getSourceIcon(src)} -
+
{getSourceLabel(src)} {#if src.join_type && src.join_type !== 'primary'} - + {src.join_type} {/if} @@ -147,18 +147,18 @@ {#if sinks().length > 0} -
-
+
+
Sinks
{#each sinks() as sink} {@const SinkIcon = getSinkIcon(sink)} -
+
{getSinkLabel(sink)} {#if sink.operation} - + {sink.operation} {/if} @@ -169,7 +169,7 @@ {#if sources().length === 0 && sinks().length === 0} -
+
Click to configure data source & sink
{/if} diff --git a/studio/frontend/src/lib/components/graph/renderers/nodes/LLMNode.svelte b/studio/frontend/src/lib/components/graph/renderers/nodes/LLMNode.svelte index d243a8f3..6d6ba534 100644 --- a/studio/frontend/src/lib/components/graph/renderers/nodes/LLMNode.svelte +++ b/studio/frontend/src/lib/components/graph/renderers/nodes/LLMNode.svelte @@ -34,12 +34,12 @@
{#if data.model}
- - {data.model.provider} + + {data.model.provider}
{/if} {#if temperature !== undefined || maxTokens !== undefined} -
+
{#if temperature !== undefined} @@ -52,7 +52,7 @@
{/if} {#if toolCount > 0} -
+
{toolCount} tool{toolCount !== 1 ? 's' : ''}
diff --git a/studio/frontend/src/lib/components/graph/renderers/nodes/MultiLLMNode.svelte b/studio/frontend/src/lib/components/graph/renderers/nodes/MultiLLMNode.svelte index a544a4d2..7c7902e6 100644 --- a/studio/frontend/src/lib/components/graph/renderers/nodes/MultiLLMNode.svelte +++ b/studio/frontend/src/lib/components/graph/renderers/nodes/MultiLLMNode.svelte @@ -37,12 +37,12 @@ {#each modelEntries.slice(0, 3) as [label, config]}
- - + + {label}
-
+
{config.name} @@ -56,12 +56,12 @@
{/each} {#if modelCount > 3} -
+
+{modelCount - 3} more model{modelCount - 3 !== 1 ? 's' : ''}
{/if} {:else} -
+
No models configured
{/if} diff --git a/studio/frontend/src/lib/components/graph/renderers/nodes/NodeWrapper.svelte b/studio/frontend/src/lib/components/graph/renderers/nodes/NodeWrapper.svelte index 1212d486..9bc98adf 100644 --- a/studio/frontend/src/lib/components/graph/renderers/nodes/NodeWrapper.svelte +++ b/studio/frontend/src/lib/components/graph/renderers/nodes/NodeWrapper.svelte @@ -52,10 +52,10 @@ let statusColor = $derived(() => { if (!executionState) return ''; switch (executionState.status) { - case 'running': return 'border-blue-500 shadow-blue-500/25'; - case 'completed': return 'border-green-500'; - case 'failed': return 'border-red-500'; - default: return 'border-gray-300 dark:border-gray-600'; + case 'running': return 'border-info shadow-info/25'; + case 'completed': return 'border-status-completed'; + case 'failed': return 'border-error'; + default: return 'border-surface-border'; } }); @@ -71,22 +71,21 @@
{#if showTargetHandle} {/if} @@ -102,11 +101,11 @@
-
+
{label}
{#if sublabel} -
+
{sublabel}
{/if} @@ -116,13 +115,13 @@ {#if StatusIcon() && showStatusIcon()}
{#if executionState?.status === 'running'} - + {:else if executionState?.status === 'completed'} - + {:else if executionState?.status === 'failed'} - + {:else} - + {/if}
{/if} @@ -137,7 +136,7 @@ {#if executionState?.duration_ms} -
+
Duration: {executionState.duration_ms}ms
{/if} @@ -147,7 +146,7 @@ {/if}
diff --git a/studio/frontend/src/lib/components/graph/renderers/nodes/OutputNode.svelte b/studio/frontend/src/lib/components/graph/renderers/nodes/OutputNode.svelte index 9c7aa5f7..9960fe0e 100644 --- a/studio/frontend/src/lib/components/graph/renderers/nodes/OutputNode.svelte +++ b/studio/frontend/src/lib/components/graph/renderers/nodes/OutputNode.svelte @@ -64,12 +64,12 @@ {#if generatorName()}
-
+
Generator
-
- +
+ {generatorName()}
@@ -79,29 +79,29 @@ {#if outputMapKeys().length > 0}
-
+
Output Map
{#each outputMapKeys() as key} {@const mapping = data.output_config?.output_map?.[key]} -
+
{key.split('->').pop() || key} - + {#if mapping?.from} - {mapping.from} + {mapping.from} {:else if mapping?.value !== undefined} - static + static {/if} {#if mapping?.transform} - + fn {/if}
{/each} {#if totalMappings() > 4} -
+
+{totalMappings() - 4} more...
{/if} @@ -110,7 +110,7 @@ {#if !generatorName() && outputMapKeys().length === 0} -
+
Click to configure output mapping
{/if} diff --git a/studio/frontend/src/lib/components/graph/renderers/nodes/SubgraphNode.svelte b/studio/frontend/src/lib/components/graph/renderers/nodes/SubgraphNode.svelte index 590e027c..b233156b 100644 --- a/studio/frontend/src/lib/components/graph/renderers/nodes/SubgraphNode.svelte +++ b/studio/frontend/src/lib/components/graph/renderers/nodes/SubgraphNode.svelte @@ -161,36 +161,34 @@ {#if hasInnerGraph}
-
-
+
+
-
+
{data.summary || data.id}
-
+
{data.inner_graph?.nodes.length || 0} nodes {#if hasOverrides} - + {overrideCount} @@ -201,7 +199,7 @@
@@ -261,7 +259,7 @@ {@const y = (innerNode.position?.y || 0) - bounds.minY + INNER_PADDING}
@@ -275,11 +273,11 @@ >
- + {innerNode.summary || innerNode.id} {#if innerNode.inner_graph && innerNode.inner_graph.nodes?.length > 0} - +{innerNode.inner_graph.nodes.length} + +{innerNode.inner_graph.nodes.length} {/if}
@@ -288,7 +286,7 @@ {#if data.executionState?.duration_ms} -
+
Duration: {data.executionState.duration_ms}ms
{/if} @@ -299,41 +297,40 @@
{:else}
-
-
+
+
-
+
{data.summary || data.id}
-
+
{data.subgraph_path || 'Subgraph'} {#if hasOverrides} - + {overrideCount} @@ -343,17 +340,17 @@ {#if data.executionState?.status === 'running'} - + {:else if data.executionState?.status === 'completed'} - + {:else if data.executionState?.status === 'failed'} - + {/if}
{#if data.executionState?.duration_ms} -
+
Duration: {data.executionState.duration_ms}ms
{/if} @@ -364,7 +361,7 @@
{/if} diff --git a/studio/frontend/src/lib/components/graph/renderers/nodes/WeightedSamplerNode.svelte b/studio/frontend/src/lib/components/graph/renderers/nodes/WeightedSamplerNode.svelte index 56e9e22b..73eb5499 100644 --- a/studio/frontend/src/lib/components/graph/renderers/nodes/WeightedSamplerNode.svelte +++ b/studio/frontend/src/lib/components/graph/renderers/nodes/WeightedSamplerNode.svelte @@ -45,22 +45,22 @@ {#if attributeNames().length > 0}
-
+
Attributes
{#each attributeNames() as attrName} {@const attr = data.sampler_config?.attributes[attrName]} -
+
{attrName} - : - + : + {attr?.values?.length ?? 0} values
{/each} {#if totalAttributes() > 4} -
+
+{totalAttributes() - 4} more...
{/if} @@ -69,7 +69,7 @@ {#if attributeNames().length === 0} -
+
Click to configure sampler attributes
{/if} diff --git a/studio/frontend/src/lib/components/home/HomeView.svelte b/studio/frontend/src/lib/components/home/HomeView.svelte index 8ce7b23d..39309240 100644 --- a/studio/frontend/src/lib/components/home/HomeView.svelte +++ b/studio/frontend/src/lib/components/home/HomeView.svelte @@ -2,10 +2,9 @@ import { workflowStore, executionStore, uiStore, type Execution } from '$lib/stores/workflow.svelte'; import { pushState } from '$app/navigation'; import { - Plus, GitBranch, History, Play, CheckCircle2, XCircle, Clock, Loader2, ArrowRight, + Plus, History, CheckCircle2, XCircle, Clock, Loader2, ArrowRight, Zap, TrendingUp, TrendingDown, Activity, Layers, RefreshCw, Sparkles, Search, Timer, - DollarSign, BarChart3, Settings, BookOpen, Cpu, Database, ArrowUpRight, Ban, - FolderOpen, Library + DollarSign, ArrowUpRight, Ban, FolderOpen, Library, Brain, Workflow, Play } from 'lucide-svelte'; let workflows = $derived(workflowStore.workflows); @@ -55,22 +54,22 @@ }; }); - let recentWorkflows = $derived(workflows.slice(0, 6)); - let recentRuns = $derived(executionHistory.slice(0, 6)); + let recentWorkflows = $derived(workflows.slice(0, 5)); + let recentRuns = $derived(executionHistory.slice(0, 5)); // Filtered items based on search let filteredWorkflows = $derived(() => { if (!searchQuery.trim()) return recentWorkflows; const q = searchQuery.toLowerCase(); - return workflows.filter(w => w.name.toLowerCase().includes(q)).slice(0, 6); + return workflows.filter(w => w.name.toLowerCase().includes(q)).slice(0, 5); }); - const statusConfig: Record = { - pending: { icon: Clock, color: 'text-gray-500', bg: 'bg-gray-100 dark:bg-gray-800', label: 'Pending' }, - running: { icon: Loader2, color: 'text-blue-500', bg: 'bg-blue-100 dark:bg-blue-900/30', label: 'Running' }, - completed: { icon: CheckCircle2, color: 'text-emerald-500', bg: 'bg-emerald-100 dark:bg-emerald-900/30', label: 'Completed' }, - failed: { icon: XCircle, color: 'text-red-500', bg: 'bg-red-100 dark:bg-red-900/30', label: 'Failed' }, - cancelled: { icon: Ban, color: 'text-red-500', bg: 'bg-red-100 dark:bg-red-900/30', label: 'Cancelled' } + const statusConfig: Record = { + pending: { icon: Clock, color: 'text-text-muted', bg: 'bg-surface-tertiary', border: 'border-[var(--border)]', label: 'Pending' }, + running: { icon: Loader2, color: 'text-info', bg: 'bg-info-light', border: 'border-info-border', label: 'Running' }, + completed: { icon: CheckCircle2, color: 'text-success', bg: 'bg-success-light', border: 'border-success-border', label: 'Completed' }, + failed: { icon: XCircle, color: 'text-error', bg: 'bg-error-light', border: 'border-error-border', label: 'Failed' }, + cancelled: { icon: Ban, color: 'text-warning', bg: 'bg-warning-light', border: 'border-warning-border', label: 'Cancelled' } }; function navigate(view: string, params?: Record) { @@ -144,171 +143,260 @@ if (usd < 0.01) return `<$0.01`; return `$${usd.toFixed(2)}`; } + - - -
-
- -
-
- -
- +
+ +
+ +
+
+
+ +
+ +
+
+ +
+
+
+ +
+
+
+
+

+ SyGra + Studio +

+

+ Synthetic data generation workflows +

+
-
-

- SyGra - Studio -

-

- Synthetic data generation workflows -

+
+
+ + +
+
-
-
- - -
- -
-
- -
- - - - + + + + + + + + +
+
+ +
-
-
- Success Rate - {#if stats().successRate >= 80}{:else}{/if} + +
+
+ Success Rate +
+ {#if stats().successRate >= 80} + + {:else} + + {/if} +
+
+
{stats().successRate}%
+
{stats().completedRuns} of {stats().totalRuns} runs
+ +
+
-
{stats().successRate}%
-
{stats().completedRuns}/{stats().totalRuns} runs
-
-
- Total Tokens - + + +
+
+ Total Tokens +
+ +
-
{formatNumber(stats().totalTokens)}
-
across all runs
+
{formatNumber(stats().totalTokens)}
+
across all runs
-
-
- Total Cost - + + +
+
+ Total Cost +
+ +
-
{formatCost(stats().totalCost)}
-
API usage
+
{formatCost(stats().totalCost)}
+
API usage
-
-
- Avg Duration - + + +
+
+ Avg Duration +
+ +
-
{formatDuration(stats().avgDuration)}
-
per run
+
{formatDuration(stats().avgDuration)}
+
per run
-
-
- Running - + + +
+
+ Running +
+ +
-
{stats().runningRuns}
-
active now
+
{stats().runningRuns}
+
active now
+ {#if stats().runningRuns > 0} +
+
+
+ {/if}
-
-
-

- - {searchQuery ? 'Search Results' : 'Recent Workflows'} -

-
-
- {#each filteredWorkflows() as workflow (workflow.id)} - {:else} -
-
+
+
-

{searchQuery ? 'No matching workflows' : 'No workflows yet'}

-

{searchQuery ? 'Try a different search term' : 'Create your first workflow to get started'}

+

{searchQuery ? 'No matching workflows' : 'No workflows yet'}

+

{searchQuery ? 'Try a different search term' : 'Create your first workflow to get started'}

{#if !searchQuery} - {/if}
@@ -317,44 +405,56 @@
-
-
-

- - Recent Activity -

-
-
- {#each recentRuns as run (run.id)} +
+ {#each recentRuns as run, i (run.id)} {@const effectiveStatus = getEffectiveStatusForRun(run)} {@const status = statusConfig[effectiveStatus] || statusConfig.pending} {@const StatusIcon = status.icon} - {:else} -
-
- +
+
+
-

No runs yet

-

Execute a workflow to see activity here

+

No runs yet

+

Execute a workflow to see activity here

{/each}
@@ -363,48 +463,109 @@ {#if workflows.length === 0} -
-
-
- -
-

- Getting Started -

-
-

- Welcome to SyGra Studio! Follow these steps to create your first synthetic data workflow: -

-
-
-
- 1 +
+
+
+
+
-

Create a Workflow

-

- Click "Create Workflow" to build your first data generation pipeline. -

-
-
-
- 2 +
+

Getting Started

+

Create your first synthetic data workflow

-

Add Nodes

-

- Drag and drop LLM, Lambda, Sampler, and other nodes to build your workflow. -

-
-
- 3 + +
+
+
+ 1 +
+

Create a Workflow

+

+ Click "New Workflow" to build your first data generation pipeline. +

+
+
+
+ 2 +
+

Add Nodes

+

+ Drag and drop LLM, Lambda, Sampler, and other nodes to build your workflow. +

+
+
+
+ 3 +
+

Execute & Monitor

+

+ Run your workflow and monitor execution progress in real-time. +

-

Execute & Monitor

-

- Run your workflow and monitor execution progress in real-time. -

{/if}
+ + diff --git a/studio/frontend/src/lib/components/library/LibraryView.svelte b/studio/frontend/src/lib/components/library/LibraryView.svelte index e9f4f17d..4ce46ec4 100644 --- a/studio/frontend/src/lib/components/library/LibraryView.svelte +++ b/studio/frontend/src/lib/components/library/LibraryView.svelte @@ -4,7 +4,7 @@ Search, Plus, Boxes, Wrench, RefreshCw, X, Upload, Download, LayoutList, LayoutGrid, MoreVertical, Trash2, Copy, Edit3, Brain, Database, Shuffle, Bot, Puzzle, Globe, Layers, Clock, - CheckSquare, Square, MinusSquare, Code, Eye + CheckSquare, Square, MinusSquare, Code, Eye, Library, Play } from 'lucide-svelte'; import { recipeStore, RECIPE_CATEGORIES, type Recipe, type RecipeCategory } from '$lib/stores/recipe.svelte'; import { toolStore, TOOL_CATEGORIES, DEFAULT_TOOL_CODE, type Tool, type ToolCategory } from '$lib/stores/tool.svelte'; @@ -159,18 +159,15 @@ // Calculate position with viewport boundary detection const menuWidth = 160; - const menuHeight = 200; // Approximate menu height + const menuHeight = 200; const padding = 8; let x = e.clientX; let y = e.clientY; - // Adjust if menu would go off right edge if (x + menuWidth + padding > window.innerWidth) { x = window.innerWidth - menuWidth - padding; } - - // Adjust if menu would go off bottom edge if (y + menuHeight + padding > window.innerHeight) { y = window.innerHeight - menuHeight - padding; } @@ -327,64 +324,69 @@ -
+
-
-
-
-

Library

-

- {currentItems.length} of {totalCount} {activeTab} - {#if selectionCount > 0} - • {selectionCount} selected - {/if} -

+
+
+
+
+ +
+
+

Library

+

+ {currentItems.length} of {totalCount} {activeTab} + {#if selectionCount > 0} + • {selectionCount} selected + {/if} +

+
-
+
-
+
-
+
{#if selectionCount > 0} - - -
+
{/if} - {#if activeTab === 'tools'} - {/if} @@ -396,34 +398,34 @@
- +
{#if activeTab === 'recipes'}
- + {#each RECIPE_CATEGORIES as cat} - + {/each}
{:else}
- + {#each TOOL_CATEGORIES as cat} - + {/each}
{/if} {#if hasActiveFilters} - @@ -431,11 +433,11 @@
-
- -
@@ -446,7 +448,7 @@
{#if currentItems.length === 0}
-
+
{#if activeTab === 'recipes'} {:else} @@ -454,8 +456,8 @@ {/if}
{#if totalCount === 0} -

No {activeTab} yet

-

+

No {activeTab} yet

+

{#if activeTab === 'recipes'} Save workflow subgraphs as recipes to reuse them across workflows. {:else} @@ -463,76 +465,95 @@ {/if}

- {#if activeTab === 'tools'} - {/if}
{:else} -

No matching {activeTab}

-

Try adjusting your search or filters

+

No matching {activeTab}

+

Try adjusting your search or filters

{/if}
{:else if viewMode === 'card'} -
+
{#each currentItems as item (item.id)} {@const isRecipe = activeTab === 'recipes'} {@const Icon = isRecipe ? recipeCategoryIcons[(item as Recipe).category] : toolCategoryIcons[(item as Tool).category]} {@const color = getCategoryColor(isRecipe ? (item as Recipe).category : (item as Tool).category, isRecipe ? 'recipe' : 'tool')}
isRecipe ? (previewRecipe = item as Recipe) : handleEdit(item.id)} > -
-
-
- -
- -
+ +
+ +
+ + +
+ +
+ +
+ +
+
+
- -
-

{item.name}

- {#if item.description} -

{item.description}

- {/if} - {#if isRecipe} -
- {(item as Recipe).nodeCount} nodes +
+

{item.name}

+ {#if item.description} +

{item.description}

+ {/if}
- {:else} -
{(item as Tool).import_path}
- {/if} -
-
- {formatDate(item.updatedAt)} - {#if isRecipe} -
+ + +
+ {#if isRecipe} +
+ + {(item as Recipe).nodeCount} nodes +
+ {:else} +
{(item as Tool).import_path}
+ {/if} +
+ + + - {:else} - - {/if} + {:else} + + Edit Tool + {/if} + +
+ + +
+ {formatDate(item.updatedAt)}
{/each} @@ -540,76 +561,74 @@ {:else} - + - - - - - - + + + + + - + {#each currentItems as item (item.id)} {@const isRecipe = activeTab === 'recipes'} {@const Icon = isRecipe ? recipeCategoryIcons[(item as Recipe).category] : toolCategoryIcons[(item as Tool).category]} {@const color = getCategoryColor(isRecipe ? (item as Recipe).category : (item as Tool).category, isRecipe ? 'recipe' : 'tool')} {@const catLabel = isRecipe ? RECIPE_CATEGORIES.find(c => c.value === (item as Recipe).category)?.label : TOOL_CATEGORIES.find(c => c.value === (item as Tool).category)?.label} - isRecipe ? (previewRecipe = item as Recipe) : handleEdit(item.id)}> + isRecipe ? (previewRecipe = item as Recipe) : handleEdit(item.id)}> - - + - +
- + NameCategory{activeTab === 'recipes' ? 'Nodes' : 'Path'}UpdatedActionsNameCategory{activeTab === 'recipes' ? 'Nodes' : 'Path'}UpdatedActions
- -
-
+
+
- {item.name} + {item.name} {#if item.description} - {item.description} + {item.description} {/if}
{catLabel} + {catLabel} {#if isRecipe} - {(item as Recipe).nodeCount} nodes + {(item as Recipe).nodeCount} nodes {:else} {(item as Tool).import_path} {/if} {formatDate(item.updatedAt)}{formatDate(item.updatedAt)}
- {#if isRecipe} - - {:else} - - {/if} - +
@@ -624,24 +643,24 @@ {#if contextMenuId} -
e.stopPropagation()}> +
e.stopPropagation()}> {#if activeTab === 'recipes'} - {:else} - {/if} - - -
-
@@ -649,23 +668,23 @@ {#if showToolEditor} -
-
-
-

{editingTool ? 'Edit Tool' : 'Create Tool'}

-
- +
- - + +
- + ({ value: c.value, label: c.label }))} bind:value={toolFormCategory} @@ -676,25 +695,25 @@
- - + +
- - + +
- -
+ +
-
- - +
diff --git a/studio/frontend/src/lib/components/library/RecipeLibrary.svelte b/studio/frontend/src/lib/components/library/RecipeLibrary.svelte index 41bbeecf..d1f24999 100644 --- a/studio/frontend/src/lib/components/library/RecipeLibrary.svelte +++ b/studio/frontend/src/lib/components/library/RecipeLibrary.svelte @@ -198,15 +198,20 @@ -
+
-
-
-
- -

Recipe Library

+
+
+
+
+ +
+
+

Recipe Library

+

Reusable workflow patterns

+
{#if selectionCount > 0} - + {selectionCount} selected {/if} @@ -215,14 +220,16 @@ {#if selectionCount > 0} -
+
{/if} {/if}
- +
-
+
{#if showCategoryFilter} -
+
{#each RECIPE_CATEGORIES as cat} @@ -332,14 +345,14 @@
-
+
{#if recipeStore.filteredRecipes.length === 0} -
-
- +
+
+
-

No recipes yet

-

+

No recipes yet

+

{#if recipeStore.searchQuery || recipeStore.selectedCategory !== 'all'} No recipes match your search. Try adjusting your filters. {:else} @@ -348,85 +361,87 @@

{:else} -
+
{#each recipeStore.filteredRecipes as recipe (recipe.id)} {@const Icon = categoryIcons[recipe.category]} {@const isSelected = selectedRecipeIds.has(recipe.id)}
handleAddRecipe(recipe)} >
-
+
- +
-

+

{recipe.name}

{#if recipe.description} -

+

{recipe.description}

{/if} -
- +
+ {recipe.nodeCount} nodes - + {formatDate(recipe.updatedAt)}
{#if recipe.tags.length > 0} -
+
{#each recipe.tags.slice(0, 3) as tag} - + {tag} {/each} {#if recipe.tags.length > 3} - +{recipe.tags.length - 3} + +{recipe.tags.length - 3} {/if}
{/if} @@ -434,19 +449,19 @@
-
+
@@ -457,9 +472,9 @@
-
-
- {recipeStore.recipes.length} recipes in library +
+
+ {recipeStore.recipes.length} recipes in library {#if recipeStore.filteredRecipes.length !== recipeStore.recipes.length} {recipeStore.filteredRecipes.length} shown {/if} @@ -470,37 +485,40 @@ {#if contextMenuRecipe}
e.stopPropagation()} > -
+
diff --git a/studio/frontend/src/lib/components/library/RecipePreviewModal.svelte b/studio/frontend/src/lib/components/library/RecipePreviewModal.svelte index 7dd6710b..9e01a0d6 100644 --- a/studio/frontend/src/lib/components/library/RecipePreviewModal.svelte +++ b/studio/frontend/src/lib/components/library/RecipePreviewModal.svelte @@ -134,7 +134,7 @@ tabindex="-1" >
e.stopPropagation()} onkeydown={(e) => e.stopPropagation()} role="dialog" @@ -142,47 +142,47 @@ tabindex="-1" > -
+
-

+

{recipe.name}

{#if recipe.description} -

+

{recipe.description}

{/if}
-
+
-
+
{recipe.nodeCount} nodes
-
+
Updated {formatDate(recipe.updatedAt)}
{#if recipe.author} -
+
{recipe.author}
{/if} {#if recipe.tags.length > 0}
- + {#each recipe.tags as tag} - + {tag} {/each} @@ -192,7 +192,7 @@
-
+
-
+
Scroll to zoom • Drag to pan
-
-
+
+
Node types: {recipe.nodeTypes.join(', ')}
- +
+
+
+
+ +
+
+

Tools

+

Create and manage reusable tools for LLM/Agent nodes

+
+
+
+ + +
-
+
- +
-
+
{#each TOOL_CATEGORIES as cat} @@ -268,67 +275,69 @@
{#if toolStore.filteredTools.length === 0}
-
- +
+
{#if toolStore.tools.length === 0} -

No tools yet

-

+

No tools yet

+

Create your first tool to use in LLM and Agent nodes. Tools are Python functions decorated with @tool.

{:else} -

No matching tools

-

Try adjusting your search or filters

+

No matching tools

+

Try adjusting your search or filters

{/if}
{:else} -
+
{#each toolStore.filteredTools as tool (tool.id)} {@const Icon = categoryIcons[tool.category]}
-
-
+
+
- +
-

+

{tool.name}

{#if tool.description} -

+

{tool.description}

{/if} -
+
{tool.import_path}
-
+
{formatDate(tool.updatedAt)}
@@ -342,37 +351,40 @@ {#if contextMenuTool}
e.stopPropagation()} > -
+
@@ -380,16 +392,22 @@ {#if showCreateModal} -
-
+
+
-
-

- {editingTool ? 'Edit Tool' : 'Create Tool'} -

+
+
+
+ +
+

+ {editingTool ? 'Edit Tool' : 'Create Tool'} +

+
@@ -397,20 +415,21 @@
-
+
-
-
-
+
-
-
@@ -450,32 +471,33 @@
-