見出し画像

Python学習:ollama-pythonモジュールの例で喋らしてみる

Ollamaの日本語表示がちゃんとなるようになったことと、githubにわかりやすい整理がされたので、チャレンジです。

最初は、下記でollamaモジュールを使えるようにします。(自分の場合llamaenvという名前のconda環境でやってます)

pip install ollama

それから、exampleにある async-chat-stream の main.pyの中身を、GPT-4にいろいろ教えてもらいます。

最初に読み込むモジュールの解説 (import ollamaは省いた)

import shutil
import asyncio
import argparse

もちろんです。以下に、Pythonの`shutil`、`asyncio`、`argparse`モジュールについて説明します。

### shutil (shell utilities)
`shutil`モジュールは、ファイル操作のための高水準なインターフェイスを提供します。これにはファイルのコピー、移動、名前の変更、削除などが含まれます。`shutil`を使うと、ディレクトリツリーの操作やファイルのメタデータのコピーも簡単にできます。

例えば、`shutil.copy(src, dst)`は`src`から`dst`にファイルをコピーし、`shutil.move(src, dst)`はファイルやディレクトリを`src`から`dst`に移動します。

### asyncio
`asyncio`は、非同期I/Oを使用した並行コードの書き方をサポートするモジュールです。イベントループを中心に構築され、コルーチンやタスクといった概念を用いて非同期プログラミングを実現します。

Python 3.5以降では、`async`と`await`というキーワードが導入され、非同期プログラムの可読性と使いやすさが大幅に向上しました。`asyncio`を使うことで、ネットワーク接続、データベースの操作、ファイル読み書きなどを効率的に行えます。

### argparse
`argparse`モジュールは、コマンドライン引数をプログラムで使いやすく解析するための機能を提供します。スクリプトがコマンドラインから引数を受け取る際に、その引数をパースして個々のオプションを適切な型に変換し、プログラム内で利用しやすい形で提供します。

`argparse`はオプションのデフォルト値の設定、ヘルプメッセージの自動生成、ユーザーが提供した引数の検証などを行うことができます。

例えば、以下のように`argparse`を使ってコマンドライン引数を定義することができます。

```python
import argparse

parser = argparse.ArgumentParser(description="Example script.")
parser.add_argument('input', help="Input file")
parser.add_argument('-o', '--output', help="Output file")
args = parser.parse_args()

print(args.input)
if args.output:
print(args.output)
```

この例では、位置引数`input`と、オプション引数`-o`または`--output`を定義しています。プログラムを実行する際にこれらの引数を提供することで、スクリプト内で値を使用することができます。

これらのモジュールはPythonの標準ライブラリの一部ですので、conda環境やその他のPython環境に別途インストールする必要はありません。Pythonをインストールするとデフォルトで利用可能になります。shutil
asyncio
argparse


これらはすべてPythonのインストールと共に提供されるため、追加でインストールする手順を踏む必要はなく、インポートして直ちに使用できます。

GPT-4

最初の部分の解説

async def speak(speaker, content):
  if speaker:
    p = await asyncio.create_subprocess_exec(speaker, content)
    await p.communicate()

Pythonでは、`def`の前に `async`を付けることで、その関数を非同期関数(コルーチン)として定義します。非同期関数は、`asyncio` モジュールのイベントループを介して実行される非同期コードです。以下は `async`を使用する理由とその意味についての詳細です。

### asyncの意味とは
`async`キーワードは、Python 3.5以降で導入されたもので、非同期プログラミングをサポートするためのものです。通常の関数定義の前に `async` を付けることで、その関数は「コルーチン関数」となり、以下の特性を持ちます。

- コルーチン関数は、呼び出されたときに直接実行されるのではなく、コルーチンオブジェクトを返します。このオブジェクトをイベントループに渡すことで、非同期に実行されます。
- コルーチン関数の内部では、`await`キーワードを使用して、他の非同期操作が完了するまでの間、コルーチンの実行を一時停止し、イベントループに制御を戻すことができます。
- 非同期操作が完了すると、イベントループはコルーチンの実行を再開します。

