Позвольте LLM взаимодействовать с внешними функциями и API.


Быстрый старт

Запустите LM Studio как сервер

Чтобы использовать LM Studio программно из вашего собственного кода, запустите LM Studio как локальный сервер.

Вы можете включить сервер на вкладке Разработчик (Developer) в LM Studio или через CLI lms:

lms server start

Установите lms, выполнив npx lmstudio install-cli

Это позволит вам взаимодействовать с LM Studio через REST API. Для введения в REST API LM Studio смотрите Обзор REST API.

Загрузите модель

Вы можете загрузить модель на вкладке Чат (Chat) или Разработчик (Developer) в LM Studio, или через CLI lms:

lms load

Скопируйте, вставьте и запустите пример!

  • Curl
    • Запрос вызова инструмента в один ход (Single Turn Tool Call Request)
  • Python
    • Вызов инструмента в один ход + использование инструмента (Single Turn Tool Call + Tool Use)
    • Пример в несколько ходов (Multi-Turn Example)
    • Пример продвинутого агента (Advanced Agent Example)

Использование инструментов

Что такое "Использование инструментов"?

Использование инструментов описывает процесс, при котором:

  • LLM выводят текст с запросом на вызов функций (LLM не могут напрямую выполнять код)
  • Ваш код выполняет эти функции
  • Ваш код передаёт результаты обратно в LLM.

Высокоуровневый поток

Ниже представлена схема взаимодействия при использовании инструментов:

НАСТРОЙКА: LLM + Список инструментов Получить ввод от пользователя LLM получает промпт с сообщениями Нужны инструменты? Да Нет Ответ инструмента Выполнить инструменты Добавить результаты в сообщения Обычный ответ

Детальный поток

LM Studio поддерживает использование инструментов через эндпоинт /v1/chat/completions, когда в параметре tools тела запроса передаются определения функций. Инструменты задаются как массив определений функций, описывающих их параметры и использование, например:

Это следует тому же формату, что и OpenAI Function Calling API, и ожидается, что будет работать через клиентские SDK OpenAI.

Мы будем использовать lmstudio-community/Qwen2.5-7B-Instruct-GGUF в качестве модели в этом примере потока.

  1. Вы предоставляете список инструментов LLM. Это инструменты, которые модель может запрашивать для вызова. Например:

    // список инструментов не зависит от модели
    [
      {
        "type": "function",
        "function": {
          "name": "get_delivery_date",
          "description": "Получить дату доставки для заказа клиента",
          "parameters": {
            "type": "object",
            "properties": {
              "order_id": {
                "type": "string"
              }
            },
            "required": ["order_id"]
          }
        }
      }
    ]

    Этот список будет внедрён в системный промпт (system prompt) модели в зависимости от шаблона чата модели. Для 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 или любые другие инструменты. Они могут только выводить текст, который затем может быть разобран для программного вызова функций.

  2. При получении промпта LLM может решить либо:

    • (а) Вызвать один или несколько инструментов
      User: Get me the delivery date for order 123
      Model: <tool_call>
      {"name": "get_delivery_date", "arguments": {"order_id": "123"}}
      </tool_call>
    • (б) Ответить обычным образом
      User: Hi
      Model: Hello! How can I assist you today?
  3. LM Studio разбирает текстовый вывод модели в объект ответа chat.completion, совместимый с OpenAI.

    • Если модели был предоставлен доступ к tools, LM Studio попытается разобрать вызовы инструментов в поле response.choices[0].message.tool_calls объекта ответа chat.completion.
    • Если 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.

  4. Ваш код разбирает ответ chat.completion, чтобы проверить наличие вызовов инструментов от модели, затем вызывает соответствующие инструменты с параметрами, указанными моделью. Ваш код затем добавляет как:

    1. Сообщение вызова инструмента от модели
    2. Результат вызова инструмента

    в массив messages, чтобы отправить обратно модели

    # псевдокод, см. примеры для копируемых фрагментов
    if response.has_tool_calls:
        for each tool_call:
            # Извлечь имя функции и аргументы
            function_to_call = tool_call.name     # например, "get_delivery_date"
            args = tool_call.arguments            # например, {"order_id": "123"}
    
            # Выполнить функцию
            result = execute_function(function_to_call, args)
    
            # Добавить результат в разговор
            add_to_messages([
                ASSISTANT_TOOL_CALL_MESSAGE,      # Запрос на использование инструмента
                TOOL_RESULT_MESSAGE               # Ответ инструмента
            ])
    else:
        # Обычный ответ без инструментов
        add_to_messages(response.content)
  5. Затем LLM снова получает промпт с обновлённым массивом сообщений, но без доступа к инструментам. Это потому что:

    • LLM уже имеет результаты инструментов в истории разговора
    • Мы хотим, чтобы LLM предоставила окончательный ответ пользователю, а не вызывала больше инструментов
    # Пример сообщений
    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
  6. Цикл продолжается обратно на шаге 2 потока

Примечание: Это педантичный поток использования инструментов. Однако вы, безусловно, можете экспериментировать с этим потоком, чтобы лучше соответствовать вашему варианту использования.

Поддерживаемые модели

Через LM Studio, все модели поддерживают хотя бы некоторую степень использования инструментов.

Однако в настоящее время существуют два уровня поддержки, которые могут повлиять на качество работы: Native (нативный) и Default (по умолчанию).

Модели с нативной поддержкой инструментов будут иметь значок молотка в приложении и обычно работают лучше в сценариях использования инструментов.

Нативная поддержка инструментов

"Нативная" поддержка инструментов означает, что оба условия выполнены:

  1. Модель имеет шаблон чата, который поддерживает использование инструментов (обычно это означает, что модель была обучена для использования инструментов)
    • Это то, что будет использоваться для форматирования массива tools в системный промпт и указания модели, как форматировать вызовы инструментов
    • Пример: шаблон чата Qwen2.5-Instruct
  2. LM Studio поддерживает формат использования инструментов этой модели
    • Требуется для того, чтобы LM Studio правильно вводила историю чата в шаблон чата и разбирала вызовы инструментов, которые выводит модель, в объект chat.completion

Модели, которые в настоящее время имеют нативную поддержку инструментов в LM Studio (может измениться):

  • Qwen
    • GGUF lmstudio-community/Qwen2.5-7B-Instruct-GGUF (4.68 ГБ)
    • MLX mlx-community/Qwen2.5-7B-Instruct-4bit (4.30 ГБ)
  • Llama-3.1, Llama-3.2
    • GGUF lmstudio-community/Meta-Llama-3.1-8B-Instruct-GGUF (4.92 ГБ)
    • MLX mlx-community/Meta-Llama-3.1-8B-Instruct-8bit (8.54 ГБ)
  • Mistral
    • GGUF bartowski/Ministral-8B-Instruct-2410-GGUF (4.67 ГБ)
    • MLX mlx-community/Ministral-8B-Instruct-2410-4bit (4.67 ГБ)

Поддержка инструментов по умолчанию

"Поддержка инструментов по умолчанию" означает, что либо:

  1. Модель не имеет шаблона чата, который поддерживает использование инструментов (обычно это означает, что модель не была обучена для использования инструментов)
  2. LM Studio в настоящее время не поддерживает формат использования инструментов этой модели

Под капотом поддержка инструментов по умолчанию работает путём:

  • Предоставления моделям пользовательского системного промпта и формата вызова инструмента по умолчанию для использования
  • Преобразования сообщений роли tool в роль user, чтобы шаблоны чата без роли tool были совместимы
  • Преобразования tool_calls роли assistant в формат вызова инструмента по умолчанию

Результаты будут различаться в зависимости от модели.

Вы можете увидеть формат по умолчанию, выполнив 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 http://localhost: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' для параметра query,
  • 'electronics' для параметра category
  • '50' для параметра max_price

и верните мне результаты"

Поле tool_calls нужно будет разобрать для вызова реальных функций/API. Ниже примеры демонстрируют, как это сделать.

Примеры использования python

Использование инструментов раскрывается в полной мере при использовании с языками программирования, такими как python, где вы можете реализовать функции, указанные в поле tools, чтобы программно вызывать их, когда модель запрашивает.

Пример в один ход

Ниже приведён простой пример в один ход (модель вызывается только один раз) для включения возможности модели вызывать функцию say_hello, которая печатает приветствие в консоль:

single-turn-example.py

from openai import OpenAI

# Подключение к LM Studio
client = OpenAI(base_url="http://localhost:1234/v1", api_key="lm-studio")

# Определяем простую функцию
def say_hello(name: str) -> str:
    print(f"Hello, {name}!")

# Сообщаем ИИ о нашей функции
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"]
            }
        }
    }
]

# Просим ИИ использовать нашу функцию
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
)

# Получаем имя, которое ИИ хочет использовать для приветствия
# (Предполагается, что ИИ запросил вызов инструмента и этот вызов - say_hello)
tool_call = response.choices[0].message.tool_calls[0]
name = eval(tool_call.function.arguments)["name"]

# Фактически вызываем функцию say_hello
say_hello(name) # Печатает: 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 с разными именами.

Пример в несколько ходов

Теперь немного более сложный пример.

В этом примере мы:

  1. Включим возможность модели вызывать функцию get_delivery_date
  2. Передадим результат вызова этой функции обратно модели, чтобы она могла выполнить запрос пользователя в виде обычного текста

multi-turn-example.py (нажмите, чтобы развернуть)

from datetime import datetime, timedelta
import json
import random
from openai import OpenAI

# Указываем на локальный сервер
client = OpenAI(base_url="http://localhost:1234/v1", api_key="lm-studio")
model = "lmstudio-community/qwen2.5-7b-instruct"

def get_delivery_date(order_id: str) -> datetime:
    # Генерируем случайную дату доставки между сегодня и через 14 дней
    # в реальном сценарии эта функция запрашивала бы базу данных или 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)

# Извлекаем аргументы для get_delivery_date
# Обратите внимание, что этот код предполагает, что мы уже определили, что модель сгенерировала вызов функции.
tool_call = response.choices[0].message.tool_calls[0]
arguments = json.loads(tool_call.function.arguments)

