Skip to content

TexteaInc/funix

main
Switch branches/tags

Name already in use

A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?
Code

Latest commit

 

Git stats

Files

Permalink
Failed to load latest commit information.

Funix.io
The laziest way to build AI/data demos apps in Python.


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.

Hello, world!

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:

screenshots/hello.png

Hello, Gen AI (in no additional code)!

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

openai.api_key = os.environ.get("OPENAI_KEY")

def ChatGPT(prompt: str) -> str:
    completion = openai.ChatCompletion.create(
        messages=[{"role": "user", "content": prompt}],
        model="gpt-3.5-turbo"
    )
    return completion["choices"][0]["message"]["content"]

With the magical command funix -l chatGPT_lazy.py, you will get a web app like this: screenshots/ChatGPT_lazy.png

Love Funix? Give us a star

Borrowed from AppFlowy

Zen: The types become widgets

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

four input types

Defining your own types (coming soon)

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

Being lazy: less (code) is better

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.

Gradio vs. Funix

The UI made in Funix looks like below. By leveraging the Markdown syntax, the output layout can be done easily.

Hangman in Funix

Themes: more than colors and fonts

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
    }
  },
}

Features to build apps instead of demos

History: Keep your call logs

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.

history sidebar

Multipage data passing and session/state management

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

session

Prefill

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

Secret: Access control

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.

secret

Gallery

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)

Chatbot from any generative LLMs on HuggingFace

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

Multiturn chat

ChatGPT, multi-turn

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)

Multiturn chat

Shortest Dall-E web app in Python

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

Dalle demo

Compound UIs

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 the table_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

table plot demo static

Bioinformatics: vector stripping

examples/bioinformatics/vector_strip.py πŸ‘ˆ Click to see source code

bioinfo vector strip

Multimedia inputs and outputs

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()

shipping example

Layout and EasyPost shipping

examples/layout_easypost_shipping.py πŸ‘ˆ Click me to see source code

shipping example

Usage

Installation

  • 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

Building the frontend

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:

  1. cd frontend
  2. yarn install
  3. yarn start

Building the frontend with MUI Pro

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:

  1. Install Node.js and Yarn;
  2. Create a file called .env in the frontend folder;
  3. Add MUI_PRO_LICENSE_KEY=[your_key] to the file;
  4. Run yarn funix:build to build the frontend;
  5. Done!

Command line options

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.

Call funix in Python

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__)

Exposing a Funix-converted app to the public

funix [module] --host [your_server_ip]

How to contribute

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.

Acknowledgement

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.

Team

The Funix team at Textea consists of: