ドキュメント

API

ツール利用

LLMが外部関数やAPIと対話できるようにします。

ツール利用により、LLMはLM StudioのREST API(または任意のOpenAIクライアント)経由で、/v1/chat/completionsエンドポイントを通じて外部関数やAPIの呼び出しをリクエストできるようになります。これにより、機能がテキスト出力以外にも拡張されます。


クイックスタート

1. LM Studioをサーバーとして起動する

LM Studioを自身のコードからプログラムで利用するには、LM Studioをローカルサーバーとして実行してください。

サーバーは、LM Studioの「開発者」タブから、またはlms CLI経由で有効にすることができます。

lms server start
lmsをインストールするには、npx lmstudio install-cliを実行してください。

これにより、OpenAIライクなREST API経由でLM Studioと対話できるようになります。LM StudioのOpenAIライクなAPIの概要については、LM Studioをサーバーとして実行するを参照してください。

2. モデルをロードする

モデルは、LM Studioの「チャット」または「開発者」タブから、またはlms CLI経由でロードできます。

lms load

3. 例をコピー、ペーストして実行する!


ツール利用

「ツール利用」とは具体的に何ですか?

ツール利用とは、

  • LLMが関数呼び出しをリクエストするテキストを出力すること(LLMは直接コードを実行できません)。
  • あなたのコードがそれらの関数を実行すること。
  • あなたのコードがその結果をLLMにフィードバックすること。

ハイレベルなフロー

┌──────────────────────────┐
│ SETUP: LLM + Tool list   │
└──────────┬───────────────┘

┌──────────────────────────┐
│    Get user input        │◄────┐
└──────────┬───────────────┘     │
           ▼                     │
┌──────────────────────────┐     │
│ LLM prompted w/messages  │     │
└──────────┬───────────────┘     │
           ▼                     │
     Needs tools?                │
      │         │                │
    Yes         No               │
      │         │                │
      ▼         └────────────┐   │
┌─────────────┐              │   │
│Tool Response│              │   │
└──────┬──────┘              │   │
       ▼                     │   │
┌─────────────┐              │   │
│Execute tools│              │   │
└──────┬──────┘              │   │
       ▼                     ▼   │
┌─────────────┐          ┌───────────┐
│Add results  │          │  Normal   │
│to messages  │          │ response  │
└──────┬──────┘          └─────┬─────┘
       │                       ▲
       └───────────────────────┘

詳細なフロー

LM Studioは、リクエストボディのtoolsパラメータに関数定義が与えられた場合、/v1/chat/completionsエンドポイントを通じてツール利用をサポートします。ツールは、パラメータと使用方法を記述した関数定義の配列として指定されます。例:

これはOpenAIのFunction Calling APIと同じ形式に従っており、OpenAIクライアントSDK経由で動作することが期待されます。