### asyncの利点
非同期関数を使用する主な利点は、I/Oバウンド(入出力に時間がかかる)操作や高レイテンシ(待ち時間)操作を効率的に扱えることです。たとえば、ネットワークリクエストやデータベース操作、ファイルの読み書きなどの操作では、データの送受信やディスクへのアクセスに時間がかかることがあります。`async`/`await`を使用すると、これらの待ち時間中に他のコードを実行することができ、全体のアプリケーションの応答性を高めることができます。

### async関数の使用例
以下は、非同期関数を使ったシンプルな例です。

```python
import asyncio

async def my_coroutine():
    print('Start')
    await asyncio.sleep(1) # 1秒間非同期に待機
    print('End')
```

このコードでは、`my_coroutine`関数は非同期関数であり、`print('Start')`を実行した後、非同期に1秒間待機します。この待機中に、イベントループは他のタスクを実行することができます。1秒後には、`print('End')`が実行されます。

### 非同期関数の実行
非同期関数を実行するには、イベントループを使用します。

```python
import asyncio

async def main():
    await my_coroutine() # 非同期関数を待機

# Python 3.7以降ではasyncio.run()でイベントループを簡単に実行できます
asyncio.run(main())
```

この例では、`main` 関数はコルーチン `my_coroutine` を `await` しており、`asyncio.run(main())` によってイベントループが開始され、非同期関数が実行されます。

こちらのコードは `asyncio` モジュールを使用して、非同期的に外部プロセスを生成し、それと通信する例です。`async def` で始まる部分はコルーチンを定義しており、`await` 式を使って非同期の操作を待機することができます。具体的なコードの説明は以下の通りです。

```python
async def speak(speaker, content):
```
この行は `speak` という非同期関数(コルーチン)を定義しています。`speaker` と `content` という2つの引数を取ります。

```python
if speaker:
```
ここでは、関数に渡された `speaker` 引数が `True` であるかどうかをチェックしています。`speaker` が `None` や空文字列(`""`)、`0`、`False`、または空のコレクション(例えば空のリストや辞書)の場合、この文は `False` と評価され、次の行には進みません。

```python
p = await asyncio.create_subprocess_exec(speaker, content)
```
この行では `asyncio.create_subprocess_exec` 関数を非同期的に呼び出しています。この関数は渡された `speaker` を実行ファイルの名前、`content` をその実行ファイルに渡す引数として使用して、新しいサブプロセスを作成します。この関数は `subprocess.Popen` に似ていますが、非同期で動作する点が異なります。`await` キーワードは、サブプロセスの起動が完了するまで現在のコルーチンの実行を一時停止します。

```python
await p.communicate()
```
最後に、この非同期関数は、作成したサブプロセスオブジェクト `p` との通信を開始します。`communicate` メソッドはサブプロセスの終了を待ち、その出力をキャプチャします。このメソッドも非同期であり、`await` を使ってサブプロセスが終了するのを待機します。

要約すると、この非同期関数 `speak` は、`speaker` に有効な値が渡された場合にのみ、それを実行ファイル名として新しいサブプロセスを非同期的に起動し、そのプロセスが終了するのを待つことで、外部のプログラムやコマンドを実行する機能を提供します。

mainの最初の部分の解説

async def main():
  parser = argparse.ArgumentParser()
  parser.add_argument('--speak', default=False, action='store_true')
  args = parser.parse_args()

  speaker = None
  if not args.speak:
    ...
  elif say := shutil.which('say'):
    speaker = say
  elif (espeak := shutil.which('espeak')) or (espeak := shutil.which('espeak-ng')):
    speaker = espeak

このコードは、コマンドライン引数を処理し、利用可能な音声合成ツールを検出するための非同期関数 `main` の一部です。以下にコードの説明を続けます。

```python
async def main():
```
これは、非同期実行をするための `main` コルーチン関数を定義しています。

```python
parser = argparse.ArgumentParser()
```
`argparse` モジュールの `ArgumentParser` オブジェクトを作成してコマンドライン引数のパーサーを初期化しています。

