Funix turns an ordinary Python function into a web app in as simple as one command. With Funix, a data/ML engineer can build web apps without any knowledge of UI, UX, or even Funix. The UI widgets are generated automatically from the function's signature based on a type-to-widget mapping defined in a theme. The theme may be defined by the UI team and shared across apps.
- Minimalist: A Funix app has much shorter code than an app using other frameworks because Funix leverages what's already in Python or Python's ecosystem.
- Abstraction: UI control based on variable types rather than individual variables, for centralized UI styling, simple maintenance, and cross-app reusability and consistency.
- Declarative: Manage all UI aspects using JSON strings.
- Resourceful: Bridge integrate popular Python libraries like Matplotlib and IPython with widely-used UI modules such as MUI, and expose UI controls to Python users through JSON.
- Apps, not demos: Funix comes with the following features so what you build is not just a demo in your company's Intranet but an app accessible to the entire world: history, access control, multipage apps, session management, and cloud deployment (via Funix-Deploy).
Funix supports common built-in data types of Python, and major scientific types such as matplotlib.figure.Figure
or IPython.display
. The widgets are built on top of popular UI frameworks such as Material UI, and their props
are explosed for users to configure in JSON without knowledge about React or JavaScript.
First, install Funix:
pip install funix
Here is a type-hinted Python function saved in the file hello.py
:
def hello(your_name: str) -> str:
return f"Hello, {your_name}."
Running this command
funix -l hello.py # -l for lazy mode using all default settings
will turn it into a web app running at http://localhost:3000
:
You can wrap any Python function into a web app in Funix. We believe that in the Gen AI era, software tools will have very few simple UI widgets, mostly texts, and thus the traditional way to building GUI that requires manually creating widgets for each function I/O is not suitable. Funix is designed to address this problem. For example, the OpenAI's ChatGPT function below, which str-to-str.
# Filename: chapGPT_lazy.py
import os # Python's native
import openai # you cannot skip it
def ChatGPT(prompt: str) -> str:
client = openai.Client()
response = client.chat.completions.create(
messages=[{"role": "user", "content": prompt}],
model="gpt-3.5-turbo"
)
return response.choices[0].message.content
With the magical command funix -l chatGPT_lazy.py
, you will get a web app like this:
The Zen of Funix is to choose widgets for function I/Os based on their types, instead of picking and customizing a widget for each I/O. In this sense, Funix to Python is like CSS to HTML or macros to LaTeX. Ideally, a Data Scientist or a Machine Learning Engineer should NOT touch anything UI -- that's the job of the UI team. The example below shows how four common Python's built-in data types are mapped to widgets.
import typing
import funix
def my_chatbot(
prompt: str,
advanced_features: bool = False,
model: typing.Literal['GPT-3.5', 'GPT-4.0',
'Llama-2', 'Falcon-7B']= 'GPT-4.0',
max_token: range(100, 200, 20)=140
) -> str:
pass
You can define your own types and associate widgets to them, just like in CSS you can define a class or in LaTeX you can define a new control command. For example, the str
type is mapped to a single-line text input box where the text is displayed as you type. It is not suitable for passwords or API tokens. The code below defines a new type password
that replaces characters with asterisks.
import funix
@funix.new_funix_type(
widget=[
"@mui/material/TextField", # the component in MUI
{"type":"password"} # props of TextField in MUI
]
)
class password(str): # the new type, inherited from str
pass
@funix.funix()
def foo(
x: password, # string hidden as asterisks
y: str # normal string
) -> None:
pass
The type-to-widget zen of Funix means writing less code. Often, you only need the core logic of your app. In contrast, in other frameworks, the same function I/O needs to appear twice: once in the function's signature, and the other time in the widget creation code. If you modify the interface of the function, you have to modify the widget creation code as well. This is redundant and doubles the effort.
For example, below is the implementation of the hangman game in Funix (left) and Gradio (right), respectively. In the Funix case (left), the only thing besides the core function is adding a friendly prompt to the argument letter
, which cannot be done using Python's native syntax. In addition, Funix leverages as much of Python's native syntax as possible. Here, we use a global
variable to maintain the state/session and even passing values across pages/functions.
The UI made in Funix looks like below. By leveraging the Markdown syntax, the output layout can be done easily.
A Funix theme defines the mapping from data types to widgets. It further exposes the props
of the UI components that embody the widgets in JSON so you can control the UI without knowing Javascript. Most of components used in Funix are from Material UI.
For example, below is an example theme configure. To know more about how to define and apply a theme, please refer to the Themes section in the reference manual.
{
"name": "test_theme",
"widgets": { // dict, map types to widgets
"str": "inputbox",
"int": "slider[0,100,2]",
"float": ["slider", { "min": 0, "max": 100, "step": 2 }],
"Literal": "radio"
},
"props": { // exposing props of UI components
"slider": { // Funix' sliders are MUI's Sliders
"color": "#99ff00" // an MUI's Slider has a prop called color
},
"radio": { // Funix' radio are MUI's radiobuttons
"size": "medium" // an MUI's radiobutton has a prop called size
}
},
}
Click to expand
A feature request that we hear from many users is that they do not use a Funix-converted app as a demo that they will use only a couple of time but as a real app that they will use again and again. In this case, they want to keep the call history of the app. Funix supports this feature by default. You can access the history of your app by clicking the history button on the top right corner of the app.
Click to expand
A real app usually comes with multiple page, e.g., setting OpenAI token key in one page and then use the token in other GenAI pages. Functions in the same .py
script will become different pages of one app. Any global variable can be used to pass data between functions. When you start Funix with the flag -t
, it will further sessionize the global variables so that different users can have their own sessions. Below is a simple example and the corresponding GIF animation. In the GIF animation you can see that the value y
differs in two browser sessions.
import funix
y = "The default value of y."
@funix.funix()
def set_y(x: str="123") -> str:
global y
y = x
return "Y has been changed. Now check it in the get_y() page."
@funix.funix()
def get_y() -> str:
return y
Click to expand
A special case of passing data between functions is to use the return value of one function as the value in the widget of an argument of another function. This is called prefill. Funix support prefill by using a decorator attribute pre_fill
. Below is an example and the corresponding GIF animation.
import funix
def first_action(x: int) -> int:
return x - 1
def second_action(message: str) -> list[str]:
return message.split(" ")
def third_action(x: int, y: int) -> dict:
return {"x": x, "y": y}
@funix.funix(
pre_fill={
"a": first_action,
"b": (second_action, -1),
"c": (third_action, "x")
}
)
def final_action(a: int, b: str, c: int) -> str:
return f"{a} {b} {c}"
Click to expand
Note: This is not a strong way to protect your app.
To protect your code (e.g., OpenAI-related functions, which may result in some financial loss), you can use the secret
option.
funix my_app.py --secret my_secret_token # use a token provided by you
# or
funix my_app.py --secret True # randomly generate a token
The token will be printed on the Terminal. For example,
$ funix hello.py --secret True
Secrets:
---------------
Name: hello
Secret: 8c9f55d0eb74adbb3c87a445ea0ae92f
Link: http://127.0.0.1:3000/hello?secret=8c9f55d0eb74adbb3c87a445ea0ae92f
The token needs to be included in the URL or manually entered in order to execute the app.
More examples in QuickStart Guide, Reference Manual, or the ./examples
folder.
- ChatPaper (It's like the popular ChatPDF. But in Funix, only 70 lines of code needed.)
- mFlux (synthetic biology)
examples/AI/huggingface.py
π Toggle me to show source code
import os, json, typing # Python's native
import requests # pip install requests
API_TOKEN = os.getenv("HF_TOKEN") # "Please set your API token as an environment variable named HF_TOKEN. You can get your token from https://huggingface.co/settings/token"
def huggingface(
model_name: typing.Literal[
"gpt2",
"bigcode/starcoder",
"google/flan-t5-large"] = "gpt2",
prompt: str = "Who is Einstein?") -> str:
payload = {"inputs": prompt} # not all models use this query and output formats. Hence, we limit the models above.
API_URL = f"https://api-inference.huggingface.co/models/{model_name}"
headers = {"Authorization": f"Bearer {API_TOKEN}"}
response = requests.post(API_URL, headers=headers, json=payload)
if "error" in response.json():
return response.json()["error"]
else:
return response.json()[0]["generated_text"]
examples/AI/chatGPT_multi_turn.py
π Toggle me to show source code
import os
import IPython
import openai
openai.api_key = os.environ.get("OPENAI_KEY")
messages = [] # list of dicts, dict keys: role, content, system
def print_messages_html(messages):
printout = ""
for message in messages:
if message["role"] == "user":
align, left, name = "left", "0%", "You"
elif message["role"] == "assistant":
align, left, name = "right", "30%", "ChatGPT"
printout += f'<div style="position: relative; left: {left}; width: 70%"><b>{name}</b>: {message["content"]}</div>'
return printout
import funix
@funix.funix(
direction="column-reverse",
)
def ChatGPT_multi_turn(current_message: str) -> IPython.display.HTML:
current_message = current_message.strip()
messages.append({"role": "user", "content": current_message})
completion = openai.ChatCompletion.create(
messages=messages,
model='gpt-3.5-turbo',
max_tokens=100,
)
chatgpt_response = completion["choices"][0]["message"]["content"]
messages.append({"role": "assistant", "content": chatgpt_response})
# return print_messages_markdown(messages)
return print_messages_html(messages)
from funix import funix # add line one
from funix.hint import Images # add line two
import openai # pip install openai
openai.api_key = os.environ.get("OPENAI_KEY")
@funix() # add line three
def dalle(prompt: str = "a cat") -> Image:
response = openai.Image.create(prompt=prompt)
return response["data"][0]["url"]
The table is copi-able from Excel! The plot is interactive!
examples/slider_table_plot.py
π Toggle me to show source code
The code below uses an inelegant solution by manually choosing the
sheet
widget. We are working on a better solution by feeding a Pandas DataFrame to thetable_plot
function to automate.
from typing import List
import matplotlib.pyplot as plt
from matplotlib.figure import Figure
@funix(
widgets={
"a": "sheet",
"b": ["sheet", "slider[0,1,0.01]"]
}
)
# below is a simple matplotlib function
def table_plot(a: List[int], b: List[float]) -> Figure:
fig = plt.figure()
plt.plot(a, b)
return fig
examples/bioinformatics/vector_strip.py
π Click to see source code
examples/multimedia/rgb2gray.py
π Toggle me to show source code
import io # Python's native
import PIL # the Python Image Library
import funix
@funix.funix(
title="Convert color images to grayscale images",
)
def gray_it(image: funix.hint.BytesImage) -> funix.hint.Image:
img = PIL.Image.open(io.BytesIO(image))
gray = PIL.ImageOps.grayscale(img)
output = io.BytesIO()
gray.save(output, format="PNG")
return output.getvalue()
examples/layout_easypost_shipping.py
π Click me to see source code
-
From PyPI (stable)
pip install funix
-
From GitHub (latest)
pip install "git+https://github.com/TexteaInc/funix.git"
-
Local development
git clone https://github.com/TexteaInc/funix cd funix pip install -e .
Add
--prefix=~/.local
if pip insists to install to system paths. See #24 and #23
In the last two cases above, you will need to compile the frontend by yourself. Suppose you are in the funix
folder. Then run the following commands:
cd frontend
yarn install
yarn start
Our table widget uses advanced features in MUI Pro. If you have a MUI Pro license, you can build the frontend with MUI Pro by following the steps below:
- Install Node.js and Yarn;
- Create a file called
.env
in thefrontend
folder; - Add
MUI_PRO_LICENSE_KEY=[your_key]
to the file; - Run
yarn funix:build
to build the frontend; - Done!
usage: funix [-h] [-H 0.0.0.0] [-p 3000] [-F] [-B] [-l] [-P] [-d] [-t] [-g None] [-r None] [-s None] [-D None] [--version] [file_folder_or_module_name]
Funix: Building web apps without manually creating widgets
Funix turns your Python function into a web app
by building the UI from the function's signature,
based on the mapping from variable types to UI widgets,
customizable per-widget or kept consistent across apps via themes.
Just write your core logic and leave the rest to Funix.
Visit us at http://funix.io
positional arguments:
file_folder_or_module_name
The Python module containing functions to be turned into web apps by Funix. For example, if your functions are in the file `hello.py`, you should pass `hello.py` here. if you want to turn a module called `hello` into a web app, you should pass `hello`
here, and with --package or -P flag. if you want to turn a full folder called `examples` into a web app, you should pass `examples` here.
options:
-h, --help show this help message and exit
-H 0.0.0.0, --host 0.0.0.0
Host of Funix
-p 3000, --port 3000 Port of Funix
-F, --no-frontend Disable frontend server
-B, --no-browser Disable auto open browser
-l, --lazy Load functions without decorator
-P, --package Enable package mode
-d, --dev Enable development mode
-t, --transform Transform the globals to a session variables
-g None, --from-git None
Import module from git
-r None, --repo-dir None
The directories in the repo that need to be used
-s None, --secret None
The secret key for the full app
-D None, --default None
The default function to run
--version, -v show program's version number and exit
The command funix
above is equivalent to python -m funix
if you have installed Funix.
Besides starting Funix from the command line, you can also use a self-contained .py
script:
import funix
@funix.funix()
def hello(your_name: str) -> str:
return f"Hello, {your_name}."
if __name__ == "__main__":
funix.run(__file__)
funix [module] --host [your_server_ip]
Funix is open-sourced under the MIT License. Community contribution is not only welcomed but desired. Feel free to fork and make a pull request when you are ready. You can also report bugs, suggest new features via the issue tracker or our Discord server.
Funix draws inspiration from FastAPI and Plac: building software interfaces by inferring from function signartures containing type hints. We port this idea from the backend (FastAPI) or the terminal (Python-Fire) to the frontend. We also wanna thank Streamlit, Gradio, PyWebIO, and Pynecone. They inspired us. We are just too lazy to manually define widgets imperatively. Funixβs backend is implemented in Flask and the frontend in Material UI. Lastly, Funix was made possible with the generous investment from Miracle Plus (formerly Y Combinator China) to Textea Inc.
The Funix team at Textea consists of: