Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added script for turning any nada program and test into a streamlit app #36

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
161 changes: 161 additions & 0 deletions generate-streamlit-app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import questionary
from pathlib import Path
import os
import yaml
import subprocess
import shutil
import sys

STREAMLIT_APP_TEMPLATE = '''# This file was automatically generated by the generate-streamlit-app script.
# To run this file: from the root directory run `streamlit run streamlit_demo_apps/app_{program_name}.py`

import sys
import os

sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
import streamlit_app

program_name = "{program_name}"
program_test_name = "{program_test_name}"

def main():
current_dir = os.path.dirname(os.path.abspath(__file__))
path_nada_bin = os.path.join(current_dir, "compiled_nada_programs", f"{{program_name}}.nada.bin")
path_nada_json = os.path.join(current_dir, "compiled_nada_programs", f"{{program_name}}.nada.json")

if not os.path.exists(path_nada_bin):
raise FileNotFoundError(f"Add `{{program_name}}.nada.bin` to the compiled_nada_programs folder.")
if not os.path.exists(path_nada_json):
raise FileNotFoundError(f"Run nada build --mir-json and add `{{program_name}}.nada.json` to the compiled_nada_programs folder.")

streamlit_app.main(program_test_name, path_nada_bin, path_nada_json)

if __name__ == "__main__":
main()
'''

def get_programs(directory):
return sorted([f.stem for f in Path(directory).glob('*.py') if f.is_file()])

def get_test_files(directory, program_name):
matching_files = []
for file in Path(directory).glob('*.yaml'):
try:
with open(file, 'r') as f:
test_data = yaml.safe_load(f)
if test_data and 'program' in test_data and test_data['program'] == program_name:
matching_files.append(file)
except yaml.YAMLError:
print(f"Error reading {file}. Skipping.")
except Exception as e:
print(f"Unexpected error reading {file}: {e}. Skipping.")
return matching_files

def select_program_and_test():
programs = get_programs('src')
if not programs:
print("No Python programs found in 'src' directory.")
return None, None

selected_program = questionary.select(
"Select an existing program to create a streamlit app demo:",
choices=programs
).ask()

test_files = get_test_files('tests', selected_program)
if not test_files:
print(f"No test files found for '{selected_program}' in 'tests' directory.")
return selected_program, None

selected_test = questionary.select(
"Select a test file for starting input values:",
choices=[f.name for f in test_files]
).ask()

return selected_program, selected_test

def build_nada_program(program_name):
try:
subprocess.run(['nada', 'build', program_name, '--mir-json'], check=True)
print(f"Successfully built {program_name}")
return True
except subprocess.CalledProcessError as e:
print(f"Error building {program_name}: {e}")
return False
except FileNotFoundError:
print("Error: 'nada' command not found. Make sure it's installed and in your PATH.")
return False

def copy_nada_files(program_name):
source_dir = Path('target')
dest_dir = Path('streamlit_demo_apps/compiled_nada_programs')

for ext in ['.nada.json', '.nada.bin']:
source_file = source_dir / f"{program_name}{ext}"
dest_file = dest_dir / f"{program_name}{ext}"

if source_file.exists():
shutil.copy2(source_file, dest_file)
print(f"Copied {source_file} to {dest_file}")
else:
print(f"Warning: {source_file} not found")

def create_streamlit_app(program_name, test_name):
try:
app_content = STREAMLIT_APP_TEMPLATE.format(
program_name=program_name,
program_test_name=test_name
)

app_file_path = Path('streamlit_demo_apps') / f"app_{program_name}.py"
print(f"Attempting to create file at: {app_file_path.absolute()}")

# Ensure the directory exists
app_file_path.parent.mkdir(parents=True, exist_ok=True)

with open(app_file_path, 'w') as f:
f.write(app_content)
print(f"Created Streamlit app file: {app_file_path}")

if app_file_path.exists():
print(f"Streamlit app file successfully created at {app_file_path}")
return app_file_path
else:
print(f"Error: File creation verified failed for {app_file_path}")
return None
except Exception as e:
print(f"Error creating Streamlit app file: {e}")
return None

def run_streamlit_app(app_path):
try:
print(f"Attempting to run Streamlit app: {app_path}")
subprocess.run([sys.executable, '-m', 'streamlit', 'run', str(app_path)], check=True)
except subprocess.CalledProcessError as e:
print(f"Error running Streamlit app: {e}")
except Exception as e:
print(f"Unexpected error running Streamlit app: {e}")

def main():
program, test = select_program_and_test()

if program:
print(f"Selected program: {program}")
if test:
print(f"Selected test file: {test}")

with open(os.path.join('tests', test), 'r') as file:
test_data = yaml.safe_load(file)
print("\nTest file contents:")
print(yaml.dump(test_data, default_flow_style=False))

