diff --git a/lib/ruby_llm/providers/gemini/tools.rb b/lib/ruby_llm/providers/gemini/tools.rb index 6ed53e17c..8cb85b1c3 100644 --- a/lib/ruby_llm/providers/gemini/tools.rb +++ b/lib/ruby_llm/providers/gemini/tools.rb @@ -54,22 +54,22 @@ def function_declaration_for(tool) { name: tool.name, description: tool.description, - parameters: { - type: 'OBJECT', - properties: format_parameters(tool.parameters), - required: tool.parameters.select { |_, p| p.required }.keys.map(&:to_s) - } - } + parameters: tool.parameters.any? ? format_parameters(tool.parameters) : nil + }.compact end # Format tool parameters for Gemini API def format_parameters(parameters) - parameters.transform_values do |param| - { - type: param_type_for_gemini(param.type), - description: param.description - }.compact - end + { + type: 'OBJECT', + properties: parameters.transform_values do |param| + { + type: param_type_for_gemini(param.type), + description: param.description + }.compact + end, + required: parameters.select { |_, p| p.required }.keys.map(&:to_s) + } end # Convert RubyLLM param types to Gemini API types diff --git a/spec/fixtures/vcr_cassettes/chat_function_calling_claude-3-5-haiku-20241022_can_use_tools_without_parameters.yml b/spec/fixtures/vcr_cassettes/chat_function_calling_claude-3-5-haiku-20241022_can_use_tools_without_parameters.yml new file mode 100644 index 000000000..625509fae --- /dev/null +++ b/spec/fixtures/vcr_cassettes/chat_function_calling_claude-3-5-haiku-20241022_can_use_tools_without_parameters.yml @@ -0,0 +1,168 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.anthropic.com/v1/messages + body: + encoding: UTF-8 + string: '{"model":"claude-3-5-haiku-20241022","messages":[{"role":"user","content":"What''s + the best language to learn?"}],"temperature":0.7,"stream":false,"max_tokens":8192,"tools":[{"name":"best_language_to_learn","description":"Gets + the best language to learn","input_schema":{"type":"object","properties":{},"required":[]}}]}' + headers: + User-Agent: + - Faraday v2.12.2 + X-Api-Key: + - "" + Anthropic-Version: + - '2023-06-01' + Content-Type: + - application/json + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + response: + status: + code: 200 + message: OK + headers: + Date: + - Wed, 26 Mar 2025 12:36:24 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Anthropic-Ratelimit-Requests-Limit: + - '50' + Anthropic-Ratelimit-Requests-Remaining: + - '49' + Anthropic-Ratelimit-Requests-Reset: + - '2025-03-26T12:36:22Z' + Anthropic-Ratelimit-Input-Tokens-Limit: + - '50000' + Anthropic-Ratelimit-Input-Tokens-Remaining: + - '50000' + Anthropic-Ratelimit-Input-Tokens-Reset: + - '2025-03-26T12:36:23Z' + Anthropic-Ratelimit-Output-Tokens-Limit: + - '10000' + Anthropic-Ratelimit-Output-Tokens-Remaining: + - '10000' + Anthropic-Ratelimit-Output-Tokens-Reset: + - '2025-03-26T12:36:23Z' + Anthropic-Ratelimit-Tokens-Limit: + - '60000' + Anthropic-Ratelimit-Tokens-Remaining: + - '60000' + Anthropic-Ratelimit-Tokens-Reset: + - '2025-03-26T12:36:23Z' + Request-Id: + - "" + Anthropic-Organization-Id: + - 382da897-d586-4e0c-bd1c-a9407bbe3b7a + Via: + - 1.1 google + Cf-Cache-Status: + - DYNAMIC + X-Robots-Tag: + - none + Server: + - cloudflare + Cf-Ray: + - "" + body: + encoding: ASCII-8BIT + string: '{"id":"msg_016N5q71zpXwLnoYPDYjpi92","type":"message","role":"assistant","model":"claude-3-5-haiku-20241022","content":[{"type":"text","text":"I''ll + help you find out the best language to learn by using the available tool."},{"type":"tool_use","id":"toolu_01HiFoEeZiQJ6CFwCNzT6Nwu","name":"best_language_to_learn","input":{}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":327,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":58}}' + recorded_at: Wed, 26 Mar 2025 12:36:24 GMT +- request: + method: post + uri: https://api.anthropic.com/v1/messages + body: + encoding: UTF-8 + string: '{"model":"claude-3-5-haiku-20241022","messages":[{"role":"user","content":"What''s + the best language to learn?"},{"role":"assistant","content":[{"type":"text","text":"I''ll + help you find out the best language to learn by using the available tool."},{"type":"tool_use","id":"toolu_01HiFoEeZiQJ6CFwCNzT6Nwu","name":"best_language_to_learn","input":{}}]},{"role":"user","content":[{"type":"tool_result","tool_use_id":"toolu_01HiFoEeZiQJ6CFwCNzT6Nwu","content":"Ruby"}]}],"temperature":0.7,"stream":false,"max_tokens":8192,"tools":[{"name":"best_language_to_learn","description":"Gets + the best language to learn","input_schema":{"type":"object","properties":{},"required":[]}}]}' + headers: + User-Agent: + - Faraday v2.12.2 + X-Api-Key: + - "" + Anthropic-Version: + - '2023-06-01' + Content-Type: + - application/json + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + response: + status: + code: 200 + message: OK + headers: + Date: + - Wed, 26 Mar 2025 12:36:30 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Anthropic-Ratelimit-Requests-Limit: + - '50' + Anthropic-Ratelimit-Requests-Remaining: + - '49' + Anthropic-Ratelimit-Requests-Reset: + - '2025-03-26T12:36:25Z' + Anthropic-Ratelimit-Input-Tokens-Limit: + - '50000' + Anthropic-Ratelimit-Input-Tokens-Remaining: + - '50000' + Anthropic-Ratelimit-Input-Tokens-Reset: + - '2025-03-26T12:36:27Z' + Anthropic-Ratelimit-Output-Tokens-Limit: + - '10000' + Anthropic-Ratelimit-Output-Tokens-Remaining: + - '10000' + Anthropic-Ratelimit-Output-Tokens-Reset: + - '2025-03-26T12:36:30Z' + Anthropic-Ratelimit-Tokens-Limit: + - '60000' + Anthropic-Ratelimit-Tokens-Remaining: + - '60000' + Anthropic-Ratelimit-Tokens-Reset: + - '2025-03-26T12:36:27Z' + Request-Id: + - "" + Anthropic-Organization-Id: + - 382da897-d586-4e0c-bd1c-a9407bbe3b7a + Via: + - 1.1 google + Cf-Cache-Status: + - DYNAMIC + X-Robots-Tag: + - none + Server: + - cloudflare + Cf-Ray: + - "" + body: + encoding: ASCII-8BIT + string: '{"id":"msg_01VXzvCFsc6ggwCM6qbGXKTK","type":"message","role":"assistant","model":"claude-3-5-haiku-20241022","content":[{"type":"text","text":"According + to the tool, Ruby is recommended as the best language to learn. Ruby is a + dynamic, object-oriented programming language known for its simplicity and + readability. It''s particularly popular for web development, especially with + the Ruby on Rails framework. \n\nSome reasons why Ruby might be a great language + to learn:\n1. Easy to read and write, with a clean and elegant syntax\n2. + Great for web development and scripting\n3. Strong community support\n4. Used + in many startups and tech companies\n5. Good for beginners due to its intuitive + nature\n\nHowever, the \"best\" language can depend on your personal goals, + such as:\n- Web development\n- Data science\n- Mobile app development\n- Game + development\n- Career opportunities\n\nWould you like to know more about Ruby + or discuss how it might fit your specific learning objectives?"}],"stop_reason":"end_turn","stop_sequence":null,"usage":{"input_tokens":397,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":182}}' + recorded_at: Wed, 26 Mar 2025 12:36:30 GMT +recorded_with: VCR 6.3.1 diff --git a/spec/fixtures/vcr_cassettes/chat_function_calling_gemini-2_0-flash_can_use_tools_without_parameters.yml b/spec/fixtures/vcr_cassettes/chat_function_calling_gemini-2_0-flash_can_use_tools_without_parameters.yml new file mode 100644 index 000000000..51236722c --- /dev/null +++ b/spec/fixtures/vcr_cassettes/chat_function_calling_gemini-2_0-flash_can_use_tools_without_parameters.yml @@ -0,0 +1,174 @@ +--- +http_interactions: +- request: + method: post + uri: https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent + body: + encoding: UTF-8 + string: '{"contents":[{"role":"user","parts":[{"text":"What''s the best language + to learn?"}]}],"generationConfig":{"temperature":0.7},"tools":[{"functionDeclarations":[{"name":"best_language_to_learn","description":"Gets + the best language to learn"}]}]}' + headers: + User-Agent: + - Faraday v2.12.2 + X-Goog-Api-Key: + - "" + Content-Type: + - application/json + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + response: + status: + code: 200 + message: OK + headers: + Content-Type: + - application/json; charset=UTF-8 + Vary: + - Origin + - Referer + - X-Origin + Date: + - Wed, 26 Mar 2025 12:36:31 GMT + Server: + - scaffolding on HTTPServer2 + X-Xss-Protection: + - '0' + X-Frame-Options: + - SAMEORIGIN + X-Content-Type-Options: + - nosniff + Server-Timing: + - gfet4t7; dur=505 + Alt-Svc: + - h3=":443"; ma=2592000,h3-29=":443"; ma=2592000 + Transfer-Encoding: + - chunked + body: + encoding: ASCII-8BIT + string: | + { + "candidates": [ + { + "content": { + "parts": [ + { + "functionCall": { + "name": "best_language_to_learn", + "args": {} + } + } + ], + "role": "model" + }, + "finishReason": "STOP", + "avgLogprobs": -0.00011707750880824668 + } + ], + "usageMetadata": { + "promptTokenCount": 22, + "candidatesTokenCount": 7, + "totalTokenCount": 29, + "promptTokensDetails": [ + { + "modality": "TEXT", + "tokenCount": 22 + } + ], + "candidatesTokensDetails": [ + { + "modality": "TEXT", + "tokenCount": 7 + } + ] + }, + "modelVersion": "gemini-2.0-flash" + } + recorded_at: Wed, 26 Mar 2025 12:36:31 GMT +- request: + method: post + uri: https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent + body: + encoding: UTF-8 + string: '{"contents":[{"role":"user","parts":[{"text":"What''s the best language + to learn?"}]},{"role":"model","parts":[{"functionCall":{"name":"best_language_to_learn","args":{}}}]},{"role":"user","parts":[{"functionResponse":{"name":"59a92e0a-8267-4a36-b42f-6c8f41da2add","response":{"name":"59a92e0a-8267-4a36-b42f-6c8f41da2add","content":"Ruby"}}}]}],"generationConfig":{"temperature":0.7},"tools":[{"functionDeclarations":[{"name":"best_language_to_learn","description":"Gets + the best language to learn"}]}]}' + headers: + User-Agent: + - Faraday v2.12.2 + X-Goog-Api-Key: + - "" + Content-Type: + - application/json + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + response: + status: + code: 200 + message: OK + headers: + Content-Type: + - application/json; charset=UTF-8 + Vary: + - Origin + - Referer + - X-Origin + Date: + - Wed, 26 Mar 2025 12:36:31 GMT + Server: + - scaffolding on HTTPServer2 + X-Xss-Protection: + - '0' + X-Frame-Options: + - SAMEORIGIN + X-Content-Type-Options: + - nosniff + Server-Timing: + - gfet4t7; dur=472 + Alt-Svc: + - h3=":443"; ma=2592000,h3-29=":443"; ma=2592000 + Transfer-Encoding: + - chunked + body: + encoding: ASCII-8BIT + string: | + { + "candidates": [ + { + "content": { + "parts": [ + { + "text": "I think Ruby is the best language to learn.\n" + } + ], + "role": "model" + }, + "finishReason": "STOP", + "avgLogprobs": -0.11496557972647926 + } + ], + "usageMetadata": { + "promptTokenCount": 98, + "candidatesTokenCount": 11, + "totalTokenCount": 109, + "promptTokensDetails": [ + { + "modality": "TEXT", + "tokenCount": 98 + } + ], + "candidatesTokensDetails": [ + { + "modality": "TEXT", + "tokenCount": 11 + } + ] + }, + "modelVersion": "gemini-2.0-flash" + } + recorded_at: Wed, 26 Mar 2025 12:36:31 GMT +recorded_with: VCR 6.3.1 diff --git a/spec/fixtures/vcr_cassettes/chat_function_calling_gpt-4o-mini_can_use_tools_without_parameters.yml b/spec/fixtures/vcr_cassettes/chat_function_calling_gpt-4o-mini_can_use_tools_without_parameters.yml new file mode 100644 index 000000000..57e087283 --- /dev/null +++ b/spec/fixtures/vcr_cassettes/chat_function_calling_gpt-4o-mini_can_use_tools_without_parameters.yml @@ -0,0 +1,231 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.openai.com/v1/chat/completions + body: + encoding: UTF-8 + string: '{"model":"gpt-4o-mini","messages":[{"role":"user","content":"What''s + the best language to learn?"}],"temperature":0.7,"stream":false,"tools":[{"type":"function","function":{"name":"best_language_to_learn","description":"Gets + the best language to learn","parameters":{"type":"object","properties":{},"required":[]}}}],"tool_choice":"auto"}' + headers: + User-Agent: + - Faraday v2.12.2 + Authorization: + - Bearer + Content-Type: + - application/json + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + response: + status: + code: 200 + message: OK + headers: + Date: + - Wed, 26 Mar 2025 12:36:32 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Access-Control-Expose-Headers: + - X-Request-ID + Openai-Organization: + - "" + Openai-Processing-Ms: + - '835' + Openai-Version: + - '2020-10-01' + X-Ratelimit-Limit-Requests: + - '10000' + X-Ratelimit-Limit-Tokens: + - '200000' + X-Ratelimit-Remaining-Requests: + - '9999' + X-Ratelimit-Remaining-Tokens: + - '199988' + X-Ratelimit-Reset-Requests: + - 8.64s + X-Ratelimit-Reset-Tokens: + - 3ms + X-Request-Id: + - "" + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Cf-Cache-Status: + - DYNAMIC + Set-Cookie: + - "" + - "" + X-Content-Type-Options: + - nosniff + Server: + - cloudflare + Cf-Ray: + - "" + Alt-Svc: + - h3=":443"; ma=86400 + body: + encoding: ASCII-8BIT + string: | + { + "id": "chatcmpl-BFKHgIGAwLUS0wTdClrDXbEzwGlhE", + "object": "chat.completion", + "created": 1742992592, + "model": "gpt-4o-mini-2024-07-18", + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": null, + "tool_calls": [ + { + "id": "call_rSFV6B5evXpOXNsNQfv379HV", + "type": "function", + "function": { + "name": "best_language_to_learn", + "arguments": "{}" + } + } + ], + "refusal": null, + "annotations": [] + }, + "logprobs": null, + "finish_reason": "tool_calls" + } + ], + "usage": { + "prompt_tokens": 48, + "completion_tokens": 14, + "total_tokens": 62, + "prompt_tokens_details": { + "cached_tokens": 0, + "audio_tokens": 0 + }, + "completion_tokens_details": { + "reasoning_tokens": 0, + "audio_tokens": 0, + "accepted_prediction_tokens": 0, + "rejected_prediction_tokens": 0 + } + }, + "service_tier": "default", + "system_fingerprint": "fp_27322b4e16" + } + recorded_at: Wed, 26 Mar 2025 12:36:32 GMT +- request: + method: post + uri: https://api.openai.com/v1/chat/completions + body: + encoding: UTF-8 + string: '{"model":"gpt-4o-mini","messages":[{"role":"user","content":"What''s + the best language to learn?"},{"role":"assistant","tool_calls":[{"id":"call_rSFV6B5evXpOXNsNQfv379HV","type":"function","function":{"name":"best_language_to_learn","arguments":"{}"}}]},{"role":"tool","content":"Ruby","tool_call_id":"call_rSFV6B5evXpOXNsNQfv379HV"}],"temperature":0.7,"stream":false,"tools":[{"type":"function","function":{"name":"best_language_to_learn","description":"Gets + the best language to learn","parameters":{"type":"object","properties":{},"required":[]}}}],"tool_choice":"auto"}' + headers: + User-Agent: + - Faraday v2.12.2 + Authorization: + - Bearer + Content-Type: + - application/json + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + response: + status: + code: 200 + message: OK + headers: + Date: + - Wed, 26 Mar 2025 12:36:33 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Access-Control-Expose-Headers: + - X-Request-ID + Openai-Organization: + - "" + Openai-Processing-Ms: + - '261' + Openai-Version: + - '2020-10-01' + X-Ratelimit-Limit-Requests: + - '10000' + X-Ratelimit-Limit-Tokens: + - '200000' + X-Ratelimit-Remaining-Requests: + - '9998' + X-Ratelimit-Remaining-Tokens: + - '199986' + X-Ratelimit-Reset-Requests: + - 16.249s + X-Ratelimit-Reset-Tokens: + - 4ms + X-Request-Id: + - "" + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Cf-Cache-Status: + - DYNAMIC + Set-Cookie: + - "" + - "" + X-Content-Type-Options: + - nosniff + Server: + - cloudflare + Cf-Ray: + - "" + Alt-Svc: + - h3=":443"; ma=86400 + body: + encoding: ASCII-8BIT + string: | + { + "id": "chatcmpl-BFKHhaUl3iiFyJeMb0nD071b7EYPZ", + "object": "chat.completion", + "created": 1742992593, + "model": "gpt-4o-mini-2024-07-18", + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": "The best language to learn is Ruby.", + "refusal": null, + "annotations": [] + }, + "logprobs": null, + "finish_reason": "stop" + } + ], + "usage": { + "prompt_tokens": 73, + "completion_tokens": 10, + "total_tokens": 83, + "prompt_tokens_details": { + "cached_tokens": 0, + "audio_tokens": 0 + }, + "completion_tokens_details": { + "reasoning_tokens": 0, + "audio_tokens": 0, + "accepted_prediction_tokens": 0, + "rejected_prediction_tokens": 0 + } + }, + "service_tier": "default", + "system_fingerprint": "fp_bbfba58e46" + } + recorded_at: Wed, 26 Mar 2025 12:36:33 GMT +recorded_with: VCR 6.3.1 diff --git a/spec/ruby_llm/chat_tools_spec.rb b/spec/ruby_llm/chat_tools_spec.rb index 0de23b9bb..daa0821a5 100644 --- a/spec/ruby_llm/chat_tools_spec.rb +++ b/spec/ruby_llm/chat_tools_spec.rb @@ -16,6 +16,14 @@ def execute(latitude:, longitude:) end end + class BestLanguageToLearn < RubyLLM::Tool # rubocop:disable Lint/ConstantDefinitionInBlock,RSpec/LeakyConstantDeclaration + description 'Gets the best language to learn' + + def execute + 'Ruby' + end + end + describe 'function calling' do [ 'claude-3-5-haiku-20241022', @@ -44,6 +52,12 @@ def execute(latitude:, longitude:) expect(response.content).to include('10') end + it "#{model} can use tools without parameters" do + chat = RubyLLM.chat(model: model).with_tool(BestLanguageToLearn) + response = chat.ask("What's the best language to learn?") + expect(response.content).to include('Ruby') + end + it "#{model} can use tools with multi-turn streaming conversations" do # rubocop:disable RSpec/ExampleLength,RSpec/MultipleExpectations chat = RubyLLM.chat(model: model) .with_tool(Weather)