```python
parser.add_argument('--speak', default=False, action='store_true')
```
この行では、`--speak` というコマンドラインオプションを追加しています。このオプションが指定された場合は `True` を、そうでない場合はデフォルトの `False` を返します。`action='store_true'` は、このオプションが指定されたときに `True` を自動的に割り当てるためのアクションです。

```python
args = parser.parse_args()
```
`parse_args` メソッドを使ってコマンドライン引数を解析し、その結果を `args` に格納しています。

```python
speaker = None
```
`speaker` という変数を `None` で初期化しています。この変数は後で使用可能な音声合成ツールのパスを保持するために使用されます。

```python
if not args.speak:
...
```
ここでは、`--speak` オプションがコマンドラインで指定されていない場合の条件分岐をしています。この場合、音声合成ツールを探す必要がないため、何らかの別の処理を行うコードが続くことになります(`...` は省略部分を示しています)。

```python
elif say := shutil.which('say'):
speaker = say
```
この `elif` ブロックでは、Python 3.8で導入された代入式(ウォルラス演算子 `:=`)を使用しています。`shutil.which` 関数を使用して `say` コマンド(macOSの音声合成ツール)のパスを検索し、もし存在すればそのパスを `speaker` 変数に代入します。

```python
elif (espeak := shutil.which('espeak')) or (espeak := shutil.which('espeak-ng')):
speaker = espeak
```
この `elif` ブロックでは、`espeak` または `espeak-ng` コマンド(LinuxやWindowsで使用される音声合成ツール)のパスを `shutil.which` 関数を使って検索しています。どちらかのツールが見つかれば、そのパスが `speaker` 変数に代入されます。

このコードの目的は、システム上で利用可能な音声合成ツールを自動的に検出し、そのツールのパスを `speaker` 変数に設定することです。その後、この `speaker` 変数が `speak` 関数で使用されると推察されます。

Macだけでなく、WindowsやLinuxでも音声発話ができるようです。macでは say コマンドというのがあるのを知りました。

あとスクリプトを走らせる時に --speach をつけるか否かで音声発話の設定ができるということですね。

mainのwhile部分の解説

  while True:
    if content_in := input('>>> '):
      messages.append({'role': 'user', 'content': content_in})

      content_out = ''
      message = {'role': 'assistant', 'content': ''}
      async for response in await client.chat(model='mistral', messages=messages, stream=True):
        if response['done']:
          messages.append(message)

        content = response['message']['content']
        print(content, end='', flush=True)

        content_out += content
        if content in ['.', '!', '?', '\n']:
          await speak(speaker, content_out)
          content_out = ''

        message['content'] += content

      if content_out:
        await speak(speaker, content_out)
      print()

このコードは、ユーザーの入力を受け取り、非同期に処理して音声で応答する非同期のチャットループを実装する部分です。詳細に説明します。

```python
while True:
```
`while True` は無限ループを作ります。これは、ユーザーが明示的に終了を指示するまで、プログラムが継続してユーザーの入力を受け付けることを意味します。

```python
if content_in := input('>>> '):
    messages.append({'role': 'user', 'content': content_in})
```
代入式 `:=` を使って、ユーザーの入力を `content_in` 変数に代入し、何かしらの内容(空文字列でない)が入力された場合、それを `messages` リストにユーザーのメッセージとして追加します。

```python
content_out = ''
message = {'role': 'assistant', 'content': ''}
```
変数 `content_out` は、アシスタントからの応答を一時的に格納するための文字列です。`message` は、アシスタントのメッセージを表す辞書で、応答が完了したら `messages` リストに追加されます。

```python
async for response in await client.chat(model='mistral', messages=messages, stream=True):
```
ここで `client.chat` 関数により、モデル `mistral` に対して非同期のチャットセッションを開始します。`stream=True` は、応答がストリーム(リアルタイム)で返されることを意味します。この `async for` ループは非同期イテレータからリアルタイムの応答をループ処理します。