order_id = arguments.get("order_id")

# Вызываем функцию get_delivery_date с извлечённым 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,
        }
    ],
}

# Создаём сообщение, содержащее результат вызова функции
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,
}

# Подготавливаем полезную нагрузку для вызова завершения чата
completion_messages_payload = [
    messages[0],
    messages[1],
    assistant_tool_call_request_message,
    function_call_result_message,
]

# Вызываем эндпоинт завершений чата OpenAI API, чтобы отправить результат вызова инструмента обратно модели
# 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 с локально определёнными функциями для создания "агента" - системы, которая объединяет языковую модель с пользовательскими функциями для понимания запросов и выполнения действий за пределами базовой генерации текста.

Агент в примере ниже может:

  1. Открывать безопасные URL в вашем браузере по умолчанию
  2. Проверять текущее время
  3. Анализировать директории в вашей файловой системе

agent-chat-example.py (нажмите, чтобы развернуть)

import json
from urllib.parse import urlparse
import webbrowser
from datetime import datetime
import os
from openai import OpenAI

# Указываем на локальный сервер
client = OpenAI(base_url="http://localhost: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)  # Возвращает True, если есть действительное сетевое расположение
    except Exception:
        return False

def open_safe_url(url: str) -> dict:
    # Список разрешённых доменов (расширяйте по мере необходимости)
    SAFE_DOMAINS = {
        "lmstudio.ai",
        "huggingface.co",
        "github.com",
        "google.com",
        "wikipedia.org",
        "weather.com",
        "stackoverflow.com",
        "python.org",
        "docs.python.org",
    }

    try:
        # Добавляем http://, если схема отсутствует
        if not url.startswith(('http://', 'https://')):
            url = 'http://' + url

        # Проверяем формат URL
        if not is_valid_url(url):
            return {"status": "error", "message": f"Invalid URL format: {url}"}

        # Разбираем URL и проверяем домен
        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:
    """Получить текущее системное время с информацией о часовом поясе"""
    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:
    """Подсчитать и категоризировать файлы в директории"""
    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
                # Добавляем размер содержимого директории
                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):
    """Обработать несколько вызовов инструментов и вернуть окончательный ответ и обновлённые сообщения"""
    # Получаем все вызовы инструментов из ответа
    tool_calls = response.choices[0].message.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
        ],
    }

    # Добавляем сообщение вызова инструмента ассистента в историю
    messages.append(assistant_tool_call_message)

    # Обрабатываем каждый вызов инструмента и собираем результаты
    tool_results = []
    for tool_call in tool_calls:
        # Для функций без аргументов используем пустой dict
        arguments = (
            json.loads(tool_call.function.arguments)
            if tool_call.function.arguments.strip()
            else {}
        )

        # Определяем, какую функцию вызвать, на основе имени вызова инструмента
        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 попытался вызвать несуществующую функцию, пропускаем
            continue

        # Добавляем сообщение результата
        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)

    # Получаем окончательный ответ
    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:
        # Получаем ввод пользователя
        user_input = input("\nYou: ").strip()

        # Проверяем команду выхода
        if user_input.lower() == "quit":
            print("Assistant: Goodbye!")
            break

        # Добавляем сообщение пользователя в разговор
        messages.append({"role": "user", "content": user_input})

        try:
            # Получаем начальный ответ
            response = client.chat.completions.create(
                model=model,
                messages=messages,
                tools=tools,
            )

            # Проверяем, включает ли ответ вызовы инструментов
            if response.choices[0].message.tool_calls:
                # Обрабатываем все вызовы инструментов и получаем окончательный ответ
                final_response = process_tool_calls(response, messages)
                print("\nAssistant:", final_response.choices[0].message.content)

                # Добавляем окончательный ответ ассистента в сообщения
                messages.append(
                    {
                        "role": "assistant",
                        "content": final_response.choices[0].message.content,
                    }
                )
            else:
                # Если вызова инструмента нет, просто печатаем ответ
                print("\nAssistant:", response.choices[0].message.content)

                # Добавляем ответ ассистента в сообщения
                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/completions (stream=true), вызовы инструментов отправляются частями. Имена функций и аргументы отправляются по частям через chunk.choices[0].delta.tool_calls.function.name и chunk.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://localhost: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):
    """Обработка потоковых ответов от API"""
    collected_text = ""
    tool_calls = []
    first_chunk = True

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

        # Обработка обычного текстового вывода
        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

        # Обработка вызовов инструментов
        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})

        # Получаем начальный ответ
        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})

        # Обработка вызовов инструментов, если есть
        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})

            # Выполняем вызовы инструментов
            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"]
                    })

            # Получаем окончательный ответ после выполнения инструмента
            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:

Дополнительные ресурсы

OpenAI-совместимые эндпоинты

Используйте локальные модели с любыми инструментами, разработанными для OpenAI API

Справочник API

Полный справочник по REST API-эндпоинтам LM Studio

Локальный сервер

Запуск API-сервера LLM на localhost с настройками сервера LM Studio



Оригинал страницы