if build_nada_program(program):
copy_nada_files(program)
app_path = create_streamlit_app(program, os.path.splitext(test)[0] if test else '')
if app_path:
run_streamlit_app(app_path)
else:
print("No program selected.")

if __name__ == "__main__":
main()
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ nada-test
# streamlit demo dependencies
pyyaml
streamlit
questionary

# nillion_client_script dependencies for streamlit demo
py-nillion-client
Expand Down
56 changes: 35 additions & 21 deletions streamlit_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,39 @@ def parse_nada_json(json_data):

return input_info, output_info

import streamlit as st

def create_party_inputs(input_info, input_values):
party_names = sorted(set(info['party'] for info in input_info.values()))
updated_input_values = input_values.copy()

if len(party_names) > 1:
# Create two columns if there's more than one party
columns = st.columns(2)
else:
# Create a single column if there's only one party
columns = [st.columns(1)[0]]
oceans404 marked this conversation as resolved.
Show resolved Hide resolved

# Distribute parties between the columns
for i, party_name in enumerate(party_names):
with columns[i % len(columns)]:
st.subheader(f"{party_name}'s Inputs")
for input_name, value in input_values.items():
if input_info[input_name]['party'] == party_name:
input_type = input_info[input_name]['type']
if input_type == 'SecretBoolean':
updated_input_values[input_name] = st.checkbox(
label=f"{input_type}: {input_name}",
value=bool(value)
)
else:
updated_input_values[input_name] = st.number_input(
label=f"{input_type}: {input_name}",
value=value
)

return updated_input_values

def main(nada_test_file_name=None, path_nada_bin=None, path_nada_json=None):
# pass test name in via the command line
if nada_test_file_name is None:
Expand Down Expand Up @@ -156,32 +189,15 @@ def main(nada_test_file_name=None, path_nada_bin=None, path_nada_json=None):
st.code(program_code, language='python')

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
st.code(program_code, language='python')
with st.expander(f"Nada Program: {program_name}"):
st.code(program_code, language='python')

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Discovered you can make this collapsible. Makes it easier to go straight to the inputs instead of looking at the code. just a suggestion.

image


# Display inputs grouped by party, alphabetized
updated_input_values = {}
# Get unique party names and sort them alphabetically
party_names = sorted(set(info['party'] for info in input_info.values()))

for party_name in party_names:
st.subheader(f"{party_name}'s Inputs")
for input_name, value in input_values.items():
if input_info[input_name]['party'] == party_name:
input_type = input_info[input_name]['type']
if input_type == 'SecretBoolean':
updated_input_values[input_name] = st.checkbox(
label=f"{input_type}: {input_name}",
value=bool(value)
)
else:
updated_input_values[input_name] = st.number_input(
label=f"{input_type}: {input_name}",
value=value
)
updated_input_values = create_party_inputs(input_info, input_values)

output_parties = list(set(output['party'] for output in output_info.values()))

should_store_inputs = st.checkbox("Store secret inputs before running blind computation", value=False)

# Button to store inputs with a loading screen
if st.button('Run blind computation'):
st.divider()
# Conditional spinner text
if should_store_inputs:
spinner_text = "Storing the Nada program, storing inputs, and running blind computation on the Nillion Network Testnet..."
Expand All @@ -203,8 +219,6 @@ def main(nada_test_file_name=None, path_nada_bin=None, path_nada_json=None):
# Call the async store_inputs_and_run_blind_computation function and wait for it to complete
result_message = asyncio.run(store_inputs_and_run_blind_computation(input_data, program_name, output_parties, nilchain_private_key, path_nada_bin, cluster_id_from_streamlit_config, grpc_endpoint_from_streamlit_config, chain_id_from_streamlit_config, bootnodes, should_store_inputs))

st.divider()

st.subheader("Nada Program Result")

st.text('Output(s)')
Expand Down
70 changes: 49 additions & 21 deletions streamlit_demo_apps/README.md
Original file line number Diff line number Diff line change
@@ -1,49 +1,77 @@
# Deploying Streamlit Apps

Deployed Streamlit apps live here in the streamlit_demo_apps folder.
Follow the steps to deploy a live Streamlit app for your Nada program and test file. The app will connect to the Nillion Testnet to store your Nada program, store secret inputs (or use computation time secrets), and run blind computation.

## How to add a new Streamlit App

### 0. Create a streamlit secrets file and add your nilchain private key within `.streamlit/secrets.toml`
### 0. Create a streamlit secrets file

Run this command to create a `.streamlit/secrets.toml` copied from the example.

```
cp .streamlit/secrets.toml.example .streamlit/secrets.toml
```

### 1. Create an app file in the streamlit_demo_apps folder

Check out the addition app file example:

`app_addition.py`
Add your Nilchain private key to the .streamlit/secrets.toml file. The private key must be linked to a funded Nillion Testnet address that was created using a Google account (not a mnemonic). This allows you to retrieve the private key from Keplr. If you don’t have a Testnet wallet yet, you can learn how to create one here: https://docs.nillion.com/testnet-guides

### 2. Copy the compiled Nada program files from the target/ folder into the streamlit_demo_apps/compiled_nada_programs folder
### 1. Run the script to generate a new streamlit app for your program

Check out the compiled Nada program files for addition:
From the root folder of this repo, run the generate-streamlit-app script:

nada binary `addition.nada.bin`
nada json `addition.nada.json`
```
python3 generate-streamlit-app.py
```

### 3. Update your app file with the corresponding program name and program test name
### 2. Follow the prompts to

Check out the addition app file example:
- Select an existing program (from the src/ directory)
- Select an existing yaml test file for your program (from the tests/ directory)

`app_addition.py`
This will generate a Streamlit app file: streamlit*demo_apps/app*[your_program_name].py. The script will run the Streamlit app locally with this command
oceans404 marked this conversation as resolved.
Show resolved Hide resolved

```
program_name = 'addition'
program_test_name = 'addition_test'
streamlit run streamlit_demo_apps/app_[your_program_name].py`
```

### 4. Test your Streamlit app locally
### 3. Test your Streamlit app locally

Make sure the apps will work when deployed by testing this command from the root folder.
View the app in your browser to make sure everything works as expected.

### 4. Commit your code to GitHub

Add and commit your new streamlit app code to your forked Github repo. (Code must be connected to a remote, open source GitHub repository to deploy a Streamlit app.)
Copy link

@crypblizz8 crypblizz8 Sep 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Forking repo was not mentioned in the main repo README / here. Should we explicitly mention this higher in case?

With my test - i cloned repo instead of forking it 😅


```
streamlit run streamlit_demo_apps/[app_file_name].py
git add .
git commit -m "my new streamlit nillion app"
git push origin main
```

For example to make sure the addition app will work when deployed, run
Once you've committed the open source code, you can click the "deploy" button within your local streamlit app. Sign in with Github and select the "Deploy Now" on Streamlit Community Cloud option to deploy the app for free.

<img width="1000" alt="Streamlit Community Cloud" src="https://github.com/user-attachments/assets/74a70b4e-506c-41df-8d59-f949871c9a4e">


### 5. Deploy your app from Streamlit.io

When you click "Deploy Now" from your local app, you'll be taken to streamlit.io and asked to log in with Github to create a new Streamlit app. Set the main file path to your new app `streamlit_demo_apps/app_[your_program_name].py`

<img width="1000" alt="streamlit settings" src="https://github.com/user-attachments/assets/e3821aa4-44b6-4f16-8400-97e531dfef23">

#### Add your Nilchain Private Key using Advanced Settings > Secrets

Go to "Advanced settings" and in Secrets, copy in the contents of your .streamlit/secrets.toml file. At a minimum, make sure to add your secret private key:

```
streamlit run streamlit_demo_apps/app_addition.py
nilchain_private_key = "YOUR_FUNDED_PRIVATE_KEY"
```

<img width="1000" alt="advanced settings" src="https://github.com/user-attachments/assets/6b48b225-60b7-41bd-8591-c04419131bf8">

Save and click "Deploy" to deploy your testnet-connected Streamlit app.

### 6. Access Your Live Streamlit App

Once deployed, you’ll get a live link to your Nillion Testnet Streamlit app!

Example live Streamlit App: https://stephs-nada-multiplication-app.streamlit.app/
12 changes: 8 additions & 4 deletions streamlit_demo_apps/app_addition.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,26 @@
# This file was automatically generated by the generate-streamlit-app script.
# To run this file: from the root directory run `streamlit run streamlit_demo_apps/app_addition.py`

import sys
import os

sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
import streamlit_app
import streamlit_app

program_name = 'addition'
program_test_name = 'addition_test'
program_name = "addition"
program_test_name = "addition_test"

def main():
current_dir = os.path.dirname(os.path.abspath(__file__))
path_nada_bin = os.path.join(current_dir, "compiled_nada_programs", f"{program_name}.nada.bin")
path_nada_json = os.path.join(current_dir, "compiled_nada_programs", f"{program_name}.nada.json")

if not os.path.exists(path_nada_bin):
raise FileNotFoundError(f"Add `{program_name}.nada.bin` to the compiled_nada_programs folder.")
if not os.path.exists(path_nada_json):
raise FileNotFoundError(f"Run nada build --mir-json and add `{program_name}.nada.json` to the compiled_nada_programs folder.")

streamlit_app.main(program_test_name, path_nada_bin, path_nada_json)

if __name__ == "__main__":
main()

Loading
Loading