🦊

AIコーディングの実力: Shiny for Pythonでの検証

に公開

LLMによるコード生成が広く用いられるようになってきました。
https://www.nikkei.com/article/DGXZQOGN25E2R0V20C25A4000000/
そこで、LLMにリクエストを投げる、簡単なウェブアプリケーションを生成する一例を作成しました。
pythonから利用するケースがLLMでは多い前提で、pythonのフレームワークを対象としました。

フレームワークの選定

Streamlit は、手軽に始められるオープンソースのアプリケーションフレームワークです。
(原文: Streamlit is an open-source app framework that is a breeze to get started with. https://streamlit.io/)
自分で使ってみて、これには同意する部分がかなりあります。
しかしながら、前記のステートメントの範囲を超えると、フィットしないところが出てきました。

Streamlit は、ユーザー入力に変更があるたびに全体を再実行することによって、開発を単純化しています。この戦略は、最初期段階では効率的に機能しますが、スコープが拡大するにつれて、すぐに制約となってきます。
(原文: The idea of Streamlit is to simplify application development by rerunning the entire application script whenever any user input changes. This strategy leads to a great initial user experience, but quickly becomes constricting as your application grows in scope. https://shiny.posit.co/py/docs/comp-streamlit.html)
という指摘は、簡潔で的を射たものだと思っています。

この指摘をしているPositのフレームワークである、Shiny for Pythonを使ってみたいと思いました。
プログラムは生成AIに書いてもらうと、最初期段階も楽なのでは、という試みです。

最初に生成されたコード

次のプロンプトでコードの生成を依頼します。

shiny for pythonを使った、OpenAIのo3-miniをエンジンとしたチャットのプログラムを作成してください。
OPENAI_API_KEYは環境変数として定義済みです

返ってきたコードは次の通りです。

from shiny import App, ui, reactive
import openai
import os

# OpenAI APIキーを環境変数から取得
openai.api_key = os.getenv("OPENAI_API_KEY")

# UIの定義
app_ui = ui.page_fluid(
    ui.h2("OpenAI Chat with o3-mini"),
    ui.input_text("user_input", "Enter your message:", placeholder="Type your message here..."),
    ui.output_text_verbatim("chat_response"),
    ui.action_button("send", "Send")
)

# サーバーロジックの定義
def server(input, output, session):
    @reactive.Effect
    @reactive.event(input.send)
    def handle_chat():
        user_message = input.user_input()
        if not user_message.strip():
            output.chat_response.set("Please enter a valid message.")
            return
        
        try:
            # OpenAI APIを使用して応答を取得
            response = openai.Completion.create(
                engine="o3-mini",
                prompt=user_message,
                max_tokens=150
            )
            output.chat_response.set(response.choices[0].text.strip())
        except Exception as e:
            output.chat_response.set(f"Error: {str(e)}")

# アプリケーションの作成
app = App(app_ui, server)

inputとoutputを明示的にわけて、デコレータを使って記述するのはわかりやすい印象です。
また、セッション変数やコールバック関数を書いていくよりも手間が少ないと感じました。
リアクティビティもミニマム(?)ではないかと。リアクティビティの詳細については、
チートシートに記載されています。
https://rstudio.github.io/cheatsheets/html/shiny-python.html

コードの修正

このコードを実行するにあたって、importされているパッケージをインストールします。

python3 -m pip install shiny openai

次のコマンドで実行すると、

shiny run app:app

エラーがでました。

(oaplatform) oaplatform $ shiny run app:app
Traceback (most recent call last):
  File "/home/kit/act/oaplatform/bin/shiny", line 8, in <module>
    sys.exit(main())
             ^^^^^^
  File "/home/kit/act/oaplatform/lib/python3.12/site-packages/click/core.py", line 1161, in __call__
    return self.main(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/kit/act/oaplatform/lib/python3.12/site-packages/click/core.py", line 1082, in main
    rv = self.invoke(ctx)
         ^^^^^^^^^^^^^^^^
  File "/home/kit/act/oaplatform/lib/python3.12/site-packages/click/core.py", line 1697, in invoke
    return _process_result(sub_ctx.command.invoke(sub_ctx))
                           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/kit/act/oaplatform/lib/python3.12/site-packages/click/core.py", line 1443, in invoke
    return ctx.invoke(self.callback, **ctx.params)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/kit/act/oaplatform/lib/python3.12/site-packages/click/core.py", line 788, in invoke
    return __callback(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/kit/act/oaplatform/lib/python3.12/site-packages/shiny/_main.py", line 194, in run
    return run_app(
           ^^^^^^^^
  File "/home/kit/act/oaplatform/lib/python3.12/site-packages/shiny/_main.py", line 402, in run_app
    uvicorn.run(  # pyright: ignore[reportUnknownMemberType]
  File "/home/kit/act/oaplatform/lib/python3.12/site-packages/uvicorn/main.py", line 580, in run
    server.run()
  File "/home/kit/act/oaplatform/lib/python3.12/site-packages/uvicorn/server.py", line 66, in run
    return asyncio.run(self.serve(sockets=sockets))
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.12/asyncio/runners.py", line 194, in run
    return runner.run(main)
           ^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.12/asyncio/runners.py", line 118, in run
    return self._loop.run_until_complete(task)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.12/asyncio/base_events.py", line 687, in run_until_complete
    return future.result()
           ^^^^^^^^^^^^^^^
  File "/home/kit/act/oaplatform/lib/python3.12/site-packages/uvicorn/server.py", line 70, in serve
    await self._serve(sockets)
  File "/home/kit/act/oaplatform/lib/python3.12/site-packages/uvicorn/server.py", line 77, in _serve
    config.load()
  File "/home/kit/act/oaplatform/lib/python3.12/site-packages/uvicorn/config.py", line 435, in load
    self.loaded_app = import_from_string(self.app)
                      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/kit/act/oaplatform/lib/python3.12/site-packages/uvicorn/importer.py", line 19, in import_from_string
    module = importlib.import_module(module_str)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.12/importlib/__init__.py", line 90, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "<frozen importlib._bootstrap>", line 1387, in _gcd_import
  File "<frozen importlib._bootstrap>", line 1360, in _find_and_load
  File "<frozen importlib._bootstrap>", line 1331, in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 935, in _load_unlocked
  File "<frozen importlib._bootstrap_external>", line 995, in exec_module
  File "<frozen importlib._bootstrap>", line 488, in _call_with_frames_removed
  File "/home/kit/act/oaplatform/app.py", line 13, in <module>
    ui.action_button("send", "Send")
    ^^^^^^^^^^^^^^^^
AttributeError: module 'shiny.ui' has no attribute 'action_button'

ui.action_buttonはないので、ui.input_action_buttonに修正すると画面を出力できました。
Positのサイト ( https://shiny.posit.co/py/api/core/ )によるとこちらが正でした。

ここで、生成AIについて教えてください、と入力してsendボタンを押すと、

エラーとなり、メッセージは、

Traceback (most recent call last):
  File "/home/kit/act/oaplatform/app.py", line 28, in handle_chat
    response = openai.Completion.create(
               ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/kit/act/oaplatform/lib/python3.12/site-packages/openai/lib/_old_api.py", line 39, in __call__
    raise APIRemovedInV1(symbol=self._symbol)
openai.lib._old_api.APIRemovedInV1: 

You tried to access openai.Completion, but this is no longer supported in openai>=1.0.0 - see the README at https://github.com/openai/openai-python for the API.

You can run `openai migrate` to automatically upgrade your codebase to use the 1.0.0 interface. 

Alternatively, you can pin your installation to the old version, e.g. `pip install openai==0.28`

A detailed migration guide is available here: https://github.com/openai/openai-python/discussions/742


During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/home/kit/act/oaplatform/lib/python3.12/site-packages/shiny/reactive/_reactives.py", line 584, in _run
    await self._fn()
  File "/home/kit/act/oaplatform/lib/python3.12/site-packages/shiny/_utils.py", line 279, in fn_async
    return fn(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^
  File "/home/kit/act/oaplatform/lib/python3.12/site-packages/shiny/reactive/_reactives.py", line 901, in new_user_fn
    return user_fn()
           ^^^^^^^^^
  File "/home/kit/act/oaplatform/app.py", line 35, in handle_chat
    output.chat_response.set(f"Error: {str(e)}")
    ^^^^^^^^^^^^^^^^^^^^
AttributeError: 'Outputs' object has no attribute 'chat_response'
/home/kit/act/oaplatform/lib/python3.12/site-packages/shiny/reactive/_reactives.py:566: ReactiveWarning: Error in Effect: 'Outputs' object has no attribute 'chat_response'
  await self._run()
Unhandled error: 'Outputs' object has no attribute 'chat_response'
INFO:     connection closed

openaiのバージョンが上がって、インターフェースが変わったことが原因なので、

from openai import OpenAI
openai_client = OpenAI(
    api_key=os.environ.get("OPENAI_API_KEY"),
)

を追加して、

response = openai_client.responses.create(
        model="o3-mini",
        input=input,
    )
return response.output_text

の形式でメッセージを授受するようにします。

Shinyの書き方として、outputするオブジェクトを関数名にするので、

def chat_response():#handle_chat():

また、デコレータは@reactive.Effectではなく、@render.textが適切なため、renderもimport。
o3-miniも使えなくなっていたので、4.1-miniに変更して

# from shiny import App, ui, reactive
from shiny import App, ui, reactive, render
import openai
from openai import OpenAI
import os

# OpenAI APIキーを環境変数から取得
openai.api_key = os.getenv("OPENAI_API_KEY")

# UIの定義
app_ui = ui.page_fluid(
    ui.h2("OpenAI Chat with 4.1-mini"),
    ui.input_text("user_input", "Enter your message:", placeholder="Type your message here..."),
    ui.output_text_verbatim("chat_response"),
    # ui.action_button("send", "Send")
    ui.input_action_button("send", "Send")
)


openai_client = OpenAI(
    api_key=os.environ.get("OPENAI_API_KEY"),
)


# サーバーロジックの定義
def server(input, output, session):
    #@reactive.Effect
    @render.text
    @reactive.event(input.send)
    def chat_response():#handle_chat():
        # user_message = input.user_input()
        user_message = input.user_input.get()
        if not user_message:
        #if not user_message.strip():
            # output.chat_response.set("Please enter a valid message.")
            # return
            return "Please enter a valid message."
        
        try:
            # OpenAI APIを使用して応答を取得
            # response = openai.Completion.create(
            #     engine="o3-mini",
            #     prompt=user_message,
            #     max_tokens=150
            # )
            response = openai_client.responses.create(
                #model="o3-mini",
                model="gpt-4.1-mini",
                input=user_message,
            )
            #output.chat_response.set(response.choices[0].text.strip())
            return response.output_text
        except Exception as e:
            #output.chat_response.set(f"Error: {str(e)}")
            return f"Error: {str(e)}"

# アプリケーションの作成
app = App(app_ui, server)

というプログラムが今回の着地です。
これを実行して、プロンプトを入力、sendボタンを押下した結果が次になります。

LLMの出力はマークダウン形式ですが、そのままテキストとして出力されています。
今回はここで終わりですが、パースしてきれいに出力していきたいところです。

おわりに

この試みでは、生成されたコードがそのまま使える、とはなりませんでしたが、
LLM以前と比べると、とても便利になったと感じています。
学習可能なコードの量が多いフレームワークやライブラリバージョンであれば、より手間のかからないコード生成が期待できそうです。

一方で、冒頭の記事中で、コードの半分をAIが書く、という記載がありました。
半分というのが行数の半分という意味だとすると、動かないコードが散らばっている場合は
うれしさも中くらいかもしれません。さらなる進化を期待です!

CareNet Engineers

Discussion