Skip to content

Commit

Permalink
Merge pull request #36 from NillionNetwork/feat/questionary
Browse files Browse the repository at this point in the history
Added script for turning any nada program and test into a streamlit app
  • Loading branch information
oceans404 authored Oct 10, 2024
2 parents 1465877 + 5f4d9ef commit 8471b2f
Show file tree
Hide file tree
Showing 18 changed files with 284 additions and 134 deletions.
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
59 changes: 37 additions & 22 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]]

# 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 @@ -153,35 +186,19 @@ def main(nada_test_file_name=None, path_nada_bin=None, path_nada_json=None):

# Display the program code
st.subheader(f"{program_name}.py")
st.code(program_code, language='python')
with st.expander(f"Nada Program: {program_name}"):
st.code(program_code, language='python')

# 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 +220,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
69 changes: 49 additions & 20 deletions streamlit_demo_apps/README.md
Original file line number Diff line number Diff line change
@@ -1,49 +1,78 @@
# 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. 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. Fork this repo

### 1. 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:
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

`app_addition.py`
### 2. Run the script to generate a new streamlit app for your program

### 2. Copy the compiled Nada program files from the target/ folder into the streamlit_demo_apps/compiled_nada_programs folder
From the root folder of this repo, run the generate-streamlit-app script:

Check out the compiled Nada program files for addition:

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
### 3. 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

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

### 4. 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.

### 5. 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.)

```
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">

### 6. 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.

### 7. 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

0 comments on commit 8471b2f

Please sign in to comment.