この例のフローでは、モデルとしてlmstudio-community/Qwen2.5-7B-Instruct-GGUFを使用します。

  • LLMにツールのリストを提供します。これらはモデルが呼び出しを*リクエスト*できるツールです。例:

    // the list of tools is model-agnostic
    [
      {
        "type": "function",
        "function": {
          "name": "get_delivery_date",
          "description": "Get the delivery date for a customer's order",
          "parameters": {
            "type": "object",
            "properties": {
              "order_id": {
                "type": "string"
              }
            },
            "required": ["order_id"]
          }
        }
      }
    ]
    

    このリストは、モデルのチャットテンプレートに応じて、モデルのシステムプロンプトに挿入されます。Qwen2.5-Instructの場合、これは次のようになります。

    <|im_start|>system
    You are Qwen, created by Alibaba Cloud. You are a helpful assistant.
    
    # Tools
    
    You may call one or more functions to assist with the user query.
    
    You are provided with function signatures within <tools></tools> XML tags:
    <tools>
    {"type": "function", "function": {"name": "get_delivery_date", "description": "Get the delivery date for a customer's order", "parameters": {"type": "object", "properties": {"order_id": {"type": "string"}}, "required": ["order_id"]}}}
    </tools>
    
    For each function call, return a json object with function name and arguments within <tool_call></tool_call> XML tags:
    <tool_call>
    {"name": <function-name>, "arguments": <args-json-object>}
    </tool_call><|im_end|>
    

    重要: LLMは直接関数、API、またはその他のツールを呼び出すことができないため、これらのツールへの呼び出しを*リクエスト*できるだけです。LLMができるのはテキストを出力することだけで、そのテキストを解析してプログラムで関数を呼び出すことができます。

  • プロンプトが与えられると、LLMは次のいずれかを選択できます。

    • (a) 1つ以上のツールを呼び出す
    User: Get me the delivery date for order 123
    Model: <tool_call>
    {"name": "get_delivery_date", "arguments": {"order_id": "123"}}
    </tool_call>
    
    • (b) 通常通り応答する
    User: Hi
    Model: Hello! How can I assist you today?
    
  • LM Studioは、モデルからのテキスト出力を、OpenAI互換のchat.completionレスポンスオブジェクトに解析します。

    • モデルにtoolsへのアクセスが与えられている場合、LM Studioはツール呼び出しをchat.completionレスポンスオブジェクトのresponse.choices[0].message.tool_callsフィールドに解析しようとします。
    • LM Studioが*正しくフォーマットされた*ツール呼び出しを解析できない場合、レスポンスは標準のresponse.choices[0].message.contentフィールドに返されます。
    • 注意: 小規模なモデルやツール利用のためにトレーニングされていないモデルは、不適切にフォーマットされたツール呼び出しを出力する可能性があり、その結果LM Studioはそれらをtool_callsフィールドに解析できません。これは、tool_callsが期待どおりに受信できない場合のトラブルシューティングに役立ちます。Qwen2.5-Instructのツール呼び出しの不適切なフォーマットの例
    <tool_call>
    ["name": "get_delivery_date", function: "date"]
    </tool_call>
    

    括弧が正しくなく、呼び出しがname, argument形式に従っていないことに注意してください。

  • あなたのコードはchat.completionレスポンスを解析してツール呼び出しをチェックし、モデルによって指定されたパラメータで適切なツールを呼び出します。その後、あなたのコードは以下を両方とも追加します。

    • モデルのツール呼び出しメッセージ
    • ツール呼び出しの結果

    messages配列に、モデルに送り返すために。

    # pseudocode, see examples for copy-paste snippets
    if response.has_tool_calls:
        for each tool_call:
            # Extract function name & args
            function_to_call = tool_call.name     # e.g. "get_delivery_date"
            args = tool_call.arguments            # e.g. {"order_id": "123"}
    
            # Execute the function
            result = execute_function(function_to_call, args)
    
            # Add result to conversation
            add_to_messages([
                ASSISTANT_TOOL_CALL_MESSAGE,      # The request to use the tool
                TOOL_RESULT_MESSAGE               # The tool's response
            ])
    else:
        # Normal response without tools
        add_to_messages(response.content)
    
  • LLMは、更新されたメッセージ配列で再度プロンプトされますが、ツールへのアクセスはありません。これはなぜなら:

    • LLMはすでに会話履歴にツール結果を持っています。
    • LLMに、ユーザーへの最終応答を提供してもらいたいのであって、さらにツールを呼び出してほしくないため。
    # Example messages
    messages = [
        {"role": "user", "content": "When will order 123 be delivered?"},
        {"role": "assistant", "function_call": {
            "name": "get_delivery_date",
            "arguments": {"order_id": "123"}
        }},
        {"role": "tool", "content": "2024-03-15"},
    ]
    response = client.chat.completions.create(
        model="lmstudio-community/qwen2.5-7b-instruct",
        messages=messages
    )
    

    この呼び出し後のresponse.choices[0].message.contentフィールドは、次のようになっている可能性があります。

    Your order #123 will be delivered on March 15th, 2024
    
  • ループはフローのステップ2に戻ります。

注:これはツール利用のための*厳格な*フローです。しかし、あなたのユースケースに最適なフローにするために、このフローを実験することができます。


サポートされているモデル

LM Studioを通じて、*すべて*のモデルが少なくともある程度のツール利用をサポートしています。

しかし、現在、エクスペリエンスの質に影響を与える可能性のある2つのレベルのサポートがあります:ネイティブとデフォルト。

ネイティブツール利用サポートを持つモデルは、アプリ内でハンマーのバッジが表示され、一般的にツール利用シナリオでより良いパフォーマンスを発揮します。

ネイティブツール利用サポート

「ネイティブ」ツール利用サポートとは、以下の両方が該当することを意味します。

  • モデルにツール利用をサポートするチャットテンプレートがある(通常、モデルがツール利用のためにトレーニングされていることを意味します)。
    • これは、tools配列をシステムプロンプトにフォーマットし、モデルにツール呼び出しのフォーマット方法を指示するために使用されます。
    • 例:Qwen2.5-Instructチャットテンプレート
  • LM Studioはそのモデルのツール利用フォーマットをサポートしています。
    • LM Studioがチャット履歴をチャットテンプレートに正しく入力し、モデルが出力するツール呼び出しをchat.completionオブジェクトに解析するために必要です。

現在LM Studioでネイティブツール利用サポートを持つモデル(変更される可能性があります)

デフォルトツール利用サポート

「デフォルト」ツール利用サポートとは、*どちらか*を意味します。

  • モデルにツール利用をサポートするチャットテンプレートがない(通常、モデルがツール利用のためにトレーニングされていないことを意味します)。
  • LM Studioが現在そのモデルのツール利用フォーマットをサポートしていない。

内部的には、デフォルトのツール利用は以下のように機能します。

  • モデルにカスタムシステムプロンプトとデフォルトのツール呼び出しフォーマットを与える。
  • toolロールのメッセージをuserロールに変換し、toolロールを持たないチャットテンプレートとの互換性を保つ。
  • assistantロールのtool_callsをデフォルトのツール呼び出しフォーマットに変換する。

結果はモデルによって異なります。

ターミナルでlms log streamを実行し、次にネイティブツール利用サポートを持たないモデルにtoolsを指定してチャット補完リクエストを送信すると、デフォルトフォーマットを確認できます。デフォルトフォーマットは変更される可能性があります。

デフォルトのツール利用フォーマットの例を表示するには展開してください。
 % lms log stream
Streaming logs from LM Studio

timestamp: 11/13/2024, 9:35:15 AM
type: llm.prediction.input
modelIdentifier: gemma-2-2b-it
modelPath: lmstudio-community/gemma-2-2b-it-GGUF/gemma-2-2b-it-Q4_K_M.gguf
input: "<start_of_turn>system
You are a tool-calling AI. You can request calls to available tools with this EXACT format:
[TOOL_REQUEST]{"name": "tool_name", "arguments": {"param1": "value1"}}[END_TOOL_REQUEST]

AVAILABLE TOOLS:
{
  "type": "toolArray",
  "tools": [
    {
      "type": "function",
      "function": {
        "name": "get_delivery_date",
        "description": "Get the delivery date for a customer's order",
        "parameters": {
          "type": "object",
          "properties": {
            "order_id": {
              "type": "string"
            }
          },
          "required": [
            "order_id"
          ]
        }
      }
    }
  ]
}

RULES:
- Only use tools from AVAILABLE TOOLS
- Include all required arguments
- Use one [TOOL_REQUEST] block per tool
- Never use [TOOL_RESULT]
- If you decide to call one or more tools, there should be no other text in your message

Examples:
"Check Paris weather"
[TOOL_REQUEST]{"name": "get_weather", "arguments": {"location": "Paris"}}[END_TOOL_REQUEST]

"Send email to John about meeting and open browser"
[TOOL_REQUEST]{"name": "send_email", "arguments": {"to": "John", "subject": "meeting"}}[END_TOOL_REQUEST]
[TOOL_REQUEST]{"name": "open_browser", "arguments": {}}[END_TOOL_REQUEST]

Respond conversationally if no matching tools exist.<end_of_turn>
<start_of_turn>user
Get me delivery date for order 123<end_of_turn>
<start_of_turn>model
"

モデルがこのフォーマットに正確に従ってツールを呼び出す場合、つまり

[TOOL_REQUEST]{"name": "get_delivery_date", "arguments": {"order_id": "123"}}[END_TOOL_REQUEST]

LM Studioは、ネイティブサポートモデルと同様に、それらのツール呼び出しをchat.completionsオブジェクトに解析できます。

ネイティブツール利用サポートを持たないすべてのモデルは、デフォルトのツール利用サポートを持ちます。


curlを使用した例

この例では、curlユーティリティを使用して、モデルがツール呼び出しをリクエストする様子を示します。

この例をMacまたはLinuxで実行するには、任意のターミナルを使用してください。Windowsでは、Git Bashを使用してください。

curl https://:1234/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{
    "model": "lmstudio-community/qwen2.5-7b-instruct",
    "messages": [{"role": "user", "content": "What dell products do you have under $50 in electronics?"}],
    "tools": [
      {
        "type": "function",
        "function": {
          "name": "search_products",
          "description": "Search the product catalog by various criteria. Use this whenever a customer asks about product availability, pricing, or specifications.",
          "parameters": {
            "type": "object",
            "properties": {
              "query": {
                "type": "string",
                "description": "Search terms or product name"
              },
              "category": {
                "type": "string",
                "description": "Product category to filter by",
                "enum": ["electronics", "clothing", "home", "outdoor"]
              },
              "max_price": {
                "type": "number",
                "description": "Maximum price in dollars"
              }
            },
            "required": ["query"],
            "additionalProperties": false
          }
        }
      }
    ]
  }'

/v1/chat/completionsで認識されるすべてのパラメータが尊重され、利用可能なツールの配列はtoolsフィールドで提供されるべきです。

モデルが、ユーザーメッセージをツール呼び出しで最もよく満たすと判断した場合、ツール呼び出しリクエストオブジェクトの配列がchoices[0].message.tool_callsフィールドに提供されます。

トップレベルのレスポンスオブジェクトのfinish_reasonフィールドも"tool_calls"で入力されます。

上記のcurlリクエストに対するレスポンスの例は次のようになります。

{
  "id": "chatcmpl-gb1t1uqzefudice8ntxd9i",
  "object": "chat.completion",
  "created": 1730913210,
  "model": "lmstudio-community/qwen2.5-7b-instruct",
  "choices": [
    {
      "index": 0,
      "logprobs": null,
      "finish_reason": "tool_calls",
      "message": {
        "role": "assistant",
        "tool_calls": [
          {
            "id": "365174485",
            "type": "function",
            "function": {
              "name": "search_products",
              "arguments": "{\"query\":\"dell\",\"category\":\"electronics\",\"max_price\":50}"
            }
          }
        ]
      }
    }
  ],
  "usage": {
    "prompt_tokens": 263,
    "completion_tokens": 34,
    "total_tokens": 297
  },
  "system_fingerprint": "lmstudio-community/qwen2.5-7b-instruct"
}

平易な言葉で言うと、上記のレスポンスは、モデルが次のように言っていると考えることができます。

search_products関数を、引数

  • クエリパラメータに「dell」、
  • カテゴリパラメータに「electronics」、
  • 最大価格パラメータに「50」

を渡して呼び出してください。そして結果を返してください。」

tool_callsフィールドは、実際の関数/APIを呼び出すために解析する必要があります。以下の例は、その方法を示しています。


Pythonを使用した例

ツール利用は、Pythonのようなプログラミング言語と組み合わせることで真価を発揮します。そこでは、モデルがリクエストしたときにプログラムで呼び出すために、toolsフィールドで指定された関数を実装できます。

シングルターン例

以下は、モデルがsay_helloという名前の関数を呼び出して、コンソールに挨拶メッセージを出力できるようにする、シンプルなシングルターン(モデルは一度だけ呼び出されます)の例です。

single-turn-example.py

from openai import OpenAI

# Connect to LM Studio
client = OpenAI(base_url="https://:1234/v1", api_key="lm-studio")

# Define a simple function
def say_hello(name: str) → str:
    print(f"Hello, {name}!")

# Tell the AI about our function
tools = [
    {
        "type": "function",
        "function": {
            "name": "say_hello",
            "description": "Says hello to someone",
            "parameters": {
                "type": "object",
                "properties": {
                    "name": {
                        "type": "string",
                        "description": "The person's name"
                    }
                },
                "required": ["name"]
            }
        }
    }
]

# Ask the AI to use our function
response = client.chat.completions.create(
    model="lmstudio-community/qwen2.5-7b-instruct",
    messages=[{"role": "user", "content": "Can you say hello to Bob the Builder?"}],
    tools=tools
)

# Get the name the AI wants to use a tool to say hello to
# (Assumes the AI has requested a tool call and that tool call is say_hello)
tool_call = response.choices[0].message.tool_calls[0]
name = eval(tool_call.function.arguments)["name"]

# Actually call the say_hello function
say_hello(name) # Prints: Hello, Bob the Builder!

このスクリプトをコンソールから実行すると、次のような結果が表示されます。

→ % python single-turn-example.py
Hello, Bob the Builder!

名前を変更して、

messages=[{"role": "user", "content": "Can you say hello to Bob the Builder?"}]

モデルがsay_hello関数を異なる名前で呼び出すのを確認してください。

マルチターン例

それでは、もう少し複雑な例を見てみましょう。

この例では、次のことを行います。

  • get_delivery_date関数を呼び出せるようにする。
  • その関数を呼び出した結果をモデルに再度渡し、モデルがプレーンテキストでユーザーのリクエストに応えられるようにする。
multi-turn-example.py(展開して表示)
from datetime import datetime, timedelta
import json
import random
from openai import OpenAI

# Point to the local server
client = OpenAI(base_url="https://:1234/v1", api_key="lm-studio")
model = "lmstudio-community/qwen2.5-7b-instruct"


def get_delivery_date(order_id: str) → datetime:
    # Generate a random delivery date between today and 14 days from now
    # in a real-world scenario, this function would query a database or API
    today = datetime.now()
    random_days = random.randint(1, 14)
    delivery_date = today + timedelta(days=random_days)
    print(
        f"\nget_delivery_date function returns delivery date:\n\n{delivery_date}",
        flush=True,
    )
    return delivery_date


tools = [
    {
        "type": "function",
        "function": {
            "name": "get_delivery_date",
            "description": "Get the delivery date for a customer's order. Call this whenever you need to know the delivery date, for example when a customer asks 'Where is my package'",
            "parameters": {
                "type": "object",
                "properties": {
                    "order_id": {
                        "type": "string",
                        "description": "The customer's order ID.",
                    },
                },
                "required": ["order_id"],
                "additionalProperties": False,
            },
        },
    }
]

messages = [
    {
        "role": "system",
        "content": "You are a helpful customer support assistant. Use the supplied tools to assist the user.",
    },
    {
        "role": "user",
        "content": "Give me the delivery date and time for order number 1017",
    },
]

# LM Studio
response = client.chat.completions.create(
    model=model,
    messages=messages,
    tools=tools,
)

print("\nModel response requesting tool call:\n", flush=True)
print(response, flush=True)

# Extract the arguments for get_delivery_date
# Note this code assumes we have already determined that the model generated a function call.
tool_call = response.choices[0].message.tool_calls[0]
arguments = json.loads(tool_call.function.arguments)

order_id = arguments.get("order_id")

# Call the get_delivery_date function with the extracted order_id
delivery_date = get_delivery_date(order_id)

assistant_tool_call_request_message = {
    "role": "assistant",
    "tool_calls": [
        {
            "id": response.choices[0].message.tool_calls[0].id,
            "type": response.choices[0].message.tool_calls[0].type,
            "function": response.choices[0].message.tool_calls[0].function,
        }
    ],
}

# Create a message containing the result of the function call
function_call_result_message = {
    "role": "tool",
    "content": json.dumps(
        {
            "order_id": order_id,
            "delivery_date": delivery_date.strftime("%Y-%m-%d %H:%M:%S"),
        }
    ),
    "tool_call_id": response.choices[0].message.tool_calls[0].id,
}

# Prepare the chat completion call payload
completion_messages_payload = [
    messages[0],
    messages[1],
    assistant_tool_call_request_message,
    function_call_result_message,
]

# Call the OpenAI API's chat completions endpoint to send the tool call result back to the model
# LM Studio
response = client.chat.completions.create(
    model=model,
    messages=completion_messages_payload,
)

print("\nFinal model response with knowledge of the tool call result:\n", flush=True)
print(response.choices[0].message.content, flush=True)

このスクリプトをコンソールから実行すると、次のような結果が表示されます。

→ % python multi-turn-example.py

Model response requesting tool call:

ChatCompletion(id='chatcmpl-wwpstqqu94go4hvclqnpwn', choices=[Choice(finish_reason='tool_calls', index=0, logprobs=None, message=ChatCompletionMessage(content=None, refusal=None, role='assistant', function_call=None, tool_calls=[ChatCompletionMessageToolCall(id='377278620', function=Function(arguments='{"order_id":"1017"}', name='get_delivery_date'), type='function')]))], created=1730916196, model='lmstudio-community/qwen2.5-7b-instruct', object='chat.completion', service_tier=None, system_fingerprint='lmstudio-community/qwen2.5-7b-instruct', usage=CompletionUsage(completion_tokens=24, prompt_tokens=223, total_tokens=247, completion_tokens_details=None, prompt_tokens_details=None))

get_delivery_date function returns delivery date:

2024-11-19 13:03:17.773298

Final model response with knowledge of the tool call result:

Your order number 1017 is scheduled for delivery on November 19, 2024, at 13:03 PM.

高度なエージェント例

上記の原則を拡張して、LM Studioモデルとローカルで定義された関数を組み合わせて「エージェント」を作成できます。これは、言語モデルとカスタム関数をペアにして、リクエストを理解し、基本的なテキスト生成を超えたアクションを実行するシステムです。

以下の例のエージェントは、次のことができます。

  • 安全なURLをデフォルトのブラウザで開く。
  • 現在時刻を確認する。
  • ファイルシステムのディレクトリを分析する。
agent-chat-example.py(展開して表示)
import json
from urllib.parse import urlparse
import webbrowser
from datetime import datetime
import os
from openai import OpenAI

# Point to the local server
client = OpenAI(base_url="https://:1234/v1", api_key="lm-studio")
model = "lmstudio-community/qwen2.5-7b-instruct"


def is_valid_url(url: str) → bool:

    try:
        result = urlparse(url)
        return bool(result.netloc)  # Returns True if there's a valid network location
    except Exception:
        return False


def open_safe_url(url: str) → dict:
    # List of allowed domains (expand as needed)
    SAFE_DOMAINS = {
        "lmstudio.ai",
        "github.com",
        "google.com",
        "wikipedia.org",
        "weather.com",
        "stackoverflow.com",
        "python.org",
        "docs.python.org",
    }

    try:
        # Add http:// if no scheme is present
        if not url.startswith(('http://', 'https://')):
            url = 'http://' + url

        # Validate URL format
        if not is_valid_url(url):
            return {"status": "error", "message": f"Invalid URL format: {url}"}

        # Parse the URL and check domain
        parsed_url = urlparse(url)
        domain = parsed_url.netloc.lower()
        base_domain = ".".join(domain.split(".")[-2:])

        if base_domain in SAFE_DOMAINS:
            webbrowser.open(url)
            return {"status": "success", "message": f"Opened {url} in browser"}
        else:
            return {
                "status": "error",
                "message": f"Domain {domain} not in allowed list",
            }
    except Exception as e:
        return {"status": "error", "message": str(e)}


def get_current_time() → dict:
    """Get the current system time with timezone information"""
    try:
        current_time = datetime.now()
        timezone = datetime.now().astimezone().tzinfo
        formatted_time = current_time.strftime("%Y-%m-%d %H:%M:%S %Z")
        return {
            "status": "success",
            "time": formatted_time,
            "timezone": str(timezone),
            "timestamp": current_time.timestamp(),
        }
    except Exception as e:
        return {"status": "error", "message": str(e)}


def analyze_directory(path: str = ".") → dict:
    """Count and categorize files in a directory"""
    try:
        stats = {
            "total_files": 0,
            "total_dirs": 0,
            "file_types": {},
            "total_size_bytes": 0,
        }

        for entry in os.scandir(path):
            if entry.is_file():
                stats["total_files"] += 1
                ext = os.path.splitext(entry.name)[1].lower() or "no_extension"
                stats["file_types"][ext] = stats["file_types"].get(ext, 0) + 1
                stats["total_size_bytes"] += entry.stat().st_size
            elif entry.is_dir():
                stats["total_dirs"] += 1
                # Add size of directory contents
                for root, _, files in os.walk(entry.path):
                    for file in files:
                        try:
                            stats["total_size_bytes"] += os.path.getsize(os.path.join(root, file))
                        except (OSError, FileNotFoundError):
                            continue

        return {"status": "success", "stats": stats, "path": os.path.abspath(path)}
    except Exception as e:
        return {"status": "error", "message": str(e)}


tools = [
    {
        "type": "function",
        "function": {
            "name": "open_safe_url",
            "description": "Open a URL in the browser if it's deemed safe",
            "parameters": {
                "type": "object",
                "properties": {
                    "url": {
                        "type": "string",
                        "description": "The URL to open",
                    },
                },
                "required": ["url"],
            },
        },
    },
    {
        "type": "function",
        "function": {
            "name": "get_current_time",
            "description": "Get the current system time with timezone information",
            "parameters": {
                "type": "object",
                "properties": {},
                "required": [],
            },
        },
    },
    {
        "type": "function",
        "function": {
            "name": "analyze_directory",
            "description": "Analyze the contents of a directory, counting files and folders",
            "parameters": {
                "type": "object",
                "properties": {
                    "path": {
                        "type": "string",
                        "description": "The directory path to analyze. Defaults to current directory if not specified.",
                    },
                },
                "required": [],
            },
        },
    },
]


def process_tool_calls(response, messages):
    """Process multiple tool calls and return the final response and updated messages"""
    # Get all tool calls from the response
    tool_calls = response.choices[0].message.tool_calls

    # Create the assistant message with tool calls
    assistant_tool_call_message = {
        "role": "assistant",
        "tool_calls": [
            {
                "id": tool_call.id,
                "type": tool_call.type,
                "function": tool_call.function,
            }
            for tool_call in tool_calls
        ],
    }

    # Add the assistant's tool call message to the history
    messages.append(assistant_tool_call_message)

    # Process each tool call and collect results
    tool_results = []
    for tool_call in tool_calls:
        # For functions with no arguments, use empty dict
        arguments = (
            json.loads(tool_call.function.arguments)
            if tool_call.function.arguments.strip()
            else {}
        )

        # Determine which function to call based on the tool call name
        if tool_call.function.name == "open_safe_url":
            result = open_safe_url(arguments["url"])
        elif tool_call.function.name == "get_current_time":
            result = get_current_time()
        elif tool_call.function.name == "analyze_directory":
            path = arguments.get("path", ".")
            result = analyze_directory(path)
        else:
            # llm tried to call a function that doesn't exist, skip
            continue

        # Add the result message
        tool_result_message = {
            "role": "tool",
            "content": json.dumps(result),
            "tool_call_id": tool_call.id,
        }
        tool_results.append(tool_result_message)
        messages.append(tool_result_message)

    # Get the final response
    final_response = client.chat.completions.create(
        model=model,
        messages=messages,
    )

    return final_response


def chat():
    messages = [
        {
            "role": "system",
            "content": "You are a helpful assistant that can open safe web links, tell the current time, and analyze directory contents. Use these capabilities whenever they might be helpful.",
        }
    ]

    print(
        "Assistant: Hello! I can help you open safe web links, tell you the current time, and analyze directory contents. What would you like me to do?"
    )
    print("(Type 'quit' to exit)")

    while True:
        # Get user input
        user_input = input("\nYou: ").strip()

        # Check for quit command
        if user_input.lower() == "quit":
            print("Assistant: Goodbye!")
            break

        # Add user message to conversation
        messages.append({"role": "user", "content": user_input})

        try:
            # Get initial response
            response = client.chat.completions.create(
                model=model,
                messages=messages,
                tools=tools,
            )

            # Check if the response includes tool calls
            if response.choices[0].message.tool_calls:
                # Process all tool calls and get final response
                final_response = process_tool_calls(response, messages)
                print("\nAssistant:", final_response.choices[0].message.content)

                # Add assistant's final response to messages
                messages.append(
                    {
                        "role": "assistant",
                        "content": final_response.choices[0].message.content,
                    }
                )
            else:
                # If no tool call, just print the response
                print("\nAssistant:", response.choices[0].message.content)

                # Add assistant's response to messages
                messages.append(
                    {
                        "role": "assistant",
                        "content": response.choices[0].message.content,
                    }
                )

        except Exception as e:
            print(f"\nAn error occurred: {str(e)}")
            exit(1)


if __name__ == "__main__":
    chat()

このスクリプトをコンソールから実行すると、エージェントとチャットできるようになります。

→ % python agent-example.py
Assistant: Hello! I can help you open safe web links, tell you the current time, and analyze directory contents. What would you like me to do?
(Type 'quit' to exit)

You: What time is it?

Assistant: The current time is 14:11:40 (EST) as of November 6, 2024.

You: What time is it now?

Assistant: The current time is 14:13:59 (EST) as of November 6, 2024.

You: Open lmstudio.ai

Assistant: The link to lmstudio.ai has been opened in your default web browser.

You: What's in my current directory?

Assistant: Your current directory at `/Users/matt/project` contains a total of 14 files and 8 directories. Here's the breakdown:

- Files without an extension: 3
- `.mjs` files: 2
- `.ts` (TypeScript) files: 3
- Markdown (`md`) file: 1
- JSON files: 4
- TOML file: 1

The total size of these items is 1,566,990,604 bytes.

You: Thank you!

Assistant: You're welcome! If you have any other questions or need further assistance, feel free to ask.

You:

ストリーミング

/v1/chat/completionsstream=true)でストリーミングする場合、ツール呼び出しはチャンクで送信されます。関数名と引数は、chunk.choices[0].delta.tool_calls.function.namechunk.choices[0].delta.tool_calls.function.argumentsを介して断片的に送信されます。

例えば、get_current_weather(location="San Francisco")を呼び出す場合、ストリーミングされるChoiceDeltaToolCallは、各chunk.choices[0].delta.tool_calls[0]オブジェクト内で次のようになります。

ChoiceDeltaToolCall(index=0, id='814890118', function=ChoiceDeltaToolCallFunction(arguments='', name='get_current_weather'), type='function')
ChoiceDeltaToolCall(index=0, id=None, function=ChoiceDeltaToolCallFunction(arguments='{"', name=None), type=None)
ChoiceDeltaToolCall(index=0, id=None, function=ChoiceDeltaToolCallFunction(arguments='location', name=None), type=None)
ChoiceDeltaToolCall(index=0, id=None, function=ChoiceDeltaToolCallFunction(arguments='":"', name=None), type=None)
ChoiceDeltaToolCall(index=0, id=None, function=ChoiceDeltaToolCallFunction(arguments='San Francisco', name=None), type=None)
ChoiceDeltaToolCall(index=0, id=None, function=ChoiceDeltaToolCallFunction(arguments='"}', name=None), type=None)