```python
if response['done']:
    messages.append(message)
```
`response` 辞書の `done` キーが `True` の場合、これはアシスタントが応答を完了したことを意味し、`message` を `messages` リストに追加します。

```python
content = response['message']['content']
print(content, end='', flush=True)
```
応答から得られたメッセージの内容を取得し、端末に出力します。`flush=True` を指定すると、出力がバッファに保持されずにすぐに端末に表示されます。

```python
content_out += content
if content in ['.', '!', '?', '\n']:
    await speak(speaker, content_out)
    content_out = ''
```
応答の内容を `content_out` に追加します。文章の終わりを示す句読点や改行が含まれている場合、`speak` コルーチンを呼び出して内容を音声出力し、`content_out` をリセットします。

```python
message['content'] += content
```
アシスタントのメッセージに応答内容を追加します。

```python
if content_out:
    await speak(speaker, content_out)
print()
```
ループの最後で、もし `content_out` にまだ出力すべきテキストが残っていれば、それを音声で出力し、改行を出力してループを継続します。

このコードの `speak` 関数は、`speaker` 変数に格納された音声合成ツールを使用して、渡されたテキストを音声で出力する非同期関数です。この関数の詳細は示されていませんが、おそらく渡されたテキストを音声に変換して再生する機能を持っているでしょう。

スクリプト最後の部分

try:
  asyncio.run(main())
except (KeyboardInterrupt, EOFError):
  ...

このコードスニペットは、Pythonの例外処理と非同期実行を使用して、`main` 関数(おそらくチャットボットのメインループを含む)を実行し、特定のユーザーによる中断を処理する部分です。具体的には以下のように動作します。

```python
try:
asyncio.run(main())
```

`asyncio.run()` 関数は、与えられたコルーチン(この場合は `main()`)を実行し、完了するまでプログラムの実行をブロックします。`main()` が非同期関数(`async def` で定義されている)の場合、この関数はイベントループを開始し、`main()` が完了するまでループを実行します。

```python
except (KeyboardInterrupt, EOFError):
...
```

`try` ブロック内のコードが実行されている間に `KeyboardInterrupt` や `EOFError` 例外が発生した場合(例えば、ユーザーが Ctrl+C を押してプログラムの実行を中断したり、入力の終わり(EOF)が検出された場合)、`except` ブロックが実行されます。

`...` は、ここに何らかのクリーンアップコードや終了メッセージを表示するコードなど、例外が発生した際の処理を書く場所です。具体的なコードが省略されているため、何をするかはプログラムの要件によります。例えば、オープンしたファイルをクローズする、データベースのコネクションを閉じる、ユーザーにメッセージを表示するなどの処理が含まれるかもしれません。

全体として、このコードは非同期の `main` 関数を安全に実行し、ユーザーが手動で中断した場合に適切に対応するためのものです。プログラムが正常に終了するか、予期せぬ中断があった場合には、プログラムのクリーンアップ処理や終了処理を適切に行うことができます。

以上です、全体のコードはこちらです。

mistralはいれていないので、openchatにモデルを置き換えて、適当にファイル名をつけて、python hogehoge.py --speak で動かしたら、入力を促す>>> がでたので、文字を入力したらエラーとなりました。

ollamaを常駐させた状態で動かしたら、ちゃんと動いてしゃべりました。macのデフォの音声なので、英語の応答は変でしたが、日本語の応答は普通の日本語で音声がでてきました。
(アクセスビリティ →  読み上げコンテンツ → 読み上げ言語 で英語を選んだら普通の英語でしゃべってくれます。)

終了方法:Ctrl + C では終わりませんでした。Ctrl + Z で終了しました。多分、ollama が走っているからかと思います。

たった59行のスクリプトで喋るようになったのでPythonすごいなと思いました!


#AI #AIとやってみた #やってみた #Python入門 #Ollama #大規模言語モデル




この記事が参加している募集

やってみた

AIとやってみた

この記事を最後までご覧いただき、ありがとうございます!もしも私の活動を応援していただけるなら、大変嬉しく思います。