これらのチャンクは、ストリーム全体で蓄積して、実行するための完全な関数シグネチャを形成する必要があります。

以下の例は、/v1/chat/completionsストリーミングエンドポイント(stream=true)を通じて、シンプルなツール拡張チャットボットを作成する方法を示しています。

tool-streaming-chatbot.py(展開して表示)
from openai import OpenAI
import time

client = OpenAI(base_url="http://127.0.0.1:1234/v1", api_key="lm-studio")
MODEL = "lmstudio-community/qwen2.5-7b-instruct"

TIME_TOOL = {
    "type": "function",
    "function": {
        "name": "get_current_time",
        "description": "Get the current time, only if asked",
        "parameters": {"type": "object", "properties": {}},
    },
}

def get_current_time():
    return {"time": time.strftime("%H:%M:%S")}

def process_stream(stream, add_assistant_label=True):
    """Handle streaming responses from the API"""
    collected_text = ""
    tool_calls = []
    first_chunk = True

    for chunk in stream:
        delta = chunk.choices[0].delta

        # Handle regular text output
        if delta.content:
            if first_chunk:
                print()
                if add_assistant_label:
                    print("Assistant:", end=" ", flush=True)
                first_chunk = False
            print(delta.content, end="", flush=True)
            collected_text += delta.content

        # Handle tool calls
        elif delta.tool_calls:
            for tc in delta.tool_calls:
                if len(tool_calls) <= tc.index:
                    tool_calls.append({
                        "id": "", "type": "function",
                        "function": {"name": "", "arguments": ""}
                    })
                tool_calls[tc.index] = {
                    "id": (tool_calls[tc.index]["id"] + (tc.id or "")),
                    "type": "function",
                    "function": {
                        "name": (tool_calls[tc.index]["function"]["name"] + (tc.function.name or "")),
                        "arguments": (tool_calls[tc.index]["function"]["arguments"] + (tc.function.arguments or ""))
                    }
                }
    return collected_text, tool_calls

def chat_loop():
    messages = []
    print("Assistant: Hi! I am an AI agent empowered with the ability to tell the current time (Type 'quit' to exit)")

    while True:
        user_input = input("\nYou: ").strip()
        if user_input.lower() == "quit":
            break

        messages.append({"role": "user", "content": user_input})

        # Get initial response
        response_text, tool_calls = process_stream(
            client.chat.completions.create(
                model=MODEL,
                messages=messages,
                tools=[TIME_TOOL],
                stream=True,
                temperature=0.2
            )
        )

        if not tool_calls:
            print()

        text_in_first_response = len(response_text) > 0
        if text_in_first_response:
            messages.append({"role": "assistant", "content": response_text})

        # Handle tool calls if any
        if tool_calls:
            tool_name = tool_calls[0]["function"]["name"]
            print()
            if not text_in_first_response:
                print("Assistant:", end=" ", flush=True)
            print(f"**Calling Tool: {tool_name}**")
            messages.append({"role": "assistant", "tool_calls": tool_calls})

            # Execute tool calls
            for tool_call in tool_calls:
                if tool_call["function"]["name"] == "get_current_time":
                    result = get_current_time()
                    messages.append({
                        "role": "tool",
                        "content": str(result),
                        "tool_call_id": tool_call["id"]
                    })

            # Get final response after tool execution
            final_response, _ = process_stream(
                client.chat.completions.create(
                    model=MODEL,
                    messages=messages,
                    stream=True
                ),
                add_assistant_label=False
            )

            if final_response:
                print()
                messages.append({"role": "assistant", "content": final_response})

if __name__ == "__main__":
    chat_loop()

このスクリプトをコンソールから実行して、ボットとチャットできます。

→ % python tool-streaming-chatbot.py
Assistant: Hi! I am an AI agent empowered with the ability to tell the current time (Type 'quit' to exit)

You: Tell me a joke, then tell me the current time

Assistant: Sure! Here's a light joke for you: Why don't scientists trust atoms? Because they make up everything.

Now, let me get the current time for you.

**Calling Tool: get_current_time**

The current time is 18:49:31. Enjoy your day!

You:

コミュニティ

他の LM Studio ユーザーとチャットし、LLM、ハードウェアなどについて LM Studio Discord サーバーで議論しましょう。

このページのソースはGitHubで利用可能です。